lotus / inc /lilyParser /lilyDocument.ts
k-l-lambda's picture
commit lotus dist.
d605f27
raw
history blame
29.9 kB
import TextSource from "../textSource";
import {LILY_STAFF_SIZE_DEFAULT} from "../constants";
import {
parseRaw,
BaseTerm, Assignment, LiteralString, Command, Variable, MarkupCommand, Grace, AfterGrace, Include, Version, Block, InlineBlock,
Scheme, Chord, BriefChord, Lyric, MusicBlock, SimultaneousList, ContextedMusic, Divide, Tempo, PostEvent, Primitive, ChordElement, MusicEvent,
SchemePointer, Comment, Language, StemDirection,
} from "./lilyTerms";
import LilyInterpreter from "./lilyInterpreter";
import {MAIN_SCORE_NAME, DocLocation} from "./utils";
// eslint-disable-next-line
import {Root} from "./lilyTerms";
type AttributeValue = number | boolean | string | BaseTerm;
interface AttributeValueHandle {
value: AttributeValue;
};
export interface LilyDocumentAttribute {
[key: string]: AttributeValueHandle
};
export interface LilyDocumentAttributeReadOnly {
staffSize: number;
[key: string]: AttributeValue
};
export default class LilyDocument {
root: Root;
cacheInterpreter?: LilyInterpreter;
reservedVariables?: Set<string>;
constructor (data: object) {
//console.log("raw data:", data);
this.root = parseRaw(data);
}
toString () {
return this.root.join();
//return this.root.serialize();
}
interpret ({useCached = true} = {}): LilyInterpreter {
if (!useCached || !this.cacheInterpreter) {
this.cacheInterpreter = new LilyInterpreter();
this.cacheInterpreter.interpretDocument(this);
}
return this.cacheInterpreter;
}
globalAttributes ({readonly = false} = {}): LilyDocumentAttribute | LilyDocumentAttributeReadOnly {
const globalStaffSize = this.root.getField("set-global-staff-size");
const header = this.root.getBlock("header");
let paper = this.root.getBlock("paper");
const layoutStaffSize = paper && paper.getField("layout-set-staff-size");
let staffSize = globalStaffSize || layoutStaffSize;
if (!readonly) {
let sectionsDirty = false;
if (!staffSize) {
this.root.sections.push(new Scheme({exp: {proto: "SchemeFunction", func: "set-global-staff-size", args: [LILY_STAFF_SIZE_DEFAULT]}}));
staffSize = this.root.getField("set-global-staff-size");
sectionsDirty = true;
}
// A4 paper size
const DEFAULT_PAPER_WIDTH = {
proto: "Assignment",
key: "paper-width",
value: {proto: "NumberUnit", number: 21, unit: "\\cm"},
};
const DEFAULT_PAPER_HEIGHT = {
proto: "Assignment",
key: "paper-height",
value: {proto: "NumberUnit", number: 29.71, unit: "\\cm"},
};
if (!paper) {
paper = new Block({
block: "score",
head: "\\paper",
body: [DEFAULT_PAPER_WIDTH, DEFAULT_PAPER_HEIGHT],
});
this.root.sections.push(paper);
sectionsDirty = true;
}
if (!paper.getField("paper-width"))
paper.body.push(parseRaw(DEFAULT_PAPER_WIDTH));
if (!paper.getField("paper-height"))
paper.body.push(parseRaw(DEFAULT_PAPER_HEIGHT));
if (sectionsDirty)
this.root.reorderSections();
}
else
staffSize = staffSize || {value: LILY_STAFF_SIZE_DEFAULT};
const blockPropertyCommon = (block: Block, key: string) => ({
get value () {
if (!block)
return undefined;
const item = block.getField(key);
if (!item)
return undefined;
return item.value;
},
set value (value) {
console.assert(!!block, "block is null.");
if (value === undefined) // delete field
block.body = block.body.filter(assign => !(assign instanceof Assignment) || assign.key !== key);
else {
const item = block.getField(key);
if (item)
item.value = parseRaw(value);
else
block.body.push(new Assignment({key, value}));
}
},
});
const paperPropertyCommon = key => blockPropertyCommon(paper, key);
const paperPropertySchemeToken = key => ({
get value () {
if (!paper)
return undefined;
const item = paper.getField(key);
if (!item)
return undefined;
return item.value.exp;
},
set value (value) {
console.assert(!!paper, "paper is null.");
const item = paper.getField(key);
if (item)
item.value.exp = value;
else
paper.body.push(new Assignment({key, value: {proto: "Scheme", exp: value}}));
},
});
let midiBlock = null;
const scores = this.root.sections.filter(section => section instanceof Block && section.head === "\\score") as Block[];
for (const score of scores) {
midiBlock = score.body.find(term => term instanceof Block && term.head === "\\midi");
if (midiBlock)
break;
}
const midiTempo = {
get value (): Tempo {
return midiBlock && midiBlock.body.find(term => term instanceof Tempo);
},
set value (value: Tempo) {
if (!midiBlock) {
const score = this.root.getBlock("score");
if (score) {
midiBlock = new Block({block: "score", head: "\\midi", body: []});
score.body.push(midiBlock);
}
else
console.warn("no score block, midiTempo assign failed.");
}
if (midiBlock) {
midiBlock.body = midiBlock.body.filter(term => !(term instanceof Tempo));
midiBlock.body.push(value);
}
},
};
const assignments = this.root.entries.filter(term => term instanceof Assignment) as Assignment[];
const assignmentTable = assignments.reduce((table, assign) => ((table[assign.key.toString()] = assign.query(assign.key)), table), {});
const headerFields = [
"title", "subtitle", "subsubtitle", "composer", "poet", "arranger", "opus", "copyright", "instrument", "dedication", "tagline",
].reduce((dict, field) => ((dict[field] = blockPropertyCommon(header, field)), dict), {});
const attributes = {
staffSize,
midiTempo,
...headerFields,
paperWidth: paperPropertyCommon("paper-width"),
paperHeight: paperPropertyCommon("paper-height"),
topMargin: paperPropertyCommon("top-margin"),
bottomMargin: paperPropertyCommon("bottom-margin"),
leftMargin: paperPropertyCommon("left-margin"),
rightMargin: paperPropertyCommon("right-margin"),
systemSpacing: paperPropertySchemeToken("system-system-spacing.basic-distance"),
topMarkupSpacing: paperPropertySchemeToken("top-markup-spacing.basic-distance"),
raggedLast: paperPropertySchemeToken("ragged-last"),
raggedBottom: paperPropertySchemeToken("ragged-bottom"),
raggedLastBottom: paperPropertySchemeToken("ragged-last-bottom"),
printPageNumber: paperPropertySchemeToken("print-page-number"),
...assignmentTable,
};
if (readonly)
Object.keys(attributes).forEach(key => attributes[key] = attributes[key] && attributes[key].value);
return attributes;
}
globalAttributesReadOnly (): LilyDocumentAttributeReadOnly {
const attributes = this.globalAttributes() as any;
Object.keys(attributes).forEach(key => attributes[key] = attributes[key] && attributes[key].value);
return attributes;
}
markup (docMarkup: LilyDocument) {
// copy attributes
const attrS = this.globalAttributes() as LilyDocumentAttribute;
const attrM = docMarkup.globalAttributesReadOnly();
[
"staffSize", "midiTempo", "paperWidth", "paperHeight",
"topMargin", "bottomMargin", "leftMargin", "rightMargin",
"systemSpacing", "topMarkupSpacing", "raggedLast", "raggedBottom", "raggedLastBottom",
"printPageNumber",
].forEach(field => {
if (attrM[field] !== undefined) {
if (typeof attrS[field].value === "object" && attrS[field].value && (attrS[field].value as any).set)
(attrS[field].value as any).set(attrM[field]);
else
attrS[field].value = attrM[field];
}
});
// execute commands list
const commands = docMarkup.root.getField("LotusCommands");
const cmdList = commands && commands.value && commands.value.args && commands.value.args[0].body;
if (cmdList && Array.isArray(cmdList)) {
for (const command of cmdList) {
if (command.exp && this[command.exp])
this[command.exp]();
else
console.warn("unexpected markup command:", command);
}
}
// copy LotusOption assignments
const assignments = docMarkup.root.entries.filter(term => term instanceof Assignment && /^LotusOption\..+/.test(term.key.toString()));
assignments.forEach(assignment => this.root.sections.push(assignment.clone()));
// copy score blocks
const layoutBody = [];
const midiBody = [];
const score = docMarkup.root.getBlock("score");
if (score) {
const layout = score.body.find(term => term instanceof Block && term.head === "\\layout") as Block;
if (layout)
layout.body.forEach(term => layoutBody.push(term.clone()));
const midi = score.body.find(term => term instanceof Block && term.head === "\\midi") as Block;
if (midi)
midi.body.forEach(term => midiBody.push(term.clone()));
}
if (layoutBody.length || midiBody.length) {
const thisScore = this.root.getBlock("score");
if (thisScore) {
const layout = thisScore.body.find(term => term instanceof Block && term.head === "\\layout") as Block;
if (layout)
layout.body.push(...layoutBody);
const midi = thisScore.body.find(term => term instanceof Block && term.head === "\\midi") as Block;
if (midi)
midi.body.push(...midiBody);
}
}
}
getVariables (): Set<string> {
return new Set(this.root.findAll(Variable).map(variable => variable.name));
}
// deprecated
getMusicTracks ({expand = false} = {}): MusicBlock[] {
const score = this.root.getBlock("score");
if (!score)
return null;
let tracks = [];
// extract sequential music blocks from score block
score.forEachTopTerm(MusicBlock, block => {
tracks.push(block);
});
// expand variables in tracks
if (expand)
tracks = tracks.map(track => track.clone().expandVariables(this.root));
return tracks;
}
getLocationTickTable (): {[key: string]: number} {
const notes = this.root.findAll(term => (term instanceof ChordElement) || (term instanceof MusicEvent));
return notes.reduce((table, note) => {
if (note._location && Number.isFinite(note._tick))
table[`${note._location.lines[0]}:${note._location.columns[0]}`] = note._tick;
return table;
}, {});
}
// update terms' _location data according to a serialized source
relocate (source: string = this.toString()) {
this.root.relocate(source);
}
appendIncludeFile (filename: string) {
if (!this.root.includeFiles.includes(filename)) {
const versionPos = this.root.sections.findIndex(term => term instanceof Version);
this.root.sections.splice(versionPos + 1, 0,
new Include({cmd: "include", args: [LiteralString.fromString(filename)]}));
}
}
removeStaffGroup () {
const score = this.root.getBlock("score");
if (score) {
score.body.forEach(item => {
if (item instanceof SimultaneousList)
item.removeStaffGroup();
});
}
}
fixTinyTrillSpans () {
// TODO: replace successive \startTrillSpan & \stopTrillSpan with ^\trill
}
removeMusicCommands (cmds: string | string[]) {
cmds = Array.isArray(cmds) ? cmds : [cmds];
const isToRemoved = item => (item instanceof Command) && cmds.includes(item.cmd);
this.root.forEachTerm(MusicBlock, block => {
block.body = block.body.filter(item => !isToRemoved(item));
});
}
removeTrillSpans () {
this.removeMusicCommands(["startTrillSpan", "stopTrillSpan"]);
}
removeBreaks () {
this.removeMusicCommands("break");
}
removePageBreaks () {
this.removeMusicCommands("pageBreak");
}
scoreBreakBefore (enabled = true) {
const score = this.root.getBlock("score");
if (score) {
let header = score.entries.find((entry: any) => entry.head === "\\header") as Block;
if (!header) {
header = new Block({head: "\\header", body: []});
score.body.push(header);
}
let breakbefore = header.getField("breakbefore");
if (breakbefore)
breakbefore = breakbefore.value;
else {
breakbefore = new Scheme({exp: true});
header.body.push(new Assignment({key: "breakbefore", value: breakbefore}));
}
breakbefore.exp = enabled;
}
else
console.warn("no score block");
}
unfoldRepeats () {
const score = this.root.getBlock("score");
const musicList = score ? score.body : this.root.sections;
let count = 0;
musicList.forEach((term, i) => {
if (term.isMusic && (term as Command).cmd !== "unfoldRepeats") {
const unfold = new Command({cmd: "unfoldRepeats", args: [term]});
musicList.splice(i, 1, unfold);
++count;
}
});
if (!count)
console.warn("no music term to unfold");
return count;
}
containsRepeat (): boolean {
const termContainsRepeat = (term: BaseTerm): boolean => {
if (!term.entries)
return false;
const subTerms = term.entries.filter(term => term instanceof BaseTerm);
for (const term of subTerms) {
if ((term as Command).cmd === "repeat")
return true;
}
for (const term of subTerms) {
if (termContainsRepeat(term))
return true;
}
return false;
};
return termContainsRepeat(this.root);
}
removeEmptySubMusicBlocks () {
this.root.forEachTerm(MusicBlock, block => {
block.body = block.body.filter(term => !(term instanceof MusicBlock && term.body.length === 0));
});
}
mergeContinuousGraces () {
this.removeEmptySubMusicBlocks();
const isGraceCommand = term => term instanceof Grace;
const isGraceInnerTerm = term => isGraceCommand(term) || term instanceof Divide || term instanceof PostEvent;
this.root.forEachTerm(MusicBlock, block => {
const groups = [];
let currentGroup = null;
block.body.forEach((term, i) => {
if (currentGroup) {
if (isGraceInnerTerm(term)) {
currentGroup.count++;
if (currentGroup.count === 2)
groups.push(currentGroup);
}
else
currentGroup = null;
}
else {
if (isGraceCommand(term))
currentGroup = {start: i, count: 1};
}
});
let offset = 0;
groups.forEach(group => {
const startIndex = group.start + offset;
const mainBody = new MusicBlock({body: []});
for (let i = startIndex; i < startIndex + group.count; ++ i) {
const term = block.body[i];
const music = isGraceCommand(term) ? term.args[0] : term;
if (music instanceof MusicBlock)
mainBody.body.push(...music.body);
else
mainBody.body.push(music);
}
block.body[startIndex].args[0] = mainBody;
block.body.splice(startIndex + 1, group.count - 1);
offset -= group.count - 1;
});
});
}
mergeContinuousEmptyAfterGraces () {
const isEmptyAfterGrace = term => term instanceof AfterGrace && term.args[0] instanceof MusicBlock && term.args[0].body.length === 0;
const isGraceInnerTerm = term => isEmptyAfterGrace(term) || term instanceof Divide || term instanceof PostEvent;
this.root.forEachTerm(MusicBlock, block => {
const groups = [];
let currentGroup = null;
block.body.forEach((term, i) => {
if (currentGroup) {
if (isGraceInnerTerm(term)) {
currentGroup.count++;
if (currentGroup.count === 2)
groups.push(currentGroup);
}
else
currentGroup = null;
}
else {
if (isEmptyAfterGrace(term))
currentGroup = {start: i, count: 1};
}
});
let offset = 0;
groups.forEach(group => {
const startIndex = group.start + offset;
const mainBody = new MusicBlock({body: []});
for (let i = startIndex; i < startIndex + group.count; ++ i) {
const term = block.body[i];
const music = isEmptyAfterGrace(term) ? term.args[1] : term;
if (music instanceof MusicBlock)
mainBody.body.push(...music.body);
else
mainBody.body.push(music);
}
block.body[startIndex].args[1] = mainBody;
block.body.splice(startIndex + 1, group.count - 1);
offset -= group.count - 1;
});
});
}
fixInvalidKeys (mode = "major") {
this.root.forEachTerm(Command, cmd => {
if (cmd.cmd === "key") {
if (cmd.args[1] === "\\none")
cmd.args[1] = "\\" + mode;
}
});
}
fixInvalidBriefChords () {
this.root.forEachTerm(BriefChord, chord => {
const items = chord.body.items;
if (items) {
// merge multiple ^ items
while (items.filter(item => item === "^").length > 1) {
const index = items.lastIndexOf("^");
items.splice(index, 1, ".");
}
}
});
}
fixInvalidMarkupWords () {
this.root.forEachTerm(MarkupCommand, cmd => {
//console.log("markup:", cmd);
cmd.forEachTerm(InlineBlock, block => {
// replace scheme expression by literal string
block.body = block.body.map(term => {
if (term instanceof Scheme)
return LiteralString.fromString(term.join().replace(/\s+$/, ""));
if (typeof term === "string" && term.includes("$"))
return LiteralString.fromString(term);
return term;
});
});
});
}
fixNestedRepeat () {
// \repeat { \repeat { P1 } \alternative { {P2} } } \alternative { {P3} }
// ->
// \repeat { P1 } \alternative { {P2} {P3} }
this.root.forEachTerm(Command, cmd => {
if (cmd.isRepeatWithAlternative) {
const block = cmd.args[2];
const alternative = cmd.args[3].args[0];
const lastMusic = block.body[block.body.length - 1];
if (lastMusic && lastMusic.isRepeatWithAlternative) {
block.body.splice(block.body.length - 1, 1, ...lastMusic.args[2].body);
alternative.body = [...lastMusic.args[3].args[0].body, ...alternative.body];
}
}
});
}
fixEmptyContextedStaff () {
// staff.1 << >> staff.2 << voice.1 {} voice.2 {} >>
// ->
// staff.1 << voice.1 {} >> staff.2 << voice.2 {} >>
const subMusics = (simul: SimultaneousList) => simul.list.filter(term => term instanceof ContextedMusic);
const score = this.root.getBlock("score");
score.forEachTerm(SimultaneousList, simul => {
const staves = simul.list.filter(term => term instanceof ContextedMusic && term.body instanceof SimultaneousList);
if (staves.length > 1) {
const staff1 = staves[0].body;
const staff2 = staves[1].body;
if (subMusics(staff1).length === 0 && subMusics(staff2).length > 1) {
const index = staff2.list.findIndex(term => term instanceof ContextedMusic);
const [music] = staff2.list.splice(index, 1);
staff1.list.push(music);
}
}
});
}
removeEmptyContextedStaff () {
const subMusics = (simul: SimultaneousList) => simul.list.filter(term => term instanceof ContextedMusic);
const score = this.root.getBlock("score");
score.forEachTerm(SimultaneousList, simul => {
simul.list = simul.list.filter(term => !(term instanceof ContextedMusic) || !(term.body instanceof SimultaneousList)
|| subMusics(term.body).length > 0);
});
}
redivide () {
this.root.forEachTopTerm(MusicBlock, (block: MusicBlock) => block.redivide());
}
makeMIDIDedicatedScore (): Block {
const block = this.root.findFirst(term => term instanceof Block && term.head === "\\score" && term.isMIDIDedicated) as Block;
if (block)
return block;
const score = this.root.getBlock("score");
const newScore = score.clone();
newScore.body = newScore.body.filter(term => !(term instanceof Block && term.head === "\\layout"));
score.body = score.body.filter(term => !(term instanceof Block && term.head === "\\midi"));
this.root.sections.push(newScore);
return newScore;
}
excludeChordTracksFromMIDI () {
// if there is chord mode music in score, duplicate score block as a dedicated MIDI score which excludes chord mode music.
let contains = false;
const isChordMusic = term => term instanceof ContextedMusic
&& term.head instanceof Command && term.head.args[0] === "ChordNames";
const isBlock = (head, term) => term instanceof Block && term.head === head;
// TODO: midiMusic forked in interpreter issue
//this.abstractMainScore();
const score = this.root.getBlock("score");
const newScore = score.clone() as Block;
newScore.forEachTerm(SimultaneousList, simul => {
const trimmedList = simul.list.filter(term => !isChordMusic(term));
if (trimmedList.length < simul.list.length) {
simul.list = trimmedList;
contains = true;
}
});
newScore._headComment = Comment.createSingle(" midi output");
if (contains) {
const trimmedBody = score.body.filter(term => !isBlock("\\midi", term));
if (trimmedBody.length < score.body.length) {
score.body = trimmedBody;
newScore.body = newScore.body.filter(term => !isBlock("\\layout", term));
this.root.sections.push(newScore);
}
}
}
// [deprecated]
// generate tied notehead location candidates
getTiedNoteLocations (source: TextSource): DocLocation[] {
const chordPairs: [Chord, Chord][] = [];
const hasMusicBlock = term => {
if (term instanceof MusicBlock)
return true;
if (term instanceof Command)
return term.args.filter(arg => arg instanceof MusicBlock).length > 0;
return false;
};
this.root.forEachTerm(MusicBlock, (block: MusicBlock) => {
for (const voice of block.voices) {
let lastChord: Chord = null;
let tying = false;
let afterBlock = false;
let atHead = true;
for (const chunk of voice.body) {
for (const term of chunk.terms) {
if (term instanceof Primitive && term.exp === "~") {
tying = true;
afterBlock = false;
}
else if (hasMusicBlock(term)) {
afterBlock = true;
tying = false;
//console.log("afterBlock:", term);
}
else if (term instanceof Chord) {
if (tying && lastChord)
chordPairs.push([lastChord, term]);
// maybe there is a tie at tail of the last block
else if (afterBlock)
chordPairs.push([null, term]);
// maybe there is a tie before the current block
else if (atHead)
chordPairs.push([null, term]);
// PENDING: maybe some user-defined command block contains tie at tail.
atHead = false;
afterBlock = false;
tying = false;
lastChord = term;
if (term.post_events) {
for (const event of term.post_events) {
if (event instanceof PostEvent && event.arg === "~")
tying = true;
}
}
}
}
}
}
});
//console.log("chordPairs:", chordPairs);
const locations = [];
chordPairs.forEach(pair => {
const forePitches = pair[0] && new Set(pair[0].pitchNames);
const chordSource = source.slice(pair[1]._location.lines, pair[1]._location.columns);
const pitchColumns = TextSource.matchPositions(/\w+/g, chordSource);
pair[1].pitchNames
.map((pitch, index) => ({pitch, index}))
.filter(({pitch}) => !forePitches || forePitches.has(pitch) || pitch === "q")
.forEach(({index}) => locations.push([
pair[1]._location.lines[0], // line
pair[1]._location.columns[0] + pitchColumns[index], // column
]));
});
return locations;
}
// generate tied notehead location candidates
getTiedNoteLocations2 (): DocLocation[] {
const locations = [];
this.root.forEachTerm(Chord, chord => chord.pitches.forEach(pitch => {
if (pitch._tied)
locations.push([pitch._location.lines[0], pitch._location.columns[0]]);
}));
return locations;
}
getBriefChordLocations (): DocLocation[] {
const locations = [];
this.root.forEachTerm(BriefChord,
chord => locations.push([chord._location.lines[0], chord._location.columns[0]]));
return locations;
}
getLyricLocations (): DocLocation[] {
const locations = [];
this.root.forEachTerm(Lyric,
lyric => locations.push([lyric._location.lines[0], lyric._location.columns[0]]));
return locations;
}
/*removeAloneSpacer () {
this.root.forEachTopTerm(MusicBlock, block => {
const aloneSpacers = cc(block.musicChunks.filter(chunk => chunk.size === 1 && chunk.terms[0].isSpacer).map(chunk => chunk.terms));
//console.log("aloneSpacers:", aloneSpacers.map(s => s._location));
if (aloneSpacers.length) {
const removeInBlock = block => block.body = block.body.filter(term => !aloneSpacers.includes(term));
removeInBlock(block);
block.forEachTerm(MusicBlock, removeInBlock);
}
});
}*/
unfoldDurationMultipliers () {
this.root.forEachTerm(MusicBlock, block => {
block.unfoldDurationMultipliers();
});
}
appendMIDIInstrumentsFromName () {
const isSet = (term: BaseTerm, keyPattern: RegExp): boolean => term instanceof Command && term.cmd === "set" && keyPattern.test((term.args[0] as Assignment).key.toString());
const append = (body: BaseTerm[]) => {
const ntIndex = body.findIndex(term => isSet(term, /\.instrumentName/));
if (ntIndex >= 0 && !body.some(term => isSet(term, /\.midiInstrument/))) {
const nameAssign = (body[ntIndex] as Command).args[0] as Assignment;
const key = nameAssign.key.toString().replace(/\.instrumentName/, ".midiInstrument");
body.splice(ntIndex + 1, 0, Command.createSet(key, nameAssign.value));
}
};
this.root.forEachTopTerm(Block, block => {
if (block.head === "\\score") {
block.forEachTerm(SimultaneousList, simu => append(simu.list));
block.forEachTerm(MusicBlock, musicBlock => append(musicBlock.body));
}
});
}
useMidiInstrumentChannelMapping () {
this.appendMIDIInstrumentsFromName();
const midiBlock = this.root.findFirst(term => term instanceof Block && term.head === "\\midi") as Block;
if (!midiBlock) {
console.warn("no MIDI block found.");
return;
}
const channelMapping = midiBlock.findFirst(term => term instanceof Assignment && term.key === "midiChannelMapping") as Assignment;
if (channelMapping)
channelMapping.value = new Scheme({exp: new SchemePointer({value: "instrument"})});
else {
midiBlock.body.push(parseRaw({
proto: "Block",
block: "context",
head: "\\context",
body: [
{proto: "Command",cmd: "Score",args: []},
{proto: "Assignment", key: "midiChannelMapping", value: {proto: "Scheme", exp: {proto: "SchemePointer", value: "instrument"}}},
],
}));
}
}
formalize () {
if (!this.root.findFirst(Version))
this.root.sections.unshift(Version.default);
if (!this.root.findFirst(Language))
this.root.sections.splice(1, 0, Language.make("english"));
if (!this.root.getBlock("header"))
this.root.sections.splice(2, 0, new Block({block: "header", head: "\\header", body:[]}));
if (!this.root.getBlock("score")) {
const topMusics = this.root.sections.filter(section => section.isMusic);
this.root.sections = this.root.sections.filter(section => !section.isMusic);
const score = new Block({block: "score", head: "\\score", body: [
...topMusics,
new Block({block: "score", head: "\\layout", body: []}),
new Block({block: "score", head: "\\midi", body: []}),
]});
this.root.sections.push(score);
}
}
convertStaffToPianoStaff () {
const score = this.root.getBlock("score");
if (score) {
const pstaff = score.findFirst(term => term instanceof ContextedMusic && term.head.cmd === "new" && term.head.args[0] === "Staff") as ContextedMusic;
if (pstaff) {
pstaff.head.args[0] = "PianoStaff";
if (pstaff.body instanceof SimultaneousList) {
pstaff.body.list = [].concat(...pstaff.body.list.map(term => {
if (term instanceof ContextedMusic) {
const subMusics = term.list.filter(sub => sub instanceof ContextedMusic);
return subMusics.map(music => {
const staff = term.clone();
staff.list = [
...term.list.filter(sub => !(sub instanceof ContextedMusic)),
music,
];
staff.head.cmd = "new";
return staff;
});
}
else
return [term];
}));
}
}
}
}
pruneStemDirections () {
this.root.forEachTerm(MusicBlock, block => {
let direction = null;
const redundants = [];
block.body.forEach(term => {
if (term instanceof StemDirection) {
if (term.direction === direction)
redundants.push(term);
else
direction = term.direction;
}
else if (term instanceof Command && term.findFirst(MusicBlock))
direction = null;
});
block.body = block.body.filter(term => !redundants.includes(term));
});
}
removeRepeats () {
this.root.forEachTerm(MusicBlock, block => block.spreadRepeatBlocks());
}
articulateMIDIOutput () {
const ARTICULATE_FILENAME = "articulate-lotus.ly";
this.abstractMainScore();
const midiScore = this.makeMIDIDedicatedScore();
if (!this.root.includeFiles.includes(ARTICULATE_FILENAME)) {
let pos = this.root.sections.indexOf(midiScore);
if (pos < 0)
pos = Math.min(this.root.sections.length, 3);
this.root.sections.splice(pos, 0, Include.create(ARTICULATE_FILENAME));
}
midiScore.body = midiScore.body.map(term => {
if (term.isMusic && !(term instanceof Command && term.cmd === "articulate"))
return new Command({cmd: "articulate", args: [term]});
return term;
});
}
removeInvalidExpressionsOnRests (): number {
const isInvalidPostEvent = (event: PostEvent | string): boolean =>
[".", "!", "_"].includes(event instanceof PostEvent ? event.arg as string : event);
let count = 0;
this.root.forEachTerm(MusicEvent, (term: MusicEvent) => {
if (term.isRest) {
if (term.post_events.some(isInvalidPostEvent)) {
term.post_events = term.post_events.filter(event => !isInvalidPostEvent(event));
++count;
}
}
});
return count;
}
abstractMainScore () {
const score = this.root.getBlock("score");
const music = score.body.find(term => term.isMusic);
if (music && !(music instanceof Variable)) {
const sectionIndex = this.root.sections.indexOf(score);
const assignment = new Assignment({
key: MAIN_SCORE_NAME,
value: music,
});
this.root.sections.splice(sectionIndex, 0, assignment);
score.body = score.body.map(term => term === music ? new Variable({name: MAIN_SCORE_NAME}) : term);
}
}
absoluteBlocksToRelative () {
this.root.forEachTopTerm(Assignment, assignment => {
if (assignment.value instanceof MusicBlock) {
const relative = assignment.value.absoluteToRelative();
if (relative)
assignment.value = relative;
}
});
}
};