musicxml-io 0.2.9 → 0.2.12

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
@@ -23,18 +23,66 @@ __export(src_exports, {
23
23
  STEPS: () => STEPS,
24
24
  STEP_SEMITONES: () => STEP_SEMITONES,
25
25
  ValidationException: () => ValidationException,
26
+ addArticulation: () => addArticulation,
27
+ addBeam: () => addBeam,
28
+ addBowing: () => addBowing,
29
+ addBreathMark: () => addBreathMark,
30
+ addCaesura: () => addCaesura,
31
+ addChord: () => addChord,
26
32
  addChordNote: () => addChordNote,
33
+ addChordNoteChecked: () => addChordNoteChecked,
34
+ addChordSymbol: () => addChordSymbol,
35
+ addCoda: () => addCoda,
36
+ addDaCapo: () => addDaCapo,
37
+ addDalSegno: () => addDalSegno,
38
+ addDynamics: () => addDynamics,
39
+ addEnding: () => addEnding,
40
+ addFermata: () => addFermata,
41
+ addFine: () => addFine,
42
+ addFingering: () => addFingering,
43
+ addGraceNote: () => addGraceNote,
44
+ addHarmony: () => addHarmony,
45
+ addLyric: () => addLyric,
27
46
  addNote: () => addNote,
47
+ addNoteChecked: () => addNoteChecked,
48
+ addOctaveShift: () => addOctaveShift,
49
+ addOrnament: () => addOrnament,
50
+ addPart: () => addPart,
51
+ addPedal: () => addPedal,
52
+ addRehearsalMark: () => addRehearsalMark,
53
+ addRepeat: () => addRepeat,
54
+ addRepeatBarline: () => addRepeatBarline,
55
+ addSegno: () => addSegno,
56
+ addSlur: () => addSlur,
57
+ addStringNumber: () => addStringNumber,
58
+ addTempo: () => addTempo,
59
+ addText: () => addText,
60
+ addTextDirection: () => addTextDirection,
61
+ addTie: () => addTie,
62
+ addToCoda: () => addToCoda,
63
+ addVoice: () => addVoice,
64
+ addWedge: () => addWedge,
28
65
  assertMeasureValid: () => assertMeasureValid,
29
66
  assertValid: () => assertValid,
67
+ autoBeam: () => autoBeam,
30
68
  buildVoiceToStaffMap: () => buildVoiceToStaffMap,
31
69
  buildVoiceToStaffMapForPart: () => buildVoiceToStaffMapForPart,
70
+ changeBarline: () => changeBarline,
71
+ changeClef: () => changeClef,
32
72
  changeKey: () => changeKey,
73
+ changeNoteDuration: () => changeNoteDuration,
33
74
  changeTime: () => changeTime,
75
+ convertToGrace: () => convertToGrace,
76
+ copyNotes: () => copyNotes,
77
+ copyNotesMultiMeasure: () => copyNotesMultiMeasure,
34
78
  countNotes: () => countNotes,
79
+ createTuplet: () => createTuplet,
80
+ cutNotes: () => cutNotes,
35
81
  decodeBuffer: () => decodeBuffer,
36
82
  deleteMeasure: () => deleteMeasure,
37
83
  deleteNote: () => deleteNote,
84
+ deleteNoteChecked: () => deleteNoteChecked,
85
+ duplicatePart: () => duplicatePart,
38
86
  exportMidi: () => exportMidi,
39
87
  findBarlines: () => findBarlines,
40
88
  findDirectionsByType: () => findDirectionsByType,
@@ -128,7 +176,9 @@ __export(src_exports, {
128
176
  hasTieStop: () => hasTieStop,
129
177
  hasTuplet: () => hasTuplet,
130
178
  inferStaff: () => inferStaff,
179
+ insertClefChange: () => insertClefChange,
131
180
  insertMeasure: () => insertMeasure,
181
+ insertNote: () => insertNote,
132
182
  isChordNote: () => isChordNote,
133
183
  isCompressed: () => isCompressed,
134
184
  isCueNote: () => isCueNote,
@@ -141,19 +191,65 @@ __export(src_exports, {
141
191
  isValid: () => isValid,
142
192
  iterateEntries: () => iterateEntries,
143
193
  iterateNotes: () => iterateNotes,
194
+ lowerAccidental: () => lowerAccidental,
144
195
  measureRoundtrip: () => measureRoundtrip,
196
+ modifyDynamics: () => modifyDynamics,
145
197
  modifyNoteDuration: () => modifyNoteDuration,
198
+ modifyNoteDurationChecked: () => modifyNoteDurationChecked,
146
199
  modifyNotePitch: () => modifyNotePitch,
200
+ modifyNotePitchChecked: () => modifyNotePitchChecked,
201
+ modifyTempo: () => modifyTempo,
202
+ moveNoteToStaff: () => moveNoteToStaff,
147
203
  parse: () => parse,
148
204
  parseAuto: () => parseAuto,
149
205
  parseCompressed: () => parseCompressed,
150
206
  parseFile: () => parseFile,
207
+ pasteNotes: () => pasteNotes,
208
+ pasteNotesMultiMeasure: () => pasteNotesMultiMeasure,
151
209
  pitchToSemitone: () => pitchToSemitone,
210
+ raiseAccidental: () => raiseAccidental,
211
+ removeArticulation: () => removeArticulation,
212
+ removeBeam: () => removeBeam,
213
+ removeBowing: () => removeBowing,
214
+ removeBreathMark: () => removeBreathMark,
215
+ removeCaesura: () => removeCaesura,
216
+ removeChordSymbol: () => removeChordSymbol,
217
+ removeDynamics: () => removeDynamics,
218
+ removeEnding: () => removeEnding,
219
+ removeFermata: () => removeFermata,
220
+ removeFingering: () => removeFingering,
221
+ removeGraceNote: () => removeGraceNote,
222
+ removeHarmony: () => removeHarmony,
223
+ removeLyric: () => removeLyric,
224
+ removeNote: () => removeNote,
225
+ removeOctaveShift: () => removeOctaveShift,
226
+ removeOrnament: () => removeOrnament,
227
+ removePart: () => removePart,
228
+ removePedal: () => removePedal,
229
+ removeRepeat: () => removeRepeat,
230
+ removeRepeatBarline: () => removeRepeatBarline,
231
+ removeSlur: () => removeSlur,
232
+ removeStringNumber: () => removeStringNumber,
233
+ removeTempo: () => removeTempo,
234
+ removeTie: () => removeTie,
235
+ removeTuplet: () => removeTuplet,
236
+ removeWedge: () => removeWedge,
152
237
  scoresEqual: () => scoresEqual,
153
238
  serialize: () => serialize,
154
239
  serializeCompressed: () => serializeCompressed,
155
240
  serializeToFile: () => serializeToFile,
241
+ setBarline: () => setBarline,
242
+ setBeaming: () => setBeaming,
243
+ setNotePitch: () => setNotePitch,
244
+ setNotePitchBySemitone: () => setNotePitchBySemitone,
245
+ setStaves: () => setStaves,
246
+ shiftNotePitch: () => shiftNotePitch,
247
+ stopOctaveShift: () => stopOctaveShift,
156
248
  transpose: () => transpose,
249
+ transposeChecked: () => transposeChecked,
250
+ updateChordSymbol: () => updateChordSymbol,
251
+ updateHarmony: () => updateHarmony,
252
+ updateLyric: () => updateLyric,
157
253
  validate: () => validate,
158
254
  validateBackupForward: () => validateBackupForward,
159
255
  validateBeams: () => validateBeams,
@@ -1601,8 +1697,8 @@ function parseDirection(elements, attrs) {
1601
1697
  });
1602
1698
  for (const el of elements) {
1603
1699
  if (el["direction-type"]) {
1604
- const parsed = parseDirectionType(el["direction-type"]);
1605
- if (parsed) {
1700
+ const parsedTypes = parseDirectionTypes(el["direction-type"]);
1701
+ for (const parsed of parsedTypes) {
1606
1702
  direction.directionTypes.push(parsed);
1607
1703
  }
1608
1704
  }
@@ -1639,7 +1735,8 @@ function parseDirection(elements, attrs) {
1639
1735
  }
1640
1736
  return direction;
1641
1737
  }
1642
- function parseDirectionType(elements) {
1738
+ function parseDirectionTypes(elements) {
1739
+ const results = [];
1643
1740
  for (const el of elements) {
1644
1741
  if (el["dynamics"]) {
1645
1742
  const dynAttrs = getAttributes(el);
@@ -1679,10 +1776,12 @@ function parseDirectionType(elements) {
1679
1776
  if (dynAttrs["default-y"]) result.defaultY = parseFloat(dynAttrs["default-y"]);
1680
1777
  if (dynAttrs["relative-x"]) result.relativeX = parseFloat(dynAttrs["relative-x"]);
1681
1778
  if (dynAttrs["halign"]) result.halign = dynAttrs["halign"];
1682
- return result;
1779
+ results.push(result);
1780
+ break;
1683
1781
  }
1684
1782
  }
1685
1783
  }
1784
+ continue;
1686
1785
  }
1687
1786
  if (el["wedge"]) {
1688
1787
  const wedgeAttrs = getAttributes(el);
@@ -1692,8 +1791,9 @@ function parseDirectionType(elements) {
1692
1791
  if (wedgeAttrs["spread"]) result.spread = parseFloat(wedgeAttrs["spread"]);
1693
1792
  if (wedgeAttrs["default-y"]) result.defaultY = parseFloat(wedgeAttrs["default-y"]);
1694
1793
  if (wedgeAttrs["relative-x"]) result.relativeX = parseFloat(wedgeAttrs["relative-x"]);
1695
- return result;
1794
+ results.push(result);
1696
1795
  }
1796
+ continue;
1697
1797
  }
1698
1798
  if (el["metronome"]) {
1699
1799
  const metAttrs = getAttributes(el);
@@ -1733,28 +1833,29 @@ function parseDirectionType(elements) {
1733
1833
  if (metAttrs["default-y"]) result.defaultY = parseFloat(metAttrs["default-y"]);
1734
1834
  if (metAttrs["font-family"]) result.fontFamily = metAttrs["font-family"];
1735
1835
  if (metAttrs["font-size"]) result.fontSize = metAttrs["font-size"];
1736
- return result;
1836
+ results.push(result);
1737
1837
  }
1838
+ continue;
1738
1839
  }
1739
1840
  if (el["words"]) {
1740
1841
  const a = getAttributes(el);
1741
1842
  const text = extractText(el["words"]);
1742
- if (text) {
1743
- const result = { kind: "words", text };
1744
- if (a["default-x"]) result.defaultX = parseFloat(a["default-x"]);
1745
- if (a["default-y"]) result.defaultY = parseFloat(a["default-y"]);
1746
- if (a["relative-x"]) result.relativeX = parseFloat(a["relative-x"]);
1747
- if (a["font-family"]) result.fontFamily = a["font-family"];
1748
- if (a["font-size"]) result.fontSize = a["font-size"];
1749
- if (a["font-style"]) result.fontStyle = a["font-style"];
1750
- if (a["font-weight"]) result.fontWeight = a["font-weight"];
1751
- if (a["xml:lang"]) result.xmlLang = a["xml:lang"];
1752
- if (a["justify"]) result.justify = a["justify"];
1753
- if (a["color"]) result.color = a["color"];
1754
- if (a["xml:space"]) result.xmlSpace = a["xml:space"];
1755
- if (a["halign"]) result.halign = a["halign"];
1756
- return result;
1757
- }
1843
+ const result = { kind: "words", text: text || "" };
1844
+ if (a["default-x"]) result.defaultX = parseFloat(a["default-x"]);
1845
+ if (a["default-y"]) result.defaultY = parseFloat(a["default-y"]);
1846
+ if (a["relative-x"]) result.relativeX = parseFloat(a["relative-x"]);
1847
+ if (a["relative-y"]) result.relativeY = parseFloat(a["relative-y"]);
1848
+ if (a["font-family"]) result.fontFamily = a["font-family"];
1849
+ if (a["font-size"]) result.fontSize = a["font-size"];
1850
+ if (a["font-style"]) result.fontStyle = a["font-style"];
1851
+ if (a["font-weight"]) result.fontWeight = a["font-weight"];
1852
+ if (a["xml:lang"]) result.xmlLang = a["xml:lang"];
1853
+ if (a["justify"]) result.justify = a["justify"];
1854
+ if (a["color"]) result.color = a["color"];
1855
+ if (a["xml:space"]) result.xmlSpace = a["xml:space"];
1856
+ if (a["halign"]) result.halign = a["halign"];
1857
+ results.push(result);
1858
+ continue;
1758
1859
  }
1759
1860
  if (el["rehearsal"]) {
1760
1861
  const a = getAttributes(el);
@@ -1766,8 +1867,9 @@ function parseDirectionType(elements) {
1766
1867
  if (a["default-y"]) result.defaultY = parseFloat(a["default-y"]);
1767
1868
  if (a["font-size"]) result.fontSize = a["font-size"];
1768
1869
  if (a["font-weight"]) result.fontWeight = a["font-weight"];
1769
- return result;
1870
+ results.push(result);
1770
1871
  }
1872
+ continue;
1771
1873
  }
1772
1874
  if (el["bracket"]) {
1773
1875
  const bracketAttrs = getAttributes(el);
@@ -1779,8 +1881,9 @@ function parseDirectionType(elements) {
1779
1881
  if (bracketAttrs["line-type"]) result.lineType = bracketAttrs["line-type"];
1780
1882
  if (bracketAttrs["default-y"]) result.defaultY = parseFloat(bracketAttrs["default-y"]);
1781
1883
  if (bracketAttrs["relative-x"]) result.relativeX = parseFloat(bracketAttrs["relative-x"]);
1782
- return result;
1884
+ results.push(result);
1783
1885
  }
1886
+ continue;
1784
1887
  }
1785
1888
  if (el["dashes"]) {
1786
1889
  const dashAttrs = getAttributes(el);
@@ -1791,8 +1894,9 @@ function parseDirectionType(elements) {
1791
1894
  if (dashAttrs["dash-length"]) result.dashLength = parseFloat(dashAttrs["dash-length"]);
1792
1895
  if (dashAttrs["default-y"]) result.defaultY = parseFloat(dashAttrs["default-y"]);
1793
1896
  if (dashAttrs["space-length"]) result.spaceLength = parseFloat(dashAttrs["space-length"]);
1794
- return result;
1897
+ results.push(result);
1795
1898
  }
1899
+ continue;
1796
1900
  }
1797
1901
  if (el["accordion-registration"]) {
1798
1902
  const accContent = el["accordion-registration"];
@@ -1812,7 +1916,8 @@ function parseDirectionType(elements) {
1812
1916
  result.low = true;
1813
1917
  }
1814
1918
  }
1815
- return result;
1919
+ results.push(result);
1920
+ continue;
1816
1921
  }
1817
1922
  if (el["other-direction"]) {
1818
1923
  const otherAttrs = getAttributes(el);
@@ -1824,24 +1929,31 @@ function parseDirectionType(elements) {
1824
1929
  if (otherAttrs["default-y"]) result.defaultY = parseFloat(otherAttrs["default-y"]);
1825
1930
  if (otherAttrs["halign"]) result.halign = otherAttrs["halign"];
1826
1931
  if (otherAttrs["print-object"] === "no") result.printObject = false;
1827
- return result;
1932
+ results.push(result);
1933
+ break;
1828
1934
  }
1829
1935
  }
1936
+ continue;
1830
1937
  }
1831
1938
  if (el["segno"] !== void 0) {
1832
- return { kind: "segno" };
1939
+ results.push({ kind: "segno" });
1940
+ continue;
1833
1941
  }
1834
1942
  if (el["coda"] !== void 0) {
1835
- return { kind: "coda" };
1943
+ results.push({ kind: "coda" });
1944
+ continue;
1836
1945
  }
1837
1946
  if (el["eyeglasses"] !== void 0) {
1838
- return { kind: "eyeglasses" };
1947
+ results.push({ kind: "eyeglasses" });
1948
+ continue;
1839
1949
  }
1840
1950
  if (el["damp"] !== void 0) {
1841
- return { kind: "damp" };
1951
+ results.push({ kind: "damp" });
1952
+ continue;
1842
1953
  }
1843
1954
  if (el["damp-all"] !== void 0) {
1844
- return { kind: "damp-all" };
1955
+ results.push({ kind: "damp-all" });
1956
+ continue;
1845
1957
  }
1846
1958
  if (el["scordatura"] !== void 0) {
1847
1959
  const scordContent = el["scordatura"];
@@ -1864,7 +1976,8 @@ function parseDirectionType(elements) {
1864
1976
  }
1865
1977
  }
1866
1978
  }
1867
- return { kind: "scordatura", accords: accords.length > 0 ? accords : void 0 };
1979
+ results.push({ kind: "scordatura", accords: accords.length > 0 ? accords : void 0 });
1980
+ continue;
1868
1981
  }
1869
1982
  if (el["harp-pedals"] !== void 0) {
1870
1983
  const harpContent = el["harp-pedals"];
@@ -1882,15 +1995,17 @@ function parseDirectionType(elements) {
1882
1995
  }
1883
1996
  }
1884
1997
  }
1885
- return { kind: "harp-pedals", pedalTunings: pedalTunings.length > 0 ? pedalTunings : void 0 };
1998
+ results.push({ kind: "harp-pedals", pedalTunings: pedalTunings.length > 0 ? pedalTunings : void 0 });
1999
+ continue;
1886
2000
  }
1887
2001
  if (el["image"] !== void 0) {
1888
2002
  const imgAttrs = getAttributes(el);
1889
- return {
2003
+ results.push({
1890
2004
  kind: "image",
1891
2005
  source: imgAttrs["source"],
1892
2006
  type: imgAttrs["type"]
1893
- };
2007
+ });
2008
+ continue;
1894
2009
  }
1895
2010
  if (el["pedal"]) {
1896
2011
  const pedalAttrs = getAttributes(el);
@@ -1902,8 +2017,9 @@ function parseDirectionType(elements) {
1902
2017
  if (pedalAttrs["default-y"]) result.defaultY = parseFloat(pedalAttrs["default-y"]);
1903
2018
  if (pedalAttrs["relative-x"]) result.relativeX = parseFloat(pedalAttrs["relative-x"]);
1904
2019
  if (pedalAttrs["halign"]) result.halign = pedalAttrs["halign"];
1905
- return result;
2020
+ results.push(result);
1906
2021
  }
2022
+ continue;
1907
2023
  }
1908
2024
  if (el["octave-shift"]) {
1909
2025
  const shiftAttrs = getAttributes(el);
@@ -1911,8 +2027,9 @@ function parseDirectionType(elements) {
1911
2027
  if (shiftType === "up" || shiftType === "down" || shiftType === "stop") {
1912
2028
  const result = { kind: "octave-shift", type: shiftType };
1913
2029
  if (shiftAttrs["size"]) result.size = parseInt(shiftAttrs["size"], 10);
1914
- return result;
2030
+ results.push(result);
1915
2031
  }
2032
+ continue;
1916
2033
  }
1917
2034
  if (el["swing"]) {
1918
2035
  const swingContent = el["swing"];
@@ -1946,10 +2063,11 @@ function parseDirectionType(elements) {
1946
2063
  }
1947
2064
  }
1948
2065
  }
1949
- return result;
2066
+ results.push(result);
2067
+ continue;
1950
2068
  }
