@scorelabs/core 1.0.2 → 1.0.4
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/README.md +10 -10
- package/dist/importers/MusicXMLParser.d.ts +2 -0
- package/dist/importers/MusicXMLParser.js +388 -78
- package/dist/models/Instrument.d.ts +1 -0
- package/dist/models/Instrument.js +13 -2
- package/dist/models/Measure.js +22 -5
- package/dist/models/Note.d.ts +31 -6
- package/dist/models/Note.js +104 -39
- package/dist/models/NoteSet.d.ts +14 -3
- package/dist/models/NoteSet.js +125 -26
- package/dist/models/Pitch.js +7 -1
- package/dist/models/Score.d.ts +5 -1
- package/dist/models/Score.js +58 -25
- package/dist/models/types.d.ts +16 -2
- package/dist/models/types.js +11 -0
- package/dist/utils/tier.d.ts +36 -0
- package/dist/utils/tier.js +112 -0
- package/package.json +34 -31
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import JSZip from 'jszip';
|
|
2
|
-
import {
|
|
2
|
+
import { getInstrumentByProgram } from '../models/Instrument';
|
|
3
|
+
import { Clef, Duration, Accidental, Articulation, BarlineStyle, HairpinType, OttavaType, Ornament, Bowing, StemDirection, GlissandoType, NoteheadShape, Arpeggio, } from '../models/types';
|
|
3
4
|
/**
|
|
4
5
|
* MusicXML Parser
|
|
5
6
|
*
|
|
@@ -10,10 +11,12 @@ export class MusicXMLParser {
|
|
|
10
11
|
currentKeyFifths = 0;
|
|
11
12
|
currentBeats = 4;
|
|
12
13
|
currentBeatType = 4;
|
|
14
|
+
currentSymbol = 'normal';
|
|
13
15
|
instrumentPitchMap = new Map();
|
|
14
16
|
_domParser;
|
|
15
17
|
constructor(domParserInstance) {
|
|
16
|
-
this._domParser =
|
|
18
|
+
this._domParser =
|
|
19
|
+
domParserInstance || (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
|
|
17
20
|
}
|
|
18
21
|
parseFromString(xmlString) {
|
|
19
22
|
if (!this._domParser) {
|
|
@@ -45,37 +48,55 @@ export class MusicXMLParser {
|
|
|
45
48
|
}
|
|
46
49
|
parse(xmlString) {
|
|
47
50
|
this.instrumentPitchMap.clear();
|
|
51
|
+
// Strip potential BOM and leading garbage
|
|
52
|
+
const firstBracket = xmlString.indexOf('<');
|
|
53
|
+
if (firstBracket === -1) {
|
|
54
|
+
const preview = xmlString.trim().substring(0, 50).replace(/\n/g, ' ');
|
|
55
|
+
throw new Error(`Invalid MusicXML: No '<' found in content. Content: "${preview}..."`);
|
|
56
|
+
}
|
|
57
|
+
xmlString = xmlString.substring(firstBracket).trim();
|
|
48
58
|
const doc = this.parseFromString(xmlString);
|
|
49
59
|
const parseError = doc.querySelector('parsererror');
|
|
50
|
-
if (parseError)
|
|
51
|
-
|
|
60
|
+
if (parseError) {
|
|
61
|
+
const preview = xmlString.substring(0, 100).replace(/\n/g, ' ');
|
|
62
|
+
throw new Error(`XML parsing error: ${parseError.textContent}. Content preview: "${preview}..."`);
|
|
63
|
+
}
|
|
52
64
|
const root = doc.documentElement;
|
|
53
65
|
if (root.tagName !== 'score-partwise')
|
|
54
66
|
throw new Error(`Unsupported format: ${root.tagName}`);
|
|
55
67
|
const title = this.parseTitle(doc);
|
|
56
68
|
const subtitle = this.parseSubtitle(doc);
|
|
57
69
|
const composer = this.parseComposer(doc);
|
|
58
|
-
const
|
|
59
|
-
const parts = this.parseParts(doc,
|
|
70
|
+
const partInfo = this.parsePartList(doc);
|
|
71
|
+
const parts = this.parseParts(doc, partInfo);
|
|
60
72
|
let globalBpm = 120;
|
|
61
73
|
let globalTempoDuration = Duration.Quarter;
|
|
62
74
|
let globalTempoIsDotted = false;
|
|
75
|
+
let globalTempoText = '';
|
|
63
76
|
if (parts.length > 0 && parts[0].staves.length > 0 && parts[0].staves[0].measures.length > 0) {
|
|
64
77
|
const firstMeasure = parts[0].staves[0].measures[0];
|
|
65
78
|
if (firstMeasure.tempo) {
|
|
66
79
|
globalBpm = firstMeasure.tempo.bpm;
|
|
67
80
|
globalTempoDuration = firstMeasure.tempo.duration;
|
|
68
81
|
globalTempoIsDotted = firstMeasure.tempo.isDotted;
|
|
82
|
+
globalTempoText = firstMeasure.tempo.text || '';
|
|
69
83
|
}
|
|
70
84
|
}
|
|
71
85
|
const score = {
|
|
72
|
-
title,
|
|
73
|
-
|
|
86
|
+
title,
|
|
87
|
+
subtitle,
|
|
88
|
+
composer,
|
|
89
|
+
timeSignature: {
|
|
90
|
+
beats: this.currentBeats,
|
|
91
|
+
beatType: this.currentBeatType,
|
|
92
|
+
symbol: this.currentSymbol !== 'normal' ? this.currentSymbol : undefined,
|
|
93
|
+
},
|
|
74
94
|
keySignature: { fifths: this.currentKeyFifths },
|
|
75
95
|
parts,
|
|
76
96
|
bpm: globalBpm,
|
|
77
97
|
tempoDuration: globalTempoDuration,
|
|
78
98
|
tempoIsDotted: globalTempoIsDotted,
|
|
99
|
+
tempoText: globalTempoText,
|
|
79
100
|
};
|
|
80
101
|
return this.postProcess(score);
|
|
81
102
|
}
|
|
@@ -118,13 +139,27 @@ export class MusicXMLParser {
|
|
|
118
139
|
return doc.querySelector('identification creator')?.textContent ?? 'Unknown';
|
|
119
140
|
}
|
|
120
141
|
parsePartList(doc) {
|
|
121
|
-
const
|
|
142
|
+
const partInfo = new Map();
|
|
122
143
|
const scoreParts = doc.querySelectorAll('part-list score-part');
|
|
123
144
|
for (const scorePart of Array.from(scoreParts)) {
|
|
124
145
|
const id = scorePart.getAttribute('id');
|
|
125
|
-
const name = this.getText(scorePart, 'part-name');
|
|
126
|
-
|
|
127
|
-
|
|
146
|
+
const name = this.getText(scorePart, 'part-name') || 'Part';
|
|
147
|
+
let instrument = getInstrumentByProgram(0); // Default Piano
|
|
148
|
+
// Find first midi-instrument to determine main instrument
|
|
149
|
+
const firstMidiInst = scorePart.querySelector('midi-instrument');
|
|
150
|
+
if (firstMidiInst) {
|
|
151
|
+
const programStr = this.getText(firstMidiInst, 'midi-program');
|
|
152
|
+
if (programStr) {
|
|
153
|
+
const program = parseInt(programStr);
|
|
154
|
+
// MusicXML uses 1-128, MIDI uses 0-127. Adjust if needed.
|
|
155
|
+
// Often it's accurate to 1-based, so checking bounds.
|
|
156
|
+
// Assuming input is 1-based as per standard
|
|
157
|
+
instrument = getInstrumentByProgram(Math.max(0, program - 1));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (id) {
|
|
161
|
+
partInfo.set(id, { name: name.trim(), instrument });
|
|
162
|
+
}
|
|
128
163
|
const midiInstruments = scorePart.querySelectorAll('midi-instrument');
|
|
129
164
|
for (const midiInst of Array.from(midiInstruments)) {
|
|
130
165
|
const instId = midiInst.getAttribute('id');
|
|
@@ -133,15 +168,17 @@ export class MusicXMLParser {
|
|
|
133
168
|
this.instrumentPitchMap.set(instId, unpitched);
|
|
134
169
|
}
|
|
135
170
|
}
|
|
136
|
-
return
|
|
171
|
+
return partInfo;
|
|
137
172
|
}
|
|
138
|
-
parseParts(doc,
|
|
173
|
+
parseParts(doc, partInfo) {
|
|
139
174
|
const parts = [];
|
|
140
175
|
const partElements = doc.querySelectorAll('part');
|
|
141
176
|
for (const partElement of Array.from(partElements)) {
|
|
142
177
|
const id = partElement.getAttribute('id');
|
|
143
|
-
const
|
|
144
|
-
|
|
178
|
+
const info = partInfo.get(id ?? '');
|
|
179
|
+
const name = info?.name ?? 'Part';
|
|
180
|
+
const instrument = info?.instrument;
|
|
181
|
+
parts.push({ name, staves: this.parsePart(partElement), instrument });
|
|
145
182
|
}
|
|
146
183
|
return parts;
|
|
147
184
|
}
|
|
@@ -154,7 +191,10 @@ export class MusicXMLParser {
|
|
|
154
191
|
if (stavesNode)
|
|
155
192
|
numStaves = parseInt(stavesNode.textContent || '1');
|
|
156
193
|
}
|
|
157
|
-
const staves = Array.from({ length: numStaves }, () => ({
|
|
194
|
+
const staves = Array.from({ length: numStaves }, () => ({
|
|
195
|
+
clef: 'treble',
|
|
196
|
+
measures: [],
|
|
197
|
+
}));
|
|
158
198
|
const staffClefs = Array(numStaves).fill(Clef.Treble);
|
|
159
199
|
for (const measureElement of Array.from(measureElements)) {
|
|
160
200
|
let measureTimeSignature;
|
|
@@ -174,15 +214,20 @@ export class MusicXMLParser {
|
|
|
174
214
|
if (beatType !== null)
|
|
175
215
|
this.currentBeatType = beatType;
|
|
176
216
|
const timeElement = attributes.querySelector('time');
|
|
177
|
-
let symbol = 'normal';
|
|
178
217
|
if (timeElement) {
|
|
179
218
|
const symAttr = timeElement.getAttribute('symbol');
|
|
180
219
|
if (symAttr === 'common')
|
|
181
|
-
|
|
182
|
-
if (symAttr === 'cut')
|
|
183
|
-
|
|
220
|
+
this.currentSymbol = 'common';
|
|
221
|
+
else if (symAttr === 'cut')
|
|
222
|
+
this.currentSymbol = 'cut';
|
|
223
|
+
else
|
|
224
|
+
this.currentSymbol = 'normal';
|
|
184
225
|
}
|
|
185
|
-
measureTimeSignature = {
|
|
226
|
+
measureTimeSignature = {
|
|
227
|
+
beats: this.currentBeats,
|
|
228
|
+
beatType: this.currentBeatType,
|
|
229
|
+
symbol: this.currentSymbol !== 'normal' ? this.currentSymbol : undefined,
|
|
230
|
+
};
|
|
186
231
|
}
|
|
187
232
|
const clefElements = attributes.querySelectorAll('clef');
|
|
188
233
|
clefElements.forEach((clef) => {
|
|
@@ -205,25 +250,6 @@ export class MusicXMLParser {
|
|
|
205
250
|
let rehearsalMark = undefined;
|
|
206
251
|
let systemText = undefined;
|
|
207
252
|
let contextDynamic = undefined;
|
|
208
|
-
const directions = measureElement.querySelectorAll('direction');
|
|
209
|
-
directions.forEach((dir) => {
|
|
210
|
-
const metronome = dir.querySelector('metronome');
|
|
211
|
-
if (metronome) {
|
|
212
|
-
const beatUnit = this.getText(metronome, 'beat-unit') || 'quarter';
|
|
213
|
-
const bpm = parseInt(this.getText(metronome, 'per-minute') || '120');
|
|
214
|
-
const isDotted = metronome.querySelector('beat-unit-dot') !== null;
|
|
215
|
-
measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted };
|
|
216
|
-
}
|
|
217
|
-
const rehearsal = dir.querySelector('rehearsal');
|
|
218
|
-
if (rehearsal)
|
|
219
|
-
rehearsalMark = rehearsal.textContent || undefined;
|
|
220
|
-
const words = dir.querySelector('words');
|
|
221
|
-
if (words)
|
|
222
|
-
systemText = words.textContent || undefined;
|
|
223
|
-
const dynamics = dir.querySelector('dynamics');
|
|
224
|
-
if (dynamics && dynamics.firstElementChild)
|
|
225
|
-
contextDynamic = dynamics.firstElementChild.nodeName.toLowerCase();
|
|
226
|
-
});
|
|
227
253
|
const { repeats, volta, barlineStyle } = this.parseBarlines(measureElement);
|
|
228
254
|
const measureVoices = Array.from({ length: numStaves }, () => []);
|
|
229
255
|
const children = Array.from(measureElement.children);
|
|
@@ -231,9 +257,72 @@ export class MusicXMLParser {
|
|
|
231
257
|
let currentNoteSetMap = new Map();
|
|
232
258
|
const beamGroupIdMap = new Map();
|
|
233
259
|
let globalBeamId = 1;
|
|
260
|
+
let currentHairpin = undefined;
|
|
261
|
+
let currentOttava = undefined;
|
|
262
|
+
let currentPedal = undefined;
|
|
234
263
|
children.forEach((child) => {
|
|
235
|
-
if (child.nodeName === 'harmony')
|
|
236
|
-
|
|
264
|
+
if (child.nodeName === 'harmony') {
|
|
265
|
+
const harmonyData = this.parseHarmony(child);
|
|
266
|
+
if (harmonyData)
|
|
267
|
+
pendingChord = harmonyData;
|
|
268
|
+
}
|
|
269
|
+
else if (child.nodeName === 'direction') {
|
|
270
|
+
const metronome = child.querySelector('metronome');
|
|
271
|
+
if (metronome) {
|
|
272
|
+
const beatUnit = this.getText(metronome, 'beat-unit') || 'quarter';
|
|
273
|
+
const bpm = parseInt(this.getText(metronome, 'per-minute') || '120');
|
|
274
|
+
const isDotted = metronome.querySelector('beat-unit-dot') !== null;
|
|
275
|
+
measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted };
|
|
276
|
+
}
|
|
277
|
+
const rehearsal = child.querySelector('rehearsal');
|
|
278
|
+
if (rehearsal)
|
|
279
|
+
rehearsalMark = rehearsal.textContent || undefined;
|
|
280
|
+
const words = child.querySelector('words');
|
|
281
|
+
if (words) {
|
|
282
|
+
if (measureTempo)
|
|
283
|
+
measureTempo.text = words.textContent || undefined;
|
|
284
|
+
else
|
|
285
|
+
systemText = words.textContent || undefined;
|
|
286
|
+
}
|
|
287
|
+
const wedge = child.querySelector('wedge');
|
|
288
|
+
if (wedge) {
|
|
289
|
+
const typeAttr = wedge.getAttribute('type');
|
|
290
|
+
if (typeAttr === 'crescendo' || typeAttr === 'diminuendo' || typeAttr === 'stop') {
|
|
291
|
+
currentHairpin = {
|
|
292
|
+
type: typeAttr === 'crescendo' ? HairpinType.Crescendo : HairpinType.Decrescendo,
|
|
293
|
+
placement: typeAttr === 'stop' ? 'stop' : 'start',
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const octShift = child.querySelector('octave-shift');
|
|
298
|
+
if (octShift) {
|
|
299
|
+
const typeAttr = octShift.getAttribute('type');
|
|
300
|
+
const size = parseInt(octShift.getAttribute('size') || '8');
|
|
301
|
+
if (typeAttr === 'up' || typeAttr === 'down' || typeAttr === 'stop') {
|
|
302
|
+
currentOttava = {
|
|
303
|
+
type: size === 15
|
|
304
|
+
? typeAttr === 'up'
|
|
305
|
+
? OttavaType.QuindicesimaAlta
|
|
306
|
+
: OttavaType.QuindicesimaBassa
|
|
307
|
+
: typeAttr === 'up'
|
|
308
|
+
? OttavaType.OttavaAlta
|
|
309
|
+
: OttavaType.OttavaBassa,
|
|
310
|
+
placement: typeAttr === 'stop' ? 'stop' : 'start',
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const pedalNode = child.querySelector('pedal');
|
|
315
|
+
if (pedalNode) {
|
|
316
|
+
const typeAttr = pedalNode.getAttribute('type');
|
|
317
|
+
if (typeAttr === 'start' || typeAttr === 'stop') {
|
|
318
|
+
currentPedal = { type: 'sustain', placement: typeAttr };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const dynamics = child.querySelector('dynamics');
|
|
322
|
+
if (dynamics && dynamics.firstElementChild) {
|
|
323
|
+
contextDynamic = dynamics.firstElementChild.nodeName.toLowerCase();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
237
326
|
else if (child.nodeName === 'note') {
|
|
238
327
|
const staffNum = parseInt(this.getText(child, 'staff') || '1');
|
|
239
328
|
const staffIdx = Math.min(Math.max(staffNum - 1, 0), numStaves - 1);
|
|
@@ -258,13 +347,27 @@ export class MusicXMLParser {
|
|
|
258
347
|
}
|
|
259
348
|
}
|
|
260
349
|
if (pendingChord) {
|
|
261
|
-
note.chord = pendingChord;
|
|
350
|
+
note.chord = pendingChord.symbol;
|
|
351
|
+
if (pendingChord.diagram)
|
|
352
|
+
note.fretboardDiagram = pendingChord.diagram;
|
|
262
353
|
pendingChord = undefined;
|
|
263
354
|
}
|
|
264
355
|
if (contextDynamic) {
|
|
265
356
|
note.dynamic = contextDynamic;
|
|
266
357
|
contextDynamic = undefined;
|
|
267
358
|
}
|
|
359
|
+
if (currentHairpin) {
|
|
360
|
+
note.hairpin = currentHairpin;
|
|
361
|
+
currentHairpin = undefined;
|
|
362
|
+
}
|
|
363
|
+
if (currentOttava) {
|
|
364
|
+
note.ottava = currentOttava;
|
|
365
|
+
currentOttava = undefined;
|
|
366
|
+
}
|
|
367
|
+
if (currentPedal) {
|
|
368
|
+
note.pedal = currentPedal;
|
|
369
|
+
currentPedal = undefined;
|
|
370
|
+
}
|
|
268
371
|
if (isChord) {
|
|
269
372
|
const existing = currentNoteSetMap.get(staffIdx);
|
|
270
373
|
if (existing)
|
|
@@ -293,7 +396,7 @@ export class MusicXMLParser {
|
|
|
293
396
|
measureVoices[sIdx].push({ notes });
|
|
294
397
|
}
|
|
295
398
|
for (let i = 0; i < numStaves; i++) {
|
|
296
|
-
|
|
399
|
+
const measure = {
|
|
297
400
|
voices: [measureVoices[i]],
|
|
298
401
|
timeSignature: measureTimeSignature,
|
|
299
402
|
keySignature: measureKeySignature,
|
|
@@ -304,7 +407,8 @@ export class MusicXMLParser {
|
|
|
304
407
|
rehearsalMark: i === 0 ? rehearsalMark : undefined,
|
|
305
408
|
systemText: i === 0 ? systemText : undefined,
|
|
306
409
|
barlineStyle,
|
|
307
|
-
}
|
|
410
|
+
};
|
|
411
|
+
staves[i].measures.push(measure);
|
|
308
412
|
staves[i].clef = staffClefs[i];
|
|
309
413
|
}
|
|
310
414
|
}
|
|
@@ -312,6 +416,7 @@ export class MusicXMLParser {
|
|
|
312
416
|
}
|
|
313
417
|
parseNote(noteElement) {
|
|
314
418
|
const isRest = noteElement.querySelector('rest') !== null;
|
|
419
|
+
const isGrace = noteElement.querySelector('grace') !== null;
|
|
315
420
|
const type = this.getText(noteElement, 'type');
|
|
316
421
|
const duration = type ? this.mapDuration(type) : Duration.Quarter;
|
|
317
422
|
const isDotted = noteElement.querySelector('dot') !== null;
|
|
@@ -356,11 +461,35 @@ export class MusicXMLParser {
|
|
|
356
461
|
const noteheadEl = this.getText(noteElement, 'notehead');
|
|
357
462
|
if (noteheadEl) {
|
|
358
463
|
if (noteheadEl === 'x')
|
|
359
|
-
notehead =
|
|
464
|
+
notehead = NoteheadShape.Cross;
|
|
360
465
|
else if (['diamond', 'triangle', 'slash', 'square'].includes(noteheadEl))
|
|
361
466
|
notehead = noteheadEl;
|
|
362
467
|
}
|
|
468
|
+
const stem = this.getText(noteElement, 'stem');
|
|
469
|
+
let stemDirection = undefined;
|
|
470
|
+
if (stem === 'up')
|
|
471
|
+
stemDirection = StemDirection.Up;
|
|
472
|
+
else if (stem === 'down')
|
|
473
|
+
stemDirection = StemDirection.Down;
|
|
363
474
|
const notations = noteElement.querySelector('notations');
|
|
475
|
+
// Tuplet ratio
|
|
476
|
+
let tuplet = undefined;
|
|
477
|
+
const timeMod = noteElement.querySelector('time-modification');
|
|
478
|
+
if (timeMod) {
|
|
479
|
+
const actual = this.getNumber(timeMod, 'actual-notes') || 3;
|
|
480
|
+
const normal = this.getNumber(timeMod, 'normal-notes') || 2;
|
|
481
|
+
tuplet = { actual, normal, type: 'middle' };
|
|
482
|
+
}
|
|
483
|
+
if (notations) {
|
|
484
|
+
const tupletEl = notations.querySelector('tuplet');
|
|
485
|
+
if (tupletEl && tuplet) {
|
|
486
|
+
const type = tupletEl.getAttribute('type');
|
|
487
|
+
if (type === 'start')
|
|
488
|
+
tuplet.type = 'start';
|
|
489
|
+
else if (type === 'stop')
|
|
490
|
+
tuplet.type = 'stop';
|
|
491
|
+
}
|
|
492
|
+
}
|
|
364
493
|
let slur = undefined;
|
|
365
494
|
const slurEl = notations?.querySelector('slur');
|
|
366
495
|
if (slurEl) {
|
|
@@ -375,48 +504,149 @@ export class MusicXMLParser {
|
|
|
375
504
|
}
|
|
376
505
|
}
|
|
377
506
|
const tie = noteElement.querySelector('tie')?.getAttribute('type') === 'start' || undefined;
|
|
378
|
-
|
|
507
|
+
const articulations = [];
|
|
379
508
|
const articEl = notations?.querySelector('articulations');
|
|
380
509
|
if (articEl) {
|
|
381
510
|
if (articEl.querySelector('staccato'))
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
511
|
+
articulations.push(Articulation.Staccato);
|
|
512
|
+
if (articEl.querySelector('accent'))
|
|
513
|
+
articulations.push(Articulation.Accent);
|
|
514
|
+
if (articEl.querySelector('tenuto'))
|
|
515
|
+
articulations.push(Articulation.Tenuto);
|
|
516
|
+
if (articEl.querySelector('marcato'))
|
|
517
|
+
articulations.push(Articulation.Marcato);
|
|
518
|
+
if (articEl.querySelector('staccatissimo'))
|
|
519
|
+
articulations.push(Articulation.Staccatissimo);
|
|
520
|
+
if (articEl.querySelector('caesura'))
|
|
521
|
+
articulations.push(Articulation.Caesura);
|
|
522
|
+
if (articEl.querySelector('breath-mark'))
|
|
523
|
+
articulations.push(Articulation.BreathMark);
|
|
391
524
|
}
|
|
392
525
|
if (notations?.querySelector('fermata'))
|
|
393
|
-
|
|
526
|
+
articulations.push(Articulation.Fermata);
|
|
527
|
+
let arpeggio = undefined;
|
|
528
|
+
const arpeggiateEl = notations?.querySelector('arpeggiate');
|
|
529
|
+
if (arpeggiateEl) {
|
|
530
|
+
const dir = arpeggiateEl.getAttribute('direction');
|
|
531
|
+
if (dir === 'up')
|
|
532
|
+
arpeggio = Arpeggio.Up;
|
|
533
|
+
else if (dir === 'down')
|
|
534
|
+
arpeggio = Arpeggio.Down;
|
|
535
|
+
else
|
|
536
|
+
arpeggio = Arpeggio.Normal;
|
|
537
|
+
}
|
|
538
|
+
const ornaments = notations?.querySelector('ornaments');
|
|
539
|
+
let ornament = undefined;
|
|
540
|
+
if (ornaments) {
|
|
541
|
+
if (ornaments.querySelector('trill-mark'))
|
|
542
|
+
ornament = Ornament.Trill;
|
|
543
|
+
else if (ornaments.querySelector('mordent'))
|
|
544
|
+
ornament = Ornament.Mordent;
|
|
545
|
+
else if (ornaments.querySelector('inverted-mordent'))
|
|
546
|
+
ornament = Ornament.InvertedMordent;
|
|
547
|
+
else if (ornaments.querySelector('turn'))
|
|
548
|
+
ornament = Ornament.Turn;
|
|
549
|
+
else if (ornaments.querySelector('inverted-turn'))
|
|
550
|
+
ornament = Ornament.InvertedTurn;
|
|
551
|
+
else if (ornaments.querySelector('tremolo'))
|
|
552
|
+
ornament = Ornament.Tremolo;
|
|
553
|
+
}
|
|
554
|
+
const technical = notations?.querySelector('technical');
|
|
555
|
+
let bowing = undefined;
|
|
556
|
+
let fingering = undefined;
|
|
557
|
+
let fret = undefined;
|
|
558
|
+
let stringNumber = undefined;
|
|
559
|
+
let isStringCircled = undefined;
|
|
560
|
+
let palmMute = undefined;
|
|
561
|
+
let hammerOn = undefined;
|
|
562
|
+
let pullOff = undefined;
|
|
563
|
+
if (technical) {
|
|
564
|
+
if (technical.querySelector('up-bow'))
|
|
565
|
+
bowing = Bowing.UpBow;
|
|
566
|
+
else if (technical.querySelector('down-bow'))
|
|
567
|
+
bowing = Bowing.DownBow;
|
|
568
|
+
const fingeringEl = technical.querySelector('fingering');
|
|
569
|
+
if (fingeringEl)
|
|
570
|
+
fingering = parseInt(fingeringEl.textContent || '0');
|
|
571
|
+
const fretEl = technical.querySelector('fret');
|
|
572
|
+
if (fretEl)
|
|
573
|
+
fret = parseInt(fretEl.textContent || '0');
|
|
574
|
+
const stringEl = technical.querySelector('string');
|
|
575
|
+
if (stringEl) {
|
|
576
|
+
stringNumber = parseInt(stringEl.textContent || '0');
|
|
577
|
+
isStringCircled = true;
|
|
578
|
+
}
|
|
579
|
+
const palmMuteEl = technical.querySelector('palm-mute');
|
|
580
|
+
if (palmMuteEl)
|
|
581
|
+
palmMute = palmMuteEl.getAttribute('type') || 'start';
|
|
582
|
+
const hammerOnEl = technical.querySelector('hammer-on');
|
|
583
|
+
if (hammerOnEl)
|
|
584
|
+
hammerOn = hammerOnEl.getAttribute('type') || 'start';
|
|
585
|
+
const pullOffEl = technical.querySelector('pull-off');
|
|
586
|
+
if (pullOffEl)
|
|
587
|
+
pullOff = pullOffEl.getAttribute('type') || 'start';
|
|
588
|
+
}
|
|
589
|
+
let glissando = undefined;
|
|
590
|
+
const glissEl = notations?.querySelector('glissando') || notations?.querySelector('slide');
|
|
591
|
+
if (glissEl) {
|
|
592
|
+
const type = glissEl.getAttribute('type');
|
|
593
|
+
if (type === 'start' || type === 'stop') {
|
|
594
|
+
glissando = {
|
|
595
|
+
type: glissEl.nodeName === 'slide' ? GlissandoType.Straight : GlissandoType.Wavy,
|
|
596
|
+
placement: type,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
394
600
|
const notesDyn = notations?.querySelector('dynamics');
|
|
395
601
|
const dynamic = notesDyn?.firstElementChild?.nodeName.toLowerCase();
|
|
396
602
|
const lyrics = [];
|
|
397
603
|
noteElement.querySelectorAll('lyric').forEach((lyricEl) => {
|
|
398
|
-
|
|
604
|
+
const text = this.getText(lyricEl, 'text') || '';
|
|
399
605
|
const syllabic = this.getText(lyricEl, 'syllabic');
|
|
606
|
+
const extend = lyricEl.querySelector('extend') !== null;
|
|
400
607
|
const number = parseInt(lyricEl.getAttribute('number') || '1');
|
|
401
|
-
if (text) {
|
|
402
|
-
|
|
403
|
-
text
|
|
404
|
-
|
|
608
|
+
if (text || extend) {
|
|
609
|
+
lyrics[number - 1] = {
|
|
610
|
+
text,
|
|
611
|
+
syllabic: syllabic || undefined,
|
|
612
|
+
isExtension: extend || undefined,
|
|
613
|
+
};
|
|
405
614
|
}
|
|
406
615
|
});
|
|
407
616
|
return {
|
|
408
|
-
duration,
|
|
409
|
-
|
|
410
|
-
|
|
617
|
+
duration,
|
|
618
|
+
pitch: { midiNumber, step: this.stepToNumber(step), alter, octave },
|
|
619
|
+
isDotted,
|
|
620
|
+
accidental,
|
|
621
|
+
slur,
|
|
622
|
+
tie,
|
|
623
|
+
articulations: articulations.length > 0 ? articulations : undefined,
|
|
624
|
+
dynamic,
|
|
625
|
+
arpeggio,
|
|
626
|
+
lyrics: lyrics.length > 0 ? lyrics : undefined,
|
|
627
|
+
notehead,
|
|
628
|
+
isGrace: isGrace || undefined,
|
|
629
|
+
tuplet,
|
|
630
|
+
ornament,
|
|
631
|
+
bowing,
|
|
632
|
+
fingering,
|
|
633
|
+
stemDirection,
|
|
634
|
+
glissando,
|
|
635
|
+
fret,
|
|
636
|
+
string: stringNumber,
|
|
637
|
+
isStringCircled,
|
|
638
|
+
palmMute,
|
|
639
|
+
hammerOn,
|
|
640
|
+
pullOff,
|
|
411
641
|
};
|
|
412
642
|
}
|
|
413
643
|
parseHarmony(harmonyElement) {
|
|
414
644
|
const root = harmonyElement.querySelector('root');
|
|
415
645
|
if (!root)
|
|
416
|
-
return
|
|
646
|
+
return null;
|
|
417
647
|
const step = this.getText(root, 'root-step');
|
|
418
648
|
if (!step)
|
|
419
|
-
return
|
|
649
|
+
return null;
|
|
420
650
|
const alter = this.getNumber(root, 'root-alter') || 0;
|
|
421
651
|
const kind = this.getText(harmonyElement, 'kind') || 'major';
|
|
422
652
|
let accidental = '';
|
|
@@ -470,6 +700,12 @@ export class MusicXMLParser {
|
|
|
470
700
|
kindNotation = 'm6';
|
|
471
701
|
break;
|
|
472
702
|
}
|
|
703
|
+
// Parse Frame (Fretboard Diagram)
|
|
704
|
+
let diagram = undefined;
|
|
705
|
+
const frame = harmonyElement.querySelector('frame');
|
|
706
|
+
if (frame) {
|
|
707
|
+
diagram = this.parseFrame(frame);
|
|
708
|
+
}
|
|
473
709
|
const bass = harmonyElement.querySelector('bass');
|
|
474
710
|
let bassPart = '';
|
|
475
711
|
if (bass) {
|
|
@@ -484,7 +720,58 @@ export class MusicXMLParser {
|
|
|
484
720
|
bassPart = '/' + bStep + bAcc;
|
|
485
721
|
}
|
|
486
722
|
}
|
|
487
|
-
return step + accidental + kindNotation + bassPart;
|
|
723
|
+
return { symbol: step + accidental + kindNotation + bassPart, diagram };
|
|
724
|
+
}
|
|
725
|
+
parseFrame(frame) {
|
|
726
|
+
const strings = this.getNumber(frame, 'frame-strings') || 6;
|
|
727
|
+
const frets = this.getNumber(frame, 'frame-frets') || 5;
|
|
728
|
+
const firstFret = this.getNumber(frame, 'first-fret') || 1;
|
|
729
|
+
const dots = [];
|
|
730
|
+
const barres = [];
|
|
731
|
+
const openStrings = [];
|
|
732
|
+
const mutedStrings = [];
|
|
733
|
+
const openBarres = new Map(); // fret -> startString
|
|
734
|
+
frame.querySelectorAll('frame-note').forEach((fn) => {
|
|
735
|
+
const string = this.getNumber(fn, 'string');
|
|
736
|
+
const fret = this.getNumber(fn, 'fret');
|
|
737
|
+
const fingering = this.getText(fn, 'fingering');
|
|
738
|
+
const barre = fn.querySelector('barre')?.getAttribute('type'); // start/stop
|
|
739
|
+
if (string !== null && fret !== null) {
|
|
740
|
+
if (fret === 0) {
|
|
741
|
+
openStrings.push(string);
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
dots.push({ string, fret, label: fingering || undefined });
|
|
745
|
+
}
|
|
746
|
+
if (barre === 'start') {
|
|
747
|
+
openBarres.set(fret, string);
|
|
748
|
+
}
|
|
749
|
+
else if (barre === 'stop') {
|
|
750
|
+
const startString = openBarres.get(fret);
|
|
751
|
+
if (startString !== undefined) {
|
|
752
|
+
barres.push({
|
|
753
|
+
fret,
|
|
754
|
+
startString: Math.min(startString, string),
|
|
755
|
+
endString: Math.max(startString, string),
|
|
756
|
+
});
|
|
757
|
+
openBarres.delete(fret);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
// MusicXML handling of barres is complex (start/stop on strings).
|
|
763
|
+
// Simple implementation: Group dots/barres later or parse strictly if provided.
|
|
764
|
+
// For now, let's extract basic dots.
|
|
765
|
+
// Note: Real parsing of barres requires tracking the 'start' and 'stop' across strings for the same fret.
|
|
766
|
+
return {
|
|
767
|
+
strings,
|
|
768
|
+
frets,
|
|
769
|
+
startingFret: firstFret,
|
|
770
|
+
dots,
|
|
771
|
+
barres: barres.length > 0 ? barres : undefined,
|
|
772
|
+
openStrings: openStrings.length > 0 ? openStrings : undefined,
|
|
773
|
+
mutedStrings: mutedStrings.length > 0 ? mutedStrings : undefined,
|
|
774
|
+
};
|
|
488
775
|
}
|
|
489
776
|
parseBarlines(measureElement) {
|
|
490
777
|
const repeats = [];
|
|
@@ -502,7 +789,11 @@ export class MusicXMLParser {
|
|
|
502
789
|
barlineStyle = BarlineStyle.Dotted;
|
|
503
790
|
}
|
|
504
791
|
if (barline.querySelector('repeat'))
|
|
505
|
-
repeats.push({
|
|
792
|
+
repeats.push({
|
|
793
|
+
type: barline.querySelector('repeat')?.getAttribute('direction') === 'forward'
|
|
794
|
+
? 'start'
|
|
795
|
+
: 'end',
|
|
796
|
+
});
|
|
506
797
|
const ending = barline.querySelector('ending');
|
|
507
798
|
if (ending) {
|
|
508
799
|
const type = ending.getAttribute('type');
|
|
@@ -554,7 +845,12 @@ export class MusicXMLParser {
|
|
|
554
845
|
state[step] = alter;
|
|
555
846
|
}
|
|
556
847
|
else if (state[step] !== alter) {
|
|
557
|
-
note.accidental =
|
|
848
|
+
note.accidental =
|
|
849
|
+
alter === 0
|
|
850
|
+
? Accidental.Natural
|
|
851
|
+
: alter === 1
|
|
852
|
+
? Accidental.Sharp
|
|
853
|
+
: Accidental.Flat;
|
|
558
854
|
state[step] = alter;
|
|
559
855
|
}
|
|
560
856
|
});
|
|
@@ -569,17 +865,31 @@ export class MusicXMLParser {
|
|
|
569
865
|
return (octave + 1) * 12 + (steps[step.toUpperCase()] ?? 0) + alter;
|
|
570
866
|
}
|
|
571
867
|
mapDuration(type) {
|
|
572
|
-
const map = {
|
|
868
|
+
const map = {
|
|
869
|
+
whole: Duration.Whole,
|
|
870
|
+
half: Duration.Half,
|
|
871
|
+
quarter: Duration.Quarter,
|
|
872
|
+
eighth: Duration.Eighth,
|
|
873
|
+
'16th': Duration.Sixteenth,
|
|
874
|
+
'32nd': Duration.ThirtySecond,
|
|
875
|
+
'64th': Duration.SixtyFourth,
|
|
876
|
+
};
|
|
573
877
|
return map[type] ?? Duration.Quarter;
|
|
574
878
|
}
|
|
575
879
|
mapClef(sign, line = 0) {
|
|
576
880
|
switch (sign.toUpperCase()) {
|
|
577
|
-
case 'G':
|
|
578
|
-
|
|
579
|
-
case '
|
|
580
|
-
|
|
581
|
-
case '
|
|
582
|
-
|
|
881
|
+
case 'G':
|
|
882
|
+
return Clef.Treble;
|
|
883
|
+
case 'F':
|
|
884
|
+
return Clef.Bass;
|
|
885
|
+
case 'C':
|
|
886
|
+
return line === 4 ? Clef.Tenor : Clef.Alto;
|
|
887
|
+
case 'PERCUSSION':
|
|
888
|
+
return Clef.Percussion;
|
|
889
|
+
case 'TAB':
|
|
890
|
+
return Clef.Tab;
|
|
891
|
+
default:
|
|
892
|
+
return Clef.Treble;
|
|
583
893
|
}
|
|
584
894
|
}
|
|
585
895
|
mapAccidental(alter) {
|