@scorelabs/core 1.0.10 → 1.0.13

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 (61) hide show
  1. package/dist/constants.d.ts +4 -0
  2. package/dist/constants.js +10 -0
  3. package/dist/importers/MusicXMLParser.d.ts +4 -1
  4. package/dist/importers/MusicXMLParser.js +564 -75
  5. package/dist/importers/index.d.ts +1 -1
  6. package/dist/importers/index.js +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/models/Measure.d.ts +25 -5
  10. package/dist/models/Measure.js +173 -35
  11. package/dist/models/Note.d.ts +38 -9
  12. package/dist/models/Note.js +104 -47
  13. package/dist/models/NoteSet.d.ts +14 -4
  14. package/dist/models/NoteSet.js +38 -4
  15. package/dist/models/Part.d.ts +6 -6
  16. package/dist/models/Part.js +4 -4
  17. package/dist/models/Pitch.d.ts +1 -1
  18. package/dist/models/Pitch.js +1 -1
  19. package/dist/models/PreMeasure.d.ts +1 -1
  20. package/dist/models/Score.d.ts +13 -9
  21. package/dist/models/Score.js +164 -126
  22. package/dist/models/Staff.d.ts +5 -5
  23. package/dist/models/Staff.js +4 -4
  24. package/dist/models/index.d.ts +10 -10
  25. package/dist/models/index.js +10 -10
  26. package/dist/types/AccidentalDisplay.d.ts +6 -0
  27. package/dist/types/AccidentalDisplay.js +1 -0
  28. package/dist/types/Arpeggio.d.ts +2 -1
  29. package/dist/types/Arpeggio.js +1 -0
  30. package/dist/types/Articulation.d.ts +3 -1
  31. package/dist/types/Articulation.js +2 -0
  32. package/dist/types/Beam.d.ts +4 -0
  33. package/dist/types/Beam.js +1 -0
  34. package/dist/types/Duration.d.ts +5 -1
  35. package/dist/types/Duration.js +27 -14
  36. package/dist/types/Grace.d.ts +7 -0
  37. package/dist/types/Grace.js +1 -0
  38. package/dist/types/Hairpin.d.ts +2 -1
  39. package/dist/types/Language.d.ts +6 -0
  40. package/dist/types/Language.js +7 -0
  41. package/dist/types/Lyric.d.ts +2 -0
  42. package/dist/types/Ornament.d.ts +23 -1
  43. package/dist/types/Ornament.js +9 -0
  44. package/dist/types/Pedal.d.ts +3 -2
  45. package/dist/types/Placement.d.ts +7 -0
  46. package/dist/types/Placement.js +1 -0
  47. package/dist/types/Repeat.d.ts +1 -0
  48. package/dist/types/RestDisplay.d.ts +6 -0
  49. package/dist/types/RestDisplay.js +1 -0
  50. package/dist/types/Slur.d.ts +1 -0
  51. package/dist/types/Technical.d.ts +20 -0
  52. package/dist/types/Technical.js +5 -0
  53. package/dist/types/Tempo.d.ts +1 -1
  54. package/dist/types/Tie.d.ts +4 -0
  55. package/dist/types/Tie.js +1 -0
  56. package/dist/types/Tuplet.d.ts +6 -0
  57. package/dist/types/User.d.ts +13 -7
  58. package/dist/types/User.js +9 -7
  59. package/dist/types/index.d.ts +8 -0
  60. package/dist/types/index.js +8 -0
  61. package/package.json +2 -1
@@ -1,8 +1,8 @@
1
- import { Measure } from './Measure';
2
- import { NoteSet } from './NoteSet';
3
- import { Part, PartJSON } from './Part';
4
- import { Staff } from './Staff';
5
- import { Clef, Duration, Genre, KeySignature, TimeSignature } from './types';
1
+ import { Measure } from './Measure.js';
2
+ import { NoteSet } from './NoteSet.js';
3
+ import { Part, PartJSON } from './Part.js';
4
+ import { Staff } from './Staff.js';
5
+ import { Clef, Duration, Genre, KeySignature, TimeSignature } from './types.js';
6
6
  /**
7
7
  * Represents a complete musical score.
8
8
  */
