musicxml-io 0.2.0 → 0.2.7

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.
@@ -20,36 +20,110 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/operations/index.ts
21
21
  var operations_exports = {};
22
22
  __export(operations_exports, {
23
+ addArticulation: () => addArticulation,
24
+ addBeam: () => addBeam,
25
+ addBowing: () => addBowing,
26
+ addBreathMark: () => addBreathMark,
27
+ addCaesura: () => addCaesura,
23
28
  addChord: () => addChord,
24
29
  addChordNote: () => addChordNote,
25
30
  addChordNoteChecked: () => addChordNoteChecked,
31
+ addCoda: () => addCoda,
32
+ addDaCapo: () => addDaCapo,
33
+ addDalSegno: () => addDalSegno,
34
+ addDynamics: () => addDynamics,
35
+ addEnding: () => addEnding,
36
+ addFermata: () => addFermata,
37
+ addFine: () => addFine,
38
+ addFingering: () => addFingering,
39
+ addGraceNote: () => addGraceNote,
40
+ addHarmony: () => addHarmony,
41
+ addLyric: () => addLyric,
26
42
  addNote: () => addNote,
27
43
  addNoteChecked: () => addNoteChecked,
44
+ addOctaveShift: () => addOctaveShift,
45
+ addOrnament: () => addOrnament,
28
46
  addPart: () => addPart,
47
+ addPedal: () => addPedal,
48
+ addRehearsalMark: () => addRehearsalMark,
49
+ addRepeatBarline: () => addRepeatBarline,
50
+ addSegno: () => addSegno,
51
+ addSlur: () => addSlur,
52
+ addStringNumber: () => addStringNumber,
53
+ addTempo: () => addTempo,
54
+ addTextDirection: () => addTextDirection,
55
+ addTie: () => addTie,
56
+ addToCoda: () => addToCoda,
29
57
  addVoice: () => addVoice,
58
+ addWedge: () => addWedge,
59
+ autoBeam: () => autoBeam,
60
+ changeBarline: () => changeBarline,
30
61
  changeKey: () => changeKey,
31
62
  changeNoteDuration: () => changeNoteDuration,
32
63
  changeTime: () => changeTime,
64
+ convertToGrace: () => convertToGrace,
65
+ copyNotes: () => copyNotes,
66
+ copyNotesMultiMeasure: () => copyNotesMultiMeasure,
67
+ createTuplet: () => createTuplet,
68
+ cutNotes: () => cutNotes,
33
69
  deleteMeasure: () => deleteMeasure,
34
70
  deleteNote: () => deleteNote,
35
71
  deleteNoteChecked: () => deleteNoteChecked,
36
72
  duplicatePart: () => duplicatePart,
73
+ insertClefChange: () => insertClefChange,
37
74
  insertMeasure: () => insertMeasure,
38
75
  insertNote: () => insertNote,
76
+ lowerAccidental: () => lowerAccidental,
39
77
  modifyNoteDuration: () => modifyNoteDuration,
40
78
  modifyNoteDurationChecked: () => modifyNoteDurationChecked,
41
79
  modifyNotePitch: () => modifyNotePitch,
42
80
  modifyNotePitchChecked: () => modifyNotePitchChecked,
43
81
  moveNoteToStaff: () => moveNoteToStaff,
82
+ pasteNotes: () => pasteNotes,
83
+ pasteNotesMultiMeasure: () => pasteNotesMultiMeasure,
84
+ raiseAccidental: () => raiseAccidental,
85
+ removeArticulation: () => removeArticulation,
86
+ removeBeam: () => removeBeam,
87
+ removeBowing: () => removeBowing,
88
+ removeBreathMark: () => removeBreathMark,
89
+ removeCaesura: () => removeCaesura,
90
+ removeDynamics: () => removeDynamics,
91
+ removeEnding: () => removeEnding,
92
+ removeFermata: () => removeFermata,
93
+ removeFingering: () => removeFingering,
94
+ removeGraceNote: () => removeGraceNote,
95
+ removeHarmony: () => removeHarmony,
96
+ removeLyric: () => removeLyric,
44
97
  removeNote: () => removeNote,
98
+ removeOctaveShift: () => removeOctaveShift,
99
+ removeOrnament: () => removeOrnament,
45
100
  removePart: () => removePart,
101
+ removePedal: () => removePedal,
102
+ removeRepeatBarline: () => removeRepeatBarline,
103
+ removeSlur: () => removeSlur,
104
+ removeStringNumber: () => removeStringNumber,
105
+ removeTempo: () => removeTempo,
106
+ removeTie: () => removeTie,
107
+ removeTuplet: () => removeTuplet,
108
+ removeWedge: () => removeWedge,
46
109
  setNotePitch: () => setNotePitch,
110
+ setNotePitchBySemitone: () => setNotePitchBySemitone,
47
111
  setStaves: () => setStaves,
112
+ shiftNotePitch: () => shiftNotePitch,
113
+ stopOctaveShift: () => stopOctaveShift,
48
114
  transpose: () => transpose,
49
- transposeChecked: () => transposeChecked
115
+ transposeChecked: () => transposeChecked,
116
+ updateHarmony: () => updateHarmony,
117
+ updateLyric: () => updateLyric
50
118
  });
51
119
  module.exports = __toCommonJS(operations_exports);
52
120
 
121
+ // src/id.ts
122
+ var import_nanoid = require("nanoid");
123
+ function generateId() {
124
+ return "i" + (0, import_nanoid.nanoid)(10);
125
+ }
126
+
53
127
  // src/utils/index.ts
54
128
  var STEPS = ["C", "D", "E", "F", "G", "A", "B"];
55
129
  var STEP_SEMITONES = {
@@ -61,6 +135,128 @@ var STEP_SEMITONES = {
61
135
  "A": 9,
62
136
  "B": 11
63
137
  };
138
+ var SHARP_ORDER = ["F", "C", "G", "D", "A", "E", "B"];
139
+ var FLAT_ORDER = ["B", "E", "A", "D", "G", "C", "F"];
140
+ function pitchToSemitone(pitch) {
141
+ return pitch.octave * 12 + STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0);
142
+ }
143
+ function getAlterForStepInKey(step, key) {
144
+ const fifths = key.fifths;
145
+ if (fifths > 0) {
146
+ const sharps = SHARP_ORDER.slice(0, fifths);
147
+ return sharps.includes(step) ? 1 : 0;
148
+ } else if (fifths < 0) {
149
+ const flats = FLAT_ORDER.slice(0, -fifths);
150
+ return flats.includes(step) ? -1 : 0;
151
+ }
152
+ return 0;
153
+ }
154
+ function getAlteredStepsInKey(key) {
155
+ const alterations = /* @__PURE__ */ new Map();
156
+ const fifths = key.fifths;
157
+ if (fifths > 0) {
158
+ SHARP_ORDER.slice(0, fifths).forEach((step) => alterations.set(step, 1));
159
+ } else if (fifths < 0) {
160
+ FLAT_ORDER.slice(0, -fifths).forEach((step) => alterations.set(step, -1));
161
+ }
162
+ return alterations;
163
+ }
164
+ function getAccidentalsInMeasure(measure, upToPosition, voice) {
165
+ const accidentals = /* @__PURE__ */ new Map();
166
+ let position = 0;
167
+ for (const entry of measure.entries) {
168
+ if (position >= upToPosition) break;
169
+ if (entry.type === "note") {
170
+ if (voice === void 0 || entry.voice === voice) {
171
+ if (entry.pitch && entry.accidental) {
172
+ const key = `${entry.pitch.step}${entry.pitch.octave}`;
173
+ accidentals.set(key, entry.pitch.alter ?? 0);
174
+ }
175
+ }
176
+ if (!entry.chord) {
177
+ position += entry.duration;
178
+ }
179
+ } else if (entry.type === "backup") {
180
+ position -= entry.duration;
181
+ } else if (entry.type === "forward") {
182
+ position += entry.duration;
183
+ }
184
+ }
185
+ return accidentals;
186
+ }
187
+ function semitoneToKeyAwarePitch(semitone, key, options) {
188
+ const octave = Math.floor(semitone / 12);
189
+ const pitchClass = (semitone % 12 + 12) % 12;
190
+ const keyPreferSharp = key.fifths >= 0;
191
+ const preferSharp = options?.preferSharp ?? keyPreferSharp;
192
+ for (const step of STEPS) {
193
+ const stepSemitone = STEP_SEMITONES[step];
194
+ if (stepSemitone === pitchClass) {
195
+ return { step, octave };
196
+ }
197
+ }
198
+ const keyAlterations = getAlteredStepsInKey(key);
199
+ for (const step of STEPS) {
200
+ const stepSemitone = STEP_SEMITONES[step];
201
+ const keyAlter = keyAlterations.get(step) ?? 0;
202
+ if ((stepSemitone + keyAlter) % 12 === pitchClass) {
203
+ return { step, octave, alter: keyAlter };
204
+ }
205
+ }
206
+ if (preferSharp) {
207
+ for (const step of STEPS) {
208
+ const stepSemitone = STEP_SEMITONES[step];
209
+ const diff = (pitchClass - stepSemitone + 12) % 12;
210
+ if (diff === 1) {
211
+ return { step, octave, alter: 1 };
212
+ }
213
+ }
214
+ for (const step of STEPS) {
215
+ const stepSemitone = STEP_SEMITONES[step];
216
+ const diff = (pitchClass - stepSemitone + 12) % 12;
217
+ if (diff === 2) {
218
+ return { step, octave, alter: 2 };
219
+ }
220
+ }
221
+ } else {
222
+ for (const step of STEPS) {
223
+ const stepSemitone = STEP_SEMITONES[step];
224
+ const diff = (stepSemitone - pitchClass + 12) % 12;
225
+ if (diff === 1) {
226
+ return { step, octave, alter: -1 };
227
+ }
228
+ }
229
+ for (const step of STEPS) {
230
+ const stepSemitone = STEP_SEMITONES[step];
231
+ const diff = (stepSemitone - pitchClass + 12) % 12;
232
+ if (diff === 2) {
233
+ return { step, octave, alter: -2 };
234
+ }
235
+ }
236
+ }
237
+ return { step: "C", octave, alter: pitchClass };
238
+ }
239
+ function determineAccidental(pitch, key, accidentalsInMeasure) {
240
+ const noteKey = `${pitch.step}${pitch.octave}`;
241
+ const alter = pitch.alter ?? 0;
242
+ const keyAlter = getAlterForStepInKey(pitch.step, key);
243
+ const previousAlter = accidentalsInMeasure.get(noteKey);
244
+ if (previousAlter !== void 0) {
245
+ if (alter === previousAlter) {
246
+ return void 0;
247
+ }
248
+ } else {
249
+ if (alter === keyAlter) {
250
+ return void 0;
251
+ }
252
+ }
253
+ if (alter === 0) return "natural";
254
+ if (alter === 1) return "sharp";
255
+ if (alter === -1) return "flat";
256
+ if (alter === 2) return "double-sharp";
257
+ if (alter === -2) return "double-flat";
258
+ return void 0;
259
+ }
64
260
  function createPositionState() {
65
261
  return { position: 0, lastNonChordPosition: 0 };
66
262
  }
@@ -87,6 +283,14 @@ function updatePositionForEntry(state, entry) {
87
283
  return pos;
88
284
  }
89
285
  }
286
+ function getAbsolutePositionForNote(note, measure) {
287
+ const state = createPositionState();
288
+ for (const entry of measure.entries) {
289
+ if (entry === note) return entry.chord ? state.lastNonChordPosition : state.position;
290
+ updatePositionForEntry(state, entry);
291
+ }
292
+ return state.position;
293
+ }
90
294
  function getMeasureEndPosition(measure) {
91
295
  const state = createPositionState();
92
296
  for (const entry of measure.entries) updatePositionForEntry(state, entry);
@@ -956,6 +1160,27 @@ function getMeasureContext(score, partIndex, measureIndex) {
956
1160
  };
957
1161
  }
958
1162
 
1163
+ // src/query/index.ts
1164
+ function getAttributesAtMeasure(score, options) {
1165
+ const part = score.parts[options.part];
1166
+ if (!part) return {};
1167
+ const targetMeasure = parseInt(String(options.measure), 10);
1168
+ const result = {};
1169
+ for (const m of part.measures) {
1170
+ const mNum = parseInt(m.number, 10);
1171
+ if (!isNaN(targetMeasure) && !isNaN(mNum) && mNum > targetMeasure) break;
1172
+ if (m.attributes) {
1173
+ if (m.attributes.divisions !== void 0) result.divisions = m.attributes.divisions;
1174
+ if (m.attributes.time !== void 0) result.time = m.attributes.time;
1175
+ if (m.attributes.key !== void 0) result.key = m.attributes.key;
1176
+ if (m.attributes.clef !== void 0) result.clef = m.attributes.clef;
1177
+ if (m.attributes.staves !== void 0) result.staves = m.attributes.staves;
1178
+ if (m.attributes.transpose !== void 0) result.transpose = m.attributes.transpose;
1179
+ }
1180
+ }
1181
+ return result;
1182
+ }
1183
+
959
1184
  // src/operations/index.ts
