@scorelabs/core 1.0.9 → 1.0.10
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/importers/MusicXMLParser.d.ts +5 -0
- package/dist/importers/MusicXMLParser.js +189 -89
- package/dist/models/Measure.d.ts +3 -1
- package/dist/models/Measure.js +126 -19
- package/dist/models/Note.d.ts +7 -7
- package/dist/models/Note.js +2 -2
- package/dist/models/NoteSet.d.ts +8 -8
- package/dist/models/Score.d.ts +4 -4
- 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
|
@@ -13,12 +13,17 @@ export declare class MusicXMLParser {
|
|
|
13
13
|
private instrumentPitchMap;
|
|
14
14
|
private _domParser;
|
|
15
15
|
constructor(domParserInstance?: DOMParser | unknown);
|
|
16
|
+
private querySelector;
|
|
17
|
+
private getChildren;
|
|
18
|
+
private getFirstElementChild;
|
|
19
|
+
private querySelectorAll;
|
|
16
20
|
private parseFromString;
|
|
17
21
|
static getXMLFromBinary(data: ArrayBuffer, domParser?: DOMParser | unknown): Promise<string>;
|
|
18
22
|
parseBinary(data: ArrayBuffer): Promise<ScoreJSON>;
|
|
19
23
|
parse(xmlString: string): ScoreJSON;
|
|
20
24
|
private parseSubtitle;
|
|
21
25
|
private parseTitle;
|
|
26
|
+
private parseCopyright;
|
|
22
27
|
private parseComposer;
|
|
23
28
|
private parsePartList;
|
|
24
29
|
private parseParts;
|
|
@@ -18,6 +18,52 @@ export class MusicXMLParser {
|
|
|
18
18
|
this._domParser =
|
|
19
19
|
domParserInstance || (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
|
|
20
20
|
}
|
|
21
|
+
querySelector(el, selector) {
|
|
22
|
+
if (el.querySelector)
|
|
23
|
+
return el.querySelector(selector);
|
|
24
|
+
const parts = selector.split(' ');
|
|
25
|
+
let current = el;
|
|
26
|
+
for (const part of parts) {
|
|
27
|
+
if (!current)
|
|
28
|
+
return null;
|
|
29
|
+
const results = current.getElementsByTagName(part);
|
|
30
|
+
if (results.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
current = results[0];
|
|
33
|
+
}
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
getChildren(el) {
|
|
37
|
+
const parent = el;
|
|
38
|
+
if (parent.children)
|
|
39
|
+
return Array.from(parent.children);
|
|
40
|
+
return Array.from(el.childNodes).filter((node) => node.nodeType === 1);
|
|
41
|
+
}
|
|
42
|
+
getFirstElementChild(el) {
|
|
43
|
+
if (el.firstElementChild)
|
|
44
|
+
return el.firstElementChild;
|
|
45
|
+
const children = this.getChildren(el);
|
|
46
|
+
return children.length > 0 ? children[0] : null;
|
|
47
|
+
}
|
|
48
|
+
querySelectorAll(el, selector) {
|
|
49
|
+
if (el.querySelectorAll)
|
|
50
|
+
return Array.from(el.querySelectorAll(selector));
|
|
51
|
+
const parts = selector.split(' ');
|
|
52
|
+
if (parts.length === 1) {
|
|
53
|
+
return Array.from(el.getElementsByTagName(parts[0]));
|
|
54
|
+
}
|
|
55
|
+
// Simple nested selector support (only two levels for now as needed)
|
|
56
|
+
if (parts.length === 2) {
|
|
57
|
+
const firstLevel = Array.from(el.getElementsByTagName(parts[0]));
|
|
58
|
+
const results = [];
|
|
59
|
+
firstLevel.forEach((parent) => {
|
|
60
|
+
const children = Array.from(parent.getElementsByTagName(parts[1]));
|
|
61
|
+
results.push(...children);
|
|
62
|
+
});
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
21
67
|
parseFromString(xmlString) {
|
|
22
68
|
if (!this._domParser) {
|
|
23
69
|
throw new Error('No DOMParser available. Please provide one in the constructor for Node.js environments.');
|
|
@@ -35,7 +81,7 @@ export class MusicXMLParser {
|
|
|
35
81
|
if (!_parser)
|
|
36
82
|
throw new Error('No DOMParser available');
|
|
37
83
|
const containerDoc = _parser.parseFromString(containerXml, 'application/xml');
|
|
38
|
-
const rootfile =
|
|
84
|
+
const rootfile = (new MusicXMLParser(_parser)).querySelector(containerDoc, 'rootfile');
|
|
39
85
|
const fullPath = rootfile?.getAttribute('full-path');
|
|
40
86
|
if (!fullPath)
|
|
41
87
|
throw new Error('Invalid MXL: Could not find main score file path');
|
|
@@ -63,7 +109,7 @@ export class MusicXMLParser {
|
|
|
63
109
|
}
|
|
64
110
|
xmlString = xmlString.substring(firstBracket).trim();
|
|
65
111
|
const doc = this.parseFromString(xmlString);
|
|
66
|
-
const parseError =
|
|
112
|
+
const parseError = this.querySelector(doc, 'parsererror');
|
|
67
113
|
if (parseError) {
|
|
68
114
|
const preview = xmlString.substring(0, 100).replace(/\n/g, ' ');
|
|
69
115
|
throw new Error(`XML parsing error: ${parseError.textContent}. Content preview: "${preview}..."`);
|
|
@@ -74,6 +120,7 @@ export class MusicXMLParser {
|
|
|
74
120
|
const title = this.parseTitle(doc);
|
|
75
121
|
const subtitle = this.parseSubtitle(doc);
|
|
76
122
|
const composer = this.parseComposer(doc);
|
|
123
|
+
const copyright = this.parseCopyright(doc);
|
|
77
124
|
const partInfo = this.parsePartList(doc);
|
|
78
125
|
const parts = this.parseParts(doc, partInfo);
|
|
79
126
|
let globalBpm = 120;
|
|
@@ -104,6 +151,7 @@ export class MusicXMLParser {
|
|
|
104
151
|
tempoDuration: globalTempoDuration,
|
|
105
152
|
tempoIsDotted: globalTempoIsDotted,
|
|
106
153
|
tempoText: globalTempoText,
|
|
154
|
+
copyright,
|
|
107
155
|
};
|
|
108
156
|
return this.postProcess(score);
|
|
109
157
|
}
|
|
@@ -111,7 +159,7 @@ export class MusicXMLParser {
|
|
|
111
159
|
let subtitle = this.getText(doc.documentElement, 'movement-subtitle');
|
|
112
160
|
if (subtitle)
|
|
113
161
|
return subtitle;
|
|
114
|
-
const work =
|
|
162
|
+
const work = this.querySelector(doc, 'work');
|
|
115
163
|
if (work) {
|
|
116
164
|
subtitle = this.getText(work, 'work-subtitle');
|
|
117
165
|
if (subtitle)
|
|
@@ -120,11 +168,11 @@ export class MusicXMLParser {
|
|
|
120
168
|
if (subtitle)
|
|
121
169
|
return subtitle;
|
|
122
170
|
}
|
|
123
|
-
const credits =
|
|
171
|
+
const credits = this.querySelectorAll(doc, 'credit');
|
|
124
172
|
for (const credit of Array.from(credits)) {
|
|
125
|
-
const type =
|
|
173
|
+
const type = this.querySelector(credit, 'credit-type');
|
|
126
174
|
if (type?.textContent?.toLowerCase() === 'subtitle') {
|
|
127
|
-
const words =
|
|
175
|
+
const words = this.querySelector(credit, 'credit-words');
|
|
128
176
|
if (words?.textContent)
|
|
129
177
|
return words.textContent;
|
|
130
178
|
}
|
|
@@ -137,23 +185,42 @@ export class MusicXMLParser {
|
|
|
137
185
|
title = this.getText(doc.documentElement, 'movement-title');
|
|
138
186
|
return title ?? 'Untitled';
|
|
139
187
|
}
|
|
188
|
+
parseCopyright(doc) {
|
|
189
|
+
const identification = this.querySelector(doc, 'identification');
|
|
190
|
+
if (identification) {
|
|
191
|
+
const rights = this.getText(identification, 'rights');
|
|
192
|
+
if (rights)
|
|
193
|
+
return rights;
|
|
194
|
+
}
|
|
195
|
+
const credits = this.querySelectorAll(doc, 'credit');
|
|
196
|
+
for (const credit of Array.from(credits)) {
|
|
197
|
+
const type = this.querySelector(credit, 'credit-type');
|
|
198
|
+
if (type?.textContent?.toLowerCase() === 'rights' ||
|
|
199
|
+
type?.textContent?.toLowerCase() === 'copyright') {
|
|
200
|
+
const words = this.querySelector(credit, 'credit-words');
|
|
201
|
+
if (words?.textContent)
|
|
202
|
+
return words.textContent;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return '';
|
|
206
|
+
}
|
|
140
207
|
parseComposer(doc) {
|
|
141
|
-
const creators =
|
|
208
|
+
const creators = this.querySelectorAll(doc, 'identification creator');
|
|
142
209
|
for (const creator of Array.from(creators)) {
|
|
143
210
|
if (creator.getAttribute('type') === 'composer')
|
|
144
211
|
return creator.textContent ?? 'Unknown';
|
|
145
212
|
}
|
|
146
|
-
return
|
|
213
|
+
return this.querySelector(doc, 'identification creator')?.textContent ?? 'Unknown';
|
|
147
214
|
}
|
|
148
215
|
parsePartList(doc) {
|
|
149
216
|
const partInfo = new Map();
|
|
150
|
-
const scoreParts =
|
|
217
|
+
const scoreParts = this.querySelectorAll(doc, 'part-list score-part');
|
|
151
218
|
for (const scorePart of Array.from(scoreParts)) {
|
|
152
219
|
const id = scorePart.getAttribute('id');
|
|
153
220
|
const name = this.getText(scorePart, 'part-name') || 'Part';
|
|
154
221
|
let instrument;
|
|
155
222
|
// Find first midi-instrument to determine main instrument
|
|
156
|
-
const firstMidiInst =
|
|
223
|
+
const firstMidiInst = this.querySelector(scorePart, 'midi-instrument');
|
|
157
224
|
if (firstMidiInst) {
|
|
158
225
|
const programStr = this.getText(firstMidiInst, 'midi-program');
|
|
159
226
|
if (programStr) {
|
|
@@ -176,7 +243,7 @@ export class MusicXMLParser {
|
|
|
176
243
|
if (id) {
|
|
177
244
|
partInfo.set(id, { name: name.trim(), instrument });
|
|
178
245
|
}
|
|
179
|
-
const midiInstruments =
|
|
246
|
+
const midiInstruments = this.querySelectorAll(scorePart, 'midi-instrument');
|
|
180
247
|
for (const midiInst of Array.from(midiInstruments)) {
|
|
181
248
|
const instId = midiInst.getAttribute('id');
|
|
182
249
|
const unpitched = this.getNumber(midiInst, 'midi-unpitched');
|
|
@@ -188,7 +255,7 @@ export class MusicXMLParser {
|
|
|
188
255
|
}
|
|
189
256
|
parseParts(doc, partInfo) {
|
|
190
257
|
const parts = [];
|
|
191
|
-
const partElements =
|
|
258
|
+
const partElements = this.querySelectorAll(doc, 'part');
|
|
192
259
|
for (const partElement of Array.from(partElements)) {
|
|
193
260
|
const id = partElement.getAttribute('id');
|
|
194
261
|
const info = partInfo.get(id ?? '');
|
|
@@ -199,11 +266,11 @@ export class MusicXMLParser {
|
|
|
199
266
|
return parts;
|
|
200
267
|
}
|
|
201
268
|
parsePart(partElement) {
|
|
202
|
-
const measureElements =
|
|
269
|
+
const measureElements = this.querySelectorAll(partElement, 'measure');
|
|
203
270
|
let numStaves = 1;
|
|
204
|
-
const firstMeasure =
|
|
271
|
+
const firstMeasure = this.querySelector(partElement, 'measure');
|
|
205
272
|
if (firstMeasure) {
|
|
206
|
-
const stavesNode =
|
|
273
|
+
const stavesNode = this.querySelector(firstMeasure, 'attributes staves');
|
|
207
274
|
if (stavesNode)
|
|
208
275
|
numStaves = parseInt(stavesNode.textContent || '1');
|
|
209
276
|
}
|
|
@@ -212,12 +279,17 @@ export class MusicXMLParser {
|
|
|
212
279
|
measures: [],
|
|
213
280
|
}));
|
|
214
281
|
const staffClefs = Array(numStaves).fill(Clef.Treble);
|
|
215
|
-
|
|
282
|
+
const lastVoicePerStaff = new Map();
|
|
283
|
+
let divisions = 1;
|
|
284
|
+
for (const [mIdx, measureElement] of Array.from(measureElements).entries()) {
|
|
216
285
|
let measureTimeSignature;
|
|
217
286
|
let measureKeySignature;
|
|
218
|
-
|
|
219
|
-
const attributes =
|
|
287
|
+
let isPickup = measureElement.getAttribute('implicit') === 'yes';
|
|
288
|
+
const attributes = this.querySelector(measureElement, 'attributes');
|
|
220
289
|
if (attributes) {
|
|
290
|
+
const divNode = this.querySelector(attributes, 'divisions');
|
|
291
|
+
if (divNode)
|
|
292
|
+
divisions = parseInt(divNode.textContent || '1');
|
|
221
293
|
const fifths = this.getNumber(attributes, 'key fifths');
|
|
222
294
|
if (fifths !== null) {
|
|
223
295
|
this.currentKeyFifths = fifths;
|
|
@@ -229,7 +301,7 @@ export class MusicXMLParser {
|
|
|
229
301
|
this.currentBeats = beats;
|
|
230
302
|
if (beatType !== null)
|
|
231
303
|
this.currentBeatType = beatType;
|
|
232
|
-
const timeElement =
|
|
304
|
+
const timeElement = this.querySelector(attributes, 'time');
|
|
233
305
|
if (timeElement) {
|
|
234
306
|
const symAttr = timeElement.getAttribute('symbol');
|
|
235
307
|
if (symAttr === 'common')
|
|
@@ -245,9 +317,10 @@ export class MusicXMLParser {
|
|
|
245
317
|
symbol: this.currentSymbol !== 'normal' ? this.currentSymbol : undefined,
|
|
246
318
|
};
|
|
247
319
|
}
|
|
248
|
-
const clefElements =
|
|
320
|
+
const clefElements = this.querySelectorAll(attributes, 'clef');
|
|
249
321
|
clefElements.forEach((clef) => {
|
|
250
|
-
const
|
|
322
|
+
const numberAttrs = clef.getAttribute('number');
|
|
323
|
+
const number = numberAttrs ? parseInt(numberAttrs) : 1;
|
|
251
324
|
const sign = this.getText(clef, 'sign');
|
|
252
325
|
if (sign && number <= numStaves) {
|
|
253
326
|
const line = this.getNumber(clef, 'line') || 0;
|
|
@@ -255,7 +328,7 @@ export class MusicXMLParser {
|
|
|
255
328
|
staffClefs[number - 1] = this.mapClef(sign, line, octaveChange);
|
|
256
329
|
}
|
|
257
330
|
});
|
|
258
|
-
const staffDetails =
|
|
331
|
+
const staffDetails = this.querySelectorAll(attributes, 'staff-details');
|
|
259
332
|
staffDetails.forEach((sd) => {
|
|
260
333
|
const number = parseInt(sd.getAttribute('number') || '1');
|
|
261
334
|
const lines = this.getNumber(sd, 'staff-lines');
|
|
@@ -272,7 +345,7 @@ export class MusicXMLParser {
|
|
|
272
345
|
const staffVoices = new Map();
|
|
273
346
|
for (let i = 0; i < numStaves; i++)
|
|
274
347
|
staffVoices.set(i, new Map());
|
|
275
|
-
const children =
|
|
348
|
+
const children = this.getChildren(measureElement);
|
|
276
349
|
let pendingChord = undefined;
|
|
277
350
|
let currentNoteSetMap = new Map();
|
|
278
351
|
const beamGroupIdMap = new Map();
|
|
@@ -287,24 +360,24 @@ export class MusicXMLParser {
|
|
|
287
360
|
pendingChord = harmonyData;
|
|
288
361
|
}
|
|
289
362
|
else if (child.nodeName === 'direction') {
|
|
290
|
-
const metronome =
|
|
363
|
+
const metronome = this.querySelector(child, 'metronome');
|
|
291
364
|
if (metronome) {
|
|
292
365
|
const beatUnit = this.getText(metronome, 'beat-unit') || 'quarter';
|
|
293
366
|
const bpm = parseInt(this.getText(metronome, 'per-minute') || '120');
|
|
294
|
-
const isDotted =
|
|
367
|
+
const isDotted = this.querySelector(metronome, 'beat-unit-dot') !== null;
|
|
295
368
|
measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted };
|
|
296
369
|
}
|
|
297
|
-
const rehearsal =
|
|
370
|
+
const rehearsal = this.querySelector(child, 'rehearsal');
|
|
298
371
|
if (rehearsal)
|
|
299
372
|
rehearsalMark = rehearsal.textContent || undefined;
|
|
300
|
-
const words =
|
|
373
|
+
const words = this.querySelector(child, 'words');
|
|
301
374
|
if (words) {
|
|
302
375
|
if (measureTempo)
|
|
303
376
|
measureTempo.text = words.textContent || undefined;
|
|
304
377
|
else
|
|
305
378
|
systemText = words.textContent || undefined;
|
|
306
379
|
}
|
|
307
|
-
const wedge =
|
|
380
|
+
const wedge = this.querySelector(child, 'wedge');
|
|
308
381
|
if (wedge) {
|
|
309
382
|
const typeAttr = wedge.getAttribute('type');
|
|
310
383
|
if (typeAttr === 'crescendo' || typeAttr === 'diminuendo' || typeAttr === 'stop') {
|
|
@@ -314,7 +387,7 @@ export class MusicXMLParser {
|
|
|
314
387
|
};
|
|
315
388
|
}
|
|
316
389
|
}
|
|
317
|
-
const octShift =
|
|
390
|
+
const octShift = this.querySelector(child, 'octave-shift');
|
|
318
391
|
if (octShift) {
|
|
319
392
|
const typeAttr = octShift.getAttribute('type');
|
|
320
393
|
const size = parseInt(octShift.getAttribute('size') || '8');
|
|
@@ -331,25 +404,27 @@ export class MusicXMLParser {
|
|
|
331
404
|
};
|
|
332
405
|
}
|
|
333
406
|
}
|
|
334
|
-
const pedalNode =
|
|
407
|
+
const pedalNode = this.querySelector(child, 'pedal');
|
|
335
408
|
if (pedalNode) {
|
|
336
409
|
const typeAttr = pedalNode.getAttribute('type');
|
|
337
410
|
if (typeAttr === 'start' || typeAttr === 'stop') {
|
|
338
411
|
currentPedal = { type: 'sustain', placement: typeAttr };
|
|
339
412
|
}
|
|
340
413
|
}
|
|
341
|
-
const dynamics =
|
|
342
|
-
if (dynamics
|
|
343
|
-
|
|
414
|
+
const dynamics = this.querySelector(child, 'dynamics');
|
|
415
|
+
if (dynamics) {
|
|
416
|
+
const firstChild = this.getFirstElementChild(dynamics);
|
|
417
|
+
if (firstChild)
|
|
418
|
+
contextDynamic = firstChild.nodeName.toLowerCase();
|
|
344
419
|
}
|
|
345
420
|
}
|
|
346
421
|
else if (child.nodeName === 'note') {
|
|
347
422
|
const staffNum = parseInt(this.getText(child, 'staff') || '1');
|
|
348
423
|
const staffIdx = Math.min(Math.max(staffNum - 1, 0), numStaves - 1);
|
|
349
|
-
const isChord =
|
|
424
|
+
const isChord = this.querySelector(child, 'chord') !== null;
|
|
350
425
|
const note = this.parseNote(child);
|
|
351
426
|
if (note) {
|
|
352
|
-
const beamEl =
|
|
427
|
+
const beamEl = this.querySelector(child, 'beam[number="1"]');
|
|
353
428
|
if (beamEl) {
|
|
354
429
|
const beamType = beamEl.textContent;
|
|
355
430
|
if (beamType === 'begin') {
|
|
@@ -389,6 +464,7 @@ export class MusicXMLParser {
|
|
|
389
464
|
currentPedal = undefined;
|
|
390
465
|
}
|
|
391
466
|
const voiceId = this.getText(child, 'voice') || '1';
|
|
467
|
+
lastVoicePerStaff.set(staffIdx, voiceId);
|
|
392
468
|
const voiceMap = staffVoices.get(staffIdx);
|
|
393
469
|
if (!voiceMap.has(voiceId))
|
|
394
470
|
voiceMap.set(voiceId, []);
|
|
@@ -411,7 +487,7 @@ export class MusicXMLParser {
|
|
|
411
487
|
else if (child.nodeName === 'backup' || child.nodeName === 'forward') {
|
|
412
488
|
for (const [sIdx, notes] of currentNoteSetMap.entries()) {
|
|
413
489
|
if (notes.length > 0) {
|
|
414
|
-
const voiceId =
|
|
490
|
+
const voiceId = lastVoicePerStaff.get(sIdx) || '1';
|
|
415
491
|
const voiceMap = staffVoices.get(sIdx);
|
|
416
492
|
if (!voiceMap.has(voiceId))
|
|
417
493
|
voiceMap.set(voiceId, []);
|
|
@@ -421,16 +497,39 @@ export class MusicXMLParser {
|
|
|
421
497
|
currentNoteSetMap.clear();
|
|
422
498
|
}
|
|
423
499
|
});
|
|
500
|
+
// Cleanup: push remaining notes
|
|
424
501
|
for (const [sIdx, notes] of currentNoteSetMap.entries()) {
|
|
425
502
|
if (notes.length > 0) {
|
|
426
|
-
|
|
427
|
-
const voiceId = '1'; // Defaulting to 1 for final push if not tracked
|
|
503
|
+
const voiceId = lastVoicePerStaff.get(sIdx) || '1';
|
|
428
504
|
const voiceMap = staffVoices.get(sIdx);
|
|
429
505
|
if (!voiceMap.has(voiceId))
|
|
430
506
|
voiceMap.set(voiceId, []);
|
|
431
507
|
voiceMap.get(voiceId).push({ notes });
|
|
432
508
|
}
|
|
433
509
|
}
|
|
510
|
+
// Check if it's a pickup measure if mIdx is 0 and implicit is not set
|
|
511
|
+
if (mIdx === 0 && !isPickup) {
|
|
512
|
+
// Calculate the actual duration of the first staff's first voice
|
|
513
|
+
const vMap0 = staffVoices.get(0);
|
|
514
|
+
const v1 = vMap0.get('1') || vMap0.get(Array.from(vMap0.keys())[0]);
|
|
515
|
+
if (v1) {
|
|
516
|
+
// Check duration of first voice in first staff
|
|
517
|
+
const expectedDivisions = this.currentBeats * (divisions * 4 / this.currentBeatType);
|
|
518
|
+
let totalMeasureDivs = 0;
|
|
519
|
+
const notes = this.querySelectorAll(measureElement, 'note');
|
|
520
|
+
// Find first voice in first staff
|
|
521
|
+
for (const n of notes) {
|
|
522
|
+
if ((this.getText(n, 'staff') || '1') === '1' && (this.getText(n, 'voice') || '1') === (Array.from(vMap0.keys())[0] || '1')) {
|
|
523
|
+
if (this.querySelector(n, 'chord'))
|
|
524
|
+
continue;
|
|
525
|
+
totalMeasureDivs += this.getNumber(n, 'duration') || 0;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (totalMeasureDivs > 0 && totalMeasureDivs < expectedDivisions - 1) {
|
|
529
|
+
isPickup = true;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
434
533
|
for (let i = 0; i < numStaves; i++) {
|
|
435
534
|
const vMap = staffVoices.get(i);
|
|
436
535
|
let voicesIndices = Array.from(vMap.keys()).sort();
|
|
@@ -480,21 +579,21 @@ export class MusicXMLParser {
|
|
|
480
579
|
return staves;
|
|
481
580
|
}
|
|
482
581
|
parseNote(noteElement) {
|
|
483
|
-
const isRest =
|
|
484
|
-
const isGrace =
|
|
582
|
+
const isRest = this.querySelector(noteElement, 'rest') !== null;
|
|
583
|
+
const isGrace = this.querySelector(noteElement, 'grace') !== null;
|
|
485
584
|
const type = this.getText(noteElement, 'type');
|
|
486
585
|
const duration = type ? this.mapDuration(type) : Duration.Quarter;
|
|
487
|
-
const isDotted =
|
|
586
|
+
const isDotted = this.querySelector(noteElement, 'dot') !== null;
|
|
488
587
|
if (isRest)
|
|
489
588
|
return { duration, isRest: true, isDotted };
|
|
490
589
|
let step = 'C', octave = 4, alter = 0;
|
|
491
|
-
const unpitched =
|
|
590
|
+
const unpitched = this.querySelector(noteElement, 'unpitched');
|
|
492
591
|
if (unpitched) {
|
|
493
592
|
step = this.getText(unpitched, 'display-step') || 'C';
|
|
494
593
|
octave = this.getNumber(unpitched, 'display-octave') ?? 4;
|
|
495
594
|
}
|
|
496
595
|
else {
|
|
497
|
-
const pitch =
|
|
596
|
+
const pitch = this.querySelector(noteElement, 'pitch');
|
|
498
597
|
if (!pitch)
|
|
499
598
|
return null;
|
|
500
599
|
step = this.getText(pitch, 'step') || 'C';
|
|
@@ -502,7 +601,7 @@ export class MusicXMLParser {
|
|
|
502
601
|
alter = this.getNumber(pitch, 'alter') ?? 0;
|
|
503
602
|
}
|
|
504
603
|
let midiNumber = this.pitchToMidi(step, octave, alter);
|
|
505
|
-
const instrument =
|
|
604
|
+
const instrument = this.querySelector(noteElement, 'instrument');
|
|
506
605
|
if (instrument) {
|
|
507
606
|
const instId = instrument.getAttribute('id');
|
|
508
607
|
if (instId && this.instrumentPitchMap.has(instId))
|
|
@@ -536,17 +635,17 @@ export class MusicXMLParser {
|
|
|
536
635
|
stemDirection = StemDirection.Up;
|
|
537
636
|
else if (stem === 'down')
|
|
538
637
|
stemDirection = StemDirection.Down;
|
|
539
|
-
const notations =
|
|
638
|
+
const notations = this.querySelector(noteElement, 'notations');
|
|
540
639
|
// Tuplet ratio
|
|
541
640
|
let tuplet = undefined;
|
|
542
|
-
const timeMod =
|
|
641
|
+
const timeMod = this.querySelector(noteElement, 'time-modification');
|
|
543
642
|
if (timeMod) {
|
|
544
643
|
const actual = this.getNumber(timeMod, 'actual-notes') || 3;
|
|
545
644
|
const normal = this.getNumber(timeMod, 'normal-notes') || 2;
|
|
546
645
|
tuplet = { actual, normal, type: 'middle' };
|
|
547
646
|
}
|
|
548
647
|
if (notations) {
|
|
549
|
-
const tupletEl =
|
|
648
|
+
const tupletEl = this.querySelector(notations, 'tuplet');
|
|
550
649
|
if (tupletEl && tuplet) {
|
|
551
650
|
const type = tupletEl.getAttribute('type');
|
|
552
651
|
if (type === 'start')
|
|
@@ -556,7 +655,7 @@ export class MusicXMLParser {
|
|
|
556
655
|
}
|
|
557
656
|
}
|
|
558
657
|
let slur = undefined;
|
|
559
|
-
const slurEl = notations
|
|
658
|
+
const slurEl = notations ? this.querySelector(notations, 'slur') : null;
|
|
560
659
|
if (slurEl) {
|
|
561
660
|
const type = slurEl.getAttribute('type');
|
|
562
661
|
if (type === 'start' || type === 'stop') {
|
|
@@ -568,29 +667,29 @@ export class MusicXMLParser {
|
|
|
568
667
|
slur.direction = 'down';
|
|
569
668
|
}
|
|
570
669
|
}
|
|
571
|
-
const tie =
|
|
670
|
+
const tie = this.querySelector(noteElement, 'tie')?.getAttribute('type') === 'start' || undefined;
|
|
572
671
|
const articulations = [];
|
|
573
|
-
const articEl = notations
|
|
672
|
+
const articEl = notations ? this.querySelector(notations, 'articulations') : null;
|
|
574
673
|
if (articEl) {
|
|
575
|
-
if (
|
|
674
|
+
if (this.querySelector(articEl, 'staccato'))
|
|
576
675
|
articulations.push(Articulation.Staccato);
|
|
577
|
-
if (
|
|
676
|
+
if (this.querySelector(articEl, 'accent'))
|
|
578
677
|
articulations.push(Articulation.Accent);
|
|
579
|
-
if (
|
|
678
|
+
if (this.querySelector(articEl, 'tenuto'))
|
|
580
679
|
articulations.push(Articulation.Tenuto);
|
|
581
|
-
if (
|
|
680
|
+
if (this.querySelector(articEl, 'marcato'))
|
|
582
681
|
articulations.push(Articulation.Marcato);
|
|
583
|
-
if (
|
|
682
|
+
if (this.querySelector(articEl, 'staccatissimo'))
|
|
584
683
|
articulations.push(Articulation.Staccatissimo);
|
|
585
|
-
if (
|
|
684
|
+
if (this.querySelector(articEl, 'caesura'))
|
|
586
685
|
articulations.push(Articulation.Caesura);
|
|
587
|
-
if (
|
|
686
|
+
if (this.querySelector(articEl, 'breath-mark'))
|
|
588
687
|
articulations.push(Articulation.BreathMark);
|
|
589
688
|
}
|
|
590
|
-
if (notations
|
|
689
|
+
if (notations && this.querySelector(notations, 'fermata'))
|
|
591
690
|
articulations.push(Articulation.Fermata);
|
|
592
691
|
let arpeggio = undefined;
|
|
593
|
-
const arpeggiateEl = notations
|
|
692
|
+
const arpeggiateEl = notations ? this.querySelector(notations, 'arpeggiate') : null;
|
|
594
693
|
if (arpeggiateEl) {
|
|
595
694
|
const dir = arpeggiateEl.getAttribute('direction');
|
|
596
695
|
if (dir === 'up')
|
|
@@ -600,23 +699,23 @@ export class MusicXMLParser {
|
|
|
600
699
|
else
|
|
601
700
|
arpeggio = Arpeggio.Normal;
|
|
602
701
|
}
|
|
603
|
-
const ornaments = notations
|
|
702
|
+
const ornaments = notations ? this.querySelector(notations, 'ornaments') : null;
|
|
604
703
|
let ornament = undefined;
|
|
605
704
|
if (ornaments) {
|
|
606
|
-
if (
|
|
705
|
+
if (this.querySelector(ornaments, 'trill-mark'))
|
|
607
706
|
ornament = Ornament.Trill;
|
|
608
|
-
else if (
|
|
707
|
+
else if (this.querySelector(ornaments, 'mordent'))
|
|
609
708
|
ornament = Ornament.Mordent;
|
|
610
|
-
else if (
|
|
709
|
+
else if (this.querySelector(ornaments, 'inverted-mordent'))
|
|
611
710
|
ornament = Ornament.InvertedMordent;
|
|
612
|
-
else if (
|
|
711
|
+
else if (this.querySelector(ornaments, 'turn'))
|
|
613
712
|
ornament = Ornament.Turn;
|
|
614
|
-
else if (
|
|
713
|
+
else if (this.querySelector(ornaments, 'inverted-turn'))
|
|
615
714
|
ornament = Ornament.InvertedTurn;
|
|
616
|
-
else if (
|
|
715
|
+
else if (this.querySelector(ornaments, 'tremolo'))
|
|
617
716
|
ornament = Ornament.Tremolo;
|
|
618
717
|
}
|
|
619
|
-
const technical = notations
|
|
718
|
+
const technical = notations ? this.querySelector(notations, 'technical') : null;
|
|
620
719
|
let bowing = undefined;
|
|
621
720
|
let fingering = undefined;
|
|
622
721
|
let fret = undefined;
|
|
@@ -626,33 +725,33 @@ export class MusicXMLParser {
|
|
|
626
725
|
let hammerOn = undefined;
|
|
627
726
|
let pullOff = undefined;
|
|
628
727
|
if (technical) {
|
|
629
|
-
if (
|
|
728
|
+
if (this.querySelector(technical, 'up-bow'))
|
|
630
729
|
bowing = Bowing.UpBow;
|
|
631
|
-
else if (
|
|
730
|
+
else if (this.querySelector(technical, 'down-bow'))
|
|
632
731
|
bowing = Bowing.DownBow;
|
|
633
|
-
const fingeringEl =
|
|
732
|
+
const fingeringEl = this.querySelector(technical, 'fingering');
|
|
634
733
|
if (fingeringEl)
|
|
635
734
|
fingering = parseInt(fingeringEl.textContent || '0');
|
|
636
|
-
const fretEl =
|
|
735
|
+
const fretEl = this.querySelector(technical, 'fret');
|
|
637
736
|
if (fretEl)
|
|
638
737
|
fret = parseInt(fretEl.textContent || '0');
|
|
639
|
-
const stringEl =
|
|
738
|
+
const stringEl = this.querySelector(technical, 'string');
|
|
640
739
|
if (stringEl) {
|
|
641
740
|
stringNumber = parseInt(stringEl.textContent || '0');
|
|
642
741
|
isStringCircled = true;
|
|
643
742
|
}
|
|
644
|
-
const palmMuteEl =
|
|
743
|
+
const palmMuteEl = this.querySelector(technical, 'palm-mute');
|
|
645
744
|
if (palmMuteEl)
|
|
646
745
|
palmMute = palmMuteEl.getAttribute('type') || 'start';
|
|
647
|
-
const hammerOnEl =
|
|
746
|
+
const hammerOnEl = this.querySelector(technical, 'hammer-on');
|
|
648
747
|
if (hammerOnEl)
|
|
649
748
|
hammerOn = hammerOnEl.getAttribute('type') || 'start';
|
|
650
|
-
const pullOffEl =
|
|
749
|
+
const pullOffEl = this.querySelector(technical, 'pull-off');
|
|
651
750
|
if (pullOffEl)
|
|
652
751
|
pullOff = pullOffEl.getAttribute('type') || 'start';
|
|
653
752
|
}
|
|
654
753
|
let glissando = undefined;
|
|
655
|
-
const glissEl = notations
|
|
754
|
+
const glissEl = notations ? (this.querySelector(notations, 'glissando') || this.querySelector(notations, 'slide')) : null;
|
|
656
755
|
if (glissEl) {
|
|
657
756
|
const type = glissEl.getAttribute('type');
|
|
658
757
|
if (type === 'start' || type === 'stop') {
|
|
@@ -662,13 +761,14 @@ export class MusicXMLParser {
|
|
|
662
761
|
};
|
|
663
762
|
}
|
|
664
763
|
}
|
|
665
|
-
const notesDyn = notations
|
|
666
|
-
const
|
|
764
|
+
const notesDyn = notations ? this.querySelector(notations, 'dynamics') : null;
|
|
765
|
+
const firstChild = notesDyn ? this.getFirstElementChild(notesDyn) : null;
|
|
766
|
+
const dynamic = firstChild?.nodeName.toLowerCase();
|
|
667
767
|
const lyrics = [];
|
|
668
|
-
|
|
768
|
+
this.querySelectorAll(noteElement, 'lyric').forEach((lyricEl) => {
|
|
669
769
|
const text = this.getText(lyricEl, 'text') || '';
|
|
670
770
|
const syllabic = this.getText(lyricEl, 'syllabic');
|
|
671
|
-
const extend =
|
|
771
|
+
const extend = this.querySelector(lyricEl, 'extend') !== null;
|
|
672
772
|
const number = parseInt(lyricEl.getAttribute('number') || '1');
|
|
673
773
|
if (text || extend) {
|
|
674
774
|
lyrics[number - 1] = {
|
|
@@ -706,7 +806,7 @@ export class MusicXMLParser {
|
|
|
706
806
|
};
|
|
707
807
|
}
|
|
708
808
|
parseHarmony(harmonyElement) {
|
|
709
|
-
const root =
|
|
809
|
+
const root = this.querySelector(harmonyElement, 'root');
|
|
710
810
|
if (!root)
|
|
711
811
|
return null;
|
|
712
812
|
const step = this.getText(root, 'root-step');
|
|
@@ -767,11 +867,11 @@ export class MusicXMLParser {
|
|
|
767
867
|
}
|
|
768
868
|
// Parse Frame (Fretboard Diagram)
|
|
769
869
|
let diagram = undefined;
|
|
770
|
-
const frame =
|
|
870
|
+
const frame = this.querySelector(harmonyElement, 'frame');
|
|
771
871
|
if (frame) {
|
|
772
872
|
diagram = this.parseFrame(frame);
|
|
773
873
|
}
|
|
774
|
-
const bass =
|
|
874
|
+
const bass = this.querySelector(harmonyElement, 'bass');
|
|
775
875
|
let bassPart = '';
|
|
776
876
|
if (bass) {
|
|
777
877
|
const bStep = this.getText(bass, 'bass-step');
|
|
@@ -796,11 +896,11 @@ export class MusicXMLParser {
|
|
|
796
896
|
const openStrings = [];
|
|
797
897
|
const mutedStrings = [];
|
|
798
898
|
const openBarres = new Map(); // fret -> startString
|
|
799
|
-
|
|
899
|
+
this.querySelectorAll(frame, 'frame-note').forEach((fn) => {
|
|
800
900
|
const string = this.getNumber(fn, 'string');
|
|
801
901
|
const fret = this.getNumber(fn, 'fret');
|
|
802
902
|
const fingering = this.getText(fn, 'fingering');
|
|
803
|
-
const barre =
|
|
903
|
+
const barre = this.querySelector(fn, 'barre')?.getAttribute('type'); // start/stop
|
|
804
904
|
if (string !== null && fret !== null) {
|
|
805
905
|
if (fret === 0) {
|
|
806
906
|
openStrings.push(string);
|
|
@@ -842,7 +942,7 @@ export class MusicXMLParser {
|
|
|
842
942
|
const repeats = [];
|
|
843
943
|
let volta = undefined;
|
|
844
944
|
let barlineStyle = BarlineStyle.Regular;
|
|
845
|
-
|
|
945
|
+
this.querySelectorAll(measureElement, 'barline').forEach((barline) => {
|
|
846
946
|
const location = barline.getAttribute('location') || 'right';
|
|
847
947
|
const barStyle = this.getText(barline, 'bar-style');
|
|
848
948
|
if (location === 'right') {
|
|
@@ -853,7 +953,7 @@ export class MusicXMLParser {
|
|
|
853
953
|
else if (barStyle === 'dotted')
|
|
854
954
|
barlineStyle = BarlineStyle.Dotted;
|
|
855
955
|
}
|
|
856
|
-
const repeatEl =
|
|
956
|
+
const repeatEl = this.querySelector(barline, 'repeat');
|
|
857
957
|
if (repeatEl) {
|
|
858
958
|
const direction = repeatEl.getAttribute('direction');
|
|
859
959
|
const times = repeatEl.getAttribute('times');
|
|
@@ -862,7 +962,7 @@ export class MusicXMLParser {
|
|
|
862
962
|
times: times ? parseInt(times) : undefined,
|
|
863
963
|
});
|
|
864
964
|
}
|
|
865
|
-
const ending =
|
|
965
|
+
const ending = this.querySelector(barline, 'ending');
|
|
866
966
|
if (ending) {
|
|
867
967
|
const type = ending.getAttribute('type');
|
|
868
968
|
const numberAttr = ending.getAttribute('number') || '1';
|
|
@@ -983,7 +1083,7 @@ export class MusicXMLParser {
|
|
|
983
1083
|
return map[step.toUpperCase()] ?? 0;
|
|
984
1084
|
}
|
|
985
1085
|
getText(element, tagName) {
|
|
986
|
-
const child =
|
|
1086
|
+
const child = this.querySelector(element, tagName);
|
|
987
1087
|
return child?.textContent ?? null;
|
|
988
1088
|
}
|
|
989
1089
|
getNumber(element, tagName) {
|
package/dist/models/Measure.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export declare class Measure {
|
|
|
29
29
|
replaceNoteSet(index: number, newNoteSet: NoteSet, voiceIndex?: number): Measure;
|
|
30
30
|
replaceNote(noteIndex: number, newNote: Note, voiceIndex?: number): Measure;
|
|
31
31
|
deleteNote(noteIndex: number, voiceIndex?: number): Measure;
|
|
32
|
-
autoBeam(timeSign
|
|
32
|
+
autoBeam(timeSign?: TimeSignature): Measure;
|
|
33
33
|
withTimeSignature(timeSignature?: TimeSignature): Measure;
|
|
34
34
|
withKeySignature(keySignature?: KeySignature): Measure;
|
|
35
35
|
withClef(clef?: Clef): Measure;
|
|
@@ -46,6 +46,8 @@ export declare class Measure {
|
|
|
46
46
|
withSystemText(text?: string): Measure;
|
|
47
47
|
withBarlineStyle(style: BarlineStyle): Measure;
|
|
48
48
|
fillVoiceWithRests(voiceIndex: number, targetDuration: number): Measure;
|
|
49
|
+
simplifyRests(voiceIndex?: number): Measure;
|
|
50
|
+
private decomposeToRests;
|
|
49
51
|
toJSON(): MeasureJSON;
|
|
50
52
|
}
|
|
51
53
|
export interface MeasureJSON {
|
package/dist/models/Measure.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Note } from './Note';
|
|
2
2
|
import { NoteSet } from './NoteSet';
|
|
3
|
-
import { BarlineStyle, DURATION_VALUES, decomposeDuration, } from './types';
|
|
3
|
+
import { BarlineStyle, DURATION_VALUES, Duration, decomposeDuration, } from './types';
|
|
4
4
|
/**
|
|
5
5
|
* Represents a single measure containing note sets, potentially in multiple voices.
|
|
6
6
|
*/
|
|
@@ -94,7 +94,11 @@ export class Measure {
|
|
|
94
94
|
}
|
|
95
95
|
const newVoices = [...this.voices];
|
|
96
96
|
newVoices[voiceIndex] = updatedVoice;
|
|
97
|
-
|
|
97
|
+
let result = this.withVoices(newVoices).simplifyRests(voiceIndex);
|
|
98
|
+
if (this.timeSignature) {
|
|
99
|
+
result = result.autoBeam(this.timeSignature);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
98
102
|
}
|
|
99
103
|
getTotalDuration(voiceIndex = 0) {
|
|
100
104
|
const voice = this.voices[voiceIndex];
|
|
@@ -141,10 +145,56 @@ export class Measure {
|
|
|
141
145
|
const voice = [...this.voices[voiceIndex]];
|
|
142
146
|
if (index >= voice.length)
|
|
143
147
|
return this;
|
|
144
|
-
voice[index]
|
|
148
|
+
const oldVal = voice[index].getDurationValue();
|
|
149
|
+
const newVal = newNoteSet.getDurationValue();
|
|
150
|
+
// Use a copy for modifications
|
|
151
|
+
const updatedVoice = [...voice];
|
|
152
|
+
if (Math.abs(oldVal - newVal) < 0.001) {
|
|
153
|
+
updatedVoice[index] = newNoteSet;
|
|
154
|
+
}
|
|
155
|
+
else if (newVal < oldVal) {
|
|
156
|
+
const gap = oldVal - newVal;
|
|
157
|
+
updatedVoice[index] = newNoteSet;
|
|
158
|
+
const rests = this.decomposeToRests(gap);
|
|
159
|
+
updatedVoice.splice(index + 1, 0, ...rests);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const delta = newVal - oldVal;
|
|
163
|
+
let available = 0;
|
|
164
|
+
for (let i = index + 1; i < updatedVoice.length; i++) {
|
|
165
|
+
available += updatedVoice[i].getDurationValue();
|
|
166
|
+
}
|
|
167
|
+
// If there's enough space to consume, do it.
|
|
168
|
+
// If not, we still replace it but it will overflow (which simplifyRests might fix or layout will handle)
|
|
169
|
+
updatedVoice[index] = newNoteSet;
|
|
170
|
+
let consumed = 0;
|
|
171
|
+
let removeCount = 0;
|
|
172
|
+
const replacements = [];
|
|
173
|
+
for (let i = index + 1; i < voice.length; i++) {
|
|
174
|
+
const ns = voice[i];
|
|
175
|
+
const val = ns.getDurationValue();
|
|
176
|
+
if (consumed + val <= delta + 0.001) {
|
|
177
|
+
consumed += val;
|
|
178
|
+
removeCount++;
|
|
179
|
+
if (consumed >= delta - 0.001)
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const remaining = consumed + val - delta;
|
|
184
|
+
replacements.push(...this.decomposeToRests(remaining));
|
|
185
|
+
removeCount++;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
updatedVoice.splice(index + 1, removeCount, ...replacements);
|
|
190
|
+
}
|
|
145
191
|
const newVoices = [...this.voices];
|
|
146
|
-
newVoices[voiceIndex] =
|
|
147
|
-
|
|
192
|
+
newVoices[voiceIndex] = updatedVoice;
|
|
193
|
+
let result = this.withVoices(newVoices).simplifyRests(voiceIndex);
|
|
194
|
+
if (this.timeSignature) {
|
|
195
|
+
result = result.autoBeam(this.timeSignature);
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
148
198
|
}
|
|
149
199
|
replaceNote(noteIndex, newNote, voiceIndex = 0) {
|
|
150
200
|
return this.replaceNoteSet(noteIndex, new NoteSet([newNote]), voiceIndex);
|
|
@@ -155,13 +205,20 @@ export class Measure {
|
|
|
155
205
|
const voice = [...this.voices[voiceIndex]];
|
|
156
206
|
if (noteIndex < 0 || noteIndex >= voice.length)
|
|
157
207
|
return this;
|
|
158
|
-
|
|
208
|
+
// Instead of splicing, replace with a rest of the same duration
|
|
209
|
+
const target = voice[noteIndex];
|
|
210
|
+
voice[noteIndex] = target.withRest(true);
|
|
159
211
|
const newVoices = [...this.voices];
|
|
160
212
|
newVoices[voiceIndex] = voice;
|
|
161
|
-
return this.withVoices(newVoices);
|
|
213
|
+
return this.withVoices(newVoices).simplifyRests(voiceIndex);
|
|
162
214
|
}
|
|
163
215
|
autoBeam(timeSign) {
|
|
216
|
+
const ts = timeSign || this.timeSignature;
|
|
217
|
+
if (!ts)
|
|
218
|
+
return this;
|
|
164
219
|
const newVoices = this.voices.map((voice) => {
|
|
220
|
+
if (voice.length === 0)
|
|
221
|
+
return [];
|
|
165
222
|
let currentOffset = 0;
|
|
166
223
|
let lastGroupId = undefined;
|
|
167
224
|
let lastGroupStartTime = 0;
|
|
@@ -171,28 +228,38 @@ export class Measure {
|
|
|
171
228
|
const noteDuration = ns.getDurationValue();
|
|
172
229
|
let groupToAssign = undefined;
|
|
173
230
|
if (ns.isBeamable()) {
|
|
174
|
-
const isSixteenth = noteDuration < 0.5;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
231
|
+
const isSixteenth = noteDuration < 0.5 - 0.001;
|
|
232
|
+
// Determine the logical "beat length" for beaming purposes
|
|
233
|
+
let beamBeatLength = 4 / ts.beatType; // Default is one beat
|
|
234
|
+
if (ts.beatType === 4) {
|
|
235
|
+
if (ts.beats === 4 || ts.beats === 2) {
|
|
236
|
+
// In 4/4 or 2/4, eighth notes are often beamed in groups of 2 beats (length 2.0)
|
|
237
|
+
// unless there are sixteenth notes which force 1-beat groupings.
|
|
238
|
+
beamBeatLength = (lastGroupHasSixteenths || isSixteenth) ? 1.0 : 2.0;
|
|
239
|
+
}
|
|
181
240
|
}
|
|
182
|
-
else if (
|
|
183
|
-
|
|
241
|
+
else if (ts.beatType === 8) {
|
|
242
|
+
if (ts.beats % 3 === 0) {
|
|
243
|
+
// Compound meter (6/8, 9/8, 12/8): Beam by 3 eighth notes (1.5 length)
|
|
244
|
+
beamBeatLength = 1.5;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
// Simple meter in 8 (e.g. 2/8, 4/8): Beam by quarter note (1.0 length)
|
|
248
|
+
beamBeatLength = 1.0;
|
|
249
|
+
}
|
|
184
250
|
}
|
|
185
|
-
const currentBeatId = Math.floor(currentOffset /
|
|
251
|
+
const currentBeatId = Math.floor(currentOffset / beamBeatLength + 0.001);
|
|
186
252
|
const endOffset = currentOffset + noteDuration;
|
|
187
|
-
const crossesBeat = Math.floor(endOffset /
|
|
253
|
+
const crossesBeat = Math.floor(endOffset / beamBeatLength - 0.001) !== currentBeatId;
|
|
188
254
|
const sameBeatAsLast = lastGroupId !== undefined &&
|
|
189
|
-
Math.floor(lastGroupStartTime /
|
|
255
|
+
Math.floor(lastGroupStartTime / beamBeatLength + 0.001) === currentBeatId;
|
|
190
256
|
if (sameBeatAsLast && !crossesBeat) {
|
|
191
257
|
groupToAssign = lastGroupId;
|
|
192
258
|
if (isSixteenth)
|
|
193
259
|
lastGroupHasSixteenths = true;
|
|
194
260
|
}
|
|
195
261
|
else {
|
|
262
|
+
// Start a new group
|
|
196
263
|
lastGroupId = Math.floor(Math.random() * 1000000) + 1;
|
|
197
264
|
lastGroupStartTime = currentOffset;
|
|
198
265
|
lastGroupHasSixteenths = isSixteenth;
|
|
@@ -200,12 +267,14 @@ export class Measure {
|
|
|
200
267
|
}
|
|
201
268
|
}
|
|
202
269
|
else {
|
|
270
|
+
// Non-beamable note (Quarter, Half, etc.) or Rest
|
|
203
271
|
lastGroupId = undefined;
|
|
204
272
|
lastGroupHasSixteenths = false;
|
|
205
273
|
}
|
|
206
274
|
tempNoteSets.push(ns.withBeamGroup(groupToAssign));
|
|
207
275
|
currentOffset += noteDuration;
|
|
208
276
|
}
|
|
277
|
+
// Cleanup: groups of 1 note are not beamed
|
|
209
278
|
const groupCounts = new Map();
|
|
210
279
|
for (const ns of tempNoteSets) {
|
|
211
280
|
if (ns.beamGroup !== undefined) {
|
|
@@ -294,6 +363,44 @@ export class Measure {
|
|
|
294
363
|
}
|
|
295
364
|
return this.withVoices(newVoices);
|
|
296
365
|
}
|
|
366
|
+
simplifyRests(voiceIndex = 0) {
|
|
367
|
+
const voice = this.voices[voiceIndex];
|
|
368
|
+
if (!voice || voice.length === 0)
|
|
369
|
+
return this;
|
|
370
|
+
const totalDur = this.getTotalDuration(voiceIndex);
|
|
371
|
+
const expectedDur = this.timeSignature ? (this.timeSignature.beats * 4) / this.timeSignature.beatType : 4.0;
|
|
372
|
+
// Special Case: If the whole measure is rests, just return a single whole rest (or appropriate duration)
|
|
373
|
+
const allRests = voice.every(ns => ns.isRest);
|
|
374
|
+
if (allRests && Math.abs(totalDur - expectedDur) < 0.001) {
|
|
375
|
+
const newVoice = [new NoteSet([new Note(Duration.Whole, undefined, true)])];
|
|
376
|
+
const newVoices = [...this.voices];
|
|
377
|
+
newVoices[voiceIndex] = newVoice;
|
|
378
|
+
return this.withVoices(newVoices);
|
|
379
|
+
}
|
|
380
|
+
const newVoice = [];
|
|
381
|
+
let currentRestSum = 0;
|
|
382
|
+
for (const ns of voice) {
|
|
383
|
+
if (ns.isRest) {
|
|
384
|
+
currentRestSum += ns.getDurationValue();
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
if (currentRestSum > 0.001) {
|
|
388
|
+
newVoice.push(...this.decomposeToRests(currentRestSum));
|
|
389
|
+
currentRestSum = 0;
|
|
390
|
+
}
|
|
391
|
+
newVoice.push(ns);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (currentRestSum > 0.001) {
|
|
395
|
+
newVoice.push(...this.decomposeToRests(currentRestSum));
|
|
396
|
+
}
|
|
397
|
+
const newVoices = [...this.voices];
|
|
398
|
+
newVoices[voiceIndex] = newVoice;
|
|
399
|
+
return this.withVoices(newVoices);
|
|
400
|
+
}
|
|
401
|
+
decomposeToRests(value) {
|
|
402
|
+
return decomposeDuration(value).map((d) => new NoteSet([new Note(d.duration, undefined, true, d.isDotted)]));
|
|
403
|
+
}
|
|
297
404
|
toJSON() {
|
|
298
405
|
return {
|
|
299
406
|
voices: this.voices.map((v) => v.map((ns) => ns.toJSON())),
|
package/dist/models/Note.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare class Note {
|
|
|
17
17
|
readonly tuplet?: Tuplet | undefined;
|
|
18
18
|
readonly hairpin?: Hairpin | undefined;
|
|
19
19
|
readonly isGrace: boolean;
|
|
20
|
-
readonly lyric?:
|
|
20
|
+
readonly lyric?: Lyric | undefined;
|
|
21
21
|
readonly chord?: string | undefined;
|
|
22
22
|
readonly glissando?: Glissando | undefined;
|
|
23
23
|
readonly arpeggio?: Arpeggio | undefined;
|
|
@@ -27,7 +27,7 @@ export declare class Note {
|
|
|
27
27
|
readonly fret?: number | undefined;
|
|
28
28
|
readonly string?: number | undefined;
|
|
29
29
|
readonly fretboardDiagram?: FretboardDiagram | undefined;
|
|
30
|
-
readonly lyrics?:
|
|
30
|
+
readonly lyrics?: Lyric[] | undefined;
|
|
31
31
|
readonly staffText?: string | undefined;
|
|
32
32
|
readonly color?: string | undefined;
|
|
33
33
|
readonly notehead?: NoteheadShape | undefined;
|
|
@@ -40,7 +40,7 @@ export declare class Note {
|
|
|
40
40
|
readonly pullOff?: "start" | "stop" | undefined;
|
|
41
41
|
constructor(duration: Duration, pitch?: Pitch | undefined, // undefined for rests
|
|
42
42
|
isRest?: boolean, isDotted?: boolean, accidental?: Accidental | undefined, beamGroup?: number | undefined, // Group ID for beamed notes
|
|
43
|
-
articulations?: Articulation[], dynamic?: Dynamic | undefined, tie?: boolean | undefined, slur?: Slur | undefined, tuplet?: Tuplet | undefined, hairpin?: Hairpin | undefined, isGrace?: boolean, lyric?:
|
|
43
|
+
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);
|
|
44
44
|
/**
|
|
45
45
|
* Get all lyrics as standardized objects
|
|
46
46
|
*/
|
|
@@ -70,8 +70,8 @@ export declare class Note {
|
|
|
70
70
|
withTie(tie?: boolean): Note;
|
|
71
71
|
withSlur(slur?: Slur): Note;
|
|
72
72
|
withTuplet(tuplet?: Tuplet): Note;
|
|
73
|
-
withLyric(lyric?:
|
|
74
|
-
withLyrics(lyrics:
|
|
73
|
+
withLyric(lyric?: Lyric): Note;
|
|
74
|
+
withLyrics(lyrics: Lyric[]): Note;
|
|
75
75
|
withHairpin(hairpin?: Hairpin): Note;
|
|
76
76
|
withAccidental(accidental?: Accidental): Note;
|
|
77
77
|
withDuration(duration: Duration, isDotted?: boolean): Note;
|
|
@@ -117,7 +117,7 @@ export interface NoteJSON {
|
|
|
117
117
|
tuplet?: Tuplet;
|
|
118
118
|
hairpin?: Hairpin;
|
|
119
119
|
isGrace?: boolean;
|
|
120
|
-
lyric?:
|
|
120
|
+
lyric?: Lyric;
|
|
121
121
|
chord?: string;
|
|
122
122
|
glissando?: Glissando;
|
|
123
123
|
arpeggio?: Arpeggio;
|
|
@@ -127,7 +127,7 @@ export interface NoteJSON {
|
|
|
127
127
|
fret?: number;
|
|
128
128
|
string?: number;
|
|
129
129
|
fretboardDiagram?: FretboardDiagram;
|
|
130
|
-
lyrics?:
|
|
130
|
+
lyrics?: Lyric[];
|
|
131
131
|
staffText?: string;
|
|
132
132
|
color?: string;
|
|
133
133
|
notehead?: string;
|
package/dist/models/Note.js
CHANGED
|
@@ -81,10 +81,10 @@ export class Note {
|
|
|
81
81
|
*/
|
|
82
82
|
get allLyrics() {
|
|
83
83
|
if (this.lyrics) {
|
|
84
|
-
return this.lyrics
|
|
84
|
+
return this.lyrics;
|
|
85
85
|
}
|
|
86
86
|
if (this.lyric) {
|
|
87
|
-
return [
|
|
87
|
+
return [this.lyric];
|
|
88
88
|
}
|
|
89
89
|
return [];
|
|
90
90
|
}
|
package/dist/models/NoteSet.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare class NoteSet {
|
|
|
14
14
|
withDuration(duration: Duration, isDotted?: boolean): NoteSet;
|
|
15
15
|
withBeamGroup(group?: number): NoteSet;
|
|
16
16
|
withNotes(notes: Note[]): NoteSet;
|
|
17
|
-
withLyrics(lyrics:
|
|
17
|
+
withLyrics(lyrics: Lyric[]): NoteSet;
|
|
18
18
|
withTuplet(tuplet?: Tuplet): NoteSet;
|
|
19
19
|
withPitch(pitch: Pitch): NoteSet;
|
|
20
20
|
withAccidental(accidental?: Accidental): NoteSet;
|
|
@@ -45,14 +45,14 @@ export declare class NoteSet {
|
|
|
45
45
|
withPalmMute(type?: 'start' | 'stop'): NoteSet;
|
|
46
46
|
withStringCircled(isStringCircled?: boolean): NoteSet;
|
|
47
47
|
withStemDirection(dir?: StemDirection): NoteSet;
|
|
48
|
-
withLyric(lyric?:
|
|
48
|
+
withLyric(lyric?: Lyric): NoteSet;
|
|
49
49
|
withStaffText(text?: string): NoteSet;
|
|
50
50
|
withFretboardDiagram(diagram?: FretboardDiagram): NoteSet;
|
|
51
51
|
get pitch(): Pitch | undefined;
|
|
52
52
|
get accidental(): Accidental | undefined;
|
|
53
53
|
get tie(): boolean | undefined;
|
|
54
54
|
get slur(): Slur | undefined;
|
|
55
|
-
get articulations(): Articulation[];
|
|
55
|
+
get articulations(): Articulation[] | undefined;
|
|
56
56
|
get articulation(): Articulation | undefined;
|
|
57
57
|
get ornament(): Ornament | undefined;
|
|
58
58
|
get dynamic(): Dynamic | undefined;
|
|
@@ -68,14 +68,14 @@ export declare class NoteSet {
|
|
|
68
68
|
get bowing(): Bowing | undefined;
|
|
69
69
|
get fingering(): number | undefined;
|
|
70
70
|
get allLyrics(): Lyric[];
|
|
71
|
-
get lyrics():
|
|
72
|
-
get lyric():
|
|
71
|
+
get lyrics(): Lyric[] | undefined;
|
|
72
|
+
get lyric(): Lyric | undefined;
|
|
73
73
|
get chord(): string | undefined;
|
|
74
74
|
get fretboardDiagram(): FretboardDiagram | undefined;
|
|
75
75
|
get staffText(): string | undefined;
|
|
76
|
-
get hammerOn():
|
|
77
|
-
get pullOff():
|
|
78
|
-
get palmMute():
|
|
76
|
+
get hammerOn(): 'start' | 'stop' | undefined;
|
|
77
|
+
get pullOff(): 'start' | 'stop' | undefined;
|
|
78
|
+
get palmMute(): 'start' | 'stop' | undefined;
|
|
79
79
|
get isStringCircled(): boolean | undefined;
|
|
80
80
|
get stemDirection(): StemDirection | undefined;
|
|
81
81
|
toJSON(): NoteSetJSON;
|
package/dist/models/Score.d.ts
CHANGED
|
@@ -19,13 +19,13 @@ export declare class Score {
|
|
|
19
19
|
readonly lyricist: string;
|
|
20
20
|
readonly swing: boolean;
|
|
21
21
|
readonly subtitle: string;
|
|
22
|
-
readonly genre: Genre
|
|
22
|
+
readonly genre: Genre;
|
|
23
23
|
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
|
|
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);
|
|
25
25
|
withTitle(title: string): Score;
|
|
26
26
|
withComposer(composer: string): Score;
|
|
27
27
|
withSubtitle(subtitle: string): Score;
|
|
28
|
-
withGenre(genre: Genre
|
|
28
|
+
withGenre(genre: Genre): Score;
|
|
29
29
|
getMeasureCount(): number;
|
|
30
30
|
getTimeSignatureAt(measureIndex: number): TimeSignature;
|
|
31
31
|
getKeySignatureAt(measureIndex: number): KeySignature;
|
|
@@ -85,6 +85,6 @@ export interface ScoreJSON {
|
|
|
85
85
|
lyricist?: string;
|
|
86
86
|
swing?: boolean;
|
|
87
87
|
subtitle?: string;
|
|
88
|
-
genre?: Genre
|
|
88
|
+
genre?: Genre;
|
|
89
89
|
tempoText?: string;
|
|
90
90
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WARNING: DO NOT ADD SENSITIVE LOGIC OR PRIVATE CONSTANTS TO THIS FILE.
|
|
3
|
+
* This package (@scorelabs/core) is used in public-facing frontend applications
|
|
4
|
+
* and could be exposed. Keep all token generation, verification, and encryption
|
|
5
|
+
* logic in the 'backend' or 'website' utilities.
|
|
6
|
+
*/
|
|
7
|
+
export type UserTier = 'free' | 'premium';
|
|
8
|
+
export interface UserData {
|
|
9
|
+
accessToken?: string;
|
|
10
|
+
access_token?: string;
|
|
11
|
+
uuid?: string | number;
|
|
12
|
+
name?: string;
|
|
13
|
+
first_name?: string;
|
|
14
|
+
last_name?: string;
|
|
15
|
+
username?: string;
|
|
16
|
+
email?: string;
|
|
17
|
+
is_admin?: number | boolean;
|
|
18
|
+
id?: string | number;
|
|
19
|
+
tier?: UserTier | string;
|
|
20
|
+
picture?: string;
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WARNING: DO NOT ADD SENSITIVE LOGIC OR PRIVATE CONSTANTS TO THIS FILE.
|
|
3
|
+
* This package (@scorelabs/core) is used in public-facing frontend applications
|
|
4
|
+
* and could be exposed. Keep all token generation, verification, and encryption
|
|
5
|
+
* logic in the 'backend' or 'website' utilities.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scorelabs/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Core logic and models for ScoreLabs music notation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"release:major": "npm version major && npm publish --access public",
|
|
25
25
|
"release": "npm run release:patch",
|
|
26
26
|
"test": "vitest",
|
|
27
|
+
"test:coverage": "vitest run --coverage",
|
|
27
28
|
"lint": "eslint ."
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/jszip": "^3.4.0",
|
|
34
35
|
"@types/node": "^25.2.3",
|
|
36
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
35
37
|
"typescript": "^5.9.3",
|
|
36
38
|
"vitest": "^4.0.18"
|
|
37
39
|
}
|