@scorelabs/core 1.0.3 → 1.0.5

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