Spaces:
Sleeping
Sleeping
| 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<string, BaseTerm> = 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<string> = new Set; | |
| statements: BaseTerm[] = []; | |
| paper: Block = null; | |
| layout: Block = null; | |
| scores: Block[] = []; | |
| layoutMusic: MusicPerformance; | |
| midiMusic: MusicPerformance; | |
| functionalCommand?: Variable; | |
| reservedVariables: Set<string> = 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<string>) { | |
| 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; | |
| } | |
| }; | |