1951
2069
  }
1952
- return null;
2070
+ return results;
1953
2071
  }
1954
2072
  function parseBarline(elements, attrs) {
1955
2073
  const location = attrs["location"] || "right";
@@ -2830,7 +2948,7 @@ function validateBeams(measure, location) {
2830
2948
  const entry = measure.entries[entryIndex];
2831
2949
  if (entry.type !== "note" || !entry.beam) continue;
2832
2950
  for (const beam of entry.beam) {
2833
- const beamKey = `${beam.number}-${entry.voice}-${entry.staff ?? 1}`;
2951
+ const beamKey = `${beam.number}-${entry.voice}`;
2834
2952
  if (beam.type === "begin") {
2835
2953
  if (openBeams.has(beamKey)) {
2836
2954
  errors.push({
@@ -2841,7 +2959,7 @@ function validateBeams(measure, location) {
2841
2959
  details: { beamNumber: beam.number }
2842
2960
  });
2843
2961
  }
2844
- openBeams.set(beamKey, entryIndex);
2962
+ openBeams.set(beamKey, { entryIndex, staff: entry.staff ?? 1 });
2845
2963
  } else if (beam.type === "end") {
2846
2964
  if (!openBeams.has(beamKey)) {
2847
2965
  errors.push({
@@ -2857,8 +2975,8 @@ function validateBeams(measure, location) {
2857
2975
  }
2858
2976
  }
2859
2977
  }
2860
- for (const [beamKey, startIndex] of openBeams.entries()) {
2861
- const [beamNumber, voice, staff] = beamKey.split("-").map(Number);
2978
+ for (const [beamKey, { entryIndex: startIndex, staff }] of openBeams.entries()) {
2979
+ const [beamNumber, voice] = beamKey.split("-").map(Number);
2862
2980
  errors.push({
2863
2981
  code: "BEAM_BEGIN_WITHOUT_END",
2864
2982
  level: "error",
@@ -4752,6 +4870,7 @@ function serializeDirectionType(dirType, indent) {
4752
4870
  if (dirType.defaultX !== void 0) wordAttrs += ` default-x="${dirType.defaultX}"`;
4753
4871
  if (dirType.defaultY !== void 0) wordAttrs += ` default-y="${dirType.defaultY}"`;
4754
4872
  if (dirType.relativeX !== void 0) wordAttrs += ` relative-x="${dirType.relativeX}"`;
4873
+ if (dirType.relativeY !== void 0) wordAttrs += ` relative-y="${dirType.relativeY}"`;
4755
4874
  if (dirType.fontFamily) wordAttrs += ` font-family="${escapeXml(dirType.fontFamily)}"`;
4756
4875
  if (dirType.fontSize) wordAttrs += ` font-size="${escapeXml(dirType.fontSize)}"`;
4757
4876
  if (dirType.fontStyle) wordAttrs += ` font-style="${escapeXml(dirType.fontStyle)}"`;
@@ -5528,9 +5647,128 @@ var STEP_SEMITONES = {
5528
5647
  "A": 9,
5529
5648
  "B": 11
5530
5649
  };
5650
+ var SHARP_ORDER = ["F", "C", "G", "D", "A", "E", "B"];
5651
+ var FLAT_ORDER = ["B", "E", "A", "D", "G", "C", "F"];
5531
5652
  function pitchToSemitone(pitch) {
5532
5653
  return pitch.octave * 12 + STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0);
5533
5654
  }
5655
+ function getAlterForStepInKey(step, key) {
5656
+ const fifths = key.fifths;
5657
+ if (fifths > 0) {
5658
+ const sharps = SHARP_ORDER.slice(0, fifths);
5659
+ return sharps.includes(step) ? 1 : 0;
5660
+ } else if (fifths < 0) {
5661
+ const flats = FLAT_ORDER.slice(0, -fifths);
5662
+ return flats.includes(step) ? -1 : 0;
5663
+ }
5664
+ return 0;
5665
+ }
5666
+ function getAlteredStepsInKey(key) {
5667
+ const alterations = /* @__PURE__ */ new Map();
5668
+ const fifths = key.fifths;
5669
+ if (fifths > 0) {
5670
+ SHARP_ORDER.slice(0, fifths).forEach((step) => alterations.set(step, 1));
5671
+ } else if (fifths < 0) {
5672
+ FLAT_ORDER.slice(0, -fifths).forEach((step) => alterations.set(step, -1));
5673
+ }
5674
+ return alterations;
5675
+ }
5676
+ function getAccidentalsInMeasure(measure, upToPosition, voice) {
5677
+ const accidentals = /* @__PURE__ */ new Map();
5678
+ let position = 0;
5679
+ for (const entry of measure.entries) {
5680
+ if (position >= upToPosition) break;
5681
+ if (entry.type === "note") {
5682
+ if (voice === void 0 || entry.voice === voice) {
5683
+ if (entry.pitch && entry.accidental) {
5684
+ const key = `${entry.pitch.step}${entry.pitch.octave}`;
5685
+ accidentals.set(key, entry.pitch.alter ?? 0);
5686
+ }
5687
+ }
5688
+ if (!entry.chord) {
5689
+ position += entry.duration;
5690
+ }
5691
+ } else if (entry.type === "backup") {
5692
+ position -= entry.duration;
5693
+ } else if (entry.type === "forward") {
5694
+ position += entry.duration;
5695
+ }
5696
+ }
5697
+ return accidentals;
5698
+ }
5699
+ function semitoneToKeyAwarePitch(semitone, key, options) {
5700
+ const octave = Math.floor(semitone / 12);
5701
+ const pitchClass = (semitone % 12 + 12) % 12;
5702
+ const keyPreferSharp = key.fifths >= 0;
5703
+ const preferSharp = options?.preferSharp ?? keyPreferSharp;
5704
+ for (const step of STEPS) {
5705
+ const stepSemitone = STEP_SEMITONES[step];
5706
+ if (stepSemitone === pitchClass) {
5707
+ return { step, octave };
5708
+ }
5709
+ }
5710
+ const keyAlterations = getAlteredStepsInKey(key);
5711
+ for (const step of STEPS) {
5712
+ const stepSemitone = STEP_SEMITONES[step];
5713
+ const keyAlter = keyAlterations.get(step) ?? 0;
5714
+ if ((stepSemitone + keyAlter) % 12 === pitchClass) {
5715
+ return { step, octave, alter: keyAlter };
5716
+ }
5717
+ }
5718
+ if (preferSharp) {
5719
+ for (const step of STEPS) {
5720
+ const stepSemitone = STEP_SEMITONES[step];
5721
+ const diff = (pitchClass - stepSemitone + 12) % 12;
5722
+ if (diff === 1) {
5723
+ return { step, octave, alter: 1 };
5724
+ }
5725
+ }
5726
+ for (const step of STEPS) {
5727
+ const stepSemitone = STEP_SEMITONES[step];
5728
+ const diff = (pitchClass - stepSemitone + 12) % 12;
5729
+ if (diff === 2) {
5730
+ return { step, octave, alter: 2 };
5731
+ }
5732
+ }
5733
+ } else {
5734
+ for (const step of STEPS) {
5735
+ const stepSemitone = STEP_SEMITONES[step];
5736
+ const diff = (stepSemitone - pitchClass + 12) % 12;
5737
+ if (diff === 1) {
5738
+ return { step, octave, alter: -1 };
5739
+ }
5740
+ }
5741
+ for (const step of STEPS) {
5742
+ const stepSemitone = STEP_SEMITONES[step];
5743
+ const diff = (stepSemitone - pitchClass + 12) % 12;
5744
+ if (diff === 2) {
5745
+ return { step, octave, alter: -2 };
5746
+ }
5747
+ }
5748
+ }
5749
+ return { step: "C", octave, alter: pitchClass };
5750
+ }
5751
+ function determineAccidental(pitch, key, accidentalsInMeasure) {
5752
+ const noteKey = `${pitch.step}${pitch.octave}`;
5753
+ const alter = pitch.alter ?? 0;
5754
+ const keyAlter = getAlterForStepInKey(pitch.step, key);
5755
+ const previousAlter = accidentalsInMeasure.get(noteKey);
5756
+ if (previousAlter !== void 0) {
5757
+ if (alter === previousAlter) {
5758
+ return void 0;
5759
+ }
5760
+ } else {
5761
+ if (alter === keyAlter) {
5762
+ return void 0;
5763
+ }
5764
+ }
5765
+ if (alter === 0) return "natural";
5766
+ if (alter === 1) return "sharp";
5767
+ if (alter === -1) return "flat";
5768
+ if (alter === 2) return "double-sharp";
5769
+ if (alter === -2) return "double-flat";
5770
+ return void 0;
5771
+ }
5534
5772
  function createPositionState() {
5535
5773
  return { position: 0, lastNonChordPosition: 0 };
5536
5774
  }
@@ -7138,6 +7376,34 @@ function operationError(code, message, location = {}, details) {
7138
7376
  function cloneScore(score) {
7139
7377
  return JSON.parse(JSON.stringify(score));
7140
7378
  }
7379
+ function cloneNoteWithNewId(note) {
7380
+ const cloned = JSON.parse(JSON.stringify(note));
7381
+ cloned._id = generateId();
7382
+ return cloned;
7383
+ }
7384
+ function cloneEntryWithNewId(entry) {
7385
+ const cloned = JSON.parse(JSON.stringify(entry));
7386
+ cloned._id = generateId();
7387
+ return cloned;
7388
+ }
7389
+ function cloneMeasureWithNewIds(measure) {
7390
+ const cloned = JSON.parse(JSON.stringify(measure));
7391
+ cloned._id = generateId();
7392
+ cloned.entries = cloned.entries.map((entry) => cloneEntryWithNewId(entry));
7393
+ if (cloned.barlines) {
7394
+ cloned.barlines = cloned.barlines.map((barline) => ({
7395
+ ...barline,
7396
+ _id: generateId()
7397
+ }));
7398
+ }
7399
+ return cloned;
7400
+ }
7401
+ function clonePartWithNewIds(part) {
7402
+ const cloned = JSON.parse(JSON.stringify(part));
7403
+ cloned._id = generateId();
7404
+ cloned.measures = cloned.measures.map((measure) => cloneMeasureWithNewIds(measure));
7405
+ return cloned;
7406
+ }
7141
7407
  function getMeasureDuration(divisions, time) {
7142
7408
  const beats = parseInt(time.beats, 10);
7143
7409
  if (isNaN(beats)) return divisions * 4;
@@ -7579,6 +7845,189 @@ function setNotePitch(score, options) {
7579
7845
  }
7580
7846
  return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7581
7847
  }
7848
+ function setNotePitchBySemitone(score, options) {
7849
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7850
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7851
+ }
7852
+ const part = score.parts[options.partIndex];
7853
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7854
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7855
+ }
7856
+ const result = cloneScore(score);
7857
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7858
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
7859
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
7860
+ const keySignature = attrs.key ?? { fifths: 0 };
7861
+ let noteCount = 0;
7862
+ for (const entry of measure.entries) {
7863
+ if (entry.type === "note" && !entry.rest) {
7864
+ if (noteCount === options.noteIndex) {
7865
+ const notePosition = getAbsolutePositionForNote(entry, measure);
7866
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
7867
+ const newPitch = semitoneToKeyAwarePitch(options.semitone, keySignature, {
7868
+ preferSharp: options.preferSharp
7869
+ });
7870
+ const accidental = determineAccidental(newPitch, keySignature, accidentalsInMeasure);
7871
+ entry.pitch = newPitch;
7872
+ if (accidental) {
7873
+ entry.accidental = { value: accidental };
7874
+ } else {
7875
+ delete entry.accidental;
7876
+ }
7877
+ return success(result);
7878
+ }
7879
+ noteCount++;
7880
+ }
7881
+ }
7882
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7883
+ }
7884
+ function shiftNotePitch(score, options) {
7885
+ if (options.semitones === 0) {
7886
+ return success(score);
7887
+ }
7888
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7889
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7890
+ }
7891
+ const part = score.parts[options.partIndex];
7892
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7893
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7894
+ }
7895
+ const measure = part.measures[options.measureIndex];
7896
+ let noteCount = 0;
7897
+ let currentSemitone = null;
7898
+ for (const entry of measure.entries) {
7899
+ if (entry.type === "note" && !entry.rest) {
7900
+ if (noteCount === options.noteIndex) {
7901
+ if (!entry.pitch) {
7902
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7903
+ }
7904
+ currentSemitone = pitchToSemitone(entry.pitch);
7905
+ break;
7906
+ }
7907
+ noteCount++;
7908
+ }
7909
+ }
7910
+ if (currentSemitone === null) {
7911
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7912
+ }
7913
+ return setNotePitchBySemitone(score, {
7914
+ partIndex: options.partIndex,
7915
+ measureIndex: options.measureIndex,
7916
+ noteIndex: options.noteIndex,
7917
+ semitone: currentSemitone + options.semitones,
7918
+ preferSharp: options.preferSharp
7919
+ });
7920
+ }
7921
+ function raiseAccidental(score, options) {
7922
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7923
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7924
+ }
7925
+ const part = score.parts[options.partIndex];
7926
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7927
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7928
+ }
7929
+ const result = cloneScore(score);
7930
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7931
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
7932
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
7933
+ const keySignature = attrs.key ?? { fifths: 0 };
7934
+ let noteCount = 0;
7935
+ for (const entry of measure.entries) {
7936
+ if (entry.type === "note" && !entry.rest) {
7937
+ if (noteCount === options.noteIndex) {
7938
+ if (!entry.pitch) {
7939
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7940
+ }
7941
+ const currentAlter = entry.pitch.alter ?? 0;
7942
+ const newAlter = currentAlter + 1;
7943
+ if (newAlter > 2) {
7944
+ return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot raise accidental beyond double-sharp (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7945
+ }
7946
+ entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
7947
+ const notePosition = getAbsolutePositionForNote(entry, measure);
7948
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
7949
+ const accidental = determineAccidental(entry.pitch, keySignature, accidentalsInMeasure);
7950
+ if (accidental) {
7951
+ entry.accidental = { value: accidental };
7952
+ } else {
7953
+ delete entry.accidental;
7954
+ }
7955
+ return success(result);
7956
+ }
7957
+ noteCount++;
7958
+ }
7959
+ }
7960
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7961
+ }
7962
+ function lowerAccidental(score, options) {
7963
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7964
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7965
+ }
7966
+ const part = score.parts[options.partIndex];
7967
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7968
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7969
+ }
7970
+ const result = cloneScore(score);
7971
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7972
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
7973
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
7974
+ const keySignature = attrs.key ?? { fifths: 0 };
7975
+ let noteCount = 0;
7976
+ for (const entry of measure.entries) {
7977
+ if (entry.type === "note" && !entry.rest) {
7978
+ if (noteCount === options.noteIndex) {
7979
+ if (!entry.pitch) {
7980
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7981
+ }
7982
+ const currentAlter = entry.pitch.alter ?? 0;
7983
+ const newAlter = currentAlter - 1;
7984
+ if (newAlter < -2) {
7985
+ return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot lower accidental beyond double-flat (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7986
+ }
7987
+ entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
7988
+ const notePosition = getAbsolutePositionForNote(entry, measure);
7989
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
7990
+ const accidental = determineAccidental(entry.pitch, keySignature, accidentalsInMeasure);
7991
+ if (accidental) {
7992
+ entry.accidental = { value: accidental };
7993
+ } else {
7994
+ delete entry.accidental;
7995
+ }
7996
+ return success(result);
7997
+ }
7998
+ noteCount++;
7999
+ }
8000
+ }
8001
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8002
+ }
8003
+ function addVoice(score, options) {
8004
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8005
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8006
+ }
8007
+ const part = score.parts[options.partIndex];
8008
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8009
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8010
+ }
8011
+ const result = cloneScore(score);
8012
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8013
+ const existingVoiceEntries = getVoiceEntries(measure, options.voice, options.staff);
8014
+ if (existingVoiceEntries.length > 0) {
8015
+ return failure([operationError(
8016
+ "NOTE_CONFLICT",
8017
+ `Voice ${options.voice} already exists in this measure`,
8018
+ { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice }
8019
+ )]);
8020
+ }
8021
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8022
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
8023
+ const rest = createRest(measureDuration, options.voice, options.staff);
8024
+ const currentEnd = getMeasureEndPosition(measure);
8025
+ if (currentEnd > 0) {
8026
+ measure.entries.push({ _id: generateId(), type: "backup", duration: currentEnd });
8027
+ }
8028
+ measure.entries.push(rest);
8029
+ return success(result);
8030
+ }
7582
8031
  function transposePitch(pitch, semitones) {
7583
8032
  const currentSemitone = STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0) + pitch.octave * 12;
7584
8033
  const targetSemitone = currentSemitone + semitones;
@@ -7618,6 +8067,163 @@ function transpose(score, semitones) {
7618
8067
  }
7619
8068
  return success(result);
7620
8069
  }
8070
+ function addPart(score, options) {
8071
+ if (score.parts.find((p) => p.id === options.id)) {
8072
+ return failure([operationError("DUPLICATE_PART_ID", `Part ID "${options.id}" already exists`, { partId: options.id })]);
8073
+ }
8074
+ const result = cloneScore(score);
8075
+ const insertIndex = options.insertIndex ?? result.parts.length;
8076
+ const partInfo = {
8077
+ _id: generateId(),
8078
+ type: "score-part",
8079
+ id: options.id,
8080
+ name: options.name,
8081
+ abbreviation: options.abbreviation
8082
+ };
8083
+ let partListInsertIndex = result.partList.length;
8084
+ let partCount = 0;
8085
+ for (let i = 0; i < result.partList.length; i++) {
8086
+ if (result.partList[i].type === "score-part") {
8087
+ if (partCount === insertIndex) {
8088
+ partListInsertIndex = i;
8089
+ break;
8090
+ }
8091
+ partCount++;
8092
+ }
8093
+ }
8094
+ result.partList.splice(partListInsertIndex, 0, partInfo);
8095
+ const measureCount = result.parts.length > 0 ? result.parts[0].measures.length : 1;
8096
+ const newPart = { _id: generateId(), id: options.id, measures: [] };
8097
+ for (let i = 0; i < measureCount; i++) {
8098
+ const measureNumber = result.parts.length > 0 ? result.parts[0].measures[i]?.number ?? String(i + 1) : String(i + 1);
8099
+ const measure = { _id: generateId(), number: measureNumber, entries: [] };
8100
+ if (i === 0) {
8101
+ measure.attributes = {
8102
+ divisions: options.divisions ?? 4,
8103
+ time: options.time ?? { beats: "4", beatType: 4 },
8104
+ key: options.key ?? { fifths: 0 },
8105
+ clef: options.clef ? [options.clef] : [{ sign: "G", line: 2 }]
8106
+ };
8107
+ }
8108
+ newPart.measures.push(measure);
8109
+ }
8110
+ result.parts.splice(insertIndex, 0, newPart);
8111
+ const validationResult = validate(result, { checkPartReferences: true, checkPartStructure: true });
8112
+ if (!validationResult.valid) {
8113
+ return failure(validationResult.errors);
8114
+ }
8115
+ return success(result, validationResult.warnings);
8116
+ }
8117
+ function removePart(score, partId) {
8118
+ const partIndex = score.parts.findIndex((p) => p.id === partId);
8119
+ if (partIndex === -1) {
8120
+ return failure([operationError("PART_NOT_FOUND", `Part "${partId}" not found`, { partId })]);
8121
+ }
8122
+ if (score.parts.length <= 1) {
8123
+ return failure([operationError("PART_NOT_FOUND", "Cannot remove the only remaining part", { partId })]);
8124
+ }
8125
+ const result = cloneScore(score);
8126
+ result.parts.splice(partIndex, 1);
8127
+ const partListIndex = result.partList.findIndex((e) => e.type === "score-part" && e.id === partId);
8128
+ if (partListIndex !== -1) {
8129
+ result.partList.splice(partListIndex, 1);
8130
+ }
8131
+ return success(result);
8132
+ }
8133
+ function duplicatePart(score, options) {
8134
+ const sourceIndex = score.parts.findIndex((p) => p.id === options.sourcePartId);
8135
+ if (sourceIndex === -1) {
8136
+ return failure([operationError("PART_NOT_FOUND", `Source part "${options.sourcePartId}" not found`, { partId: options.sourcePartId })]);
8137
+ }
8138
+ if (score.parts.find((p) => p.id === options.newPartId)) {
8139
+ return failure([operationError("DUPLICATE_PART_ID", `Part ID "${options.newPartId}" already exists`, { partId: options.newPartId })]);
8140
+ }
8141
+ const result = cloneScore(score);
8142
+ const sourcePart = result.parts[sourceIndex];
8143
+ const newPart = clonePartWithNewIds(sourcePart);
8144
+ newPart.id = options.newPartId;
8145
+ const sourcePartInfo = result.partList.find((e) => e.type === "score-part" && e.id === options.sourcePartId);
8146
+ const newPartInfo = {
8147
+ _id: generateId(),
8148
+ type: "score-part",
8149
+ id: options.newPartId,
8150
+ name: options.newPartName ?? sourcePartInfo?.name,
8151
+ abbreviation: sourcePartInfo?.abbreviation
8152
+ };
8153
+ result.parts.splice(sourceIndex + 1, 0, newPart);
8154
+ const partListSourceIndex = result.partList.findIndex((e) => e.type === "score-part" && e.id === options.sourcePartId);
8155
+ if (partListSourceIndex !== -1) {
8156
+ result.partList.splice(partListSourceIndex + 1, 0, newPartInfo);
8157
+ } else {
8158
+ result.partList.push(newPartInfo);
8159
+ }
8160
+ return success(result);
8161
+ }
8162
+ function setStaves(score, options) {
8163
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8164
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8165
+ }
8166
+ if (options.staves < 1) {
8167
+ return failure([operationError("INVALID_STAFF", `Staves count must be at least 1`, { partIndex: options.partIndex })]);
8168
+ }
8169
+ const result = cloneScore(score);
8170
+ const part = result.parts[options.partIndex];
8171
+ const fromMeasureIndex = options.fromMeasure ?? 0;
8172
+ const measure = part.measures[fromMeasureIndex];
8173
+ if (!measure) {
8174
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${fromMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: fromMeasureIndex })]);
8175
+ }
8176
+ if (!measure.attributes) {
8177
+ measure.attributes = {};
8178
+ }
8179
+ measure.attributes.staves = options.staves;
8180
+ if (options.clefs) {
8181
+ measure.attributes.clef = options.clefs;
8182
+ } else {
8183
+ const existingClefs = measure.attributes.clef ?? [];
8184
+ const newClefs = [...existingClefs];
8185
+ for (let staff = existingClefs.length + 1; staff <= options.staves; staff++) {
8186
+ newClefs.push(staff === 2 ? { sign: "F", line: 4, staff } : { sign: "G", line: 2, staff });
8187
+ }
8188
+ measure.attributes.clef = newClefs;
8189
+ }
8190
+ const validationResult = validate(result, { checkVoiceStaff: true, checkStaffStructure: true });
8191
+ if (!validationResult.valid) {
8192
+ return failure(validationResult.errors);
8193
+ }
8194
+ return success(result, validationResult.warnings);
8195
+ }
8196
+ function moveNoteToStaff(score, options) {
8197
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8198
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8199
+ }
8200
+ const part = score.parts[options.partIndex];
8201
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8202
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8203
+ }
8204
+ if (options.targetStaff < 1) {
8205
+ return failure([operationError("INVALID_STAFF", `Target staff must be at least 1`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8206
+ }
8207
+ const result = cloneScore(score);
8208
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8209
+ let noteCount = 0;
8210
+ for (const entry of measure.entries) {
8211
+ if (entry.type === "note" && !entry.rest) {
8212
+ if (noteCount === options.noteIndex) {
8213
+ entry.staff = options.targetStaff;
8214
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8215
+ const errors = validateMeasureLocal(measure, context, { checkVoiceStaff: true });
8216
+ const criticalErrors = errors.filter((e) => e.level === "error");
8217
+ if (criticalErrors.length > 0) {
8218
+ return failure(criticalErrors);
8219
+ }
8220
+ return success(result, errors.filter((e) => e.level !== "error"));
8221
+ }
8222
+ noteCount++;
8223
+ }
8224
+ }
8225
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8226
+ }
7621
8227
  function changeKey(score, key, options) {
7622
8228
  const result = cloneScore(score);
7623
8229
  const targetMeasure = String(options.fromMeasure);
@@ -7682,36 +8288,2839 @@ function deleteMeasure(score, measureNumber) {
7682
8288
  }
7683
8289
  return result;
7684
8290
  }
7685
- var addNote = (score, options) => {
7686
- const result = insertNote(score, {
7687
- partIndex: options.partIndex,
7688
- measureIndex: options.measureIndex,
7689
- voice: options.voice,
7690
- staff: options.staff,
7691
- position: options.position,
7692
- pitch: options.note.pitch ?? { step: "C", octave: 4 },
7693
- duration: options.note.duration,
7694
- noteType: options.note.noteType,
7695
- dots: options.note.dots
7696
- });
7697
- return result.success ? result.data : score;
7698
- };
7699
- var deleteNote = (score, options) => {
7700
- const result = removeNote(score, options);
7701
- return result.success ? result.data : score;
7702
- };
7703
- var addChordNote = (score, options) => {
7704
- const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
7705
- return result.success ? result.data : score;
7706
- };
7707
- var modifyNotePitch = (score, options) => {
7708
- const result = setNotePitch(score, options);
7709
- return result.success ? result.data : score;
7710
- };
7711
- var modifyNoteDuration = (score, options) => {
7712
- const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
7713
- return result.success ? result.data : score;
7714
- };
8291
+ function findNoteByIndex(measure, noteIndex) {
8292
+ let noteCount = 0;
8293
+ for (let i = 0; i < measure.entries.length; i++) {
8294
+ const entry = measure.entries[i];
8295
+ if (entry.type === "note" && !entry.rest) {
8296
+ if (noteCount === noteIndex) {
8297
+ return { note: entry, entryIndex: i };
8298
+ }
8299
+ noteCount++;
8300
+ }
8301
+ }
8302
+ return null;
8303
+ }
8304
+ function pitchesEqual2(p1, p2) {
8305
+ return p1.step === p2.step && p1.octave === p2.octave && (p1.alter ?? 0) === (p2.alter ?? 0);
8306
+ }
8307
+ function addTie(score, options) {
8308
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8309
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8310
+ }
8311
+ const part = score.parts[options.partIndex];
8312
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
8313
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8314
+ }
8315
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
8316
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8317
+ }
8318
+ const result = cloneScore(score);
8319
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
8320
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
8321
+ const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
8322
+ if (!startResult) {
8323
+ return failure([operationError("NOTE_NOT_FOUND", `Start note index ${options.startNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8324
+ }
8325
+ const endResult = findNoteByIndex(endMeasure, options.endNoteIndex);
8326
+ if (!endResult) {
8327
+ return failure([operationError("NOTE_NOT_FOUND", `End note index ${options.endNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8328
+ }
8329
+ const startNote = startResult.note;
8330
+ const endNote = endResult.note;
8331
+ if (!startNote.pitch || !endNote.pitch) {
8332
+ return failure([operationError("TIE_INVALID_TARGET", "Cannot tie notes without pitch", { partIndex: options.partIndex })]);
8333
+ }
8334
+ if (!pitchesEqual2(startNote.pitch, endNote.pitch)) {
8335
+ return failure([operationError("TIE_PITCH_MISMATCH", "Tied notes must have the same pitch", { partIndex: options.partIndex }, { startPitch: startNote.pitch, endPitch: endNote.pitch })]);
8336
+ }
8337
+ if (startNote.tie?.type === "start" || startNote.tie?.type === "continue") {
8338
+ return failure([operationError("TIE_ALREADY_EXISTS", "Start note already has a tie start", { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8339
+ }
8340
+ startNote.tie = { type: "start" };
8341
+ if (!startNote.notations) startNote.notations = [];
8342
+ startNote.notations.push({ type: "tied", tiedType: "start" });
8343
+ endNote.tie = { type: "stop" };
8344
+ if (!endNote.notations) endNote.notations = [];
8345
+ endNote.notations.push({ type: "tied", tiedType: "stop" });
8346
+ const validationResult = validate(result, { checkTies: true });
8347
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
8348
+ if (criticalErrors.length > 0) {
8349
+ return failure(criticalErrors);
8350
+ }
8351
+ return success(result, validationResult.warnings);
8352
+ }
8353
+ function removeTie(score, options) {
8354
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8355
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8356
+ }
8357
+ const part = score.parts[options.partIndex];
8358
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8359
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8360
+ }
8361
+ const result = cloneScore(score);
8362
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8363
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8364
+ if (!noteResult) {
8365
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8366
+ }
8367
+ const note = noteResult.note;
8368
+ if (!note.tie) {
8369
+ return failure([operationError("TIE_NOT_FOUND", "Note does not have a tie", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8370
+ }
8371
+ delete note.tie;
8372
+ delete note.ties;
8373
+ if (note.notations) {
8374
+ note.notations = note.notations.filter((n) => n.type !== "tied");
8375
+ if (note.notations.length === 0) {
8376
+ delete note.notations;
8377
+ }
8378
+ }
8379
+ return success(result);
8380
+ }
8381
+ function addSlur(score, options) {
8382
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8383
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8384
+ }
8385
+ const part = score.parts[options.partIndex];
8386
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
8387
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8388
+ }
8389
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
8390
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8391
+ }
8392
+ const result = cloneScore(score);
8393
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
8394
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
8395
+ const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
8396
+ if (!startResult) {
8397
+ return failure([operationError("NOTE_NOT_FOUND", `Start note index ${options.startNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8398
+ }
8399
+ const endResult = findNoteByIndex(endMeasure, options.endNoteIndex);
8400
+ if (!endResult) {
8401
+ return failure([operationError("NOTE_NOT_FOUND", `End note index ${options.endNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8402
+ }
8403
+ const startNote = startResult.note;
8404
+ const endNote = endResult.note;
8405
+ const slurNumber = options.number ?? 1;
8406
+ if (startNote.notations?.some((n) => n.type === "slur" && n.slurType === "start" && (n.number ?? 1) === slurNumber)) {
8407
+ return failure([operationError("SLUR_ALREADY_EXISTS", `Slur ${slurNumber} already starts on this note`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8408
+ }
8409
+ if (!startNote.notations) startNote.notations = [];
8410
+ startNote.notations.push({
8411
+ type: "slur",
8412
+ slurType: "start",
8413
+ number: slurNumber,
8414
+ placement: options.placement
8415
+ });
8416
+ if (!endNote.notations) endNote.notations = [];
8417
+ endNote.notations.push({
8418
+ type: "slur",
8419
+ slurType: "stop",
8420
+ number: slurNumber
8421
+ });
8422
+ const validationResult = validate(result, { checkSlurs: true });
8423
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
8424
+ if (criticalErrors.length > 0) {
8425
+ return failure(criticalErrors);
8426
+ }
8427
+ return success(result, validationResult.warnings);
8428
+ }
8429
+ function removeSlur(score, options) {
8430
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8431
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8432
+ }
8433
+ const part = score.parts[options.partIndex];
8434
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8435
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8436
+ }
8437
+ const result = cloneScore(score);
8438
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8439
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8440
+ if (!noteResult) {
8441
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8442
+ }
8443
+ const note = noteResult.note;
8444
+ const slurNumber = options.number ?? 1;
8445
+ if (!note.notations?.some((n) => n.type === "slur" && (n.number ?? 1) === slurNumber)) {
8446
+ return failure([operationError("SLUR_NOT_FOUND", `Slur ${slurNumber} not found on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8447
+ }
8448
+ note.notations = note.notations.filter((n) => !(n.type === "slur" && (n.number ?? 1) === slurNumber));
8449
+ if (note.notations.length === 0) {
8450
+ delete note.notations;
8451
+ }
8452
+ return success(result);
8453
+ }
8454
+ function addArticulation(score, options) {
8455
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8456
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8457
+ }
8458
+ const part = score.parts[options.partIndex];
8459
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8460
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8461
+ }
8462
+ const result = cloneScore(score);
8463
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8464
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8465
+ if (!noteResult) {
8466
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8467
+ }
8468
+ const note = noteResult.note;
8469
+ if (note.notations?.some((n) => n.type === "articulation" && n.articulation === options.articulation)) {
8470
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Articulation ${options.articulation} already exists on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8471
+ }
8472
+ if (!note.notations) note.notations = [];
8473
+ note.notations.push({
8474
+ type: "articulation",
8475
+ articulation: options.articulation,
8476
+ placement: options.placement
8477
+ });
8478
+ return success(result);
8479
+ }
8480
+ function removeArticulation(score, options) {
8481
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8482
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8483
+ }
8484
+ const part = score.parts[options.partIndex];
8485
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8486
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8487
+ }
8488
+ const result = cloneScore(score);
8489
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8490
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8491
+ if (!noteResult) {
8492
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8493
+ }
8494
+ const note = noteResult.note;
8495
+ if (!note.notations?.some((n) => n.type === "articulation" && n.articulation === options.articulation)) {
8496
+ return failure([operationError("ARTICULATION_NOT_FOUND", `Articulation ${options.articulation} not found on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8497
+ }
8498
+ note.notations = note.notations.filter((n) => !(n.type === "articulation" && n.articulation === options.articulation));
8499
+ if (note.notations.length === 0) {
8500
+ delete note.notations;
8501
+ }
8502
+ return success(result);
8503
+ }
8504
+ function getInsertPositionForDirection(measure, targetPosition) {
8505
+ let position = 0;
8506
+ let insertIndex = 0;
8507
+ for (let i = 0; i < measure.entries.length; i++) {
8508
+ const entry = measure.entries[i];
8509
+ if (position >= targetPosition) {
8510
+ return insertIndex;
8511
+ }
8512
+ if (entry.type === "note" && !entry.chord) {
8513
+ position += entry.duration;
8514
+ } else if (entry.type === "backup") {
8515
+ position -= entry.duration;
8516
+ } else if (entry.type === "forward") {
8517
+ position += entry.duration;
8518
+ }
8519
+ insertIndex = i + 1;
8520
+ }
8521
+ return insertIndex;
8522
+ }
8523
+ function addDynamics(score, options) {
8524
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8525
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8526
+ }
8527
+ const part = score.parts[options.partIndex];
8528
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8529
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8530
+ }
8531
+ if (options.position < 0) {
8532
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8533
+ }
8534
+ const result = cloneScore(score);
8535
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8536
+ const directionEntry = {
8537
+ _id: generateId(),
8538
+ type: "direction",
8539
+ directionTypes: [{
8540
+ kind: "dynamics",
8541
+ value: options.dynamics
8542
+ }],
8543
+ placement: options.placement ?? "below",
8544
+ staff: options.staff
8545
+ };
8546
+ const insertIndex = getInsertPositionForDirection(measure, options.position);
8547
+ measure.entries.splice(insertIndex, 0, directionEntry);
8548
+ return success(result);
8549
+ }
8550
+ function removeDynamics(score, options) {
8551
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8552
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8553
+ }
8554
+ const part = score.parts[options.partIndex];
8555
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8556
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8557
+ }
8558
+ const result = cloneScore(score);
8559
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8560
+ let directionCount = 0;
8561
+ let targetIndex = -1;
8562
+ for (let i = 0; i < measure.entries.length; i++) {
8563
+ const entry = measure.entries[i];
8564
+ if (entry.type === "direction") {
8565
+ const hasDynamics = entry.directionTypes.some((dt) => dt.kind === "dynamics");
8566
+ if (hasDynamics) {
8567
+ if (directionCount === options.directionIndex) {
8568
+ targetIndex = i;
8569
+ break;
8570
+ }
8571
+ directionCount++;
8572
+ }
8573
+ }
8574
+ }
8575
+ if (targetIndex === -1) {
8576
+ return failure([operationError("DYNAMICS_NOT_FOUND", `Dynamics direction index ${options.directionIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8577
+ }
8578
+ measure.entries.splice(targetIndex, 1);
8579
+ return success(result);
8580
+ }
8581
+ function modifyDynamics(score, options) {
8582
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8583
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8584
+ }
8585
+ const part = score.parts[options.partIndex];
8586
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8587
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8588
+ }
8589
+ const result = cloneScore(score);
8590
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8591
+ let dynamicsCount = 0;
8592
+ let targetIndex = -1;
8593
+ for (let i = 0; i < measure.entries.length; i++) {
8594
+ const entry = measure.entries[i];
8595
+ if (entry.type === "direction") {
8596
+ const hasDynamics = entry.directionTypes.some((dt) => dt.kind === "dynamics");
8597
+ if (hasDynamics) {
8598
+ if (dynamicsCount === options.directionIndex) {
8599
+ targetIndex = i;
8600
+ break;
8601
+ }
8602
+ dynamicsCount++;
8603
+ }
8604
+ }
8605
+ }
8606
+ if (targetIndex === -1) {
8607
+ return failure([operationError("DYNAMICS_NOT_FOUND", `Dynamics direction index ${options.directionIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8608
+ }
8609
+ const direction = measure.entries[targetIndex];
8610
+ const dynamicsType = direction.directionTypes.find((dt) => dt.kind === "dynamics");
8611
+ if (dynamicsType && dynamicsType.kind === "dynamics") {
8612
+ dynamicsType.value = options.dynamics;
8613
+ }
8614
+ if (options.placement !== void 0) {
8615
+ direction.placement = options.placement;
8616
+ }
8617
+ return success(result);
8618
+ }
8619
+ function insertClefChange(score, options) {
8620
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8621
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8622
+ }
8623
+ const part = score.parts[options.partIndex];
8624
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8625
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8626
+ }
8627
+ if (options.position < 0) {
8628
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8629
+ }
8630
+ const validSigns = ["G", "F", "C", "percussion", "TAB"];
8631
+ if (!validSigns.includes(options.clef.sign)) {
8632
+ return failure([operationError("INVALID_CLEF", `Invalid clef sign: ${options.clef.sign}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8633
+ }
8634
+ const result = cloneScore(score);
8635
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8636
+ if (options.position === 0) {
8637
+ if (!measure.attributes) {
8638
+ measure.attributes = {};
8639
+ }
8640
+ const staff = options.clef.staff ?? 1;
8641
+ if (!measure.attributes.clef) {
8642
+ measure.attributes.clef = [];
8643
+ }
8644
+ const existingIndex = measure.attributes.clef.findIndex((c) => (c.staff ?? 1) === staff);
8645
+ if (existingIndex >= 0) {
8646
+ measure.attributes.clef[existingIndex] = options.clef;
8647
+ } else {
8648
+ measure.attributes.clef.push(options.clef);
8649
+ }
8650
+ } else {
8651
+ const attributesEntry = {
8652
+ _id: generateId(),
8653
+ type: "attributes",
8654
+ attributes: {
8655
+ clef: [options.clef]
8656
+ }
8657
+ };
8658
+ const insertIndex = getInsertPositionForDirection(measure, options.position);
8659
+ measure.entries.splice(insertIndex, 0, attributesEntry);
8660
+ }
8661
+ const validationResult = validate(result, { checkStaffStructure: true });
8662
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
8663
+ if (criticalErrors.length > 0) {
8664
+ return failure(criticalErrors);
8665
+ }
8666
+ return success(result, validationResult.warnings);
8667
+ }
8668
+ var addNote = (score, options) => {
8669
+ const result = insertNote(score, {
8670
+ partIndex: options.partIndex,
8671
+ measureIndex: options.measureIndex,
8672
+ voice: options.voice,
8673
+ staff: options.staff,
8674
+ position: options.position,
8675
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
8676
+ duration: options.note.duration,
8677
+ noteType: options.note.noteType,
8678
+ dots: options.note.dots
8679
+ });
8680
+ return result.success ? result.data : score;
8681
+ };
8682
+ var deleteNote = (score, options) => {
8683
+ const result = removeNote(score, options);
8684
+ return result.success ? result.data : score;
8685
+ };
8686
+ var addChordNote = (score, options) => {
8687
+ const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
8688
+ return result.success ? result.data : score;
8689
+ };
8690
+ var modifyNotePitch = (score, options) => {
8691
+ const result = setNotePitch(score, options);
8692
+ return result.success ? result.data : score;
8693
+ };
8694
+ var modifyNoteDuration = (score, options) => {
8695
+ const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
8696
+ return result.success ? result.data : score;
8697
+ };
8698
+ var addNoteChecked = (score, options) => {
8699
+ return insertNote(score, {
8700
+ partIndex: options.partIndex,
8701
+ measureIndex: options.measureIndex,
8702
+ voice: options.voice,
8703
+ staff: options.staff,
8704
+ position: options.position,
8705
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
8706
+ duration: options.note.duration,
8707
+ noteType: options.note.noteType,
8708
+ dots: options.note.dots
8709
+ });
8710
+ };
8711
+ var deleteNoteChecked = removeNote;
8712
+ var addChordNoteChecked = (score, options) => {
8713
+ return addChord(score, { ...options, noteIndex: options.afterNoteIndex });
8714
+ };
8715
+ var modifyNotePitchChecked = setNotePitch;
8716
+ var modifyNoteDurationChecked = (score, options) => {
8717
+ return changeNoteDuration(score, { ...options, newDuration: options.duration });
8718
+ };
8719
+ var transposeChecked = transpose;
8720
+ function createTuplet(score, options) {
8721
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8722
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8723
+ }
8724
+ const part = score.parts[options.partIndex];
8725
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8726
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8727
+ }
8728
+ if (options.noteCount < 2) {
8729
+ return failure([operationError("INVALID_DURATION", "Tuplet must contain at least 2 notes", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8730
+ }
8731
+ if (options.actualNotes < 2 || options.normalNotes < 1) {
8732
+ return failure([operationError("INVALID_DURATION", "Invalid tuplet ratio", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8733
+ }
8734
+ const result = cloneScore(score);
8735
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8736
+ const notes = [];
8737
+ let noteCount = 0;
8738
+ for (let i = 0; i < measure.entries.length; i++) {
8739
+ const entry = measure.entries[i];
8740
+ if (entry.type === "note" && !entry.rest && !entry.chord) {
8741
+ if (noteCount >= options.startNoteIndex && noteCount < options.startNoteIndex + options.noteCount) {
8742
+ notes.push({ note: entry, entryIndex: i });
8743
+ }
8744
+ noteCount++;
8745
+ }
8746
+ }
8747
+ if (notes.length !== options.noteCount) {
8748
+ return failure([operationError("NOTE_NOT_FOUND", `Could not find ${options.noteCount} notes starting at index ${options.startNoteIndex}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8749
+ }
8750
+ const voice = notes[0].note.voice;
8751
+ const staff = notes[0].note.staff;
8752
+ if (!notes.every((n) => n.note.voice === voice)) {
8753
+ return failure([operationError("NOTE_CONFLICT", "All notes in a tuplet must be in the same voice", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice })]);
8754
+ }
8755
+ if (!notes.every((n) => n.note.staff === staff)) {
8756
+ return failure([operationError("NOTE_CONFLICT", "All notes in a tuplet must be on the same staff", { partIndex: options.partIndex, measureIndex: options.measureIndex, staff })]);
8757
+ }
8758
+ const tupletNumber = 1;
8759
+ for (let i = 0; i < notes.length; i++) {
8760
+ const { note } = notes[i];
8761
+ note.timeModification = {
8762
+ actualNotes: options.actualNotes,
8763
+ normalNotes: options.normalNotes
8764
+ };
8765
+ if (!note.notations) note.notations = [];
8766
+ if (i === 0) {
8767
+ note.notations.push({
8768
+ type: "tuplet",
8769
+ tupletType: "start",
8770
+ number: tupletNumber,
8771
+ bracket: options.bracket ?? true,
8772
+ showNumber: options.showNumber ?? "actual",
8773
+ tupletActual: { tupletNumber: options.actualNotes },
8774
+ tupletNormal: { tupletNumber: options.normalNotes }
8775
+ });
8776
+ } else if (i === notes.length - 1) {
8777
+ note.notations.push({
8778
+ type: "tuplet",
8779
+ tupletType: "stop",
8780
+ number: tupletNumber
8781
+ });
8782
+ }
8783
+ }
8784
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8785
+ const errors = validateMeasureLocal(measure, context, {
8786
+ checkTuplets: true,
8787
+ checkMeasureDuration: true
8788
+ });
8789
+ const criticalErrors = errors.filter((e) => e.level === "error");
8790
+ if (criticalErrors.length > 0) {
8791
+ return failure(criticalErrors);
8792
+ }
8793
+ return success(result, errors.filter((e) => e.level !== "error"));
8794
+ }
8795
+ function removeTuplet(score, options) {
8796
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8797
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8798
+ }
8799
+ const part = score.parts[options.partIndex];
8800
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8801
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8802
+ }
8803
+ const result = cloneScore(score);
8804
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8805
+ let noteCount = 0;
8806
+ let targetNote = null;
8807
+ let targetEntryIndex = -1;
8808
+ for (let i = 0; i < measure.entries.length; i++) {
8809
+ const entry = measure.entries[i];
8810
+ if (entry.type === "note" && !entry.rest) {
8811
+ if (noteCount === options.noteIndex) {
8812
+ targetNote = entry;
8813
+ targetEntryIndex = i;
8814
+ break;
8815
+ }
8816
+ noteCount++;
8817
+ }
8818
+ }
8819
+ if (!targetNote || targetEntryIndex === -1) {
8820
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8821
+ }
8822
+ if (!targetNote.timeModification) {
8823
+ return failure([operationError("NOTE_NOT_FOUND", "Note is not part of a tuplet", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8824
+ }
8825
+ const voice = targetNote.voice;
8826
+ const staff = targetNote.staff;
8827
+ const actualNotes = targetNote.timeModification.actualNotes;
8828
+ const normalNotes = targetNote.timeModification.normalNotes;
8829
+ const tupletNotes = [];
8830
+ let inTuplet = false;
8831
+ let currentTupletNumber;
8832
+ for (const entry of measure.entries) {
8833
+ if (entry.type !== "note" || entry.rest) continue;
8834
+ if (entry.voice !== voice || entry.staff !== staff) continue;
8835
+ const hasSameTimeModification = entry.timeModification?.actualNotes === actualNotes && entry.timeModification?.normalNotes === normalNotes;
8836
+ const tupletStart = entry.notations?.find(
8837
+ (n) => n.type === "tuplet" && n.tupletType === "start"
8838
+ );
8839
+ const tupletStop = entry.notations?.find(
8840
+ (n) => n.type === "tuplet" && n.tupletType === "stop" && (currentTupletNumber === void 0 || n.number === currentTupletNumber)
8841
+ );
8842
+ if (tupletStart && tupletStart.type === "tuplet") {
8843
+ inTuplet = true;
8844
+ currentTupletNumber = tupletStart.number;
8845
+ }
8846
+ if (inTuplet && hasSameTimeModification) {
8847
+ tupletNotes.push(entry);
8848
+ }
8849
+ if (tupletStop && inTuplet) {
8850
+ if (tupletNotes.includes(targetNote)) {
8851
+ break;
8852
+ } else {
8853
+ tupletNotes.length = 0;
8854
+ inTuplet = false;
8855
+ currentTupletNumber = void 0;
8856
+ }
8857
+ }
8858
+ }
8859
+ if (tupletNotes.length === 0) {
8860
+ tupletNotes.push(targetNote);
8861
+ }
8862
+ for (const note of tupletNotes) {
8863
+ delete note.timeModification;
8864
+ if (note.notations) {
8865
+ note.notations = note.notations.filter((n) => n.type !== "tuplet");
8866
+ if (note.notations.length === 0) {
8867
+ delete note.notations;
8868
+ }
8869
+ }
8870
+ }
8871
+ return success(result);
8872
+ }
8873
+ function addBeam(score, options) {
8874
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8875
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8876
+ }
8877
+ const part = score.parts[options.partIndex];
8878
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8879
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8880
+ }
8881
+ if (options.noteCount < 2) {
8882
+ return failure([operationError("INVALID_DURATION", "Beam must contain at least 2 notes", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8883
+ }
8884
+ const result = cloneScore(score);
8885
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8886
+ const beamLevel = options.beamLevel ?? 1;
8887
+ const notes = [];
8888
+ let noteCount = 0;
8889
+ for (const entry of measure.entries) {
8890
+ if (entry.type === "note" && !entry.rest && !entry.chord) {
8891
+ if (noteCount >= options.startNoteIndex && noteCount < options.startNoteIndex + options.noteCount) {
8892
+ notes.push(entry);
8893
+ }
8894
+ noteCount++;
8895
+ }
8896
+ }
8897
+ if (notes.length !== options.noteCount) {
8898
+ return failure([operationError("NOTE_NOT_FOUND", `Could not find ${options.noteCount} notes starting at index ${options.startNoteIndex}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8899
+ }
8900
+ const voice = notes[0].voice;
8901
+ if (!notes.every((n) => n.voice === voice)) {
8902
+ return failure([operationError("NOTE_CONFLICT", "All beamed notes must be in the same voice", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice })]);
8903
+ }
8904
+ for (let i = 0; i < notes.length; i++) {
8905
+ const note = notes[i];
8906
+ if (!note.beam) {
8907
+ note.beam = [];
8908
+ }
8909
+ note.beam = note.beam.filter((b) => b.number !== beamLevel);
8910
+ let beamType;
8911
+ if (i === 0) {
8912
+ beamType = "begin";
8913
+ } else if (i === notes.length - 1) {
8914
+ beamType = "end";
8915
+ } else {
8916
+ beamType = "continue";
8917
+ }
8918
+ note.beam.push({
8919
+ number: beamLevel,
8920
+ type: beamType
8921
+ });
8922
+ }
8923
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8924
+ const errors = validateMeasureLocal(measure, context, { checkBeams: true });
8925
+ const criticalErrors = errors.filter((e) => e.level === "error");
8926
+ if (criticalErrors.length > 0) {
8927
+ return failure(criticalErrors);
8928
+ }
8929
+ return success(result, errors.filter((e) => e.level !== "error"));
8930
+ }
8931
+ function removeBeam(score, options) {
8932
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8933
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8934
+ }
8935
+ const part = score.parts[options.partIndex];
8936
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8937
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8938
+ }
8939
+ const result = cloneScore(score);
8940
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8941
+ let noteCount = 0;
8942
+ let targetNote = null;
8943
+ for (const entry of measure.entries) {
8944
+ if (entry.type === "note" && !entry.rest) {
8945
+ if (noteCount === options.noteIndex) {
8946
+ targetNote = entry;
8947
+ break;
8948
+ }
8949
+ noteCount++;
8950
+ }
8951
+ }
8952
+ if (!targetNote) {
8953
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8954
+ }
8955
+ if (!targetNote.beam || targetNote.beam.length === 0) {
8956
+ return failure([operationError("NOTE_NOT_FOUND", "Note is not part of a beam group", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8957
+ }
8958
+ const voice = targetNote.voice;
8959
+ const staff = targetNote.staff;
8960
+ const beamNotes = [];
8961
+ let inBeam = false;
8962
+ const targetBeamLevel = options.beamLevel ?? targetNote.beam[0]?.number ?? 1;
8963
+ for (const entry of measure.entries) {
8964
+ if (entry.type !== "note" || entry.rest) continue;
8965
+ if (entry.voice !== voice || entry.staff !== staff) continue;
8966
+ const beamInfo = entry.beam?.find((b) => b.number === targetBeamLevel);
8967
+ if (!beamInfo) {
8968
+ if (inBeam) {
8969
+ break;
8970
+ }
8971
+ continue;
8972
+ }
8973
+ if (beamInfo.type === "begin") {
8974
+ inBeam = true;
8975
+ beamNotes.push(entry);
8976
+ } else if (beamInfo.type === "continue") {
8977
+ if (inBeam) beamNotes.push(entry);
8978
+ } else if (beamInfo.type === "end") {
8979
+ beamNotes.push(entry);
8980
+ if (beamNotes.includes(targetNote)) {
8981
+ break;
8982
+ } else {
8983
+ beamNotes.length = 0;
8984
+ inBeam = false;
8985
+ }
8986
+ }
8987
+ }
8988
+ if (beamNotes.length === 0) {
8989
+ beamNotes.push(targetNote);
8990
+ }
8991
+ for (const note of beamNotes) {
8992
+ if (note.beam) {
8993
+ if (options.beamLevel !== void 0) {
8994
+ note.beam = note.beam.filter((b) => b.number !== options.beamLevel);
8995
+ } else {
8996
+ note.beam = [];
8997
+ }
8998
+ if (note.beam.length === 0) {
8999
+ delete note.beam;
9000
+ }
9001
+ }
9002
+ }
9003
+ return success(result);
9004
+ }
9005
+ function autoBeam(score, options) {
9006
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9007
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9008
+ }
9009
+ const part = score.parts[options.partIndex];
9010
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9011
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9012
+ }
9013
+ const result = cloneScore(score);
9014
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9015
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
9016
+ const divisions = context.divisions;
9017
+ const time = context.time ?? { beats: "4", beatType: 4 };
9018
+ const beatDuration = 4 / time.beatType * divisions;
9019
+ for (const entry of measure.entries) {
9020
+ if (entry.type === "note") {
9021
+ delete entry.beam;
9022
+ }
9023
+ }
9024
+ const notesByVoice = /* @__PURE__ */ new Map();
9025
+ let position = 0;
9026
+ for (const entry of measure.entries) {
9027
+ if (entry.type === "note") {
9028
+ if (!entry.chord && !entry.rest) {
9029
+ const voice = entry.voice;
9030
+ if (options.voice === void 0 || voice === options.voice) {
9031
+ if (!notesByVoice.has(voice)) {
9032
+ notesByVoice.set(voice, []);
9033
+ }
9034
+ notesByVoice.get(voice).push({ note: entry, position });
9035
+ }
9036
+ }
9037
+ if (!entry.chord) {
9038
+ position += entry.duration;
9039
+ }
9040
+ } else if (entry.type === "backup") {
9041
+ position -= entry.duration;
9042
+ } else if (entry.type === "forward") {
9043
+ position += entry.duration;
9044
+ }
9045
+ }
9046
+ for (const [, notes] of notesByVoice) {
9047
+ const beatGroups = [];
9048
+ let currentBeat = -1;
9049
+ let currentGroup = [];
9050
+ for (const { note, position: notePos } of notes) {
9051
+ if (note.duration > beatDuration / 2) {
9052
+ if (currentGroup.length >= 2) {
9053
+ beatGroups.push(currentGroup);
9054
+ }
9055
+ currentGroup = [];
9056
+ currentBeat = -1;
9057
+ continue;
9058
+ }
9059
+ const beat = Math.floor(notePos / beatDuration);
9060
+ if (options.groupByBeat !== false && beat !== currentBeat) {
9061
+ if (currentGroup.length >= 2) {
9062
+ beatGroups.push(currentGroup);
9063
+ }
9064
+ currentGroup = [{ note, position: notePos }];
9065
+ currentBeat = beat;
9066
+ } else {
9067
+ currentGroup.push({ note, position: notePos });
9068
+ }
9069
+ }
9070
+ if (currentGroup.length >= 2) {
9071
+ beatGroups.push(currentGroup);
9072
+ }
9073
+ for (const group of beatGroups) {
9074
+ for (let i = 0; i < group.length; i++) {
9075
+ const { note } = group[i];
9076
+ if (!note.beam) {
9077
+ note.beam = [];
9078
+ }
9079
+ let beamType;
9080
+ if (i === 0) {
9081
+ beamType = "begin";
9082
+ } else if (i === group.length - 1) {
9083
+ beamType = "end";
9084
+ } else {
9085
+ beamType = "continue";
9086
+ }
9087
+ note.beam.push({
9088
+ number: 1,
9089
+ type: beamType
9090
+ });
9091
+ }
9092
+ }
9093
+ }
9094
+ const errors = validateMeasureLocal(measure, context, { checkBeams: true });
9095
+ const criticalErrors = errors.filter((e) => e.level === "error");
9096
+ if (criticalErrors.length > 0) {
9097
+ return failure(criticalErrors);
9098
+ }
9099
+ return success(result, errors.filter((e) => e.level !== "error"));
9100
+ }
9101
+ function copyNotes(score, options) {
9102
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9103
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9104
+ }
9105
+ const part = score.parts[options.partIndex];
9106
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9107
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9108
+ }
9109
+ if (options.startPosition >= options.endPosition) {
9110
+ return failure([operationError("INVALID_POSITION", "Start position must be less than end position", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9111
+ }
9112
+ const measure = part.measures[options.measureIndex];
9113
+ const copiedNotes = [];
9114
+ let position = 0;
9115
+ for (const entry of measure.entries) {
9116
+ if (entry.type === "note") {
9117
+ if (entry.voice === options.voice && (options.staff === void 0 || (entry.staff ?? 1) === options.staff)) {
9118
+ if (!entry.chord) {
9119
+ const noteEnd = position + entry.duration;
9120
+ if (position < options.endPosition && noteEnd > options.startPosition) {
9121
+ const clonedNote = cloneNoteWithNewId(entry);
9122
+ if (clonedNote.tie) {
9123
+ }
9124
+ copiedNotes.push({
9125
+ relativePosition: position - options.startPosition,
9126
+ note: clonedNote
9127
+ });
9128
+ }
9129
+ position += entry.duration;
9130
+ } else {
9131
+ if (copiedNotes.length > 0) {
9132
+ const lastCopied = copiedNotes[copiedNotes.length - 1];
9133
+ if (lastCopied.note.voice === entry.voice && (options.staff === void 0 || (lastCopied.note.staff ?? 1) === (entry.staff ?? 1))) {
9134
+ const clonedNote = cloneNoteWithNewId(entry);
9135
+ copiedNotes.push({
9136
+ relativePosition: lastCopied.relativePosition,
9137
+ note: clonedNote
9138
+ });
9139
+ }
9140
+ }
9141
+ }
9142
+ } else if (!entry.chord) {
9143
+ position += entry.duration;
9144
+ }
9145
+ } else if (entry.type === "backup") {
9146
+ position -= entry.duration;
9147
+ } else if (entry.type === "forward") {
9148
+ position += entry.duration;
9149
+ }
9150
+ }
9151
+ if (copiedNotes.length === 0) {
9152
+ return failure([operationError("NOTE_NOT_FOUND", "No notes found in the specified range", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice })]);
9153
+ }
9154
+ const selection = {
9155
+ source: {
9156
+ partIndex: options.partIndex,
9157
+ measureIndex: options.measureIndex,
9158
+ startPosition: options.startPosition,
9159
+ endPosition: options.endPosition,
9160
+ voice: options.voice,
9161
+ staff: options.staff
9162
+ },
9163
+ notes: copiedNotes,
9164
+ duration: options.endPosition - options.startPosition
9165
+ };
9166
+ return success(selection);
9167
+ }
9168
+ function pasteNotes(score, options) {
9169
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9170
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9171
+ }
9172
+ const part = score.parts[options.partIndex];
9173
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9174
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9175
+ }
9176
+ if (options.position < 0) {
9177
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9178
+ }
9179
+ const result = cloneScore(score);
9180
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9181
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
9182
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
9183
+ const targetVoice = options.voice ?? options.selection.source.voice;
9184
+ const targetStaff = options.staff ?? options.selection.source.staff;
9185
+ const pasteEnd = options.position + options.selection.duration;
9186
+ if (pasteEnd > measureDuration) {
9187
+ return failure([operationError(
9188
+ "EXCEEDS_MEASURE",
9189
+ `Paste would exceed measure duration (ends at ${pasteEnd}, measure is ${measureDuration})`,
9190
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
9191
+ { pasteEnd, measureDuration }
9192
+ )]);
9193
+ }
9194
+ const voiceEntries = getVoiceEntries(measure, targetVoice, targetStaff);
9195
+ if (options.overwrite !== false) {
9196
+ const entriesToKeep = voiceEntries.filter((e) => {
9197
+ if (e.entry.type !== "note") return true;
9198
+ const note = e.entry;
9199
+ if (note.rest) return true;
9200
+ return e.endPosition <= options.position || e.position >= pasteEnd;
9201
+ });
9202
+ const newEntries = [];
9203
+ for (const { position, entry } of entriesToKeep) {
9204
+ if (entry.type === "note") {
9205
+ newEntries.push({ position, entry });
9206
+ }
9207
+ }
9208
+ for (const { relativePosition, note } of options.selection.notes) {
9209
+ const pastePosition = options.position + Math.max(0, relativePosition);
9210
+ const newNote = cloneNoteWithNewId(note);
9211
+ newNote.voice = targetVoice;
9212
+ if (targetStaff !== void 0) {
9213
+ newNote.staff = targetStaff;
9214
+ }
9215
+ delete newNote.tie;
9216
+ delete newNote.ties;
9217
+ if (newNote.notations) {
9218
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
9219
+ if (newNote.notations.length === 0) {
9220
+ delete newNote.notations;
9221
+ }
9222
+ }
9223
+ newEntries.push({ position: pastePosition, entry: newNote });
9224
+ }
9225
+ measure.entries = rebuildMeasureWithVoice(
9226
+ measure,
9227
+ targetVoice,
9228
+ newEntries,
9229
+ measureDuration,
9230
+ targetStaff
9231
+ );
9232
+ } else {
9233
+ const { hasNotes: hasNotes2, conflictingNotes } = hasNotesInRange(voiceEntries, options.position, pasteEnd);
9234
+ if (hasNotes2) {
9235
+ return failure([operationError(
9236
+ "NOTE_CONFLICT",
9237
+ `Paste range ${options.position}-${pasteEnd} conflicts with existing notes`,
9238
+ { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: targetVoice },
9239
+ { conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
9240
+ )]);
9241
+ }
9242
+ const existingNotes = voiceEntries.filter((e) => e.entry.type === "note").map((e) => ({ position: e.position, entry: e.entry }));
9243
+ for (const { relativePosition, note } of options.selection.notes) {
9244
+ const pastePosition = options.position + Math.max(0, relativePosition);
9245
+ const newNote = cloneNoteWithNewId(note);
9246
+ newNote.voice = targetVoice;
9247
+ if (targetStaff !== void 0) {
9248
+ newNote.staff = targetStaff;
9249
+ }
9250
+ delete newNote.tie;
9251
+ delete newNote.ties;
9252
+ if (newNote.notations) {
9253
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
9254
+ if (newNote.notations.length === 0) {
9255
+ delete newNote.notations;
9256
+ }
9257
+ }
9258
+ existingNotes.push({ position: pastePosition, entry: newNote });
9259
+ }
9260
+ measure.entries = rebuildMeasureWithVoice(
9261
+ measure,
9262
+ targetVoice,
9263
+ existingNotes,
9264
+ measureDuration,
9265
+ targetStaff
9266
+ );
9267
+ }
9268
+ const errors = validateMeasureLocal(measure, context, {
9269
+ checkMeasureDuration: true,
9270
+ checkPosition: true,
9271
+ checkVoiceStaff: true
9272
+ });
9273
+ const criticalErrors = errors.filter((e) => e.level === "error");
9274
+ if (criticalErrors.length > 0) {
9275
+ return failure(criticalErrors);
9276
+ }
9277
+ return success(result, errors.filter((e) => e.level !== "error"));
9278
+ }
9279
+ function cutNotes(score, options) {
9280
+ const copyResult = copyNotes(score, options);
9281
+ if (!copyResult.success) {
9282
+ return failure(copyResult.errors);
9283
+ }
9284
+ const selection = copyResult.data;
9285
+ const result = cloneScore(score);
9286
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9287
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
9288
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
9289
+ const voiceEntries = getVoiceEntries(measure, options.voice, options.staff);
9290
+ const entriesToKeep = voiceEntries.filter((e) => {
9291
+ if (e.entry.type !== "note") return true;
9292
+ const note = e.entry;
9293
+ if (note.rest) return true;
9294
+ return e.endPosition <= options.startPosition || e.position >= options.endPosition;
9295
+ });
9296
+ const newEntries = [];
9297
+ for (const { position, entry } of entriesToKeep) {
9298
+ if (entry.type === "note") {
9299
+ newEntries.push({ position, entry });
9300
+ }
9301
+ }
9302
+ measure.entries = rebuildMeasureWithVoice(
9303
+ measure,
9304
+ options.voice,
9305
+ newEntries,
9306
+ measureDuration,
9307
+ options.staff
9308
+ );
9309
+ const errors = validateMeasureLocal(measure, context, {
9310
+ checkMeasureDuration: true,
9311
+ checkPosition: true,
9312
+ checkVoiceStaff: true
9313
+ });
9314
+ const criticalErrors = errors.filter((e) => e.level === "error");
9315
+ if (criticalErrors.length > 0) {
9316
+ return failure(criticalErrors);
9317
+ }
9318
+ return success(
9319
+ { score: result, selection },
9320
+ errors.filter((e) => e.level !== "error")
9321
+ );
9322
+ }
9323
+ function copyNotesMultiMeasure(score, options) {
9324
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9325
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9326
+ }
9327
+ const part = score.parts[options.partIndex];
9328
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
9329
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
9330
+ }
9331
+ if (options.endMeasureIndex < options.startMeasureIndex || options.endMeasureIndex >= part.measures.length) {
9332
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
9333
+ }
9334
+ const selection = {
9335
+ source: {
9336
+ partIndex: options.partIndex,
9337
+ startMeasureIndex: options.startMeasureIndex,
9338
+ endMeasureIndex: options.endMeasureIndex,
9339
+ voice: options.voice,
9340
+ staff: options.staff
9341
+ },
9342
+ measures: []
9343
+ };
9344
+ for (let measureIndex = options.startMeasureIndex; measureIndex <= options.endMeasureIndex; measureIndex++) {
9345
+ const measure = part.measures[measureIndex];
9346
+ const measureOffset = measureIndex - options.startMeasureIndex;
9347
+ const copiedNotes = [];
9348
+ let position = 0;
9349
+ for (const entry of measure.entries) {
9350
+ if (entry.type === "note") {
9351
+ if (entry.voice === options.voice && (options.staff === void 0 || (entry.staff ?? 1) === options.staff)) {
9352
+ if (!entry.chord && !entry.rest) {
9353
+ const clonedNote = cloneNoteWithNewId(entry);
9354
+ copiedNotes.push({
9355
+ relativePosition: position,
9356
+ note: clonedNote
9357
+ });
9358
+ position += entry.duration;
9359
+ } else if (entry.chord && copiedNotes.length > 0) {
9360
+ const clonedNote = cloneNoteWithNewId(entry);
9361
+ copiedNotes.push({
9362
+ relativePosition: copiedNotes[copiedNotes.length - 1].relativePosition,
9363
+ note: clonedNote
9364
+ });
9365
+ } else if (!entry.chord) {
9366
+ position += entry.duration;
9367
+ }
9368
+ } else if (!entry.chord) {
9369
+ position += entry.duration;
9370
+ }
9371
+ } else if (entry.type === "backup") {
9372
+ position -= entry.duration;
9373
+ } else if (entry.type === "forward") {
9374
+ position += entry.duration;
9375
+ }
9376
+ }
9377
+ if (copiedNotes.length > 0) {
9378
+ selection.measures.push({
9379
+ measureOffset,
9380
+ notes: copiedNotes
9381
+ });
9382
+ }
9383
+ }
9384
+ if (selection.measures.length === 0) {
9385
+ return failure([operationError("NOTE_NOT_FOUND", "No notes found in the specified range", { partIndex: options.partIndex, voice: options.voice })]);
9386
+ }
9387
+ return success(selection);
9388
+ }
9389
+ function pasteNotesMultiMeasure(score, options) {
9390
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9391
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9392
+ }
9393
+ const part = score.parts[options.partIndex];
9394
+ const measureCount = options.selection.measures.length > 0 ? options.selection.measures[options.selection.measures.length - 1].measureOffset + 1 : 0;
9395
+ if (options.startMeasureIndex + measureCount > part.measures.length) {
9396
+ return failure([operationError("MEASURE_NOT_FOUND", `Not enough measures to paste (need ${measureCount})`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
9397
+ }
9398
+ let result = cloneScore(score);
9399
+ const targetVoice = options.voice ?? options.selection.source.voice;
9400
+ const targetStaff = options.staff ?? options.selection.source.staff;
9401
+ for (const measureData of options.selection.measures) {
9402
+ const measureIndex = options.startMeasureIndex + measureData.measureOffset;
9403
+ const measure = result.parts[options.partIndex].measures[measureIndex];
9404
+ const context = getMeasureContext(result, options.partIndex, measureIndex);
9405
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
9406
+ const voiceEntries = getVoiceEntries(measure, targetVoice, targetStaff);
9407
+ let entriesToKeep;
9408
+ if (options.overwrite !== false) {
9409
+ entriesToKeep = voiceEntries.filter((e) => e.entry.type === "note" && e.entry.rest).map((e) => ({ position: e.position, entry: e.entry }));
9410
+ } else {
9411
+ entriesToKeep = voiceEntries.filter((e) => e.entry.type === "note").map((e) => ({ position: e.position, entry: e.entry }));
9412
+ }
9413
+ for (const { relativePosition, note } of measureData.notes) {
9414
+ const newNote = cloneNoteWithNewId(note);
9415
+ newNote.voice = targetVoice;
9416
+ if (targetStaff !== void 0) {
9417
+ newNote.staff = targetStaff;
9418
+ }
9419
+ delete newNote.tie;
9420
+ delete newNote.ties;
9421
+ if (newNote.notations) {
9422
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
9423
+ if (newNote.notations.length === 0) {
9424
+ delete newNote.notations;
9425
+ }
9426
+ }
9427
+ entriesToKeep.push({ position: relativePosition, entry: newNote });
9428
+ }
9429
+ measure.entries = rebuildMeasureWithVoice(
9430
+ measure,
9431
+ targetVoice,
9432
+ entriesToKeep,
9433
+ measureDuration,
9434
+ targetStaff
9435
+ );
9436
+ const errors = validateMeasureLocal(measure, context, {
9437
+ checkMeasureDuration: true,
9438
+ checkPosition: true,
9439
+ checkVoiceStaff: true
9440
+ });
9441
+ const criticalErrors = errors.filter((e) => e.level === "error");
9442
+ if (criticalErrors.length > 0) {
9443
+ return failure(criticalErrors);
9444
+ }
9445
+ }
9446
+ return success(result);
9447
+ }
9448
+ function addTempo(score, options) {
9449
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9450
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9451
+ }
9452
+ const part = score.parts[options.partIndex];
9453
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9454
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9455
+ }
9456
+ if (options.bpm <= 0) {
9457
+ return failure([operationError("INVALID_DURATION", "BPM must be positive", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9458
+ }
9459
+ const result = cloneScore(score);
9460
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9461
+ const directionTypes = [];
9462
+ directionTypes.push({
9463
+ kind: "metronome",
9464
+ beatUnit: options.beatUnit ?? "quarter",
9465
+ beatUnitDot: options.beatUnitDot,
9466
+ perMinute: options.bpm
9467
+ });
9468
+ if (options.text) {
9469
+ directionTypes.push({
9470
+ kind: "words",
9471
+ text: options.text,
9472
+ fontWeight: "bold"
9473
+ });
9474
+ }
9475
+ const direction = {
9476
+ _id: generateId(),
9477
+ type: "direction",
9478
+ directionTypes,
9479
+ placement: options.placement ?? "above",
9480
+ sound: { tempo: options.bpm }
9481
+ };
9482
+ insertDirectionAtPosition(measure, direction, options.position);
9483
+ return success(result);
9484
+ }
9485
+ function removeTempo(score, options) {
9486
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9487
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9488
+ }
9489
+ const part = score.parts[options.partIndex];
9490
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9491
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9492
+ }
9493
+ const result = cloneScore(score);
9494
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9495
+ const tempoDirectionIndices = [];
9496
+ for (let i = 0; i < measure.entries.length; i++) {
9497
+ const entry = measure.entries[i];
9498
+ if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "metronome")) {
9499
+ tempoDirectionIndices.push(i);
9500
+ }
9501
+ }
9502
+ if (tempoDirectionIndices.length === 0) {
9503
+ return failure([operationError("TEMPO_NOT_FOUND", "No tempo marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9504
+ }
9505
+ const targetIndex = options.directionIndex ?? 0;
9506
+ if (targetIndex < 0 || targetIndex >= tempoDirectionIndices.length) {
9507
+ return failure([operationError("TEMPO_NOT_FOUND", `Tempo direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9508
+ }
9509
+ measure.entries.splice(tempoDirectionIndices[targetIndex], 1);
9510
+ return success(result);
9511
+ }
9512
+ function modifyTempo(score, options) {
9513
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9514
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9515
+ }
9516
+ const part = score.parts[options.partIndex];
9517
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9518
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9519
+ }
9520
+ const result = cloneScore(score);
9521
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9522
+ const tempoDirectionIndices = [];
9523
+ for (let i = 0; i < measure.entries.length; i++) {
9524
+ const entry = measure.entries[i];
9525
+ if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "metronome")) {
9526
+ tempoDirectionIndices.push(i);
9527
+ }
9528
+ }
9529
+ if (tempoDirectionIndices.length === 0) {
9530
+ return failure([operationError("TEMPO_NOT_FOUND", "No tempo marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9531
+ }
9532
+ const targetIndex = options.directionIndex ?? 0;
9533
+ if (targetIndex < 0 || targetIndex >= tempoDirectionIndices.length) {
9534
+ return failure([operationError("TEMPO_NOT_FOUND", `Tempo direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9535
+ }
9536
+ const direction = measure.entries[tempoDirectionIndices[targetIndex]];
9537
+ const metronome = direction.directionTypes.find((dt) => dt.kind === "metronome");
9538
+ if (metronome && metronome.kind === "metronome") {
9539
+ if (options.bpm !== void 0) {
9540
+ metronome.perMinute = options.bpm;
9541
+ }
9542
+ if (options.beatUnit !== void 0) {
9543
+ metronome.beatUnit = options.beatUnit;
9544
+ }
9545
+ if (options.beatUnitDot !== void 0) {
9546
+ metronome.beatUnitDot = options.beatUnitDot;
9547
+ }
9548
+ }
9549
+ if (options.text !== void 0) {
9550
+ const wordsIndex = direction.directionTypes.findIndex((dt) => dt.kind === "words");
9551
+ if (wordsIndex >= 0) {
9552
+ const words = direction.directionTypes[wordsIndex];
9553
+ if (words.kind === "words") {
9554
+ words.text = options.text;
9555
+ }
9556
+ } else if (options.text) {
9557
+ direction.directionTypes.push({
9558
+ kind: "words",
9559
+ text: options.text,
9560
+ fontWeight: "bold"
9561
+ });
9562
+ }
9563
+ }
9564
+ if (options.bpm !== void 0 && direction.sound) {
9565
+ direction.sound.tempo = options.bpm;
9566
+ }
9567
+ if (options.placement !== void 0) {
9568
+ direction.placement = options.placement;
9569
+ }
9570
+ return success(result);
9571
+ }
9572
+ function addWedge(score, options) {
9573
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9574
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9575
+ }
9576
+ const part = score.parts[options.partIndex];
9577
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
9578
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
9579
+ }
9580
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
9581
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
9582
+ }
9583
+ if (options.endMeasureIndex < options.startMeasureIndex || options.endMeasureIndex === options.startMeasureIndex && options.endPosition <= options.startPosition) {
9584
+ return failure([operationError("INVALID_RANGE", "End position must be after start position", { partIndex: options.partIndex })]);
9585
+ }
9586
+ const result = cloneScore(score);
9587
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
9588
+ const startDirection = {
9589
+ _id: generateId(),
9590
+ type: "direction",
9591
+ directionTypes: [{
9592
+ kind: "wedge",
9593
+ type: options.type
9594
+ }],
9595
+ placement: options.placement ?? "below",
9596
+ staff: options.staff
9597
+ };
9598
+ insertDirectionAtPosition(startMeasure, startDirection, options.startPosition);
9599
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
9600
+ const endDirection = {
9601
+ _id: generateId(),
9602
+ type: "direction",
9603
+ directionTypes: [{
9604
+ kind: "wedge",
9605
+ type: "stop"
9606
+ }],
9607
+ placement: options.placement ?? "below",
9608
+ staff: options.staff
9609
+ };
9610
+ insertDirectionAtPosition(endMeasure, endDirection, options.endPosition);
9611
+ return success(result);
9612
+ }
9613
+ function removeWedge(score, options) {
9614
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9615
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9616
+ }
9617
+ const part = score.parts[options.partIndex];
9618
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9619
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9620
+ }
9621
+ const result = cloneScore(score);
9622
+ const wedgeStarts = [];
9623
+ for (let mi = options.measureIndex; mi < result.parts[options.partIndex].measures.length; mi++) {
9624
+ const measure = result.parts[options.partIndex].measures[mi];
9625
+ for (let ei = 0; ei < measure.entries.length; ei++) {
9626
+ const entry = measure.entries[ei];
9627
+ if (entry.type === "direction") {
9628
+ const wedgeType = entry.directionTypes.find((dt) => dt.kind === "wedge");
9629
+ if (wedgeType && wedgeType.kind === "wedge" && (wedgeType.type === "crescendo" || wedgeType.type === "diminuendo")) {
9630
+ wedgeStarts.push({ measureIndex: mi, entryIndex: ei });
9631
+ }
9632
+ }
9633
+ }
9634
+ if (mi === options.measureIndex && wedgeStarts.length > 0) break;
9635
+ }
9636
+ if (wedgeStarts.length === 0) {
9637
+ return failure([operationError("WEDGE_NOT_FOUND", "No wedge found starting in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9638
+ }
9639
+ const targetIndex = options.directionIndex ?? 0;
9640
+ if (targetIndex >= wedgeStarts.length) {
9641
+ return failure([operationError("WEDGE_NOT_FOUND", `Wedge direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9642
+ }
9643
+ const startInfo = wedgeStarts[targetIndex];
9644
+ const startMeasure = result.parts[options.partIndex].measures[startInfo.measureIndex];
9645
+ startMeasure.entries.splice(startInfo.entryIndex, 1);
9646
+ for (let mi = startInfo.measureIndex; mi < result.parts[options.partIndex].measures.length; mi++) {
9647
+ const measure = result.parts[options.partIndex].measures[mi];
9648
+ for (let ei = 0; ei < measure.entries.length; ei++) {
9649
+ const entry = measure.entries[ei];
9650
+ if (entry.type === "direction") {
9651
+ const wedgeType = entry.directionTypes.find((dt) => dt.kind === "wedge" && dt.type === "stop");
9652
+ if (wedgeType) {
9653
+ measure.entries.splice(ei, 1);
9654
+ return success(result);
9655
+ }
9656
+ }
9657
+ }
9658
+ }
9659
+ return success(result);
9660
+ }
9661
+ function addFermata(score, options) {
9662
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9663
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9664
+ }
9665
+ const part = score.parts[options.partIndex];
9666
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9667
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9668
+ }
9669
+ const result = cloneScore(score);
9670
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9671
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9672
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9673
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9674
+ }
9675
+ const note = notes[options.noteIndex];
9676
+ if (note.notations?.some((n) => n.type === "fermata")) {
9677
+ return failure([operationError("FERMATA_ALREADY_EXISTS", "Note already has a fermata", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9678
+ }
9679
+ if (!note.notations) {
9680
+ note.notations = [];
9681
+ }
9682
+ const fermataNotation = {
9683
+ type: "fermata",
9684
+ shape: options.shape ?? "normal",
9685
+ fermataType: options.fermataType ?? "upright",
9686
+ placement: options.placement ?? "above"
9687
+ };
9688
+ note.notations.push(fermataNotation);
9689
+ return success(result);
9690
+ }
9691
+ function removeFermata(score, options) {
9692
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9693
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9694
+ }
9695
+ const part = score.parts[options.partIndex];
9696
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9697
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9698
+ }
9699
+ const result = cloneScore(score);
9700
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9701
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9702
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9703
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9704
+ }
9705
+ const note = notes[options.noteIndex];
9706
+ const fermataIndex = note.notations?.findIndex((n) => n.type === "fermata");
9707
+ if (fermataIndex === void 0 || fermataIndex === -1) {
9708
+ return failure([operationError("FERMATA_NOT_FOUND", "Note does not have a fermata", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9709
+ }
9710
+ note.notations.splice(fermataIndex, 1);
9711
+ if (note.notations.length === 0) {
9712
+ delete note.notations;
9713
+ }
9714
+ return success(result);
9715
+ }
9716
+ function addOrnament(score, options) {
9717
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9718
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9719
+ }
9720
+ const part = score.parts[options.partIndex];
9721
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9722
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9723
+ }
9724
+ const result = cloneScore(score);
9725
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9726
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9727
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9728
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9729
+ }
9730
+ const note = notes[options.noteIndex];
9731
+ if (note.notations?.some((n) => n.type === "ornament" && n.ornament === options.ornament)) {
9732
+ return failure([operationError("ORNAMENT_ALREADY_EXISTS", `Note already has ornament: ${options.ornament}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9733
+ }
9734
+ if (!note.notations) {
9735
+ note.notations = [];
9736
+ }
9737
+ const ornamentNotation = {
9738
+ type: "ornament",
9739
+ ornament: options.ornament,
9740
+ placement: options.placement,
9741
+ accidentalMark: options.accidentalMark
9742
+ };
9743
+ note.notations.push(ornamentNotation);
9744
+ return success(result);
9745
+ }
9746
+ function removeOrnament(score, options) {
9747
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9748
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9749
+ }
9750
+ const part = score.parts[options.partIndex];
9751
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9752
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9753
+ }
9754
+ const result = cloneScore(score);
9755
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9756
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9757
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9758
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9759
+ }
9760
+ const note = notes[options.noteIndex];
9761
+ const ornamentIndex = options.ornament ? note.notations?.findIndex((n) => n.type === "ornament" && n.ornament === options.ornament) : note.notations?.findIndex((n) => n.type === "ornament");
9762
+ if (ornamentIndex === void 0 || ornamentIndex === -1) {
9763
+ return failure([operationError("ORNAMENT_NOT_FOUND", "Note does not have the specified ornament", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9764
+ }
9765
+ note.notations.splice(ornamentIndex, 1);
9766
+ if (note.notations.length === 0) {
9767
+ delete note.notations;
9768
+ }
9769
+ return success(result);
9770
+ }
9771
+ function addPedal(score, options) {
9772
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9773
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9774
+ }
9775
+ const part = score.parts[options.partIndex];
9776
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9777
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9778
+ }
9779
+ const result = cloneScore(score);
9780
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9781
+ const direction = {
9782
+ _id: generateId(),
9783
+ type: "direction",
9784
+ directionTypes: [{
9785
+ kind: "pedal",
9786
+ type: options.pedalType,
9787
+ line: options.line
9788
+ }],
9789
+ placement: options.placement ?? "below"
9790
+ };
9791
+ insertDirectionAtPosition(measure, direction, options.position);
9792
+ return success(result);
9793
+ }
9794
+ function removePedal(score, options) {
9795
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9796
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9797
+ }
9798
+ const part = score.parts[options.partIndex];
9799
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9800
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9801
+ }
9802
+ const result = cloneScore(score);
9803
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9804
+ const pedalIndices = [];
9805
+ for (let i = 0; i < measure.entries.length; i++) {
9806
+ const entry = measure.entries[i];
9807
+ if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "pedal")) {
9808
+ pedalIndices.push(i);
9809
+ }
9810
+ }
9811
+ if (pedalIndices.length === 0) {
9812
+ return failure([operationError("PEDAL_NOT_FOUND", "No pedal marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9813
+ }
9814
+ const targetIndex = options.directionIndex ?? 0;
9815
+ if (targetIndex < 0 || targetIndex >= pedalIndices.length) {
9816
+ return failure([operationError("PEDAL_NOT_FOUND", `Pedal direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9817
+ }
9818
+ measure.entries.splice(pedalIndices[targetIndex], 1);
9819
+ return success(result);
9820
+ }
9821
+ function addTextDirection(score, options) {
9822
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9823
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9824
+ }
9825
+ const part = score.parts[options.partIndex];
9826
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9827
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9828
+ }
9829
+ if (!options.text.trim()) {
9830
+ return failure([operationError("INVALID_TEXT", "Text cannot be empty", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9831
+ }
9832
+ const result = cloneScore(score);
9833
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9834
+ const direction = {
9835
+ _id: generateId(),
9836
+ type: "direction",
9837
+ directionTypes: [{
9838
+ kind: "words",
9839
+ text: options.text,
9840
+ fontStyle: options.fontStyle,
9841
+ fontWeight: options.fontWeight
9842
+ }],
9843
+ placement: options.placement ?? "above"
9844
+ };
9845
+ insertDirectionAtPosition(measure, direction, options.position);
9846
+ return success(result);
9847
+ }
9848
+ function addRehearsalMark(score, options) {
9849
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9850
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9851
+ }
9852
+ const part = score.parts[options.partIndex];
9853
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9854
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9855
+ }
9856
+ const result = cloneScore(score);
9857
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9858
+ const direction = {
9859
+ _id: generateId(),
9860
+ type: "direction",
9861
+ directionTypes: [{
9862
+ kind: "rehearsal",
9863
+ text: options.text,
9864
+ enclosure: options.enclosure ?? "square"
9865
+ }],
9866
+ placement: "above"
9867
+ };
9868
+ insertDirectionAtPosition(measure, direction, 0);
9869
+ return success(result);
9870
+ }
9871
+ function insertDirectionAtPosition(measure, direction, position) {
9872
+ let currentPosition = 0;
9873
+ let insertIndex = 0;
9874
+ for (let i = 0; i < measure.entries.length; i++) {
9875
+ const entry = measure.entries[i];
9876
+ if (currentPosition >= position) {
9877
+ insertIndex = i;
9878
+ break;
9879
+ }
9880
+ if (entry.type === "note" && !entry.chord) {
9881
+ currentPosition += entry.duration;
9882
+ } else if (entry.type === "forward") {
9883
+ currentPosition += entry.duration;
9884
+ } else if (entry.type === "backup") {
9885
+ currentPosition -= entry.duration;
9886
+ }
9887
+ insertIndex = i + 1;
9888
+ }
9889
+ measure.entries.splice(insertIndex, 0, direction);
9890
+ }
9891
+ function addRepeatBarline(score, options) {
9892
+ const { partIndex, measureIndex, direction, times } = options;
9893
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9894
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9895
+ }
9896
+ const part = score.parts[partIndex];
9897
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9898
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9899
+ }
9900
+ const result = cloneScore(score);
9901
+ const location = direction === "forward" ? "left" : "right";
9902
+ const barStyle = direction === "forward" ? "heavy-light" : "light-heavy";
9903
+ for (const p of result.parts) {
9904
+ if (measureIndex >= p.measures.length) continue;
9905
+ const measure = p.measures[measureIndex];
9906
+ if (!measure.barlines) {
9907
+ measure.barlines = [];
9908
+ }
9909
+ const existingIndex = measure.barlines.findIndex((b) => b.location === location && b.repeat);
9910
+ if (existingIndex >= 0) {
9911
+ return failure([operationError("REPEAT_ALREADY_EXISTS", `Repeat barline already exists at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9912
+ }
9913
+ const nonRepeatIndex = measure.barlines.findIndex((b) => b.location === location && !b.repeat);
9914
+ if (nonRepeatIndex >= 0) {
9915
+ measure.barlines.splice(nonRepeatIndex, 1);
9916
+ }
9917
+ measure.barlines.push({
9918
+ _id: generateId(),
9919
+ location,
9920
+ barStyle,
9921
+ repeat: {
9922
+ direction,
9923
+ times
9924
+ }
9925
+ });
9926
+ }
9927
+ return success(result);
9928
+ }
9929
+ function removeRepeatBarline(score, options) {
9930
+ const { partIndex, measureIndex, location } = options;
9931
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9932
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9933
+ }
9934
+ const part = score.parts[partIndex];
9935
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9936
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9937
+ }
9938
+ const measure = part.measures[measureIndex];
9939
+ if (!measure.barlines) {
9940
+ return failure([operationError("REPEAT_NOT_FOUND", `No repeat barline found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9941
+ }
9942
+ const existingIndex = measure.barlines.findIndex((b) => b.location === location && b.repeat);
9943
+ if (existingIndex < 0) {
9944
+ return failure([operationError("REPEAT_NOT_FOUND", `No repeat barline found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9945
+ }
9946
+ const result = cloneScore(score);
9947
+ for (const p of result.parts) {
9948
+ if (measureIndex >= p.measures.length) continue;
9949
+ const m = p.measures[measureIndex];
9950
+ if (m.barlines) {
9951
+ const idx = m.barlines.findIndex((b) => b.location === location && b.repeat);
9952
+ if (idx >= 0) {
9953
+ m.barlines.splice(idx, 1);
9954
+ }
9955
+ if (m.barlines.length === 0) {
9956
+ delete m.barlines;
9957
+ }
9958
+ }
9959
+ }
9960
+ return success(result);
9961
+ }
9962
+ function addEnding(score, options) {
9963
+ const { partIndex, measureIndex, number, type } = options;
9964
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9965
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9966
+ }
9967
+ const part = score.parts[partIndex];
9968
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9969
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9970
+ }
9971
+ const result = cloneScore(score);
9972
+ const location = type === "start" ? "left" : "right";
9973
+ for (const p of result.parts) {
9974
+ if (measureIndex >= p.measures.length) continue;
9975
+ const measure = p.measures[measureIndex];
9976
+ if (!measure.barlines) {
9977
+ measure.barlines = [];
9978
+ }
9979
+ let barline = measure.barlines.find((b) => b.location === location);
9980
+ if (!barline) {
9981
+ barline = { _id: generateId(), location };
9982
+ measure.barlines.push(barline);
9983
+ }
9984
+ if (barline.ending) {
9985
+ return failure([operationError("ENDING_ALREADY_EXISTS", `Ending already exists at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9986
+ }
9987
+ barline.ending = { number, type };
9988
+ }
9989
+ return success(result);
9990
+ }
9991
+ function removeEnding(score, options) {
9992
+ const { partIndex, measureIndex, location } = options;
9993
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9994
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9995
+ }
9996
+ const part = score.parts[partIndex];
9997
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9998
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9999
+ }
10000
+ const measure = part.measures[measureIndex];
10001
+ const barline = measure.barlines?.find((b) => b.location === location && b.ending);
10002
+ if (!barline) {
10003
+ return failure([operationError("ENDING_NOT_FOUND", `No ending found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
10004
+ }
10005
+ const result = cloneScore(score);
10006
+ for (const p of result.parts) {
10007
+ if (measureIndex >= p.measures.length) continue;
10008
+ const m = p.measures[measureIndex];
10009
+ if (m.barlines) {
10010
+ const bl = m.barlines.find((b) => b.location === location);
10011
+ if (bl) {
10012
+ delete bl.ending;
10013
+ if (!bl.barStyle && !bl.repeat && !bl.ending) {
10014
+ const idx = m.barlines.indexOf(bl);
10015
+ m.barlines.splice(idx, 1);
10016
+ }
10017
+ }
10018
+ if (m.barlines.length === 0) {
10019
+ delete m.barlines;
10020
+ }
10021
+ }
10022
+ }
10023
+ return success(result);
10024
+ }
10025
+ function changeBarline(score, options) {
10026
+ const { partIndex, measureIndex, location, barStyle } = options;
10027
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10028
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10029
+ }
10030
+ const part = score.parts[partIndex];
10031
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10032
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10033
+ }
10034
+ const result = cloneScore(score);
10035
+ for (const p of result.parts) {
10036
+ if (measureIndex >= p.measures.length) continue;
10037
+ const measure = p.measures[measureIndex];
10038
+ if (!measure.barlines) {
10039
+ measure.barlines = [];
10040
+ }
10041
+ let barline = measure.barlines.find((b) => b.location === location);
10042
+ if (!barline) {
10043
+ barline = { _id: generateId(), location };
10044
+ measure.barlines.push(barline);
10045
+ }
10046
+ barline.barStyle = barStyle;
10047
+ }
10048
+ return success(result);
10049
+ }
10050
+ function addSegno(score, options) {
10051
+ const { partIndex, measureIndex, position = 0 } = options;
10052
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10053
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10054
+ }
10055
+ const part = score.parts[partIndex];
10056
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10057
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10058
+ }
10059
+ const result = cloneScore(score);
10060
+ const measure = result.parts[partIndex].measures[measureIndex];
10061
+ const direction = {
10062
+ _id: generateId(),
10063
+ type: "direction",
10064
+ directionTypes: [{ kind: "segno" }],
10065
+ placement: "above"
10066
+ };
10067
+ insertDirectionAtPosition(measure, direction, position);
10068
+ return success(result);
10069
+ }
10070
+ function addCoda(score, options) {
10071
+ const { partIndex, measureIndex, position = 0 } = options;
10072
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10073
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10074
+ }
10075
+ const part = score.parts[partIndex];
10076
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10077
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10078
+ }
10079
+ const result = cloneScore(score);
10080
+ const measure = result.parts[partIndex].measures[measureIndex];
10081
+ const direction = {
10082
+ _id: generateId(),
10083
+ type: "direction",
10084
+ directionTypes: [{ kind: "coda" }],
10085
+ placement: "above"
10086
+ };
10087
+ insertDirectionAtPosition(measure, direction, position);
10088
+ return success(result);
10089
+ }
10090
+ function addDaCapo(score, options) {
10091
+ const { partIndex, measureIndex, position } = options;
10092
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10093
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10094
+ }
10095
+ const part = score.parts[partIndex];
10096
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10097
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10098
+ }
10099
+ const result = cloneScore(score);
10100
+ const measure = result.parts[partIndex].measures[measureIndex];
10101
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
10102
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
10103
+ const insertPos = position ?? measureDuration;
10104
+ const direction = {
10105
+ _id: generateId(),
10106
+ type: "direction",
10107
+ directionTypes: [{ kind: "words", text: "D.C." }],
10108
+ placement: "above"
10109
+ };
10110
+ insertDirectionAtPosition(measure, direction, insertPos);
10111
+ const sound = {
10112
+ _id: generateId(),
10113
+ type: "sound",
10114
+ dacapo: true
10115
+ };
10116
+ measure.entries.push(sound);
10117
+ return success(result);
10118
+ }
10119
+ function addDalSegno(score, options) {
10120
+ const { partIndex, measureIndex, position } = options;
10121
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10122
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10123
+ }
10124
+ const part = score.parts[partIndex];
10125
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10126
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10127
+ }
10128
+ const result = cloneScore(score);
10129
+ const measure = result.parts[partIndex].measures[measureIndex];
10130
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
10131
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
10132
+ const insertPos = position ?? measureDuration;
10133
+ const direction = {
10134
+ _id: generateId(),
10135
+ type: "direction",
10136
+ directionTypes: [{ kind: "words", text: "D.S." }],
10137
+ placement: "above"
10138
+ };
10139
+ insertDirectionAtPosition(measure, direction, insertPos);
10140
+ const sound = {
10141
+ _id: generateId(),
10142
+ type: "sound",
10143
+ dalsegno: "segno"
10144
+ };
10145
+ measure.entries.push(sound);
10146
+ return success(result);
10147
+ }
10148
+ function addFine(score, options) {
10149
+ const { partIndex, measureIndex, position } = options;
10150
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10151
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10152
+ }
10153
+ const part = score.parts[partIndex];
10154
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10155
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10156
+ }
10157
+ const result = cloneScore(score);
10158
+ const measure = result.parts[partIndex].measures[measureIndex];
10159
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
10160
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
10161
+ const insertPos = position ?? measureDuration;
10162
+ const direction = {
10163
+ _id: generateId(),
10164
+ type: "direction",
10165
+ directionTypes: [{ kind: "words", text: "Fine" }],
10166
+ placement: "above"
10167
+ };
10168
+ insertDirectionAtPosition(measure, direction, insertPos);
10169
+ const sound = {
10170
+ _id: generateId(),
10171
+ type: "sound",
10172
+ fine: true
10173
+ };
10174
+ measure.entries.push(sound);
10175
+ return success(result);
10176
+ }
10177
+ function addToCoda(score, options) {
10178
+ const { partIndex, measureIndex, position } = options;
10179
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10180
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10181
+ }
10182
+ const part = score.parts[partIndex];
10183
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10184
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10185
+ }
10186
+ const result = cloneScore(score);
10187
+ const measure = result.parts[partIndex].measures[measureIndex];
10188
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
10189
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
10190
+ const insertPos = position ?? measureDuration;
10191
+ const direction = {
10192
+ _id: generateId(),
10193
+ type: "direction",
10194
+ directionTypes: [{ kind: "words", text: "To Coda" }],
10195
+ placement: "above"
10196
+ };
10197
+ insertDirectionAtPosition(measure, direction, insertPos);
10198
+ const sound = {
10199
+ _id: generateId(),
10200
+ type: "sound",
10201
+ tocoda: "coda"
10202
+ };
10203
+ measure.entries.push(sound);
10204
+ return success(result);
10205
+ }
10206
+ function addGraceNote(score, options) {
10207
+ const { partIndex, measureIndex, targetNoteIndex, pitch, noteType = "eighth", slash = true, voice, staff } = options;
10208
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10209
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10210
+ }
10211
+ const part = score.parts[partIndex];
10212
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10213
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10214
+ }
10215
+ const measure = part.measures[measureIndex];
10216
+ let noteCount = 0;
10217
+ let targetEntryIndex = -1;
10218
+ let targetNote = null;
10219
+ for (let i = 0; i < measure.entries.length; i++) {
10220
+ const entry = measure.entries[i];
10221
+ if (entry.type === "note" && !entry.chord) {
10222
+ if (noteCount === targetNoteIndex) {
10223
+ targetEntryIndex = i;
10224
+ targetNote = entry;
10225
+ break;
10226
+ }
10227
+ noteCount++;
10228
+ }
10229
+ }
10230
+ if (targetEntryIndex < 0 || !targetNote) {
10231
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${targetNoteIndex} not found`, { partIndex, measureIndex })]);
10232
+ }
10233
+ const result = cloneScore(score);
10234
+ const resultMeasure = result.parts[partIndex].measures[measureIndex];
10235
+ const graceNote = {
10236
+ _id: generateId(),
10237
+ type: "note",
10238
+ pitch,
10239
+ duration: 0,
10240
+ // Grace notes have no duration
10241
+ voice: voice ?? targetNote.voice,
10242
+ staff: staff ?? targetNote.staff,
10243
+ noteType,
10244
+ grace: {
10245
+ slash
10246
+ }
10247
+ };
10248
+ resultMeasure.entries.splice(targetEntryIndex, 0, graceNote);
10249
+ return success(result);
10250
+ }
10251
+ function removeGraceNote(score, options) {
10252
+ const { partIndex, measureIndex, graceNoteIndex } = options;
10253
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10254
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10255
+ }
10256
+ const part = score.parts[partIndex];
10257
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10258
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10259
+ }
10260
+ const measure = part.measures[measureIndex];
10261
+ let graceCount = 0;
10262
+ let targetEntryIndex = -1;
10263
+ for (let i = 0; i < measure.entries.length; i++) {
10264
+ const entry = measure.entries[i];
10265
+ if (entry.type === "note" && entry.grace) {
10266
+ if (graceCount === graceNoteIndex) {
10267
+ targetEntryIndex = i;
10268
+ break;
10269
+ }
10270
+ graceCount++;
10271
+ }
10272
+ }
10273
+ if (targetEntryIndex < 0) {
10274
+ return failure([operationError("GRACE_NOTE_NOT_FOUND", `Grace note at index ${graceNoteIndex} not found`, { partIndex, measureIndex })]);
10275
+ }
10276
+ const result = cloneScore(score);
10277
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
10278
+ return success(result);
10279
+ }
10280
+ function convertToGrace(score, options) {
10281
+ const { partIndex, measureIndex, noteIndex, slash = true } = options;
10282
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10283
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10284
+ }
10285
+ const part = score.parts[partIndex];
10286
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10287
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10288
+ }
10289
+ const measure = part.measures[measureIndex];
10290
+ let noteCount = 0;
10291
+ let targetEntryIndex = -1;
10292
+ for (let i = 0; i < measure.entries.length; i++) {
10293
+ const entry = measure.entries[i];
10294
+ if (entry.type === "note" && !entry.chord) {
10295
+ if (noteCount === noteIndex) {
10296
+ targetEntryIndex = i;
10297
+ break;
10298
+ }
10299
+ noteCount++;
10300
+ }
10301
+ }
10302
+ if (targetEntryIndex < 0) {
10303
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10304
+ }
10305
+ const targetEntry = measure.entries[targetEntryIndex];
10306
+ if (targetEntry.type !== "note") {
10307
+ return failure([operationError("NOTE_NOT_FOUND", `Entry at index is not a note`, { partIndex, measureIndex })]);
10308
+ }
10309
+ if (targetEntry.grace) {
10310
+ return failure([operationError("INVALID_GRACE_NOTE", `Note is already a grace note`, { partIndex, measureIndex })]);
10311
+ }
10312
+ const result = cloneScore(score);
10313
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10314
+ resultNote.grace = { slash };
10315
+ resultNote.duration = 0;
10316
+ return success(result);
10317
+ }
10318
+ function addLyric(score, options) {
10319
+ const { partIndex, measureIndex, noteIndex, text, syllabic = "single", verse = 1, extend = false } = options;
10320
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10321
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10322
+ }
10323
+ const part = score.parts[partIndex];
10324
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10325
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10326
+ }
10327
+ const measure = part.measures[measureIndex];
10328
+ let noteCount = 0;
10329
+ let targetEntryIndex = -1;
10330
+ for (let i = 0; i < measure.entries.length; i++) {
10331
+ const entry = measure.entries[i];
10332
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10333
+ if (noteCount === noteIndex) {
10334
+ targetEntryIndex = i;
10335
+ break;
10336
+ }
10337
+ noteCount++;
10338
+ }
10339
+ }
10340
+ if (targetEntryIndex < 0) {
10341
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10342
+ }
10343
+ const targetEntry = measure.entries[targetEntryIndex];
10344
+ if (targetEntry.type !== "note") {
10345
+ return failure([operationError("NOTE_NOT_FOUND", `Entry is not a note`, { partIndex, measureIndex })]);
10346
+ }
10347
+ if (targetEntry.lyrics?.some((l) => l.number === verse)) {
10348
+ return failure([operationError("LYRIC_ALREADY_EXISTS", `Lyric for verse ${verse} already exists on this note`, { partIndex, measureIndex })]);
10349
+ }
10350
+ const result = cloneScore(score);
10351
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10352
+ if (!resultNote.lyrics) {
10353
+ resultNote.lyrics = [];
10354
+ }
10355
+ const lyric = {
10356
+ number: verse,
10357
+ syllabic,
10358
+ text,
10359
+ extend
10360
+ };
10361
+ resultNote.lyrics.push(lyric);
10362
+ return success(result);
10363
+ }
10364
+ function removeLyric(score, options) {
10365
+ const { partIndex, measureIndex, noteIndex, verse } = options;
10366
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10367
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10368
+ }
10369
+ const part = score.parts[partIndex];
10370
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10371
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10372
+ }
10373
+ const measure = part.measures[measureIndex];
10374
+ let noteCount = 0;
10375
+ let targetEntryIndex = -1;
10376
+ for (let i = 0; i < measure.entries.length; i++) {
10377
+ const entry = measure.entries[i];
10378
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10379
+ if (noteCount === noteIndex) {
10380
+ targetEntryIndex = i;
10381
+ break;
10382
+ }
10383
+ noteCount++;
10384
+ }
10385
+ }
10386
+ if (targetEntryIndex < 0) {
10387
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10388
+ }
10389
+ const targetEntry = measure.entries[targetEntryIndex];
10390
+ if (targetEntry.type !== "note" || !targetEntry.lyrics || targetEntry.lyrics.length === 0) {
10391
+ return failure([operationError("LYRIC_NOT_FOUND", `No lyrics found on note`, { partIndex, measureIndex })]);
10392
+ }
10393
+ if (verse !== void 0) {
10394
+ const lyricIndex = targetEntry.lyrics.findIndex((l) => l.number === verse);
10395
+ if (lyricIndex < 0) {
10396
+ return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
10397
+ }
10398
+ }
10399
+ const result = cloneScore(score);
10400
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10401
+ if (verse !== void 0) {
10402
+ resultNote.lyrics = resultNote.lyrics.filter((l) => l.number !== verse);
10403
+ } else {
10404
+ delete resultNote.lyrics;
10405
+ }
10406
+ if (resultNote.lyrics && resultNote.lyrics.length === 0) {
10407
+ delete resultNote.lyrics;
10408
+ }
10409
+ return success(result);
10410
+ }
10411
+ function updateLyric(score, options) {
10412
+ const { partIndex, measureIndex, noteIndex, verse = 1, text, syllabic, extend } = options;
10413
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10414
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10415
+ }
10416
+ const part = score.parts[partIndex];
10417
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10418
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10419
+ }
10420
+ const measure = part.measures[measureIndex];
10421
+ let noteCount = 0;
10422
+ let targetEntryIndex = -1;
10423
+ for (let i = 0; i < measure.entries.length; i++) {
10424
+ const entry = measure.entries[i];
10425
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10426
+ if (noteCount === noteIndex) {
10427
+ targetEntryIndex = i;
10428
+ break;
10429
+ }
10430
+ noteCount++;
10431
+ }
10432
+ }
10433
+ if (targetEntryIndex < 0) {
10434
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10435
+ }
10436
+ const targetEntry = measure.entries[targetEntryIndex];
10437
+ if (targetEntry.type !== "note" || !targetEntry.lyrics) {
10438
+ return failure([operationError("LYRIC_NOT_FOUND", `No lyrics found on note`, { partIndex, measureIndex })]);
10439
+ }
10440
+ const lyricIndex = targetEntry.lyrics.findIndex((l) => l.number === verse);
10441
+ if (lyricIndex < 0) {
10442
+ return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
10443
+ }
10444
+ const result = cloneScore(score);
10445
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10446
+ const lyric = resultNote.lyrics[lyricIndex];
10447
+ if (text !== void 0) {
10448
+ lyric.text = text;
10449
+ }
10450
+ if (syllabic !== void 0) {
10451
+ lyric.syllabic = syllabic;
10452
+ }
10453
+ if (extend !== void 0) {
10454
+ lyric.extend = extend;
10455
+ }
10456
+ return success(result);
10457
+ }
10458
+ function addHarmony(score, options) {
10459
+ const { partIndex, measureIndex, position, root, kind, kindText, bass, degrees, staff, placement = "above" } = options;
10460
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10461
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10462
+ }
10463
+ const part = score.parts[partIndex];
10464
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10465
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10466
+ }
10467
+ const validSteps = ["A", "B", "C", "D", "E", "F", "G"];
10468
+ if (!validSteps.includes(root.step.toUpperCase())) {
10469
+ return failure([operationError("INVALID_HARMONY", `Invalid root step: ${root.step}`, { partIndex, measureIndex })]);
10470
+ }
10471
+ if (bass && !validSteps.includes(bass.step.toUpperCase())) {
10472
+ return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
10473
+ }
10474
+ const result = cloneScore(score);
10475
+ const measure = result.parts[partIndex].measures[measureIndex];
10476
+ const harmony = {
10477
+ _id: generateId(),
10478
+ type: "harmony",
10479
+ root: {
10480
+ rootStep: root.step.toUpperCase(),
10481
+ rootAlter: root.alter
10482
+ },
10483
+ kind,
10484
+ kindText,
10485
+ bass: bass ? {
10486
+ bassStep: bass.step.toUpperCase(),
10487
+ bassAlter: bass.alter
10488
+ } : void 0,
10489
+ degrees: degrees?.map((d) => ({
10490
+ degreeValue: d.value,
10491
+ degreeAlter: d.alter,
10492
+ degreeType: d.type
10493
+ })),
10494
+ staff,
10495
+ placement
10496
+ };
10497
+ let currentPosition = 0;
10498
+ let insertIndex = 0;
10499
+ for (let i = 0; i < measure.entries.length; i++) {
10500
+ const entry = measure.entries[i];
10501
+ if (currentPosition >= position) {
10502
+ insertIndex = i;
10503
+ break;
10504
+ }
10505
+ if (entry.type === "note" && !entry.chord) {
10506
+ currentPosition += entry.duration;
10507
+ } else if (entry.type === "forward") {
10508
+ currentPosition += entry.duration;
10509
+ } else if (entry.type === "backup") {
10510
+ currentPosition -= entry.duration;
10511
+ }
10512
+ insertIndex = i + 1;
10513
+ }
10514
+ measure.entries.splice(insertIndex, 0, harmony);
10515
+ return success(result);
10516
+ }
10517
+ function removeHarmony(score, options) {
10518
+ const { partIndex, measureIndex, harmonyIndex } = options;
10519
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10520
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10521
+ }
10522
+ const part = score.parts[partIndex];
10523
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10524
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10525
+ }
10526
+ const measure = part.measures[measureIndex];
10527
+ let harmonyCount = 0;
10528
+ let targetEntryIndex = -1;
10529
+ for (let i = 0; i < measure.entries.length; i++) {
10530
+ const entry = measure.entries[i];
10531
+ if (entry.type === "harmony") {
10532
+ if (harmonyCount === harmonyIndex) {
10533
+ targetEntryIndex = i;
10534
+ break;
10535
+ }
10536
+ harmonyCount++;
10537
+ }
10538
+ }
10539
+ if (targetEntryIndex < 0) {
10540
+ return failure([operationError("HARMONY_NOT_FOUND", `Harmony at index ${harmonyIndex} not found`, { partIndex, measureIndex })]);
10541
+ }
10542
+ const result = cloneScore(score);
10543
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
10544
+ return success(result);
10545
+ }
10546
+ function updateHarmony(score, options) {
10547
+ const { partIndex, measureIndex, harmonyIndex, root, kind, kindText, bass, degrees } = options;
10548
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10549
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10550
+ }
10551
+ const part = score.parts[partIndex];
10552
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10553
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10554
+ }
10555
+ const measure = part.measures[measureIndex];
10556
+ let harmonyCount = 0;
10557
+ let targetEntryIndex = -1;
10558
+ for (let i = 0; i < measure.entries.length; i++) {
10559
+ const entry = measure.entries[i];
10560
+ if (entry.type === "harmony") {
10561
+ if (harmonyCount === harmonyIndex) {
10562
+ targetEntryIndex = i;
10563
+ break;
10564
+ }
10565
+ harmonyCount++;
10566
+ }
10567
+ }
10568
+ if (targetEntryIndex < 0) {
10569
+ return failure([operationError("HARMONY_NOT_FOUND", `Harmony at index ${harmonyIndex} not found`, { partIndex, measureIndex })]);
10570
+ }
10571
+ const validSteps = ["A", "B", "C", "D", "E", "F", "G"];
10572
+ if (root && !validSteps.includes(root.step.toUpperCase())) {
10573
+ return failure([operationError("INVALID_HARMONY", `Invalid root step: ${root.step}`, { partIndex, measureIndex })]);
10574
+ }
10575
+ if (bass && !validSteps.includes(bass.step.toUpperCase())) {
10576
+ return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
10577
+ }
10578
+ const result = cloneScore(score);
10579
+ const harmony = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10580
+ if (root) {
10581
+ harmony.root = {
10582
+ rootStep: root.step.toUpperCase(),
10583
+ rootAlter: root.alter
10584
+ };
10585
+ }
10586
+ if (kind !== void 0) {
10587
+ harmony.kind = kind;
10588
+ }
10589
+ if (kindText !== void 0) {
10590
+ harmony.kindText = kindText;
10591
+ }
10592
+ if (bass !== void 0) {
10593
+ if (bass === null) {
10594
+ delete harmony.bass;
10595
+ } else {
10596
+ harmony.bass = {
10597
+ bassStep: bass.step.toUpperCase(),
10598
+ bassAlter: bass.alter
10599
+ };
10600
+ }
10601
+ }
10602
+ if (degrees !== void 0) {
10603
+ if (degrees === null) {
10604
+ delete harmony.degrees;
10605
+ } else {
10606
+ harmony.degrees = degrees.map((d) => ({
10607
+ degreeValue: d.value,
10608
+ degreeAlter: d.alter,
10609
+ degreeType: d.type
10610
+ }));
10611
+ }
10612
+ }
10613
+ return success(result);
10614
+ }
10615
+ function addFingering(score, options) {
10616
+ const { partIndex, measureIndex, noteIndex, fingering, substitution = false, alternate = false, placement } = options;
10617
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10618
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10619
+ }
10620
+ const part = score.parts[partIndex];
10621
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10622
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10623
+ }
10624
+ const measure = part.measures[measureIndex];
10625
+ let noteCount = 0;
10626
+ let targetEntryIndex = -1;
10627
+ for (let i = 0; i < measure.entries.length; i++) {
10628
+ const entry = measure.entries[i];
10629
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10630
+ if (noteCount === noteIndex) {
10631
+ targetEntryIndex = i;
10632
+ break;
10633
+ }
10634
+ noteCount++;
10635
+ }
10636
+ }
10637
+ if (targetEntryIndex < 0) {
10638
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10639
+ }
10640
+ const result = cloneScore(score);
10641
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10642
+ if (!resultNote.notations) {
10643
+ resultNote.notations = [];
10644
+ }
10645
+ resultNote.notations.push({
10646
+ type: "technical",
10647
+ technical: "fingering",
10648
+ fingering,
10649
+ fingeringSubstitution: substitution || void 0,
10650
+ fingeringAlternate: alternate || void 0,
10651
+ placement
10652
+ });
10653
+ return success(result);
10654
+ }
10655
+ function removeFingering(score, options) {
10656
+ const { partIndex, measureIndex, noteIndex } = options;
10657
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10658
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10659
+ }
10660
+ const part = score.parts[partIndex];
10661
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10662
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10663
+ }
10664
+ const measure = part.measures[measureIndex];
10665
+ let noteCount = 0;
10666
+ let targetEntryIndex = -1;
10667
+ for (let i = 0; i < measure.entries.length; i++) {
10668
+ const entry = measure.entries[i];
10669
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10670
+ if (noteCount === noteIndex) {
10671
+ targetEntryIndex = i;
10672
+ break;
10673
+ }
10674
+ noteCount++;
10675
+ }
10676
+ }
10677
+ if (targetEntryIndex < 0) {
10678
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10679
+ }
10680
+ const targetEntry = measure.entries[targetEntryIndex];
10681
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10682
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10683
+ }
10684
+ const fingeringIndex = targetEntry.notations.findIndex(
10685
+ (n) => n.type === "technical" && n.technical === "fingering"
10686
+ );
10687
+ if (fingeringIndex < 0) {
10688
+ return failure([operationError("NOTE_NOT_FOUND", `No fingering found on note`, { partIndex, measureIndex })]);
10689
+ }
10690
+ const result = cloneScore(score);
10691
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10692
+ resultNote.notations.splice(fingeringIndex, 1);
10693
+ if (resultNote.notations.length === 0) {
10694
+ delete resultNote.notations;
10695
+ }
10696
+ return success(result);
10697
+ }
10698
+ function addBowing(score, options) {
10699
+ const { partIndex, measureIndex, noteIndex, bowingType, placement } = options;
10700
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10701
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10702
+ }
10703
+ const part = score.parts[partIndex];
10704
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10705
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10706
+ }
10707
+ const measure = part.measures[measureIndex];
10708
+ let noteCount = 0;
10709
+ let targetEntryIndex = -1;
10710
+ for (let i = 0; i < measure.entries.length; i++) {
10711
+ const entry = measure.entries[i];
10712
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10713
+ if (noteCount === noteIndex) {
10714
+ targetEntryIndex = i;
10715
+ break;
10716
+ }
10717
+ noteCount++;
10718
+ }
10719
+ }
10720
+ if (targetEntryIndex < 0) {
10721
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10722
+ }
10723
+ const result = cloneScore(score);
10724
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10725
+ if (!resultNote.notations) {
10726
+ resultNote.notations = [];
10727
+ }
10728
+ resultNote.notations.push({
10729
+ type: "technical",
10730
+ technical: bowingType,
10731
+ placement
10732
+ });
10733
+ return success(result);
10734
+ }
10735
+ function removeBowing(score, options) {
10736
+ const { partIndex, measureIndex, noteIndex, bowingType } = options;
10737
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10738
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10739
+ }
10740
+ const part = score.parts[partIndex];
10741
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10742
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10743
+ }
10744
+ const measure = part.measures[measureIndex];
10745
+ let noteCount = 0;
10746
+ let targetEntryIndex = -1;
10747
+ for (let i = 0; i < measure.entries.length; i++) {
10748
+ const entry = measure.entries[i];
10749
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10750
+ if (noteCount === noteIndex) {
10751
+ targetEntryIndex = i;
10752
+ break;
10753
+ }
10754
+ noteCount++;
10755
+ }
10756
+ }
10757
+ if (targetEntryIndex < 0) {
10758
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10759
+ }
10760
+ const targetEntry = measure.entries[targetEntryIndex];
10761
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10762
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10763
+ }
10764
+ const bowingIndex = targetEntry.notations.findIndex((n) => {
10765
+ if (n.type !== "technical") return false;
10766
+ if (bowingType) return n.technical === bowingType;
10767
+ return n.technical === "up-bow" || n.technical === "down-bow";
10768
+ });
10769
+ if (bowingIndex < 0) {
10770
+ return failure([operationError("NOTE_NOT_FOUND", `No bowing found on note`, { partIndex, measureIndex })]);
10771
+ }
10772
+ const result = cloneScore(score);
10773
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10774
+ resultNote.notations.splice(bowingIndex, 1);
10775
+ if (resultNote.notations.length === 0) {
10776
+ delete resultNote.notations;
10777
+ }
10778
+ return success(result);
10779
+ }
10780
+ function addStringNumber(score, options) {
10781
+ const { partIndex, measureIndex, noteIndex, stringNumber, placement } = options;
10782
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10783
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10784
+ }
10785
+ const part = score.parts[partIndex];
10786
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10787
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10788
+ }
10789
+ if (stringNumber < 1) {
10790
+ return failure([operationError("INVALID_POSITION", `String number must be positive`, { partIndex, measureIndex })]);
10791
+ }
10792
+ const measure = part.measures[measureIndex];
10793
+ let noteCount = 0;
10794
+ let targetEntryIndex = -1;
10795
+ for (let i = 0; i < measure.entries.length; i++) {
10796
+ const entry = measure.entries[i];
10797
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10798
+ if (noteCount === noteIndex) {
10799
+ targetEntryIndex = i;
10800
+ break;
10801
+ }
10802
+ noteCount++;
10803
+ }
10804
+ }
10805
+ if (targetEntryIndex < 0) {
10806
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10807
+ }
10808
+ const result = cloneScore(score);
10809
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10810
+ if (!resultNote.notations) {
10811
+ resultNote.notations = [];
10812
+ }
10813
+ resultNote.notations.push({
10814
+ type: "technical",
10815
+ technical: "string",
10816
+ string: stringNumber,
10817
+ placement
10818
+ });
10819
+ return success(result);
10820
+ }
10821
+ function removeStringNumber(score, options) {
10822
+ const { partIndex, measureIndex, noteIndex } = options;
10823
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10824
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10825
+ }
10826
+ const part = score.parts[partIndex];
10827
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10828
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10829
+ }
10830
+ const measure = part.measures[measureIndex];
10831
+ let noteCount = 0;
10832
+ let targetEntryIndex = -1;
10833
+ for (let i = 0; i < measure.entries.length; i++) {
10834
+ const entry = measure.entries[i];
10835
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10836
+ if (noteCount === noteIndex) {
10837
+ targetEntryIndex = i;
10838
+ break;
10839
+ }
10840
+ noteCount++;
10841
+ }
10842
+ }
10843
+ if (targetEntryIndex < 0) {
10844
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10845
+ }
10846
+ const targetEntry = measure.entries[targetEntryIndex];
10847
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10848
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10849
+ }
10850
+ const stringIndex = targetEntry.notations.findIndex(
10851
+ (n) => n.type === "technical" && n.technical === "string"
10852
+ );
10853
+ if (stringIndex < 0) {
10854
+ return failure([operationError("NOTE_NOT_FOUND", `No string number found on note`, { partIndex, measureIndex })]);
10855
+ }
10856
+ const result = cloneScore(score);
10857
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10858
+ resultNote.notations.splice(stringIndex, 1);
10859
+ if (resultNote.notations.length === 0) {
10860
+ delete resultNote.notations;
10861
+ }
10862
+ return success(result);
10863
+ }
10864
+ function addOctaveShift(score, options) {
10865
+ const { partIndex, measureIndex, position, shiftType, size = 8 } = options;
10866
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10867
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10868
+ }
10869
+ const part = score.parts[partIndex];
10870
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10871
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10872
+ }
10873
+ const result = cloneScore(score);
10874
+ const measure = result.parts[partIndex].measures[measureIndex];
10875
+ const direction = {
10876
+ _id: generateId(),
10877
+ type: "direction",
10878
+ directionTypes: [{
10879
+ kind: "octave-shift",
10880
+ type: shiftType,
10881
+ size
10882
+ }],
10883
+ placement: shiftType === "down" ? "above" : "below"
10884
+ };
10885
+ insertDirectionAtPosition(measure, direction, position);
10886
+ return success(result);
10887
+ }
10888
+ function stopOctaveShift(score, options) {
10889
+ const { partIndex, measureIndex, position, size = 8 } = options;
10890
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10891
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10892
+ }
10893
+ const part = score.parts[partIndex];
10894
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10895
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10896
+ }
10897
+ const result = cloneScore(score);
10898
+ const measure = result.parts[partIndex].measures[measureIndex];
10899
+ const direction = {
10900
+ _id: generateId(),
10901
+ type: "direction",
10902
+ directionTypes: [{
10903
+ kind: "octave-shift",
10904
+ type: "stop",
10905
+ size
10906
+ }]
10907
+ };
10908
+ insertDirectionAtPosition(measure, direction, position);
10909
+ return success(result);
10910
+ }
10911
+ function removeOctaveShift(score, options) {
10912
+ const { partIndex, measureIndex, octaveShiftIndex = 0 } = options;
10913
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10914
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10915
+ }
10916
+ const part = score.parts[partIndex];
10917
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10918
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10919
+ }
10920
+ const measure = part.measures[measureIndex];
10921
+ let shiftCount = 0;
10922
+ let targetEntryIndex = -1;
10923
+ for (let i = 0; i < measure.entries.length; i++) {
10924
+ const entry = measure.entries[i];
10925
+ if (entry.type === "direction") {
10926
+ const hasOctaveShift = entry.directionTypes.some((dt) => dt.kind === "octave-shift");
10927
+ if (hasOctaveShift) {
10928
+ if (shiftCount === octaveShiftIndex) {
10929
+ targetEntryIndex = i;
10930
+ break;
10931
+ }
10932
+ shiftCount++;
10933
+ }
10934
+ }
10935
+ }
10936
+ if (targetEntryIndex < 0) {
10937
+ return failure([operationError("NOTE_NOT_FOUND", `Octave shift at index ${octaveShiftIndex} not found`, { partIndex, measureIndex })]);
10938
+ }
10939
+ const result = cloneScore(score);
10940
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
10941
+ return success(result);
10942
+ }
10943
+ function addBreathMark(score, options) {
10944
+ const { partIndex, measureIndex, noteIndex, placement = "above" } = options;
10945
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10946
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10947
+ }
10948
+ const part = score.parts[partIndex];
10949
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10950
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10951
+ }
10952
+ const measure = part.measures[measureIndex];
10953
+ let noteCount = 0;
10954
+ let targetEntryIndex = -1;
10955
+ for (let i = 0; i < measure.entries.length; i++) {
10956
+ const entry = measure.entries[i];
10957
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10958
+ if (noteCount === noteIndex) {
10959
+ targetEntryIndex = i;
10960
+ break;
10961
+ }
10962
+ noteCount++;
10963
+ }
10964
+ }
10965
+ if (targetEntryIndex < 0) {
10966
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10967
+ }
10968
+ const result = cloneScore(score);
10969
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10970
+ if (!resultNote.notations) {
10971
+ resultNote.notations = [];
10972
+ }
10973
+ const existingBreathMark = resultNote.notations.find(
10974
+ (n) => n.type === "articulation" && n.articulation === "breath-mark"
10975
+ );
10976
+ if (existingBreathMark) {
10977
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Breath mark already exists on note`, { partIndex, measureIndex })]);
10978
+ }
10979
+ resultNote.notations.push({
10980
+ type: "articulation",
10981
+ articulation: "breath-mark",
10982
+ placement
10983
+ });
10984
+ return success(result);
10985
+ }
10986
+ function removeBreathMark(score, options) {
10987
+ const { partIndex, measureIndex, noteIndex } = options;
10988
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10989
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10990
+ }
10991
+ const part = score.parts[partIndex];
10992
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10993
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10994
+ }
10995
+ const measure = part.measures[measureIndex];
10996
+ let noteCount = 0;
10997
+ let targetEntryIndex = -1;
10998
+ for (let i = 0; i < measure.entries.length; i++) {
10999
+ const entry = measure.entries[i];
11000
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
11001
+ if (noteCount === noteIndex) {
11002
+ targetEntryIndex = i;
11003
+ break;
11004
+ }
11005
+ noteCount++;
11006
+ }
11007
+ }
11008
+ if (targetEntryIndex < 0) {
11009
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
11010
+ }
11011
+ const targetEntry = measure.entries[targetEntryIndex];
11012
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
11013
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
11014
+ }
11015
+ const breathMarkIndex = targetEntry.notations.findIndex(
11016
+ (n) => n.type === "articulation" && n.articulation === "breath-mark"
11017
+ );
11018
+ if (breathMarkIndex < 0) {
11019
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No breath mark found on note`, { partIndex, measureIndex })]);
11020
+ }
11021
+ const result = cloneScore(score);
11022
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
11023
+ resultNote.notations.splice(breathMarkIndex, 1);
11024
+ if (resultNote.notations.length === 0) {
11025
+ delete resultNote.notations;
11026
+ }
11027
+ return success(result);
11028
+ }
11029
+ function addCaesura(score, options) {
11030
+ const { partIndex, measureIndex, noteIndex, placement = "above" } = options;
11031
+ if (partIndex < 0 || partIndex >= score.parts.length) {
11032
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
11033
+ }
11034
+ const part = score.parts[partIndex];
11035
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
11036
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
11037
+ }
11038
+ const measure = part.measures[measureIndex];
11039
+ let noteCount = 0;
11040
+ let targetEntryIndex = -1;
11041
+ for (let i = 0; i < measure.entries.length; i++) {
11042
+ const entry = measure.entries[i];
11043
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
11044
+ if (noteCount === noteIndex) {
11045
+ targetEntryIndex = i;
11046
+ break;
11047
+ }
11048
+ noteCount++;
11049
+ }
11050
+ }
11051
+ if (targetEntryIndex < 0) {
11052
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
11053
+ }
11054
+ const result = cloneScore(score);
11055
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
11056
+ if (!resultNote.notations) {
11057
+ resultNote.notations = [];
11058
+ }
11059
+ const existingCaesura = resultNote.notations.find(
11060
+ (n) => n.type === "articulation" && n.articulation === "caesura"
11061
+ );
11062
+ if (existingCaesura) {
11063
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Caesura already exists on note`, { partIndex, measureIndex })]);
11064
+ }
11065
+ resultNote.notations.push({
11066
+ type: "articulation",
11067
+ articulation: "caesura",
11068
+ placement
11069
+ });
11070
+ return success(result);
11071
+ }
11072
+ function removeCaesura(score, options) {
11073
+ const { partIndex, measureIndex, noteIndex } = options;
11074
+ if (partIndex < 0 || partIndex >= score.parts.length) {
11075
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
11076
+ }
11077
+ const part = score.parts[partIndex];
11078
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
11079
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
11080
+ }
11081
+ const measure = part.measures[measureIndex];
11082
+ let noteCount = 0;
11083
+ let targetEntryIndex = -1;
11084
+ for (let i = 0; i < measure.entries.length; i++) {
11085
+ const entry = measure.entries[i];
11086
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
11087
+ if (noteCount === noteIndex) {
11088
+ targetEntryIndex = i;
11089
+ break;
11090
+ }
11091
+ noteCount++;
11092
+ }
11093
+ }
11094
+ if (targetEntryIndex < 0) {
11095
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
11096
+ }
11097
+ const targetEntry = measure.entries[targetEntryIndex];
11098
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
11099
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
11100
+ }
11101
+ const caesuraIndex = targetEntry.notations.findIndex(
11102
+ (n) => n.type === "articulation" && n.articulation === "caesura"
11103
+ );
11104
+ if (caesuraIndex < 0) {
11105
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No caesura found on note`, { partIndex, measureIndex })]);
11106
+ }
11107
+ const result = cloneScore(score);
11108
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
11109
+ resultNote.notations.splice(caesuraIndex, 1);
11110
+ if (resultNote.notations.length === 0) {
11111
+ delete resultNote.notations;
11112
+ }
11113
+ return success(result);
11114
+ }
11115
+ var addText = addTextDirection;
11116
+ var setBeaming = autoBeam;
11117
+ var addChordSymbol = addHarmony;
11118
+ var removeChordSymbol = removeHarmony;
11119
+ var updateChordSymbol = updateHarmony;
11120
+ var changeClef = insertClefChange;
11121
+ var setBarline = changeBarline;
11122
+ var addRepeat = addRepeatBarline;
11123
+ var removeRepeat = removeRepeatBarline;
7715
11124
 
