@scorelabs/core 1.0.10 → 1.0.13
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 +4 -1
- package/dist/importers/MusicXMLParser.js +564 -75
- 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 +25 -5
- package/dist/models/Measure.js +173 -35
- package/dist/models/Note.d.ts +38 -9
- package/dist/models/Note.js +104 -47
- package/dist/models/NoteSet.d.ts +14 -4
- package/dist/models/NoteSet.js +38 -4
- package/dist/models/Part.d.ts +6 -6
- package/dist/models/Part.js +4 -4
- 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 +164 -126
- package/dist/models/Staff.d.ts +5 -5
- package/dist/models/Staff.js +4 -4
- package/dist/models/index.d.ts +10 -10
- package/dist/models/index.js +10 -10
- package/dist/types/AccidentalDisplay.d.ts +6 -0
- package/dist/types/AccidentalDisplay.js +1 -0
- package/dist/types/Arpeggio.d.ts +2 -1
- package/dist/types/Arpeggio.js +1 -0
- package/dist/types/Articulation.d.ts +3 -1
- package/dist/types/Articulation.js +2 -0
- package/dist/types/Beam.d.ts +4 -0
- package/dist/types/Beam.js +1 -0
- package/dist/types/Duration.d.ts +5 -1
- package/dist/types/Duration.js +27 -14
- package/dist/types/Grace.d.ts +7 -0
- package/dist/types/Grace.js +1 -0
- package/dist/types/Hairpin.d.ts +2 -1
- package/dist/types/Language.d.ts +6 -0
- package/dist/types/Language.js +7 -0
- package/dist/types/Lyric.d.ts +2 -0
- package/dist/types/Ornament.d.ts +23 -1
- package/dist/types/Ornament.js +9 -0
- package/dist/types/Pedal.d.ts +3 -2
- package/dist/types/Placement.d.ts +7 -0
- package/dist/types/Placement.js +1 -0
- package/dist/types/Repeat.d.ts +1 -0
- package/dist/types/RestDisplay.d.ts +6 -0
- package/dist/types/RestDisplay.js +1 -0
- package/dist/types/Slur.d.ts +1 -0
- package/dist/types/Technical.d.ts +20 -0
- package/dist/types/Technical.js +5 -0
- package/dist/types/Tempo.d.ts +1 -1
- package/dist/types/Tie.d.ts +4 -0
- package/dist/types/Tie.js +1 -0
- package/dist/types/Tuplet.d.ts +6 -0
- package/dist/types/User.d.ts +13 -7
- package/dist/types/User.js +9 -7
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +8 -0
- package/package.json +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
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, HarmonicType, NoteheadShape, Ornament, OttavaType, StemDirection, calculateDurationValue, decomposeDuration, } from '../models/types.js';
|
|
4
4
|
/**
|
|
5
5
|
* MusicXML Parser
|
|
6
6
|
*
|
|
@@ -14,6 +14,7 @@ export class MusicXMLParser {
|
|
|
14
14
|
currentSymbol = 'normal';
|
|
15
15
|
instrumentPitchMap = new Map();
|
|
16
16
|
_domParser;
|
|
17
|
+
activeWedgeTypes = new Map();
|
|
17
18
|
constructor(domParserInstance) {
|
|
18
19
|
this._domParser =
|
|
19
20
|
domParserInstance || (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
|
|
@@ -81,7 +82,7 @@ export class MusicXMLParser {
|
|
|
81
82
|
if (!_parser)
|
|
82
83
|
throw new Error('No DOMParser available');
|
|
83
84
|
const containerDoc = _parser.parseFromString(containerXml, 'application/xml');
|
|
84
|
-
const rootfile =
|
|
85
|
+
const rootfile = new MusicXMLParser(_parser).querySelector(containerDoc, 'rootfile');
|
|
85
86
|
const fullPath = rootfile?.getAttribute('full-path');
|
|
86
87
|
if (!fullPath)
|
|
87
88
|
throw new Error('Invalid MXL: Could not find main score file path');
|
|
@@ -120,6 +121,7 @@ export class MusicXMLParser {
|
|
|
120
121
|
const title = this.parseTitle(doc);
|
|
121
122
|
const subtitle = this.parseSubtitle(doc);
|
|
122
123
|
const composer = this.parseComposer(doc);
|
|
124
|
+
const lyricist = this.parseLyricist(doc);
|
|
123
125
|
const copyright = this.parseCopyright(doc);
|
|
124
126
|
const partInfo = this.parsePartList(doc);
|
|
125
127
|
const parts = this.parseParts(doc, partInfo);
|
|
@@ -127,13 +129,15 @@ export class MusicXMLParser {
|
|
|
127
129
|
let globalTempoDuration = Duration.Quarter;
|
|
128
130
|
let globalTempoIsDotted = false;
|
|
129
131
|
let globalTempoText = '';
|
|
132
|
+
let globalTempoDotCount = 0;
|
|
130
133
|
if (parts.length > 0 && parts[0].staves.length > 0 && parts[0].staves[0].measures.length > 0) {
|
|
131
134
|
const firstMeasure = parts[0].staves[0].measures[0];
|
|
132
135
|
if (firstMeasure.tempo) {
|
|
133
136
|
globalBpm = firstMeasure.tempo.bpm;
|
|
134
137
|
globalTempoDuration = firstMeasure.tempo.duration;
|
|
135
|
-
globalTempoIsDotted = firstMeasure.tempo.isDotted;
|
|
136
138
|
globalTempoText = firstMeasure.tempo.text || '';
|
|
139
|
+
globalTempoDotCount = firstMeasure.tempo.dotCount || 0;
|
|
140
|
+
globalTempoIsDotted = globalTempoDotCount > 0;
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
const score = {
|
|
@@ -149,9 +153,10 @@ export class MusicXMLParser {
|
|
|
149
153
|
parts,
|
|
150
154
|
bpm: globalBpm,
|
|
151
155
|
tempoDuration: globalTempoDuration,
|
|
152
|
-
tempoIsDotted: globalTempoIsDotted,
|
|
153
156
|
tempoText: globalTempoText,
|
|
157
|
+
tempoDotCount: globalTempoDotCount || (globalTempoIsDotted ? 1 : 0),
|
|
154
158
|
copyright,
|
|
159
|
+
lyricist,
|
|
155
160
|
};
|
|
156
161
|
return this.postProcess(score);
|
|
157
162
|
}
|
|
@@ -210,7 +215,38 @@ export class MusicXMLParser {
|
|
|
210
215
|
if (creator.getAttribute('type') === 'composer')
|
|
211
216
|
return creator.textContent ?? 'Unknown';
|
|
212
217
|
}
|
|
213
|
-
|
|
218
|
+
// If no type="composer", but there is only one creator, assume it's the composer
|
|
219
|
+
if (creators.length === 1)
|
|
220
|
+
return creators[0].textContent ?? 'Unknown';
|
|
221
|
+
// Check credits for composer type
|
|
222
|
+
const credits = this.querySelectorAll(doc, 'credit');
|
|
223
|
+
for (const credit of Array.from(credits)) {
|
|
224
|
+
const type = this.querySelector(credit, 'credit-type');
|
|
225
|
+
if (type?.textContent?.toLowerCase() === 'composer') {
|
|
226
|
+
const words = this.querySelector(credit, 'credit-words');
|
|
227
|
+
if (words?.textContent)
|
|
228
|
+
return words.textContent;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return creators.length > 0 ? (creators[0].textContent ?? 'Unknown') : 'Unknown';
|
|
232
|
+
}
|
|
233
|
+
parseLyricist(doc) {
|
|
234
|
+
const creators = this.querySelectorAll(doc, 'identification creator');
|
|
235
|
+
for (const creator of Array.from(creators)) {
|
|
236
|
+
if (creator.getAttribute('type') === 'lyricist')
|
|
237
|
+
return creator.textContent ?? '';
|
|
238
|
+
}
|
|
239
|
+
// Check credits for lyricist type
|
|
240
|
+
const credits = this.querySelectorAll(doc, 'credit');
|
|
241
|
+
for (const credit of Array.from(credits)) {
|
|
242
|
+
const type = this.querySelector(credit, 'credit-type');
|
|
243
|
+
if (type?.textContent?.toLowerCase() === 'lyricist') {
|
|
244
|
+
const words = this.querySelector(credit, 'credit-words');
|
|
245
|
+
if (words?.textContent)
|
|
246
|
+
return words.textContent;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return '';
|
|
214
250
|
}
|
|
215
251
|
parsePartList(doc) {
|
|
216
252
|
const partInfo = new Map();
|
|
@@ -281,10 +317,16 @@ export class MusicXMLParser {
|
|
|
281
317
|
const staffClefs = Array(numStaves).fill(Clef.Treble);
|
|
282
318
|
const lastVoicePerStaff = new Map();
|
|
283
319
|
let divisions = 1;
|
|
320
|
+
let initialClefsSet = false;
|
|
284
321
|
for (const [mIdx, measureElement] of Array.from(measureElements).entries()) {
|
|
285
322
|
let measureTimeSignature;
|
|
286
323
|
let measureKeySignature;
|
|
324
|
+
let multiMeasureRestCount;
|
|
287
325
|
let isPickup = measureElement.getAttribute('implicit') === 'yes';
|
|
326
|
+
let currentSystemDistance;
|
|
327
|
+
let currentTopSystemDistance;
|
|
328
|
+
const currentStaffDistances = new Map();
|
|
329
|
+
const explicitClefs = Array(numStaves).fill(undefined);
|
|
288
330
|
const attributes = this.querySelector(measureElement, 'attributes');
|
|
289
331
|
if (attributes) {
|
|
290
332
|
const divNode = this.querySelector(attributes, 'divisions');
|
|
@@ -325,7 +367,9 @@ export class MusicXMLParser {
|
|
|
325
367
|
if (sign && number <= numStaves) {
|
|
326
368
|
const line = this.getNumber(clef, 'line') || 0;
|
|
327
369
|
const octaveChange = this.getNumber(clef, 'clef-octave-change') || 0;
|
|
328
|
-
|
|
370
|
+
const mapped = this.mapClef(sign, line, octaveChange);
|
|
371
|
+
staffClefs[number - 1] = mapped;
|
|
372
|
+
explicitClefs[number - 1] = mapped;
|
|
329
373
|
}
|
|
330
374
|
});
|
|
331
375
|
const staffDetails = this.querySelectorAll(attributes, 'staff-details');
|
|
@@ -335,10 +379,15 @@ export class MusicXMLParser {
|
|
|
335
379
|
if (lines !== null && number <= numStaves)
|
|
336
380
|
staves[number - 1].lineCount = lines;
|
|
337
381
|
});
|
|
382
|
+
const multipleRest = this.getNumber(attributes, 'measure-style multiple-rest');
|
|
383
|
+
if (multipleRest !== null && multipleRest > 1) {
|
|
384
|
+
multiMeasureRestCount = multipleRest;
|
|
385
|
+
}
|
|
338
386
|
}
|
|
339
387
|
let measureTempo = undefined;
|
|
340
388
|
let rehearsalMark = undefined;
|
|
341
389
|
let systemText = undefined;
|
|
390
|
+
const roadmapDirections = [];
|
|
342
391
|
let contextDynamic = undefined;
|
|
343
392
|
const { repeats, volta, barlineStyle } = this.parseBarlines(measureElement);
|
|
344
393
|
// Map of staffIndex -> Map of voiceId -> NoteSetJSON[]
|
|
@@ -347,10 +396,12 @@ export class MusicXMLParser {
|
|
|
347
396
|
staffVoices.set(i, new Map());
|
|
348
397
|
const children = this.getChildren(measureElement);
|
|
349
398
|
let pendingChord = undefined;
|
|
350
|
-
|
|
399
|
+
const currentNoteSetMap = new Map();
|
|
351
400
|
const beamGroupIdMap = new Map();
|
|
401
|
+
const beamGroupIdByVoice = new Map();
|
|
352
402
|
let globalBeamId = 1;
|
|
353
|
-
|
|
403
|
+
const currentHairpins = new Map();
|
|
404
|
+
const lastNoteOnStaff = new Map();
|
|
354
405
|
let currentOttava = undefined;
|
|
355
406
|
let currentPedal = undefined;
|
|
356
407
|
children.forEach((child) => {
|
|
@@ -364,27 +415,88 @@ export class MusicXMLParser {
|
|
|
364
415
|
if (metronome) {
|
|
365
416
|
const beatUnit = this.getText(metronome, 'beat-unit') || 'quarter';
|
|
366
417
|
const bpm = parseInt(this.getText(metronome, 'per-minute') || '120');
|
|
367
|
-
const
|
|
368
|
-
measureTempo = { bpm, duration: this.mapDuration(beatUnit),
|
|
418
|
+
const dotCount = this.querySelectorAll(metronome, 'beat-unit-dot').length;
|
|
419
|
+
measureTempo = { bpm, duration: this.mapDuration(beatUnit), dotCount };
|
|
369
420
|
}
|
|
370
421
|
const rehearsal = this.querySelector(child, 'rehearsal');
|
|
371
422
|
if (rehearsal)
|
|
372
423
|
rehearsalMark = rehearsal.textContent || undefined;
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
424
|
+
const wordsNodes = this.querySelectorAll(child, 'words');
|
|
425
|
+
wordsNodes.forEach((words) => {
|
|
426
|
+
const wordsText = (words.textContent || '').trim();
|
|
427
|
+
const normalizedWords = wordsText.toLowerCase().replace(/\s+/g, ' ');
|
|
428
|
+
let roadmapWord;
|
|
429
|
+
if (/\bd\.?\s*c\.?\b|\bdacapo\b/.test(normalizedWords)) {
|
|
430
|
+
roadmapWord = 'D.C.';
|
|
431
|
+
}
|
|
432
|
+
else if (/\bd\.?\s*s\.?\b|\bdal segno\b|\bdalsegno\b/.test(normalizedWords)) {
|
|
433
|
+
roadmapWord = 'D.S.';
|
|
434
|
+
}
|
|
435
|
+
else if (/\bto coda\b|\bal coda\b/.test(normalizedWords)) {
|
|
436
|
+
roadmapWord = 'To Coda';
|
|
437
|
+
}
|
|
438
|
+
else if (/\bfine\b/.test(normalizedWords)) {
|
|
439
|
+
roadmapWord = 'Fine';
|
|
440
|
+
}
|
|
441
|
+
if (roadmapWord && !roadmapDirections.includes(roadmapWord)) {
|
|
442
|
+
roadmapDirections.push(roadmapWord);
|
|
443
|
+
}
|
|
444
|
+
else if (measureTempo && !measureTempo.text) {
|
|
445
|
+
measureTempo.text = wordsText || undefined;
|
|
446
|
+
}
|
|
447
|
+
else if (!systemText) {
|
|
448
|
+
systemText = wordsText || undefined;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
if (this.querySelector(child, 'segno') && !roadmapDirections.includes('𝄋 Segno')) {
|
|
452
|
+
roadmapDirections.push('𝄋 Segno');
|
|
453
|
+
}
|
|
454
|
+
if (this.querySelector(child, 'coda') && !roadmapDirections.includes('𝄌 Coda')) {
|
|
455
|
+
roadmapDirections.push('𝄌 Coda');
|
|
456
|
+
}
|
|
457
|
+
const soundNode = this.querySelector(child, 'sound');
|
|
458
|
+
if (soundNode) {
|
|
459
|
+
if (soundNode.getAttribute('dacapo') === 'yes' && !roadmapDirections.includes('D.C.')) {
|
|
460
|
+
roadmapDirections.push('D.C.');
|
|
461
|
+
}
|
|
462
|
+
if (soundNode.getAttribute('dalsegno') === 'yes' &&
|
|
463
|
+
!roadmapDirections.includes('D.S.')) {
|
|
464
|
+
roadmapDirections.push('D.S.');
|
|
465
|
+
}
|
|
466
|
+
if (soundNode.getAttribute('tocoda') === 'yes' &&
|
|
467
|
+
!roadmapDirections.includes('To Coda')) {
|
|
468
|
+
roadmapDirections.push('To Coda');
|
|
469
|
+
}
|
|
470
|
+
if (soundNode.getAttribute('fine') === 'yes' && !roadmapDirections.includes('Fine')) {
|
|
471
|
+
roadmapDirections.push('Fine');
|
|
472
|
+
}
|
|
379
473
|
}
|
|
380
474
|
const wedge = this.querySelector(child, 'wedge');
|
|
381
475
|
if (wedge) {
|
|
382
476
|
const typeAttr = wedge.getAttribute('type');
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
477
|
+
const staffIdx = this.getNumber(child, 'staff') || 1;
|
|
478
|
+
if (typeAttr === 'crescendo' ||
|
|
479
|
+
typeAttr === 'diminuendo' ||
|
|
480
|
+
typeAttr === 'stop' ||
|
|
481
|
+
typeAttr === 'continue') {
|
|
482
|
+
if (typeAttr === 'crescendo') {
|
|
483
|
+
this.activeWedgeTypes.set(staffIdx, HairpinType.Crescendo);
|
|
484
|
+
}
|
|
485
|
+
else if (typeAttr === 'diminuendo') {
|
|
486
|
+
this.activeWedgeTypes.set(staffIdx, HairpinType.Decrescendo);
|
|
487
|
+
}
|
|
488
|
+
const type = this.activeWedgeTypes.get(staffIdx) || HairpinType.Crescendo;
|
|
489
|
+
const hp = {
|
|
490
|
+
type,
|
|
386
491
|
placement: typeAttr === 'stop' ? 'stop' : 'start',
|
|
387
492
|
};
|
|
493
|
+
// If it's a stop, and we just had a note on this staff, attach it immediately.
|
|
494
|
+
if (typeAttr === 'stop' && lastNoteOnStaff.has(staffIdx)) {
|
|
495
|
+
lastNoteOnStaff.get(staffIdx).hairpin = hp;
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
currentHairpins.set(staffIdx, hp);
|
|
499
|
+
}
|
|
388
500
|
}
|
|
389
501
|
}
|
|
390
502
|
const octShift = this.querySelector(child, 'octave-shift');
|
|
@@ -407,37 +519,118 @@ export class MusicXMLParser {
|
|
|
407
519
|
const pedalNode = this.querySelector(child, 'pedal');
|
|
408
520
|
if (pedalNode) {
|
|
409
521
|
const typeAttr = pedalNode.getAttribute('type');
|
|
410
|
-
|
|
411
|
-
|
|
522
|
+
const hasLine = pedalNode.getAttribute('line') === 'yes';
|
|
523
|
+
const hasSign = pedalNode.getAttribute('sign') === 'yes';
|
|
524
|
+
if (typeAttr === 'start' ||
|
|
525
|
+
typeAttr === 'stop' ||
|
|
526
|
+
typeAttr === 'change' ||
|
|
527
|
+
typeAttr === 'continue') {
|
|
528
|
+
let style = 'text';
|
|
529
|
+
if (hasLine && hasSign)
|
|
530
|
+
style = 'mixed';
|
|
531
|
+
else if (hasLine)
|
|
532
|
+
style = 'bracket';
|
|
533
|
+
currentPedal = {
|
|
534
|
+
type: 'sustain',
|
|
535
|
+
placement: typeAttr,
|
|
536
|
+
style,
|
|
537
|
+
};
|
|
412
538
|
}
|
|
413
539
|
}
|
|
414
540
|
const dynamics = this.querySelector(child, 'dynamics');
|
|
415
541
|
if (dynamics) {
|
|
416
542
|
const firstChild = this.getFirstElementChild(dynamics);
|
|
417
|
-
if (firstChild)
|
|
418
|
-
|
|
543
|
+
if (firstChild) {
|
|
544
|
+
const dyn = firstChild.nodeName.toLowerCase();
|
|
545
|
+
const staffIdx = this.getNumber(child, 'staff') || 1;
|
|
546
|
+
if (lastNoteOnStaff.has(staffIdx)) {
|
|
547
|
+
lastNoteOnStaff.get(staffIdx).dynamic = dyn;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
contextDynamic = dyn;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else if (child.nodeName === 'print') {
|
|
556
|
+
const newSystem = child.getAttribute('new-system') === 'yes';
|
|
557
|
+
const newPage = child.getAttribute('new-page') === 'yes';
|
|
558
|
+
if (mIdx > 0) {
|
|
559
|
+
if (newSystem || newPage) {
|
|
560
|
+
staves.forEach((s) => {
|
|
561
|
+
const prev = s.measures[mIdx - 1];
|
|
562
|
+
if (prev) {
|
|
563
|
+
if (newSystem)
|
|
564
|
+
prev.systemBreak = true;
|
|
565
|
+
if (newPage)
|
|
566
|
+
prev.pageBreak = true;
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const systemLayout = this.querySelector(child, 'system-layout');
|
|
572
|
+
if (systemLayout) {
|
|
573
|
+
const systemDistance = this.getNumber(systemLayout, 'system-distance');
|
|
574
|
+
if (systemDistance !== null)
|
|
575
|
+
currentSystemDistance = systemDistance;
|
|
576
|
+
const topSystemDistance = this.getNumber(systemLayout, 'top-system-distance');
|
|
577
|
+
if (topSystemDistance !== null)
|
|
578
|
+
currentTopSystemDistance = topSystemDistance;
|
|
419
579
|
}
|
|
580
|
+
const staffLayouts = this.querySelectorAll(child, 'staff-layout');
|
|
581
|
+
staffLayouts.forEach((sl) => {
|
|
582
|
+
const staffNum = parseInt(sl.getAttribute('number') || '1');
|
|
583
|
+
const staffDistance = this.getNumber(sl, 'staff-distance');
|
|
584
|
+
if (staffDistance !== null)
|
|
585
|
+
currentStaffDistances.set(staffNum, staffDistance);
|
|
586
|
+
});
|
|
420
587
|
}
|
|
421
588
|
else if (child.nodeName === 'note') {
|
|
422
589
|
const staffNum = parseInt(this.getText(child, 'staff') || '1');
|
|
423
590
|
const staffIdx = Math.min(Math.max(staffNum - 1, 0), numStaves - 1);
|
|
591
|
+
const voiceId = this.getText(child, 'voice') || '1';
|
|
424
592
|
const isChord = this.querySelector(child, 'chord') !== null;
|
|
425
|
-
const note = this.parseNote(child);
|
|
593
|
+
const note = this.parseNote(child, divisions);
|
|
426
594
|
if (note) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
595
|
+
lastNoteOnStaff.set(staffNum, note);
|
|
596
|
+
const beamElements = this.querySelectorAll(child, 'beam');
|
|
597
|
+
const beamLevels = {};
|
|
598
|
+
beamElements.forEach((beamEl) => {
|
|
599
|
+
const numberAttr = beamEl.getAttribute('number');
|
|
600
|
+
const level = numberAttr ? parseInt(numberAttr, 10) : 1;
|
|
601
|
+
if (!Number.isFinite(level) || level < 1)
|
|
602
|
+
return;
|
|
603
|
+
const beamTypeRaw = (beamEl.textContent || '').trim().toLowerCase();
|
|
604
|
+
if (beamTypeRaw === 'begin' ||
|
|
605
|
+
beamTypeRaw === 'continue' ||
|
|
606
|
+
beamTypeRaw === 'end' ||
|
|
607
|
+
beamTypeRaw === 'forward hook' ||
|
|
608
|
+
beamTypeRaw === 'backward hook') {
|
|
609
|
+
beamLevels[level] = beamTypeRaw;
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
if (Object.keys(beamLevels).length > 0) {
|
|
613
|
+
note.beamLevels = beamLevels;
|
|
614
|
+
}
|
|
615
|
+
const primaryBeamType = beamLevels[1];
|
|
616
|
+
if (primaryBeamType) {
|
|
617
|
+
if (primaryBeamType === 'begin') {
|
|
431
618
|
const newId = globalBeamId++;
|
|
432
619
|
beamGroupIdMap.set(staffIdx, newId);
|
|
620
|
+
beamGroupIdByVoice.set(voiceId, newId);
|
|
433
621
|
note.beamGroup = newId;
|
|
434
622
|
}
|
|
435
|
-
else if (
|
|
436
|
-
const currentId = beamGroupIdMap.get(staffIdx);
|
|
623
|
+
else if (primaryBeamType === 'continue' || primaryBeamType === 'end') {
|
|
624
|
+
const currentId = beamGroupIdMap.get(staffIdx) ?? beamGroupIdByVoice.get(voiceId);
|
|
437
625
|
if (currentId !== undefined) {
|
|
438
626
|
note.beamGroup = currentId;
|
|
439
|
-
|
|
627
|
+
beamGroupIdMap.set(staffIdx, currentId);
|
|
628
|
+
if (primaryBeamType === 'end') {
|
|
440
629
|
beamGroupIdMap.delete(staffIdx);
|
|
630
|
+
if (beamGroupIdByVoice.get(voiceId) === currentId) {
|
|
631
|
+
beamGroupIdByVoice.delete(voiceId);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
441
634
|
}
|
|
442
635
|
}
|
|
443
636
|
}
|
|
@@ -451,9 +644,9 @@ export class MusicXMLParser {
|
|
|
451
644
|
note.dynamic = contextDynamic;
|
|
452
645
|
contextDynamic = undefined;
|
|
453
646
|
}
|
|
454
|
-
if (
|
|
455
|
-
note.hairpin =
|
|
456
|
-
|
|
647
|
+
if (currentHairpins.has(staffNum)) {
|
|
648
|
+
note.hairpin = currentHairpins.get(staffNum);
|
|
649
|
+
currentHairpins.delete(staffNum);
|
|
457
650
|
}
|
|
458
651
|
if (currentOttava) {
|
|
459
652
|
note.ottava = currentOttava;
|
|
@@ -463,7 +656,6 @@ export class MusicXMLParser {
|
|
|
463
656
|
note.pedal = currentPedal;
|
|
464
657
|
currentPedal = undefined;
|
|
465
658
|
}
|
|
466
|
-
const voiceId = this.getText(child, 'voice') || '1';
|
|
467
659
|
lastVoicePerStaff.set(staffIdx, voiceId);
|
|
468
660
|
const voiceMap = staffVoices.get(staffIdx);
|
|
469
661
|
if (!voiceMap.has(voiceId))
|
|
@@ -514,12 +706,13 @@ export class MusicXMLParser {
|
|
|
514
706
|
const v1 = vMap0.get('1') || vMap0.get(Array.from(vMap0.keys())[0]);
|
|
515
707
|
if (v1) {
|
|
516
708
|
// Check duration of first voice in first staff
|
|
517
|
-
const expectedDivisions = this.currentBeats * (divisions * 4 / this.currentBeatType);
|
|
709
|
+
const expectedDivisions = this.currentBeats * ((divisions * 4) / this.currentBeatType);
|
|
518
710
|
let totalMeasureDivs = 0;
|
|
519
711
|
const notes = this.querySelectorAll(measureElement, 'note');
|
|
520
712
|
// Find first voice in first staff
|
|
521
713
|
for (const n of notes) {
|
|
522
|
-
if ((this.getText(n, 'staff') || '1') === '1' &&
|
|
714
|
+
if ((this.getText(n, 'staff') || '1') === '1' &&
|
|
715
|
+
(this.getText(n, 'voice') || '1') === (Array.from(vMap0.keys())[0] || '1')) {
|
|
523
716
|
if (this.querySelector(n, 'chord'))
|
|
524
717
|
continue;
|
|
525
718
|
totalMeasureDivs += this.getNumber(n, 'duration') || 0;
|
|
@@ -535,26 +728,37 @@ export class MusicXMLParser {
|
|
|
535
728
|
let voicesIndices = Array.from(vMap.keys()).sort();
|
|
536
729
|
if (voicesIndices.length === 0)
|
|
537
730
|
voicesIndices = ['1'];
|
|
538
|
-
const voices = voicesIndices.map(id => vMap.get(id) || []);
|
|
731
|
+
const voices = voicesIndices.map((id) => vMap.get(id) || []);
|
|
539
732
|
const measure = {
|
|
540
733
|
voices: voices,
|
|
541
734
|
timeSignature: measureTimeSignature,
|
|
542
735
|
keySignature: measureKeySignature,
|
|
543
736
|
isPickup: isPickup || undefined,
|
|
737
|
+
clef: explicitClefs[i],
|
|
544
738
|
tempo: i === 0 ? measureTempo : undefined,
|
|
545
739
|
repeats: repeats.length > 0 ? repeats : undefined,
|
|
546
|
-
volta,
|
|
740
|
+
volta: i === 0 ? volta : undefined,
|
|
547
741
|
rehearsalMark: i === 0 ? rehearsalMark : undefined,
|
|
548
742
|
systemText: i === 0 ? systemText : undefined,
|
|
743
|
+
multiMeasureRestCount,
|
|
744
|
+
roadmapDirections: i === 0 && roadmapDirections.length > 0 ? roadmapDirections : undefined,
|
|
549
745
|
barlineStyle,
|
|
746
|
+
systemDistance: i === 0 ? currentSystemDistance : undefined,
|
|
747
|
+
staffDistance: currentStaffDistances.get(i + 1),
|
|
748
|
+
topSystemDistance: i === 0 ? currentTopSystemDistance : undefined,
|
|
550
749
|
};
|
|
750
|
+
if (mIdx === 0 && !initialClefsSet) {
|
|
751
|
+
staves[i].clef = staffClefs[i];
|
|
752
|
+
if (i === numStaves - 1)
|
|
753
|
+
initialClefsSet = true;
|
|
754
|
+
}
|
|
551
755
|
// Padding with rests if not a pickup
|
|
552
756
|
if (!isPickup) {
|
|
553
757
|
const targetDur = (this.currentBeats * 4) / this.currentBeatType;
|
|
554
758
|
measure.voices = measure.voices.map((v) => {
|
|
555
759
|
const currentDur = v.reduce((sum, ns) => {
|
|
556
|
-
|
|
557
|
-
|
|
760
|
+
return (sum +
|
|
761
|
+
calculateDurationValue(ns.notes[0].duration, ns.notes[0].dotCount || (ns.notes[0].dotCount ? 1 : 0)));
|
|
558
762
|
}, 0);
|
|
559
763
|
if (currentDur < targetDur - 0.001) {
|
|
560
764
|
const gap = targetDur - currentDur;
|
|
@@ -563,7 +767,7 @@ export class MusicXMLParser {
|
|
|
563
767
|
{
|
|
564
768
|
duration: d.duration,
|
|
565
769
|
isRest: true,
|
|
566
|
-
|
|
770
|
+
dotCount: d.dotCount,
|
|
567
771
|
},
|
|
568
772
|
],
|
|
569
773
|
}));
|
|
@@ -573,19 +777,97 @@ export class MusicXMLParser {
|
|
|
573
777
|
});
|
|
574
778
|
}
|
|
575
779
|
staves[i].measures.push(measure);
|
|
576
|
-
staves[i].clef = staffClefs[i];
|
|
577
780
|
}
|
|
578
781
|
}
|
|
579
782
|
return staves;
|
|
580
783
|
}
|
|
581
|
-
parseNote(noteElement) {
|
|
784
|
+
parseNote(noteElement, divisions) {
|
|
582
785
|
const isRest = this.querySelector(noteElement, 'rest') !== null;
|
|
583
|
-
const
|
|
786
|
+
const restNode = this.querySelector(noteElement, 'rest');
|
|
787
|
+
const graceEl = this.querySelector(noteElement, 'grace');
|
|
788
|
+
const isGrace = graceEl !== null;
|
|
584
789
|
const type = this.getText(noteElement, 'type');
|
|
585
|
-
const
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
790
|
+
const typeEl = this.querySelector(noteElement, 'type');
|
|
791
|
+
const isCue = this.querySelector(noteElement, 'cue') !== null || typeEl?.getAttribute('size') === 'cue';
|
|
792
|
+
let duration = type ? this.mapDuration(type) : Duration.Quarter;
|
|
793
|
+
let dotCount = this.querySelectorAll(noteElement, 'dot').length;
|
|
794
|
+
if (!type && !isGrace) {
|
|
795
|
+
const numDuration = this.getNumber(noteElement, 'duration');
|
|
796
|
+
if (numDuration !== null && divisions > 0) {
|
|
797
|
+
const qBeats = numDuration / divisions;
|
|
798
|
+
if (qBeats >= 4) {
|
|
799
|
+
duration = Duration.Whole;
|
|
800
|
+
dotCount = 0;
|
|
801
|
+
}
|
|
802
|
+
else if (qBeats >= 3) {
|
|
803
|
+
duration = Duration.Half;
|
|
804
|
+
dotCount = 1;
|
|
805
|
+
}
|
|
806
|
+
else if (qBeats >= 2) {
|
|
807
|
+
duration = Duration.Half;
|
|
808
|
+
dotCount = 0;
|
|
809
|
+
}
|
|
810
|
+
else if (qBeats >= 1.5) {
|
|
811
|
+
duration = Duration.Quarter;
|
|
812
|
+
dotCount = 1;
|
|
813
|
+
}
|
|
814
|
+
else if (qBeats >= 1) {
|
|
815
|
+
duration = Duration.Quarter;
|
|
816
|
+
dotCount = 0;
|
|
817
|
+
}
|
|
818
|
+
else if (qBeats >= 0.75) {
|
|
819
|
+
duration = Duration.Eighth;
|
|
820
|
+
dotCount = 1;
|
|
821
|
+
}
|
|
822
|
+
else if (qBeats >= 0.5) {
|
|
823
|
+
duration = Duration.Eighth;
|
|
824
|
+
dotCount = 0;
|
|
825
|
+
}
|
|
826
|
+
else if (qBeats >= 0.25) {
|
|
827
|
+
duration = Duration.Sixteenth;
|
|
828
|
+
dotCount = 0;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (isRest) {
|
|
833
|
+
const displayStepText = this.getText(restNode || noteElement, 'display-step');
|
|
834
|
+
const displayOctave = this.getNumber(restNode || noteElement, 'display-octave') ?? undefined;
|
|
835
|
+
const placement = this.getPlacement(noteElement);
|
|
836
|
+
const restDisplay = displayStepText ||
|
|
837
|
+
displayOctave !== undefined ||
|
|
838
|
+
placement?.defaultY !== undefined ||
|
|
839
|
+
placement?.relativeY !== undefined
|
|
840
|
+
? {
|
|
841
|
+
displayStep: displayStepText ? this.stepToNumber(displayStepText) : undefined,
|
|
842
|
+
displayOctave,
|
|
843
|
+
defaultY: placement?.defaultY,
|
|
844
|
+
relativeY: placement?.relativeY,
|
|
845
|
+
}
|
|
846
|
+
: undefined;
|
|
847
|
+
return { duration, isRest: true, dotCount, restDisplay, placement };
|
|
848
|
+
}
|
|
849
|
+
let grace;
|
|
850
|
+
if (graceEl) {
|
|
851
|
+
const slashAttr = graceEl.getAttribute('slash');
|
|
852
|
+
const stealTimePreviousAttr = graceEl.getAttribute('steal-time-previous');
|
|
853
|
+
const stealTimeFollowingAttr = graceEl.getAttribute('steal-time-following');
|
|
854
|
+
const makeTimeAttr = graceEl.getAttribute('make-time');
|
|
855
|
+
const stealTimePrevious = stealTimePreviousAttr
|
|
856
|
+
? parseInt(stealTimePreviousAttr, 10)
|
|
857
|
+
: undefined;
|
|
858
|
+
const stealTimeFollowing = stealTimeFollowingAttr
|
|
859
|
+
? parseInt(stealTimeFollowingAttr, 10)
|
|
860
|
+
: undefined;
|
|
861
|
+
const makeTime = makeTimeAttr ? parseInt(makeTimeAttr, 10) : undefined;
|
|
862
|
+
const cueSize = typeEl?.getAttribute('size') === 'cue';
|
|
863
|
+
grace = {
|
|
864
|
+
slash: slashAttr === 'yes',
|
|
865
|
+
stealTimePrevious: Number.isFinite(stealTimePrevious) ? stealTimePrevious : undefined,
|
|
866
|
+
stealTimeFollowing: Number.isFinite(stealTimeFollowing) ? stealTimeFollowing : undefined,
|
|
867
|
+
makeTime: Number.isFinite(makeTime) ? makeTime : undefined,
|
|
868
|
+
cueSize: cueSize || undefined,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
589
871
|
let step = 'C', octave = 4, alter = 0;
|
|
590
872
|
const unpitched = this.querySelector(noteElement, 'unpitched');
|
|
591
873
|
if (unpitched) {
|
|
@@ -608,7 +890,16 @@ export class MusicXMLParser {
|
|
|
608
890
|
midiNumber = this.instrumentPitchMap.get(instId);
|
|
609
891
|
}
|
|
610
892
|
let accidental = this.mapAccidental(alter);
|
|
611
|
-
const
|
|
893
|
+
const accidentalNode = this.querySelector(noteElement, 'accidental');
|
|
894
|
+
const accidentalEl = accidentalNode?.textContent?.trim() || '';
|
|
895
|
+
const accidentalDisplay = accidentalNode
|
|
896
|
+
? {
|
|
897
|
+
cautionary: accidentalNode.getAttribute('cautionary') === 'yes' || undefined,
|
|
898
|
+
editorial: accidentalNode.getAttribute('editorial') === 'yes' || undefined,
|
|
899
|
+
parenthesized: accidentalNode.getAttribute('parentheses') === 'yes' || undefined,
|
|
900
|
+
bracketed: accidentalNode.getAttribute('bracket') === 'yes' || undefined,
|
|
901
|
+
}
|
|
902
|
+
: undefined;
|
|
612
903
|
if (accidentalEl) {
|
|
613
904
|
if (accidentalEl === 'sharp')
|
|
614
905
|
accidental = Accidental.Sharp;
|
|
@@ -636,7 +927,7 @@ export class MusicXMLParser {
|
|
|
636
927
|
else if (stem === 'down')
|
|
637
928
|
stemDirection = StemDirection.Down;
|
|
638
929
|
const notations = this.querySelector(noteElement, 'notations');
|
|
639
|
-
// Tuplet ratio
|
|
930
|
+
// Tuplet ratio and engraving metadata
|
|
640
931
|
let tuplet = undefined;
|
|
641
932
|
const timeMod = this.querySelector(noteElement, 'time-modification');
|
|
642
933
|
if (timeMod) {
|
|
@@ -652,22 +943,81 @@ export class MusicXMLParser {
|
|
|
652
943
|
tuplet.type = 'start';
|
|
653
944
|
else if (type === 'stop')
|
|
654
945
|
tuplet.type = 'stop';
|
|
946
|
+
const numberAttr = tupletEl.getAttribute('number');
|
|
947
|
+
const number = numberAttr ? parseInt(numberAttr, 10) : undefined;
|
|
948
|
+
if (Number.isFinite(number)) {
|
|
949
|
+
tuplet.number = number;
|
|
950
|
+
}
|
|
951
|
+
const placement = tupletEl.getAttribute('placement');
|
|
952
|
+
if (placement === 'above' || placement === 'below') {
|
|
953
|
+
tuplet.placement = placement;
|
|
954
|
+
}
|
|
955
|
+
const bracket = tupletEl.getAttribute('bracket');
|
|
956
|
+
if (bracket === 'yes' || bracket === 'no') {
|
|
957
|
+
tuplet.bracket = bracket;
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
tuplet.bracket = 'auto';
|
|
961
|
+
}
|
|
962
|
+
const showNumber = tupletEl.getAttribute('show-number');
|
|
963
|
+
if (showNumber === 'none' || showNumber === 'actual' || showNumber === 'both') {
|
|
964
|
+
tuplet.showNumber = showNumber;
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
tuplet.showNumber = 'actual';
|
|
968
|
+
}
|
|
969
|
+
const actualDisplay = this.getNumber(tupletEl, 'tuplet-actual tuplet-number');
|
|
970
|
+
if (actualDisplay !== null) {
|
|
971
|
+
tuplet.actualDisplay = actualDisplay;
|
|
972
|
+
}
|
|
973
|
+
const normalDisplay = this.getNumber(tupletEl, 'tuplet-normal tuplet-number');
|
|
974
|
+
if (normalDisplay !== null) {
|
|
975
|
+
tuplet.normalDisplay = normalDisplay;
|
|
976
|
+
}
|
|
655
977
|
}
|
|
656
978
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
979
|
+
const slurs = [];
|
|
980
|
+
if (notations) {
|
|
981
|
+
this.querySelectorAll(notations, 'slur').forEach((slurEl) => {
|
|
982
|
+
const type = slurEl.getAttribute('type');
|
|
983
|
+
if (type !== 'start' && type !== 'stop')
|
|
984
|
+
return;
|
|
663
985
|
const orientation = slurEl.getAttribute('orientation');
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
986
|
+
const direction = orientation === 'over' ? 'up' : orientation === 'under' ? 'down' : undefined;
|
|
987
|
+
const numberAttr = slurEl.getAttribute('number');
|
|
988
|
+
const number = numberAttr ? parseInt(numberAttr, 10) : undefined;
|
|
989
|
+
slurs.push({ placement: type, direction, number: Number.isFinite(number) ? number : 1 });
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
const slur = slurs.find((s) => s.placement === 'start') || slurs[0];
|
|
993
|
+
const tieSpans = [];
|
|
994
|
+
this.querySelectorAll(noteElement, 'tie').forEach((tieEl) => {
|
|
995
|
+
const type = tieEl.getAttribute('type');
|
|
996
|
+
if (type === 'start' || type === 'stop' || type === 'continue') {
|
|
997
|
+
tieSpans.push({ placement: type, number: 1 });
|
|
668
998
|
}
|
|
999
|
+
});
|
|
1000
|
+
if (notations) {
|
|
1001
|
+
this.querySelectorAll(notations, 'tied').forEach((tiedEl) => {
|
|
1002
|
+
const type = tiedEl.getAttribute('type');
|
|
1003
|
+
if (type !== 'start' && type !== 'stop' && type !== 'continue')
|
|
1004
|
+
return;
|
|
1005
|
+
const numberAttr = tiedEl.getAttribute('number');
|
|
1006
|
+
const number = numberAttr ? parseInt(numberAttr, 10) : 1;
|
|
1007
|
+
tieSpans.push({ placement: type, number: Number.isFinite(number) ? number : 1 });
|
|
1008
|
+
});
|
|
669
1009
|
}
|
|
670
|
-
const
|
|
1010
|
+
const dedupedTieSpans = [];
|
|
1011
|
+
const tieSpanKeys = new Set();
|
|
1012
|
+
tieSpans.forEach((span) => {
|
|
1013
|
+
const key = `${span.placement}:${span.number || 1}`;
|
|
1014
|
+
if (!tieSpanKeys.has(key)) {
|
|
1015
|
+
tieSpanKeys.add(key);
|
|
1016
|
+
dedupedTieSpans.push(span);
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
const tie = dedupedTieSpans.some((span) => span.placement === 'start' || span.placement === 'continue') ||
|
|
1020
|
+
undefined;
|
|
671
1021
|
const articulations = [];
|
|
672
1022
|
const articEl = notations ? this.querySelector(notations, 'articulations') : null;
|
|
673
1023
|
if (articEl) {
|
|
@@ -690,6 +1040,7 @@ export class MusicXMLParser {
|
|
|
690
1040
|
articulations.push(Articulation.Fermata);
|
|
691
1041
|
let arpeggio = undefined;
|
|
692
1042
|
const arpeggiateEl = notations ? this.querySelector(notations, 'arpeggiate') : null;
|
|
1043
|
+
const nonArpeggiateEl = notations ? this.querySelector(notations, 'non-arpeggiate') : null;
|
|
693
1044
|
if (arpeggiateEl) {
|
|
694
1045
|
const dir = arpeggiateEl.getAttribute('direction');
|
|
695
1046
|
if (dir === 'up')
|
|
@@ -699,8 +1050,13 @@ export class MusicXMLParser {
|
|
|
699
1050
|
else
|
|
700
1051
|
arpeggio = Arpeggio.Normal;
|
|
701
1052
|
}
|
|
1053
|
+
else if (nonArpeggiateEl) {
|
|
1054
|
+
arpeggio = Arpeggio.NonArpeggiate;
|
|
1055
|
+
}
|
|
702
1056
|
const ornaments = notations ? this.querySelector(notations, 'ornaments') : null;
|
|
703
1057
|
let ornament = undefined;
|
|
1058
|
+
let ornamentDetails = undefined;
|
|
1059
|
+
let vibrato = undefined;
|
|
704
1060
|
if (ornaments) {
|
|
705
1061
|
if (this.querySelector(ornaments, 'trill-mark'))
|
|
706
1062
|
ornament = Ornament.Trill;
|
|
@@ -708,12 +1064,85 @@ export class MusicXMLParser {
|
|
|
708
1064
|
ornament = Ornament.Mordent;
|
|
709
1065
|
else if (this.querySelector(ornaments, 'inverted-mordent'))
|
|
710
1066
|
ornament = Ornament.InvertedMordent;
|
|
1067
|
+
else if (this.querySelector(ornaments, 'delayed-turn'))
|
|
1068
|
+
ornament = Ornament.DelayedTurn;
|
|
711
1069
|
else if (this.querySelector(ornaments, 'turn'))
|
|
712
1070
|
ornament = Ornament.Turn;
|
|
713
1071
|
else if (this.querySelector(ornaments, 'inverted-turn'))
|
|
714
1072
|
ornament = Ornament.InvertedTurn;
|
|
715
|
-
else if (this.querySelector(ornaments, 'tremolo'))
|
|
716
|
-
|
|
1073
|
+
else if (this.querySelector(ornaments, 'tremolo')) {
|
|
1074
|
+
const tremoloEl = this.querySelector(ornaments, 'tremolo');
|
|
1075
|
+
const type = tremoloEl?.getAttribute('type') || 'single';
|
|
1076
|
+
const rawStrokes = parseInt((tremoloEl?.textContent || '3').trim(), 10);
|
|
1077
|
+
const strokes = Number.isFinite(rawStrokes) ? Math.max(1, Math.min(4, rawStrokes)) : 3;
|
|
1078
|
+
if (type === 'start' || type === 'stop' || type === 'continue') {
|
|
1079
|
+
const measuredMap = {
|
|
1080
|
+
1: Ornament.TremoloMeasured1,
|
|
1081
|
+
2: Ornament.TremoloMeasured2,
|
|
1082
|
+
3: Ornament.TremoloMeasured3,
|
|
1083
|
+
4: Ornament.TremoloMeasured4,
|
|
1084
|
+
};
|
|
1085
|
+
ornament = measuredMap[strokes];
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
const unmeasuredMap = {
|
|
1089
|
+
1: Ornament.Tremolo1,
|
|
1090
|
+
2: Ornament.Tremolo2,
|
|
1091
|
+
3: Ornament.Tremolo3,
|
|
1092
|
+
4: Ornament.Tremolo4,
|
|
1093
|
+
};
|
|
1094
|
+
ornament = unmeasuredMap[strokes] || Ornament.Tremolo;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const wavyLineEl = this.querySelector(ornaments, 'wavy-line');
|
|
1098
|
+
if (wavyLineEl) {
|
|
1099
|
+
const typeAttr = wavyLineEl.getAttribute('type');
|
|
1100
|
+
const type = typeAttr === 'start' || typeAttr === 'stop' || typeAttr === 'continue'
|
|
1101
|
+
? typeAttr
|
|
1102
|
+
: undefined;
|
|
1103
|
+
const numberAttr = wavyLineEl.getAttribute('number');
|
|
1104
|
+
const number = numberAttr ? parseInt(numberAttr, 10) : undefined;
|
|
1105
|
+
if (type) {
|
|
1106
|
+
ornamentDetails = {
|
|
1107
|
+
...(ornamentDetails || {}),
|
|
1108
|
+
wavyLine: {
|
|
1109
|
+
type,
|
|
1110
|
+
number: Number.isFinite(number) ? number : undefined,
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
const accidentalMarks = [];
|
|
1116
|
+
this.querySelectorAll(ornaments, 'accidental-mark').forEach((markEl) => {
|
|
1117
|
+
const text = (markEl.textContent || '').trim();
|
|
1118
|
+
let accidental;
|
|
1119
|
+
if (text === 'sharp')
|
|
1120
|
+
accidental = Accidental.Sharp;
|
|
1121
|
+
else if (text === 'flat')
|
|
1122
|
+
accidental = Accidental.Flat;
|
|
1123
|
+
else if (text === 'natural')
|
|
1124
|
+
accidental = Accidental.Natural;
|
|
1125
|
+
else if (text === 'double-sharp')
|
|
1126
|
+
accidental = Accidental.DoubleSharp;
|
|
1127
|
+
else if (text === 'flat-flat')
|
|
1128
|
+
accidental = Accidental.DoubleFlat;
|
|
1129
|
+
if (!accidental)
|
|
1130
|
+
return;
|
|
1131
|
+
const placementAttr = markEl.getAttribute('placement');
|
|
1132
|
+
const placement = placementAttr === 'above' || placementAttr === 'below' ? placementAttr : undefined;
|
|
1133
|
+
accidentalMarks.push({ accidental, placement });
|
|
1134
|
+
});
|
|
1135
|
+
if (this.querySelector(ornaments, 'vibrato')) {
|
|
1136
|
+
const vibEl = this.querySelector(ornaments, 'vibrato');
|
|
1137
|
+
const type = vibEl?.getAttribute('type');
|
|
1138
|
+
vibrato = type === 'start' || type === 'stop' ? type : 'single';
|
|
1139
|
+
}
|
|
1140
|
+
if (accidentalMarks.length > 0) {
|
|
1141
|
+
ornamentDetails = {
|
|
1142
|
+
...(ornamentDetails || {}),
|
|
1143
|
+
accidentalMarks,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
717
1146
|
}
|
|
718
1147
|
const technical = notations ? this.querySelector(notations, 'technical') : null;
|
|
719
1148
|
let bowing = undefined;
|
|
@@ -724,6 +1153,8 @@ export class MusicXMLParser {
|
|
|
724
1153
|
let palmMute = undefined;
|
|
725
1154
|
let hammerOn = undefined;
|
|
726
1155
|
let pullOff = undefined;
|
|
1156
|
+
let harmonic = undefined;
|
|
1157
|
+
let bend = undefined;
|
|
727
1158
|
if (technical) {
|
|
728
1159
|
if (this.querySelector(technical, 'up-bow'))
|
|
729
1160
|
bowing = Bowing.UpBow;
|
|
@@ -749,9 +1180,23 @@ export class MusicXMLParser {
|
|
|
749
1180
|
const pullOffEl = this.querySelector(technical, 'pull-off');
|
|
750
1181
|
if (pullOffEl)
|
|
751
1182
|
pullOff = pullOffEl.getAttribute('type') || 'start';
|
|
1183
|
+
const harmonicEl = this.querySelector(technical, 'harmonic');
|
|
1184
|
+
if (harmonicEl) {
|
|
1185
|
+
const natural = this.querySelector(harmonicEl, 'natural');
|
|
1186
|
+
harmonic = { type: natural ? HarmonicType.Natural : HarmonicType.Artificial };
|
|
1187
|
+
}
|
|
1188
|
+
const bendEl = this.querySelector(technical, 'bend');
|
|
1189
|
+
if (bendEl) {
|
|
1190
|
+
const alterEl = this.querySelector(bendEl, 'bend-alter');
|
|
1191
|
+
const alter = alterEl ? parseFloat(alterEl.textContent || '0') : 0;
|
|
1192
|
+
const release = this.querySelector(technical, 'release');
|
|
1193
|
+
bend = { alter, release: !!release };
|
|
1194
|
+
}
|
|
752
1195
|
}
|
|
753
1196
|
let glissando = undefined;
|
|
754
|
-
const glissEl = notations
|
|
1197
|
+
const glissEl = notations
|
|
1198
|
+
? this.querySelector(notations, 'glissando') || this.querySelector(notations, 'slide')
|
|
1199
|
+
: null;
|
|
755
1200
|
if (glissEl) {
|
|
756
1201
|
const type = glissEl.getAttribute('type');
|
|
757
1202
|
if (type === 'start' || type === 'stop') {
|
|
@@ -775,24 +1220,31 @@ export class MusicXMLParser {
|
|
|
775
1220
|
text,
|
|
776
1221
|
syllabic: syllabic || undefined,
|
|
777
1222
|
isExtension: extend || undefined,
|
|
1223
|
+
placement: this.getPlacement(lyricEl),
|
|
778
1224
|
};
|
|
779
1225
|
}
|
|
780
1226
|
});
|
|
781
1227
|
return {
|
|
782
1228
|
duration,
|
|
783
1229
|
pitch: { midiNumber, step: this.stepToNumber(step), alter, octave },
|
|
784
|
-
|
|
1230
|
+
dotCount,
|
|
785
1231
|
accidental,
|
|
1232
|
+
accidentalDisplay,
|
|
786
1233
|
slur,
|
|
787
1234
|
tie,
|
|
1235
|
+
tieSpans: dedupedTieSpans.length > 0 ? dedupedTieSpans : undefined,
|
|
1236
|
+
slurs: slurs.length > 0 ? slurs : undefined,
|
|
788
1237
|
articulations: articulations.length > 0 ? articulations : undefined,
|
|
789
1238
|
dynamic,
|
|
790
1239
|
arpeggio,
|
|
791
1240
|
lyrics: lyrics.length > 0 ? lyrics : undefined,
|
|
792
1241
|
notehead,
|
|
793
1242
|
isGrace: isGrace || undefined,
|
|
1243
|
+
isCue: isCue || undefined,
|
|
1244
|
+
grace,
|
|
794
1245
|
tuplet,
|
|
795
1246
|
ornament,
|
|
1247
|
+
ornamentDetails,
|
|
796
1248
|
bowing,
|
|
797
1249
|
fingering,
|
|
798
1250
|
stemDirection,
|
|
@@ -803,6 +1255,10 @@ export class MusicXMLParser {
|
|
|
803
1255
|
palmMute,
|
|
804
1256
|
hammerOn,
|
|
805
1257
|
pullOff,
|
|
1258
|
+
harmonic,
|
|
1259
|
+
bend,
|
|
1260
|
+
vibrato,
|
|
1261
|
+
placement: this.getPlacement(noteElement),
|
|
806
1262
|
};
|
|
807
1263
|
}
|
|
808
1264
|
parseHarmony(harmonyElement) {
|
|
@@ -965,18 +1421,21 @@ export class MusicXMLParser {
|
|
|
965
1421
|
const ending = this.querySelector(barline, 'ending');
|
|
966
1422
|
if (ending) {
|
|
967
1423
|
const type = ending.getAttribute('type');
|
|
968
|
-
const numberAttr = ending.getAttribute('number')
|
|
1424
|
+
const numberAttr = ending.getAttribute('number');
|
|
969
1425
|
// Handle "1,2" or "1 2"
|
|
970
1426
|
const numbers = numberAttr
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1427
|
+
? numberAttr
|
|
1428
|
+
.split(/[,\s]+/)
|
|
1429
|
+
.map((n) => parseInt(n))
|
|
1430
|
+
.filter((n) => !isNaN(n))
|
|
1431
|
+
: [];
|
|
1432
|
+
const placement = ending.getAttribute('placement');
|
|
974
1433
|
if (type === 'start')
|
|
975
|
-
volta = { type: 'start', numbers };
|
|
1434
|
+
volta = { type: 'start', numbers, placement: placement || undefined };
|
|
976
1435
|
else if (type === 'stop')
|
|
977
|
-
volta = { type: 'stop', numbers };
|
|
1436
|
+
volta = { type: 'stop', numbers, placement: placement || undefined };
|
|
978
1437
|
else if (type === 'discontinue')
|
|
979
|
-
volta = { type: 'both', numbers };
|
|
1438
|
+
volta = { type: 'both', numbers, placement: placement || undefined };
|
|
980
1439
|
}
|
|
981
1440
|
});
|
|
982
1441
|
return { repeats, volta, barlineStyle };
|
|
@@ -1011,8 +1470,17 @@ export class MusicXMLParser {
|
|
|
1011
1470
|
return;
|
|
1012
1471
|
const step = stepNames[note.pitch.step];
|
|
1013
1472
|
const alter = note.pitch.alter ?? 0;
|
|
1473
|
+
const hasIncomingTie = (note.tieSpans || []).some((span) => span.placement === 'stop' || span.placement === 'continue');
|
|
1474
|
+
const isCourtesyAccidental = !!note.accidentalDisplay?.cautionary ||
|
|
1475
|
+
!!note.accidentalDisplay?.editorial ||
|
|
1476
|
+
!!note.accidentalDisplay?.parenthesized;
|
|
1477
|
+
if (hasIncomingTie && !isCourtesyAccidental) {
|
|
1478
|
+
note.accidental = undefined;
|
|
1479
|
+
state[step] = alter;
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1014
1482
|
if (note.accidental) {
|
|
1015
|
-
if (state[step] === alter)
|
|
1483
|
+
if (state[step] === alter && !isCourtesyAccidental)
|
|
1016
1484
|
note.accidental = undefined;
|
|
1017
1485
|
else
|
|
1018
1486
|
state[step] = alter;
|
|
@@ -1090,4 +1558,25 @@ export class MusicXMLParser {
|
|
|
1090
1558
|
const text = this.getText(element, tagName);
|
|
1091
1559
|
return text !== null ? parseFloat(text) : null;
|
|
1092
1560
|
}
|
|
1561
|
+
getPlacement(element) {
|
|
1562
|
+
const defaultX = element.getAttribute('default-x');
|
|
1563
|
+
const defaultY = element.getAttribute('default-y');
|
|
1564
|
+
const relativeX = element.getAttribute('relative-x');
|
|
1565
|
+
const relativeY = element.getAttribute('relative-y');
|
|
1566
|
+
const placement = element.getAttribute('placement');
|
|
1567
|
+
if (defaultX === null &&
|
|
1568
|
+
defaultY === null &&
|
|
1569
|
+
relativeX === null &&
|
|
1570
|
+
relativeY === null &&
|
|
1571
|
+
placement === null) {
|
|
1572
|
+
return undefined;
|
|
1573
|
+
}
|
|
1574
|
+
return {
|
|
1575
|
+
defaultX: defaultX !== null ? parseFloat(defaultX) : undefined,
|
|
1576
|
+
defaultY: defaultY !== null ? parseFloat(defaultY) : undefined,
|
|
1577
|
+
relativeX: relativeX !== null ? parseFloat(relativeX) : undefined,
|
|
1578
|
+
relativeY: relativeY !== null ? parseFloat(relativeY) : undefined,
|
|
1579
|
+
aboveBelow: placement === 'above' || placement === 'below' ? placement : undefined,
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1093
1582
|
}
|