UNPKG

smoosic

Version:

<sub>[Github site](https://github.com/Smoosic/smoosic) | [source documentation](https://smoosic.github.io/Smoosic/release/docs/modules.html) | [change notes](https://aarondavidnewman.github.io/Smoosic/changes.html) | [application](https://smoosic.github.i

1,001 lines (972 loc) 29.4 kB
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. /** * Classes to support {@link SmoNote}. Notes have pitches and a duration, and other * modifiers that can affect display or playback. * @module /smo/data/note */ import { smoSerialize } from '../../common/serializationHelpers'; import { SmoNoteModifierBase, SmoArticulation, SmoLyric, SmoGraceNote, SmoMicrotone, SmoOrnament, SmoDynamicText, SmoArpeggio, SmoArticulationParametersSer, GraceNoteParamsSer, SmoOrnamentParamsSer, SmoMicrotoneParamsSer, SmoClefChangeParamsSer, SmoClefChange, SmoLyricParamsSer, SmoDynamicTextSer, SmoTabNote, SmoTabNoteParamsSer, SmoTabNoteParams, SmoFretPosition} from './noteModifiers'; import { SmoMusic } from './music'; import { Ticks, Pitch, SmoAttrs, Transposable, PitchLetter, SvgBox, getId } from './common'; import { FontInfo, vexCanonicalNotes } from '../../common/vex'; import { SmoTupletParamsSer } from './tuplet'; /** * @category SmoObject */ export interface TupletInfo { id: string; } // @internal export type NoteType = 'n' | 'r' | '/'; // @internal export type NoteStringParam = 'noteHead' | 'clef'; // @internal export const NoteStringParams: NoteStringParam[] = ['noteHead', 'clef']; // @internal export type NoteNumberParam = 'beamBeats' | 'flagState' | 'beamState'; // @internal export const NoteNumberParams: NoteNumberParam[] = ['beamBeats', 'flagState', 'beamState']; // @internal export type NoteBooleanParam = 'hidden' | 'isCue'; // @internal export const NoteBooleanParams: NoteBooleanParam[] = ['hidden','isCue']; /** * Constructor parameters for a note. Usually you will call * {@link SmoNote.defaults}, and modify the parameters you need to change. * @param noteType * @param noteHead is non-empty, a Vex notehead code TODO make a record<> * @param clef determines how the pitch is placed on the staff * @param textModifiers are lyrics, chords, dynamics * @param articulations * @param graceNotes * @param ornaments * @param tones * @param tuplet tuplet info, if the note is part of a tuplet * @param endBeam true if this is the last note in a beam * @param fillStyle for special effects, for instance to highlight active voice * @param hidden indicates the note (usually a rest) is invisible (transparent) * @param beamBeats how many ticks to use before beaming a group * @param beamState the beaming configuration * @param flagState up down auto * @param ticks duration * @param stemTicks visible duration (todo update this comment) * @param pitches SmoPitch array * @param isCue tiny notes * @category SmoObject */ export interface SmoNoteParams { /** note, rest, slash */ noteType: NoteType, /** * custom note head, defaults to black or open (based on duration) */ noteHead: string, /** * clef of this note, determines leger lines and sound */ clef: string, /** * lyrics, annotations */ textModifiers: SmoNoteModifierBase[], /** * articulations attached to the note */ articulations: SmoArticulation[], /** * grace notes before the note */ graceNotes: SmoGraceNote[], /** * ornaments attached to the note */ ornaments: SmoOrnament[], /** * microtones attached to the note */ tones: SmoMicrotone[], /** * arpeggio on the note */ arpeggio?: SmoArpeggio, /** * if this note is part of a tuplet */ tupletId: string | null, /* * If a custom tab note is assigned to this note */ tabNote?: SmoTabNote, /** * fill, for the pretty */ fillStyle: string | null, /** * indicates 'hidden' note. Useful for padding beginning/end of partial measures */ hidden: boolean, /** * how many notes to beam before creating a new beam group */ beamBeats: number, /** * auto-beam, force end or secondary */ beamState: number, /** * up, down, auto */ flagState: number, /** * note duration */ ticks: Ticks, /** * visible duration */ stemTicks: number, /** * pitch for leger lines and sounds */ pitches: Pitch[], /** * draw cue sized */ isCue: boolean, /** * indicates this note goes with a clef change */ clefNote: SmoClefChangeParamsSer } export type SmoNoteTextModifierSer = SmoLyricParamsSer | SmoDynamicTextSer; /** * The serializable bits of a Note. Notes will always * have a type, and if a sounded note, can contain pitches. It will always * contains ticks. * @category serialization */ export interface SmoNoteParamsSer { /** constructor */ ctor: string; /** attributes for identity */ attrs: SmoAttrs; /** note, rest, slash */ noteType: NoteType, /** * custom note head, defaults to black or open (based on duration) */ noteHead: string, /** * clef of this note, determines leger lines and sound */ clef: string, /** * lyrics, annotations */ textModifiers: SmoNoteTextModifierSer[], /** * articulations attached to the note */ articulations: SmoArticulationParametersSer, /** * grace notes before the note */ graceNotes: GraceNoteParamsSer[], /** * ornaments attached to the note */ ornaments: SmoOrnamentParamsSer[], /** * microtones attached to the note */ tones: SmoMicrotoneParamsSer[], /** * arpeggio on the note */ arpeggio?: SmoArticulationParametersSer, /** * if this note is part of a tuplet */ tupletId?: string, /** * If a custom tab note is here, keep track of it */ tabNote?: SmoTabNoteParamsSer, /** * does this note end the secondary beam group */ endSecondaryBeam: boolean, /** * fill, for the pretty */ fillStyle: string | null, /** * indicates 'hidden' note. Useful for padding beginning/end of partial measures */ hidden: boolean, /** * how many notes to beam before creating a new beam group */ beamBeats: number, /** * auto, end, secondary, continue */ beamState: number, /** * up, down, auto */ flagState: number, /** * note duration */ ticks: Ticks, /** * visible duration (todo: update this comment) */ stemTicks: number, /** * pitch for leger lines and sounds */ pitches: Pitch[], /** * draw cue sized */ isCue: boolean, /** * indicates this note goes with a clef change */ clefNote? : SmoClefChangeParamsSer } function isSmoNoteParamsSer(params: Partial<SmoNoteParamsSer>): params is SmoNoteParamsSer { if (params.ctor && params.ctor === 'SmoNote') { return true; } return false; } export function isSmoNote(transposable: Transposable): transposable is SmoNote { if (Array.isArray((transposable as any).graceNotes)) { return true; } return false; } /** * SmoNote contains the pitch and duration of a note or chord. * It can also contain arrays of modifiers like lyrics, articulations etc. * Also information about the beaming, flag etc. * @category SmoObject * */ export class SmoNote implements Transposable { constructor(params: SmoNoteParams) { const defs = SmoNote.defaults; // Handle legacy beam state const aparam = params as any; if (typeof(aparam['endBeam']) === 'boolean') { if (aparam.endBeam) { params.beamState = SmoNote.beamStates.end; } } NoteStringParams.forEach((param) => { this[param] = params[param] ? params[param] : defs[param]; }); this.tupletId = params.tupletId; this.noteType = params.noteType ? params.noteType : defs.noteType; NoteNumberParams.forEach((param) => { this[param] = params[param] ? params[param] : defs[param]; }); NoteBooleanParams.forEach((param) => { this[param] = params[param] ? params[param] : defs[param]; }); if (params.clefNote) { this.clefNote = new SmoClefChange(params.clefNote); } if (params.tabNote) { this.tabNote = new SmoTabNote(params.tabNote); } const pitches = params.pitches ? params.pitches : defs.pitches; const ticks = params.ticks ? params.ticks : defs.ticks; this.ticks = JSON.parse(JSON.stringify(ticks)); this.stemTicks = params.stemTicks ? params.stemTicks : defs.stemTicks; this.pitches = JSON.parse(JSON.stringify(pitches)); this.clef = params.clef ? params.clef : defs.clef; this.fillStyle = params.fillStyle ? params.fillStyle : ''; // legacy tuplet, now we just need the tuplet id if ((params as any).tuplet) { this.tupletId = (params as any).tuplet.id; } this.attrs = { id: getId().toString(), type: 'SmoNote' }; // else inherit } static get flagStates() { return { auto: 0, up: 1, down: 2 }; } static get beamStates() { return { auto: 0, continue: 1, end: 2, secondary: 3 }; } static get beamStateMax() { return SmoNote.beamStates.secondary; } // Note type and ID attrs: SmoAttrs; flagState: number = SmoNote.flagStates.auto; beamState: number = SmoNote.beamStates.auto; textModifiers: SmoNoteModifierBase[] = []; articulations: SmoArticulation[] = []; ornaments: SmoOrnament[] = []; pitches: Pitch[] = []; noteHead: string = ''; arpeggio?: SmoArpeggio; tabNote?: SmoTabNote; clef: string = 'treble'; clefNote: SmoClefChange | null = null; graceNotes: SmoGraceNote[] = []; noteType: NoteType = 'n'; fillStyle: string = ''; hidden: boolean = false; tupletId: string | null = null; tones: SmoMicrotone[] = []; endBeam: boolean = false; endSecondaryBeam: boolean = false; ticks: Ticks = { numerator: 4096, denominator: 1, remainder: 0 }; stemTicks: number = 4096; beamBeats: number = 4096; beam_group: SmoAttrs | null = null; renderId: string | null = null; keySignature: string = 'c'; logicalBox: SvgBox | null = null; isCue: boolean = false; hasTabNote: boolean = true; accidentalsRendered: string[] = [];// set by renderer if accidental is to display /** * used in serialization * @internal */ static get parameterArray() { return ['ticks', 'pitches', 'noteType', 'tuplet', 'clef', 'isCue', 'stemTicks', 'beamBeats', 'flagState', 'beamState', 'noteHead', 'fillStyle', 'hidden', 'arpeggio', 'clefNote', 'tupletId']; } /** * Default constructor parameters. We always return a copy so the caller can modify it */ static get defaults(): SmoNoteParams { return JSON.parse(JSON.stringify({ noteType: 'n', noteHead: 'n', clef: 'treble', textModifiers: [], articulations: [], graceNotes: [], ornaments: [], tones: [], fillStyle: '', hidden: false, beamBeats: 4096, isCue: false, beamState: SmoNote.beamStates.auto, flagState: SmoNote.flagStates.auto, ticks: { numerator: 4096, denominator: 1, remainder: 0 }, stemTicks: 4096, pitches: [{ letter: 'b', octave: 4, accidental: 'n' }], })); } /** * Up, down auto (tri-state) */ toggleFlagState() { this.flagState = (this.flagState + 1) % 3; } //todo: double check this get dots() { const vexDuration = SmoMusic.closestSmoDurationFromTicks(this.stemTicks); if (!vexDuration) { return 0; } return vexDuration.dots; } private _addModifier(dynamic: SmoDynamicText, toAdd: boolean) { var tms = []; this.textModifiers.forEach((tm) => { if (tm.attrs.type !== dynamic.attrs.type) { tms.push(tm); } }); if (toAdd) { tms.push(dynamic); } this.textModifiers = tms; } setArticulation(articulation: SmoArticulation, set: boolean) { var tms = []; this.articulations.forEach((tm) => { if (tm.articulation !== articulation.articulation) { tms.push(tm); } }); if (set) { tms.push(articulation); } this.articulations = tms; } clearArticulations() { this.articulations = []; } getArticulations() { return this.articulations; } getArticulation(stringCode: string) { return this.articulations.find((aa) => aa.articulation === stringCode); } getOrnament(stringCode: string) { return this.ornaments.find((aa) => aa.ornament === stringCode); } /** * Add a new dynamic to thisnote * @param dynamic */ addDynamic(dynamic: SmoDynamicText) { this._addModifier(dynamic, true); } /** * Remove the dynamic from this note. * @param dynamic */ removeDynamic(dynamic: SmoDynamicText) { this._addModifier(dynamic, false); } /** * Get all note modifiers of a type, either a lyric or a dynamic * @param type ctor * @returns */ getModifiers(type: string) { var ms = this.textModifiers.filter((mod) => mod.attrs.type === type ); return ms; } setArpeggio(arp: SmoArpeggio) { this.arpeggio = arp; } /** * * @returns the longest lyric, used for formatting */ longestLyric(): SmoLyric | null { const tms: SmoNoteModifierBase[] = this.textModifiers.filter((mod: SmoNoteModifierBase) => mod.attrs.type === 'SmoLyric' && (mod as SmoLyric).parser === SmoLyric.parsers.lyric ); if (!tms.length) { return null; } return tms.reduce((m1, m2) => (m1 as SmoLyric).getText().length > (m2 as SmoLyric).getText().length ? m1 : m2 ) as SmoLyric; } /** Add a lyric to this note, replacing another in the same verse */ addLyric(lyric: SmoLyric) { const tms = this.textModifiers.filter((mod: SmoNoteModifierBase) => mod.attrs.type !== 'SmoLyric' || (mod as SmoLyric).parser !== lyric.parser || (mod as SmoLyric).verse !== lyric.verse ); tms.push(lyric); this.textModifiers = tms; } /** * @returns array of lyrics that are lyrics */ getTrueLyrics(): SmoLyric[] { const ms = this.textModifiers.filter((mod) => mod.attrs.type === 'SmoLyric' && (mod as SmoLyric).parser === SmoLyric.parsers.lyric); ms.sort((a, b) => (a as SmoLyric).verse - (b as SmoLyric).verse); return (ms as SmoLyric[]); } /** * * @returns array of SmoLyric whose parsers are chord */ getChords(): SmoLyric[] { const ms = this.textModifiers.filter((mod) => mod.attrs.type === 'SmoLyric' && (mod as SmoLyric).parser === SmoLyric.parsers.chord ); return ms as SmoLyric[]; } /** * * @param lyric lyric to remove, find the best match if there are multiples */ removeLyric(lyric: SmoLyric) { const tms = this.textModifiers.filter((mod: SmoNoteModifierBase) => mod.attrs.type !== 'SmoLyric' || (mod as SmoLyric).verse !== lyric.verse || (mod as SmoLyric).parser !== lyric.parser ); this.textModifiers = tms; } /** * * @param verse * @param parser * @returns */ getLyricForVerse(verse: number, parser: number) { return this.textModifiers.filter((mod) => mod.attrs.type === 'SmoLyric' && (mod as SmoLyric).parser === parser && (mod as SmoLyric).verse === verse ); } /** * * @param fontInfo */ setLyricFont(fontInfo: FontInfo) { const lyrics = this.getTrueLyrics(); lyrics.forEach((lyric) => { lyric.fontInfo = JSON.parse(JSON.stringify(fontInfo)); }); } /** * @param adjustNoteWidth if true, vex will consider the lyric width when formatting the measure */ setLyricAdjustWidth(adjustNoteWidth: boolean) { const lyrics = this.getTrueLyrics(); lyrics.forEach((lyric) => { lyric.adjustNoteWidth = adjustNoteWidth; }); } setChordAdjustWidth(adjustNoteWidth: boolean) { const chords = this.getChords(); chords.forEach((chord) => { chord.adjustNoteWidth = adjustNoteWidth; }); } setChordFont(fontInfo: FontInfo) { const chords = this.getChords(); chords.forEach((chord) => { chord.fontInfo = JSON.parse(JSON.stringify(fontInfo)); }); } getOrnaments() { return this.ornaments.filter((oo) => oo.isJazz() === false && typeof(SmoOrnament.textNoteOrnaments[oo.ornament]) !== 'string'); } getJazzOrnaments() { return this.ornaments.filter((oo) => oo.isJazz()); } getTextOrnaments() { return this.ornaments.filter((oo) => typeof(SmoOrnament.textNoteOrnaments[oo.ornament]) === 'string'); } /** * Toggle the ornament up/down/off * @param ornament */ toggleOrnament(ornament: SmoOrnament) { const aix = this.ornaments.filter((a) => a.attrs.type === 'SmoOrnament' && a.ornament === ornament.ornament ); if (!aix.length) { this.ornaments.push(ornament); } else { this.ornaments = []; } } setOrnament(ornament: SmoOrnament, set: boolean) { const aix = this.ornaments.filter((a) => a.ornament !== ornament.ornament ); this.ornaments = aix; if (set) { this.ornaments.push(ornament); } } setTabNote(params: SmoTabNoteParams) { this.tabNote = new SmoTabNote(params); this.tabNote.isAssigned = true; } clearTabNote() { this.tabNote = undefined; } /** * Toggle the ornament up/down/off * @param articulation */ toggleArticulation(articulation: SmoArticulation) { var aix = this.articulations.findIndex((a) => a.articulation === articulation.articulation ); if (aix >= 0) { const cur = this.articulations[aix]; if (cur.position === SmoArticulation.positions.above) { cur.position = SmoArticulation.positions.below; return; } else { this.setArticulation(articulation, false); return; } } this.setArticulation(articulation, true); } /** * Sort pitches in pitch order, Vex likes to receive pitches in order * @param note */ static sortPitches(note: Transposable) { const canon = vexCanonicalNotes(); const keyIndex = ((pitch: Pitch) => canon.indexOf(pitch.letter) + pitch.octave * 12 ); note.pitches.sort((a, b) => keyIndex(a) - keyIndex(b)); } setNoteHead(noteHead: string) { if (this.noteHead === noteHead) { this.noteHead = ''; } else { this.noteHead = noteHead; } } /** * * @param graceNote * @param offset the index from the first grace note */ addGraceNote(graceNote: SmoGraceNote, offset: number) { if (typeof(offset) === 'undefined') { offset = 0; } graceNote.clef = this.clef; this.graceNotes.push(graceNote); } removeGraceNote(offset: number) { if (offset >= this.graceNotes.length) { return; } this.graceNotes.splice(offset, 1); } getGraceNotes() { return this.graceNotes; } /** * Add another pitch to this note at `offset` 1/2 steps * @param note * @param offset */ static addPitchOffset(note: Transposable, offset: number): void { if (note.pitches.length === 0) { return; } note.noteType = 'n'; const pitch = note.pitches[0]; note.pitches.push(SmoMusic.getKeyOffset(pitch, offset)); SmoNote.sortPitches(note); } /** * Add another pitch to this note at `offset` 1/2 steps * @param offset * @returns */ addPitchOffset(offset: number) { if (this.pitches.length === 0) { return; } this.noteType = 'n'; const pitch = this.pitches[0]; this.pitches.push(SmoMusic.getKeyOffset(pitch, offset)); SmoNote.sortPitches(this); } toggleRest() { this.noteType = (this.noteType === 'r' ? 'n' : 'r'); } toggleSlash() { this.noteType = (this.noteType === '/' ? 'n' : '/'); } makeSlash() { this.noteType = '/'; } makeRest() { this.noteType = 'r'; } isRest() { return this.noteType === 'r'; } isSlash() { return this.noteType === '/'; } isHidden() { return this.hidden; } makeNote() { this.noteType = 'n'; // clear fill style if we were hiding rests this.fillStyle = ''; this.hidden = false; } /** * set note opacity on/off * @param val */ makeHidden(val: boolean) { this.hidden = val; this.fillStyle = val ? '#aaaaaa7f' : ''; } /** * Return true if this note is part of a tuplet */ get isTuplet(): boolean { return typeof(this.tupletId) !== 'undefined' && this.tupletId !== null && this.tupletId.length > 0; } /** * we only support a single microtone, not sure if vex supports multiple * @param tone */ addMicrotone(tone: SmoMicrotone) { const ar = this.tones.filter((tn: SmoMicrotone) => tn.pitchIndex !== tone.pitchIndex); ar.push(tone); this.tones = ar; } removeMicrotone() { this.tones = []; } getMicrotone(toneIndex: number) { return this.tones.find((tn) => tn.pitchIndex === toneIndex); } getMicrotones() { return this.tones; } /** * cycle through the list of enharmonics for this note. * @param pitch * @returns */ static toggleEnharmonic(pitch: Pitch) { const lastLetter = pitch.letter; let vexPitch = SmoMusic.stripVexOctave(SmoMusic.pitchToVexKey(pitch)); vexPitch = SmoMusic.getEnharmonic(vexPitch); pitch.letter = vexPitch[0] as PitchLetter; pitch.accidental = vexPitch.length > 1 ? vexPitch.substring(1, vexPitch.length) : 'n'; pitch.octave += SmoMusic.letterChangedOctave(lastLetter, pitch.letter); return pitch; } /** * transpose a note or grace note to a key-friendly enharmonic * @param pitchArray * @param offset * @param originalKey - keySignature from original note * @param destinationKey - keySignature we are transposing into * @returns */ transpose(pitchArray: number[], offset: number, originalKey: string, destinationKey: string): Transposable { return SmoNote.transpose(this, pitchArray, offset, originalKey, destinationKey); } /** * used to add chord and pitch by piano widget * @param pitch */ toggleAddPitch(pitch: Pitch) { const pitches: Pitch[] = []; let exists = false; this.pitches.forEach((o) => { if (o.letter !== pitch.letter || o.octave !== pitch.octave || o.accidental !== pitch.accidental) { pitches.push(o); } else { exists = true; } }); this.pitches = pitches; if (!exists) { this.pitches.push(JSON.parse(JSON.stringify(pitch))); this.noteType = 'n'; } SmoNote.sortPitches(this); } /** * @param note note to transpose * @param pitchArray an array of indices (not pitches) that indicate which pitches get altered if a chord * @param offset in 1/2 step * @param originalKey original key for enharmonic-friendly key * @param destinationKey destination key signature * @returns */ static transpose(note: Transposable, pitchArray: number[], offset: number, originalKey: string, destinationKey: string): Transposable { let index: number = 0; let j: number = 0; if (offset === 0 && originalKey === destinationKey) { return note; } // If no specific pitch, use all the pitches if (pitchArray.length === 0) { pitchArray = Array.from(note.pitches.keys()); } for (j = 0; j < pitchArray.length; ++j) { index = pitchArray[j]; if (index + 1 > note.pitches.length) { SmoNote.addPitchOffset(note, offset); } else { const original = JSON.parse(JSON.stringify(note.pitches[index])); const pitch = SmoMusic.transposePitchForKey(original, originalKey, destinationKey, offset); note.pitches[index] = pitch; } } // If the fret position can be adjusted on the current string, keep the tab note. Else // delete the tab note, and auto-generate it to display default if (isSmoNote(note)) { const sn: SmoNote = note; if (sn.tabNote && sn.tabNote.positions.length > 0) { const frets: SmoFretPosition[] = []; sn.tabNote.positions.forEach((pos) => { if (pos.fret + offset > 0) { frets.push({ string: pos.string, fret: pos.fret + offset}); } }); if (frets.length) { sn.tabNote.positions = frets; } else { sn.tabNote = undefined; } } } SmoNote.sortPitches(note); return note; } get tickCount() { return this.ticks.numerator / this.ticks.denominator + this.ticks.remainder; } /** * Copy the note, give it unique id * @param note * @returns */ static clone(note: SmoNote) { var rv = SmoNote.deserialize(note.serialize()); // make sure id is unique rv.attrs = { id: getId().toString(), type: 'SmoNote' }; return rv; } /** * @param note * @param ticks * @returns A note identical to `note` but with different duration */ static cloneWithDuration(note: SmoNote, ticks: Ticks | number, stemTicks: number | null = null) { if (typeof(ticks) === 'number') { ticks = { numerator: ticks, denominator: 1, remainder: 0 }; } const rv = SmoNote.clone(note); rv.ticks = ticks; if (stemTicks === null) { rv.stemTicks = ticks.numerator + ticks.remainder; } else { rv.stemTicks = stemTicks; } return rv; } static serializeModifier(modifiers: SmoNoteModifierBase[]) : object[] { const rv: object[] = []; modifiers.forEach((modifier: SmoNoteModifierBase) => { rv.push(modifier.serialize()); }); return rv; } private _serializeModifiers(params: any) { params.textModifiers = SmoNote.serializeModifier(this.textModifiers); params.graceNotes = SmoNote.serializeModifier(this.graceNotes); params.articulations = SmoNote.serializeModifier(this.articulations); params.ornaments = SmoNote.serializeModifier(this.ornaments); params.tones = SmoNote.serializeModifier(this.tones); if (this.arpeggio) { params.arpeggio = this.arpeggio.serialize(); } } /** * @returns a JSON object that can be used to create this note */ serialize(): SmoNoteParamsSer { var params: Partial<SmoNoteParamsSer> = { ctor: 'SmoNote' }; smoSerialize.serializedMergeNonDefault(SmoNote.defaults, SmoNote.parameterArray, this, params); if (this.tabNote) { params.tabNote = this.tabNote.serialize(); } if (this.clefNote) { params.clefNote = this.clefNote.serialize(); } if (params.ticks) { params.ticks = JSON.parse(JSON.stringify(params.ticks)); } this._serializeModifiers(params); if (!isSmoNoteParamsSer(params)) { throw 'bad note ' + JSON.stringify(params); } return params; } /** * restore note modifiers and create a SmoNote object * @param jsonObj * @returns */ static deserialize(jsonObj: any) { //legacy note if (jsonObj.ticks && jsonObj.stemTicks === undefined) { if (jsonObj.tupletId || jsonObj.tuplet) { jsonObj['stemTicks'] = SmoMusic.closestBeamDuration(jsonObj.ticks.numerator / jsonObj.ticks.denominator + jsonObj.ticks.remainder)!.ticks; } else { jsonObj['stemTicks'] = SmoMusic.closestSmoDurationFromTicks(jsonObj.ticks.numerator / jsonObj.ticks.denominator + jsonObj.ticks.remainder)!.ticks; } } var note = new SmoNote(jsonObj); if (jsonObj.textModifiers) { jsonObj.textModifiers.forEach((mod: any) => { note.textModifiers.push(SmoNoteModifierBase.deserialize(mod)); }); } if (jsonObj.graceNotes) { jsonObj.graceNotes.forEach((mod: any) => { note.graceNotes.push(SmoNoteModifierBase.deserialize(mod)); }); } if (jsonObj.ornaments) { jsonObj.ornaments.forEach((mod: any) => { note.ornaments.push(SmoNoteModifierBase.deserialize(mod)); }); } if (jsonObj.articulations) { jsonObj.articulations.forEach((mod: any) => { note.articulations.push(SmoNoteModifierBase.deserialize(mod)); }); } if (jsonObj.tones) { jsonObj.tones.forEach((mod: any) => { note.tones.push(SmoNoteModifierBase.deserialize(mod)); }); } // Due to a bug, text modifiers were serialized into noteModifiers array if (jsonObj.noteModifiers) { jsonObj.noteModifiers.forEach((mod: any) => { note.textModifiers.push(SmoNoteModifierBase.deserialize(mod)); }); } if (jsonObj.arpeggio) { note.arpeggio = SmoNoteModifierBase.deserialize(jsonObj.arpeggio); } if (jsonObj.clefNote) { note.clefNote = SmoNoteModifierBase.deserialize(jsonObj.clefNote); } return note; } }