@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.
Files changed (61) hide show
  1. package/dist/constants.d.ts +4 -0
  2. package/dist/constants.js +10 -0
  3. package/dist/importers/MusicXMLParser.d.ts +4 -1
  4. package/dist/importers/MusicXMLParser.js +564 -75
  5. package/dist/importers/index.d.ts +1 -1
  6. package/dist/importers/index.js +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/models/Measure.d.ts +25 -5
  10. package/dist/models/Measure.js +173 -35
  11. package/dist/models/Note.d.ts +38 -9
  12. package/dist/models/Note.js +104 -47
  13. package/dist/models/NoteSet.d.ts +14 -4
  14. package/dist/models/NoteSet.js +38 -4
  15. package/dist/models/Part.d.ts +6 -6
  16. package/dist/models/Part.js +4 -4
  17. package/dist/models/Pitch.d.ts +1 -1
  18. package/dist/models/Pitch.js +1 -1
  19. package/dist/models/PreMeasure.d.ts +1 -1
  20. package/dist/models/Score.d.ts +13 -9
  21. package/dist/models/Score.js +164 -126
  22. package/dist/models/Staff.d.ts +5 -5
  23. package/dist/models/Staff.js +4 -4
  24. package/dist/models/index.d.ts +10 -10
  25. package/dist/models/index.js +10 -10
  26. package/dist/types/AccidentalDisplay.d.ts +6 -0
  27. package/dist/types/AccidentalDisplay.js +1 -0
  28. package/dist/types/Arpeggio.d.ts +2 -1
  29. package/dist/types/Arpeggio.js +1 -0
  30. package/dist/types/Articulation.d.ts +3 -1
  31. package/dist/types/Articulation.js +2 -0
  32. package/dist/types/Beam.d.ts +4 -0
  33. package/dist/types/Beam.js +1 -0
  34. package/dist/types/Duration.d.ts +5 -1
  35. package/dist/types/Duration.js +27 -14
  36. package/dist/types/Grace.d.ts +7 -0
  37. package/dist/types/Grace.js +1 -0
  38. package/dist/types/Hairpin.d.ts +2 -1
  39. package/dist/types/Language.d.ts +6 -0
  40. package/dist/types/Language.js +7 -0
  41. package/dist/types/Lyric.d.ts +2 -0
  42. package/dist/types/Ornament.d.ts +23 -1
  43. package/dist/types/Ornament.js +9 -0
  44. package/dist/types/Pedal.d.ts +3 -2
  45. package/dist/types/Placement.d.ts +7 -0
  46. package/dist/types/Placement.js +1 -0
  47. package/dist/types/Repeat.d.ts +1 -0
  48. package/dist/types/RestDisplay.d.ts +6 -0
  49. package/dist/types/RestDisplay.js +1 -0
  50. package/dist/types/Slur.d.ts +1 -0
  51. package/dist/types/Technical.d.ts +20 -0
  52. package/dist/types/Technical.js +5 -0
  53. package/dist/types/Tempo.d.ts +1 -1
  54. package/dist/types/Tie.d.ts +4 -0
  55. package/dist/types/Tie.js +1 -0
  56. package/dist/types/Tuplet.d.ts +6 -0
  57. package/dist/types/User.d.ts +13 -7
  58. package/dist/types/User.js +9 -7
  59. package/dist/types/index.d.ts +8 -0
  60. package/dist/types/index.js +8 -0
  61. 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, DURATION_VALUES, Duration, GlissandoType, HairpinType, NoteheadShape, Ornament, OttavaType, StemDirection, decomposeDuration, } from '../models/types';
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 = (new MusicXMLParser(_parser)).querySelector(containerDoc, '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
- return this.querySelector(doc, 'identification creator')?.textContent ?? 'Unknown';
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
- 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;
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
- let currentNoteSetMap = new Map();
399
+ const currentNoteSetMap = new Map();
351
400
  const beamGroupIdMap = new Map();
401
+ const beamGroupIdByVoice = new Map();
352
402
  let globalBeamId = 1;
353
- let currentHairpin = undefined;
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 isDotted = this.querySelector(metronome, 'beat-unit-dot') !== null;
368
- measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted };
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 words = this.querySelector(child, 'words');
374
- if (words) {
375
- if (measureTempo)
376
- measureTempo.text = words.textContent || undefined;
377
- else
378
- 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
+ }
379
473
  }
380
474
  const wedge = this.querySelector(child, 'wedge');
381
475
  if (wedge) {
382
476
  const typeAttr = wedge.getAttribute('type');
383
- if (typeAttr === 'crescendo' || typeAttr === 'diminuendo' || typeAttr === 'stop') {
384
- currentHairpin = {
385
- 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,
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
- if (typeAttr === 'start' || typeAttr === 'stop') {
411
- 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
+ };
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
- 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
+ }
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
- const beamEl = this.querySelector(child, 'beam[number="1"]');
428
- if (beamEl) {
429
- const beamType = beamEl.textContent;
430
- 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') {
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 (beamType === 'continue' || beamType === 'end') {
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
- if (beamType === 'end')
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 (currentHairpin) {
455
- note.hairpin = currentHairpin;
456
- currentHairpin = undefined;
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' && (this.getText(n, 'voice') || '1') === (Array.from(vMap0.keys())[0] || '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
- const base = DURATION_VALUES[ns.notes[0].duration];
557
- return sum + (ns.notes[0].isDotted ? base * 1.5 : base);
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
- isDotted: d.isDotted,
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 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;
584
789
  const type = this.getText(noteElement, 'type');
585
- const duration = type ? this.mapDuration(type) : Duration.Quarter;
586
- const isDotted = this.querySelector(noteElement, 'dot') !== null;
587
- if (isRest)
588
- return { duration, isRest: true, isDotted };
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 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;
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
- let slur = undefined;
658
- const slurEl = notations ? this.querySelector(notations, 'slur') : null;
659
- if (slurEl) {
660
- const type = slurEl.getAttribute('type');
661
- if (type === 'start' || type === 'stop') {
662
- 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;
663
985
  const orientation = slurEl.getAttribute('orientation');
664
- if (orientation === 'over')
665
- slur.direction = 'up';
666
- else if (orientation === 'under')
667
- 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 });
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 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;
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
- 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
+ }
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 ? (this.querySelector(notations, 'glissando') || this.querySelector(notations, 'slide')) : null;
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
- isDotted,
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') || '1';
1424
+ const numberAttr = ending.getAttribute('number');
969
1425
  // Handle "1,2" or "1 2"
970
1426
  const numbers = numberAttr
971
- .split(/[,\s]+/)
972
- .map((n) => parseInt(n))
973
- .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');
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
  }