@@ -14,18 +14,19 @@ export declare class Score {
14
14
  readonly parts: Part[];
15
15
  readonly bpm: number;
16
16
  readonly tempoDuration: Duration;
17
- readonly tempoIsDotted: boolean;
18
17
  readonly copyright: string;
19
18
  readonly lyricist: string;
20
19
  readonly swing: boolean;
21
20
  readonly subtitle: string;
22
21
  readonly genre: Genre;
23
22
  readonly tempoText: string;
24
- constructor(title: string, composer: string, timeSignature: TimeSignature, keySignature: KeySignature, parts: Part[], bpm?: number, tempoDuration?: Duration, tempoIsDotted?: boolean, copyright?: string, lyricist?: string, swing?: boolean, subtitle?: string, genre?: Genre, tempoText?: string);
23
+ readonly tempoDotCount: number;
24
+ constructor(title: string, composer: string, timeSignature: TimeSignature, keySignature: KeySignature, parts: Part[], bpm?: number, tempoDuration?: Duration, copyright?: string, lyricist?: string, swing?: boolean, subtitle?: string, genre?: Genre, tempoText?: string, tempoDotCount?: number);
25
25
  withTitle(title: string): Score;
26
26
  withComposer(composer: string): Score;
27
27
  withSubtitle(subtitle: string): Score;
28
28
  withGenre(genre: Genre): Score;
29
+ withBpm(bpm: number): Score;
29
30
  getMeasureCount(): number;
30
31
  getTimeSignatureAt(measureIndex: number): TimeSignature;
31
32
  getKeySignatureAt(measureIndex: number): KeySignature;
@@ -55,9 +56,10 @@ export declare class Score {
55
56
  private reflowStaff;
56
57
  replaceMeasure(partIndex: number, staffIndex: number, measureIndex: number, newMeasure: Measure, autoBeam?: boolean): Score;
57
58
  deleteNote(partIndex: number, staffIndex: number, measureIndex: number, noteIndex: number, voiceIndex?: number): Score;
58
- changeNoteDuration(partIndex: number, staffIndex: number, measureIndex: number, noteIndex: number, newDuration: Duration, isDotted?: boolean, voiceIndex?: number): Score;
59
+ changeNoteDuration(partIndex: number, staffIndex: number, measureIndex: number, noteIndex: number, newDuration: Duration, dotCount?: number, voiceIndex?: number): Score;
60
+ moveNoteToVoice(partIndex: number, staffIndex: number, measureIndex: number, noteIndex: number, fromVoiceIndex: number, toVoiceIndex: number): Score;
59
61
  pasteNotes(partIndex: number, staffIndex: number, measureIndex: number, noteIndex: number, notesToPaste: NoteSet[]): Score;
60
- withTempo(bpm: number, duration: Duration, isDotted: boolean): Score;
62
+ withTempo(bpm: number, duration: Duration, dotCount?: number): Score;
61
63
  withTempoText(text: string): Score;
62
64
  withSwing(swing: boolean): Score;
63
65
  withLyricist(lyricist: string): Score;
@@ -70,6 +72,7 @@ export declare class Score {
70
72
  replaceStaff(partIndex: number, staffIndex: number, newStaff: Staff): Score;
71
73
  addMeasure(index: number, measure: Measure): Score;
72
74
  deleteMeasure(index: number): Score;
75
+ deleteMeasures(startIndex: number, endIndex: number): Score;
73
76
  getPlaybackSequence(): number[];
74
77
  }
75
78
  export interface ScoreJSON {
@@ -80,6 +83,7 @@ export interface ScoreJSON {
80
83
  parts: PartJSON[];
81
84
  bpm?: number;
82
85
  tempoDuration?: Duration;
86
+ tempoDotCount?: number;
83
87
  tempoIsDotted?: boolean;
84
88
  copyright?: string;
85
89
  lyricist?: string;
@@ -1,6 +1,6 @@
1
- import { Measure } from './Measure';
2
- import { Part } from './Part';
3
- import { Duration, Genre, decomposeDuration } from './types';
1
+ import { Measure } from './Measure.js';
2
+ import { Part } from './Part.js';
3
+ import { Duration, Genre, decomposeDuration } from './types.js';
4
4
  /**
5
5
  * Represents a complete musical score.
6
6
  */
@@ -12,14 +12,14 @@ export class Score {
12
12
  parts;
13
13
  bpm;
14
14
  tempoDuration;
15
- tempoIsDotted;
16
15
  copyright;
17
16
  lyricist;
18
17
  swing;
19
18
  subtitle;
20
19
  genre;
21
20
  tempoText;
22
- constructor(title, composer, timeSignature, keySignature, parts, bpm = 120, tempoDuration = Duration.Quarter, tempoIsDotted = false, copyright = '', lyricist = '', swing = false, subtitle = '', genre = Genre.Unknown, tempoText = '') {
21
+ tempoDotCount;
22
+ constructor(title, composer, timeSignature, keySignature, parts, bpm = 120, tempoDuration = Duration.Quarter, copyright = '', lyricist = '', swing = false, subtitle = '', genre = Genre.Unknown, tempoText = '', tempoDotCount = 0) {
23
23
  this.title = title;
24
24
  this.composer = composer;
25
25
  this.timeSignature = timeSignature;
@@ -27,25 +27,28 @@ export class Score {
27
27
  this.parts = parts;
28
28
  this.bpm = bpm;
29
29
  this.tempoDuration = tempoDuration;
30
- this.tempoIsDotted = tempoIsDotted;
31
30
  this.copyright = copyright;
32
31
  this.lyricist = lyricist;
33
32
  this.swing = swing;
34
33
  this.subtitle = subtitle;
35
34
  this.genre = genre;
36
35
  this.tempoText = tempoText;
36
+ this.tempoDotCount = tempoDotCount;
37
37
  }
38
38
  withTitle(title) {
39
- 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);
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);
40
40
  }
41
41
  withComposer(composer) {
42
- 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);
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);
43
43
  }
44
44
  withSubtitle(subtitle) {
45
- 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);
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);
46
46
  }
47
47
  withGenre(genre) {
48
- 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);
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);
49
+ }
50
+ withBpm(bpm) {
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);
49
52
  }
