@scorelabs/core 1.0.11 → 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/importers/MusicXMLParser.d.ts +2 -0
- package/dist/importers/MusicXMLParser.js +518 -71
- package/dist/models/Measure.d.ts +19 -2
- package/dist/models/Measure.js +79 -27
- package/dist/models/Note.d.ts +35 -5
- package/dist/models/Note.js +97 -42
- package/dist/models/NoteSet.d.ts +11 -2
- package/dist/models/NoteSet.js +33 -2
- package/dist/models/Part.d.ts +1 -1
- package/dist/models/Part.js +2 -2
- package/dist/models/Score.d.ts +5 -5
- package/dist/models/Score.js +144 -121
- package/dist/models/Staff.d.ts +1 -1
- package/dist/models/Staff.js +2 -2
- 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 +1 -1
- package/dist/types/Duration.js +14 -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 +0 -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,7 +1,6 @@
|
|
|
1
1
|
import JSZip from 'jszip';
|
|
2
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.
|
|
3
|
+
import { Accidental, Arpeggio, Articulation, BarlineStyle, Bowing, Clef, Duration, GlissandoType, HairpinType, HarmonicType, NoteheadShape, Ornament, OttavaType, StemDirection, calculateDurationValue, decomposeDuration, } from '../models/types.js';
|
|
5
4
|
/**
|
|
6
5
|
* MusicXML Parser
|
|
7
6
|
*
|
|
@@ -15,6 +14,7 @@ export class MusicXMLParser {
|
|
|
15
14
|
currentSymbol = 'normal';
|
|
16
15
|
instrumentPitchMap = new Map();
|
|
17
16
|
_domParser;
|
|
17
|
+
activeWedgeTypes = new Map();
|
|
18
18
|
constructor(domParserInstance) {
|
|
19
19
|
this._domParser =
|
|
20
20
|
domParserInstance || (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
|
|
@@ -135,9 +135,9 @@ export class MusicXMLParser {
|
|
|
135
135
|
if (firstMeasure.tempo) {
|
|
136
136
|
globalBpm = firstMeasure.tempo.bpm;
|
|
137
137
|
globalTempoDuration = firstMeasure.tempo.duration;
|
|
138
|
-
globalTempoIsDotted = firstMeasure.tempo.isDotted;
|
|
139
138
|
globalTempoText = firstMeasure.tempo.text || '';
|
|
140
139
|
globalTempoDotCount = firstMeasure.tempo.dotCount || 0;
|
|
140
|
+
globalTempoIsDotted = globalTempoDotCount > 0;
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
const score = {
|
|
@@ -153,9 +153,8 @@ export class MusicXMLParser {
|
|
|
153
153
|
parts,
|
|
154
154
|
bpm: globalBpm,
|
|
155
155
|
tempoDuration: globalTempoDuration,
|
|
156
|
-
tempoIsDotted: globalTempoIsDotted,
|
|
157
156
|
tempoText: globalTempoText,
|
|
158
|
-
tempoDotCount: globalTempoDotCount,
|
|
157
|
+
tempoDotCount: globalTempoDotCount || (globalTempoIsDotted ? 1 : 0),
|
|
159
158
|
copyright,
|
|
160
159
|
lyricist,
|
|
161
160
|
};
|
|
@@ -218,7 +217,7 @@ export class MusicXMLParser {
|
|
|
218
217
|
}
|
|
219
218
|
// If no type="composer", but there is only one creator, assume it's the composer
|
|
220
219
|
if (creators.length === 1)
|
|
221
|
-
return
|
|
220
|
+
return creators[0].textContent ?? 'Unknown';
|
|
222
221
|
// Check credits for composer type
|
|
223
222
|
const credits = this.querySelectorAll(doc, 'credit');
|
|
224
223
|
for (const credit of Array.from(credits)) {
|
|
@@ -229,7 +228,7 @@ export class MusicXMLParser {
|
|
|
229
228
|
return words.textContent;
|
|
230
229
|
}
|
|
231
230
|
}
|
|
232
|
-
return creators.length > 0 ? creators[0].textContent ?? 'Unknown' : 'Unknown';
|
|
231
|
+
return creators.length > 0 ? (creators[0].textContent ?? 'Unknown') : 'Unknown';
|
|
233
232
|
}
|
|
234
233
|
parseLyricist(doc) {
|
|
235
234
|
const creators = this.querySelectorAll(doc, 'identification creator');
|
|
@@ -318,10 +317,16 @@ export class MusicXMLParser {
|
|
|
318
317
|
const staffClefs = Array(numStaves).fill(Clef.Treble);
|
|
319
318
|
const lastVoicePerStaff = new Map();
|
|
320
319
|
let divisions = 1;
|
|
320
|
+
let initialClefsSet = false;
|
|
321
321
|
for (const [mIdx, measureElement] of Array.from(measureElements).entries()) {
|
|
322
322
|
let measureTimeSignature;
|
|
323
323
|
let measureKeySignature;
|
|
324
|
+
let multiMeasureRestCount;
|
|
324
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);
|
|
325
330
|
const attributes = this.querySelector(measureElement, 'attributes');
|
|
326
331
|
if (attributes) {
|
|
327
332
|
const divNode = this.querySelector(attributes, 'divisions');
|
|
@@ -362,7 +367,9 @@ export class MusicXMLParser {
|
|
|
362
367
|
if (sign && number <= numStaves) {
|
|
363
368
|
const line = this.getNumber(clef, 'line') || 0;
|
|
364
369
|
const octaveChange = this.getNumber(clef, 'clef-octave-change') || 0;
|
|
365
|
-
|
|
370
|
+
const mapped = this.mapClef(sign, line, octaveChange);
|
|
371
|
+
staffClefs[number - 1] = mapped;
|
|
372
|
+
explicitClefs[number - 1] = mapped;
|
|
366
373
|
}
|
|
367
374
|
});
|
|
368
375
|
const staffDetails = this.querySelectorAll(attributes, 'staff-details');
|
|
@@ -372,10 +379,15 @@ export class MusicXMLParser {
|
|
|
372
379
|
if (lines !== null && number <= numStaves)
|
|
373
380
|
staves[number - 1].lineCount = lines;
|
|
374
381
|
});
|
|
382
|
+
const multipleRest = this.getNumber(attributes, 'measure-style multiple-rest');
|
|
383
|
+
if (multipleRest !== null && multipleRest > 1) {
|
|
384
|
+
multiMeasureRestCount = multipleRest;
|
|
385
|
+
}
|
|
375
386
|
}
|
|
376
387
|
let measureTempo = undefined;
|
|
377
388
|
let rehearsalMark = undefined;
|
|
378
389
|
let systemText = undefined;
|
|
390
|
+
const roadmapDirections = [];
|
|
379
391
|
let contextDynamic = undefined;
|
|
380
392
|
const { repeats, volta, barlineStyle } = this.parseBarlines(measureElement);
|
|
381
393
|
// Map of staffIndex -> Map of voiceId -> NoteSetJSON[]
|
|
@@ -386,8 +398,10 @@ export class MusicXMLParser {
|
|
|
386
398
|
let pendingChord = undefined;
|
|
387
399
|
const currentNoteSetMap = new Map();
|
|
388
400
|
const beamGroupIdMap = new Map();
|
|
401
|
+
const beamGroupIdByVoice = new Map();
|
|
389
402
|
let globalBeamId = 1;
|
|
390
|
-
|
|
403
|
+
const currentHairpins = new Map();
|
|
404
|
+
const lastNoteOnStaff = new Map();
|
|
391
405
|
let currentOttava = undefined;
|
|
392
406
|
let currentPedal = undefined;
|
|
393
407
|
children.forEach((child) => {
|
|
@@ -402,27 +416,87 @@ export class MusicXMLParser {
|
|
|
402
416
|
const beatUnit = this.getText(metronome, 'beat-unit') || 'quarter';
|
|
403
417
|
const bpm = parseInt(this.getText(metronome, 'per-minute') || '120');
|
|
404
418
|
const dotCount = this.querySelectorAll(metronome, 'beat-unit-dot').length;
|
|
405
|
-
|
|
406
|
-
measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted, dotCount };
|
|
419
|
+
measureTempo = { bpm, duration: this.mapDuration(beatUnit), dotCount };
|
|
407
420
|
}
|
|
408
421
|
const rehearsal = this.querySelector(child, 'rehearsal');
|
|
409
422
|
if (rehearsal)
|
|
410
423
|
rehearsalMark = rehearsal.textContent || undefined;
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
+
}
|
|
417
473
|
}
|
|
418
474
|
const wedge = this.querySelector(child, 'wedge');
|
|
419
475
|
if (wedge) {
|
|
420
476
|
const typeAttr = wedge.getAttribute('type');
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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,
|
|
424
491
|
placement: typeAttr === 'stop' ? 'stop' : 'start',
|
|
425
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
|
+
}
|
|
426
500
|
}
|
|
427
501
|
}
|
|
428
502
|
const octShift = this.querySelector(child, 'octave-shift');
|
|
@@ -445,37 +519,118 @@ export class MusicXMLParser {
|
|
|
445
519
|
const pedalNode = this.querySelector(child, 'pedal');
|
|
446
520
|
if (pedalNode) {
|
|
447
521
|
const typeAttr = pedalNode.getAttribute('type');
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
};
|
|
450
538
|
}
|
|
451
539
|
}
|
|
452
540
|
const dynamics = this.querySelector(child, 'dynamics');
|
|
453
541
|
if (dynamics) {
|
|
454
542
|
const firstChild = this.getFirstElementChild(dynamics);
|
|
455
|
-
if (firstChild)
|
|
456
|
-
|
|
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
|
+
}
|
|
457
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;
|
|
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
|
+
});
|
|
458
587
|
}
|
|
459
588
|
else if (child.nodeName === 'note') {
|
|
460
589
|
const staffNum = parseInt(this.getText(child, 'staff') || '1');
|
|
461
590
|
const staffIdx = Math.min(Math.max(staffNum - 1, 0), numStaves - 1);
|
|
591
|
+
const voiceId = this.getText(child, 'voice') || '1';
|
|
462
592
|
const isChord = this.querySelector(child, 'chord') !== null;
|
|
463
|
-
const note = this.parseNote(child);
|
|
593
|
+
const note = this.parseNote(child, divisions);
|
|
464
594
|
if (note) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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') {
|
|
469
618
|
const newId = globalBeamId++;
|
|
470
619
|
beamGroupIdMap.set(staffIdx, newId);
|
|
620
|
+
beamGroupIdByVoice.set(voiceId, newId);
|
|
471
621
|
note.beamGroup = newId;
|
|
472
622
|
}
|
|
473
|
-
else if (
|
|
474
|
-
const currentId = beamGroupIdMap.get(staffIdx);
|
|
623
|
+
else if (primaryBeamType === 'continue' || primaryBeamType === 'end') {
|
|
624
|
+
const currentId = beamGroupIdMap.get(staffIdx) ?? beamGroupIdByVoice.get(voiceId);
|
|
475
625
|
if (currentId !== undefined) {
|
|
476
626
|
note.beamGroup = currentId;
|
|
477
|
-
|
|
627
|
+
beamGroupIdMap.set(staffIdx, currentId);
|
|
628
|
+
if (primaryBeamType === 'end') {
|
|
478
629
|
beamGroupIdMap.delete(staffIdx);
|
|
630
|
+
if (beamGroupIdByVoice.get(voiceId) === currentId) {
|
|
631
|
+
beamGroupIdByVoice.delete(voiceId);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
479
634
|
}
|
|
480
635
|
}
|
|
481
636
|
}
|
|
@@ -489,9 +644,9 @@ export class MusicXMLParser {
|
|
|
489
644
|
note.dynamic = contextDynamic;
|
|
490
645
|
contextDynamic = undefined;
|
|
491
646
|
}
|
|
492
|
-
if (
|
|
493
|
-
note.hairpin =
|
|
494
|
-
|
|
647
|
+
if (currentHairpins.has(staffNum)) {
|
|
648
|
+
note.hairpin = currentHairpins.get(staffNum);
|
|
649
|
+
currentHairpins.delete(staffNum);
|
|
495
650
|
}
|
|
496
651
|
if (currentOttava) {
|
|
497
652
|
note.ottava = currentOttava;
|
|
@@ -501,7 +656,6 @@ export class MusicXMLParser {
|
|
|
501
656
|
note.pedal = currentPedal;
|
|
502
657
|
currentPedal = undefined;
|
|
503
658
|
}
|
|
504
|
-
const voiceId = this.getText(child, 'voice') || '1';
|
|
505
659
|
lastVoicePerStaff.set(staffIdx, voiceId);
|
|
506
660
|
const voiceMap = staffVoices.get(staffIdx);
|
|
507
661
|
if (!voiceMap.has(voiceId))
|
|
@@ -580,19 +734,31 @@ export class MusicXMLParser {
|
|
|
580
734
|
timeSignature: measureTimeSignature,
|
|
581
735
|
keySignature: measureKeySignature,
|
|
582
736
|
isPickup: isPickup || undefined,
|
|
737
|
+
clef: explicitClefs[i],
|
|
583
738
|
tempo: i === 0 ? measureTempo : undefined,
|
|
584
739
|
repeats: repeats.length > 0 ? repeats : undefined,
|
|
585
|
-
volta,
|
|
740
|
+
volta: i === 0 ? volta : undefined,
|
|
586
741
|
rehearsalMark: i === 0 ? rehearsalMark : undefined,
|
|
587
742
|
systemText: i === 0 ? systemText : undefined,
|
|
743
|
+
multiMeasureRestCount,
|
|
744
|
+
roadmapDirections: i === 0 && roadmapDirections.length > 0 ? roadmapDirections : undefined,
|
|
588
745
|
barlineStyle,
|
|
746
|
+
systemDistance: i === 0 ? currentSystemDistance : undefined,
|
|
747
|
+
staffDistance: currentStaffDistances.get(i + 1),
|
|
748
|
+
topSystemDistance: i === 0 ? currentTopSystemDistance : undefined,
|
|
589
749
|
};
|
|
750
|
+
if (mIdx === 0 && !initialClefsSet) {
|
|
751
|
+
staves[i].clef = staffClefs[i];
|
|
752
|
+
if (i === numStaves - 1)
|
|
753
|
+
initialClefsSet = true;
|
|
754
|
+
}
|
|
590
755
|
// Padding with rests if not a pickup
|
|
591
756
|
if (!isPickup) {
|
|
592
757
|
const targetDur = (this.currentBeats * 4) / this.currentBeatType;
|
|
593
758
|
measure.voices = measure.voices.map((v) => {
|
|
594
759
|
const currentDur = v.reduce((sum, ns) => {
|
|
595
|
-
return sum +
|
|
760
|
+
return (sum +
|
|
761
|
+
calculateDurationValue(ns.notes[0].duration, ns.notes[0].dotCount || (ns.notes[0].dotCount ? 1 : 0)));
|
|
596
762
|
}, 0);
|
|
597
763
|
if (currentDur < targetDur - 0.001) {
|
|
598
764
|
const gap = targetDur - currentDur;
|
|
@@ -601,7 +767,7 @@ export class MusicXMLParser {
|
|
|
601
767
|
{
|
|
602
768
|
duration: d.duration,
|
|
603
769
|
isRest: true,
|
|
604
|
-
|
|
770
|
+
dotCount: d.dotCount,
|
|
605
771
|
},
|
|
606
772
|
],
|
|
607
773
|
}));
|
|
@@ -611,20 +777,97 @@ export class MusicXMLParser {
|
|
|
611
777
|
});
|
|
612
778
|
}
|
|
613
779
|
staves[i].measures.push(measure);
|
|
614
|
-
staves[i].clef = staffClefs[i];
|
|
615
780
|
}
|
|
616
781
|
}
|
|
617
782
|
return staves;
|
|
618
783
|
}
|
|
619
|
-
parseNote(noteElement) {
|
|
784
|
+
parseNote(noteElement, divisions) {
|
|
620
785
|
const isRest = this.querySelector(noteElement, 'rest') !== null;
|
|
621
|
-
const
|
|
786
|
+
const restNode = this.querySelector(noteElement, 'rest');
|
|
787
|
+
const graceEl = this.querySelector(noteElement, 'grace');
|
|
788
|
+
const isGrace = graceEl !== null;
|
|
622
789
|
const type = this.getText(noteElement, 'type');
|
|
623
|
-
const
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
+
}
|
|
628
871
|
let step = 'C', octave = 4, alter = 0;
|
|
629
872
|
const unpitched = this.querySelector(noteElement, 'unpitched');
|
|
630
873
|
if (unpitched) {
|
|
@@ -647,7 +890,16 @@ export class MusicXMLParser {
|
|
|
647
890
|
midiNumber = this.instrumentPitchMap.get(instId);
|
|
648
891
|
}
|
|
649
892
|
let accidental = this.mapAccidental(alter);
|
|
650
|
-
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;
|
|
651
903
|
if (accidentalEl) {
|
|
652
904
|
if (accidentalEl === 'sharp')
|
|
653
905
|
accidental = Accidental.Sharp;
|
|
@@ -675,7 +927,7 @@ export class MusicXMLParser {
|
|
|
675
927
|
else if (stem === 'down')
|
|
676
928
|
stemDirection = StemDirection.Down;
|
|
677
929
|
const notations = this.querySelector(noteElement, 'notations');
|
|
678
|
-
// Tuplet ratio
|
|
930
|
+
// Tuplet ratio and engraving metadata
|
|
679
931
|
let tuplet = undefined;
|
|
680
932
|
const timeMod = this.querySelector(noteElement, 'time-modification');
|
|
681
933
|
if (timeMod) {
|
|
@@ -691,22 +943,81 @@ export class MusicXMLParser {
|
|
|
691
943
|
tuplet.type = 'start';
|
|
692
944
|
else if (type === 'stop')
|
|
693
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
|
+
}
|
|
694
977
|
}
|
|
695
978
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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;
|
|
702
985
|
const orientation = slurEl.getAttribute('orientation');
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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 });
|
|
707
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
|
+
});
|
|
708
1009
|
}
|
|
709
|
-
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;
|
|
710
1021
|
const articulations = [];
|
|
711
1022
|
const articEl = notations ? this.querySelector(notations, 'articulations') : null;
|
|
712
1023
|
if (articEl) {
|
|
@@ -729,6 +1040,7 @@ export class MusicXMLParser {
|
|
|
729
1040
|
articulations.push(Articulation.Fermata);
|
|
730
1041
|
let arpeggio = undefined;
|
|
731
1042
|
const arpeggiateEl = notations ? this.querySelector(notations, 'arpeggiate') : null;
|
|
1043
|
+
const nonArpeggiateEl = notations ? this.querySelector(notations, 'non-arpeggiate') : null;
|
|
732
1044
|
if (arpeggiateEl) {
|
|
733
1045
|
const dir = arpeggiateEl.getAttribute('direction');
|
|
734
1046
|
if (dir === 'up')
|
|
@@ -738,8 +1050,13 @@ export class MusicXMLParser {
|
|
|
738
1050
|
else
|
|
739
1051
|
arpeggio = Arpeggio.Normal;
|
|
740
1052
|
}
|
|
1053
|
+
else if (nonArpeggiateEl) {
|
|
1054
|
+
arpeggio = Arpeggio.NonArpeggiate;
|
|
1055
|
+
}
|
|
741
1056
|
const ornaments = notations ? this.querySelector(notations, 'ornaments') : null;
|
|
742
1057
|
let ornament = undefined;
|
|
1058
|
+
let ornamentDetails = undefined;
|
|
1059
|
+
let vibrato = undefined;
|
|
743
1060
|
if (ornaments) {
|
|
744
1061
|
if (this.querySelector(ornaments, 'trill-mark'))
|
|
745
1062
|
ornament = Ornament.Trill;
|
|
@@ -747,12 +1064,85 @@ export class MusicXMLParser {
|
|
|
747
1064
|
ornament = Ornament.Mordent;
|
|
748
1065
|
else if (this.querySelector(ornaments, 'inverted-mordent'))
|
|
749
1066
|
ornament = Ornament.InvertedMordent;
|
|
1067
|
+
else if (this.querySelector(ornaments, 'delayed-turn'))
|
|
1068
|
+
ornament = Ornament.DelayedTurn;
|
|
750
1069
|
else if (this.querySelector(ornaments, 'turn'))
|
|
751
1070
|
ornament = Ornament.Turn;
|
|
752
1071
|
else if (this.querySelector(ornaments, 'inverted-turn'))
|
|
753
1072
|
ornament = Ornament.InvertedTurn;
|
|
754
|
-
else if (this.querySelector(ornaments, 'tremolo'))
|
|
755
|
-
|
|
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
|
+
}
|
|
756
1146
|
}
|
|
757
1147
|
const technical = notations ? this.querySelector(notations, 'technical') : null;
|
|
758
1148
|
let bowing = undefined;
|
|
@@ -763,6 +1153,8 @@ export class MusicXMLParser {
|
|
|
763
1153
|
let palmMute = undefined;
|
|
764
1154
|
let hammerOn = undefined;
|
|
765
1155
|
let pullOff = undefined;
|
|
1156
|
+
let harmonic = undefined;
|
|
1157
|
+
let bend = undefined;
|
|
766
1158
|
if (technical) {
|
|
767
1159
|
if (this.querySelector(technical, 'up-bow'))
|
|
768
1160
|
bowing = Bowing.UpBow;
|
|
@@ -788,6 +1180,18 @@ export class MusicXMLParser {
|
|
|
788
1180
|
const pullOffEl = this.querySelector(technical, 'pull-off');
|
|
789
1181
|
if (pullOffEl)
|
|
790
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
|
+
}
|
|
791
1195
|
}
|
|
792
1196
|
let glissando = undefined;
|
|
793
1197
|
const glissEl = notations
|
|
@@ -816,25 +1220,31 @@ export class MusicXMLParser {
|
|
|
816
1220
|
text,
|
|
817
1221
|
syllabic: syllabic || undefined,
|
|
818
1222
|
isExtension: extend || undefined,
|
|
1223
|
+
placement: this.getPlacement(lyricEl),
|
|
819
1224
|
};
|
|
820
1225
|
}
|
|
821
1226
|
});
|
|
822
1227
|
return {
|
|
823
1228
|
duration,
|
|
824
1229
|
pitch: { midiNumber, step: this.stepToNumber(step), alter, octave },
|
|
825
|
-
isDotted,
|
|
826
1230
|
dotCount,
|
|
827
1231
|
accidental,
|
|
1232
|
+
accidentalDisplay,
|
|
828
1233
|
slur,
|
|
829
1234
|
tie,
|
|
1235
|
+
tieSpans: dedupedTieSpans.length > 0 ? dedupedTieSpans : undefined,
|
|
1236
|
+
slurs: slurs.length > 0 ? slurs : undefined,
|
|
830
1237
|
articulations: articulations.length > 0 ? articulations : undefined,
|
|
831
1238
|
dynamic,
|
|
832
1239
|
arpeggio,
|
|
833
1240
|
lyrics: lyrics.length > 0 ? lyrics : undefined,
|
|
834
1241
|
notehead,
|
|
835
1242
|
isGrace: isGrace || undefined,
|
|
1243
|
+
isCue: isCue || undefined,
|
|
1244
|
+
grace,
|
|
836
1245
|
tuplet,
|
|
837
1246
|
ornament,
|
|
1247
|
+
ornamentDetails,
|
|
838
1248
|
bowing,
|
|
839
1249
|
fingering,
|
|
840
1250
|
stemDirection,
|
|
@@ -845,6 +1255,10 @@ export class MusicXMLParser {
|
|
|
845
1255
|
palmMute,
|
|
846
1256
|
hammerOn,
|
|
847
1257
|
pullOff,
|
|
1258
|
+
harmonic,
|
|
1259
|
+
bend,
|
|
1260
|
+
vibrato,
|
|
1261
|
+
placement: this.getPlacement(noteElement),
|
|
848
1262
|
};
|
|
849
1263
|
}
|
|
850
1264
|
parseHarmony(harmonyElement) {
|
|
@@ -1007,18 +1421,21 @@ export class MusicXMLParser {
|
|
|
1007
1421
|
const ending = this.querySelector(barline, 'ending');
|
|
1008
1422
|
if (ending) {
|
|
1009
1423
|
const type = ending.getAttribute('type');
|
|
1010
|
-
const numberAttr = ending.getAttribute('number')
|
|
1424
|
+
const numberAttr = ending.getAttribute('number');
|
|
1011
1425
|
// Handle "1,2" or "1 2"
|
|
1012
1426
|
const numbers = numberAttr
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1427
|
+
? numberAttr
|
|
1428
|
+
.split(/[,\s]+/)
|
|
1429
|
+
.map((n) => parseInt(n))
|
|
1430
|
+
.filter((n) => !isNaN(n))
|
|
1431
|
+
: [];
|
|
1432
|
+
const placement = ending.getAttribute('placement');
|
|
1016
1433
|
if (type === 'start')
|
|
1017
|
-
volta = { type: 'start', numbers };
|
|
1434
|
+
volta = { type: 'start', numbers, placement: placement || undefined };
|
|
1018
1435
|
else if (type === 'stop')
|
|
1019
|
-
volta = { type: 'stop', numbers };
|
|
1436
|
+
volta = { type: 'stop', numbers, placement: placement || undefined };
|
|
1020
1437
|
else if (type === 'discontinue')
|
|
1021
|
-
volta = { type: 'both', numbers };
|
|
1438
|
+
volta = { type: 'both', numbers, placement: placement || undefined };
|
|
1022
1439
|
}
|
|
1023
1440
|
});
|
|
1024
1441
|
return { repeats, volta, barlineStyle };
|
|
@@ -1053,8 +1470,17 @@ export class MusicXMLParser {
|
|
|
1053
1470
|
return;
|
|
1054
1471
|
const step = stepNames[note.pitch.step];
|
|
1055
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
|
+
}
|
|
1056
1482
|
if (note.accidental) {
|
|
1057
|
-
if (state[step] === alter)
|
|
1483
|
+
if (state[step] === alter && !isCourtesyAccidental)
|
|
1058
1484
|
note.accidental = undefined;
|
|
1059
1485
|
else
|
|
1060
1486
|
state[step] = alter;
|
|
@@ -1132,4 +1558,25 @@ export class MusicXMLParser {
|
|
|
1132
1558
|
const text = this.getText(element, tagName);
|
|
1133
1559
|
return text !== null ? parseFloat(text) : null;
|
|
1134
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
|
+
}
|
|
1135
1582
|
}
|