From fc665fce09c8e90072f84727ef510c913ad8df78 Mon Sep 17 00:00:00 2001 From: Nenad Strangar <44262111+strangarnenad@users.noreply.github.com> Date: Mon, 18 Mar 2024 19:07:53 +0000 Subject: [PATCH 01/14] removed DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 7dc6f5e3f83981e93f8e08bedc985ffa885f3fd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK-HOvd6h5=fx=B^+g|K>;8^MdRG$K?HA=WOk7lK&P3zglZiM!EkQj*l7wbC2k zLG%fH6<@%|(eKR6mh{J46`2EPzR8($X3l&eXEH=2dgI6=Y7vnQWwe@THVBWiuF00R zoCgXyM|6XIfJqGyO-KLPnlu%0b@0Ux$9q9F+Qe^5i&!`-xVVF;?r8(hyrBv z1}$eLb3%^UhP*!4Ht5h}+NXQer-#5jI$fniW>}BdL4Gl)i?LEk@fj_shtz`c1R_)@P=yM8#SkhSc31oP8s`dCI0=3E5PD^yZzw{pj`>~bPQq7cTEl>0V3~o2 zdTjCezyJID|8kIdG7K07{woHA)gSbG*pfb5*EYvztqVPZvasJ=p$tKzk7L>4qxcF` a3dUUS0DX;fg|I-(kAS4XG=_mc%D^v@RJDWv From 6561326da9294d25ca5c19cf344e4029cac08330 Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Mon, 25 Mar 2024 18:13:52 +0100 Subject: [PATCH 02/14] Initial implementation --- src/application/exports.ts | 4 +- src/common/vex.ts | 18 +- src/render/vex/toVex.ts | 25 +- src/render/vex/vxMeasure.ts | 63 +-- src/smo/data/measure.ts | 327 +++++++++------ src/smo/data/note.ts | 30 +- src/smo/data/tuplet.ts | 394 +++++++++--------- src/smo/midi/midiToSmo.ts | 23 +- src/smo/mxml/smoToXml.ts | 25 +- src/smo/mxml/xmlState.ts | 7 +- src/smo/xform/audioTrack.ts | 6 +- src/smo/xform/beamers.ts | 78 ++-- src/smo/xform/copypaste.ts | 590 +++++++++++++-------------- src/smo/xform/operations.ts | 98 +---- src/smo/xform/tickDuration.ts | 743 ++++++++++------------------------ tests/file-load.ts | 2 +- 16 files changed, 1081 insertions(+), 1352 deletions(-) diff --git a/src/application/exports.ts b/src/application/exports.ts index e359abcc..d14f7d9c 100644 --- a/src/application/exports.ts +++ b/src/application/exports.ts @@ -128,7 +128,7 @@ import { SuiSampleMedia } from '../render/audio/samples'; import { SmoScore, engravingFontTypes, isEngravingFont } from '../smo/data/score'; import { UndoBuffer } from '../smo/xform/undo'; import { SmoNote } from '../smo/data/note'; -import { SmoDuration } from '../smo/xform/tickDuration'; +// import { SmoDuration } from '../smo/xform/tickDuration'; import { createLoadTests } from '../../tests/file-load'; import { SmoStaffHairpin, StaffModifierBase, SmoInstrument, SmoSlur, SmoTie, SmoStaffTextBracket } from '../smo/data/staffModifiers'; import { SmoMeasure } from '../smo/data/measure'; @@ -232,7 +232,7 @@ export const Smo = { SmoOrnament, SmoArticulation, SmoDynamicText, SmoGraceNote, SmoMicrotone, SmoLyric, SmoArpeggio, SmoClefChange, // Smo Transformers - SmoSelection, SmoSelector, SmoDuration, UndoBuffer, SmoToVex, SmoOperation, + SmoSelection, SmoSelector, /*SmoDuration,*/ UndoBuffer, SmoToVex, SmoOperation, // new score bootstrap // help strings cardKeysHtmlEn, cardNotesLetterHtmlEn, cardNotesChromaticHtmlEn, cardNotesChordsHtmlEn, diff --git a/src/common/vex.ts b/src/common/vex.ts index df05ba17..74a70ce4 100644 --- a/src/common/vex.ts +++ b/src/common/vex.ts @@ -14,6 +14,7 @@ ClefNote as VexClefNote, * Most of the differences are trivial - e.g. different naming conventions for variables. */ import { smoSerialize } from "./serializationHelpers"; +import { SmoMusic } from "../smo/data/music"; // export type Vex = SmoVex; export const VexFlow = SmoVex.Flow; const VF = VexFlow; @@ -53,8 +54,11 @@ export interface GlyphInfo { // DI interfaces to create vexflow objects export interface CreateVexNoteParams { - isTuplet: boolean, measureIndex: number, clef: string, - closestTicks: string, exactTicks: string, keys: string[], + isTuplet: boolean, + measureIndex: number, + clef: string, + stemTicks: string, + keys: string[], noteType: string }; @@ -186,10 +190,12 @@ export function getVexTuplets(params: SmoVexTupletParams) { export function getVexNoteParameters(params: CreateVexNoteParams) { // If this is a tuplet, we only get the duration so the appropriate stem // can be rendered. Vex calculates the actual ticks later when the tuplet is made - var duration = - params.isTuplet ? - params.closestTicks : - params.exactTicks; + // var duration = + // params.isTuplet ? + // params.closestTicks : + // params.exactTicks; + + var duration: any = params.stemTicks; if (typeof (duration) === 'undefined') { console.warn('bad duration in measure ' + params.measureIndex); duration = '8'; diff --git a/src/render/vex/toVex.ts b/src/render/vex/toVex.ts index fb5d2ed1..76b81270 100644 --- a/src/render/vex/toVex.ts +++ b/src/render/vex/toVex.ts @@ -78,10 +78,12 @@ function smoNoteToGraceNotes(smoNote: SmoNote, strs: string[]) { } } function smoNoteToStaveNote(smoNote: SmoNote) { - const duration = - smoNote.isTuplet ? - SmoMusic.closestVexDuration(smoNote.tickCount) : - SmoMusic.ticksToDuration[smoNote.tickCount]; + // const duration = + // smoNote.isTuplet ? + // SmoMusic.closestVexDuration(smoNote.tickCount) : + // SmoMusic.ticksToDuration[smoNote.tickCount]; + + const duration = SmoMusic.ticksToDuration[smoNote.stemTicks]; const sn: StaveNoteStruct = { clef: smoNote.clef, duration, @@ -427,20 +429,19 @@ function createBeamGroups(smoMeasure: SmoMeasure, strs: string[]) { } function createTuplets(smoMeasure: SmoMeasure, strs: string[]) { smoMeasure.voices.forEach((voice, voiceIx) => { - const tps = smoMeasure.tuplets.filter((tp) => tp.voice === voiceIx); + const tps = smoMeasure.tupletTrees.filter((tp) => tp.voice === voiceIx); for (var i = 0; i < tps.length; ++i) { const tp = tps[i]; const nar: string[] = []; - for (var j = 0; j < tp.notes.length; ++j) { - const note = tp.notes[j]; + for ( let note of smoMeasure.tupletNotes(tp)) { const vexNote = `${note.attrs.id}`; nar.push(vexNote); } - const direction = tp.getStemDirection(smoMeasure.clef) === SmoNote.flagStates.up ? + const direction = smoMeasure.getStemDirectionForTuplet(tp) === SmoNote.flagStates.up ? VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM; const tpParams: TupletOptions = { - num_notes: tp.num_notes, - notes_occupied: tp.notes_occupied, + num_notes: tp.numNotes, + notes_occupied: tp.notesOccupied, ratioed: false, bracketed: true, location: direction @@ -493,9 +494,9 @@ function createMeasure(smoMeasure: SmoMeasure, heightOffset: number, strs: strin strs.push(`${bg.attrs.id}.setContext(context);`); strs.push(`${bg.attrs.id}.draw();`) }); - smoMeasure.tuplets.forEach((tp) => { + smoMeasure.tupletTrees.forEach((tp) => { strs.push(`${tp.attrs.id}.setContext(context).draw();`) - }) + }); } // ## SmoToVex // Simple serialize class that produced VEX note and voice objects diff --git a/src/render/vex/vxMeasure.ts b/src/render/vex/vxMeasure.ts index a4d3bc4d..4250e90e 100644 --- a/src/render/vex/vxMeasure.ts +++ b/src/render/vex/vxMeasure.ts @@ -27,6 +27,7 @@ import { VexFlow, Stave,StemmableNote, Note, Beam, Tuplet, Voice, } from '../../common/vex'; import { VxMeasureIf, VexNoteModifierIf, VxNote } from './vxNote'; +import { SmoTuplet } from '../../smo/data/tuplet'; const VF = VexFlow; declare var $: any; @@ -142,15 +143,17 @@ export class VxMeasure implements VxMeasureIf { createVexNote(smoNote: SmoNote, tickIndex: number, voiceIx: number) { let vexNote: Note | null = null; let timestamp = new Date().valueOf(); - const closestTicks = SmoMusic.closestVexDuration(smoNote.tickCount); - const exactTicks = SmoMusic.ticksToDuration[smoNote.tickCount]; + const stemTicks = SmoMusic.ticksToDuration[smoNote.stemTicks]; const noteHead = smoNote.isRest() ? 'r' : smoNote.noteHead; const keys = SmoMusic.smoPitchesToVexKeys(smoNote.pitches, 0, noteHead); const smoNoteParams = { - isTuplet: smoNote.isTuplet, measureIndex: this.smoMeasure.measureNumber.measureIndex, - clef: smoNote.clef, - closestTicks, exactTicks, keys, - noteType: smoNote.noteType }; + isTuplet: smoNote.isTuplet, + measureIndex: this.smoMeasure.measureNumber.measureIndex, + clef: smoNote.clef, + stemTicks, + keys, + noteType: smoNote.noteType + }; const { noteParams, duration } = getVexNoteParameters(smoNoteParams); if (smoNote.noteType === '/') { // vexNote = new VF.GlyphNote('\uE504', { duration }); @@ -321,37 +324,37 @@ export class VxMeasure implements VxMeasureIf { } } - /** - * Create the VF tuplet objects based on the smo tuplet objects - * @param vix - */ - // createVexTuplets(vix: number) { - var j = 0; - var i = 0; this.vexTuplets = []; this.tupletToVexMap = {}; - for (i = 0; i < this.smoMeasure.tuplets.length; ++i) { - const tp = this.smoMeasure.tuplets[i]; + for (let i = 0; i < this.smoMeasure.tupletTrees.length; ++i) { + const tp = this.smoMeasure.tupletTrees[i]; if (tp.voice !== vix) { continue; } - const vexNotes: Note[] = []; - for (j = 0; j < tp.notes.length; ++j) { - const smoNote = tp.notes[j]; - vexNotes.push(this.noteToVexMap[smoNote.attrs.id]); - } - const location = tp.getStemDirection(this.smoMeasure.clef) === SmoNote.flagStates.up ? - VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM; - const smoTupletParams = { - vexNotes, - numNotes: tp.numNotes, - notesOccupied: tp.note_ticks_occupied, - location + const traverseTupletTree = ( parentTuplet: SmoTuplet): void => { + const vexNotes = []; + for (let smoNote of this.smoMeasure.tupletNotes(parentTuplet)) { + vexNotes.push(this.noteToVexMap[smoNote.attrs.id]); + } + const location = this.smoMeasure.getStemDirectionForTuplet(parentTuplet) === SmoNote.flagStates.up ? + VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM; + const smoTupletParams = { + vexNotes, + numNotes: parentTuplet.numNotes, + notesOccupied: parentTuplet.notesOccupied, + location + } + const vexTuplet = getVexTuplets(smoTupletParams); + + this.tupletToVexMap[parentTuplet.attrs.id] = vexTuplet; + this.vexTuplets.push(vexTuplet); + for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { + const tuplet = parentTuplet.childrenTuplets[i]; + traverseTupletTree(tuplet); + } } - const vexTuplet = getVexTuplets(smoTupletParams); - this.tupletToVexMap[tp.attrs.id] = vexTuplet; - this.vexTuplets.push(vexTuplet); + traverseTupletTree(tp); } } diff --git a/src/smo/data/measure.ts b/src/smo/data/measure.ts index d94b9c08..ea4c8399 100644 --- a/src/smo/data/measure.ts +++ b/src/smo/data/measure.ts @@ -135,7 +135,7 @@ export const SmoMeasureStringParams: SmoMeasureStringParam[] = ['keySignature']; export interface SmoMeasureParams { timeSignature: TimeSignature, keySignature: string, - tuplets: SmoTuplet[], + tupletTrees: SmoTuplet[], transposeIndex: number, lines: number, staffY: number, @@ -177,7 +177,7 @@ export interface SmoMeasureParamsSer { /** * a list of tuplets (serialized) */ - tuplets: SmoTupletParamsSer[], + tupletTrees: SmoTupletParamsSer[], /** * transpose the notes up/down. TODO: this should not be serialized * as its part of the instrument parameters @@ -225,7 +225,7 @@ export interface SmoMeasureParamsSer { */ function isSmoMeasureParamsSer(params: Partial):params is SmoMeasureParamsSer { if (!Array.isArray(params.voices) || - !Array.isArray(params.tuplets) || !Array.isArray(params.modifiers) || + !Array.isArray(params.tupletTrees) || !Array.isArray(params.modifiers) || typeof(params?.measureNumber?.measureIndex) !== 'number') { return false; } @@ -249,7 +249,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { static readonly _defaults: SmoMeasureParams = { timeSignature: SmoMeasure.timeSignatureDefault, keySignature: 'C', - tuplets: [], + tupletTrees: [], transposeIndex: 0, modifiers: [], staffY: 40, @@ -302,7 +302,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { */ keySignature: string = ''; canceledKeySignature: string = ''; - tuplets: SmoTuplet[] = []; + tupletTrees: SmoTuplet[] = []; repeatSymbol: boolean = false; repeatCount: number = 0; /** @@ -400,7 +400,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { } } this.voices = params.voices ? params.voices : []; - this.tuplets = params.tuplets ? params.tuplets : []; + this.tupletTrees = params.tupletTrees ? params.tupletTrees : []; this.modifiers = params.modifiers ? params.modifiers : defaults.modifiers; this.setDefaultBarlines(); this.keySignature = SmoMusic.vexKeySigWithOffset(this.keySignature, this.transposeIndex); @@ -532,12 +532,12 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { smoSerialize.serializedMergeNonDefault(SmoMeasure.defaults, SmoMeasure.serializableAttributes, this, params); // measure number can't be defaulted b/c tempos etc. can map to default measure params.measureNumber = JSON.parse(JSON.stringify(this.measureNumber)); - params.tuplets = []; + params.tupletTrees = []; params.voices = []; params.modifiers = []; - this.tuplets.forEach((tuplet) => { - params.tuplets!.push(tuplet.serialize()); + this.tupletTrees.forEach((tuplet) => { + params.tupletTrees!.push(tuplet.serialize()); }); this.voices.forEach((voice) => { @@ -604,20 +604,21 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { } } - const tuplets = []; - for (j = 0; j < jsonObj.tuplets.length; ++j) { + //todo: implement this + // const tuplets = []; + for (j = 0; j < jsonObj.tupletTrees.length; ++j) { const tupJson = SmoTuplet.defaults; - smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj.tuplets[j], tupJson); - const noteAr = noteSum.filter((nn: any) => - nn.isTuplet && nn.tuplet.id === tupJson.attrs!.id); - - // Bug fix: A tuplet with no notes may be been overwritten - // in a copy/paste operation - if (noteAr.length > 0) { - tupJson.notes = noteAr; - const tuplet = new SmoTuplet(tupJson); - tuplets.push(tuplet); - } + smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj.tupletTrees[j], tupJson); + // const noteAr = noteSum.filter((nn: any) => + // nn.isTuplet && nn.tuplet.id === tupJson.attrs!.id); + + // // Bug fix: A tuplet with no notes may be been overwritten + // // in a copy/paste operation + // if (noteAr.length > 0) { + // tupJson.notes = noteAr; + // const tuplet = new SmoTuplet(tupJson); + // tuplets.push(tuplet); + // } } const modifiers: SmoMeasureModifierBase[] = []; @@ -655,7 +656,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { } params.keySignature = jsonObj.keySignature ?? 'C'; params.voices = voices; - params.tuplets = tuplets; + // params.tuplets = tuplets; params.modifiers = modifiers; const rv = new SmoMeasure(params); // Handle migration for measure-mapped parameters @@ -753,6 +754,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { nextNote.noteType = 'r'; nextNote.clef = clef; nextNote.ticks.numerator = noteTick; + nextNote.stemTicks = noteTick; pnotes.push(new SmoNote(nextNote)); ticks += noteTick; } @@ -899,79 +901,80 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { this.svg.staffX = Math.round(x); } /** + * todo nenad: adjust implementation * A time signature has possibly changed. add/remove notes to * match the new length */ alignNotesWithTimeSignature() { - const tsTicks = SmoMusic.timeSignatureToTicks(this.timeSignature.timeSignature); - if (tsTicks === this.getMaxTicksVoice()) { - return; - } - const replaceNoteWithDuration = (target: number, ar: SmoNote[], note: SmoNote) => { - const fitNote = new SmoNote(SmoNote.defaults); - const duration = SmoMusic.closestDurationTickLtEq(target); - if (duration > 128) { - fitNote.ticks = { numerator: duration, denominator: 1, remainder: 0 }; - fitNote.pitches = note.pitches; - fitNote.noteType = note.noteType; - fitNote.clef = note.clef; - ar.push(fitNote); - } - } - const voices: SmoVoice[] = []; - const tuplets: SmoTuplet[] = []; - for (var i = 0; i < this.voices.length; ++i) { - const voice = this.voices[i]; - const newNotes: SmoNote[] = []; - let voiceTicks = 0; - for (var j = 0; j < voice.notes.length; ++j) { - const note = voice.notes[j]; - // if a tuplet, make sure the whole tuplet fits. - if (note.isTuplet) { - const tuplet = this.getTupletForNote(note); - if (tuplet) { - // remaining notes of an approved tuplet, just add them - if (tuplet.startIndex !== j) { - newNotes.push(note); - continue; - } - else if (tuplet.tickCount + voiceTicks <= tsTicks) { - // first note of the tuplet, it fits, add it - voiceTicks += tuplet.tickCount; - newNotes.push(note); - tuplets.push(tuplet); - } else { - // tuplet will not fit. Make a note as close to remainder as possible and add it - replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); - voiceTicks = tsTicks; - break; - } - } else { // missing tuplet, now what? - console.warn('missing tuplet info'); - replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); - voiceTicks = tsTicks; - } - } else { - if (note.tickCount + voiceTicks <= tsTicks) { - newNotes.push(note); - voiceTicks += note.tickCount; - } else { - replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); - voiceTicks = tsTicks; - break; - } - } - } - if (tsTicks - voiceTicks > 128) { - const np = SmoNote.defaults; - np.clef = this.clef; - const nnote = new SmoNote(np); - replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, nnote); - } - voices.push({ notes: newNotes }); - } - this.voices = voices; - this.tuplets = tuplets; + // const tsTicks = SmoMusic.timeSignatureToTicks(this.timeSignature.timeSignature); + // if (tsTicks === this.getMaxTicksVoice()) { + // return; + // } + // const replaceNoteWithDuration = (target: number, ar: SmoNote[], note: SmoNote) => { + // const fitNote = new SmoNote(SmoNote.defaults); + // const duration = SmoMusic.closestDurationTickLtEq(target); + // if (duration > 128) { + // fitNote.ticks = { numerator: duration, denominator: 1, remainder: 0 }; + // fitNote.pitches = note.pitches; + // fitNote.noteType = note.noteType; + // fitNote.clef = note.clef; + // ar.push(fitNote); + // } + // } + // const voices: SmoVoice[] = []; + // const tuplets: SmoTuplet[] = []; + // for (var i = 0; i < this.voices.length; ++i) { + // const voice = this.voices[i]; + // const newNotes: SmoNote[] = []; + // let voiceTicks = 0; + // for (var j = 0; j < voice.notes.length; ++j) { + // const note = voice.notes[j]; + // // if a tuplet, make sure the whole tuplet fits. + // if (note.isTuplet) { + // const tuplet = this.getTupletForNote(note); + // if (tuplet) { + // // remaining notes of an approved tuplet, just add them + // if (tuplet.startIndex !== j) { + // newNotes.push(note); + // continue; + // } + // else if (tuplet.tickCount + voiceTicks <= tsTicks) { + // // first note of the tuplet, it fits, add it + // voiceTicks += tuplet.tickCount; + // newNotes.push(note); + // tuplets.push(tuplet); + // } else { + // // tuplet will not fit. Make a note as close to remainder as possible and add it + // replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); + // voiceTicks = tsTicks; + // break; + // } + // } else { // missing tuplet, now what? + // console.warn('missing tuplet info'); + // replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); + // voiceTicks = tsTicks; + // } + // } else { + // if (note.tickCount + voiceTicks <= tsTicks) { + // newNotes.push(note); + // voiceTicks += note.tickCount; + // } else { + // replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); + // voiceTicks = tsTicks; + // break; + // } + // } + // } + // if (tsTicks - voiceTicks > 128) { + // const np = SmoNote.defaults; + // np.clef = this.clef; + // const nnote = new SmoNote(np); + // replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, nnote); + // } + // voices.push({ notes: newNotes }); + // } + // this.voices = voices; + // this.tuplets = tuplets; } /** * Get rendered or estimated start y @@ -1170,6 +1173,34 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { return ticks; } + /** + * Count all the ticks up to the provided tickIndex + * @param voiceIndex + * @param tickIndex + */ + getNotePositionInTicks(voiceIndex: number, tickIndex: number): number { + let rv = 0; + for (let i = 0; i < tickIndex; i++) { + const note = this.voices[voiceIndex].notes[i]; + rv += note.tickCount; + } + return rv; + } + + /** + * Count all the ticks up to the provided tickIndex + * @param voiceIndex + * @param tickIndex + */ + getTickCountForNote(voiceIndex: number, note: SmoNote): number { + let rv = 0; + for (let i = 0; i < this.voices[voiceIndex].notes.length; i++) { + const currentNote = this.voices[voiceIndex].notes[i]; + rv += note.tickCount; + } + return rv; + } + getClosestTickCountIndex(voiceIndex: number, tickCount: number): number { let i = 0; let rv = 0; @@ -1228,25 +1259,35 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { }); } - // ### tuplet methods. - // - // #### tupletNotes - tupletNotes(tuplet: SmoTuplet) { - let j = 0; - let i = 0; - const tnotes = []; - for (j = 0; j < this.voices.length; ++j) { - const vnotes = this.voices[j].notes; - for (i = 0; i < vnotes.length; ++i) { - const note = vnotes[i] as SmoNote; - if (note.tuplet && note.tuplet.id === tuplet.attrs.id) { - tnotes.push(vnotes[i]); - } + tupletNotes(smoTuplet: SmoTuplet): SmoNote[] { + let tupletNotes: SmoNote[] = []; + for (let i = smoTuplet.startIndex; i <= smoTuplet.endIndex; i++) { + const note = this.voices[smoTuplet.voice].notes[i]; + tupletNotes.push(note); + } + return tupletNotes; + } + + getStemDirectionForTuplet(smoTuplet: SmoTuplet) { + let note: SmoNote | null = null; + for (let currentNote of this.tupletNotes(smoTuplet)) { + if (currentNote.noteType === 'n') { + note = currentNote; + break; } } - return tnotes; + + if (!note) { + return SmoNote.flagStates.down; + } + if (note.flagState !== SmoNote.flagStates.auto) { + return note.flagState; + } + return SmoMusic.pitchToLedgerLine(this.clef, note.pitches[0]) + >= 2 ? SmoNote.flagStates.up : SmoNote.flagStates.down; } + // #### tupletIndex // return the index of the given tuplet tupletIndex(tuplet: SmoTuplet) { @@ -1266,33 +1307,81 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { // #### getTupletForNote // Finds the tuplet for a given note, or null if there isn't one. - getTupletForNote(note: SmoNote | null): SmoTuplet | null { - let i = 0; + getTupletForNoteIndex(voiceIx: number, noteIx: number): SmoTuplet | null { + const tuplets = this.getTupletHierarchyForNoteIndex(voiceIx, noteIx); + if(tuplets.length) { + return tuplets[tuplets.length - 1]; + } + return null; + } + + // Finds the tuplet hierarchy for a given note. + getTupletHierarchyForNoteIndex(voiceIx: number, noteIx: number): SmoTuplet[] { + const note: SmoNote | undefined = this.voices[voiceIx]?.notes[noteIx]; if (!note) { - return null; + return []; } if (!note.isTuplet) { - return null; + return []; + } + + 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 < this.tupletTrees.length; i++) { + const tuplet: SmoTuplet = this.tupletTrees[i]; + if (tuplet.startIndex <= noteIx && tuplet.endIndex >= noteIx) { + traverseTupletTree(tuplet); + break; + } } - for (i = 0; i < this.tuplets.length; ++i) { - const tuplet = this.tuplets[i]; - if (note.tuplet !== null && tuplet.attrs.id === note.tuplet.id) { - return tuplet; + + return tupletHierarchy; + } + + adjustTupletIndexes(tick: number, diff: number) { + + const traverseTupletTree = ( parentTuplet: SmoTuplet): void => { + parentTuplet.endIndex += diff; + if(parentTuplet.startIndex > tick) { + parentTuplet.startIndex += diff; + } + for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { + const tuplet = parentTuplet.childrenTuplets[i]; + traverseTupletTree(tuplet); + } + } + + //find tuplet tree + for (let i = 0; i < this.tupletTrees.length; i++) { + const tuplet: SmoTuplet = this.tupletTrees[i]; + if (tuplet.endIndex >= tick) { + traverseTupletTree(tuplet); + break; } } - return null; } removeTupletForNote(note: SmoNote) { let i = 0; const tuplets = []; - for (i = 0; i < this.tuplets.length; ++i) { - const tuplet = this.tuplets[i]; + for (i = 0; i < this.tupletTrees.length; ++i) { + const tuplet = this.tupletTrees[i]; if (note.tuplet !== null && note.tuplet.id !== tuplet.attrs.id) { tuplets.push(tuplet); } } - this.tuplets = tuplets; + this.tupletTrees = tuplets; } setClef(clef: Clef) { const oldClef = this.clef; diff --git a/src/smo/data/note.ts b/src/smo/data/note.ts index 7077788a..2bb86815 100644 --- a/src/smo/data/note.ts +++ b/src/smo/data/note.ts @@ -24,9 +24,9 @@ export type NoteStringParam = 'noteHead' | 'clef'; // @internal export const NoteStringParams: NoteStringParam[] = ['noteHead', 'clef']; // @internal -export type NoteNumberParam = 'beamBeats' | 'flagState'; +export type NoteNumberParam = 'beamBeats' | 'flagState' | 'stemTicks'; // @internal -export const NoteNumberParams: NoteNumberParam[] = ['beamBeats', 'flagState']; +export const NoteNumberParams: NoteNumberParam[] = ['beamBeats', 'flagState', 'stemTicks']; // @internal export type NoteBooleanParam = 'hidden' | 'endBeam' | 'isCue'; // @internal @@ -49,6 +49,7 @@ export const NoteBooleanParams: NoteBooleanParam[] = ['hidden', 'endBeam', 'isCu * @param beamBeats how many ticks to use before beaming a group * @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 SmoParameters @@ -116,6 +117,10 @@ export interface SmoNoteParams { * note duration */ ticks: Ticks, + /** + * visible duration + */ + stemTicks: number, /** * pitch for leger lines and sounds */ @@ -203,6 +208,10 @@ export interface SmoNoteParamsSer { * note duration */ ticks: Ticks, + /** + * visible duration (todo: update this comment) + */ + stemTicks: number, /** * pitch for leger lines and sounds */ @@ -280,6 +289,7 @@ export class SmoNote implements Transposable { tones: SmoMicrotone[] = []; endBeam: 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; @@ -319,6 +329,7 @@ export class SmoNote implements Transposable { denominator: 1, remainder: 0 }, + stemTicks: 4096, pitches: [{ letter: 'b', octave: 4, @@ -333,11 +344,9 @@ export class SmoNote implements Transposable { this.flagState = (this.flagState + 1) % 3; } + //todo: double check this get dots() { - if (this.isTuplet) { - return 0; - } - const vexDuration = SmoMusic.closestSmoDurationFromTicks(this.tickCount); + const vexDuration = SmoMusic.closestSmoDurationFromTicks(this.stemTicks); if (!vexDuration) { return 0; } @@ -779,12 +788,19 @@ export class SmoNote implements Transposable { * @param ticks * @returns A note identical to `note` but with different duration */ - static cloneWithDuration(note: SmoNote, ticks: Ticks | number) { + 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[] { diff --git a/src/smo/data/tuplet.ts b/src/smo/data/tuplet.ts index c98dae52..6b7a331c 100644 --- a/src/smo/data/tuplet.ts +++ b/src/smo/data/tuplet.ts @@ -5,7 +5,7 @@ * @module /smo/data/tuplet */ import { smoSerialize } from '../../common/serializationHelpers'; -import { SmoNote, SmoNoteParamsSer } from './note'; +import { SmoNote, SmoNoteParamsSer, TupletInfo } from './note'; import { SmoMusic } from './music'; import { SmoNoteModifierBase } from './noteModifiers'; import { getId, SmoAttrs, Clef } from './common'; @@ -19,16 +19,15 @@ import { getId, SmoAttrs, Clef } from './common'; * @category SmoParameters */ export interface SmoTupletParams { - notes: SmoNote[], - attrs?: SmoAttrs, numNotes: number, + notesOccupied: number, stemTicks: number, totalTicks: number, - durationMap: number[], ratioed: boolean, bracketed: boolean, voice: number, - startIndex: number + startIndex: number, + endIndex: number, } /** * serializabl bits of SmoTuplet @@ -43,26 +42,23 @@ export interface SmoTupletParamsSer { * attributes for ID */ attrs: SmoAttrs, - /** - * info about the serialized notes - */ - notes: SmoNoteParamsSer[], /** * numNotes in the duplet (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, - /** - * map of notes to ticks - */ - durationMap: number[], /** * whether to use the : */ @@ -75,10 +71,15 @@ export interface SmoTupletParamsSer { * which voice the tuplet applies to */ voice: number, - /** - * the start tick index of the measure - */ - startIndex: number + + startIndex: number, + + endIndex: number, + + parentTuplet: TupletInfo | null, + + childrenTuplets: SmoTupletParamsSer[] + } /** @@ -102,48 +103,46 @@ function isSmoTupletParamsSer(params: Partial): params is Sm export class SmoTuplet { static get defaults(): SmoTupletParams { return JSON.parse(JSON.stringify({ - notes: [], numNotes: 3, + notesOccupied: 2, stemTicks: 2048, + startIndex: 0, + endIndex: 0, totalTicks: 4096, // how many ticks this tuple takes up - durationMap: [1.0, 1.0, 1.0], bracketed: true, voice: 0, - ratioed: false, - startIndex: 0 + ratioed: false })); } attrs: SmoAttrs; - notes: SmoNote[]; numNotes: number = 3; + notesOccupied: number = 2; stemTicks: number = 2048; totalTicks: number = 4096; - durationMap: number[] = [1.0, 1.0, 1.0]; 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', 'durationMap', 'numNotes']; + const paramAr = ['stemTicks', 'ticks', 'totalTicks', 'numNotes']; const rv = {}; smoSerialize.serializedMerge(paramAr, this, rv); return rv; } static get parameterArray() { - return ['stemTicks', 'ticks', 'totalTicks', - 'durationMap', 'attrs', 'ratioed', 'bracketed', 'voice', 'startIndex', 'numNotes']; + return ['stemTicks', 'ticks', 'totalTicks', 'startIndex', 'endIndex', + 'attrs', 'ratioed', 'bracketed', 'voice', 'numNotes', 'childrenTuplets', 'parentTuplet']; } serialize(): SmoTupletParamsSer { - const params:Partial = { - notes: [] + const params = { + ctor: 'SmoTuplet' }; - this.notes.forEach((nn) => { - params.notes!.push(nn.serialize()); - }); - params.ctor = 'SmoTuplet'; smoSerialize.serializedMergeNonDefault(SmoTuplet.defaults, SmoTuplet.parameterArray, this, params); if (!isSmoTupletParamsSer(params)) { @@ -166,203 +165,190 @@ export class SmoTuplet { constructor(params: SmoTupletParams) { smoSerialize.vexMerge(this, SmoTuplet.defaults); smoSerialize.serializedMerge(SmoTuplet.parameterArray, params, this); - this.notes = params.notes; this.attrs = { id: getId().toString(), type: 'SmoTuplet' }; - this._adjustTicks(); } static get longestTuplet() { return 8192; } + + //todo: implement this static cloneTuplet(tuplet: SmoTuplet): SmoTuplet { - let i = 0; - const noteAr = tuplet.notes; - const durationMap = JSON.parse(JSON.stringify(tuplet.durationMap)); // deep copy array + return new SmoTuplet({ + stemTicks: 0, + totalTicks: 0, + ratioed: false, + bracketed: true, + startIndex: 0, + endIndex: 0, + voice: 0, + numNotes: 3, + notesOccupied: 2, + }); + } - // Add any remainders for oddlets - const totalTicks = noteAr.map((nn) => nn.ticks.numerator + nn.ticks.remainder) - .reduce((acc, nn) => acc + nn); + // static cloneTuplet(tuplet: SmoTuplet): SmoTuplet { + // let i = 0; + // const noteAr = tuplet.notes; + // const durationMap = JSON.parse(JSON.stringify(tuplet.durationMap)); // deep copy array - const numNotes: number = tuplet.numNotes; - const stemTicks = SmoTuplet.calculateStemTicks(totalTicks, numNotes); + // // Add any remainders for oddlets + // const totalTicks = noteAr.map((nn) => nn.ticks.numerator + nn.ticks.remainder) + // .reduce((acc, nn) => acc + nn); - const tupletNotes: SmoNote[] = []; + // const numNotes: number = tuplet.numNotes; + // const stemTicks = SmoTuplet.calculateStemTicks(totalTicks, numNotes); - noteAr.forEach((note) => { - const textModifiers = note.textModifiers; - // Note preserver remainder - note = SmoNote.cloneWithDuration(note, { - numerator: stemTicks * tuplet.durationMap[i], - denominator: 1, - remainder: note.ticks.remainder - }); + // const tupletNotes: SmoNote[] = []; - // Don't clone modifiers, except for first one. - if (i === 0) { - const ntmAr: any = []; - textModifiers.forEach((tm) => { - const ntm = SmoNoteModifierBase.deserialize(tm); - ntmAr.push(ntm); - }); - note.textModifiers = ntmAr; - } - i += 1; - tupletNotes.push(note); - }); - const rv = new SmoTuplet({ - numNotes: tuplet.numNotes, - voice: tuplet.voice, - notes: tupletNotes, - stemTicks, - totalTicks, - ratioed: false, - bracketed: true, - startIndex: tuplet.startIndex, - durationMap - }); - return rv; - } + // noteAr.forEach((note) => { + // const textModifiers = note.textModifiers; + // // Note preserver remainder + // note = SmoNote.cloneWithDuration(note, { + // numerator: stemTicks * tuplet.durationMap[i], + // denominator: 1, + // remainder: note.ticks.remainder + // }); - _adjustTicks() { - let i = 0; - const sum = this.durationSum; - for (i = 0; i < this.notes.length; ++i) { - const note = this.notes[i]; - // TODO: notes_occupied needs to consider vex duration - note.ticks.denominator = 1; - note.ticks.numerator = Math.floor((this.totalTicks * this.durationMap[i]) / sum); - note.tuplet = this.attrs; - } + // // Don't clone modifiers, except for first one. + // if (i === 0) { + // const ntmAr: any = []; + // textModifiers.forEach((tm) => { + // const ntm = SmoNoteModifierBase.deserialize(tm); + // ntmAr.push(ntm); + // }); + // note.textModifiers = ntmAr; + // } + // i += 1; + // tupletNotes.push(note); + // }); + // const rv = new SmoTuplet({ + // numNotes: tuplet.numNotes, + // voice: tuplet.voice, + // notes: tupletNotes, + // stemTicks, + // totalTicks, + // ratioed: false, + // bracketed: true, + // startIndex: tuplet.startIndex, + // durationMap + // }); + // return rv; + // } - // put all the remainder in the first note of the tuplet - const noteTicks = this.notes.map((nn) => nn.tickCount) - .reduce((acc, dd) => acc + dd); - // bug fix: if this is a clones tuplet, remainder is already set - this.notes[0].ticks.remainder = - this.notes[0].ticks.remainder + this.totalTicks - noteTicks; - } - getIndexOfNote(note: SmoNote | null): number { - let rv = -1; - let i = 0; - if (!note) { - return -1; - } - for (i = 0; i < this.notes.length; ++i) { - const tn = this.notes[i]; - if (note.attrs.id === tn.attrs.id) { - rv = i; - } - } - return rv; - } + // _adjustTicks() { + // let i = 0; + // const sum = this.durationSum; + // for (i = 0; i < this.notes.length; ++i) { + // const note = this.notes[i]; + // // TODO: notes_occupied needs to consider vex duration + // note.ticks.denominator = 1; + // note.ticks.numerator = Math.floor((this.totalTicks * this.durationMap[i]) / sum); + // note.tuplet = this.attrs; + // } - split(combineIndex: number) { - let i = 0; - const multiplier = 0.5; - const nnotes: SmoNote[] = []; - const nmap: number[] = []; - for (i = 0; i < this.notes.length; ++i) { - const note = this.notes[i]; - if (i === combineIndex) { - nmap.push(this.durationMap[i] * multiplier); - nmap.push(this.durationMap[i] * multiplier); - note.ticks.numerator *= multiplier; + // // put all the remainder in the first note of the tuplet + // const noteTicks = this.notes.map((nn) => nn.tickCount) + // .reduce((acc, dd) => acc + dd); + // // bug fix: if this is a clones tuplet, remainder is already set + // this.notes[0].ticks.remainder = + // this.notes[0].ticks.remainder + this.totalTicks - noteTicks; + // } + // getIndexOfNote(note: SmoNote | null): number { + // let rv = -1; + // let i = 0; + // if (!note) { + // return -1; + // } + // for (i = 0; i < this.notes.length; ++i) { + // const tn = this.notes[i]; + // if (note.attrs.id === tn.attrs.id) { + // rv = i; + // } + // } + // return rv; + // } - const onote = SmoNote.clone(note); - // remainder is for the whole tuplet, so don't duplicate that. - onote.ticks.remainder = 0; - nnotes.push(note); - nnotes.push(onote); - } else { - nmap.push(this.durationMap[i]); - nnotes.push(note); - } - } - this.notes = nnotes; - this.durationMap = nmap; - } - combine(startIndex: number, endIndex: number) { - let i = 0; - let base = 0.0; - let acc = 0.0; - // can't combine in this way, too many notes - if (this.notes.length <= endIndex || startIndex >= endIndex) { - return this; - } - for (i = startIndex; i <= endIndex; ++i) { - acc += this.durationMap[i]; - if (i === startIndex) { - base = this.durationMap[i]; - } else if (this.durationMap[i] !== base) { - // Can't combine non-equal tuplet notes - return this; - } - } - // how much each combined value will be multiplied by - const multiplier = acc / base; + // split(combineIndex: number) { + // let i = 0; + // const multiplier = 0.5; + // const nnotes: SmoNote[] = []; + // const nmap: number[] = []; + // for (i = 0; i < this.notes.length; ++i) { + // const note = this.notes[i]; + // if (i === combineIndex) { + // nmap.push(this.durationMap[i] * multiplier); + // nmap.push(this.durationMap[i] * multiplier); + // note.ticks.numerator *= multiplier; - const nmap = []; - const nnotes = []; - // adjust the duration map - for (i = 0; i < this.notes.length; ++i) { - const note = this.notes[i]; - // notes that don't change are unchanged - if (i < startIndex || i > endIndex) { - nmap.push(this.durationMap[i]); - nnotes.push(note); - } - // changed note with combined duration - if (i === startIndex) { - note.ticks.numerator = note.ticks.numerator * multiplier; - nmap.push(acc); - nnotes.push(note); - } - // other notes after startIndex are removed from the map. - } - this.notes = nnotes; - this.durationMap = nmap; - return this; - } + // const onote = SmoNote.clone(note); + // // remainder is for the whole tuplet, so don't duplicate that. + // onote.ticks.remainder = 0; + // nnotes.push(note); + // nnotes.push(onote); + // } else { + // nmap.push(this.durationMap[i]); + // nnotes.push(note); + // } + // } + // this.notes = nnotes; + // this.durationMap = nmap; + // } + // combine(startIndex: number, endIndex: number) { + // let i = 0; + // let base = 0.0; + // let acc = 0.0; + // // can't combine in this way, too many notes + // if (this.notes.length <= endIndex || startIndex >= endIndex) { + // return this; + // } + // for (i = startIndex; i <= endIndex; ++i) { + // acc += this.durationMap[i]; + // if (i === startIndex) { + // base = this.durationMap[i]; + // } else if (this.durationMap[i] !== base) { + // // Can't combine non-equal tuplet notes + // return this; + // } + // } + // // how much each combined value will be multiplied by + // const multiplier = acc / base; - // ### getStemDirection - // Return the stem direction, so we can bracket the correct place - getStemDirection(clef: Clef) { - const note = this.notes.find((nn) => nn.noteType === 'n'); - if (!note) { - return SmoNote.flagStates.down; - } - if (note.flagState !== SmoNote.flagStates.auto) { - return note.flagState; - } - return SmoMusic.pitchToStaffLine(clef, note.pitches[0]) - >= 3 ? SmoNote.flagStates.down : SmoNote.flagStates.up; - } - get durationSum() { - let acc = 0; - let i = 0; - for (i = 0; i < this.durationMap.length; ++i) { - acc += this.durationMap[i]; - } - return Math.round(acc); - } + // const nmap = []; + // const nnotes = []; + // // adjust the duration map + // for (i = 0; i < this.notes.length; ++i) { + // const note = this.notes[i]; + // // notes that don't change are unchanged + // if (i < startIndex || i > endIndex) { + // nmap.push(this.durationMap[i]); + // nnotes.push(note); + // } + // // changed note with combined duration + // if (i === startIndex) { + // note.ticks.numerator = note.ticks.numerator * multiplier; + // nmap.push(acc); + // nnotes.push(note); + // } + // // other notes after startIndex are removed from the map. + // } + // this.notes = nnotes; + // this.durationMap = nmap; + // return this; + // } + + + //todo: adjust naming get num_notes() { - return this.durationSum; + return this.numNotes; } get notes_occupied() { return Math.floor(this.totalTicks / this.stemTicks); } - get note_ticks_occupied() { - return this.totalTicks / this.stemTicks; - } + get tickCount() { - let rv = 0; - let i = 0; - for (i = 0; i < this.notes.length; ++i) { - const note = this.notes[i]; - rv += (note.ticks.numerator / note.ticks.denominator) + note.ticks.remainder; - } - return rv; + return this.totalTicks; } } diff --git a/src/smo/midi/midiToSmo.ts b/src/smo/midi/midiToSmo.ts index 611db19e..21f918ef 100644 --- a/src/smo/midi/midiToSmo.ts +++ b/src/smo/midi/midiToSmo.ts @@ -303,17 +303,18 @@ export class MidiToSmo { const note = new SmoNote(defs); SmoNote.sortPitches(note); measure.voices[0].notes.push(note); - if (ev.tupletInfo !== null && ev.tupletInfo.isLast === true) { - const voiceLen = measure.voices[0].notes.length; - const tupletNotes = [note, measure.voices[0].notes[voiceLen - 2], measure.voices[0].notes[voiceLen - 3]]; - const defs = SmoTuplet.defaults; - defs.notes = tupletNotes; - defs.stemTicks = ev.tupletInfo.stemTicks; - defs.numNotes = ev.tupletInfo.numNotes; - defs.totalTicks = ev.tupletInfo.totalTicks; - defs.startIndex = voiceLen - 3; - measure.tuplets.push(new SmoTuplet(defs)); - } + //todo: needs to be check for nested tuplets + // if (ev.tupletInfo !== null && ev.tupletInfo.isLast === true) { + // const voiceLen = measure.voices[0].notes.length; + // const tupletNotes = [note, measure.voices[0].notes[voiceLen - 2], measure.voices[0].notes[voiceLen - 3]]; + // const defs = SmoTuplet.defaults; + // defs.notes = tupletNotes; + // defs.stemTicks = ev.tupletInfo.stemTicks; + // defs.numNotes = ev.tupletInfo.numNotes; + // defs.totalTicks = ev.tupletInfo.totalTicks; + // defs.startIndex = voiceLen - 3; + // measure.tuplets.push(new SmoTuplet(defs)); + // } if (ev.isTied) { this.addToTieMap(measureIndex); } diff --git a/src/smo/mxml/smoToXml.ts b/src/smo/mxml/smoToXml.ts index 7cf5bd0c..6eeb4ab7 100644 --- a/src/smo/mxml/smoToXml.ts +++ b/src/smo/mxml/smoToXml.ts @@ -533,17 +533,18 @@ export class SmoToXml { } static tupletNotation(notationsElement: Element, tuplet: SmoTuplet, note: SmoNote) { const nn = XmlHelpers.createTextElementChild; - if (tuplet.getIndexOfNote(note) === 0) { - const tupletElement = nn(notationsElement, 'tuplet', null, ''); - XmlHelpers.createAttributes(tupletElement, { - number: 1, type: 'start' - }); - } else if (tuplet.getIndexOfNote(note) === tuplet.notes.length - 1) { - const tupletElement = nn(notationsElement, 'tuplet', null, ''); - XmlHelpers.createAttributes(tupletElement, { - number: 1, type: 'stop' - }); - } + //todo nenad: adjust implementation + // if (tuplet.getIndexOfNote(note) === 0) { + // const tupletElement = nn(notationsElement, 'tuplet', null, ''); + // XmlHelpers.createAttributes(tupletElement, { + // number: 1, type: 'start' + // }); + // } else if (tuplet.getIndexOfNote(note) === tuplet.notes.length - 1) { + // const tupletElement = nn(notationsElement, 'tuplet', null, ''); + // XmlHelpers.createAttributes(tupletElement, { + // number: 1, type: 'stop' + // }); + // } } /** @@ -783,7 +784,7 @@ export class SmoToXml { } const duration = note.tickCount; smoState.measureTicks += duration; - const tuplet = measure.getTupletForNote(note); + const tuplet = measure.getTupletForNoteIndex(smoState.voiceIndex, smoState.voiceTickIndex); nn(noteElement, 'duration', { duration }, 'duration'); SmoToXml.tie(noteElement, smoState); nn(noteElement, 'voice', { voice: smoState.voiceIndex }, 'voice'); diff --git a/src/smo/mxml/xmlState.ts b/src/smo/mxml/xmlState.ts index 2f063c83..ea5897b2 100644 --- a/src/smo/mxml/xmlState.ts +++ b/src/smo/mxml/xmlState.ts @@ -536,6 +536,7 @@ export class XmlState { } }); } + //todo: adjust implementation // ### backtrackTuplets // If we received a tuplet end, go back through the voice // and construct the SmoTuplet. @@ -556,8 +557,8 @@ export class XmlState { i += 1; } const tp = SmoTuplet.defaults; - tp.notes = notes; - tp.durationMap = durationMap; + // tp.notes = notes; + // tp.durationMap = durationMap; tp.voice = voiceId; const tuplet = new SmoTuplet(tp); // Store the tuplet with the staff ID and voice so we @@ -586,7 +587,7 @@ export class XmlState { const completed: XmlCompletedTuplet[] = []; this.completedTuplets.forEach((tuplet) => { if (tuplet.voiceId === voiceId && tuplet.staffId === staffId) { - smoMeasure.tuplets.push(tuplet.tuplet); + // smoMeasure.tuplets.push(tuplet.tuplet); } else { completed.push(tuplet); } diff --git a/src/smo/xform/audioTrack.ts b/src/smo/xform/audioTrack.ts index 7f621a43..498fed35 100644 --- a/src/smo/xform/audioTrack.ts +++ b/src/smo/xform/audioTrack.ts @@ -523,14 +523,14 @@ export class SmoAudioScore { // update staff features of slur/tie/cresc. this.getSlurInfo(track, selection); this.getHairpinInfo(track, selection); - const tuplet = measure.getTupletForNote(note); - if (tuplet && tuplet.getIndexOfNote(note) === 0) { + const tuplet = measure.getTupletForNoteIndex(voiceIx, noteIx); + if (tuplet && tuplet.startIndex === noteIx) { tupletTicks = tuplet.tickCount / this.timeDiv; } if (tupletTicks) { // tuplet likely won't fit evenly in ticks, so // use remainder in last tuplet note. - if (tuplet && tuplet.getIndexOfNote(note) === tuplet.notes.length - 1) { + if (tuplet && tuplet.endIndex === noteIx) { duration = tupletTicks; tupletTicks = 0; } else { diff --git a/src/smo/xform/beamers.ts b/src/smo/xform/beamers.ts index 185c0906..3c1ee7e1 100644 --- a/src/smo/xform/beamers.ts +++ b/src/smo/xform/beamers.ts @@ -137,41 +137,55 @@ export class SmoBeamer { } // beam tuplets - if (note.isTuplet) { - const tuplet = this.measure.getTupletForNote(note); - // The underlying notes must have been deleted. - if (!tuplet) { - return; - } - const tupletIndex = tuplet.getIndexOfNote(note); - const ult = tuplet.notes[tuplet.notes.length - 1]; - const first = tuplet.notes[0]; + // if (note.isTuplet) { + // const tuplet = this.measure.getTupletForNote(note); + // // The underlying notes must have been deleted. + // if (!tuplet) { + // return; + // } - if (first.endBeam) { - this._advanceGroup(); - return; - } + // const first = tuplet.getFirstNote(); + // if (!first) { + // return; + // } - // is this beamable length-wise - const stemTicks = SmoMusic.closestDurationTickLtEq(note.tickCount) * tuplet.durationMap[tupletIndex]; - if (note.noteType === 'n' && stemTicks < 4096) { - this.currentGroup.push(note); - } - // Ultimate note in tuplet - if (ult.attrs.id === note.attrs.id && !this._isRemainingTicksBeamable(tickmap, index)) { - this._completeGroup(tickmap.voice); - this._advanceGroup(); - } - return; - } + // const ult = tuplet.getLastNote(); + // if (!ult) { + // return; + // } + + // if (first.endBeam) { + // this._advanceGroup(); + // return; + // } + + // // is this beamable length-wise + // if (note.noteType === 'n' && note.stemTicks < 4096) { + // this.currentGroup.push(note); + // } + // // Ultimate note in tuplet + // if (ult.attrs.id === note.attrs.id && !this._isRemainingTicksBeamable(tickmap, index)) { + // this._completeGroup(tickmap.voice); + // this._advanceGroup(); + // } + // return; + // } // don't beam > 1/4 note in 4/4 time. Don't beam rests. - if (tickmap.deltaMap[index] >= 4096 || (note.isRest() && this.currentGroup.length === 0)) { + if (note.stemTicks >= 4096 || (note.isRest() && this.currentGroup.length === 0)) { this._completeGroup(tickmap.voice); this._advanceGroup(); return; } + //if areTupletElementsDifferent(noteOne, noteTwo) + //this._completeGroup(tickmap.voice); + //this._advanceGroup(); + if (index > 0 && !SmoBeamer.areTupletElementsTheSame(tickmap.notes[index - 1], tickmap.notes[index])) { + this._completeGroup(tickmap.voice); + this._advanceGroup(); + } + this.currentGroup.push(note); if (note.endBeam) { this._completeGroup(tickmap.voice); @@ -189,4 +203,16 @@ export class SmoBeamer { this._advanceGroup(); } } + + public static areTupletElementsTheSame(noteOne: SmoNote, noteTwo: SmoNote): boolean { + if (noteOne.tuplet === null && noteTwo.tuplet === null) { + return true; + } + if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tuplet!.id == noteTwo.tuplet!.id) { + return true; + } + + return false; + } + } diff --git a/src/smo/xform/copypaste.ts b/src/smo/xform/copypaste.ts index ab9b4993..891508d6 100644 --- a/src/smo/xform/copypaste.ts +++ b/src/smo/xform/copypaste.ts @@ -60,77 +60,77 @@ export class PasteBuffer { this.score = score; } setSelections(score: SmoScore, selections: SmoSelection[]) { - this.notes = []; - this.noteIndex = 0; - this.score = score; - if (selections.length < 1) { - return; - } - this.tupletNoteMap = {}; - const first = selections[0]; - const last = selections[selections.length - 1]; - if (!first.note || !last.note) { - return; - } + // this.notes = []; + // this.noteIndex = 0; + // this.score = score; + // if (selections.length < 1) { + // return; + // } + // this.tupletNoteMap = {}; + // const first = selections[0]; + // const last = selections[selections.length - 1]; + // if (!first.note || !last.note) { + // return; + // } - const startTuplet: SmoTuplet | null = first.measure.getTupletForNote(first.note); - if (startTuplet) { - if (startTuplet.getIndexOfNote(first.note) !== 0) { - return; // can't paste from the middle of a tuplet - } - } - const endTuplet: SmoTuplet | null = last.measure.getTupletForNote(last.note); - if (endTuplet) { - if (endTuplet.getIndexOfNote(last.note) !== endTuplet.notes.length - 1) { - return; // can't paste part of a tuplet. - } - } - this._populateSelectArray(selections); + // const startTuplet: SmoTuplet | null = first.measure.getTupletForNote(first.note); + // if (startTuplet) { + // if (startTuplet.getIndexOfNote(first.note) !== 0) { + // return; // can't paste from the middle of a tuplet + // } + // } + // const endTuplet: SmoTuplet | null = last.measure.getTupletForNote(last.note); + // if (endTuplet) { + // if (endTuplet.getIndexOfNote(last.note) !== endTuplet.notes.length - 1) { + // return; // can't paste part of a tuplet. + // } + // } + // this._populateSelectArray(selections); } // ### _populateSelectArray // copy the selected notes into the paste buffer with their original locations. _populateSelectArray(selections: SmoSelection[]) { - let selector: SmoSelector = SmoSelector.default; - this.modifiers = []; - selections.forEach((selection) => { - selector = JSON.parse(JSON.stringify(selection.selector)); - const mod: StaffModifierBase[] = selection.staff.getModifiersAt(selector); - if (mod.length) { - mod.forEach((modifier: StaffModifierBase) => { - const cp: StaffModifierBase = StaffModifierBase.deserialize(modifier.serialize()); - cp.attrs.id = getId().toString(); - this.modifiers.push(cp); - }); - } - const isTuplet: boolean = selection?.note?.isTuplet ?? false; - // We store copy in concert pitch. The originalKey is the original key of the copy. - // the destKey is the originalKey in concert pitch. - const originalKey = selection.measure.keySignature; - const keyOffset = -1 * selection.measure.transposeIndex; - const destKey = SmoMusic.vexKeySignatureTranspose(originalKey, keyOffset).toLocaleLowerCase(); - if (isTuplet) { - const tuplet = (selection.measure.getTupletForNote(selection.note) as SmoTuplet); - const index = tuplet.getIndexOfNote(selection.note); - if (index === 0) { - const ntuplet = SmoTuplet.cloneTuplet(tuplet); - this.tupletNoteMap[ntuplet.attrs.id] = ntuplet; - ntuplet.notes.forEach((nnote) => { - const xposeNote = SmoNote.transpose(SmoNote.clone(nnote), - [], -1 * selection.measure.transposeIndex, selection.measure.keySignature, destKey) as SmoNote; - this.notes.push({ selector, note: xposeNote, originalKey: destKey }); - selector = JSON.parse(JSON.stringify(selector)); - selector.tick += 1; - }); - } - } else if (selection.note) { - const note = SmoNote.transpose(SmoNote.clone(selection.note), - [], keyOffset, selection.measure.keySignature, destKey) as SmoNote; - this.notes.push({ selector, note, originalKey: destKey }); - } - }); - this.notes.sort((a, b) => - SmoSelector.gt(a.selector, b.selector) ? 1 : -1 - ); + // let selector: SmoSelector = SmoSelector.default; + // this.modifiers = []; + // selections.forEach((selection) => { + // selector = JSON.parse(JSON.stringify(selection.selector)); + // const mod: StaffModifierBase[] = selection.staff.getModifiersAt(selector); + // if (mod.length) { + // mod.forEach((modifier: StaffModifierBase) => { + // const cp: StaffModifierBase = StaffModifierBase.deserialize(modifier.serialize()); + // cp.attrs.id = getId().toString(); + // this.modifiers.push(cp); + // }); + // } + // const isTuplet: boolean = selection?.note?.isTuplet ?? false; + // // We store copy in concert pitch. The originalKey is the original key of the copy. + // // the destKey is the originalKey in concert pitch. + // const originalKey = selection.measure.keySignature; + // const keyOffset = -1 * selection.measure.transposeIndex; + // const destKey = SmoMusic.vexKeySignatureTranspose(originalKey, keyOffset).toLocaleLowerCase(); + // if (isTuplet) { + // const tuplet = (selection.measure.getTupletForNote(selection.note) as SmoTuplet); + // const index = tuplet.getIndexOfNote(selection.note); + // if (index === 0) { + // const ntuplet = SmoTuplet.cloneTuplet(tuplet); + // this.tupletNoteMap[ntuplet.attrs.id] = ntuplet; + // ntuplet.notes.forEach((nnote) => { + // const xposeNote = SmoNote.transpose(SmoNote.clone(nnote), + // [], -1 * selection.measure.transposeIndex, selection.measure.keySignature, destKey) as SmoNote; + // this.notes.push({ selector, note: xposeNote, originalKey: destKey }); + // selector = JSON.parse(JSON.stringify(selector)); + // selector.tick += 1; + // }); + // } + // } else if (selection.note) { + // const note = SmoNote.transpose(SmoNote.clone(selection.note), + // [], keyOffset, selection.measure.keySignature, destKey) as SmoNote; + // this.notes.push({ selector, note, originalKey: destKey }); + // } + // }); + // this.notes.sort((a, b) => + // SmoSelector.gt(a.selector, b.selector) ? 1 : -1 + // ); } clearSelections() { @@ -197,39 +197,39 @@ export class PasteBuffer { let j = 0; let ticksToFill = tickmap.durationMap[startTick]; // TODO: bug here, need to handle tuplets in pre-part, create new tuplet - for (i = 0; i < measure.voices[voiceIndex].notes.length; ++i) { - const note = measure.voices[voiceIndex].notes[i]; - // If this is a tuplet, clone all the notes at once. - if (note.isTuplet && ticksToFill >= note.tickCount) { - const tuplet = measure.getTupletForNote(note); - if (!tuplet) { - continue; // we remove the tuplet after first iteration - } - const ntuplet: SmoTuplet = SmoTuplet.cloneTuplet(tuplet); - voice.notes = voice.notes.concat(ntuplet.notes as SmoNote[]); - measure.removeTupletForNote(note); - measure.tuplets.push(ntuplet); - ticksToFill -= tuplet.tickCount; - } else if (ticksToFill >= note.tickCount) { - ticksToFill -= note.tickCount; - voice.notes.push(SmoNote.clone(note)); - } else { - const duration = note.tickCount - ticksToFill; - const durMap = SmoMusic.gcdMap(duration); - for (j = 0; j < durMap.length; ++j) { - const dd = durMap[j]; - SmoNote.cloneWithDuration(note, { - numerator: dd, - denominator: 1, - remainder: 0 - }); - } - ticksToFill = 0; - } - if (ticksToFill < 1) { - break; - } - } + // for (i = 0; i < measure.voices[voiceIndex].notes.length; ++i) { + // const note = measure.voices[voiceIndex].notes[i]; + // // If this is a tuplet, clone all the notes at once. + // if (note.isTuplet && ticksToFill >= note.tickCount) { + // const tuplet = measure.getTupletForNote(note); + // if (!tuplet) { + // continue; // we remove the tuplet after first iteration + // } + // const ntuplet: SmoTuplet = SmoTuplet.cloneTuplet(tuplet); + // voice.notes = voice.notes.concat(ntuplet.notes as SmoNote[]); + // measure.removeTupletForNote(note); + // measure.tuplets.push(ntuplet); + // ticksToFill -= tuplet.tickCount; + // } else if (ticksToFill >= note.tickCount) { + // ticksToFill -= note.tickCount; + // voice.notes.push(SmoNote.clone(note)); + // } else { + // const duration = note.tickCount - ticksToFill; + // const durMap = SmoMusic.gcdMap(duration); + // for (j = 0; j < durMap.length; ++j) { + // const dd = durMap[j]; + // SmoNote.cloneWithDuration(note, { + // numerator: dd, + // denominator: 1, + // remainder: 0 + // }); + // } + // ticksToFill = 0; + // } + // if (ticksToFill < 1) { + // break; + // } + // } return voice; } @@ -314,13 +314,13 @@ export class PasteBuffer { * @returns */ static tupletOverlapIndex(t1: SmoTuplet, measure: SmoMeasure) { - for (var i = 0; i < measure.tuplets.length; ++i) { - const tt = measure.tuplets[i]; - // TODO: what about other kinds of overlap? - if (tt.startIndex === t1.startIndex) { - return i; - } - } + // for (var i = 0; i < measure.tuplets.length; ++i) { + // const tt = measure.tuplets[i]; + // // TODO: what about other kinds of overlap? + // if (tt.startIndex === t1.startIndex) { + // return i; + // } + // } return -1; } /** @@ -339,79 +339,79 @@ export class PasteBuffer { let j = 0; let tupletsPushed = 0; const totalDuration = tickmap.totalDuration; - while (currentDuration < totalDuration && this.noteIndex < this.notes.length) { - if (!this.score) { - return; - } - const selection = this.notes[this.noteIndex]; - const note = selection.note; - if (note.noteType === 'n') { - const pitchAr: number[] = []; - note.pitches.forEach((pitch, ix) => { - pitchAr.push(ix); - }); - SmoNote.transpose(note, pitchAr, measure.transposeIndex, selection.originalKey, measure.keySignature); - } - this._populateModifier(selection.selector, startSelector, this.score.staves[selection.selector.staff]); - if (note.isTuplet) { - const tuplet = this.tupletNoteMap[(note.tuplet as TupletInfo).id]; - const ntuplet = SmoTuplet.cloneTuplet(tuplet); - ntuplet.startIndex = voice.notes.length; - this.noteIndex += ntuplet.notes.length; - startSelector.tick += ntuplet.notes.length; - currentDuration += tuplet.tickCount; - for (i = 0; i < ntuplet.notes.length; ++i) { - const tn = ntuplet.notes[i]; - tn.clef = measure.clef; - voice.notes.push(tn); - } - const tix = PasteBuffer.tupletOverlapIndex(ntuplet, measure); - // If this is overlapping an existing tuplet in the target measure, replace it - if (tix >= 0) { - measure.tuplets[tix] = ntuplet; - } else { - measure.tuplets.push(ntuplet); - } - } else if (currentDuration + note.tickCount <= totalDuration && this.remainder === 0) { - // The whole note fits in the measure, paste it. - const nnote = SmoNote.clone(note); - nnote.clef = measure.clef; - voice.notes.push(nnote); - currentDuration += note.tickCount; - this.noteIndex += 1; - startSelector.tick += 1; - } else if (this.remainder > 0) { - // This is a note that spilled over the last measure - const nnote = SmoNote.cloneWithDuration(note, { - numerator: this.remainder, - denominator: 1, - remainder: 0 - }); - nnote.clef = measure.clef; - voice.notes.push(nnote); - currentDuration += this.remainder; - this.remainder = 0; - } else { - // The note won't fit, so we split it in 2 and paste the remainder in the next measure. - // TODO: tie the last note to this one. - const partial = totalDuration - currentDuration; - const dar = SmoMusic.gcdMap(partial); - for (j = 0; j < dar.length; ++j) { - const ddd = dar[j]; - const vnote = SmoNote.cloneWithDuration(note, { - numerator: ddd, - denominator: 1, - remainder: 0 - }); - voice.notes.push(vnote); - } - currentDuration += partial; + // while (currentDuration < totalDuration && this.noteIndex < this.notes.length) { + // if (!this.score) { + // return; + // } + // const selection = this.notes[this.noteIndex]; + // const note = selection.note; + // if (note.noteType === 'n') { + // const pitchAr: number[] = []; + // note.pitches.forEach((pitch, ix) => { + // pitchAr.push(ix); + // }); + // SmoNote.transpose(note, pitchAr, measure.transposeIndex, selection.originalKey, measure.keySignature); + // } + // this._populateModifier(selection.selector, startSelector, this.score.staves[selection.selector.staff]); + // if (note.isTuplet) { + // const tuplet = this.tupletNoteMap[(note.tuplet as TupletInfo).id]; + // const ntuplet = SmoTuplet.cloneTuplet(tuplet); + // ntuplet.startIndex = voice.notes.length; + // this.noteIndex += ntuplet.notes.length; + // startSelector.tick += ntuplet.notes.length; + // currentDuration += tuplet.tickCount; + // for (i = 0; i < ntuplet.notes.length; ++i) { + // const tn = ntuplet.notes[i]; + // tn.clef = measure.clef; + // voice.notes.push(tn); + // } + // const tix = PasteBuffer.tupletOverlapIndex(ntuplet, measure); + // // If this is overlapping an existing tuplet in the target measure, replace it + // if (tix >= 0) { + // measure.tuplets[tix] = ntuplet; + // } else { + // measure.tuplets.push(ntuplet); + // } + // } else if (currentDuration + note.tickCount <= totalDuration && this.remainder === 0) { + // // The whole note fits in the measure, paste it. + // const nnote = SmoNote.clone(note); + // nnote.clef = measure.clef; + // voice.notes.push(nnote); + // currentDuration += note.tickCount; + // this.noteIndex += 1; + // startSelector.tick += 1; + // } else if (this.remainder > 0) { + // // This is a note that spilled over the last measure + // const nnote = SmoNote.cloneWithDuration(note, { + // numerator: this.remainder, + // denominator: 1, + // remainder: 0 + // }); + // nnote.clef = measure.clef; + // voice.notes.push(nnote); + // currentDuration += this.remainder; + // this.remainder = 0; + // } else { + // // The note won't fit, so we split it in 2 and paste the remainder in the next measure. + // // TODO: tie the last note to this one. + // const partial = totalDuration - currentDuration; + // const dar = SmoMusic.gcdMap(partial); + // for (j = 0; j < dar.length; ++j) { + // const ddd = dar[j]; + // const vnote = SmoNote.cloneWithDuration(note, { + // numerator: ddd, + // denominator: 1, + // remainder: 0 + // }); + // voice.notes.push(vnote); + // } + // currentDuration += partial; - // Set the remaining length of the current note, this will be added to the - // next measure with the previous note's pitches - this.remainder = note.tickCount - partial; - } - } + // // Set the remaining length of the current note, this will be added to the + // // next measure with the previous note's pitches + // this.remainder = note.tickCount - partial; + // } + // } } // ### _populatePost @@ -421,34 +421,34 @@ export class PasteBuffer { let startTicks = PasteBuffer._countTicks(voice); let existingIndex = 0; const totalDuration = tickmap.totalDuration; - while (startTicks < totalDuration) { - // Find the point in the music where the paste area runs out, or as close as we can get. - existingIndex = tickmap.durationMap.indexOf(startTicks); - existingIndex = (existingIndex < 0) ? measure.voices[voiceIndex].notes.length - 1 : existingIndex; - const note = measure.voices[voiceIndex].notes[existingIndex]; - if (note.isTuplet) { - const tuplet = measure.getTupletForNote(note) as SmoTuplet; - const ntuplet = SmoTuplet.cloneTuplet(tuplet); - startTicks += tuplet.tickCount; - voice.notes = voice.notes.concat(ntuplet.notes); - measure.tuplets.push(ntuplet); - measure.removeTupletForNote(note); - } else { - const ticksLeft = totalDuration - startTicks; - if (ticksLeft >= note.tickCount) { - startTicks += note.tickCount; - voice.notes.push(SmoNote.clone(note)); - } else { - const remainder = totalDuration - startTicks; - voice.notes.push(SmoNote.cloneWithDuration(note, { - numerator: remainder, - denominator: 1, - remainder: 0 - })); - startTicks = totalDuration; - } - } - } + // while (startTicks < totalDuration) { + // // Find the point in the music where the paste area runs out, or as close as we can get. + // existingIndex = tickmap.durationMap.indexOf(startTicks); + // existingIndex = (existingIndex < 0) ? measure.voices[voiceIndex].notes.length - 1 : existingIndex; + // const note = measure.voices[voiceIndex].notes[existingIndex]; + // if (note.isTuplet) { + // const tuplet = measure.getTupletForNote(note) as SmoTuplet; + // const ntuplet = SmoTuplet.cloneTuplet(tuplet); + // startTicks += tuplet.tickCount; + // voice.notes = voice.notes.concat(ntuplet.notes); + // measure.tuplets.push(ntuplet); + // measure.removeTupletForNote(note); + // } else { + // const ticksLeft = totalDuration - startTicks; + // if (ticksLeft >= note.tickCount) { + // startTicks += note.tickCount; + // voice.notes.push(SmoNote.clone(note)); + // } else { + // const remainder = totalDuration - startTicks; + // voice.notes.push(SmoNote.cloneWithDuration(note, { + // numerator: remainder, + // denominator: 1, + // remainder: 0 + // })); + // startTicks = totalDuration; + // } + // } + // } } _pasteVoiceSer(ser: any, vobj: any, voiceIx: number) { @@ -470,96 +470,96 @@ export class PasteBuffer { } pasteSelections(selector: SmoSelector) { - let i = 0; - if (this.notes.length < 1) { - return; - } - const maxCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); - const minCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); - const backupNotes: PasteNote[] = []; - this.notes.forEach((bb) => { - const note = (SmoNote.deserialize(bb.note.serialize())); - const selector = JSON.parse(JSON.stringify(bb.selector)); - backupNotes.push({ note, selector, originalKey: bb.originalKey }); - }); - this.destination = selector; - if (minCutVoice === maxCutVoice && minCutVoice > this.destination.voice) { - this.destination.voice = minCutVoice; + // let i = 0; + // if (this.notes.length < 1) { + // return; + // } + // const maxCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); + // const minCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); + // const backupNotes: PasteNote[] = []; + // this.notes.forEach((bb) => { + // const note = (SmoNote.deserialize(bb.note.serialize())); + // const selector = JSON.parse(JSON.stringify(bb.selector)); + // backupNotes.push({ note, selector, originalKey: bb.originalKey }); + // }); + // this.destination = selector; + // if (minCutVoice === maxCutVoice && minCutVoice > this.destination.voice) { + // this.destination.voice = minCutVoice; - } - this.modifiersToPlace = []; - if (this.notes.length < 1) { - return; - } - if (!this.score) { - return; - } - this.noteIndex = 0; - this.measureIndex = -1; - this.remainder = 0; - const voices = this._populateVoice(this.destination.voice); - const measureSel = JSON.parse(JSON.stringify(this.destination)); - const selectors: SmoSelector[] = []; - for (i = 0; i < this.measures.length && i < voices.length; ++i) { - const measure: SmoMeasure = this.measures[i]; - const nvoice: SmoVoice = voices[i]; - const ser: any = measure.serialize(); - // Make sure the key is concert pitch, it is what measure constructor expects - ser.transposeIndex = measure.transposeIndex; // default values are undefined, make sure the transpose is valid - ser.keySignature = SmoMusic.vexKeySigWithOffset(measure.keySignature, -1 * measure.transposeIndex); - ser.timeSignature = measure.timeSignature.serialize(); - ser.tempo = measure.tempo.serialize(); - const vobj: any = { - notes: [] - }; - nvoice.notes.forEach((note: SmoNote) => { - vobj.notes.push(note.serialize()); - }); + // } + // this.modifiersToPlace = []; + // if (this.notes.length < 1) { + // return; + // } + // if (!this.score) { + // return; + // } + // this.noteIndex = 0; + // this.measureIndex = -1; + // this.remainder = 0; + // const voices = this._populateVoice(this.destination.voice); + // const measureSel = JSON.parse(JSON.stringify(this.destination)); + // const selectors: SmoSelector[] = []; + // for (i = 0; i < this.measures.length && i < voices.length; ++i) { + // const measure: SmoMeasure = this.measures[i]; + // const nvoice: SmoVoice = voices[i]; + // const ser: any = measure.serialize(); + // // Make sure the key is concert pitch, it is what measure constructor expects + // ser.transposeIndex = measure.transposeIndex; // default values are undefined, make sure the transpose is valid + // ser.keySignature = SmoMusic.vexKeySigWithOffset(measure.keySignature, -1 * measure.transposeIndex); + // ser.timeSignature = measure.timeSignature.serialize(); + // ser.tempo = measure.tempo.serialize(); + // const vobj: any = { + // notes: [] + // }; + // nvoice.notes.forEach((note: SmoNote) => { + // vobj.notes.push(note.serialize()); + // }); - // TODO: figure out how to do this with multiple voices - this._pasteVoiceSer(ser, vobj, this.destination.voice); - const nmeasure = SmoMeasure.deserialize(ser); - // If this is the non-display buffer, don't try to reset the display rectangles. - // Q: Is this even required since we are going to re-render? - // A: yes, because until we do, the replaced measure needs the formatting info - if (measure.svg.logicalBox && measure.svg.logicalBox.width > 0) { - nmeasure.setBox(SvgHelpers.smoBox(measure.svg.logicalBox), 'copypaste'); - nmeasure.setX(measure.svg.logicalBox.x, 'copyPaste'); - nmeasure.setWidth(measure.svg.logicalBox.width, 'copypaste'); - nmeasure.setY(measure.svg.logicalBox.y, 'copypaste'); - nmeasure.svg.element = measure.svg.element; - } - ['forceClef', 'forceKeySignature', 'forceTimeSignature', 'forceTempo'].forEach((flag) => { - (nmeasure as any)[flag] = (measure.svg as any)[flag]; - }); - this.score.replaceMeasure(measureSel, nmeasure); - measureSel.measure += 1; - selectors.push( - { staff: selector.staff, measure: nmeasure.measureNumber.measureIndex, voice: 0, tick: 0, pitches: [] } - ); - } - this.replacementMeasures = []; - selectors.forEach((selector: SmoSelector) => { - const nsel: SmoSelection | null = SmoSelection.measureSelection(this.score as SmoScore, selector.staff, selector.measure); - if (nsel) { - this.replacementMeasures.push(nsel); - } - }); - this.modifiersToPlace.forEach((mod) => { - let selection = SmoSelection.selectionFromSelector(this.score!, mod.modifier.endSelector); - while (selection && mod.ticksToStart !== 0) { - if (mod.ticksToStart < 0) { - selection = SmoSelection.nextNoteSelectionFromSelector(this.score!, selection.selector); - } else { - selection = SmoSelection.lastNoteSelectionFromSelector(this.score!, selection.selector); - } - mod.ticksToStart -= 1 * Math.sign(mod.ticksToStart); - } - if (selection) { - mod.modifier.startSelector = JSON.parse(JSON.stringify(selection.selector)); - selection.staff.addStaffModifier(mod.modifier); - } - }); - this.notes = backupNotes; + // // TODO: figure out how to do this with multiple voices + // this._pasteVoiceSer(ser, vobj, this.destination.voice); + // const nmeasure = SmoMeasure.deserialize(ser); + // // If this is the non-display buffer, don't try to reset the display rectangles. + // // Q: Is this even required since we are going to re-render? + // // A: yes, because until we do, the replaced measure needs the formatting info + // if (measure.svg.logicalBox && measure.svg.logicalBox.width > 0) { + // nmeasure.setBox(SvgHelpers.smoBox(measure.svg.logicalBox), 'copypaste'); + // nmeasure.setX(measure.svg.logicalBox.x, 'copyPaste'); + // nmeasure.setWidth(measure.svg.logicalBox.width, 'copypaste'); + // nmeasure.setY(measure.svg.logicalBox.y, 'copypaste'); + // nmeasure.svg.element = measure.svg.element; + // } + // ['forceClef', 'forceKeySignature', 'forceTimeSignature', 'forceTempo'].forEach((flag) => { + // (nmeasure as any)[flag] = (measure.svg as any)[flag]; + // }); + // this.score.replaceMeasure(measureSel, nmeasure); + // measureSel.measure += 1; + // selectors.push( + // { staff: selector.staff, measure: nmeasure.measureNumber.measureIndex, voice: 0, tick: 0, pitches: [] } + // ); + // } + // this.replacementMeasures = []; + // selectors.forEach((selector: SmoSelector) => { + // const nsel: SmoSelection | null = SmoSelection.measureSelection(this.score as SmoScore, selector.staff, selector.measure); + // if (nsel) { + // this.replacementMeasures.push(nsel); + // } + // }); + // this.modifiersToPlace.forEach((mod) => { + // let selection = SmoSelection.selectionFromSelector(this.score!, mod.modifier.endSelector); + // while (selection && mod.ticksToStart !== 0) { + // if (mod.ticksToStart < 0) { + // selection = SmoSelection.nextNoteSelectionFromSelector(this.score!, selection.selector); + // } else { + // selection = SmoSelection.lastNoteSelectionFromSelector(this.score!, selection.selector); + // } + // mod.ticksToStart -= 1 * Math.sign(mod.ticksToStart); + // } + // if (selection) { + // mod.modifier.startSelector = JSON.parse(JSON.stringify(selection.selector)); + // selection.staff.addStaffModifier(mod.modifier); + // } + // }); + // this.notes = backupNotes; } } diff --git a/src/smo/xform/operations.ts b/src/smo/xform/operations.ts index 0f982e53..7427a6b3 100644 --- a/src/smo/xform/operations.ts +++ b/src/smo/xform/operations.ts @@ -18,8 +18,9 @@ import { SmoSystemGroup } from '../data/scoreModifiers'; import { SmoTextGroup } from '../data/scoreText'; import { SmoSelection, SmoSelector, ModifierTab } from './selections'; import { - SmoDuration, SmoContractNoteActor, SmoStretchNoteActor, SmoMakeTupletActor, - SmoUnmakeTupletActor, SmoContractTupletActor + /*SmoDuration,*/ /*SmoContractNoteActor, SmoStretchNoteActor,*/ SmoMakeTupletActor, + /*SmoUnmakeTupletActor,*/ + SmoChangeDurationActor, /*SmoContractTupletActor*/ } from './tickDuration'; import { SmoBeamer } from './beamers'; /** @@ -168,13 +169,8 @@ export class SmoOperation { // note, if possible. Works on tuplets also. static doubleDuration(selection: SmoSelection) { const note = selection.note; - const measure = selection.measure; - const tuplet = measure.getTupletForNote(note); - if (!tuplet) { - SmoDuration.doubleDurationNonTuplet(selection); - } else { - SmoDuration.doubleDurationTuplet(selection); - } + const newDuration = note!.stemTicks * 2 + SmoChangeDurationActor.apply(selection, newDuration); return true; } @@ -186,50 +182,17 @@ export class SmoOperation { const note = selection.note as SmoNote; let divisor = 2; const measure = selection.measure; - const tuplet = measure.getTupletForNote(note); - if (measure.timeSignature.actualBeats % 3 === 0 && note.tickCount === 6144) { - // special behavior, if this is dotted 1/4 in 6/8, split to 3 - divisor = 3; - } - if (!tuplet) { - const nticks = note.tickCount / divisor; - if (!SmoMusic.validDurations[nticks]) { - return; - } - SmoContractNoteActor.apply({ - startIndex: selection.selector.tick, - measure: selection.measure, - voice: selection.selector.voice, - newTicks: nticks - }); - SmoBeamer.applyBeams(measure); - } else { - const startIndex = measure.tupletIndex(tuplet) + tuplet.getIndexOfNote(note); - SmoContractTupletActor.apply({ - changeIndex: startIndex, - measure, - voice: selection.selector.voice - }); - } + const nticks = note.stemTicks / divisor; + SmoChangeDurationActor.apply(selection, nticks); + SmoBeamer.applyBeams(measure); + return true; } // ## makeTuplet // ## Description // Makes a non-tuplet into a tuplet of equal value. static makeTuplet(selection: SmoSelection, numNotes: number) { - const note = selection.note as SmoNote; - const measure = selection.measure; - if (measure.getTupletForNote(note)) { - return; - } - const nticks = note.tickCount; - SmoMakeTupletActor.apply({ - index: selection.selector.tick, - totalTicks: nticks, - numNotes, - measure: selection.measure, - voice: selection.selector.voice - }); + SmoMakeTupletActor.apply(selection, numNotes); } static addStaffModifier(selection: SmoSelection, modifier: StaffModifierBase) { selection.staff.addStaffModifier(modifier); @@ -333,24 +296,7 @@ export class SmoOperation { // ## Description // Makes a tuplet into a single with the duration of the whole tuplet static unmakeTuplet(selection: SmoSelection) { - const note = selection.note; - const measure = selection.measure; - if (!measure.getTupletForNote(note)) { - return; - } - const tuplet = measure.getTupletForNote(note); - if (tuplet === null) { - return; - } - const startIndex = measure.tupletIndex(tuplet); - const endIndex = tuplet.notes.length + startIndex - 1; - - SmoUnmakeTupletActor.apply({ - startIndex, - endIndex, - measure, - voice: selection.selector.voice - }); + //todo Nenad: implement this } // ## dotDuration @@ -360,8 +306,8 @@ export class SmoOperation { static dotDuration(selection: SmoSelection) { const note = selection.note as SmoNote; const measure = selection.measure; - const nticks = SmoMusic.getNextDottedLevel(note.tickCount); - if (nticks === note.tickCount) { + const nticks = SmoMusic.getNextDottedLevel(note.stemTicks); + if (nticks === note.stemTicks) { return; } // Don't dot if the thing on the right of the . is too small @@ -383,12 +329,7 @@ export class SmoOperation { if (!SmoMusic.validDurations[selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].tickCount / 2]) { return; } - SmoStretchNoteActor.apply({ - startIndex: selection.selector.tick, - measure, - voice: selection.selector.voice, - newTicks: nticks - }); + SmoChangeDurationActor.apply(selection, nticks); } // ## undotDuration @@ -398,16 +339,11 @@ export class SmoOperation { static undotDuration(selection: SmoSelection) { const note = selection.note as SmoNote; const measure = selection.measure; - const nticks = SmoMusic.getPreviousDottedLevel(note.tickCount); - if (nticks === note.tickCount) { + const nticks = SmoMusic.getPreviousDottedLevel(note.stemTicks); + if (nticks === note.stemTicks) { return; } - SmoContractNoteActor.apply({ - startIndex: selection.selector.tick, - measure, - voice: selection.selector.voice, - newTicks: nticks - }); + SmoChangeDurationActor.apply(selection, nticks); } static transposeScore(score: SmoScore, offset: number) { diff --git a/src/smo/xform/tickDuration.ts b/src/smo/xform/tickDuration.ts index 8b6ce6bb..0fa92b51 100644 --- a/src/smo/xform/tickDuration.ts +++ b/src/smo/xform/tickDuration.ts @@ -21,584 +21,247 @@ export abstract class TickIteratorBase { return null; } } -/** - * SmoDuration: change the duration of a note, maybe at the expense of some - * other note. - * @category SmoTransform - */ -export class SmoDuration { - /** - * doubleDurationNonTuplet - * double the duration of the selection, consuming the next note or - * possibly split it in half and consume that. Simple operation so - * do it inline - * @param selection - * @returns - */ - static doubleDurationNonTuplet(selection: SmoSelection) { - const note: SmoNote | null = selection?.note; - const measure: SmoMeasure = selection.measure; - if (note === null) { - return; - } - const selector: SmoSelector = selection.selector; - const voices: SmoVoice[] | undefined = measure?.voices; - const voice: SmoVoice = voices[selector.voice]; - const notes: SmoNote[] = voice?.notes; - let i = 0; - const nticks: Ticks = { numerator: note.tickCount * 2, denominator: 1, remainder: 0 }; - const replNote = SmoNote.cloneWithDuration(note, nticks); - let ticksUsed = note.tickCount; - const newNotes = []; - for (i = 0; i < selector.tick; ++i) { - newNotes.push(notes[i]); - } - for (i = selector.tick + 1; i < notes.length; ++i) { - const nnote = notes[i]; - ticksUsed += nnote.tickCount; - if (ticksUsed >= nticks.numerator) { - break; - } - } - const remainder = ticksUsed - nticks.numerator; - if (remainder < 0) { - return; - } - newNotes.push(replNote); - if (remainder > 0) { - const lmap = SmoMusic.gcdMap(remainder); - lmap.forEach((duration) => { - newNotes.push(SmoNote.cloneWithDuration(note, duration)); - }); - } - for (i = i + 1; i < notes.length; ++i) { - newNotes.push(notes[i]); - } - // If any tuplets got removed while extending the notes, - voice.notes = newNotes; - const measureTuplets: SmoTuplet[] = []; - const allTuplets: SmoTuplet[] | undefined = measure?.tuplets; - allTuplets?.forEach((tuplet: SmoTuplet) => { - const testNotes = measure?.tupletNotes(tuplet); - if (testNotes?.length === tuplet.notes.length) { - measureTuplets.push(tuplet); - } +export class SmoMakeTupletActor { + private note: SmoNote; + private measure: SmoMeasure; + private selector: SmoSelector; + private voices: SmoVoice[]; + private voice: SmoVoice; + + private totalTicks: number; + private parentTuplet: SmoTuplet | null; + private numNotes: number; + private notesOccupied: number; + private tupletNotes: SmoNote[] = []; + private stemTicks: number; + private tuplet: SmoTuplet; + + constructor(selection: SmoSelection, numNotes: number) { + this.note = selection.note!; + this.measure = selection.measure; + + this.selector = selection.selector; + this.voices = this.measure.voices; + this.voice = this.voices[this.selector.voice]; + this.numNotes = numNotes; + + this.totalTicks = this.note.tickCount; + this.parentTuplet = this.measure.getTupletForNoteIndex(this.selector.voice, this.selector.tick); + + this.stemTicks = SmoTuplet.calculateStemTicks(this.note.stemTicks, this.numNotes); + this.notesOccupied = this.note.stemTicks / this.stemTicks; + + this.tuplet = new SmoTuplet({ + stemTicks: this.stemTicks, + totalTicks: this.totalTicks, + ratioed: false, + bracketed: true, + voice: this.selector.voice, + numNotes: this.numNotes, + notesOccupied: this.notesOccupied, + startIndex: this.selector.tick, + endIndex: this.selector.tick }); - measure.tuplets = measureTuplets; + } - /** - * double duration, tuplet form. Increase the first selection and consume the - * following note. Also a simple operation - * @param selection - * @returns - */ - static doubleDurationTuplet(selection: SmoSelection) { - let i: number = 0; - const measure: SmoMeasure = selection.measure; - const note: SmoNote | null = selection?.note; - if (note === null) { + static apply(selection: SmoSelection, numNotes: number) { + if (!selection?.note) { return; } - const notes = measure.voices[selection.selector.voice].notes; - const tuplet: SmoTuplet | null = measure.getTupletForNote(note); - if (tuplet === null) { - return; - } - const startIndex = selection.selector.tick - tuplet.startIndex; + const actor = new SmoMakeTupletActor(selection, numNotes); + actor.initialize(); + } - const startLength: number = tuplet.notes.length; - tuplet.combine(startIndex, startIndex + 1); - if (tuplet.notes.length >= startLength) { - return; - } - const newNotes = []; + public initialize() { + this.measure.clearBeamGroups(); + this.tupletNotes = this._generateTupletNotes(); + this.tuplet.endIndex += this.tupletNotes.length - 1; + this.measure.adjustTupletIndexes(this.selector.tick, this.tupletNotes.length - 1); - for (i = 0; i < tuplet.startIndex; ++i) { - newNotes.push(notes[i]); - } - tuplet.notes.forEach((note) => { - newNotes.push(note); - }); - for (i = i + tuplet.notes.length + 1; i < notes.length; ++i) { - newNotes.push(notes[i]); - } - measure.voices[selection.selector.voice].notes = newNotes; - } -} -/** - * SmoTickIterator - * this is a local helper class that follows a pattern of iterating of the notes. Most of the - * duration changers iterate over a selection, and return: - * - A note, if the duration changes - * - An array of notes, if the notes split - * - null if the note stays the same - * - empty array, remove the note from the group - * @category SmoTransform - */ -export class SmoTickIterator { - notes: SmoNote[] = []; - newNotes: SmoNote[] = []; - actor: TickIteratorBase; - measure: SmoMeasure; - voice: number = 0; - keySignature: string; - constructor(measure: SmoMeasure, actor: TickIteratorBase, voiceIndex: number) { - this.notes = measure.voices[voiceIndex].notes; - this.measure = measure; - this.voice = typeof (voiceIndex) === 'number' ? voiceIndex : 0; - this.newNotes = []; - // eslint-disable-next-line - this.actor = actor; - this.keySignature = 'C'; - } - static nullActor(note: SmoNote) { - return note; - } - /** - * - * @param measure {SmoMeasure} - * @param actor {} - * @param voiceIndex - */ - static iterateOverTicks(measure: SmoMeasure, actor: TickIteratorBase, voiceIndex: number) { - measure.clearBeamGroups(); - const transformer = new SmoTickIterator(measure, actor, voiceIndex); - transformer.run(); - measure.voices[voiceIndex].notes = transformer.notes; - } - // ### transformNote - // call the actors for each note, and put the result in the note array. - // The note from the original array is copied and sent to each actor. - // - // Because the resulting array can have a different number of notes than the existing - // array, the actors communicate with the transformer in the following, jquery-ish - // but somewhat unintuitive way: - // - // 1. if the actor returns null, the next actor is called and the results of that actor are used - // 2. if all the actors return null, the copy is used. - // 3. if a note object is returned, that is used for the current tick and no more actors are called. - // 4. if an array of notes is returned, it is concatenated to the existing note array and no more actors are called. - // Note that *return note;* and *return [note];* produce the same result. - // 5. if an empty array [] is returned, that copy is not added to the result. The note is effectively deleted. - iterateOverTick(tickmap: TickMap, index: number, note: SmoNote) { - const actor: TickIteratorBase = this.actor; - const newNote: SmoNote[] | SmoNote | null = actor.iterateOverTick(note, tickmap, index); - if (newNote === null) { - this.newNotes.push(note); // no change - return note; - } - if (Array.isArray(newNote)) { - if (newNote.length === 0) { - return null; - } - this.newNotes = this.newNotes.concat(newNote); - return null; + if (this.parentTuplet !== null) { + this.parentTuplet.childrenTuplets.push(this.tuplet); + this.tuplet.parentTuplet = { id: this.parentTuplet.attrs.id }; + // const index = this.parentTuplet.getIndexOfNote(this.note); + // this.parentTuplet.tickables.splice(index, 1, this.tuplet); + } else { + this.measure.tupletTrees.push(this.tuplet); } - this.newNotes.push(newNote as SmoNote); - return null; + + this.voice.notes.splice(this.selector.tick, 1, ...this.tupletNotes); + console.log(this.voice); } - run() { - let i = 0; - const tickmap = this.measure.tickmapForVoice(this.voice); - for (i = 0; i < tickmap.durationMap.length; ++i) { - this.iterateOverTick(tickmap, i, this.measure.voices[this.voice].notes[i]); + private _generateTupletNotes(): SmoNote[] { + const tupletNotes: SmoNote[] = []; + for (let i = 0; i < this.numNotes; ++i) { + const numerator = this.totalTicks / this.numNotes; + const note: SmoNote = SmoNote.cloneWithDuration(this.note, { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }, this.stemTicks); + // Don't clone modifiers, except for first one. + note.textModifiers = i === 0 ? note.textModifiers : []; + note.tuplet = this.tuplet.attrs; + tupletNotes.push(note); } - this.notes = this.newNotes; - return this.newNotes; + + return tupletNotes; } } -/** - * used to create a contract/dilate operation on a note via {@link SmoContractNoteActor} - * @category SmoTransform - */ -export interface SmoContractNoteParams { - startIndex: number, - measure: SmoMeasure, - voice: number, - newTicks: number -} -/** - * Contract the duration of a note, filling in the space with another note - * or rest. - * @category SmoTransform - * */ -export class SmoContractNoteActor extends TickIteratorBase { - startIndex: number; - tickmap: TickMap; - newTicks: number; - measure: SmoMeasure; - voice: number; - constructor(params: SmoContractNoteParams) { - super(); - this.startIndex = params.startIndex; - this.measure = params.measure; - this.voice = params.voice; - this.tickmap = this.measure.tickmapForVoice(this.voice); - this.newTicks = params.newTicks; - } - static apply(params: SmoContractNoteParams) { - const actor = new SmoContractNoteActor(params); - SmoTickIterator.iterateOverTicks(actor.measure, - actor, actor.voice); - } - iterateOverTick(note: SmoNote, tickmap: TickMap, index: number): SmoNote | SmoNote[] | null { - let i = 0; - if (index === this.startIndex) { - const notes: SmoNote[] = []; - const noteCount = Math.floor(note.ticks.numerator / this.newTicks); - let remainder = note.ticks.numerator; - /** - * Replace 1 note with noteCOunt notes of newTIcks duration - * old map: - * d . d . . - * new map: - * d d d . . - */ - for (i = 0; i < noteCount; ++i) { - // first note, retain modifiers so clone. Otherwise just - // retain pitches - if (i === 0) { - const nn = SmoNote.clone(note); - nn.ticks = { numerator: this.newTicks, denominator: 1, remainder: 0 }; - notes.push(nn); - } else { - const nnote = new SmoNote(SmoNote.defaults); - nnote.clef = note.clef; - nnote.pitches = JSON.parse(JSON.stringify(note.pitches)); - nnote.ticks = { numerator: this.newTicks, denominator: 1, remainder: 0 }; - nnote.beamBeats = note.beamBeats; - notes.push(nnote); - } - remainder = remainder - this.newTicks; - } - // make sure remnainder is not too short - if (remainder > 0) { - if (remainder < 128) { - return null; - } - const nnote = new SmoNote(SmoNote.defaults); - nnote.clef = note.clef; - nnote.pitches = JSON.parse(JSON.stringify(note.pitches)); - nnote.ticks = { numerator: remainder, denominator: 1, remainder: 0 }; - nnote.beamBeats = note.beamBeats; - notes.push(nnote); - } - return notes; - } - return null; + +export class SmoChangeDurationActor { + private newStemTicks: number; + private note: SmoNote; + private measure: SmoMeasure; + private selector: SmoSelector; + private voices: SmoVoice[]; + private voice: SmoVoice; + private notes: SmoNote[]; + + constructor(selection: SmoSelection, newStemTicks: number) { + this.newStemTicks = newStemTicks; + this.note = selection.note!; + this.measure = selection.measure; + this.selector = selection.selector; + this.voices = this.measure.voices; + this.voice = this.voices[this.selector.voice]; + this.notes = this.voice.notes; } -} -/** - * used to create a contract/dilate operation on a note via {@link SmoContractTupletActor} - * @category SmoTransform - */ -export interface SmoContractTupletParams { - changeIndex: number, - measure: SmoMeasure, - voice: number -} -/** - * Shrink the duration of a note in a tuplet by creating additional notes - * @category SmoTransform - */ -export class SmoContractTupletActor extends TickIteratorBase { - changeIndex: number; - measure: SmoMeasure; - voice: number; - tuplet: SmoTuplet | null; - oldLength: number = 0; - tupletIndex: number = 0; - splitIndex: number = 0; - constructor(params: SmoContractTupletParams) { - super(); - this.changeIndex = params.changeIndex; - this.measure = params.measure; - this.voice = params.voice; - this.tuplet = this.measure.getTupletForNote(this.measure.voices[this.voice].notes[this.changeIndex]); - if (this.tuplet === null) { + static apply(selection: SmoSelection, newDuration: number) { + if (!selection?.note) { return; } - this.oldLength = this.tuplet.notes.length; - this.tupletIndex = this.measure.tupletIndex(this.tuplet); - this.splitIndex = this.changeIndex - this.tupletIndex; - this.tuplet.split(this.splitIndex); + const actor = new SmoChangeDurationActor(selection, newDuration); + actor.initialize(); } - static apply(params: SmoContractTupletParams) { - const actor = new SmoContractTupletActor(params); - SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice); - } - iterateOverTick(note: SmoNote, tickmap: TickMap, index: number) { - if (this.tuplet === null) { - return null; - } - if (index < this.tupletIndex) { - return note; - } - if (index >= this.tupletIndex + this.oldLength) { - return note; - } - if (index === this.changeIndex) { - return this.tuplet.notes; - } - return []; - } -} -/** - * Constructor params for {@link SmoUnmakeTupletActor} - * @category SmoTransform - */ -export interface SmoUnmakeTupletParams { - startIndex: number, - endIndex: number, - measure: SmoMeasure, - voice: number -} -/** - * Convert a tuplet into a single note that takes up the whole duration - * @category SmoTransform - */ -export class SmoUnmakeTupletActor extends TickIteratorBase { - startIndex: number = 0; - endIndex: number = 0; - measure: SmoMeasure; - voice: number; - constructor(parameters: SmoUnmakeTupletParams) { - super(); - this.startIndex = parameters.startIndex; - this.endIndex = parameters.endIndex; - this.measure = parameters.measure; - this.voice = parameters.voice; - } - static apply(params: SmoUnmakeTupletParams) { - const actor = new SmoUnmakeTupletActor(params); - SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice); - } - iterateOverTick(note: SmoNote, tickmap: TickMap, index: number) { - if (index < this.startIndex || index > this.endIndex) { - return null; - } - if (index === this.startIndex) { - const tuplet = this.measure.getTupletForNote(note); - if (tuplet === null) { - return []; - } - const ticks = tuplet.totalTicks; - const nn: SmoNote = SmoNote.cloneWithDuration(note, { numerator: ticks, denominator: 1, remainder: 0 }); - nn.tuplet = {} as TupletInfo; - this.measure.removeTupletForNote(note); - return [nn]; + public initialize() { + this.measure.clearBeamGroups(); + if (this.newStemTicks > this.note.stemTicks) { + //if new stemTicks is crossing tuplet boundary, + //clear tuplets that are in the way and only then proceed to actually stretch the note + //todo: clearTuplets(); + this.stretchNote(); + } else if (this.newStemTicks < this.note.stemTicks) { + this.contractNote(); + } else { + return; } - return []; } -} -/** - * constructor parameters for {@link SmoMakeTupletActor} - * @category SmoTransform - */ -export interface SmoMakeTupletParams { - index: number, - totalTicks: number, - numNotes: number, - measure: SmoMeasure, - voice: number -} -/** - * Turn a tuplet into a non-tuplet of the same length - * @category SmoTransform - * - * */ -export class SmoMakeTupletActor extends TickIteratorBase { - measure: SmoMeasure; - durationMap: number[]; - numNotes: number; - stemTicks: number; - totalTicks: number; - rangeToSkip: number[]; - tuplet: SmoNote[]; - voice: number; - index: number; - constructor(params: SmoMakeTupletParams) { - let i = 0; - super(); - this.measure = params.measure; - this.numNotes = params.numNotes; - this.durationMap = []; - this.totalTicks = params.totalTicks; - this.voice = params.voice; - this.index = params.index; - for (i = 0; i < this.numNotes; ++i) { - this.durationMap.push(1.0); - } - this.stemTicks = SmoTuplet.calculateStemTicks(this.totalTicks, this.numNotes); - this.rangeToSkip = this._rangeToSkip(); - this.tuplet = []; - } - static apply(params: SmoMakeTupletParams) { - const actor = new SmoMakeTupletActor(params); - SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice); - } - _rangeToSkip(): number[] { + /** + * todo: handle ticks reminder + * Expand the note and absorb all notes to the right that fall within the new duration. + * If the duration of the last note does not completely fit within the new duration, fill in the remainder with new notes. + */ + private stretchNote() { let i = 0; - if (this.measure === null) { - return []; - } - const ticks = this.measure.tickmapForVoice(this.voice); - let accum = 0; - const rv = []; - rv.push(this.index); - for (i = 0; i < ticks.deltaMap.length; ++i) { - if (i >= this.index) { - accum += ticks.deltaMap[i]; + + let newTicks: Ticks = { numerator: this.newStemTicks, denominator: 1, remainder: 0 }; + + const multiplier = this.note.tickCount / this.note.stemTicks; + + if (this.note.isTuplet) { + const numerator = this.newStemTicks * multiplier; + newTicks = { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }; + } + + const replacingNote = SmoNote.cloneWithDuration(this.note, newTicks, this.newStemTicks); + + let stemTicksUsed = this.note.stemTicks; + const allNotes = []; + // const newNotes = []; + for (i = 0; i < this.selector.tick; ++i) { + allNotes.push(this.notes[i]); + } + for (i = this.selector.tick + 1; i < this.notes.length; ++i) { + const nnote = this.notes[i]; + //in case notes are part of the tuplet they need to belong to the same tuplet + //this check is only temporarely here, it should never come to this + if (nnote.isTuplet && !this.areNotesInSameTuplet(this.note, nnote)) { + break; } - if (accum >= this.totalTicks) { - rv.push(i); + stemTicksUsed += nnote.stemTicks; + if (stemTicksUsed >= this.newStemTicks) { break; } } - return rv; - } - iterateOverTick(note: SmoNote, tickmap: TickMap, index: number) { - let i = 0; - // if our tuplet replaces this note, make sure we make it go away. - if (index > this.index && index <= this.rangeToSkip[1]) { - return []; - } - if (this.measure === null) { - return []; + const remainder = stemTicksUsed - this.newStemTicks; + if (remainder < 0) { + return; } - if (index !== this.index) { - return null; + + allNotes.push(replacingNote); + // newNotes.push(replacingNote); + + if (remainder > 0) { + const lmap = SmoMusic.gcdMap(remainder); + lmap.forEach((stemTick) => { + const nnote = SmoNote.cloneWithDuration(this.note, stemTick * multiplier, stemTick) + allNotes.push(nnote); + // newNotes.push(nnote); + }); } - for (i = 0; i < this.numNotes; ++i) { - note = SmoNote.cloneWithDuration(note, { numerator: this.stemTicks, denominator: 1, remainder: 0 }); - // Don't clone modifiers, except for first one. - note.textModifiers = i === 0 ? note.textModifiers : []; - this.tuplet.push(note); + + for (i = i + 1 ; i < this.notes.length; ++i) { + allNotes.push(this.notes[i]); } - const tuplet = new SmoTuplet({ - notes: this.tuplet, - stemTicks: this.stemTicks, - totalTicks: this.totalTicks, - ratioed: false, - bracketed: true, - startIndex: index, - durationMap: this.durationMap, - voice: tickmap.voice, - numNotes: this.numNotes - }); - this.measure.tuplets.push(tuplet); - return this.tuplet; + + const noteCountDiff = allNotes.length - this.voice.notes.length; + this.measure.adjustTupletIndexes(this.selector.tick, noteCountDiff); + this.voice.notes = allNotes; + } -} -/** - * Constructor when we want to double or dot the duration of a note (stretch) - * for {@link SmoStretchNoteActor} - * @param startIndex tick index into the measure - * @param measure the container measure - * @param voice the voice index - * @param newTicks the ticks the new note will take up - * @category SmoTransform - */ -export interface SmoStretchNoteParams { - startIndex: number, - measure: SmoMeasure, - voice: number, - newTicks: number -} -/** - * increase the length of a note, removing future notes in the measure as required - * @category SmoTransform - */ -export class SmoStretchNoteActor extends TickIteratorBase { - startIndex: number; - tickmap: TickMap; - newTicks: number; - startTick: number; - divisor: number; - durationMap: number[]; - skipFromStart: number; - skipFromEnd: number; - measure: SmoMeasure; - voice: number; - constructor(params: SmoStretchNoteParams) { - let mapIx = 0; + /** + * todo: handle ticks reminder + * replace duration of current note and fill in the rest with new notes + */ + private contractNote() { let i = 0; - super(); - this.startIndex = params.startIndex; - this.measure = params.measure; - this.voice = params.voice; - this.tickmap = this.measure.tickmapForVoice(this.voice); - this.newTicks = params.newTicks; - this.startTick = this.tickmap.durationMap[this.startIndex]; - const currentTicks = this.tickmap.deltaMap[this.startIndex]; - const endTick = this.tickmap.durationMap[this.startIndex] + this.newTicks; - this.divisor = -1; - this.durationMap = []; - this.skipFromStart = this.startIndex + 1; - this.skipFromEnd = this.startIndex + 1; - this.durationMap.push(this.newTicks); - - mapIx = this.tickmap.durationMap.indexOf(endTick); - - const remaining = this.tickmap.deltaMap.slice(this.startIndex, this.tickmap.durationMap.length).reduce((accum, x) => x + accum); - if (remaining === this.newTicks) { - mapIx = this.tickmap.deltaMap.length; - } - // If there is no tickable at the end point, try to split the next note - /** - * old map: - * d . d . - * split map: - * d . d d - * new map: - * d . . d - */ - if (mapIx < 0) { - const ndelta = this.tickmap.deltaMap[this.startIndex + 1]; - const needed = this.newTicks - currentTicks; - const exp = ndelta / needed; - // Next tick does not divide evenly into this, or next tick is shorter than this - if (Math.round(ndelta / exp) - ndelta / exp !== 0 || ndelta < 256) { - this.durationMap = []; - } else if (ndelta / exp + this.startTick + this.newTicks <= this.tickmap.totalDuration) { - this.durationMap.push(ndelta - (ndelta / exp)); - } else { - // there is no way to do this... - this.durationMap = []; - } - } else { - // If this note now takes up the space of other notes, remove those notes - for (i = this.startIndex + 1; i < mapIx; ++i) { - this.durationMap.push(0); - } + let newTicks: Ticks = { numerator: this.newStemTicks, denominator: 1, remainder: 0 }; + + const multiplier = this.note.tickCount / this.note.stemTicks; + + if (this.note.isTuplet) { + const numerator = this.newStemTicks * multiplier; + newTicks = { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }; + } + + const replacingNote = SmoNote.cloneWithDuration(this.note, newTicks, this.newStemTicks); + const stemTicksUsed = this.note.stemTicks; + const allNotes = []; + // const newNotes = []; + for (i = 0; i < this.selector.tick; ++i) { + allNotes.push(this.notes[i]); + } + const remainder = stemTicksUsed - this.newStemTicks; + allNotes.push(replacingNote); + // newNotes.push(replacingNote); + + if (remainder > 0) { + const lmap = SmoMusic.gcdMap(remainder); + lmap.forEach((stemTick) => { + const nnote = SmoNote.cloneWithDuration(this.note, stemTick * multiplier, stemTick); + allNotes.push(nnote); + // newNotes.push(nnote); + }); } - } - static apply(params: SmoStretchNoteParams) { - const actor = new SmoStretchNoteActor(params); - SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice); - } - iterateOverTick(note: SmoNote, tickmap: TickMap, index: number) { - if (this.durationMap.length === 0) { - return null; + for (i = this.selector.tick + 1; i < this.notes.length; ++i) { + allNotes.push(this.notes[i]); } - if (index >= this.startIndex && index < this.startIndex + this.durationMap.length) { - const mapIndex = index - this.startIndex; - const ticks = this.durationMap[mapIndex]; - if (ticks === 0) { - return []; - } - note = SmoNote.cloneWithDuration(note, { numerator: ticks, denominator: 1, remainder: 0 }); - return [note]; + + const noteCountDiff = allNotes.length - this.voice.notes.length; + this.measure.adjustTupletIndexes(this.selector.tick, noteCountDiff); + this.voice.notes = allNotes; + } + + private areNotesInSameTuplet(noteOne: SmoNote, noteTwo: SmoNote): boolean { + if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tuplet!.id == noteTwo.tuplet!.id) { + return true; } - return null; + return false; } } diff --git a/tests/file-load.ts b/tests/file-load.ts index 1cdaba41..847b1fd3 100644 --- a/tests/file-load.ts +++ b/tests/file-load.ts @@ -39,7 +39,7 @@ export function createLoadTests(): void { midiScore = (new MidiToSmo(parseMidi(midiData.value), 1024)).convert(); await view.changeScore(midiScore); QUnit.test('loadMidi2', assert => { - assert.equal(midiScore.staves[0].measures[0].tuplets.length, 1); + assert.equal(midiScore.staves[0].measures[0].tupletTrees.length, 1); }); midiData = new SuiXhrLoader(midiKeyPath); await midiData.loadAsync(); From c262a55456e5bb954ecabf967b538ba7ae6d80c7 Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Fri, 29 Mar 2024 01:17:38 +0100 Subject: [PATCH 03/14] enable serialization --- src/smo/data/measure.ts | 16 ++++++++-------- src/smo/data/note.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/smo/data/measure.ts b/src/smo/data/measure.ts index ea4c8399..dc7bdc67 100644 --- a/src/smo/data/measure.ts +++ b/src/smo/data/measure.ts @@ -605,19 +605,19 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { } //todo: implement this - // const tuplets = []; + const tuplets = []; for (j = 0; j < jsonObj.tupletTrees.length; ++j) { const tupJson = SmoTuplet.defaults; smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj.tupletTrees[j], tupJson); // const noteAr = noteSum.filter((nn: any) => - // nn.isTuplet && nn.tuplet.id === tupJson.attrs!.id); + // nn.isTuplet && nn.tuplet.id === tupJson.attrs!.id); - // // Bug fix: A tuplet with no notes may be been overwritten - // // in a copy/paste operation + // Bug fix: A tuplet with no notes may be been overwritten + // in a copy/paste operation // if (noteAr.length > 0) { - // tupJson.notes = noteAr; - // const tuplet = new SmoTuplet(tupJson); - // tuplets.push(tuplet); + // tupJson.notes = noteAr; + const tuplet = new SmoTuplet(tupJson); + tuplets.push(tuplet); // } } @@ -656,7 +656,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { } params.keySignature = jsonObj.keySignature ?? 'C'; params.voices = voices; - // params.tuplets = tuplets; + params.tupletTrees = tuplets; params.modifiers = modifiers; const rv = new SmoMeasure(params); // Handle migration for measure-mapped parameters diff --git a/src/smo/data/note.ts b/src/smo/data/note.ts index 2bb86815..03bba28f 100644 --- a/src/smo/data/note.ts +++ b/src/smo/data/note.ts @@ -302,7 +302,7 @@ export class SmoNote implements Transposable { * @internal */ static get parameterArray() { - return ['ticks', 'pitches', 'noteType', 'tuplet', 'clef', 'isCue', + return ['ticks', 'pitches', 'noteType', 'tuplet', 'clef', 'isCue', 'stemTicks', 'endBeam', 'beamBeats', 'flagState', 'noteHead', 'fillStyle', 'hidden', 'arpeggio', 'clefNote']; } /** From 38cc010ef4e8373531d81bb9a07fb7201731a15b Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Mon, 19 Aug 2024 23:10:03 +0200 Subject: [PATCH 04/14] implementation with iterateOverTick --- src/render/vex/toVex.ts | 50 +-- src/render/vex/vxMeasure.ts | 6 +- src/smo/data/measure.ts | 174 ++------ src/smo/data/tuplet.ts | 365 ++++++++--------- src/smo/mxml/smoToXml.ts | 4 +- src/smo/xform/audioTrack.ts | 3 +- src/smo/xform/copypaste.ts | 721 ++++++++++++++++++---------------- src/smo/xform/operations.ts | 79 +++- src/smo/xform/tickDuration.ts | 527 ++++++++++++++++--------- 9 files changed, 1035 insertions(+), 894 deletions(-) diff --git a/src/render/vex/toVex.ts b/src/render/vex/toVex.ts index 76b81270..7dd28958 100644 --- a/src/render/vex/toVex.ts +++ b/src/render/vex/toVex.ts @@ -427,30 +427,31 @@ function createBeamGroups(smoMeasure: SmoMeasure, strs: string[]) { } }); } +//todo nenad: implement this function createTuplets(smoMeasure: SmoMeasure, strs: string[]) { - smoMeasure.voices.forEach((voice, voiceIx) => { - const tps = smoMeasure.tupletTrees.filter((tp) => tp.voice === voiceIx); - for (var i = 0; i < tps.length; ++i) { - const tp = tps[i]; - const nar: string[] = []; - for ( let note of smoMeasure.tupletNotes(tp)) { - const vexNote = `${note.attrs.id}`; - nar.push(vexNote); - } - const direction = smoMeasure.getStemDirectionForTuplet(tp) === SmoNote.flagStates.up ? - VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM; - const tpParams: TupletOptions = { - num_notes: tp.numNotes, - notes_occupied: tp.notesOccupied, - ratioed: false, - bracketed: true, - location: direction - }; - const tpParamString = JSON.stringify(tpParams); - const narString = '[' + nar.join(',') + ']'; - strs.push(`const ${tp.attrs.id} = new VF.Tuplet(${narString}, JSON.parse('${tpParamString}'));`); - } - }); + // smoMeasure.voices.forEach((voice, voiceIx) => { + // const tps = smoMeasure.tupletTrees.filter((tp) => tp.voice === voiceIx); + // for (var i = 0; i < tps.length; ++i) { + // const tp = tps[i]; + // const nar: string[] = []; + // for ( let note of smoMeasure.tupletNotes(tp)) { + // const vexNote = `${note.attrs.id}`; + // nar.push(vexNote); + // } + // const direction = smoMeasure.getStemDirectionForTuplet(tp) === SmoNote.flagStates.up ? + // VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM; + // const tpParams: TupletOptions = { + // num_notes: tp.numNotes, + // notes_occupied: tp.notesOccupied, + // ratioed: false, + // bracketed: true, + // location: direction + // }; + // const tpParamString = JSON.stringify(tpParams); + // const narString = '[' + nar.join(',') + ']'; + // strs.push(`const ${tp.attrs.id} = new VF.Tuplet(${narString}, JSON.parse('${tpParamString}'));`); + // } + // }); } function createMeasure(smoMeasure: SmoMeasure, heightOffset: number, strs: string[]) { const ssid = 'stave' + smoMeasure.attrs.id; @@ -494,8 +495,9 @@ function createMeasure(smoMeasure: SmoMeasure, heightOffset: number, strs: strin strs.push(`${bg.attrs.id}.setContext(context);`); strs.push(`${bg.attrs.id}.draw();`) }); + //todo nenad: implement this smoMeasure.tupletTrees.forEach((tp) => { - strs.push(`${tp.attrs.id}.setContext(context).draw();`) + // strs.push(`${tp.attrs.id}.setContext(context).draw();`) }); } // ## SmoToVex diff --git a/src/render/vex/vxMeasure.ts b/src/render/vex/vxMeasure.ts index 4250e90e..681fb4cb 100644 --- a/src/render/vex/vxMeasure.ts +++ b/src/render/vex/vxMeasure.ts @@ -328,8 +328,8 @@ export class VxMeasure implements VxMeasureIf { this.vexTuplets = []; this.tupletToVexMap = {}; for (let i = 0; i < this.smoMeasure.tupletTrees.length; ++i) { - const tp = this.smoMeasure.tupletTrees[i]; - if (tp.voice !== vix) { + const tupletTree = this.smoMeasure.tupletTrees[i]; + if (tupletTree.voice !== vix) { continue; } const traverseTupletTree = ( parentTuplet: SmoTuplet): void => { @@ -354,7 +354,7 @@ export class VxMeasure implements VxMeasureIf { traverseTupletTree(tuplet); } } - traverseTupletTree(tp); + traverseTupletTree(tupletTree.tuplet); } } diff --git a/src/smo/data/measure.ts b/src/smo/data/measure.ts index dc7bdc67..6ba42482 100644 --- a/src/smo/data/measure.ts +++ b/src/smo/data/measure.ts @@ -17,7 +17,7 @@ import { TimeSignatureParametersSer, SmoMeasureFormatParamsSer, SmoTempoTextParamsSer } from './measureModifiers'; import { SmoNote, NoteType, SmoNoteParamsSer } from './note'; -import { SmoTuplet, SmoTupletParamsSer, SmoTupletParams } from './tuplet'; +import { SmoTuplet, SmoTupletParamsSer, SmoTupletParams, SmoTupletTreeParamsSer, SmoTupletTree } from './tuplet'; import { layoutDebug } from '../../render/sui/layoutDebug'; import { SvgHelpers } from '../../render/sui/svgHelpers'; import { TickMap } from '../xform/tickMap'; @@ -135,7 +135,7 @@ export const SmoMeasureStringParams: SmoMeasureStringParam[] = ['keySignature']; export interface SmoMeasureParams { timeSignature: TimeSignature, keySignature: string, - tupletTrees: SmoTuplet[], + tupletTrees: SmoTupletTree[], transposeIndex: number, lines: number, staffY: number, @@ -161,11 +161,11 @@ export interface SmoMeasureParamsSer { /** * constructor */ - ctor: string; + ctor: string, /** * id of the measure */ - attrs: SmoAttrs; + attrs: SmoAttrs, /** * time signature serialization */ @@ -177,7 +177,7 @@ export interface SmoMeasureParamsSer { /** * a list of tuplets (serialized) */ - tupletTrees: SmoTupletParamsSer[], + tupletTrees: SmoTupletTreeParamsSer[], /** * transpose the notes up/down. TODO: this should not be serialized * as its part of the instrument parameters @@ -302,7 +302,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { */ keySignature: string = ''; canceledKeySignature: string = ''; - tupletTrees: SmoTuplet[] = []; + tupletTrees: SmoTupletTree[] = []; repeatSymbol: boolean = false; repeatCount: number = 0; /** @@ -536,8 +536,8 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { params.voices = []; params.modifiers = []; - this.tupletTrees.forEach((tuplet) => { - params.tupletTrees!.push(tuplet.serialize()); + this.tupletTrees.forEach((tupletTree) => { + params.tupletTrees!.push(tupletTree.serialize()); }); this.voices.forEach((voice) => { @@ -589,7 +589,6 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { let j = 0; let i = 0; const voices: SmoVoice[] = []; - const noteSum = []; for (j = 0; j < jsonObj.voices.length; ++j) { const voice = jsonObj.voices[j]; const notes: SmoNote[] = []; @@ -600,27 +599,9 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { const noteParams = voice.notes[i]; const smoNote = SmoNote.deserialize(noteParams); notes.push(smoNote); - noteSum.push(smoNote); } } - //todo: implement this - const tuplets = []; - for (j = 0; j < jsonObj.tupletTrees.length; ++j) { - const tupJson = SmoTuplet.defaults; - smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj.tupletTrees[j], tupJson); - // const noteAr = noteSum.filter((nn: any) => - // nn.isTuplet && nn.tuplet.id === tupJson.attrs!.id); - - // Bug fix: A tuplet with no notes may be been overwritten - // in a copy/paste operation - // if (noteAr.length > 0) { - // tupJson.notes = noteAr; - const tuplet = new SmoTuplet(tupJson); - tuplets.push(tuplet); - // } - } - const modifiers: SmoMeasureModifierBase[] = []; jsonObj.modifiers.forEach((modParams: any) => { const modifier: SmoMeasureModifierBase = SmoMeasureModifierBase.deserialize(modParams); @@ -656,19 +637,41 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { } params.keySignature = jsonObj.keySignature ?? 'C'; params.voices = voices; - params.tupletTrees = tuplets; + // params.tupletTrees = tuplets; params.modifiers = modifiers; - const rv = new SmoMeasure(params); + const measure = new SmoMeasure(params); // Handle migration for measure-mapped parameters - rv.modifiers.forEach((mod) => { + measure.modifiers.forEach((mod) => { if (mod.ctor === 'SmoTempoText') { - rv.tempo = (mod as SmoTempoText); + measure.tempo = (mod as SmoTempoText); } }); - if (!rv.tempo) { - rv.tempo = new SmoTempoText(SmoTempoText.defaults); + if (!measure.tempo) { + measure.tempo = new SmoTempoText(SmoTempoText.defaults); } - return rv; + + for (j = 0; j < jsonObj.tupletTrees.length; ++j) { + const tupletTreeJson = jsonObj.tupletTrees[j]; + const tupletTree = SmoTupletTree.deserialize(tupletTreeJson); + measure.tupletTrees.push(tupletTree); + } + + //deserialization of a legacy tuplets + //legacy schema had measure.tuplets, it is measure.tupletTrees now + if ((jsonObj as any).tuplets !== undefined) { + for (j = 0; j < (jsonObj as any).tuplets.length; ++j) { + const tupJson = (jsonObj as any).tuplets[j]; + const tuplet: SmoTuplet = SmoTuplet.deserialize(tupJson); + const tupletTree: SmoTupletTree = new SmoTupletTree({tuplet: tuplet}); + measure.tupletTrees.push(tupletTree); + } + } + + return measure; + } + + static clone(measure: SmoMeasure): SmoMeasure { + return SmoMeasure.deserialize(measure.serialize()); } /** @@ -1201,17 +1204,17 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { return rv; } - getClosestTickCountIndex(voiceIndex: number, tickCount: number): number { + getClosestIndexFromTickCount(voiceIndex: number, tickCount: number): number { let i = 0; let rv = 0; for (i = 0; i < this.voices[voiceIndex].notes.length; ++i) { const note = this.voices[voiceIndex].notes[i]; - if (note.tickCount + rv > tickCount) { - return rv; + if (note.tickCount + rv >= tickCount) { + return i; } rv += note.tickCount; } - return rv; + return i; } isPickup(): boolean { @@ -1288,101 +1291,10 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { } - // #### tupletIndex - // return the index of the given tuplet - tupletIndex(tuplet: SmoTuplet) { - let j = 0; - let i = 0; - for (j = 0; j < this.voices.length; ++j) { - const notes = this.voices[j].notes; - for (i = 0; i < notes.length; ++i) { - const note = notes[i] as SmoNote; - if (note.tuplet && note.tuplet.id === tuplet.attrs.id) { - return i; - } - } - } - return -1; - } - - // #### getTupletForNote - // Finds the tuplet for a given note, or null if there isn't one. - getTupletForNoteIndex(voiceIx: number, noteIx: number): SmoTuplet | null { - const tuplets = this.getTupletHierarchyForNoteIndex(voiceIx, noteIx); - if(tuplets.length) { - return tuplets[tuplets.length - 1]; - } - return null; - } - - // Finds the tuplet hierarchy for a given note. - getTupletHierarchyForNoteIndex(voiceIx: number, noteIx: number): SmoTuplet[] { - const note: SmoNote | undefined = this.voices[voiceIx]?.notes[noteIx]; - if (!note) { - return []; - } - if (!note.isTuplet) { - return []; - } - - 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 < this.tupletTrees.length; i++) { - const tuplet: SmoTuplet = this.tupletTrees[i]; - if (tuplet.startIndex <= noteIx && tuplet.endIndex >= noteIx) { - traverseTupletTree(tuplet); - break; - } - } - - return tupletHierarchy; - } - - adjustTupletIndexes(tick: number, diff: number) { - - const traverseTupletTree = ( parentTuplet: SmoTuplet): void => { - parentTuplet.endIndex += diff; - if(parentTuplet.startIndex > tick) { - parentTuplet.startIndex += diff; - } - for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { - const tuplet = parentTuplet.childrenTuplets[i]; - traverseTupletTree(tuplet); - } - } + - //find tuplet tree - for (let i = 0; i < this.tupletTrees.length; i++) { - const tuplet: SmoTuplet = this.tupletTrees[i]; - if (tuplet.endIndex >= tick) { - traverseTupletTree(tuplet); - break; - } - } - } + - removeTupletForNote(note: SmoNote) { - let i = 0; - const tuplets = []; - for (i = 0; i < this.tupletTrees.length; ++i) { - const tuplet = this.tupletTrees[i]; - if (note.tuplet !== null && note.tuplet.id !== tuplet.attrs.id) { - tuplets.push(tuplet); - } - } - this.tupletTrees = tuplets; - } setClef(clef: Clef) { const oldClef = this.clef; this.clef = clef; diff --git a/src/smo/data/tuplet.ts b/src/smo/data/tuplet.ts index 6b7a331c..a9207663 100644 --- a/src/smo/data/tuplet.ts +++ b/src/smo/data/tuplet.ts @@ -9,6 +9,149 @@ import { SmoNote, SmoNoteParamsSer, TupletInfo } from './note'; import { SmoMusic } from './music'; import { SmoNoteModifierBase } from './noteModifiers'; import { getId, SmoAttrs, Clef } from './common'; +import { SmoMeasure } from './measure'; +import {tuplets} from "vexflow_smoosic/build/esm/types/tests/formatter/tests"; + + +export interface SmoTupletTreeParams { + tuplet: SmoTuplet +} + +export interface SmoTupletTreeParamsSer { + /** + * constructor + */ + ctor: string, + /** + * root tuplet + */ + tuplet: SmoTupletParamsSer +} + +export class SmoTupletTree { + + /** + * root tuplet + */ + tuplet: SmoTuplet; + + constructor(params: SmoTupletTreeParams) { + this.tuplet = params.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; + } + } + } + + 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 @@ -43,7 +186,7 @@ export interface SmoTupletParamsSer { */ attrs: SmoAttrs, /** - * numNotes in the duplet (not necessarily same as notes array size) + * numNotes in the tuplet (not necessarily same as notes array size) */ numNotes: number, /** @@ -135,22 +278,48 @@ export class SmoTuplet { } static get parameterArray() { - return ['stemTicks', 'ticks', 'totalTicks', 'startIndex', 'endIndex', - 'attrs', 'ratioed', 'bracketed', 'voice', 'numNotes', 'childrenTuplets', 'parentTuplet']; + return ['stemTicks', 'totalTicks', 'startIndex', 'endIndex', + 'attrs', 'ratioed', 'bracketed', 'voice', 'numNotes']; } serialize(): SmoTupletParamsSer { - const params = { - ctor: 'SmoTuplet' - }; - smoSerialize.serializedMergeNonDefault(SmoTuplet.defaults, - SmoTuplet.parameterArray, this, params); + const params: Partial = {}; + 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; + // We need to calculate the endIndex based on length of notes array + // Legacy schema had notes array, but we now demarcate tuplet with startIndex and endIndex + // Legacy schema did not have notesOccupied, we need to calculate it. + if ((jsonObj as any).notes !== undefined) { + const numberOfNotes = (jsonObj as any).notes.length; + tupJson.endIndex = jsonObj.startIndex + numberOfNotes; + tupJson.notesOccupied = jsonObj.totalTicks / jsonObj.stemTicks; + } + + smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj, tupJson); + const tuplet = new SmoTuplet(tupJson); + tuplet.parentTuplet = jsonObj.parentTuplet ? jsonObj.parentTuplet : null; + 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; @@ -162,184 +331,28 @@ export class SmoTuplet { } return stemTicks * 2; } + constructor(params: SmoTupletParams) { - smoSerialize.vexMerge(this, SmoTuplet.defaults); - smoSerialize.serializedMerge(SmoTuplet.parameterArray, params, this); + const defs = SmoTuplet.defaults; + this.numNotes = params.numNotes ? params.numNotes : defs.numNotes; + this.notesOccupied = params.notesOccupied ? params.notesOccupied : defs.notesOccupied; + this.stemTicks = params.stemTicks ? params.stemTicks : defs.stemTicks; + this.totalTicks = params.totalTicks ? params.totalTicks : defs.totalTicks; + this.bracketed = params.bracketed ? params.bracketed : defs.bracketed; + this.voice = params.voice ? params.voice : defs.voice; + this.ratioed = params.ratioed ? params.ratioed : defs.ratioed; + this.startIndex = params.startIndex ? params.startIndex : defs.startIndex; + this.endIndex = params.endIndex ? params.endIndex : defs.endIndex; this.attrs = { id: getId().toString(), type: 'SmoTuplet' }; } + static get longestTuplet() { return 8192; } - //todo: implement this - static cloneTuplet(tuplet: SmoTuplet): SmoTuplet { - return new SmoTuplet({ - stemTicks: 0, - totalTicks: 0, - ratioed: false, - bracketed: true, - startIndex: 0, - endIndex: 0, - voice: 0, - numNotes: 3, - notesOccupied: 2, - }); - } - - // static cloneTuplet(tuplet: SmoTuplet): SmoTuplet { - // let i = 0; - // const noteAr = tuplet.notes; - // const durationMap = JSON.parse(JSON.stringify(tuplet.durationMap)); // deep copy array - - // // Add any remainders for oddlets - // const totalTicks = noteAr.map((nn) => nn.ticks.numerator + nn.ticks.remainder) - // .reduce((acc, nn) => acc + nn); - - // const numNotes: number = tuplet.numNotes; - // const stemTicks = SmoTuplet.calculateStemTicks(totalTicks, numNotes); - - // const tupletNotes: SmoNote[] = []; - - // noteAr.forEach((note) => { - // const textModifiers = note.textModifiers; - // // Note preserver remainder - // note = SmoNote.cloneWithDuration(note, { - // numerator: stemTicks * tuplet.durationMap[i], - // denominator: 1, - // remainder: note.ticks.remainder - // }); - - // // Don't clone modifiers, except for first one. - // if (i === 0) { - // const ntmAr: any = []; - // textModifiers.forEach((tm) => { - // const ntm = SmoNoteModifierBase.deserialize(tm); - // ntmAr.push(ntm); - // }); - // note.textModifiers = ntmAr; - // } - // i += 1; - // tupletNotes.push(note); - // }); - // const rv = new SmoTuplet({ - // numNotes: tuplet.numNotes, - // voice: tuplet.voice, - // notes: tupletNotes, - // stemTicks, - // totalTicks, - // ratioed: false, - // bracketed: true, - // startIndex: tuplet.startIndex, - // durationMap - // }); - // return rv; - // } - - // _adjustTicks() { - // let i = 0; - // const sum = this.durationSum; - // for (i = 0; i < this.notes.length; ++i) { - // const note = this.notes[i]; - // // TODO: notes_occupied needs to consider vex duration - // note.ticks.denominator = 1; - // note.ticks.numerator = Math.floor((this.totalTicks * this.durationMap[i]) / sum); - // note.tuplet = this.attrs; - // } - - // // put all the remainder in the first note of the tuplet - // const noteTicks = this.notes.map((nn) => nn.tickCount) - // .reduce((acc, dd) => acc + dd); - // // bug fix: if this is a clones tuplet, remainder is already set - // this.notes[0].ticks.remainder = - // this.notes[0].ticks.remainder + this.totalTicks - noteTicks; - // } - // getIndexOfNote(note: SmoNote | null): number { - // let rv = -1; - // let i = 0; - // if (!note) { - // return -1; - // } - // for (i = 0; i < this.notes.length; ++i) { - // const tn = this.notes[i]; - // if (note.attrs.id === tn.attrs.id) { - // rv = i; - // } - // } - // return rv; - // } - - // split(combineIndex: number) { - // let i = 0; - // const multiplier = 0.5; - // const nnotes: SmoNote[] = []; - // const nmap: number[] = []; - // for (i = 0; i < this.notes.length; ++i) { - // const note = this.notes[i]; - // if (i === combineIndex) { - // nmap.push(this.durationMap[i] * multiplier); - // nmap.push(this.durationMap[i] * multiplier); - // note.ticks.numerator *= multiplier; - - // const onote = SmoNote.clone(note); - // // remainder is for the whole tuplet, so don't duplicate that. - // onote.ticks.remainder = 0; - // nnotes.push(note); - // nnotes.push(onote); - // } else { - // nmap.push(this.durationMap[i]); - // nnotes.push(note); - // } - // } - // this.notes = nnotes; - // this.durationMap = nmap; - // } - // combine(startIndex: number, endIndex: number) { - // let i = 0; - // let base = 0.0; - // let acc = 0.0; - // // can't combine in this way, too many notes - // if (this.notes.length <= endIndex || startIndex >= endIndex) { - // return this; - // } - // for (i = startIndex; i <= endIndex; ++i) { - // acc += this.durationMap[i]; - // if (i === startIndex) { - // base = this.durationMap[i]; - // } else if (this.durationMap[i] !== base) { - // // Can't combine non-equal tuplet notes - // return this; - // } - // } - // // how much each combined value will be multiplied by - // const multiplier = acc / base; - - // const nmap = []; - // const nnotes = []; - // // adjust the duration map - // for (i = 0; i < this.notes.length; ++i) { - // const note = this.notes[i]; - // // notes that don't change are unchanged - // if (i < startIndex || i > endIndex) { - // nmap.push(this.durationMap[i]); - // nnotes.push(note); - // } - // // changed note with combined duration - // if (i === startIndex) { - // note.ticks.numerator = note.ticks.numerator * multiplier; - // nmap.push(acc); - // nnotes.push(note); - // } - // // other notes after startIndex are removed from the map. - // } - // this.notes = nnotes; - // this.durationMap = nmap; - // return this; - // } - - //todo: adjust naming get num_notes() { return this.numNotes; diff --git a/src/smo/mxml/smoToXml.ts b/src/smo/mxml/smoToXml.ts index 6eeb4ab7..089a316b 100644 --- a/src/smo/mxml/smoToXml.ts +++ b/src/smo/mxml/smoToXml.ts @@ -10,7 +10,7 @@ import { SmoBarline, TimeSignature, SmoRehearsalMark, SmoMeasureModifierBase } f import { SmoStaffHairpin, SmoSlur, SmoTie } from '../data/staffModifiers'; import { SmoArticulation, SmoLyric, SmoOrnament } from '../data/noteModifiers'; import { SmoSelector } from '../xform/selections'; -import { SmoTuplet } from '../data/tuplet'; +import { SmoTuplet, SmoTupletTree } from '../data/tuplet'; import { XmlHelpers } from './xmlHelpers'; import { SmoTempoText } from '../data/measureModifiers'; @@ -784,7 +784,7 @@ export class SmoToXml { } const duration = note.tickCount; smoState.measureTicks += duration; - const tuplet = measure.getTupletForNoteIndex(smoState.voiceIndex, smoState.voiceTickIndex); + const tuplet = SmoTupletTree.getTupletForNoteIndex(measure.tupletTrees, smoState.voiceIndex, smoState.voiceTickIndex); nn(noteElement, 'duration', { duration }, 'duration'); SmoToXml.tie(noteElement, smoState); nn(noteElement, 'voice', { voice: smoState.voiceIndex }, 'voice'); diff --git a/src/smo/xform/audioTrack.ts b/src/smo/xform/audioTrack.ts index 498fed35..96a39111 100644 --- a/src/smo/xform/audioTrack.ts +++ b/src/smo/xform/audioTrack.ts @@ -10,6 +10,7 @@ import { SmoNote } from '../data/note'; import { Pitch } from '../data/common'; import { SmoSystemStaff } from '../data/systemStaff'; import { SmoAudioPitch } from '../data/music'; +import { SmoTupletTree } from '../data/tuplet'; export interface SmoAudioRepeat { startRepeat: number, @@ -523,7 +524,7 @@ export class SmoAudioScore { // update staff features of slur/tie/cresc. this.getSlurInfo(track, selection); this.getHairpinInfo(track, selection); - const tuplet = measure.getTupletForNoteIndex(voiceIx, noteIx); + const tuplet = SmoTupletTree.getTupletForNoteIndex(measure.tupletTrees, voiceIx, noteIx); if (tuplet && tuplet.startIndex === noteIx) { tupletTicks = tuplet.tickCount / this.timeDiv; } diff --git a/src/smo/xform/copypaste.ts b/src/smo/xform/copypaste.ts index 891508d6..950f76f8 100644 --- a/src/smo/xform/copypaste.ts +++ b/src/smo/xform/copypaste.ts @@ -4,13 +4,14 @@ import { SmoSelection, SmoSelector } from './selections'; import { SmoNote, TupletInfo } from '../data/note'; import { SmoMeasure, SmoVoice } from '../data/measure'; import { StaffModifierBase } from '../data/staffModifiers'; -import { SmoTuplet } from '../data/tuplet'; +import {SmoTuplet, SmoTupletTree, SmoTupletTreeParams} from '../data/tuplet'; import { SmoMusic } from '../data/music'; import { SvgHelpers } from '../../render/sui/svgHelpers'; import { SmoScore } from '../data/score'; import { TickMap } from './tickMap'; import { SmoSystemStaff } from '../data/systemStaff'; import { getId } from '../data/common'; +import {SmoUnmakeTupletActor} from "./tickDuration"; /** * Used to calculate the offset and transposition of a note to be pasted @@ -18,8 +19,10 @@ import { getId } from '../data/common'; export interface PasteNote { note: SmoNote, selector: SmoSelector, - originalKey: string + originalKey: string, + tupletStart: SmoTupletTree | null } + /** * Used when pasting staff modifiers like slurs to calculate the * offset @@ -36,19 +39,20 @@ export interface ModifierPlacement { */ export class PasteBuffer { notes: PasteNote[]; + totalDuration: number; noteIndex: number; measures: SmoMeasure[]; measureIndex: number; remainder: number; replacementMeasures: SmoSelection[]; score: SmoScore | null = null; - tupletNoteMap: Record = { }; modifiers: StaffModifierBase[] = []; modifiersToPlace: ModifierPlacement[] = []; destination: SmoSelector = SmoSelector.default; staffSelectors: SmoSelector[] = []; constructor() { this.notes = []; + this.totalDuration = 0; this.noteIndex = 0; this.measures = []; this.measureIndex = -1; @@ -60,77 +64,76 @@ export class PasteBuffer { this.score = score; } setSelections(score: SmoScore, selections: SmoSelection[]) { - // this.notes = []; - // this.noteIndex = 0; - // this.score = score; - // if (selections.length < 1) { - // return; - // } - // this.tupletNoteMap = {}; - // const first = selections[0]; - // const last = selections[selections.length - 1]; - // if (!first.note || !last.note) { - // return; - // } - - // const startTuplet: SmoTuplet | null = first.measure.getTupletForNote(first.note); - // if (startTuplet) { - // if (startTuplet.getIndexOfNote(first.note) !== 0) { - // return; // can't paste from the middle of a tuplet - // } - // } - // const endTuplet: SmoTuplet | null = last.measure.getTupletForNote(last.note); - // if (endTuplet) { - // if (endTuplet.getIndexOfNote(last.note) !== endTuplet.notes.length - 1) { - // return; // can't paste part of a tuplet. - // } - // } - // this._populateSelectArray(selections); + this.notes = []; + this.noteIndex = 0; + this.score = score; + if (selections.length < 1) { + return; + } + // this.tupletNoteMap = []; + const first = selections[0]; + const last = selections[selections.length - 1]; + if (!first.note || !last.note) { + return; + } + const startTupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(first.measure.tupletTrees, first.selector.voice, first.selector.tick); + if (startTupletTree) { + if (startTupletTree.startIndex !== first.selector.tick) { + return; // can't copy from the middle of a tuplet + } + } + const endTupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(last.measure.tupletTrees, last.selector.voice, last.selector.tick); + if (endTupletTree) { + if (endTupletTree.endIndex !== last.selector.tick) { + return; // can't copy part of a tuplet. + } + } + this._populateSelectArray(selections); } // ### _populateSelectArray // copy the selected notes into the paste buffer with their original locations. _populateSelectArray(selections: SmoSelection[]) { - // let selector: SmoSelector = SmoSelector.default; - // this.modifiers = []; - // selections.forEach((selection) => { - // selector = JSON.parse(JSON.stringify(selection.selector)); - // const mod: StaffModifierBase[] = selection.staff.getModifiersAt(selector); - // if (mod.length) { - // mod.forEach((modifier: StaffModifierBase) => { - // const cp: StaffModifierBase = StaffModifierBase.deserialize(modifier.serialize()); - // cp.attrs.id = getId().toString(); - // this.modifiers.push(cp); - // }); - // } - // const isTuplet: boolean = selection?.note?.isTuplet ?? false; - // // We store copy in concert pitch. The originalKey is the original key of the copy. - // // the destKey is the originalKey in concert pitch. - // const originalKey = selection.measure.keySignature; - // const keyOffset = -1 * selection.measure.transposeIndex; - // const destKey = SmoMusic.vexKeySignatureTranspose(originalKey, keyOffset).toLocaleLowerCase(); - // if (isTuplet) { - // const tuplet = (selection.measure.getTupletForNote(selection.note) as SmoTuplet); - // const index = tuplet.getIndexOfNote(selection.note); - // if (index === 0) { - // const ntuplet = SmoTuplet.cloneTuplet(tuplet); - // this.tupletNoteMap[ntuplet.attrs.id] = ntuplet; - // ntuplet.notes.forEach((nnote) => { - // const xposeNote = SmoNote.transpose(SmoNote.clone(nnote), - // [], -1 * selection.measure.transposeIndex, selection.measure.keySignature, destKey) as SmoNote; - // this.notes.push({ selector, note: xposeNote, originalKey: destKey }); - // selector = JSON.parse(JSON.stringify(selector)); - // selector.tick += 1; - // }); - // } - // } else if (selection.note) { - // const note = SmoNote.transpose(SmoNote.clone(selection.note), - // [], keyOffset, selection.measure.keySignature, destKey) as SmoNote; - // this.notes.push({ selector, note, originalKey: destKey }); - // } - // }); - // this.notes.sort((a, b) => - // SmoSelector.gt(a.selector, b.selector) ? 1 : -1 - // ); + let selector: SmoSelector = SmoSelector.default; + this.modifiers = []; + selections.forEach((selection) => { + selector = JSON.parse(JSON.stringify(selection.selector)); + const mod: StaffModifierBase[] = selection.staff.getModifiersAt(selector); + if (mod.length) { + mod.forEach((modifier: StaffModifierBase) => { + const cp: StaffModifierBase = StaffModifierBase.deserialize(modifier.serialize()); + cp.attrs.id = getId().toString(); + this.modifiers.push(cp); + }); + } + + if (selection.note) { + // We store copy in concert pitch. The originalKey is the original key of the copy. + // the destKey is the originalKey in concert pitch. + const originalKey = selection.measure.keySignature; + const keyOffset = -1 * selection.measure.transposeIndex; + const destKey = SmoMusic.vexKeySignatureTranspose(originalKey, keyOffset).toLocaleLowerCase(); + const note = SmoNote.transpose(SmoNote.clone(selection.note),[], keyOffset, selection.measure.keySignature, destKey) as SmoNote; + const pasteNote: PasteNote = { + selector, + note, + originalKey: destKey, + tupletStart: null + }; + if (selection.note.isTuplet) { + const tupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(selection.measure.tupletTrees, selection.selector.voice, selection.selector.tick); + //const index = tuplet.getIndexOfNote(selection.note); + if (tupletTree && tupletTree.startIndex === selection.selector.tick) { + pasteNote.tupletStart = SmoTupletTree.clone(tupletTree); + } + } + + this.notes.push(pasteNote); + this.totalDuration += note.tickCount; + } + }); + this.notes.sort((a, b) => + SmoSelector.gt(a.selector, b.selector) ? 1 : -1 + ); } clearSelections() { @@ -146,45 +149,88 @@ export class PasteBuffer { return (typeof(rv) !== 'undefined' && rv.length) ? rv[0] : null; } - // ### _populateMeasureArray + _alignVoices(measure: SmoMeasure, voiceIndex: number) { + while (measure.voices.length <= voiceIndex) { + measure.populateVoice(measure.voices.length); + } + } + // Before pasting, populate an array of existing measures from the paste destination // so we know how to place the notes. - _populateMeasureArray() { - if (!this.score || !this.destination) { - return; - } - let measureSelection = SmoSelection.measureSelection(this.score, this.destination.staff, this.destination.measure); + _populateMeasureArray(selector: SmoSelector) { + let measureSelection = SmoSelection.measureSelection(this.score!, selector.staff, selector.measure); if (!measureSelection) { return; } const measure = measureSelection.measure; - while (measure.voices.length <= this.destination.voice) { - measure.populateVoice(measure.voices.length); - } - const tickmap = measure.tickmapForVoice(this.destination.voice); - let currentDuration = tickmap.durationMap[this.destination.tick]; + this._alignVoices(measure, selector.voice); this.measures = []; this.staffSelectors = []; - this.measures.push(measure); - this.notes.forEach((selection: PasteNote) => { - if (currentDuration + selection.note.tickCount > tickmap.totalDuration && measureSelection !== null) { + const clonedMeasure = SmoMeasure.clone(measureSelection.measure); + clonedMeasure.svg = measureSelection.measure.svg; + this.measures.push(clonedMeasure); + + const firstMeasure = this.measures[0]; + const tickmapForFirstMeasure = firstMeasure.tickmapForVoice(selector.voice); + + let currentDuration = tickmapForFirstMeasure.durationMap[selector.tick]; + const measureTotalDuration = tickmapForFirstMeasure.totalDuration; + for (let i: number = 0; i < this.notes.length; i++) { + const selection: PasteNote = this.notes[i]; + if (selection.tupletStart) { + // const tupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(this.tupletNoteMap, selection.selector.voice, selection.selector.tick); + if (currentDuration + selection.tupletStart.totalTicks > measureTotalDuration && measureSelection !== null) { + //if tuplet does not fit in a measure as a whole we cannot paste it, it is ether the whole thing or nothing + //reset everything that has been changed so far and return + this.measures = []; + this.staffSelectors = []; + return; + } + } + if (currentDuration + selection.note.tickCount > measureTotalDuration && measureSelection !== null) { // If this note will overlap the measure boundary, the note will be split in 2 with the // remainder going to the next measure. If they line up exactly, the remainder is 0. - const remainder = (currentDuration + selection.note.tickCount) - tickmap.totalDuration; + const remainder = (currentDuration + selection.note.tickCount) - measureTotalDuration; currentDuration = remainder; - measureSelection = SmoSelection.measureSelection(this.score as SmoScore, - measureSelection.selector.staff, - measureSelection.selector.measure + 1); + measureSelection = SmoSelection.measureSelection(this.score as SmoScore, measureSelection.selector.staff,measureSelection.selector.measure + 1); // If the paste buffer overlaps the end of the score, we can't paste (TODO: add a measure in this case) if (measureSelection != null) { - this.measures.push(measureSelection.measure); + const clonedMeasure = SmoMeasure.clone(measureSelection.measure); + clonedMeasure.svg = measureSelection.measure.svg; + this.measures.push(clonedMeasure); + // firstMeasureTickmap = measureSelection.measure.tickmapForVoice(selector.voice); } } else if (measureSelection != null) { currentDuration += selection.note.tickCount; } - }); + } + + const lastMeasure = this.measures[this.measures.length - 1]; + + //adjust the beginning of the paste + //adjust this.destination if beginning of the paste is in the middle of a tuplet + //set destination to have a tick index of the first note in the tuplet + this.destination = selector; + const firstTupletTree = SmoTupletTree.getTupletForNoteIndex(firstMeasure.tupletTrees, selector.voice, selector.tick); + if (firstTupletTree) { + this.destination.tick = firstTupletTree.startIndex;//use this as a new selector.tick + } + + if (this.measures.length > 1) { + this._removeOverlappingTuplets(firstMeasure, selector.tick, firstMeasure.voices[selector.voice].notes.length - 1, selector.voice); + this._removeOverlappingTuplets(lastMeasure, 0, lastMeasure.getClosestIndexFromTickCount(selector.voice, currentDuration), selector.voice); + } else { + this._removeOverlappingTuplets(firstMeasure, selector.tick, lastMeasure.getClosestIndexFromTickCount(selector.voice, currentDuration), selector.voice); + } + + //if there are more than 2 measures remove tuplets from all but first and last measure. + if (this.measures.length > 2) { + for(let i = 1; i < this.measures.length - 2; i++) { + this.measures[i].tupletTrees = []; + } + } } // ### _populatePre @@ -193,55 +239,28 @@ export class PasteBuffer { const voice: SmoVoice = { notes: [] }; - let i = 0; - let j = 0; - let ticksToFill = tickmap.durationMap[startTick]; - // TODO: bug here, need to handle tuplets in pre-part, create new tuplet - // for (i = 0; i < measure.voices[voiceIndex].notes.length; ++i) { - // const note = measure.voices[voiceIndex].notes[i]; - // // If this is a tuplet, clone all the notes at once. - // if (note.isTuplet && ticksToFill >= note.tickCount) { - // const tuplet = measure.getTupletForNote(note); - // if (!tuplet) { - // continue; // we remove the tuplet after first iteration - // } - // const ntuplet: SmoTuplet = SmoTuplet.cloneTuplet(tuplet); - // voice.notes = voice.notes.concat(ntuplet.notes as SmoNote[]); - // measure.removeTupletForNote(note); - // measure.tuplets.push(ntuplet); - // ticksToFill -= tuplet.tickCount; - // } else if (ticksToFill >= note.tickCount) { - // ticksToFill -= note.tickCount; - // voice.notes.push(SmoNote.clone(note)); - // } else { - // const duration = note.tickCount - ticksToFill; - // const durMap = SmoMusic.gcdMap(duration); - // for (j = 0; j < durMap.length; ++j) { - // const dd = durMap[j]; - // SmoNote.cloneWithDuration(note, { - // numerator: dd, - // denominator: 1, - // remainder: 0 - // }); - // } - // ticksToFill = 0; - // } - // if (ticksToFill < 1) { - // break; - // } - // } + + for (let i = 0; i < startTick; i++) { + const note = measure.voices[voiceIndex].notes[i]; + voice.notes.push(SmoNote.clone(note)); + } + return voice; } + /** + * + * @param voiceIndex + */ // ### _populateVoice // ### Description: // Create a new voice for a new measure in the paste destination - _populateVoice(voiceIndex: number): SmoVoice[] { - this._populateMeasureArray(); + _populateVoice(): SmoVoice[] { + // this._populateMeasureArray(); const measures = this.measures; let measure = measures[0]; let tickmap = measure.tickmapForVoice(this.destination.voice); - let voice = this._populatePre(voiceIndex, measure, this.destination.tick, tickmap); + let voice = this._populatePre(this.destination.voice, measure, this.destination.tick, tickmap); let startSelector = JSON.parse(JSON.stringify(this.destination)); this.measureIndex = 0; const measureVoices = []; @@ -262,7 +281,7 @@ export class PasteBuffer { startSelector = { staff: startSelector.staff, measure: startSelector.measure, - voice: voiceIndex, + voice: this.destination.voice, tick: 0 }; this.measureIndex += 1; @@ -271,7 +290,7 @@ export class PasteBuffer { break; } } - this._populatePost(voice, voiceIndex, measure, tickmap); + this._populatePost(voice, this.destination.voice, measure, tickmap); return measureVoices; } @@ -307,21 +326,39 @@ export class PasteBuffer { }); } } + /** - * Figure out if the tuplet overlaps an existing tuplet in the target measure - * @param t1 - * @param measure - * @returns + * + * @param measure + * @param startIndex + * @param endIndex + * @param voiceIndex + * @private */ - static tupletOverlapIndex(t1: SmoTuplet, measure: SmoMeasure) { - // for (var i = 0; i < measure.tuplets.length; ++i) { - // const tt = measure.tuplets[i]; - // // TODO: what about other kinds of overlap? - // if (tt.startIndex === t1.startIndex) { - // return i; - // } - // } - return -1; + private _removeOverlappingTuplets(measure: SmoMeasure, startIndex: number, endIndex: number, voiceIndex: number): void { + const tupletsToDelete: SmoTupletTree[] = []; + for (let i = 0; i < measure.tupletTrees.length; ++i) { + const tupletTree = measure.tupletTrees[i]; + if (startIndex >= tupletTree.startIndex && startIndex <= tupletTree.endIndex) { + tupletsToDelete.push(tupletTree); + break; + } + if (endIndex >= tupletTree.startIndex && endIndex <= tupletTree.endIndex) { + tupletsToDelete.push(tupletTree); + break; + } + } + + //todo: check if we need to remove tuplets in descending order + for (let i: number = 0; i < tupletsToDelete.length; i++) { + const tupletTree: SmoTupletTree = tupletsToDelete[i]; + SmoUnmakeTupletActor.apply({ + startIndex: tupletTree.startIndex, + endIndex: tupletTree.endIndex, + measure: measure, + voice: voiceIndex + }); + } } /** * Start copying the paste buffer into the destination by copying the notes and working out @@ -339,122 +376,112 @@ export class PasteBuffer { let j = 0; let tupletsPushed = 0; const totalDuration = tickmap.totalDuration; - // while (currentDuration < totalDuration && this.noteIndex < this.notes.length) { - // if (!this.score) { - // return; - // } - // const selection = this.notes[this.noteIndex]; - // const note = selection.note; - // if (note.noteType === 'n') { - // const pitchAr: number[] = []; - // note.pitches.forEach((pitch, ix) => { - // pitchAr.push(ix); - // }); - // SmoNote.transpose(note, pitchAr, measure.transposeIndex, selection.originalKey, measure.keySignature); - // } - // this._populateModifier(selection.selector, startSelector, this.score.staves[selection.selector.staff]); - // if (note.isTuplet) { - // const tuplet = this.tupletNoteMap[(note.tuplet as TupletInfo).id]; - // const ntuplet = SmoTuplet.cloneTuplet(tuplet); - // ntuplet.startIndex = voice.notes.length; - // this.noteIndex += ntuplet.notes.length; - // startSelector.tick += ntuplet.notes.length; - // currentDuration += tuplet.tickCount; - // for (i = 0; i < ntuplet.notes.length; ++i) { - // const tn = ntuplet.notes[i]; - // tn.clef = measure.clef; - // voice.notes.push(tn); - // } - // const tix = PasteBuffer.tupletOverlapIndex(ntuplet, measure); - // // If this is overlapping an existing tuplet in the target measure, replace it - // if (tix >= 0) { - // measure.tuplets[tix] = ntuplet; - // } else { - // measure.tuplets.push(ntuplet); - // } - // } else if (currentDuration + note.tickCount <= totalDuration && this.remainder === 0) { - // // The whole note fits in the measure, paste it. - // const nnote = SmoNote.clone(note); - // nnote.clef = measure.clef; - // voice.notes.push(nnote); - // currentDuration += note.tickCount; - // this.noteIndex += 1; - // startSelector.tick += 1; - // } else if (this.remainder > 0) { - // // This is a note that spilled over the last measure - // const nnote = SmoNote.cloneWithDuration(note, { - // numerator: this.remainder, - // denominator: 1, - // remainder: 0 - // }); - // nnote.clef = measure.clef; - // voice.notes.push(nnote); - // currentDuration += this.remainder; - // this.remainder = 0; - // } else { - // // The note won't fit, so we split it in 2 and paste the remainder in the next measure. - // // TODO: tie the last note to this one. - // const partial = totalDuration - currentDuration; - // const dar = SmoMusic.gcdMap(partial); - // for (j = 0; j < dar.length; ++j) { - // const ddd = dar[j]; - // const vnote = SmoNote.cloneWithDuration(note, { - // numerator: ddd, - // denominator: 1, - // remainder: 0 - // }); - // voice.notes.push(vnote); - // } - // currentDuration += partial; - - // // Set the remaining length of the current note, this will be added to the - // // next measure with the previous note's pitches - // this.remainder = note.tickCount - partial; - // } - // } + while (currentDuration < totalDuration && this.noteIndex < this.notes.length) { + if (!this.score) { + return; + } + const selection: PasteNote = this.notes[this.noteIndex]; + const note: SmoNote = selection.note; + if (note.noteType === 'n') { + const pitchAr: number[] = []; + note.pitches.forEach((pitch, ix) => { + pitchAr.push(ix); + }); + SmoNote.transpose(note, pitchAr, measure.transposeIndex, selection.originalKey, measure.keySignature); + } + this._populateModifier(selection.selector, startSelector, this.score.staves[selection.selector.staff]); + + if (currentDuration + note.tickCount <= totalDuration && this.remainder === 0) { + // The whole note fits in the measure, paste it. + //If this note is a tuplet, and specifically if it is the beginning of a tuplet, we need to handle it + //NOTE: tuplets never cross measure boundary, we made sure this is handled here: @see this._populateMeasureArray() + if (selection.tupletStart) { + const tupletTree: SmoTupletTree = SmoTupletTree.clone(selection.tupletStart); + const startIndex: number = voice.notes.length; + const diff: number = startIndex - tupletTree.startIndex; + SmoTupletTree.adjustTupletIndexes([tupletTree], selection.selector.voice,-1, diff); + measure.tupletTrees.push(tupletTree); + } + + const nnote: SmoNote = SmoNote.clone(note); + nnote.clef = measure.clef; + voice.notes.push(nnote); + currentDuration += note.tickCount; + this.noteIndex += 1; + startSelector.tick += 1; + } else if (this.remainder > 0) { + // This is a note that spilled over the last measure + const nnote = SmoNote.cloneWithDuration(note, { + numerator: this.remainder, + denominator: 1, + remainder: 0 + }); + nnote.clef = measure.clef; + voice.notes.push(nnote); + currentDuration += this.remainder; + this.remainder = 0; + } else { + // The note won't fit, so we split it in 2 and paste the remainder in the next measure. + // TODO: tie the last note to this one. + const partial = totalDuration - currentDuration; + const dar = SmoMusic.gcdMap(partial); + for (j = 0; j < dar.length; ++j) { + const ddd = dar[j]; + const vnote = SmoNote.cloneWithDuration(note, { + numerator: ddd, + denominator: 1, + remainder: 0 + }); + voice.notes.push(vnote); + } + currentDuration += partial; + + // Set the remaining length of the current note, this will be added to the + // next measure with the previous note's pitches + this.remainder = note.tickCount - partial; + } + } } // ### _populatePost // When we paste, we replace entire measures. Populate the last measure from the end of paste to the // end of the measure with notes in the existing measure. _populatePost(voice: SmoVoice, voiceIndex: number, measure: SmoMeasure, tickmap: TickMap) { - let startTicks = PasteBuffer._countTicks(voice); - let existingIndex = 0; - const totalDuration = tickmap.totalDuration; - // while (startTicks < totalDuration) { - // // Find the point in the music where the paste area runs out, or as close as we can get. - // existingIndex = tickmap.durationMap.indexOf(startTicks); - // existingIndex = (existingIndex < 0) ? measure.voices[voiceIndex].notes.length - 1 : existingIndex; - // const note = measure.voices[voiceIndex].notes[existingIndex]; - // if (note.isTuplet) { - // const tuplet = measure.getTupletForNote(note) as SmoTuplet; - // const ntuplet = SmoTuplet.cloneTuplet(tuplet); - // startTicks += tuplet.tickCount; - // voice.notes = voice.notes.concat(ntuplet.notes); - // measure.tuplets.push(ntuplet); - // measure.removeTupletForNote(note); - // } else { - // const ticksLeft = totalDuration - startTicks; - // if (ticksLeft >= note.tickCount) { - // startTicks += note.tickCount; - // voice.notes.push(SmoNote.clone(note)); - // } else { - // const remainder = totalDuration - startTicks; - // voice.notes.push(SmoNote.cloneWithDuration(note, { - // numerator: remainder, - // denominator: 1, - // remainder: 0 - // })); - // startTicks = totalDuration; - // } - // } - // } + let endOfPasteDuration = PasteBuffer._countTicks(voice); + let existingIndex = measure.getClosestIndexFromTickCount(voiceIndex, endOfPasteDuration); + if (existingIndex > tickmap.durationMap.length - 1) { + return; + } + let existingDuration = tickmap.durationMap[existingIndex]; + let endOfExistingDuration = existingDuration + tickmap.deltaMap[existingIndex]; + + let startIndexToAdjustRemainingTuplets = voice.notes.length; + let diffToAdjustRemainingTuplets: number = startIndexToAdjustRemainingTuplets - existingIndex - 1; + + + if (Math.round(endOfPasteDuration) < Math.round(endOfExistingDuration)) { + //pasted notes ended somewhere in the middle of an existing note + //we need to remove the existing note and fill in the difference between the end of our pasted note and beginning of the next one + const note = measure.voices[voiceIndex].notes[existingIndex]; + const lmap = SmoMusic.gcdMap(endOfExistingDuration - endOfPasteDuration); + lmap.forEach((stemTick) => { + const nnote = SmoNote.cloneWithDuration(note, stemTick); + voice.notes.push(nnote); + }); + diffToAdjustRemainingTuplets += lmap.length; + existingIndex++; + } + SmoTupletTree.adjustTupletIndexes(measure.tupletTrees, voiceIndex, startIndexToAdjustRemainingTuplets, diffToAdjustRemainingTuplets); + + for (let i = existingIndex; i < measure.voices[voiceIndex].notes.length - 1; i++) { + voice.notes.push(SmoNote.clone(measure.voices[voiceIndex].notes[i])); + } } - _pasteVoiceSer(ser: any, vobj: any, voiceIx: number) { + _pasteVoiceSer(serializedMeasure: any, vobj: any, voiceIx: number) { const voices: any[] = []; - let ix = 0; - ser.voices.forEach((vc: any) => { + let ix = 0; + serializedMeasure.voices.forEach((vc: any) => { if (ix !== voiceIx) { voices.push(vc); } else { @@ -463,103 +490,107 @@ export class PasteBuffer { ix += 1; }); // If we are pasting into a measure that doesn't contain this voice, add the voice - if (ser.voices.length <= voiceIx) { + if (serializedMeasure.voices.length <= voiceIx) { voices.push(vobj); } - ser.voices = voices; + serializedMeasure.voices = voices; } pasteSelections(selector: SmoSelector) { - // let i = 0; - // if (this.notes.length < 1) { - // return; - // } - // const maxCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); - // const minCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); - // const backupNotes: PasteNote[] = []; - // this.notes.forEach((bb) => { - // const note = (SmoNote.deserialize(bb.note.serialize())); - // const selector = JSON.parse(JSON.stringify(bb.selector)); - // backupNotes.push({ note, selector, originalKey: bb.originalKey }); - // }); - // this.destination = selector; - // if (minCutVoice === maxCutVoice && minCutVoice > this.destination.voice) { - // this.destination.voice = minCutVoice; - - // } - // this.modifiersToPlace = []; - // if (this.notes.length < 1) { - // return; - // } - // if (!this.score) { - // return; - // } - // this.noteIndex = 0; - // this.measureIndex = -1; - // this.remainder = 0; - // const voices = this._populateVoice(this.destination.voice); - // const measureSel = JSON.parse(JSON.stringify(this.destination)); - // const selectors: SmoSelector[] = []; - // for (i = 0; i < this.measures.length && i < voices.length; ++i) { - // const measure: SmoMeasure = this.measures[i]; - // const nvoice: SmoVoice = voices[i]; - // const ser: any = measure.serialize(); - // // Make sure the key is concert pitch, it is what measure constructor expects - // ser.transposeIndex = measure.transposeIndex; // default values are undefined, make sure the transpose is valid - // ser.keySignature = SmoMusic.vexKeySigWithOffset(measure.keySignature, -1 * measure.transposeIndex); - // ser.timeSignature = measure.timeSignature.serialize(); - // ser.tempo = measure.tempo.serialize(); - // const vobj: any = { - // notes: [] - // }; - // nvoice.notes.forEach((note: SmoNote) => { - // vobj.notes.push(note.serialize()); - // }); - - // // TODO: figure out how to do this with multiple voices - // this._pasteVoiceSer(ser, vobj, this.destination.voice); - // const nmeasure = SmoMeasure.deserialize(ser); - // // If this is the non-display buffer, don't try to reset the display rectangles. - // // Q: Is this even required since we are going to re-render? - // // A: yes, because until we do, the replaced measure needs the formatting info - // if (measure.svg.logicalBox && measure.svg.logicalBox.width > 0) { - // nmeasure.setBox(SvgHelpers.smoBox(measure.svg.logicalBox), 'copypaste'); - // nmeasure.setX(measure.svg.logicalBox.x, 'copyPaste'); - // nmeasure.setWidth(measure.svg.logicalBox.width, 'copypaste'); - // nmeasure.setY(measure.svg.logicalBox.y, 'copypaste'); - // nmeasure.svg.element = measure.svg.element; - // } - // ['forceClef', 'forceKeySignature', 'forceTimeSignature', 'forceTempo'].forEach((flag) => { - // (nmeasure as any)[flag] = (measure.svg as any)[flag]; - // }); - // this.score.replaceMeasure(measureSel, nmeasure); - // measureSel.measure += 1; - // selectors.push( - // { staff: selector.staff, measure: nmeasure.measureNumber.measureIndex, voice: 0, tick: 0, pitches: [] } - // ); - // } - // this.replacementMeasures = []; - // selectors.forEach((selector: SmoSelector) => { - // const nsel: SmoSelection | null = SmoSelection.measureSelection(this.score as SmoScore, selector.staff, selector.measure); - // if (nsel) { - // this.replacementMeasures.push(nsel); - // } - // }); - // this.modifiersToPlace.forEach((mod) => { - // let selection = SmoSelection.selectionFromSelector(this.score!, mod.modifier.endSelector); - // while (selection && mod.ticksToStart !== 0) { - // if (mod.ticksToStart < 0) { - // selection = SmoSelection.nextNoteSelectionFromSelector(this.score!, selection.selector); - // } else { - // selection = SmoSelection.lastNoteSelectionFromSelector(this.score!, selection.selector); - // } - // mod.ticksToStart -= 1 * Math.sign(mod.ticksToStart); - // } - // if (selection) { - // mod.modifier.startSelector = JSON.parse(JSON.stringify(selection.selector)); - // selection.staff.addStaffModifier(mod.modifier); - // } - // }); - // this.notes = backupNotes; + let i = 0; + if (this.notes.length < 1) { + return; + } + if (!this.score) { + return; + } + const maxCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); + const minCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); + const backupNotes: PasteNote[] = []; + this.notes.forEach((bb) => { + const note = (SmoNote.deserialize(bb.note.serialize())); + const selector = JSON.parse(JSON.stringify(bb.selector)); + let tupletStart = bb.tupletStart; + if (tupletStart) { + tupletStart = SmoTupletTree.deserialize(bb.tupletStart!.serialize()); + } + backupNotes.push({ note, selector, originalKey: bb.originalKey, tupletStart }); + }); + if (minCutVoice === maxCutVoice && minCutVoice > selector.voice) { + selector.voice = minCutVoice; + } + this.modifiersToPlace = []; + this.noteIndex = 0; + this.measureIndex = -1; + this.remainder = 0; + this._populateMeasureArray(selector); + if (this.measures.length === 0) { + return; + } + + const voices = this._populateVoice(); + const measureSel = JSON.parse(JSON.stringify(this.destination)); + const selectors: SmoSelector[] = []; + for (i = 0; i < this.measures.length && i < voices.length; ++i) { + const measure: SmoMeasure = this.measures[i]; + const nvoice: SmoVoice = voices[i]; + const ser: any = measure.serialize(); + // Make sure the key is concert pitch, it is what measure constructor expects + ser.transposeIndex = measure.transposeIndex; // default values are undefined, make sure the transpose is valid + ser.keySignature = SmoMusic.vexKeySigWithOffset(measure.keySignature, -1 * measure.transposeIndex); + ser.timeSignature = measure.timeSignature.serialize(); + ser.tempo = measure.tempo.serialize(); + const vobj: any = { + notes: [] + }; + nvoice.notes.forEach((note: SmoNote) => { + vobj.notes.push(note.serialize()); + }); + + // TODO: figure out how to do this with multiple voices + this._pasteVoiceSer(ser, vobj, this.destination.voice); + const nmeasure = SmoMeasure.deserialize(ser); + // If this is the non-display buffer, don't try to reset the display rectangles. + // Q: Is this even required since we are going to re-render? + // A: yes, because until we do, the replaced measure needs the formatting info + if (measure.svg.logicalBox && measure.svg.logicalBox.width > 0) { + nmeasure.setBox(SvgHelpers.smoBox(measure.svg.logicalBox), 'copypaste'); + nmeasure.setX(measure.svg.logicalBox.x, 'copyPaste'); + nmeasure.setWidth(measure.svg.logicalBox.width, 'copypaste'); + nmeasure.setY(measure.svg.logicalBox.y, 'copypaste'); + nmeasure.svg.element = measure.svg.element; + } + ['forceClef', 'forceKeySignature', 'forceTimeSignature', 'forceTempo'].forEach((flag) => { + (nmeasure as any)[flag] = (measure.svg as any)[flag]; + }); + this.score.replaceMeasure(measureSel, nmeasure); + measureSel.measure += 1; + selectors.push( + { staff: selector.staff, measure: nmeasure.measureNumber.measureIndex, voice: 0, tick: 0, pitches: [] } + ); + } + this.replacementMeasures = []; + selectors.forEach((selector: SmoSelector) => { + const nsel: SmoSelection | null = SmoSelection.measureSelection(this.score as SmoScore, selector.staff, selector.measure); + if (nsel) { + this.replacementMeasures.push(nsel); + } + }); + this.modifiersToPlace.forEach((mod) => { + let selection = SmoSelection.selectionFromSelector(this.score!, mod.modifier.endSelector); + while (selection && mod.ticksToStart !== 0) { + if (mod.ticksToStart < 0) { + selection = SmoSelection.nextNoteSelectionFromSelector(this.score!, selection.selector); + } else { + selection = SmoSelection.lastNoteSelectionFromSelector(this.score!, selection.selector); + } + mod.ticksToStart -= 1 * Math.sign(mod.ticksToStart); + } + if (selection) { + mod.modifier.startSelector = JSON.parse(JSON.stringify(selection.selector)); + selection.staff.addStaffModifier(mod.modifier); + } + }); + this.notes = backupNotes; } } diff --git a/src/smo/xform/operations.ts b/src/smo/xform/operations.ts index 7427a6b3..d32f18ea 100644 --- a/src/smo/xform/operations.ts +++ b/src/smo/xform/operations.ts @@ -17,12 +17,9 @@ import { SmoStaffHairpin, SmoSlur, SmoTie, StaffModifierBase, SmoTieParams, SmoI import { SmoSystemGroup } from '../data/scoreModifiers'; import { SmoTextGroup } from '../data/scoreText'; import { SmoSelection, SmoSelector, ModifierTab } from './selections'; -import { - /*SmoDuration,*/ /*SmoContractNoteActor, SmoStretchNoteActor,*/ SmoMakeTupletActor, - /*SmoUnmakeTupletActor,*/ - SmoChangeDurationActor, /*SmoContractTupletActor*/ -} from './tickDuration'; +import { SmoContractNoteActor, SmoStretchNoteActor, SmoMakeTupletActor, SmoUnmakeTupletActor, SmoStretchNoteParams, SmoContractNoteParams, SmoMakeTupletParams} from './tickDuration'; import { SmoBeamer } from './beamers'; +import { SmoTupletTree } from '../data/tuplet'; /** * supported operations for {@link SmoOperation.batchSelectionOperation} to change a note's duration */ @@ -169,8 +166,15 @@ export class SmoOperation { // note, if possible. Works on tuplets also. static doubleDuration(selection: SmoSelection) { const note = selection.note; - const newDuration = note!.stemTicks * 2 - SmoChangeDurationActor.apply(selection, newDuration); + const newStemTicks = note!.stemTicks * 2; + + SmoStretchNoteActor.apply({ + startIndex: selection.selector.tick, + measure: selection.measure, + voice: selection.selector.voice, + newStemTicks: newStemTicks + }); + return true; } @@ -182,8 +186,14 @@ export class SmoOperation { const note = selection.note as SmoNote; let divisor = 2; const measure = selection.measure; - const nticks = note.stemTicks / divisor; - SmoChangeDurationActor.apply(selection, nticks); + const newStemTicks = note.stemTicks / divisor; + + SmoContractNoteActor.apply({ + startIndex: selection.selector.tick, + measure: measure, + voice: selection.selector.voice, + newStemTicks: newStemTicks + }); SmoBeamer.applyBeams(measure); return true; } @@ -192,7 +202,12 @@ export class SmoOperation { // ## Description // Makes a non-tuplet into a tuplet of equal value. static makeTuplet(selection: SmoSelection, numNotes: number) { - SmoMakeTupletActor.apply(selection, numNotes); + SmoMakeTupletActor.apply({ + measure: selection.measure, + numNotes: numNotes, + voice: selection.selector.voice, + index: selection.selector.tick + }); } static addStaffModifier(selection: SmoSelection, modifier: StaffModifierBase) { selection.staff.addStaffModifier(modifier); @@ -296,7 +311,21 @@ export class SmoOperation { // ## Description // Makes a tuplet into a single with the duration of the whole tuplet static unmakeTuplet(selection: SmoSelection) { - //todo Nenad: implement this + const selector = selection.selector; + const measure = selection.measure; + + const tuplets = SmoTupletTree.getTupletHierarchyForNoteIndex(measure.tupletTrees, selector.voice, selector.tick); + if (!tuplets.length) { + return; + } + const tuplet = tuplets[0]; + + SmoUnmakeTupletActor.apply({ + startIndex: tuplet.startIndex, + endIndex: tuplet.endIndex, + measure: measure, + voice: selector.voice + }); } // ## dotDuration @@ -306,14 +335,14 @@ export class SmoOperation { static dotDuration(selection: SmoSelection) { const note = selection.note as SmoNote; const measure = selection.measure; - const nticks = SmoMusic.getNextDottedLevel(note.stemTicks); - if (nticks === note.stemTicks) { + const newStemTicks = SmoMusic.getNextDottedLevel(note.stemTicks); + if (newStemTicks === note.stemTicks) { return; } // Don't dot if the thing on the right of the . is too small - const dotCount = SmoMusic.smoTicksToVexDots(nticks); + const dotCount = SmoMusic.smoTicksToVexDots(newStemTicks); const multiplier = Math.pow(2, dotCount); - const baseDot = SmoMusic.closestDurationTickLtEq(nticks) / (multiplier * 2); + const baseDot = SmoMusic.closestDurationTickLtEq(newStemTicks) / (multiplier * 2); if (baseDot <= 128) { return; } @@ -329,7 +358,13 @@ export class SmoOperation { if (!SmoMusic.validDurations[selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].tickCount / 2]) { return; } - SmoChangeDurationActor.apply(selection, nticks); + + SmoStretchNoteActor.apply({ + startIndex: selection.selector.tick, + measure: measure, + voice: selection.selector.voice, + newStemTicks: newStemTicks + }); } // ## undotDuration @@ -339,11 +374,17 @@ export class SmoOperation { static undotDuration(selection: SmoSelection) { const note = selection.note as SmoNote; const measure = selection.measure; - const nticks = SmoMusic.getPreviousDottedLevel(note.stemTicks); - if (nticks === note.stemTicks) { + const newStemTicks = SmoMusic.getPreviousDottedLevel(note.stemTicks); + if (newStemTicks === note.stemTicks) { return; } - SmoChangeDurationActor.apply(selection, nticks); + + SmoContractNoteActor.apply({ + startIndex: selection.selector.tick, + measure: measure, + voice: selection.selector.voice, + newStemTicks: newStemTicks + }); } static transposeScore(score: SmoScore, offset: number) { diff --git a/src/smo/xform/tickDuration.ts b/src/smo/xform/tickDuration.ts index 0fa92b51..0928d26d 100644 --- a/src/smo/xform/tickDuration.ts +++ b/src/smo/xform/tickDuration.ts @@ -1,9 +1,8 @@ // [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. import { SmoNote, TupletInfo } from '../data/note'; -import { SmoTuplet } from '../data/tuplet'; +import { SmoTuplet, SmoTupletTree } from '../data/tuplet'; import { SmoMusic } from '../data/music'; -import { SmoSelector, SmoSelection } from './selections'; import { SmoMeasure, SmoVoice } from '../data/measure'; import { Ticks } from '../data/common'; import { TickMap } from './tickMap'; @@ -22,246 +21,388 @@ export abstract class TickIteratorBase { } } -export class SmoMakeTupletActor { - private note: SmoNote; - private measure: SmoMeasure; - private selector: SmoSelector; - private voices: SmoVoice[]; - private voice: SmoVoice; - - private totalTicks: number; - private parentTuplet: SmoTuplet | null; - private numNotes: number; - private notesOccupied: number; - private tupletNotes: SmoNote[] = []; - private stemTicks: number; - private tuplet: SmoTuplet; - - constructor(selection: SmoSelection, numNotes: number) { - this.note = selection.note!; - this.measure = selection.measure; - - this.selector = selection.selector; - this.voices = this.measure.voices; - this.voice = this.voices[this.selector.voice]; - this.numNotes = numNotes; - - this.totalTicks = this.note.tickCount; - this.parentTuplet = this.measure.getTupletForNoteIndex(this.selector.voice, this.selector.tick); - - this.stemTicks = SmoTuplet.calculateStemTicks(this.note.stemTicks, this.numNotes); - this.notesOccupied = this.note.stemTicks / this.stemTicks; - - this.tuplet = new SmoTuplet({ - stemTicks: this.stemTicks, - totalTicks: this.totalTicks, - ratioed: false, - bracketed: true, - voice: this.selector.voice, - numNotes: this.numNotes, - notesOccupied: this.notesOccupied, - startIndex: this.selector.tick, - endIndex: this.selector.tick - }); - +/** + * SmoTickIterator + * this is a local helper class that follows a pattern of iterating of the notes. Most of the + * duration changers iterate over a selection, and return: + * - A note, if the duration changes + * - An array of notes, if the notes split + * - null if the note stays the same + * - empty array, remove the note from the group + * @category SmoTransform + */ +export class SmoTickIterator { + notes: SmoNote[] = []; + newNotes: SmoNote[] = []; + actor: TickIteratorBase; + measure: SmoMeasure; + voice: number = 0; + keySignature: string; + constructor(measure: SmoMeasure, actor: TickIteratorBase, voiceIndex: number) { + this.notes = measure.voices[voiceIndex].notes; + this.measure = measure; + this.voice = typeof (voiceIndex) === 'number' ? voiceIndex : 0; + this.newNotes = []; + // eslint-disable-next-line + this.actor = actor; + this.keySignature = 'C'; } - - static apply(selection: SmoSelection, numNotes: number) { - if (!selection?.note) { - return; - } - const actor = new SmoMakeTupletActor(selection, numNotes); - actor.initialize(); + static nullActor(note: SmoNote) { + return note; } - - public initialize() { - this.measure.clearBeamGroups(); - this.tupletNotes = this._generateTupletNotes(); - this.tuplet.endIndex += this.tupletNotes.length - 1; - this.measure.adjustTupletIndexes(this.selector.tick, this.tupletNotes.length - 1); - - if (this.parentTuplet !== null) { - this.parentTuplet.childrenTuplets.push(this.tuplet); - this.tuplet.parentTuplet = { id: this.parentTuplet.attrs.id }; - // const index = this.parentTuplet.getIndexOfNote(this.note); - // this.parentTuplet.tickables.splice(index, 1, this.tuplet); - } else { - this.measure.tupletTrees.push(this.tuplet); + /** + * + * @param measure {SmoMeasure} + * @param actor {} + * @param voiceIndex + */ + static iterateOverTicks(measure: SmoMeasure, actor: TickIteratorBase, voiceIndex: number) { + measure.clearBeamGroups(); + const transformer = new SmoTickIterator(measure, actor, voiceIndex); + transformer.run(); + measure.voices[voiceIndex].notes = transformer.notes; + } + // ### transformNote + // call the actors for each note, and put the result in the note array. + // The note from the original array is copied and sent to each actor. + // + // Because the resulting array can have a different number of notes than the existing + // array, the actors communicate with the transformer in the following, jquery-ish + // but somewhat unintuitive way: + // + // 1. if the actor returns null, the next actor is called and the results of that actor are used + // 2. if all the actors return null, the copy is used. + // 3. if a note object is returned, that is used for the current tick and no more actors are called. + // 4. if an array of notes is returned, it is concatenated to the existing note array and no more actors are called. + // Note that *return note;* and *return [note];* produce the same result. + // 5. if an empty array [] is returned, that copy is not added to the result. The note is effectively deleted. + iterateOverTick(tickmap: TickMap, index: number, note: SmoNote) { + const actor: TickIteratorBase = this.actor; + const newNote: SmoNote[] | SmoNote | null = actor.iterateOverTick(note, tickmap, index); + if (newNote === null) { + this.newNotes.push(note); // no change + return note; } - - this.voice.notes.splice(this.selector.tick, 1, ...this.tupletNotes); - console.log(this.voice); + if (Array.isArray(newNote)) { + if (newNote.length === 0) { + return null; + } + this.newNotes = this.newNotes.concat(newNote); + return null; + } + this.newNotes.push(newNote as SmoNote); + return null; } - private _generateTupletNotes(): SmoNote[] { - const tupletNotes: SmoNote[] = []; - for (let i = 0; i < this.numNotes; ++i) { - const numerator = this.totalTicks / this.numNotes; - const note: SmoNote = SmoNote.cloneWithDuration(this.note, { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }, this.stemTicks); - // Don't clone modifiers, except for first one. - note.textModifiers = i === 0 ? note.textModifiers : []; - note.tuplet = this.tuplet.attrs; - tupletNotes.push(note); + run() { + let i = 0; + const tickmap = this.measure.tickmapForVoice(this.voice); + for (i = 0; i < tickmap.durationMap.length; ++i) { + this.iterateOverTick(tickmap, i, this.measure.voices[this.voice].notes[i]); } - - return tupletNotes; + this.notes = this.newNotes; + return this.newNotes; } } - -export class SmoChangeDurationActor { - private newStemTicks: number; - private note: SmoNote; - private measure: SmoMeasure; - private selector: SmoSelector; - private voices: SmoVoice[]; - private voice: SmoVoice; - private notes: SmoNote[]; - - constructor(selection: SmoSelection, newStemTicks: number) { - this.newStemTicks = newStemTicks; - this.note = selection.note!; - this.measure = selection.measure; - this.selector = selection.selector; - this.voices = this.measure.voices; - this.voice = this.voices[this.selector.voice]; - this.notes = this.voice.notes; +/** + * used to create a contract/dilate operation on a note via {@link SmoContractNoteActor} + * @category SmoTransform + */ +export interface SmoContractNoteParams { + startIndex: number, + measure: SmoMeasure, + voice: number, + newStemTicks: number +} +/** + * Contract the duration of a note, filling in the space with another note + * or rest. + * @category SmoTransform + * */ +export class SmoContractNoteActor extends TickIteratorBase { + startIndex: number; + newStemTicks: number; + measure: SmoMeasure; + voice: number; + constructor(params: SmoContractNoteParams) { + super(); + this.startIndex = params.startIndex; + this.measure = params.measure; + this.voice = params.voice; + this.newStemTicks = params.newStemTicks; } - - static apply(selection: SmoSelection, newDuration: number) { - if (!selection?.note) { - return; - } - const actor = new SmoChangeDurationActor(selection, newDuration); - actor.initialize(); + static apply(params: SmoContractNoteParams) { + const actor = new SmoContractNoteActor(params); + SmoTickIterator.iterateOverTicks(actor.measure, + actor, actor.voice); } + iterateOverTick(note: SmoNote, tickmap: TickMap, index: number): SmoNote | SmoNote[] | null { + if (index === this.startIndex) { + let newTicks: Ticks = { numerator: this.newStemTicks, denominator: 1, remainder: 0 }; + const multiplier = note.tickCount / note.stemTicks; + + if (note.isTuplet) { + const numerator = this.newStemTicks * multiplier; + newTicks = { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }; + } + + const replacingNote = SmoNote.cloneWithDuration(note, newTicks, this.newStemTicks); + const oldStemTicks = note.stemTicks; + const notes = []; + const remainderStemTicks = oldStemTicks - this.newStemTicks; + + notes.push(replacingNote); + + if (remainderStemTicks > 0) { + if (remainderStemTicks < 128) { + return null; + } + const lmap = SmoMusic.gcdMap(remainderStemTicks); + lmap.forEach((stemTick) => { + const nnote = SmoNote.cloneWithDuration(note, stemTick * multiplier, stemTick); + notes.push(nnote); + }); + } - public initialize() { - this.measure.clearBeamGroups(); - if (this.newStemTicks > this.note.stemTicks) { - //if new stemTicks is crossing tuplet boundary, - //clear tuplets that are in the way and only then proceed to actually stretch the note - //todo: clearTuplets(); - this.stretchNote(); - } else if (this.newStemTicks < this.note.stemTicks) { - this.contractNote(); - } else { - return; + SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, index, notes.length - 1); + return notes; } + return null; } +} - /** - * todo: handle ticks reminder - * Expand the note and absorb all notes to the right that fall within the new duration. - * If the duration of the last note does not completely fit within the new duration, fill in the remainder with new notes. - */ - private stretchNote() { - let i = 0; - +/** + * Constructor when we want to double or dot the duration of a note (stretch) + * for {@link SmoStretchNoteActor} + * @param startIndex tick index into the measure + * @param measure the container measure + * @param voice the voice index + * @param newTicks the ticks the new note will take up + * @category SmoTransform + */ +export interface SmoStretchNoteParams { + startIndex: number, + measure: SmoMeasure, + voice: number, + newStemTicks: number +} +/** + * increase the length of a note, removing future notes in the measure as required + * @category SmoTransform + */ +export class SmoStretchNoteActor extends TickIteratorBase { + startIndex: number; + newStemTicks: number; + measure: SmoMeasure; + voice: number; + notes: SmoNote[]; + notesToInsert: SmoNote[] = []; + numberOfNotesToDelete: number = 0; + constructor(params: SmoStretchNoteParams) { + super(); + this.startIndex = params.startIndex; + this.measure = params.measure; + this.voice = params.voice; + this.newStemTicks = params.newStemTicks; + this.notes = this.measure.voices[this.voice].notes; + + const originalNote: SmoNote = this.notes[this.startIndex]; let newTicks: Ticks = { numerator: this.newStemTicks, denominator: 1, remainder: 0 }; - - const multiplier = this.note.tickCount / this.note.stemTicks; - - if (this.note.isTuplet) { + const multiplier = originalNote.tickCount / originalNote.stemTicks; + if (originalNote.isTuplet) { const numerator = this.newStemTicks * multiplier; newTicks = { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }; } - const replacingNote = SmoNote.cloneWithDuration(this.note, newTicks, this.newStemTicks); + const replacingNote = SmoNote.cloneWithDuration(originalNote, newTicks, this.newStemTicks); - let stemTicksUsed = this.note.stemTicks; - const allNotes = []; - // const newNotes = []; - for (i = 0; i < this.selector.tick; ++i) { - allNotes.push(this.notes[i]); - } - for (i = this.selector.tick + 1; i < this.notes.length; ++i) { + let stemTicksUsed = originalNote.stemTicks; + for (let i = this.startIndex + 1; i < this.notes.length; ++i) { const nnote = this.notes[i]; //in case notes are part of the tuplet they need to belong to the same tuplet //this check is only temporarely here, it should never come to this - if (nnote.isTuplet && !this.areNotesInSameTuplet(this.note, nnote)) { + if (nnote.isTuplet && !this.areNotesInSameTuplet(originalNote, nnote)) { break; } stemTicksUsed += nnote.stemTicks; + ++this.numberOfNotesToDelete; if (stemTicksUsed >= this.newStemTicks) { break; } } const remainder = stemTicksUsed - this.newStemTicks; - if (remainder < 0) { - return; - } - - allNotes.push(replacingNote); - // newNotes.push(replacingNote); - - if (remainder > 0) { + if (remainder >= 0) { + this.notesToInsert.push(replacingNote); const lmap = SmoMusic.gcdMap(remainder); lmap.forEach((stemTick) => { - const nnote = SmoNote.cloneWithDuration(this.note, stemTick * multiplier, stemTick) - allNotes.push(nnote); - // newNotes.push(nnote); + const nnote = SmoNote.cloneWithDuration(originalNote, stemTick * multiplier, stemTick) + this.notesToInsert.push(nnote); }); + const noteCountDiff = (this.notesToInsert.length - this.numberOfNotesToDelete) - 1; + SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, this.startIndex, noteCountDiff); } - - for (i = i + 1 ; i < this.notes.length; ++i) { - allNotes.push(this.notes[i]); - } + } + static apply(params: SmoStretchNoteParams) { + const actor = new SmoStretchNoteActor(params); + SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice); + } + iterateOverTick(note: SmoNote, tickmap: TickMap, index: number) { + if (this.startIndex === index && this.notesToInsert.length) { + return this.notesToInsert; + } else if (index > this.startIndex && this.numberOfNotesToDelete > 0) { + --this.numberOfNotesToDelete; + return []; + } + return null; + } - const noteCountDiff = allNotes.length - this.voice.notes.length; - this.measure.adjustTupletIndexes(this.selector.tick, noteCountDiff); - this.voice.notes = allNotes; - + private areNotesInSameTuplet(noteOne: SmoNote, noteTwo: SmoNote): boolean { + if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tuplet!.id == noteTwo.tuplet!.id) { + return true; + } + return false; } +} - /** - * todo: handle ticks reminder - * replace duration of current note and fill in the rest with new notes - */ - private contractNote() { - let i = 0; - let newTicks: Ticks = { numerator: this.newStemTicks, denominator: 1, remainder: 0 }; +/** + * constructor parameters for {@link SmoMakeTupletActor} + * @category SmoTransform + */ +export interface SmoMakeTupletParams { + measure: SmoMeasure, + numNotes: number, + voice: number, + index: number +} +/** + * Turn a tuplet into a non-tuplet of the same length + * @category SmoTransform + * + * */ +export class SmoMakeTupletActor extends TickIteratorBase { + measure: SmoMeasure; + numNotes: number; + voice: number; + index: number; + + constructor(params: SmoMakeTupletParams) { + super(); + this.measure = params.measure; + this.index = params.index; + this.voice = params.voice; + this.numNotes = params.numNotes; + } + static apply(params: SmoMakeTupletParams) { + const actor = new SmoMakeTupletActor(params); + SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice); + } + + iterateOverTick(note: SmoNote, tickmap: TickMap, index: number) { + if (this.measure === null) { + return []; + } + if (index !== this.index) { + return null; + } - const multiplier = this.note.tickCount / this.note.stemTicks; + this.measure.clearBeamGroups(); + const stemTicks = SmoTuplet.calculateStemTicks(note.stemTicks, this.numNotes); + const notesOccupied = note.stemTicks / stemTicks; - if (this.note.isTuplet) { - const numerator = this.newStemTicks * multiplier; - newTicks = { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }; - } + const tuplet = new SmoTuplet({ + numNotes: this.numNotes, + notesOccupied: notesOccupied, + stemTicks: stemTicks, + totalTicks: note.tickCount, + ratioed: false, + bracketed: true, + voice: this.voice, + startIndex: this.index, + endIndex: this.index, + }); - const replacingNote = SmoNote.cloneWithDuration(this.note, newTicks, this.newStemTicks); - const stemTicksUsed = this.note.stemTicks; - const allNotes = []; - // const newNotes = []; - for (i = 0; i < this.selector.tick; ++i) { - allNotes.push(this.notes[i]); - } - const remainder = stemTicksUsed - this.newStemTicks; - allNotes.push(replacingNote); - // newNotes.push(replacingNote); + const tupletNotes = this._generateNotesForTuplet(tuplet, note, stemTicks); + tuplet.endIndex += tupletNotes.length - 1; - if (remainder > 0) { - const lmap = SmoMusic.gcdMap(remainder); - lmap.forEach((stemTick) => { - const nnote = SmoNote.cloneWithDuration(this.note, stemTick * multiplier, stemTick); - allNotes.push(nnote); - // newNotes.push(nnote); - }); + SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, index, tupletNotes.length - 1); + const parentTuplet: SmoTuplet | null = SmoTupletTree.getTupletForNoteIndex(this.measure.tupletTrees, this.voice, this.index); + if (parentTuplet === null) { + const tupletTree = new SmoTupletTree({tuplet: tuplet}); + this.measure.tupletTrees.push(tupletTree); + } else { + parentTuplet.childrenTuplets.push(tuplet); } - for (i = this.selector.tick + 1; i < this.notes.length; ++i) { - allNotes.push(this.notes[i]); + + return tupletNotes; + } + + private _generateNotesForTuplet(tuplet: SmoTuplet, originalNote: SmoNote, stemTicks: number): SmoNote[] { + const totalTicks = originalNote.tickCount; + const tupletNotes: SmoNote[] = []; + for (let i = 0; i < this.numNotes; ++i) { + const numerator = totalTicks / this.numNotes; + const note: SmoNote = SmoNote.cloneWithDuration(originalNote, { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }, stemTicks); + // Don't clone modifiers, except for first one. + note.textModifiers = i === 0 ? note.textModifiers : []; + note.tuplet = tuplet.attrs; + tupletNotes.push(note); } + return tupletNotes; + } +} + - const noteCountDiff = allNotes.length - this.voice.notes.length; - this.measure.adjustTupletIndexes(this.selector.tick, noteCountDiff); - this.voice.notes = allNotes; +/** + * Constructor params for {@link SmoUnmakeTupletActor} + * @category SmoTransform + */ +export interface SmoUnmakeTupletParams { + startIndex: number, + endIndex: number, + measure: SmoMeasure, + voice: number +} +/** + * Convert a tuplet into a single note that takes up the whole duration + * @category SmoTransform + */ +export class SmoUnmakeTupletActor extends TickIteratorBase { + startIndex: number = 0; + endIndex: number = 0; + measure: SmoMeasure; + voice: number; + constructor(parameters: SmoUnmakeTupletParams) { + super(); + this.startIndex = parameters.startIndex; + this.endIndex = parameters.endIndex; + this.measure = parameters.measure; + this.voice = parameters.voice; + } + static apply(params: SmoUnmakeTupletParams) { + const actor = new SmoUnmakeTupletActor(params); + SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice); } + iterateOverTick(note: SmoNote, tickmap: TickMap, index: number) { + if (index < this.startIndex || index > this.endIndex) { + return null; + } + if (index === this.startIndex) { + const tuplet = SmoTupletTree.getTupletForNoteIndex(this.measure.tupletTrees, this.voice, index); + if (tuplet === null) { + return []; + } - private areNotesInSameTuplet(noteOne: SmoNote, noteTwo: SmoNote): boolean { - if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tuplet!.id == noteTwo.tuplet!.id) { - return true; + const ticks = tuplet.totalTicks; + const nn: SmoNote = SmoNote.cloneWithDuration(note, { numerator: ticks, denominator: 1, remainder: 0 }); + nn.tuplet = null; + SmoTupletTree.removeTupletForNoteIndex(this.measure, this.voice, index); + SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, this.startIndex, this.startIndex - this.endIndex); + + return [nn]; } - return false; + return []; } } + From e22ce0f8cf4545b56162c209c30b6cad0eb9a8a1 Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Fri, 30 Aug 2024 18:39:32 +0200 Subject: [PATCH 05/14] initial implementation of xml import --- package-lock.json | 381 +++++++++++++++++++------------------ src/smo/mxml/smoToXml.ts | 68 ++++--- src/smo/mxml/xmlHelpers.ts | 115 +++++++++-- src/smo/mxml/xmlState.ts | 224 ++++++++++++++++------ src/smo/mxml/xmlToSmo.ts | 8 +- 5 files changed, 507 insertions(+), 289 deletions(-) diff --git a/package-lock.json b/package-lock.json index d1bc17f0..f0fbb987 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "smoosic", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "smoosic", - "version": "1.0.4", + "version": "1.0.5", "license": "ISC", "dependencies": { "midi-parser-js": "^4.0.4", @@ -28,16 +28,6 @@ "xsd-validator": "^1.1.1" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", @@ -49,9 +39,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "peer": true, "engines": { @@ -59,13 +49,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", - "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "peer": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -208,6 +198,7 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "peer": true, "dependencies": { @@ -223,6 +214,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true, "peer": true }, @@ -283,9 +275,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", @@ -389,9 +381,9 @@ "dev": true }, "node_modules/@types/eslint": { - "version": "8.56.6", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.6.tgz", - "integrity": "sha512-ymwc+qb1XkjT/gfoQwxIeHZ6ixH23A+tCT2ADSA/DPVKzAjwYkTXBMCQ/f6fe4wEa85Lhp26VPeUxI7wMhAi7A==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -417,11 +409,11 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/node": { - "version": "20.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.2.tgz", - "integrity": "sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==", + "version": "22.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.1.tgz", + "integrity": "sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -788,10 +780,25 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, "engines": { "node": ">=0.4.0" } @@ -841,15 +848,15 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -918,6 +925,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "delegates": "^1.0.0", @@ -984,20 +992,20 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "funding": [ { "type": "opencollective", @@ -1013,10 +1021,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -1041,9 +1049,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001603", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001603.tgz", - "integrity": "sha512-iL2iSS0eDILMb9n5yKQoTBim9jMZ0Yrk8g0N9K7UzYyWnfIKzXBZD5ngpM37ZcL/cv0Mli8XtVMRYMQAfFpi5Q==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "funding": [ { "type": "opencollective", @@ -1084,9 +1092,9 @@ } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "engines": { "node": ">=6.0" } @@ -1178,9 +1186,9 @@ } }, "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", - "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", @@ -1242,9 +1250,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -1315,9 +1323,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.722", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.722.tgz", - "integrity": "sha512-5nLE0TWFFpZ80Crhtp4pIp8LXCztjYX41yUcV6b+bKR2PqzjskTMOOlBi1VjBHlvHwS+4gar7kNKOrsbsewEZQ==" + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.12.tgz", + "integrity": "sha512-tIhPkdlEoCL1Y+PToq3zRNehUaKp3wBX/sr7aclAWdIWjvqAe/Im/H0SiCM4c1Q8BLPHCdoJTol+ZblflydehA==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -1326,9 +1334,9 @@ "dev": true }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -1352,9 +1360,9 @@ } }, "node_modules/envinfo": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", - "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", + "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", "dev": true, "bin": { "envinfo": "dist/cli.js" @@ -1364,9 +1372,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", - "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==" + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==" }, "node_modules/escalade": { "version": "3.1.2", @@ -1575,9 +1583,9 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "peer": true, "dependencies": { @@ -1687,6 +1695,12 @@ "dev": true, "peer": true }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "dev": true + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -1719,9 +1733,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1822,6 +1836,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -1842,6 +1857,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -1956,9 +1972,9 @@ } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -1982,9 +1998,9 @@ } }, "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "dependencies": { "pkg-dir": "^4.2.0", @@ -2014,6 +2030,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "dependencies": { "once": "^1.3.0", @@ -2036,12 +2053,15 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2191,9 +2211,9 @@ "peer": true }, "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==" }, "node_modules/keyv": { "version": "4.5.4", @@ -2277,17 +2297,6 @@ "dev": true, "peer": true }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -2349,11 +2358,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -2458,9 +2467,9 @@ "dev": true }, "node_modules/nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", "dev": true }, "node_modules/natural-compare": { @@ -2496,9 +2505,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" }, "node_modules/nopt": { "version": "5.0.0", @@ -2533,6 +2542,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "are-we-there-yet": "^2.0.0", @@ -2560,18 +2570,18 @@ } }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "peer": true, "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -2669,9 +2679,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -2861,6 +2871,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -2934,15 +2945,15 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -2968,12 +2979,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -3193,16 +3201,16 @@ } }, "node_modules/table/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -3242,9 +3250,9 @@ } }, "node_modules/terser": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.0.tgz", - "integrity": "sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==", + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -3318,9 +3326,9 @@ } }, "node_modules/terser/node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "bin": { "acorn": "bin/acorn" }, @@ -3423,9 +3431,9 @@ } }, "node_modules/ts-node/node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3482,9 +3490,9 @@ } }, "node_modules/typedoc": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.12.tgz", - "integrity": "sha512-F+qhkK2VoTweDXd1c42GS/By2DvI2uDF4/EpG424dTexSHdtCH52C6IcAvMA6jR3DzAWZjHpUOW+E02kyPNUNw==", + "version": "0.25.13", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.13.tgz", + "integrity": "sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==", "dependencies": { "lunr": "^2.3.9", "marked": "^4.3.0", @@ -3510,9 +3518,9 @@ } }, "node_modules/typedoc/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -3524,9 +3532,9 @@ } }, "node_modules/typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3536,9 +3544,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unicorn-magic": { "version": "0.1.0", @@ -3553,9 +3561,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "funding": [ { "type": "opencollective", @@ -3571,8 +3579,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -3624,9 +3632,9 @@ "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==" }, "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -3642,9 +3650,9 @@ "dev": true }, "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -3652,10 +3660,10 @@ "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", + "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", + "enhanced-resolve": "^5.17.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -3766,9 +3774,9 @@ } }, "node_modules/webpack/node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "bin": { "acorn": "bin/acorn" }, @@ -3776,10 +3784,10 @@ "node": ">=0.4.0" } }, - "node_modules/webpack/node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/webpack/node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "peerDependencies": { "acorn": "^8" } @@ -3841,6 +3849,16 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3870,7 +3888,8 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/yn": { "version": "3.1.1", diff --git a/src/smo/mxml/smoToXml.ts b/src/smo/mxml/smoToXml.ts index 0515d833..b658cc69 100644 --- a/src/smo/mxml/smoToXml.ts +++ b/src/smo/mxml/smoToXml.ts @@ -49,7 +49,8 @@ export interface SmoState { beamState: number, beamTicks: number, timeSignature?: TimeSignature, - tempo?: SmoTempoText + tempo?: SmoTempoText, + currentTupletLevel: number, // not sure about the name } /** @@ -80,7 +81,8 @@ export class SmoToXml { lyricState: {}, measureTicks: 0, beamState: 0, - beamTicks: 4096 + beamTicks: 4096, + currentTupletLevel: 0, })); } /** @@ -493,34 +495,44 @@ export class SmoToXml { /** * /score-partwise/measure/note/time-modification * /score-partwise/measure/note/tuplet - * @param noteElement - * @param notationsElement - * @param smoState - * @returns */ - static tupletTime(noteElement: Element, tuplet: SmoTuplet, smoState: SmoState) { + static tupletTime(noteElement: Element, note: SmoNote, measure: SmoMeasure, smoState: SmoState) { + const tuplets: SmoTuplet[] = SmoTupletTree.getTupletHierarchyForNoteIndex(measure.tupletTrees, smoState.voiceIndex, smoState.voiceTickIndex); + let actualNotes: number = 1; + let normalNotes: number = 1; + for (let i = 0; i < tuplets.length; i++) { + const tuplet = tuplets[i]; + actualNotes *= tuplet.numNotes; + normalNotes *= tuplet.notesOccupied; + } const nn = XmlHelpers.createTextElementChild; const obj = { - actualNotes: tuplet.numNotes, normalNotes: tuplet.notes_occupied + actualNotes: actualNotes, normalNotes: normalNotes }; const timeModification = nn(noteElement, 'time-modification', null, ''); nn(timeModification, 'actual-notes', obj, 'actualNotes'); nn(timeModification, 'normal-notes', obj, 'normalNotes'); } - static tupletNotation(notationsElement: Element, tuplet: SmoTuplet, note: SmoNote) { - const nn = XmlHelpers.createTextElementChild; - //todo nenad: adjust implementation - // if (tuplet.getIndexOfNote(note) === 0) { - // const tupletElement = nn(notationsElement, 'tuplet', null, ''); - // XmlHelpers.createAttributes(tupletElement, { - // number: 1, type: 'start' - // }); - // } else if (tuplet.getIndexOfNote(note) === tuplet.notes.length - 1) { - // const tupletElement = nn(notationsElement, 'tuplet', null, ''); - // XmlHelpers.createAttributes(tupletElement, { - // number: 1, type: 'stop' - // }); - // } + static tupletNotation(notationsElement: Element, note: SmoNote, measure: SmoMeasure, smoState: SmoState) { + const tuplets: SmoTuplet[] = SmoTupletTree.getTupletHierarchyForNoteIndex(measure.tupletTrees, smoState.voiceIndex, smoState.voiceTickIndex); + for (let i = 0; i < tuplets.length; i++) { + const tuplet: SmoTuplet = tuplets[i]; + const nn = XmlHelpers.createTextElementChild; + + if (tuplet.startIndex === smoState.voiceTickIndex) {//START + const tupletElement = nn(notationsElement, 'tuplet', null, ''); + smoState.currentTupletLevel++; + XmlHelpers.createAttributes(tupletElement, { + number: smoState.currentTupletLevel, type: 'start' + }); + } else if (tuplet.endIndex === smoState.voiceTickIndex) {//STOP + const tupletElement = nn(notationsElement, 'tuplet', null, ''); + XmlHelpers.createAttributes(tupletElement, { + number: smoState.currentTupletLevel, type: 'stop' + }); + smoState.currentTupletLevel--; + } + } } /** @@ -764,10 +776,10 @@ export class SmoToXml { nn(noteElement, 'duration', { duration }, 'duration'); SmoToXml.tie(noteElement, smoState); nn(noteElement, 'voice', { voice: smoState.voiceIndex }, 'voice'); - let typeTickCount = note.tickCount; - if (tuplet) { - typeTickCount = tuplet.stemTicks; - } + let typeTickCount = note.stemTicks; + // if (tuplet) { + // typeTickCount = tuplet.stemTicks; + // } nn(noteElement, 'type', { type: XmlHelpers.closestStemType(typeTickCount) }, 'type'); const dots = SmoMusic.smoTicksToVexDots(note.tickCount); @@ -777,7 +789,7 @@ export class SmoToXml { // time modification (tuplet) comes before notations which have tuplet beaming rules // also before stem if (tuplet) { - SmoToXml.tupletTime(noteElement, tuplet, smoState); + SmoToXml.tupletTime(noteElement, note, measure, smoState); } if (note.flagState === SmoNote.flagStates.up) { nn(noteElement, 'stem', { direction: 'up' }, 'direction'); @@ -799,7 +811,7 @@ export class SmoToXml { } SmoToXml.tied(notationsElement, smoState); if (tuplet) { - SmoToXml.tupletNotation(notationsElement, tuplet, note); + SmoToXml.tupletNotation(notationsElement, note, measure, smoState); } const ornaments = note.getOrnaments(); if (ornaments.length) { diff --git a/src/smo/mxml/xmlHelpers.ts b/src/smo/mxml/xmlHelpers.ts index 5dcb0fd7..2893e8bf 100644 --- a/src/smo/mxml/xmlHelpers.ts +++ b/src/smo/mxml/xmlHelpers.ts @@ -7,6 +7,7 @@ import { SmoNote } from '../data/note'; import { Pitch, PitchLetter, createXmlAttributes, createXmlAttribute } from '../data/common'; import { SmoSelector } from '../xform/selections'; import { SmoBarline } from '../data/measureModifiers'; +import { XmlTupletData } from './xmlState'; export interface XmlOrnamentData { ctor: string, @@ -36,9 +37,19 @@ export interface XmlTieType { /** * Store tuplet information when parsing xml */ -export interface XmlTupletData { - number: number, type: string +export interface XmlTupletType { + number: number, + type: string, + data: XmlTupletData | null, } + +export interface XmlTimeModificationType { + actualNotes: number, + normalNotes: number, + normalType: number, + //normalDot, todo: check if just bool or list of dots (probably list of dots) +} + export interface XmlEndingData { numbers: number[], type: string } @@ -341,12 +352,13 @@ export class XmlHelpers { rv.tickCount = XmlHelpers.durationFromType(noteNode, def); rv.duration = (divisions / 4096) * rv.tickCount; } + //todo nenad: seems like this is not needed since we keep stemTicks directly on the note object now // If this is a tuplet, we adjust the note duration back to the graphical type // and SMO will create the tuplet after. We keep track of tuplet data though for beaming - if (timeAlteration) { - rv.tickCount = (rv.tickCount * timeAlteration.noteCount) / timeAlteration.noteDuration; - rv.alteration = timeAlteration; - } + // if (timeAlteration) { + // rv.tickCount = (rv.tickCount * timeAlteration.noteCount) / timeAlteration.noteDuration; + // rv.alteration = timeAlteration; + // } return rv; } @@ -393,19 +405,90 @@ export class XmlHelpers { }); return rv; } - static getTupletData(noteNode: Element): XmlTupletData[] { - const rv: XmlTupletData[] = []; - const nNodes = [...noteNode.getElementsByTagName('notations')]; - nNodes.forEach((nNode) => { - const slurNodes = [...nNode.getElementsByTagName('tuplet')]; - slurNodes.forEach((slurNode) => { - const number = parseInt(slurNode.getAttribute('number') as string, 10); - const type = slurNode.getAttribute('type') as string; - rv.push({ number, type }); + + static getTimeModificationType(noteNode: Element): XmlTimeModificationType | null { + const timeModificationNode = noteNode.querySelector('time-modification'); + let xmlTimeModification: XmlTimeModificationType | null = null; + if (timeModificationNode) { + const actualNotesNode = timeModificationNode.querySelector('actual-notes'); + const normalNotesNode = timeModificationNode.querySelector('normal-notes'); + const normalTypeNode = timeModificationNode.querySelector('normal-type'); + const normalType = normalTypeNode?.textContent ? XmlHelpers.noteTypesToSmoMap[normalTypeNode?.textContent] ?? null : null; + if (actualNotesNode?.textContent && normalNotesNode?.textContent && normalType) { + const actualNotes = parseInt(actualNotesNode.textContent, 10); + const normalNotes = parseInt(normalNotesNode.textContent, 10); + xmlTimeModification = { + actualNotes: actualNotes, + normalNotes: normalNotes, + normalType: normalType + }; + } + } + return xmlTimeModification; + } + + static getTupletData(noteNode: Element): XmlTupletType[] { + const rv: XmlTupletType[] = []; + const timeModification = XmlHelpers.getTimeModificationType(noteNode); + const notationNode = noteNode.querySelector('notations'); + if (notationNode) { + const tupletNodes = [...notationNode.getElementsByTagName('tuplet')]; + tupletNodes.forEach((tupletNode) => { + const number = parseInt(tupletNode.getAttribute('number') as string, 10) as number; + const type = tupletNode.getAttribute('type') as string; + const xmlTupletType: XmlTupletType = { + number: number, + type: type, + data: null + }; + if (type === 'start') { + let tupletActual = null; + let tupletNormal = null; + const tupletActualNode = tupletNode.querySelector('tuplet-actual'); + if (tupletActualNode) { + const tupletNumberNode = tupletActualNode.querySelector('tuplet-number'); + const tupletTypeNode = tupletActualNode.querySelector('tuplet-type'); + const tupletTypeContent = tupletTypeNode?.textContent; + const tupletType = tupletTypeContent ? XmlHelpers.noteTypesToSmoMap[tupletTypeContent] ?? null : null; + if (tupletNumberNode?.textContent && tupletType) { + const tupletNumber = parseInt(tupletNumberNode.textContent, 10); + tupletActual = {tupletNumber: tupletNumber, tupletType: tupletType}; + } + } + const tupletNormalNode = tupletNode.querySelector('tuplet-normal'); + if (tupletNormalNode) { + const tupletNumberNode = tupletNormalNode.querySelector('tuplet-number'); + const tupletTypeNode = tupletNormalNode.querySelector('tuplet-type'); + const tupletTypeContent = tupletTypeNode?.textContent; + const tupletType = tupletTypeContent ? XmlHelpers.noteTypesToSmoMap[tupletTypeContent] ?? null : null; + if (tupletNumberNode?.textContent && tupletType) { + const tupletNumber = parseInt(tupletNumberNode.textContent, 10); + tupletNormal = {tupletNumber: tupletNumber, tupletType: tupletType}; + } + } + if (tupletActual && tupletNormal) { + const xmlTupletData: XmlTupletData = { + stemTicks: tupletActual.tupletType, + numNotes: tupletActual.tupletNumber, + notesOccupied: (tupletActual.tupletType / tupletNormal.tupletType) * tupletNormal.tupletNumber + }; + xmlTupletType.data = xmlTupletData; + } else if (timeModification) { + const xmlTupletData: XmlTupletData = { + stemTicks: timeModification.normalType, + numNotes: timeModification.actualNotes, + notesOccupied: timeModification.normalNotes + }; + xmlTupletType.data = xmlTupletData; + } + } + rv.push(xmlTupletType); }); - }); + } + return rv; } + static articulationsAndOrnaments(noteNode: Element): SmoNoteModifierBase[] { const rv: SmoNoteModifierBase[] = []; const nNodes = [...noteNode.getElementsByTagName('notations')]; diff --git a/src/smo/mxml/xmlState.ts b/src/smo/mxml/xmlState.ts index 7df454e2..971cdcfc 100644 --- a/src/smo/mxml/xmlState.ts +++ b/src/smo/mxml/xmlState.ts @@ -1,19 +1,27 @@ // [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. -import { XmlHelpers, XmlLyricData, XmlDurationAlteration, XmlTieType, XmlSlurType, XmlTupletData } from './xmlHelpers'; -import { SmoScore } from '../data/score'; -import { SmoSystemGroup, SmoFormattingManager } from '../data/scoreModifiers'; -import { SmoSystemStaff } from '../data/systemStaff'; -import { SmoTie, SmoStaffHairpin, SmoSlur, SmoSlurParams, SmoInstrument, SmoInstrumentParams, TieLine } from '../data/staffModifiers'; -import { SmoBarline, SmoMeasureModifierBase, SmoRehearsalMark, SmoTempoText } from '../data/measureModifiers'; -import { SmoPartInfo } from '../data/partInfo'; -import { SmoMeasure } from '../data/measure'; -import { SmoNote } from '../data/note'; -import { SmoLyric, SmoDynamicText, SmoGraceNote } from '../data/noteModifiers'; -import { SmoTuplet } from '../data/tuplet'; -import { Clef } from '../data/common'; -import { SmoMusic } from '../data/music'; -import { SmoSelector, SmoSelection } from '../xform/selections'; +import {XmlDurationAlteration, XmlHelpers, XmlLyricData, XmlSlurType, XmlTieType, XmlTupletType} from './xmlHelpers'; +import {SmoScore} from '../data/score'; +import {SmoFormattingManager, SmoSystemGroup} from '../data/scoreModifiers'; +import {SmoSystemStaff} from '../data/systemStaff'; +import { + SmoInstrument, + SmoInstrumentParams, + SmoSlur, + SmoSlurParams, + SmoStaffHairpin, + SmoTie, + TieLine +} from '../data/staffModifiers'; +import {SmoBarline, SmoMeasureModifierBase, SmoRehearsalMark, SmoTempoText} from '../data/measureModifiers'; +import {SmoPartInfo} from '../data/partInfo'; +import {SmoMeasure} from '../data/measure'; +import {SmoNote} from '../data/note'; +import {SmoDynamicText, SmoGraceNote, SmoLyric} from '../data/noteModifiers'; +import {SmoTuplet, SmoTupletTree} from '../data/tuplet'; +import {Clef} from '../data/common'; +import {SmoMusic} from '../data/music'; +import {SmoSelection, SmoSelector} from '../xform/selections'; export interface XmlClefInfo { clef: string, staffId: number @@ -57,13 +65,37 @@ export interface XmlCompletedTies { fromPitch: number, toPitch: number } + export interface XmlCompletedTuplet { - tuplet: SmoTuplet, staffId: number, voiceId: number + tuplet: SmoTuplet, + staffId: number, + voiceId: number +} +export class XmlTupletStateTreeNode { + tupletState: XmlTupletState; + children: XmlTupletStateTreeNode[]; + constructor(tupletState: XmlTupletState) { + this.tupletState = tupletState; + this.children = []; + } +} + +export interface XmlCompletedTupletState { + tupletState: XmlTupletState, + staffId: number, + voiceId: number } export interface XmlTupletState { - start: SmoSelector, - end: SmoSelector + start: SmoSelector | null, + end: SmoSelector | null, + data: XmlTupletData | null, +} +export interface XmlTupletData { + numNotes: number, + notesOccupied: number, + stemTicks: number, } + export interface XmlEnding { start: number, end: number, @@ -107,7 +139,10 @@ export class XmlState { verseMap: Record = {}; measureNumber: number = 0; formattingManager = new SmoFormattingManager(SmoFormattingManager.defaults); - tuplets: Record = {}; + + completedTupletStates: XmlCompletedTupletState[] = []; + tupletStatesInProgress: Record = {}; + tickCursor: number = 0; tempo: SmoTempoText = new SmoTempoText(SmoTempoText.defaults); staffArray: XmlStaffInfo[] = []; @@ -157,7 +192,7 @@ export class XmlState { if (isNaN(this.measureNumber)) { this.measureNumber = oldMeasure + 1; } - this.tuplets = {}; + this.tupletStatesInProgress = {}; this.tickCursor = 0; this.tempo = SmoMeasureModifierBase.deserialize(this.tempo.serialize()); this.tempo.display = false; @@ -536,67 +571,132 @@ export class XmlState { } }); } - //todo: adjust implementation - // ### backtrackTuplets - // If we received a tuplet end, go back through the voice - // and construct the SmoTuplet. - backtrackTuplets(voice: XmlVoiceInfo, tupletNumber: number, staffId: number, voiceId: number) { - const tupletState = this.tuplets[tupletNumber]; - let i = tupletState.start.tick; - const notes = []; - const durationMap = []; - while (i < voice.notes.length) { - const note = voice.notes[i]; - notes.push(note); - if (i === tupletState.start.tick) { - durationMap.push(1.0); - } else { - const prev = voice.notes[i - 1]; - durationMap.push(note.ticks.numerator / prev.ticks.numerator); - } - i += 1; - } - const tp = SmoTuplet.defaults; - // tp.notes = notes; - // tp.durationMap = durationMap; - tp.voice = voiceId; - const tuplet = new SmoTuplet(tp); - // Store the tuplet with the staff ID and voice so we - // can add it to the right measure when it's created. - this.completedTuplets.push({ tuplet, staffId, voiceId }); - } + + + // ### updateTupletStates // react to a tuplet start or stop directive - updateTupletStates(tupletInfos: XmlTupletData[], voice: XmlVoiceInfo, staffIndex: number, voiceIndex: number) { + // we need to handle start and stop directives that appear out of order + updateTupletStates(tupletInfos: XmlTupletType[], voice: XmlVoiceInfo, staffIndex: number, voiceIndex: number) { + // this.tickCursor; const tick = voice.notes.length - 1; tupletInfos.forEach((tupletInfo) => { + let tupletState: XmlTupletState | undefined = this.tupletStatesInProgress[tupletInfo.number]; + if (tupletState == undefined) { + tupletState = { + start: null, + end: null, + data: null, + }; + this.tupletStatesInProgress[tupletInfo.number] = tupletState; + } if (tupletInfo.type === 'start') { - this.tuplets[tupletInfo.number] = { - start: { staff: staffIndex, measure: this.measureNumber, voice: voiceIndex, tick, pitches: [] }, - end: SmoSelector.default + tupletState.start = { + staff: staffIndex, measure: this.measureNumber, voice: voiceIndex, tick, pitches: [] }; + tupletState.data = tupletInfo.data; } else if (tupletInfo.type === 'stop') { - this.tuplets[tupletInfo.number].end = { + tupletState.end = { staff: staffIndex, measure: this.measureNumber, voice: voiceIndex, tick, pitches: [] }; - this.backtrackTuplets(voice, tupletInfo.number, staffIndex, voiceIndex); + } + if (tupletState.start != null && tupletState.end != null) { + this.completedTupletStates.push({ + tupletState: tupletState, + staffId: staffIndex, + voiceId: voiceIndex + }); + delete this.tupletStatesInProgress[tupletInfo.number]; } }); } addTupletsToMeasure(smoMeasure: SmoMeasure, staffId: number, voiceId: number) { - const completed: XmlCompletedTuplet[] = []; - this.completedTuplets.forEach((tuplet) => { - if (tuplet.voiceId === voiceId && tuplet.staffId === staffId) { - // smoMeasure.tuplets.push(tuplet.tuplet); - } else { - completed.push(tuplet); + const tupletStates = this.findCompletedTupletStatesByStaffAndVoice(staffId, voiceId); + const xmlTupletStateTrees = this.buildXmlTupletStateTrees(tupletStates); + const notes: SmoNote[] = smoMeasure.voices[voiceId].notes; + smoMeasure.tupletTrees = this.buildSmoTupletTreesFromXmlTupletStateTrees(xmlTupletStateTrees, notes); + } + private findCompletedTupletStatesByStaffAndVoice(staffId: number, voiceId: number): XmlTupletState[] { + const tupletStates: XmlTupletState[] = []; + this.completedTupletStates.forEach((completedTupletState) => { + if (completedTupletState.staffId === staffId && completedTupletState.voiceId === voiceId) { + tupletStates.push(completedTupletState.tupletState); } }); - this.completedTuplets = completed; + return tupletStates; } + private buildXmlTupletStateTrees(tupletStates: XmlTupletState[]): XmlTupletStateTreeNode[] { + let sortedTupletStates = this.sortTupletStates(tupletStates); + let roots: XmlTupletStateTreeNode[] = []; + let activeNodes: XmlTupletStateTreeNode[] = []; + for (let tupletState of sortedTupletStates) { + let node = new XmlTupletStateTreeNode(tupletState); + let placed = false; + while (activeNodes.length > 0) { + let lastNode = activeNodes[activeNodes.length - 1]; + if (tupletState.start!.tick <= lastNode.tupletState.end!.tick) { + lastNode.children.push(node); + placed = true; + break; + } else { + activeNodes.pop(); + } + } + if (!placed) { + roots.push(node); + } + activeNodes.push(node); + } + return roots; + } + private sortTupletStates(tupletStates: XmlTupletState[]): XmlTupletState[] { + return tupletStates.sort((a, b) => { + if (a.start === b.start) { + return a.end!.tick - b.end!.tick; + } + return a.start!.tick - b.start!.tick; + }); + } + /** + * Create SmoTuplets out of completedTupletStates + */ + buildSmoTupletTreesFromXmlTupletStateTrees(xmlTupletStateTrees: XmlTupletStateTreeNode[], notes: SmoNote[]): SmoTupletTree[] { + const smoTupletTrees: SmoTupletTree[] = []; + const traverseXmlTupletStateTree = (xmlTupletStateTreeNode: XmlTupletStateTreeNode): SmoTuplet => { + const smoTupletParams = SmoTuplet.defaults; + const xmlTupletState = xmlTupletStateTreeNode.tupletState; + if (xmlTupletState.data) { + smoTupletParams.numNotes = xmlTupletState.data.numNotes; + smoTupletParams.notesOccupied = xmlTupletState.data.notesOccupied; + smoTupletParams.stemTicks = xmlTupletState.data.stemTicks; + } + smoTupletParams.startIndex = xmlTupletState.start!.tick; + smoTupletParams.endIndex = xmlTupletState.end!.tick; + for (let i = smoTupletParams.startIndex; i <= smoTupletParams.endIndex; i++) { + smoTupletParams.totalTicks += notes[i].tickCount; + } + smoTupletParams.voice = xmlTupletState.start!.voice; + const smoTuplet = new SmoTuplet(smoTupletParams); + for (let i = 0; i < xmlTupletStateTreeNode.children.length; i++) { + const childSmoTuplet = traverseXmlTupletStateTree(xmlTupletStateTreeNode.children[i]); + childSmoTuplet.parentTuplet = {id: smoTuplet.attrs.id}; + smoTuplet.childrenTuplets.push(childSmoTuplet); + } + return smoTuplet; + }; + + for (let i = 0; i < xmlTupletStateTrees.length; i++) { + const xmlTupletStateTreeNode = xmlTupletStateTrees[i]; + const tuplet: SmoTuplet = traverseXmlTupletStateTree(xmlTupletStateTreeNode); + smoTupletTrees.push(new SmoTupletTree({tuplet: tuplet})); + } + return smoTupletTrees; + } + + getSystems(): SmoSystemGroup[] { const rv: SmoSystemGroup[] = []; - this.systems.forEach((system) => { + this.systems.forEach((system: { startSelector: SmoSelector; endSelector: SmoSelector; leftConnector: number; }) => { const params = SmoSystemGroup.defaults; params.startSelector = system.startSelector; params.endSelector = system.endSelector; diff --git a/src/smo/mxml/xmlToSmo.ts b/src/smo/mxml/xmlToSmo.ts index f8fdd407..db54bc68 100644 --- a/src/smo/mxml/xmlToSmo.ts +++ b/src/smo/mxml/xmlToSmo.ts @@ -621,6 +621,8 @@ export class XmlToSmo { const noteType = restNode.length ? 'r' : 'n'; const durationData = XmlHelpers.ticksFromDuration(noteElement, divisions, 4096); const tickCount = durationData.tickCount; + //todo nenad: we probably need to handle dotted durations + const stemTicks = XmlHelpers.durationFromType(noteElement, 4096); if (chordNode.length === 0) { xmlState.staffArray[staffIndex].voices[voiceIndex].ticksUsed += tickCount; } @@ -648,6 +650,7 @@ export class XmlToSmo { // If this is a non-grace note, add any grace notes to the note since SMO // treats them as note modifiers noteData.ticks = { numerator: tickCount, denominator: 1, remainder: 0 }; + noteData.stemTicks = stemTicks; noteData.flagState = flagState; noteData.clef = clefString; xmlState.previousNote = new SmoNote(noteData); @@ -698,6 +701,7 @@ export class XmlToSmo { xmlState.updateSlurStates(slurInfos); xmlState.updateTieStates(tieInfos); voice.notes.push(xmlState.previousNote); + //todo nenad: check if we need to change something with 'alteration' xmlState.updateBeamState(beamState, durationData.alteration, voice, voiceIndex); xmlState.updateTupletStates(tupletInfos, voice, staffIndex, voiceIndex); @@ -792,14 +796,14 @@ export class XmlToSmo { // voices not in array, put them in an array Object.keys(staffData.voices).forEach((voiceKey) => { const voice = staffData.voices[voiceKey]; - xmlState.addTupletsToMeasure(smoMeasure, staffData.clefInfo.staffId, - parseInt(voiceKey, 10)); voice.notes.forEach((note) => { if (!note.clef) { note.clef = smoMeasure.clef; } }); smoMeasure.voices.push(voice); + const voiceId = smoMeasure.voices.length - 1; + xmlState.addTupletsToMeasure(smoMeasure, staffData.clefInfo.staffId, voiceId); }); if (smoMeasure.voices.length === 0) { smoMeasure.voices.push({ notes: SmoMeasure.getDefaultNotes(smoMeasure) }); From 65f7734228a82ea0ef0254eccb8e7f27275fd6ee Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Sat, 31 Aug 2024 12:47:04 +0200 Subject: [PATCH 06/14] initial implementation of xml Export --- src/smo/mxml/smoToXml.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/smo/mxml/smoToXml.ts b/src/smo/mxml/smoToXml.ts index b658cc69..7e5f7e3f 100644 --- a/src/smo/mxml/smoToXml.ts +++ b/src/smo/mxml/smoToXml.ts @@ -497,7 +497,7 @@ export class SmoToXml { * /score-partwise/measure/note/tuplet */ static tupletTime(noteElement: Element, note: SmoNote, measure: SmoMeasure, smoState: SmoState) { - const tuplets: SmoTuplet[] = SmoTupletTree.getTupletHierarchyForNoteIndex(measure.tupletTrees, smoState.voiceIndex, smoState.voiceTickIndex); + const tuplets: SmoTuplet[] = SmoTupletTree.getTupletHierarchyForNoteIndex(measure.tupletTrees, smoState.voiceIndex - 1, smoState.voiceTickIndex); let actualNotes: number = 1; let normalNotes: number = 1; for (let i = 0; i < tuplets.length; i++) { @@ -514,7 +514,7 @@ export class SmoToXml { nn(timeModification, 'normal-notes', obj, 'normalNotes'); } static tupletNotation(notationsElement: Element, note: SmoNote, measure: SmoMeasure, smoState: SmoState) { - const tuplets: SmoTuplet[] = SmoTupletTree.getTupletHierarchyForNoteIndex(measure.tupletTrees, smoState.voiceIndex, smoState.voiceTickIndex); + const tuplets: SmoTuplet[] = SmoTupletTree.getTupletHierarchyForNoteIndex(measure.tupletTrees, smoState.voiceIndex - 1, smoState.voiceTickIndex); for (let i = 0; i < tuplets.length; i++) { const tuplet: SmoTuplet = tuplets[i]; const nn = XmlHelpers.createTextElementChild; @@ -525,6 +525,16 @@ export class SmoToXml { XmlHelpers.createAttributes(tupletElement, { number: smoState.currentTupletLevel, type: 'start' }); + + const tupletType = XmlHelpers.ticksToNoteTypeMap[tuplet.stemTicks]; + + const tupletActual = nn(tupletElement, 'tuplet-actual', null, ''); + nn(tupletActual, 'tuplet-number', tuplet, 'numNotes'); + nn(tupletActual, 'tuplet-type', tupletType, ''); + + const tupletNormal = nn(tupletElement, 'tuplet-normal', null, ''); + nn(tupletNormal, 'tuplet-number', tuplet, 'notesOccupied'); + nn(tupletNormal, 'tuplet-type', tupletType, ''); } else if (tuplet.endIndex === smoState.voiceTickIndex) {//STOP const tupletElement = nn(notationsElement, 'tuplet', null, ''); XmlHelpers.createAttributes(tupletElement, { @@ -772,7 +782,7 @@ export class SmoToXml { } const duration = note.tickCount; smoState.measureTicks += duration; - const tuplet = SmoTupletTree.getTupletForNoteIndex(measure.tupletTrees, smoState.voiceIndex, smoState.voiceTickIndex); + const tuplet = SmoTupletTree.getTupletForNoteIndex(measure.tupletTrees, smoState.voiceIndex - 1, smoState.voiceTickIndex); nn(noteElement, 'duration', { duration }, 'duration'); SmoToXml.tie(noteElement, smoState); nn(noteElement, 'voice', { voice: smoState.voiceIndex }, 'voice'); From 367859bfe0e0afa8f7e51caa68fb65bd342d50ef Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Mon, 2 Sep 2024 22:25:18 +0200 Subject: [PATCH 07/14] handle remainder on note ticks --- src/smo/xform/tickDuration.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/smo/xform/tickDuration.ts b/src/smo/xform/tickDuration.ts index 0928d26d..2cb5ec2a 100644 --- a/src/smo/xform/tickDuration.ts +++ b/src/smo/xform/tickDuration.ts @@ -341,14 +341,17 @@ export class SmoMakeTupletActor extends TickIteratorBase { private _generateNotesForTuplet(tuplet: SmoTuplet, originalNote: SmoNote, stemTicks: number): SmoNote[] { const totalTicks = originalNote.tickCount; const tupletNotes: SmoNote[] = []; + const numerator = totalTicks / this.numNotes; for (let i = 0; i < this.numNotes; ++i) { - const numerator = totalTicks / this.numNotes; - const note: SmoNote = SmoNote.cloneWithDuration(originalNote, { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }, stemTicks); + const note: SmoNote = SmoNote.cloneWithDuration(originalNote, { numerator: Math.floor(numerator), denominator: 1, remainder: 0 }, stemTicks); // Don't clone modifiers, except for first one. note.textModifiers = i === 0 ? note.textModifiers : []; note.tuplet = tuplet.attrs; tupletNotes.push(note); } + if (numerator % 1) { + tupletNotes[0].ticks.numerator += 1; + } return tupletNotes; } } From d8ae9cfa2323ccba9f5049813bd356584521056f Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Wed, 4 Sep 2024 23:09:12 +0200 Subject: [PATCH 08/14] handle remainder on note ticks --- src/smo/xform/tickDuration.ts | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/smo/xform/tickDuration.ts b/src/smo/xform/tickDuration.ts index 2cb5ec2a..034c5c59 100644 --- a/src/smo/xform/tickDuration.ts +++ b/src/smo/xform/tickDuration.ts @@ -145,7 +145,7 @@ export class SmoContractNoteActor extends TickIteratorBase { if (note.isTuplet) { const numerator = this.newStemTicks * multiplier; newTicks = { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }; - } + } const replacingNote = SmoNote.cloneWithDuration(note, newTicks, this.newStemTicks); const oldStemTicks = note.stemTicks; @@ -159,11 +159,22 @@ export class SmoContractNoteActor extends TickIteratorBase { return null; } const lmap = SmoMusic.gcdMap(remainderStemTicks); + lmap.forEach((stemTick) => { - const nnote = SmoNote.cloneWithDuration(note, stemTick * multiplier, stemTick); + const numerator = stemTick * multiplier; + const nnote = SmoNote.cloneWithDuration(note, {numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1}, stemTick); notes.push(nnote); }); } + //accumulate all remainders in the first note + let remainder: number = 0; + notes.forEach((note: SmoNote) => { + if (note.ticks.remainder > 0) { + remainder += note.ticks.remainder; + note.ticks.remainder = 0; + } + }); + notes[0].ticks.numerator += Math.round(remainder); SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, index, notes.length - 1); return notes; @@ -231,16 +242,28 @@ export class SmoStretchNoteActor extends TickIteratorBase { break; } } - const remainder = stemTicksUsed - this.newStemTicks; - if (remainder >= 0) { + const remainingAmount = stemTicksUsed - this.newStemTicks; + if (remainingAmount >= 0) { this.notesToInsert.push(replacingNote); - const lmap = SmoMusic.gcdMap(remainder); + const lmap = SmoMusic.gcdMap(remainingAmount); lmap.forEach((stemTick) => { - const nnote = SmoNote.cloneWithDuration(originalNote, stemTick * multiplier, stemTick) + const numerator = stemTick * multiplier; + const nnote = SmoNote.cloneWithDuration(originalNote, {numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1}, stemTick) this.notesToInsert.push(nnote); }); const noteCountDiff = (this.notesToInsert.length - this.numberOfNotesToDelete) - 1; SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, this.startIndex, noteCountDiff); + + //accumulate all remainders in the first note + let remainder: number = 0; + this.notesToInsert.forEach((note: SmoNote) => { + if (note.ticks.remainder > 0) { + remainder += note.ticks.remainder; + note.ticks.remainder = 0; + } + }); + this.notesToInsert[0].ticks.numerator += Math.round(remainder); + } } static apply(params: SmoStretchNoteParams) { From 3d0aaff1590b07b75ba6b5cbf68174eb3ea1e821 Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Fri, 6 Sep 2024 00:18:10 +0200 Subject: [PATCH 09/14] adjusted implementation of alignNotesWithTimeSignature() function. --- src/smo/data/measure.ts | 141 ++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 70 deletions(-) diff --git a/src/smo/data/measure.ts b/src/smo/data/measure.ts index eb202653..2dafa755 100644 --- a/src/smo/data/measure.ts +++ b/src/smo/data/measure.ts @@ -902,80 +902,81 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { this.svg.staffX = Math.round(x); } /** - * todo nenad: adjust implementation * A time signature has possibly changed. add/remove notes to * match the new length */ alignNotesWithTimeSignature() { - // const tsTicks = SmoMusic.timeSignatureToTicks(this.timeSignature.timeSignature); - // if (tsTicks === this.getMaxTicksVoice()) { - // return; - // } - // const replaceNoteWithDuration = (target: number, ar: SmoNote[], note: SmoNote) => { - // const fitNote = new SmoNote(SmoNote.defaults); - // const duration = SmoMusic.closestDurationTickLtEq(target); - // if (duration > 128) { - // fitNote.ticks = { numerator: duration, denominator: 1, remainder: 0 }; - // fitNote.pitches = note.pitches; - // fitNote.noteType = note.noteType; - // fitNote.clef = note.clef; - // ar.push(fitNote); - // } - // } - // const voices: SmoVoice[] = []; - // const tuplets: SmoTuplet[] = []; - // for (var i = 0; i < this.voices.length; ++i) { - // const voice = this.voices[i]; - // const newNotes: SmoNote[] = []; - // let voiceTicks = 0; - // for (var j = 0; j < voice.notes.length; ++j) { - // const note = voice.notes[j]; - // // if a tuplet, make sure the whole tuplet fits. - // if (note.isTuplet) { - // const tuplet = this.getTupletForNote(note); - // if (tuplet) { - // // remaining notes of an approved tuplet, just add them - // if (tuplet.startIndex !== j) { - // newNotes.push(note); - // continue; - // } - // else if (tuplet.tickCount + voiceTicks <= tsTicks) { - // // first note of the tuplet, it fits, add it - // voiceTicks += tuplet.tickCount; - // newNotes.push(note); - // tuplets.push(tuplet); - // } else { - // // tuplet will not fit. Make a note as close to remainder as possible and add it - // replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); - // voiceTicks = tsTicks; - // break; - // } - // } else { // missing tuplet, now what? - // console.warn('missing tuplet info'); - // replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); - // voiceTicks = tsTicks; - // } - // } else { - // if (note.tickCount + voiceTicks <= tsTicks) { - // newNotes.push(note); - // voiceTicks += note.tickCount; - // } else { - // replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); - // voiceTicks = tsTicks; - // break; - // } - // } - // } - // if (tsTicks - voiceTicks > 128) { - // const np = SmoNote.defaults; - // np.clef = this.clef; - // const nnote = new SmoNote(np); - // replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, nnote); - // } - // voices.push({ notes: newNotes }); - // } - // this.voices = voices; - // this.tuplets = tuplets; + const tsTicks = SmoMusic.timeSignatureToTicks(this.timeSignature.timeSignature); + if (tsTicks === this.getMaxTicksVoice()) { + return; + } + const replaceNoteWithDuration = (target: number, ar: SmoNote[], note: SmoNote) => { + const fitNote = new SmoNote(SmoNote.defaults); + const duration = SmoMusic.closestDurationTickLtEq(target); + if (duration > 128) { + fitNote.ticks = { numerator: duration, denominator: 1, remainder: 0 }; + fitNote.stemTicks = duration; + fitNote.pitches = note.pitches; + fitNote.noteType = note.noteType; + fitNote.clef = note.clef; + ar.push(fitNote); + } + } + const voices: SmoVoice[] = []; + const tuplets: SmoTuplet[] = []; + for (var i = 0; i < this.voices.length; ++i) { + const voice = this.voices[i]; + const newNotes: SmoNote[] = []; + let voiceTicks = 0; + for (var j = 0; j < voice.notes.length; ++j) { + const note = voice.notes[j]; + // if a tuplet, make sure the whole tuplet fits. + if (note.isTuplet) { + const tupletTree = SmoTupletTree.getTupletTreeForNoteIndex(this.tupletTrees, i, j); + if (tupletTree) { + // remaining notes of an approved tuplet, just add them + if (tupletTree.startIndex !== j) { + newNotes.push(note); + continue; + } + else if (tupletTree.totalTicks + voiceTicks <= tsTicks) { + // first note of the tuplet, it fits, add it + voiceTicks += tupletTree.totalTicks; + newNotes.push(note); + } else { + // tuplet will not fit. Replace tuplet with a note as close to remainder as possible and add it + // remove tuplet + note.tuplet = null + replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); + voiceTicks = tsTicks; + SmoTupletTree.removeTupletForNoteIndex(this, i, j); + break; + } + } else { // missing tuplet, now what? + console.warn('missing tuplet info'); + replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); + voiceTicks = tsTicks; + } + } else { + if (note.tickCount + voiceTicks <= tsTicks) { + newNotes.push(note); + voiceTicks += note.tickCount; + } else { + replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); + voiceTicks = tsTicks; + break; + } + } + } + if (tsTicks - voiceTicks > 128) { + const np = SmoNote.defaults; + np.clef = this.clef; + const nnote = new SmoNote(np); + replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, nnote); + } + voices.push({ notes: newNotes }); + } + this.voices = voices; } get measureNumberDbg(): string { return `${this.measureNumber.measureIndex}/${this.measureNumber.systemIndex}/${this.measureNumber.staffId}`; From a80a4cf168344cb1ccce0eed741a9d786244ae32 Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Thu, 12 Sep 2024 19:25:52 +0200 Subject: [PATCH 10/14] fix on dot duration --- src/smo/data/music.ts | 15 +++++++++++---- src/smo/xform/operations.ts | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/smo/data/music.ts b/src/smo/data/music.ts index 362c088e..65b94d82 100644 --- a/src/smo/data/music.ts +++ b/src/smo/data/music.ts @@ -1437,7 +1437,8 @@ export class SmoMusic { SmoMusic.highestDuration / 16, // 8th SmoMusic.highestDuration / 32, // 16th SmoMusic.highestDuration / 64, // 32nd - SmoMusic.highestDuration / 128 // 64th + SmoMusic.highestDuration / 128, // 64th + SmoMusic.highestDuration / 256 // 128th ]; static durationsAscending = [ SmoMusic.highestDuration / 256, // 128th @@ -1525,7 +1526,7 @@ export class SmoMusic { let i = 0; const durations = ['1/2', '1', '2', '4', '8', '16', '32', '64', '128', '256']; const _ticksToDurationsF = () => { - for (i = 0; i < SmoMusic.durationsDescending.length - 1; ++i) { + for (i = 0; i <= SmoMusic.durationsDescending.length - 1; ++i) { let j = 0; let dots = ''; let ticks = 0; @@ -1707,7 +1708,10 @@ export class SmoMusic { static getNextDottedLevel(ticks: number): number { const ticksOrNull = SmoMusic.closestSmoDurationFromTicks(ticks); if (ticksOrNull && ticksOrNull.index > 0) { - return SmoMusic.validDurations[SmoMusic._validDurationKeys[ticksOrNull.index - 1]].ticks; + const newDuration = SmoMusic.validDurations[SmoMusic._validDurationKeys[ticksOrNull.index - 1]]; + if (newDuration.baseTicks === ticksOrNull.baseTicks) { + return newDuration.ticks; + } } return ticks; } @@ -1718,7 +1722,10 @@ export class SmoMusic { static getPreviousDottedLevel(ticks: number): number { const ticksOrNull = SmoMusic.closestSmoDurationFromTicks(ticks); if (ticksOrNull && ticksOrNull.index < SmoMusic._validDurationKeys.length + 1) { - return SmoMusic.validDurations[SmoMusic._validDurationKeys[ticksOrNull.index + 1]].ticks; + const newDuration = SmoMusic.validDurations[SmoMusic._validDurationKeys[ticksOrNull.index + 1]]; + if (newDuration.baseTicks === ticksOrNull.baseTicks) { + return newDuration.ticks; + } } return ticks; } diff --git a/src/smo/xform/operations.ts b/src/smo/xform/operations.ts index d153e052..837197f5 100644 --- a/src/smo/xform/operations.ts +++ b/src/smo/xform/operations.ts @@ -360,12 +360,12 @@ export class SmoOperation { if (selection.selector.tick + 1 === selection.measure.voices[selection.selector.voice].notes.length) { return; } - if (selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].tickCount > note.tickCount) { + if (selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].stemTicks > note.stemTicks) { console.log('too long'); return; } // is dot too short? - if (!SmoMusic.validDurations[selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].tickCount / 2]) { + if (!SmoMusic.validDurations[selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].stemTicks / 2]) { return; } From 350bc0af2d0e01991f15da77d36da49b2895528f Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Fri, 13 Sep 2024 22:24:47 +0200 Subject: [PATCH 11/14] fix on legacy note and tuplet deserialization --- src/smo/data/measure.ts | 36 ++++++++++++++++++----------------- src/smo/data/music.ts | 18 ++++++++++++++++++ src/smo/data/note.ts | 33 ++++++++++++++++++++++---------- src/smo/data/tuplet.ts | 10 ++++++---- src/smo/xform/beamers.ts | 7 +++++-- src/smo/xform/tickDuration.ts | 6 +++--- 6 files changed, 74 insertions(+), 36 deletions(-) diff --git a/src/smo/data/measure.ts b/src/smo/data/measure.ts index 2dafa755..b6414c1c 100644 --- a/src/smo/data/measure.ts +++ b/src/smo/data/measure.ts @@ -635,7 +635,24 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { } params.keySignature = jsonObj.keySignature ?? 'C'; params.voices = voices; - // params.tupletTrees = tuplets; + + if ((jsonObj as any).tupletTrees !== undefined) { + for (j = 0; j < jsonObj.tupletTrees.length; ++j) { + const tupletTreeJson = jsonObj.tupletTrees[j]; + const tupletTree = SmoTupletTree.deserialize(tupletTreeJson); + params.tupletTrees.push(tupletTree); + } + } + //deserialization of a legacy tuplets + //legacy schema had measure.tuplets, it is measure.tupletTrees now + if ((jsonObj as any).tuplets !== undefined) { + for (j = 0; j < (jsonObj as any).tuplets.length; ++j) { + const tupJson = (jsonObj as any).tuplets[j]; + const tuplet: SmoTuplet = SmoTuplet.deserialize(tupJson); + const tupletTree: SmoTupletTree = new SmoTupletTree({tuplet: tuplet}); + params.tupletTrees.push(tupletTree); + } + } params.modifiers = modifiers; const measure = new SmoMeasure(params); // Handle migration for measure-mapped parameters @@ -648,22 +665,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { measure.tempo = new SmoTempoText(SmoTempoText.defaults); } - for (j = 0; j < jsonObj.tupletTrees.length; ++j) { - const tupletTreeJson = jsonObj.tupletTrees[j]; - const tupletTree = SmoTupletTree.deserialize(tupletTreeJson); - measure.tupletTrees.push(tupletTree); - } - //deserialization of a legacy tuplets - //legacy schema had measure.tuplets, it is measure.tupletTrees now - if ((jsonObj as any).tuplets !== undefined) { - for (j = 0; j < (jsonObj as any).tuplets.length; ++j) { - const tupJson = (jsonObj as any).tuplets[j]; - const tuplet: SmoTuplet = SmoTuplet.deserialize(tupJson); - const tupletTree: SmoTupletTree = new SmoTupletTree({tuplet: tuplet}); - measure.tupletTrees.push(tupletTree); - } - } return measure; } @@ -946,7 +948,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { } else { // tuplet will not fit. Replace tuplet with a note as close to remainder as possible and add it // remove tuplet - note.tuplet = null + note.tupletId = null replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); voiceTicks = tsTicks; SmoTupletTree.removeTupletForNoteIndex(this, i, j); diff --git a/src/smo/data/music.ts b/src/smo/data/music.ts index 65b94d82..0efb02a2 100644 --- a/src/smo/data/music.ts +++ b/src/smo/data/music.ts @@ -1666,6 +1666,24 @@ export class SmoMusic { stemTicks = stemTicks * 2; return SmoMusic.ticksToDuration[stemTicks]; } + //todo: figure out a better name + // ## closestSmoDuration + // ## Description: + // return the closest smo duration >= to the actual number of ticks. Used in beaming + // triplets which have fewer ticks then their stem would normally indicate. + static closestSmoDuration(ticks: number): SimpleDuration { + let stemTicks = SmoMusic.highestDuration; + + // The stem value is the type on the non-tuplet note, e.g. 1/8 note + // for a triplet. + while (ticks <= stemTicks) { + stemTicks = stemTicks / 2; + } + stemTicks = stemTicks * 2; + return SmoMusic.validDurations[stemTicks]; + } + + // ### closestDurationTickLtEq // Price is right style, closest tick value without going over. Used to pad diff --git a/src/smo/data/note.ts b/src/smo/data/note.ts index 59e3dc0f..824a0cd5 100644 --- a/src/smo/data/note.ts +++ b/src/smo/data/note.ts @@ -28,9 +28,9 @@ export type NoteStringParam = 'noteHead' | 'clef'; // @internal export const NoteStringParams: NoteStringParam[] = ['noteHead', 'clef']; // @internal -export type NoteNumberParam = 'beamBeats' | 'flagState' | 'stemTicks'; +export type NoteNumberParam = 'beamBeats' | 'flagState'; // @internal -export const NoteNumberParams: NoteNumberParam[] = ['beamBeats', 'flagState', 'stemTicks']; +export const NoteNumberParams: NoteNumberParam[] = ['beamBeats', 'flagState']; // @internal export type NoteBooleanParam = 'hidden' | 'endBeam' | 'isCue'; // @internal @@ -96,7 +96,7 @@ export interface SmoNoteParams { /** * if this note is part of a tuplet */ - tuplet: TupletInfo | undefined, + tupletId: string | null, /* * If a custom tab note is assigned to this note */ @@ -192,7 +192,7 @@ export interface SmoNoteParamsSer { /** * if this note is part of a tuplet */ - tuplet: SmoTupletParamsSer | undefined, + tupletId?: string, /** * If a custom tab note is here, keep track of it */ @@ -263,6 +263,7 @@ export class SmoNote implements Transposable { 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]; @@ -276,15 +277,18 @@ export class SmoNote implements Transposable { if (params.tabNote) { this.tabNote = new SmoTabNote(params.tabNote); } - const ticks = params.ticks ? params.ticks : defs.ticks; 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 : ''; - if (params.tuplet) { - this.tuplet = params.tuplet; + // 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' @@ -309,7 +313,7 @@ export class SmoNote implements Transposable { noteType: NoteType = 'n'; fillStyle: string = ''; hidden: boolean = false; - tuplet: TupletInfo | null = null; + tupletId: string | null = null; tones: SmoMicrotone[] = []; endBeam: boolean = false; ticks: Ticks = { numerator: 4096, denominator: 1, remainder: 0 }; @@ -328,7 +332,8 @@ export class SmoNote implements Transposable { */ static get parameterArray() { return ['ticks', 'pitches', 'noteType', 'tuplet', 'clef', 'isCue', 'stemTicks', - 'endBeam', 'beamBeats', 'flagState', 'noteHead', 'fillStyle', 'hidden', 'arpeggio', 'clefNote']; + 'endBeam', 'beamBeats', 'flagState', 'noteHead', 'fillStyle', 'hidden', 'arpeggio', 'clefNote', + 'tupletId']; } /** * Default constructor parameters. We always return a copy so the caller can modify it @@ -697,7 +702,7 @@ export class SmoNote implements Transposable { * Return true if this note is part of a tuplet */ get isTuplet(): boolean { - return this.tuplet !== null && typeof(this.tuplet.id) !== 'undefined'; + return typeof(this.tupletId) !== 'undefined' && this.tupletId !== null && this.tupletId.length > 0; } addMicrotone(tone: SmoMicrotone) { @@ -902,6 +907,14 @@ export class SmoNote implements Transposable { * @returns */ static deserialize(jsonObj: any) { + //legacy note + if (jsonObj.ticks && jsonObj.stemTicks === undefined) { + if (jsonObj.tupletId || jsonObj.tuplet) { + jsonObj['stemTicks'] = SmoMusic.closestSmoDuration(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) => { diff --git a/src/smo/data/tuplet.ts b/src/smo/data/tuplet.ts index a9207663..4a7fcb69 100644 --- a/src/smo/data/tuplet.ts +++ b/src/smo/data/tuplet.ts @@ -306,16 +306,18 @@ export class SmoTuplet { // Legacy schema did not have notesOccupied, we need to calculate it. if ((jsonObj as any).notes !== undefined) { const numberOfNotes = (jsonObj as any).notes.length; - tupJson.endIndex = jsonObj.startIndex + numberOfNotes; + tupJson.endIndex = jsonObj.startIndex + numberOfNotes - 1; tupJson.notesOccupied = jsonObj.totalTicks / jsonObj.stemTicks; } smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj, tupJson); const tuplet = new SmoTuplet(tupJson); tuplet.parentTuplet = jsonObj.parentTuplet ? jsonObj.parentTuplet : null; - for (let i = 0; i < jsonObj.childrenTuplets.length; i++) { - const childTuplet = SmoTuplet.deserialize(jsonObj.childrenTuplets[i]); - tuplet.childrenTuplets.push(childTuplet); + 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; } diff --git a/src/smo/xform/beamers.ts b/src/smo/xform/beamers.ts index 8e3c4684..c6d8d20a 100644 --- a/src/smo/xform/beamers.ts +++ b/src/smo/xform/beamers.ts @@ -231,10 +231,13 @@ export class SmoBeamer { } public static areTupletElementsTheSame(noteOne: SmoNote, noteTwo: SmoNote): boolean { - if (noteOne.tuplet === null && noteTwo.tuplet === null) { + if (typeof(noteOne.tupletId) === 'undefined' && typeof(noteTwo.tupletId) === 'undefined') { return true; } - if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tuplet!.id == noteTwo.tuplet!.id) { + if (noteOne.tupletId === null && noteTwo.tupletId === null) { + return true; + } + if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tupletId == noteTwo.tupletId) { return true; } diff --git a/src/smo/xform/tickDuration.ts b/src/smo/xform/tickDuration.ts index 034c5c59..344653dc 100644 --- a/src/smo/xform/tickDuration.ts +++ b/src/smo/xform/tickDuration.ts @@ -281,7 +281,7 @@ export class SmoStretchNoteActor extends TickIteratorBase { } private areNotesInSameTuplet(noteOne: SmoNote, noteTwo: SmoNote): boolean { - if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tuplet!.id == noteTwo.tuplet!.id) { + if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tupletId == noteTwo.tupletId) { return true; } return false; @@ -369,7 +369,7 @@ export class SmoMakeTupletActor extends TickIteratorBase { const note: SmoNote = SmoNote.cloneWithDuration(originalNote, { numerator: Math.floor(numerator), denominator: 1, remainder: 0 }, stemTicks); // Don't clone modifiers, except for first one. note.textModifiers = i === 0 ? note.textModifiers : []; - note.tuplet = tuplet.attrs; + note.tupletId = tuplet.attrs.id; tupletNotes.push(note); } if (numerator % 1) { @@ -422,7 +422,7 @@ export class SmoUnmakeTupletActor extends TickIteratorBase { const ticks = tuplet.totalTicks; const nn: SmoNote = SmoNote.cloneWithDuration(note, { numerator: ticks, denominator: 1, remainder: 0 }); - nn.tuplet = null; + nn.tupletId = null; SmoTupletTree.removeTupletForNoteIndex(this.measure, this.voice, index); SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, this.startIndex, this.startIndex - this.endIndex); From 3ee8db7aa38cd9df0a015f1bf3f4f7e8c5739619 Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Sun, 15 Sep 2024 01:14:58 +0200 Subject: [PATCH 12/14] implemented toVex export --- src/render/vex/toVex.ts | 75 +++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/src/render/vex/toVex.ts b/src/render/vex/toVex.ts index 5f5d58f9..6ad62c6b 100644 --- a/src/render/vex/toVex.ts +++ b/src/render/vex/toVex.ts @@ -5,7 +5,7 @@ import { SmoNote } from '../../smo/data/note'; import { SmoMeasure, SmoVoice, MeasureTickmaps } from '../../smo/data/measure'; import { SmoScore } from '../../smo/data/score'; import { SmoArticulation, SmoLyric, SmoOrnament } from '../../smo/data/noteModifiers'; -import { VexFlow, StaveNoteStruct, TupletOptions, vexOrnaments } from '../../common/vex'; +import {VexFlow, StaveNoteStruct, TupletOptions, vexOrnaments, getVexTuplets} from '../../common/vex'; import { SmoBarline, SmoRehearsalMark } from '../../smo/data/measureModifiers'; import { SmoSelection, SmoSelector } from '../../smo/xform/selections'; import { SmoSystemStaff } from '../../smo/data/systemStaff'; @@ -14,6 +14,7 @@ import { SmoSystemGroup } from '../../smo/data/scoreModifiers'; import { StaffModifierBase, SmoStaffHairpin, SmoSlur, SmoTie, SmoStaffTextBracket } from '../../smo/data/staffModifiers'; import { toVexBarlineType, vexBarlineType, vexBarlinePosition, toVexBarlinePosition, leftConnectorVx, rightConnectorVx, toVexVolta, getVexChordBlocks } from '../../render/vex/smoAdapter'; +import {SmoTuplet} from "../../smo/data/tuplet"; @@ -78,11 +79,6 @@ function smoNoteToGraceNotes(smoNote: SmoNote, strs: string[]) { } } function smoNoteToStaveNote(smoNote: SmoNote) { - // const duration = - // smoNote.isTuplet ? - // SmoMusic.closestVexDuration(smoNote.tickCount) : - // SmoMusic.ticksToDuration[smoNote.tickCount]; - const duration = SmoMusic.ticksToDuration[smoNote.stemTicks]; const sn: StaveNoteStruct = { clef: smoNote.clef, @@ -427,31 +423,40 @@ function createBeamGroups(smoMeasure: SmoMeasure, strs: string[]) { } }); } -//todo nenad: implement this function createTuplets(smoMeasure: SmoMeasure, strs: string[]) { - // smoMeasure.voices.forEach((voice, voiceIx) => { - // const tps = smoMeasure.tupletTrees.filter((tp) => tp.voice === voiceIx); - // for (var i = 0; i < tps.length; ++i) { - // const tp = tps[i]; - // const nar: string[] = []; - // for ( let note of smoMeasure.tupletNotes(tp)) { - // const vexNote = `${note.attrs.id}`; - // nar.push(vexNote); - // } - // const direction = smoMeasure.getStemDirectionForTuplet(tp) === SmoNote.flagStates.up ? - // VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM; - // const tpParams: TupletOptions = { - // num_notes: tp.numNotes, - // notes_occupied: tp.notesOccupied, - // ratioed: false, - // bracketed: true, - // location: direction - // }; - // const tpParamString = JSON.stringify(tpParams); - // const narString = '[' + nar.join(',') + ']'; - // strs.push(`const ${tp.attrs.id} = new VF.Tuplet(${narString}, JSON.parse('${tpParamString}'));`); - // } - // }); + smoMeasure.voices.forEach((voice, voiceIx) => { + for (let i = 0; i < smoMeasure.tupletTrees.length; ++i) { + const tupletTree = smoMeasure.tupletTrees[i]; + if (tupletTree.voice !== voiceIx) { + continue; + } + const traverseTupletTree = ( parentTuplet: SmoTuplet): void => { + const vexNotes = []; + for (let smoNote of smoMeasure.tupletNotes(parentTuplet)) { + const vexNote = `${smoNote.attrs.id}`; + vexNotes.push(vexNote); + } + const direction = smoMeasure.getStemDirectionForTuplet(parentTuplet) === SmoNote.flagStates.up ? + VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM; + const tpParams: TupletOptions = { + num_notes: parentTuplet.numNotes, + notes_occupied: parentTuplet.notesOccupied, + ratioed: false, + bracketed: true, + location: direction + }; + const tpParamString = JSON.stringify(tpParams); + const vexNotesString = '[' + vexNotes.join(',') + ']'; + strs.push(`const ${parentTuplet.attrs.id} = new VF.Tuplet(${vexNotesString}, JSON.parse('${tpParamString}'));`); + + for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { + const tuplet = parentTuplet.childrenTuplets[i]; + traverseTupletTree(tuplet); + } + } + traverseTupletTree(tupletTree.tuplet); + } + }); } function createMeasure(smoMeasure: SmoMeasure, heightOffset: number, strs: string[]) { const ssid = 'stave' + smoMeasure.id; @@ -495,9 +500,15 @@ function createMeasure(smoMeasure: SmoMeasure, heightOffset: number, strs: strin strs.push(`${bg.attrs.id}.setContext(context);`); strs.push(`${bg.attrs.id}.draw();`) }); - //todo nenad: implement this smoMeasure.tupletTrees.forEach((tp) => { - // strs.push(`${tp.attrs.id}.setContext(context).draw();`) + const traverseTupletTree = ( parentTuplet: SmoTuplet): void => { + strs.push(`${parentTuplet.attrs.id}.setContext(context).draw();`) + for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { + const tuplet = parentTuplet.childrenTuplets[i]; + traverseTupletTree(tuplet); + } + } + traverseTupletTree(tp.tuplet); }); } // ## SmoToVex From 563e53c5c1b1553344c2f30987a672a27decf56e Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Sun, 15 Sep 2024 19:18:30 +0200 Subject: [PATCH 13/14] adjusted midiToSmo export --- src/common/vex.ts | 7 ------- src/smo/midi/midiToSmo.ts | 41 ++++++++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/common/vex.ts b/src/common/vex.ts index 7cf4a873..83f11e0b 100644 --- a/src/common/vex.ts +++ b/src/common/vex.ts @@ -204,13 +204,6 @@ export function getVexTuplets(params: SmoVexTupletParams) { return vexTuplet; } export function getVexNoteParameters(params: CreateVexNoteParams): { noteParams: StaveNoteStruct, duration: string } { - // If this is a tuplet, we only get the duration so the appropriate stem - // can be rendered. Vex calculates the actual ticks later when the tuplet is made - // var duration = - // params.isTuplet ? - // params.closestTicks : - // params.exactTicks; - var duration: any = params.stemTicks; if (typeof (duration) === 'undefined') { console.warn('bad duration in measure ' + params.measureIndex); diff --git a/src/smo/midi/midiToSmo.ts b/src/smo/midi/midiToSmo.ts index 57e26bac..21d91288 100644 --- a/src/smo/midi/midiToSmo.ts +++ b/src/smo/midi/midiToSmo.ts @@ -14,7 +14,7 @@ import { SmoScore } from "../data/score"; import { SmoLayoutManager } from "../data/scoreModifiers"; import { SmoTie } from "../data/staffModifiers"; import { SmoSystemStaff } from "../data/systemStaff"; -import { SmoTuplet } from "../data/tuplet"; +import {SmoTuplet, SmoTupletTree} from "../data/tuplet"; import { SmoOperation } from "../xform/operations"; export type MidiEventType = 'text' | 'copyrightNotice' | 'trackName' | 'instrumentName' | 'lyrics' | 'marker' | @@ -305,18 +305,20 @@ export class MidiToSmo { const note = new SmoNote(defs); SmoNote.sortPitches(note); measure.voices[0].notes.push(note); - //todo: needs to be check for nested tuplets - // if (ev.tupletInfo !== null && ev.tupletInfo.isLast === true) { - // const voiceLen = measure.voices[0].notes.length; - // const tupletNotes = [note, measure.voices[0].notes[voiceLen - 2], measure.voices[0].notes[voiceLen - 3]]; - // const defs = SmoTuplet.defaults; - // defs.notes = tupletNotes; - // defs.stemTicks = ev.tupletInfo.stemTicks; - // defs.numNotes = ev.tupletInfo.numNotes; - // defs.totalTicks = ev.tupletInfo.totalTicks; - // defs.startIndex = voiceLen - 3; - // measure.tuplets.push(new SmoTuplet(defs)); - // } + if (ev.tupletInfo !== null && ev.tupletInfo.isLast === true) { + const voiceLen = measure.voices[0].notes.length; + const tupletNotes = [note, measure.voices[0].notes[voiceLen - 2], measure.voices[0].notes[voiceLen - 3]]; + const defs = SmoTuplet.defaults; + defs.stemTicks = ev.tupletInfo.stemTicks; + defs.numNotes = ev.tupletInfo.numNotes; + defs.totalTicks = ev.tupletInfo.totalTicks; + defs.startIndex = voiceLen - 3; + defs.endIndex = voiceLen - 1; + const tuplet = new SmoTuplet(defs); + this.adjustTupletNotes(tupletNotes, tuplet); + const tupletTree: SmoTupletTree = new SmoTupletTree({tuplet: tuplet}); + measure.tupletTrees.push(tupletTree); + } if (ev.isTied) { this.addToTieMap(measureIndex); } @@ -327,6 +329,19 @@ export class MidiToSmo { }); return measures; } + + adjustTupletNotes(notes: SmoNote[], tuplet: SmoTuplet) { + const numerator = tuplet.totalTicks / tuplet.numNotes; + for (let i = 0; i < notes.length; ++i) { + const note = notes[i]; + note.ticks = { numerator: Math.floor(numerator), denominator: 1, remainder: 0 } + note.stemTicks = tuplet.stemTicks; + note.tupletId = tuplet.attrs.id; + } + if (numerator % 1) { + notes[0].ticks.numerator += 1; + } + } /** * @param ticks * @returns the length in ticks of a triplet, if this looks like a triplet. Otherwise 0 From 51ad7ef21d46014086646a2f43c7570bf9d57edb Mon Sep 17 00:00:00 2001 From: nenadstrangar Date: Sun, 15 Sep 2024 19:24:25 +0200 Subject: [PATCH 14/14] renamed closestSmoDuration to closestBeamDuration --- src/smo/data/music.ts | 6 +++--- src/smo/data/note.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/smo/data/music.ts b/src/smo/data/music.ts index 0efb02a2..02327835 100644 --- a/src/smo/data/music.ts +++ b/src/smo/data/music.ts @@ -1666,12 +1666,12 @@ export class SmoMusic { stemTicks = stemTicks * 2; return SmoMusic.ticksToDuration[stemTicks]; } - //todo: figure out a better name - // ## closestSmoDuration + + // ## closestBeamDuration // ## Description: // return the closest smo duration >= to the actual number of ticks. Used in beaming // triplets which have fewer ticks then their stem would normally indicate. - static closestSmoDuration(ticks: number): SimpleDuration { + static closestBeamDuration(ticks: number): SimpleDuration { let stemTicks = SmoMusic.highestDuration; // The stem value is the type on the non-tuplet note, e.g. 1/8 note diff --git a/src/smo/data/note.ts b/src/smo/data/note.ts index 824a0cd5..a5b61985 100644 --- a/src/smo/data/note.ts +++ b/src/smo/data/note.ts @@ -910,7 +910,7 @@ export class SmoNote implements Transposable { //legacy note if (jsonObj.ticks && jsonObj.stemTicks === undefined) { if (jsonObj.tupletId || jsonObj.tuplet) { - jsonObj['stemTicks'] = SmoMusic.closestSmoDuration(jsonObj.ticks.numerator / jsonObj.ticks.denominator + jsonObj.ticks.remainder)!.ticks; + 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; }