50
53
  getMeasureCount() {
51
54
  return this.parts[0]?.getMeasureCount() ?? 0;
@@ -88,7 +91,7 @@ export class Score {
88
91
  }
89
92
  static fromJSON(data) {
90
93
  const parts = data.parts.map((p) => Part.fromJSON(p));
91
- 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 ?? '');
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));
92
95
  }
93
96
  transpose(semitones) {
94
97
  const semitoneToFifths = {
@@ -112,7 +115,7 @@ export class Score {
112
115
  newFifths -= 12;
113
116
  while (newFifths < -7)
114
117
  newFifths += 12;
115
- 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);
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);
116
119
  }
117
120
  replaceNote(partIndex, staffIndex, measureIndex, noteIndex, newNote, voiceIndex = 0) {
118
121
  if (partIndex < 0 || partIndex >= this.parts.length)
@@ -122,16 +125,15 @@ export class Score {
122
125
  return this.replaceMeasure(partIndex, staffIndex, measureIndex, updatedMeasure, true);
123
126
  }
124
127
  withKeySignature(keySig) {
125
- 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);
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);
126
129
  }
127
130
  withTimeSignature(timeSig) {
128
- 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);
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);
129
132
  }
130
133
  updateMeasureSignatures(measureIndex, signatures, autoBeam = true) {
131
- let score = this;
132
- for (let pIdx = 0; pIdx < this.parts.length; pIdx++) {
133
- for (let sIdx = 0; sIdx < this.parts[pIdx].staves.length; sIdx++) {
134
- 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];
135
137
  if (currentMsr) {
136
138
  let updatedMsr = currentMsr;
137
139
  if (signatures.keySignature !== undefined) {
@@ -143,45 +145,51 @@ export class Score {
143
145
  if (signatures.clef !== undefined) {
144
146
  updatedMsr = updatedMsr.withClef(signatures.clef);
145
147
  }
146
- 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);
147
155
  }
148
- }
149
- }
156
+ return staff;
157
+ });
158
+ return part.withStaves(newStaves);
159
+ });
160
+ const newScore = this.withParts(newParts);
150
161
  if (signatures.timeSignature !== undefined) {
151
- score = score.reflow(measureIndex, autoBeam);
162
+ return newScore.reflow(measureIndex, autoBeam);
152
163
  }
