Spaces:
Sleeping
Sleeping
| 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 = <T>(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<number, MusicChunk>; | |
| 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<number, MusicChunk>(); | |
| 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, string> = 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; | |
| }; | |