@scorelabs/core 1.0.9 → 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.
@@ -13,12 +13,17 @@ export declare class MusicXMLParser {
13
13
  private instrumentPitchMap;
14
14
  private _domParser;
15
15
  constructor(domParserInstance?: DOMParser | unknown);
16
+ private querySelector;
17
+ private getChildren;
18
+ private getFirstElementChild;
19
+ private querySelectorAll;
16
20
  private parseFromString;
17
21
  static getXMLFromBinary(data: ArrayBuffer, domParser?: DOMParser | unknown): Promise<string>;
18
22
  parseBinary(data: ArrayBuffer): Promise<ScoreJSON>;
19
23
  parse(xmlString: string): ScoreJSON;
20
24
  private parseSubtitle;
21
25
  private parseTitle;
26
+ private parseCopyright;
22
27
  private parseComposer;
23
28
  private parsePartList;
24
29
  private parseParts;
@@ -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,23 +185,42 @@ 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
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) {
@@ -176,7 +243,7 @@ export class MusicXMLParser {
176
243
  if (id) {
177
244
  partInfo.set(id, { name: name.trim(), instrument });
178
245
  }
179
- const midiInstruments = scorePart.querySelectorAll('midi-instrument');
246
+ const midiInstruments = this.querySelectorAll(scorePart, 'midi-instrument');
180
247
  for (const midiInst of Array.from(midiInstruments)) {
181
248
  const instId = midiInst.getAttribute('id');
182
249
  const unpitched = this.getNumber(midiInst, 'midi-unpitched');
@@ -188,7 +255,7 @@ export class MusicXMLParser {
188
255
  }
189
256
  parseParts(doc, partInfo) {
190
257
  const parts = [];
191
- const partElements = doc.querySelectorAll('part');
258
+ const partElements = this.querySelectorAll(doc, 'part');
192
259
  for (const partElement of Array.from(partElements)) {
193
260
  const id = partElement.getAttribute('id');
194
261
  const info = partInfo.get(id ?? '');
@@ -199,11 +266,11 @@ export class MusicXMLParser {
199
266
  return parts;
200
267
  }
201
268
  parsePart(partElement) {
202
- const measureElements = partElement.querySelectorAll('measure');
269
+ const measureElements = this.querySelectorAll(partElement, 'measure');
203
270
  let numStaves = 1;
204
- const firstMeasure = partElement.querySelector('measure');
271
+ const firstMeasure = this.querySelector(partElement, 'measure');
205
272
  if (firstMeasure) {
206
- const stavesNode = firstMeasure.querySelector('attributes staves');
273
+ const stavesNode = this.querySelector(firstMeasure, 'attributes staves');
207
274
  if (stavesNode)
208
275
  numStaves = parseInt(stavesNode.textContent || '1');
209
276
  }
@@ -212,12 +279,17 @@ export class MusicXMLParser {
212
279
  measures: [],
213
280
  }));
214
281
  const staffClefs = Array(numStaves).fill(Clef.Treble);
215
- 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()) {
216
285
  let measureTimeSignature;
217
286
  let measureKeySignature;
218
- const isPickup = measureElement.getAttribute('implicit') === 'yes';
219
- const attributes = measureElement.querySelector('attributes');
287
+ let isPickup = measureElement.getAttribute('implicit') === 'yes';
288
+ const attributes = this.querySelector(measureElement, 'attributes');
220
289
  if (attributes) {
290
+ const divNode = this.querySelector(attributes, 'divisions');
291
+ if (divNode)
292
+ divisions = parseInt(divNode.textContent || '1');
221
293
  const fifths = this.getNumber(attributes, 'key fifths');
222
294
  if (fifths !== null) {
223
295
  this.currentKeyFifths = fifths;
@@ -229,7 +301,7 @@ export class MusicXMLParser {
229
301
  this.currentBeats = beats;
230
302
  if (beatType !== null)
231
303
  this.currentBeatType = beatType;
232
- const timeElement = attributes.querySelector('time');
304
+ const timeElement = this.querySelector(attributes, 'time');
233
305
  if (timeElement) {
234
306
  const symAttr = timeElement.getAttribute('symbol');
235
307
  if (symAttr === 'common')
@@ -245,9 +317,10 @@ export class MusicXMLParser {
245
317
  symbol: this.currentSymbol !== 'normal' ? this.currentSymbol : undefined,
246
318
  };
247
319
  }
248
- const clefElements = attributes.querySelectorAll('clef');
320
+ const clefElements = this.querySelectorAll(attributes, 'clef');
249
321
  clefElements.forEach((clef) => {
250
- const number = parseInt(clef.getAttribute('number') || '1');
322
+ const numberAttrs = clef.getAttribute('number');
323
+ const number = numberAttrs ? parseInt(numberAttrs) : 1;
251
324
  const sign = this.getText(clef, 'sign');
252
325
  if (sign && number <= numStaves) {
253
326
  const line = this.getNumber(clef, 'line') || 0;
@@ -255,7 +328,7 @@ export class MusicXMLParser {
255
328
  staffClefs[number - 1] = this.mapClef(sign, line, octaveChange);
256
329
  }
257
330
  });
258
- const staffDetails = attributes.querySelectorAll('staff-details');
331
+ const staffDetails = this.querySelectorAll(attributes, 'staff-details');
259
332
  staffDetails.forEach((sd) => {
260
333
  const number = parseInt(sd.getAttribute('number') || '1');
261
334
  const lines = this.getNumber(sd, 'staff-lines');
@@ -272,7 +345,7 @@ export class MusicXMLParser {
272
345
  const staffVoices = new Map();
273
346
  for (let i = 0; i < numStaves; i++)
274
347
  staffVoices.set(i, new Map());
275
- const children = Array.from(measureElement.children);
348
+ const children = this.getChildren(measureElement);
276
349
  let pendingChord = undefined;
277
350
  let currentNoteSetMap = new Map();
278
351
  const beamGroupIdMap = new Map();
@@ -287,24 +360,24 @@ export class MusicXMLParser {
287
360
  pendingChord = harmonyData;
288
361
  }
289
362
  else if (child.nodeName === 'direction') {
290
- const metronome = child.querySelector('metronome');
363
+ const metronome = this.querySelector(child, 'metronome');
291
364
  if (metronome) {
292
365
  const beatUnit = this.getText(metronome, 'beat-unit') || 'quarter';
293
366
  const bpm = parseInt(this.getText(metronome, 'per-minute') || '120');
294
- const isDotted = metronome.querySelector('beat-unit-dot') !== null;
367
+ const isDotted = this.querySelector(metronome, 'beat-unit-dot') !== null;
295
368
  measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted };
296
369
  }
297
- const rehearsal = child.querySelector('rehearsal');
370
+ const rehearsal = this.querySelector(child, 'rehearsal');
298
371
  if (rehearsal)
299
372
  rehearsalMark = rehearsal.textContent || undefined;
300
- const words = child.querySelector('words');
373
+ const words = this.querySelector(child, 'words');
301
374
  if (words) {
302
375
  if (measureTempo)
303
376
  measureTempo.text = words.textContent || undefined;
304
377
  else
305
378
  systemText = words.textContent || undefined;
306
379
  }
307
- const wedge = child.querySelector('wedge');
380
+ const wedge = this.querySelector(child, 'wedge');
308
381
  if (wedge) {
309
382
  const typeAttr = wedge.getAttribute('type');
310
383
  if (typeAttr === 'crescendo' || typeAttr === 'diminuendo' || typeAttr === 'stop') {
@@ -314,7 +387,7 @@ export class MusicXMLParser {
314
387
  };
315
388
  }
316
389
  }
317
- const octShift = child.querySelector('octave-shift');
390
+ const octShift = this.querySelector(child, 'octave-shift');
318
391
  if (octShift) {
319
392
  const typeAttr = octShift.getAttribute('type');
320
393
  const size = parseInt(octShift.getAttribute('size') || '8');
@@ -331,25 +404,27 @@ export class MusicXMLParser {
331
404
  };
332
405
  }
333
406
  }
334
- const pedalNode = child.querySelector('pedal');
407
+ const pedalNode = this.querySelector(child, 'pedal');
335
408
  if (pedalNode) {
336
409
  const typeAttr = pedalNode.getAttribute('type');
337
410
  if (typeAttr === 'start' || typeAttr === 'stop') {
338
411
  currentPedal = { type: 'sustain', placement: typeAttr };
339
412
  }
340
413
  }
341
- const dynamics = child.querySelector('dynamics');
342
- if (dynamics && dynamics.firstElementChild) {
343
- 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();
344
419
  }
345
420
  }
346
421
  else if (child.nodeName === 'note') {
347
422
  const staffNum = parseInt(this.getText(child, 'staff') || '1');
348
423
  const staffIdx = Math.min(Math.max(staffNum - 1, 0), numStaves - 1);
349
- const isChord = child.querySelector('chord') !== null;
424
+ const isChord = this.querySelector(child, 'chord') !== null;
350
425
  const note = this.parseNote(child);
351
426
  if (note) {
352
- const beamEl = child.querySelector('beam[number="1"]');
427
+ const beamEl = this.querySelector(child, 'beam[number="1"]');
353
428
  if (beamEl) {
354
429
  const beamType = beamEl.textContent;
355
430
  if (beamType === 'begin') {
@@ -389,6 +464,7 @@ export class MusicXMLParser {
389
464
  currentPedal = undefined;
390
465
  }
391
466
  const voiceId = this.getText(child, 'voice') || '1';
467
+ lastVoicePerStaff.set(staffIdx, voiceId);
392
468
  const voiceMap = staffVoices.get(staffIdx);
393
469
  if (!voiceMap.has(voiceId))
394
470
  voiceMap.set(voiceId, []);
@@ -411,7 +487,7 @@ export class MusicXMLParser {
411
487
  else if (child.nodeName === 'backup' || child.nodeName === 'forward') {
412
488
  for (const [sIdx, notes] of currentNoteSetMap.entries()) {
413
489
  if (notes.length > 0) {
414
- const voiceId = this.getText(child.previousElementSibling, 'voice') || '1';
490
+ const voiceId = lastVoicePerStaff.get(sIdx) || '1';
415
491
  const voiceMap = staffVoices.get(sIdx);
416
492
  if (!voiceMap.has(voiceId))
417
493
  voiceMap.set(voiceId, []);
@@ -421,16 +497,39 @@ export class MusicXMLParser {
421
497
  currentNoteSetMap.clear();
422
498
  }
423
499
  });
500
+ // Cleanup: push remaining notes
424
501
  for (const [sIdx, notes] of currentNoteSetMap.entries()) {
425
502
  if (notes.length > 0) {
426
- // Last note in measure
427
- const voiceId = '1'; // Defaulting to 1 for final push if not tracked
503
+ const voiceId = lastVoicePerStaff.get(sIdx) || '1';
428
504
  const voiceMap = staffVoices.get(sIdx);
429
505
  if (!voiceMap.has(voiceId))
430
506
  voiceMap.set(voiceId, []);
431
507
  voiceMap.get(voiceId).push({ notes });
432
508
  }
433
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
+ }
532
+ }
434
533
  for (let i = 0; i < numStaves; i++) {
435
534
  const vMap = staffVoices.get(i);
436
535
  let voicesIndices = Array.from(vMap.keys()).sort();
@@ -480,21 +579,21 @@ export class MusicXMLParser {
480
579
  return staves;
481
580
  }
482
581
  parseNote(noteElement) {
483
- const isRest = noteElement.querySelector('rest') !== null;
484
- const isGrace = noteElement.querySelector('grace') !== null;
582
+ const isRest = this.querySelector(noteElement, 'rest') !== null;
583
+ const isGrace = this.querySelector(noteElement, 'grace') !== null;
485
584
  const type = this.getText(noteElement, 'type');
486
585
  const duration = type ? this.mapDuration(type) : Duration.Quarter;
487
- const isDotted = noteElement.querySelector('dot') !== null;
586
+ const isDotted = this.querySelector(noteElement, 'dot') !== null;
488
587
  if (isRest)
489
588
  return { duration, isRest: true, isDotted };
490
589
  let step = 'C', octave = 4, alter = 0;
491
- const unpitched = noteElement.querySelector('unpitched');
590
+ const unpitched = this.querySelector(noteElement, 'unpitched');
492
591
  if (unpitched) {
493
592
  step = this.getText(unpitched, 'display-step') || 'C';
494
593
  octave = this.getNumber(unpitched, 'display-octave') ?? 4;
495
594
  }
496
595
  else {
497
- const pitch = noteElement.querySelector('pitch');
596
+ const pitch = this.querySelector(noteElement, 'pitch');
498
597
  if (!pitch)
499
598
  return null;
500
599
  step = this.getText(pitch, 'step') || 'C';
@@ -502,7 +601,7 @@ export class MusicXMLParser {
502
601
  alter = this.getNumber(pitch, 'alter') ?? 0;
503
602
  }
504
603
  let midiNumber = this.pitchToMidi(step, octave, alter);
505
- const instrument = noteElement.querySelector('instrument');
604
+ const instrument = this.querySelector(noteElement, 'instrument');
506
605
  if (instrument) {
507
606
  const instId = instrument.getAttribute('id');
508
607
  if (instId && this.instrumentPitchMap.has(instId))
@@ -536,17 +635,17 @@ export class MusicXMLParser {
536
635
  stemDirection = StemDirection.Up;
537
636
  else if (stem === 'down')
538
637
  stemDirection = StemDirection.Down;
539
- const notations = noteElement.querySelector('notations');
638
+ const notations = this.querySelector(noteElement, 'notations');
540
639
  // Tuplet ratio
541
640
  let tuplet = undefined;
542
- const timeMod = noteElement.querySelector('time-modification');
641
+ const timeMod = this.querySelector(noteElement, 'time-modification');
543
642
  if (timeMod) {
544
643
  const actual = this.getNumber(timeMod, 'actual-notes') || 3;
545
644
  const normal = this.getNumber(timeMod, 'normal-notes') || 2;
546
645
  tuplet = { actual, normal, type: 'middle' };
547
646
  }
548
647
  if (notations) {
549
- const tupletEl = notations.querySelector('tuplet');
648
+ const tupletEl = this.querySelector(notations, 'tuplet');
550
649
  if (tupletEl && tuplet) {
551
650
  const type = tupletEl.getAttribute('type');
552
651
  if (type === 'start')
@@ -556,7 +655,7 @@ export class MusicXMLParser {
556
655
  }
557
656
  }
558
657
  let slur = undefined;
559
- const slurEl = notations?.querySelector('slur');
658
+ const slurEl = notations ? this.querySelector(notations, 'slur') : null;
560
659
  if (slurEl) {
561
660
  const type = slurEl.getAttribute('type');
562
661
  if (type === 'start' || type === 'stop') {
@@ -568,29 +667,29 @@ export class MusicXMLParser {
568
667
  slur.direction = 'down';
569
668
  }
570
669
  }
571
- const tie = noteElement.querySelector('tie')?.getAttribute('type') === 'start' || undefined;
670
+ const tie = this.querySelector(noteElement, 'tie')?.getAttribute('type') === 'start' || undefined;
572
671
  const articulations = [];
573
- const articEl = notations?.querySelector('articulations');
672
+ const articEl = notations ? this.querySelector(notations, 'articulations') : null;
574
673
  if (articEl) {
575
- if (articEl.querySelector('staccato'))
674
+ if (this.querySelector(articEl, 'staccato'))
576
675
  articulations.push(Articulation.Staccato);
577
- if (articEl.querySelector('accent'))
676
+ if (this.querySelector(articEl, 'accent'))
578
677
  articulations.push(Articulation.Accent);
579
- if (articEl.querySelector('tenuto'))
678
+ if (this.querySelector(articEl, 'tenuto'))
580
679
  articulations.push(Articulation.Tenuto);
581
- if (articEl.querySelector('marcato'))
680
+ if (this.querySelector(articEl, 'marcato'))
582
681
  articulations.push(Articulation.Marcato);
583
- if (articEl.querySelector('staccatissimo'))
682
+ if (this.querySelector(articEl, 'staccatissimo'))
584
683
  articulations.push(Articulation.Staccatissimo);
585
- if (articEl.querySelector('caesura'))
684
+ if (this.querySelector(articEl, 'caesura'))
586
685
  articulations.push(Articulation.Caesura);
587
- if (articEl.querySelector('breath-mark'))
686
+ if (this.querySelector(articEl, 'breath-mark'))
588
687
  articulations.push(Articulation.BreathMark);
589
688
  }
590
- if (notations?.querySelector('fermata'))
689
+ if (notations && this.querySelector(notations, 'fermata'))
591
690
  articulations.push(Articulation.Fermata);
592
691
  let arpeggio = undefined;
593
- const arpeggiateEl = notations?.querySelector('arpeggiate');
692
+ const arpeggiateEl = notations ? this.querySelector(notations, 'arpeggiate') : null;
594
693
  if (arpeggiateEl) {
595
694
  const dir = arpeggiateEl.getAttribute('direction');
596
695
  if (dir === 'up')
@@ -600,23 +699,23 @@ export class MusicXMLParser {
600
699
  else
601
700
  arpeggio = Arpeggio.Normal;
602
701
  }
603
- const ornaments = notations?.querySelector('ornaments');
702
+ const ornaments = notations ? this.querySelector(notations, 'ornaments') : null;
604
703
  let ornament = undefined;
605
704
  if (ornaments) {
606
- if (ornaments.querySelector('trill-mark'))
705
+ if (this.querySelector(ornaments, 'trill-mark'))
607
706
  ornament = Ornament.Trill;
608
- else if (ornaments.querySelector('mordent'))
707
+ else if (this.querySelector(ornaments, 'mordent'))
609
708
  ornament = Ornament.Mordent;
610
- else if (ornaments.querySelector('inverted-mordent'))
709
+ else if (this.querySelector(ornaments, 'inverted-mordent'))
611
710
  ornament = Ornament.InvertedMordent;
612
- else if (ornaments.querySelector('turn'))
711
+ else if (this.querySelector(ornaments, 'turn'))
613
712
  ornament = Ornament.Turn;
614
- else if (ornaments.querySelector('inverted-turn'))
713
+ else if (this.querySelector(ornaments, 'inverted-turn'))
615
714
  ornament = Ornament.InvertedTurn;
616
- else if (ornaments.querySelector('tremolo'))
715
+ else if (this.querySelector(ornaments, 'tremolo'))
617
716
  ornament = Ornament.Tremolo;
618
717
  }
619
- const technical = notations?.querySelector('technical');
718
+ const technical = notations ? this.querySelector(notations, 'technical') : null;
620
719
  let bowing = undefined;
621
720
  let fingering = undefined;
622
721
  let fret = undefined;
@@ -626,33 +725,33 @@ export class MusicXMLParser {
626
725
  let hammerOn = undefined;
627
726
  let pullOff = undefined;
628
727
  if (technical) {
629
- if (technical.querySelector('up-bow'))
728
+ if (this.querySelector(technical, 'up-bow'))
630
729
  bowing = Bowing.UpBow;
631
- else if (technical.querySelector('down-bow'))
730
+ else if (this.querySelector(technical, 'down-bow'))
632
731
  bowing = Bowing.DownBow;
633
- const fingeringEl = technical.querySelector('fingering');
732
+ const fingeringEl = this.querySelector(technical, 'fingering');
634
733
  if (fingeringEl)
635
734
  fingering = parseInt(fingeringEl.textContent || '0');
636
- const fretEl = technical.querySelector('fret');
735
+ const fretEl = this.querySelector(technical, 'fret');
637
736
  if (fretEl)
638
737
  fret = parseInt(fretEl.textContent || '0');
639
- const stringEl = technical.querySelector('string');
738
+ const stringEl = this.querySelector(technical, 'string');
640
739
  if (stringEl) {
641
740
  stringNumber = parseInt(stringEl.textContent || '0');
642
741
  isStringCircled = true;
643
742
  }
644
- const palmMuteEl = technical.querySelector('palm-mute');
743
+ const palmMuteEl = this.querySelector(technical, 'palm-mute');
645
744
  if (palmMuteEl)
646
745
  palmMute = palmMuteEl.getAttribute('type') || 'start';
647
- const hammerOnEl = technical.querySelector('hammer-on');
746
+ const hammerOnEl = this.querySelector(technical, 'hammer-on');
648
747
  if (hammerOnEl)
649
748
  hammerOn = hammerOnEl.getAttribute('type') || 'start';
650
- const pullOffEl = technical.querySelector('pull-off');
749
+ const pullOffEl = this.querySelector(technical, 'pull-off');
651
750
  if (pullOffEl)
652
751
  pullOff = pullOffEl.getAttribute('type') || 'start';
653
752
  }
654
753
  let glissando = undefined;
655
- const glissEl = notations?.querySelector('glissando') || notations?.querySelector('slide');
754
+ const glissEl = notations ? (this.querySelector(notations, 'glissando') || this.querySelector(notations, 'slide')) : null;
656
755
  if (glissEl) {
657
756
  const type = glissEl.getAttribute('type');
658
757
  if (type === 'start' || type === 'stop') {
@@ -662,13 +761,14 @@ export class MusicXMLParser {
662
761
  };
663
762
  }
664
763
  }
665
- const notesDyn = notations?.querySelector('dynamics');
666
- 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();
667
767
  const lyrics = [];
668
- noteElement.querySelectorAll('lyric').forEach((lyricEl) => {
768
+ this.querySelectorAll(noteElement, 'lyric').forEach((lyricEl) => {
669
769
  const text = this.getText(lyricEl, 'text') || '';
670
770
  const syllabic = this.getText(lyricEl, 'syllabic');
671
- const extend = lyricEl.querySelector('extend') !== null;
771
+ const extend = this.querySelector(lyricEl, 'extend') !== null;
672
772
  const number = parseInt(lyricEl.getAttribute('number') || '1');
673
773
  if (text || extend) {
674
774
  lyrics[number - 1] = {
@@ -706,7 +806,7 @@ export class MusicXMLParser {
706
806
  };
707
807
  }
708
808
  parseHarmony(harmonyElement) {
709
- const root = harmonyElement.querySelector('root');
809
+ const root = this.querySelector(harmonyElement, 'root');
710
810
  if (!root)
711
811
  return null;
712
812
  const step = this.getText(root, 'root-step');
@@ -767,11 +867,11 @@ export class MusicXMLParser {
767
867
  }
768
868
  // Parse Frame (Fretboard Diagram)
769
869
  let diagram = undefined;
770
- const frame = harmonyElement.querySelector('frame');
870
+ const frame = this.querySelector(harmonyElement, 'frame');
771
871
  if (frame) {
772
872
  diagram = this.parseFrame(frame);
773
873
  }
774
- const bass = harmonyElement.querySelector('bass');
874
+ const bass = this.querySelector(harmonyElement, 'bass');
775
875
  let bassPart = '';
776
876
  if (bass) {
777
877
  const bStep = this.getText(bass, 'bass-step');
@@ -796,11 +896,11 @@ export class MusicXMLParser {
796
896
  const openStrings = [];
797
897
  const mutedStrings = [];
798
898
  const openBarres = new Map(); // fret -> startString
799
- frame.querySelectorAll('frame-note').forEach((fn) => {
899
+ this.querySelectorAll(frame, 'frame-note').forEach((fn) => {
800
900
  const string = this.getNumber(fn, 'string');
801
901
  const fret = this.getNumber(fn, 'fret');
802
902
  const fingering = this.getText(fn, 'fingering');
803
- const barre = fn.querySelector('barre')?.getAttribute('type'); // start/stop
903
+ const barre = this.querySelector(fn, 'barre')?.getAttribute('type'); // start/stop
804
904
  if (string !== null && fret !== null) {
805
905
  if (fret === 0) {
806
906
  openStrings.push(string);
@@ -842,7 +942,7 @@ export class MusicXMLParser {
842
942
  const repeats = [];
843
943
  let volta = undefined;
844
944
  let barlineStyle = BarlineStyle.Regular;
845
- measureElement.querySelectorAll('barline').forEach((barline) => {
945
+ this.querySelectorAll(measureElement, 'barline').forEach((barline) => {
846
946
  const location = barline.getAttribute('location') || 'right';
847
947
  const barStyle = this.getText(barline, 'bar-style');
848
948
  if (location === 'right') {
@@ -853,7 +953,7 @@ export class MusicXMLParser {
853
953
  else if (barStyle === 'dotted')
854
954
  barlineStyle = BarlineStyle.Dotted;
855
955
  }
856
- const repeatEl = barline.querySelector('repeat');
956
+ const repeatEl = this.querySelector(barline, 'repeat');
857
957
  if (repeatEl) {
858
958
  const direction = repeatEl.getAttribute('direction');
859
959
  const times = repeatEl.getAttribute('times');
@@ -862,7 +962,7 @@ export class MusicXMLParser {
862
962
  times: times ? parseInt(times) : undefined,
863
963
  });
864
964
  }
865
- const ending = barline.querySelector('ending');
965
+ const ending = this.querySelector(barline, 'ending');
866
966
  if (ending) {
867
967
  const type = ending.getAttribute('type');
868
968
  const numberAttr = ending.getAttribute('number') || '1';
@@ -983,7 +1083,7 @@ export class MusicXMLParser {
983
1083
  return map[step.toUpperCase()] ?? 0;
984
1084
  }
985
1085
  getText(element, tagName) {
986
- const child = element.querySelector(tagName);
1086
+ const child = this.querySelector(element, tagName);
987
1087
  return child?.textContent ?? null;
988
1088
  }
989
1089
  getNumber(element, tagName) {
@@ -29,7 +29,7 @@ export declare class Measure {
29
29
  replaceNoteSet(index: number, newNoteSet: NoteSet, voiceIndex?: number): Measure;
30
30
  replaceNote(noteIndex: number, newNote: Note, voiceIndex?: number): Measure;
31
31
  deleteNote(noteIndex: number, voiceIndex?: number): Measure;
32
- autoBeam(timeSign: TimeSignature): Measure;
32
+ autoBeam(timeSign?: TimeSignature): Measure;
33
33
  withTimeSignature(timeSignature?: TimeSignature): Measure;
34
34
  withKeySignature(keySignature?: KeySignature): Measure;
35
35
  withClef(clef?: Clef): Measure;
@@ -46,6 +46,8 @@ export declare class Measure {
46
46
  withSystemText(text?: string): Measure;
47
47
  withBarlineStyle(style: BarlineStyle): Measure;
48
48
  fillVoiceWithRests(voiceIndex: number, targetDuration: number): Measure;
49
+ simplifyRests(voiceIndex?: number): Measure;
50
+ private decomposeToRests;
49
51
  toJSON(): MeasureJSON;
50
52
  }
51
53
  export interface MeasureJSON {
@@ -1,6 +1,6 @@
1
1
  import { Note } from './Note';
2
2
  import { NoteSet } from './NoteSet';
3
- import { BarlineStyle, DURATION_VALUES, decomposeDuration, } from './types';
3
+ import { BarlineStyle, DURATION_VALUES, Duration, decomposeDuration, } from './types';
4
4
  /**
5
5
  * Represents a single measure containing note sets, potentially in multiple voices.
6
6
  */
@@ -94,7 +94,11 @@ export class Measure {
94
94
  }
95
95
  const newVoices = [...this.voices];
96
96
  newVoices[voiceIndex] = updatedVoice;
97
- return this.withVoices(newVoices);
97
+ let result = this.withVoices(newVoices).simplifyRests(voiceIndex);
98
+ if (this.timeSignature) {
99
+ result = result.autoBeam(this.timeSignature);
100
+ }
101
+ return result;
98
102
  }
99
103
  getTotalDuration(voiceIndex = 0) {
100
104
  const voice = this.voices[voiceIndex];
@@ -141,10 +145,56 @@ export class Measure {
141
145
  const voice = [...this.voices[voiceIndex]];
142
146
  if (index >= voice.length)
143
147
  return this;
144
- voice[index] = newNoteSet;
148
+ const oldVal = voice[index].getDurationValue();
149
+ const newVal = newNoteSet.getDurationValue();
150
+ // Use a copy for modifications
151
+ const updatedVoice = [...voice];
152
+ if (Math.abs(oldVal - newVal) < 0.001) {
153
+ updatedVoice[index] = newNoteSet;
154
+ }
155
+ else if (newVal < oldVal) {
156
+ const gap = oldVal - newVal;
157
+ updatedVoice[index] = newNoteSet;
158
+ const rests = this.decomposeToRests(gap);
159
+ updatedVoice.splice(index + 1, 0, ...rests);
160
+ }
161
+ else {
162
+ const delta = newVal - oldVal;
163
+ let available = 0;
164
+ for (let i = index + 1; i < updatedVoice.length; i++) {
165
+ available += updatedVoice[i].getDurationValue();
166
+ }
167
+ // If there's enough space to consume, do it.
168
+ // If not, we still replace it but it will overflow (which simplifyRests might fix or layout will handle)
169
+ updatedVoice[index] = newNoteSet;
170
+ let consumed = 0;
171
+ let removeCount = 0;
172
+ const replacements = [];
173
+ for (let i = index + 1; i < voice.length; i++) {
174
+ const ns = voice[i];
175
+ const val = ns.getDurationValue();
176
+ if (consumed + val <= delta + 0.001) {
177
+ consumed += val;
178
+ removeCount++;
179
+ if (consumed >= delta - 0.001)
180
+ break;
181
+ }
182
+ else {
183
+ const remaining = consumed + val - delta;
184
+ replacements.push(...this.decomposeToRests(remaining));
185
+ removeCount++;
186
+ break;
187
+ }
188
+ }
189
+ updatedVoice.splice(index + 1, removeCount, ...replacements);
190
+ }
145
191
  const newVoices = [...this.voices];
146
- newVoices[voiceIndex] = voice;
147
- return this.withVoices(newVoices);
192
+ newVoices[voiceIndex] = updatedVoice;
193
+ let result = this.withVoices(newVoices).simplifyRests(voiceIndex);
194
+ if (this.timeSignature) {
195
+ result = result.autoBeam(this.timeSignature);
196
+ }
197
+ return result;
148
198
  }
149
199
  replaceNote(noteIndex, newNote, voiceIndex = 0) {
150
200
  return this.replaceNoteSet(noteIndex, new NoteSet([newNote]), voiceIndex);
@@ -155,13 +205,20 @@ export class Measure {
155
205
  const voice = [...this.voices[voiceIndex]];
156
206
  if (noteIndex < 0 || noteIndex >= voice.length)
157
207
  return this;
158
- voice.splice(noteIndex, 1);
208
+ // Instead of splicing, replace with a rest of the same duration
209
+ const target = voice[noteIndex];
210
+ voice[noteIndex] = target.withRest(true);
159
211
  const newVoices = [...this.voices];
160
212
  newVoices[voiceIndex] = voice;
161
- return this.withVoices(newVoices);
213
+ return this.withVoices(newVoices).simplifyRests(voiceIndex);
162
214
  }
163
215
  autoBeam(timeSign) {
216
+ const ts = timeSign || this.timeSignature;
217
+ if (!ts)
218
+ return this;
164
219
  const newVoices = this.voices.map((voice) => {
220
+ if (voice.length === 0)
221
+ return [];
165
222
  let currentOffset = 0;
166
223
  let lastGroupId = undefined;
167
224
  let lastGroupStartTime = 0;
@@ -171,28 +228,38 @@ export class Measure {
171
228
  const noteDuration = ns.getDurationValue();
172
229
  let groupToAssign = undefined;
173
230
  if (ns.isBeamable()) {
174
- const isSixteenth = noteDuration < 0.5;
175
- let currentBeatLength = 4 / timeSign.beatType;
176
- if (timeSign.beatType === 8 && timeSign.beats % 3 === 0) {
177
- currentBeatLength = 1.5;
178
- }
179
- else if (timeSign.beatType === 4 && (timeSign.beats === 4 || timeSign.beats === 2)) {
180
- currentBeatLength = lastGroupHasSixteenths || isSixteenth ? 1.0 : 2.0;
231
+ const isSixteenth = noteDuration < 0.5 - 0.001;
232
+ // Determine the logical "beat length" for beaming purposes
233
+ let beamBeatLength = 4 / ts.beatType; // Default is one beat
234
+ if (ts.beatType === 4) {
235
+ if (ts.beats === 4 || ts.beats === 2) {
236
+ // In 4/4 or 2/4, eighth notes are often beamed in groups of 2 beats (length 2.0)
237
+ // unless there are sixteenth notes which force 1-beat groupings.
238
+ beamBeatLength = (lastGroupHasSixteenths || isSixteenth) ? 1.0 : 2.0;
239
+ }
181
240
  }
182
- else if (timeSign.beatType === 2 && timeSign.beats === 2) {
183
- currentBeatLength = lastGroupHasSixteenths || isSixteenth ? 1.0 : 2.0;
241
+ else if (ts.beatType === 8) {
242
+ if (ts.beats % 3 === 0) {
243
+ // Compound meter (6/8, 9/8, 12/8): Beam by 3 eighth notes (1.5 length)
244
+ beamBeatLength = 1.5;
245
+ }
246
+ else {
247
+ // Simple meter in 8 (e.g. 2/8, 4/8): Beam by quarter note (1.0 length)
248
+ beamBeatLength = 1.0;
249
+ }
184
250
  }
185
- const currentBeatId = Math.floor(currentOffset / currentBeatLength + 0.001);
251
+ const currentBeatId = Math.floor(currentOffset / beamBeatLength + 0.001);
186
252
  const endOffset = currentOffset + noteDuration;
187
- const crossesBeat = Math.floor(endOffset / currentBeatLength - 0.001) !== currentBeatId;
253
+ const crossesBeat = Math.floor(endOffset / beamBeatLength - 0.001) !== currentBeatId;
188
254
  const sameBeatAsLast = lastGroupId !== undefined &&
189
- Math.floor(lastGroupStartTime / currentBeatLength + 0.001) === currentBeatId;
255
+ Math.floor(lastGroupStartTime / beamBeatLength + 0.001) === currentBeatId;
190
256
  if (sameBeatAsLast && !crossesBeat) {
191
257
  groupToAssign = lastGroupId;
192
258
  if (isSixteenth)
193
259
  lastGroupHasSixteenths = true;
194
260
  }
195
261
  else {
262
+ // Start a new group
196
263
  lastGroupId = Math.floor(Math.random() * 1000000) + 1;
197
264
  lastGroupStartTime = currentOffset;
198
265
  lastGroupHasSixteenths = isSixteenth;
@@ -200,12 +267,14 @@ export class Measure {
200
267
  }
201
268
  }
202
269
  else {
270
+ // Non-beamable note (Quarter, Half, etc.) or Rest
203
271
  lastGroupId = undefined;
204
272
  lastGroupHasSixteenths = false;
205
273
  }
206
274
  tempNoteSets.push(ns.withBeamGroup(groupToAssign));
207
275
  currentOffset += noteDuration;
208
276
  }
277
+ // Cleanup: groups of 1 note are not beamed
209
278
  const groupCounts = new Map();
210
279
  for (const ns of tempNoteSets) {
211
280
  if (ns.beamGroup !== undefined) {
@@ -294,6 +363,44 @@ export class Measure {
294
363
  }
295
364
  return this.withVoices(newVoices);
296
365
  }
366
+ simplifyRests(voiceIndex = 0) {
367
+ const voice = this.voices[voiceIndex];
368
+ if (!voice || voice.length === 0)
369
+ return this;
370
+ const totalDur = this.getTotalDuration(voiceIndex);
371
+ const expectedDur = this.timeSignature ? (this.timeSignature.beats * 4) / this.timeSignature.beatType : 4.0;
372
+ // Special Case: If the whole measure is rests, just return a single whole rest (or appropriate duration)
373
+ const allRests = voice.every(ns => ns.isRest);
374
+ if (allRests && Math.abs(totalDur - expectedDur) < 0.001) {
375
+ const newVoice = [new NoteSet([new Note(Duration.Whole, undefined, true)])];
376
+ const newVoices = [...this.voices];
377
+ newVoices[voiceIndex] = newVoice;
378
+ return this.withVoices(newVoices);
379
+ }
380
+ const newVoice = [];
381
+ let currentRestSum = 0;
382
+ for (const ns of voice) {
383
+ if (ns.isRest) {
384
+ currentRestSum += ns.getDurationValue();
385
+ }
386
+ else {
387
+ if (currentRestSum > 0.001) {
388
+ newVoice.push(...this.decomposeToRests(currentRestSum));
389
+ currentRestSum = 0;
390
+ }
391
+ newVoice.push(ns);
392
+ }
393
+ }
394
+ if (currentRestSum > 0.001) {
395
+ newVoice.push(...this.decomposeToRests(currentRestSum));
396
+ }
397
+ const newVoices = [...this.voices];
398
+ newVoices[voiceIndex] = newVoice;
399
+ return this.withVoices(newVoices);
400
+ }
401
+ decomposeToRests(value) {
402
+ return decomposeDuration(value).map((d) => new NoteSet([new Note(d.duration, undefined, true, d.isDotted)]));
403
+ }
297
404
  toJSON() {
298
405
  return {
299
406
  voices: this.voices.map((v) => v.map((ns) => ns.toJSON())),
@@ -17,7 +17,7 @@ export declare class Note {
17
17
  readonly tuplet?: Tuplet | undefined;
18
18
  readonly hairpin?: Hairpin | undefined;
19
19
  readonly isGrace: boolean;
20
- readonly lyric?: string | undefined;
20
+ readonly lyric?: Lyric | undefined;
21
21
  readonly chord?: string | undefined;
22
22
  readonly glissando?: Glissando | undefined;
23
23
  readonly arpeggio?: Arpeggio | undefined;
@@ -27,7 +27,7 @@ export declare class Note {
27
27
  readonly fret?: number | undefined;
28
28
  readonly string?: number | undefined;
29
29
  readonly fretboardDiagram?: FretboardDiagram | undefined;
30
- readonly lyrics?: (string | Lyric)[] | undefined;
30
+ readonly lyrics?: Lyric[] | undefined;
31
31
  readonly staffText?: string | undefined;
32
32
  readonly color?: string | undefined;
33
33
  readonly notehead?: NoteheadShape | undefined;
@@ -40,7 +40,7 @@ export declare class Note {
40
40
  readonly pullOff?: "start" | "stop" | undefined;
41
41
  constructor(duration: Duration, pitch?: Pitch | undefined, // undefined for rests
42
42
  isRest?: boolean, isDotted?: boolean, accidental?: Accidental | undefined, beamGroup?: number | undefined, // Group ID for beamed notes
43
- articulations?: Articulation[], dynamic?: Dynamic | undefined, tie?: boolean | undefined, slur?: Slur | undefined, tuplet?: Tuplet | undefined, hairpin?: Hairpin | undefined, isGrace?: boolean, lyric?: string | undefined, chord?: string | undefined, glissando?: Glissando | undefined, arpeggio?: Arpeggio | undefined, ottava?: Ottava | undefined, pedal?: Pedal | undefined, ornament?: Ornament | undefined, fret?: number | undefined, string?: number | undefined, fretboardDiagram?: FretboardDiagram | undefined, lyrics?: (string | Lyric)[] | undefined, staffText?: string | undefined, color?: string | undefined, notehead?: NoteheadShape | undefined, bowing?: Bowing | undefined, fingering?: number | undefined, stemDirection?: StemDirection | undefined, isStringCircled?: boolean | undefined, palmMute?: "start" | "stop" | undefined, hammerOn?: "start" | "stop" | undefined, pullOff?: "start" | "stop" | undefined);
43
+ articulations?: Articulation[], dynamic?: Dynamic | undefined, tie?: boolean | undefined, slur?: Slur | undefined, tuplet?: Tuplet | undefined, hairpin?: Hairpin | undefined, isGrace?: boolean, lyric?: Lyric | undefined, chord?: string | undefined, glissando?: Glissando | undefined, arpeggio?: Arpeggio | undefined, ottava?: Ottava | undefined, pedal?: Pedal | undefined, ornament?: Ornament | undefined, fret?: number | undefined, string?: number | undefined, fretboardDiagram?: FretboardDiagram | undefined, lyrics?: Lyric[] | undefined, staffText?: string | undefined, color?: string | undefined, notehead?: NoteheadShape | undefined, bowing?: Bowing | undefined, fingering?: number | undefined, stemDirection?: StemDirection | undefined, isStringCircled?: boolean | undefined, palmMute?: "start" | "stop" | undefined, hammerOn?: "start" | "stop" | undefined, pullOff?: "start" | "stop" | undefined);
44
44
  /**
45
45
  * Get all lyrics as standardized objects
46
46
  */
@@ -70,8 +70,8 @@ export declare class Note {
70
70
  withTie(tie?: boolean): Note;
71
71
  withSlur(slur?: Slur): Note;
72
72
  withTuplet(tuplet?: Tuplet): Note;
73
- withLyric(lyric?: string): Note;
74
- withLyrics(lyrics: (string | Lyric)[]): Note;
73
+ withLyric(lyric?: Lyric): Note;
74
+ withLyrics(lyrics: Lyric[]): Note;
75
75
  withHairpin(hairpin?: Hairpin): Note;
76
76
  withAccidental(accidental?: Accidental): Note;
77
77
  withDuration(duration: Duration, isDotted?: boolean): Note;
@@ -117,7 +117,7 @@ export interface NoteJSON {
117
117
  tuplet?: Tuplet;
118
118
  hairpin?: Hairpin;
119
119
  isGrace?: boolean;
120
- lyric?: string;
120
+ lyric?: Lyric;
121
121
  chord?: string;
122
122
  glissando?: Glissando;
123
123
  arpeggio?: Arpeggio;
@@ -127,7 +127,7 @@ export interface NoteJSON {
127
127
  fret?: number;
128
128
  string?: number;
129
129
  fretboardDiagram?: FretboardDiagram;
130
- lyrics?: (string | Lyric)[];
130
+ lyrics?: Lyric[];
131
131
  staffText?: string;
132
132
  color?: string;
133
133
  notehead?: string;
@@ -81,10 +81,10 @@ export class Note {
81
81
  */
82
82
  get allLyrics() {
83
83
  if (this.lyrics) {
84
- return this.lyrics.map((l) => (typeof l === 'string' ? { text: l } : l));
84
+ return this.lyrics;
85
85
  }
86
86
  if (this.lyric) {
87
- return [{ text: this.lyric }];
87
+ return [this.lyric];
88
88
  }
89
89
  return [];
90
90
  }
@@ -14,7 +14,7 @@ export declare class NoteSet {
14
14
  withDuration(duration: Duration, isDotted?: boolean): NoteSet;
15
15
  withBeamGroup(group?: number): NoteSet;
16
16
  withNotes(notes: Note[]): NoteSet;
17
- withLyrics(lyrics: (string | Lyric)[]): NoteSet;
17
+ withLyrics(lyrics: Lyric[]): NoteSet;
18
18
  withTuplet(tuplet?: Tuplet): NoteSet;
19
19
  withPitch(pitch: Pitch): NoteSet;
20
20
  withAccidental(accidental?: Accidental): NoteSet;
@@ -45,14 +45,14 @@ export declare class NoteSet {
45
45
  withPalmMute(type?: 'start' | 'stop'): NoteSet;
46
46
  withStringCircled(isStringCircled?: boolean): NoteSet;
47
47
  withStemDirection(dir?: StemDirection): NoteSet;
48
- withLyric(lyric?: string): NoteSet;
48
+ withLyric(lyric?: Lyric): NoteSet;
49
49
  withStaffText(text?: string): NoteSet;
50
50
  withFretboardDiagram(diagram?: FretboardDiagram): NoteSet;
51
51
  get pitch(): Pitch | undefined;
52
52
  get accidental(): Accidental | undefined;
53
53
  get tie(): boolean | undefined;
54
54
  get slur(): Slur | undefined;
55
- get articulations(): Articulation[];
55
+ get articulations(): Articulation[] | undefined;
56
56
  get articulation(): Articulation | undefined;
57
57
  get ornament(): Ornament | undefined;
58
58
  get dynamic(): Dynamic | undefined;
@@ -68,14 +68,14 @@ export declare class NoteSet {
68
68
  get bowing(): Bowing | undefined;
69
69
  get fingering(): number | undefined;
70
70
  get allLyrics(): Lyric[];
71
- get lyrics(): (string | Lyric)[] | undefined;
72
- get lyric(): string | undefined;
71
+ get lyrics(): Lyric[] | undefined;
72
+ get lyric(): Lyric | undefined;
73
73
  get chord(): string | undefined;
74
74
  get fretboardDiagram(): FretboardDiagram | undefined;
75
75
  get staffText(): string | undefined;
76
- get hammerOn(): "start" | "stop" | undefined;
77
- get pullOff(): "start" | "stop" | undefined;
78
- get palmMute(): "start" | "stop" | undefined;
76
+ get hammerOn(): 'start' | 'stop' | undefined;
77
+ get pullOff(): 'start' | 'stop' | undefined;
78
+ get palmMute(): 'start' | 'stop' | undefined;
79
79
  get isStringCircled(): boolean | undefined;
80
80
  get stemDirection(): StemDirection | undefined;
81
81
  toJSON(): NoteSetJSON;
@@ -19,13 +19,13 @@ export declare class Score {
19
19
  readonly lyricist: string;
20
20
  readonly swing: boolean;
21
21
  readonly subtitle: string;
22
- readonly genre: Genre | string;
22
+ readonly genre: Genre;
23
23
  readonly tempoText: string;
24
- constructor(title: string, composer: string, timeSignature: TimeSignature, keySignature: KeySignature, parts: Part[], bpm?: number, tempoDuration?: Duration, tempoIsDotted?: boolean, copyright?: string, lyricist?: string, swing?: boolean, subtitle?: string, genre?: Genre | string, tempoText?: string);
24
+ constructor(title: string, composer: string, timeSignature: TimeSignature, keySignature: KeySignature, parts: Part[], bpm?: number, tempoDuration?: Duration, tempoIsDotted?: boolean, copyright?: string, lyricist?: string, swing?: boolean, subtitle?: string, genre?: Genre, tempoText?: string);
25
25
  withTitle(title: string): Score;
26
26
  withComposer(composer: string): Score;
27
27
  withSubtitle(subtitle: string): Score;
28
- withGenre(genre: Genre | string): Score;
28
+ withGenre(genre: Genre): Score;
29
29
  getMeasureCount(): number;
30
30
  getTimeSignatureAt(measureIndex: number): TimeSignature;
31
31
  getKeySignatureAt(measureIndex: number): KeySignature;
@@ -85,6 +85,6 @@ export interface ScoreJSON {
85
85
  lyricist?: string;
86
86
  swing?: boolean;
87
87
  subtitle?: string;
88
- genre?: Genre | string;
88
+ genre?: Genre;
89
89
  tempoText?: string;
90
90
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * WARNING: DO NOT ADD SENSITIVE LOGIC OR PRIVATE CONSTANTS TO THIS FILE.
3
+ * This package (@scorelabs/core) is used in public-facing frontend applications
4
+ * and could be exposed. Keep all token generation, verification, and encryption
5
+ * logic in the 'backend' or 'website' utilities.
6
+ */
7
+ export type UserTier = 'free' | 'premium';
8
+ export interface UserData {
9
+ accessToken?: string;
10
+ access_token?: string;
11
+ uuid?: string | number;
12
+ name?: string;
13
+ first_name?: string;
14
+ last_name?: string;
15
+ username?: string;
16
+ email?: string;
17
+ is_admin?: number | boolean;
18
+ id?: string | number;
19
+ tier?: UserTier | string;
20
+ picture?: string;
21
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * WARNING: DO NOT ADD SENSITIVE LOGIC OR PRIVATE CONSTANTS TO THIS FILE.
3
+ * This package (@scorelabs/core) is used in public-facing frontend applications
4
+ * and could be exposed. Keep all token generation, verification, and encryption
5
+ * logic in the 'backend' or 'website' utilities.
6
+ */
7
+ export {};
@@ -24,3 +24,4 @@ export * from './Pedal.js';
24
24
  export * from './Slur.js';
25
25
  export * from './Tuplet.js';
26
26
  export * from './Repeat.js';
27
+ export * from './User.js';
@@ -24,3 +24,4 @@ export * from './Pedal.js';
24
24
  export * from './Slur.js';
25
25
  export * from './Tuplet.js';
26
26
  export * from './Repeat.js';
27
+ export * from './User.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scorelabs/core",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Core logic and models for ScoreLabs music notation",
5
5
  "type": "module",
6
6
  "files": [
@@ -24,6 +24,7 @@
24
24
  "release:major": "npm version major && npm publish --access public",
25
25
  "release": "npm run release:patch",
26
26
  "test": "vitest",
27
+ "test:coverage": "vitest run --coverage",
27
28
  "lint": "eslint ."
28
29
  },
29
30
  "dependencies": {
@@ -32,6 +33,7 @@
32
33
  "devDependencies": {
33
34
  "@types/jszip": "^3.4.0",
34
35
  "@types/node": "^25.2.3",
36
+ "@vitest/coverage-v8": "^4.0.18",
35
37
  "typescript": "^5.9.3",
36
38
  "vitest": "^4.0.18"
37
39
  }