153
- return score;
164
+ return newScore;
154
165
  }
155
166
  /**
156
167
  * Reflows the notes starting from a specific measure index.
157
168
  * This is useful when time signatures change and notes need to be redistributed.
158
169
  */
159
170
  reflow(fromMeasureIndex, autoBeam = true) {
160
- let currentScore = this;
161
171
  const allStaves = this.getAllStaves();
162
- for (const { partIndex, staffIndex } of allStaves) {
163
- currentScore = currentScore.reflowStaff(partIndex, staffIndex, fromMeasureIndex, autoBeam);
164
- }
172
+ const scoreAfterReflow = allStaves.reduce((accScore, { partIndex, staffIndex }) => accScore.reflowStaff(partIndex, staffIndex, fromMeasureIndex, autoBeam), this);
165
173
  // Harmonize measure count across all staves
166
- const maxMeasures = Math.max(...currentScore.parts.flatMap((p) => p.staves.map((s) => s.measures.length)));
167
- 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)));
168
176
  if (maxMeasures !== minMeasures) {
169
- for (let pIdx = 0; pIdx < currentScore.parts.length; pIdx++) {
170
- for (let sIdx = 0; sIdx < currentScore.parts[pIdx].staves.length; sIdx++) {
171
- const staff = currentScore.parts[pIdx].staves[sIdx];
177
+ return scoreAfterReflow.parts.reduce((accScore1, part, pIdx) => {
178
+ return part.staves.reduce((accScore2, staff, sIdx) => {
172
179
  if (staff.measures.length < maxMeasures) {
173
- let updatedMsrs = [...staff.measures];
180
+ const updatedMsrs = [...staff.measures];
174
181
  for (let i = staff.measures.length; i < maxMeasures; i++) {
175
- const ts = currentScore.getTimeSignatureAt(i);
182
+ const ts = accScore2.getTimeSignatureAt(i);
176
183
  const targetDur = ts.beats * (4 / ts.beatType);
177
184
  updatedMsrs.push(new Measure([[]]).fillVoiceWithRests(0, targetDur));
178
185
  }
179
- currentScore = currentScore.replaceStaff(pIdx, sIdx, staff.withMeasures(updatedMsrs));
186
+ return accScore2.replaceStaff(pIdx, sIdx, staff.withMeasures(updatedMsrs));
180
187
  }
181
- }
182
- }
188
+ return accScore2;
189
+ }, accScore1);
190
+ }, scoreAfterReflow);
183
191
  }
184
- return currentScore;
192
+ return scoreAfterReflow;
185
193
  }
