@scorelabs/core 1.0.11 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/importers/MusicXMLParser.d.ts +2 -0
  2. package/dist/importers/MusicXMLParser.js +518 -71
  3. package/dist/models/Measure.d.ts +19 -2
  4. package/dist/models/Measure.js +79 -27
  5. package/dist/models/Note.d.ts +35 -5
  6. package/dist/models/Note.js +97 -42
  7. package/dist/models/NoteSet.d.ts +11 -2
  8. package/dist/models/NoteSet.js +33 -2
  9. package/dist/models/Part.d.ts +1 -1
  10. package/dist/models/Part.js +2 -2
  11. package/dist/models/Score.d.ts +5 -5
  12. package/dist/models/Score.js +144 -121
  13. package/dist/models/Staff.d.ts +1 -1
  14. package/dist/models/Staff.js +2 -2
  15. package/dist/types/AccidentalDisplay.d.ts +6 -0
  16. package/dist/types/AccidentalDisplay.js +1 -0
  17. package/dist/types/Arpeggio.d.ts +2 -1
  18. package/dist/types/Arpeggio.js +1 -0
  19. package/dist/types/Articulation.d.ts +3 -1
  20. package/dist/types/Articulation.js +2 -0
  21. package/dist/types/Beam.d.ts +4 -0
  22. package/dist/types/Beam.js +1 -0
  23. package/dist/types/Duration.d.ts +1 -1
  24. package/dist/types/Duration.js +14 -14
  25. package/dist/types/Grace.d.ts +7 -0
  26. package/dist/types/Grace.js +1 -0
  27. package/dist/types/Hairpin.d.ts +2 -1
  28. package/dist/types/Language.d.ts +6 -0
  29. package/dist/types/Language.js +7 -0
  30. package/dist/types/Lyric.d.ts +2 -0
  31. package/dist/types/Ornament.d.ts +23 -1
  32. package/dist/types/Ornament.js +9 -0
  33. package/dist/types/Pedal.d.ts +3 -2
  34. package/dist/types/Placement.d.ts +7 -0
  35. package/dist/types/Placement.js +1 -0
  36. package/dist/types/Repeat.d.ts +1 -0
  37. package/dist/types/RestDisplay.d.ts +6 -0
  38. package/dist/types/RestDisplay.js +1 -0
  39. package/dist/types/Slur.d.ts +1 -0
  40. package/dist/types/Technical.d.ts +20 -0
  41. package/dist/types/Technical.js +5 -0
  42. package/dist/types/Tempo.d.ts +0 -1
  43. package/dist/types/Tie.d.ts +4 -0
  44. package/dist/types/Tie.js +1 -0
  45. package/dist/types/Tuplet.d.ts +6 -0
  46. package/dist/types/User.d.ts +13 -7
  47. package/dist/types/User.js +9 -7
  48. package/dist/types/index.d.ts +8 -0
  49. package/dist/types/index.js +8 -0
  50. package/package.json +2 -1
