@scorelabs/core 1.0.7 → 1.0.10

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 (78) hide show
  1. package/dist/importers/MusicXMLParser.d.ts +7 -2
  2. package/dist/importers/MusicXMLParser.js +261 -103
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +2 -2
  5. package/dist/models/Instrument.d.ts +8 -19
  6. package/dist/models/Instrument.js +77 -23
  7. package/dist/models/Measure.d.ts +5 -1
  8. package/dist/models/Measure.js +132 -19
  9. package/dist/models/Note.d.ts +7 -7
  10. package/dist/models/Note.js +2 -2
  11. package/dist/models/NoteSet.d.ts +44 -42
  12. package/dist/models/NoteSet.js +5 -2
  13. package/dist/models/Pitch.js +4 -0
  14. package/dist/models/PreMeasure.d.ts +24 -0
  15. package/dist/models/PreMeasure.js +30 -0
  16. package/dist/models/Score.d.ts +20 -5
  17. package/dist/models/Score.js +190 -5
  18. package/dist/models/index.d.ts +1 -0
  19. package/dist/models/index.js +1 -0
  20. package/dist/models/types.d.ts +2 -213
  21. package/dist/models/types.js +2 -213
  22. package/dist/types/Accidental.d.ts +7 -0
  23. package/dist/types/Accidental.js +8 -0
  24. package/dist/types/Arpeggio.d.ts +5 -0
  25. package/dist/types/Arpeggio.js +6 -0
  26. package/dist/types/Articulation.d.ts +10 -0
  27. package/dist/types/Articulation.js +11 -0
  28. package/dist/types/BarlineStyle.d.ts +9 -0
  29. package/dist/types/BarlineStyle.js +10 -0
  30. package/dist/types/Bowing.d.ts +4 -0
  31. package/dist/types/Bowing.js +5 -0
  32. package/dist/types/Clef.d.ts +10 -0
  33. package/dist/types/Clef.js +11 -0
  34. package/dist/types/Duration.d.ts +20 -0
  35. package/dist/types/Duration.js +60 -0
  36. package/dist/types/Dynamic.d.ts +12 -0
  37. package/dist/types/Dynamic.js +13 -0
  38. package/dist/types/Fretboard.d.ts +19 -0
  39. package/dist/types/Fretboard.js +1 -0
  40. package/dist/types/Genre.d.ts +27 -0
  41. package/dist/types/Genre.js +28 -0
  42. package/dist/types/Glissando.d.ts +8 -0
  43. package/dist/types/Glissando.js +5 -0
  44. package/dist/types/Hairpin.d.ts +10 -0
  45. package/dist/types/Hairpin.js +11 -0
  46. package/dist/types/InstrumentPreset.d.ts +11 -0
  47. package/dist/types/InstrumentPreset.js +12 -0
  48. package/dist/types/InstrumentType.d.ts +8 -0
  49. package/dist/types/InstrumentType.js +9 -0
  50. package/dist/types/KeySignature.d.ts +3 -0
  51. package/dist/types/KeySignature.js +1 -0
  52. package/dist/types/Lyric.d.ts +11 -0
  53. package/dist/types/Lyric.js +7 -0
  54. package/dist/types/NoteheadShape.d.ts +8 -0
  55. package/dist/types/NoteheadShape.js +9 -0
  56. package/dist/types/Ornament.d.ts +8 -0
  57. package/dist/types/Ornament.js +9 -0
  58. package/dist/types/Ottava.d.ts +10 -0
  59. package/dist/types/Ottava.js +7 -0
  60. package/dist/types/Pedal.d.ts +4 -0
  61. package/dist/types/Pedal.js +1 -0
  62. package/dist/types/Repeat.d.ts +8 -0
  63. package/dist/types/Repeat.js +1 -0
  64. package/dist/types/Slur.d.ts +4 -0
  65. package/dist/types/Slur.js +1 -0
  66. package/dist/types/StemDirection.d.ts +4 -0
  67. package/dist/types/StemDirection.js +5 -0
  68. package/dist/types/Tempo.d.ts +8 -0
  69. package/dist/types/Tempo.js +1 -0
  70. package/dist/types/TimeSignature.d.ts +6 -0
  71. package/dist/types/TimeSignature.js +3 -0
  72. package/dist/types/Tuplet.d.ts +5 -0
  73. package/dist/types/Tuplet.js +1 -0
  74. package/dist/types/User.d.ts +21 -0
  75. package/dist/types/User.js +7 -0
  76. package/dist/types/index.d.ts +27 -0
  77. package/dist/types/index.js +27 -0
  78. package/package.json +3 -1
@@ -1,6 +1,6 @@
1
1
  import JSZip from 'jszip';