186
194
  isTied(ns) {
187
195
  return !!ns.notes[0].tie;
@@ -201,10 +209,10 @@ export class Score {
201
209
  const staff = this.parts[pIdx].staves[sIdx];
202
210
  const measures = staff.measures;
203
211
  const maxVoices = Math.max(...measures.map((m) => m.voices.length), 1);
204
- let updatedMeasures = [...measures];
212
+ const updatedMeasures = [...measures];
205
213
  for (let vIdx = 0; vIdx < maxVoices; vIdx++) {
206
214
  // 1. Collect notes and merge tied notes into logical streams
207
- let stream = [];
215
+ const stream = [];
208
216
  for (let mIdx = fromMeasureIndex; mIdx < measures.length; mIdx++) {
209
217
  const msr = measures[mIdx];
210
218
  const voice = msr.voices[vIdx] || [];
@@ -251,7 +259,7 @@ export class Score {
251
259
  const newNoteSets = parts.map((p, idx) => {
252
260
  const isLastOfLogical = Math.abs(toTake - logicalNote.duration) < 0.001 && idx === parts.length - 1;
253
261
  return logicalNote.ns
254
- .withDuration(p.duration, p.isDotted)
262
+ .withDuration(p.duration, p.dotCount)
255
263
  .withTie(isLastOfLogical ? !!logicalNote.ns.notes[0].tie : true);
256
264
  });
257
265
  msr = msr.withVoices(msr.voices.map((v, i) => (i === vIdx ? [...v, ...newNoteSets] : v)));
@@ -295,7 +303,7 @@ export class Score {
295
303
  }
296
304
  const newParts = [...this.parts];
297
305
  newParts[partIndex] = newParts[partIndex].replaceMeasure(staffIndex, measureIndex, measureToUse);
298
- 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);
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);
299
307
  }
300
308
  deleteNote(partIndex, staffIndex, measureIndex, noteIndex, voiceIndex = 0) {
301
309
  if (partIndex < 0 || partIndex >= this.parts.length)
@@ -304,88 +312,99 @@ export class Score {
304
312
  const updatedMeasure = measure.deleteNote(noteIndex, voiceIndex);
305
313
  return this.replaceMeasure(partIndex, staffIndex, measureIndex, updatedMeasure, true);
306
314
  }
307
- changeNoteDuration(partIndex, staffIndex, measureIndex, noteIndex, newDuration, isDotted = false, voiceIndex = 0) {
315
+ changeNoteDuration(partIndex, staffIndex, measureIndex, noteIndex, newDuration, dotCount = 0, voiceIndex = 0) {
316
+ if (partIndex < 0 || partIndex >= this.parts.length)
317
+ return this;
318
+ const measure = this.parts[partIndex].staves[staffIndex].measures[measureIndex];
319
+ const updatedMeasure = measure.changeNoteDuration(noteIndex, newDuration, dotCount, voiceIndex);
320
+ return this.replaceMeasure(partIndex, staffIndex, measureIndex, updatedMeasure, true);
321
+ }
322
+ moveNoteToVoice(partIndex, staffIndex, measureIndex, noteIndex, fromVoiceIndex, toVoiceIndex) {
308
323
  if (partIndex < 0 || partIndex >= this.parts.length)
309
324
  return this;
310
325
  const measure = this.parts[partIndex].staves[staffIndex].measures[measureIndex];
311
- const updatedMeasure = measure.changeNoteDuration(noteIndex, newDuration, isDotted, voiceIndex);
326
+ if (!measure)
327
+ return this;
328
+ const updatedMeasure = measure.moveNoteToVoice(fromVoiceIndex, noteIndex, toVoiceIndex);
312
329
  return this.replaceMeasure(partIndex, staffIndex, measureIndex, updatedMeasure, true);
313
330
  }
314
331
  pasteNotes(partIndex, staffIndex, measureIndex, noteIndex, notesToPaste) {
315
332
  if (partIndex < 0 || partIndex >= this.parts.length)
316
333
  return this;
317
- let newScore = this;
318
- let currentMeasureIdx = measureIndex;
319
- let currentNoteIdx = noteIndex;
320
- const queue = [...notesToPaste];
321
- while (queue.length > 0) {
322
- const noteToPaste = queue.shift();
323
- 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];
324
343
  if (!part)
325
344
  break;
326
345
  const staff = part.staves[staffIndex];
327
- if (!staff || currentMeasureIdx >= staff.measures.length)
346
+ if (!staff || state.mIdx >= staff.measures.length)
328
347
  break;
329
- const measure = staff.measures[currentMeasureIdx];
330
- if (currentNoteIdx >= measure.notes.length) {
331
- currentMeasureIdx++;
332
- currentNoteIdx = 0;
333
- 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);
334
353
  continue;
335
354
  }
336
355
  let available = 0;
337
- for (let i = currentNoteIdx; i < measure.notes.length; i++)
356
+ for (let i = state.nIdx; i < measure.notes.length; i++)
338
357
  available += measure.notes[i].getDurationValue();
339
358
  const needed = noteToPaste.getDurationValue();
340
359
  if (needed <= available + 0.001) {
341
- newScore = newScore.changeNoteDuration(partIndex, staffIndex, currentMeasureIdx, currentNoteIdx, noteToPaste.duration, noteToPaste.isDotted);
342
- newScore = newScore.replaceNote(partIndex, staffIndex, currentMeasureIdx, currentNoteIdx, noteToPaste);
343
- 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++;
344
363
  }
345
364
  else {
346
365
  const parts = decomposeDuration(available);
347
366
  if (parts.length === 0) {
348
- currentMeasureIdx++;
349
- currentNoteIdx = 0;
350
- queue.unshift(noteToPaste);
367
+ state.mIdx++;
368
+ state.nIdx = 0;
369
+ state.queue.unshift(noteToPaste);
351
370
  continue;
352
371
  }
353
372
  const fillPart = parts[0];
354
373
  const filledNote = noteToPaste
355
- .withDuration(fillPart.duration, fillPart.isDotted)
374
+ .withDuration(fillPart.duration, fillPart.dotCount)
356
375
  .withTie(true);
357
- newScore = newScore.changeNoteDuration(partIndex, staffIndex, currentMeasureIdx, currentNoteIdx, fillPart.duration, fillPart.isDotted);
358
- 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);
359
378
  const remainderParts = decomposeDuration(needed - fillPart.val);
360
379
  const remainderNotes = remainderParts.map((p, idx) => {
361
380
  const isLast = idx === remainderParts.length - 1;
362
381
  return noteToPaste
363
- .withDuration(p.duration, p.isDotted)
364
- .withTie(isLast ? !!noteToPaste.tie : true);
382
+ .withDuration(p.duration, p.dotCount)
383
+ .withTie(isLast ? !!noteToPaste.notes[0].tie : true);
365
384
  });
366
- queue.unshift(...remainderNotes);
367
- currentNoteIdx++;
385
+ state.queue.unshift(...remainderNotes);
386
+ state.nIdx++;
368
387
  }
369
388
  }
370
- return newScore;
389
+ return state.score;
371
390
  }
372
- withTempo(bpm, duration, isDotted) {
373
- 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);
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);
374
393
  }
