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

417 lines (366 loc) 11.9 kB
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. /** * Classes to support {@link SmoTuplet} * @module /smo/data/tuplet */ import { smoSerialize } from '../../common/serializationHelpers'; import { SmoNote, SmoNoteParamsSer, TupletInfo } from './note'; import { SmoMusic } from './music'; import { SmoNoteModifierBase } from './noteModifiers'; import { getId, SmoAttrs, Clef } from './common'; import {SmoMeasure, SmoVoice} from './measure'; import {tuplets} from "vexflow_smoosic/build/esm/types/tests/formatter/tests"; /** * @category SmoObject */ export interface SmoTupletTreeParams { tuplet: SmoTuplet } /** * @category serialization */ export interface SmoTupletTreeParamsSer { /** * constructor */ ctor: string, /** * root tuplet */ tuplet: SmoTupletParamsSer } /** * @category SmoObject */ export class SmoTupletTree { /** * root tuplet */ tuplet: SmoTuplet; constructor(params: SmoTupletTreeParams) { this.tuplet = params.tuplet; } static syncTupletIds(tupletTrees: SmoTupletTree[], voices: SmoVoice[]) { const traverseTupletTree = (parentTuplet: SmoTuplet): void => { const notes: SmoNote[] = voices[parentTuplet.voice].notes; for (let i = parentTuplet.startIndex; i <= parentTuplet.endIndex; i++) { const note: SmoNote = notes[i]; note.tupletId = parentTuplet.attrs.id; } for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { const tuplet = parentTuplet.childrenTuplets[i]; traverseTupletTree(tuplet); } }; //traverse tuplet tree for (let i = 0; i < tupletTrees.length; i++) { const tupletTree: SmoTupletTree = tupletTrees[i]; traverseTupletTree(tupletTree.tuplet); } } static adjustTupletIndexes(tupletTrees: SmoTupletTree[], voice: number, startTick: number, diff: number) { const traverseTupletTree = (parentTuplet: SmoTuplet): void => { if (parentTuplet.endIndex >= startTick) { parentTuplet.endIndex += diff; if(parentTuplet.startIndex > startTick) { parentTuplet.startIndex += diff; } } for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { const tuplet = parentTuplet.childrenTuplets[i]; traverseTupletTree(tuplet); } } //traverse tuplet tree for (let i = 0; i < tupletTrees.length; i++) { const tupletTree: SmoTupletTree = tupletTrees[i]; if (tupletTree.endIndex >= startTick && tupletTree.voice == voice) { traverseTupletTree(tupletTree.tuplet); } } } static getTupletForNoteIndex(tupletTrees: SmoTupletTree[], voiceIx: number, noteIx: number): SmoTuplet | null { const tuplets = SmoTupletTree.getTupletHierarchyForNoteIndex(tupletTrees, voiceIx, noteIx); if(tuplets.length) { return tuplets[tuplets.length - 1]; } return null; } static getTupletTreeForNoteIndex(tupletTrees: SmoTupletTree[], voiceIx: number, noteIx: number): SmoTupletTree | null { for (let i = 0; i < tupletTrees.length; i++) { const tupletTree: SmoTupletTree = tupletTrees[i]; if (tupletTree.startIndex <= noteIx && tupletTree.endIndex >= noteIx && tupletTree.voice == voiceIx) { return tupletTree; } } return null; } // Finds the tuplet hierarchy for a given note index. static getTupletHierarchyForNoteIndex(tupletTrees: SmoTupletTree[], voiceIx: number, noteIx: number): SmoTuplet[] { let tupletHierarchy: SmoTuplet[] = []; const traverseTupletTree = ( parentTuplet: SmoTuplet): void => { tupletHierarchy.push(parentTuplet); for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { const tuplet = parentTuplet.childrenTuplets[i]; if (tuplet.startIndex <= noteIx && tuplet.endIndex >= noteIx) { traverseTupletTree(tuplet); break; } } } //find tuplet tree for (let i = 0; i < tupletTrees.length; i++) { const tupletTree: SmoTupletTree = tupletTrees[i]; if (tupletTree.startIndex <= noteIx && tupletTree.endIndex >= noteIx && tupletTree.voice == voiceIx) { traverseTupletTree(tupletTree.tuplet); break; } } return tupletHierarchy; } static removeTupletForNoteIndex(measure: SmoMeasure, voiceIx: number, noteIx: number) { for (let i = 0; i < measure.tupletTrees.length; i++) { const tupletTree: SmoTupletTree = measure.tupletTrees[i]; if (tupletTree.startIndex <= noteIx && tupletTree.endIndex >= noteIx && tupletTree.voice == voiceIx) { measure.tupletTrees.splice(i, 1); break; } } } /** * Determines whether two notes are part of the same tuplet. * @param noteOne * @param noteTwo */ static areNotesPartOfTheSameTuplet(noteOne: SmoNote, noteTwo: SmoNote): boolean { if (noteOne.tupletId === noteTwo.tupletId) { return true; } return false; } static areTupletsBothNull(noteOne: SmoNote, noteTwo: SmoNote): boolean { return (noteOne.tupletId ?? null) === null && (noteTwo.tupletId ?? null) === null; } serialize(): SmoTupletTreeParamsSer { const params = { ctor: 'SmoTupletTree', tuplet: this.tuplet.serialize() }; return params; } static deserialize(jsonObj: SmoTupletTreeParamsSer): SmoTupletTree { const tuplet = SmoTuplet.deserialize(jsonObj.tuplet); return new SmoTupletTree({tuplet: tuplet}); } static clone(tupletTree: SmoTupletTree): SmoTupletTree { return SmoTupletTree.deserialize(tupletTree.serialize()); } get startIndex() { return this.tuplet.startIndex; } get endIndex() { return this.tuplet.endIndex; } get voice() { return this.tuplet.voice; } get totalTicks() { return this.tuplet.totalTicks; } } /** * Parameters for tuplet construction * @param notes - runtime instance of tuplet has an actual instance of * notes. The note instances are created by the deserilization of the * measure. We serialize the note parameters so we can identify the correct notes * when deserializing. * @category SmoObject */ export interface SmoTupletParams { numNotes: number, notesOccupied: number, stemTicks: number, totalTicks: number, ratioed: boolean, bracketed: boolean, voice: number, startIndex: number, endIndex: number, } /** * serializabl bits of SmoTuplet * @category serialization */ export interface SmoTupletParamsSer { /** * constructor */ ctor: string, /** * attributes for ID */ attrs: SmoAttrs, /** * numNotes in the tuplet (not necessarily same as notes array size) */ numNotes: number, /** * */ notesOccupied: number, /** * used to decide how to beam, 2048 for 1/4 triplet for instance */ stemTicks: number, /** * total ticks to squeeze numNotes */ totalTicks: number, /** * whether to use the : */ ratioed: boolean, /** * whether to show the brackets */ bracketed: boolean, /** * which voice the tuplet applies to */ voice: number, startIndex: number, endIndex: number, parentTuplet: TupletInfo | null, childrenTuplets: SmoTupletParamsSer[] } /** * tuplets must be serialized with their id attribute, enforce this * @param params a possible-valid SmoTupletParamsSer * @returns */ function isSmoTupletParamsSer(params: Partial<SmoTupletParamsSer>): params is SmoTupletParamsSer { if (!params.ctor || !(params.ctor === 'SmoTuplet')) { return false; } if (!params.attrs || !(typeof(params.attrs.id) === 'string')) { return false; } return true; } /** * A tuplet is a container for notes within a measure * @category SmoObject */ export class SmoTuplet { static get defaults(): SmoTupletParams { return JSON.parse(JSON.stringify({ numNotes: 3, notesOccupied: 2, stemTicks: 2048, startIndex: 0, endIndex: 0, totalTicks: 4096, // how many ticks this tuple takes up bracketed: true, voice: 0, ratioed: false })); } attrs: SmoAttrs; numNotes: number = 3; notesOccupied: number = 2; stemTicks: number = 2048; totalTicks: number = 4096; bracketed: boolean = true; voice: number = 0; ratioed: boolean = false; parentTuplet: TupletInfo | null = null; childrenTuplets: SmoTuplet[] = []; startIndex: number = 0; endIndex: number = 0; get clonedParams() { const paramAr = ['stemTicks', 'ticks', 'totalTicks', 'numNotes']; const rv = {}; smoSerialize.serializedMerge(paramAr, this, rv); return rv; } static get parameterArray() { return ['stemTicks', 'totalTicks', 'startIndex', 'endIndex', 'attrs', 'ratioed', 'bracketed', 'voice', 'numNotes']; } serialize(): SmoTupletParamsSer { const params: Partial<SmoTupletParamsSer> = {}; params.ctor = 'SmoTuplet'; params.childrenTuplets = []; smoSerialize.serializedMergeNonDefault(SmoTuplet.defaults, SmoTuplet.parameterArray, this, params); this.childrenTuplets.forEach((tuplet) => { params.childrenTuplets!.push(tuplet.serialize()); }); if (!isSmoTupletParamsSer(params)) { throw 'bad tuplet ' + JSON.stringify(params); } return params; } static deserialize(jsonObj: SmoTupletParamsSer): SmoTuplet { const tupJson = SmoTuplet.defaults; smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj, tupJson); // Legacy schema did not have notesOccupied, we need to calculate it. if ((jsonObj as any).notes !== undefined) { //todo: notesOccupied can probably be removed tupJson.notesOccupied = tupJson.totalTicks / tupJson.stemTicks; } const tuplet = new SmoTuplet(tupJson); tuplet.parentTuplet = jsonObj.parentTuplet ? jsonObj.parentTuplet : null; if (jsonObj.childrenTuplets !== undefined) { for (let i = 0; i < jsonObj.childrenTuplets.length; i++) { const childTuplet = SmoTuplet.deserialize(jsonObj.childrenTuplets[i]); tuplet.childrenTuplets.push(childTuplet); } } return tuplet; } static calculateStemTicks(totalTicks: number, numNotes: number) { const stemValue = totalTicks / numNotes; let stemTicks = SmoTuplet.longestTuplet; // The stem value is the type on the non-tuplet note, e.g. 1/8 note // for a triplet. while (stemValue < stemTicks) { stemTicks = stemTicks / 2; } if (numNotes === 2) { return stemTicks; } return stemTicks * 2; } constructor(params: SmoTupletParams) { const defs = SmoTuplet.defaults; const mergedParams = { ...defs, ...params }; this.numNotes = mergedParams.numNotes; this.notesOccupied = mergedParams.notesOccupied; this.stemTicks = mergedParams.stemTicks; this.totalTicks = mergedParams.totalTicks; this.bracketed = mergedParams.bracketed; this.voice = mergedParams.voice; this.ratioed = mergedParams.ratioed; this.startIndex = mergedParams.startIndex; this.endIndex = mergedParams.endIndex; this.attrs = { id: getId().toString(), type: 'SmoTuplet' }; this.stemTicks = SmoTuplet.calculateStemTicks(this.totalTicks, this.numNotes); } static get longestTuplet() { return 8192; } //todo: adjust naming get num_notes() { return this.numNotes; } get notes_occupied() { return Math.floor(this.totalTicks / this.stemTicks); } get tickCount() { return this.totalTicks; } }