@scorelabs/core 1.0.1

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.
@@ -0,0 +1,110 @@
1
+ import { Clef } from './types';
2
+ /**
3
+ * Pitch representation using MIDI numbers and explicit spelling (step, alter, octave).
4
+ */
5
+ export class Pitch {
6
+ midiNumber;
7
+ step; // 0=C, 1=D, 2=E, 3=F, 4=G, 5=A, 6=B
8
+ alter; // -1=flat, 0=natural, 1=sharp, etc.
9
+ octave;
10
+ constructor(midiNumber, step, alter, octave) {
11
+ this.midiNumber = midiNumber;
12
+ if (step !== undefined && alter !== undefined && octave !== undefined) {
13
+ this.step = step;
14
+ this.alter = alter;
15
+ this.octave = octave;
16
+ }
17
+ else {
18
+ // Default spelling based on MIDI number (favors sharps)
19
+ this.octave = Math.floor(midiNumber / 12) - 1;
20
+ const chromaticToDiatonic = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6];
21
+ this.step = chromaticToDiatonic[midiNumber % 12];
22
+ const diatonicToChromatic = [0, 2, 4, 5, 7, 9, 11];
23
+ this.alter = (midiNumber % 12) - diatonicToChromatic[this.step];
24
+ }
25
+ }
26
+ getNoteName() {
27
+ const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
28
+ return noteNames[this.midiNumber % 12];
29
+ }
30
+ getOctave() {
31
+ return this.octave;
32
+ }
33
+ getStaffPosition(clef) {
34
+ const middleLineAbsStep = {
35
+ [Clef.Treble]: 4 * 7 + 6, // B4
36
+ [Clef.Bass]: 3 * 7 + 1, // D3
37
+ [Clef.Alto]: 4 * 7 + 0, // C4
38
+ [Clef.Tenor]: 3 * 7 + 5, // A3
39
+ [Clef.Percussion]: 4 * 7 + 6, // B4
40
+ [Clef.Tab]: 4 * 7 + 0, // C4
41
+ };
42
+ const referenceAbsStep = middleLineAbsStep[clef];
43
+ return this.getAbsoluteDiatonicStep() - referenceAbsStep;
44
+ }
45
+ getAbsoluteDiatonicStep() {
46
+ return this.octave * 7 + this.step;
47
+ }
48
+ getDiatonicStep() {
49
+ return this.step;
50
+ }
51
+ transposeDiatonic(steps) {
52
+ const totalSteps = this.octave * 7 + this.step + steps;
53
+ const newOctave = Math.floor(totalSteps / 7);
54
+ const newStep = ((totalSteps % 7) + 7) % 7;
55
+ const diatonicToChromatic = [0, 2, 4, 5, 7, 9, 11];
56
+ const newMidi = (newOctave + 1) * 12 + diatonicToChromatic[newStep];
57
+ return new Pitch(newMidi, newStep, 0, newOctave);
58
+ }
59
+ withEnharmonicNext() {
60
+ const diatonicToChromatic = [0, 2, 4, 5, 7, 9, 11];
61
+ const candidates = [];
62
+ for (let o = this.octave - 1; o <= this.octave + 1; o++) {
63
+ for (let s = 0; s < 7; s++) {
64
+ const baseMidi = (o + 1) * 12 + diatonicToChromatic[s];
65
+ const diff = this.midiNumber - baseMidi;
66
+ if (Math.abs(diff) <= 2) {
67
+ candidates.push({ step: s, alter: diff, octave: o });
68
+ }
69
+ }
70
+ }
71
+ candidates.sort((a, b) => a.octave * 7 + a.step - (b.octave * 7 + b.step));
72
+ const currentIndex = candidates.findIndex((c) => c.step === this.step && c.alter === this.alter && c.octave === this.octave);
73
+ const nextIndex = (currentIndex + 1) % candidates.length;
74
+ const next = candidates[nextIndex];
75
+ return new Pitch(this.midiNumber, next.step, next.alter, next.octave);
76
+ }
77
+ static fromNoteName(note, octave) {
78
+ const baseSteps = {
79
+ C: 0, D: 1, E: 2, F: 3, G: 4, A: 5, B: 6,
80
+ };
81
+ const stepLabel = note[0].toUpperCase();
82
+ const step = baseSteps[stepLabel] ?? 0;
83
+ let alter = 0;
84
+ if (note.includes('#'))
85
+ alter = note.split('#').length - 1;
86
+ if (note.includes('b'))
87
+ alter = -(note.split('b').length - 1);
88
+ if (note.includes('x'))
89
+ alter = (note.split('x').length - 1) * 2;
90
+ const diatonicToChromatic = [0, 2, 4, 5, 7, 9, 11];
91
+ const midiNumber = (octave + 1) * 12 + diatonicToChromatic[step] + alter;
92
+ return new Pitch(midiNumber, step, alter, octave);
93
+ }
94
+ static fromStaffPosition(clef, staffPosition) {
95
+ const middleLineAbsStep = {
96
+ [Clef.Treble]: 4 * 7 + 6, // B4
97
+ [Clef.Bass]: 3 * 7 + 1, // D3
98
+ [Clef.Alto]: 4 * 7 + 0, // C4
99
+ [Clef.Tenor]: 3 * 7 + 5, // A3
100
+ [Clef.Percussion]: 4 * 7 + 0,
101
+ [Clef.Tab]: 4 * 7 + 0,
102
+ };
103
+ const targetAbsStep = middleLineAbsStep[clef] + staffPosition;
104
+ const newOctave = Math.floor(targetAbsStep / 7);
105
+ const newStep = ((targetAbsStep % 7) + 7) % 7;
106
+ const diatonicToChromatic = [0, 2, 4, 5, 7, 9, 11];
107
+ const newMidi = (newOctave + 1) * 12 + diatonicToChromatic[newStep];
108
+ return new Pitch(newMidi, newStep, 0, newOctave);
109
+ }
110
+ }
@@ -0,0 +1,71 @@
1
+ import { TimeSignature, KeySignature, Duration, Genre } from './types';
2
+ import { Part, PartJSON } from './Part';
3
+ import { Staff } from './Staff';
4
+ import { NoteSet } from './NoteSet';
5
+ import { Measure } from './Measure';
6
+ /**
7
+ * Represents a complete musical score.
8
+ */
9
+ export declare class Score {
10
+ readonly title: string;
11
+ readonly composer: string;
12
+ readonly timeSignature: TimeSignature;
13
+ readonly keySignature: KeySignature;
14
+ readonly parts: Part[];
15
+ readonly bpm: number;
16
+ readonly tempoDuration: Duration;
17
+ readonly tempoIsDotted: boolean;
18
+ readonly copyright: string;
19
+ readonly lyricist: string;
20
+ readonly swing: boolean;
21
+ readonly subtitle: string;
22
+ readonly genre: Genre | string;
23
+ 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 | string);
24
+ withTitle(title: string): Score;
25
+ withComposer(composer: string): Score;
26
+ withSubtitle(subtitle: string): Score;
27
+ withGenre(genre: Genre | string): Score;
28
+ getMeasureCount(): number;
29
+ getTimeSignatureAt(measureIndex: number): TimeSignature;
30
+ getKeySignatureAt(measureIndex: number): KeySignature;
31
+ getAllStaves(): {
32
+ partIndex: number;
33
+ staffIndex: number;
34
+ staff: Staff;
35
+ }[];
36
+ static empty(): Score;
37
+ static fromJSON(data: ScoreJSON): Score;
38
+ transpose(semitones: number): Score;
39
+ replaceNote(partIndex: number, staffIndex: number, measureIndex: number, noteIndex: number, newNote: NoteSet, voiceIndex?: number): Score;
40
+ replaceMeasure(partIndex: number, staffIndex: number, measureIndex: number, newMeasure: Measure, autoBeam?: boolean): Score;
41
+ deleteNote(partIndex: number, staffIndex: number, measureIndex: number, noteIndex: number, voiceIndex?: number): Score;
42
+ changeNoteDuration(partIndex: number, staffIndex: number, measureIndex: number, noteIndex: number, newDuration: Duration, isDotted?: boolean, voiceIndex?: number): Score;
43
+ pasteNotes(partIndex: number, staffIndex: number, measureIndex: number, noteIndex: number, notesToPaste: NoteSet[]): Score;
44
+ withTempo(bpm: number, duration: Duration, isDotted: boolean): Score;
45
+ withSwing(swing: boolean): Score;
46
+ withLyricist(lyricist: string): Score;
47
+ withCopyright(copyright: string): Score;
48
+ toJSON(): ScoreJSON;
49
+ replacePart(partIndex: number, newPart: Part): Score;
50
+ addPart(newPart: Part): Score;
51
+ removePart(partIndex: number): Score;
52
+ replaceStaff(partIndex: number, staffIndex: number, newStaff: Staff): Score;
53
+ addMeasure(index: number, measure: Measure): Score;
54
+ deleteMeasure(index: number): Score;
55
+ getPlaybackSequence(): number[];
56
+ }
57
+ export interface ScoreJSON {
58
+ title: string;
59
+ composer: string;
60
+ timeSignature: TimeSignature;
61
+ keySignature: KeySignature;
62
+ parts: PartJSON[];
63
+ bpm?: number;
64
+ tempoDuration?: Duration;
65
+ tempoIsDotted?: boolean;
66
+ copyright?: string;
67
+ lyricist?: string;
68
+ swing?: boolean;
69
+ subtitle?: string;
70
+ genre?: Genre | string;
71
+ }
@@ -0,0 +1,284 @@
1
+ import { Duration, decomposeDuration, Genre } from './types';
2
+ import { Part } from './Part';
3
+ /**
4
+ * Represents a complete musical score.
5
+ */
6
+ export class Score {
7
+ title;
8
+ composer;
9
+ timeSignature;
10
+ keySignature;
11
+ parts;
12
+ bpm;
13
+ tempoDuration;
14
+ tempoIsDotted;
15
+ copyright;
16
+ lyricist;
17
+ swing;
18
+ subtitle;
19
+ genre;
20
+ constructor(title, composer, timeSignature, keySignature, parts, bpm = 120, tempoDuration = Duration.Quarter, tempoIsDotted = false, copyright = '', lyricist = '', swing = false, subtitle = '', genre = Genre.Unknown) {
21
+ this.title = title;
22
+ this.composer = composer;
23
+ this.timeSignature = timeSignature;
24
+ this.keySignature = keySignature;
25
+ this.parts = parts;
26
+ this.bpm = bpm;
27
+ this.tempoDuration = tempoDuration;
28
+ this.tempoIsDotted = tempoIsDotted;
29
+ this.copyright = copyright;
30
+ this.lyricist = lyricist;
31
+ this.swing = swing;
32
+ this.subtitle = subtitle;
33
+ this.genre = genre;
34
+ }
35
+ withTitle(title) {
36
+ 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);
37
+ }
38
+ withComposer(composer) {
39
+ 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);
40
+ }
41
+ withSubtitle(subtitle) {
42
+ 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);
43
+ }
44
+ withGenre(genre) {
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, this.subtitle, genre);
46
+ }
47
+ getMeasureCount() {
48
+ return this.parts[0]?.getMeasureCount() ?? 0;
49
+ }
50
+ getTimeSignatureAt(measureIndex) {
51
+ let current = this.timeSignature;
52
+ const part = this.parts[0];
53
+ if (part && part.staves[0]) {
54
+ const measures = part.staves[0].measures;
55
+ for (let i = 0; i <= measureIndex && i < measures.length; i++) {
56
+ if (measures[i].timeSignature)
57
+ current = measures[i].timeSignature;
58
+ }
59
+ }
60
+ return current;
61
+ }
62
+ getKeySignatureAt(measureIndex) {
63
+ let current = this.keySignature;
64
+ const part = this.parts[0];
65
+ if (part && part.staves[0]) {
66
+ const measures = part.staves[0].measures;
67
+ for (let i = 0; i <= measureIndex && i < measures.length; i++) {
68
+ if (measures[i].keySignature)
69
+ current = measures[i].keySignature;
70
+ }
71
+ }
72
+ return current;
73
+ }
74
+ getAllStaves() {
75
+ const result = [];
76
+ this.parts.forEach((part, partIndex) => {
77
+ part.staves.forEach((staff, staffIndex) => {
78
+ result.push({ partIndex, staffIndex, staff });
79
+ });
80
+ });
81
+ return result;
82
+ }
83
+ static empty() {
84
+ return new Score('', '', { beats: 4, beatType: 4 }, { fifths: 0 }, []);
85
+ }
86
+ static fromJSON(data) {
87
+ const parts = data.parts.map((p) => Part.fromJSON(p));
88
+ 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);
89
+ }
90
+ transpose(semitones) {
91
+ const semitoneToFifths = {
92
+ 0: 0, 1: 7, 2: 2, 3: -3, 4: 4, 5: -1, 6: 6, 7: 1, 8: -4, 9: 3, 10: -2, 11: 5
93
+ };
94
+ const normalizedSemitones = ((semitones % 12) + 12) % 12;
95
+ const fifthsChange = semitoneToFifths[normalizedSemitones];
96
+ let newFifths = this.keySignature.fifths + fifthsChange;
97
+ while (newFifths > 7)
98
+ newFifths -= 12;
99
+ while (newFifths < -7)
100
+ newFifths += 12;
101
+ 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);
102
+ }
103
+ replaceNote(partIndex, staffIndex, measureIndex, noteIndex, newNote, voiceIndex = 0) {
104
+ if (partIndex < 0 || partIndex >= this.parts.length)
105
+ return this;
106
+ const measure = this.parts[partIndex].staves[staffIndex].measures[measureIndex];
107
+ const updatedMeasure = measure.replaceNoteSet(noteIndex, newNote, voiceIndex);
108
+ return this.replaceMeasure(partIndex, staffIndex, measureIndex, updatedMeasure, true);
109
+ }
110
+ replaceMeasure(partIndex, staffIndex, measureIndex, newMeasure, autoBeam = true) {
111
+ if (partIndex < 0 || partIndex >= this.parts.length)
112
+ return this;
113
+ let measureToUse = newMeasure;
114
+ if (autoBeam) {
115
+ const ts = this.getTimeSignatureAt(measureIndex);
116
+ measureToUse = newMeasure.autoBeam(ts);
117
+ }
118
+ const newParts = [...this.parts];
119
+ newParts[partIndex] = newParts[partIndex].replaceMeasure(staffIndex, measureIndex, measureToUse);
120
+ 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);
121
+ }
122
+ deleteNote(partIndex, staffIndex, measureIndex, noteIndex, voiceIndex = 0) {
123
+ if (partIndex < 0 || partIndex >= this.parts.length)
124
+ return this;
125
+ const measure = this.parts[partIndex].staves[staffIndex].measures[measureIndex];
126
+ const updatedMeasure = measure.deleteNote(noteIndex, voiceIndex);
127
+ return this.replaceMeasure(partIndex, staffIndex, measureIndex, updatedMeasure, true);
128
+ }
129
+ changeNoteDuration(partIndex, staffIndex, measureIndex, noteIndex, newDuration, isDotted = false, voiceIndex = 0) {
130
+ if (partIndex < 0 || partIndex >= this.parts.length)
131
+ return this;
132
+ const measure = this.parts[partIndex].staves[staffIndex].measures[measureIndex];
133
+ const updatedMeasure = measure.changeNoteDuration(noteIndex, newDuration, isDotted, voiceIndex);
134
+ return this.replaceMeasure(partIndex, staffIndex, measureIndex, updatedMeasure, true);
135
+ }
136
+ pasteNotes(partIndex, staffIndex, measureIndex, noteIndex, notesToPaste) {
137
+ if (partIndex < 0 || partIndex >= this.parts.length)
138
+ return this;
139
+ let newScore = this;
140
+ let currentMeasureIdx = measureIndex;
141
+ let currentNoteIdx = noteIndex;
142
+ const queue = [...notesToPaste];
143
+ while (queue.length > 0) {
144
+ const noteToPaste = queue.shift();
145
+ const part = newScore.parts[partIndex];
146
+ if (!part)
147
+ break;
148
+ const staff = part.staves[staffIndex];
149
+ if (!staff || currentMeasureIdx >= staff.measures.length)
150
+ break;
151
+ const measure = staff.measures[currentMeasureIdx];
152
+ if (currentNoteIdx >= measure.notes.length) {
153
+ currentMeasureIdx++;
154
+ currentNoteIdx = 0;
155
+ queue.unshift(noteToPaste);
156
+ continue;
157
+ }
158
+ let available = 0;
159
+ for (let i = currentNoteIdx; i < measure.notes.length; i++)
160
+ available += measure.notes[i].getDurationValue();
161
+ const needed = noteToPaste.getDurationValue();
162
+ if (needed <= available + 0.001) {
163
+ newScore = newScore.changeNoteDuration(partIndex, staffIndex, currentMeasureIdx, currentNoteIdx, noteToPaste.duration, noteToPaste.isDotted);
164
+ newScore = newScore.replaceNote(partIndex, staffIndex, currentMeasureIdx, currentNoteIdx, noteToPaste);
165
+ currentNoteIdx++;
166
+ }
167
+ else {
168
+ const parts = decomposeDuration(available);
169
+ if (parts.length === 0) {
170
+ currentMeasureIdx++;
171
+ currentNoteIdx = 0;
172
+ queue.unshift(noteToPaste);
173
+ continue;
174
+ }
175
+ const fillPart = parts[0];
176
+ const filledNote = noteToPaste.withDuration(fillPart.duration, fillPart.isDotted).withTie(true);
177
+ newScore = newScore.changeNoteDuration(partIndex, staffIndex, currentMeasureIdx, currentNoteIdx, fillPart.duration, fillPart.isDotted);
178
+ newScore = newScore.replaceNote(partIndex, staffIndex, currentMeasureIdx, currentNoteIdx, filledNote);
179
+ const remainderParts = decomposeDuration(needed - fillPart.val);
180
+ const remainderNotes = remainderParts.map((p, idx) => {
181
+ const isLast = idx === remainderParts.length - 1;
182
+ return noteToPaste.withDuration(p.duration, p.isDotted).withTie(isLast ? !!noteToPaste.tie : true);
183
+ });
184
+ queue.unshift(...remainderNotes);
185
+ currentNoteIdx++;
186
+ }
187
+ }
188
+ return newScore;
189
+ }
190
+ withTempo(bpm, duration, isDotted) {
191
+ 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);
192
+ }
193
+ withSwing(swing) {
194
+ 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);
195
+ }
196
+ withLyricist(lyricist) {
197
+ 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);
198
+ }
199
+ withCopyright(copyright) {
200
+ 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);
201
+ }
202
+ toJSON() {
203
+ return {
204
+ title: this.title, composer: this.composer, timeSignature: this.timeSignature, keySignature: this.keySignature,
205
+ parts: this.parts.map((p) => p.toJSON()), bpm: this.bpm, tempoDuration: this.tempoDuration,
206
+ tempoIsDotted: this.tempoIsDotted, copyright: this.copyright, lyricist: this.lyricist,
207
+ swing: this.swing, subtitle: this.subtitle, genre: this.genre,
208
+ };
209
+ }
210
+ replacePart(partIndex, newPart) {
211
+ if (partIndex < 0 || partIndex >= this.parts.length)
212
+ return this;
213
+ const newParts = [...this.parts];
214
+ newParts[partIndex] = newPart;
215
+ 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);
216
+ }
217
+ addPart(newPart) {
218
+ 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);
219
+ }
220
+ removePart(partIndex) {
221
+ if (partIndex < 0 || partIndex >= this.parts.length)
222
+ return this;
223
+ const newParts = this.parts.filter((_, i) => i !== partIndex);
224
+ 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);
225
+ }
226
+ replaceStaff(partIndex, staffIndex, newStaff) {
227
+ if (partIndex < 0 || partIndex >= this.parts.length)
228
+ return this;
229
+ const newParts = [...this.parts];
230
+ newParts[partIndex] = newParts[partIndex].replaceStaff(staffIndex, newStaff);
231
+ 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);
232
+ }
233
+ addMeasure(index, measure) {
234
+ 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);
235
+ }
236
+ deleteMeasure(index) {
237
+ 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);
238
+ }
239
+ getPlaybackSequence() {
240
+ const measures = this.parts[0]?.staves[0]?.measures || [];
241
+ const sequence = [];
242
+ let i = 0;
243
+ let blockStart = 0;
244
+ const repeatCount = new Map();
245
+ while (i < measures.length) {
246
+ const m = measures[i];
247
+ if (m.repeats.some((r) => r.type === 'start'))
248
+ blockStart = i;
249
+ const endRepeat = m.repeats.find((r) => r.type === 'end');
250
+ let iteration = 1;
251
+ if (endRepeat)
252
+ iteration = (repeatCount.get(i) || 0) + 1;
253
+ else {
254
+ let nextEndIdx = -1;
255
+ for (let j = i; j < measures.length; j++) {
256
+ if (measures[j].repeats.some((r) => r.type === 'end')) {
257
+ nextEndIdx = j;
258
+ break;
259
+ }
260
+ }
261
+ if (nextEndIdx !== -1)
262
+ iteration = (repeatCount.get(nextEndIdx) || 0) + 1;
263
+ }
264
+ if (m.volta && m.volta.number !== iteration) {
265
+ i++;
266
+ continue;
267
+ }
268
+ sequence.push(i);
269
+ if (endRepeat) {
270
+ const maxTimes = endRepeat.times || 2;
271
+ const currentCount = repeatCount.get(i) || 0;
272
+ if (currentCount + 1 < maxTimes) {
273
+ repeatCount.set(i, currentCount + 1);
274
+ i = blockStart;
275
+ continue;
276
+ }
277
+ else
278
+ repeatCount.set(i, 0);
279
+ }
280
+ i++;
281
+ }
282
+ return sequence;
283
+ }
284
+ }
@@ -0,0 +1,36 @@
1
+ import { Clef, Duration } from './types';
2
+ import { Measure, MeasureJSON } from './Measure';
3
+ import { Pitch } from './Pitch';
4
+ import { Note } from './Note';
5
+ /**
6
+ * Represents a single staff (one set of 5 lines with a clef).
7
+ */
8
+ export declare class Staff {
9
+ readonly clef: Clef;
10
+ readonly measures: Measure[];
11
+ readonly lineCount: number;
12
+ readonly tuning?: Pitch[] | undefined;
13
+ constructor(clef: Clef, measures: Measure[], lineCount?: number, tuning?: Pitch[] | undefined);
14
+ getMeasureCount(): number;
15
+ static fromJSON(data: StaffJSON): Staff;
16
+ transpose(semitones: number): Staff;
17
+ replaceNote(measureIndex: number, noteIndex: number, newNote: Note, voiceIndex?: number): Staff;
18
+ replaceMeasure(measureIndex: number, newMeasure: Measure): Staff;
19
+ deleteNote(measureIndex: number, noteIndex: number, voiceIndex?: number): Staff;
20
+ changeNoteDuration(measureIndex: number, noteIndex: number, newDuration: Duration, isDotted?: boolean, voiceIndex?: number): Staff;
21
+ toJSON(): StaffJSON;
22
+ withClef(clef: Clef): Staff;
23
+ withLineCount(lineCount: number): Staff;
24
+ withTuning(tuning: Pitch[]): Staff;
25
+ withMeasures(measures: Measure[]): Staff;
26
+ addMeasure(index: number, measure: Measure): Staff;
27
+ deleteMeasure(index: number): Staff;
28
+ }
29
+ export interface StaffJSON {
30
+ clef: string;
31
+ measures: MeasureJSON[];
32
+ lineCount?: number;
33
+ tuning?: {
34
+ midiNumber: number;
35
+ }[];
36
+ }
@@ -0,0 +1,89 @@
1
+ import { Measure } from './Measure';
2
+ import { Pitch } from './Pitch';
3
+ /**
4
+ * Represents a single staff (one set of 5 lines with a clef).
5
+ */
6
+ export class Staff {
7
+ clef;
8
+ measures;
9
+ lineCount;
10
+ tuning;
11
+ constructor(clef, measures, lineCount = 5, tuning) {
12
+ this.clef = clef;
13
+ this.measures = measures;
14
+ this.lineCount = lineCount;
15
+ this.tuning = tuning;
16
+ }
17
+ getMeasureCount() {
18
+ return this.measures.length;
19
+ }
20
+ static fromJSON(data) {
21
+ const measures = data.measures.map((m) => Measure.fromJSON(m));
22
+ const tuning = data.tuning?.map((t) => new Pitch(t.midiNumber));
23
+ return new Staff(data.clef, measures, data.lineCount ?? 5, tuning);
24
+ }
25
+ transpose(semitones) {
26
+ return new Staff(this.clef, this.measures.map((m) => m.transpose(semitones)), this.lineCount, this.tuning?.map((p) => new Pitch(p.midiNumber + semitones)));
27
+ }
28
+ replaceNote(measureIndex, noteIndex, newNote, voiceIndex = 0) {
29
+ if (measureIndex < 0 || measureIndex >= this.measures.length)
30
+ return this;
31
+ const newMeasures = [...this.measures];
32
+ newMeasures[measureIndex] = newMeasures[measureIndex].replaceNote(noteIndex, newNote, voiceIndex);
33
+ return new Staff(this.clef, newMeasures, this.lineCount, this.tuning);
34
+ }
35
+ replaceMeasure(measureIndex, newMeasure) {
36
+ if (measureIndex < 0 || measureIndex >= this.measures.length)
37
+ return this;
38
+ const newMeasures = [...this.measures];
39
+ newMeasures[measureIndex] = newMeasure;
40
+ return new Staff(this.clef, newMeasures, this.lineCount, this.tuning);
41
+ }
42
+ deleteNote(measureIndex, noteIndex, voiceIndex = 0) {
43
+ if (measureIndex < 0 || measureIndex >= this.measures.length)
44
+ return this;
45
+ const newMeasures = [...this.measures];
46
+ newMeasures[measureIndex] = newMeasures[measureIndex].deleteNote(noteIndex, voiceIndex);
47
+ return new Staff(this.clef, newMeasures, this.lineCount, this.tuning);
48
+ }
49
+ changeNoteDuration(measureIndex, noteIndex, newDuration, isDotted = false, voiceIndex = 0) {
50
+ if (measureIndex < 0 || measureIndex >= this.measures.length)
51
+ return this;
52
+ const newMeasures = [...this.measures];
53
+ newMeasures[measureIndex] = newMeasures[measureIndex].changeNoteDuration(noteIndex, newDuration, isDotted, voiceIndex);
54
+ return new Staff(this.clef, newMeasures, this.lineCount, this.tuning);
55
+ }
56
+ toJSON() {
57
+ return {
58
+ clef: this.clef,
59
+ measures: this.measures.map((m) => m.toJSON()),
60
+ lineCount: this.lineCount,
61
+ tuning: this.tuning?.map((p) => ({ midiNumber: p.midiNumber })),
62
+ };
63
+ }
64
+ withClef(clef) {
65
+ return new Staff(clef, this.measures, this.lineCount, this.tuning);
66
+ }
67
+ withLineCount(lineCount) {
68
+ return new Staff(this.clef, this.measures, lineCount, this.tuning);
69
+ }
70
+ withTuning(tuning) {
71
+ return new Staff(this.clef, this.measures, this.lineCount, tuning);
72
+ }
73
+ withMeasures(measures) {
74
+ return new Staff(this.clef, measures, this.lineCount, this.tuning);
75
+ }
76
+ addMeasure(index, measure) {
77
+ const newMeasures = [...this.measures];
78
+ const insertIndex = Math.min(Math.max(0, index), newMeasures.length);
79
+ newMeasures.splice(insertIndex, 0, measure);
80
+ return new Staff(this.clef, newMeasures, this.lineCount, this.tuning);
81
+ }
82
+ deleteMeasure(index) {
83
+ if (index < 0 || index >= this.measures.length)
84
+ return this;
85
+ const newMeasures = [...this.measures];
86
+ newMeasures.splice(index, 1);
87
+ return new Staff(this.clef, newMeasures, this.lineCount, this.tuning);
88
+ }
89
+ }
@@ -0,0 +1,9 @@
1
+ export * from './types';
2
+ export * from './Pitch';
3
+ export * from './Note';
4
+ export * from './NoteSet';
5
+ export * from './Measure';
6
+ export * from './Staff';
7
+ export * from './Part';
8
+ export * from './Score';
9
+ export * from './Instrument';
@@ -0,0 +1,9 @@
1
+ export * from './types';
2
+ export * from './Pitch';
3
+ export * from './Note';
4
+ export * from './NoteSet';
5
+ export * from './Measure';
6
+ export * from './Staff';
7
+ export * from './Part';
8
+ export * from './Score';
9
+ export * from './Instrument';