375
394
  withTempoText(text) {
376
- 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);
377
396
  }
378
397
  withSwing(swing) {
379
- 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);
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);
380
399
  }
381
400
  withLyricist(lyricist) {
382
- 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);
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);
383
402
  }
384
403
  withCopyright(copyright) {
385
- 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);
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);
386
405
  }
387
406
  withParts(parts) {
388
- 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);
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);
389
408
  }
390
409
  toJSON() {
391
410
  return {
@@ -396,13 +415,13 @@ export class Score {
396
415
  parts: this.parts.map((p) => p.toJSON()),
397
416
  bpm: this.bpm,
398
417
  tempoDuration: this.tempoDuration,
399
- tempoIsDotted: this.tempoIsDotted,
400
418
  copyright: this.copyright,
401
419
  lyricist: this.lyricist,
402
420
  swing: this.swing,
403
421
  subtitle: this.subtitle,
404
422
  genre: this.genre,
405
423
  tempoText: this.tempoText,
424
+ tempoDotCount: this.tempoDotCount,
406
425
  };
407
426
  }
408
427
  replacePart(partIndex, newPart) {
@@ -410,92 +429,111 @@ export class Score {
410
429
  return this;
411
430
  const newParts = [...this.parts];
412
431
  newParts[partIndex] = newPart;
413
- 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);
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);
414
433
  }
415
434
  addPart(newPart) {
416
- 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);
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);
417
436
  }
418
437
  removePart(partIndex) {
419
438
  if (partIndex < 0 || partIndex >= this.parts.length)
420
439
  return this;
421
440
  const newParts = this.parts.filter((_, i) => i !== partIndex);
422
- 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);
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);
423
442
  }
424
443
  replaceStaff(partIndex, staffIndex, newStaff) {
425
444
  if (partIndex < 0 || partIndex >= this.parts.length)
426
445
  return this;
427
446
  const newParts = [...this.parts];
428
447
  newParts[partIndex] = newParts[partIndex].replaceStaff(staffIndex, newStaff);
429
- 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);
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);
430
449
  }
431
450
  addMeasure(index, measure) {
432
- 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);
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);
433
452
  }
434
453
  deleteMeasure(index) {
435
- 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);
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;
436
465
  }