@@ -12,7 +12,6 @@ export class Score {
12
12
  parts;
13
13
  bpm;
14
14
  tempoDuration;
15
- tempoIsDotted;
16
15
  copyright;
17
16
  lyricist;
18
17
  swing;
@@ -20,7 +19,7 @@ export class Score {
20
19
  genre;
21
20
  tempoText;
22
21
  tempoDotCount;
23
- constructor(title, composer, timeSignature, keySignature, parts, bpm = 120, tempoDuration = Duration.Quarter, tempoIsDotted = false, copyright = '', lyricist = '', swing = false, subtitle = '', genre = Genre.Unknown, tempoText = '', tempoDotCount = 0) {
22
+ constructor(title, composer, timeSignature, keySignature, parts, bpm = 120, tempoDuration = Duration.Quarter, copyright = '', lyricist = '', swing = false, subtitle = '', genre = Genre.Unknown, tempoText = '', tempoDotCount = 0) {
24
23
  this.title = title;
25
24
  this.composer = composer;
26
25
  this.timeSignature = timeSignature;
@@ -28,7 +27,6 @@ export class Score {
28
27
  this.parts = parts;
29
28
  this.bpm = bpm;
30
29
  this.tempoDuration = tempoDuration;
31
- this.tempoIsDotted = tempoIsDotted;
32
30
  this.copyright = copyright;
33
31
  this.lyricist = lyricist;
34
32
  this.swing = swing;
@@ -38,19 +36,19 @@ export class Score {
38
36
  this.tempoDotCount = tempoDotCount;
39
37
  }
40
38
  withTitle(title) {
41
- return new Score(title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
39
+ return new Score(title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
42
40
  }
43
41
  withComposer(composer) {
44
- return new Score(this.title, composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
42
+ return new Score(this.title, composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
45
43
  }
46
44
  withSubtitle(subtitle) {
47
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, subtitle, this.genre, this.tempoText, this.tempoDotCount);
45
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, subtitle, this.genre, this.tempoText, this.tempoDotCount);
48
46
  }
49
47
  withGenre(genre) {
50
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, genre, this.tempoText, this.tempoDotCount);
48
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, genre, this.tempoText, this.tempoDotCount);
51
49
  }
52
50
  withBpm(bpm) {
53
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
51
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
54
52
  }
55
53
  getMeasureCount() {
56
54
  return this.parts[0]?.getMeasureCount() ?? 0;
@@ -93,7 +91,7 @@ export class Score {
93
91
  }
94
92
  static fromJSON(data) {
95
93
  const parts = data.parts.map((p) => Part.fromJSON(p));
96
- return new Score(data.title, data.composer, data.timeSignature, data.keySignature, parts, data.bpm ?? 120, data.tempoDuration ?? Duration.Quarter, data.tempoIsDotted ?? false, data.copyright ?? '', data.lyricist ?? '', data.swing ?? false, data.subtitle ?? '', data.genre || Genre.Unknown, data.tempoText ?? '', data.tempoDotCount ?? (data.tempoIsDotted ? 1 : 0));
94
+ return new Score(data.title, data.composer, data.timeSignature, data.keySignature, parts, data.bpm ?? 120, data.tempoDuration ?? Duration.Quarter, data.copyright ?? '', data.lyricist ?? '', data.swing ?? false, data.subtitle ?? '', data.genre || Genre.Unknown, data.tempoText ?? '', data.tempoDotCount ?? (data.tempoIsDotted ? 1 : 0));
97
95
  }
98
96
  transpose(semitones) {
99
97
  const semitoneToFifths = {
@@ -117,7 +115,7 @@ export class Score {
117
115
  newFifths -= 12;
118
116
  while (newFifths < -7)
119
117
  newFifths += 12;
120
- return new Score(this.title, this.composer, this.timeSignature, { fifths: newFifths }, this.parts.map((p) => p.transpose(semitones)), this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
118
+ return new Score(this.title, this.composer, this.timeSignature, { fifths: newFifths }, this.parts.map((p) => p.transpose(semitones)), this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
121
119
  }
122
120
  replaceNote(partIndex, staffIndex, measureIndex, noteIndex, newNote, voiceIndex = 0) {
123
121
  if (partIndex < 0 || partIndex >= this.parts.length)
@@ -127,16 +125,15 @@ export class Score {
127
125
  return this.replaceMeasure(partIndex, staffIndex, measureIndex, updatedMeasure, true);
128
126
  }
129
127
  withKeySignature(keySig) {
130
- return new Score(this.title, this.composer, this.timeSignature, keySig, this.parts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
128
+ return new Score(this.title, this.composer, this.timeSignature, keySig, this.parts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
131
129
  }
132
130
  withTimeSignature(timeSig) {
133
- return new Score(this.title, this.composer, timeSig, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
131
+ return new Score(this.title, this.composer, timeSig, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
134
132
  }
135
133
  updateMeasureSignatures(measureIndex, signatures, autoBeam = true) {
136
- let score = this;
137
- for (let pIdx = 0; pIdx < this.parts.length; pIdx++) {
138
- for (let sIdx = 0; sIdx < this.parts[pIdx].staves.length; sIdx++) {
139
- const currentMsr = score.parts[pIdx].staves[sIdx].measures[measureIndex];
134
+ const newParts = this.parts.map((part) => {
135
+ const newStaves = part.staves.map((staff) => {
136
+ const currentMsr = staff.measures[measureIndex];
140
137
  if (currentMsr) {
141
138
  let updatedMsr = currentMsr;
142
139
  if (signatures.keySignature !== undefined) {
@@ -148,45 +145,51 @@ export class Score {
148
145
  if (signatures.clef !== undefined) {
149
146
  updatedMsr = updatedMsr.withClef(signatures.clef);
150
147
  }
151
- score = score.replaceMeasure(pIdx, sIdx, measureIndex, updatedMsr, autoBeam);
148
+ if (autoBeam) {
149
+ const ts = signatures.timeSignature ?? this.getTimeSignatureAt(measureIndex);
150
+ updatedMsr = updatedMsr.autoBeam(ts);
151
+ }
152
+ const newMeasures = [...staff.measures];
153
+ newMeasures[measureIndex] = updatedMsr;
154
+ return staff.withMeasures(newMeasures);
152
155
  }
153
- }
154
- }
156
+ return staff;
157
+ });
158
+ return part.withStaves(newStaves);
159
+ });
160
+ const newScore = this.withParts(newParts);
155
161
  if (signatures.timeSignature !== undefined) {
156
- score = score.reflow(measureIndex, autoBeam);
162
+ return newScore.reflow(measureIndex, autoBeam);
157
163
  }
158
- return score;
164
+ return newScore;
159
165
  }
160
166
  /**
161
167
  * Reflows the notes starting from a specific measure index.
162
168
  * This is useful when time signatures change and notes need to be redistributed.
163
169
  */
164
170
  reflow(fromMeasureIndex, autoBeam = true) {
165
- let currentScore = this;
166
171
  const allStaves = this.getAllStaves();
167
- for (const { partIndex, staffIndex } of allStaves) {
168
- currentScore = currentScore.reflowStaff(partIndex, staffIndex, fromMeasureIndex, autoBeam);
169
- }
172
+ const scoreAfterReflow = allStaves.reduce((accScore, { partIndex, staffIndex }) => accScore.reflowStaff(partIndex, staffIndex, fromMeasureIndex, autoBeam), this);
170
173
  // Harmonize measure count across all staves
171
- const maxMeasures = Math.max(...currentScore.parts.flatMap((p) => p.staves.map((s) => s.measures.length)));
172
- const minMeasures = Math.min(...currentScore.parts.flatMap((p) => p.staves.map((s) => s.measures.length)));
174
+ const maxMeasures = Math.max(...scoreAfterReflow.parts.flatMap((p) => p.staves.map((s) => s.measures.length)));
175
+ const minMeasures = Math.min(...scoreAfterReflow.parts.flatMap((p) => p.staves.map((s) => s.measures.length)));
173
176
  if (maxMeasures !== minMeasures) {
174
- for (let pIdx = 0; pIdx < currentScore.parts.length; pIdx++) {
175
- for (let sIdx = 0; sIdx < currentScore.parts[pIdx].staves.length; sIdx++) {
176
- const staff = currentScore.parts[pIdx].staves[sIdx];
177
+ return scoreAfterReflow.parts.reduce((accScore1, part, pIdx) => {
178
+ return part.staves.reduce((accScore2, staff, sIdx) => {
177
179
  if (staff.measures.length < maxMeasures) {
178
180
  const updatedMsrs = [...staff.measures];
179
181
  for (let i = staff.measures.length; i < maxMeasures; i++) {
180
- const ts = currentScore.getTimeSignatureAt(i);
182
+ const ts = accScore2.getTimeSignatureAt(i);
181
183
  const targetDur = ts.beats * (4 / ts.beatType);
182
184
  updatedMsrs.push(new Measure([[]]).fillVoiceWithRests(0, targetDur));
183
185
  }
184
- currentScore = currentScore.replaceStaff(pIdx, sIdx, staff.withMeasures(updatedMsrs));
186
+ return accScore2.replaceStaff(pIdx, sIdx, staff.withMeasures(updatedMsrs));
185
187
  }
186
- }
187
- }
188
+ return accScore2;
189
+ }, accScore1);
190
+ }, scoreAfterReflow);
188
191
  }
189
- return currentScore;
192
+ return scoreAfterReflow;
190
193
  }
191
194
  isTied(ns) {
192
195
  return !!ns.notes[0].tie;
@@ -256,7 +259,7 @@ export class Score {
256
259
  const newNoteSets = parts.map((p, idx) => {
257
260
  const isLastOfLogical = Math.abs(toTake - logicalNote.duration) < 0.001 && idx === parts.length - 1;
258
261
  return logicalNote.ns
259
- .withDuration(p.duration, p.isDotted)
262
+ .withDuration(p.duration, p.dotCount)
260
263
  .withTie(isLastOfLogical ? !!logicalNote.ns.notes[0].tie : true);
261
264
  });
262
265
  msr = msr.withVoices(msr.voices.map((v, i) => (i === vIdx ? [...v, ...newNoteSets] : v)));
@@ -300,7 +303,7 @@ export class Score {
300
303
  }
301
304
  const newParts = [...this.parts];
302
305
  newParts[partIndex] = newParts[partIndex].replaceMeasure(staffIndex, measureIndex, measureToUse);
303
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, newParts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
306
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, newParts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
304
307
  }
305
308
  deleteNote(partIndex, staffIndex, measureIndex, noteIndex, voiceIndex = 0) {
306
309
  if (partIndex < 0 || partIndex >= this.parts.length)
@@ -309,11 +312,11 @@ export class Score {
309
312
  const updatedMeasure = measure.deleteNote(noteIndex, voiceIndex);
310
313
  return this.replaceMeasure(partIndex, staffIndex, measureIndex, updatedMeasure, true);
311
314
  }
312
- changeNoteDuration(partIndex, staffIndex, measureIndex, noteIndex, newDuration, isDotted = false, voiceIndex = 0) {
315
+ changeNoteDuration(partIndex, staffIndex, measureIndex, noteIndex, newDuration, dotCount = 0, voiceIndex = 0) {
313
316
  if (partIndex < 0 || partIndex >= this.parts.length)
314
317
  return this;
315
318
  const measure = this.parts[partIndex].staves[staffIndex].measures[measureIndex];
316
- const updatedMeasure = measure.changeNoteDuration(noteIndex, newDuration, isDotted, voiceIndex);
319
+ const updatedMeasure = measure.changeNoteDuration(noteIndex, newDuration, dotCount, voiceIndex);
317
320
  return this.replaceMeasure(partIndex, staffIndex, measureIndex, updatedMeasure, true);
318
321
  }
319
322
  moveNoteToVoice(partIndex, staffIndex, measureIndex, noteIndex, fromVoiceIndex, toVoiceIndex) {
@@ -328,78 +331,80 @@ export class Score {
328
331
  pasteNotes(partIndex, staffIndex, measureIndex, noteIndex, notesToPaste) {
329
332
  if (partIndex < 0 || partIndex >= this.parts.length)
330
333
  return this;
331
- let newScore = this;
332
- let currentMeasureIdx = measureIndex;
333
- let currentNoteIdx = noteIndex;
334
- const queue = [...notesToPaste];
335
- while (queue.length > 0) {
336
- const noteToPaste = queue.shift();
337
- const part = newScore.parts[partIndex];
334
+ const state = {
335
+ score: this,
336
+ mIdx: measureIndex,
337
+ nIdx: noteIndex,
338
+ queue: [...notesToPaste],
339
+ };
340
+ while (state.queue.length > 0) {
341
+ const noteToPaste = state.queue.shift();
342
+ const part = state.score.parts[partIndex];
338
343
  if (!part)
339
344
  break;
340
345
  const staff = part.staves[staffIndex];
341
- if (!staff || currentMeasureIdx >= staff.measures.length)
346
+ if (!staff || state.mIdx >= staff.measures.length)
342
347
  break;
343
- const measure = staff.measures[currentMeasureIdx];
344
- if (currentNoteIdx >= measure.notes.length) {
345
- currentMeasureIdx++;
346
- currentNoteIdx = 0;
347
- queue.unshift(noteToPaste);
348
+ const measure = staff.measures[state.mIdx];
349
+ if (state.nIdx >= measure.notes.length) {
350
+ state.mIdx++;
351
+ state.nIdx = 0;
352
+ state.queue.unshift(noteToPaste);
348
353
  continue;
349
354
  }
350
355
  let available = 0;
351
- for (let i = currentNoteIdx; i < measure.notes.length; i++)
356
+ for (let i = state.nIdx; i < measure.notes.length; i++)
352
357
  available += measure.notes[i].getDurationValue();
353
358
  const needed = noteToPaste.getDurationValue();
354
359
  if (needed <= available + 0.001) {
355
- newScore = newScore.changeNoteDuration(partIndex, staffIndex, currentMeasureIdx, currentNoteIdx, noteToPaste.duration, noteToPaste.isDotted);
356
- newScore = newScore.replaceNote(partIndex, staffIndex, currentMeasureIdx, currentNoteIdx, noteToPaste);
357
- currentNoteIdx++;
360
+ state.score = state.score.changeNoteDuration(partIndex, staffIndex, state.mIdx, state.nIdx, noteToPaste.duration, noteToPaste.dotCount);
361
+ state.score = state.score.replaceNote(partIndex, staffIndex, state.mIdx, state.nIdx, noteToPaste);
362
+ state.nIdx++;
358
363
  }
359
364
  else {
360
365
  const parts = decomposeDuration(available);
361
366
  if (parts.length === 0) {
362
- currentMeasureIdx++;
363
- currentNoteIdx = 0;
364
- queue.unshift(noteToPaste);
367
+ state.mIdx++;
368
+ state.nIdx = 0;
369
+ state.queue.unshift(noteToPaste);
365
370
  continue;
366
371
  }
367
372
  const fillPart = parts[0];
368
373
  const filledNote = noteToPaste
369
- .withDuration(fillPart.duration, fillPart.isDotted)
374
+ .withDuration(fillPart.duration, fillPart.dotCount)
370
375
  .withTie(true);
371
- newScore = newScore.changeNoteDuration(partIndex, staffIndex, currentMeasureIdx, currentNoteIdx, fillPart.duration, fillPart.isDotted);
372
- newScore = newScore.replaceNote(partIndex, staffIndex, currentMeasureIdx, currentNoteIdx, filledNote);
376
+ state.score = state.score.changeNoteDuration(partIndex, staffIndex, state.mIdx, state.nIdx, fillPart.duration, fillPart.dotCount);
377
+ state.score = state.score.replaceNote(partIndex, staffIndex, state.mIdx, state.nIdx, filledNote);
373
378
  const remainderParts = decomposeDuration(needed - fillPart.val);
374
379
  const remainderNotes = remainderParts.map((p, idx) => {
375
380
  const isLast = idx === remainderParts.length - 1;
376
381
  return noteToPaste
377
- .withDuration(p.duration, p.isDotted)
378
- .withTie(isLast ? !!noteToPaste.tie : true);
382
+ .withDuration(p.duration, p.dotCount)
383
+ .withTie(isLast ? !!noteToPaste.notes[0].tie : true);
379
384
  });
380
- queue.unshift(...remainderNotes);
381
- currentNoteIdx++;
385
+ state.queue.unshift(...remainderNotes);
386
+ state.nIdx++;
382
387
  }
383
388
  }
384
- return newScore;
389
+ return state.score;
385
390
  }
386
- withTempo(bpm, duration, isDotted) {
387
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, bpm, duration, isDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
391
+ withTempo(bpm, duration, dotCount = 0) {
392
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, bpm, duration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, dotCount);
388
393
  }
389
394
  withTempoText(text) {
390
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, text);
395
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, text, this.tempoDotCount);
391
396
  }
392
397
  withSwing(swing) {
393
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
398
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
394
399
  }
395
400
  withLyricist(lyricist) {
396
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
401
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.copyright, lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
397
402
  }
398
403
  withCopyright(copyright) {
399
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, this.tempoIsDotted, copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
404
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, this.bpm, this.tempoDuration, copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
400
405
  }
401
406
  withParts(parts) {
402
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, parts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
407
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, parts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
403
408
  }
404
409
  toJSON() {
405
410
  return {
@@ -410,7 +415,6 @@ export class Score {
410
415
  parts: this.parts.map((p) => p.toJSON()),
411
416
  bpm: this.bpm,
412
417
  tempoDuration: this.tempoDuration,
413
- tempoIsDotted: this.tempoIsDotted,
414
418
  copyright: this.copyright,
415
419
  lyricist: this.lyricist,
416
420
  swing: this.swing,
@@ -425,92 +429,111 @@ export class Score {
425
429
  return this;
426
430
  const newParts = [...this.parts];
427
431
  newParts[partIndex] = newPart;
428
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, newParts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
432
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, newParts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
429
433
  }
430
434
  addPart(newPart) {
431
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, [...this.parts, newPart], this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
435
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, [...this.parts, newPart], this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
432
436
  }
433
437
  removePart(partIndex) {
434
438
  if (partIndex < 0 || partIndex >= this.parts.length)
435
439
  return this;
436
440
  const newParts = this.parts.filter((_, i) => i !== partIndex);
437
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, newParts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
441
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, newParts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
438
442
  }
439
443
  replaceStaff(partIndex, staffIndex, newStaff) {
440
444
  if (partIndex < 0 || partIndex >= this.parts.length)
441
445
  return this;
442
446
  const newParts = [...this.parts];
443
447
  newParts[partIndex] = newParts[partIndex].replaceStaff(staffIndex, newStaff);
444
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, newParts, this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
448
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, newParts, this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
445
449
  }
446
450
  addMeasure(index, measure) {
447
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts.map((p) => p.addMeasure(index, measure)), this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
451
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts.map((p) => p.addMeasure(index, measure)), this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
448
452
  }
449
453
  deleteMeasure(index) {
450
- return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts.map((p) => p.deleteMeasure(index)), this.bpm, this.tempoDuration, this.tempoIsDotted, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
454
+ return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts.map((p) => p.deleteMeasure(index)), this.bpm, this.tempoDuration, this.copyright, this.lyricist, this.swing, this.subtitle, this.genre, this.tempoText, this.tempoDotCount);
455
+ }
456
+ deleteMeasures(startIndex, endIndex) {
457
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
458
+ let currentScore = this;
459
+ const start = Math.min(startIndex, endIndex);
460
+ const end = Math.max(startIndex, endIndex);
461
+ for (let i = end; i >= start; i--) {
462
+ currentScore = currentScore.deleteMeasure(i);
463
+ }
464
+ return currentScore;
451
465
  }
452
466
  getPlaybackSequence() {
453
467
  const measures = this.parts[0]?.staves[0]?.measures || [];
454
468
  const sequence = [];
455
469
  let i = 0;
456
470
  const startRepeatStack = [0]; // Implicit start at 0
457
- let justJumpedTo = -1;
458
- const repeatCount = new Map();
471
+ const iterationMap = new Map(); // startMeasureIndex -> currentIteration
472
+ iterationMap.set(0, 1);
473
+ const repeatJumpCount = new Map(); // endMeasureIndex -> count
459
474
  let safetyCounter = 0;
460
475
  const MAX_SEQUENCE = 10000;
476
+ let activeVoltaNumbers = [];
477
+ let pendingPop = false;
461
478
  while (i < measures.length && safetyCounter < MAX_SEQUENCE) {
462
479
  safetyCounter++;
463
480
  const m = measures[i];
464
- // Handle Start Repeats
481
+ // If we finished a repeat pass but the next measure still has a volta,
482
+ // we delay popping the stack so the volta logic can see the correct iteration count.
483
+ if (pendingPop && !m.volta) {
484
+ if (startRepeatStack.length > 1) {
485
+ startRepeatStack.pop();
486
+ }
487
+ pendingPop = false;
488
+ }
489
+ // 1. Handle Start Repeat
465
490
  if (m.repeats.some((r) => r.type === 'start')) {
466
- // Only push if we didn't just jump here (avoid re-pushing on loop reentry)
467
- if (i !== justJumpedTo) {
491
+ if (startRepeatStack[startRepeatStack.length - 1] !== i) {
468
492
  startRepeatStack.push(i);
493
+ iterationMap.set(i, 1);
469
494
  }
470
495
  }
471
- // Reset jump flag after processing start repeats check
472
- if (i !== justJumpedTo)
473
- justJumpedTo = -1;
474
- const endRepeat = m.repeats.find((r) => r.type === 'end');
475
- let iteration = 1;
476
- // Calculate current iteration for Volta logic
477
- if (endRepeat)
478
- iteration = (repeatCount.get(i) || 0) + 1;
479
- else {
480
- let nextEndIdx = -1;
481
- for (let j = i; j < measures.length; j++) {
482
- if (measures[j].repeats.some((r) => r.type === 'end')) {
483
- nextEndIdx = j;
484
- break;
485
- }
496
+ // 2. Identify current repetition context
497
+ const currentStartIdx = startRepeatStack[startRepeatStack.length - 1];
498
+ const iteration = iterationMap.get(currentStartIdx) || 1;
499
+ // 3. Volta Logic
500
+ if (m.volta) {
501
+ if (m.volta.type === 'start' || m.volta.type === 'both') {
502
+ activeVoltaNumbers = m.volta.numbers;
486
503
  }
487
- if (nextEndIdx !== -1)
488
- iteration = (repeatCount.get(nextEndIdx) || 0) + 1;
489
504
  }
490
- if (m.volta && !m.volta.numbers.includes(iteration)) {
491
- i++;
492
- continue;
505
+ const effectiveNumbers = (m.volta && m.volta.numbers.length > 0)
506
+ ? m.volta.numbers
507
+ : activeVoltaNumbers;
508
+ const hasVoltaContext = m.volta || activeVoltaNumbers.length > 0;
509
+ const shouldSkip = hasVoltaContext &&
510
+ effectiveNumbers.length > 0 &&
511
+ !effectiveNumbers.includes(iteration);
512
+ if (!shouldSkip) {
513
+ sequence.push(i);
514
+ }
515
+ // 4. Reset volta if stopped
516
+ if (m.volta && (m.volta.type === 'stop' || m.volta.type === 'both')) {
517
+ activeVoltaNumbers = [];
493
518
  }
494
- sequence.push(i);
519
+ // 5. Handle End Repeat
520
+ const endRepeat = m.repeats.find((r) => r.type === 'end');
495
521
  if (endRepeat) {
496
522
  const maxTimes = endRepeat.times || 2;
497
- const currentCount = repeatCount.get(i) || 0;
523
+ const currentCount = repeatJumpCount.get(i) || 0;
498
524
  if (currentCount + 1 < maxTimes) {
499
- repeatCount.set(i, currentCount + 1);
500
- // Jump to the nearest start repeat
501
- const target = startRepeatStack[startRepeatStack.length - 1];
502
- i = target;
503
- justJumpedTo = target;
504
- continue;
525
+ repeatJumpCount.set(i, currentCount + 1);
526
+ iterationMap.set(currentStartIdx, iteration + 1);
527
+ i = currentStartIdx;
528
+ activeVoltaNumbers = []; // Reset volta context when jumping back
529
+ pendingPop = false;
530
+ continue; // Jump back
505
531
  }
506
532
  else {
507
- // Finished loop
508
- repeatCount.set(i, 0);
509
- // Pop the start repeat if we have one (keeping the implicit 0)
510
- if (startRepeatStack.length > 1) {
511
- startRepeatStack.pop();
512
- }
513
- // Do not pop 0, so unmatched repeats will default to 0
533
+ // Finished this repeat group
534
+ repeatJumpCount.set(i, 0);
535
+ pendingPop = true;
536
+ activeVoltaNumbers = [];
514
537
  }
515
538
  }
516
539
  i++;
@@ -17,7 +17,7 @@ export declare class Staff {
17
17
  replaceNote(measureIndex: number, noteIndex: number, newNote: Note, voiceIndex?: number): Staff;
18
18
  replaceMeasure(measureIndex: number, newMeasure: Measure): Staff;
19
19
  deleteNote(measureIndex: number, noteIndex: number, voiceIndex?: number): Staff;
20
- changeNoteDuration(measureIndex: number, noteIndex: number, newDuration: Duration, isDotted?: boolean, voiceIndex?: number): Staff;
20
+ changeNoteDuration(measureIndex: number, noteIndex: number, newDuration: Duration, dotCount?: number, voiceIndex?: number): Staff;
21
21
  toJSON(): StaffJSON;
22
22
  withClef(clef: Clef): Staff;
23
23
  withLineCount(lineCount: number): Staff;
@@ -46,11 +46,11 @@ export class Staff {
46
46
  newMeasures[measureIndex] = newMeasures[measureIndex].deleteNote(noteIndex, voiceIndex);
47
47
  return new Staff(this.clef, newMeasures, this.lineCount, this.tuning);
48
48
  }
49
- changeNoteDuration(measureIndex, noteIndex, newDuration, isDotted = false, voiceIndex = 0) {
49
+ changeNoteDuration(measureIndex, noteIndex, newDuration, dotCount = 0, voiceIndex = 0) {
50
50
  if (measureIndex < 0 || measureIndex >= this.measures.length)
51
51
  return this;
52
52
  const newMeasures = [...this.measures];
53
- newMeasures[measureIndex] = newMeasures[measureIndex].changeNoteDuration(noteIndex, newDuration, isDotted, voiceIndex);
53
+ newMeasures[measureIndex] = newMeasures[measureIndex].changeNoteDuration(noteIndex, newDuration, dotCount, voiceIndex);
54
54
  return new Staff(this.clef, newMeasures, this.lineCount, this.tuning);
55
55
  }
56
56
  toJSON() {
@@ -0,0 +1,6 @@
1
+ export interface AccidentalDisplay {
2
+ cautionary?: boolean;
3
+ editorial?: boolean;
4
+ parenthesized?: boolean;
5
+ bracketed?: boolean;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  export declare enum Arpeggio {
2
2
  Normal = "normal",
3
3
  Up = "up",
4
- Down = "down"
4
+ Down = "down",
5
+ NonArpeggiate = "non-arpeggiate"
5
6
  }
@@ -3,4 +3,5 @@ export var Arpeggio;
3
3
  Arpeggio["Normal"] = "normal";
4
4
  Arpeggio["Up"] = "up";
5
5
  Arpeggio["Down"] = "down";
6
+ Arpeggio["NonArpeggiate"] = "non-arpeggiate";
6
7
  })(Arpeggio || (Arpeggio = {}));
@@ -6,5 +6,7 @@ export declare enum Articulation {
6
6
  Fermata = "fermata",
7
7
  Staccatissimo = "staccatissimo",
8
8
  Caesura = "caesura",
9
- BreathMark = "breath-mark"
9
+ BreathMark = "breath-mark",
10
+ Legato = "legato",
11
+ Portato = "portato"
10
12
  }
@@ -8,4 +8,6 @@ export var Articulation;
8
8
  Articulation["Staccatissimo"] = "staccatissimo";
9
9
  Articulation["Caesura"] = "caesura";
10
10
  Articulation["BreathMark"] = "breath-mark";
11
+ Articulation["Legato"] = "legato";
12
+ Articulation["Portato"] = "portato";
11
13
  })(Articulation || (Articulation = {}));
@@ -0,0 +1,4 @@
1
+ export type BeamValue = 'begin' | 'continue' | 'end' | 'forward hook' | 'backward hook';
2
+ export interface BeamLevels {
3
+ [level: number]: BeamValue;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -19,6 +19,6 @@ export declare function calculateDurationValue(duration: Duration, dotCount?: nu
19
19
  */
20
20
  export declare function decomposeDuration(value: number): {
21
21
  duration: Duration;
22
- isDotted: boolean;
22
+ dotCount: number;
23
23
  val: number;
24
24
  }[];
@@ -42,26 +42,26 @@ export function decomposeDuration(value) {
42
42
  const result = [];
43
43
  let remaining = value;
44
44
  const options = [
45
- { val: 4, dur: Duration.Whole, dot: false },
46
- { val: 3, dur: Duration.Half, dot: true },
47
- { val: 2, dur: Duration.Half, dot: false },
48
- { val: 1.5, dur: Duration.Quarter, dot: true },
49
- { val: 1, dur: Duration.Quarter, dot: false },
50
- { val: 0.75, dur: Duration.Eighth, dot: true },
51
- { val: 0.5, dur: Duration.Eighth, dot: false },
52
- { val: 0.375, dur: Duration.Sixteenth, dot: true },
53
- { val: 0.25, dur: Duration.Sixteenth, dot: false },
54
- { val: 0.1875, dur: Duration.ThirtySecond, dot: true },
55
- { val: 0.125, dur: Duration.ThirtySecond, dot: false },
56
- { val: 0.09375, dur: Duration.SixtyFourth, dot: true },
57
- { val: 0.0625, dur: Duration.SixtyFourth, dot: false },
45
+ { val: 4, dur: Duration.Whole, dot: 0 },
46
+ { val: 3, dur: Duration.Half, dot: 1 },
47
+ { val: 2, dur: Duration.Half, dot: 0 },
48
+ { val: 1.5, dur: Duration.Quarter, dot: 1 },
49
+ { val: 1, dur: Duration.Quarter, dot: 0 },
50
+ { val: 0.75, dur: Duration.Eighth, dot: 1 },
51
+ { val: 0.5, dur: Duration.Eighth, dot: 0 },
52
+ { val: 0.375, dur: Duration.Sixteenth, dot: 1 },
53
+ { val: 0.25, dur: Duration.Sixteenth, dot: 0 },
54
+ { val: 0.1875, dur: Duration.ThirtySecond, dot: 1 },
55
+ { val: 0.125, dur: Duration.ThirtySecond, dot: 0 },
56
+ { val: 0.09375, dur: Duration.SixtyFourth, dot: 1 },
57
+ { val: 0.0625, dur: Duration.SixtyFourth, dot: 0 },
58
58
  ];
59
59
  // Protect against infinite loop with max iterations
60
60
  let loops = 0;
61
61
  while (remaining > 0.001 && loops < 100) {
62
62
  const match = options.find((o) => o.val <= remaining + 0.001);
63
63
  if (match) {
64
- result.push({ duration: match.dur, isDotted: match.dot, val: match.val });
64
+ result.push({ duration: match.dur, dotCount: match.dot, val: match.val });
65
65
  remaining -= match.val;
66
66
  }
67
67
  else {
@@ -0,0 +1,7 @@
1
+ export interface GraceNote {
2
+ slash?: boolean;
3
+ stealTimePrevious?: number;
4
+ stealTimeFollowing?: number;
5
+ makeTime?: number;
6
+ cueSize?: boolean;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,9 @@
1
+ import { Placement } from './Placement.js';
1
2
  export declare enum HairpinType {
2
3
  Crescendo = "crescendo",
3
4
  Decrescendo = "decrescendo"
4
5
  }
5
- export interface Hairpin {
6
+ export interface Hairpin extends Placement {
6
7
  type: HairpinType;
7
8
  placement: 'start' | 'stop';
8
9
  }