960
1185
  function success(data, warnings) {
961
1186
  return { success: true, data, warnings };
@@ -975,6 +1200,34 @@ function operationError(code, message, location = {}, details) {
975
1200
  function cloneScore(score) {
976
1201
  return JSON.parse(JSON.stringify(score));
977
1202
  }
1203
+ function cloneNoteWithNewId(note) {
1204
+ const cloned = JSON.parse(JSON.stringify(note));
1205
+ cloned._id = generateId();
1206
+ return cloned;
1207
+ }
1208
+ function cloneEntryWithNewId(entry) {
1209
+ const cloned = JSON.parse(JSON.stringify(entry));
1210
+ cloned._id = generateId();
1211
+ return cloned;
1212
+ }
1213
+ function cloneMeasureWithNewIds(measure) {
1214
+ const cloned = JSON.parse(JSON.stringify(measure));
1215
+ cloned._id = generateId();
1216
+ cloned.entries = cloned.entries.map((entry) => cloneEntryWithNewId(entry));
1217
+ if (cloned.barlines) {
1218
+ cloned.barlines = cloned.barlines.map((barline) => ({
1219
+ ...barline,
1220
+ _id: generateId()
1221
+ }));
1222
+ }
1223
+ return cloned;
1224
+ }
1225
+ function clonePartWithNewIds(part) {
1226
+ const cloned = JSON.parse(JSON.stringify(part));
1227
+ cloned._id = generateId();
1228
+ cloned.measures = cloned.measures.map((measure) => cloneMeasureWithNewIds(measure));
1229
+ return cloned;
1230
+ }
978
1231
  function getMeasureDuration(divisions, time) {
979
1232
  const beats = parseInt(time.beats, 10);
980
1233
  if (isNaN(beats)) return divisions * 4;
@@ -1037,6 +1290,7 @@ function hasNotesInRange(voiceEntries, startPos, endPos) {
1037
1290
  }
1038
1291
  function createRest(duration, voice, staff) {
1039
1292
  return {
1293
+ _id: generateId(),
1040
1294
  type: "note",
1041
1295
  rest: { displayStep: void 0, displayOctave: void 0 },
1042
1296
  duration,
@@ -1098,10 +1352,11 @@ function rebuildMeasureWithVoice(measure, voice, newEntries, measureDuration, st
1098
1352
  for (const { position: targetPos, entry } of allEntries) {
1099
1353
  const diff = targetPos - currentPosition;
1100
1354
  if (diff < 0) {
1101
- result.push({ type: "backup", duration: -diff });
1355
+ result.push({ _id: generateId(), type: "backup", duration: -diff });
1102
1356
  currentPosition = targetPos;
1103
1357
  } else if (diff > 0) {
1104
1358
  result.push({
1359
+ _id: generateId(),
1105
1360
  type: "forward",
1106
1361
  duration: diff,
1107
1362
  voice: entry.type === "note" ? entry.voice : 1,
@@ -1156,6 +1411,7 @@ function insertNote(score, options) {
1156
1411
  )]);
1157
1412
  }
1158
1413
  const newNote = {
1414
+ _id: generateId(),
1159
1415
  type: "note",
1160
1416
  pitch: options.pitch,
1161
1417
  duration: options.duration,
@@ -1262,6 +1518,7 @@ function addChord(score, options) {
1262
1518
  return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1263
1519
  }
1264
1520
  const chordNote = {
1521
+ _id: generateId(),
1265
1522
  type: "note",
1266
1523
  pitch: options.pitch,
1267
1524
  duration: targetEntry.duration,
@@ -1412,6 +1669,161 @@ function setNotePitch(score, options) {
1412
1669
  }
1413
1670
  return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1414
1671
  }
1672
+ function setNotePitchBySemitone(score, options) {
1673
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1674
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1675
+ }
1676
+ const part = score.parts[options.partIndex];
1677
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1678
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1679
+ }
1680
+ const result = cloneScore(score);
1681
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
1682
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
1683
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
1684
+ const keySignature = attrs.key ?? { fifths: 0 };
1685
+ let noteCount = 0;
1686
+ for (const entry of measure.entries) {
1687
+ if (entry.type === "note" && !entry.rest) {
1688
+ if (noteCount === options.noteIndex) {
1689
+ const notePosition = getAbsolutePositionForNote(entry, measure);
1690
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
1691
+ const newPitch = semitoneToKeyAwarePitch(options.semitone, keySignature, {
1692
+ preferSharp: options.preferSharp
1693
+ });
1694
+ const accidental = determineAccidental(newPitch, keySignature, accidentalsInMeasure);
1695
+ entry.pitch = newPitch;
1696
+ if (accidental) {
1697
+ entry.accidental = { value: accidental };
1698
+ } else {
1699
+ delete entry.accidental;
1700
+ }
1701
+ return success(result);
1702
+ }
1703
+ noteCount++;
1704
+ }
1705
+ }
1706
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1707
+ }
1708
+ function shiftNotePitch(score, options) {
1709
+ if (options.semitones === 0) {
1710
+ return success(score);
1711
+ }
1712
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1713
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1714
+ }
1715
+ const part = score.parts[options.partIndex];
1716
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1717
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1718
+ }
1719
+ const measure = part.measures[options.measureIndex];
1720
+ let noteCount = 0;
1721
+ let currentSemitone = null;
1722
+ for (const entry of measure.entries) {
1723
+ if (entry.type === "note" && !entry.rest) {
1724
+ if (noteCount === options.noteIndex) {
1725
+ if (!entry.pitch) {
1726
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1727
+ }
1728
+ currentSemitone = pitchToSemitone(entry.pitch);
1729
+ break;
1730
+ }
1731
+ noteCount++;
1732
+ }
1733
+ }
1734
+ if (currentSemitone === null) {
1735
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1736
+ }
1737
+ return setNotePitchBySemitone(score, {
1738
+ partIndex: options.partIndex,
1739
+ measureIndex: options.measureIndex,
1740
+ noteIndex: options.noteIndex,
1741
+ semitone: currentSemitone + options.semitones,
1742
+ preferSharp: options.preferSharp
1743
+ });
1744
+ }
1745
+ function raiseAccidental(score, options) {
1746
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1747
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1748
+ }
1749
+ const part = score.parts[options.partIndex];
1750
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1751
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1752
+ }
1753
+ const result = cloneScore(score);
1754
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
1755
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
1756
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
1757
+ const keySignature = attrs.key ?? { fifths: 0 };
1758
+ let noteCount = 0;
1759
+ for (const entry of measure.entries) {
1760
+ if (entry.type === "note" && !entry.rest) {
1761
+ if (noteCount === options.noteIndex) {
1762
+ if (!entry.pitch) {
1763
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1764
+ }
1765
+ const currentAlter = entry.pitch.alter ?? 0;
1766
+ const newAlter = currentAlter + 1;
1767
+ if (newAlter > 2) {
1768
+ return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot raise accidental beyond double-sharp (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1769
+ }
1770
+ entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
1771
+ const notePosition = getAbsolutePositionForNote(entry, measure);
1772
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
1773
+ const accidental = determineAccidental(entry.pitch, keySignature, accidentalsInMeasure);
1774
+ if (accidental) {
1775
+ entry.accidental = { value: accidental };
1776
+ } else {
1777
+ delete entry.accidental;
1778
+ }
1779
+ return success(result);
1780
+ }
1781
+ noteCount++;
1782
+ }
1783
+ }
1784
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1785
+ }
1786
+ function lowerAccidental(score, options) {
1787
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1788
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1789
+ }
1790
+ const part = score.parts[options.partIndex];
1791
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1792
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1793
+ }
1794
+ const result = cloneScore(score);
1795
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
1796
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
1797
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
1798
+ const keySignature = attrs.key ?? { fifths: 0 };
1799
+ let noteCount = 0;
1800
+ for (const entry of measure.entries) {
1801
+ if (entry.type === "note" && !entry.rest) {
1802
+ if (noteCount === options.noteIndex) {
1803
+ if (!entry.pitch) {
1804
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1805
+ }
1806
+ const currentAlter = entry.pitch.alter ?? 0;
1807
+ const newAlter = currentAlter - 1;
1808
+ if (newAlter < -2) {
1809
+ return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot lower accidental beyond double-flat (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1810
+ }
1811
+ entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
1812
+ const notePosition = getAbsolutePositionForNote(entry, measure);
1813
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
1814
+ const accidental = determineAccidental(entry.pitch, keySignature, accidentalsInMeasure);
1815
+ if (accidental) {
1816
+ entry.accidental = { value: accidental };
1817
+ } else {
1818
+ delete entry.accidental;
1819
+ }
1820
+ return success(result);
1821
+ }
1822
+ noteCount++;
1823
+ }
1824
+ }
1825
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1826
+ }
1415
1827
  function addVoice(score, options) {
1416
1828
  if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1417
1829
  return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
@@ -1435,7 +1847,7 @@ function addVoice(score, options) {
1435
1847
  const rest = createRest(measureDuration, options.voice, options.staff);
1436
1848
  const currentEnd = getMeasureEndPosition(measure);
1437
1849
  if (currentEnd > 0) {
1438
- measure.entries.push({ type: "backup", duration: currentEnd });
1850
+ measure.entries.push({ _id: generateId(), type: "backup", duration: currentEnd });
1439
1851
  }
1440
1852
  measure.entries.push(rest);
1441
1853
  return success(result);
@@ -1486,6 +1898,7 @@ function addPart(score, options) {
1486
1898
  const result = cloneScore(score);
1487
1899
  const insertIndex = options.insertIndex ?? result.parts.length;
1488
1900
  const partInfo = {
1901
+ _id: generateId(),
1489
1902
  type: "score-part",
1490
1903
  id: options.id,
1491
1904
  name: options.name,
@@ -1504,10 +1917,10 @@ function addPart(score, options) {
1504
1917
  }
1505
1918
  result.partList.splice(partListInsertIndex, 0, partInfo);
1506
1919
  const measureCount = result.parts.length > 0 ? result.parts[0].measures.length : 1;
1507
- const newPart = { id: options.id, measures: [] };
1920
+ const newPart = { _id: generateId(), id: options.id, measures: [] };
1508
1921
  for (let i = 0; i < measureCount; i++) {
1509
1922
  const measureNumber = result.parts.length > 0 ? result.parts[0].measures[i]?.number ?? String(i + 1) : String(i + 1);
1510
- const measure = { number: measureNumber, entries: [] };
1923
+ const measure = { _id: generateId(), number: measureNumber, entries: [] };
1511
1924
  if (i === 0) {
1512
1925
  measure.attributes = {
1513
1926
  divisions: options.divisions ?? 4,
@@ -1551,10 +1964,11 @@ function duplicatePart(score, options) {
1551
1964
  }
1552
1965
  const result = cloneScore(score);
1553
1966
  const sourcePart = result.parts[sourceIndex];
1554
- const newPart = JSON.parse(JSON.stringify(sourcePart));
1967
+ const newPart = clonePartWithNewIds(sourcePart);
1555
1968
  newPart.id = options.newPartId;
1556
1969
  const sourcePartInfo = result.partList.find((e) => e.type === "score-part" && e.id === options.sourcePartId);
1557
1970
  const newPartInfo = {
1971
+ _id: generateId(),
1558
1972
  type: "score-part",
1559
1973
  id: options.newPartId,
1560
1974
  name: options.newPartName ?? sourcePartInfo?.name,
@@ -1668,7 +2082,7 @@ function insertMeasure(score, options) {
1668
2082
  if (insertIndex === -1) continue;
1669
2083
  const numericPart = parseInt(targetMeasure, 10);
1670
2084
  const newMeasureNumber = String(isNaN(numericPart) ? insertIndex + 2 : numericPart + 1);
1671
- const newMeasure = { number: newMeasureNumber, entries: [] };
2085
+ const newMeasure = { _id: generateId(), number: newMeasureNumber, entries: [] };
1672
2086
  if (options.copyAttributes && part.measures[insertIndex].attributes) {
1673
2087
  newMeasure.attributes = { ...part.measures[insertIndex].attributes };
1674
2088
  }
@@ -1698,85 +2112,2827 @@ function deleteMeasure(score, measureNumber) {
1698
2112
  }
1699
2113
  return result;
1700
2114
  }
1701
- var addNote = (score, options) => {
1702
- const result = insertNote(score, {
1703
- partIndex: options.partIndex,
1704
- measureIndex: options.measureIndex,
1705
- voice: options.voice,
1706
- staff: options.staff,
1707
- position: options.position,
1708
- pitch: options.note.pitch ?? { step: "C", octave: 4 },
1709
- duration: options.note.duration,
1710
- noteType: options.note.noteType,
1711
- dots: options.note.dots
2115
+ function findNoteByIndex(measure, noteIndex) {
2116
+ let noteCount = 0;
2117
+ for (let i = 0; i < measure.entries.length; i++) {
2118
+ const entry = measure.entries[i];
2119
+ if (entry.type === "note" && !entry.rest) {
2120
+ if (noteCount === noteIndex) {
2121
+ return { note: entry, entryIndex: i };
2122
+ }
2123
+ noteCount++;
2124
+ }
2125
+ }
2126
+ return null;
2127
+ }
2128
+ function pitchesEqual(p1, p2) {
2129
+ return p1.step === p2.step && p1.octave === p2.octave && (p1.alter ?? 0) === (p2.alter ?? 0);
2130
+ }
2131
+ function addTie(score, options) {
2132
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2133
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2134
+ }
2135
+ const part = score.parts[options.partIndex];
2136
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
2137
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
2138
+ }
2139
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
2140
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
2141
+ }
2142
+ const result = cloneScore(score);
2143
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
2144
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
2145
+ const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
2146
+ if (!startResult) {
2147
+ return failure([operationError("NOTE_NOT_FOUND", `Start note index ${options.startNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
2148
+ }
2149
+ const endResult = findNoteByIndex(endMeasure, options.endNoteIndex);
2150
+ if (!endResult) {
2151
+ return failure([operationError("NOTE_NOT_FOUND", `End note index ${options.endNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
2152
+ }
2153
+ const startNote = startResult.note;
2154
+ const endNote = endResult.note;
2155
+ if (!startNote.pitch || !endNote.pitch) {
2156
+ return failure([operationError("TIE_INVALID_TARGET", "Cannot tie notes without pitch", { partIndex: options.partIndex })]);
2157
+ }
2158
+ if (!pitchesEqual(startNote.pitch, endNote.pitch)) {
2159
+ return failure([operationError("TIE_PITCH_MISMATCH", "Tied notes must have the same pitch", { partIndex: options.partIndex }, { startPitch: startNote.pitch, endPitch: endNote.pitch })]);
2160
+ }
2161
+ if (startNote.tie?.type === "start" || startNote.tie?.type === "continue") {
2162
+ return failure([operationError("TIE_ALREADY_EXISTS", "Start note already has a tie start", { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
2163
+ }
2164
+ startNote.tie = { type: "start" };
2165
+ if (!startNote.notations) startNote.notations = [];
2166
+ startNote.notations.push({ type: "tied", tiedType: "start" });
2167
+ endNote.tie = { type: "stop" };
2168
+ if (!endNote.notations) endNote.notations = [];
2169
+ endNote.notations.push({ type: "tied", tiedType: "stop" });
2170
+ const validationResult = validate(result, { checkTies: true });
2171
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
2172
+ if (criticalErrors.length > 0) {
2173
+ return failure(criticalErrors);
2174
+ }
2175
+ return success(result, validationResult.warnings);
2176
+ }
2177
+ function removeTie(score, options) {
2178
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2179
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2180
+ }
2181
+ const part = score.parts[options.partIndex];
2182
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2183
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2184
+ }
2185
+ const result = cloneScore(score);
2186
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2187
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
2188
+ if (!noteResult) {
2189
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2190
+ }
2191
+ const note = noteResult.note;
2192
+ if (!note.tie) {
2193
+ return failure([operationError("TIE_NOT_FOUND", "Note does not have a tie", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2194
+ }
2195
+ delete note.tie;
2196
+ delete note.ties;
2197
+ if (note.notations) {
2198
+ note.notations = note.notations.filter((n) => n.type !== "tied");
2199
+ if (note.notations.length === 0) {
2200
+ delete note.notations;
2201
+ }
2202
+ }
2203
+ return success(result);
2204
+ }
2205
+ function addSlur(score, options) {
2206
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2207
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2208
+ }
2209
+ const part = score.parts[options.partIndex];
2210
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
2211
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
2212
+ }
2213
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
2214
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
2215
+ }
2216
+ const result = cloneScore(score);
2217
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
2218
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
2219
+ const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
2220
+ if (!startResult) {
2221
+ return failure([operationError("NOTE_NOT_FOUND", `Start note index ${options.startNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
2222
+ }
2223
+ const endResult = findNoteByIndex(endMeasure, options.endNoteIndex);
2224
+ if (!endResult) {
2225
+ return failure([operationError("NOTE_NOT_FOUND", `End note index ${options.endNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
2226
+ }
2227
+ const startNote = startResult.note;
2228
+ const endNote = endResult.note;
2229
+ const slurNumber = options.number ?? 1;
2230
+ if (startNote.notations?.some((n) => n.type === "slur" && n.slurType === "start" && (n.number ?? 1) === slurNumber)) {
2231
+ return failure([operationError("SLUR_ALREADY_EXISTS", `Slur ${slurNumber} already starts on this note`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
2232
+ }
2233
+ if (!startNote.notations) startNote.notations = [];
2234
+ startNote.notations.push({
2235
+ type: "slur",
2236
+ slurType: "start",
2237
+ number: slurNumber,
2238
+ placement: options.placement
1712
2239
  });
1713
- return result.success ? result.data : score;
1714
- };
1715
- var deleteNote = (score, options) => {
1716
- const result = removeNote(score, options);
1717
- return result.success ? result.data : score;
1718
- };
1719
- var addChordNote = (score, options) => {
1720
- const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
1721
- return result.success ? result.data : score;
1722
- };
1723
- var modifyNotePitch = (score, options) => {
1724
- const result = setNotePitch(score, options);
1725
- return result.success ? result.data : score;
1726
- };
1727
- var modifyNoteDuration = (score, options) => {
1728
- const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
1729
- return result.success ? result.data : score;
1730
- };
1731
- var addNoteChecked = (score, options) => {
1732
- return insertNote(score, {
1733
- partIndex: options.partIndex,
1734
- measureIndex: options.measureIndex,
1735
- voice: options.voice,
1736
- staff: options.staff,
1737
- position: options.position,
1738
- pitch: options.note.pitch ?? { step: "C", octave: 4 },
1739
- duration: options.note.duration,
1740
- noteType: options.note.noteType,
1741
- dots: options.note.dots
2240
+ if (!endNote.notations) endNote.notations = [];
2241
+ endNote.notations.push({
2242
+ type: "slur",
2243
+ slurType: "stop",
2244
+ number: slurNumber
1742
2245
  });
1743
- };
1744
- var deleteNoteChecked = removeNote;
1745
- var addChordNoteChecked = (score, options) => {
1746
- return addChord(score, { ...options, noteIndex: options.afterNoteIndex });
1747
- };
1748
- var modifyNotePitchChecked = setNotePitch;
1749
- var modifyNoteDurationChecked = (score, options) => {
2246
+ const validationResult = validate(result, { checkSlurs: true });
2247
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
2248
+ if (criticalErrors.length > 0) {
2249
+ return failure(criticalErrors);
2250
+ }
2251
+ return success(result, validationResult.warnings);
2252
+ }
2253
+ function removeSlur(score, options) {
2254
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2255
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2256
+ }
2257
+ const part = score.parts[options.partIndex];
2258
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2259
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2260
+ }
2261
+ const result = cloneScore(score);
2262
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2263
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
2264
+ if (!noteResult) {
2265
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2266
+ }
2267
+ const note = noteResult.note;
2268
+ const slurNumber = options.number ?? 1;
2269
+ if (!note.notations?.some((n) => n.type === "slur" && (n.number ?? 1) === slurNumber)) {
2270
+ return failure([operationError("SLUR_NOT_FOUND", `Slur ${slurNumber} not found on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2271
+ }
2272
+ note.notations = note.notations.filter((n) => !(n.type === "slur" && (n.number ?? 1) === slurNumber));
2273
+ if (note.notations.length === 0) {
2274
+ delete note.notations;
2275
+ }
2276
+ return success(result);
2277
+ }
2278
+ function addArticulation(score, options) {
2279
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2280
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2281
+ }
2282
+ const part = score.parts[options.partIndex];
2283
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2284
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2285
+ }
2286
+ const result = cloneScore(score);
2287
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2288
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
2289
+ if (!noteResult) {
2290
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2291
+ }
2292
+ const note = noteResult.note;
2293
+ if (note.notations?.some((n) => n.type === "articulation" && n.articulation === options.articulation)) {
2294
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Articulation ${options.articulation} already exists on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2295
+ }
2296
+ if (!note.notations) note.notations = [];
2297
+ note.notations.push({
2298
+ type: "articulation",
2299
+ articulation: options.articulation,
2300
+ placement: options.placement
2301
+ });
2302
+ return success(result);
2303
+ }
2304
+ function removeArticulation(score, options) {
2305
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2306
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2307
+ }
2308
+ const part = score.parts[options.partIndex];
2309
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2310
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2311
+ }
2312
+ const result = cloneScore(score);
2313
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2314
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
2315
+ if (!noteResult) {
2316
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2317
+ }
2318
+ const note = noteResult.note;
2319
+ if (!note.notations?.some((n) => n.type === "articulation" && n.articulation === options.articulation)) {
2320
+ return failure([operationError("ARTICULATION_NOT_FOUND", `Articulation ${options.articulation} not found on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2321
+ }
2322
+ note.notations = note.notations.filter((n) => !(n.type === "articulation" && n.articulation === options.articulation));
2323
+ if (note.notations.length === 0) {
2324
+ delete note.notations;
2325
+ }
2326
+ return success(result);
2327
+ }
2328
+ function getInsertPositionForDirection(measure, targetPosition) {
2329
+ let position = 0;
2330
+ let insertIndex = 0;
2331
+ for (let i = 0; i < measure.entries.length; i++) {
2332
+ const entry = measure.entries[i];
2333
+ if (position >= targetPosition) {
2334
+ return insertIndex;
2335
+ }
2336
+ if (entry.type === "note" && !entry.chord) {
2337
+ position += entry.duration;
2338
+ } else if (entry.type === "backup") {
2339
+ position -= entry.duration;
2340
+ } else if (entry.type === "forward") {
2341
+ position += entry.duration;
2342
+ }
2343
+ insertIndex = i + 1;
2344
+ }
2345
+ return insertIndex;
2346
+ }
2347
+ function addDynamics(score, options) {
2348
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2349
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2350
+ }
2351
+ const part = score.parts[options.partIndex];
2352
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2353
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2354
+ }
2355
+ if (options.position < 0) {
2356
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2357
+ }
2358
+ const result = cloneScore(score);
2359
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2360
+ const directionEntry = {
2361
+ _id: generateId(),
2362
+ type: "direction",
2363
+ directionTypes: [{
2364
+ kind: "dynamics",
2365
+ value: options.dynamics
2366
+ }],
2367
+ placement: options.placement ?? "below",
2368
+ staff: options.staff
2369
+ };
2370
+ const insertIndex = getInsertPositionForDirection(measure, options.position);
2371
+ measure.entries.splice(insertIndex, 0, directionEntry);
2372
+ return success(result);
2373
+ }
2374
+ function removeDynamics(score, options) {
2375
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2376
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2377
+ }
2378
+ const part = score.parts[options.partIndex];
2379
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2380
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2381
+ }
2382
+ const result = cloneScore(score);
2383
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2384
+ let directionCount = 0;
2385
+ let targetIndex = -1;
2386
+ for (let i = 0; i < measure.entries.length; i++) {
2387
+ const entry = measure.entries[i];
2388
+ if (entry.type === "direction") {
2389
+ const hasDynamics = entry.directionTypes.some((dt) => dt.kind === "dynamics");
2390
+ if (hasDynamics) {
2391
+ if (directionCount === options.directionIndex) {
2392
+ targetIndex = i;
2393
+ break;
2394
+ }
2395
+ directionCount++;
2396
+ }
2397
+ }
2398
+ }
2399
+ if (targetIndex === -1) {
2400
+ return failure([operationError("DYNAMICS_NOT_FOUND", `Dynamics direction index ${options.directionIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2401
+ }
2402
+ measure.entries.splice(targetIndex, 1);
2403
+ return success(result);
2404
+ }
2405
+ function insertClefChange(score, options) {
2406
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2407
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2408
+ }
2409
+ const part = score.parts[options.partIndex];
2410
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2411
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2412
+ }
2413
+ if (options.position < 0) {
2414
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2415
+ }
2416
+ const validSigns = ["G", "F", "C", "percussion", "TAB"];
2417
+ if (!validSigns.includes(options.clef.sign)) {
2418
+ return failure([operationError("INVALID_CLEF", `Invalid clef sign: ${options.clef.sign}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2419
+ }
2420
+ const result = cloneScore(score);
2421
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2422
+ if (options.position === 0) {
2423
+ if (!measure.attributes) {
2424
+ measure.attributes = {};
2425
+ }
2426
+ const staff = options.clef.staff ?? 1;
2427
+ if (!measure.attributes.clef) {
2428
+ measure.attributes.clef = [];
2429
+ }
2430
+ const existingIndex = measure.attributes.clef.findIndex((c) => (c.staff ?? 1) === staff);
2431
+ if (existingIndex >= 0) {
2432
+ measure.attributes.clef[existingIndex] = options.clef;
2433
+ } else {
2434
+ measure.attributes.clef.push(options.clef);
2435
+ }
2436
+ } else {
2437
+ const attributesEntry = {
2438
+ _id: generateId(),
2439
+ type: "attributes",
2440
+ attributes: {
2441
+ clef: [options.clef]
2442
+ }
2443
+ };
2444
+ const insertIndex = getInsertPositionForDirection(measure, options.position);
2445
+ measure.entries.splice(insertIndex, 0, attributesEntry);
2446
+ }
2447
+ const validationResult = validate(result, { checkStaffStructure: true });
2448
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
2449
+ if (criticalErrors.length > 0) {
2450
+ return failure(criticalErrors);
2451
+ }
2452
+ return success(result, validationResult.warnings);
2453
+ }
2454
+ var addNote = (score, options) => {
2455
+ const result = insertNote(score, {
2456
+ partIndex: options.partIndex,
2457
+ measureIndex: options.measureIndex,
2458
+ voice: options.voice,
2459
+ staff: options.staff,
2460
+ position: options.position,
2461
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
2462
+ duration: options.note.duration,
2463
+ noteType: options.note.noteType,
2464
+ dots: options.note.dots
2465
+ });
2466
+ return result.success ? result.data : score;
2467
+ };
2468
+ var deleteNote = (score, options) => {
2469
+ const result = removeNote(score, options);
2470
+ return result.success ? result.data : score;
2471
+ };
2472
+ var addChordNote = (score, options) => {
2473
+ const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
2474
+ return result.success ? result.data : score;
2475
+ };
2476
+ var modifyNotePitch = (score, options) => {
2477
+ const result = setNotePitch(score, options);
2478
+ return result.success ? result.data : score;
2479
+ };
2480
+ var modifyNoteDuration = (score, options) => {
2481
+ const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
2482
+ return result.success ? result.data : score;
2483
+ };
2484
+ var addNoteChecked = (score, options) => {
2485
+ return insertNote(score, {
2486
+ partIndex: options.partIndex,
2487
+ measureIndex: options.measureIndex,
2488
+ voice: options.voice,
2489
+ staff: options.staff,
2490
+ position: options.position,
2491
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
2492
+ duration: options.note.duration,
2493
+ noteType: options.note.noteType,
2494
+ dots: options.note.dots
2495
+ });
2496
+ };
2497
+ var deleteNoteChecked = removeNote;
2498
+ var addChordNoteChecked = (score, options) => {
2499
+ return addChord(score, { ...options, noteIndex: options.afterNoteIndex });
2500
+ };
2501
+ var modifyNotePitchChecked = setNotePitch;
2502
+ var modifyNoteDurationChecked = (score, options) => {
1750
2503
  return changeNoteDuration(score, { ...options, newDuration: options.duration });
1751
2504
  };
1752
2505
  var transposeChecked = transpose;
2506
+ function createTuplet(score, options) {
2507
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2508
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2509
+ }
2510
+ const part = score.parts[options.partIndex];
2511
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2512
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2513
+ }
2514
+ if (options.noteCount < 2) {
2515
+ return failure([operationError("INVALID_DURATION", "Tuplet must contain at least 2 notes", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2516
+ }
2517
+ if (options.actualNotes < 2 || options.normalNotes < 1) {
2518
+ return failure([operationError("INVALID_DURATION", "Invalid tuplet ratio", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2519
+ }
2520
+ const result = cloneScore(score);
2521
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2522
+ const notes = [];
2523
+ let noteCount = 0;
2524
+ for (let i = 0; i < measure.entries.length; i++) {
2525
+ const entry = measure.entries[i];
2526
+ if (entry.type === "note" && !entry.rest && !entry.chord) {
2527
+ if (noteCount >= options.startNoteIndex && noteCount < options.startNoteIndex + options.noteCount) {
2528
+ notes.push({ note: entry, entryIndex: i });
2529
+ }
2530
+ noteCount++;
2531
+ }
2532
+ }
2533
+ if (notes.length !== options.noteCount) {
2534
+ return failure([operationError("NOTE_NOT_FOUND", `Could not find ${options.noteCount} notes starting at index ${options.startNoteIndex}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2535
+ }
2536
+ const voice = notes[0].note.voice;
2537
+ const staff = notes[0].note.staff;
2538
+ if (!notes.every((n) => n.note.voice === voice)) {
2539
+ return failure([operationError("NOTE_CONFLICT", "All notes in a tuplet must be in the same voice", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice })]);
2540
+ }
2541
+ if (!notes.every((n) => n.note.staff === staff)) {
2542
+ return failure([operationError("NOTE_CONFLICT", "All notes in a tuplet must be on the same staff", { partIndex: options.partIndex, measureIndex: options.measureIndex, staff })]);
2543
+ }
2544
+ const tupletNumber = 1;
2545
+ for (let i = 0; i < notes.length; i++) {
2546
+ const { note } = notes[i];
2547
+ note.timeModification = {
2548
+ actualNotes: options.actualNotes,
2549
+ normalNotes: options.normalNotes
2550
+ };
2551
+ if (!note.notations) note.notations = [];
2552
+ if (i === 0) {
2553
+ note.notations.push({
2554
+ type: "tuplet",
2555
+ tupletType: "start",
2556
+ number: tupletNumber,
2557
+ bracket: options.bracket ?? true,
2558
+ showNumber: options.showNumber ?? "actual",
2559
+ tupletActual: { tupletNumber: options.actualNotes },
2560
+ tupletNormal: { tupletNumber: options.normalNotes }
2561
+ });
2562
+ } else if (i === notes.length - 1) {
2563
+ note.notations.push({
2564
+ type: "tuplet",
2565
+ tupletType: "stop",
2566
+ number: tupletNumber
2567
+ });
2568
+ }
2569
+ }
2570
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
2571
+ const errors = validateMeasureLocal(measure, context, {
2572
+ checkTuplets: true,
2573
+ checkMeasureDuration: true
2574
+ });
2575
+ const criticalErrors = errors.filter((e) => e.level === "error");
2576
+ if (criticalErrors.length > 0) {
2577
+ return failure(criticalErrors);
2578
+ }
2579
+ return success(result, errors.filter((e) => e.level !== "error"));
2580
+ }
2581
+ function removeTuplet(score, options) {
2582
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2583
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2584
+ }
2585
+ const part = score.parts[options.partIndex];
2586
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2587
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2588
+ }
2589
+ const result = cloneScore(score);
2590
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2591
+ let noteCount = 0;
2592
+ let targetNote = null;
2593
+ let targetEntryIndex = -1;
2594
+ for (let i = 0; i < measure.entries.length; i++) {
2595
+ const entry = measure.entries[i];
2596
+ if (entry.type === "note" && !entry.rest) {
2597
+ if (noteCount === options.noteIndex) {
2598
+ targetNote = entry;
2599
+ targetEntryIndex = i;
2600
+ break;
2601
+ }
2602
+ noteCount++;
2603
+ }
2604
+ }
2605
+ if (!targetNote || targetEntryIndex === -1) {
2606
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2607
+ }
2608
+ if (!targetNote.timeModification) {
2609
+ return failure([operationError("NOTE_NOT_FOUND", "Note is not part of a tuplet", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2610
+ }
2611
+ const voice = targetNote.voice;
2612
+ const staff = targetNote.staff;
2613
+ const actualNotes = targetNote.timeModification.actualNotes;
2614
+ const normalNotes = targetNote.timeModification.normalNotes;
2615
+ const tupletNotes = [];
2616
+ let inTuplet = false;
2617
+ let currentTupletNumber;
2618
+ for (const entry of measure.entries) {
2619
+ if (entry.type !== "note" || entry.rest) continue;
2620
+ if (entry.voice !== voice || entry.staff !== staff) continue;
2621
+ const hasSameTimeModification = entry.timeModification?.actualNotes === actualNotes && entry.timeModification?.normalNotes === normalNotes;
2622
+ const tupletStart = entry.notations?.find(
2623
+ (n) => n.type === "tuplet" && n.tupletType === "start"
2624
+ );
2625
+ const tupletStop = entry.notations?.find(
2626
+ (n) => n.type === "tuplet" && n.tupletType === "stop" && (currentTupletNumber === void 0 || n.number === currentTupletNumber)
2627
+ );
2628
+ if (tupletStart && tupletStart.type === "tuplet") {
2629
+ inTuplet = true;
2630
+ currentTupletNumber = tupletStart.number;
2631
+ }
2632
+ if (inTuplet && hasSameTimeModification) {
2633
+ tupletNotes.push(entry);
2634
+ }
2635
+ if (tupletStop && inTuplet) {
2636
+ if (tupletNotes.includes(targetNote)) {
2637
+ break;
2638
+ } else {
2639
+ tupletNotes.length = 0;
2640
+ inTuplet = false;
2641
+ currentTupletNumber = void 0;
2642
+ }
2643
+ }
2644
+ }
2645
+ if (tupletNotes.length === 0) {
2646
+ tupletNotes.push(targetNote);
2647
+ }
2648
+ for (const note of tupletNotes) {
2649
+ delete note.timeModification;
2650
+ if (note.notations) {
2651
+ note.notations = note.notations.filter((n) => n.type !== "tuplet");
2652
+ if (note.notations.length === 0) {
2653
+ delete note.notations;
2654
+ }
2655
+ }
2656
+ }
2657
+ return success(result);
2658
+ }
2659
+ function addBeam(score, options) {
2660
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2661
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2662
+ }
2663
+ const part = score.parts[options.partIndex];
2664
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2665
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2666
+ }
2667
+ if (options.noteCount < 2) {
2668
+ return failure([operationError("INVALID_DURATION", "Beam must contain at least 2 notes", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2669
+ }
2670
+ const result = cloneScore(score);
2671
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2672
+ const beamLevel = options.beamLevel ?? 1;
2673
+ const notes = [];
2674
+ let noteCount = 0;
2675
+ for (const entry of measure.entries) {
2676
+ if (entry.type === "note" && !entry.rest && !entry.chord) {
2677
+ if (noteCount >= options.startNoteIndex && noteCount < options.startNoteIndex + options.noteCount) {
2678
+ notes.push(entry);
2679
+ }
2680
+ noteCount++;
2681
+ }
2682
+ }
2683
+ if (notes.length !== options.noteCount) {
2684
+ return failure([operationError("NOTE_NOT_FOUND", `Could not find ${options.noteCount} notes starting at index ${options.startNoteIndex}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2685
+ }
2686
+ const voice = notes[0].voice;
2687
+ if (!notes.every((n) => n.voice === voice)) {
2688
+ return failure([operationError("NOTE_CONFLICT", "All beamed notes must be in the same voice", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice })]);
2689
+ }
2690
+ for (let i = 0; i < notes.length; i++) {
2691
+ const note = notes[i];
2692
+ if (!note.beam) {
2693
+ note.beam = [];
2694
+ }
2695
+ note.beam = note.beam.filter((b) => b.number !== beamLevel);
2696
+ let beamType;
2697
+ if (i === 0) {
2698
+ beamType = "begin";
2699
+ } else if (i === notes.length - 1) {
2700
+ beamType = "end";
2701
+ } else {
2702
+ beamType = "continue";
2703
+ }
2704
+ note.beam.push({
2705
+ number: beamLevel,
2706
+ type: beamType
2707
+ });
2708
+ }
2709
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
2710
+ const errors = validateMeasureLocal(measure, context, { checkBeams: true });
2711
+ const criticalErrors = errors.filter((e) => e.level === "error");
2712
+ if (criticalErrors.length > 0) {
2713
+ return failure(criticalErrors);
2714
+ }
2715
+ return success(result, errors.filter((e) => e.level !== "error"));
2716
+ }
2717
+ function removeBeam(score, options) {
2718
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2719
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2720
+ }
2721
+ const part = score.parts[options.partIndex];
2722
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2723
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2724
+ }
2725
+ const result = cloneScore(score);
2726
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2727
+ let noteCount = 0;
2728
+ let targetNote = null;
2729
+ for (const entry of measure.entries) {
2730
+ if (entry.type === "note" && !entry.rest) {
2731
+ if (noteCount === options.noteIndex) {
2732
+ targetNote = entry;
2733
+ break;
2734
+ }
2735
+ noteCount++;
2736
+ }
2737
+ }
2738
+ if (!targetNote) {
2739
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2740
+ }
2741
+ if (!targetNote.beam || targetNote.beam.length === 0) {
2742
+ return failure([operationError("NOTE_NOT_FOUND", "Note is not part of a beam group", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2743
+ }
2744
+ const voice = targetNote.voice;
2745
+ const staff = targetNote.staff;
2746
+ const beamNotes = [];
2747
+ let inBeam = false;
2748
+ const targetBeamLevel = options.beamLevel ?? targetNote.beam[0]?.number ?? 1;
2749
+ for (const entry of measure.entries) {
2750
+ if (entry.type !== "note" || entry.rest) continue;
2751
+ if (entry.voice !== voice || entry.staff !== staff) continue;
2752
+ const beamInfo = entry.beam?.find((b) => b.number === targetBeamLevel);
2753
+ if (!beamInfo) {
2754
+ if (inBeam) {
2755
+ break;
2756
+ }
2757
+ continue;
2758
+ }
2759
+ if (beamInfo.type === "begin") {
2760
+ inBeam = true;
2761
+ beamNotes.push(entry);
2762
+ } else if (beamInfo.type === "continue") {
2763
+ if (inBeam) beamNotes.push(entry);
2764
+ } else if (beamInfo.type === "end") {
2765
+ beamNotes.push(entry);
2766
+ if (beamNotes.includes(targetNote)) {
2767
+ break;
2768
+ } else {
2769
+ beamNotes.length = 0;
2770
+ inBeam = false;
2771
+ }
2772
+ }
2773
+ }
2774
+ if (beamNotes.length === 0) {
2775
+ beamNotes.push(targetNote);
2776
+ }
2777
+ for (const note of beamNotes) {
2778
+ if (note.beam) {
2779
+ if (options.beamLevel !== void 0) {
2780
+ note.beam = note.beam.filter((b) => b.number !== options.beamLevel);
2781
+ } else {
2782
+ note.beam = [];
2783
+ }
2784
+ if (note.beam.length === 0) {
2785
+ delete note.beam;
2786
+ }
2787
+ }
2788
+ }
2789
+ return success(result);
2790
+ }
2791
+ function autoBeam(score, options) {
2792
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2793
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2794
+ }
2795
+ const part = score.parts[options.partIndex];
2796
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2797
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2798
+ }
2799
+ const result = cloneScore(score);
2800
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2801
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
2802
+ const divisions = context.divisions;
2803
+ const time = context.time ?? { beats: "4", beatType: 4 };
2804
+ const beatDuration = 4 / time.beatType * divisions;
2805
+ for (const entry of measure.entries) {
2806
+ if (entry.type === "note") {
2807
+ delete entry.beam;
2808
+ }
2809
+ }
2810
+ const notesByVoice = /* @__PURE__ */ new Map();
2811
+ let position = 0;
2812
+ for (const entry of measure.entries) {
2813
+ if (entry.type === "note") {
2814
+ if (!entry.chord && !entry.rest) {
2815
+ const voice = entry.voice;
2816
+ if (options.voice === void 0 || voice === options.voice) {
2817
+ if (!notesByVoice.has(voice)) {
2818
+ notesByVoice.set(voice, []);
2819
+ }
2820
+ notesByVoice.get(voice).push({ note: entry, position });
2821
+ }
2822
+ }
2823
+ if (!entry.chord) {
2824
+ position += entry.duration;
2825
+ }
2826
+ } else if (entry.type === "backup") {
2827
+ position -= entry.duration;
2828
+ } else if (entry.type === "forward") {
2829
+ position += entry.duration;
2830
+ }
2831
+ }
2832
+ for (const [, notes] of notesByVoice) {
2833
+ const beatGroups = [];
2834
+ let currentBeat = -1;
2835
+ let currentGroup = [];
2836
+ for (const { note, position: notePos } of notes) {
2837
+ if (note.duration > beatDuration / 2) {
2838
+ if (currentGroup.length >= 2) {
2839
+ beatGroups.push(currentGroup);
2840
+ }
2841
+ currentGroup = [];
2842
+ currentBeat = -1;
2843
+ continue;
2844
+ }
2845
+ const beat = Math.floor(notePos / beatDuration);
2846
+ if (options.groupByBeat !== false && beat !== currentBeat) {
2847
+ if (currentGroup.length >= 2) {
2848
+ beatGroups.push(currentGroup);
2849
+ }
2850
+ currentGroup = [{ note, position: notePos }];
2851
+ currentBeat = beat;
2852
+ } else {
2853
+ currentGroup.push({ note, position: notePos });
2854
+ }
2855
+ }
2856
+ if (currentGroup.length >= 2) {
2857
+ beatGroups.push(currentGroup);
2858
+ }
2859
+ for (const group of beatGroups) {
2860
+ for (let i = 0; i < group.length; i++) {
2861
+ const { note } = group[i];
2862
+ if (!note.beam) {
2863
+ note.beam = [];
2864
+ }
2865
+ let beamType;
2866
+ if (i === 0) {
2867
+ beamType = "begin";
2868
+ } else if (i === group.length - 1) {
2869
+ beamType = "end";
2870
+ } else {
2871
+ beamType = "continue";
2872
+ }
2873
+ note.beam.push({
2874
+ number: 1,
2875
+ type: beamType
2876
+ });
2877
+ }
2878
+ }
2879
+ }
2880
+ const errors = validateMeasureLocal(measure, context, { checkBeams: true });
2881
+ const criticalErrors = errors.filter((e) => e.level === "error");
2882
+ if (criticalErrors.length > 0) {
2883
+ return failure(criticalErrors);
2884
+ }
2885
+ return success(result, errors.filter((e) => e.level !== "error"));
2886
+ }
2887
+ function copyNotes(score, options) {
2888
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2889
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2890
+ }
2891
+ const part = score.parts[options.partIndex];
2892
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2893
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2894
+ }
2895
+ if (options.startPosition >= options.endPosition) {
2896
+ return failure([operationError("INVALID_POSITION", "Start position must be less than end position", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2897
+ }
2898
+ const measure = part.measures[options.measureIndex];
2899
+ const copiedNotes = [];
2900
+ let position = 0;
2901
+ for (const entry of measure.entries) {
2902
+ if (entry.type === "note") {
2903
+ if (entry.voice === options.voice && (options.staff === void 0 || (entry.staff ?? 1) === options.staff)) {
2904
+ if (!entry.chord) {
2905
+ const noteEnd = position + entry.duration;
2906
+ if (position < options.endPosition && noteEnd > options.startPosition) {
2907
+ const clonedNote = cloneNoteWithNewId(entry);
2908
+ if (clonedNote.tie) {
2909
+ }
2910
+ copiedNotes.push({
2911
+ relativePosition: position - options.startPosition,
2912
+ note: clonedNote
2913
+ });
2914
+ }
2915
+ position += entry.duration;
2916
+ } else {
2917
+ if (copiedNotes.length > 0) {
2918
+ const lastCopied = copiedNotes[copiedNotes.length - 1];
2919
+ if (lastCopied.note.voice === entry.voice && (options.staff === void 0 || (lastCopied.note.staff ?? 1) === (entry.staff ?? 1))) {
2920
+ const clonedNote = cloneNoteWithNewId(entry);
2921
+ copiedNotes.push({
2922
+ relativePosition: lastCopied.relativePosition,
2923
+ note: clonedNote
2924
+ });
2925
+ }
2926
+ }
2927
+ }
2928
+ } else if (!entry.chord) {
2929
+ position += entry.duration;
2930
+ }
2931
+ } else if (entry.type === "backup") {
2932
+ position -= entry.duration;
2933
+ } else if (entry.type === "forward") {
2934
+ position += entry.duration;
2935
+ }
2936
+ }
2937
+ if (copiedNotes.length === 0) {
2938
+ return failure([operationError("NOTE_NOT_FOUND", "No notes found in the specified range", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice })]);
2939
+ }
2940
+ const selection = {
2941
+ source: {
2942
+ partIndex: options.partIndex,
2943
+ measureIndex: options.measureIndex,
2944
+ startPosition: options.startPosition,
2945
+ endPosition: options.endPosition,
2946
+ voice: options.voice,
2947
+ staff: options.staff
2948
+ },
2949
+ notes: copiedNotes,
2950
+ duration: options.endPosition - options.startPosition
2951
+ };
2952
+ return success(selection);
2953
+ }
2954
+ function pasteNotes(score, options) {
2955
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
2956
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
2957
+ }
2958
+ const part = score.parts[options.partIndex];
2959
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2960
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2961
+ }
2962
+ if (options.position < 0) {
2963
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2964
+ }
2965
+ const result = cloneScore(score);
2966
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
2967
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
2968
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
2969
+ const targetVoice = options.voice ?? options.selection.source.voice;
2970
+ const targetStaff = options.staff ?? options.selection.source.staff;
2971
+ const pasteEnd = options.position + options.selection.duration;
2972
+ if (pasteEnd > measureDuration) {
2973
+ return failure([operationError(
2974
+ "EXCEEDS_MEASURE",
2975
+ `Paste would exceed measure duration (ends at ${pasteEnd}, measure is ${measureDuration})`,
2976
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
2977
+ { pasteEnd, measureDuration }
2978
+ )]);
2979
+ }
2980
+ const voiceEntries = getVoiceEntries(measure, targetVoice, targetStaff);
2981
+ if (options.overwrite !== false) {
2982
+ const entriesToKeep = voiceEntries.filter((e) => {
2983
+ if (e.entry.type !== "note") return true;
2984
+ const note = e.entry;
2985
+ if (note.rest) return true;
2986
+ return e.endPosition <= options.position || e.position >= pasteEnd;
2987
+ });
2988
+ const newEntries = [];
2989
+ for (const { position, entry } of entriesToKeep) {
2990
+ if (entry.type === "note") {
2991
+ newEntries.push({ position, entry });
2992
+ }
2993
+ }
2994
+ for (const { relativePosition, note } of options.selection.notes) {
2995
+ const pastePosition = options.position + Math.max(0, relativePosition);
2996
+ const newNote = cloneNoteWithNewId(note);
2997
+ newNote.voice = targetVoice;
2998
+ if (targetStaff !== void 0) {
2999
+ newNote.staff = targetStaff;
3000
+ }
3001
+ delete newNote.tie;
3002
+ delete newNote.ties;
3003
+ if (newNote.notations) {
3004
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
3005
+ if (newNote.notations.length === 0) {
3006
+ delete newNote.notations;
3007
+ }
3008
+ }
3009
+ newEntries.push({ position: pastePosition, entry: newNote });
3010
+ }
3011
+ measure.entries = rebuildMeasureWithVoice(
3012
+ measure,
3013
+ targetVoice,
3014
+ newEntries,
3015
+ measureDuration,
3016
+ targetStaff
3017
+ );
3018
+ } else {
3019
+ const { hasNotes, conflictingNotes } = hasNotesInRange(voiceEntries, options.position, pasteEnd);
3020
+ if (hasNotes) {
3021
+ return failure([operationError(
3022
+ "NOTE_CONFLICT",
3023
+ `Paste range ${options.position}-${pasteEnd} conflicts with existing notes`,
3024
+ { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: targetVoice },
3025
+ { conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
3026
+ )]);
3027
+ }
3028
+ const existingNotes = voiceEntries.filter((e) => e.entry.type === "note").map((e) => ({ position: e.position, entry: e.entry }));
3029
+ for (const { relativePosition, note } of options.selection.notes) {
3030
+ const pastePosition = options.position + Math.max(0, relativePosition);
3031
+ const newNote = cloneNoteWithNewId(note);
3032
+ newNote.voice = targetVoice;
3033
+ if (targetStaff !== void 0) {
3034
+ newNote.staff = targetStaff;
3035
+ }
3036
+ delete newNote.tie;
3037
+ delete newNote.ties;
3038
+ if (newNote.notations) {
3039
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
3040
+ if (newNote.notations.length === 0) {
3041
+ delete newNote.notations;
3042
+ }
3043
+ }
3044
+ existingNotes.push({ position: pastePosition, entry: newNote });
3045
+ }
3046
+ measure.entries = rebuildMeasureWithVoice(
3047
+ measure,
3048
+ targetVoice,
3049
+ existingNotes,
3050
+ measureDuration,
3051
+ targetStaff
3052
+ );
3053
+ }
3054
+ const errors = validateMeasureLocal(measure, context, {
3055
+ checkMeasureDuration: true,
3056
+ checkPosition: true,
3057
+ checkVoiceStaff: true
3058
+ });
3059
+ const criticalErrors = errors.filter((e) => e.level === "error");
3060
+ if (criticalErrors.length > 0) {
3061
+ return failure(criticalErrors);
3062
+ }
3063
+ return success(result, errors.filter((e) => e.level !== "error"));
3064
+ }
3065
+ function cutNotes(score, options) {
3066
+ const copyResult = copyNotes(score, options);
3067
+ if (!copyResult.success) {
3068
+ return failure(copyResult.errors);
3069
+ }
3070
+ const selection = copyResult.data;
3071
+ const result = cloneScore(score);
3072
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
3073
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
3074
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
3075
+ const voiceEntries = getVoiceEntries(measure, options.voice, options.staff);
3076
+ const entriesToKeep = voiceEntries.filter((e) => {
3077
+ if (e.entry.type !== "note") return true;
3078
+ const note = e.entry;
3079
+ if (note.rest) return true;
3080
+ return e.endPosition <= options.startPosition || e.position >= options.endPosition;
3081
+ });
3082
+ const newEntries = [];
3083
+ for (const { position, entry } of entriesToKeep) {
3084
+ if (entry.type === "note") {
3085
+ newEntries.push({ position, entry });
3086
+ }
3087
+ }
3088
+ measure.entries = rebuildMeasureWithVoice(
3089
+ measure,
3090
+ options.voice,
3091
+ newEntries,
3092
+ measureDuration,
3093
+ options.staff
3094
+ );
3095
+ const errors = validateMeasureLocal(measure, context, {
3096
+ checkMeasureDuration: true,
3097
+ checkPosition: true,
3098
+ checkVoiceStaff: true
3099
+ });
3100
+ const criticalErrors = errors.filter((e) => e.level === "error");
3101
+ if (criticalErrors.length > 0) {
3102
+ return failure(criticalErrors);
3103
+ }
3104
+ return success(
3105
+ { score: result, selection },
3106
+ errors.filter((e) => e.level !== "error")
3107
+ );
3108
+ }
3109
+ function copyNotesMultiMeasure(score, options) {
3110
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3111
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3112
+ }
3113
+ const part = score.parts[options.partIndex];
3114
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
3115
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
3116
+ }
3117
+ if (options.endMeasureIndex < options.startMeasureIndex || options.endMeasureIndex >= part.measures.length) {
3118
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
3119
+ }
3120
+ const selection = {
3121
+ source: {
3122
+ partIndex: options.partIndex,
3123
+ startMeasureIndex: options.startMeasureIndex,
3124
+ endMeasureIndex: options.endMeasureIndex,
3125
+ voice: options.voice,
3126
+ staff: options.staff
3127
+ },
3128
+ measures: []
3129
+ };
3130
+ for (let measureIndex = options.startMeasureIndex; measureIndex <= options.endMeasureIndex; measureIndex++) {
3131
+ const measure = part.measures[measureIndex];
3132
+ const measureOffset = measureIndex - options.startMeasureIndex;
3133
+ const copiedNotes = [];
3134
+ let position = 0;
3135
+ for (const entry of measure.entries) {
3136
+ if (entry.type === "note") {
3137
+ if (entry.voice === options.voice && (options.staff === void 0 || (entry.staff ?? 1) === options.staff)) {
3138
+ if (!entry.chord && !entry.rest) {
3139
+ const clonedNote = cloneNoteWithNewId(entry);
3140
+ copiedNotes.push({
3141
+ relativePosition: position,
3142
+ note: clonedNote
3143
+ });
3144
+ position += entry.duration;
3145
+ } else if (entry.chord && copiedNotes.length > 0) {
3146
+ const clonedNote = cloneNoteWithNewId(entry);
3147
+ copiedNotes.push({
3148
+ relativePosition: copiedNotes[copiedNotes.length - 1].relativePosition,
3149
+ note: clonedNote
3150
+ });
3151
+ } else if (!entry.chord) {
3152
+ position += entry.duration;
3153
+ }
3154
+ } else if (!entry.chord) {
3155
+ position += entry.duration;
3156
+ }
3157
+ } else if (entry.type === "backup") {
3158
+ position -= entry.duration;
3159
+ } else if (entry.type === "forward") {
3160
+ position += entry.duration;
3161
+ }
3162
+ }
3163
+ if (copiedNotes.length > 0) {
3164
+ selection.measures.push({
3165
+ measureOffset,
3166
+ notes: copiedNotes
3167
+ });
3168
+ }
3169
+ }
3170
+ if (selection.measures.length === 0) {
3171
+ return failure([operationError("NOTE_NOT_FOUND", "No notes found in the specified range", { partIndex: options.partIndex, voice: options.voice })]);
3172
+ }
3173
+ return success(selection);
3174
+ }
3175
+ function pasteNotesMultiMeasure(score, options) {
3176
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3177
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3178
+ }
3179
+ const part = score.parts[options.partIndex];
3180
+ const measureCount = options.selection.measures.length > 0 ? options.selection.measures[options.selection.measures.length - 1].measureOffset + 1 : 0;
3181
+ if (options.startMeasureIndex + measureCount > part.measures.length) {
3182
+ return failure([operationError("MEASURE_NOT_FOUND", `Not enough measures to paste (need ${measureCount})`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
3183
+ }
3184
+ let result = cloneScore(score);
3185
+ const targetVoice = options.voice ?? options.selection.source.voice;
3186
+ const targetStaff = options.staff ?? options.selection.source.staff;
3187
+ for (const measureData of options.selection.measures) {
3188
+ const measureIndex = options.startMeasureIndex + measureData.measureOffset;
3189
+ const measure = result.parts[options.partIndex].measures[measureIndex];
3190
+ const context = getMeasureContext(result, options.partIndex, measureIndex);
3191
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
3192
+ const voiceEntries = getVoiceEntries(measure, targetVoice, targetStaff);
3193
+ let entriesToKeep;
3194
+ if (options.overwrite !== false) {
3195
+ entriesToKeep = voiceEntries.filter((e) => e.entry.type === "note" && e.entry.rest).map((e) => ({ position: e.position, entry: e.entry }));
3196
+ } else {
3197
+ entriesToKeep = voiceEntries.filter((e) => e.entry.type === "note").map((e) => ({ position: e.position, entry: e.entry }));
3198
+ }
3199
+ for (const { relativePosition, note } of measureData.notes) {
3200
+ const newNote = cloneNoteWithNewId(note);
3201
+ newNote.voice = targetVoice;
3202
+ if (targetStaff !== void 0) {
3203
+ newNote.staff = targetStaff;
3204
+ }
3205
+ delete newNote.tie;
3206
+ delete newNote.ties;
3207
+ if (newNote.notations) {
3208
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
3209
+ if (newNote.notations.length === 0) {
3210
+ delete newNote.notations;
3211
+ }
3212
+ }
3213
+ entriesToKeep.push({ position: relativePosition, entry: newNote });
3214
+ }
3215
+ measure.entries = rebuildMeasureWithVoice(
3216
+ measure,
3217
+ targetVoice,
3218
+ entriesToKeep,
3219
+ measureDuration,
3220
+ targetStaff
3221
+ );
3222
+ const errors = validateMeasureLocal(measure, context, {
3223
+ checkMeasureDuration: true,
3224
+ checkPosition: true,
3225
+ checkVoiceStaff: true
3226
+ });
3227
+ const criticalErrors = errors.filter((e) => e.level === "error");
3228
+ if (criticalErrors.length > 0) {
3229
+ return failure(criticalErrors);
3230
+ }
3231
+ }
3232
+ return success(result);
3233
+ }
3234
+ function addTempo(score, options) {
3235
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3236
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3237
+ }
3238
+ const part = score.parts[options.partIndex];
3239
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3240
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3241
+ }
3242
+ if (options.bpm <= 0) {
3243
+ return failure([operationError("INVALID_DURATION", "BPM must be positive", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3244
+ }
3245
+ const result = cloneScore(score);
3246
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
3247
+ const directionTypes = [];
3248
+ directionTypes.push({
3249
+ kind: "metronome",
3250
+ beatUnit: options.beatUnit ?? "quarter",
3251
+ beatUnitDot: options.beatUnitDot,
3252
+ perMinute: options.bpm
3253
+ });
3254
+ if (options.text) {
3255
+ directionTypes.push({
3256
+ kind: "words",
3257
+ text: options.text,
3258
+ fontWeight: "bold"
3259
+ });
3260
+ }
3261
+ const direction = {
3262
+ _id: generateId(),
3263
+ type: "direction",
3264
+ directionTypes,
3265
+ placement: options.placement ?? "above",
3266
+ sound: { tempo: options.bpm }
3267
+ };
3268
+ insertDirectionAtPosition(measure, direction, options.position);
3269
+ return success(result);
3270
+ }
3271
+ function removeTempo(score, options) {
3272
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3273
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3274
+ }
3275
+ const part = score.parts[options.partIndex];
3276
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3277
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3278
+ }
3279
+ const result = cloneScore(score);
3280
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
3281
+ const tempoDirectionIndices = [];
3282
+ for (let i = 0; i < measure.entries.length; i++) {
3283
+ const entry = measure.entries[i];
3284
+ if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "metronome")) {
3285
+ tempoDirectionIndices.push(i);
3286
+ }
3287
+ }
3288
+ if (tempoDirectionIndices.length === 0) {
3289
+ return failure([operationError("TEMPO_NOT_FOUND", "No tempo marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3290
+ }
3291
+ const targetIndex = options.directionIndex ?? 0;
3292
+ if (targetIndex < 0 || targetIndex >= tempoDirectionIndices.length) {
3293
+ return failure([operationError("TEMPO_NOT_FOUND", `Tempo direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3294
+ }
3295
+ measure.entries.splice(tempoDirectionIndices[targetIndex], 1);
3296
+ return success(result);
3297
+ }
3298
+ function addWedge(score, options) {
3299
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3300
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3301
+ }
3302
+ const part = score.parts[options.partIndex];
3303
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
3304
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
3305
+ }
3306
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
3307
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
3308
+ }
3309
+ if (options.endMeasureIndex < options.startMeasureIndex || options.endMeasureIndex === options.startMeasureIndex && options.endPosition <= options.startPosition) {
3310
+ return failure([operationError("INVALID_RANGE", "End position must be after start position", { partIndex: options.partIndex })]);
3311
+ }
3312
+ const result = cloneScore(score);
3313
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
3314
+ const startDirection = {
3315
+ _id: generateId(),
3316
+ type: "direction",
3317
+ directionTypes: [{
3318
+ kind: "wedge",
3319
+ type: options.type
3320
+ }],
3321
+ placement: options.placement ?? "below",
3322
+ staff: options.staff
3323
+ };
3324
+ insertDirectionAtPosition(startMeasure, startDirection, options.startPosition);
3325
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
3326
+ const endDirection = {
3327
+ _id: generateId(),
3328
+ type: "direction",
3329
+ directionTypes: [{
3330
+ kind: "wedge",
3331
+ type: "stop"
3332
+ }],
3333
+ placement: options.placement ?? "below",
3334
+ staff: options.staff
3335
+ };
3336
+ insertDirectionAtPosition(endMeasure, endDirection, options.endPosition);
3337
+ return success(result);
3338
+ }
3339
+ function removeWedge(score, options) {
3340
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3341
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3342
+ }
3343
+ const part = score.parts[options.partIndex];
3344
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3345
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3346
+ }
3347
+ const result = cloneScore(score);
3348
+ const wedgeStarts = [];
3349
+ for (let mi = options.measureIndex; mi < result.parts[options.partIndex].measures.length; mi++) {
3350
+ const measure = result.parts[options.partIndex].measures[mi];
3351
+ for (let ei = 0; ei < measure.entries.length; ei++) {
3352
+ const entry = measure.entries[ei];
3353
+ if (entry.type === "direction") {
3354
+ const wedgeType = entry.directionTypes.find((dt) => dt.kind === "wedge");
3355
+ if (wedgeType && wedgeType.kind === "wedge" && (wedgeType.type === "crescendo" || wedgeType.type === "diminuendo")) {
3356
+ wedgeStarts.push({ measureIndex: mi, entryIndex: ei });
3357
+ }
3358
+ }
3359
+ }
3360
+ if (mi === options.measureIndex && wedgeStarts.length > 0) break;
3361
+ }
3362
+ if (wedgeStarts.length === 0) {
3363
+ return failure([operationError("WEDGE_NOT_FOUND", "No wedge found starting in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3364
+ }
3365
+ const targetIndex = options.directionIndex ?? 0;
3366
+ if (targetIndex >= wedgeStarts.length) {
3367
+ return failure([operationError("WEDGE_NOT_FOUND", `Wedge direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3368
+ }
3369
+ const startInfo = wedgeStarts[targetIndex];
3370
+ const startMeasure = result.parts[options.partIndex].measures[startInfo.measureIndex];
3371
+ startMeasure.entries.splice(startInfo.entryIndex, 1);
3372
+ for (let mi = startInfo.measureIndex; mi < result.parts[options.partIndex].measures.length; mi++) {
3373
+ const measure = result.parts[options.partIndex].measures[mi];
3374
+ for (let ei = 0; ei < measure.entries.length; ei++) {
3375
+ const entry = measure.entries[ei];
3376
+ if (entry.type === "direction") {
3377
+ const wedgeType = entry.directionTypes.find((dt) => dt.kind === "wedge" && dt.type === "stop");
3378
+ if (wedgeType) {
3379
+ measure.entries.splice(ei, 1);
3380
+ return success(result);
3381
+ }
3382
+ }
3383
+ }
3384
+ }
3385
+ return success(result);
3386
+ }
3387
+ function addFermata(score, options) {
3388
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3389
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3390
+ }
3391
+ const part = score.parts[options.partIndex];
3392
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3393
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3394
+ }
3395
+ const result = cloneScore(score);
3396
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
3397
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
3398
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
3399
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3400
+ }
3401
+ const note = notes[options.noteIndex];
3402
+ if (note.notations?.some((n) => n.type === "fermata")) {
3403
+ return failure([operationError("FERMATA_ALREADY_EXISTS", "Note already has a fermata", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3404
+ }
3405
+ if (!note.notations) {
3406
+ note.notations = [];
3407
+ }
3408
+ const fermataNotation = {
3409
+ type: "fermata",
3410
+ shape: options.shape ?? "normal",
3411
+ fermataType: options.fermataType ?? "upright",
3412
+ placement: options.placement ?? "above"
3413
+ };
3414
+ note.notations.push(fermataNotation);
3415
+ return success(result);
3416
+ }
3417
+ function removeFermata(score, options) {
3418
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3419
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3420
+ }
3421
+ const part = score.parts[options.partIndex];
3422
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3423
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3424
+ }
3425
+ const result = cloneScore(score);
3426
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
3427
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
3428
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
3429
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3430
+ }
3431
+ const note = notes[options.noteIndex];
3432
+ const fermataIndex = note.notations?.findIndex((n) => n.type === "fermata");
3433
+ if (fermataIndex === void 0 || fermataIndex === -1) {
3434
+ return failure([operationError("FERMATA_NOT_FOUND", "Note does not have a fermata", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3435
+ }
3436
+ note.notations.splice(fermataIndex, 1);
3437
+ if (note.notations.length === 0) {
3438
+ delete note.notations;
3439
+ }
3440
+ return success(result);
3441
+ }
3442
+ function addOrnament(score, options) {
3443
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3444
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3445
+ }
3446
+ const part = score.parts[options.partIndex];
3447
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3448
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3449
+ }
3450
+ const result = cloneScore(score);
3451
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
3452
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
3453
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
3454
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3455
+ }
3456
+ const note = notes[options.noteIndex];
3457
+ if (note.notations?.some((n) => n.type === "ornament" && n.ornament === options.ornament)) {
3458
+ return failure([operationError("ORNAMENT_ALREADY_EXISTS", `Note already has ornament: ${options.ornament}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3459
+ }
3460
+ if (!note.notations) {
3461
+ note.notations = [];
3462
+ }
3463
+ const ornamentNotation = {
3464
+ type: "ornament",
3465
+ ornament: options.ornament,
3466
+ placement: options.placement,
3467
+ accidentalMark: options.accidentalMark
3468
+ };
3469
+ note.notations.push(ornamentNotation);
3470
+ return success(result);
3471
+ }
3472
+ function removeOrnament(score, options) {
3473
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3474
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3475
+ }
3476
+ const part = score.parts[options.partIndex];
3477
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3478
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3479
+ }
3480
+ const result = cloneScore(score);
3481
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
3482
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
3483
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
3484
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3485
+ }
3486
+ const note = notes[options.noteIndex];
3487
+ const ornamentIndex = options.ornament ? note.notations?.findIndex((n) => n.type === "ornament" && n.ornament === options.ornament) : note.notations?.findIndex((n) => n.type === "ornament");
3488
+ if (ornamentIndex === void 0 || ornamentIndex === -1) {
3489
+ return failure([operationError("ORNAMENT_NOT_FOUND", "Note does not have the specified ornament", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3490
+ }
3491
+ note.notations.splice(ornamentIndex, 1);
3492
+ if (note.notations.length === 0) {
3493
+ delete note.notations;
3494
+ }
3495
+ return success(result);
3496
+ }
3497
+ function addPedal(score, options) {
3498
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3499
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3500
+ }
3501
+ const part = score.parts[options.partIndex];
3502
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3503
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3504
+ }
3505
+ const result = cloneScore(score);
3506
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
3507
+ const direction = {
3508
+ _id: generateId(),
3509
+ type: "direction",
3510
+ directionTypes: [{
3511
+ kind: "pedal",
3512
+ type: options.pedalType,
3513
+ line: options.line
3514
+ }],
3515
+ placement: options.placement ?? "below"
3516
+ };
3517
+ insertDirectionAtPosition(measure, direction, options.position);
3518
+ return success(result);
3519
+ }
3520
+ function removePedal(score, options) {
3521
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3522
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3523
+ }
3524
+ const part = score.parts[options.partIndex];
3525
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3526
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3527
+ }
3528
+ const result = cloneScore(score);
3529
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
3530
+ const pedalIndices = [];
3531
+ for (let i = 0; i < measure.entries.length; i++) {
3532
+ const entry = measure.entries[i];
3533
+ if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "pedal")) {
3534
+ pedalIndices.push(i);
3535
+ }
3536
+ }
3537
+ if (pedalIndices.length === 0) {
3538
+ return failure([operationError("PEDAL_NOT_FOUND", "No pedal marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3539
+ }
3540
+ const targetIndex = options.directionIndex ?? 0;
3541
+ if (targetIndex < 0 || targetIndex >= pedalIndices.length) {
3542
+ return failure([operationError("PEDAL_NOT_FOUND", `Pedal direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3543
+ }
3544
+ measure.entries.splice(pedalIndices[targetIndex], 1);
3545
+ return success(result);
3546
+ }
3547
+ function addTextDirection(score, options) {
3548
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3549
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3550
+ }
3551
+ const part = score.parts[options.partIndex];
3552
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3553
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3554
+ }
3555
+ if (!options.text.trim()) {
3556
+ return failure([operationError("INVALID_TEXT", "Text cannot be empty", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3557
+ }
3558
+ const result = cloneScore(score);
3559
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
3560
+ const direction = {
3561
+ _id: generateId(),
3562
+ type: "direction",
3563
+ directionTypes: [{
3564
+ kind: "words",
3565
+ text: options.text,
3566
+ fontStyle: options.fontStyle,
3567
+ fontWeight: options.fontWeight
3568
+ }],
3569
+ placement: options.placement ?? "above"
3570
+ };
3571
+ insertDirectionAtPosition(measure, direction, options.position);
3572
+ return success(result);
3573
+ }
3574
+ function addRehearsalMark(score, options) {
3575
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
3576
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
3577
+ }
3578
+ const part = score.parts[options.partIndex];
3579
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3580
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3581
+ }
3582
+ const result = cloneScore(score);
3583
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
3584
+ const direction = {
3585
+ _id: generateId(),
3586
+ type: "direction",
3587
+ directionTypes: [{
3588
+ kind: "rehearsal",
3589
+ text: options.text,
3590
+ enclosure: options.enclosure ?? "square"
3591
+ }],
3592
+ placement: "above"
3593
+ };
3594
+ insertDirectionAtPosition(measure, direction, 0);
3595
+ return success(result);
3596
+ }
3597
+ function insertDirectionAtPosition(measure, direction, position) {
3598
+ let currentPosition = 0;
3599
+ let insertIndex = 0;
3600
+ for (let i = 0; i < measure.entries.length; i++) {
3601
+ const entry = measure.entries[i];
3602
+ if (currentPosition >= position) {
3603
+ insertIndex = i;
3604
+ break;
3605
+ }
3606
+ if (entry.type === "note" && !entry.chord) {
3607
+ currentPosition += entry.duration;
3608
+ } else if (entry.type === "forward") {
3609
+ currentPosition += entry.duration;
3610
+ } else if (entry.type === "backup") {
3611
+ currentPosition -= entry.duration;
3612
+ }
3613
+ insertIndex = i + 1;
3614
+ }
3615
+ measure.entries.splice(insertIndex, 0, direction);
3616
+ }
3617
+ function addRepeatBarline(score, options) {
3618
+ const { partIndex, measureIndex, direction, times } = options;
3619
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3620
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3621
+ }
3622
+ const part = score.parts[partIndex];
3623
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3624
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3625
+ }
3626
+ const result = cloneScore(score);
3627
+ const location = direction === "forward" ? "left" : "right";
3628
+ const barStyle = direction === "forward" ? "heavy-light" : "light-heavy";
3629
+ for (const p of result.parts) {
3630
+ if (measureIndex >= p.measures.length) continue;
3631
+ const measure = p.measures[measureIndex];
3632
+ if (!measure.barlines) {
3633
+ measure.barlines = [];
3634
+ }
3635
+ const existingIndex = measure.barlines.findIndex((b) => b.location === location && b.repeat);
3636
+ if (existingIndex >= 0) {
3637
+ return failure([operationError("REPEAT_ALREADY_EXISTS", `Repeat barline already exists at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
3638
+ }
3639
+ const nonRepeatIndex = measure.barlines.findIndex((b) => b.location === location && !b.repeat);
3640
+ if (nonRepeatIndex >= 0) {
3641
+ measure.barlines.splice(nonRepeatIndex, 1);
3642
+ }
3643
+ measure.barlines.push({
3644
+ _id: generateId(),
3645
+ location,
3646
+ barStyle,
3647
+ repeat: {
3648
+ direction,
3649
+ times
3650
+ }
3651
+ });
3652
+ }
3653
+ return success(result);
3654
+ }
3655
+ function removeRepeatBarline(score, options) {
3656
+ const { partIndex, measureIndex, location } = options;
3657
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3658
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3659
+ }
3660
+ const part = score.parts[partIndex];
3661
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3662
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3663
+ }
3664
+ const measure = part.measures[measureIndex];
3665
+ if (!measure.barlines) {
3666
+ return failure([operationError("REPEAT_NOT_FOUND", `No repeat barline found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
3667
+ }
3668
+ const existingIndex = measure.barlines.findIndex((b) => b.location === location && b.repeat);
3669
+ if (existingIndex < 0) {
3670
+ return failure([operationError("REPEAT_NOT_FOUND", `No repeat barline found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
3671
+ }
3672
+ const result = cloneScore(score);
3673
+ for (const p of result.parts) {
3674
+ if (measureIndex >= p.measures.length) continue;
3675
+ const m = p.measures[measureIndex];
3676
+ if (m.barlines) {
3677
+ const idx = m.barlines.findIndex((b) => b.location === location && b.repeat);
3678
+ if (idx >= 0) {
3679
+ m.barlines.splice(idx, 1);
3680
+ }
3681
+ if (m.barlines.length === 0) {
3682
+ delete m.barlines;
3683
+ }
3684
+ }
3685
+ }
3686
+ return success(result);
3687
+ }
3688
+ function addEnding(score, options) {
3689
+ const { partIndex, measureIndex, number, type } = options;
3690
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3691
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3692
+ }
3693
+ const part = score.parts[partIndex];
3694
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3695
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3696
+ }
3697
+ const result = cloneScore(score);
3698
+ const location = type === "start" ? "left" : "right";
3699
+ for (const p of result.parts) {
3700
+ if (measureIndex >= p.measures.length) continue;
3701
+ const measure = p.measures[measureIndex];
3702
+ if (!measure.barlines) {
3703
+ measure.barlines = [];
3704
+ }
3705
+ let barline = measure.barlines.find((b) => b.location === location);
3706
+ if (!barline) {
3707
+ barline = { _id: generateId(), location };
3708
+ measure.barlines.push(barline);
3709
+ }
3710
+ if (barline.ending) {
3711
+ return failure([operationError("ENDING_ALREADY_EXISTS", `Ending already exists at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
3712
+ }
3713
+ barline.ending = { number, type };
3714
+ }
3715
+ return success(result);
3716
+ }
3717
+ function removeEnding(score, options) {
3718
+ const { partIndex, measureIndex, location } = options;
3719
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3720
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3721
+ }
3722
+ const part = score.parts[partIndex];
3723
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3724
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3725
+ }
3726
+ const measure = part.measures[measureIndex];
3727
+ const barline = measure.barlines?.find((b) => b.location === location && b.ending);
3728
+ if (!barline) {
3729
+ return failure([operationError("ENDING_NOT_FOUND", `No ending found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
3730
+ }
3731
+ const result = cloneScore(score);
3732
+ for (const p of result.parts) {
3733
+ if (measureIndex >= p.measures.length) continue;
3734
+ const m = p.measures[measureIndex];
3735
+ if (m.barlines) {
3736
+ const bl = m.barlines.find((b) => b.location === location);
3737
+ if (bl) {
3738
+ delete bl.ending;
3739
+ if (!bl.barStyle && !bl.repeat && !bl.ending) {
3740
+ const idx = m.barlines.indexOf(bl);
3741
+ m.barlines.splice(idx, 1);
3742
+ }
3743
+ }
3744
+ if (m.barlines.length === 0) {
3745
+ delete m.barlines;
3746
+ }
3747
+ }
3748
+ }
3749
+ return success(result);
3750
+ }
3751
+ function changeBarline(score, options) {
3752
+ const { partIndex, measureIndex, location, barStyle } = options;
3753
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3754
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3755
+ }
3756
+ const part = score.parts[partIndex];
3757
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3758
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3759
+ }
3760
+ const result = cloneScore(score);
3761
+ for (const p of result.parts) {
3762
+ if (measureIndex >= p.measures.length) continue;
3763
+ const measure = p.measures[measureIndex];
3764
+ if (!measure.barlines) {
3765
+ measure.barlines = [];
3766
+ }
3767
+ let barline = measure.barlines.find((b) => b.location === location);
3768
+ if (!barline) {
3769
+ barline = { _id: generateId(), location };
3770
+ measure.barlines.push(barline);
3771
+ }
3772
+ barline.barStyle = barStyle;
3773
+ }
3774
+ return success(result);
3775
+ }
3776
+ function addSegno(score, options) {
3777
+ const { partIndex, measureIndex, position = 0 } = options;
3778
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3779
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3780
+ }
3781
+ const part = score.parts[partIndex];
3782
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3783
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3784
+ }
3785
+ const result = cloneScore(score);
3786
+ const measure = result.parts[partIndex].measures[measureIndex];
3787
+ const direction = {
3788
+ _id: generateId(),
3789
+ type: "direction",
3790
+ directionTypes: [{ kind: "segno" }],
3791
+ placement: "above"
3792
+ };
3793
+ insertDirectionAtPosition(measure, direction, position);
3794
+ return success(result);
3795
+ }
3796
+ function addCoda(score, options) {
3797
+ const { partIndex, measureIndex, position = 0 } = options;
3798
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3799
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3800
+ }
3801
+ const part = score.parts[partIndex];
3802
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3803
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3804
+ }
3805
+ const result = cloneScore(score);
3806
+ const measure = result.parts[partIndex].measures[measureIndex];
3807
+ const direction = {
3808
+ _id: generateId(),
3809
+ type: "direction",
3810
+ directionTypes: [{ kind: "coda" }],
3811
+ placement: "above"
3812
+ };
3813
+ insertDirectionAtPosition(measure, direction, position);
3814
+ return success(result);
3815
+ }
3816
+ function addDaCapo(score, options) {
3817
+ const { partIndex, measureIndex, position } = options;
3818
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3819
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3820
+ }
3821
+ const part = score.parts[partIndex];
3822
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3823
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3824
+ }
3825
+ const result = cloneScore(score);
3826
+ const measure = result.parts[partIndex].measures[measureIndex];
3827
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
3828
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
3829
+ const insertPos = position ?? measureDuration;
3830
+ const direction = {
3831
+ _id: generateId(),
3832
+ type: "direction",
3833
+ directionTypes: [{ kind: "words", text: "D.C." }],
3834
+ placement: "above"
3835
+ };
3836
+ insertDirectionAtPosition(measure, direction, insertPos);
3837
+ const sound = {
3838
+ _id: generateId(),
3839
+ type: "sound",
3840
+ dacapo: true
3841
+ };
3842
+ measure.entries.push(sound);
3843
+ return success(result);
3844
+ }
3845
+ function addDalSegno(score, options) {
3846
+ const { partIndex, measureIndex, position } = options;
3847
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3848
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3849
+ }
3850
+ const part = score.parts[partIndex];
3851
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3852
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3853
+ }
3854
+ const result = cloneScore(score);
3855
+ const measure = result.parts[partIndex].measures[measureIndex];
3856
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
3857
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
3858
+ const insertPos = position ?? measureDuration;
3859
+ const direction = {
3860
+ _id: generateId(),
3861
+ type: "direction",
3862
+ directionTypes: [{ kind: "words", text: "D.S." }],
3863
+ placement: "above"
3864
+ };
3865
+ insertDirectionAtPosition(measure, direction, insertPos);
3866
+ const sound = {
3867
+ _id: generateId(),
3868
+ type: "sound",
3869
+ dalsegno: "segno"
3870
+ };
3871
+ measure.entries.push(sound);
3872
+ return success(result);
3873
+ }
3874
+ function addFine(score, options) {
3875
+ const { partIndex, measureIndex, position } = options;
3876
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3877
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3878
+ }
3879
+ const part = score.parts[partIndex];
3880
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3881
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3882
+ }
3883
+ const result = cloneScore(score);
3884
+ const measure = result.parts[partIndex].measures[measureIndex];
3885
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
3886
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
3887
+ const insertPos = position ?? measureDuration;
3888
+ const direction = {
3889
+ _id: generateId(),
3890
+ type: "direction",
3891
+ directionTypes: [{ kind: "words", text: "Fine" }],
3892
+ placement: "above"
3893
+ };
3894
+ insertDirectionAtPosition(measure, direction, insertPos);
3895
+ const sound = {
3896
+ _id: generateId(),
3897
+ type: "sound",
3898
+ fine: true
3899
+ };
3900
+ measure.entries.push(sound);
3901
+ return success(result);
3902
+ }
3903
+ function addToCoda(score, options) {
3904
+ const { partIndex, measureIndex, position } = options;
3905
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3906
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3907
+ }
3908
+ const part = score.parts[partIndex];
3909
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3910
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3911
+ }
3912
+ const result = cloneScore(score);
3913
+ const measure = result.parts[partIndex].measures[measureIndex];
3914
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
3915
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
3916
+ const insertPos = position ?? measureDuration;
3917
+ const direction = {
3918
+ _id: generateId(),
3919
+ type: "direction",
3920
+ directionTypes: [{ kind: "words", text: "To Coda" }],
3921
+ placement: "above"
3922
+ };
3923
+ insertDirectionAtPosition(measure, direction, insertPos);
3924
+ const sound = {
3925
+ _id: generateId(),
3926
+ type: "sound",
3927
+ tocoda: "coda"
3928
+ };
3929
+ measure.entries.push(sound);
3930
+ return success(result);
3931
+ }
3932
+ function addGraceNote(score, options) {
3933
+ const { partIndex, measureIndex, targetNoteIndex, pitch, noteType = "eighth", slash = true, voice, staff } = options;
3934
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3935
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3936
+ }
3937
+ const part = score.parts[partIndex];
3938
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3939
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3940
+ }
3941
+ const measure = part.measures[measureIndex];
3942
+ let noteCount = 0;
3943
+ let targetEntryIndex = -1;
3944
+ let targetNote = null;
3945
+ for (let i = 0; i < measure.entries.length; i++) {
3946
+ const entry = measure.entries[i];
3947
+ if (entry.type === "note" && !entry.chord) {
3948
+ if (noteCount === targetNoteIndex) {
3949
+ targetEntryIndex = i;
3950
+ targetNote = entry;
3951
+ break;
3952
+ }
3953
+ noteCount++;
3954
+ }
3955
+ }
3956
+ if (targetEntryIndex < 0 || !targetNote) {
3957
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${targetNoteIndex} not found`, { partIndex, measureIndex })]);
3958
+ }
3959
+ const result = cloneScore(score);
3960
+ const resultMeasure = result.parts[partIndex].measures[measureIndex];
3961
+ const graceNote = {
3962
+ _id: generateId(),
3963
+ type: "note",
3964
+ pitch,
3965
+ duration: 0,
3966
+ // Grace notes have no duration
3967
+ voice: voice ?? targetNote.voice,
3968
+ staff: staff ?? targetNote.staff,
3969
+ noteType,
3970
+ grace: {
3971
+ slash
3972
+ }
3973
+ };
3974
+ resultMeasure.entries.splice(targetEntryIndex, 0, graceNote);
3975
+ return success(result);
3976
+ }
3977
+ function removeGraceNote(score, options) {
3978
+ const { partIndex, measureIndex, graceNoteIndex } = options;
3979
+ if (partIndex < 0 || partIndex >= score.parts.length) {
3980
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
3981
+ }
3982
+ const part = score.parts[partIndex];
3983
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
3984
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3985
+ }
3986
+ const measure = part.measures[measureIndex];
3987
+ let graceCount = 0;
3988
+ let targetEntryIndex = -1;
3989
+ for (let i = 0; i < measure.entries.length; i++) {
3990
+ const entry = measure.entries[i];
3991
+ if (entry.type === "note" && entry.grace) {
3992
+ if (graceCount === graceNoteIndex) {
3993
+ targetEntryIndex = i;
3994
+ break;
3995
+ }
3996
+ graceCount++;
3997
+ }
3998
+ }
3999
+ if (targetEntryIndex < 0) {
4000
+ return failure([operationError("GRACE_NOTE_NOT_FOUND", `Grace note at index ${graceNoteIndex} not found`, { partIndex, measureIndex })]);
4001
+ }
4002
+ const result = cloneScore(score);
4003
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
4004
+ return success(result);
4005
+ }
4006
+ function convertToGrace(score, options) {
4007
+ const { partIndex, measureIndex, noteIndex, slash = true } = options;
4008
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4009
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4010
+ }
4011
+ const part = score.parts[partIndex];
4012
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4013
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4014
+ }
4015
+ const measure = part.measures[measureIndex];
4016
+ let noteCount = 0;
4017
+ let targetEntryIndex = -1;
4018
+ for (let i = 0; i < measure.entries.length; i++) {
4019
+ const entry = measure.entries[i];
4020
+ if (entry.type === "note" && !entry.chord) {
4021
+ if (noteCount === noteIndex) {
4022
+ targetEntryIndex = i;
4023
+ break;
4024
+ }
4025
+ noteCount++;
4026
+ }
4027
+ }
4028
+ if (targetEntryIndex < 0) {
4029
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4030
+ }
4031
+ const targetEntry = measure.entries[targetEntryIndex];
4032
+ if (targetEntry.type !== "note") {
4033
+ return failure([operationError("NOTE_NOT_FOUND", `Entry at index is not a note`, { partIndex, measureIndex })]);
4034
+ }
4035
+ if (targetEntry.grace) {
4036
+ return failure([operationError("INVALID_GRACE_NOTE", `Note is already a grace note`, { partIndex, measureIndex })]);
4037
+ }
4038
+ const result = cloneScore(score);
4039
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4040
+ resultNote.grace = { slash };
4041
+ resultNote.duration = 0;
4042
+ return success(result);
4043
+ }
4044
+ function addLyric(score, options) {
4045
+ const { partIndex, measureIndex, noteIndex, text, syllabic = "single", verse = 1, extend = false } = options;
4046
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4047
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4048
+ }
4049
+ const part = score.parts[partIndex];
4050
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4051
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4052
+ }
4053
+ const measure = part.measures[measureIndex];
4054
+ let noteCount = 0;
4055
+ let targetEntryIndex = -1;
4056
+ for (let i = 0; i < measure.entries.length; i++) {
4057
+ const entry = measure.entries[i];
4058
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4059
+ if (noteCount === noteIndex) {
4060
+ targetEntryIndex = i;
4061
+ break;
4062
+ }
4063
+ noteCount++;
4064
+ }
4065
+ }
4066
+ if (targetEntryIndex < 0) {
4067
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4068
+ }
4069
+ const targetEntry = measure.entries[targetEntryIndex];
4070
+ if (targetEntry.type !== "note") {
4071
+ return failure([operationError("NOTE_NOT_FOUND", `Entry is not a note`, { partIndex, measureIndex })]);
4072
+ }
4073
+ if (targetEntry.lyrics?.some((l) => l.number === verse)) {
4074
+ return failure([operationError("LYRIC_ALREADY_EXISTS", `Lyric for verse ${verse} already exists on this note`, { partIndex, measureIndex })]);
4075
+ }
4076
+ const result = cloneScore(score);
4077
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4078
+ if (!resultNote.lyrics) {
4079
+ resultNote.lyrics = [];
4080
+ }
4081
+ const lyric = {
4082
+ number: verse,
4083
+ syllabic,
4084
+ text,
4085
+ extend
4086
+ };
4087
+ resultNote.lyrics.push(lyric);
4088
+ return success(result);
4089
+ }
4090
+ function removeLyric(score, options) {
4091
+ const { partIndex, measureIndex, noteIndex, verse } = options;
4092
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4093
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4094
+ }
4095
+ const part = score.parts[partIndex];
4096
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4097
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4098
+ }
4099
+ const measure = part.measures[measureIndex];
4100
+ let noteCount = 0;
4101
+ let targetEntryIndex = -1;
4102
+ for (let i = 0; i < measure.entries.length; i++) {
4103
+ const entry = measure.entries[i];
4104
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4105
+ if (noteCount === noteIndex) {
4106
+ targetEntryIndex = i;
4107
+ break;
4108
+ }
4109
+ noteCount++;
4110
+ }
4111
+ }
4112
+ if (targetEntryIndex < 0) {
4113
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4114
+ }
4115
+ const targetEntry = measure.entries[targetEntryIndex];
4116
+ if (targetEntry.type !== "note" || !targetEntry.lyrics || targetEntry.lyrics.length === 0) {
4117
+ return failure([operationError("LYRIC_NOT_FOUND", `No lyrics found on note`, { partIndex, measureIndex })]);
4118
+ }
4119
+ if (verse !== void 0) {
4120
+ const lyricIndex = targetEntry.lyrics.findIndex((l) => l.number === verse);
4121
+ if (lyricIndex < 0) {
4122
+ return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
4123
+ }
4124
+ }
4125
+ const result = cloneScore(score);
4126
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4127
+ if (verse !== void 0) {
4128
+ resultNote.lyrics = resultNote.lyrics.filter((l) => l.number !== verse);
4129
+ } else {
4130
+ delete resultNote.lyrics;
4131
+ }
4132
+ if (resultNote.lyrics && resultNote.lyrics.length === 0) {
4133
+ delete resultNote.lyrics;
4134
+ }
4135
+ return success(result);
4136
+ }
4137
+ function updateLyric(score, options) {
4138
+ const { partIndex, measureIndex, noteIndex, verse = 1, text, syllabic, extend } = options;
4139
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4140
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4141
+ }
4142
+ const part = score.parts[partIndex];
4143
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4144
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4145
+ }
4146
+ const measure = part.measures[measureIndex];
4147
+ let noteCount = 0;
4148
+ let targetEntryIndex = -1;
4149
+ for (let i = 0; i < measure.entries.length; i++) {
4150
+ const entry = measure.entries[i];
4151
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4152
+ if (noteCount === noteIndex) {
4153
+ targetEntryIndex = i;
4154
+ break;
4155
+ }
4156
+ noteCount++;
4157
+ }
4158
+ }
4159
+ if (targetEntryIndex < 0) {
4160
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4161
+ }
4162
+ const targetEntry = measure.entries[targetEntryIndex];
4163
+ if (targetEntry.type !== "note" || !targetEntry.lyrics) {
4164
+ return failure([operationError("LYRIC_NOT_FOUND", `No lyrics found on note`, { partIndex, measureIndex })]);
4165
+ }
4166
+ const lyricIndex = targetEntry.lyrics.findIndex((l) => l.number === verse);
4167
+ if (lyricIndex < 0) {
4168
+ return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
4169
+ }
4170
+ const result = cloneScore(score);
4171
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4172
+ const lyric = resultNote.lyrics[lyricIndex];
4173
+ if (text !== void 0) {
4174
+ lyric.text = text;
4175
+ }
4176
+ if (syllabic !== void 0) {
4177
+ lyric.syllabic = syllabic;
4178
+ }
4179
+ if (extend !== void 0) {
4180
+ lyric.extend = extend;
4181
+ }
4182
+ return success(result);
4183
+ }
4184
+ function addHarmony(score, options) {
4185
+ const { partIndex, measureIndex, position, root, kind, kindText, bass, degrees, staff, placement = "above" } = options;
4186
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4187
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4188
+ }
4189
+ const part = score.parts[partIndex];
4190
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4191
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4192
+ }
4193
+ const validSteps = ["A", "B", "C", "D", "E", "F", "G"];
4194
+ if (!validSteps.includes(root.step.toUpperCase())) {
4195
+ return failure([operationError("INVALID_HARMONY", `Invalid root step: ${root.step}`, { partIndex, measureIndex })]);
4196
+ }
4197
+ if (bass && !validSteps.includes(bass.step.toUpperCase())) {
4198
+ return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
4199
+ }
4200
+ const result = cloneScore(score);
4201
+ const measure = result.parts[partIndex].measures[measureIndex];
4202
+ const harmony = {
4203
+ _id: generateId(),
4204
+ type: "harmony",
4205
+ root: {
4206
+ rootStep: root.step.toUpperCase(),
4207
+ rootAlter: root.alter
4208
+ },
4209
+ kind,
4210
+ kindText,
4211
+ bass: bass ? {
4212
+ bassStep: bass.step.toUpperCase(),
4213
+ bassAlter: bass.alter
4214
+ } : void 0,
4215
+ degrees: degrees?.map((d) => ({
4216
+ degreeValue: d.value,
4217
+ degreeAlter: d.alter,
4218
+ degreeType: d.type
4219
+ })),
4220
+ staff,
4221
+ placement
4222
+ };
4223
+ let currentPosition = 0;
4224
+ let insertIndex = 0;
4225
+ for (let i = 0; i < measure.entries.length; i++) {
4226
+ const entry = measure.entries[i];
4227
+ if (currentPosition >= position) {
4228
+ insertIndex = i;
4229
+ break;
4230
+ }
4231
+ if (entry.type === "note" && !entry.chord) {
4232
+ currentPosition += entry.duration;
4233
+ } else if (entry.type === "forward") {
4234
+ currentPosition += entry.duration;
4235
+ } else if (entry.type === "backup") {
4236
+ currentPosition -= entry.duration;
4237
+ }
4238
+ insertIndex = i + 1;
4239
+ }
4240
+ measure.entries.splice(insertIndex, 0, harmony);
4241
+ return success(result);
4242
+ }
4243
+ function removeHarmony(score, options) {
4244
+ const { partIndex, measureIndex, harmonyIndex } = options;
4245
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4246
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4247
+ }
4248
+ const part = score.parts[partIndex];
4249
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4250
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4251
+ }
4252
+ const measure = part.measures[measureIndex];
4253
+ let harmonyCount = 0;
4254
+ let targetEntryIndex = -1;
4255
+ for (let i = 0; i < measure.entries.length; i++) {
4256
+ const entry = measure.entries[i];
4257
+ if (entry.type === "harmony") {
4258
+ if (harmonyCount === harmonyIndex) {
4259
+ targetEntryIndex = i;
4260
+ break;
4261
+ }
4262
+ harmonyCount++;
4263
+ }
4264
+ }
4265
+ if (targetEntryIndex < 0) {
4266
+ return failure([operationError("HARMONY_NOT_FOUND", `Harmony at index ${harmonyIndex} not found`, { partIndex, measureIndex })]);
4267
+ }
4268
+ const result = cloneScore(score);
4269
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
4270
+ return success(result);
4271
+ }
4272
+ function updateHarmony(score, options) {
4273
+ const { partIndex, measureIndex, harmonyIndex, root, kind, kindText, bass, degrees } = options;
4274
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4275
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4276
+ }
4277
+ const part = score.parts[partIndex];
4278
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4279
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4280
+ }
4281
+ const measure = part.measures[measureIndex];
4282
+ let harmonyCount = 0;
4283
+ let targetEntryIndex = -1;
4284
+ for (let i = 0; i < measure.entries.length; i++) {
4285
+ const entry = measure.entries[i];
4286
+ if (entry.type === "harmony") {
4287
+ if (harmonyCount === harmonyIndex) {
4288
+ targetEntryIndex = i;
4289
+ break;
4290
+ }
4291
+ harmonyCount++;
4292
+ }
4293
+ }
4294
+ if (targetEntryIndex < 0) {
4295
+ return failure([operationError("HARMONY_NOT_FOUND", `Harmony at index ${harmonyIndex} not found`, { partIndex, measureIndex })]);
4296
+ }
4297
+ const validSteps = ["A", "B", "C", "D", "E", "F", "G"];
4298
+ if (root && !validSteps.includes(root.step.toUpperCase())) {
4299
+ return failure([operationError("INVALID_HARMONY", `Invalid root step: ${root.step}`, { partIndex, measureIndex })]);
4300
+ }
4301
+ if (bass && !validSteps.includes(bass.step.toUpperCase())) {
4302
+ return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
4303
+ }
4304
+ const result = cloneScore(score);
4305
+ const harmony = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4306
+ if (root) {
4307
+ harmony.root = {
4308
+ rootStep: root.step.toUpperCase(),
4309
+ rootAlter: root.alter
4310
+ };
4311
+ }
4312
+ if (kind !== void 0) {
4313
+ harmony.kind = kind;
4314
+ }
4315
+ if (kindText !== void 0) {
4316
+ harmony.kindText = kindText;
4317
+ }
4318
+ if (bass !== void 0) {
4319
+ if (bass === null) {
4320
+ delete harmony.bass;
4321
+ } else {
4322
+ harmony.bass = {
4323
+ bassStep: bass.step.toUpperCase(),
4324
+ bassAlter: bass.alter
4325
+ };
4326
+ }
4327
+ }
4328
+ if (degrees !== void 0) {
4329
+ if (degrees === null) {
4330
+ delete harmony.degrees;
4331
+ } else {
4332
+ harmony.degrees = degrees.map((d) => ({
4333
+ degreeValue: d.value,
4334
+ degreeAlter: d.alter,
4335
+ degreeType: d.type
4336
+ }));
4337
+ }
4338
+ }
4339
+ return success(result);
4340
+ }
4341
+ function addFingering(score, options) {
4342
+ const { partIndex, measureIndex, noteIndex, fingering, substitution = false, alternate = false, placement } = options;
4343
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4344
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4345
+ }
4346
+ const part = score.parts[partIndex];
4347
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4348
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4349
+ }
4350
+ const measure = part.measures[measureIndex];
4351
+ let noteCount = 0;
4352
+ let targetEntryIndex = -1;
4353
+ for (let i = 0; i < measure.entries.length; i++) {
4354
+ const entry = measure.entries[i];
4355
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4356
+ if (noteCount === noteIndex) {
4357
+ targetEntryIndex = i;
4358
+ break;
4359
+ }
4360
+ noteCount++;
4361
+ }
4362
+ }
4363
+ if (targetEntryIndex < 0) {
4364
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4365
+ }
4366
+ const result = cloneScore(score);
4367
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4368
+ if (!resultNote.notations) {
4369
+ resultNote.notations = [];
4370
+ }
4371
+ resultNote.notations.push({
4372
+ type: "technical",
4373
+ technical: "fingering",
4374
+ fingering,
4375
+ fingeringSubstitution: substitution || void 0,
4376
+ fingeringAlternate: alternate || void 0,
4377
+ placement
4378
+ });
4379
+ return success(result);
4380
+ }
4381
+ function removeFingering(score, options) {
4382
+ const { partIndex, measureIndex, noteIndex } = options;
4383
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4384
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4385
+ }
4386
+ const part = score.parts[partIndex];
4387
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4388
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4389
+ }
4390
+ const measure = part.measures[measureIndex];
4391
+ let noteCount = 0;
4392
+ let targetEntryIndex = -1;
4393
+ for (let i = 0; i < measure.entries.length; i++) {
4394
+ const entry = measure.entries[i];
4395
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4396
+ if (noteCount === noteIndex) {
4397
+ targetEntryIndex = i;
4398
+ break;
4399
+ }
4400
+ noteCount++;
4401
+ }
4402
+ }
4403
+ if (targetEntryIndex < 0) {
4404
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4405
+ }
4406
+ const targetEntry = measure.entries[targetEntryIndex];
4407
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
4408
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
4409
+ }
4410
+ const fingeringIndex = targetEntry.notations.findIndex(
4411
+ (n) => n.type === "technical" && n.technical === "fingering"
4412
+ );
4413
+ if (fingeringIndex < 0) {
4414
+ return failure([operationError("NOTE_NOT_FOUND", `No fingering found on note`, { partIndex, measureIndex })]);
4415
+ }
4416
+ const result = cloneScore(score);
4417
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4418
+ resultNote.notations.splice(fingeringIndex, 1);
4419
+ if (resultNote.notations.length === 0) {
4420
+ delete resultNote.notations;
4421
+ }
4422
+ return success(result);
4423
+ }
4424
+ function addBowing(score, options) {
4425
+ const { partIndex, measureIndex, noteIndex, bowingType, placement } = options;
4426
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4427
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4428
+ }
4429
+ const part = score.parts[partIndex];
4430
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4431
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4432
+ }
4433
+ const measure = part.measures[measureIndex];
4434
+ let noteCount = 0;
4435
+ let targetEntryIndex = -1;
4436
+ for (let i = 0; i < measure.entries.length; i++) {
4437
+ const entry = measure.entries[i];
4438
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4439
+ if (noteCount === noteIndex) {
4440
+ targetEntryIndex = i;
4441
+ break;
4442
+ }
4443
+ noteCount++;
4444
+ }
4445
+ }
4446
+ if (targetEntryIndex < 0) {
4447
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4448
+ }
4449
+ const result = cloneScore(score);
4450
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4451
+ if (!resultNote.notations) {
4452
+ resultNote.notations = [];
4453
+ }
4454
+ resultNote.notations.push({
4455
+ type: "technical",
4456
+ technical: bowingType,
4457
+ placement
4458
+ });
4459
+ return success(result);
4460
+ }
4461
+ function removeBowing(score, options) {
4462
+ const { partIndex, measureIndex, noteIndex, bowingType } = options;
4463
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4464
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4465
+ }
4466
+ const part = score.parts[partIndex];
4467
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4468
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4469
+ }
4470
+ const measure = part.measures[measureIndex];
4471
+ let noteCount = 0;
4472
+ let targetEntryIndex = -1;
4473
+ for (let i = 0; i < measure.entries.length; i++) {
4474
+ const entry = measure.entries[i];
4475
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4476
+ if (noteCount === noteIndex) {
4477
+ targetEntryIndex = i;
4478
+ break;
4479
+ }
4480
+ noteCount++;
4481
+ }
4482
+ }
4483
+ if (targetEntryIndex < 0) {
4484
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4485
+ }
4486
+ const targetEntry = measure.entries[targetEntryIndex];
4487
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
4488
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
4489
+ }
4490
+ const bowingIndex = targetEntry.notations.findIndex((n) => {
4491
+ if (n.type !== "technical") return false;
4492
+ if (bowingType) return n.technical === bowingType;
4493
+ return n.technical === "up-bow" || n.technical === "down-bow";
4494
+ });
4495
+ if (bowingIndex < 0) {
4496
+ return failure([operationError("NOTE_NOT_FOUND", `No bowing found on note`, { partIndex, measureIndex })]);
4497
+ }
4498
+ const result = cloneScore(score);
4499
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4500
+ resultNote.notations.splice(bowingIndex, 1);
4501
+ if (resultNote.notations.length === 0) {
4502
+ delete resultNote.notations;
4503
+ }
4504
+ return success(result);
4505
+ }
4506
+ function addStringNumber(score, options) {
4507
+ const { partIndex, measureIndex, noteIndex, stringNumber, placement } = options;
4508
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4509
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4510
+ }
4511
+ const part = score.parts[partIndex];
4512
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4513
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4514
+ }
4515
+ if (stringNumber < 1) {
4516
+ return failure([operationError("INVALID_POSITION", `String number must be positive`, { partIndex, measureIndex })]);
4517
+ }
4518
+ const measure = part.measures[measureIndex];
4519
+ let noteCount = 0;
4520
+ let targetEntryIndex = -1;
4521
+ for (let i = 0; i < measure.entries.length; i++) {
4522
+ const entry = measure.entries[i];
4523
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4524
+ if (noteCount === noteIndex) {
4525
+ targetEntryIndex = i;
4526
+ break;
4527
+ }
4528
+ noteCount++;
4529
+ }
4530
+ }
4531
+ if (targetEntryIndex < 0) {
4532
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4533
+ }
4534
+ const result = cloneScore(score);
4535
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4536
+ if (!resultNote.notations) {
4537
+ resultNote.notations = [];
4538
+ }
4539
+ resultNote.notations.push({
4540
+ type: "technical",
4541
+ technical: "string",
4542
+ string: stringNumber,
4543
+ placement
4544
+ });
4545
+ return success(result);
4546
+ }
4547
+ function removeStringNumber(score, options) {
4548
+ const { partIndex, measureIndex, noteIndex } = options;
4549
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4550
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4551
+ }
4552
+ const part = score.parts[partIndex];
4553
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4554
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4555
+ }
4556
+ const measure = part.measures[measureIndex];
4557
+ let noteCount = 0;
4558
+ let targetEntryIndex = -1;
4559
+ for (let i = 0; i < measure.entries.length; i++) {
4560
+ const entry = measure.entries[i];
4561
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4562
+ if (noteCount === noteIndex) {
4563
+ targetEntryIndex = i;
4564
+ break;
4565
+ }
4566
+ noteCount++;
4567
+ }
4568
+ }
4569
+ if (targetEntryIndex < 0) {
4570
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4571
+ }
4572
+ const targetEntry = measure.entries[targetEntryIndex];
4573
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
4574
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
4575
+ }
4576
+ const stringIndex = targetEntry.notations.findIndex(
4577
+ (n) => n.type === "technical" && n.technical === "string"
4578
+ );
4579
+ if (stringIndex < 0) {
4580
+ return failure([operationError("NOTE_NOT_FOUND", `No string number found on note`, { partIndex, measureIndex })]);
4581
+ }
4582
+ const result = cloneScore(score);
4583
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4584
+ resultNote.notations.splice(stringIndex, 1);
4585
+ if (resultNote.notations.length === 0) {
4586
+ delete resultNote.notations;
4587
+ }
4588
+ return success(result);
4589
+ }
4590
+ function addOctaveShift(score, options) {
4591
+ const { partIndex, measureIndex, position, shiftType, size = 8 } = options;
4592
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4593
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4594
+ }
4595
+ const part = score.parts[partIndex];
4596
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4597
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4598
+ }
4599
+ const result = cloneScore(score);
4600
+ const measure = result.parts[partIndex].measures[measureIndex];
4601
+ const direction = {
4602
+ _id: generateId(),
4603
+ type: "direction",
4604
+ directionTypes: [{
4605
+ kind: "octave-shift",
4606
+ type: shiftType,
4607
+ size
4608
+ }],
4609
+ placement: shiftType === "down" ? "above" : "below"
4610
+ };
4611
+ insertDirectionAtPosition(measure, direction, position);
4612
+ return success(result);
4613
+ }
4614
+ function stopOctaveShift(score, options) {
4615
+ const { partIndex, measureIndex, position, size = 8 } = options;
4616
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4617
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4618
+ }
4619
+ const part = score.parts[partIndex];
4620
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4621
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4622
+ }
4623
+ const result = cloneScore(score);
4624
+ const measure = result.parts[partIndex].measures[measureIndex];
4625
+ const direction = {
4626
+ _id: generateId(),
4627
+ type: "direction",
4628
+ directionTypes: [{
4629
+ kind: "octave-shift",
4630
+ type: "stop",
4631
+ size
4632
+ }]
4633
+ };
4634
+ insertDirectionAtPosition(measure, direction, position);
4635
+ return success(result);
4636
+ }
4637
+ function removeOctaveShift(score, options) {
4638
+ const { partIndex, measureIndex, octaveShiftIndex = 0 } = options;
4639
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4640
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4641
+ }
4642
+ const part = score.parts[partIndex];
4643
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4644
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4645
+ }
4646
+ const measure = part.measures[measureIndex];
4647
+ let shiftCount = 0;
4648
+ let targetEntryIndex = -1;
4649
+ for (let i = 0; i < measure.entries.length; i++) {
4650
+ const entry = measure.entries[i];
4651
+ if (entry.type === "direction") {
4652
+ const hasOctaveShift = entry.directionTypes.some((dt) => dt.kind === "octave-shift");
4653
+ if (hasOctaveShift) {
4654
+ if (shiftCount === octaveShiftIndex) {
4655
+ targetEntryIndex = i;
4656
+ break;
4657
+ }
4658
+ shiftCount++;
4659
+ }
4660
+ }
4661
+ }
4662
+ if (targetEntryIndex < 0) {
4663
+ return failure([operationError("NOTE_NOT_FOUND", `Octave shift at index ${octaveShiftIndex} not found`, { partIndex, measureIndex })]);
4664
+ }
4665
+ const result = cloneScore(score);
4666
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
4667
+ return success(result);
4668
+ }
4669
+ function addBreathMark(score, options) {
4670
+ const { partIndex, measureIndex, noteIndex, placement = "above" } = options;
4671
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4672
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4673
+ }
4674
+ const part = score.parts[partIndex];
4675
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4676
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4677
+ }
4678
+ const measure = part.measures[measureIndex];
4679
+ let noteCount = 0;
4680
+ let targetEntryIndex = -1;
4681
+ for (let i = 0; i < measure.entries.length; i++) {
4682
+ const entry = measure.entries[i];
4683
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4684
+ if (noteCount === noteIndex) {
4685
+ targetEntryIndex = i;
4686
+ break;
4687
+ }
4688
+ noteCount++;
4689
+ }
4690
+ }
4691
+ if (targetEntryIndex < 0) {
4692
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4693
+ }
4694
+ const result = cloneScore(score);
4695
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4696
+ if (!resultNote.notations) {
4697
+ resultNote.notations = [];
4698
+ }
4699
+ const existingBreathMark = resultNote.notations.find(
4700
+ (n) => n.type === "articulation" && n.articulation === "breath-mark"
4701
+ );
4702
+ if (existingBreathMark) {
4703
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Breath mark already exists on note`, { partIndex, measureIndex })]);
4704
+ }
4705
+ resultNote.notations.push({
4706
+ type: "articulation",
4707
+ articulation: "breath-mark",
4708
+ placement
4709
+ });
4710
+ return success(result);
4711
+ }
4712
+ function removeBreathMark(score, options) {
4713
+ const { partIndex, measureIndex, noteIndex } = options;
4714
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4715
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4716
+ }
4717
+ const part = score.parts[partIndex];
4718
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4719
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4720
+ }
4721
+ const measure = part.measures[measureIndex];
4722
+ let noteCount = 0;
4723
+ let targetEntryIndex = -1;
4724
+ for (let i = 0; i < measure.entries.length; i++) {
4725
+ const entry = measure.entries[i];
4726
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4727
+ if (noteCount === noteIndex) {
4728
+ targetEntryIndex = i;
4729
+ break;
4730
+ }
4731
+ noteCount++;
4732
+ }
4733
+ }
4734
+ if (targetEntryIndex < 0) {
4735
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4736
+ }
4737
+ const targetEntry = measure.entries[targetEntryIndex];
4738
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
4739
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
4740
+ }
4741
+ const breathMarkIndex = targetEntry.notations.findIndex(
4742
+ (n) => n.type === "articulation" && n.articulation === "breath-mark"
4743
+ );
4744
+ if (breathMarkIndex < 0) {
4745
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No breath mark found on note`, { partIndex, measureIndex })]);
4746
+ }
4747
+ const result = cloneScore(score);
4748
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4749
+ resultNote.notations.splice(breathMarkIndex, 1);
4750
+ if (resultNote.notations.length === 0) {
4751
+ delete resultNote.notations;
4752
+ }
4753
+ return success(result);
4754
+ }
4755
+ function addCaesura(score, options) {
4756
+ const { partIndex, measureIndex, noteIndex, placement = "above" } = options;
4757
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4758
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4759
+ }
4760
+ const part = score.parts[partIndex];
4761
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4762
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4763
+ }
4764
+ const measure = part.measures[measureIndex];
4765
+ let noteCount = 0;
4766
+ let targetEntryIndex = -1;
4767
+ for (let i = 0; i < measure.entries.length; i++) {
4768
+ const entry = measure.entries[i];
4769
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4770
+ if (noteCount === noteIndex) {
4771
+ targetEntryIndex = i;
4772
+ break;
4773
+ }
4774
+ noteCount++;
4775
+ }
4776
+ }
4777
+ if (targetEntryIndex < 0) {
4778
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4779
+ }
4780
+ const result = cloneScore(score);
4781
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4782
+ if (!resultNote.notations) {
4783
+ resultNote.notations = [];
4784
+ }
4785
+ const existingCaesura = resultNote.notations.find(
4786
+ (n) => n.type === "articulation" && n.articulation === "caesura"
4787
+ );
4788
+ if (existingCaesura) {
4789
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Caesura already exists on note`, { partIndex, measureIndex })]);
4790
+ }
4791
+ resultNote.notations.push({
4792
+ type: "articulation",
4793
+ articulation: "caesura",
4794
+ placement
4795
+ });
4796
+ return success(result);
4797
+ }
4798
+ function removeCaesura(score, options) {
4799
+ const { partIndex, measureIndex, noteIndex } = options;
4800
+ if (partIndex < 0 || partIndex >= score.parts.length) {
4801
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
4802
+ }
4803
+ const part = score.parts[partIndex];
4804
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
4805
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4806
+ }
4807
+ const measure = part.measures[measureIndex];
4808
+ let noteCount = 0;
4809
+ let targetEntryIndex = -1;
4810
+ for (let i = 0; i < measure.entries.length; i++) {
4811
+ const entry = measure.entries[i];
4812
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
4813
+ if (noteCount === noteIndex) {
4814
+ targetEntryIndex = i;
4815
+ break;
4816
+ }
4817
+ noteCount++;
4818
+ }
4819
+ }
4820
+ if (targetEntryIndex < 0) {
4821
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4822
+ }
4823
+ const targetEntry = measure.entries[targetEntryIndex];
4824
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
4825
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
4826
+ }
4827
+ const caesuraIndex = targetEntry.notations.findIndex(
4828
+ (n) => n.type === "articulation" && n.articulation === "caesura"
4829
+ );
4830
+ if (caesuraIndex < 0) {
4831
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No caesura found on note`, { partIndex, measureIndex })]);
4832
+ }
4833
+ const result = cloneScore(score);
4834
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4835
+ resultNote.notations.splice(caesuraIndex, 1);
4836
+ if (resultNote.notations.length === 0) {
4837
+ delete resultNote.notations;
4838
+ }
4839
+ return success(result);
4840
+ }
1753
4841
  // Annotate the CommonJS export names for ESM import in node:
