Spaces:
Sleeping
Sleeping
| 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; | |
| } | |
| }); | |
| } | |
| }; | |