@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.
Files changed (50) hide show
  1. package/dist/importers/MusicXMLParser.d.ts +2 -0
  2. package/dist/importers/MusicXMLParser.js +518 -71
  3. package/dist/models/Measure.d.ts +19 -2
  4. package/dist/models/Measure.js +79 -27
  5. package/dist/models/Note.d.ts +35 -5
  6. package/dist/models/Note.js +97 -42
  7. package/dist/models/NoteSet.d.ts +11 -2
  8. package/dist/models/NoteSet.js +33 -2
  9. package/dist/models/Part.d.ts +1 -1
  10. package/dist/models/Part.js +2 -2
  11. package/dist/models/Score.d.ts +5 -5
  12. package/dist/models/Score.js +144 -121
  13. package/dist/models/Staff.d.ts +1 -1
  14. package/dist/models/Staff.js +2 -2
  15. package/dist/types/AccidentalDisplay.d.ts +6 -0
  16. package/dist/types/AccidentalDisplay.js +1 -0
  17. package/dist/types/Arpeggio.d.ts +2 -1
  18. package/dist/types/Arpeggio.js +1 -0
  19. package/dist/types/Articulation.d.ts +3 -1
  20. package/dist/types/Articulation.js +2 -0
  21. package/dist/types/Beam.d.ts +4 -0
  22. package/dist/types/Beam.js +1 -0
  23. package/dist/types/Duration.d.ts +1 -1
  24. package/dist/types/Duration.js +14 -14
  25. package/dist/types/Grace.d.ts +7 -0
  26. package/dist/types/Grace.js +1 -0
  27. package/dist/types/Hairpin.d.ts +2 -1
  28. package/dist/types/Language.d.ts +6 -0
  29. package/dist/types/Language.js +7 -0
  30. package/dist/types/Lyric.d.ts +2 -0
  31. package/dist/types/Ornament.d.ts +23 -1
  32. package/dist/types/Ornament.js +9 -0
  33. package/dist/types/Pedal.d.ts +3 -2
  34. package/dist/types/Placement.d.ts +7 -0
  35. package/dist/types/Placement.js +1 -0
  36. package/dist/types/Repeat.d.ts +1 -0
  37. package/dist/types/RestDisplay.d.ts +6 -0
  38. package/dist/types/RestDisplay.js +1 -0
  39. package/dist/types/Slur.d.ts +1 -0
  40. package/dist/types/Technical.d.ts +20 -0
  41. package/dist/types/Technical.js +5 -0
  42. package/dist/types/Tempo.d.ts +0 -1
  43. package/dist/types/Tie.d.ts +4 -0
  44. package/dist/types/Tie.js +1 -0
  45. package/dist/types/Tuplet.d.ts +6 -0
  46. package/dist/types/User.d.ts +13 -7
  47. package/dist/types/User.js +9 -7
  48. package/dist/types/index.d.ts +8 -0
  49. package/dist/types/index.js +8 -0
  50. 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 (creators[0].textContent ?? 'Unknown');
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
- staffClefs[number - 1] = this.mapClef(sign, line, octaveChange);
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
- let currentHairpin = undefined;
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
- const isDotted = dotCount > 0;
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 words = this.querySelector(child, 'words');
412
- if (words) {
413
- if (measureTempo)
414
- measureTempo.text = words.textContent || undefined;
415
- else
416
- systemText = words.textContent || undefined;
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
- if (typeAttr === 'crescendo' || typeAttr === 'diminuendo' || typeAttr === 'stop') {
422
- currentHairpin = {
423
- type: typeAttr === 'crescendo' ? HairpinType.Crescendo : HairpinType.Decrescendo,
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
- if (typeAttr === 'start' || typeAttr === 'stop') {
449
- currentPedal = { type: 'sustain', placement: typeAttr };
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
- contextDynamic = firstChild.nodeName.toLowerCase();
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
- const beamEl = this.querySelector(child, 'beam[number="1"]');
466
- if (beamEl) {
467
- const beamType = beamEl.textContent;
468
- if (beamType === 'begin') {
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 (beamType === 'continue' || beamType === 'end') {
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
- if (beamType === 'end')
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 (currentHairpin) {
493
- note.hairpin = currentHairpin;
494
- currentHairpin = undefined;
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 + calculateDurationValue(ns.notes[0].duration, ns.notes[0].dotCount || (ns.notes[0].isDotted ? 1 : 0));
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
- isDotted: d.isDotted,
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 isGrace = this.querySelector(noteElement, 'grace') !== null;
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 duration = type ? this.mapDuration(type) : Duration.Quarter;
624
- const dotCount = this.querySelectorAll(noteElement, 'dot').length;
625
- const isDotted = dotCount > 0;
626
- if (isRest)
627
- return { duration, isRest: true, isDotted, dotCount };
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 accidentalEl = this.getText(noteElement, 'accidental');
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
- let slur = undefined;
697
- const slurEl = notations ? this.querySelector(notations, 'slur') : null;
698
- if (slurEl) {
699
- const type = slurEl.getAttribute('type');
700
- if (type === 'start' || type === 'stop') {
701
- slur = { placement: type };
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
- if (orientation === 'over')
704
- slur.direction = 'up';
705
- else if (orientation === 'under')
706
- slur.direction = 'down';
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 tie = this.querySelector(noteElement, 'tie')?.getAttribute('type') === 'start' || undefined;
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
- ornament = Ornament.Tremolo;
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') || '1';
1424
+ const numberAttr = ending.getAttribute('number');
1011
1425
  // Handle "1,2" or "1 2"
1012
1426
  const numbers = numberAttr
1013
- .split(/[,\s]+/)
1014
- .map((n) => parseInt(n))
1015
- .filter((n) => !isNaN(n));
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
  }