2
- import { getInstrumentByProgram } from '../models/Instrument';
3
- import { Accidental, Arpeggio, Articulation, BarlineStyle, Bowing, Clef, Duration, GlissandoType, HairpinType, NoteheadShape, Ornament, OttavaType, StemDirection, } from '../models/types';
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';
4
4
  /**
5
5
  * MusicXML Parser
6
6
  *
@@ -18,6 +18,52 @@ export class MusicXMLParser {
18
18
  this._domParser =
19
19
  domParserInstance || (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
20
20
  }
21
+ querySelector(el, selector) {
22
+ if (el.querySelector)
23
+ return el.querySelector(selector);
24
+ const parts = selector.split(' ');
25
+ let current = el;
26
+ for (const part of parts) {
27
+ if (!current)
28
+ return null;
29
+ const results = current.getElementsByTagName(part);
30
+ if (results.length === 0)
31
+ return null;
32
+ current = results[0];
33
+ }
34
+ return current;
35
+ }
36
+ getChildren(el) {
37
+ const parent = el;
38
+ if (parent.children)
39
+ return Array.from(parent.children);
40
+ return Array.from(el.childNodes).filter((node) => node.nodeType === 1);
41
+ }
42
+ getFirstElementChild(el) {
43
+ if (el.firstElementChild)
44
+ return el.firstElementChild;
45
+ const children = this.getChildren(el);
46
+ return children.length > 0 ? children[0] : null;
47
+ }
48
+ querySelectorAll(el, selector) {
49
+ if (el.querySelectorAll)
50
+ return Array.from(el.querySelectorAll(selector));
51
+ const parts = selector.split(' ');
52
+ if (parts.length === 1) {
53
+ return Array.from(el.getElementsByTagName(parts[0]));
54
+ }
55
+ // Simple nested selector support (only two levels for now as needed)
56
+ if (parts.length === 2) {
57
+ const firstLevel = Array.from(el.getElementsByTagName(parts[0]));
58
+ const results = [];
59
+ firstLevel.forEach((parent) => {
60
+ const children = Array.from(parent.getElementsByTagName(parts[1]));
61
+ results.push(...children);
62
+ });
63
+ return results;
64
+ }
65
+ return [];
66
+ }
21
67
  parseFromString(xmlString) {
22
68
  if (!this._domParser) {
23
69
  throw new Error('No DOMParser available. Please provide one in the constructor for Node.js environments.');
@@ -35,7 +81,7 @@ export class MusicXMLParser {
35
81
  if (!_parser)
36
82
  throw new Error('No DOMParser available');
37
83
  const containerDoc = _parser.parseFromString(containerXml, 'application/xml');
38
- const rootfile = containerDoc.querySelector('rootfile');
84
+ const rootfile = (new MusicXMLParser(_parser)).querySelector(containerDoc, 'rootfile');
39
85
  const fullPath = rootfile?.getAttribute('full-path');
40
86
  if (!fullPath)
41
87
  throw new Error('Invalid MXL: Could not find main score file path');
@@ -63,7 +109,7 @@ export class MusicXMLParser {
63
109
  }
64
110
  xmlString = xmlString.substring(firstBracket).trim();
65
111
  const doc = this.parseFromString(xmlString);
66
- const parseError = doc.querySelector('parsererror');
112
+ const parseError = this.querySelector(doc, 'parsererror');
67
113
  if (parseError) {
68
114
  const preview = xmlString.substring(0, 100).replace(/\n/g, ' ');
69
115
  throw new Error(`XML parsing error: ${parseError.textContent}. Content preview: "${preview}..."`);
@@ -74,6 +120,7 @@ export class MusicXMLParser {
74
120
  const title = this.parseTitle(doc);
75
121
  const subtitle = this.parseSubtitle(doc);
76
122
  const composer = this.parseComposer(doc);
123
+ const copyright = this.parseCopyright(doc);
77
124
  const partInfo = this.parsePartList(doc);
78
125
  const parts = this.parseParts(doc, partInfo);
79
126
  let globalBpm = 120;
@@ -104,6 +151,7 @@ export class MusicXMLParser {
104
151
  tempoDuration: globalTempoDuration,
105
152
  tempoIsDotted: globalTempoIsDotted,
106
153
  tempoText: globalTempoText,
154
+ copyright,
107
155
  };
108
156
  return this.postProcess(score);
109
157
  }
@@ -111,7 +159,7 @@ export class MusicXMLParser {
111
159
  let subtitle = this.getText(doc.documentElement, 'movement-subtitle');
112
160
  if (subtitle)
113
161
  return subtitle;
114
- const work = doc.querySelector('work');
162
+ const work = this.querySelector(doc, 'work');
115
163
  if (work) {
116
164
  subtitle = this.getText(work, 'work-subtitle');
117
165
  if (subtitle)
@@ -120,11 +168,11 @@ export class MusicXMLParser {
120
168
  if (subtitle)
121
169
  return subtitle;
122
170
  }
123
- const credits = doc.querySelectorAll('credit');
171
+ const credits = this.querySelectorAll(doc, 'credit');
124
172
  for (const credit of Array.from(credits)) {
125
- const type = credit.querySelector('credit-type');
173
+ const type = this.querySelector(credit, 'credit-type');
126
174
  if (type?.textContent?.toLowerCase() === 'subtitle') {
127
- const words = credit.querySelector('credit-words');
175
+ const words = this.querySelector(credit, 'credit-words');
128
176
  if (words?.textContent)
129
177
  return words.textContent;
130
178
  }
@@ -137,37 +185,65 @@ export class MusicXMLParser {
137
185
  title = this.getText(doc.documentElement, 'movement-title');
138
186
  return title ?? 'Untitled';
139
187
  }
188
+ parseCopyright(doc) {
189
+ const identification = this.querySelector(doc, 'identification');
190
+ if (identification) {
191
+ const rights = this.getText(identification, 'rights');
192
+ if (rights)
193
+ return rights;
194
+ }
195
+ const credits = this.querySelectorAll(doc, 'credit');
196
+ for (const credit of Array.from(credits)) {
197
+ const type = this.querySelector(credit, 'credit-type');
198
+ if (type?.textContent?.toLowerCase() === 'rights' ||
199
+ type?.textContent?.toLowerCase() === 'copyright') {
200
+ const words = this.querySelector(credit, 'credit-words');
201
+ if (words?.textContent)
202
+ return words.textContent;
203
+ }
204
+ }
205
+ return '';
206
+ }
140
207
  parseComposer(doc) {
141
- const creators = doc.querySelectorAll('identification creator');
208
+ const creators = this.querySelectorAll(doc, 'identification creator');
142
209
  for (const creator of Array.from(creators)) {
143
210
  if (creator.getAttribute('type') === 'composer')
144
211
  return creator.textContent ?? 'Unknown';
145
212
  }
146
- return doc.querySelector('identification creator')?.textContent ?? 'Unknown';
213
+ return this.querySelector(doc, 'identification creator')?.textContent ?? 'Unknown';
147
214
  }
148
215
  parsePartList(doc) {
149
216
  const partInfo = new Map();
150
- const scoreParts = doc.querySelectorAll('part-list score-part');
217
+ const scoreParts = this.querySelectorAll(doc, 'part-list score-part');
151
218
  for (const scorePart of Array.from(scoreParts)) {
152
219
  const id = scorePart.getAttribute('id');
153
220
  const name = this.getText(scorePart, 'part-name') || 'Part';
154
- let instrument = getInstrumentByProgram(0); // Default Piano
221
+ let instrument;
155
222
  // Find first midi-instrument to determine main instrument
156
- const firstMidiInst = scorePart.querySelector('midi-instrument');
223
+ const firstMidiInst = this.querySelector(scorePart, 'midi-instrument');
157
224
  if (firstMidiInst) {
158
225
  const programStr = this.getText(firstMidiInst, 'midi-program');
159
226
  if (programStr) {
160
227
  const program = parseInt(programStr);
161
- // MusicXML uses 1-128, MIDI uses 0-127. Adjust if needed.
162
- // Often it's accurate to 1-based, so checking bounds.
163
- // Assuming input is 1-based as per standard
164
228
  instrument = getInstrumentByProgram(Math.max(0, program - 1));
165
229
  }
166
230
  }
231
+ // Fallback: guess by part name
232
+ if (!instrument || instrument.sound === 'piano') {
233
+ const guessed = guessInstrumentByName(name);
234
+ // Only override if the guess is more specific than default piano
235
+ if (guessed.sound !== 'piano') {
236
+ instrument = guessed;
237
+ }
238
+ }
239
+ // Final default
240
+ if (!instrument) {
241
+ instrument = getInstrumentByProgram(0);
242
+ }
167
243
  if (id) {
168
244
  partInfo.set(id, { name: name.trim(), instrument });
169
245
  }
170
- const midiInstruments = scorePart.querySelectorAll('midi-instrument');
246
+ const midiInstruments = this.querySelectorAll(scorePart, 'midi-instrument');
171
247
  for (const midiInst of Array.from(midiInstruments)) {
172
248
  const instId = midiInst.getAttribute('id');
173
249
  const unpitched = this.getNumber(midiInst, 'midi-unpitched');
@@ -179,7 +255,7 @@ export class MusicXMLParser {
179
255
  }
180
256
  parseParts(doc, partInfo) {
181
257
  const parts = [];
182
- const partElements = doc.querySelectorAll('part');
258
+ const partElements = this.querySelectorAll(doc, 'part');
183
259
  for (const partElement of Array.from(partElements)) {
184
260
  const id = partElement.getAttribute('id');
185
261
  const info = partInfo.get(id ?? '');
@@ -190,11 +266,11 @@ export class MusicXMLParser {
190
266
  return parts;
191
267
  }
192
268
  parsePart(partElement) {
193
- const measureElements = partElement.querySelectorAll('measure');
269
+ const measureElements = this.querySelectorAll(partElement, 'measure');
194
270
  let numStaves = 1;
195
- const firstMeasure = partElement.querySelector('measure');
271
+ const firstMeasure = this.querySelector(partElement, 'measure');
196
272
  if (firstMeasure) {
197
- const stavesNode = firstMeasure.querySelector('attributes staves');
273
+ const stavesNode = this.querySelector(firstMeasure, 'attributes staves');
198
274
  if (stavesNode)
199
275
  numStaves = parseInt(stavesNode.textContent || '1');
200
276
  }
@@ -203,12 +279,17 @@ export class MusicXMLParser {
203
279
  measures: [],
204
280
  }));
205
281
  const staffClefs = Array(numStaves).fill(Clef.Treble);
206
- for (const measureElement of Array.from(measureElements)) {
282
+ const lastVoicePerStaff = new Map();
283
+ let divisions = 1;
284
+ for (const [mIdx, measureElement] of Array.from(measureElements).entries()) {
207
285
  let measureTimeSignature;
208
286
  let measureKeySignature;
209
- const isPickup = measureElement.getAttribute('implicit') === 'yes';
210
- const attributes = measureElement.querySelector('attributes');
287
+ let isPickup = measureElement.getAttribute('implicit') === 'yes';
288
+ const attributes = this.querySelector(measureElement, 'attributes');
211
289
  if (attributes) {
290
+ const divNode = this.querySelector(attributes, 'divisions');
291
+ if (divNode)
292
+ divisions = parseInt(divNode.textContent || '1');
212
293
  const fifths = this.getNumber(attributes, 'key fifths');
213
294
  if (fifths !== null) {
214
295
  this.currentKeyFifths = fifths;
@@ -220,7 +301,7 @@ export class MusicXMLParser {
220
301
  this.currentBeats = beats;
221
302
  if (beatType !== null)
222
303
  this.currentBeatType = beatType;
223
- const timeElement = attributes.querySelector('time');
304
+ const timeElement = this.querySelector(attributes, 'time');
224
305
  if (timeElement) {
225
306
  const symAttr = timeElement.getAttribute('symbol');
226
307
  if (symAttr === 'common')
@@ -236,16 +317,18 @@ export class MusicXMLParser {
236
317
  symbol: this.currentSymbol !== 'normal' ? this.currentSymbol : undefined,
237
318
  };
238
319
  }
239
- const clefElements = attributes.querySelectorAll('clef');
320
+ const clefElements = this.querySelectorAll(attributes, 'clef');
240
321
  clefElements.forEach((clef) => {
241
- const number = parseInt(clef.getAttribute('number') || '1');
322
+ const numberAttrs = clef.getAttribute('number');
323
+ const number = numberAttrs ? parseInt(numberAttrs) : 1;
242
324
  const sign = this.getText(clef, 'sign');
243
325
  if (sign && number <= numStaves) {
244
326
  const line = this.getNumber(clef, 'line') || 0;
245
- staffClefs[number - 1] = this.mapClef(sign, line);
327
+ const octaveChange = this.getNumber(clef, 'clef-octave-change') || 0;
328
+ staffClefs[number - 1] = this.mapClef(sign, line, octaveChange);
246
329
  }
247
330
  });
248
- const staffDetails = attributes.querySelectorAll('staff-details');
331
+ const staffDetails = this.querySelectorAll(attributes, 'staff-details');
249
332
  staffDetails.forEach((sd) => {
250
333
  const number = parseInt(sd.getAttribute('number') || '1');
251
334
  const lines = this.getNumber(sd, 'staff-lines');
@@ -258,8 +341,11 @@ export class MusicXMLParser {
258
341
  let systemText = undefined;
259
342
  let contextDynamic = undefined;
260
343
  const { repeats, volta, barlineStyle } = this.parseBarlines(measureElement);
261
- const measureVoices = Array.from({ length: numStaves }, () => []);
262
- const children = Array.from(measureElement.children);
344
+ // Map of staffIndex -> Map of voiceId -> NoteSetJSON[]
345
+ const staffVoices = new Map();
346
+ for (let i = 0; i < numStaves; i++)
347
+ staffVoices.set(i, new Map());
348
+ const children = this.getChildren(measureElement);
263
349
  let pendingChord = undefined;
264
350
  let currentNoteSetMap = new Map();
265
351
  const beamGroupIdMap = new Map();
@@ -274,24 +360,24 @@ export class MusicXMLParser {
274
360
  pendingChord = harmonyData;
275
361
  }
276
362
  else if (child.nodeName === 'direction') {
277
- const metronome = child.querySelector('metronome');
363
+ const metronome = this.querySelector(child, 'metronome');
278
364
  if (metronome) {
279
365
  const beatUnit = this.getText(metronome, 'beat-unit') || 'quarter';
280
366
  const bpm = parseInt(this.getText(metronome, 'per-minute') || '120');
281
- const isDotted = metronome.querySelector('beat-unit-dot') !== null;
367
+ const isDotted = this.querySelector(metronome, 'beat-unit-dot') !== null;
282
368
  measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted };
283
369
  }
284
- const rehearsal = child.querySelector('rehearsal');
370
+ const rehearsal = this.querySelector(child, 'rehearsal');
285
371
  if (rehearsal)
286
372
  rehearsalMark = rehearsal.textContent || undefined;
287
- const words = child.querySelector('words');
373
+ const words = this.querySelector(child, 'words');
288
374
  if (words) {
289
375
  if (measureTempo)
290
376
  measureTempo.text = words.textContent || undefined;
291
377
  else
292
378
  systemText = words.textContent || undefined;
293
379
  }
294
- const wedge = child.querySelector('wedge');
380
+ const wedge = this.querySelector(child, 'wedge');
295
381
  if (wedge) {
296
382
  const typeAttr = wedge.getAttribute('type');
297
383
  if (typeAttr === 'crescendo' || typeAttr === 'diminuendo' || typeAttr === 'stop') {
@@ -301,7 +387,7 @@ export class MusicXMLParser {
301
387
  };
302
388
  }
303
389
  }
304
- const octShift = child.querySelector('octave-shift');
390
+ const octShift = this.querySelector(child, 'octave-shift');
305
391
  if (octShift) {
306
392
  const typeAttr = octShift.getAttribute('type');
307
393
  const size = parseInt(octShift.getAttribute('size') || '8');
@@ -318,25 +404,27 @@ export class MusicXMLParser {
318
404
  };
319
405
  }
320
406
  }
321
- const pedalNode = child.querySelector('pedal');
407
+ const pedalNode = this.querySelector(child, 'pedal');
322
408
  if (pedalNode) {
323
409
  const typeAttr = pedalNode.getAttribute('type');
324
410
  if (typeAttr === 'start' || typeAttr === 'stop') {
325
411
  currentPedal = { type: 'sustain', placement: typeAttr };
326
412
  }
327
413
  }
328
- const dynamics = child.querySelector('dynamics');
329
- if (dynamics && dynamics.firstElementChild) {
330
- contextDynamic = dynamics.firstElementChild.nodeName.toLowerCase();
414
+ const dynamics = this.querySelector(child, 'dynamics');
415
+ if (dynamics) {
416
+ const firstChild = this.getFirstElementChild(dynamics);
417
+ if (firstChild)
418
+ contextDynamic = firstChild.nodeName.toLowerCase();
331
419
  }
332
420
  }
333
421
  else if (child.nodeName === 'note') {
334
422
  const staffNum = parseInt(this.getText(child, 'staff') || '1');
335
423
  const staffIdx = Math.min(Math.max(staffNum - 1, 0), numStaves - 1);
336
- const isChord = child.querySelector('chord') !== null;
424
+ const isChord = this.querySelector(child, 'chord') !== null;
337
425
  const note = this.parseNote(child);
338
426
  if (note) {
339
- const beamEl = child.querySelector('beam[number="1"]');
427
+ const beamEl = this.querySelector(child, 'beam[number="1"]');
340
428
  if (beamEl) {
341
429
  const beamType = beamEl.textContent;
342
430
  if (beamType === 'begin') {
@@ -375,6 +463,12 @@ export class MusicXMLParser {
375
463
  note.pedal = currentPedal;
376
464
  currentPedal = undefined;
377
465
  }
466
+ const voiceId = this.getText(child, 'voice') || '1';
467
+ lastVoicePerStaff.set(staffIdx, voiceId);
468
+ const voiceMap = staffVoices.get(staffIdx);
469
+ if (!voiceMap.has(voiceId))
470
+ voiceMap.set(voiceId, []);
471
+ const voiceNoteSets = voiceMap.get(voiceId);
378
472
  if (isChord) {
379
473
  const existing = currentNoteSetMap.get(staffIdx);
380
474
  if (existing)
@@ -385,26 +479,65 @@ export class MusicXMLParser {
385
479
  else {
386
480
  const existing = currentNoteSetMap.get(staffIdx);
387
481
  if (existing)
388
- measureVoices[staffIdx].push({ notes: existing });
482
+ voiceNoteSets.push({ notes: existing });
389
483
  currentNoteSetMap.set(staffIdx, [note]);
390
484
  }
391
485
  }
392
486
  }
393
487
  else if (child.nodeName === 'backup' || child.nodeName === 'forward') {
394
488
  for (const [sIdx, notes] of currentNoteSetMap.entries()) {
395
- if (notes.length > 0)
396
- measureVoices[sIdx].push({ notes });
489
+ if (notes.length > 0) {
490
+ const voiceId = lastVoicePerStaff.get(sIdx) || '1';
491
+ const voiceMap = staffVoices.get(sIdx);
492
+ if (!voiceMap.has(voiceId))
493
+ voiceMap.set(voiceId, []);
494
+ voiceMap.get(voiceId).push({ notes });
495
+ }
397
496
  }
398
497
  currentNoteSetMap.clear();
399
498
  }
400
499
  });
500
+ // Cleanup: push remaining notes
401
501
  for (const [sIdx, notes] of currentNoteSetMap.entries()) {
402
- if (notes.length > 0)
403
- measureVoices[sIdx].push({ notes });
502
+ if (notes.length > 0) {
503
+ const voiceId = lastVoicePerStaff.get(sIdx) || '1';
504
+ const voiceMap = staffVoices.get(sIdx);
505
+ if (!voiceMap.has(voiceId))
506
+ voiceMap.set(voiceId, []);
507
+ voiceMap.get(voiceId).push({ notes });
508
+ }
509
+ }
510
+ // Check if it's a pickup measure if mIdx is 0 and implicit is not set
511
+ if (mIdx === 0 && !isPickup) {
512
+ // Calculate the actual duration of the first staff's first voice
513
+ const vMap0 = staffVoices.get(0);
514
+ const v1 = vMap0.get('1') || vMap0.get(Array.from(vMap0.keys())[0]);
515
+ if (v1) {
516
+ // Check duration of first voice in first staff
517
+ const expectedDivisions = this.currentBeats * (divisions * 4 / this.currentBeatType);
518
+ let totalMeasureDivs = 0;
519
+ const notes = this.querySelectorAll(measureElement, 'note');
520
+ // Find first voice in first staff
521
+ for (const n of notes) {
522
+ if ((this.getText(n, 'staff') || '1') === '1' && (this.getText(n, 'voice') || '1') === (Array.from(vMap0.keys())[0] || '1')) {
523
+ if (this.querySelector(n, 'chord'))
524
+ continue;
525
+ totalMeasureDivs += this.getNumber(n, 'duration') || 0;
526
+ }
527
+ }
528
+ if (totalMeasureDivs > 0 && totalMeasureDivs < expectedDivisions - 1) {
529
+ isPickup = true;
530
+ }
531
+ }
404
532
  }
405
533
  for (let i = 0; i < numStaves; i++) {
534
+ const vMap = staffVoices.get(i);
535
+ let voicesIndices = Array.from(vMap.keys()).sort();
536
+ if (voicesIndices.length === 0)
537
+ voicesIndices = ['1'];
538
+ const voices = voicesIndices.map(id => vMap.get(id) || []);
406
539
  const measure = {
407
- voices: [measureVoices[i]],
540
+ voices: voices,
408
541
  timeSignature: measureTimeSignature,
409
542
  keySignature: measureKeySignature,
410
543
  isPickup: isPickup || undefined,
@@ -415,6 +548,30 @@ export class MusicXMLParser {
415
548
  systemText: i === 0 ? systemText : undefined,
416
549
  barlineStyle,
417
550
  };
551
+ // Padding with rests if not a pickup
552
+ if (!isPickup) {
553
+ const targetDur = (this.currentBeats * 4) / this.currentBeatType;
554
+ measure.voices = measure.voices.map((v) => {
555
+ 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);
558
+ }, 0);
559
+ if (currentDur < targetDur - 0.001) {
560
+ const gap = targetDur - currentDur;
561
+ const rests = decomposeDuration(gap).map((d) => ({
562
+ notes: [
563
+ {
564
+ duration: d.duration,
565
+ isRest: true,
566
+ isDotted: d.isDotted,
567
+ },
568
+ ],
569
+ }));
570
+ return [...v, ...rests];
571
+ }
572
+ return v;
573
+ });
574
+ }
418
575
  staves[i].measures.push(measure);
419
576
  staves[i].clef = staffClefs[i];
420
577
  }
@@ -422,21 +579,21 @@ export class MusicXMLParser {
422
579
  return staves;
423
580
  }
424
581
  parseNote(noteElement) {
425
- const isRest = noteElement.querySelector('rest') !== null;
426
- const isGrace = noteElement.querySelector('grace') !== null;
582
+ const isRest = this.querySelector(noteElement, 'rest') !== null;
583
+ const isGrace = this.querySelector(noteElement, 'grace') !== null;
427
584
  const type = this.getText(noteElement, 'type');
428
585
  const duration = type ? this.mapDuration(type) : Duration.Quarter;
429
- const isDotted = noteElement.querySelector('dot') !== null;
586
+ const isDotted = this.querySelector(noteElement, 'dot') !== null;
430
587
  if (isRest)
431
588
  return { duration, isRest: true, isDotted };
432
589
  let step = 'C', octave = 4, alter = 0;
433
- const unpitched = noteElement.querySelector('unpitched');
590
+ const unpitched = this.querySelector(noteElement, 'unpitched');
434
591
  if (unpitched) {
435
592
  step = this.getText(unpitched, 'display-step') || 'C';
436
593
  octave = this.getNumber(unpitched, 'display-octave') ?? 4;
437
594
  }
438
595
  else {
439
- const pitch = noteElement.querySelector('pitch');
596
+ const pitch = this.querySelector(noteElement, 'pitch');
440
597
  if (!pitch)
441
598
  return null;
442
599
  step = this.getText(pitch, 'step') || 'C';
@@ -444,7 +601,7 @@ export class MusicXMLParser {
444
601
  alter = this.getNumber(pitch, 'alter') ?? 0;
445
602
  }
446
603
  let midiNumber = this.pitchToMidi(step, octave, alter);
447
- const instrument = noteElement.querySelector('instrument');
604
+ const instrument = this.querySelector(noteElement, 'instrument');
448
605
  if (instrument) {
449
606
  const instId = instrument.getAttribute('id');
450
607
  if (instId && this.instrumentPitchMap.has(instId))
@@ -478,17 +635,17 @@ export class MusicXMLParser {
478
635
  stemDirection = StemDirection.Up;
479
636
  else if (stem === 'down')
480
637
  stemDirection = StemDirection.Down;
481
- const notations = noteElement.querySelector('notations');
638
+ const notations = this.querySelector(noteElement, 'notations');
482
639
  // Tuplet ratio
483
640
  let tuplet = undefined;
484
- const timeMod = noteElement.querySelector('time-modification');
641
+ const timeMod = this.querySelector(noteElement, 'time-modification');
485
642
  if (timeMod) {
486
643
  const actual = this.getNumber(timeMod, 'actual-notes') || 3;
487
644
  const normal = this.getNumber(timeMod, 'normal-notes') || 2;
488
645
  tuplet = { actual, normal, type: 'middle' };
489
646
  }
490
647
  if (notations) {
491
- const tupletEl = notations.querySelector('tuplet');
648
+ const tupletEl = this.querySelector(notations, 'tuplet');
492
649
  if (tupletEl && tuplet) {
493
650
  const type = tupletEl.getAttribute('type');
494
651
  if (type === 'start')
@@ -498,7 +655,7 @@ export class MusicXMLParser {
498
655
  }
499
656
  }
500
657
  let slur = undefined;
501
- const slurEl = notations?.querySelector('slur');
658
+ const slurEl = notations ? this.querySelector(notations, 'slur') : null;
502
659
  if (slurEl) {
503
660
  const type = slurEl.getAttribute('type');
504
661
  if (type === 'start' || type === 'stop') {
@@ -510,29 +667,29 @@ export class MusicXMLParser {
510
667
  slur.direction = 'down';
511
668
  }
512
669
  }
513
- const tie = noteElement.querySelector('tie')?.getAttribute('type') === 'start' || undefined;
670
+ const tie = this.querySelector(noteElement, 'tie')?.getAttribute('type') === 'start' || undefined;
514
671
  const articulations = [];
515
- const articEl = notations?.querySelector('articulations');
672
+ const articEl = notations ? this.querySelector(notations, 'articulations') : null;
516
673
  if (articEl) {
517
- if (articEl.querySelector('staccato'))
674
+ if (this.querySelector(articEl, 'staccato'))
518
675
  articulations.push(Articulation.Staccato);
519
- if (articEl.querySelector('accent'))
676
+ if (this.querySelector(articEl, 'accent'))
520
677
  articulations.push(Articulation.Accent);
521
- if (articEl.querySelector('tenuto'))
678
+ if (this.querySelector(articEl, 'tenuto'))
522
679
  articulations.push(Articulation.Tenuto);
523
- if (articEl.querySelector('marcato'))
680
+ if (this.querySelector(articEl, 'marcato'))
524
681
  articulations.push(Articulation.Marcato);
525
- if (articEl.querySelector('staccatissimo'))
682
+ if (this.querySelector(articEl, 'staccatissimo'))
526
683
  articulations.push(Articulation.Staccatissimo);
527
- if (articEl.querySelector('caesura'))
684
+ if (this.querySelector(articEl, 'caesura'))
528
685
  articulations.push(Articulation.Caesura);
529
- if (articEl.querySelector('breath-mark'))
686
+ if (this.querySelector(articEl, 'breath-mark'))
530
687
  articulations.push(Articulation.BreathMark);
531
688
  }
532
- if (notations?.querySelector('fermata'))
689
+ if (notations && this.querySelector(notations, 'fermata'))
533
690
  articulations.push(Articulation.Fermata);
534
691
  let arpeggio = undefined;
535
- const arpeggiateEl = notations?.querySelector('arpeggiate');
692
+ const arpeggiateEl = notations ? this.querySelector(notations, 'arpeggiate') : null;
536
693
  if (arpeggiateEl) {
537
694
  const dir = arpeggiateEl.getAttribute('direction');
538
695
  if (dir === 'up')
@@ -542,23 +699,23 @@ export class MusicXMLParser {
542
699
  else
543
700
  arpeggio = Arpeggio.Normal;
544
701
  }
545
- const ornaments = notations?.querySelector('ornaments');
702
+ const ornaments = notations ? this.querySelector(notations, 'ornaments') : null;
546
703
  let ornament = undefined;
547
704
  if (ornaments) {
548
- if (ornaments.querySelector('trill-mark'))
705
+ if (this.querySelector(ornaments, 'trill-mark'))
549
706
  ornament = Ornament.Trill;
550
- else if (ornaments.querySelector('mordent'))
707
+ else if (this.querySelector(ornaments, 'mordent'))
551
708
  ornament = Ornament.Mordent;
552
- else if (ornaments.querySelector('inverted-mordent'))
709
+ else if (this.querySelector(ornaments, 'inverted-mordent'))
553
710
  ornament = Ornament.InvertedMordent;
554
- else if (ornaments.querySelector('turn'))
711
+ else if (this.querySelector(ornaments, 'turn'))
555
712
  ornament = Ornament.Turn;
556
- else if (ornaments.querySelector('inverted-turn'))
713
+ else if (this.querySelector(ornaments, 'inverted-turn'))
557
714
  ornament = Ornament.InvertedTurn;
558
- else if (ornaments.querySelector('tremolo'))
715
+ else if (this.querySelector(ornaments, 'tremolo'))
559
716
  ornament = Ornament.Tremolo;
560
717
  }
561
- const technical = notations?.querySelector('technical');
718
+ const technical = notations ? this.querySelector(notations, 'technical') : null;
562
719
  let bowing = undefined;
563
720
  let fingering = undefined;
564
721
  let fret = undefined;
@@ -568,33 +725,33 @@ export class MusicXMLParser {
568
725
  let hammerOn = undefined;
569
726
  let pullOff = undefined;
570
727
  if (technical) {
571
- if (technical.querySelector('up-bow'))
728
+ if (this.querySelector(technical, 'up-bow'))
572
729
  bowing = Bowing.UpBow;
573
- else if (technical.querySelector('down-bow'))
730
+ else if (this.querySelector(technical, 'down-bow'))
574
731
  bowing = Bowing.DownBow;
575
- const fingeringEl = technical.querySelector('fingering');
732
+ const fingeringEl = this.querySelector(technical, 'fingering');
576
733
  if (fingeringEl)
577
734
  fingering = parseInt(fingeringEl.textContent || '0');
578
- const fretEl = technical.querySelector('fret');
735
+ const fretEl = this.querySelector(technical, 'fret');
579
736
  if (fretEl)
580
737
  fret = parseInt(fretEl.textContent || '0');
581
- const stringEl = technical.querySelector('string');
738
+ const stringEl = this.querySelector(technical, 'string');
582
739
  if (stringEl) {
583
740
  stringNumber = parseInt(stringEl.textContent || '0');
584
741
  isStringCircled = true;
585
742
  }
586
- const palmMuteEl = technical.querySelector('palm-mute');
743
+ const palmMuteEl = this.querySelector(technical, 'palm-mute');
587
744
  if (palmMuteEl)
588
745
  palmMute = palmMuteEl.getAttribute('type') || 'start';
589
- const hammerOnEl = technical.querySelector('hammer-on');
746
+ const hammerOnEl = this.querySelector(technical, 'hammer-on');
590
747
  if (hammerOnEl)
591
748
  hammerOn = hammerOnEl.getAttribute('type') || 'start';
592
- const pullOffEl = technical.querySelector('pull-off');
749
+ const pullOffEl = this.querySelector(technical, 'pull-off');
593
750
  if (pullOffEl)
594
751
  pullOff = pullOffEl.getAttribute('type') || 'start';
595
752
  }
596
753
  let glissando = undefined;
597
- const glissEl = notations?.querySelector('glissando') || notations?.querySelector('slide');
754
+ const glissEl = notations ? (this.querySelector(notations, 'glissando') || this.querySelector(notations, 'slide')) : null;
598
755
  if (glissEl) {
599
756
  const type = glissEl.getAttribute('type');
600
757
  if (type === 'start' || type === 'stop') {
@@ -604,13 +761,14 @@ export class MusicXMLParser {
604
761
  };
605
762
  }
606
763
  }
607
- const notesDyn = notations?.querySelector('dynamics');
608
- const dynamic = notesDyn?.firstElementChild?.nodeName.toLowerCase();
764
+ const notesDyn = notations ? this.querySelector(notations, 'dynamics') : null;
765
+ const firstChild = notesDyn ? this.getFirstElementChild(notesDyn) : null;
766
+ const dynamic = firstChild?.nodeName.toLowerCase();
609
767
  const lyrics = [];
610
- noteElement.querySelectorAll('lyric').forEach((lyricEl) => {
768
+ this.querySelectorAll(noteElement, 'lyric').forEach((lyricEl) => {
611
769
  const text = this.getText(lyricEl, 'text') || '';
612
770
  const syllabic = this.getText(lyricEl, 'syllabic');
613
- const extend = lyricEl.querySelector('extend') !== null;
771
+ const extend = this.querySelector(lyricEl, 'extend') !== null;
614
772
  const number = parseInt(lyricEl.getAttribute('number') || '1');
615
773
  if (text || extend) {
616
774
  lyrics[number - 1] = {
@@ -648,7 +806,7 @@ export class MusicXMLParser {
648
806
  };
649
807
  }
650
808
  parseHarmony(harmonyElement) {
651
- const root = harmonyElement.querySelector('root');
809
+ const root = this.querySelector(harmonyElement, 'root');
652
810
  if (!root)
653
811
  return null;
654
812
  const step = this.getText(root, 'root-step');
@@ -709,11 +867,11 @@ export class MusicXMLParser {
709
867
  }
710
868
  // Parse Frame (Fretboard Diagram)
711
869
  let diagram = undefined;
712
- const frame = harmonyElement.querySelector('frame');
870
+ const frame = this.querySelector(harmonyElement, 'frame');
713
871
  if (frame) {
714
872
  diagram = this.parseFrame(frame);
715
873
  }
716
- const bass = harmonyElement.querySelector('bass');
874
+ const bass = this.querySelector(harmonyElement, 'bass');
717
875
  let bassPart = '';
718
876
  if (bass) {
719
877
  const bStep = this.getText(bass, 'bass-step');
@@ -738,11 +896,11 @@ export class MusicXMLParser {
738
896
  const openStrings = [];
739
897
  const mutedStrings = [];
740
898
  const openBarres = new Map(); // fret -> startString
741
- frame.querySelectorAll('frame-note').forEach((fn) => {
899
+ this.querySelectorAll(frame, 'frame-note').forEach((fn) => {
742
900
  const string = this.getNumber(fn, 'string');
743
901
  const fret = this.getNumber(fn, 'fret');
744
902
  const fingering = this.getText(fn, 'fingering');
745
- const barre = fn.querySelector('barre')?.getAttribute('type'); // start/stop
903
+ const barre = this.querySelector(fn, 'barre')?.getAttribute('type'); // start/stop
746
904
  if (string !== null && fret !== null) {
747
905
  if (fret === 0) {
748
906
  openStrings.push(string);
@@ -784,7 +942,7 @@ export class MusicXMLParser {
784
942
  const repeats = [];
785
943
  let volta = undefined;
786
944
  let barlineStyle = BarlineStyle.Regular;
787
- measureElement.querySelectorAll('barline').forEach((barline) => {
945
+ this.querySelectorAll(measureElement, 'barline').forEach((barline) => {
788
946
  const location = barline.getAttribute('location') || 'right';
789
947
  const barStyle = this.getText(barline, 'bar-style');
790
948
  if (location === 'right') {
@@ -795,7 +953,7 @@ export class MusicXMLParser {
795
953
  else if (barStyle === 'dotted')
796
954
  barlineStyle = BarlineStyle.Dotted;
797
955
  }
798
- const repeatEl = barline.querySelector('repeat');
956
+ const repeatEl = this.querySelector(barline, 'repeat');
799
957
  if (repeatEl) {
800
958
  const direction = repeatEl.getAttribute('direction');
801
959
  const times = repeatEl.getAttribute('times');
@@ -804,7 +962,7 @@ export class MusicXMLParser {
804
962
  times: times ? parseInt(times) : undefined,
805
963
  });
806
964
  }
807
- const ending = barline.querySelector('ending');
965
+ const ending = this.querySelector(barline, 'ending');
808
966
  if (ending) {
809
967
  const type = ending.getAttribute('type');
810
968
  const numberAttr = ending.getAttribute('number') || '1';
@@ -891,12 +1049,12 @@ export class MusicXMLParser {
891
1049
  };
892
1050
  return map[type] ?? Duration.Quarter;
893
1051
  }
894
- mapClef(sign, line = 0) {
1052
+ mapClef(sign, line = 0, octaveChange = 0) {
895
1053
  switch (sign.toUpperCase()) {
896
1054
  case 'G':
897
- return Clef.Treble;
1055
+ return octaveChange === -1 ? Clef.Treble8vaBassa : Clef.Treble;
898
1056
  case 'F':
899
- return Clef.Bass;
1057
+ return octaveChange === -1 ? Clef.Bass8vaBassa : Clef.Bass;
900
1058
  case 'C':
901
1059
  return line === 4 ? Clef.Tenor : Clef.Alto;
902
1060
  case 'PERCUSSION':
@@ -925,7 +1083,7 @@ export class MusicXMLParser {
925
1083
  return map[step.toUpperCase()] ?? 0;
926
1084
  }
927
1085
  getText(element, tagName) {
928
- const child = element.querySelector(tagName);
1086
+ const child = this.querySelector(element, tagName);
929
1087
  return child?.textContent ?? null;
930
1088
  }
931
1089
  getNumber(element, tagName) {