@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,6 +1,7 @@
|
|
|
1
1
|
import JSZip from 'jszip';
|
|
2
|
-
import { getInstrumentByProgram, guessInstrumentByName } from '../models/Instrument';
|
|
3
|
-
import { Accidental, Arpeggio, Articulation, BarlineStyle, Bowing, Clef,
|
|
2
|
+
import { getInstrumentByProgram, guessInstrumentByName } from '../models/Instrument.js';
|
|
3
|
+
import { Accidental, Arpeggio, Articulation, BarlineStyle, Bowing, Clef, Duration, GlissandoType, HairpinType, NoteheadShape, Ornament, OttavaType, StemDirection, calculateDurationValue, decomposeDuration, } from '../models/types.js';
|
|
4
|
+
// TODO: Write tests for MusicXMLParser. It is important to test it thoroughly.
|
|
4
5
|
/**
|
|
5
6
|
* MusicXML Parser
|
|
6
7
|
*
|
|
@@ -18,6 +19,52 @@ export class MusicXMLParser {
|
|
|
18
19
|
this._domParser =
|
|
19
20
|
domParserInstance || (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
|
|
20
21
|
}
|
|
22
|
+
querySelector(el, selector) {
|
|
23
|
+
if (el.querySelector)
|
|
24
|
+
return el.querySelector(selector);
|
|
25
|
+
const parts = selector.split(' ');
|
|
26
|
+
let current = el;
|
|
27
|
+
for (const part of parts) {
|
|
28
|
+
if (!current)
|
|
29
|
+
return null;
|
|
30
|
+
const results = current.getElementsByTagName(part);
|
|
31
|
+
if (results.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
current = results[0];
|
|
34
|
+
}
|
|
35
|
+
return current;
|
|
36
|
+
}
|
|
37
|
+
getChildren(el) {
|
|
38
|
+
const parent = el;
|
|
39
|
+
if (parent.children)
|
|
40
|
+
return Array.from(parent.children);
|
|
41
|
+
return Array.from(el.childNodes).filter((node) => node.nodeType === 1);
|
|
42
|
+
}
|
|
43
|
+
getFirstElementChild(el) {
|
|
44
|
+
if (el.firstElementChild)
|
|
45
|
+
return el.firstElementChild;
|
|
46
|
+
const children = this.getChildren(el);
|
|
47
|
+
return children.length > 0 ? children[0] : null;
|
|
48
|
+
}
|
|
49
|
+
querySelectorAll(el, selector) {
|
|
50
|
+
if (el.querySelectorAll)
|
|
51
|
+
return Array.from(el.querySelectorAll(selector));
|
|
52
|
+
const parts = selector.split(' ');
|
|
53
|
+
if (parts.length === 1) {
|
|
54
|
+
return Array.from(el.getElementsByTagName(parts[0]));
|
|
55
|
+
}
|
|
56
|
+
// Simple nested selector support (only two levels for now as needed)
|
|
57
|
+
if (parts.length === 2) {
|
|
58
|
+
const firstLevel = Array.from(el.getElementsByTagName(parts[0]));
|
|
59
|
+
const results = [];
|
|
60
|
+
firstLevel.forEach((parent) => {
|
|
61
|
+
const children = Array.from(parent.getElementsByTagName(parts[1]));
|
|
62
|
+
results.push(...children);
|
|
63
|
+
});
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
21
68
|
parseFromString(xmlString) {
|
|
22
69
|
if (!this._domParser) {
|
|
23
70
|
throw new Error('No DOMParser available. Please provide one in the constructor for Node.js environments.');
|
|
@@ -35,7 +82,7 @@ export class MusicXMLParser {
|
|
|
35
82
|
if (!_parser)
|
|
36
83
|
throw new Error('No DOMParser available');
|
|
37
84
|
const containerDoc = _parser.parseFromString(containerXml, 'application/xml');
|
|
38
|
-
const rootfile =
|
|
85
|
+
const rootfile = new MusicXMLParser(_parser).querySelector(containerDoc, 'rootfile');
|
|
39
86
|
const fullPath = rootfile?.getAttribute('full-path');
|
|
40
87
|
if (!fullPath)
|
|
41
88
|
throw new Error('Invalid MXL: Could not find main score file path');
|
|
@@ -63,7 +110,7 @@ export class MusicXMLParser {
|
|
|
63
110
|
}
|
|
64
111
|
xmlString = xmlString.substring(firstBracket).trim();
|
|
65
112
|
const doc = this.parseFromString(xmlString);
|
|
66
|
-
const parseError =
|
|
113
|
+
const parseError = this.querySelector(doc, 'parsererror');
|
|
67
114
|
if (parseError) {
|
|
68
115
|
const preview = xmlString.substring(0, 100).replace(/\n/g, ' ');
|
|
69
116
|
throw new Error(`XML parsing error: ${parseError.textContent}. Content preview: "${preview}..."`);
|
|
@@ -74,12 +121,15 @@ export class MusicXMLParser {
|
|
|
74
121
|
const title = this.parseTitle(doc);
|
|
75
122
|
const subtitle = this.parseSubtitle(doc);
|
|
76
123
|
const composer = this.parseComposer(doc);
|
|
124
|
+
const lyricist = this.parseLyricist(doc);
|
|
125
|
+
const copyright = this.parseCopyright(doc);
|
|
77
126
|
const partInfo = this.parsePartList(doc);
|
|
78
127
|
const parts = this.parseParts(doc, partInfo);
|
|
79
128
|
let globalBpm = 120;
|
|
80
129
|
let globalTempoDuration = Duration.Quarter;
|
|
81
130
|
let globalTempoIsDotted = false;
|
|
82
131
|
let globalTempoText = '';
|
|
132
|
+
let globalTempoDotCount = 0;
|
|
83
133
|
if (parts.length > 0 && parts[0].staves.length > 0 && parts[0].staves[0].measures.length > 0) {
|
|
84
134
|
const firstMeasure = parts[0].staves[0].measures[0];
|
|
85
135
|
if (firstMeasure.tempo) {
|
|
@@ -87,6 +137,7 @@ export class MusicXMLParser {
|
|
|
87
137
|
globalTempoDuration = firstMeasure.tempo.duration;
|
|
88
138
|
globalTempoIsDotted = firstMeasure.tempo.isDotted;
|
|
89
139
|
globalTempoText = firstMeasure.tempo.text || '';
|
|
140
|
+
globalTempoDotCount = firstMeasure.tempo.dotCount || 0;
|
|
90
141
|
}
|
|
91
142
|
}
|
|
92
143
|
const score = {
|
|
@@ -104,6 +155,9 @@ export class MusicXMLParser {
|
|
|
104
155
|
tempoDuration: globalTempoDuration,
|
|
105
156
|
tempoIsDotted: globalTempoIsDotted,
|
|
106
157
|
tempoText: globalTempoText,
|
|
158
|
+
tempoDotCount: globalTempoDotCount,
|
|
159
|
+
copyright,
|
|
160
|
+
lyricist,
|
|
107
161
|
};
|
|
108
162
|
return this.postProcess(score);
|
|
109
163
|
}
|
|
@@ -111,7 +165,7 @@ export class MusicXMLParser {
|
|
|
111
165
|
let subtitle = this.getText(doc.documentElement, 'movement-subtitle');
|
|
112
166
|
if (subtitle)
|
|
113
167
|
return subtitle;
|
|
114
|
-
const work =
|
|
168
|
+
const work = this.querySelector(doc, 'work');
|
|
115
169
|
if (work) {
|
|
116
170
|
subtitle = this.getText(work, 'work-subtitle');
|
|
117
171
|
if (subtitle)
|
|
@@ -120,11 +174,11 @@ export class MusicXMLParser {
|
|
|
120
174
|
if (subtitle)
|
|
121
175
|
return subtitle;
|
|
122
176
|
}
|
|
123
|
-
const credits =
|
|
177
|
+
const credits = this.querySelectorAll(doc, 'credit');
|
|
124
178
|
for (const credit of Array.from(credits)) {
|
|
125
|
-
const type =
|
|
179
|
+
const type = this.querySelector(credit, 'credit-type');
|
|
126
180
|
if (type?.textContent?.toLowerCase() === 'subtitle') {
|
|
127
|
-
const words =
|
|
181
|
+
const words = this.querySelector(credit, 'credit-words');
|
|
128
182
|
if (words?.textContent)
|
|
129
183
|
return words.textContent;
|
|
130
184
|
}
|
|
@@ -137,23 +191,73 @@ export class MusicXMLParser {
|
|
|
137
191
|
title = this.getText(doc.documentElement, 'movement-title');
|
|
138
192
|
return title ?? 'Untitled';
|
|
139
193
|
}
|
|
194
|
+
parseCopyright(doc) {
|
|
195
|
+
const identification = this.querySelector(doc, 'identification');
|
|
196
|
+
if (identification) {
|
|
197
|
+
const rights = this.getText(identification, 'rights');
|
|
198
|
+
if (rights)
|
|
199
|
+
return rights;
|
|
200
|
+
}
|
|
201
|
+
const credits = this.querySelectorAll(doc, 'credit');
|
|
202
|
+
for (const credit of Array.from(credits)) {
|
|
203
|
+
const type = this.querySelector(credit, 'credit-type');
|
|
204
|
+
if (type?.textContent?.toLowerCase() === 'rights' ||
|
|
205
|
+
type?.textContent?.toLowerCase() === 'copyright') {
|
|
206
|
+
const words = this.querySelector(credit, 'credit-words');
|
|
207
|
+
if (words?.textContent)
|
|
208
|
+
return words.textContent;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return '';
|
|
212
|
+
}
|
|
140
213
|
parseComposer(doc) {
|
|
141
|
-
const creators =
|
|
214
|
+
const creators = this.querySelectorAll(doc, 'identification creator');
|
|
142
215
|
for (const creator of Array.from(creators)) {
|
|
143
216
|
if (creator.getAttribute('type') === 'composer')
|
|
144
217
|
return creator.textContent ?? 'Unknown';
|
|
145
218
|
}
|
|
146
|
-
|
|
219
|
+
// If no type="composer", but there is only one creator, assume it's the composer
|
|
220
|
+
if (creators.length === 1)
|
|
221
|
+
return (creators[0].textContent ?? 'Unknown');
|
|
222
|
+
// Check credits for composer type
|
|
223
|
+
const credits = this.querySelectorAll(doc, 'credit');
|
|
224
|
+
for (const credit of Array.from(credits)) {
|
|
225
|
+
const type = this.querySelector(credit, 'credit-type');
|
|
226
|
+
if (type?.textContent?.toLowerCase() === 'composer') {
|
|
227
|
+
const words = this.querySelector(credit, 'credit-words');
|
|
228
|
+
if (words?.textContent)
|
|
229
|
+
return words.textContent;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return creators.length > 0 ? creators[0].textContent ?? 'Unknown' : 'Unknown';
|
|
233
|
+
}
|
|
234
|
+
parseLyricist(doc) {
|
|
235
|
+
const creators = this.querySelectorAll(doc, 'identification creator');
|
|
236
|
+
for (const creator of Array.from(creators)) {
|
|
237
|
+
if (creator.getAttribute('type') === 'lyricist')
|
|
238
|
+
return creator.textContent ?? '';
|
|
239
|
+
}
|
|
240
|
+
// Check credits for lyricist type
|
|
241
|
+
const credits = this.querySelectorAll(doc, 'credit');
|
|
242
|
+
for (const credit of Array.from(credits)) {
|
|
243
|
+
const type = this.querySelector(credit, 'credit-type');
|
|
244
|
+
if (type?.textContent?.toLowerCase() === 'lyricist') {
|
|
245
|
+
const words = this.querySelector(credit, 'credit-words');
|
|
246
|
+
if (words?.textContent)
|
|
247
|
+
return words.textContent;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return '';
|
|
147
251
|
}
|
|
148
252
|
parsePartList(doc) {
|
|
149
253
|
const partInfo = new Map();
|
|
150
|
-
const scoreParts =
|
|
254
|
+
const scoreParts = this.querySelectorAll(doc, 'part-list score-part');
|
|
151
255
|
for (const scorePart of Array.from(scoreParts)) {
|
|
152
256
|
const id = scorePart.getAttribute('id');
|
|
153
257
|
const name = this.getText(scorePart, 'part-name') || 'Part';
|
|
154
258
|
let instrument;
|
|
155
259
|
// Find first midi-instrument to determine main instrument
|
|
156
|
-
const firstMidiInst =
|
|
260
|
+
const firstMidiInst = this.querySelector(scorePart, 'midi-instrument');
|
|
157
261
|
if (firstMidiInst) {
|
|
158
262
|
const programStr = this.getText(firstMidiInst, 'midi-program');
|
|
159
263
|
if (programStr) {
|
|
@@ -176,7 +280,7 @@ export class MusicXMLParser {
|
|
|
176
280
|
if (id) {
|
|
177
281
|
partInfo.set(id, { name: name.trim(), instrument });
|
|
178
282
|
}
|
|
179
|
-
const midiInstruments =
|
|
283
|
+
const midiInstruments = this.querySelectorAll(scorePart, 'midi-instrument');
|
|
180
284
|
for (const midiInst of Array.from(midiInstruments)) {
|
|
181
285
|
const instId = midiInst.getAttribute('id');
|
|
182
286
|
const unpitched = this.getNumber(midiInst, 'midi-unpitched');
|
|
@@ -188,7 +292,7 @@ export class MusicXMLParser {
|
|
|
188
292
|
}
|
|
189
293
|
parseParts(doc, partInfo) {
|
|
190
294
|
const parts = [];
|
|
191
|
-
const partElements =
|
|
295
|
+
const partElements = this.querySelectorAll(doc, 'part');
|
|
192
296
|
for (const partElement of Array.from(partElements)) {
|
|
193
297
|
const id = partElement.getAttribute('id');
|
|
194
298
|
const info = partInfo.get(id ?? '');
|
|
@@ -199,11 +303,11 @@ export class MusicXMLParser {
|
|
|
199
303
|
return parts;
|
|
200
304
|
}
|
|
201
305
|
parsePart(partElement) {
|
|
202
|
-
const measureElements =
|
|
306
|
+
const measureElements = this.querySelectorAll(partElement, 'measure');
|
|
203
307
|
let numStaves = 1;
|
|
204
|
-
const firstMeasure =
|
|
308
|
+
const firstMeasure = this.querySelector(partElement, 'measure');
|
|
205
309
|
if (firstMeasure) {
|
|
206
|
-
const stavesNode =
|
|
310
|
+
const stavesNode = this.querySelector(firstMeasure, 'attributes staves');
|
|
207
311
|
if (stavesNode)
|
|
208
312
|
numStaves = parseInt(stavesNode.textContent || '1');
|
|
209
313
|
}
|
|
@@ -212,12 +316,17 @@ export class MusicXMLParser {
|
|
|
212
316
|
measures: [],
|
|
213
317
|
}));
|
|
214
318
|
const staffClefs = Array(numStaves).fill(Clef.Treble);
|
|
215
|
-
|
|
319
|
+
const lastVoicePerStaff = new Map();
|
|
320
|
+
let divisions = 1;
|
|
321
|
+
for (const [mIdx, measureElement] of Array.from(measureElements).entries()) {
|
|
216
322
|
let measureTimeSignature;
|
|
217
323
|
let measureKeySignature;
|
|
218
|
-
|
|
219
|
-
const attributes =
|
|
324
|
+
let isPickup = measureElement.getAttribute('implicit') === 'yes';
|
|
325
|
+
const attributes = this.querySelector(measureElement, 'attributes');
|
|
220
326
|
if (attributes) {
|
|
327
|
+
const divNode = this.querySelector(attributes, 'divisions');
|
|
328
|
+
if (divNode)
|
|
329
|
+
divisions = parseInt(divNode.textContent || '1');
|
|
221
330
|
const fifths = this.getNumber(attributes, 'key fifths');
|
|
222
331
|
if (fifths !== null) {
|
|
223
332
|
this.currentKeyFifths = fifths;
|
|
@@ -229,7 +338,7 @@ export class MusicXMLParser {
|
|
|
229
338
|
this.currentBeats = beats;
|
|
230
339
|
if (beatType !== null)
|
|
231
340
|
this.currentBeatType = beatType;
|
|
232
|
-
const timeElement =
|
|
341
|
+
const timeElement = this.querySelector(attributes, 'time');
|
|
233
342
|
if (timeElement) {
|
|
234
343
|
const symAttr = timeElement.getAttribute('symbol');
|
|
235
344
|
if (symAttr === 'common')
|
|
@@ -245,9 +354,10 @@ export class MusicXMLParser {
|
|
|
245
354
|
symbol: this.currentSymbol !== 'normal' ? this.currentSymbol : undefined,
|
|
246
355
|
};
|
|
247
356
|
}
|
|
248
|
-
const clefElements =
|
|
357
|
+
const clefElements = this.querySelectorAll(attributes, 'clef');
|
|
249
358
|
clefElements.forEach((clef) => {
|
|
250
|
-
const
|
|
359
|
+
const numberAttrs = clef.getAttribute('number');
|
|
360
|
+
const number = numberAttrs ? parseInt(numberAttrs) : 1;
|
|
251
361
|
const sign = this.getText(clef, 'sign');
|
|
252
362
|
if (sign && number <= numStaves) {
|
|
253
363
|
const line = this.getNumber(clef, 'line') || 0;
|
|
@@ -255,7 +365,7 @@ export class MusicXMLParser {
|
|
|
255
365
|
staffClefs[number - 1] = this.mapClef(sign, line, octaveChange);
|
|
256
366
|
}
|
|
257
367
|
});
|
|
258
|
-
const staffDetails =
|
|
368
|
+
const staffDetails = this.querySelectorAll(attributes, 'staff-details');
|
|
259
369
|
staffDetails.forEach((sd) => {
|
|
260
370
|
const number = parseInt(sd.getAttribute('number') || '1');
|
|
261
371
|
const lines = this.getNumber(sd, 'staff-lines');
|
|
@@ -272,9 +382,9 @@ export class MusicXMLParser {
|
|
|
272
382
|
const staffVoices = new Map();
|
|
273
383
|
for (let i = 0; i < numStaves; i++)
|
|
274
384
|
staffVoices.set(i, new Map());
|
|
275
|
-
const children =
|
|
385
|
+
const children = this.getChildren(measureElement);
|
|
276
386
|
let pendingChord = undefined;
|
|
277
|
-
|
|
387
|
+
const currentNoteSetMap = new Map();
|
|
278
388
|
const beamGroupIdMap = new Map();
|
|
279
389
|
let globalBeamId = 1;
|
|
280
390
|
let currentHairpin = undefined;
|
|
@@ -287,24 +397,25 @@ export class MusicXMLParser {
|
|
|
287
397
|
pendingChord = harmonyData;
|
|
288
398
|
}
|
|
289
399
|
else if (child.nodeName === 'direction') {
|
|
290
|
-
const metronome =
|
|
400
|
+
const metronome = this.querySelector(child, 'metronome');
|
|
291
401
|
if (metronome) {
|
|
292
402
|
const beatUnit = this.getText(metronome, 'beat-unit') || 'quarter';
|
|
293
403
|
const bpm = parseInt(this.getText(metronome, 'per-minute') || '120');
|
|
294
|
-
const
|
|
295
|
-
|
|
404
|
+
const dotCount = this.querySelectorAll(metronome, 'beat-unit-dot').length;
|
|
405
|
+
const isDotted = dotCount > 0;
|
|
406
|
+
measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted, dotCount };
|
|
296
407
|
}
|
|
297
|
-
const rehearsal =
|
|
408
|
+
const rehearsal = this.querySelector(child, 'rehearsal');
|
|
298
409
|
if (rehearsal)
|
|
299
410
|
rehearsalMark = rehearsal.textContent || undefined;
|
|
300
|
-
const words =
|
|
411
|
+
const words = this.querySelector(child, 'words');
|
|
301
412
|
if (words) {
|
|
302
413
|
if (measureTempo)
|
|
303
414
|
measureTempo.text = words.textContent || undefined;
|
|
304
415
|
else
|
|
305
416
|
systemText = words.textContent || undefined;
|
|
306
417
|
}
|
|
307
|
-
const wedge =
|
|
418
|
+
const wedge = this.querySelector(child, 'wedge');
|
|
308
419
|
if (wedge) {
|
|
309
420
|
const typeAttr = wedge.getAttribute('type');
|
|
310
421
|
if (typeAttr === 'crescendo' || typeAttr === 'diminuendo' || typeAttr === 'stop') {
|
|
@@ -314,7 +425,7 @@ export class MusicXMLParser {
|
|
|
314
425
|
};
|
|
315
426
|
}
|
|
316
427
|
}
|
|
317
|
-
const octShift =
|
|
428
|
+
const octShift = this.querySelector(child, 'octave-shift');
|
|
318
429
|
if (octShift) {
|
|
319
430
|
const typeAttr = octShift.getAttribute('type');
|
|
320
431
|
const size = parseInt(octShift.getAttribute('size') || '8');
|
|
@@ -331,25 +442,27 @@ export class MusicXMLParser {
|
|
|
331
442
|
};
|
|
332
443
|
}
|
|
333
444
|
}
|
|
334
|
-
const pedalNode =
|
|
445
|
+
const pedalNode = this.querySelector(child, 'pedal');
|
|
335
446
|
if (pedalNode) {
|
|
336
447
|
const typeAttr = pedalNode.getAttribute('type');
|
|
337
448
|
if (typeAttr === 'start' || typeAttr === 'stop') {
|
|
338
449
|
currentPedal = { type: 'sustain', placement: typeAttr };
|
|
339
450
|
}
|
|
340
451
|
}
|
|
341
|
-
const dynamics =
|
|
342
|
-
if (dynamics
|
|
343
|
-
|
|
452
|
+
const dynamics = this.querySelector(child, 'dynamics');
|
|
453
|
+
if (dynamics) {
|
|
454
|
+
const firstChild = this.getFirstElementChild(dynamics);
|
|
455
|
+
if (firstChild)
|
|
456
|
+
contextDynamic = firstChild.nodeName.toLowerCase();
|
|
344
457
|
}
|
|
345
458
|
}
|
|
346
459
|
else if (child.nodeName === 'note') {
|
|
347
460
|
const staffNum = parseInt(this.getText(child, 'staff') || '1');
|
|
348
461
|
const staffIdx = Math.min(Math.max(staffNum - 1, 0), numStaves - 1);
|
|
349
|
-
const isChord =
|
|
462
|
+
const isChord = this.querySelector(child, 'chord') !== null;
|
|
350
463
|
const note = this.parseNote(child);
|
|
351
464
|
if (note) {
|
|
352
|
-
const beamEl =
|
|
465
|
+
const beamEl = this.querySelector(child, 'beam[number="1"]');
|
|
353
466
|
if (beamEl) {
|
|
354
467
|
const beamType = beamEl.textContent;
|
|
355
468
|
if (beamType === 'begin') {
|
|
@@ -389,6 +502,7 @@ export class MusicXMLParser {
|
|
|
389
502
|
currentPedal = undefined;
|
|
390
503
|
}
|
|
391
504
|
const voiceId = this.getText(child, 'voice') || '1';
|
|
505
|
+
lastVoicePerStaff.set(staffIdx, voiceId);
|
|
392
506
|
const voiceMap = staffVoices.get(staffIdx);
|
|
393
507
|
if (!voiceMap.has(voiceId))
|
|
394
508
|
voiceMap.set(voiceId, []);
|
|
@@ -411,7 +525,7 @@ export class MusicXMLParser {
|
|
|
411
525
|
else if (child.nodeName === 'backup' || child.nodeName === 'forward') {
|
|
412
526
|
for (const [sIdx, notes] of currentNoteSetMap.entries()) {
|
|
413
527
|
if (notes.length > 0) {
|
|
414
|
-
const voiceId =
|
|
528
|
+
const voiceId = lastVoicePerStaff.get(sIdx) || '1';
|
|
415
529
|
const voiceMap = staffVoices.get(sIdx);
|
|
416
530
|
if (!voiceMap.has(voiceId))
|
|
417
531
|
voiceMap.set(voiceId, []);
|
|
@@ -421,22 +535,46 @@ export class MusicXMLParser {
|
|
|
421
535
|
currentNoteSetMap.clear();
|
|
422
536
|
}
|
|
423
537
|
});
|
|
538
|
+
// Cleanup: push remaining notes
|
|
424
539
|
for (const [sIdx, notes] of currentNoteSetMap.entries()) {
|
|
425
540
|
if (notes.length > 0) {
|
|
426
|
-
|
|
427
|
-
const voiceId = '1'; // Defaulting to 1 for final push if not tracked
|
|
541
|
+
const voiceId = lastVoicePerStaff.get(sIdx) || '1';
|
|
428
542
|
const voiceMap = staffVoices.get(sIdx);
|
|
429
543
|
if (!voiceMap.has(voiceId))
|
|
430
544
|
voiceMap.set(voiceId, []);
|
|
431
545
|
voiceMap.get(voiceId).push({ notes });
|
|
432
546
|
}
|
|
433
547
|
}
|
|
548
|
+
// Check if it's a pickup measure if mIdx is 0 and implicit is not set
|
|
549
|
+
if (mIdx === 0 && !isPickup) {
|
|
550
|
+
// Calculate the actual duration of the first staff's first voice
|
|
551
|
+
const vMap0 = staffVoices.get(0);
|
|
552
|
+
const v1 = vMap0.get('1') || vMap0.get(Array.from(vMap0.keys())[0]);
|
|
553
|
+
if (v1) {
|
|
554
|
+
// Check duration of first voice in first staff
|
|
555
|
+
const expectedDivisions = this.currentBeats * ((divisions * 4) / this.currentBeatType);
|
|
556
|
+
let totalMeasureDivs = 0;
|
|
557
|
+
const notes = this.querySelectorAll(measureElement, 'note');
|
|
558
|
+
// Find first voice in first staff
|
|
559
|
+
for (const n of notes) {
|
|
560
|
+
if ((this.getText(n, 'staff') || '1') === '1' &&
|
|
561
|
+
(this.getText(n, 'voice') || '1') === (Array.from(vMap0.keys())[0] || '1')) {
|
|
562
|
+
if (this.querySelector(n, 'chord'))
|
|
563
|
+
continue;
|
|
564
|
+
totalMeasureDivs += this.getNumber(n, 'duration') || 0;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (totalMeasureDivs > 0 && totalMeasureDivs < expectedDivisions - 1) {
|
|
568
|
+
isPickup = true;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
434
572
|
for (let i = 0; i < numStaves; i++) {
|
|
435
573
|
const vMap = staffVoices.get(i);
|
|
436
574
|
let voicesIndices = Array.from(vMap.keys()).sort();
|
|
437
575
|
if (voicesIndices.length === 0)
|
|
438
576
|
voicesIndices = ['1'];
|
|
439
|
-
const voices = voicesIndices.map(id => vMap.get(id) || []);
|
|
577
|
+
const voices = voicesIndices.map((id) => vMap.get(id) || []);
|
|
440
578
|
const measure = {
|
|
441
579
|
voices: voices,
|
|
442
580
|
timeSignature: measureTimeSignature,
|
|
@@ -454,8 +592,7 @@ export class MusicXMLParser {
|
|
|
454
592
|
const targetDur = (this.currentBeats * 4) / this.currentBeatType;
|
|
455
593
|
measure.voices = measure.voices.map((v) => {
|
|
456
594
|
const currentDur = v.reduce((sum, ns) => {
|
|
457
|
-
|
|
458
|
-
return sum + (ns.notes[0].isDotted ? base * 1.5 : base);
|
|
595
|
+
return sum + calculateDurationValue(ns.notes[0].duration, ns.notes[0].dotCount || (ns.notes[0].isDotted ? 1 : 0));
|
|
459
596
|
}, 0);
|
|
460
597
|
if (currentDur < targetDur - 0.001) {
|
|
461
598
|
const gap = targetDur - currentDur;
|
|
@@ -480,21 +617,22 @@ export class MusicXMLParser {
|
|
|
480
617
|
return staves;
|
|
481
618
|
}
|
|
482
619
|
parseNote(noteElement) {
|
|
483
|
-
const isRest =
|
|
484
|
-
const isGrace =
|
|
620
|
+
const isRest = this.querySelector(noteElement, 'rest') !== null;
|
|
621
|
+
const isGrace = this.querySelector(noteElement, 'grace') !== null;
|
|
485
622
|
const type = this.getText(noteElement, 'type');
|
|
486
623
|
const duration = type ? this.mapDuration(type) : Duration.Quarter;
|
|
487
|
-
const
|
|
624
|
+
const dotCount = this.querySelectorAll(noteElement, 'dot').length;
|
|
625
|
+
const isDotted = dotCount > 0;
|
|
488
626
|
if (isRest)
|
|
489
|
-
return { duration, isRest: true, isDotted };
|
|
627
|
+
return { duration, isRest: true, isDotted, dotCount };
|
|
490
628
|
let step = 'C', octave = 4, alter = 0;
|
|
491
|
-
const unpitched =
|
|
629
|
+
const unpitched = this.querySelector(noteElement, 'unpitched');
|
|
492
630
|
if (unpitched) {
|
|
493
631
|
step = this.getText(unpitched, 'display-step') || 'C';
|
|
494
632
|
octave = this.getNumber(unpitched, 'display-octave') ?? 4;
|
|
495
633
|
}
|
|
496
634
|
else {
|
|
497
|
-
const pitch =
|
|
635
|
+
const pitch = this.querySelector(noteElement, 'pitch');
|
|
498
636
|
if (!pitch)
|
|
499
637
|
return null;
|
|
500
638
|
step = this.getText(pitch, 'step') || 'C';
|
|
@@ -502,7 +640,7 @@ export class MusicXMLParser {
|
|
|
502
640
|
alter = this.getNumber(pitch, 'alter') ?? 0;
|
|
503
641
|
}
|
|
504
642
|
let midiNumber = this.pitchToMidi(step, octave, alter);
|
|
505
|
-
const instrument =
|
|
643
|
+
const instrument = this.querySelector(noteElement, 'instrument');
|
|
506
644
|
if (instrument) {
|
|
507
645
|
const instId = instrument.getAttribute('id');
|
|
508
646
|
if (instId && this.instrumentPitchMap.has(instId))
|
|
@@ -536,17 +674,17 @@ export class MusicXMLParser {
|
|
|
536
674
|
stemDirection = StemDirection.Up;
|
|
537
675
|
else if (stem === 'down')
|
|
538
676
|
stemDirection = StemDirection.Down;
|
|
539
|
-
const notations =
|
|
677
|
+
const notations = this.querySelector(noteElement, 'notations');
|
|
540
678
|
// Tuplet ratio
|
|
541
679
|
let tuplet = undefined;
|
|
542
|
-
const timeMod =
|
|
680
|
+
const timeMod = this.querySelector(noteElement, 'time-modification');
|
|
543
681
|
if (timeMod) {
|
|
544
682
|
const actual = this.getNumber(timeMod, 'actual-notes') || 3;
|
|
545
683
|
const normal = this.getNumber(timeMod, 'normal-notes') || 2;
|
|
546
684
|
tuplet = { actual, normal, type: 'middle' };
|
|
547
685
|
}
|
|
548
686
|
if (notations) {
|
|
549
|
-
const tupletEl =
|
|
687
|
+
const tupletEl = this.querySelector(notations, 'tuplet');
|
|
550
688
|
if (tupletEl && tuplet) {
|
|
551
689
|
const type = tupletEl.getAttribute('type');
|
|
552
690
|
if (type === 'start')
|
|
@@ -556,7 +694,7 @@ export class MusicXMLParser {
|
|
|
556
694
|
}
|
|
557
695
|
}
|
|
558
696
|
let slur = undefined;
|
|
559
|
-
const slurEl = notations
|
|
697
|
+
const slurEl = notations ? this.querySelector(notations, 'slur') : null;
|
|
560
698
|
if (slurEl) {
|
|
561
699
|
const type = slurEl.getAttribute('type');
|
|
562
700
|
if (type === 'start' || type === 'stop') {
|
|
@@ -568,29 +706,29 @@ export class MusicXMLParser {
|
|
|
568
706
|
slur.direction = 'down';
|
|
569
707
|
}
|
|
570
708
|
}
|
|
571
|
-
const tie =
|
|
709
|
+
const tie = this.querySelector(noteElement, 'tie')?.getAttribute('type') === 'start' || undefined;
|
|
572
710
|
const articulations = [];
|
|
573
|
-
const articEl = notations
|
|
711
|
+
const articEl = notations ? this.querySelector(notations, 'articulations') : null;
|
|
574
712
|
if (articEl) {
|
|
575
|
-
if (
|
|
713
|
+
if (this.querySelector(articEl, 'staccato'))
|
|
576
714
|
articulations.push(Articulation.Staccato);
|
|
577
|
-
if (
|
|
715
|
+
if (this.querySelector(articEl, 'accent'))
|
|
578
716
|
articulations.push(Articulation.Accent);
|
|
579
|
-
if (
|
|
717
|
+
if (this.querySelector(articEl, 'tenuto'))
|
|
580
718
|
articulations.push(Articulation.Tenuto);
|
|
581
|
-
if (
|
|
719
|
+
if (this.querySelector(articEl, 'marcato'))
|
|
582
720
|
articulations.push(Articulation.Marcato);
|
|
583
|
-
if (
|
|
721
|
+
if (this.querySelector(articEl, 'staccatissimo'))
|
|
584
722
|
articulations.push(Articulation.Staccatissimo);
|
|
585
|
-
if (
|
|
723
|
+
if (this.querySelector(articEl, 'caesura'))
|
|
586
724
|
articulations.push(Articulation.Caesura);
|
|
587
|
-
if (
|
|
725
|
+
if (this.querySelector(articEl, 'breath-mark'))
|
|
588
726
|
articulations.push(Articulation.BreathMark);
|
|
589
727
|
}
|
|
590
|
-
if (notations
|
|
728
|
+
if (notations && this.querySelector(notations, 'fermata'))
|
|
591
729
|
articulations.push(Articulation.Fermata);
|
|
592
730
|
let arpeggio = undefined;
|
|
593
|
-
const arpeggiateEl = notations
|
|
731
|
+
const arpeggiateEl = notations ? this.querySelector(notations, 'arpeggiate') : null;
|
|
594
732
|
if (arpeggiateEl) {
|
|
595
733
|
const dir = arpeggiateEl.getAttribute('direction');
|
|
596
734
|
if (dir === 'up')
|
|
@@ -600,23 +738,23 @@ export class MusicXMLParser {
|
|
|
600
738
|
else
|
|
601
739
|
arpeggio = Arpeggio.Normal;
|
|
602
740
|
}
|
|
603
|
-
const ornaments = notations
|
|
741
|
+
const ornaments = notations ? this.querySelector(notations, 'ornaments') : null;
|
|
604
742
|
let ornament = undefined;
|
|
605
743
|
if (ornaments) {
|
|
606
|
-
if (
|
|
744
|
+
if (this.querySelector(ornaments, 'trill-mark'))
|
|
607
745
|
ornament = Ornament.Trill;
|
|
608
|
-
else if (
|
|
746
|
+
else if (this.querySelector(ornaments, 'mordent'))
|
|
609
747
|
ornament = Ornament.Mordent;
|
|
610
|
-
else if (
|
|
748
|
+
else if (this.querySelector(ornaments, 'inverted-mordent'))
|
|
611
749
|
ornament = Ornament.InvertedMordent;
|
|
612
|
-
else if (
|
|
750
|
+
else if (this.querySelector(ornaments, 'turn'))
|
|
613
751
|
ornament = Ornament.Turn;
|
|
614
|
-
else if (
|
|
752
|
+
else if (this.querySelector(ornaments, 'inverted-turn'))
|
|
615
753
|
ornament = Ornament.InvertedTurn;
|
|
616
|
-
else if (
|
|
754
|
+
else if (this.querySelector(ornaments, 'tremolo'))
|
|
617
755
|
ornament = Ornament.Tremolo;
|
|
618
756
|
}
|
|
619
|
-
const technical = notations
|
|
757
|
+
const technical = notations ? this.querySelector(notations, 'technical') : null;
|
|
620
758
|
let bowing = undefined;
|
|
621
759
|
let fingering = undefined;
|
|
622
760
|
let fret = undefined;
|
|
@@ -626,33 +764,35 @@ export class MusicXMLParser {
|
|
|
626
764
|
let hammerOn = undefined;
|
|
627
765
|
let pullOff = undefined;
|
|
628
766
|
if (technical) {
|
|
629
|
-
if (
|
|
767
|
+
if (this.querySelector(technical, 'up-bow'))
|
|
630
768
|
bowing = Bowing.UpBow;
|
|
631
|
-
else if (
|
|
769
|
+
else if (this.querySelector(technical, 'down-bow'))
|
|
632
770
|
bowing = Bowing.DownBow;
|
|
633
|
-
const fingeringEl =
|
|
771
|
+
const fingeringEl = this.querySelector(technical, 'fingering');
|
|
634
772
|
if (fingeringEl)
|
|
635
773
|
fingering = parseInt(fingeringEl.textContent || '0');
|
|
636
|
-
const fretEl =
|
|
774
|
+
const fretEl = this.querySelector(technical, 'fret');
|
|
637
775
|
if (fretEl)
|
|
638
776
|
fret = parseInt(fretEl.textContent || '0');
|
|
639
|
-
const stringEl =
|
|
777
|
+
const stringEl = this.querySelector(technical, 'string');
|
|
640
778
|
if (stringEl) {
|
|
641
779
|
stringNumber = parseInt(stringEl.textContent || '0');
|
|
642
780
|
isStringCircled = true;
|
|
643
781
|
}
|
|
644
|
-
const palmMuteEl =
|
|
782
|
+
const palmMuteEl = this.querySelector(technical, 'palm-mute');
|
|
645
783
|
if (palmMuteEl)
|
|
646
784
|
palmMute = palmMuteEl.getAttribute('type') || 'start';
|
|
647
|
-
const hammerOnEl =
|
|
785
|
+
const hammerOnEl = this.querySelector(technical, 'hammer-on');
|
|
648
786
|
if (hammerOnEl)
|
|
649
787
|
hammerOn = hammerOnEl.getAttribute('type') || 'start';
|
|
650
|
-
const pullOffEl =
|
|
788
|
+
const pullOffEl = this.querySelector(technical, 'pull-off');
|
|
651
789
|
if (pullOffEl)
|
|
652
790
|
pullOff = pullOffEl.getAttribute('type') || 'start';
|
|
653
791
|
}
|
|
654
792
|
let glissando = undefined;
|
|
655
|
-
const glissEl = notations
|
|
793
|
+
const glissEl = notations
|
|
794
|
+
? this.querySelector(notations, 'glissando') || this.querySelector(notations, 'slide')
|
|
795
|
+
: null;
|
|
656
796
|
if (glissEl) {
|
|
657
797
|
const type = glissEl.getAttribute('type');
|
|
658
798
|
if (type === 'start' || type === 'stop') {
|
|
@@ -662,13 +802,14 @@ export class MusicXMLParser {
|
|
|
662
802
|
};
|
|
663
803
|
}
|
|
664
804
|
}
|
|
665
|
-
const notesDyn = notations
|
|
666
|
-
const
|
|
805
|
+
const notesDyn = notations ? this.querySelector(notations, 'dynamics') : null;
|
|
806
|
+
const firstChild = notesDyn ? this.getFirstElementChild(notesDyn) : null;
|
|
807
|
+
const dynamic = firstChild?.nodeName.toLowerCase();
|
|
667
808
|
const lyrics = [];
|
|
668
|
-
|
|
809
|
+
this.querySelectorAll(noteElement, 'lyric').forEach((lyricEl) => {
|
|
669
810
|
const text = this.getText(lyricEl, 'text') || '';
|
|
670
811
|
const syllabic = this.getText(lyricEl, 'syllabic');
|
|
671
|
-
const extend =
|
|
812
|
+
const extend = this.querySelector(lyricEl, 'extend') !== null;
|
|
672
813
|
const number = parseInt(lyricEl.getAttribute('number') || '1');
|
|
673
814
|
if (text || extend) {
|
|
674
815
|
lyrics[number - 1] = {
|
|
@@ -682,6 +823,7 @@ export class MusicXMLParser {
|
|
|
682
823
|
duration,
|
|
683
824
|
pitch: { midiNumber, step: this.stepToNumber(step), alter, octave },
|
|
684
825
|
isDotted,
|
|
826
|
+
dotCount,
|
|
685
827
|
accidental,
|
|
686
828
|
slur,
|
|
687
829
|
tie,
|
|
@@ -706,7 +848,7 @@ export class MusicXMLParser {
|
|
|
706
848
|
};
|
|
707
849
|
}
|
|
708
850
|
parseHarmony(harmonyElement) {
|
|
709
|
-
const root =
|
|
851
|
+
const root = this.querySelector(harmonyElement, 'root');
|
|
710
852
|
if (!root)
|
|
711
853
|
return null;
|
|
712
854
|
const step = this.getText(root, 'root-step');
|
|
@@ -767,11 +909,11 @@ export class MusicXMLParser {
|
|
|
767
909
|
}
|
|
768
910
|
// Parse Frame (Fretboard Diagram)
|
|
769
911
|
let diagram = undefined;
|
|
770
|
-
const frame =
|
|
912
|
+
const frame = this.querySelector(harmonyElement, 'frame');
|
|
771
913
|
if (frame) {
|
|
772
914
|
diagram = this.parseFrame(frame);
|
|
773
915
|
}
|
|
774
|
-
const bass =
|
|
916
|
+
const bass = this.querySelector(harmonyElement, 'bass');
|
|
775
917
|
let bassPart = '';
|
|
776
918
|
if (bass) {
|
|
777
919
|
const bStep = this.getText(bass, 'bass-step');
|
|
@@ -796,11 +938,11 @@ export class MusicXMLParser {
|
|
|
796
938
|
const openStrings = [];
|
|
797
939
|
const mutedStrings = [];
|
|
798
940
|
const openBarres = new Map(); // fret -> startString
|
|
799
|
-
|
|
941
|
+
this.querySelectorAll(frame, 'frame-note').forEach((fn) => {
|
|
800
942
|
const string = this.getNumber(fn, 'string');
|
|
801
943
|
const fret = this.getNumber(fn, 'fret');
|
|
802
944
|
const fingering = this.getText(fn, 'fingering');
|
|
803
|
-
const barre =
|
|
945
|
+
const barre = this.querySelector(fn, 'barre')?.getAttribute('type'); // start/stop
|
|
804
946
|
if (string !== null && fret !== null) {
|
|
805
947
|
if (fret === 0) {
|
|
806
948
|
openStrings.push(string);
|
|
@@ -842,7 +984,7 @@ export class MusicXMLParser {
|
|
|
842
984
|
const repeats = [];
|
|
843
985
|
let volta = undefined;
|
|
844
986
|
let barlineStyle = BarlineStyle.Regular;
|
|
845
|
-
|
|
987
|
+
this.querySelectorAll(measureElement, 'barline').forEach((barline) => {
|
|
846
988
|
const location = barline.getAttribute('location') || 'right';
|
|
847
989
|
const barStyle = this.getText(barline, 'bar-style');
|
|
848
990
|
if (location === 'right') {
|
|
@@ -853,7 +995,7 @@ export class MusicXMLParser {
|
|
|
853
995
|
else if (barStyle === 'dotted')
|
|
854
996
|
barlineStyle = BarlineStyle.Dotted;
|
|
855
997
|
}
|
|
856
|
-
const repeatEl =
|
|
998
|
+
const repeatEl = this.querySelector(barline, 'repeat');
|
|
857
999
|
if (repeatEl) {
|
|
858
1000
|
const direction = repeatEl.getAttribute('direction');
|
|
859
1001
|
const times = repeatEl.getAttribute('times');
|
|
@@ -862,7 +1004,7 @@ export class MusicXMLParser {
|
|
|
862
1004
|
times: times ? parseInt(times) : undefined,
|
|
863
1005
|
});
|
|
864
1006
|
}
|
|
865
|
-
const ending =
|
|
1007
|
+
const ending = this.querySelector(barline, 'ending');
|
|
866
1008
|
if (ending) {
|
|
867
1009
|
const type = ending.getAttribute('type');
|
|
868
1010
|
const numberAttr = ending.getAttribute('number') || '1';
|
|
@@ -983,7 +1125,7 @@ export class MusicXMLParser {
|
|
|
983
1125
|
return map[step.toUpperCase()] ?? 0;
|
|
984
1126
|
}
|
|
985
1127
|
getText(element, tagName) {
|
|
986
|
-
const child =
|
|
1128
|
+
const child = this.querySelector(element, tagName);
|
|
987
1129
|
return child?.textContent ?? null;
|
|
988
1130
|
}
|
|
989
1131
|
getNumber(element, tagName) {
|