import {WHOLE_DURATION_MAGNITUDE, FractionNumber, lcmMulti, gcd, MAIN_SCORE_NAME} from "./utils"; import * as idioms from "./idioms"; import {LILYPOND_VERSION} from "../constants"; import * as measureLayout from "../measureLayout"; import ImplicitType from "../lilyNotation/implicitType"; import pick from "../pick"; interface Location { lines: [number, number]; columns: [number, number]; }; abstract class Locator { location: Location; constructor (term: BaseTerm) { term._location = term._location || {lines: [0, 0], columns: [0, 0]}; this.location = term._location; } abstract set (line: number, column: number): void; }; class OpenLocator extends Locator { set (line: number, column: number) { this.location.lines[0] = line; this.location.columns[0] = column; } }; class CloseLocator extends Locator { set (line: number, column: number) { this.location.lines[1] = line; this.location.columns[1] = column; } }; // concat array of array const cc = (arrays: T[][]): T[] => [].concat(...arrays); export class MusicChunk { parent: MusicBlock; terms: BaseTerm[]; static join (chunks: MusicChunk[]): BaseTerm[] { return cc(chunks.map((chunk, i) => i === chunks.length - 1 ? chunk.terms : [...chunk.terms, new Divide({})])); } constructor (parent: MusicBlock, terms: BaseTerm[] = []) { this.parent = parent; this.terms = terms; } push (term: BaseTerm) { this.terms.push(term); } get size () { return this.terms.length; } get durationMagnitude () { return this.terms.reduce((magnitude, term) => magnitude + term.durationMagnitude, 0); } }; interface MusicVoice { name?: string; body: MusicChunk[]; }; type MusicChunkMap = Map; const isNullItem = item => item === "" || item === undefined || item === null || (Array.isArray(item) && !item.length); const compact = items => cc(items.map((item, index) => isNullItem(item) ? [] : [index > 0 ? "\b" : null, item])); export const getDurationSubdivider = (term: BaseTerm): number => { if (term instanceof MusicEvent) { if (!(term instanceof Rest) || !term.isSpacer) return term.durationValue.subdivider; } else if (term instanceof MusicBlock) return lcmMulti(...term.body.map(getDurationSubdivider)); else if (term instanceof MusicChunk) return lcmMulti(...term.terms.map(getDurationSubdivider)); else if ((term instanceof Times) || (term instanceof Tuplet)) { const divider = term instanceof Tuplet ? term.divider : term.factor.reciprocal; divider.numerator *= getDurationSubdivider(term.music); return divider.reduced.numerator; } else if (term instanceof Repeat) return getDurationSubdivider(term.bodyBlock); else if (term instanceof Relative) return getDurationSubdivider(term.music); else if (term.isMusic) console.warn("[getDurationSubdivider] unexpected music term:", term); return 1; }; export const constructMusicFromMeasureLayout = (layout: measureLayout.MeasureLayout, chunks: MusicChunkMap): MusicChunk => { const joinMeasureSeq = (seq: measureLayout.MeasureSeq): BaseTerm[] => MusicChunk.join(seq.map(sublayout => constructMusicFromMeasureLayout(sublayout, chunks))); if (layout instanceof measureLayout.SingleMLayout) { const chunk = chunks.get(layout.measure); console.assert(!!chunk, "no chunk for measure:", layout.measure); return chunk; } else if (layout instanceof measureLayout.BlockMLayout) { const terms = joinMeasureSeq(layout.seq); return new MusicChunk(null, terms); } else if (layout instanceof measureLayout.VoltaMLayout) { const bodyTerms = joinMeasureSeq(layout.body); const alternative = layout.alternates && layout.alternates.map(alternate => new MusicBlock({body: joinMeasureSeq(alternate)})); const repeat = Repeat.createVolta(layout.times.toString(), new MusicBlock({body: bodyTerms}), alternative); return new MusicChunk(null, [repeat]); } else if (layout instanceof measureLayout.ABAMLayout) { const mainList = constructMusicFromMeasureLayout(layout.main, chunks); const main = mainList.terms.length === 1 ? mainList.terms[0] : new MusicBlock({body: mainList.terms}); const restTerms = joinMeasureSeq(layout.rest); const block = new MusicBlock({body: [main, ...restTerms]}); return new MusicChunk(null, [new Variable({name: "lotusRepeatABA"}), block]); } }; export class BaseTerm { _location?: Location; _measure?: number; _tick?: number; _previous?: BaseTerm; _anchorPitch?: ChordElement; _parent?: BaseTerm; _headComment: Comment; _tailComment: Comment; // lotus extensional function modifier _functional: string; constructor (data: object) { //Object.assign(this, data); for (const key in data) this[key] = parseRaw(data[key]); } serialize (): any[] { console.warn("unimplemented serilization:", this); return []; } join (): string { let words = this.serialize().filter(word => ["string", "number"].includes(typeof word)).map(word => word.toString()) as string[]; words = words.filter((word, i) => !(i && words[i - 1] === "\n" && word === "\n")); let indent = 0; const result: string[] = []; const pop = char => { if (!char || result[result.length - 1] === char) { result.pop(); return true; } }; for (const word of words) { switch (word) { case "\b": // remove last space pop(" "); continue; case "\b\n": // remove last newline while (pop("\t")) {} pop("\n"); continue; case "\n": // no space at line tail pop(" "); } if (/^(\}|>>)/.test(word)) pop("\t"); // remove the last tab result.push(word); if (/\n$/.test(word)) { if (/(\{|<<)\n$/.test(word)) ++indent; else if (/^(\}|>>)/.test(word)) --indent; if (indent) result.push(...Array(indent).fill("\t")); } else result.push(" "); } return result.join(""); } relocate (source: string = this.join()) { const words = this.serialize() .filter(word => word !== null && word !== undefined && (typeof word !== "string" || (/\S/.test(word) && !word.includes("\b")))) .map(word => typeof word === "string" ? word.replace(/\n/g, "") : word); const chars = source.split(""); let line = 1; let column = 0; let wordIndex = 0; for (let i = 0; i < chars.length; ++i) { if (wordIndex >= words.length) break; const char = chars[i]; switch (char) { case "\n": ++line; column = 0; break; case " ": case "\t": ++column; break; default: let word = words[wordIndex]; while (word instanceof Locator) { word.set(line, column); ++wordIndex; word = words[wordIndex]; } if (wordIndex >= words.length) break; word = word.toString(); if (char === word[0]) { i += word.length - 1; column += word.length; ++wordIndex; } else { //debugger; throw new Error(`unexpected char in source: [${i}]'${char}', expect: ${word}`); } } } } clone (): this { return parseRaw(JSON.parse(JSON.stringify(this))); } get entries (): BaseTerm[] { return null; } get isMusic (): boolean { return false; } get musicChunks (): MusicChunk[] { if (!this.isMusic || !this.entries) return []; return [].concat(...this.entries.map(entry => entry.musicChunks)); } get measures (): number[] { const indices = [this._measure].concat(...(this.entries || []).map(entry => entry.measures)).filter(index => Number.isInteger(index)); return Array.from(new Set(indices)); } get durationMagnitude (): number { return 0; } get proto () { return termProtoMap.get(Object.getPrototypeOf(this)); } get href (): string { if (this._location) return `${this._location.lines[0]}:${this._location.columns[0]}:${this._location.columns[1]}`; return null; } get measureLayout (): measureLayout.MeasureLayout { return null; } getField (key): any { console.assert(!!this.entries, "[BaseTerm.getField] term's entries is null:", this); for (const entry of this.entries) { const result = entry.query(key); if (result) return result; } } query (key: string): any { void(key); //console.warn("term.query not implemented:", this); } appendAssignment (key, value) { console.assert(!!this.entries, "no entries on this term."); const assign = this.getField(key); if (assign) assign.value = value; else { this.entries.push(parseRaw({ proto: "Assignment", key, value: value, })); } } findFirst (condition: Function): BaseTerm { if (!this.entries) return null; if (BaseTerm.isPrototypeOf(condition)) { const termClass = condition; condition = term => term instanceof termClass; } for (const entry of this.entries) { if (condition(entry)) return entry; if (entry instanceof BaseTerm) { const result = entry.findFirst(condition); if (result) return result; } } } findLast (condition: any): BaseTerm { if (!this.entries) return null; if (BaseTerm.isPrototypeOf(condition)) { const termClass = condition; condition = term => term instanceof termClass; } const reversedEntries = [...this.entries]; reversedEntries.reverse(); for (const entry of reversedEntries) { if (condition(entry)) return entry; if (entry instanceof BaseTerm) { const result = entry.findLast(condition); if (result) return result; } } } findAll (condition: any): any[] { if (!this.entries) return []; if (BaseTerm.isPrototypeOf(condition)) { const termClass = condition; condition = term => term instanceof termClass; } const result = []; for (const entry of this.entries) { if (condition(entry)) result.push(entry); if (entry instanceof BaseTerm) result.push(...entry.findAll(condition)); } return result; } forEachTerm (termClass, handle) { if (!this.entries) return; for (const entry of this.entries) { if (entry instanceof termClass) handle(entry); if (entry instanceof BaseTerm) entry.forEachTerm(termClass, handle); } } forEachTopTerm (termClass, handle) { if (!this.entries) return; for (const entry of this.entries) { if (entry instanceof termClass) handle(entry); else if (entry instanceof BaseTerm) entry.forEachTopTerm(termClass, handle); } } toJSON () { // exlude meta fields in JSON const fields = Object.keys(this).filter(key => !/^_/.test(key)); const data = pick(this, fields); Object.entries(data).forEach(([key, value]) => { if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof BaseTerm)) data[key] = {proto: "_PLAIN", ...value}; }); return { proto: this.proto, ...data, }; } static isTerm (x): boolean { return typeof x === "object" && x instanceof BaseTerm; } static optionalSerialize (item: any): any[] { //return BaseTerm.isTerm(item) ? (item as BaseTerm).serialize() : (item === undefined ? [] : [item]); if (!BaseTerm.isTerm(item)) return item === undefined ? [] : [item]; return [ ...BaseTerm.optionalSerialize(item._headComment), ...item.serialize(), ...(item._tailComment ? ["\b\n", "\t"] : []), ...BaseTerm.optionalSerialize(item._tailComment), ]; } static serializeScheme (item: any): any[] { if (typeof item === "boolean") item = item ? "#t" : "#f"; return BaseTerm.optionalSerialize(item); } } export class Root extends BaseTerm { sections: BaseTerm[]; serialize () { return cc(this.sections.map(section => [...BaseTerm.optionalSerialize(section), "\n\n"])); } get entries (): BaseTerm[] { return this.sections; } getBlock (head): Block { return this.entries.find((entry: any) => entry.head === head || (entry.head === "\\" + head)) as Block; } get includeFiles (): string[] { return this.sections.filter(section => section instanceof Include).map((include: Include) => include.filename); } static priorityForSection (term: BaseTerm): number { if (term instanceof Version) return 0; if (term instanceof Language) return 1; if (term instanceof Scheme) return 3; if (term instanceof Assignment) return 7; if (term instanceof Block) { switch (term.head) { case "\\header": return 2; case "\\paper": return 4; case "\\layout": return 5; case "\\score": return 10; } } return Infinity; } reorderSections () { this.sections.sort((s1, s2) => Root.priorityForSection(s1) - Root.priorityForSection(s2)); } }; export class Primitive extends BaseTerm { exp: string | number; serialize () { return [this.exp]; } }; export class LiteralString extends BaseTerm { exp: string static fromString (content: string): LiteralString { return new LiteralString({exp: JSON.stringify(content)}); } serialize () { return [this.exp]; } toString () { try { return eval(this.exp); } catch (err) { console.warn("invalid lilypond string exp:", this.exp); return this.exp; } } }; export class Command extends BaseTerm { cmd: string; args: any[]; static createSet (key: string|BaseTerm, value: BaseTerm): Command { return new Command({cmd: "set", args: [new Assignment({key, value})]}); } constructor (data) { super(data); this.args.forEach(term => { if (term instanceof MusicBlock || term instanceof Block) term._parent = this; }); } serialize () { return [ "\\" + this.cmd, ...[].concat(...this.args.map(BaseTerm.optionalSerialize)), ["break", "pageBreak", "overrideProperty"].includes(this.cmd) ? "\n" : null, ]; } get entries () { return this.args.filter(arg => arg instanceof BaseTerm); } get isMusic (): boolean { return this.args.some(arg => arg.isMusic); } get musicChunks (): MusicChunk[] { if (this.cmd === "alternative") return [].concat(...this.args[0].body.map(term => term.musicChunks)); return [].concat(...this.entries.map(entry => entry.musicChunks)); } get isRepeatWithAlternative () { return this.cmd === "repeat" && this.args[2] instanceof MusicBlock && this.args[3] && this.args[3].cmd === "alternative"; } get durationMagnitude (): number { switch (this.cmd) { // TODO: refine this in Times case "times": { const factor = eval(this.args[0]); return this.args[1].durationMagnitude * factor; } // TODO: refine this in Tuplet case "tuplet": { const factor = 1 / eval(this.args[0]); return this.args[this.args.length - 1].durationMagnitude * factor; } case "afterGrace": return this.args[0].durationMagnitude; default: if (this instanceof Grace) return 0; return this.args.filter(arg => arg instanceof BaseTerm).reduce((magnitude, term) => magnitude + term.durationMagnitude, 0); } } get measureLayout (): measureLayout.MeasureLayout { const args = [...this.args].reverse(); for (const arg of args) { const layout = arg instanceof BaseTerm && arg.measureLayout; if (layout) return layout; } return null; } getAssignmentPair (): {key: any, value: any} { if (this.args[0] instanceof Assignment) return {key: this.args[0].key, value: this.args[0].value}; if (this.args[1] instanceof Assignment) return {key: this.args[0], value: this.args[1].value}; if (typeof this.args[0] === "string") return {key: this.args[0], value: ""}; return null; } }; export class Variable extends Command { name: string constructor ({name}) { super({cmd: name, args: []}); this.name = name; } toJSON (): any { return { proto: this.proto, name: this.name, }; } queryValue (dict: BaseTerm): any { const field = dict.getField(this.name); return field && field.value; } get isMusic (): boolean { if ([MAIN_SCORE_NAME].includes(this.name)) return true; return false; } }; export class MarkupCommand extends Command { toString () { const strs = []; this.forEachTerm(LiteralString, term => strs.push(term.toString())); return strs.join("\n"); } }; export class Repeat extends Command { static createVolta (times: string, body: MusicBlock, alternative?: MusicBlock[]): Repeat { const args: any[] = [ "volta", times, body, ]; if (alternative) { args.push(new Command({ cmd: "alternative", args: [new MusicBlock({body: alternative})], })); } return new Repeat({cmd: "repeat", args}); } get type (): string { return this.args[0]; } get times () { return Number(this.args[1]); } get bodyBlock (): MusicBlock { return this.args[2]; } get alternativeBlocks (): MusicBlock[] { return this.args[3] && this.args[3].args[0].body; } // this result length equal to times, if not null get completeAlternativeBlocks (): MusicBlock[] { if (!this.alternativeBlocks || !this.alternativeBlocks.length) return null; if (this.alternativeBlocks.length >= this.times) return this.alternativeBlocks.slice(0, this.times); const list = []; for (let i = 0; i < this.times - this.alternativeBlocks.length; ++ i) list.push(this.alternativeBlocks[0]); list.push(...this.alternativeBlocks); return list; } get measureLayout (): measureLayout.MeasureLayout { switch (this.type) { case "volta": { const layout = new measureLayout.VoltaMLayout(); layout.times = this.times; layout.body = this.bodyBlock.measureLayout.seq; layout.alternates = this.alternativeBlocks && this.alternativeBlocks.map(block => block.measureLayout.seq); return layout; } case "tremolo": return this.bodyBlock.measureLayout; default: console.warn("unsupported repeat type:", this.type); } return null; } // for tremolo get sumDuration (): Duration { if (this.bodyBlock instanceof MusicEvent) return Duration.fromMagnitude(this.args[2].durationMagnitude * this.times); else if (this.bodyBlock instanceof MusicBlock) { const events = this.bodyBlock.body.filter(term => term instanceof MusicEvent); const magnitude = events.reduce((m, event) => m + event.durationMagnitude, 0) * this.times; return Duration.fromMagnitude(magnitude); } return null; } get singleTremolo (): boolean { if (this.type === "tremolo") { if (this.bodyBlock instanceof MusicEvent) return true; if (this.bodyBlock instanceof MusicBlock) { const events = this.bodyBlock.body.filter(term => term instanceof MusicEvent); return events.length === 1; } } return false; } // \repeat {body} \alternative {{alter1} {alter2}} => body alter1 body alter2 getUnfoldTerms (): BaseTerm[] { const completeAlternativeBlocks = this.completeAlternativeBlocks; const list = []; for (let i = 0; i < this.times; ++i) { list.push(...this.bodyBlock.clone().body); if (completeAlternativeBlocks) list.push(...completeAlternativeBlocks[i].clone().body); } return list; } // \repeat {body} \alternative {{alter1} {alter2}} => body alter1 alter2 getPlainTerms (): BaseTerm[] { const list = [...this.bodyBlock.clone().body]; const alternativeBlocks = this.alternativeBlocks; if (alternativeBlocks) alternativeBlocks.forEach(block => list.push(...block.clone().body)); return list; } // \repeat {body} \alternative {{alter1} {alter2}} => body alter2 getTailPassTerms (): BaseTerm[] { const list = [...this.bodyBlock.clone().body]; const alternativeBlocks = this.alternativeBlocks; if (alternativeBlocks) list.push(...alternativeBlocks[alternativeBlocks.length - 1].clone().body); return list; } }; export class Relative extends Command { static makeBlock (block: MusicBlock, {anchor}: {anchor?: ChordElement} = {}): Relative { if (!anchor) { const chord = block.findFirst(Chord) as Chord; anchor = chord && chord.anchorPitch; } return new Relative({cmd: "relative", args: [anchor, block].filter(term => term)}); } get anchor (): ChordElement { if (this.args[0] instanceof ChordElement) return this.args[0]; return null; } get music (): BaseTerm { return this.args[this.args.length - 1]; } get headChord (): Chord { return this.findFirst(Chord) as Chord; } get tailPitch (): ChordElement { const tail = this.findLast(Chord) as Chord; return tail && tail.absolutePitch; } // with side effect shiftBody (newAnchor?: ChordElement): BaseTerm[] { const headChord = this.headChord; if (newAnchor && headChord) { headChord.shiftAnchor(newAnchor); headChord._anchorPitch = null; //console.log("shiftAnchor.post:", headChord.join(), headChord); } const music = this.music; if (music instanceof MusicBlock) { //music.clearPitchCache(); return music.body; } return [music]; } } export class ParallelMusic extends Command { get varNames (): string[] { return ((this.args[0].exp as SchemePointer).value as SchemeFunction).asList as string[]; } get body (): MusicBlock { return this.args[1]; } get voices (): MusicVoice[] { const voiceNames = this.varNames; const chunks = this.body.musicChunks; const measureCount = Math.ceil(chunks.length / voiceNames.length); return voiceNames.map((name, index) => ({ name: name.toString(), body: Array(measureCount).fill(null).map((_, m) => chunks[m * voiceNames.length + index]).filter(Boolean), })); } }; export class TimeSignature extends Command { get value (): FractionNumber { return FractionNumber.fromExpression(this.args[0]); } }; export class Partial extends Command { get duration (): Duration { return this.args[0]; } }; export class Times extends Command { get factor (): FractionNumber { return FractionNumber.fromExpression(this.args[0]); } get music (): BaseTerm { return this.args[this.args.length - 1]; } }; export class Tuplet extends Command { get divider (): FractionNumber { return FractionNumber.fromExpression(this.args[0]); } get music (): BaseTerm { return this.args[this.args.length - 1]; } }; export class Grace extends Command { get music (): BaseTerm { return this.args[this.args.length - 1]; } }; export class AfterGrace extends Command { get body (): BaseTerm { return this.args[0]; } get grace (): BaseTerm { return this.args[1]; } get measureLayout (): measureLayout.MeasureLayout { return measureLayout.BlockMLayout.fromSeq([ this.body.measureLayout, this.grace.measureLayout, ]); } }; export class Clef extends Command { get clefName (): string { return this.args[0].toString(); } }; export class KeySignature extends Command { get keyPitch (): ChordElement { return new ChordElement({pitch: this.args[0], options: {proto: "_PLAIN"}}); } get key (): number { const keyPitch = this.keyPitch; const minor = this.args[1] === "\\minor"; const phonetOrder = idioms.FIFTH_PHONETS.indexOf(keyPitch.phonet); return phonetOrder + (minor ? -4 : -1) + keyPitch.alterValue * 7; } }; export class OctaveShift extends Command { get value (): number { return this.args[0].exp; } }; export class Include extends Command { static create (filename: string): Include { return new Include({cmd: "include", args: [LiteralString.fromString(filename)]}); } get filename (): string { return this.args[0].toString(); } }; export class Version extends Command { static get default (): Version { return new Version({cmd: "version", args: [LiteralString.fromString(LILYPOND_VERSION)]}); } get version (): string { return this.args[0].toString(); } }; export class Language extends Command { static make (language: string): Language { return new Language({cmd: "language", args: [LiteralString.fromString(language)]}); } get language (): string { return this.args[0].toString(); } }; export class LyricMode extends Command { get block (): MusicBlock { return this.args[0]; } }; export class ChordMode extends Command { get block (): MusicBlock { return this.args[0]; } }; export class Transposition extends Command { get transposition (): number { return this.args[0].pitchValue - 60; } }; export class StemDirection extends Command { get direction (): string { return this.cmd.substr(4); } }; export class Change extends Command { get key (): string { return this.args[0].toString(); } get value (): string { return this.args[1].toString(); } }; export class Block extends BaseTerm { block: string; head: (string|string[]); body: BaseTerm[]; constructor (data) { super(data); this.body = this.body.map(parseRawEnforce); } serialize () { const heads = Array.isArray(this.head) ? this.head : (this.head ? [this.head] : []); return [ ...heads, "{\n", ...cc(this.body.map(section => [...BaseTerm.optionalSerialize(section), "\n"])), "}\n", ]; } get entries () { return this.body; } get isMIDIDedicated () { const subBlocks = this.body.filter(term => term instanceof Block) as Block[]; return subBlocks.some(term => term.head === "\\midi") && !subBlocks.some(term => term.head === "\\layout"); } get assignmentDict (): {[key: string]: string} { const assignments = this.body.filter(term => term instanceof Assignment) as Assignment[]; return assignments.reduce((dict, assignment) => ((dict[assignment.key.toString()] = assignment.value.toString()), dict), {}); } }; export class InlineBlock extends Block { serialize () { return [ "{", ...cc(this.body.map(BaseTerm.optionalSerialize)), "}", ]; } }; export class MusicBlock extends BaseTerm { body: BaseTerm[]; static fromTerms (terms: BaseTerm[]): MusicBlock { const block = new MusicBlock({body: [...terms]}); block.clarifyHead(); return block; } constructor (data) { super(data); this.body = this.body.map(parseRawEnforce); } serialize () { return [ "{\n", ...cc(this.body.map(BaseTerm.optionalSerialize)), "\n", "}\n", ]; } clone (): this { if (this._parent) { const parent = this._parent.clone(); const block = parent.findFirst(MusicBlock); console.assert(block && block._parent === parent, "invalid block-parent relation", parent, block); return block as this; } return BaseTerm.prototype.clone.call(this) as this; } get entries () { return this.body; } get isMusic (): boolean { return true; } get musicChunks (): MusicChunk[] { const chunks = []; let currentChunk = new MusicChunk(this); const dumpChunk = () => { if (currentChunk.size) chunks.push(currentChunk); currentChunk = new MusicChunk(this); }; for (const term of this.entries) { if (term instanceof Repeat) { dumpChunk(); chunks.push(...term.musicChunks); } else if (term instanceof Divide) dumpChunk(); else currentChunk.push(term); } dumpChunk(); return chunks; } // [deprecated] // for parallelMusic only get voiceNames () { const header = this._parent as Command; if (header && header.cmd === "parallelMusic") { if (header.args[0] instanceof Scheme && header.args[0].exp instanceof SchemePointer && header.args[0].exp.value instanceof SchemeFunction) { const voices = header.args[0].exp.value.asList; return voices; } } return null; } // [deprecated] get voices (): MusicVoice[] { const voiceNames = this.voiceNames; if (!voiceNames) return [{body: this.musicChunks}]; const chunks = this.musicChunks; const measureCount = Math.ceil(chunks.length / voiceNames.length); return voiceNames.map((name, index) => ({ name: name.toString(), body: Array(measureCount).fill(null).map((_, m) => chunks[m * voiceNames.length + index]).filter(chunk => chunk), })); } get durationMagnitude (): number { return this.body.reduce((magnitude, term) => magnitude + term.durationMagnitude, 0); } get isRelative (): boolean { return this._parent instanceof Relative; } get anchorPitch (): ChordElement { if (this.isRelative) return (this._parent as Relative).anchor; return null; } get measures (): number[] { // make a continouse indices list const subterms = this.findAll(term => term.isMusic); const subIndices = [].concat(...subterms.map(term => term.measures)).filter(index => Number.isInteger(index)); if (!subIndices.length) return []; const min = Math.min(...subIndices); const max = Math.max(...subIndices); return Array(max + 1 - min).fill(null).map((_, i) => i + min); } get notes (): Chord[] { const notes = this.body.filter(term => term instanceof Chord && !term.isRest) as Chord[]; this.forEachTopTerm(MusicBlock, block => notes.push(...block.notes)); return notes; } get sonicNotes (): Chord[] { return this.notes.filter(note => !note.completeTied); } get noteTicks (): number[] { const ticks = this.sonicNotes.map(note => note._tick); return Array.from(new Set(ticks)).sort((t1, t2) => t1 - t2); } get measureTicks (): [number, number][] { const tickTable: {[key: string]: number} = {}; this.body.forEach(term => { if (Number.isFinite(term._measure) && Number.isFinite(term._tick)) { if (!Number.isFinite(tickTable[term._measure])) tickTable[term._measure] = term._tick; } }); return Object.entries(tickTable).map(([measure, tick]) => [Number(measure), tick]); } get measureLayout (): measureLayout.MeasureLayout { const seq = this.body.filter(term => term.isMusic).map(term => term.measureLayout).filter(Boolean); if (this._functional === "lotusRepeatABA") { const [main, ...rest] = seq; const layout = new measureLayout.ABAMLayout(); layout.main = main; layout.rest = measureLayout.BlockMLayout.trimSeq(rest); return layout; } return measureLayout.BlockMLayout.fromSeq(seq); } get measureChunkMap (): MusicChunkMap { const map = new Map(); this.body.forEach(term => { if (Number.isInteger(term._measure) && !(term instanceof Divide)) { if (!map.get(term._measure)) map.set(term._measure, new MusicChunk(this)); const chunk = map.get(term._measure); chunk.terms.push(term); } }); return map; } clearPitchCache () { this.forEachTerm(ChordElement, pitch => { pitch._absolutePitch = null; //pitch._previous = null; }); } updateChordAnchors () { const chord = this.findFirst(Chord) as Chord; if (chord) chord._anchorPitch = chord._anchorPitch || this.anchorPitch; this.clearPitchCache(); } // deprecated updateChordChains () { let previous: MusicEvent = null; this.updateChordAnchors(); this.forEachTerm(MusicBlock, block => block.updateChordAnchors()); this.forEachTerm(MusicEvent, event => { event._previous = previous; previous = event; }); } // with side effect spreadRepeatBlocks ({ignoreRepeat = true, keepTailPass = false} = {}): this { this.forEachTerm(MusicBlock, block => block.spreadRepeatBlocks()); this.body = cc(this.body.map(term => { if (term instanceof Repeat) { if (!ignoreRepeat) return term.getUnfoldTerms(); else if (keepTailPass) return term.getTailPassTerms(); else return term.getPlainTerms(); } else return [term]; })); return this; } /*// with side effect spreadRelativeBlocks (): this { this.forEachTerm(MusicBlock, block => block.spreadRelativeBlocks()); let anchorPitch = null; this.body = cc(this.body.map(term => { if (term instanceof Relative) { const list = term.shiftBody(anchorPitch); anchorPitch = term.tailPitch || anchorPitch; return list; } else return [term]; })); return this; }*/ // with side effect unfoldDurationMultipliers (): this { let timeDenominator = 4; const unfoldMultipliers = (term): BaseTerm[] => { if (term instanceof TimeSignature) timeDenominator = term.value.denominator; if (!(term instanceof MusicEvent) || !term.duration || !term.duration.multipliers || !term.duration.multipliers.length) return [term]; const factor = term.duration.multipliers.reduce((factor, multiplier) => factor * Number(multiplier), 1); if (!Number.isInteger(factor) || factor <= 0) return [term]; const denominator = Math.max(term.duration.denominator, timeDenominator); 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]; }; this.body = cc(this.body.map(unfoldMultipliers)); return this; } /*// pure flatten ({spreadRepeats = false} = {}): Relative { this.updateChordChains(); const chord = this.findFirst(Chord) as Chord; const anchor = this.anchorPitch || (chord && chord.anchorPitch); const block = this.clone(); if (spreadRepeats) block.spreadRepeatBlocks(); block.spreadRelativeBlocks(); block.unfoldDurationMultipliers(); return Relative.makeBlock(block, {anchor: anchor && anchor.clone()}); }*/ // with side effect expandVariables (dict: BaseTerm): this { this.body = this.body.map(term => { if (term instanceof Variable) { const value = term.queryValue(dict); const clonedValue = value instanceof BaseTerm ? value.clone() : value; if (clonedValue instanceof BaseTerm) { clonedValue.forEachTerm(MusicBlock, block => block.expandVariables(dict)); if (clonedValue instanceof MusicBlock) clonedValue.expandVariables(dict); } return clonedValue; } return term; }); return this; } // with side effects redivide ({recursive = true, measureHeads = null}: {recursive?: boolean, measureHeads?: number[]} = {}) { if (recursive) { this.forEachTerm(MusicBlock, block => { if (!block._parent || block._parent.cmd !== "alternative") block.redivide({recursive, measureHeads}); }); } // split rests if (measureHeads) { this.body = [].concat(...this.body.map(term => { if (!(term instanceof Rest) || term.name !== "s" || !Number.isInteger(term._measure)) return [term]; const nextHead = measureHeads[term._measure]; const endTick = term._tick + term.durationMagnitude; if (nextHead > 0 && endTick > nextHead) { const post_events = term.post_events; let startTick = term._tick; const rests = []; let nextMeasure; for (nextMeasure = term._measure; nextMeasure < measureHeads.length && endTick > measureHeads[nextMeasure]; ++nextMeasure) { const duration = Duration.fromMagnitude(measureHeads[nextMeasure] - startTick); if (!duration) { console.warn("invalid middle rest duration, splitting gave up:", measureHeads[nextMeasure] - startTick, term); return [term]; } const rest = new Rest({name: "s", duration, post_events: []}); rest._measure = nextMeasure; rest._lastMeasure = nextMeasure; rests.push(rest); console.assert(!!rest.duration, "middle splitted rest duration invalid:", measureHeads[nextMeasure] - startTick); startTick = measureHeads[nextMeasure]; } const duration = Duration.fromMagnitude(endTick - startTick); if (!duration) { console.warn("invalid tail rest duration, splitting gave up:", endTick - startTick, term); return [term]; } const rest = new Rest({name: "s", duration, post_events: post_events && [...post_events]}); rest._measure = nextMeasure; rest._lastMeasure = nextMeasure; rests.push(rest); console.assert(rests.reduce((sum, rest) => sum + rest.durationMagnitude, 0) === term.durationMagnitude, "duration splitting error:", rests, term); //if (rests.reduce((sum, rest) => sum + rest.durationMagnitude, 0) !== term.durationMagnitude) // debugger; return rests; } return [term]; })); } const isPostTerm = term => !term || term instanceof PostEvent || (term as Primitive).exp === "~" || ["bar", "arpeggio", "glissando", "sustainOff", "sustainOn"].includes((term as Command).cmd) ; const list = this.body.filter(term => !(term instanceof Divide)); let measure = null; for (const term of list) { if (Number.isInteger(measure) && isPostTerm(term)) term._measure = measure; else measure = term._measure; } const body: BaseTerm[] = []; const measures = new Set(); list.reverse().forEach(term => { if (term instanceof BaseTerm) { const newMeasures = term.measures.filter(m => !measures.has(m)); if (newMeasures.length) { const comment = " " + newMeasures[0] + (newMeasures.length > 1 ? "-" + Math.max(...newMeasures) : ""); if (body.length) body.push(new Divide({_tailComment: Comment.createSingle(comment)})); newMeasures.forEach(m => measures.add(m)); } } body.push(term); }); this.body = body.reverse(); } clarifyHead () { const terms = this.body; const head = terms.find(term => term.isMusic); if (head instanceof MusicEvent) { // clarify the first music event content const firstEventIndex = terms.indexOf(head); if (firstEventIndex >= 0) { const firstEvent = terms[firstEventIndex] as MusicEvent; //console.log("firstEvent:", firstEvent); if (firstEvent._previous) { const clarified = firstEvent.clarified; terms.splice(firstEventIndex, 1, clarified); //console.log("terms:", firstEventIndex, terms, clarified); } } } else if (head) { const block = head.findFirst(MusicBlock) as MusicBlock; if (block) block.clarifyHead(); else console.warn("[MusicBlock.clarifyHead] unexpected music head:", head); } } absoluteToRelative (): Relative { const anchor = this.findFirst(Chord) as Chord; if (!anchor) return null; const anchorPitch = anchor.absolutePitch; let pitch = anchorPitch; const newBody = this.clone(); newBody.forEachTerm(Chord, chord => { const newPitch = chord.absolutePitch; chord.makeRelativeTo(pitch); pitch = newPitch; }); return Relative.makeBlock(newBody, {anchor: anchorPitch}); } }; export class SimultaneousList extends BaseTerm { list: BaseTerm[]; serialize () { return [ "<<\n", ...cc(this.list.map(item => [...BaseTerm.optionalSerialize(item), "\n"])), ">>\n", ]; } removeStaffGroup () { for (let i = 0; i < this.list.length; ++i) { const item: any = this.list[i]; if (item.head instanceof Command && item.head.args && item.head.args[0] === "StaffGroup") this.list[i] = item.body; } this.list.forEach(item => { if (item instanceof SimultaneousList) item.removeStaffGroup(); }); } get isMusic (): boolean { return true; } get entries () { return this.list; } get durationMagnitude (): number { return Math.max(...this.list.filter(term => term instanceof BaseTerm).map(term => term.durationMagnitude)); } get measureLayout (): measureLayout.MeasureLayout { const track = this.list.find(term => term instanceof BaseTerm && term.measureLayout); return track && track.measureLayout; } }; export class ContextedMusic extends BaseTerm { head: Command; body: BaseTerm; lyrics?: BaseTerm; serialize () { return [ ...BaseTerm.optionalSerialize(this.head), ...BaseTerm.optionalSerialize(this.body), ...BaseTerm.optionalSerialize(this.lyrics), ]; } get isMusic (): boolean { return true; } get entries () { return [this.head, this.body]; } get type (): string { return this.head.args[0]; } get durationMagnitude (): number { return this.body.durationMagnitude; } get withClause (): Command { if (this.head.args[2] && this.head.args[2] instanceof Command && this.head.args[2].cmd === "with") return this.head.args[2]; } get contextDict (): {[key: string]: string} { const withEntries = this.withClause ? Object.entries((this.withClause.args[0] as Block).assignmentDict) : []; const entries = withEntries.map(([key, value]) => [`${this.type}.${key}`, value]); const pair = this.head.getAssignmentPair(); if (pair) entries.push([pair.key.toString(), pair.value.toString()]); return entries.reduce((dict, [key, value]) => ((dict[key] = value), dict), {}); } get list (): BaseTerm[] { if (this.body instanceof SimultaneousList) return this.body.list; return null; } set list (value: BaseTerm[]) { if (this.body instanceof SimultaneousList) this.body.list = value; } get measureLayout (): measureLayout.MeasureLayout { return this.body.measureLayout; } }; export class Divide extends BaseTerm { serialize () { return ["|", "\n"]; } } export class Scheme extends BaseTerm { exp: (boolean|BaseTerm); serialize () { if (BaseTerm.isTerm(this.exp)) return ["#", "\b", ...(this.exp as BaseTerm).serialize()]; else if (typeof this.exp === "boolean") return ["#", "\b", this.exp ? "#t" : "#f"]; // TODO: enhance grammar to parse empty scheme list //else if (this.exp === null) // return ["#", "\b", "'()"]; else return ["#", "\b", this.exp]; } query (key: string): any { if (this.exp instanceof SchemeFunction) return this.exp.query(key); } get entries () { if (this.exp instanceof BaseTerm) return [this.exp]; return []; } }; export class SchemeFunction extends BaseTerm { func: (string | BaseTerm); args: (boolean | string | BaseTerm)[]; serialize () { return [ "(", "\b", ...BaseTerm.optionalSerialize(this.func), ...cc(this.args.map(BaseTerm.serializeScheme)), "\b", ")", ]; } query (key: string): any { if (key === this.func) { const term = this; return { get value () { return term.args.length === 1 ? term.args[0] : term.args; }, set value (value) { if (term.args.length === 1) term.args[0] = value as string|BaseTerm; else term.args = value as (string|BaseTerm)[]; }, }; } } get asList (): (boolean | string | BaseTerm)[] { return [this.func, ...this.args]; } get entries () { return this.asList.filter(term => term instanceof BaseTerm) as BaseTerm[]; } }; export class SchemePair extends BaseTerm { left: any; right: any; serialize () { return [ "(", "\b", ...BaseTerm.optionalSerialize(this.left), ".", ...BaseTerm.optionalSerialize(this.right), "\b", ")", ]; } }; export class SchemePointer extends BaseTerm { value: any; serialize () { const content = this.value === null ? ["()"] : BaseTerm.optionalSerialize(this.value); return [ "'", "\b", ...content, ]; } get entries () { if (this.value instanceof BaseTerm) return [this.value]; return []; } }; export class SchemeEmbed extends BaseTerm { value: Root; serialize () { return [ "#{", ...BaseTerm.optionalSerialize(this.value), "#}", ]; } }; export class Assignment extends BaseTerm { key: (string|any[]); value: any; constructor (data) { super(data); if (this.value instanceof BaseTerm) this.value._parent = this; } serialize () { const keys = (Array.isArray(this.key) ? this.key : [this.key]).map(BaseTerm.optionalSerialize); const values = (Array.isArray(this.value) ? this.value : [this.value]).map(BaseTerm.optionalSerialize); return [ ...cc(keys), "=", ...cc(values), ]; } get entries () { if (this.value instanceof BaseTerm) return [this.value]; return null; } query (key) { if (this.key === key) { const term = this; return { get value () { return term.value; }, set value (value) { term.value = value; }, }; } } }; export class MusicEvent extends BaseTerm { duration?: Duration; post_events?: (string | PostEvent)[]; declare _previous?: MusicEvent; //_anchorPitch?: ChordElement; _lastMeasure?: number; constructor (data: object) { super(data); if (this.post_events) this.post_events = this.post_events.map(parseRaw); } getPreviousT (T) { if (this._previous instanceof T) return this._previous; if (this._previous) return this._previous.getPreviousT(T); } get durationValue (): Duration { return this.duration || (this._previous ? this._previous.durationValue : Duration.default); } get durationMagnitude (): number { return this.durationValue.magnitude; } get division (): number { return this.durationValue.division; } get withMultiplier () { return this.duration && this.duration.withMultiplier; } get isMusic (): boolean { return true; } get isTying (): boolean { return this.post_events && this.post_events.some(event => event instanceof PostEvent && event.isTying); } get isStaccato (): boolean { return this.post_events && this.post_events.some(event => event instanceof PostEvent && event.isStaccato); } // to be implement in derived classes get isRest (): boolean { return null; } get beamOn (): boolean { return this.post_events && this.post_events.includes("["); } get beamOff (): boolean { return this.post_events && this.post_events.includes("]"); } get measures (): number[] { if (!Number.isFinite(this._measure) || !Number.isFinite(this._lastMeasure)) return []; return Array(this._lastMeasure + 1 - this._measure).fill(null).map((_, i) => this._measure + i); } get measureLayout (): measureLayout.MeasureLayout { if (this.measures.length > 1) return measureLayout.BlockMLayout.fromSeq(this.measures.map(measure => measureLayout.SingleMLayout.from(measure))); if (this.measures.length === 1) return measureLayout.SingleMLayout.from(this._measure); return null; } get implicitType (): ImplicitType { if (this.post_events) { for (const event of this.post_events) { if (event instanceof PostEvent && event.arg instanceof Command) { switch (event.arg.cmd) { case "startTrillSpan": case "trill": return ImplicitType.Trill; case "turn": return ImplicitType.Turn; case "mordent": return ImplicitType.Mordent; case "prall": return ImplicitType.Prall; // Arpeggio is not implemented in 'articulate.ly' yet case "arpeggio": return ImplicitType.Arpeggio; } } } } return ImplicitType.None; } get clarified (): MusicEvent { const clarified = this instanceof Chord ? this.clarifiedChord : this.clone(); clarified.duration = this.durationValue && this.durationValue.clone(); return clarified; } }; export class Chord extends MusicEvent { pitches: (ChordElement | Command)[]; options: { exclamations?: string[], questions?: string[], rest?: string, withAngle?: boolean, }; constructor (data) { super(data); this.connectPitches(); } connectPitches () { if (this.basePitch) this.basePitch._parent = this; for (let i = 1; i < this.pitchElements.length; ++i) this.pitchElements[i]._previous = this.pitchElements[i - 1]; } get single (): boolean { return this.pitches.length === 1; } get entries () { const list: any[] = [...this.pitches]; if (Array.isArray(this.post_events)) list.push(...this.post_events); return list; } serialize () { const innerPitches = this.pitches.map(BaseTerm.optionalSerialize); const pitches = (this.single && !this.options.withAngle) ? cc(innerPitches) : [ "<", "\b", ...cc(innerPitches), "\b", ">", ]; const {exclamations, questions, rest} = this.options; const postfix = cc([...(exclamations || []), ...(questions || []), ...BaseTerm.optionalSerialize(this.duration), rest] .filter(item => item) .map(item => ["\b", item]), ).concat(...(this.post_events || []).map(BaseTerm.optionalSerialize)); return [ new OpenLocator(this), ...pitches, ...postfix, new CloseLocator(this), ]; } get pitchElements (): ChordElement[] { return this.pitches.filter(pitch => pitch instanceof ChordElement) as ChordElement[]; } get pitchNames (): string[] { return this.pitchElements.map((elem: ChordElement) => elem.pitch.replace(/'|,/g, "")); } get basePitch (): ChordElement { return this.pitchElements[0]; } get absolutePitch (): ChordElement { console.assert(!!this.basePitch, "absolutePitch on non pitch:", this.join()); return this.basePitch.absolutePitch; } get anchorPitch (): ChordElement { if (this._anchorPitch) return this._anchorPitch; const previous = this.getPreviousT(Chord); if (previous) return previous.absolutePitch; return this.basePitch; } get isRest (): boolean { return !!this.options.rest; } get completeTied (): boolean { return this.pitchElements.filter(pitch => !pitch._tied).length === 0; } get pitchesValue (): (ChordElement | Command)[] { if (this._previous instanceof Chord && this.basePitch.pitch === "q") { const pitches = this._previous.pitchesValue.map(pitch => { const newPitch = pitch.clone(); if (newPitch instanceof ChordElement) { newPitch._location = this.basePitch._location; newPitch._tied = this.basePitch._tied; newPitch._parent = (pitch as ChordElement)._parent && this; newPitch._previous = (pitch as ChordElement)._previous; } return newPitch; }); const base = pitches.find(pitch => pitch instanceof ChordElement) as ChordElement; if (base) base.pitch = base.pitch.replace(/[,']/g, ""); return pitches; } return this.pitches; } get clarifiedChord (): MusicEvent { const clarified = this.clone(); clarified.pitches = this.pitchesValue.filter(pitch => !(pitch as ChordElement)._tied).map(pitch => pitch.clone()); clarified.connectPitches(); // replace by rest if all pitches tied if (!clarified.pitches.length) return new Rest({name: "r", duration: this.duration}); return clarified; } shiftAnchor (newAnchor: ChordElement) { //console.warn("shiftAnchor:", this.join(), newAnchor.join(), this.absolutePitch.pitchValue, newAnchor.pitchValue, this.anchorPitch.pitchValue); const _location = this.basePitch._location; const shift = idioms.phonetDifferToShift(this.basePitch.phonetStep - newAnchor.phonetStep); const relativeOctave = this.basePitch.absoluteOctave(this.anchorPitch) - newAnchor.octave - shift; //console.log("_location:", _location); this.pitches[0] = ChordElement.from({ phonet: this.basePitch.phonet, alters: this.basePitch.alters, octave: relativeOctave, }); this.pitches[0]._location = _location; this.pitches[0]._parent = this; this.connectPitches(); //console.log("shiftAnchor.1:", this.join(), this.absolutePitch.pitchValue, {relativeOctave, shift, "newAnchor.octave": newAnchor.octave}); } makeRelativeTo (from: ChordElement) { const _location = this.basePitch._location; const octave = this.basePitch.relativeOctave(from); this.pitches[0] = ChordElement.from({ phonet: this.basePitch.phonet, alters: this.basePitch.alters, octave, }); this.pitches[0]._location = _location; this.pitches[0]._parent = this; } }; export class Rest extends MusicEvent { name: string; serialize () { return [ new OpenLocator(this), ...compact([ this.name, ...BaseTerm.optionalSerialize(this.duration), ]), ...cc((this.post_events || []).map(BaseTerm.optionalSerialize)), new CloseLocator(this), ]; } get isSpacer () { return this.name === "s"; } get isRest (): boolean { return true; } }; export class ChordElement extends BaseTerm { pitch: string; options: { exclamations?: string[], questions?: string[], post_events?: PostEvent[], }; declare _parent?: Chord; declare _previous?: ChordElement; _tied?: MusicEvent; _transposition?: number; // cache for property of absolutePitch _absolutePitch?: ChordElement; static from ({phonet, alters = "", octave, options = {}}): ChordElement { const octaveString = octave ? Array(Math.abs(octave)).fill(octave > 0 ? "'" : ",").join("") : ""; const pitch = phonet + (alters || "") + octaveString; return new ChordElement({pitch, options: {...options, proto: "_PLAIN"}}); } static get default (): ChordElement { return ChordElement.from({phonet: "c", octave: 0}); } constructor (data: object) { super(data); if (this.options.post_events) this.options.post_events = this.options.post_events.map(parseRaw); if (!this.pitch) console.log("null pitch:", this); } serialize () { const {exclamations, questions, post_events} = this.options; const postfix = [].concat(...[...(exclamations || []), ...(questions || [])] .filter(item => item) .map(item => ["\b", item]), ).concat(...(post_events || []).map(item => ["\b", ...BaseTerm.optionalSerialize(item)])); return [ new OpenLocator(this), this.pitch, ...postfix, new CloseLocator(this), ]; } get octave (): number { const positive = (this.pitch.match(/'/g) || []).length; const negative = (this.pitch.match(/,/g) || []).length; return positive - negative; } get phonet (): string { const ph = this.pitch.substr(0, 1); return idioms.PHONETS_ALIAS[ph] || ph; } get phonetStep (): number { return idioms.PHONETS.indexOf(this.phonet); } get alters (): string { const captures = this.pitch.substr(1).match(/^\w+/); return captures && captures[0]; } get alteredPhonet (): string { const captures = this.pitch.match(/^\w+/); return captures && captures[0]; } get anchorPitch (): ChordElement { if (this._previous) return this._previous.absolutePitch; if (this._parent) return this._parent.anchorPitch; return ChordElement.from({phonet: this.phonet, octave: 0}); } getAbsolutePitch (): ChordElement { if (this.phonet === "q") return this.anchorPitch; if (this.anchorPitch === this) return this; const octave = this.absoluteOctave(this.anchorPitch); return ChordElement.from({phonet: this.phonet, alters: this.alters, octave}); } get absolutePitch (): ChordElement { if (!this._absolutePitch) this._absolutePitch = this.getAbsolutePitch(); return this._absolutePitch; } absoluteOctave (anchor: ChordElement): number { if (this.phonet === "q") return anchor.octave; const phonetDiffer = this.phonetStep - anchor.phonetStep; const shift = idioms.phonetDifferToShift(phonetDiffer); return anchor.octave + shift + this.octave; } relativeOctave (from: ChordElement): number { if (this.phonet === "q") { if (this.anchorPitch) return this.anchorPitch.relativeOctave(from); else return 0; } const phonetDiffer = this.phonetStep - from.phonetStep; const shift = idioms.phonetDifferToShift(phonetDiffer); return this.octave - shift - from.octave; } get alterValue (): number { return idioms.ALTER_VALUES[this.alters] || 0; } get pitchValue (): number { const phonetValue = idioms.PHONET_VALUES[this.phonet]; console.assert(Number.isInteger(phonetValue), "invalid phonet:", this.phonet); console.assert(!this.alters || idioms.ALTER_VALUES[this.alters], "invalid alters:", this.alters); return 48 + this.octave * 12 + phonetValue + this.alterValue; } get absolutePitchValue (): number { return this.absolutePitch.pitchValue; } // middle C is zero get notePosition (): number { const phonet = idioms.PHONETS.indexOf(this.phonet); return (this.octave - 1) * 7 + phonet; } get absoluteNotePosition (): number { return this.absolutePitch.notePosition; } get tiedParent (): ChordElement { if (!this._tied || !(this._tied instanceof Chord)) return null; const pitch = this._tied.pitchElements.find(p => p.absolutePitchValue === this.absolutePitchValue); if (!pitch) return null; if (pitch._tied) return pitch.tiedParent; return pitch; } }; export class Duration extends BaseTerm { number: string; dots: number; multipliers?: string[]; static _default: Duration; static get default (): Duration { if (!Duration._default) Duration._default = new Duration({number: 4, dots: 0}); return Duration._default; } static fromMagnitude (magnitude: number): Duration { const MULTI = 1024; const MULTI_DURATION_MAGNITUDE = WHOLE_DURATION_MAGNITUDE * MULTI; const multiMag = magnitude * MULTI; if (!Number.isInteger(multiMag)) { console.warn("magnitude must be integer:", magnitude); return null; } const di = gcd(multiMag, MULTI_DURATION_MAGNITUDE); const denominator = MULTI_DURATION_MAGNITUDE / di; const numerator = multiMag / di; if (!Number.isInteger(Math.log2(denominator))) return new Duration({number: 1, dots: 0, multipliers: [`${numerator}/${denominator}`]}); switch (numerator) { case 1: return new Duration({number: denominator, dots: 0}); case 3: return new Duration({number: denominator / 2, dots: 1}); case 7: return new Duration({number: denominator / 4, dots: 2}); default: return new Duration({number: denominator, dots: 0, multipliers: [numerator.toString()]}); } } serialize () { const dots = Array(this.dots).fill(".").join(""); const multipliers = this.multipliers && this.multipliers.map(multiplier => `*${multiplier}`).join(""); return compact([ this.number, dots, multipliers, ]); } get withMultiplier () { return this.multipliers && this.multipliers.length > 0; } get denominator (): number { switch (this.number) { case "\\breve": return 0.5; case "\\longa": return 0.25; } return Number(this.number); } get division (): number { return Math.log2(this.denominator); } // how many smallest rhythm unit in a whole note get subdivider (): number { return this.denominator * (2 ** this.dots); } get magnitude (): number { let value = WHOLE_DURATION_MAGNITUDE / this.denominator; if (this.dots) value *= 2 - 0.5 ** this.dots; if (this.multipliers) this.multipliers.forEach(multiplier => value *= eval(multiplier)); return value; } }; interface BriefChordBody { pitch: string; duration: Duration; separator: string; items: string[]; }; export class BriefChord extends BaseTerm { body: BriefChordBody; post_events: any[]; constructor (data: object) { super(data); if (this.body) this.body.duration = parseRaw(this.body.duration); } serialize () { const {pitch, duration, separator, items} = this.body; return [ ...compact(cc([pitch, duration, separator, ...(items || [])].map(BaseTerm.optionalSerialize))), ...cc((this.post_events || []).map(BaseTerm.optionalSerialize)), ]; } get isMusic (): boolean { return true; } get durationMagnitude (): number { if (this.body.duration) return this.body.duration.magnitude; return 0; } }; export class NumberUnit extends BaseTerm { number: number; unit: string; serialize () { return [this.number, "\b", this.unit]; } set ({number, unit}) { this.number = Number(number.toFixed(2)); if (unit !== undefined) this.unit = unit; } } export class Tempo extends BaseTerm { beatsPerMinute?: number; unit?: Duration; text?: string; static fromNoteBpm (note: number, bpm: number): Tempo { return new Tempo ({ unit: new Duration({number: note.toString(), dots: 0}), beatsPerMinute: bpm, }); } serialize () { const assignment = Number.isFinite(this.beatsPerMinute) ? [...BaseTerm.optionalSerialize(this.unit), "=", this.beatsPerMinute] : []; return [ "\\tempo", ...BaseTerm.optionalSerialize(this.text), ...assignment, ]; } } const DIRECTION_CHAR = { up: "^", down: "_", middle: "-", }; export class PostEvent extends BaseTerm { direction: string; arg: string | BaseTerm; serialize () { const dir = DIRECTION_CHAR[this.direction]; const prefix = dir ? [dir, "\b"] : []; return prefix.concat(BaseTerm.optionalSerialize(this.arg)); } get entries () { if (this.arg instanceof BaseTerm) return [this.arg]; return null; } get isTying (): boolean { return this.arg === "~"; } get isStaccato (): boolean { if (this.arg instanceof Command) return ["staccato", "staccatissimo", "portato"].includes(this.arg.cmd); if ([".", "!"].includes(this.arg as string)) return true; return false; } }; export class Fingering extends BaseTerm { value: number; serialize () { return [this.value]; } }; export class Markup extends BaseTerm { head: any[]; body: (string|BaseTerm); serialize () { return [ ...cc(this.head.map(BaseTerm.optionalSerialize)), ...BaseTerm.optionalSerialize(this.body), ]; } }; export class Lyric extends MusicEvent { content: string | LiteralString; serialize () { return [ ...BaseTerm.optionalSerialize(this.content), ...BaseTerm.optionalSerialize(this.duration), ...cc((this.post_events || []).map(BaseTerm.optionalSerialize)), ]; } }; export class Comment extends BaseTerm { text: string; scoped: boolean; serialize () { return [ this.text, "\n", ]; } static createSingle (text): Comment { return new Comment({text: "%" + text}); } static createScoped (text): Comment { console.assert(!/%\}/.test(text), "invalid scoped comment text:", text); return new Comment({text: `%{${text}%}`, scoped: true}); } }; export class Unexpect extends BaseTerm { constructor (data) { super(data); console.warn("unexpected term", data); } }; export const termDictionary = { Root, LiteralString, Command, Variable, MarkupCommand, Repeat, Relative, ParallelMusic, TimeSignature, Partial, Times, Tuplet, Grace, AfterGrace, Clef, KeySignature, OctaveShift, Include, Version, Language, LyricMode, ChordMode, Transposition, StemDirection, Change, Block, InlineBlock, Scheme, SchemeFunction, SchemePair, SchemePointer, SchemeEmbed, Assignment, Duration, ChordElement, Chord, Rest, BriefChord, NumberUnit, MusicBlock, SimultaneousList, ContextedMusic, Divide, Tempo, PostEvent, Fingering, Markup, Lyric, Primitive, Comment, }; const termProtoMap: Map = Object.entries(termDictionary) .reduce((map, [name, cls]: [string, {prototype: object}]) => (map.set(cls.prototype, name), map), new Map()); const parseRawEnforce = data => { switch (typeof data) { case "string": case "number": return new Primitive({exp: data}); default: return parseRaw(data); } }; export const parseRaw = data => { if (data instanceof BaseTerm) return data; if (!data) return data; switch (typeof data) { case "object": if (Array.isArray(data)) return data.map(item => parseRaw(item)); const {proto, ...fields} = data; if (proto) { if (proto === "_PLAIN") return fields; const termClass = termDictionary[proto]; if (!termClass) throw new Error(`Unexpected term class: ${data.proto}`); return new termClass(fields); } return new Unexpect(data); } return data; };