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/19] 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|6
XIfJqGyO-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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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;
}
From bee29e988acc1b01b68bcd0dce5489ecac926b89 Mon Sep 17 00:00:00 2001
From: nenadstrangar
Date: Mon, 16 Sep 2024 10:58:31 +0200
Subject: [PATCH 15/19] adding notes on legacy tuplet deserialization
---
src/smo/data/measure.ts | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/src/smo/data/measure.ts b/src/smo/data/measure.ts
index b6414c1c..856b9e99 100644
--- a/src/smo/data/measure.ts
+++ b/src/smo/data/measure.ts
@@ -648,6 +648,22 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable {
if ((jsonObj as any).tuplets !== undefined) {
for (j = 0; j < (jsonObj as any).tuplets.length; ++j) {
const tupJson = (jsonObj as any).tuplets[j];
+
+ const tupletNotes: SmoNote[] = [];
+ params.voices.forEach((voice) => {
+ voice.notes.forEach((note) => {
+ if (note.isTuplet && note.tupletId === tupJson.id) {
+ tupletNotes.push(note);
+ }
+ });
+ });
+
+ // Bug fix: A tuplet with no notes may be been overwritten
+ // in a copy/paste operation
+ if (tupletNotes.length > 0) {
+ tupJson.notes = tupletNotes;
+ }
+
const tuplet: SmoTuplet = SmoTuplet.deserialize(tupJson);
const tupletTree: SmoTupletTree = new SmoTupletTree({tuplet: tuplet});
params.tupletTrees.push(tupletTree);
From 6746be3e681270a7a53e761bf012f6d9409bfae6 Mon Sep 17 00:00:00 2001
From: nenadstrangar
Date: Mon, 16 Sep 2024 17:19:41 +0200
Subject: [PATCH 16/19] adding notes on legacy tuplet deserialization
---
src/smo/data/measure.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/smo/data/measure.ts b/src/smo/data/measure.ts
index 856b9e99..a91d779b 100644
--- a/src/smo/data/measure.ts
+++ b/src/smo/data/measure.ts
@@ -652,7 +652,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable {
const tupletNotes: SmoNote[] = [];
params.voices.forEach((voice) => {
voice.notes.forEach((note) => {
- if (note.isTuplet && note.tupletId === tupJson.id) {
+ if (note.isTuplet && note.tupletId === tupJson.attrs.id) {
tupletNotes.push(note);
}
});
From 4a3dd9382575b11d3fbcd5d7178532740e08168d Mon Sep 17 00:00:00 2001
From: nenadstrangar
Date: Mon, 16 Sep 2024 17:59:15 +0200
Subject: [PATCH 17/19] adding notes on legacy tuplet deserialization
---
src/smo/data/measure.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/smo/data/measure.ts b/src/smo/data/measure.ts
index a91d779b..083349f5 100644
--- a/src/smo/data/measure.ts
+++ b/src/smo/data/measure.ts
@@ -649,6 +649,11 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable {
for (j = 0; j < (jsonObj as any).tuplets.length; ++j) {
const tupJson = (jsonObj as any).tuplets[j];
+ // Legacy schema had attrs.id, now it is just id
+ if ((tupJson as any).attrs && (tupJson as any).attrs.id) {
+ tupJson.id = (tupJson as any).attrs.id;
+ }
+
const tupletNotes: SmoNote[] = [];
params.voices.forEach((voice) => {
voice.notes.forEach((note) => {
From f6ef6cc0469c5b9a11a3e9a6df1ec27113b53f3f Mon Sep 17 00:00:00 2001
From: nenadstrangar
Date: Thu, 19 Sep 2024 00:24:35 +0200
Subject: [PATCH 18/19] fix on xml import
---
src/smo/data/measure.ts | 10 +++++++++-
src/smo/data/tuplet.ts | 9 +++------
src/smo/mxml/xmlHelpers.ts | 8 +++++++-
src/smo/mxml/xmlState.ts | 14 +++++++++-----
4 files changed, 28 insertions(+), 13 deletions(-)
diff --git a/src/smo/data/measure.ts b/src/smo/data/measure.ts
index 083349f5..fcf9c0de 100644
--- a/src/smo/data/measure.ts
+++ b/src/smo/data/measure.ts
@@ -655,10 +655,16 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable {
}
const tupletNotes: SmoNote[] = [];
+ let startIndex: number | null = null;
params.voices.forEach((voice) => {
- voice.notes.forEach((note) => {
+ voice.notes.forEach((note, index) => {
if (note.isTuplet && note.tupletId === tupJson.attrs.id) {
tupletNotes.push(note);
+ //we cannot trust startIndex coming from legacy json
+ //we need to count index of the first note in the tuplet
+ if (startIndex === null) {
+ startIndex = index;
+ }
}
});
});
@@ -667,6 +673,8 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable {
// in a copy/paste operation
if (tupletNotes.length > 0) {
tupJson.notes = tupletNotes;
+ tupJson.startIndex = startIndex;
+ tupJson.endIndex = tupJson.startIndex + tupletNotes.length - 1;
}
const tuplet: SmoTuplet = SmoTuplet.deserialize(tupJson);
diff --git a/src/smo/data/tuplet.ts b/src/smo/data/tuplet.ts
index 4a7fcb69..772174bc 100644
--- a/src/smo/data/tuplet.ts
+++ b/src/smo/data/tuplet.ts
@@ -301,16 +301,13 @@ export class SmoTuplet {
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
+ smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj, tupJson);
// 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 - 1;
- tupJson.notesOccupied = jsonObj.totalTicks / jsonObj.stemTicks;
+ //todo: notesOccupied can probably be removed
+ tupJson.notesOccupied = tupJson.totalTicks / tupJson.stemTicks;
}
- smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj, tupJson);
const tuplet = new SmoTuplet(tupJson);
tuplet.parentTuplet = jsonObj.parentTuplet ? jsonObj.parentTuplet : null;
if (jsonObj.childrenTuplets !== undefined) {
diff --git a/src/smo/mxml/xmlHelpers.ts b/src/smo/mxml/xmlHelpers.ts
index 2893e8bf..f78299b3 100644
--- a/src/smo/mxml/xmlHelpers.ts
+++ b/src/smo/mxml/xmlHelpers.ts
@@ -413,7 +413,13 @@ export class XmlHelpers {
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;
+ const noteTypeNode = noteNode.querySelector('type');
+ let normalType: number | null = null;
+ if (normalTypeNode) {
+ normalType = normalTypeNode.textContent ? XmlHelpers.noteTypesToSmoMap[normalTypeNode.textContent] ?? null : null;
+ } else if (noteTypeNode) {
+ normalType = noteTypeNode.textContent ? XmlHelpers.noteTypesToSmoMap[noteTypeNode.textContent] ?? null : null;
+ }
if (actualNotesNode?.textContent && normalNotesNode?.textContent && normalType) {
const actualNotes = parseInt(actualNotesNode.textContent, 10);
const normalNotes = parseInt(normalNotesNode.textContent, 10);
diff --git a/src/smo/mxml/xmlState.ts b/src/smo/mxml/xmlState.ts
index 971cdcfc..7279e4ad 100644
--- a/src/smo/mxml/xmlState.ts
+++ b/src/smo/mxml/xmlState.ts
@@ -611,19 +611,23 @@ export class XmlState {
});
}
addTupletsToMeasure(smoMeasure: SmoMeasure, staffId: number, voiceId: number) {
- const tupletStates = this.findCompletedTupletStatesByStaffAndVoice(staffId, voiceId);
+ const tupletStates = this.findAndRemoveCompletedTupletStatesByStaffAndVoice(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[] = [];
+ private findAndRemoveCompletedTupletStatesByStaffAndVoice(staffId: number, voiceId: number): XmlTupletState[] {
+ const remainingXmlTupletStates: XmlCompletedTupletState[] = [];
+ const tupletStatesForReturn: XmlTupletState[] = [];
this.completedTupletStates.forEach((completedTupletState) => {
if (completedTupletState.staffId === staffId && completedTupletState.voiceId === voiceId) {
- tupletStates.push(completedTupletState.tupletState);
+ tupletStatesForReturn.push(completedTupletState.tupletState);
+ } else {
+ remainingXmlTupletStates.push(completedTupletState)
}
});
- return tupletStates;
+ this.completedTupletStates = remainingXmlTupletStates;
+ return tupletStatesForReturn;
}
private buildXmlTupletStateTrees(tupletStates: XmlTupletState[]): XmlTupletStateTreeNode[] {
let sortedTupletStates = this.sortTupletStates(tupletStates);
From 22fbee9b3ca21a3fcf63bcceba54c7384babe849 Mon Sep 17 00:00:00 2001
From: nenadstrangar
Date: Sat, 21 Sep 2024 13:27:58 +0200
Subject: [PATCH 19/19] sync note.tupletId with tuplet.id after serialization
---
src/smo/data/measure.ts | 4 ++++
src/smo/data/tuplet.ts | 22 +++++++++++++++++++++-
src/smo/mxml/xmlToSmo.ts | 2 ++
3 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/src/smo/data/measure.ts b/src/smo/data/measure.ts
index fcf9c0de..883ccc0e 100644
--- a/src/smo/data/measure.ts
+++ b/src/smo/data/measure.ts
@@ -682,6 +682,10 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable {
params.tupletTrees.push(tupletTree);
}
}
+ if (params.tupletTrees.length) {
+ SmoTupletTree.syncTupletIds(params.tupletTrees, voices)
+ }
+
params.modifiers = modifiers;
const measure = new SmoMeasure(params);
// Handle migration for measure-mapped parameters
diff --git a/src/smo/data/tuplet.ts b/src/smo/data/tuplet.ts
index 772174bc..32fb25cb 100644
--- a/src/smo/data/tuplet.ts
+++ b/src/smo/data/tuplet.ts
@@ -9,7 +9,7 @@ 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 {SmoMeasure, SmoVoice} from './measure';
import {tuplets} from "vexflow_smoosic/build/esm/types/tests/formatter/tests";
@@ -39,6 +39,26 @@ export class SmoTupletTree {
this.tuplet = params.tuplet;
}
+ static syncTupletIds(tupletTrees: SmoTupletTree[], voices: SmoVoice[]) {
+ const traverseTupletTree = (parentTuplet: SmoTuplet): void => {
+ const notes: SmoNote[] = voices[parentTuplet.voice].notes;
+ for (let i = parentTuplet.startIndex; i <= parentTuplet.endIndex; i++) {
+ const note: SmoNote = notes[i];
+ note.tupletId = parentTuplet.attrs.id;
+ }
+ for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) {
+ const tuplet = parentTuplet.childrenTuplets[i];
+ traverseTupletTree(tuplet);
+ }
+ };
+
+ //traverse tuplet tree
+ for (let i = 0; i < tupletTrees.length; i++) {
+ const tupletTree: SmoTupletTree = tupletTrees[i];
+ traverseTupletTree(tupletTree.tuplet);
+ }
+ }
+
static adjustTupletIndexes(tupletTrees: SmoTupletTree[], voice: number, startTick: number, diff: number) {
const traverseTupletTree = (parentTuplet: SmoTuplet): void => {
if (parentTuplet.endIndex >= startTick) {
diff --git a/src/smo/mxml/xmlToSmo.ts b/src/smo/mxml/xmlToSmo.ts
index db54bc68..738171f8 100644
--- a/src/smo/mxml/xmlToSmo.ts
+++ b/src/smo/mxml/xmlToSmo.ts
@@ -19,6 +19,7 @@ import { Pitch, PitchKey, Clef } from '../data/common';
import { SmoOperation } from '../xform/operations';
import { SmoInstrument, SmoSlur, SmoTie, TieLine } from '../data/staffModifiers';
import { SmoPartInfo } from '../data/partInfo';
+import {SmoTupletTree} from "../data/tuplet";
/**
* A class that takes a music XML file and outputs a {@link SmoScore}
@@ -805,6 +806,7 @@ export class XmlToSmo {
const voiceId = smoMeasure.voices.length - 1;
xmlState.addTupletsToMeasure(smoMeasure, staffData.clefInfo.staffId, voiceId);
});
+ SmoTupletTree.syncTupletIds(smoMeasure.tupletTrees, smoMeasure.voices);
if (smoMeasure.voices.length === 0) {
smoMeasure.voices.push({ notes: SmoMeasure.getDefaultNotes(smoMeasure) });
}