@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.
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +10 -0
- package/dist/importers/MusicXMLParser.d.ts +4 -1
- package/dist/importers/MusicXMLParser.js +564 -75
- package/dist/importers/index.d.ts +1 -1
- package/dist/importers/index.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/Measure.d.ts +25 -5
- package/dist/models/Measure.js +173 -35
- package/dist/models/Note.d.ts +38 -9
- package/dist/models/Note.js +104 -47
- package/dist/models/NoteSet.d.ts +14 -4
- package/dist/models/NoteSet.js +38 -4
- package/dist/models/Part.d.ts +6 -6
- package/dist/models/Part.js +4 -4
- package/dist/models/Pitch.d.ts +1 -1
- package/dist/models/Pitch.js +1 -1
- package/dist/models/PreMeasure.d.ts +1 -1
- package/dist/models/Score.d.ts +13 -9
- package/dist/models/Score.js +164 -126
- package/dist/models/Staff.d.ts +5 -5
- package/dist/models/Staff.js +4 -4
- package/dist/models/index.d.ts +10 -10
- package/dist/models/index.js +10 -10
- package/dist/types/AccidentalDisplay.d.ts +6 -0
- package/dist/types/AccidentalDisplay.js +1 -0
- package/dist/types/Arpeggio.d.ts +2 -1
- package/dist/types/Arpeggio.js +1 -0
- package/dist/types/Articulation.d.ts +3 -1
- package/dist/types/Articulation.js +2 -0
- package/dist/types/Beam.d.ts +4 -0
- package/dist/types/Beam.js +1 -0
- package/dist/types/Duration.d.ts +5 -1
- package/dist/types/Duration.js +27 -14
- package/dist/types/Grace.d.ts +7 -0
- package/dist/types/Grace.js +1 -0
- package/dist/types/Hairpin.d.ts +2 -1
- package/dist/types/Language.d.ts +6 -0
- package/dist/types/Language.js +7 -0
- package/dist/types/Lyric.d.ts +2 -0
- package/dist/types/Ornament.d.ts +23 -1
- package/dist/types/Ornament.js +9 -0
- package/dist/types/Pedal.d.ts +3 -2
- package/dist/types/Placement.d.ts +7 -0
- package/dist/types/Placement.js +1 -0
- package/dist/types/Repeat.d.ts +1 -0
- package/dist/types/RestDisplay.d.ts +6 -0
- package/dist/types/RestDisplay.js +1 -0
- package/dist/types/Slur.d.ts +1 -0
- package/dist/types/Technical.d.ts +20 -0
- package/dist/types/Technical.js +5 -0
- package/dist/types/Tempo.d.ts +1 -1
- package/dist/types/Tie.d.ts +4 -0
- package/dist/types/Tie.js +1 -0
- package/dist/types/Tuplet.d.ts +6 -0
- package/dist/types/User.d.ts +13 -7
- package/dist/types/User.js +9 -7
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +8 -0
- package/package.json +2 -1
package/dist/models/Score.d.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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;
|
package/dist/models/Score.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
return newScore.reflow(measureIndex, autoBeam);
|
|
152
163
|
}
|
|
153
|
-
return
|
|
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
|
-
|
|
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(...
|
|
167
|
-
const minMeasures = Math.min(...
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
180
|
+
const updatedMsrs = [...staff.measures];
|
|
174
181
|
for (let i = staff.measures.length; i < maxMeasures; i++) {
|
|
175
|
-
const ts =
|
|
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
|
-
|
|
186
|
+
return accScore2.replaceStaff(pIdx, sIdx, staff.withMeasures(updatedMsrs));
|
|
180
187
|
}
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
return accScore2;
|
|
189
|
+
}, accScore1);
|
|
190
|
+
}, scoreAfterReflow);
|
|
183
191
|
}
|
|
184
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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 ||
|
|
346
|
+
if (!staff || state.mIdx >= staff.measures.length)
|
|
328
347
|
break;
|
|
329
|
-
const measure = staff.measures[
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
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 =
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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.
|
|
374
|
+
.withDuration(fillPart.duration, fillPart.dotCount)
|
|
356
375
|
.withTie(true);
|
|
357
|
-
|
|
358
|
-
|
|
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.
|
|
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
|
-
|
|
385
|
+
state.queue.unshift(...remainderNotes);
|
|
386
|
+
state.nIdx++;
|
|
368
387
|
}
|
|
369
388
|
}
|
|
370
|
-
return
|
|
389
|
+
return state.score;
|
|
371
390
|
}
|
|
372
|
-
withTempo(bpm, duration,
|
|
373
|
-
return new Score(this.title, this.composer, this.timeSignature, this.keySignature, this.parts, bpm, duration,
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
443
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
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 =
|
|
523
|
+
const currentCount = repeatJumpCount.get(i) || 0;
|
|
483
524
|
if (currentCount + 1 < maxTimes) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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++;
|
package/dist/models/Staff.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
package/dist/models/Staff.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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() {
|