437
466
  getPlaybackSequence() {
438
467
  const measures = this.parts[0]?.staves[0]?.measures || [];
439
468
  const sequence = [];
440
469
  let i = 0;
441
470
  const startRepeatStack = [0]; // Implicit start at 0
442
- let justJumpedTo = -1;
443
- const repeatCount = new Map();
471
+ const iterationMap = new Map(); // startMeasureIndex -> currentIteration
472
+ iterationMap.set(0, 1);
473
+ const repeatJumpCount = new Map(); // endMeasureIndex -> count
444
474
  let safetyCounter = 0;
445
475
  const MAX_SEQUENCE = 10000;
476
+ let activeVoltaNumbers = [];
477
+ let pendingPop = false;
446
478
  while (i < measures.length && safetyCounter < MAX_SEQUENCE) {
447
479
  safetyCounter++;
448
480
  const m = measures[i];
449
- // 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
450
490
  if (m.repeats.some((r) => r.type === 'start')) {
451
- // Only push if we didn't just jump here (avoid re-pushing on loop reentry)
452
- if (i !== justJumpedTo) {
491
+ if (startRepeatStack[startRepeatStack.length - 1] !== i) {
453
492
  startRepeatStack.push(i);
493
+ iterationMap.set(i, 1);
454
494
  }
455
495
  }
456
- // Reset jump flag after processing start repeats check
457
- if (i !== justJumpedTo)
458
- justJumpedTo = -1;
459
- const endRepeat = m.repeats.find((r) => r.type === 'end');
460
- let iteration = 1;
461
- // Calculate current iteration for Volta logic
462
- if (endRepeat)
463
- iteration = (repeatCount.get(i) || 0) + 1;
464
- else {
465
- let nextEndIdx = -1;
466
- for (let j = i; j < measures.length; j++) {
467
- if (measures[j].repeats.some((r) => r.type === 'end')) {
468
- nextEndIdx = j;
469
- break;
470
- }
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;
471
503
  }
472
- if (nextEndIdx !== -1)
473
- iteration = (repeatCount.get(nextEndIdx) || 0) + 1;
474
504
  }
475
- if (m.volta && !m.volta.numbers.includes(iteration)) {
476
- i++;
477
- 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);
478
514
  }
479
- sequence.push(i);
515
+ // 4. Reset volta if stopped
516
+ if (m.volta && (m.volta.type === 'stop' || m.volta.type === 'both')) {
517
+ activeVoltaNumbers = [];
518
+ }
519
+ // 5. Handle End Repeat
520
+ const endRepeat = m.repeats.find((r) => r.type === 'end');
480
521
  if (endRepeat) {
481
522
  const maxTimes = endRepeat.times || 2;
482
- const currentCount = repeatCount.get(i) || 0;
523
+ const currentCount = repeatJumpCount.get(i) || 0;
483
524
  if (currentCount + 1 < maxTimes) {
484
- repeatCount.set(i, currentCount + 1);
485
- // Jump to the nearest start repeat
486
- const target = startRepeatStack[startRepeatStack.length - 1];
487
- i = target;
488
- justJumpedTo = target;
489
- 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
490
531
  }
491
532
  else {
492
- // Finished loop
493
- repeatCount.set(i, 0);
494
- // Pop the start repeat if we have one (keeping the implicit 0)
495
- if (startRepeatStack.length > 1) {
496
- startRepeatStack.pop();
497
- }
498
- // 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 = [];
499
537
  }
500
538
  }
501
539
  i++;
@@ -1,7 +1,7 @@
1
- import { Measure, MeasureJSON } from './Measure';
2
- import { Note } from './Note';
3
- import { Pitch } from './Pitch';
4
- import { Clef, Duration } from './types';
1
+ import { Measure, MeasureJSON } from './Measure.js';
2
+ import { Note } from './Note.js';
3
+ import { Pitch } from './Pitch.js';
4
+ import { Clef, Duration } from './types.js';
5
5
  /**
6
6
  * Represents a single staff (one set of 5 lines with a clef).
7
7
  */
@@ -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;
@@ -1,5 +1,5 @@
1
- import { Measure } from './Measure';
2
- import { Pitch } from './Pitch';
1
+ import { Measure } from './Measure.js';
2
+ import { Pitch } from './Pitch.js';
3
3
  /**
4
4
  * Represents a single staff (one set of 5 lines with a clef).
5
5
  */
@@ -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() {