import {romanize} from "../romanNumeral"; import {WHOLE_DURATION_MAGNITUDE, GRACE_DURATION_FACTOR, FUNCTIONAL_VARIABLE_NAME_PATTERN, MAIN_SCORE_NAME, lcmMulti, lcm} from "./utils"; import {parseRaw, getDurationSubdivider, MusicChunk, constructMusicFromMeasureLayout, StemDirection} from "./lilyTerms"; import LogRecorder from "../logRecorder"; import {StaffContext, PitchContextTable} from "../pitchContext"; import * as idioms from "./idioms"; import LilyDocument from "./lilyDocument"; import * as LilyNotation from "../lilyNotation"; import { BaseTerm, Root, Block, MusicEvent, Repeat, Relative, TimeSignature, Partial, Times, Tuplet, Grace, AfterGrace, Clef, Scheme, Include, Rest, KeySignature, OctaveShift, Duration, Chord, MusicBlock, Assignment, Variable, Command, SimultaneousList, ContextedMusic, Primitive, Version, ChordMode, LyricMode, ChordElement, Language, PostEvent, Transposition, ParallelMusic, } from "./lilyTerms"; import {MeasureLayout, BlockMLayout, SingleMLayout} from "../measureLayout"; interface DurationContextStackStatus { factor?: {value: number}; tickBias?: number; tremoloDuration?: Duration; }; type MusicTransformer = (music: BaseTerm, context: TrackContext) => BaseTerm[]; type MusicListener = (music: BaseTerm, context: TrackContext) => void; type ContextDict = {[key: string]: string}; export enum TremoloType { None, Single, Pitcher, Catcher, }; interface PitchContextTerm { staffName: string; track?: number; //voiceName?: string; tick?: number; event: MusicEvent; clef?: { y: number, value: number, }; octaveShift?: number; key?: number; newMeasure?: boolean; pitches?: ChordElement[]; tickBias?: number; rest?: boolean; tremoloType?: TremoloType; }; class LilyStaffContext extends StaffContext { staffTrack: number; notes: LilyNotation.Note[] = []; channelMap: number[] = []; executeTerm (term: PitchContextTerm) { //console.log("executeTerm:", term); if (term.newMeasure) this.resetAlters(); if (term.clef) this.setClef(term.clef.y, term.clef.value); if (Number.isFinite(term.octaveShift)) this.setOctaveShift(-term.octaveShift); if (Number.isFinite(term.key)) { this.resetKeyAlters(); if (term.key) { const step = term.key > 0 ? 1 : -1; for (let p = step; p / term.key <= 1; p += step) { const index = ((step > 0 ? p - 1 : p) + 70) % 7; const note = idioms.PHONETS.indexOf(idioms.FIFTH_PHONETS[index]); this.keyAlters[note] = (this.keyAlters[note] || 0) + step; } this.dirty = true; } } if (term.pitches) { // accidental alters term.pitches.forEach(pitch => { const note = pitch.absoluteNotePosition; const alter = this.alterOnNote(note); if (pitch.alterValue !== alter) { this.alters[note] = pitch.alterValue; this.dirty = true; } }); const event = term.event; const contextIndex = this.snapshot({tick: event._tick}); const implicitType = event.implicitType || (term.tremoloType ? LilyNotation.ImplicitType.Tremolo : LilyNotation.ImplicitType.None); this.notes.push(...term.pitches.map((pitch, index) => ({ track: term.track, channel: this.channelMap[term.track] || 0, measure: event._measure, start: event._tick, duration: event.durationMagnitude, division: event.division, startTick: event._tick, endTick: event._tick + event.durationMagnitude, pitch: pitch.absolutePitchValue + (pitch._transposition || 0), velocity: 127, id: pitch.href, ids: [pitch.href], tied: !!pitch._tied, rest: event.isRest, afterGrace: !!term.tickBias, implicitType, staffTrack: this.staffTrack, contextIndex, // TODO: consider connected arpeggio & downward arpeggio chordPosition: { index, count: term.pitches.length, }, }))); term.pitches.forEach(pitch => { const tiedParent = pitch.tiedParent; if (tiedParent) { const note = this.notes.find(note => note.id === tiedParent.href); if (note) note.ids.push(pitch.href); } }); } else if (term.rest) { const event = term.event; const contextIndex = this.snapshot({tick: event._tick}); this.notes.push({ track: term.track, channel: this.channelMap[term.track] || 0, measure: event._measure, start: event._tick, duration: event.durationMagnitude, startTick: event._tick, endTick: event._tick + event.durationMagnitude, pitch: null, velocity: 0, id: event.href, ids: [event.href], tied: false, rest: true, afterGrace: !!term.tickBias, implicitType: event.implicitType, staffTrack: this.staffTrack, contextIndex, }); } } get pitchContextTable (): PitchContextTable { const items = this.track.contexts.map(context => ({ tick: context.tick, endTick: null, context, })); items.forEach((item, i) => { item.endTick = (i + 1 < items.length ? items[i + 1].tick : Infinity); }); return new PitchContextTable({items}); } }; export class MusicTrack { block: MusicBlock; anchorPitch: ChordElement; contextDict?: ContextDict = null; name?: string; measureHeads: number[]; static fromBlockAnchor (block: MusicBlock, anchorPitch: ChordElement): MusicTrack { const track = new MusicTrack; track.block = block; track.anchorPitch = anchorPitch; const context = new TrackContext(track); context.execute(track.music); return track; } get music (): BaseTerm { if (!this.block._parent) { this.block._parent = new Relative({cmd: "relative", args: this.anchorPitch ? [this.anchorPitch.clone(), this.block] : [this.block]}); this.block.updateChordAnchors(); } return this.block._parent; } get noteDurationSubdivider (): number { return getDurationSubdivider(this.block); } get durationMagnitude (): number { return this.block && this.block.durationMagnitude; } get isLyricMode (): boolean { return (this.music instanceof LyricMode) || !!this.block.findFirst(term => term instanceof LyricMode); } get isChordMode (): boolean { return (this.music instanceof ChordMode) || !!this.block.findFirst(term => term instanceof ChordMode); } get measureLayoutCode (): string { let code = this.block.measureLayout.code; if (/^\[.*\]$/.test(code)) code = code.match(/\[(.*)\]/)[1]; return code; } transform (transformer: MusicTransformer) { new TrackContext(this, {transformer}).execute(this.music); } clarifyDurations () { this.transform(term => { if (term instanceof MusicEvent) { if (!term.duration) term.duration = term.durationValue; } return [term]; }); } splitLongRests () { this.clarifyDurations(); this.transform((term, context) => { if (!(term instanceof MusicEvent) || (!term.withMultiplier && !(term instanceof Rest))) return [term]; const timeDenominator = context.time ? context.time.value.denominator : 4; const duration = term.durationValue; const denominator = Math.max(duration.denominator, timeDenominator); const isR = !(term as Rest).isSpacer; if (term.withMultiplier) { const factor = duration.multipliers.reduce((factor, multiplier) => factor * Number(multiplier), 1); if (!Number.isInteger(factor) || factor <= 0) { console.warn("invalid multiplier:", factor, duration.multipliers); return [term]; } const event = term.clone() as MusicEvent; event.duration.multipliers = []; // break duration into multiple rest events const restCount = (event.duration.magnitude / WHOLE_DURATION_MAGNITUDE) * (factor - 1) * denominator; if (!Number.isInteger(restCount)) console.warn("Rest count is not integear:", restCount, denominator, event.duration.magnitude, factor); const rests = Array(Math.floor(restCount)).fill(null).map(() => new Rest({name: "s", duration: new Duration({number: denominator, dots: 0})})); return [event, ...rests]; } else { const divider = lcm(duration.subdivider, denominator); const restCount = term.durationMagnitude * divider / WHOLE_DURATION_MAGNITUDE; console.assert(Number.isInteger(restCount), "rest count is not an integer:", restCount); if (isR && restCount > 1) console.warn("splitLongRests: 'r' was splitted into", restCount, "parts.", term._location); const list = Array(restCount).fill(null).map(() => new Rest({name: isR ? "r" : "s", duration: new Duration({number: divider, dots: 0})})); if (term.post_events) list[list.length - 1].post_events = term.post_events.map(e => e instanceof BaseTerm ? e.clone() : e); return list; } }); } spreadMusicBlocks (): boolean { let has = false; this.transform((term) => { if (term instanceof MusicBlock) { has = true; return term.body; } else return [term]; }); return has; } spreadRelativeBlocks (): boolean { // check if traverse is nessary if (!this.block.findFirst(Relative)) return false; this.transform((term, context) => { if (term instanceof Relative) { if (term.music instanceof MusicBlock) term.music.updateChordAnchors(); const terms = term.shiftBody(context.pitch); // initialize anchor pitch for track head chord if (!context.event || !context.event.getPreviousT(Chord)) { const tempBlock = new MusicBlock({body: []}); tempBlock.body = terms; const head = tempBlock.findFirst(Chord); if (head) //head._anchorPitch = this.anchorPitch; head._anchorPitch = context.pitch; } return terms; } else return [term]; }); return true; } spreadRepeatBlocks ({ignoreRepeat = true, keepTailPass = false} = {}): boolean { // check if traverse is nessary if (!this.block.findFirst(Repeat)) return false; this.transform(term => { if (term instanceof Repeat) { if (!ignoreRepeat) return term.getUnfoldTerms(); else if (keepTailPass) return term.getTailPassTerms(); else return term.getPlainTerms(); } else if (term instanceof Variable && term.name === "lotusRepeatABA") return []; else return [term]; }); return true; } flatten ({spreadRepeats = false} = {}) { this.splitLongRests(); this.spreadRelativeBlocks(); if (spreadRepeats) { while (this.spreadRepeatBlocks()) ; // expand all music blocks while (this.spreadMusicBlocks()); } } sliceMeasures (start: number, count: number): MusicTrack { this.flatten({spreadRepeats: true}); const context = new TrackContext(this); context.pitch = this.anchorPitch; this.block.updateChordAnchors(); for (const term of this.block.body) { if (Number.isInteger(term._measure)) { if (term._measure < start) context.execute(term); else break; } } const terms = context.declarations.concat(this.block.body.filter(term => term._measure >= start && term._measure < start + count)); const newBlock = MusicBlock.fromTerms(terms); return MusicTrack.fromBlockAnchor(newBlock, context.pitch); } redivide () { this.block.redivide({measureHeads: this.measureHeads}); } applyMeasureLayout (layout: MeasureLayout, {flatten = true} = {}) { //console.log("applyMeasureLayout:", this, layout); if (flatten) this.flatten({spreadRepeats: true}); const chunks = this.block.measureChunkMap; // validate layout value const indices = layout.serialize(LilyNotation.LayoutType.Ordinary); indices.forEach(index => { if (!chunks.get(index)) throw new Error(`applyMeasureLayout: measure[${index}] missed in chunk map.`); }); // append zero-duration tail chunk, e.g. \bar "|." const tailIndex = Math.max(...indices) + 1; const tailChunk = chunks.get(tailIndex); if (tailChunk && !tailChunk.durationMagnitude && layout instanceof BlockMLayout) //layout.seq.push(SingleMLayout.from(tailIndex)); layout = BlockMLayout.fromSeq([...layout.seq, SingleMLayout.from(tailIndex)]); this.block.body = constructMusicFromMeasureLayout(layout, chunks).terms; this.redivide(); } generateStaffTracks (): PitchContextTerm[] { const pcTerms: PitchContextTerm[] = []; let currentTerm = null; const commitTerm = () => { if (currentTerm) { pcTerms.push(currentTerm); currentTerm = null; } }; const getCurrentTerm = (staffName: string): PitchContextTerm => { if (currentTerm && currentTerm.staffName !== staffName) commitTerm(); if (!currentTerm) currentTerm = {staffName}; return currentTerm; }; let measureIndex = 0; const listener = (term: BaseTerm, track: TrackContext) => { getCurrentTerm(track.staffName).tick = term._tick; if (term._measure !== measureIndex) { getCurrentTerm(track.staffName).newMeasure = true; commitTerm(); measureIndex = term._measure; } if (term instanceof Chord) { const pcTerm = getCurrentTerm(track.staffName); pcTerm.event = term; pcTerm.pitches = term.pitchesValue.filter(pitch => pitch instanceof ChordElement) as ChordElement[]; pcTerm.pitches = [...pcTerm.pitches].sort((p1, p2) => p1.absolutePitchValue - p2.absolutePitchValue); if (track.tickBias) pcTerm.tickBias = track.tickBias; pcTerm.tremoloType = track.tremoloType; commitTerm(); } else if (term instanceof Rest && term.name !== "s") { const pcTerm = getCurrentTerm(track.staffName); pcTerm.event = term; pcTerm.rest = true; commitTerm(); } else if (term instanceof Clef) { //console.log("clef:", term.clefName); switch (term.clefName) { case "treble": // a treble (G4) on the 2nd staff line getCurrentTerm(track.staffName).clef = {y: 1, value: 4}; break; case "bass": // a bass (F3) on the 4th staff line getCurrentTerm(track.staffName).clef = {y: -1, value: -4}; break; case "tenor": // a tenor (C4) on the 3rd staff line getCurrentTerm(track.staffName).clef = {y: 0, value: 0}; break; } } else if (term instanceof KeySignature) getCurrentTerm(track.staffName).key = term.key; else if (term instanceof OctaveShift) getCurrentTerm(track.staffName).octaveShift = term.value; }; new TrackContext(this, {listener}).execute(this.music); return pcTerms; } }; export class TrackContext { track: MusicTrack; transformer?: MusicTransformer; listener?: MusicListener; stack: DurationContextStackStatus[] = []; // declarations staff: Command = null; clef: Clef = null; key: KeySignature = null; time: TimeSignature = null; octave: OctaveShift = null; pitch: ChordElement = null; staffName: string = null; voiceName: string = null; transposition: number = 0; // time status tick: number = 0; tickInMeasure: number = 0; measureSpan: number = WHOLE_DURATION_MAGNITUDE; measureIndex: number = 1; partialDuration: Duration = null; measureHeads: number[] = [0]; event: MusicEvent = null; tying: MusicEvent = null; staccato: boolean = false; inGrace: boolean = false; stemDirection: string = null; beamOn: boolean = false; tremoloType: TremoloType = TremoloType.None; constructor (track = new MusicTrack, {transformer = null, listener = null, contextDict = null}: { transformer?: MusicTransformer, listener?: MusicListener, contextDict?: ContextDict, } = {}) { this.track = track; this.track.contextDict = contextDict || this.track.contextDict; this.track.measureHeads = this.measureHeads; this.transformer = transformer; this.listener = listener; if (this.track.contextDict) { this.staffName = this.track.contextDict.Staff; this.voiceName = this.track.contextDict.Voice; } //console.debug("contextDict:", contextDict); } clone (): this { const ctx = {...this}; Object.setPrototypeOf(ctx, Object.getPrototypeOf(this)); return ctx; } mergeParallelClones (contexts: TrackContext[]) { const frontContext = contexts.reduce((front, context) => { const next = !front || context.tick > front.tick ? context : front; next.tying = next.tying || context.tying; next.staccato = next.staccato || context.staccato; return next; }, null); const lastContext = contexts[contexts.length - 1]; this.tick = frontContext.tick; this.tickInMeasure = frontContext.tickInMeasure; this.measureIndex = frontContext.measureIndex; this.partialDuration = frontContext.partialDuration; this.tying = frontContext.tying; this.staccato = frontContext.staccato; this.pitch = lastContext.pitch; this.event = lastContext.event; } get factor (): {value: number} { for (let i = this.stack.length - 1; i >= 0; i--) { const status = this.stack[i]; if (status.factor) return status.factor; } return null; } get tremoloDuration (): Duration { for (let i = this.stack.length - 1; i >= 0; i--) { const status = this.stack[i]; if (status.tremoloDuration) return status.tremoloDuration; } return null; } get tickBias (): number { for (let i = this.stack.length - 1; i >= 0; i--) { const status = this.stack[i]; if (status.tickBias) return status.tickBias; } return 0; } get measureIndexBias (): number { if (this.tickInMeasure + this.tickBias < -1) return -1; return 0; } get factorValue (): number { return this.factor && Number.isFinite(this.factor.value) ? this.factor.value : 1; } get currentMeasureSpan (): number { return Math.round(this.partialDuration ? this.partialDuration.magnitude : this.measureSpan); } setPitch (pitch: ChordElement) { if (!this.track.anchorPitch) this.track.anchorPitch = pitch; this.pitch = pitch; } newMeasure (measureSpan: number) { console.assert(Number.isFinite(this.measureHeads[this.measureIndex - 1]), "invalid measureHeads at", this.measureIndex - 1, this.measureHeads); this.measureHeads[this.measureIndex] = this.measureHeads[this.measureIndex - 1] + measureSpan; ++this.measureIndex; this.tickInMeasure -= measureSpan; this.partialDuration = null; } checkIncompleteMeasure () { if (this.tickInMeasure) { console.warn("incomplete measure trunated:", this.measureIndex, `${this.tickInMeasure}/${this.currentMeasureSpan}`); this.newMeasure(this.tickInMeasure); } } elapse (duration: number) { const increment = duration * this.factorValue; this.tick += increment; this.tickInMeasure += increment; while (Math.round(this.tickInMeasure) >= this.currentMeasureSpan) this.newMeasure(this.currentMeasureSpan); } push (status: DurationContextStackStatus) { this.stack.push(status); } pop () { this.stack.pop(); } processGrace (music: BaseTerm, factor = GRACE_DURATION_FACTOR) { // pull back grace notes' ticks let events = [music]; if (!(music instanceof MusicEvent)) events = music.findAll(MusicEvent); let tick = this.tick; events.reverse().forEach(event => { tick -= Math.round(event.durationMagnitude * factor * this.factorValue); event._tick = tick; event.findAll(ChordElement).forEach(note => note._tick = tick); }); } execute (term: BaseTerm) { if (!term) { console.warn("null term:", term); return; } if (!(term instanceof BaseTerm)) return; term._measure = this.measureIndex + this.measureIndexBias; term._tick = this.tick; if (term instanceof MusicEvent) { term._previous = this.event; if (term instanceof Chord) { if (!this.track.anchorPitch) this.track.anchorPitch = ChordElement.default.clone(); this.setPitch(term.absolutePitch); term.pitches.forEach(pitch => { this.execute(pitch); if (pitch instanceof ChordElement) pitch._transposition = this.transposition; }); // update tied for ChordElement // TODO: staccato trigger condition? if (this.tying /*&& !this.staccato*/ && this.event && this.event instanceof Chord) { const pitches = new Set(this.event.pitchElements.map(pitch => pitch.absolutePitch.pitch)); term.pitchElements.forEach(pitch => { if (pitches.has(pitch.absolutePitch.pitch)) pitch._tied = this.tying; //else // console.log("missed tie:", `${pitch._location.lines[0]}:${pitch._location.columns[0]}`, pitch.absolutePitch.pitch, pitches); }); if (this.staccato) console.warn("tie on staccato note:", term.href); } //console.log("chord:", term.pitches[0]); } if (term.beamOn) this.beamOn = true; else if (term.beamOff) this.beamOn = false; this.event = term; this.elapse(term.durationMagnitude); term._lastMeasure = this.tickInMeasure > 0 ? this.measureIndex : this.measureIndex - 1; this.tying = null; this.staccato = false; if (term.isTying) this.tying = term; if (term.isStaccato) this.staccato = true; } else if (term instanceof ChordElement) { // ignore } else if (term instanceof MusicBlock) { if (!this.track.block) this.track.block = term; term.updateChordAnchors(); if (this.transformer) { const body = []; for (const subterm of term.body) { const terms = this.transformer(subterm, this); terms.forEach(t => this.execute(t)); body.push(...terms); } term.body = body; } else { for (const subterm of term.body) this.execute(subterm); } } else if (term instanceof Command && term.cmd === "numericTimeSignature") this.execute(term.args[0]); else if (term instanceof TimeSignature) { this.time = term; this.measureSpan = term.value.value * WHOLE_DURATION_MAGNITUDE; } else if (term instanceof Partial) this.partialDuration = term.duration; else if (term instanceof Repeat) { switch (term.type) { case "volta": this.checkIncompleteMeasure(); this.execute(term.bodyBlock); this.checkIncompleteMeasure(); if (term.alternativeBlocks) { for (const block of term.alternativeBlocks) { this.execute(block); this.checkIncompleteMeasure(); } } break; case "tremolo": this.push({factor: {value: term.times}, tremoloDuration: term.sumDuration}); this.tremoloType = term.singleTremolo ? TremoloType.Single : TremoloType.Pitcher; this.execute(term.bodyBlock); this.tremoloType = TremoloType.None; this.pop(); break; default: console.warn("unsupported repeat type:", term.type); } } else if (term instanceof Relative) { if (term.anchor) this.setPitch(term.anchor); this.execute(term.music); } else if (term instanceof LyricMode) { // ignore lyric mode } else if (term instanceof Command && term.cmd === "lyricsto") { // ignore lyric mode } else if (term instanceof ChordMode) { // ignore chord mode } else if (term instanceof Transposition) this.transposition = term.transposition; else if (term instanceof Times) { this.push({factor: term.factor}); this.execute(term.music); this.pop(); } else if (term instanceof Tuplet) { this.push({factor: term.divider.reciprocal}); this.execute(term.music); this.pop(); } else if (term instanceof Grace) { this.inGrace = true; this.push({factor: {value: 0}}); this.execute(term.music); this.pop(); this.inGrace = false; this.processGrace(term.music); } else if (term instanceof AfterGrace) { this.execute(term.body); this.inGrace = true; this.push({factor: {value: 0}, tickBias: -term.body.durationMagnitude}); this.execute(term.grace); this.pop(); this.inGrace = false; this.processGrace(term.grace); } else if (term instanceof Clef) this.clef = term; else if (term instanceof KeySignature) this.key = term; else if (term instanceof OctaveShift) this.octave = term; else if (term instanceof Command && term.cmd === "change") { const pair = term.getAssignmentPair(); if (pair) { switch (pair.key) { case "Staff": this.staffName = pair.value.toString(); this.staff = term; break; case "Voice": this.voiceName = pair.value.toString(); break; } } } else if (term instanceof Primitive) { if (term.exp === "~") this.tying = this.event; } else if (term instanceof PostEvent) { if (term.isStaccato) this.staccato = true; } else if (term instanceof SimultaneousList) { const contexts: TrackContext[] = []; let lastContext = this; for (const subterm of term.list) { const context = this.clone(); context.pitch = lastContext.pitch; context.event = lastContext.event; context.execute(subterm); contexts.push(context); lastContext = context; } this.mergeParallelClones(contexts); } else if (term instanceof ContextedMusic) { // TODO: process contextDict this.execute(term.body); } else if (term instanceof StemDirection) this.stemDirection = term.direction; else { if (term.isMusic) console.warn("[TrackContext] unexpected music term:", term); } if (this.listener) this.listener(term, this); if (term instanceof MusicEvent && this.tremoloType === TremoloType.Pitcher) this.tremoloType = TremoloType.Catcher; } get declarations (): BaseTerm[] { return [this.staff, this.clef, this.key, this.time, this.octave].filter(term => term); } }; class MusicPerformance { staffNames: string[] = []; musicTracks: MusicTrack[] = []; get mainTrack (): MusicTrack { // find the longest track const trackPrior = (track: MusicTrack, index: number): number => -track.block.durationMagnitude + index * 1e-3; const priorTracks = this.musicTracks .filter(track => track.block._parent && !track.isChordMode && !track.isLyricMode) .map((track, index) => ({track, index})) .sort((t1, t2) => trackPrior(t1.track, t1.index) - trackPrior(t2.track, t2.index)); return priorTracks[0] ? priorTracks[0].track : null; } get trackNames (): string[] { return this.musicTracks.map(track => `${track.contextDict.Staff}:${track.contextDict.Voice}`); } get trackContextDicts (): ContextDict[] { const dicts = this.musicTracks.map(track => track.contextDict); dicts.unshift(undefined); // zero placeholder for track index from 1 in notation & SheetDocument return dicts; } get trackInstruments (): string[] { return this.musicTracks.map(track => { const instrumentKey = Object.keys(track.contextDict).find(key => /\.instrumentName/.test(key)); if (instrumentKey) return track.contextDict[instrumentKey]; return null; }); } get instrumentList (): string[] { return Array.from(new Set(this.trackInstruments)); } get channelMap (): number[] { const instrumentList = this.instrumentList; const channels = this.trackInstruments.map(instrument => instrumentList.indexOf(instrument) + 1); channels.unshift(0); return channels; } get measureLayoutCode (): string { return this.mainTrack && this.mainTrack.measureLayoutCode; } applyMeasureLayout (layout: MeasureLayout) { this.musicTracks.forEach(track => track.applyMeasureLayout(layout)); } getNotation ({logger = new LogRecorder()} = {}): LilyNotation.Notation { const pcTerms: PitchContextTerm[] = [].concat(...this.musicTracks.map((track, i) => track.generateStaffTracks().map(term => ({track: i + 1, ...term})))); //console.log("pcTerms:", pcTerms); const termsToContexts = (staffTerms: PitchContextTerm[], trackIndex: number): LilyStaffContext => { staffTerms.forEach(term => { if (term.event) term.tick = term.event._tick; }); staffTerms.sort((t1, t2) => t1.tick - t2.tick); const context = new LilyStaffContext({logger}); context.staffTrack = trackIndex; context.channelMap = this.channelMap; logger.append("staffTerms", staffTerms); //console.debug("staffTerms:", staffTerms); staffTerms.forEach(term => context.executeTerm(term)); return context; }; const staffContexts: LilyStaffContext[] = []; if (this.staffNames.length) { this.staffNames.forEach((name, trackIndex) => { const staffTerms = pcTerms.filter(term => term.staffName === name); staffContexts.push(termsToContexts(staffTerms, trackIndex)); }); } else staffContexts.push(termsToContexts(pcTerms, 0)); const notes = [] .concat(...staffContexts.map(context => context.notes)) .sort((n1, n2) => n1.startTick - n2.startTick); // append duration of tied notes to root note const pitchMap = {}; notes.forEach(note => { if (note.tied) { const root = pitchMap[note.pitch]; if (root) { root.endTick = Math.max(root.endTick, note.endTick); root.duration = root.endTick - root.startTick; } } else pitchMap[note.pitch] = note; }); const pitchContextGroup = staffContexts.map(context => context.pitchContextTable); const mainTrack = this.mainTrack; const measureHeads = mainTrack && mainTrack.measureHeads; const measureLayout = mainTrack && mainTrack.block.measureLayout; return LilyNotation.Notation.fromAbsoluteNotes(notes, measureHeads, {pitchContextGroup, measureLayout, trackNames: this.trackNames}); } getNoteDurationSubdivider (): number { const subdivider = lcmMulti(...this.musicTracks.map(track => track.noteDurationSubdivider)); return subdivider; } sliceMeasures (start: number, count: number) { this.musicTracks = this.musicTracks.map(track => { const newTrack = track.sliceMeasures(start, count); newTrack.name = track.name; // inherit name return newTrack; }); } }; export default class LilyInterpreter { variableTable: Map = new Map(); // temporary status musicTracks: MusicTrack[] = []; staffNames: string[] = []; musicTrackIndex: number = 0; musicPerformance: MusicPerformance; mainPerformance: MusicPerformance; version: Version = null; language: Language = null; header: Block = null; includeFiles: Set = new Set; statements: BaseTerm[] = []; paper: Block = null; layout: Block = null; scores: Block[] = []; layoutMusic: MusicPerformance; midiMusic: MusicPerformance; functionalCommand?: Variable; reservedVariables: Set = new Set(); static trackName (index: number): string { return `Voice_${romanize(index)}`; }; /*eval (term: BaseTerm): BaseTerm { return this.execute(term.clone()); }*/ get mainScore (): BaseTerm { return this.variableTable.get(MAIN_SCORE_NAME); }; interpretMusic (music: BaseTerm, contextDict: ContextDict): Variable { //console.log("interpretMusic:", music); const context = new TrackContext(undefined, {contextDict}); //context.execute(music.clone()); context.execute(music); context.track.spreadRelativeBlocks(); this.musicTracks.push(context.track); const varName = LilyInterpreter.trackName(++this.musicTrackIndex); context.track.name = varName; return new Variable({name: varName}); } interpretDocument (doc: LilyDocument): this { if (doc.reservedVariables) this.appendReservedVariables(doc.reservedVariables); this.execute(doc.root); return this; } createMusicPerformance () { if (this.musicTracks.length) { if (!this.musicPerformance) this.musicPerformance = new MusicPerformance(); this.staffNames.forEach(name => { if (!this.musicPerformance.staffNames.some(sn => sn === name)) this.musicPerformance.staffNames.push(name); else if (!name) console.warn("[LilyInterpreter] Multiple empty context staff name may cause error pitchContextTable:", this.musicPerformance.staffNames); }); this.musicTracks.forEach(track => this.musicPerformance.musicTracks.push(track)); this.staffNames = []; this.musicTracks = []; } } execute (term: BaseTerm, {execMusic = false, contextDict = {}}: {execMusic?: boolean, contextDict?: ContextDict} = {}): BaseTerm { if (!term) return term; if (this.functionalCommand && term.isMusic) { term._functional = this.functionalCommand.name; this.functionalCommand = null; } if (term instanceof Root) { for (const section of term.sections) { const sec = this.execute(section, {execMusic: true}); if (sec instanceof Version) this.version = sec; else if (sec instanceof Language) this.language = sec; else if (sec instanceof Scheme) this.statements.push(sec); else if (sec instanceof Block) { switch (sec.head) { case "\\header": this.header = sec; break; case "\\paper": this.paper = sec; break; case "\\layout": this.layout = sec; break; case "\\score": this.scores.push(sec); break; } } } this.createMusicPerformance(); if (this.musicPerformance) { this.layoutMusic = this.musicPerformance; this.midiMusic = this.musicPerformance; this.musicPerformance = null; } } else if (term instanceof Assignment) { if (term.key) { const name = term.key as string; const isMainScore = name === MAIN_SCORE_NAME; if (isMainScore) this.musicPerformance = null; const value = this.execute(term.value, {execMusic: isMainScore}); this.variableTable.set(name, value); if (isMainScore) this.mainPerformance = this.musicPerformance; } } else if (term instanceof Block) { switch (term.head) { case "\\score": const body = term.body.map(subterm => this.execute(subterm, {execMusic: true})); this.musicPerformance = null; return new Block({ block: term.block, head: term.head, body, }); case "\\layout": this.layoutMusic = this.musicPerformance; break; case "\\midi": this.midiMusic = this.musicPerformance; break; } } else if (term instanceof Variable) { const result = this.variableTable.get(term.name); if (!result) { if (FUNCTIONAL_VARIABLE_NAME_PATTERN.test(term.name)) { this.functionalCommand = term; return term; } else if (this.reservedVariables.has(term.name)) { // ignore reserved variables } else console.warn("uninitialized variable is referred:", term); } if (term.name === MAIN_SCORE_NAME) { this.musicPerformance = this.mainPerformance; return term; } return this.execute(result, {execMusic, contextDict}); } else if (term instanceof MusicBlock) { const result = new MusicBlock({ _parent: term._parent, _functional: term._functional, body: term.body.map(subterm => this.execute(subterm)).filter(Boolean), }); this.functionalCommand = null; if (execMusic) { const variable = this.interpretMusic(result, contextDict); return new MusicBlock({body: [variable]}); } return result; } else if (term instanceof SimultaneousList) { const list = term.list.map(subterm => this.execute(subterm, {execMusic, contextDict})).filter(term => term); this.createMusicPerformance(); return new SimultaneousList({list}); } else if (term instanceof ContextedMusic) { if (term.contextDict && typeof term.contextDict.Staff === "string") this.staffNames.push(term.contextDict.Staff); return new ContextedMusic({ head: this.execute(term.head), lyrics: this.execute(term.lyrics), body: this.execute(term.body, {execMusic, contextDict: {...contextDict, ...term.contextDict}}), }); } else if (term instanceof ParallelMusic) { term.voices.forEach(voice => { const block = new MusicBlock({ body: MusicChunk.join(voice.body), }); const value = this.execute(block); this.variableTable.set(voice.name, value); }); } else if (term instanceof Include) this.includeFiles.add(term.filename); else if (term instanceof Command) { switch (term.cmd) { case "set": if (term.args[0] instanceof Assignment) { const assign = term.args[0]; contextDict[assign.key.toString()] = assign.value.toString(); } break; } return parseRaw({proto: term.proto, cmd: term.cmd, args: term.args.map(arg => this.execute(arg, {execMusic, contextDict}))}); } return term; } updateTrackAssignments () { if (this.layoutMusic) this.layoutMusic.musicTracks.forEach(track => this.variableTable.set(track.name, track.music)); if (this.midiMusic && this.midiMusic !== this.layoutMusic) this.midiMusic.musicTracks.forEach(track => this.variableTable.set(track.name, track.music)); // update main score variable order in table const mainScore = this.mainScore; if (mainScore) { this.variableTable.delete(MAIN_SCORE_NAME); this.variableTable.set(MAIN_SCORE_NAME, mainScore); } } toDocument (): LilyDocument { this.updateTrackAssignments(); const variables = [].concat(...[this.paper, this.layout, ...this.scores, this.mainScore].filter(block => block).map(block => block.findAll(Variable).map(variable => variable.name))); const variablesUnique = Array.from(new Set(variables)); // sort variables by order in variable table const vars = [...this.variableTable.keys()]; variablesUnique.sort((v1, v2) => vars.indexOf(v1) - vars.indexOf(v2)); const assignments = variablesUnique.filter(name => this.variableTable.get(name)).map(name => new Assignment({key: name, value: this.variableTable.get(name)})); const includes = Array.from(this.includeFiles).map(filename => Include.create(filename)); const root = new Root({sections: [ this.version, this.language, this.header, ...includes, ...this.statements, this.paper, this.layout, ...assignments, ...this.scores, ].filter(section => section)}); const doc = new LilyDocument(root); doc.reservedVariables = this.reservedVariables; return doc; } sliceMeasures (start: number, count: number) { if (this.layoutMusic) this.layoutMusic.sliceMeasures(start, count); if (this.midiMusic && this.midiMusic !== this.layoutMusic) this.midiMusic.sliceMeasures(start, count); } addIncludeFile (filename: string) { this.includeFiles.add(filename); } appendReservedVariables (names: Iterable) { for (const name of names) this.reservedVariables.add(name); } getNotation ({logger = new LogRecorder()} = {}): LilyNotation.Notation { if (this.midiMusic) return this.midiMusic.getNotation({logger}); return null; } };