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; 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 { 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; } }); } };