@scorelabs/core 1.0.9 → 1.0.11

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,6 +1,7 @@
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, NoteheadShape, Ornament, OttavaType, StemDirection, calculateDurationValue, decomposeDuration, } from '../models/types.js';
4
+ // TODO: Write tests for MusicXMLParser. It is important to test it thoroughly.
4
5
  /**
5
6
  * MusicXML Parser
6
7
  *
@@ -18,6 +19,52 @@ export class MusicXMLParser {
18
19
  this._domParser =
19
20
  domParserInstance || (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
20
21
  }
22
+ querySelector(el, selector) {
23
+ if (el.querySelector)
24
+ return el.querySelector(selector);
25
+ const parts = selector.split(' ');
26
+ let current = el;
27
+ for (const part of parts) {
28
+ if (!current)
29
+ return null;
30
+ const results = current.getElementsByTagName(part);
31
+ if (results.length === 0)
32
+ return null;
33
+ current = results[0];
34
+ }
35
+ return current;
36
+ }
37
+ getChildren(el) {
38
+ const parent = el;
39
+ if (parent.children)
40
+ return Array.from(parent.children);
41
+ return Array.from(el.childNodes).filter((node) => node.nodeType === 1);
42
+ }
43
+ getFirstElementChild(el) {
44
+ if (el.firstElementChild)
45
+ return el.firstElementChild;
46
+ const children = this.getChildren(el);
47
+ return children.length > 0 ? children[0] : null;
48
+ }
49
+ querySelectorAll(el, selector) {
50
+ if (el.querySelectorAll)
51
+ return Array.from(el.querySelectorAll(selector));
52
+ const parts = selector.split(' ');
53
+ if (parts.length === 1) {
54
+ return Array.from(el.getElementsByTagName(parts[0]));
55
+ }
56
+ // Simple nested selector support (only two levels for now as needed)
57
+ if (parts.length === 2) {
58
+ const firstLevel = Array.from(el.getElementsByTagName(parts[0]));
59
+ const results = [];
60
+ firstLevel.forEach((parent) => {
61
+ const children = Array.from(parent.getElementsByTagName(parts[1]));
62
+ results.push(...children);
63
+ });
64
+ return results;
65
+ }
66
+ return [];
67
+ }
21
68
  parseFromString(xmlString) {
22
69
  if (!this._domParser) {
23
70
  throw new Error('No DOMParser available. Please provide one in the constructor for Node.js environments.');
@@ -35,7 +82,7 @@ export class MusicXMLParser {
35
82
  if (!_parser)
36
83
  throw new Error('No DOMParser available');
37
84
  const containerDoc = _parser.parseFromString(containerXml, 'application/xml');
38
- const rootfile = containerDoc.querySelector('rootfile');
85
+ const rootfile = new MusicXMLParser(_parser).querySelector(containerDoc, 'rootfile');
39
86
  const fullPath = rootfile?.getAttribute('full-path');
40
87
  if (!fullPath)
41
88
  throw new Error('Invalid MXL: Could not find main score file path');
@@ -63,7 +110,7 @@ export class MusicXMLParser {
63
110
  }
64
111
  xmlString = xmlString.substring(firstBracket).trim();
65
112
  const doc = this.parseFromString(xmlString);
66
- const parseError = doc.querySelector('parsererror');
113
+ const parseError = this.querySelector(doc, 'parsererror');
67
114
  if (parseError) {
68
115
  const preview = xmlString.substring(0, 100).replace(/\n/g, ' ');
69
116
  throw new Error(`XML parsing error: ${parseError.textContent}. Content preview: "${preview}..."`);
@@ -74,12 +121,15 @@ export class MusicXMLParser {
74
121
  const title = this.parseTitle(doc);
75
122
  const subtitle = this.parseSubtitle(doc);
76
123
  const composer = this.parseComposer(doc);
124
+ const lyricist = this.parseLyricist(doc);
125
+ const copyright = this.parseCopyright(doc);
77
126
  const partInfo = this.parsePartList(doc);
78
127
  const parts = this.parseParts(doc, partInfo);
79
128
  let globalBpm = 120;
80
129
  let globalTempoDuration = Duration.Quarter;
81
130
  let globalTempoIsDotted = false;
82
131
  let globalTempoText = '';
132
+ let globalTempoDotCount = 0;
83
133
  if (parts.length > 0 && parts[0].staves.length > 0 && parts[0].staves[0].measures.length > 0) {
84
134
  const firstMeasure = parts[0].staves[0].measures[0];
85
135
  if (firstMeasure.tempo) {
@@ -87,6 +137,7 @@ export class MusicXMLParser {
87
137
  globalTempoDuration = firstMeasure.tempo.duration;
88
138
  globalTempoIsDotted = firstMeasure.tempo.isDotted;
89
139
  globalTempoText = firstMeasure.tempo.text || '';
140
+ globalTempoDotCount = firstMeasure.tempo.dotCount || 0;
90
141
  }
91
142
  }
92
143
  const score = {
@@ -104,6 +155,9 @@ export class MusicXMLParser {
104
155
  tempoDuration: globalTempoDuration,
105
156
  tempoIsDotted: globalTempoIsDotted,
106
157
  tempoText: globalTempoText,
158
+ tempoDotCount: globalTempoDotCount,
159
+ copyright,
160
+ lyricist,
107
161
  };
108
162
  return this.postProcess(score);
109
163
  }
@@ -111,7 +165,7 @@ export class MusicXMLParser {
111
165
  let subtitle = this.getText(doc.documentElement, 'movement-subtitle');
112
166
  if (subtitle)
113
167
  return subtitle;
114
- const work = doc.querySelector('work');
168
+ const work = this.querySelector(doc, 'work');
115
169
  if (work) {
116
170
  subtitle = this.getText(work, 'work-subtitle');
117
171
  if (subtitle)
@@ -120,11 +174,11 @@ export class MusicXMLParser {
120
174
  if (subtitle)
121
175
  return subtitle;
122
176
  }
123
- const credits = doc.querySelectorAll('credit');
177
+ const credits = this.querySelectorAll(doc, 'credit');
124
178
  for (const credit of Array.from(credits)) {
125
- const type = credit.querySelector('credit-type');
179
+ const type = this.querySelector(credit, 'credit-type');
126
180
  if (type?.textContent?.toLowerCase() === 'subtitle') {
127
- const words = credit.querySelector('credit-words');
181
+ const words = this.querySelector(credit, 'credit-words');
128
182
  if (words?.textContent)
129
183
  return words.textContent;
130
184
  }
@@ -137,23 +191,73 @@ export class MusicXMLParser {
137
191
  title = this.getText(doc.documentElement, 'movement-title');
138
192
  return title ?? 'Untitled';
139
193
  }
194
+ parseCopyright(doc) {
195
+ const identification = this.querySelector(doc, 'identification');
196
+ if (identification) {
197
+ const rights = this.getText(identification, 'rights');
198
+ if (rights)
199
+ return rights;
200
+ }
201
+ const credits = this.querySelectorAll(doc, 'credit');
202
+ for (const credit of Array.from(credits)) {
203
+ const type = this.querySelector(credit, 'credit-type');
204
+ if (type?.textContent?.toLowerCase() === 'rights' ||
205
+ type?.textContent?.toLowerCase() === 'copyright') {
206
+ const words = this.querySelector(credit, 'credit-words');
207
+ if (words?.textContent)
208
+ return words.textContent;
209
+ }
210
+ }
211
+ return '';
212
+ }
140
213
  parseComposer(doc) {
141
- const creators = doc.querySelectorAll('identification creator');
214
+ const creators = this.querySelectorAll(doc, 'identification creator');
142
215
  for (const creator of Array.from(creators)) {
143
216
  if (creator.getAttribute('type') === 'composer')
144
217
  return creator.textContent ?? 'Unknown';
145
218
  }
146
- return doc.querySelector('identification creator')?.textContent ?? 'Unknown';
219
+ // If no type="composer", but there is only one creator, assume it's the composer
220
+ if (creators.length === 1)
221
+ return (creators[0].textContent ?? 'Unknown');
222
+ // Check credits for composer type
223
+ const credits = this.querySelectorAll(doc, 'credit');
224
+ for (const credit of Array.from(credits)) {
225
+ const type = this.querySelector(credit, 'credit-type');
226
+ if (type?.textContent?.toLowerCase() === 'composer') {
227
+ const words = this.querySelector(credit, 'credit-words');
228
+ if (words?.textContent)
229
+ return words.textContent;
230
+ }
231
+ }
232
+ return creators.length > 0 ? creators[0].textContent ?? 'Unknown' : 'Unknown';
233
+ }
234
+ parseLyricist(doc) {
235
+ const creators = this.querySelectorAll(doc, 'identification creator');
236
+ for (const creator of Array.from(creators)) {
237
+ if (creator.getAttribute('type') === 'lyricist')
238
+ return creator.textContent ?? '';
239
+ }
240
+ // Check credits for lyricist type
241
+ const credits = this.querySelectorAll(doc, 'credit');
242
+ for (const credit of Array.from(credits)) {
243
+ const type = this.querySelector(credit, 'credit-type');
244
+ if (type?.textContent?.toLowerCase() === 'lyricist') {
245
+ const words = this.querySelector(credit, 'credit-words');
246
+ if (words?.textContent)
247
+ return words.textContent;
248
+ }
249
+ }
250
+ return '';
147
251
  }
148
252
  parsePartList(doc) {
149
253
  const partInfo = new Map();
150
- const scoreParts = doc.querySelectorAll('part-list score-part');
254
+ const scoreParts = this.querySelectorAll(doc, 'part-list score-part');
151
255
  for (const scorePart of Array.from(scoreParts)) {
152
256
  const id = scorePart.getAttribute('id');
153
257
  const name = this.getText(scorePart, 'part-name') || 'Part';
154
258
  let instrument;
155
259
  // Find first midi-instrument to determine main instrument
156
- const firstMidiInst = scorePart.querySelector('midi-instrument');
260
+ const firstMidiInst = this.querySelector(scorePart, 'midi-instrument');
157
261
  if (firstMidiInst) {
158
262
  const programStr = this.getText(firstMidiInst, 'midi-program');
159
263
  if (programStr) {
@@ -176,7 +280,7 @@ export class MusicXMLParser {
176
280
  if (id) {
177
281
  partInfo.set(id, { name: name.trim(), instrument });
178
282
  }
179
- const midiInstruments = scorePart.querySelectorAll('midi-instrument');
283
+ const midiInstruments = this.querySelectorAll(scorePart, 'midi-instrument');
180
284
  for (const midiInst of Array.from(midiInstruments)) {
181
285
  const instId = midiInst.getAttribute('id');
182
286
  const unpitched = this.getNumber(midiInst, 'midi-unpitched');
@@ -188,7 +292,7 @@ export class MusicXMLParser {
188
292
  }
189
293
  parseParts(doc, partInfo) {
190
294
  const parts = [];
191
- const partElements = doc.querySelectorAll('part');
295
+ const partElements = this.querySelectorAll(doc, 'part');
192
296
  for (const partElement of Array.from(partElements)) {
193
297
  const id = partElement.getAttribute('id');
194
298
  const info = partInfo.get(id ?? '');
@@ -199,11 +303,11 @@ export class MusicXMLParser {
199
303
  return parts;
200
304
  }
201
305
  parsePart(partElement) {
202
- const measureElements = partElement.querySelectorAll('measure');
306
+ const measureElements = this.querySelectorAll(partElement, 'measure');
203
307
  let numStaves = 1;
204
- const firstMeasure = partElement.querySelector('measure');
308
+ const firstMeasure = this.querySelector(partElement, 'measure');
205
309
  if (firstMeasure) {
206
- const stavesNode = firstMeasure.querySelector('attributes staves');
310
+ const stavesNode = this.querySelector(firstMeasure, 'attributes staves');
207
311
  if (stavesNode)
208
312
  numStaves = parseInt(stavesNode.textContent || '1');
209
313
  }
@@ -212,12 +316,17 @@ export class MusicXMLParser {
212
316
  measures: [],
213
317
  }));
214
318
  const staffClefs = Array(numStaves).fill(Clef.Treble);
215
- for (const measureElement of Array.from(measureElements)) {
319
+ const lastVoicePerStaff = new Map();
320
+ let divisions = 1;
321
+ for (const [mIdx, measureElement] of Array.from(measureElements).entries()) {
216
322
  let measureTimeSignature;
217
323
  let measureKeySignature;
218
- const isPickup = measureElement.getAttribute('implicit') === 'yes';
219
- const attributes = measureElement.querySelector('attributes');
324
+ let isPickup = measureElement.getAttribute('implicit') === 'yes';
325
+ const attributes = this.querySelector(measureElement, 'attributes');
220
326
  if (attributes) {
327
+ const divNode = this.querySelector(attributes, 'divisions');
328
+ if (divNode)
329
+ divisions = parseInt(divNode.textContent || '1');
221
330
  const fifths = this.getNumber(attributes, 'key fifths');
222
331
  if (fifths !== null) {
223
332
  this.currentKeyFifths = fifths;
@@ -229,7 +338,7 @@ export class MusicXMLParser {
229
338
  this.currentBeats = beats;
230
339
  if (beatType !== null)
231
340
  this.currentBeatType = beatType;
232
- const timeElement = attributes.querySelector('time');
341
+ const timeElement = this.querySelector(attributes, 'time');
233
342
  if (timeElement) {
234
343
  const symAttr = timeElement.getAttribute('symbol');
235
344
  if (symAttr === 'common')
@@ -245,9 +354,10 @@ export class MusicXMLParser {
245
354
  symbol: this.currentSymbol !== 'normal' ? this.currentSymbol : undefined,
246
355
  };
247
356
  }
248
- const clefElements = attributes.querySelectorAll('clef');
357
+ const clefElements = this.querySelectorAll(attributes, 'clef');
249
358
  clefElements.forEach((clef) => {
250
- const number = parseInt(clef.getAttribute('number') || '1');
359
+ const numberAttrs = clef.getAttribute('number');
360
+ const number = numberAttrs ? parseInt(numberAttrs) : 1;
251
361
  const sign = this.getText(clef, 'sign');
252
362
  if (sign && number <= numStaves) {
253
363
  const line = this.getNumber(clef, 'line') || 0;
@@ -255,7 +365,7 @@ export class MusicXMLParser {
255
365
  staffClefs[number - 1] = this.mapClef(sign, line, octaveChange);
256
366
  }
257
367
  });
258
- const staffDetails = attributes.querySelectorAll('staff-details');
368
+ const staffDetails = this.querySelectorAll(attributes, 'staff-details');
259
369
  staffDetails.forEach((sd) => {
260
370
  const number = parseInt(sd.getAttribute('number') || '1');
261
371
  const lines = this.getNumber(sd, 'staff-lines');
@@ -272,9 +382,9 @@ export class MusicXMLParser {
272
382
  const staffVoices = new Map();
273
383
  for (let i = 0; i < numStaves; i++)
274
384
  staffVoices.set(i, new Map());
275
- const children = Array.from(measureElement.children);
385
+ const children = this.getChildren(measureElement);
276
386
  let pendingChord = undefined;
277
- let currentNoteSetMap = new Map();
387
+ const currentNoteSetMap = new Map();
278
388
  const beamGroupIdMap = new Map();
279
389
  let globalBeamId = 1;
280
390
  let currentHairpin = undefined;
@@ -287,24 +397,25 @@ export class MusicXMLParser {
287
397
  pendingChord = harmonyData;
288
398
  }
289
399
  else if (child.nodeName === 'direction') {
290
- const metronome = child.querySelector('metronome');
400
+ const metronome = this.querySelector(child, 'metronome');
291
401
  if (metronome) {
292
402
  const beatUnit = this.getText(metronome, 'beat-unit') || 'quarter';
293
403
  const bpm = parseInt(this.getText(metronome, 'per-minute') || '120');
294
- const isDotted = metronome.querySelector('beat-unit-dot') !== null;
295
- measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted };
404
+ const dotCount = this.querySelectorAll(metronome, 'beat-unit-dot').length;
405
+ const isDotted = dotCount > 0;
406
+ measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted, dotCount };
296
407
  }
297
- const rehearsal = child.querySelector('rehearsal');
408
+ const rehearsal = this.querySelector(child, 'rehearsal');
298
409
  if (rehearsal)
299
410
  rehearsalMark = rehearsal.textContent || undefined;
300
- const words = child.querySelector('words');
411
+ const words = this.querySelector(child, 'words');
301
412
  if (words) {
302
413
  if (measureTempo)
303
414
  measureTempo.text = words.textContent || undefined;
304
415
  else
305
416
  systemText = words.textContent || undefined;
306
417
  }
307
- const wedge = child.querySelector('wedge');
418
+ const wedge = this.querySelector(child, 'wedge');
308
419
  if (wedge) {
309
420
  const typeAttr = wedge.getAttribute('type');
310
421
  if (typeAttr === 'crescendo' || typeAttr === 'diminuendo' || typeAttr === 'stop') {
@@ -314,7 +425,7 @@ export class MusicXMLParser {
314
425
  };
315
426
  }
316
427
  }
317
- const octShift = child.querySelector('octave-shift');
428
+ const octShift = this.querySelector(child, 'octave-shift');
318
429
  if (octShift) {
319
430
  const typeAttr = octShift.getAttribute('type');
320
431
  const size = parseInt(octShift.getAttribute('size') || '8');
@@ -331,25 +442,27 @@ export class MusicXMLParser {
331
442
  };
332
443
  }
333
444
  }
334
- const pedalNode = child.querySelector('pedal');
445
+ const pedalNode = this.querySelector(child, 'pedal');
335
446
  if (pedalNode) {
336
447
  const typeAttr = pedalNode.getAttribute('type');
337
448
  if (typeAttr === 'start' || typeAttr === 'stop') {
338
449
  currentPedal = { type: 'sustain', placement: typeAttr };
339
450
  }
340
451
  }
341
- const dynamics = child.querySelector('dynamics');
342
- if (dynamics && dynamics.firstElementChild) {
343
- contextDynamic = dynamics.firstElementChild.nodeName.toLowerCase();
452
+ const dynamics = this.querySelector(child, 'dynamics');
453
+ if (dynamics) {
454
+ const firstChild = this.getFirstElementChild(dynamics);
455
+ if (firstChild)
456
+ contextDynamic = firstChild.nodeName.toLowerCase();
344
457
  }
345
458
  }
346
459
  else if (child.nodeName === 'note') {
347
460
  const staffNum = parseInt(this.getText(child, 'staff') || '1');
348
461
  const staffIdx = Math.min(Math.max(staffNum - 1, 0), numStaves - 1);
349
- const isChord = child.querySelector('chord') !== null;
462
+ const isChord = this.querySelector(child, 'chord') !== null;
350
463
  const note = this.parseNote(child);
351
464
  if (note) {
352
- const beamEl = child.querySelector('beam[number="1"]');
465
+ const beamEl = this.querySelector(child, 'beam[number="1"]');
353
466
  if (beamEl) {
354
467
  const beamType = beamEl.textContent;
355
468
  if (beamType === 'begin') {
@@ -389,6 +502,7 @@ export class MusicXMLParser {
389
502
  currentPedal = undefined;
390
503
  }
391
504
  const voiceId = this.getText(child, 'voice') || '1';
505
+ lastVoicePerStaff.set(staffIdx, voiceId);
392
506
  const voiceMap = staffVoices.get(staffIdx);
393
507
  if (!voiceMap.has(voiceId))
394
508
  voiceMap.set(voiceId, []);
@@ -411,7 +525,7 @@ export class MusicXMLParser {
411
525
  else if (child.nodeName === 'backup' || child.nodeName === 'forward') {
412
526
  for (const [sIdx, notes] of currentNoteSetMap.entries()) {
413
527
  if (notes.length > 0) {
414
- const voiceId = this.getText(child.previousElementSibling, 'voice') || '1';
528
+ const voiceId = lastVoicePerStaff.get(sIdx) || '1';
415
529
  const voiceMap = staffVoices.get(sIdx);
416
530
  if (!voiceMap.has(voiceId))
417
531
  voiceMap.set(voiceId, []);
@@ -421,22 +535,46 @@ export class MusicXMLParser {
421
535
  currentNoteSetMap.clear();
422
536
  }
423
537
  });
538
+ // Cleanup: push remaining notes
424
539
  for (const [sIdx, notes] of currentNoteSetMap.entries()) {
425
540
  if (notes.length > 0) {
426
- // Last note in measure
427
- const voiceId = '1'; // Defaulting to 1 for final push if not tracked
541
+ const voiceId = lastVoicePerStaff.get(sIdx) || '1';
428
542
  const voiceMap = staffVoices.get(sIdx);
429
543
  if (!voiceMap.has(voiceId))
430
544
  voiceMap.set(voiceId, []);
431
545
  voiceMap.get(voiceId).push({ notes });
432
546
  }
433
547
  }
548
+ // Check if it's a pickup measure if mIdx is 0 and implicit is not set
549
+ if (mIdx === 0 && !isPickup) {
550
+ // Calculate the actual duration of the first staff's first voice
551
+ const vMap0 = staffVoices.get(0);
552
+ const v1 = vMap0.get('1') || vMap0.get(Array.from(vMap0.keys())[0]);
553
+ if (v1) {
554
+ // Check duration of first voice in first staff
555
+ const expectedDivisions = this.currentBeats * ((divisions * 4) / this.currentBeatType);
556
+ let totalMeasureDivs = 0;
557
+ const notes = this.querySelectorAll(measureElement, 'note');
558
+ // Find first voice in first staff
559
+ for (const n of notes) {
560
+ if ((this.getText(n, 'staff') || '1') === '1' &&
561
+ (this.getText(n, 'voice') || '1') === (Array.from(vMap0.keys())[0] || '1')) {
562
+ if (this.querySelector(n, 'chord'))
563
+ continue;
564
+ totalMeasureDivs += this.getNumber(n, 'duration') || 0;
565
+ }
566
+ }
567
+ if (totalMeasureDivs > 0 && totalMeasureDivs < expectedDivisions - 1) {
568
+ isPickup = true;
569
+ }
570
+ }
571
+ }
434
572
  for (let i = 0; i < numStaves; i++) {
435
573
  const vMap = staffVoices.get(i);
436
574
  let voicesIndices = Array.from(vMap.keys()).sort();
437
575
  if (voicesIndices.length === 0)
438
576
  voicesIndices = ['1'];
439
- const voices = voicesIndices.map(id => vMap.get(id) || []);
577
+ const voices = voicesIndices.map((id) => vMap.get(id) || []);
440
578
  const measure = {
441
579
  voices: voices,
442
580
  timeSignature: measureTimeSignature,
@@ -454,8 +592,7 @@ export class MusicXMLParser {
454
592
  const targetDur = (this.currentBeats * 4) / this.currentBeatType;
455
593
  measure.voices = measure.voices.map((v) => {
456
594
  const currentDur = v.reduce((sum, ns) => {
457
- const base = DURATION_VALUES[ns.notes[0].duration];
458
- return sum + (ns.notes[0].isDotted ? base * 1.5 : base);
595
+ return sum + calculateDurationValue(ns.notes[0].duration, ns.notes[0].dotCount || (ns.notes[0].isDotted ? 1 : 0));
459
596
  }, 0);
460
597
  if (currentDur < targetDur - 0.001) {
461
598
  const gap = targetDur - currentDur;
@@ -480,21 +617,22 @@ export class MusicXMLParser {
480
617
  return staves;
481
618
  }
482
619
  parseNote(noteElement) {
483
- const isRest = noteElement.querySelector('rest') !== null;
484
- const isGrace = noteElement.querySelector('grace') !== null;
620
+ const isRest = this.querySelector(noteElement, 'rest') !== null;
621
+ const isGrace = this.querySelector(noteElement, 'grace') !== null;
485
622
  const type = this.getText(noteElement, 'type');
486
623
  const duration = type ? this.mapDuration(type) : Duration.Quarter;
487
- const isDotted = noteElement.querySelector('dot') !== null;
624
+ const dotCount = this.querySelectorAll(noteElement, 'dot').length;
625
+ const isDotted = dotCount > 0;
488
626
  if (isRest)
489
- return { duration, isRest: true, isDotted };
627
+ return { duration, isRest: true, isDotted, dotCount };
490
628
  let step = 'C', octave = 4, alter = 0;
491
- const unpitched = noteElement.querySelector('unpitched');
629
+ const unpitched = this.querySelector(noteElement, 'unpitched');
492
630
  if (unpitched) {
493
631
  step = this.getText(unpitched, 'display-step') || 'C';
494
632
  octave = this.getNumber(unpitched, 'display-octave') ?? 4;
495
633
  }
496
634
  else {
497
- const pitch = noteElement.querySelector('pitch');
635
+ const pitch = this.querySelector(noteElement, 'pitch');
498
636
  if (!pitch)
499
637
  return null;
500
638
  step = this.getText(pitch, 'step') || 'C';
@@ -502,7 +640,7 @@ export class MusicXMLParser {
502
640
  alter = this.getNumber(pitch, 'alter') ?? 0;
503
641
  }
504
642
  let midiNumber = this.pitchToMidi(step, octave, alter);
505
- const instrument = noteElement.querySelector('instrument');
643
+ const instrument = this.querySelector(noteElement, 'instrument');
506
644
  if (instrument) {
507
645
  const instId = instrument.getAttribute('id');
508
646
  if (instId && this.instrumentPitchMap.has(instId))
@@ -536,17 +674,17 @@ export class MusicXMLParser {
536
674
  stemDirection = StemDirection.Up;
537
675
  else if (stem === 'down')
538
676
  stemDirection = StemDirection.Down;
539
- const notations = noteElement.querySelector('notations');
677
+ const notations = this.querySelector(noteElement, 'notations');
540
678
  // Tuplet ratio
541
679
  let tuplet = undefined;
542
- const timeMod = noteElement.querySelector('time-modification');
680
+ const timeMod = this.querySelector(noteElement, 'time-modification');
543
681
  if (timeMod) {
544
682
  const actual = this.getNumber(timeMod, 'actual-notes') || 3;
545
683
  const normal = this.getNumber(timeMod, 'normal-notes') || 2;
546
684
  tuplet = { actual, normal, type: 'middle' };
547
685
  }
548
686
  if (notations) {
549
- const tupletEl = notations.querySelector('tuplet');
687
+ const tupletEl = this.querySelector(notations, 'tuplet');
550
688
  if (tupletEl && tuplet) {
551
689
  const type = tupletEl.getAttribute('type');
552
690
  if (type === 'start')
@@ -556,7 +694,7 @@ export class MusicXMLParser {
556
694
  }
557
695
  }
558
696
  let slur = undefined;
559
- const slurEl = notations?.querySelector('slur');
697
+ const slurEl = notations ? this.querySelector(notations, 'slur') : null;
560
698
  if (slurEl) {
561
699
  const type = slurEl.getAttribute('type');
562
700
  if (type === 'start' || type === 'stop') {
@@ -568,29 +706,29 @@ export class MusicXMLParser {
568
706
  slur.direction = 'down';
569
707
  }
570
708
  }
571
- const tie = noteElement.querySelector('tie')?.getAttribute('type') === 'start' || undefined;
709
+ const tie = this.querySelector(noteElement, 'tie')?.getAttribute('type') === 'start' || undefined;
572
710
  const articulations = [];
573
- const articEl = notations?.querySelector('articulations');
711
+ const articEl = notations ? this.querySelector(notations, 'articulations') : null;
574
712
  if (articEl) {
575
- if (articEl.querySelector('staccato'))
713
+ if (this.querySelector(articEl, 'staccato'))
576
714
  articulations.push(Articulation.Staccato);
577
- if (articEl.querySelector('accent'))
715
+ if (this.querySelector(articEl, 'accent'))
578
716
  articulations.push(Articulation.Accent);
579
- if (articEl.querySelector('tenuto'))
717
+ if (this.querySelector(articEl, 'tenuto'))
580
718
  articulations.push(Articulation.Tenuto);
581
- if (articEl.querySelector('marcato'))
719
+ if (this.querySelector(articEl, 'marcato'))
582
720
  articulations.push(Articulation.Marcato);
583
- if (articEl.querySelector('staccatissimo'))
721
+ if (this.querySelector(articEl, 'staccatissimo'))
584
722
  articulations.push(Articulation.Staccatissimo);
585
- if (articEl.querySelector('caesura'))
723
+ if (this.querySelector(articEl, 'caesura'))
586
724
  articulations.push(Articulation.Caesura);
587
- if (articEl.querySelector('breath-mark'))
725
+ if (this.querySelector(articEl, 'breath-mark'))
588
726
  articulations.push(Articulation.BreathMark);
589
727
  }
590
- if (notations?.querySelector('fermata'))
728
+ if (notations && this.querySelector(notations, 'fermata'))
591
729
  articulations.push(Articulation.Fermata);
592
730
  let arpeggio = undefined;
593
- const arpeggiateEl = notations?.querySelector('arpeggiate');
731
+ const arpeggiateEl = notations ? this.querySelector(notations, 'arpeggiate') : null;
594
732
  if (arpeggiateEl) {
595
733
  const dir = arpeggiateEl.getAttribute('direction');
596
734
  if (dir === 'up')
@@ -600,23 +738,23 @@ export class MusicXMLParser {
600
738
  else
601
739
  arpeggio = Arpeggio.Normal;
602
740
  }
603
- const ornaments = notations?.querySelector('ornaments');
741
+ const ornaments = notations ? this.querySelector(notations, 'ornaments') : null;
604
742
  let ornament = undefined;
605
743
  if (ornaments) {
606
- if (ornaments.querySelector('trill-mark'))
744
+ if (this.querySelector(ornaments, 'trill-mark'))
607
745
  ornament = Ornament.Trill;
608
- else if (ornaments.querySelector('mordent'))
746
+ else if (this.querySelector(ornaments, 'mordent'))
609
747
  ornament = Ornament.Mordent;
610
- else if (ornaments.querySelector('inverted-mordent'))
748
+ else if (this.querySelector(ornaments, 'inverted-mordent'))
611
749
  ornament = Ornament.InvertedMordent;
612
- else if (ornaments.querySelector('turn'))
750
+ else if (this.querySelector(ornaments, 'turn'))
613
751
  ornament = Ornament.Turn;
614
- else if (ornaments.querySelector('inverted-turn'))
752
+ else if (this.querySelector(ornaments, 'inverted-turn'))
615
753
  ornament = Ornament.InvertedTurn;
616
- else if (ornaments.querySelector('tremolo'))
754
+ else if (this.querySelector(ornaments, 'tremolo'))
617
755
  ornament = Ornament.Tremolo;
618
756
  }
619
- const technical = notations?.querySelector('technical');
757
+ const technical = notations ? this.querySelector(notations, 'technical') : null;
620
758
  let bowing = undefined;
621
759
  let fingering = undefined;
622
760
  let fret = undefined;
@@ -626,33 +764,35 @@ export class MusicXMLParser {
626
764
  let hammerOn = undefined;
627
765
  let pullOff = undefined;
628
766
  if (technical) {
629
- if (technical.querySelector('up-bow'))
767
+ if (this.querySelector(technical, 'up-bow'))
630
768
  bowing = Bowing.UpBow;
631
- else if (technical.querySelector('down-bow'))
769
+ else if (this.querySelector(technical, 'down-bow'))
632
770
  bowing = Bowing.DownBow;
633
- const fingeringEl = technical.querySelector('fingering');
771
+ const fingeringEl = this.querySelector(technical, 'fingering');
634
772
  if (fingeringEl)
635
773
  fingering = parseInt(fingeringEl.textContent || '0');
636
- const fretEl = technical.querySelector('fret');
774
+ const fretEl = this.querySelector(technical, 'fret');
637
775
  if (fretEl)
638
776
  fret = parseInt(fretEl.textContent || '0');
639
- const stringEl = technical.querySelector('string');
777
+ const stringEl = this.querySelector(technical, 'string');
640
778
  if (stringEl) {
641
779
  stringNumber = parseInt(stringEl.textContent || '0');
642
780
  isStringCircled = true;
643
781
  }
644
- const palmMuteEl = technical.querySelector('palm-mute');
782
+ const palmMuteEl = this.querySelector(technical, 'palm-mute');
645
783
  if (palmMuteEl)
646
784
  palmMute = palmMuteEl.getAttribute('type') || 'start';
647
- const hammerOnEl = technical.querySelector('hammer-on');
785
+ const hammerOnEl = this.querySelector(technical, 'hammer-on');
648
786
  if (hammerOnEl)
649
787
  hammerOn = hammerOnEl.getAttribute('type') || 'start';
650
- const pullOffEl = technical.querySelector('pull-off');
788
+ const pullOffEl = this.querySelector(technical, 'pull-off');
651
789
  if (pullOffEl)
652
790
  pullOff = pullOffEl.getAttribute('type') || 'start';
653
791
  }
654
792
  let glissando = undefined;
655
- const glissEl = notations?.querySelector('glissando') || notations?.querySelector('slide');
793
+ const glissEl = notations
794
+ ? this.querySelector(notations, 'glissando') || this.querySelector(notations, 'slide')
795
+ : null;
656
796
  if (glissEl) {
657
797
  const type = glissEl.getAttribute('type');
658
798
  if (type === 'start' || type === 'stop') {
@@ -662,13 +802,14 @@ export class MusicXMLParser {
662
802
  };
663
803
  }
664
804
  }
665
- const notesDyn = notations?.querySelector('dynamics');
666
- const dynamic = notesDyn?.firstElementChild?.nodeName.toLowerCase();
805
+ const notesDyn = notations ? this.querySelector(notations, 'dynamics') : null;
806
+ const firstChild = notesDyn ? this.getFirstElementChild(notesDyn) : null;
807
+ const dynamic = firstChild?.nodeName.toLowerCase();
667
808
  const lyrics = [];
668
- noteElement.querySelectorAll('lyric').forEach((lyricEl) => {
809
+ this.querySelectorAll(noteElement, 'lyric').forEach((lyricEl) => {
669
810
  const text = this.getText(lyricEl, 'text') || '';
670
811
  const syllabic = this.getText(lyricEl, 'syllabic');
671
- const extend = lyricEl.querySelector('extend') !== null;
812
+ const extend = this.querySelector(lyricEl, 'extend') !== null;
672
813
  const number = parseInt(lyricEl.getAttribute('number') || '1');
673
814
  if (text || extend) {
674
815
  lyrics[number - 1] = {
@@ -682,6 +823,7 @@ export class MusicXMLParser {
682
823
  duration,
683
824
  pitch: { midiNumber, step: this.stepToNumber(step), alter, octave },
684
825
  isDotted,
826
+ dotCount,
685
827
  accidental,
686
828
  slur,
687
829
  tie,
@@ -706,7 +848,7 @@ export class MusicXMLParser {
706
848
  };
707
849
  }
708
850
  parseHarmony(harmonyElement) {
709
- const root = harmonyElement.querySelector('root');
851
+ const root = this.querySelector(harmonyElement, 'root');
710
852
  if (!root)
711
853
  return null;
712
854
  const step = this.getText(root, 'root-step');
@@ -767,11 +909,11 @@ export class MusicXMLParser {
767
909
  }
768
910
  // Parse Frame (Fretboard Diagram)
769
911
  let diagram = undefined;
770
- const frame = harmonyElement.querySelector('frame');
912
+ const frame = this.querySelector(harmonyElement, 'frame');
771
913
  if (frame) {
772
914
  diagram = this.parseFrame(frame);
773
915
  }
774
- const bass = harmonyElement.querySelector('bass');
916
+ const bass = this.querySelector(harmonyElement, 'bass');
775
917
  let bassPart = '';
776
918
  if (bass) {
777
919
  const bStep = this.getText(bass, 'bass-step');
@@ -796,11 +938,11 @@ export class MusicXMLParser {
796
938
  const openStrings = [];
797
939
  const mutedStrings = [];
798
940
  const openBarres = new Map(); // fret -> startString
799
- frame.querySelectorAll('frame-note').forEach((fn) => {
941
+ this.querySelectorAll(frame, 'frame-note').forEach((fn) => {
800
942
  const string = this.getNumber(fn, 'string');
801
943
  const fret = this.getNumber(fn, 'fret');
802
944
  const fingering = this.getText(fn, 'fingering');
803
- const barre = fn.querySelector('barre')?.getAttribute('type'); // start/stop
945
+ const barre = this.querySelector(fn, 'barre')?.getAttribute('type'); // start/stop
804
946
  if (string !== null && fret !== null) {
805
947
  if (fret === 0) {
806
948
  openStrings.push(string);
@@ -842,7 +984,7 @@ export class MusicXMLParser {
842
984
  const repeats = [];
843
985
  let volta = undefined;
844
986
  let barlineStyle = BarlineStyle.Regular;
845
- measureElement.querySelectorAll('barline').forEach((barline) => {
987
+ this.querySelectorAll(measureElement, 'barline').forEach((barline) => {
846
988
  const location = barline.getAttribute('location') || 'right';
847
989
  const barStyle = this.getText(barline, 'bar-style');
848
990
  if (location === 'right') {
@@ -853,7 +995,7 @@ export class MusicXMLParser {
853
995
  else if (barStyle === 'dotted')
854
996
  barlineStyle = BarlineStyle.Dotted;
855
997
  }
856
- const repeatEl = barline.querySelector('repeat');
998
+ const repeatEl = this.querySelector(barline, 'repeat');
857
999
  if (repeatEl) {
858
1000
  const direction = repeatEl.getAttribute('direction');
859
1001
  const times = repeatEl.getAttribute('times');
@@ -862,7 +1004,7 @@ export class MusicXMLParser {
862
1004
  times: times ? parseInt(times) : undefined,
863
1005
  });
864
1006
  }
865
- const ending = barline.querySelector('ending');
1007
+ const ending = this.querySelector(barline, 'ending');
866
1008
  if (ending) {
867
1009
  const type = ending.getAttribute('type');
868
1010
  const numberAttr = ending.getAttribute('number') || '1';
@@ -983,7 +1125,7 @@ export class MusicXMLParser {
983
1125
  return map[step.toUpperCase()] ?? 0;
984
1126
  }
985
1127
  getText(element, tagName) {
986
- const child = element.querySelector(tagName);
1128
+ const child = this.querySelector(element, tagName);
987
1129
  return child?.textContent ?? null;
988
1130
  }
989
1131
  getNumber(element, tagName) {