musicxml-io 0.1.0 → 0.2.0

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.
package/dist/index.js CHANGED
@@ -27,6 +27,8 @@ __export(src_exports, {
27
27
  addNote: () => addNote,
28
28
  assertMeasureValid: () => assertMeasureValid,
29
29
  assertValid: () => assertValid,
30
+ buildVoiceToStaffMap: () => buildVoiceToStaffMap,
31
+ buildVoiceToStaffMapForPart: () => buildVoiceToStaffMapForPart,
30
32
  changeKey: () => changeKey,
31
33
  changeTime: () => changeTime,
32
34
  countNotes: () => countNotes,
@@ -34,36 +36,82 @@ __export(src_exports, {
34
36
  deleteMeasure: () => deleteMeasure,
35
37
  deleteNote: () => deleteNote,
36
38
  exportMidi: () => exportMidi,
39
+ findBarlines: () => findBarlines,
40
+ findDirectionsByType: () => findDirectionsByType,
37
41
  findNotes: () => findNotes,
42
+ findNotesWithNotation: () => findNotesWithNotation,
38
43
  formatLocation: () => formatLocation,
39
44
  getAbsolutePosition: () => getAbsolutePosition,
45
+ getAdjacentNotes: () => getAdjacentNotes,
40
46
  getAllNotes: () => getAllNotes,
41
47
  getAttributesAtMeasure: () => getAttributesAtMeasure,
48
+ getBeamGroups: () => getBeamGroups,
49
+ getChordProgression: () => getChordProgression,
42
50
  getChords: () => getChords,
51
+ getClefChanges: () => getClefChanges,
52
+ getClefForStaff: () => getClefForStaff,
53
+ getDirections: () => getDirections,
54
+ getDirectionsAtPosition: () => getDirectionsAtPosition,
43
55
  getDivisions: () => getDivisions,
44
56
  getDuration: () => getDuration,
57
+ getDynamics: () => getDynamics,
58
+ getEffectiveStaff: () => getEffectiveStaff,
59
+ getEndings: () => getEndings,
60
+ getEntriesAtPosition: () => getEntriesAtPosition,
61
+ getEntriesForStaff: () => getEntriesForStaff,
62
+ getEntriesInRange: () => getEntriesInRange,
63
+ getHarmonies: () => getHarmonies,
64
+ getHarmonyAtPosition: () => getHarmonyAtPosition,
65
+ getKeyChanges: () => getKeyChanges,
66
+ getLyricText: () => getLyricText,
67
+ getLyrics: () => getLyrics,
45
68
  getMeasure: () => getMeasure,
46
69
  getMeasureByIndex: () => getMeasureByIndex,
47
70
  getMeasureContext: () => getMeasureContext,
48
71
  getMeasureCount: () => getMeasureCount,
49
72
  getMeasureEndPosition: () => getMeasureEndPosition,
73
+ getNextNote: () => getNextNote,
50
74
  getNormalizedDuration: () => getNormalizedDuration,
51
75
  getNormalizedPosition: () => getNormalizedPosition,
76
+ getNotesAtPosition: () => getNotesAtPosition,
52
77
  getNotesForStaff: () => getNotesForStaff,
53
78
  getNotesForVoice: () => getNotesForVoice,
79
+ getNotesInRange: () => getNotesInRange,
80
+ getOctaveShifts: () => getOctaveShifts,
54
81
  getPartById: () => getPartById,
82
+ getPartByIndex: () => getPartByIndex,
83
+ getPartCount: () => getPartCount,
84
+ getPartIds: () => getPartIds,
55
85
  getPartIndex: () => getPartIndex,
86
+ getPedalMarkings: () => getPedalMarkings,
87
+ getPrevNote: () => getPrevNote,
88
+ getRepeatStructure: () => getRepeatStructure,
89
+ getSlurSpans: () => getSlurSpans,
90
+ getStaffRange: () => getStaffRange,
56
91
  getStaveCount: () => getStaveCount,
57
92
  getStaves: () => getStaves,
93
+ getStructuralChanges: () => getStructuralChanges,
94
+ getTempoMarkings: () => getTempoMarkings,
95
+ getTiedNoteGroups: () => getTiedNoteGroups,
96
+ getTimeChanges: () => getTimeChanges,
97
+ getTupletGroups: () => getTupletGroups,
98
+ getVerseCount: () => getVerseCount,
99
+ getVerticalSlice: () => getVerticalSlice,
100
+ getVoiceLine: () => getVoiceLine,
101
+ getVoiceLineInRange: () => getVoiceLineInRange,
58
102
  getVoices: () => getVoices,
103
+ getVoicesForStaff: () => getVoicesForStaff,
104
+ getWedges: () => getWedges,
59
105
  groupByStaff: () => groupByStaff,
60
106
  groupByVoice: () => groupByVoice,
61
107
  hasMultipleStaves: () => hasMultipleStaves,
62
108
  hasNotes: () => hasNotes,
109
+ inferStaff: () => inferStaff,
63
110
  insertMeasure: () => insertMeasure,
64
111
  isCompressed: () => isCompressed,
65
112
  isRestMeasure: () => isRestMeasure,
66
113
  isValid: () => isValid,
114
+ iterateEntries: () => iterateEntries,
67
115
  iterateNotes: () => iterateNotes,
68
116
  measureRoundtrip: () => measureRoundtrip,
69
117
  modifyNoteDuration: () => modifyNoteDuration,
@@ -77,7 +125,6 @@ __export(src_exports, {
77
125
  serialize: () => serialize,
78
126
  serializeCompressed: () => serializeCompressed,
79
127
  serializeToFile: () => serializeToFile,
80
- setDivisions: () => setDivisions,
81
128
  transpose: () => transpose,
82
129
  validate: () => validate,
83
130
  validateBackupForward: () => validateBackupForward,
@@ -478,8 +525,10 @@ function parsePartList(elements) {
478
525
  const sound = getElementText(instContent, "instrument-sound");
479
526
  if (sound) inst.sound = sound;
480
527
  if (hasElement(instContent, "solo")) inst.solo = true;
481
- const ens = getElementText(instContent, "ensemble");
482
- if (ens) inst.ensemble = parseInt(ens, 10);
528
+ if (hasElement(instContent, "ensemble")) {
529
+ const ensText = getElementText(instContent, "ensemble");
530
+ inst.ensemble = ensText ? parseInt(ensText, 10) : 0;
531
+ }
483
532
  instruments.push(inst);
484
533
  }
485
534
  }
@@ -662,6 +711,8 @@ function parseAttributes(elements) {
662
711
  if (divisions !== void 0) attrs.divisions = divisions;
663
712
  const staves = getElementTextAsInt(elements, "staves");
664
713
  if (staves !== void 0) attrs.staves = staves;
714
+ const instruments = getElementTextAsInt(elements, "instruments");
715
+ if (instruments !== void 0) attrs.instruments = instruments;
665
716
  const time = getElementContent(elements, "time");
666
717
  if (time) {
667
718
  attrs.time = parseTimeSignature(time, elements);
@@ -758,7 +809,7 @@ function parseKeySignature(elements) {
758
809
  const key = {
759
810
  fifths: parseInt(fifths || "0", 10)
760
811
  };
761
- const validModes = ["major", "minor", "dorian", "phrygian", "lydian", "mixolydian", "aeolian", "ionian", "locrian"];
812
+ const validModes = ["major", "minor", "dorian", "phrygian", "lydian", "mixolydian", "aeolian", "ionian", "locrian", "none"];
762
813
  if (mode && validModes.includes(mode)) {
763
814
  key.mode = mode;
764
815
  }
@@ -789,6 +840,12 @@ function parseClef(elements, attrs) {
789
840
  if (octaveChange !== void 0) {
790
841
  clef.clefOctaveChange = octaveChange;
791
842
  }
843
+ if (attrs["print-object"] === "no") {
844
+ clef.printObject = false;
845
+ }
846
+ if (attrs["after-barline"] === "yes") {
847
+ clef.afterBarline = true;
848
+ }
792
849
  return clef;
793
850
  }
794
851
  function parseTranspose(elements) {
@@ -998,6 +1055,7 @@ function parseNotations(elements, notationsIndex = 0) {
998
1055
  slurType: attrs["type"] || "start",
999
1056
  number: attrs["number"] ? parseInt(attrs["number"], 10) : void 0,
1000
1057
  lineType: attrs["line-type"],
1058
+ orientation: attrs["orientation"],
1001
1059
  defaultX: attrs["default-x"] ? parseFloat(attrs["default-x"]) : void 0,
1002
1060
  defaultY: attrs["default-y"] ? parseFloat(attrs["default-y"]) : void 0,
1003
1061
  bezierX: attrs["bezier-x"] ? parseFloat(attrs["bezier-x"]) : void 0,
@@ -1510,6 +1568,9 @@ function parseDirection(elements, attrs) {
1510
1568
  direction.sound = {};
1511
1569
  if (soundAttrs["tempo"]) direction.sound.tempo = parseFloat(soundAttrs["tempo"]);
1512
1570
  if (soundAttrs["dynamics"]) direction.sound.dynamics = parseFloat(soundAttrs["dynamics"]);
1571
+ if (soundAttrs["damper-pedal"]) direction.sound.damperPedal = soundAttrs["damper-pedal"];
1572
+ if (soundAttrs["soft-pedal"]) direction.sound.softPedal = soundAttrs["soft-pedal"];
1573
+ if (soundAttrs["sostenuto-pedal"]) direction.sound.sostenutoPedal = soundAttrs["sostenuto-pedal"];
1513
1574
  for (const soundEl of soundContent) {
1514
1575
  if (soundEl["midi-instrument"]) {
1515
1576
  const midiAttrs = getAttributes(soundEl);
@@ -1670,6 +1731,8 @@ function parseDirectionType(elements) {
1670
1731
  if (bracketAttrs["number"]) result.number = parseInt(bracketAttrs["number"], 10);
1671
1732
  if (bracketAttrs["line-end"]) result.lineEnd = bracketAttrs["line-end"];
1672
1733
  if (bracketAttrs["line-type"]) result.lineType = bracketAttrs["line-type"];
1734
+ if (bracketAttrs["default-y"]) result.defaultY = parseFloat(bracketAttrs["default-y"]);
1735
+ if (bracketAttrs["relative-x"]) result.relativeX = parseFloat(bracketAttrs["relative-x"]);
1673
1736
  return result;
1674
1737
  }
1675
1738
  }
@@ -1969,6 +2032,8 @@ var VALID_BAR_STYLES = /* @__PURE__ */ new Set([
1969
2032
  "light-heavy",
1970
2033
  "heavy-light",
1971
2034
  "heavy-heavy",
2035
+ "tick",
2036
+ "short",
1972
2037
  "none"
1973
2038
  ]);
1974
2039
  function isValidNoteType(value) {
@@ -2238,6 +2303,9 @@ function parseSound(elements, attrs) {
2238
2303
  if (attrs["tocoda"]) sound.tocoda = attrs["tocoda"];
2239
2304
  if (attrs["fine"] === "yes") sound.fine = true;
2240
2305
  if (attrs["forward-repeat"] === "yes") sound.forwardRepeat = true;
2306
+ if (attrs["damper-pedal"]) sound.damperPedal = attrs["damper-pedal"];
2307
+ if (attrs["soft-pedal"]) sound.softPedal = attrs["soft-pedal"];
2308
+ if (attrs["sostenuto-pedal"]) sound.sostenutoPedal = attrs["sostenuto-pedal"];
2241
2309
  for (const el of elements) {
2242
2310
  if (el["swing"]) {
2243
2311
  const swingContent = el["swing"];
@@ -2334,6 +2402,8 @@ function parseAuto(data) {
2334
2402
  var DEFAULT_OPTIONS = {
2335
2403
  checkDivisions: true,
2336
2404
  checkMeasureDuration: true,
2405
+ checkMeasureFullness: false,
2406
+ // Piano Roll semantics - opt-in
2337
2407
  checkPosition: true,
2338
2408
  checkTies: true,
2339
2409
  checkBeams: true,
@@ -2390,6 +2460,14 @@ function validate(score, options = {}) {
2390
2460
  opts.durationTolerance
2391
2461
  ));
2392
2462
  }
2463
+ if (opts.checkMeasureFullness && currentTime) {
2464
+ allErrors.push(...validateMeasureFullness(
2465
+ measure,
2466
+ currentDivisions,
2467
+ currentTime,
2468
+ location
2469
+ ));
2470
+ }
2393
2471
  if (opts.checkPosition) {
2394
2472
  allErrors.push(...validateBackupForward(measure, location));
2395
2473
  }
@@ -2538,6 +2616,92 @@ function calculateVoiceDurations(measure) {
2538
2616
  }
2539
2617
  return voiceDurations;
2540
2618
  }
2619
+ function validateMeasureFullness(measure, divisions, time, location) {
2620
+ const errors = [];
2621
+ if (time.senzaMisura) {
2622
+ return errors;
2623
+ }
2624
+ const beats = parseInt(time.beats, 10);
2625
+ if (isNaN(beats)) {
2626
+ return errors;
2627
+ }
2628
+ const expectedDuration = beats / time.beatType * 4 * divisions;
2629
+ const voiceCoverage = /* @__PURE__ */ new Map();
2630
+ let currentPosition = 0;
2631
+ for (const entry of measure.entries) {
2632
+ if (entry.type === "note") {
2633
+ const staff = entry.staff ?? 1;
2634
+ const voice = entry.voice;
2635
+ const key = `${staff}-${voice}`;
2636
+ if (!entry.chord) {
2637
+ if (!voiceCoverage.has(key)) {
2638
+ voiceCoverage.set(key, { segments: [] });
2639
+ }
2640
+ voiceCoverage.get(key).segments.push({
2641
+ start: currentPosition,
2642
+ end: currentPosition + entry.duration
2643
+ });
2644
+ currentPosition += entry.duration;
2645
+ }
2646
+ } else if (entry.type === "backup") {
2647
+ currentPosition -= entry.duration;
2648
+ } else if (entry.type === "forward") {
2649
+ const staff = entry.staff ?? 1;
2650
+ const voice = entry.voice ?? 1;
2651
+ const key = `${staff}-${voice}`;
2652
+ if (!voiceCoverage.has(key)) {
2653
+ voiceCoverage.set(key, { segments: [] });
2654
+ }
2655
+ voiceCoverage.get(key).segments.push({
2656
+ start: currentPosition,
2657
+ end: currentPosition + entry.duration
2658
+ });
2659
+ currentPosition += entry.duration;
2660
+ }
2661
+ }
2662
+ for (const [voiceKey, { segments }] of voiceCoverage.entries()) {
2663
+ const [staff, voice] = voiceKey.split("-").map(Number);
2664
+ const sorted = [...segments].sort((a, b) => a.start - b.start);
2665
+ let lastEnd = 0;
2666
+ const gaps = [];
2667
+ for (const seg of sorted) {
2668
+ if (seg.start > lastEnd) {
2669
+ gaps.push({ start: lastEnd, end: seg.start });
2670
+ }
2671
+ lastEnd = Math.max(lastEnd, seg.end);
2672
+ }
2673
+ if (lastEnd < expectedDuration) {
2674
+ gaps.push({ start: lastEnd, end: expectedDuration });
2675
+ }
2676
+ for (const gap of gaps) {
2677
+ errors.push({
2678
+ code: "VOICE_GAP",
2679
+ level: "warning",
2680
+ message: `Voice ${voice} (staff ${staff}) has gap from position ${gap.start} to ${gap.end}`,
2681
+ location: { ...location, voice, staff },
2682
+ details: {
2683
+ gapStart: gap.start,
2684
+ gapEnd: gap.end,
2685
+ gapDuration: gap.end - gap.start
2686
+ }
2687
+ });
2688
+ }
2689
+ if (lastEnd < expectedDuration) {
2690
+ errors.push({
2691
+ code: "VOICE_INCOMPLETE",
2692
+ level: "warning",
2693
+ message: `Voice ${voice} (staff ${staff}) ends at ${lastEnd}, expected ${expectedDuration}`,
2694
+ location: { ...location, voice, staff },
2695
+ details: {
2696
+ actualEnd: lastEnd,
2697
+ expectedDuration,
2698
+ missing: expectedDuration - lastEnd
2699
+ }
2700
+ });
2701
+ }
2702
+ }
2703
+ return errors;
2704
+ }
2541
2705
  function validateBackupForward(measure, location) {
2542
2706
  const errors = [];
2543
2707
  let position = 0;
@@ -3004,6 +3168,8 @@ function validateStaffStructure(part, partIndex) {
3004
3168
  }
3005
3169
  var DEFAULT_LOCAL_OPTIONS = {
3006
3170
  checkMeasureDuration: true,
3171
+ checkMeasureFullness: false,
3172
+ // Piano Roll semantics - opt-in
3007
3173
  checkPosition: true,
3008
3174
  checkBeams: true,
3009
3175
  checkTuplets: true,
@@ -3028,6 +3194,14 @@ function validateMeasureLocal(measure, context, options = {}) {
3028
3194
  opts.durationTolerance
3029
3195
  ));
3030
3196
  }
3197
+ if (opts.checkMeasureFullness && context.time) {
3198
+ errors.push(...validateMeasureFullness(
3199
+ measure,
3200
+ context.divisions,
3201
+ context.time,
3202
+ location
3203
+ ));
3204
+ }
3031
3205
  if (opts.checkPosition) {
3032
3206
  errors.push(...validateBackupForward(measure, location));
3033
3207
  }
@@ -3594,7 +3768,11 @@ function serializeScorePart(part, indent) {
3594
3768
  lines.push(`${indent} <solo/>`);
3595
3769
  }
3596
3770
  if (inst.ensemble !== void 0) {
3597
- lines.push(`${indent} <ensemble>${inst.ensemble}</ensemble>`);
3771
+ if (inst.ensemble === 0) {
3772
+ lines.push(`${indent} <ensemble/>`);
3773
+ } else {
3774
+ lines.push(`${indent} <ensemble>${inst.ensemble}</ensemble>`);
3775
+ }
3598
3776
  }
3599
3777
  lines.push(`${indent} </score-instrument>`);
3600
3778
  }
@@ -3774,6 +3952,9 @@ function serializeAttributes(attrs, indent) {
3774
3952
  if (attrs.staves !== void 0) {
3775
3953
  lines.push(`${indent} <staves>${attrs.staves}</staves>`);
3776
3954
  }
3955
+ if (attrs.instruments !== void 0) {
3956
+ lines.push(`${indent} <instruments>${attrs.instruments}</instruments>`);
3957
+ }
3777
3958
  if (attrs.clef) {
3778
3959
  for (const clef of attrs.clef) {
3779
3960
  lines.push(...serializeClef(clef, indent + " "));
@@ -3862,8 +4043,10 @@ function serializeTime(time, indent) {
3862
4043
  }
3863
4044
  function serializeClef(clef, indent) {
3864
4045
  const lines = [];
3865
- const numberAttr = clef.staff ? ` number="${clef.staff}"` : "";
3866
- lines.push(`${indent}<clef${numberAttr}>`);
4046
+ let attrs = clef.staff ? ` number="${clef.staff}"` : "";
4047
+ if (clef.printObject === false) attrs += ' print-object="no"';
4048
+ if (clef.afterBarline) attrs += ' after-barline="yes"';
4049
+ lines.push(`${indent}<clef${attrs}>`);
3867
4050
  lines.push(`${indent} <sign>${clef.sign}</sign>`);
3868
4051
  lines.push(`${indent} <line>${clef.line}</line>`);
3869
4052
  if (clef.clefOctaveChange !== void 0) {
@@ -4112,6 +4295,7 @@ function serializeNotationsGroup(notations, indent) {
4112
4295
  if (notation.number !== void 0) attrs += ` number="${notation.number}"`;
4113
4296
  attrs += ` type="${notation.slurType}"`;
4114
4297
  if (notation.lineType) attrs += ` line-type="${notation.lineType}"`;
4298
+ if (notation.orientation) attrs += ` orientation="${notation.orientation}"`;
4115
4299
  if (notation.defaultX !== void 0) attrs += ` default-x="${notation.defaultX}"`;
4116
4300
  if (notation.defaultY !== void 0) attrs += ` default-y="${notation.defaultY}"`;
4117
4301
  if (notation.bezierX !== void 0) attrs += ` bezier-x="${notation.bezierX}"`;
@@ -4425,6 +4609,15 @@ function serializeDirection(direction, indent) {
4425
4609
  if (direction.sound.dynamics !== void 0) {
4426
4610
  attrs2.push(`dynamics="${direction.sound.dynamics}"`);
4427
4611
  }
4612
+ if (direction.sound.damperPedal) {
4613
+ attrs2.push(`damper-pedal="${direction.sound.damperPedal}"`);
4614
+ }
4615
+ if (direction.sound.softPedal) {
4616
+ attrs2.push(`soft-pedal="${direction.sound.softPedal}"`);
4617
+ }
4618
+ if (direction.sound.sostenutoPedal) {
4619
+ attrs2.push(`sostenuto-pedal="${direction.sound.sostenutoPedal}"`);
4620
+ }
4428
4621
  const attrStr = attrs2.length > 0 ? ` ${attrs2.join(" ")}` : "";
4429
4622
  if (direction.sound.midiInstrument) {
4430
4623
  lines.push(`${indent} <sound${attrStr}>`);
@@ -4529,6 +4722,8 @@ function serializeDirectionType(dirType, indent) {
4529
4722
  if (dirType.number !== void 0) bracketAttrs += ` number="${dirType.number}"`;
4530
4723
  if (dirType.lineEnd) bracketAttrs += ` line-end="${dirType.lineEnd}"`;
4531
4724
  if (dirType.lineType) bracketAttrs += ` line-type="${dirType.lineType}"`;
4725
+ if (dirType.defaultY !== void 0) bracketAttrs += ` default-y="${dirType.defaultY}"`;
4726
+ if (dirType.relativeX !== void 0) bracketAttrs += ` relative-x="${dirType.relativeX}"`;
4532
4727
  lines.push(`${indent} <bracket${bracketAttrs}/>`);
4533
4728
  break;
4534
4729
  }
@@ -4863,6 +5058,9 @@ function serializeSound(sound, indent) {
4863
5058
  if (sound.tocoda) attrs.push(`tocoda="${escapeXml(sound.tocoda)}"`);
4864
5059
  if (sound.fine) attrs.push('fine="yes"');
4865
5060
  if (sound.forwardRepeat) attrs.push('forward-repeat="yes"');
5061
+ if (sound.damperPedal) attrs.push(`damper-pedal="${sound.damperPedal === true ? "yes" : sound.damperPedal}"`);
5062
+ if (sound.softPedal) attrs.push(`soft-pedal="${sound.softPedal === true ? "yes" : sound.softPedal}"`);
5063
+ if (sound.sostenutoPedal) attrs.push(`sostenuto-pedal="${sound.sostenutoPedal === true ? "yes" : sound.sostenutoPedal}"`);
4866
5064
  const attrStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
4867
5065
  if (sound.swing) {
4868
5066
  lines.push(`${indent}<sound${attrStr}>`);
@@ -5455,178 +5653,1381 @@ function getNormalizedDuration(note, options) {
5455
5653
  const currentDivisions = options.currentDivisions ?? 1;
5456
5654
  return note.duration * options.baseDivisions / currentDivisions;
5457
5655
  }
5458
-
5459
- // src/query/index.ts
5460
- function getMeasure(score, options) {
5461
- const part = score.parts[options.part];
5462
- if (!part) return void 0;
5463
- const targetMeasure = String(options.measure);
5464
- return part.measures.find((m) => m.number === targetMeasure);
5465
- }
5466
- function getMeasureByIndex(score, options) {
5467
- const part = score.parts[options.part];
5468
- if (!part) return void 0;
5469
- return part.measures[options.measureIndex];
5470
- }
5471
- function getMeasureCount(score) {
5472
- if (score.parts.length === 0) return 0;
5473
- return score.parts[0].measures.length;
5474
- }
5475
- function getDivisions(score, options) {
5476
- const part = score.parts[options.part];
5477
- if (!part) return 1;
5478
- const targetMeasure = parseInt(String(options.measure), 10);
5479
- if (isNaN(targetMeasure)) return 1;
5480
- for (let i = 0; i < part.measures.length; i++) {
5481
- const m = part.measures[i];
5482
- const mNum = parseInt(m.number, 10);
5483
- if (!isNaN(mNum) && mNum > targetMeasure) break;
5484
- if (m.attributes?.divisions !== void 0) {
5656
+ function getEntriesForStaff(measure, staff) {
5657
+ return measure.entries.filter((entry) => {
5658
+ if (entry.type === "note") {
5659
+ return (entry.staff ?? 1) === staff;
5485
5660
  }
5486
- }
5487
- let divisions = 1;
5488
- for (const m of part.measures) {
5489
- const mNum = parseInt(m.number, 10);
5490
- if (!isNaN(mNum) && mNum > targetMeasure) break;
5491
- if (m.attributes?.divisions !== void 0) {
5492
- divisions = m.attributes.divisions;
5661
+ if (entry.type === "forward") {
5662
+ return (entry.staff ?? 1) === staff;
5493
5663
  }
5494
- }
5495
- return divisions;
5664
+ if (entry.type === "direction") {
5665
+ return (entry.staff ?? 1) === staff;
5666
+ }
5667
+ if (entry.type === "backup") {
5668
+ return false;
5669
+ }
5670
+ return false;
5671
+ });
5496
5672
  }
5497
- function getAttributesAtMeasure(score, options) {
5498
- const part = score.parts[options.part];
5499
- if (!part) return {};
5500
- const targetMeasure = parseInt(String(options.measure), 10);
5501
- const result = {};
5502
- for (const m of part.measures) {
5503
- const mNum = parseInt(m.number, 10);
5504
- if (!isNaN(targetMeasure) && !isNaN(mNum) && mNum > targetMeasure) break;
5505
- if (m.attributes) {
5506
- if (m.attributes.divisions !== void 0) result.divisions = m.attributes.divisions;
5507
- if (m.attributes.time !== void 0) result.time = m.attributes.time;
5508
- if (m.attributes.key !== void 0) result.key = m.attributes.key;
5509
- if (m.attributes.clef !== void 0) result.clef = m.attributes.clef;
5510
- if (m.attributes.staves !== void 0) result.staves = m.attributes.staves;
5511
- if (m.attributes.transpose !== void 0) result.transpose = m.attributes.transpose;
5673
+ function buildVoiceToStaffMap(measure) {
5674
+ const map = /* @__PURE__ */ new Map();
5675
+ for (const entry of measure.entries) {
5676
+ if (entry.type === "note" && entry.staff !== void 0) {
5677
+ const voice = entry.voice;
5678
+ const staff = entry.staff;
5679
+ if (!map.has(voice)) {
5680
+ map.set(voice, staff);
5681
+ }
5512
5682
  }
5513
5683
  }
5514
- return result;
5684
+ return {
5685
+ get: (voice) => map.get(voice),
5686
+ has: (voice) => map.has(voice),
5687
+ entries: () => map.entries(),
5688
+ size: map.size
5689
+ };
5515
5690
  }
5516
- function findNotes(score, filter) {
5517
- const results = [];
5518
- for (const part of score.parts) {
5519
- for (const measure of part.measures) {
5520
- for (const entry of measure.entries) {
5521
- if (entry.type !== "note") continue;
5522
- if (filter.pitchRange && entry.pitch) {
5523
- const noteValue = pitchToSemitone(entry.pitch);
5524
- if (filter.pitchRange.min) {
5525
- const minValue = pitchToSemitone(filter.pitchRange.min);
5526
- if (noteValue < minValue) continue;
5527
- }
5528
- if (filter.pitchRange.max) {
5529
- const maxValue = pitchToSemitone(filter.pitchRange.max);
5530
- if (noteValue > maxValue) continue;
5531
- }
5532
- }
5533
- if (filter.voice !== void 0 && entry.voice !== filter.voice) continue;
5534
- if (filter.staff !== void 0 && (entry.staff ?? 1) !== filter.staff) continue;
5535
- if (filter.noteType !== void 0 && entry.noteType !== filter.noteType) continue;
5536
- if (filter.hasTie !== void 0) {
5537
- const hasTie = entry.tie !== void 0;
5538
- if (filter.hasTie !== hasTie) continue;
5691
+ function buildVoiceToStaffMapForPart(part) {
5692
+ const map = /* @__PURE__ */ new Map();
5693
+ for (const measure of part.measures) {
5694
+ for (const entry of measure.entries) {
5695
+ if (entry.type === "note" && entry.staff !== void 0) {
5696
+ const voice = entry.voice;
5697
+ const staff = entry.staff;
5698
+ if (!map.has(voice)) {
5699
+ map.set(voice, staff);
5539
5700
  }
5540
- results.push(entry);
5541
5701
  }
5542
5702
  }
5543
5703
  }
5544
- return results;
5704
+ return {
5705
+ get: (voice) => map.get(voice),
5706
+ has: (voice) => map.has(voice),
5707
+ entries: () => map.entries(),
5708
+ size: map.size
5709
+ };
5545
5710
  }
5546
- function getDuration(score) {
5547
- if (score.parts.length === 0) return 0;
5548
- const part = score.parts[0];
5549
- let totalDuration = 0;
5550
- let divisions = 1;
5551
- for (const measure of part.measures) {
5552
- if (measure.attributes?.divisions !== void 0) {
5553
- divisions = measure.attributes.divisions;
5554
- }
5555
- let measureDuration = 0;
5711
+ function inferStaff(entry, voiceToStaffMap) {
5712
+ if (entry.staff !== void 0) {
5713
+ return entry.staff;
5714
+ }
5715
+ const inferredStaff = voiceToStaffMap.get(entry.voice);
5716
+ if (inferredStaff !== void 0) {
5717
+ return inferredStaff;
5718
+ }
5719
+ return 1;
5720
+ }
5721
+ function getEffectiveStaff(entry, measure) {
5722
+ if (entry.staff !== void 0) {
5723
+ return entry.staff;
5724
+ }
5725
+ const map = buildVoiceToStaffMap(measure);
5726
+ return inferStaff(entry, map);
5727
+ }
5728
+ function getClefForStaff(score, options) {
5729
+ const part = score.parts[options.partIndex];
5730
+ if (!part) return void 0;
5731
+ for (let i = options.measureIndex; i >= 0; i--) {
5732
+ const measure = part.measures[i];
5556
5733
  for (const entry of measure.entries) {
5557
- if (entry.type === "note" && !entry.chord) {
5558
- measureDuration = Math.max(measureDuration, entry.duration);
5734
+ if (entry.type === "attributes" && entry.attributes.clef) {
5735
+ for (const clef of entry.attributes.clef) {
5736
+ if ((clef.staff ?? 1) === options.staff) {
5737
+ return clef;
5738
+ }
5739
+ }
5559
5740
  }
5560
5741
  }
5561
- const attrs = getAttributesAtMeasure(score, { part: 0, measure: measure.number });
5562
- if (attrs.time) {
5563
- const beats = parseInt(attrs.time.beats, 10) || 4;
5564
- const expectedDuration = beats / attrs.time.beatType * 4 * divisions;
5565
- measureDuration = Math.max(measureDuration, expectedDuration);
5742
+ if (measure.attributes?.clef) {
5743
+ for (const clef of measure.attributes.clef) {
5744
+ if ((clef.staff ?? 1) === options.staff) {
5745
+ return clef;
5746
+ }
5747
+ }
5566
5748
  }
5567
- totalDuration += measureDuration;
5568
5749
  }
5569
- return totalDuration;
5570
- }
5571
- function getPartById(score, id) {
5572
- return score.parts.find((p) => p.id === id);
5573
- }
5574
- function getPartIndex(score, id) {
5575
- return score.parts.findIndex((p) => p.id === id);
5750
+ return void 0;
5576
5751
  }
5577
- function hasMultipleStaves(score, partIndex = 0) {
5578
- const part = score.parts[partIndex];
5579
- if (!part) return false;
5580
- for (const measure of part.measures) {
5581
- if (measure.attributes?.staves !== void 0 && measure.attributes.staves > 1) {
5582
- return true;
5752
+ function getVoicesForStaff(measure, staff) {
5753
+ const voices = /* @__PURE__ */ new Set();
5754
+ for (const entry of measure.entries) {
5755
+ if (entry.type === "note") {
5756
+ const entryStaff = entry.staff ?? 1;
5757
+ if (entryStaff === staff) {
5758
+ voices.add(entry.voice);
5759
+ }
5583
5760
  }
5584
5761
  }
5585
- return false;
5762
+ return Array.from(voices).sort((a, b) => a - b);
5586
5763
  }
5587
- function getStaveCount(score, partIndex = 0) {
5764
+ function getStaffRange(score, partIndex) {
5588
5765
  const part = score.parts[partIndex];
5589
- if (!part) return 1;
5766
+ if (!part) return { min: 1, max: 1 };
5767
+ let min = 1;
5768
+ let max = 1;
5590
5769
  for (const measure of part.measures) {
5591
5770
  if (measure.attributes?.staves !== void 0) {
5592
- return measure.attributes.staves;
5771
+ max = Math.max(max, measure.attributes.staves);
5772
+ }
5773
+ for (const entry of measure.entries) {
5774
+ if (entry.type === "note" && entry.staff !== void 0) {
5775
+ max = Math.max(max, entry.staff);
5776
+ }
5593
5777
  }
5594
5778
  }
5595
- return 1;
5596
- }
5597
- function measureRoundtrip(original, exported) {
5598
- const notesOriginal = countNotes(original);
5599
- const notesExported = countNotes(exported);
5600
- const notesPreserved = Math.min(notesOriginal, notesExported);
5601
- const measuresOriginal = getMeasureCount(original);
5602
- const measuresExported = getMeasureCount(exported);
5603
- const measuresPreserved = Math.min(measuresOriginal, measuresExported);
5604
- const partsOriginal = original.parts.length;
5605
- const partsExported = exported.parts.length;
5606
- const partsPreserved = Math.min(partsOriginal, partsExported);
5607
- const preservationRate = notesOriginal > 0 ? notesPreserved / notesOriginal : 1;
5608
- return {
5609
- notesOriginal,
5610
- notesPreserved,
5611
- measuresOriginal,
5612
- measuresPreserved,
5613
- partsOriginal,
5614
- partsPreserved,
5615
- preservationRate
5616
- };
5779
+ return { min, max };
5617
5780
  }
5618
- function countNotes(score) {
5619
- let count = 0;
5620
- for (const part of score.parts) {
5621
- for (const measure of part.measures) {
5622
- for (const entry of measure.entries) {
5623
- if (entry.type === "note") {
5624
- count++;
5781
+ function getEntriesAtPosition(measure, position, options) {
5782
+ const result = [];
5783
+ const state = createPositionState();
5784
+ for (const entry of measure.entries) {
5785
+ const currentPosition = entry.type === "note" && entry.chord ? state.lastNonChordPosition : state.position;
5786
+ if (currentPosition === position) {
5787
+ if (entry.type === "note") {
5788
+ if (options?.staff !== void 0 && (entry.staff ?? 1) !== options.staff) {
5789
+ updatePositionForEntry(state, entry);
5790
+ continue;
5791
+ }
5792
+ if (options?.voice !== void 0 && entry.voice !== options.voice) {
5793
+ updatePositionForEntry(state, entry);
5794
+ continue;
5795
+ }
5796
+ if (options?.includeChordNotes === false && entry.chord) {
5797
+ updatePositionForEntry(state, entry);
5798
+ continue;
5625
5799
  }
5626
5800
  }
5801
+ result.push(entry);
5627
5802
  }
5803
+ updatePositionForEntry(state, entry);
5628
5804
  }
5629
- return count;
5805
+ return result;
5806
+ }
5807
+ function getNotesAtPosition(measure, position, options) {
5808
+ return getEntriesAtPosition(measure, position, options).filter(
5809
+ (entry) => entry.type === "note"
5810
+ );
5811
+ }
5812
+ function getEntriesInRange(measure, range, options) {
5813
+ const result = [];
5814
+ const state = createPositionState();
5815
+ for (const entry of measure.entries) {
5816
+ const currentPosition = entry.type === "note" && entry.chord ? state.lastNonChordPosition : state.position;
5817
+ if (currentPosition >= range.start && currentPosition < range.end) {
5818
+ if (entry.type === "note") {
5819
+ if (options?.staff !== void 0 && (entry.staff ?? 1) !== options.staff) {
5820
+ updatePositionForEntry(state, entry);
5821
+ continue;
5822
+ }
5823
+ if (options?.voice !== void 0 && entry.voice !== options.voice) {
5824
+ updatePositionForEntry(state, entry);
5825
+ continue;
5826
+ }
5827
+ if (options?.includeChordNotes === false && entry.chord) {
5828
+ updatePositionForEntry(state, entry);
5829
+ continue;
5830
+ }
5831
+ }
5832
+ result.push(entry);
5833
+ }
5834
+ updatePositionForEntry(state, entry);
5835
+ }
5836
+ return result;
5837
+ }
5838
+ function getNotesInRange(measure, range, options) {
5839
+ return getEntriesInRange(measure, range, options).filter(
5840
+ (entry) => entry.type === "note"
5841
+ );
5842
+ }
5843
+ function getVerticalSlice(score, options) {
5844
+ const parts = /* @__PURE__ */ new Map();
5845
+ for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
5846
+ const part = score.parts[partIndex];
5847
+ const measure = part.measures[options.measureIndex];
5848
+ if (!measure) continue;
5849
+ const notes = getNotesAtPosition(measure, options.position);
5850
+ if (notes.length > 0) {
5851
+ parts.set(partIndex, notes);
5852
+ }
5853
+ }
5854
+ return {
5855
+ measureIndex: options.measureIndex,
5856
+ position: options.position,
5857
+ parts
5858
+ };
5859
+ }
5860
+ function getVoiceLine(score, options) {
5861
+ const part = score.parts[options.partIndex];
5862
+ if (!part) {
5863
+ return { partIndex: options.partIndex, voice: options.voice, staff: options.staff, notes: [] };
5864
+ }
5865
+ const notes = [];
5866
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
5867
+ const measure = part.measures[measureIndex];
5868
+ const state = createPositionState();
5869
+ for (const entry of measure.entries) {
5870
+ if (entry.type === "note") {
5871
+ const entryStaff = entry.staff ?? 1;
5872
+ const matchesVoice = entry.voice === options.voice;
5873
+ const matchesStaff = options.staff === void 0 || entryStaff === options.staff;
5874
+ if (matchesVoice && matchesStaff) {
5875
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
5876
+ notes.push({
5877
+ note: entry,
5878
+ part,
5879
+ partIndex: options.partIndex,
5880
+ measure,
5881
+ measureIndex,
5882
+ position
5883
+ });
5884
+ }
5885
+ }
5886
+ updatePositionForEntry(state, entry);
5887
+ }
5888
+ }
5889
+ return {
5890
+ partIndex: options.partIndex,
5891
+ voice: options.voice,
5892
+ staff: options.staff,
5893
+ notes
5894
+ };
5895
+ }
5896
+ function getVoiceLineInRange(score, options) {
5897
+ const part = score.parts[options.partIndex];
5898
+ if (!part) {
5899
+ return { partIndex: options.partIndex, voice: options.voice, staff: options.staff, notes: [] };
5900
+ }
5901
+ const notes = [];
5902
+ for (let measureIndex = options.startMeasure; measureIndex <= options.endMeasure && measureIndex < part.measures.length; measureIndex++) {
5903
+ const measure = part.measures[measureIndex];
5904
+ const state = createPositionState();
5905
+ for (const entry of measure.entries) {
5906
+ if (entry.type === "note") {
5907
+ const entryStaff = entry.staff ?? 1;
5908
+ const matchesVoice = entry.voice === options.voice;
5909
+ const matchesStaff = options.staff === void 0 || entryStaff === options.staff;
5910
+ if (matchesVoice && matchesStaff) {
5911
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
5912
+ notes.push({
5913
+ note: entry,
5914
+ part,
5915
+ partIndex: options.partIndex,
5916
+ measure,
5917
+ measureIndex,
5918
+ position
5919
+ });
5920
+ }
5921
+ }
5922
+ updatePositionForEntry(state, entry);
5923
+ }
5924
+ }
5925
+ return {
5926
+ partIndex: options.partIndex,
5927
+ voice: options.voice,
5928
+ staff: options.staff,
5929
+ notes
5930
+ };
5931
+ }
5932
+ function* iterateEntries(score) {
5933
+ for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
5934
+ const part = score.parts[partIndex];
5935
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
5936
+ const measure = part.measures[measureIndex];
5937
+ const state = createPositionState();
5938
+ for (const entry of measure.entries) {
5939
+ const position = entry.type === "note" && entry.chord ? state.lastNonChordPosition : state.position;
5940
+ yield {
5941
+ entry,
5942
+ part,
5943
+ partIndex,
5944
+ measure,
5945
+ measureIndex,
5946
+ position
5947
+ };
5948
+ updatePositionForEntry(state, entry);
5949
+ }
5950
+ }
5951
+ }
5952
+ }
5953
+ function getNextNote(score, context) {
5954
+ const part = score.parts[context.partIndex];
5955
+ if (!part) return null;
5956
+ let foundCurrent = false;
5957
+ for (let measureIndex = context.measureIndex; measureIndex < part.measures.length; measureIndex++) {
5958
+ const measure = part.measures[measureIndex];
5959
+ const state = createPositionState();
5960
+ for (const entry of measure.entries) {
5961
+ if (entry.type === "note") {
5962
+ const entryPosition = entry.chord ? state.lastNonChordPosition : state.position;
5963
+ if (measureIndex === context.measureIndex && entry === context.note) {
5964
+ foundCurrent = true;
5965
+ updatePositionForEntry(state, entry);
5966
+ continue;
5967
+ }
5968
+ if (foundCurrent && entry.voice === context.note.voice && !entry.chord) {
5969
+ if (context.note.staff !== void 0) {
5970
+ if ((entry.staff ?? 1) !== context.note.staff) {
5971
+ updatePositionForEntry(state, entry);
5972
+ continue;
5973
+ }
5974
+ }
5975
+ return {
5976
+ note: entry,
5977
+ part,
5978
+ partIndex: context.partIndex,
5979
+ measure,
5980
+ measureIndex,
5981
+ position: entryPosition
5982
+ };
5983
+ }
5984
+ }
5985
+ updatePositionForEntry(state, entry);
5986
+ }
5987
+ }
5988
+ return null;
5989
+ }
5990
+ function getPrevNote(score, context) {
5991
+ const part = score.parts[context.partIndex];
5992
+ if (!part) return null;
5993
+ let lastCandidate = null;
5994
+ for (let measureIndex = 0; measureIndex <= context.measureIndex; measureIndex++) {
5995
+ const measure = part.measures[measureIndex];
5996
+ const state = createPositionState();
5997
+ for (const entry of measure.entries) {
5998
+ if (entry.type === "note") {
5999
+ const entryPosition = entry.chord ? state.lastNonChordPosition : state.position;
6000
+ if (measureIndex === context.measureIndex && entry === context.note) {
6001
+ return lastCandidate;
6002
+ }
6003
+ if (entry.voice === context.note.voice && !entry.chord) {
6004
+ if (context.note.staff !== void 0) {
6005
+ if ((entry.staff ?? 1) !== context.note.staff) {
6006
+ updatePositionForEntry(state, entry);
6007
+ continue;
6008
+ }
6009
+ }
6010
+ lastCandidate = {
6011
+ note: entry,
6012
+ part,
6013
+ partIndex: context.partIndex,
6014
+ measure,
6015
+ measureIndex,
6016
+ position: entryPosition
6017
+ };
6018
+ }
6019
+ }
6020
+ updatePositionForEntry(state, entry);
6021
+ }
6022
+ }
6023
+ return null;
6024
+ }
6025
+ function getAdjacentNotes(score, context) {
6026
+ return {
6027
+ prev: getPrevNote(score, context),
6028
+ next: getNextNote(score, context)
6029
+ };
6030
+ }
6031
+ function getDirections(score, options) {
6032
+ const results = [];
6033
+ const startPart = options?.partIndex ?? 0;
6034
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6035
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6036
+ const part = score.parts[partIndex];
6037
+ if (!part) continue;
6038
+ const startMeasure = options?.measureIndex ?? 0;
6039
+ const endMeasure = options?.measureIndex !== void 0 ? options.measureIndex + 1 : part.measures.length;
6040
+ for (let measureIndex = startMeasure; measureIndex < endMeasure; measureIndex++) {
6041
+ const measure = part.measures[measureIndex];
6042
+ if (!measure) continue;
6043
+ const state = createPositionState();
6044
+ for (const entry of measure.entries) {
6045
+ if (entry.type === "direction") {
6046
+ results.push({
6047
+ direction: entry,
6048
+ part,
6049
+ partIndex,
6050
+ measure,
6051
+ measureIndex,
6052
+ position: state.position
6053
+ });
6054
+ }
6055
+ updatePositionForEntry(state, entry);
6056
+ }
6057
+ }
6058
+ }
6059
+ return results;
6060
+ }
6061
+ function getDirectionsAtPosition(measure, position) {
6062
+ const results = [];
6063
+ const state = createPositionState();
6064
+ for (const entry of measure.entries) {
6065
+ if (entry.type === "direction" && state.position === position) {
6066
+ results.push(entry);
6067
+ }
6068
+ updatePositionForEntry(state, entry);
6069
+ }
6070
+ return results;
6071
+ }
6072
+ function findDirectionsByType(score, kind) {
6073
+ const allDirections = getDirections(score);
6074
+ return allDirections.filter(
6075
+ (d) => d.direction.directionTypes.some((dt) => dt.kind === kind)
6076
+ );
6077
+ }
6078
+ function getDynamics(score, options) {
6079
+ const results = [];
6080
+ const startPart = options?.partIndex ?? 0;
6081
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6082
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6083
+ const part = score.parts[partIndex];
6084
+ if (!part) continue;
6085
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6086
+ const measure = part.measures[measureIndex];
6087
+ const state = createPositionState();
6088
+ for (const entry of measure.entries) {
6089
+ if (entry.type === "direction") {
6090
+ for (const dirType of entry.directionTypes) {
6091
+ if (dirType.kind === "dynamics") {
6092
+ results.push({
6093
+ dynamic: dirType.value,
6094
+ direction: entry,
6095
+ part,
6096
+ partIndex,
6097
+ measure,
6098
+ measureIndex,
6099
+ position: state.position
6100
+ });
6101
+ }
6102
+ }
6103
+ }
6104
+ updatePositionForEntry(state, entry);
6105
+ }
6106
+ }
6107
+ }
6108
+ return results;
6109
+ }
6110
+ function getTempoMarkings(score) {
6111
+ const results = [];
6112
+ for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
6113
+ const part = score.parts[partIndex];
6114
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6115
+ const measure = part.measures[measureIndex];
6116
+ const state = createPositionState();
6117
+ for (const entry of measure.entries) {
6118
+ if (entry.type === "direction") {
6119
+ for (const dirType of entry.directionTypes) {
6120
+ if (dirType.kind === "metronome") {
6121
+ results.push({
6122
+ beatUnit: dirType.beatUnit,
6123
+ perMinute: dirType.perMinute,
6124
+ beatUnitDot: dirType.beatUnitDot,
6125
+ direction: entry,
6126
+ partIndex,
6127
+ measureIndex,
6128
+ position: state.position
6129
+ });
6130
+ }
6131
+ }
6132
+ }
6133
+ updatePositionForEntry(state, entry);
6134
+ }
6135
+ }
6136
+ }
6137
+ return results;
6138
+ }
6139
+ function getPedalMarkings(score, options) {
6140
+ const results = [];
6141
+ const startPart = options?.partIndex ?? 0;
6142
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6143
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6144
+ const part = score.parts[partIndex];
6145
+ if (!part) continue;
6146
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6147
+ const measure = part.measures[measureIndex];
6148
+ const state = createPositionState();
6149
+ for (const entry of measure.entries) {
6150
+ if (entry.type === "direction") {
6151
+ for (const dirType of entry.directionTypes) {
6152
+ if (dirType.kind === "pedal") {
6153
+ results.push({
6154
+ pedalType: dirType.type,
6155
+ direction: entry,
6156
+ partIndex,
6157
+ measureIndex,
6158
+ position: state.position
6159
+ });
6160
+ }
6161
+ }
6162
+ }
6163
+ updatePositionForEntry(state, entry);
6164
+ }
6165
+ }
6166
+ }
6167
+ return results;
6168
+ }
6169
+ function getWedges(score, options) {
6170
+ const results = [];
6171
+ const startPart = options?.partIndex ?? 0;
6172
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6173
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6174
+ const part = score.parts[partIndex];
6175
+ if (!part) continue;
6176
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6177
+ const measure = part.measures[measureIndex];
6178
+ const state = createPositionState();
6179
+ for (const entry of measure.entries) {
6180
+ if (entry.type === "direction") {
6181
+ for (const dirType of entry.directionTypes) {
6182
+ if (dirType.kind === "wedge") {
6183
+ results.push({
6184
+ wedgeType: dirType.type,
6185
+ direction: entry,
6186
+ partIndex,
6187
+ measureIndex,
6188
+ position: state.position
6189
+ });
6190
+ }
6191
+ }
6192
+ }
6193
+ updatePositionForEntry(state, entry);
6194
+ }
6195
+ }
6196
+ }
6197
+ return results;
6198
+ }
6199
+ function getOctaveShifts(score, options) {
6200
+ const results = [];
6201
+ const startPart = options?.partIndex ?? 0;
6202
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6203
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6204
+ const part = score.parts[partIndex];
6205
+ if (!part) continue;
6206
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6207
+ const measure = part.measures[measureIndex];
6208
+ const state = createPositionState();
6209
+ for (const entry of measure.entries) {
6210
+ if (entry.type === "direction") {
6211
+ for (const dirType of entry.directionTypes) {
6212
+ if (dirType.kind === "octave-shift") {
6213
+ results.push({
6214
+ shiftType: dirType.type,
6215
+ size: dirType.size,
6216
+ direction: entry,
6217
+ partIndex,
6218
+ measureIndex,
6219
+ position: state.position
6220
+ });
6221
+ }
6222
+ }
6223
+ }
6224
+ updatePositionForEntry(state, entry);
6225
+ }
6226
+ }
6227
+ }
6228
+ return results;
6229
+ }
6230
+ function getTiedNoteGroups(score, options) {
6231
+ const results = [];
6232
+ const startPart = options?.partIndex ?? 0;
6233
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6234
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6235
+ const part = score.parts[partIndex];
6236
+ if (!part) continue;
6237
+ const pendingTies = /* @__PURE__ */ new Map();
6238
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6239
+ const measure = part.measures[measureIndex];
6240
+ const state = createPositionState();
6241
+ for (const entry of measure.entries) {
6242
+ if (entry.type === "note" && entry.pitch) {
6243
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6244
+ const pitchKey = `${entry.pitch.step}${entry.pitch.octave}-${entry.voice}`;
6245
+ const context = {
6246
+ note: entry,
6247
+ part,
6248
+ partIndex,
6249
+ measure,
6250
+ measureIndex,
6251
+ position
6252
+ };
6253
+ const hasTieStart = entry.tie?.type === "start" || entry.ties?.some((t) => t.type === "start") || entry.notations?.some((n) => n.type === "tied" && n.tiedType === "start");
6254
+ const hasTieStop = entry.tie?.type === "stop" || entry.ties?.some((t) => t.type === "stop") || entry.notations?.some((n) => n.type === "tied" && n.tiedType === "stop");
6255
+ if (hasTieStop && pendingTies.has(pitchKey)) {
6256
+ const group = pendingTies.get(pitchKey);
6257
+ group.push(context);
6258
+ if (!hasTieStart) {
6259
+ const totalDuration = group.reduce((sum, nc) => sum + nc.note.duration, 0);
6260
+ results.push({ notes: group, totalDuration });
6261
+ pendingTies.delete(pitchKey);
6262
+ }
6263
+ } else if (hasTieStart) {
6264
+ pendingTies.set(pitchKey, [context]);
6265
+ }
6266
+ }
6267
+ updatePositionForEntry(state, entry);
6268
+ }
6269
+ }
6270
+ }
6271
+ return results;
6272
+ }
6273
+ function getSlurSpans(score, options) {
6274
+ const results = [];
6275
+ const startPart = options?.partIndex ?? 0;
6276
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6277
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6278
+ const part = score.parts[partIndex];
6279
+ if (!part) continue;
6280
+ const pendingSlurs = /* @__PURE__ */ new Map();
6281
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6282
+ const measure = part.measures[measureIndex];
6283
+ const state = createPositionState();
6284
+ for (const entry of measure.entries) {
6285
+ if (entry.type === "note") {
6286
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6287
+ const context = {
6288
+ note: entry,
6289
+ part,
6290
+ partIndex,
6291
+ measure,
6292
+ measureIndex,
6293
+ position
6294
+ };
6295
+ if (entry.notations) {
6296
+ for (const notation of entry.notations) {
6297
+ if (notation.type === "slur") {
6298
+ const slurNumber = notation.number ?? 1;
6299
+ if (notation.slurType === "start") {
6300
+ pendingSlurs.set(slurNumber, { startNote: context, notes: [context] });
6301
+ } else if (notation.slurType === "stop" && pendingSlurs.has(slurNumber)) {
6302
+ const pending = pendingSlurs.get(slurNumber);
6303
+ pending.notes.push(context);
6304
+ results.push({
6305
+ number: slurNumber,
6306
+ startNote: pending.startNote,
6307
+ endNote: context,
6308
+ notes: pending.notes
6309
+ });
6310
+ pendingSlurs.delete(slurNumber);
6311
+ } else if (notation.slurType === "continue" && pendingSlurs.has(slurNumber)) {
6312
+ pendingSlurs.get(slurNumber).notes.push(context);
6313
+ }
6314
+ }
6315
+ }
6316
+ }
6317
+ for (const [, pending] of pendingSlurs) {
6318
+ const lastNote = pending.notes[pending.notes.length - 1];
6319
+ if (lastNote !== context) {
6320
+ pending.notes.push(context);
6321
+ }
6322
+ }
6323
+ }
6324
+ updatePositionForEntry(state, entry);
6325
+ }
6326
+ }
6327
+ }
6328
+ return results;
6329
+ }
6330
+ function getTupletGroups(score, options) {
6331
+ const results = [];
6332
+ const startPart = options?.partIndex ?? 0;
6333
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6334
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6335
+ const part = score.parts[partIndex];
6336
+ if (!part) continue;
6337
+ const pendingTuplets = /* @__PURE__ */ new Map();
6338
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6339
+ const measure = part.measures[measureIndex];
6340
+ const state = createPositionState();
6341
+ for (const entry of measure.entries) {
6342
+ if (entry.type === "note") {
6343
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6344
+ const context = {
6345
+ note: entry,
6346
+ part,
6347
+ partIndex,
6348
+ measure,
6349
+ measureIndex,
6350
+ position
6351
+ };
6352
+ if (entry.notations) {
6353
+ for (const notation of entry.notations) {
6354
+ if (notation.type === "tuplet") {
6355
+ const tupletNumber = notation.number ?? 1;
6356
+ if (notation.tupletType === "start") {
6357
+ const actualNotes = entry.timeModification?.actualNotes ?? 3;
6358
+ const normalNotes = entry.timeModification?.normalNotes ?? 2;
6359
+ pendingTuplets.set(tupletNumber, {
6360
+ notes: [context],
6361
+ actualNotes,
6362
+ normalNotes
6363
+ });
6364
+ } else if (notation.tupletType === "stop" && pendingTuplets.has(tupletNumber)) {
6365
+ const pending = pendingTuplets.get(tupletNumber);
6366
+ pending.notes.push(context);
6367
+ results.push({
6368
+ number: tupletNumber,
6369
+ notes: pending.notes,
6370
+ actualNotes: pending.actualNotes,
6371
+ normalNotes: pending.normalNotes
6372
+ });
6373
+ pendingTuplets.delete(tupletNumber);
6374
+ }
6375
+ }
6376
+ }
6377
+ }
6378
+ if (entry.timeModification && !entry.notations?.some((n) => n.type === "tuplet")) {
6379
+ for (const [, pending] of pendingTuplets) {
6380
+ if (entry.timeModification.actualNotes === pending.actualNotes && entry.timeModification.normalNotes === pending.normalNotes) {
6381
+ pending.notes.push(context);
6382
+ }
6383
+ }
6384
+ }
6385
+ }
6386
+ updatePositionForEntry(state, entry);
6387
+ }
6388
+ }
6389
+ }
6390
+ return results;
6391
+ }
6392
+ function getBeamGroups(measure) {
6393
+ const results = [];
6394
+ const state = createPositionState();
6395
+ const pendingBeams = /* @__PURE__ */ new Map();
6396
+ for (const entry of measure.entries) {
6397
+ if (entry.type === "note" && entry.beam) {
6398
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6399
+ const context = {
6400
+ note: entry,
6401
+ part: {},
6402
+ // Will be set by caller if needed
6403
+ partIndex: 0,
6404
+ measure,
6405
+ measureIndex: 0,
6406
+ position
6407
+ };
6408
+ for (const beam of entry.beam) {
6409
+ const beamNumber = beam.number;
6410
+ if (beam.type === "begin") {
6411
+ pendingBeams.set(beamNumber, [context]);
6412
+ } else if (beam.type === "continue" && pendingBeams.has(beamNumber)) {
6413
+ pendingBeams.get(beamNumber).push(context);
6414
+ } else if (beam.type === "end" && pendingBeams.has(beamNumber)) {
6415
+ const group = pendingBeams.get(beamNumber);
6416
+ group.push(context);
6417
+ results.push({
6418
+ notes: group,
6419
+ beamLevel: beamNumber
6420
+ });
6421
+ pendingBeams.delete(beamNumber);
6422
+ }
6423
+ }
6424
+ }
6425
+ updatePositionForEntry(state, entry);
6426
+ }
6427
+ return results;
6428
+ }
6429
+ function findNotesWithNotation(score, notationType, options) {
6430
+ const results = [];
6431
+ const startPart = options?.partIndex ?? 0;
6432
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6433
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6434
+ const part = score.parts[partIndex];
6435
+ if (!part) continue;
6436
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6437
+ const measure = part.measures[measureIndex];
6438
+ const state = createPositionState();
6439
+ for (const entry of measure.entries) {
6440
+ if (entry.type === "note") {
6441
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6442
+ if (entry.notations?.some((n) => n.type === notationType)) {
6443
+ results.push({
6444
+ note: entry,
6445
+ part,
6446
+ partIndex,
6447
+ measure,
6448
+ measureIndex,
6449
+ position
6450
+ });
6451
+ }
6452
+ }
6453
+ updatePositionForEntry(state, entry);
6454
+ }
6455
+ }
6456
+ }
6457
+ return results;
6458
+ }
6459
+ function getHarmonies(score, options) {
6460
+ const results = [];
6461
+ const startPart = options?.partIndex ?? 0;
6462
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6463
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6464
+ const part = score.parts[partIndex];
6465
+ if (!part) continue;
6466
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6467
+ const measure = part.measures[measureIndex];
6468
+ const state = createPositionState();
6469
+ for (const entry of measure.entries) {
6470
+ if (entry.type === "harmony") {
6471
+ results.push({
6472
+ harmony: entry,
6473
+ part,
6474
+ partIndex,
6475
+ measure,
6476
+ measureIndex,
6477
+ position: state.position
6478
+ });
6479
+ }
6480
+ updatePositionForEntry(state, entry);
6481
+ }
6482
+ }
6483
+ }
6484
+ return results;
6485
+ }
6486
+ function getHarmonyAtPosition(measure, position) {
6487
+ const state = createPositionState();
6488
+ let lastHarmony;
6489
+ for (const entry of measure.entries) {
6490
+ if (entry.type === "harmony") {
6491
+ if (state.position <= position) {
6492
+ lastHarmony = entry;
6493
+ }
6494
+ }
6495
+ updatePositionForEntry(state, entry);
6496
+ if (state.position > position && lastHarmony) {
6497
+ break;
6498
+ }
6499
+ }
6500
+ return lastHarmony;
6501
+ }
6502
+ function getChordProgression(score, options) {
6503
+ const harmonies = getHarmonies(score, options);
6504
+ return harmonies.map((h) => {
6505
+ const rootAlter = h.harmony.root.rootAlter ?? 0;
6506
+ const rootAccidental = rootAlter > 0 ? "#".repeat(rootAlter) : "b".repeat(-rootAlter);
6507
+ const root = h.harmony.root.rootStep + rootAccidental;
6508
+ let bass;
6509
+ if (h.harmony.bass) {
6510
+ const bassAlter = h.harmony.bass.bassAlter ?? 0;
6511
+ const bassAccidental = bassAlter > 0 ? "#".repeat(bassAlter) : "b".repeat(-bassAlter);
6512
+ bass = h.harmony.bass.bassStep + bassAccidental;
6513
+ }
6514
+ return {
6515
+ root,
6516
+ kind: h.harmony.kind,
6517
+ bass,
6518
+ measureIndex: h.measureIndex,
6519
+ position: h.position
6520
+ };
6521
+ });
6522
+ }
6523
+ function getLyrics(score, options) {
6524
+ const results = [];
6525
+ const startPart = options?.partIndex ?? 0;
6526
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6527
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6528
+ const part = score.parts[partIndex];
6529
+ if (!part) continue;
6530
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6531
+ const measure = part.measures[measureIndex];
6532
+ const state = createPositionState();
6533
+ for (const entry of measure.entries) {
6534
+ if (entry.type === "note" && entry.lyrics) {
6535
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6536
+ for (const lyric of entry.lyrics) {
6537
+ const verse = lyric.number ?? 1;
6538
+ if (options?.verse !== void 0 && verse !== options.verse) {
6539
+ continue;
6540
+ }
6541
+ results.push({
6542
+ lyric,
6543
+ note: entry,
6544
+ part,
6545
+ partIndex,
6546
+ measure,
6547
+ measureIndex,
6548
+ position,
6549
+ verse
6550
+ });
6551
+ }
6552
+ }
6553
+ updatePositionForEntry(state, entry);
6554
+ }
6555
+ }
6556
+ }
6557
+ return results;
6558
+ }
6559
+ function getLyricText(score, options) {
6560
+ const lyrics = getLyrics(score, options);
6561
+ const verseMap = /* @__PURE__ */ new Map();
6562
+ for (const lyric of lyrics) {
6563
+ if (!verseMap.has(lyric.verse)) {
6564
+ verseMap.set(lyric.verse, []);
6565
+ }
6566
+ verseMap.get(lyric.verse).push(lyric);
6567
+ }
6568
+ const results = [];
6569
+ for (const [verse, verseLyrics] of verseMap) {
6570
+ const syllables = [];
6571
+ const textParts = [];
6572
+ for (const lyric of verseLyrics) {
6573
+ const text = lyric.lyric.text;
6574
+ syllables.push({
6575
+ text,
6576
+ position: lyric.position,
6577
+ measureIndex: lyric.measureIndex
6578
+ });
6579
+ const syllabic = lyric.lyric.syllabic;
6580
+ if (syllabic === "begin" || syllabic === "middle") {
6581
+ textParts.push(text + "-");
6582
+ } else if (syllabic === "end") {
6583
+ textParts.push(text + " ");
6584
+ } else {
6585
+ textParts.push(text + " ");
6586
+ }
6587
+ }
6588
+ results.push({
6589
+ verse,
6590
+ text: textParts.join("").trim(),
6591
+ syllables
6592
+ });
6593
+ }
6594
+ return results.sort((a, b) => a.verse - b.verse);
6595
+ }
6596
+ function getVerseCount(score, options) {
6597
+ const verses = /* @__PURE__ */ new Set();
6598
+ const startPart = options?.partIndex ?? 0;
6599
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6600
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6601
+ const part = score.parts[partIndex];
6602
+ if (!part) continue;
6603
+ for (const measure of part.measures) {
6604
+ for (const entry of measure.entries) {
6605
+ if (entry.type === "note" && entry.lyrics) {
6606
+ for (const lyric of entry.lyrics) {
6607
+ verses.add(lyric.number ?? 1);
6608
+ }
6609
+ }
6610
+ }
6611
+ }
6612
+ }
6613
+ return verses.size;
6614
+ }
6615
+ function getRepeatStructure(score, options) {
6616
+ const results = [];
6617
+ const partIndex = options?.partIndex ?? 0;
6618
+ const part = score.parts[partIndex];
6619
+ if (!part) return results;
6620
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6621
+ const measure = part.measures[measureIndex];
6622
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6623
+ if (measure.barlines) {
6624
+ for (const barline of measure.barlines) {
6625
+ if (barline.repeat) {
6626
+ results.push({
6627
+ type: barline.repeat.direction,
6628
+ times: barline.repeat.times,
6629
+ measureIndex,
6630
+ measureNumber
6631
+ });
6632
+ }
6633
+ }
6634
+ }
6635
+ }
6636
+ return results;
6637
+ }
6638
+ function findBarlines(score, options) {
6639
+ const results = [];
6640
+ const startPart = options?.partIndex ?? 0;
6641
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6642
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6643
+ const part = score.parts[partIndex];
6644
+ if (!part) continue;
6645
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6646
+ const measure = part.measures[measureIndex];
6647
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6648
+ if (measure.barlines) {
6649
+ for (const barline of measure.barlines) {
6650
+ if (options?.style !== void 0 && barline.barStyle !== options.style) {
6651
+ continue;
6652
+ }
6653
+ if (options?.repeat !== void 0) {
6654
+ const hasRepeat = barline.repeat !== void 0;
6655
+ if (hasRepeat !== options.repeat) {
6656
+ continue;
6657
+ }
6658
+ }
6659
+ results.push({
6660
+ barline,
6661
+ partIndex,
6662
+ measureIndex,
6663
+ measureNumber
6664
+ });
6665
+ }
6666
+ }
6667
+ }
6668
+ }
6669
+ return results;
6670
+ }
6671
+ function getEndings(score, options) {
6672
+ const results = [];
6673
+ const startPart = options?.partIndex ?? 0;
6674
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6675
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6676
+ const part = score.parts[partIndex];
6677
+ if (!part) continue;
6678
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6679
+ const measure = part.measures[measureIndex];
6680
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6681
+ if (measure.barlines) {
6682
+ for (const barline of measure.barlines) {
6683
+ if (barline.ending) {
6684
+ results.push({
6685
+ number: barline.ending.number,
6686
+ type: barline.ending.type,
6687
+ partIndex,
6688
+ measureIndex,
6689
+ measureNumber
6690
+ });
6691
+ }
6692
+ }
6693
+ }
6694
+ }
6695
+ }
6696
+ return results;
6697
+ }
6698
+ function getKeyChanges(score, options) {
6699
+ const results = [];
6700
+ const startPart = options?.partIndex ?? 0;
6701
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6702
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6703
+ const part = score.parts[partIndex];
6704
+ if (!part) continue;
6705
+ let lastKey;
6706
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6707
+ const measure = part.measures[measureIndex];
6708
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6709
+ const state = createPositionState();
6710
+ if (measure.attributes?.key) {
6711
+ const key = measure.attributes.key;
6712
+ if (!lastKey || lastKey.fifths !== key.fifths || lastKey.mode !== key.mode) {
6713
+ results.push({
6714
+ key,
6715
+ partIndex,
6716
+ measureIndex,
6717
+ measureNumber,
6718
+ position: 0
6719
+ });
6720
+ lastKey = key;
6721
+ }
6722
+ }
6723
+ for (const entry of measure.entries) {
6724
+ if (entry.type === "attributes" && entry.attributes.key) {
6725
+ const key = entry.attributes.key;
6726
+ if (!lastKey || lastKey.fifths !== key.fifths || lastKey.mode !== key.mode) {
6727
+ results.push({
6728
+ key,
6729
+ partIndex,
6730
+ measureIndex,
6731
+ measureNumber,
6732
+ position: state.position
6733
+ });
6734
+ lastKey = key;
6735
+ }
6736
+ }
6737
+ updatePositionForEntry(state, entry);
6738
+ }
6739
+ }
6740
+ }
6741
+ return results;
6742
+ }
6743
+ function getTimeChanges(score, options) {
6744
+ const results = [];
6745
+ const startPart = options?.partIndex ?? 0;
6746
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6747
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6748
+ const part = score.parts[partIndex];
6749
+ if (!part) continue;
6750
+ let lastTime;
6751
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6752
+ const measure = part.measures[measureIndex];
6753
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6754
+ if (measure.attributes?.time) {
6755
+ const time = measure.attributes.time;
6756
+ if (!lastTime || lastTime.beats !== time.beats || lastTime.beatType !== time.beatType) {
6757
+ results.push({
6758
+ time,
6759
+ partIndex,
6760
+ measureIndex,
6761
+ measureNumber
6762
+ });
6763
+ lastTime = time;
6764
+ }
6765
+ }
6766
+ for (const entry of measure.entries) {
6767
+ if (entry.type === "attributes" && entry.attributes.time) {
6768
+ const time = entry.attributes.time;
6769
+ if (!lastTime || lastTime.beats !== time.beats || lastTime.beatType !== time.beatType) {
6770
+ results.push({
6771
+ time,
6772
+ partIndex,
6773
+ measureIndex,
6774
+ measureNumber
6775
+ });
6776
+ lastTime = time;
6777
+ }
6778
+ }
6779
+ }
6780
+ }
6781
+ }
6782
+ return results;
6783
+ }
6784
+ function getClefChanges(score, options) {
6785
+ const results = [];
6786
+ const startPart = options?.partIndex ?? 0;
6787
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6788
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6789
+ const part = score.parts[partIndex];
6790
+ if (!part) continue;
6791
+ const lastClefs = /* @__PURE__ */ new Map();
6792
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6793
+ const measure = part.measures[measureIndex];
6794
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6795
+ const state = createPositionState();
6796
+ if (measure.attributes?.clef) {
6797
+ for (const clef of measure.attributes.clef) {
6798
+ const staff = clef.staff ?? 1;
6799
+ if (options?.staff !== void 0 && staff !== options.staff) {
6800
+ continue;
6801
+ }
6802
+ const lastClef = lastClefs.get(staff);
6803
+ if (!lastClef || lastClef.sign !== clef.sign || lastClef.line !== clef.line || lastClef.clefOctaveChange !== clef.clefOctaveChange) {
6804
+ results.push({
6805
+ clef,
6806
+ staff,
6807
+ partIndex,
6808
+ measureIndex,
6809
+ measureNumber,
6810
+ position: 0
6811
+ });
6812
+ lastClefs.set(staff, clef);
6813
+ }
6814
+ }
6815
+ }
6816
+ for (const entry of measure.entries) {
6817
+ if (entry.type === "attributes" && entry.attributes.clef) {
6818
+ for (const clef of entry.attributes.clef) {
6819
+ const staff = clef.staff ?? 1;
6820
+ if (options?.staff !== void 0 && staff !== options.staff) {
6821
+ continue;
6822
+ }
6823
+ const lastClef = lastClefs.get(staff);
6824
+ if (!lastClef || lastClef.sign !== clef.sign || lastClef.line !== clef.line || lastClef.clefOctaveChange !== clef.clefOctaveChange) {
6825
+ results.push({
6826
+ clef,
6827
+ staff,
6828
+ partIndex,
6829
+ measureIndex,
6830
+ measureNumber,
6831
+ position: state.position
6832
+ });
6833
+ lastClefs.set(staff, clef);
6834
+ }
6835
+ }
6836
+ }
6837
+ updatePositionForEntry(state, entry);
6838
+ }
6839
+ }
6840
+ }
6841
+ return results;
6842
+ }
6843
+ function getStructuralChanges(score, options) {
6844
+ return {
6845
+ keyChanges: getKeyChanges(score, options),
6846
+ timeChanges: getTimeChanges(score, options),
6847
+ clefChanges: getClefChanges(score, options)
6848
+ };
6849
+ }
6850
+ function getPartByIndex(score, index) {
6851
+ return score.parts[index];
6852
+ }
6853
+ function getPartCount(score) {
6854
+ return score.parts.length;
6855
+ }
6856
+ function getPartIds(score) {
6857
+ return score.parts.map((part) => part.id);
6858
+ }
6859
+
6860
+ // src/query/index.ts
6861
+ function getMeasure(score, options) {
6862
+ const part = score.parts[options.part];
6863
+ if (!part) return void 0;
6864
+ const targetMeasure = String(options.measure);
6865
+ return part.measures.find((m) => m.number === targetMeasure);
6866
+ }
6867
+ function getMeasureByIndex(score, options) {
6868
+ const part = score.parts[options.part];
6869
+ if (!part) return void 0;
6870
+ return part.measures[options.measureIndex];
6871
+ }
6872
+ function getMeasureCount(score) {
6873
+ if (score.parts.length === 0) return 0;
6874
+ return score.parts[0].measures.length;
6875
+ }
6876
+ function getDivisions(score, options) {
6877
+ const part = score.parts[options.part];
6878
+ if (!part) return 1;
6879
+ const targetMeasure = parseInt(String(options.measure), 10);
6880
+ if (isNaN(targetMeasure)) return 1;
6881
+ for (let i = 0; i < part.measures.length; i++) {
6882
+ const m = part.measures[i];
6883
+ const mNum = parseInt(m.number, 10);
6884
+ if (!isNaN(mNum) && mNum > targetMeasure) break;
6885
+ if (m.attributes?.divisions !== void 0) {
6886
+ }
6887
+ }
6888
+ let divisions = 1;
6889
+ for (const m of part.measures) {
6890
+ const mNum = parseInt(m.number, 10);
6891
+ if (!isNaN(mNum) && mNum > targetMeasure) break;
6892
+ if (m.attributes?.divisions !== void 0) {
6893
+ divisions = m.attributes.divisions;
6894
+ }
6895
+ }
6896
+ return divisions;
6897
+ }
6898
+ function getAttributesAtMeasure(score, options) {
6899
+ const part = score.parts[options.part];
6900
+ if (!part) return {};
6901
+ const targetMeasure = parseInt(String(options.measure), 10);
6902
+ const result = {};
6903
+ for (const m of part.measures) {
6904
+ const mNum = parseInt(m.number, 10);
6905
+ if (!isNaN(targetMeasure) && !isNaN(mNum) && mNum > targetMeasure) break;
6906
+ if (m.attributes) {
6907
+ if (m.attributes.divisions !== void 0) result.divisions = m.attributes.divisions;
6908
+ if (m.attributes.time !== void 0) result.time = m.attributes.time;
6909
+ if (m.attributes.key !== void 0) result.key = m.attributes.key;
6910
+ if (m.attributes.clef !== void 0) result.clef = m.attributes.clef;
6911
+ if (m.attributes.staves !== void 0) result.staves = m.attributes.staves;
6912
+ if (m.attributes.transpose !== void 0) result.transpose = m.attributes.transpose;
6913
+ }
6914
+ }
6915
+ return result;
6916
+ }
6917
+ function findNotes(score, filter) {
6918
+ const results = [];
6919
+ for (const part of score.parts) {
6920
+ for (const measure of part.measures) {
6921
+ for (const entry of measure.entries) {
6922
+ if (entry.type !== "note") continue;
6923
+ if (filter.pitchRange && entry.pitch) {
6924
+ const noteValue = pitchToSemitone(entry.pitch);
6925
+ if (filter.pitchRange.min) {
6926
+ const minValue = pitchToSemitone(filter.pitchRange.min);
6927
+ if (noteValue < minValue) continue;
6928
+ }
6929
+ if (filter.pitchRange.max) {
6930
+ const maxValue = pitchToSemitone(filter.pitchRange.max);
6931
+ if (noteValue > maxValue) continue;
6932
+ }
6933
+ }
6934
+ if (filter.voice !== void 0 && entry.voice !== filter.voice) continue;
6935
+ if (filter.staff !== void 0 && (entry.staff ?? 1) !== filter.staff) continue;
6936
+ if (filter.noteType !== void 0 && entry.noteType !== filter.noteType) continue;
6937
+ if (filter.hasTie !== void 0) {
6938
+ const hasTie = entry.tie !== void 0;
6939
+ if (filter.hasTie !== hasTie) continue;
6940
+ }
6941
+ results.push(entry);
6942
+ }
6943
+ }
6944
+ }
6945
+ return results;
6946
+ }
6947
+ function getDuration(score) {
6948
+ if (score.parts.length === 0) return 0;
6949
+ const part = score.parts[0];
6950
+ let totalDuration = 0;
6951
+ let divisions = 1;
6952
+ for (const measure of part.measures) {
6953
+ if (measure.attributes?.divisions !== void 0) {
6954
+ divisions = measure.attributes.divisions;
6955
+ }
6956
+ let measureDuration = 0;
6957
+ for (const entry of measure.entries) {
6958
+ if (entry.type === "note" && !entry.chord) {
6959
+ measureDuration = Math.max(measureDuration, entry.duration);
6960
+ }
6961
+ }
6962
+ const attrs = getAttributesAtMeasure(score, { part: 0, measure: measure.number });
6963
+ if (attrs.time) {
6964
+ const beats = parseInt(attrs.time.beats, 10) || 4;
6965
+ const expectedDuration = beats / attrs.time.beatType * 4 * divisions;
6966
+ measureDuration = Math.max(measureDuration, expectedDuration);
6967
+ }
6968
+ totalDuration += measureDuration;
6969
+ }
6970
+ return totalDuration;
6971
+ }
6972
+ function getPartById(score, id) {
6973
+ return score.parts.find((p) => p.id === id);
6974
+ }
6975
+ function getPartIndex(score, id) {
6976
+ return score.parts.findIndex((p) => p.id === id);
6977
+ }
6978
+ function hasMultipleStaves(score, partIndex = 0) {
6979
+ const part = score.parts[partIndex];
6980
+ if (!part) return false;
6981
+ for (const measure of part.measures) {
6982
+ if (measure.attributes?.staves !== void 0 && measure.attributes.staves > 1) {
6983
+ return true;
6984
+ }
6985
+ }
6986
+ return false;
6987
+ }
6988
+ function getStaveCount(score, partIndex = 0) {
6989
+ const part = score.parts[partIndex];
6990
+ if (!part) return 1;
6991
+ for (const measure of part.measures) {
6992
+ if (measure.attributes?.staves !== void 0) {
6993
+ return measure.attributes.staves;
6994
+ }
6995
+ }
6996
+ return 1;
6997
+ }
6998
+ function measureRoundtrip(original, exported) {
6999
+ const notesOriginal = countNotes(original);
7000
+ const notesExported = countNotes(exported);
7001
+ const notesPreserved = Math.min(notesOriginal, notesExported);
7002
+ const measuresOriginal = getMeasureCount(original);
7003
+ const measuresExported = getMeasureCount(exported);
7004
+ const measuresPreserved = Math.min(measuresOriginal, measuresExported);
7005
+ const partsOriginal = original.parts.length;
7006
+ const partsExported = exported.parts.length;
7007
+ const partsPreserved = Math.min(partsOriginal, partsExported);
7008
+ const preservationRate = notesOriginal > 0 ? notesPreserved / notesOriginal : 1;
7009
+ return {
7010
+ notesOriginal,
7011
+ notesPreserved,
7012
+ measuresOriginal,
7013
+ measuresPreserved,
7014
+ partsOriginal,
7015
+ partsPreserved,
7016
+ preservationRate
7017
+ };
7018
+ }
7019
+ function countNotes(score) {
7020
+ let count = 0;
7021
+ for (const part of score.parts) {
7022
+ for (const measure of part.measures) {
7023
+ for (const entry of measure.entries) {
7024
+ if (entry.type === "note") {
7025
+ count++;
7026
+ }
7027
+ }
7028
+ }
7029
+ }
7030
+ return count;
5630
7031
  }
5631
7032
  function scoresEqual(a, b) {
5632
7033
  if (a.parts.length !== b.parts.length) return false;
@@ -5659,9 +7060,461 @@ function pitchesEqual(a, b) {
5659
7060
  }
5660
7061
 
5661
7062
  // src/operations/index.ts
7063
+ function success(data, warnings) {
7064
+ return { success: true, data, warnings };
7065
+ }
7066
+ function failure(errors) {
7067
+ return { success: false, errors };
7068
+ }
7069
+ function operationError(code, message, location = {}, details) {
7070
+ return {
7071
+ code,
7072
+ level: "error",
7073
+ message,
7074
+ location,
7075
+ details
7076
+ };
7077
+ }
5662
7078
  function cloneScore(score) {
5663
7079
  return JSON.parse(JSON.stringify(score));
5664
7080
  }
7081
+ function getMeasureDuration(divisions, time) {
7082
+ const beats = parseInt(time.beats, 10);
7083
+ if (isNaN(beats)) return divisions * 4;
7084
+ return beats / time.beatType * 4 * divisions;
7085
+ }
7086
+ function getVoiceEntries(measure, voice, staff) {
7087
+ const result = [];
7088
+ let position = 0;
7089
+ for (let i = 0; i < measure.entries.length; i++) {
7090
+ const entry = measure.entries[i];
7091
+ if (entry.type === "note") {
7092
+ const noteStaff = entry.staff ?? 1;
7093
+ if (entry.voice === voice && (staff === void 0 || noteStaff === staff)) {
7094
+ if (!entry.chord) {
7095
+ result.push({
7096
+ entry,
7097
+ entryIndex: i,
7098
+ position,
7099
+ endPosition: position + entry.duration
7100
+ });
7101
+ position += entry.duration;
7102
+ } else {
7103
+ if (result.length > 0) {
7104
+ const prev = result[result.length - 1];
7105
+ result.push({
7106
+ entry,
7107
+ entryIndex: i,
7108
+ position: prev.position,
7109
+ endPosition: prev.endPosition
7110
+ });
7111
+ }
7112
+ }
7113
+ } else if (!entry.chord) {
7114
+ position += entry.duration;
7115
+ }
7116
+ } else if (entry.type === "backup") {
7117
+ position -= entry.duration;
7118
+ } else if (entry.type === "forward") {
7119
+ if (entry.voice === voice) {
7120
+ result.push({
7121
+ entry,
7122
+ entryIndex: i,
7123
+ position,
7124
+ endPosition: position + entry.duration
7125
+ });
7126
+ }
7127
+ position += entry.duration;
7128
+ }
7129
+ }
7130
+ return result;
7131
+ }
7132
+ function hasNotesInRange(voiceEntries, startPos, endPos) {
7133
+ const conflicting = voiceEntries.filter((e) => {
7134
+ if (e.entry.type !== "note") return false;
7135
+ const note = e.entry;
7136
+ if (note.rest) return false;
7137
+ return e.position < endPos && e.endPosition > startPos;
7138
+ });
7139
+ return { hasNotes: conflicting.length > 0, conflictingNotes: conflicting };
7140
+ }
7141
+ function createRest(duration, voice, staff) {
7142
+ return {
7143
+ type: "note",
7144
+ rest: { displayStep: void 0, displayOctave: void 0 },
7145
+ duration,
7146
+ voice,
7147
+ staff
7148
+ };
7149
+ }
7150
+ function rebuildMeasureWithVoice(measure, voice, newEntries, measureDuration, staff) {
7151
+ const otherEntries = [];
7152
+ let position = 0;
7153
+ for (const entry of measure.entries) {
7154
+ if (entry.type === "note") {
7155
+ if (entry.voice !== voice || staff !== void 0 && (entry.staff ?? 1) !== staff) {
7156
+ if (!entry.chord) {
7157
+ otherEntries.push({ position, entry });
7158
+ position += entry.duration;
7159
+ } else {
7160
+ otherEntries.push({ position, entry });
7161
+ }
7162
+ } else if (!entry.chord) {
7163
+ position += entry.duration;
7164
+ }
7165
+ } else if (entry.type === "backup") {
7166
+ position -= entry.duration;
7167
+ } else if (entry.type === "forward") {
7168
+ if (entry.voice !== voice) {
7169
+ otherEntries.push({ position, entry });
7170
+ }
7171
+ position += entry.duration;
7172
+ } else {
7173
+ otherEntries.push({ position, entry });
7174
+ }
7175
+ }
7176
+ const filledNewEntries = [];
7177
+ let currentPos = 0;
7178
+ const sortedNew = [...newEntries].sort((a, b) => a.position - b.position);
7179
+ for (const { position: notePos, entry } of sortedNew) {
7180
+ if (notePos > currentPos) {
7181
+ filledNewEntries.push({
7182
+ position: currentPos,
7183
+ entry: createRest(notePos - currentPos, voice, staff)
7184
+ });
7185
+ }
7186
+ filledNewEntries.push({ position: notePos, entry });
7187
+ if (!entry.chord) {
7188
+ currentPos = notePos + entry.duration;
7189
+ }
7190
+ }
7191
+ if (currentPos < measureDuration) {
7192
+ filledNewEntries.push({
7193
+ position: currentPos,
7194
+ entry: createRest(measureDuration - currentPos, voice, staff)
7195
+ });
7196
+ }
7197
+ const allEntries = [...otherEntries, ...filledNewEntries];
7198
+ allEntries.sort((a, b) => a.position - b.position);
7199
+ const result = [];
7200
+ let currentPosition = 0;
7201
+ for (const { position: targetPos, entry } of allEntries) {
7202
+ const diff = targetPos - currentPosition;
7203
+ if (diff < 0) {
7204
+ result.push({ type: "backup", duration: -diff });
7205
+ currentPosition = targetPos;
7206
+ } else if (diff > 0) {
7207
+ result.push({
7208
+ type: "forward",
7209
+ duration: diff,
7210
+ voice: entry.type === "note" ? entry.voice : 1,
7211
+ staff: entry.type === "note" ? entry.staff : void 0
7212
+ });
7213
+ currentPosition = targetPos;
7214
+ }
7215
+ result.push(entry);
7216
+ if (entry.type === "note" && !entry.chord) {
7217
+ currentPosition += entry.duration;
7218
+ } else if (entry.type === "forward") {
7219
+ currentPosition += entry.duration;
7220
+ }
7221
+ }
7222
+ return result;
7223
+ }
7224
+ function insertNote(score, options) {
7225
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7226
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7227
+ }
7228
+ const part = score.parts[options.partIndex];
7229
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7230
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7231
+ }
7232
+ if (options.duration <= 0) {
7233
+ return failure([operationError("INVALID_DURATION", `Duration must be positive`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7234
+ }
7235
+ if (options.position < 0) {
7236
+ return failure([operationError("INVALID_POSITION", `Position cannot be negative`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7237
+ }
7238
+ const result = cloneScore(score);
7239
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7240
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
7241
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
7242
+ const noteEnd = options.position + options.duration;
7243
+ if (noteEnd > measureDuration) {
7244
+ return failure([operationError(
7245
+ "EXCEEDS_MEASURE",
7246
+ `Note ending at ${noteEnd} exceeds measure duration ${measureDuration}`,
7247
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
7248
+ { noteEnd, measureDuration }
7249
+ )]);
7250
+ }
7251
+ const voiceEntries = getVoiceEntries(measure, options.voice, options.staff);
7252
+ const { hasNotes: hasNotes2, conflictingNotes } = hasNotesInRange(voiceEntries, options.position, noteEnd);
7253
+ if (hasNotes2) {
7254
+ return failure([operationError(
7255
+ "NOTE_CONFLICT",
7256
+ `Position ${options.position}-${noteEnd} conflicts with existing note(s)`,
7257
+ { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice },
7258
+ { conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
7259
+ )]);
7260
+ }
7261
+ const newNote = {
7262
+ type: "note",
7263
+ pitch: options.pitch,
7264
+ duration: options.duration,
7265
+ voice: options.voice,
7266
+ staff: options.staff,
7267
+ noteType: options.noteType,
7268
+ dots: options.dots
7269
+ };
7270
+ const existingNotes = voiceEntries.filter((e) => {
7271
+ if (e.entry.type !== "note") return true;
7272
+ const note = e.entry;
7273
+ if (note.rest) {
7274
+ return !(e.position < noteEnd && e.endPosition > options.position);
7275
+ }
7276
+ return true;
7277
+ }).map((e) => ({ position: e.position, entry: e.entry }));
7278
+ existingNotes.push({ position: options.position, entry: newNote });
7279
+ measure.entries = rebuildMeasureWithVoice(
7280
+ measure,
7281
+ options.voice,
7282
+ existingNotes,
7283
+ measureDuration,
7284
+ options.staff
7285
+ );
7286
+ const errors = validateMeasureLocal(measure, context, {
7287
+ checkMeasureDuration: true,
7288
+ checkPosition: true,
7289
+ checkVoiceStaff: true
7290
+ });
7291
+ const criticalErrors = errors.filter((e) => e.level === "error");
7292
+ if (criticalErrors.length > 0) {
7293
+ return failure(criticalErrors);
7294
+ }
7295
+ return success(result, errors.filter((e) => e.level !== "error"));
7296
+ }
7297
+ function removeNote(score, options) {
7298
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7299
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7300
+ }
7301
+ const part = score.parts[options.partIndex];
7302
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7303
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7304
+ }
7305
+ const result = cloneScore(score);
7306
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7307
+ let noteCount = 0;
7308
+ let targetEntry = null;
7309
+ let targetIndex = -1;
7310
+ for (let i2 = 0; i2 < measure.entries.length; i2++) {
7311
+ const entry = measure.entries[i2];
7312
+ if (entry.type === "note" && !entry.rest) {
7313
+ if (noteCount === options.noteIndex) {
7314
+ targetEntry = entry;
7315
+ targetIndex = i2;
7316
+ break;
7317
+ }
7318
+ noteCount++;
7319
+ }
7320
+ }
7321
+ if (!targetEntry || targetIndex === -1) {
7322
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7323
+ }
7324
+ measure.entries[targetIndex] = createRest(
7325
+ targetEntry.duration,
7326
+ targetEntry.voice,
7327
+ targetEntry.staff
7328
+ );
7329
+ let i = targetIndex + 1;
7330
+ while (i < measure.entries.length) {
7331
+ const entry = measure.entries[i];
7332
+ if (entry.type === "note" && entry.chord) {
7333
+ measure.entries.splice(i, 1);
7334
+ } else {
7335
+ break;
7336
+ }
7337
+ }
7338
+ return success(result);
7339
+ }
7340
+ function addChord(score, options) {
7341
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7342
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7343
+ }
7344
+ const part = score.parts[options.partIndex];
7345
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7346
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7347
+ }
7348
+ const result = cloneScore(score);
7349
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7350
+ let noteCount = 0;
7351
+ let targetEntry = null;
7352
+ let targetIndex = -1;
7353
+ for (let i = 0; i < measure.entries.length; i++) {
7354
+ const entry = measure.entries[i];
7355
+ if (entry.type === "note" && !entry.rest && !entry.chord) {
7356
+ if (noteCount === options.noteIndex) {
7357
+ targetEntry = entry;
7358
+ targetIndex = i;
7359
+ break;
7360
+ }
7361
+ noteCount++;
7362
+ }
7363
+ }
7364
+ if (!targetEntry || targetIndex === -1) {
7365
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7366
+ }
7367
+ const chordNote = {
7368
+ type: "note",
7369
+ pitch: options.pitch,
7370
+ duration: targetEntry.duration,
7371
+ voice: targetEntry.voice,
7372
+ staff: targetEntry.staff,
7373
+ chord: true,
7374
+ noteType: targetEntry.noteType,
7375
+ dots: targetEntry.dots
7376
+ };
7377
+ let insertIndex = targetIndex + 1;
7378
+ while (insertIndex < measure.entries.length) {
7379
+ const entry = measure.entries[insertIndex];
7380
+ if (entry.type === "note" && entry.chord) {
7381
+ insertIndex++;
7382
+ } else {
7383
+ break;
7384
+ }
7385
+ }
7386
+ measure.entries.splice(insertIndex, 0, chordNote);
7387
+ return success(result);
7388
+ }
7389
+ function changeNoteDuration(score, options) {
7390
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7391
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7392
+ }
7393
+ const part = score.parts[options.partIndex];
7394
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7395
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7396
+ }
7397
+ if (options.newDuration <= 0) {
7398
+ return failure([operationError("INVALID_DURATION", `Duration must be positive`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7399
+ }
7400
+ const result = cloneScore(score);
7401
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7402
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
7403
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
7404
+ let noteCount = 0;
7405
+ let targetEntry = null;
7406
+ let targetPosition = 0;
7407
+ let position = 0;
7408
+ for (const entry of measure.entries) {
7409
+ if (entry.type === "note") {
7410
+ if (!entry.rest && !entry.chord) {
7411
+ if (noteCount === options.noteIndex) {
7412
+ targetEntry = entry;
7413
+ targetPosition = position;
7414
+ break;
7415
+ }
7416
+ noteCount++;
7417
+ }
7418
+ if (!entry.chord) {
7419
+ position += entry.duration;
7420
+ }
7421
+ } else if (entry.type === "backup") {
7422
+ position -= entry.duration;
7423
+ } else if (entry.type === "forward") {
7424
+ position += entry.duration;
7425
+ }
7426
+ }
7427
+ if (!targetEntry) {
7428
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7429
+ }
7430
+ const oldDuration = targetEntry.duration;
7431
+ const newEnd = targetPosition + options.newDuration;
7432
+ if (newEnd > measureDuration) {
7433
+ return failure([operationError(
7434
+ "EXCEEDS_MEASURE",
7435
+ `New duration would exceed measure (ends at ${newEnd}, measure is ${measureDuration})`,
7436
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
7437
+ { newEnd, measureDuration }
7438
+ )]);
7439
+ }
7440
+ const voiceEntries = getVoiceEntries(measure, targetEntry.voice, targetEntry.staff);
7441
+ if (options.newDuration > oldDuration) {
7442
+ const { hasNotes: hasNotes2, conflictingNotes } = hasNotesInRange(
7443
+ voiceEntries.filter((e) => e.position !== targetPosition),
7444
+ // Exclude current note
7445
+ targetPosition + oldDuration,
7446
+ newEnd
7447
+ );
7448
+ if (hasNotes2) {
7449
+ return failure([operationError(
7450
+ "NOTE_CONFLICT",
7451
+ `Cannot extend note: conflicts with existing note(s)`,
7452
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
7453
+ { conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
7454
+ )]);
7455
+ }
7456
+ }
7457
+ targetEntry.duration = options.newDuration;
7458
+ if (options.noteType !== void 0) {
7459
+ targetEntry.noteType = options.noteType;
7460
+ }
7461
+ if (options.dots !== void 0) {
7462
+ targetEntry.dots = options.dots;
7463
+ }
7464
+ const existingNotes = voiceEntries.filter((e) => {
7465
+ if (e.position === targetPosition) return true;
7466
+ const note = e.entry;
7467
+ if (note.rest) {
7468
+ if (options.newDuration > oldDuration) {
7469
+ return !(e.position >= targetPosition + oldDuration && e.position < newEnd);
7470
+ }
7471
+ }
7472
+ return true;
7473
+ }).map((e) => ({ position: e.position, entry: e.entry }));
7474
+ const modifiedIdx = existingNotes.findIndex((e) => e.position === targetPosition);
7475
+ if (modifiedIdx >= 0) {
7476
+ existingNotes[modifiedIdx].entry = targetEntry;
7477
+ }
7478
+ measure.entries = rebuildMeasureWithVoice(
7479
+ measure,
7480
+ targetEntry.voice,
7481
+ existingNotes,
7482
+ measureDuration,
7483
+ targetEntry.staff
7484
+ );
7485
+ const errors = validateMeasureLocal(measure, context, {
7486
+ checkMeasureDuration: true,
7487
+ checkPosition: true,
7488
+ checkVoiceStaff: true
7489
+ });
7490
+ const criticalErrors = errors.filter((e) => e.level === "error");
7491
+ if (criticalErrors.length > 0) {
7492
+ return failure(criticalErrors);
7493
+ }
7494
+ return success(result, errors.filter((e) => e.level !== "error"));
7495
+ }
7496
+ function setNotePitch(score, options) {
7497
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7498
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7499
+ }
7500
+ const part = score.parts[options.partIndex];
7501
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7502
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7503
+ }
7504
+ const result = cloneScore(score);
7505
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7506
+ let noteCount = 0;
7507
+ for (const entry of measure.entries) {
7508
+ if (entry.type === "note" && !entry.rest) {
7509
+ if (noteCount === options.noteIndex) {
7510
+ entry.pitch = options.pitch;
7511
+ return success(result);
7512
+ }
7513
+ noteCount++;
7514
+ }
7515
+ }
7516
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7517
+ }
5665
7518
  function transposePitch(pitch, semitones) {
5666
7519
  const currentSemitone = STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0) + pitch.octave * 12;
5667
7520
  const targetSemitone = currentSemitone + semitones;
@@ -5688,7 +7541,7 @@ function transposePitch(pitch, semitones) {
5688
7541
  };
5689
7542
  }
5690
7543
  function transpose(score, semitones) {
5691
- if (semitones === 0) return score;
7544
+ if (semitones === 0) return success(score);
5692
7545
  const result = cloneScore(score);
5693
7546
  for (const part of result.parts) {
5694
7547
  for (const measure of part.measures) {
@@ -5699,59 +7552,7 @@ function transpose(score, semitones) {
5699
7552
  }
5700
7553
  }
5701
7554
  }
5702
- return result;
5703
- }
5704
- function addNote(score, options) {
5705
- const result = cloneScore(score);
5706
- const part = result.parts[options.partIndex];
5707
- if (!part) return result;
5708
- const measure = part.measures[options.measureIndex];
5709
- if (!measure) return result;
5710
- const newNote = {
5711
- type: "note",
5712
- voice: options.voice,
5713
- staff: options.staff,
5714
- ...options.note
5715
- };
5716
- const currentPosition = getMeasureEndPosition(measure);
5717
- const positionDiff = options.position - currentPosition;
5718
- if (positionDiff < 0) {
5719
- measure.entries.push({
5720
- type: "backup",
5721
- duration: -positionDiff
5722
- });
5723
- } else if (positionDiff > 0) {
5724
- measure.entries.push({
5725
- type: "forward",
5726
- duration: positionDiff,
5727
- voice: options.voice,
5728
- staff: options.staff
5729
- });
5730
- }
5731
- measure.entries.push(newNote);
5732
- return result;
5733
- }
5734
- function deleteNote(score, options) {
5735
- const result = cloneScore(score);
5736
- const part = result.parts[options.partIndex];
5737
- if (!part) return result;
5738
- const measure = part.measures[options.measureIndex];
5739
- if (!measure) return result;
5740
- let noteCount = 0;
5741
- let entryIndex = -1;
5742
- for (let i = 0; i < measure.entries.length; i++) {
5743
- if (measure.entries[i].type === "note") {
5744
- if (noteCount === options.noteIndex) {
5745
- entryIndex = i;
5746
- break;
5747
- }
5748
- noteCount++;
5749
- }
5750
- }
5751
- if (entryIndex !== -1) {
5752
- measure.entries.splice(entryIndex, 1);
5753
- }
5754
- return result;
7555
+ return success(result);
5755
7556
  }
5756
7557
  function changeKey(score, key, options) {
5757
7558
  const result = cloneScore(score);
@@ -5759,9 +7560,7 @@ function changeKey(score, key, options) {
5759
7560
  for (const part of result.parts) {
5760
7561
  for (const measure of part.measures) {
5761
7562
  if (measure.number === targetMeasure) {
5762
- if (!measure.attributes) {
5763
- measure.attributes = {};
5764
- }
7563
+ if (!measure.attributes) measure.attributes = {};
5765
7564
  measure.attributes.key = key;
5766
7565
  }
5767
7566
  }
@@ -5774,9 +7573,7 @@ function changeTime(score, time, options) {
5774
7573
  for (const part of result.parts) {
5775
7574
  for (const measure of part.measures) {
5776
7575
  if (measure.number === targetMeasure) {
5777
- if (!measure.attributes) {
5778
- measure.attributes = {};
5779
- }
7576
+ if (!measure.attributes) measure.attributes = {};
5780
7577
  measure.attributes.time = time;
5781
7578
  }
5782
7579
  }
@@ -5791,15 +7588,9 @@ function insertMeasure(score, options) {
5791
7588
  if (insertIndex === -1) continue;
5792
7589
  const numericPart = parseInt(targetMeasure, 10);
5793
7590
  const newMeasureNumber = String(isNaN(numericPart) ? insertIndex + 2 : numericPart + 1);
5794
- const newMeasure = {
5795
- number: newMeasureNumber,
5796
- entries: []
5797
- };
5798
- if (options.copyAttributes) {
5799
- const sourceMeasure = part.measures[insertIndex];
5800
- if (sourceMeasure.attributes) {
5801
- newMeasure.attributes = { ...sourceMeasure.attributes };
5802
- }
7591
+ const newMeasure = { number: newMeasureNumber, entries: [] };
7592
+ if (options.copyAttributes && part.measures[insertIndex].attributes) {
7593
+ newMeasure.attributes = { ...part.measures[insertIndex].attributes };
5803
7594
  }
5804
7595
  part.measures.splice(insertIndex + 1, 0, newMeasure);
5805
7596
  for (let i = insertIndex + 2; i < part.measures.length; i++) {
@@ -5827,95 +7618,36 @@ function deleteMeasure(score, measureNumber) {
5827
7618
  }
5828
7619
  return result;
5829
7620
  }
5830
- function setDivisions(score, options) {
5831
- const result = cloneScore(score);
5832
- const part = result.parts[options.partIndex];
5833
- if (!part) return result;
5834
- const measure = part.measures[options.measureIndex];
5835
- if (!measure) return result;
5836
- if (!measure.attributes) {
5837
- measure.attributes = {};
5838
- }
5839
- measure.attributes.divisions = options.divisions;
5840
- return result;
5841
- }
5842
- function addChordNote(score, options) {
5843
- const result = cloneScore(score);
5844
- const part = result.parts[options.partIndex];
5845
- if (!part) return result;
5846
- const measure = part.measures[options.measureIndex];
5847
- if (!measure) return result;
5848
- let noteCount = 0;
5849
- let entryIndex = -1;
5850
- let targetNote = null;
5851
- for (let i = 0; i < measure.entries.length; i++) {
5852
- const entry = measure.entries[i];
5853
- if (entry.type === "note") {
5854
- if (noteCount === options.afterNoteIndex) {
5855
- entryIndex = i;
5856
- targetNote = entry;
5857
- break;
5858
- }
5859
- noteCount++;
5860
- }
5861
- }
5862
- if (entryIndex !== -1 && targetNote) {
5863
- const chordNote = {
5864
- type: "note",
5865
- pitch: options.pitch,
5866
- duration: targetNote.duration,
5867
- voice: targetNote.voice,
5868
- staff: targetNote.staff,
5869
- chord: true,
5870
- noteType: targetNote.noteType,
5871
- dots: targetNote.dots
5872
- };
5873
- measure.entries.splice(entryIndex + 1, 0, chordNote);
5874
- }
5875
- return result;
5876
- }
5877
- function modifyNotePitch(score, options) {
5878
- const result = cloneScore(score);
5879
- const part = result.parts[options.partIndex];
5880
- if (!part) return result;
5881
- const measure = part.measures[options.measureIndex];
5882
- if (!measure) return result;
5883
- let noteCount = 0;
5884
- for (const entry of measure.entries) {
5885
- if (entry.type === "note") {
5886
- if (noteCount === options.noteIndex) {
5887
- entry.pitch = options.pitch;
5888
- break;
5889
- }
5890
- noteCount++;
5891
- }
5892
- }
5893
- return result;
5894
- }
5895
- function modifyNoteDuration(score, options) {
5896
- const result = cloneScore(score);
5897
- const part = result.parts[options.partIndex];
5898
- if (!part) return result;
5899
- const measure = part.measures[options.measureIndex];
5900
- if (!measure) return result;
5901
- let noteCount = 0;
5902
- for (const entry of measure.entries) {
5903
- if (entry.type === "note") {
5904
- if (noteCount === options.noteIndex) {
5905
- entry.duration = options.duration;
5906
- if (options.noteType !== void 0) {
5907
- entry.noteType = options.noteType;
5908
- }
5909
- if (options.dots !== void 0) {
5910
- entry.dots = options.dots;
5911
- }
5912
- break;
5913
- }
5914
- noteCount++;
5915
- }
5916
- }
5917
- return result;
5918
- }
7621
+ var addNote = (score, options) => {
7622
+ const result = insertNote(score, {
7623
+ partIndex: options.partIndex,
7624
+ measureIndex: options.measureIndex,
7625
+ voice: options.voice,
7626
+ staff: options.staff,
7627
+ position: options.position,
7628
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
7629
+ duration: options.note.duration,
7630
+ noteType: options.note.noteType,
7631
+ dots: options.note.dots
7632
+ });
7633
+ return result.success ? result.data : score;
7634
+ };
7635
+ var deleteNote = (score, options) => {
7636
+ const result = removeNote(score, options);
7637
+ return result.success ? result.data : score;
7638
+ };
7639
+ var addChordNote = (score, options) => {
7640
+ const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
7641
+ return result.success ? result.data : score;
7642
+ };
7643
+ var modifyNotePitch = (score, options) => {
7644
+ const result = setNotePitch(score, options);
7645
+ return result.success ? result.data : score;
7646
+ };
7647
+ var modifyNoteDuration = (score, options) => {
7648
+ const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
7649
+ return result.success ? result.data : score;
7650
+ };
5919
7651
 
5920
7652
  // src/file.ts
5921
7653
  var import_promises = require("fs/promises");
@@ -5961,6 +7693,8 @@ async function serializeToFile(score, filePath, options = {}) {
5961
7693
  addNote,
5962
7694
  assertMeasureValid,
5963
7695
  assertValid,
7696
+ buildVoiceToStaffMap,
7697
+ buildVoiceToStaffMapForPart,
5964
7698
  changeKey,
5965
7699
  changeTime,
5966
7700
  countNotes,
@@ -5968,36 +7702,82 @@ async function serializeToFile(score, filePath, options = {}) {
5968
7702
  deleteMeasure,
5969
7703
  deleteNote,
5970
7704
  exportMidi,
7705
+ findBarlines,
7706
+ findDirectionsByType,
5971
7707
  findNotes,
7708
+ findNotesWithNotation,
5972
7709
  formatLocation,
5973
7710
  getAbsolutePosition,
7711
+ getAdjacentNotes,
5974
7712
  getAllNotes,
5975
7713
  getAttributesAtMeasure,
7714
+ getBeamGroups,
7715
+ getChordProgression,
5976
7716
  getChords,
7717
+ getClefChanges,
7718
+ getClefForStaff,
7719
+ getDirections,
7720
+ getDirectionsAtPosition,
5977
7721
  getDivisions,
5978
7722
  getDuration,
7723
+ getDynamics,
7724
+ getEffectiveStaff,
7725
+ getEndings,
7726
+ getEntriesAtPosition,
7727
+ getEntriesForStaff,
7728
+ getEntriesInRange,
7729
+ getHarmonies,
7730
+ getHarmonyAtPosition,
7731
+ getKeyChanges,
7732
+ getLyricText,
7733
+ getLyrics,
5979
7734
  getMeasure,
5980
7735
  getMeasureByIndex,
5981
7736
  getMeasureContext,
5982
7737
  getMeasureCount,
5983
7738
  getMeasureEndPosition,
7739
+ getNextNote,
5984
7740
  getNormalizedDuration,
5985
7741
  getNormalizedPosition,
7742
+ getNotesAtPosition,
5986
7743
  getNotesForStaff,
5987
7744
  getNotesForVoice,
7745
+ getNotesInRange,
7746
+ getOctaveShifts,
5988
7747
  getPartById,
7748
+ getPartByIndex,
7749
+ getPartCount,
7750
+ getPartIds,
5989
7751
  getPartIndex,
7752
+ getPedalMarkings,
7753
+ getPrevNote,
7754
+ getRepeatStructure,
7755
+ getSlurSpans,
7756
+ getStaffRange,
5990
7757
  getStaveCount,
5991
7758
  getStaves,
7759
+ getStructuralChanges,
7760
+ getTempoMarkings,
7761
+ getTiedNoteGroups,
7762
+ getTimeChanges,
7763
+ getTupletGroups,
7764
+ getVerseCount,
7765
+ getVerticalSlice,
7766
+ getVoiceLine,
7767
+ getVoiceLineInRange,
5992
7768
  getVoices,
7769
+ getVoicesForStaff,
7770
+ getWedges,
5993
7771
  groupByStaff,
5994
7772
  groupByVoice,
5995
7773
  hasMultipleStaves,
5996
7774
  hasNotes,
7775
+ inferStaff,
5997
7776
  insertMeasure,
5998
7777
  isCompressed,
5999
7778
  isRestMeasure,
6000
7779
  isValid,
7780
+ iterateEntries,
6001
7781
  iterateNotes,
6002
7782
  measureRoundtrip,
6003
7783
  modifyNoteDuration,
@@ -6011,7 +7791,6 @@ async function serializeToFile(score, filePath, options = {}) {
6011
7791
  serialize,
6012
7792
  serializeCompressed,
6013
7793
  serializeToFile,
6014
- setDivisions,
6015
7794
  transpose,
6016
7795
  validate,
6017
7796
  validateBackupForward,
@@ -6030,4 +7809,3 @@ async function serializeToFile(score, filePath, options = {}) {
6030
7809
  validateVoiceStaff,
6031
7810
  withAbsolutePositions
6032
7811
  });
6033
- //# sourceMappingURL=index.js.map