@scorelabs/core 1.0.9 → 1.0.11
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 +7 -1
- package/dist/importers/MusicXMLParser.js +239 -97
- 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 +10 -5
- package/dist/models/Measure.js +221 -28
- package/dist/models/Note.d.ts +14 -15
- package/dist/models/Note.js +50 -48
- package/dist/models/NoteSet.d.ts +13 -12
- package/dist/models/NoteSet.js +7 -4
- package/dist/models/Part.d.ts +5 -5
- package/dist/models/Part.js +2 -2
- 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 +42 -27
- package/dist/models/Staff.d.ts +4 -4
- package/dist/models/Staff.js +2 -2
- package/dist/models/index.d.ts +10 -10
- package/dist/models/index.js +10 -10
- package/dist/types/Duration.d.ts +4 -0
- package/dist/types/Duration.js +13 -0
- package/dist/types/Tempo.d.ts +1 -0
- package/dist/types/User.d.ts +21 -0
- package/dist/types/User.js +7 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './MusicXMLParser';
|
|
1
|
+
export * from './MusicXMLParser.js';
|
package/dist/importers/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './MusicXMLParser';
|
|
1
|
+
export * from './MusicXMLParser.js';
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/models/Measure.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Note, NoteJSON } from './Note';
|
|
2
|
-
import { NoteSet, NoteSetJSON } from './NoteSet';
|
|
3
|
-
import { BarlineStyle, Clef, Duration, KeySignature, Repeat, Tempo, TimeSignature, Volta } from './types';
|
|
1
|
+
import { Note, NoteJSON } from './Note.js';
|
|
2
|
+
import { NoteSet, NoteSetJSON } from './NoteSet.js';
|
|
3
|
+
import { BarlineStyle, Clef, Duration, KeySignature, Repeat, Tempo, TimeSignature, Volta } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Represents a single measure containing note sets, potentially in multiple voices.
|
|
6
6
|
*/
|
|
@@ -20,16 +20,19 @@ export declare class Measure {
|
|
|
20
20
|
readonly barlineStyle: BarlineStyle;
|
|
21
21
|
constructor(voices: NoteSet[][], timeSignature?: TimeSignature | undefined, keySignature?: KeySignature | undefined, systemBreak?: boolean, pageBreak?: boolean, repeats?: Repeat[], volta?: Volta | undefined, isPickup?: boolean, clef?: Clef | undefined, tempo?: Tempo | undefined, rehearsalMark?: string | undefined, systemText?: string | undefined, barlineStyle?: BarlineStyle);
|
|
22
22
|
get notes(): NoteSet[];
|
|
23
|
+
getNoteOffset(voiceIndex: number, noteIndex: number): number;
|
|
24
|
+
moveNoteToVoice(fromVoiceIndex: number, noteIndex: number, toVoiceIndex: number): Measure;
|
|
25
|
+
splitNote(voiceIndex: number, noteIndex: number, splitOffset: number): Measure;
|
|
23
26
|
get hasStartRepeat(): boolean;
|
|
24
27
|
get hasEndRepeat(): boolean;
|
|
25
|
-
changeNoteDuration(noteIndex: number, newDuration: Duration,
|
|
28
|
+
changeNoteDuration(noteIndex: number, newDuration: Duration, dotCountOrIsDotted?: number | boolean, voiceIndex?: number): Measure;
|
|
26
29
|
getTotalDuration(voiceIndex?: number): number;
|
|
27
30
|
static fromJSON(data: MeasureJSON): Measure;
|
|
28
31
|
transpose(semitones: number): Measure;
|
|
29
32
|
replaceNoteSet(index: number, newNoteSet: NoteSet, voiceIndex?: number): Measure;
|
|
30
33
|
replaceNote(noteIndex: number, newNote: Note, voiceIndex?: number): Measure;
|
|
31
34
|
deleteNote(noteIndex: number, voiceIndex?: number): Measure;
|
|
32
|
-
autoBeam(timeSign
|
|
35
|
+
autoBeam(timeSign?: TimeSignature): Measure;
|
|
33
36
|
withTimeSignature(timeSignature?: TimeSignature): Measure;
|
|
34
37
|
withKeySignature(keySignature?: KeySignature): Measure;
|
|
35
38
|
withClef(clef?: Clef): Measure;
|
|
@@ -46,6 +49,8 @@ export declare class Measure {
|
|
|
46
49
|
withSystemText(text?: string): Measure;
|
|
47
50
|
withBarlineStyle(style: BarlineStyle): Measure;
|
|
48
51
|
fillVoiceWithRests(voiceIndex: number, targetDuration: number): Measure;
|
|
52
|
+
simplifyRests(voiceIndex?: number): Measure;
|
|
53
|
+
private decomposeToRests;
|
|
49
54
|
toJSON(): MeasureJSON;
|
|
50
55
|
}
|
|
51
56
|
export interface MeasureJSON {
|
package/dist/models/Measure.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Note } from './Note';
|
|
2
|
-
import { NoteSet } from './NoteSet';
|
|
3
|
-
import { BarlineStyle,
|
|
1
|
+
import { Note } from './Note.js';
|
|
2
|
+
import { NoteSet } from './NoteSet.js';
|
|
3
|
+
import { BarlineStyle, Duration, calculateDurationValue, decomposeDuration, } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Represents a single measure containing note sets, potentially in multiple voices.
|
|
6
6
|
*/
|
|
@@ -39,26 +39,110 @@ export class Measure {
|
|
|
39
39
|
get notes() {
|
|
40
40
|
return this.voices[0] || [];
|
|
41
41
|
}
|
|
42
|
+
getNoteOffset(voiceIndex, noteIndex) {
|
|
43
|
+
const voice = this.voices[voiceIndex];
|
|
44
|
+
if (!voice)
|
|
45
|
+
return 0;
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (let i = 0; i < noteIndex && i < voice.length; i++) {
|
|
48
|
+
offset += voice[i].getDurationValue();
|
|
49
|
+
}
|
|
50
|
+
return offset;
|
|
51
|
+
}
|
|
52
|
+
moveNoteToVoice(fromVoiceIndex, noteIndex, toVoiceIndex) {
|
|
53
|
+
if (fromVoiceIndex === toVoiceIndex)
|
|
54
|
+
return this;
|
|
55
|
+
const fromVoice = this.voices[fromVoiceIndex];
|
|
56
|
+
if (!fromVoice || noteIndex < 0 || noteIndex >= fromVoice.length)
|
|
57
|
+
return this;
|
|
58
|
+
const noteSetToMove = fromVoice[noteIndex];
|
|
59
|
+
if (noteSetToMove.isRest)
|
|
60
|
+
return this;
|
|
61
|
+
const onset = this.getNoteOffset(fromVoiceIndex, noteIndex);
|
|
62
|
+
// 1. Replace with rest in fromVoice first to get the source updated
|
|
63
|
+
let updatedMeasure = this.deleteNote(noteIndex, fromVoiceIndex);
|
|
64
|
+
// 2. Ensure toVoice exists and has rests up to total duration
|
|
65
|
+
const targetDur = this.timeSignature
|
|
66
|
+
? (this.timeSignature.beats * 4) / this.timeSignature.beatType
|
|
67
|
+
: this.getTotalDuration(fromVoiceIndex);
|
|
68
|
+
updatedMeasure = updatedMeasure.fillVoiceWithRests(toVoiceIndex, targetDur);
|
|
69
|
+
// 3. Find the note/rest at 'onset' in toVoice and split if necessary
|
|
70
|
+
let toVoice = updatedMeasure.voices[toVoiceIndex];
|
|
71
|
+
let currentOffset = 0;
|
|
72
|
+
let targetNoteIndex = -1;
|
|
73
|
+
for (let i = 0; i < toVoice.length; i++) {
|
|
74
|
+
const dur = toVoice[i].getDurationValue();
|
|
75
|
+
if (Math.abs(currentOffset - onset) < 0.001) {
|
|
76
|
+
targetNoteIndex = i;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (onset > currentOffset + 0.001 && onset < currentOffset + dur - 0.001) {
|
|
80
|
+
// Need to split
|
|
81
|
+
updatedMeasure = updatedMeasure.splitNote(toVoiceIndex, i, onset - currentOffset);
|
|
82
|
+
toVoice = updatedMeasure.voices[toVoiceIndex]; // Update reference
|
|
83
|
+
// After split, the target should be at i + (number of segments added - 1)
|
|
84
|
+
// Let's just re-scan to be safe
|
|
85
|
+
currentOffset = 0;
|
|
86
|
+
for (let j = 0; j < toVoice.length; j++) {
|
|
87
|
+
if (Math.abs(currentOffset - onset) < 0.001) {
|
|
88
|
+
targetNoteIndex = j;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
currentOffset += toVoice[j].getDurationValue();
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
currentOffset += dur;
|
|
96
|
+
}
|
|
97
|
+
if (targetNoteIndex !== -1) {
|
|
98
|
+
// We found the correct position in the prepared target voice
|
|
99
|
+
updatedMeasure = updatedMeasure.replaceNoteSet(targetNoteIndex, noteSetToMove, toVoiceIndex);
|
|
100
|
+
}
|
|
101
|
+
return updatedMeasure;
|
|
102
|
+
}
|
|
103
|
+
splitNote(voiceIndex, noteIndex, splitOffset) {
|
|
104
|
+
const voice = this.voices[voiceIndex];
|
|
105
|
+
if (!voice || noteIndex < 0 || noteIndex >= voice.length)
|
|
106
|
+
return this;
|
|
107
|
+
const target = voice[noteIndex];
|
|
108
|
+
const duration = target.getDurationValue();
|
|
109
|
+
if (splitOffset <= 0 || splitOffset >= duration - 0.001)
|
|
110
|
+
return this;
|
|
111
|
+
const leftParts = decomposeDuration(splitOffset);
|
|
112
|
+
const rightParts = decomposeDuration(duration - splitOffset);
|
|
113
|
+
const leftNotes = leftParts.map((p) => target.withDuration(p.duration, p.isDotted).withTie(target.isRest ? false : true));
|
|
114
|
+
const rightNotes = rightParts.map((p, idx) => {
|
|
115
|
+
const isLast = idx === rightParts.length - 1;
|
|
116
|
+
return target
|
|
117
|
+
.withDuration(p.duration, p.isDotted)
|
|
118
|
+
.withTie(isLast ? !!target.notes[0].tie : !target.isRest);
|
|
119
|
+
});
|
|
120
|
+
const newVoice = [...voice];
|
|
121
|
+
newVoice.splice(noteIndex, 1, ...leftNotes, ...rightNotes);
|
|
122
|
+
const newVoices = [...this.voices];
|
|
123
|
+
newVoices[voiceIndex] = newVoice;
|
|
124
|
+
return this.withVoices(newVoices);
|
|
125
|
+
}
|
|
42
126
|
get hasStartRepeat() {
|
|
43
|
-
return this.repeats.some((r) => r.type ===
|
|
127
|
+
return this.repeats.some((r) => r.type === 'start');
|
|
44
128
|
}
|
|
45
129
|
get hasEndRepeat() {
|
|
46
|
-
return this.repeats.some((r) => r.type ===
|
|
130
|
+
return this.repeats.some((r) => r.type === 'end');
|
|
47
131
|
}
|
|
48
|
-
changeNoteDuration(noteIndex, newDuration,
|
|
132
|
+
changeNoteDuration(noteIndex, newDuration, dotCountOrIsDotted = 0, voiceIndex = 0) {
|
|
49
133
|
const voice = this.voices[voiceIndex];
|
|
50
134
|
if (!voice || noteIndex < 0 || noteIndex >= voice.length)
|
|
51
135
|
return this;
|
|
52
136
|
const targetNoteSet = voice[noteIndex];
|
|
53
137
|
const oldVal = targetNoteSet.getDurationValue();
|
|
54
|
-
const
|
|
55
|
-
const newVal =
|
|
138
|
+
const dotCount = typeof dotCountOrIsDotted === 'number' ? dotCountOrIsDotted : (dotCountOrIsDotted ? 1 : 0);
|
|
139
|
+
const newVal = calculateDurationValue(newDuration, dotCount);
|
|
56
140
|
if (Math.abs(newVal - oldVal) < 0.001)
|
|
57
141
|
return this;
|
|
58
142
|
const updatedVoice = [...voice];
|
|
59
143
|
if (newVal < oldVal) {
|
|
60
144
|
const gap = oldVal - newVal;
|
|
61
|
-
updatedVoice[noteIndex] = targetNoteSet.withDuration(newDuration,
|
|
145
|
+
updatedVoice[noteIndex] = targetNoteSet.withDuration(newDuration, dotCountOrIsDotted);
|
|
62
146
|
const rests = decomposeDuration(gap).map((d) => new NoteSet([new Note(d.duration, undefined, true, d.isDotted)]));
|
|
63
147
|
updatedVoice.splice(noteIndex + 1, 0, ...rests);
|
|
64
148
|
}
|
|
@@ -70,7 +154,7 @@ export class Measure {
|
|
|
70
154
|
}
|
|
71
155
|
if (available < delta - 0.001)
|
|
72
156
|
return this;
|
|
73
|
-
updatedVoice[noteIndex] = targetNoteSet.withDuration(newDuration,
|
|
157
|
+
updatedVoice[noteIndex] = targetNoteSet.withDuration(newDuration, dotCountOrIsDotted);
|
|
74
158
|
let consumed = 0;
|
|
75
159
|
let removeCount = 0;
|
|
76
160
|
const replacements = [];
|
|
@@ -94,7 +178,11 @@ export class Measure {
|
|
|
94
178
|
}
|
|
95
179
|
const newVoices = [...this.voices];
|
|
96
180
|
newVoices[voiceIndex] = updatedVoice;
|
|
97
|
-
|
|
181
|
+
let result = this.withVoices(newVoices).simplifyRests(voiceIndex);
|
|
182
|
+
if (this.timeSignature) {
|
|
183
|
+
result = result.autoBeam(this.timeSignature);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
98
186
|
}
|
|
99
187
|
getTotalDuration(voiceIndex = 0) {
|
|
100
188
|
const voice = this.voices[voiceIndex];
|
|
@@ -141,10 +229,56 @@ export class Measure {
|
|
|
141
229
|
const voice = [...this.voices[voiceIndex]];
|
|
142
230
|
if (index >= voice.length)
|
|
143
231
|
return this;
|
|
144
|
-
voice[index]
|
|
232
|
+
const oldVal = voice[index].getDurationValue();
|
|
233
|
+
const newVal = newNoteSet.getDurationValue();
|
|
234
|
+
// Use a copy for modifications
|
|
235
|
+
const updatedVoice = [...voice];
|
|
236
|
+
if (Math.abs(oldVal - newVal) < 0.001) {
|
|
237
|
+
updatedVoice[index] = newNoteSet;
|
|
238
|
+
}
|
|
239
|
+
else if (newVal < oldVal) {
|
|
240
|
+
const gap = oldVal - newVal;
|
|
241
|
+
updatedVoice[index] = newNoteSet;
|
|
242
|
+
const rests = this.decomposeToRests(gap);
|
|
243
|
+
updatedVoice.splice(index + 1, 0, ...rests);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const delta = newVal - oldVal;
|
|
247
|
+
let available = 0;
|
|
248
|
+
for (let i = index + 1; i < updatedVoice.length; i++) {
|
|
249
|
+
available += updatedVoice[i].getDurationValue();
|
|
250
|
+
}
|
|
251
|
+
// If there's enough space to consume, do it.
|
|
252
|
+
// If not, we still replace it but it will overflow (which simplifyRests might fix or layout will handle)
|
|
253
|
+
updatedVoice[index] = newNoteSet;
|
|
254
|
+
let consumed = 0;
|
|
255
|
+
let removeCount = 0;
|
|
256
|
+
const replacements = [];
|
|
257
|
+
for (let i = index + 1; i < voice.length; i++) {
|
|
258
|
+
const ns = voice[i];
|
|
259
|
+
const val = ns.getDurationValue();
|
|
260
|
+
if (consumed + val <= delta + 0.001) {
|
|
261
|
+
consumed += val;
|
|
262
|
+
removeCount++;
|
|
263
|
+
if (consumed >= delta - 0.001)
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const remaining = consumed + val - delta;
|
|
268
|
+
replacements.push(...this.decomposeToRests(remaining));
|
|
269
|
+
removeCount++;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
updatedVoice.splice(index + 1, removeCount, ...replacements);
|
|
274
|
+
}
|
|
145
275
|
const newVoices = [...this.voices];
|
|
146
|
-
newVoices[voiceIndex] =
|
|
147
|
-
|
|
276
|
+
newVoices[voiceIndex] = updatedVoice;
|
|
277
|
+
let result = this.withVoices(newVoices).simplifyRests(voiceIndex);
|
|
278
|
+
if (this.timeSignature) {
|
|
279
|
+
result = result.autoBeam(this.timeSignature);
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
148
282
|
}
|
|
149
283
|
replaceNote(noteIndex, newNote, voiceIndex = 0) {
|
|
150
284
|
return this.replaceNoteSet(noteIndex, new NoteSet([newNote]), voiceIndex);
|
|
@@ -155,13 +289,20 @@ export class Measure {
|
|
|
155
289
|
const voice = [...this.voices[voiceIndex]];
|
|
156
290
|
if (noteIndex < 0 || noteIndex >= voice.length)
|
|
157
291
|
return this;
|
|
158
|
-
|
|
292
|
+
// Instead of splicing, replace with a rest of the same duration
|
|
293
|
+
const target = voice[noteIndex];
|
|
294
|
+
voice[noteIndex] = target.withRest(true);
|
|
159
295
|
const newVoices = [...this.voices];
|
|
160
296
|
newVoices[voiceIndex] = voice;
|
|
161
|
-
return this.withVoices(newVoices);
|
|
297
|
+
return this.withVoices(newVoices).simplifyRests(voiceIndex);
|
|
162
298
|
}
|
|
163
299
|
autoBeam(timeSign) {
|
|
300
|
+
const ts = timeSign || this.timeSignature;
|
|
301
|
+
if (!ts)
|
|
302
|
+
return this;
|
|
164
303
|
const newVoices = this.voices.map((voice) => {
|
|
304
|
+
if (voice.length === 0)
|
|
305
|
+
return [];
|
|
165
306
|
let currentOffset = 0;
|
|
166
307
|
let lastGroupId = undefined;
|
|
167
308
|
let lastGroupStartTime = 0;
|
|
@@ -171,28 +312,38 @@ export class Measure {
|
|
|
171
312
|
const noteDuration = ns.getDurationValue();
|
|
172
313
|
let groupToAssign = undefined;
|
|
173
314
|
if (ns.isBeamable()) {
|
|
174
|
-
const isSixteenth = noteDuration < 0.5;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
315
|
+
const isSixteenth = noteDuration < 0.5 - 0.001;
|
|
316
|
+
// Determine the logical "beat length" for beaming purposes
|
|
317
|
+
let beamBeatLength = 4 / ts.beatType; // Default is one beat
|
|
318
|
+
if (ts.beatType === 4) {
|
|
319
|
+
if (ts.beats === 4 || ts.beats === 2) {
|
|
320
|
+
// In 4/4 or 2/4, eighth notes are often beamed in groups of 2 beats (length 2.0)
|
|
321
|
+
// unless there are sixteenth notes which force 1-beat groupings.
|
|
322
|
+
beamBeatLength = lastGroupHasSixteenths || isSixteenth ? 1.0 : 2.0;
|
|
323
|
+
}
|
|
178
324
|
}
|
|
179
|
-
else if (
|
|
180
|
-
|
|
325
|
+
else if (ts.beatType === 8) {
|
|
326
|
+
if (ts.beats % 3 === 0) {
|
|
327
|
+
// Compound meter (6/8, 9/8, 12/8): Beam by 3 eighth notes (1.5 length)
|
|
328
|
+
beamBeatLength = 1.5;
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Simple meter in 8 (e.g. 2/8, 4/8): Beam by quarter note (1.0 length)
|
|
332
|
+
beamBeatLength = 1.0;
|
|
333
|
+
}
|
|
181
334
|
}
|
|
182
|
-
|
|
183
|
-
currentBeatLength = lastGroupHasSixteenths || isSixteenth ? 1.0 : 2.0;
|
|
184
|
-
}
|
|
185
|
-
const currentBeatId = Math.floor(currentOffset / currentBeatLength + 0.001);
|
|
335
|
+
const currentBeatId = Math.floor(currentOffset / beamBeatLength + 0.001);
|
|
186
336
|
const endOffset = currentOffset + noteDuration;
|
|
187
|
-
const crossesBeat = Math.floor(endOffset /
|
|
337
|
+
const crossesBeat = Math.floor(endOffset / beamBeatLength - 0.001) !== currentBeatId;
|
|
188
338
|
const sameBeatAsLast = lastGroupId !== undefined &&
|
|
189
|
-
Math.floor(lastGroupStartTime /
|
|
339
|
+
Math.floor(lastGroupStartTime / beamBeatLength + 0.001) === currentBeatId;
|
|
190
340
|
if (sameBeatAsLast && !crossesBeat) {
|
|
191
341
|
groupToAssign = lastGroupId;
|
|
192
342
|
if (isSixteenth)
|
|
193
343
|
lastGroupHasSixteenths = true;
|
|
194
344
|
}
|
|
195
345
|
else {
|
|
346
|
+
// Start a new group
|
|
196
347
|
lastGroupId = Math.floor(Math.random() * 1000000) + 1;
|
|
197
348
|
lastGroupStartTime = currentOffset;
|
|
198
349
|
lastGroupHasSixteenths = isSixteenth;
|
|
@@ -200,12 +351,14 @@ export class Measure {
|
|
|
200
351
|
}
|
|
201
352
|
}
|
|
202
353
|
else {
|
|
354
|
+
// Non-beamable note (Quarter, Half, etc.) or Rest
|
|
203
355
|
lastGroupId = undefined;
|
|
204
356
|
lastGroupHasSixteenths = false;
|
|
205
357
|
}
|
|
206
358
|
tempNoteSets.push(ns.withBeamGroup(groupToAssign));
|
|
207
359
|
currentOffset += noteDuration;
|
|
208
360
|
}
|
|
361
|
+
// Cleanup: groups of 1 note are not beamed
|
|
209
362
|
const groupCounts = new Map();
|
|
210
363
|
for (const ns of tempNoteSets) {
|
|
211
364
|
if (ns.beamGroup !== undefined) {
|
|
@@ -294,6 +447,46 @@ export class Measure {
|
|
|
294
447
|
}
|
|
295
448
|
return this.withVoices(newVoices);
|
|
296
449
|
}
|
|
450
|
+
simplifyRests(voiceIndex = 0) {
|
|
451
|
+
const voice = this.voices[voiceIndex];
|
|
452
|
+
if (!voice || voice.length === 0)
|
|
453
|
+
return this;
|
|
454
|
+
const totalDur = this.getTotalDuration(voiceIndex);
|
|
455
|
+
const expectedDur = this.timeSignature
|
|
456
|
+
? (this.timeSignature.beats * 4) / this.timeSignature.beatType
|
|
457
|
+
: 4.0;
|
|
458
|
+
// Special Case: If the whole measure is rests, just return a single whole rest (or appropriate duration)
|
|
459
|
+
const allRests = voice.every((ns) => ns.isRest);
|
|
460
|
+
if (allRests && Math.abs(totalDur - expectedDur) < 0.001) {
|
|
461
|
+
const newVoice = [new NoteSet([new Note(Duration.Whole, undefined, true)])];
|
|
462
|
+
const newVoices = [...this.voices];
|
|
463
|
+
newVoices[voiceIndex] = newVoice;
|
|
464
|
+
return this.withVoices(newVoices);
|
|
465
|
+
}
|
|
466
|
+
const newVoice = [];
|
|
467
|
+
let currentRestSum = 0;
|
|
468
|
+
for (const ns of voice) {
|
|
469
|
+
if (ns.isRest) {
|
|
470
|
+
currentRestSum += ns.getDurationValue();
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
if (currentRestSum > 0.001) {
|
|
474
|
+
newVoice.push(...this.decomposeToRests(currentRestSum));
|
|
475
|
+
currentRestSum = 0;
|
|
476
|
+
}
|
|
477
|
+
newVoice.push(ns);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (currentRestSum > 0.001) {
|
|
481
|
+
newVoice.push(...this.decomposeToRests(currentRestSum));
|
|
482
|
+
}
|
|
483
|
+
const newVoices = [...this.voices];
|
|
484
|
+
newVoices[voiceIndex] = newVoice;
|
|
485
|
+
return this.withVoices(newVoices);
|
|
486
|
+
}
|
|
487
|
+
decomposeToRests(value) {
|
|
488
|
+
return decomposeDuration(value).map((d) => new NoteSet([new Note(d.duration, undefined, true, d.isDotted)]));
|
|
489
|
+
}
|
|
297
490
|
toJSON() {
|
|
298
491
|
return {
|
|
299
492
|
voices: this.voices.map((v) => v.map((ns) => ns.toJSON())),
|
package/dist/models/Note.d.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import { Pitch } from './Pitch';
|
|
2
|
-
import { Accidental, Arpeggio, Articulation, Bowing, Duration, Dynamic, FretboardDiagram, Glissando, Hairpin, Lyric, NoteheadShape, Ornament, Ottava, Pedal, Slur, StemDirection, Tuplet } from './types';
|
|
3
|
-
/**
|
|
4
|
-
* Represents a single note or rest in a measure.
|
|
5
|
-
*/
|
|
1
|
+
import { Pitch } from './Pitch.js';
|
|
2
|
+
import { Accidental, Arpeggio, Articulation, Bowing, Duration, Dynamic, FretboardDiagram, Glissando, Hairpin, Lyric, NoteheadShape, Ornament, Ottava, Pedal, Slur, StemDirection, Tuplet } from './types.js';
|
|
6
3
|
export declare class Note {
|
|
7
4
|
readonly duration: Duration;
|
|
8
5
|
readonly pitch?: Pitch | undefined;
|
|
9
6
|
readonly isRest: boolean;
|
|
10
|
-
readonly isDotted: boolean;
|
|
11
7
|
readonly accidental?: Accidental | undefined;
|
|
12
8
|
readonly beamGroup?: number | undefined;
|
|
13
9
|
readonly articulations: Articulation[];
|
|
@@ -17,7 +13,7 @@ export declare class Note {
|
|
|
17
13
|
readonly tuplet?: Tuplet | undefined;
|
|
18
14
|
readonly hairpin?: Hairpin | undefined;
|
|
19
15
|
readonly isGrace: boolean;
|
|
20
|
-
readonly lyric?:
|
|
16
|
+
readonly lyric?: Lyric | undefined;
|
|
21
17
|
readonly chord?: string | undefined;
|
|
22
18
|
readonly glissando?: Glissando | undefined;
|
|
23
19
|
readonly arpeggio?: Arpeggio | undefined;
|
|
@@ -27,7 +23,7 @@ export declare class Note {
|
|
|
27
23
|
readonly fret?: number | undefined;
|
|
28
24
|
readonly string?: number | undefined;
|
|
29
25
|
readonly fretboardDiagram?: FretboardDiagram | undefined;
|
|
30
|
-
readonly lyrics?:
|
|
26
|
+
readonly lyrics?: Lyric[] | undefined;
|
|
31
27
|
readonly staffText?: string | undefined;
|
|
32
28
|
readonly color?: string | undefined;
|
|
33
29
|
readonly notehead?: NoteheadShape | undefined;
|
|
@@ -39,8 +35,10 @@ export declare class Note {
|
|
|
39
35
|
readonly hammerOn?: "start" | "stop" | undefined;
|
|
40
36
|
readonly pullOff?: "start" | "stop" | undefined;
|
|
41
37
|
constructor(duration: Duration, pitch?: Pitch | undefined, // undefined for rests
|
|
42
|
-
isRest?: boolean,
|
|
43
|
-
articulations?: Articulation[], dynamic?: Dynamic | undefined, tie?: boolean | undefined, slur?: Slur | undefined, tuplet?: Tuplet | undefined, hairpin?: Hairpin | undefined, isGrace?: boolean, lyric?:
|
|
38
|
+
isRest?: boolean, dotCountOrIsDotted?: number | boolean, accidental?: Accidental | undefined, beamGroup?: number | undefined, // Group ID for beamed notes
|
|
39
|
+
articulations?: Articulation[], dynamic?: Dynamic | undefined, tie?: boolean | undefined, slur?: Slur | undefined, tuplet?: Tuplet | undefined, hairpin?: Hairpin | undefined, isGrace?: boolean, lyric?: Lyric | undefined, chord?: string | undefined, glissando?: Glissando | undefined, arpeggio?: Arpeggio | undefined, ottava?: Ottava | undefined, pedal?: Pedal | undefined, ornament?: Ornament | undefined, fret?: number | undefined, string?: number | undefined, fretboardDiagram?: FretboardDiagram | undefined, lyrics?: Lyric[] | undefined, staffText?: string | undefined, color?: string | undefined, notehead?: NoteheadShape | undefined, bowing?: Bowing | undefined, fingering?: number | undefined, stemDirection?: StemDirection | undefined, isStringCircled?: boolean | undefined, palmMute?: "start" | "stop" | undefined, hammerOn?: "start" | "stop" | undefined, pullOff?: "start" | "stop" | undefined);
|
|
40
|
+
readonly dotCount: number;
|
|
41
|
+
get isDotted(): boolean;
|
|
44
42
|
/**
|
|
45
43
|
* Get all lyrics as standardized objects
|
|
46
44
|
*/
|
|
@@ -70,11 +68,11 @@ export declare class Note {
|
|
|
70
68
|
withTie(tie?: boolean): Note;
|
|
71
69
|
withSlur(slur?: Slur): Note;
|
|
72
70
|
withTuplet(tuplet?: Tuplet): Note;
|
|
73
|
-
withLyric(lyric?:
|
|
74
|
-
withLyrics(lyrics:
|
|
71
|
+
withLyric(lyric?: Lyric): Note;
|
|
72
|
+
withLyrics(lyrics: Lyric[]): Note;
|
|
75
73
|
withHairpin(hairpin?: Hairpin): Note;
|
|
76
74
|
withAccidental(accidental?: Accidental): Note;
|
|
77
|
-
withDuration(duration: Duration,
|
|
75
|
+
withDuration(duration: Duration, dotCountOrIsDotted?: number | boolean): Note;
|
|
78
76
|
withRest(isRest: boolean): Note;
|
|
79
77
|
withFretboardDiagram(diagram?: FretboardDiagram): Note;
|
|
80
78
|
withBeamGroup(beamGroup?: number): Note;
|
|
@@ -107,6 +105,7 @@ export interface NoteJSON {
|
|
|
107
105
|
};
|
|
108
106
|
isRest?: boolean;
|
|
109
107
|
isDotted?: boolean;
|
|
108
|
+
dotCount?: number;
|
|
110
109
|
accidental?: string;
|
|
111
110
|
beamGroup?: number;
|
|
112
111
|
articulation?: string;
|
|
@@ -117,7 +116,7 @@ export interface NoteJSON {
|
|
|
117
116
|
tuplet?: Tuplet;
|
|
118
117
|
hairpin?: Hairpin;
|
|
119
118
|
isGrace?: boolean;
|
|
120
|
-
lyric?:
|
|
119
|
+
lyric?: Lyric;
|
|
121
120
|
chord?: string;
|
|
122
121
|
glissando?: Glissando;
|
|
123
122
|
arpeggio?: Arpeggio;
|
|
@@ -127,7 +126,7 @@ export interface NoteJSON {
|
|
|
127
126
|
fret?: number;
|
|
128
127
|
string?: number;
|
|
129
128
|
fretboardDiagram?: FretboardDiagram;
|
|
130
|
-
lyrics?:
|
|
129
|
+
lyrics?: Lyric[];
|
|
131
130
|
staffText?: string;
|
|
132
131
|
color?: string;
|
|
133
132
|
notehead?: string;
|