7716
11125
  // src/file.ts
7717
11126
  var import_promises = require("fs/promises");
@@ -7844,18 +11253,66 @@ function getPartNameMap(score) {
7844
11253
  STEPS,
7845
11254
  STEP_SEMITONES,
7846
11255
  ValidationException,
11256
+ addArticulation,
11257
+ addBeam,
11258
+ addBowing,
11259
+ addBreathMark,
11260
+ addCaesura,
11261
+ addChord,
7847
11262
  addChordNote,
11263
+ addChordNoteChecked,
11264
+ addChordSymbol,
11265
+ addCoda,
11266
+ addDaCapo,
11267
+ addDalSegno,
11268
+ addDynamics,
11269
+ addEnding,
11270
+ addFermata,
11271
+ addFine,
11272
+ addFingering,
11273
+ addGraceNote,
11274
+ addHarmony,
11275
+ addLyric,
7848
11276
  addNote,
11277
+ addNoteChecked,
11278
+ addOctaveShift,
11279
+ addOrnament,
11280
+ addPart,
11281
+ addPedal,
11282
+ addRehearsalMark,
11283
+ addRepeat,
11284
+ addRepeatBarline,
11285
+ addSegno,
11286
+ addSlur,
11287
+ addStringNumber,
11288
+ addTempo,
11289
+ addText,
11290
+ addTextDirection,
11291
+ addTie,
11292
+ addToCoda,
11293
+ addVoice,
11294
+ addWedge,
7849
11295
  assertMeasureValid,
7850
11296
  assertValid,
11297
+ autoBeam,
7851
11298
  buildVoiceToStaffMap,
7852
11299
  buildVoiceToStaffMapForPart,
11300
+ changeBarline,
11301
+ changeClef,
7853
11302
  changeKey,
11303
+ changeNoteDuration,
7854
11304
  changeTime,
11305
+ convertToGrace,
11306
+ copyNotes,
11307
+ copyNotesMultiMeasure,
7855
11308
  countNotes,
11309
+ createTuplet,
11310
+ cutNotes,
7856
11311
  decodeBuffer,
7857
11312
  deleteMeasure,
7858
11313
  deleteNote,
11314
+ deleteNoteChecked,
11315
+ duplicatePart,
7859
11316
  exportMidi,
7860
11317
  findBarlines,
7861
11318
  findDirectionsByType,
@@ -7949,7 +11406,9 @@ function getPartNameMap(score) {
7949
11406
  hasTieStop,
7950
11407
  hasTuplet,
7951
11408
  inferStaff,
11409
+ insertClefChange,
7952
11410
  insertMeasure,
11411
+ insertNote,
7953
11412
  isChordNote,
7954
11413
  isCompressed,
7955
11414
  isCueNote,
@@ -7962,19 +11421,65 @@ function getPartNameMap(score) {
7962
11421
  isValid,
7963
11422
  iterateEntries,
7964
11423
  iterateNotes,
11424
+ lowerAccidental,
7965
11425
  measureRoundtrip,
11426
+ modifyDynamics,
7966
11427
  modifyNoteDuration,
11428
+ modifyNoteDurationChecked,
7967
11429
  modifyNotePitch,
11430
+ modifyNotePitchChecked,
11431
+ modifyTempo,
11432
+ moveNoteToStaff,
7968
11433
  parse,
7969
11434
  parseAuto,
7970
11435
  parseCompressed,
7971
11436
  parseFile,
11437
+ pasteNotes,
11438
+ pasteNotesMultiMeasure,
7972
11439
  pitchToSemitone,
11440
+ raiseAccidental,
11441
+ removeArticulation,
11442
+ removeBeam,
11443
+ removeBowing,
11444
+ removeBreathMark,
11445
+ removeCaesura,
11446
+ removeChordSymbol,
11447
+ removeDynamics,
11448
+ removeEnding,
11449
+ removeFermata,
11450
+ removeFingering,
11451
+ removeGraceNote,
11452
+ removeHarmony,
11453
+ removeLyric,
11454
+ removeNote,
11455
+ removeOctaveShift,
11456
+ removeOrnament,
11457
+ removePart,
11458
+ removePedal,
11459
+ removeRepeat,
11460
+ removeRepeatBarline,
11461
+ removeSlur,
11462
+ removeStringNumber,
11463
+ removeTempo,
11464
+ removeTie,
11465
+ removeTuplet,
11466
+ removeWedge,
7973
11467
  scoresEqual,
7974
11468
  serialize,
7975
11469
  serializeCompressed,
7976
11470
  serializeToFile,
11471
+ setBarline,
11472
+ setBeaming,
11473
+ setNotePitch,
11474
+ setNotePitchBySemitone,
11475
+ setStaves,
11476
+ shiftNotePitch,
11477
+ stopOctaveShift,
7977
11478
  transpose,
11479
+ transposeChecked,
11480
+ updateChordSymbol,
11481
+ updateHarmony,
11482
+ updateLyric,
7978
11483
  validate,
7979
11484
  validateBackupForward,
7980
11485
  validateBeams,