1754
4842
  0 && (module.exports = {
4843
+ addArticulation,
4844
+ addBeam,
4845
+ addBowing,
4846
+ addBreathMark,
4847
+ addCaesura,
1755
4848
  addChord,
1756
4849
  addChordNote,
1757
4850
  addChordNoteChecked,
4851
+ addCoda,
4852
+ addDaCapo,
4853
+ addDalSegno,
4854
+ addDynamics,
4855
+ addEnding,
4856
+ addFermata,
4857
+ addFine,
4858
+ addFingering,
4859
+ addGraceNote,
4860
+ addHarmony,
4861
+ addLyric,
1758
4862
  addNote,
1759
4863
  addNoteChecked,
4864
+ addOctaveShift,
4865
+ addOrnament,
1760
4866
  addPart,
4867
+ addPedal,
4868
+ addRehearsalMark,
4869
+ addRepeatBarline,
4870
+ addSegno,
4871
+ addSlur,
4872
+ addStringNumber,
4873
+ addTempo,
4874
+ addTextDirection,
4875
+ addTie,
4876
+ addToCoda,
1761
4877
  addVoice,
4878
+ addWedge,
4879
+ autoBeam,
4880
+ changeBarline,
1762
4881
  changeKey,
1763
4882
  changeNoteDuration,
1764
4883
  changeTime,
4884
+ convertToGrace,
4885
+ copyNotes,
4886
+ copyNotesMultiMeasure,
4887
+ createTuplet,
4888
+ cutNotes,
1765
4889
  deleteMeasure,
1766
4890
  deleteNote,
1767
4891
  deleteNoteChecked,
1768
4892
  duplicatePart,
4893
+ insertClefChange,
1769
4894
  insertMeasure,
1770
4895
  insertNote,
4896
+ lowerAccidental,
1771
4897
  modifyNoteDuration,
1772
4898
  modifyNoteDurationChecked,
1773
4899
  modifyNotePitch,
1774
4900
  modifyNotePitchChecked,
1775
4901
  moveNoteToStaff,
4902
+ pasteNotes,
4903
+ pasteNotesMultiMeasure,
4904
+ raiseAccidental,
4905
+ removeArticulation,
4906
+ removeBeam,
4907
+ removeBowing,
4908
+ removeBreathMark,
4909
+ removeCaesura,
4910
+ removeDynamics,
4911
+ removeEnding,
4912
+ removeFermata,
4913
+ removeFingering,
4914
+ removeGraceNote,
4915
+ removeHarmony,
4916
+ removeLyric,
1776
4917
  removeNote,
4918
+ removeOctaveShift,
4919
+ removeOrnament,
1777
4920
  removePart,
4921
+ removePedal,
4922
+ removeRepeatBarline,
4923
+ removeSlur,
4924
+ removeStringNumber,
4925
+ removeTempo,
4926
+ removeTie,
4927
+ removeTuplet,
4928
+ removeWedge,
1778
4929
  setNotePitch,
4930
+ setNotePitchBySemitone,
1779
4931
  setStaves,
4932
+ shiftNotePitch,
4933
+ stopOctaveShift,
1780
4934
  transpose,
1781
- transposeChecked
4935
+ transposeChecked,
4936
+ updateHarmony,
4937
+ updateLyric
1782
4938
  });