musicxml-io 0.2.8 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,18 +23,66 @@ __export(src_exports, {
23
23
  STEPS: () => STEPS,
24
24
  STEP_SEMITONES: () => STEP_SEMITONES,
25
25
  ValidationException: () => ValidationException,
26
+ addArticulation: () => addArticulation,
27
+ addBeam: () => addBeam,
28
+ addBowing: () => addBowing,
29
+ addBreathMark: () => addBreathMark,
30
+ addCaesura: () => addCaesura,
31
+ addChord: () => addChord,
26
32
  addChordNote: () => addChordNote,
33
+ addChordNoteChecked: () => addChordNoteChecked,
34
+ addChordSymbol: () => addChordSymbol,
35
+ addCoda: () => addCoda,
36
+ addDaCapo: () => addDaCapo,
37
+ addDalSegno: () => addDalSegno,
38
+ addDynamics: () => addDynamics,
39
+ addEnding: () => addEnding,
40
+ addFermata: () => addFermata,
41
+ addFine: () => addFine,
42
+ addFingering: () => addFingering,
43
+ addGraceNote: () => addGraceNote,
44
+ addHarmony: () => addHarmony,
45
+ addLyric: () => addLyric,
27
46
  addNote: () => addNote,
47
+ addNoteChecked: () => addNoteChecked,
48
+ addOctaveShift: () => addOctaveShift,
49
+ addOrnament: () => addOrnament,
50
+ addPart: () => addPart,
51
+ addPedal: () => addPedal,
52
+ addRehearsalMark: () => addRehearsalMark,
53
+ addRepeat: () => addRepeat,
54
+ addRepeatBarline: () => addRepeatBarline,
55
+ addSegno: () => addSegno,
56
+ addSlur: () => addSlur,
57
+ addStringNumber: () => addStringNumber,
58
+ addTempo: () => addTempo,
59
+ addText: () => addText,
60
+ addTextDirection: () => addTextDirection,
61
+ addTie: () => addTie,
62
+ addToCoda: () => addToCoda,
63
+ addVoice: () => addVoice,
64
+ addWedge: () => addWedge,
28
65
  assertMeasureValid: () => assertMeasureValid,
29
66
  assertValid: () => assertValid,
67
+ autoBeam: () => autoBeam,
30
68
  buildVoiceToStaffMap: () => buildVoiceToStaffMap,
31
69
  buildVoiceToStaffMapForPart: () => buildVoiceToStaffMapForPart,
70
+ changeBarline: () => changeBarline,
71
+ changeClef: () => changeClef,
32
72
  changeKey: () => changeKey,
73
+ changeNoteDuration: () => changeNoteDuration,
33
74
  changeTime: () => changeTime,
75
+ convertToGrace: () => convertToGrace,
76
+ copyNotes: () => copyNotes,
77
+ copyNotesMultiMeasure: () => copyNotesMultiMeasure,
34
78
  countNotes: () => countNotes,
79
+ createTuplet: () => createTuplet,
80
+ cutNotes: () => cutNotes,
35
81
  decodeBuffer: () => decodeBuffer,
36
82
  deleteMeasure: () => deleteMeasure,
37
83
  deleteNote: () => deleteNote,
84
+ deleteNoteChecked: () => deleteNoteChecked,
85
+ duplicatePart: () => duplicatePart,
38
86
  exportMidi: () => exportMidi,
39
87
  findBarlines: () => findBarlines,
40
88
  findDirectionsByType: () => findDirectionsByType,
@@ -128,7 +176,9 @@ __export(src_exports, {
128
176
  hasTieStop: () => hasTieStop,
129
177
  hasTuplet: () => hasTuplet,
130
178
  inferStaff: () => inferStaff,
179
+ insertClefChange: () => insertClefChange,
131
180
  insertMeasure: () => insertMeasure,
181
+ insertNote: () => insertNote,
132
182
  isChordNote: () => isChordNote,
133
183
  isCompressed: () => isCompressed,
134
184
  isCueNote: () => isCueNote,
@@ -141,19 +191,65 @@ __export(src_exports, {
141
191
  isValid: () => isValid,
142
192
  iterateEntries: () => iterateEntries,
143
193
  iterateNotes: () => iterateNotes,
194
+ lowerAccidental: () => lowerAccidental,
144
195
  measureRoundtrip: () => measureRoundtrip,
196
+ modifyDynamics: () => modifyDynamics,
145
197
  modifyNoteDuration: () => modifyNoteDuration,
198
+ modifyNoteDurationChecked: () => modifyNoteDurationChecked,
146
199
  modifyNotePitch: () => modifyNotePitch,
200
+ modifyNotePitchChecked: () => modifyNotePitchChecked,
201
+ modifyTempo: () => modifyTempo,
202
+ moveNoteToStaff: () => moveNoteToStaff,
147
203
  parse: () => parse,
148
204
  parseAuto: () => parseAuto,
149
205
  parseCompressed: () => parseCompressed,
150
206
  parseFile: () => parseFile,
207
+ pasteNotes: () => pasteNotes,
208
+ pasteNotesMultiMeasure: () => pasteNotesMultiMeasure,
151
209
  pitchToSemitone: () => pitchToSemitone,
210
+ raiseAccidental: () => raiseAccidental,
211
+ removeArticulation: () => removeArticulation,
212
+ removeBeam: () => removeBeam,
213
+ removeBowing: () => removeBowing,
214
+ removeBreathMark: () => removeBreathMark,
215
+ removeCaesura: () => removeCaesura,
216
+ removeChordSymbol: () => removeChordSymbol,
217
+ removeDynamics: () => removeDynamics,
218
+ removeEnding: () => removeEnding,
219
+ removeFermata: () => removeFermata,
220
+ removeFingering: () => removeFingering,
221
+ removeGraceNote: () => removeGraceNote,
222
+ removeHarmony: () => removeHarmony,
223
+ removeLyric: () => removeLyric,
224
+ removeNote: () => removeNote,
225
+ removeOctaveShift: () => removeOctaveShift,
226
+ removeOrnament: () => removeOrnament,
227
+ removePart: () => removePart,
228
+ removePedal: () => removePedal,
229
+ removeRepeat: () => removeRepeat,
230
+ removeRepeatBarline: () => removeRepeatBarline,
231
+ removeSlur: () => removeSlur,
232
+ removeStringNumber: () => removeStringNumber,
233
+ removeTempo: () => removeTempo,
234
+ removeTie: () => removeTie,
235
+ removeTuplet: () => removeTuplet,
236
+ removeWedge: () => removeWedge,
152
237
  scoresEqual: () => scoresEqual,
153
238
  serialize: () => serialize,
154
239
  serializeCompressed: () => serializeCompressed,
155
240
  serializeToFile: () => serializeToFile,
241
+ setBarline: () => setBarline,
242
+ setBeaming: () => setBeaming,
243
+ setNotePitch: () => setNotePitch,
244
+ setNotePitchBySemitone: () => setNotePitchBySemitone,
245
+ setStaves: () => setStaves,
246
+ shiftNotePitch: () => shiftNotePitch,
247
+ stopOctaveShift: () => stopOctaveShift,
156
248
  transpose: () => transpose,
249
+ transposeChecked: () => transposeChecked,
250
+ updateChordSymbol: () => updateChordSymbol,
251
+ updateHarmony: () => updateHarmony,
252
+ updateLyric: () => updateLyric,
157
253
  validate: () => validate,
158
254
  validateBackupForward: () => validateBackupForward,
159
255
  validateBeams: () => validateBeams,
@@ -3727,8 +3823,10 @@ function serializeSystemLayout(layout, indent) {
3727
3823
  }
3728
3824
  function serializeCredit(credit, indent) {
3729
3825
  const lines = [];
3730
- const pageAttr = credit.page !== void 0 ? ` page="${credit.page}"` : "";
3731
- lines.push(`${indent}<credit${pageAttr}>`);
3826
+ let attrs = "";
3827
+ if (credit._id) attrs += ` id="${escapeXml(credit._id)}"`;
3828
+ if (credit.page !== void 0) attrs += ` page="${credit.page}"`;
3829
+ lines.push(`${indent}<credit${attrs}>`);
3732
3830
  if (credit.creditType) {
3733
3831
  for (const ct of credit.creditType) {
3734
3832
  lines.push(`${indent}${indent}<credit-type>${escapeXml(ct)}</credit-type>`);
@@ -3736,19 +3834,19 @@ function serializeCredit(credit, indent) {
3736
3834
  }
3737
3835
  if (credit.creditWords) {
3738
3836
  for (const cw of credit.creditWords) {
3739
- let attrs = "";
3740
- if (cw.defaultX !== void 0) attrs += ` default-x="${cw.defaultX}"`;
3741
- if (cw.defaultY !== void 0) attrs += ` default-y="${cw.defaultY}"`;
3742
- if (cw.fontSize) attrs += ` font-size="${escapeXml(cw.fontSize)}"`;
3743
- if (cw.fontWeight) attrs += ` font-weight="${escapeXml(cw.fontWeight)}"`;
3744
- if (cw.fontStyle) attrs += ` font-style="${escapeXml(cw.fontStyle)}"`;
3745
- if (cw.justify) attrs += ` justify="${escapeXml(cw.justify)}"`;
3746
- if (cw.halign) attrs += ` halign="${escapeXml(cw.halign)}"`;
3747
- if (cw.valign) attrs += ` valign="${escapeXml(cw.valign)}"`;
3748
- if (cw.letterSpacing) attrs += ` letter-spacing="${escapeXml(cw.letterSpacing)}"`;
3749
- if (cw.xmlLang) attrs += ` xml:lang="${escapeXml(cw.xmlLang)}"`;
3750
- if (cw.xmlSpace) attrs += ` xml:space="${escapeXml(cw.xmlSpace)}"`;
3751
- lines.push(`${indent}${indent}<credit-words${attrs}>${escapeXml(cw.text)}</credit-words>`);
3837
+ let attrs2 = "";
3838
+ if (cw.defaultX !== void 0) attrs2 += ` default-x="${cw.defaultX}"`;
3839
+ if (cw.defaultY !== void 0) attrs2 += ` default-y="${cw.defaultY}"`;
3840
+ if (cw.fontSize) attrs2 += ` font-size="${escapeXml(cw.fontSize)}"`;
3841
+ if (cw.fontWeight) attrs2 += ` font-weight="${escapeXml(cw.fontWeight)}"`;
3842
+ if (cw.fontStyle) attrs2 += ` font-style="${escapeXml(cw.fontStyle)}"`;
3843
+ if (cw.justify) attrs2 += ` justify="${escapeXml(cw.justify)}"`;
3844
+ if (cw.halign) attrs2 += ` halign="${escapeXml(cw.halign)}"`;
3845
+ if (cw.valign) attrs2 += ` valign="${escapeXml(cw.valign)}"`;
3846
+ if (cw.letterSpacing) attrs2 += ` letter-spacing="${escapeXml(cw.letterSpacing)}"`;
3847
+ if (cw.xmlLang) attrs2 += ` xml:lang="${escapeXml(cw.xmlLang)}"`;
3848
+ if (cw.xmlSpace) attrs2 += ` xml:space="${escapeXml(cw.xmlSpace)}"`;
3849
+ lines.push(`${indent}${indent}<credit-words${attrs2}>${escapeXml(cw.text)}</credit-words>`);
3752
3850
  }
3753
3851
  }
3754
3852
  lines.push(`${indent}</credit>`);
@@ -3868,6 +3966,7 @@ function serializePartGroup(group, indent) {
3868
3966
  const lines = [];
3869
3967
  let attrs = ` type="${group.groupType}"`;
3870
3968
  if (group.number !== void 0) attrs += ` number="${group.number}"`;
3969
+ if (group._id) attrs += ` id="${escapeXml(group._id)}"`;
3871
3970
  lines.push(`${indent}<part-group${attrs}>`);
3872
3971
  if (group.groupName) {
3873
3972
  lines.push(`${indent} <group-name>${escapeXml(group.groupName)}</group-name>`);
@@ -3908,6 +4007,7 @@ function serializePart(part, indent) {
3908
4007
  function serializeMeasure(measure, indent) {
3909
4008
  const lines = [];
3910
4009
  let attrs = ` number="${measure.number}"`;
4010
+ if (measure._id) attrs += ` id="${escapeXml(measure._id)}"`;
3911
4011
  if (measure.width !== void 0) attrs += ` width="${measure.width}"`;
3912
4012
  if (measure.implicit) attrs += ` implicit="yes"`;
3913
4013
  lines.push(`${indent}<measure${attrs}>`);
@@ -3982,9 +4082,10 @@ function serializePrint(print, indent) {
3982
4082
  lines.push(`${indent}</print>`);
3983
4083
  return lines;
3984
4084
  }
3985
- function serializeAttributes(attrs, indent) {
4085
+ function serializeAttributes(attrs, indent, id) {
3986
4086
  const lines = [];
3987
- lines.push(`${indent}<attributes>`);
4087
+ const idAttr = id ? ` id="${escapeXml(id)}"` : "";
4088
+ lines.push(`${indent}<attributes${idAttr}>`);
3988
4089
  if (attrs.divisions !== void 0) {
3989
4090
  lines.push(`${indent} <divisions>${attrs.divisions}</divisions>`);
3990
4091
  }
@@ -4132,7 +4233,7 @@ function serializeEntry(entry, indent) {
4132
4233
  case "sound":
4133
4234
  return serializeSound(entry, indent);
4134
4235
  case "attributes":
4135
- return serializeAttributes(entry.attributes, indent);
4236
+ return serializeAttributes(entry.attributes, indent, entry._id);
4136
4237
  default:
4137
4238
  return [];
4138
4239
  }
@@ -4140,6 +4241,7 @@ function serializeEntry(entry, indent) {
4140
4241
  function serializeNote(note, indent) {
4141
4242
  const lines = [];
4142
4243
  const noteAttrs = buildAttrs({
4244
+ "id": note._id,
4143
4245
  "default-x": note.defaultX,
4144
4246
  "default-y": note.defaultY,
4145
4247
  "relative-x": note.relativeX,
@@ -4622,7 +4724,8 @@ function serializeBackup(backup, indent) {
4622
4724
  }
4623
4725
  function serializeForward(forward, indent) {
4624
4726
  const lines = [];
4625
- lines.push(`${indent}<forward>`);
4727
+ const idAttr = forward._id ? ` id="${escapeXml(forward._id)}"` : "";
4728
+ lines.push(`${indent}<forward${idAttr}>`);
4626
4729
  lines.push(`${indent} <duration>${forward.duration}</duration>`);
4627
4730
  if (forward.voice !== void 0) {
4628
4731
  lines.push(`${indent} <voice>${forward.voice}</voice>`);
@@ -4636,6 +4739,7 @@ function serializeForward(forward, indent) {
4636
4739
  function serializeDirection(direction, indent) {
4637
4740
  const lines = [];
4638
4741
  let attrs = "";
4742
+ if (direction._id) attrs += ` id="${escapeXml(direction._id)}"`;
4639
4743
  if (direction.placement) attrs += ` placement="${direction.placement}"`;
4640
4744
  if (direction.directive) attrs += ' directive="yes"';
4641
4745
  if (direction.system) attrs += ` system="${direction.system}"`;
@@ -4897,7 +5001,9 @@ function serializeDirectionType(dirType, indent) {
4897
5001
  }
4898
5002
  function serializeBarline(barline, indent) {
4899
5003
  const lines = [];
4900
- lines.push(`${indent}<barline location="${barline.location}">`);
5004
+ let attrs = ` location="${barline.location}"`;
5005
+ if (barline._id) attrs += ` id="${escapeXml(barline._id)}"`;
5006
+ lines.push(`${indent}<barline${attrs}>`);
4901
5007
  if (barline.barStyle) {
4902
5008
  lines.push(`${indent} <bar-style>${barline.barStyle}</bar-style>`);
4903
5009
  }
@@ -4990,6 +5096,7 @@ function serializeMeasureStyle(ms, indent) {
4990
5096
  function serializeHarmony(harmony, indent) {
4991
5097
  const lines = [];
4992
5098
  const attrs = buildAttrs({
5099
+ id: harmony._id,
4993
5100
  placement: harmony.placement,
4994
5101
  "print-frame": harmony.printFrame,
4995
5102
  "default-y": harmony.defaultY,
@@ -5067,6 +5174,7 @@ function serializeHarmony(harmony, indent) {
5067
5174
  function serializeFiguredBass(fb, indent) {
5068
5175
  const lines = [];
5069
5176
  let attrs = "";
5177
+ if (fb._id) attrs += ` id="${escapeXml(fb._id)}"`;
5070
5178
  if (fb.parentheses) attrs += ' parentheses="yes"';
5071
5179
  lines.push(`${indent}<figured-bass${attrs}>`);
5072
5180
  for (const fig of fb.figures) {
@@ -5098,6 +5206,7 @@ function serializeFiguredBass(fb, indent) {
5098
5206
  function serializeSound(sound, indent) {
5099
5207
  const lines = [];
5100
5208
  const attrs = [];
5209
+ if (sound._id) attrs.push(`id="${escapeXml(sound._id)}"`);
5101
5210
  if (sound.tempo !== void 0) attrs.push(`tempo="${sound.tempo}"`);
5102
5211
  if (sound.dynamics !== void 0) attrs.push(`dynamics="${sound.dynamics}"`);
5103
5212
  if (sound.dacapo) attrs.push('dacapo="yes"');
@@ -5129,7 +5238,7 @@ function serializeSound(sound, indent) {
5129
5238
  }
5130
5239
  lines.push(`${indent} </swing>`);
5131
5240
  lines.push(`${indent}</sound>`);
5132
- } else if (attrs.length === 0) {
5241
+ } else if (attrs.length === 0 && !sound._id) {
5133
5242
  lines.push(`${indent}<sound/>`);
5134
5243
  } else {
5135
5244
  lines.push(`${indent}<sound${attrStr}/>`);
@@ -5515,9 +5624,128 @@ var STEP_SEMITONES = {
5515
5624
  "A": 9,
5516
5625
  "B": 11
5517
5626
  };
5627
+ var SHARP_ORDER = ["F", "C", "G", "D", "A", "E", "B"];
5628
+ var FLAT_ORDER = ["B", "E", "A", "D", "G", "C", "F"];
5518
5629
  function pitchToSemitone(pitch) {
5519
5630
  return pitch.octave * 12 + STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0);
5520
5631
  }
5632
+ function getAlterForStepInKey(step, key) {
5633
+ const fifths = key.fifths;
5634
+ if (fifths > 0) {
5635
+ const sharps = SHARP_ORDER.slice(0, fifths);
5636
+ return sharps.includes(step) ? 1 : 0;
5637
+ } else if (fifths < 0) {
5638
+ const flats = FLAT_ORDER.slice(0, -fifths);
5639
+ return flats.includes(step) ? -1 : 0;
5640
+ }
5641
+ return 0;
5642
+ }
5643
+ function getAlteredStepsInKey(key) {
5644
+ const alterations = /* @__PURE__ */ new Map();
5645
+ const fifths = key.fifths;
5646
+ if (fifths > 0) {
5647
+ SHARP_ORDER.slice(0, fifths).forEach((step) => alterations.set(step, 1));
5648
+ } else if (fifths < 0) {
5649
+ FLAT_ORDER.slice(0, -fifths).forEach((step) => alterations.set(step, -1));
5650
+ }
5651
+ return alterations;
5652
+ }
5653
+ function getAccidentalsInMeasure(measure, upToPosition, voice) {
5654
+ const accidentals = /* @__PURE__ */ new Map();
5655
+ let position = 0;
5656
+ for (const entry of measure.entries) {
5657
+ if (position >= upToPosition) break;
5658
+ if (entry.type === "note") {
5659
+ if (voice === void 0 || entry.voice === voice) {
5660
+ if (entry.pitch && entry.accidental) {
5661
+ const key = `${entry.pitch.step}${entry.pitch.octave}`;
5662
+ accidentals.set(key, entry.pitch.alter ?? 0);
5663
+ }
5664
+ }
5665
+ if (!entry.chord) {
5666
+ position += entry.duration;
5667
+ }
5668
+ } else if (entry.type === "backup") {
5669
+ position -= entry.duration;
5670
+ } else if (entry.type === "forward") {
5671
+ position += entry.duration;
5672
+ }
5673
+ }
5674
+ return accidentals;
5675
+ }
5676
+ function semitoneToKeyAwarePitch(semitone, key, options) {
5677
+ const octave = Math.floor(semitone / 12);
5678
+ const pitchClass = (semitone % 12 + 12) % 12;
5679
+ const keyPreferSharp = key.fifths >= 0;
5680
+ const preferSharp = options?.preferSharp ?? keyPreferSharp;
5681
+ for (const step of STEPS) {
5682
+ const stepSemitone = STEP_SEMITONES[step];
5683
+ if (stepSemitone === pitchClass) {
5684
+ return { step, octave };
5685
+ }
5686
+ }
5687
+ const keyAlterations = getAlteredStepsInKey(key);
5688
+ for (const step of STEPS) {
5689
+ const stepSemitone = STEP_SEMITONES[step];
5690
+ const keyAlter = keyAlterations.get(step) ?? 0;
5691
+ if ((stepSemitone + keyAlter) % 12 === pitchClass) {
5692
+ return { step, octave, alter: keyAlter };
5693
+ }
5694
+ }
5695
+ if (preferSharp) {
5696
+ for (const step of STEPS) {
5697
+ const stepSemitone = STEP_SEMITONES[step];
5698
+ const diff = (pitchClass - stepSemitone + 12) % 12;
5699
+ if (diff === 1) {
5700
+ return { step, octave, alter: 1 };
5701
+ }
5702
+ }
5703
+ for (const step of STEPS) {
5704
+ const stepSemitone = STEP_SEMITONES[step];
5705
+ const diff = (pitchClass - stepSemitone + 12) % 12;
5706
+ if (diff === 2) {
5707
+ return { step, octave, alter: 2 };
5708
+ }
5709
+ }
5710
+ } else {
5711
+ for (const step of STEPS) {
5712
+ const stepSemitone = STEP_SEMITONES[step];
5713
+ const diff = (stepSemitone - pitchClass + 12) % 12;
5714
+ if (diff === 1) {
5715
+ return { step, octave, alter: -1 };
5716
+ }
5717
+ }
5718
+ for (const step of STEPS) {
5719
+ const stepSemitone = STEP_SEMITONES[step];
5720
+ const diff = (stepSemitone - pitchClass + 12) % 12;
5721
+ if (diff === 2) {
5722
+ return { step, octave, alter: -2 };
5723
+ }
5724
+ }
5725
+ }
5726
+ return { step: "C", octave, alter: pitchClass };
5727
+ }
5728
+ function determineAccidental(pitch, key, accidentalsInMeasure) {
5729
+ const noteKey = `${pitch.step}${pitch.octave}`;
5730
+ const alter = pitch.alter ?? 0;
5731
+ const keyAlter = getAlterForStepInKey(pitch.step, key);
5732
+ const previousAlter = accidentalsInMeasure.get(noteKey);
5733
+ if (previousAlter !== void 0) {
5734
+ if (alter === previousAlter) {
5735
+ return void 0;
5736
+ }
5737
+ } else {
5738
+ if (alter === keyAlter) {
5739
+ return void 0;
5740
+ }
5741
+ }
5742
+ if (alter === 0) return "natural";
5743
+ if (alter === 1) return "sharp";
5744
+ if (alter === -1) return "flat";
5745
+ if (alter === 2) return "double-sharp";
5746
+ if (alter === -2) return "double-flat";
5747
+ return void 0;
5748
+ }
5521
5749
  function createPositionState() {
5522
5750
  return { position: 0, lastNonChordPosition: 0 };
5523
5751
  }
@@ -7125,6 +7353,34 @@ function operationError(code, message, location = {}, details) {
7125
7353
  function cloneScore(score) {
7126
7354
  return JSON.parse(JSON.stringify(score));
7127
7355
  }
7356
+ function cloneNoteWithNewId(note) {
7357
+ const cloned = JSON.parse(JSON.stringify(note));
7358
+ cloned._id = generateId();
7359
+ return cloned;
7360
+ }
7361
+ function cloneEntryWithNewId(entry) {
7362
+ const cloned = JSON.parse(JSON.stringify(entry));
7363
+ cloned._id = generateId();
7364
+ return cloned;
7365
+ }
7366
+ function cloneMeasureWithNewIds(measure) {
7367
+ const cloned = JSON.parse(JSON.stringify(measure));
7368
+ cloned._id = generateId();
7369
+ cloned.entries = cloned.entries.map((entry) => cloneEntryWithNewId(entry));
7370
+ if (cloned.barlines) {
7371
+ cloned.barlines = cloned.barlines.map((barline) => ({
7372
+ ...barline,
7373
+ _id: generateId()
7374
+ }));
7375
+ }
7376
+ return cloned;
7377
+ }
7378
+ function clonePartWithNewIds(part) {
7379
+ const cloned = JSON.parse(JSON.stringify(part));
7380
+ cloned._id = generateId();
7381
+ cloned.measures = cloned.measures.map((measure) => cloneMeasureWithNewIds(measure));
7382
+ return cloned;
7383
+ }
7128
7384
  function getMeasureDuration(divisions, time) {
7129
7385
  const beats = parseInt(time.beats, 10);
7130
7386
  if (isNaN(beats)) return divisions * 4;
@@ -7566,6 +7822,189 @@ function setNotePitch(score, options) {
7566
7822
  }
7567
7823
  return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7568
7824
  }
7825
+ function setNotePitchBySemitone(score, options) {
7826
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7827
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7828
+ }
7829
+ const part = score.parts[options.partIndex];
7830
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7831
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7832
+ }
7833
+ const result = cloneScore(score);
7834
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7835
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
7836
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
7837
+ const keySignature = attrs.key ?? { fifths: 0 };
7838
+ let noteCount = 0;
7839
+ for (const entry of measure.entries) {
7840
+ if (entry.type === "note" && !entry.rest) {
7841
+ if (noteCount === options.noteIndex) {
7842
+ const notePosition = getAbsolutePositionForNote(entry, measure);
7843
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
7844
+ const newPitch = semitoneToKeyAwarePitch(options.semitone, keySignature, {
7845
+ preferSharp: options.preferSharp
7846
+ });
7847
+ const accidental = determineAccidental(newPitch, keySignature, accidentalsInMeasure);
7848
+ entry.pitch = newPitch;
7849
+ if (accidental) {
7850
+ entry.accidental = { value: accidental };
7851
+ } else {
7852
+ delete entry.accidental;
7853
+ }
7854
+ return success(result);
7855
+ }
7856
+ noteCount++;
7857
+ }
7858
+ }
7859
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7860
+ }
7861
+ function shiftNotePitch(score, options) {
7862
+ if (options.semitones === 0) {
7863
+ return success(score);
7864
+ }
7865
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7866
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7867
+ }
7868
+ const part = score.parts[options.partIndex];
7869
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7870
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7871
+ }
7872
+ const measure = part.measures[options.measureIndex];
7873
+ let noteCount = 0;
7874
+ let currentSemitone = null;
7875
+ for (const entry of measure.entries) {
7876
+ if (entry.type === "note" && !entry.rest) {
7877
+ if (noteCount === options.noteIndex) {
7878
+ if (!entry.pitch) {
7879
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7880
+ }
7881
+ currentSemitone = pitchToSemitone(entry.pitch);
7882
+ break;
7883
+ }
7884
+ noteCount++;
7885
+ }
7886
+ }
7887
+ if (currentSemitone === null) {
7888
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7889
+ }
7890
+ return setNotePitchBySemitone(score, {
7891
+ partIndex: options.partIndex,
7892
+ measureIndex: options.measureIndex,
7893
+ noteIndex: options.noteIndex,
7894
+ semitone: currentSemitone + options.semitones,
7895
+ preferSharp: options.preferSharp
7896
+ });
7897
+ }
7898
+ function raiseAccidental(score, options) {
7899
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7900
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7901
+ }
7902
+ const part = score.parts[options.partIndex];
7903
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7904
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7905
+ }
7906
+ const result = cloneScore(score);
7907
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7908
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
7909
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
7910
+ const keySignature = attrs.key ?? { fifths: 0 };
7911
+ let noteCount = 0;
7912
+ for (const entry of measure.entries) {
7913
+ if (entry.type === "note" && !entry.rest) {
7914
+ if (noteCount === options.noteIndex) {
7915
+ if (!entry.pitch) {
7916
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7917
+ }
7918
+ const currentAlter = entry.pitch.alter ?? 0;
7919
+ const newAlter = currentAlter + 1;
7920
+ if (newAlter > 2) {
7921
+ return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot raise accidental beyond double-sharp (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7922
+ }
7923
+ entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
7924
+ const notePosition = getAbsolutePositionForNote(entry, measure);
7925
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
7926
+ const accidental = determineAccidental(entry.pitch, keySignature, accidentalsInMeasure);
7927
+ if (accidental) {
7928
+ entry.accidental = { value: accidental };
7929
+ } else {
7930
+ delete entry.accidental;
7931
+ }
7932
+ return success(result);
7933
+ }
7934
+ noteCount++;
7935
+ }
7936
+ }
7937
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7938
+ }
7939
+ function lowerAccidental(score, options) {
7940
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7941
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7942
+ }
7943
+ const part = score.parts[options.partIndex];
7944
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7945
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7946
+ }
7947
+ const result = cloneScore(score);
7948
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7949
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
7950
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
7951
+ const keySignature = attrs.key ?? { fifths: 0 };
7952
+ let noteCount = 0;
7953
+ for (const entry of measure.entries) {
7954
+ if (entry.type === "note" && !entry.rest) {
7955
+ if (noteCount === options.noteIndex) {
7956
+ if (!entry.pitch) {
7957
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7958
+ }
7959
+ const currentAlter = entry.pitch.alter ?? 0;
7960
+ const newAlter = currentAlter - 1;
7961
+ if (newAlter < -2) {
7962
+ return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot lower accidental beyond double-flat (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7963
+ }
7964
+ entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
7965
+ const notePosition = getAbsolutePositionForNote(entry, measure);
7966
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
7967
+ const accidental = determineAccidental(entry.pitch, keySignature, accidentalsInMeasure);
7968
+ if (accidental) {
7969
+ entry.accidental = { value: accidental };
7970
+ } else {
7971
+ delete entry.accidental;
7972
+ }
7973
+ return success(result);
7974
+ }
7975
+ noteCount++;
7976
+ }
7977
+ }
7978
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7979
+ }
7980
+ function addVoice(score, options) {
7981
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7982
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7983
+ }
7984
+ const part = score.parts[options.partIndex];
7985
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7986
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7987
+ }
7988
+ const result = cloneScore(score);
7989
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7990
+ const existingVoiceEntries = getVoiceEntries(measure, options.voice, options.staff);
7991
+ if (existingVoiceEntries.length > 0) {
7992
+ return failure([operationError(
7993
+ "NOTE_CONFLICT",
7994
+ `Voice ${options.voice} already exists in this measure`,
7995
+ { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice }
7996
+ )]);
7997
+ }
7998
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
7999
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
8000
+ const rest = createRest(measureDuration, options.voice, options.staff);
8001
+ const currentEnd = getMeasureEndPosition(measure);
8002
+ if (currentEnd > 0) {
8003
+ measure.entries.push({ _id: generateId(), type: "backup", duration: currentEnd });
8004
+ }
8005
+ measure.entries.push(rest);
8006
+ return success(result);
8007
+ }
7569
8008
  function transposePitch(pitch, semitones) {
7570
8009
  const currentSemitone = STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0) + pitch.octave * 12;
7571
8010
  const targetSemitone = currentSemitone + semitones;
@@ -7605,6 +8044,163 @@ function transpose(score, semitones) {
7605
8044
  }
7606
8045
  return success(result);
7607
8046
  }
8047
+ function addPart(score, options) {
8048
+ if (score.parts.find((p) => p.id === options.id)) {
8049
+ return failure([operationError("DUPLICATE_PART_ID", `Part ID "${options.id}" already exists`, { partId: options.id })]);
8050
+ }
8051
+ const result = cloneScore(score);
8052
+ const insertIndex = options.insertIndex ?? result.parts.length;
8053
+ const partInfo = {
8054
+ _id: generateId(),
8055
+ type: "score-part",
8056
+ id: options.id,
8057
+ name: options.name,
8058
+ abbreviation: options.abbreviation
8059
+ };
8060
+ let partListInsertIndex = result.partList.length;
8061
+ let partCount = 0;
8062
+ for (let i = 0; i < result.partList.length; i++) {
8063
+ if (result.partList[i].type === "score-part") {
8064
+ if (partCount === insertIndex) {
8065
+ partListInsertIndex = i;
8066
+ break;
8067
+ }
8068
+ partCount++;
8069
+ }
8070
+ }
8071
+ result.partList.splice(partListInsertIndex, 0, partInfo);
8072
+ const measureCount = result.parts.length > 0 ? result.parts[0].measures.length : 1;
8073
+ const newPart = { _id: generateId(), id: options.id, measures: [] };
8074
+ for (let i = 0; i < measureCount; i++) {
8075
+ const measureNumber = result.parts.length > 0 ? result.parts[0].measures[i]?.number ?? String(i + 1) : String(i + 1);
8076
+ const measure = { _id: generateId(), number: measureNumber, entries: [] };
8077
+ if (i === 0) {
8078
+ measure.attributes = {
8079
+ divisions: options.divisions ?? 4,
8080
+ time: options.time ?? { beats: "4", beatType: 4 },
8081
+ key: options.key ?? { fifths: 0 },
8082
+ clef: options.clef ? [options.clef] : [{ sign: "G", line: 2 }]
8083
+ };
8084
+ }
8085
+ newPart.measures.push(measure);
8086
+ }
8087
+ result.parts.splice(insertIndex, 0, newPart);
8088
+ const validationResult = validate(result, { checkPartReferences: true, checkPartStructure: true });
8089
+ if (!validationResult.valid) {
8090
+ return failure(validationResult.errors);
8091
+ }
8092
+ return success(result, validationResult.warnings);
8093
+ }
8094
+ function removePart(score, partId) {
8095
+ const partIndex = score.parts.findIndex((p) => p.id === partId);
8096
+ if (partIndex === -1) {
8097
+ return failure([operationError("PART_NOT_FOUND", `Part "${partId}" not found`, { partId })]);
8098
+ }
8099
+ if (score.parts.length <= 1) {
8100
+ return failure([operationError("PART_NOT_FOUND", "Cannot remove the only remaining part", { partId })]);
8101
+ }
8102
+ const result = cloneScore(score);
8103
+ result.parts.splice(partIndex, 1);
8104
+ const partListIndex = result.partList.findIndex((e) => e.type === "score-part" && e.id === partId);
8105
+ if (partListIndex !== -1) {
8106
+ result.partList.splice(partListIndex, 1);
8107
+ }
8108
+ return success(result);
8109
+ }
8110
+ function duplicatePart(score, options) {
8111
+ const sourceIndex = score.parts.findIndex((p) => p.id === options.sourcePartId);
8112
+ if (sourceIndex === -1) {
8113
+ return failure([operationError("PART_NOT_FOUND", `Source part "${options.sourcePartId}" not found`, { partId: options.sourcePartId })]);
8114
+ }
8115
+ if (score.parts.find((p) => p.id === options.newPartId)) {
8116
+ return failure([operationError("DUPLICATE_PART_ID", `Part ID "${options.newPartId}" already exists`, { partId: options.newPartId })]);
8117
+ }
8118
+ const result = cloneScore(score);
8119
+ const sourcePart = result.parts[sourceIndex];
8120
+ const newPart = clonePartWithNewIds(sourcePart);
8121
+ newPart.id = options.newPartId;
8122
+ const sourcePartInfo = result.partList.find((e) => e.type === "score-part" && e.id === options.sourcePartId);
8123
+ const newPartInfo = {
8124
+ _id: generateId(),
8125
+ type: "score-part",
8126
+ id: options.newPartId,
8127
+ name: options.newPartName ?? sourcePartInfo?.name,
8128
+ abbreviation: sourcePartInfo?.abbreviation
8129
+ };
8130
+ result.parts.splice(sourceIndex + 1, 0, newPart);
8131
+ const partListSourceIndex = result.partList.findIndex((e) => e.type === "score-part" && e.id === options.sourcePartId);
8132
+ if (partListSourceIndex !== -1) {
8133
+ result.partList.splice(partListSourceIndex + 1, 0, newPartInfo);
8134
+ } else {
8135
+ result.partList.push(newPartInfo);
8136
+ }
8137
+ return success(result);
8138
+ }
8139
+ function setStaves(score, options) {
8140
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8141
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8142
+ }
8143
+ if (options.staves < 1) {
8144
+ return failure([operationError("INVALID_STAFF", `Staves count must be at least 1`, { partIndex: options.partIndex })]);
8145
+ }
8146
+ const result = cloneScore(score);
8147
+ const part = result.parts[options.partIndex];
8148
+ const fromMeasureIndex = options.fromMeasure ?? 0;
8149
+ const measure = part.measures[fromMeasureIndex];
8150
+ if (!measure) {
8151
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${fromMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: fromMeasureIndex })]);
8152
+ }
8153
+ if (!measure.attributes) {
8154
+ measure.attributes = {};
8155
+ }
8156
+ measure.attributes.staves = options.staves;
8157
+ if (options.clefs) {
8158
+ measure.attributes.clef = options.clefs;
8159
+ } else {
8160
+ const existingClefs = measure.attributes.clef ?? [];
8161
+ const newClefs = [...existingClefs];
8162
+ for (let staff = existingClefs.length + 1; staff <= options.staves; staff++) {
8163
+ newClefs.push(staff === 2 ? { sign: "F", line: 4, staff } : { sign: "G", line: 2, staff });
8164
+ }
8165
+ measure.attributes.clef = newClefs;
8166
+ }
8167
+ const validationResult = validate(result, { checkVoiceStaff: true, checkStaffStructure: true });
8168
+ if (!validationResult.valid) {
8169
+ return failure(validationResult.errors);
8170
+ }
8171
+ return success(result, validationResult.warnings);
8172
+ }
8173
+ function moveNoteToStaff(score, options) {
8174
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8175
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8176
+ }
8177
+ const part = score.parts[options.partIndex];
8178
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8179
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8180
+ }
8181
+ if (options.targetStaff < 1) {
8182
+ return failure([operationError("INVALID_STAFF", `Target staff must be at least 1`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8183
+ }
8184
+ const result = cloneScore(score);
8185
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8186
+ let noteCount = 0;
8187
+ for (const entry of measure.entries) {
8188
+ if (entry.type === "note" && !entry.rest) {
8189
+ if (noteCount === options.noteIndex) {
8190
+ entry.staff = options.targetStaff;
8191
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8192
+ const errors = validateMeasureLocal(measure, context, { checkVoiceStaff: true });
8193
+ const criticalErrors = errors.filter((e) => e.level === "error");
8194
+ if (criticalErrors.length > 0) {
8195
+ return failure(criticalErrors);
8196
+ }
8197
+ return success(result, errors.filter((e) => e.level !== "error"));
8198
+ }
8199
+ noteCount++;
8200
+ }
8201
+ }
8202
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8203
+ }
7608
8204
  function changeKey(score, key, options) {
7609
8205
  const result = cloneScore(score);
7610
8206
  const targetMeasure = String(options.fromMeasure);
@@ -7669,36 +8265,2839 @@ function deleteMeasure(score, measureNumber) {
7669
8265
  }
7670
8266
  return result;
7671
8267
  }
7672
- var addNote = (score, options) => {
7673
- const result = insertNote(score, {
7674
- partIndex: options.partIndex,
7675
- measureIndex: options.measureIndex,
7676
- voice: options.voice,
7677
- staff: options.staff,
7678
- position: options.position,
7679
- pitch: options.note.pitch ?? { step: "C", octave: 4 },
7680
- duration: options.note.duration,
7681
- noteType: options.note.noteType,
7682
- dots: options.note.dots
7683
- });
7684
- return result.success ? result.data : score;
7685
- };
7686
- var deleteNote = (score, options) => {
7687
- const result = removeNote(score, options);
7688
- return result.success ? result.data : score;
7689
- };
7690
- var addChordNote = (score, options) => {
7691
- const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
7692
- return result.success ? result.data : score;
7693
- };
7694
- var modifyNotePitch = (score, options) => {
7695
- const result = setNotePitch(score, options);
7696
- return result.success ? result.data : score;
7697
- };
7698
- var modifyNoteDuration = (score, options) => {
7699
- const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
7700
- return result.success ? result.data : score;
7701
- };
8268
+ function findNoteByIndex(measure, noteIndex) {
8269
+ let noteCount = 0;
8270
+ for (let i = 0; i < measure.entries.length; i++) {
8271
+ const entry = measure.entries[i];
8272
+ if (entry.type === "note" && !entry.rest) {
8273
+ if (noteCount === noteIndex) {
8274
+ return { note: entry, entryIndex: i };
8275
+ }
8276
+ noteCount++;
8277
+ }
8278
+ }
8279
+ return null;
8280
+ }
8281
+ function pitchesEqual2(p1, p2) {
8282
+ return p1.step === p2.step && p1.octave === p2.octave && (p1.alter ?? 0) === (p2.alter ?? 0);
8283
+ }
8284
+ function addTie(score, options) {
8285
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8286
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8287
+ }
8288
+ const part = score.parts[options.partIndex];
8289
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
8290
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8291
+ }
8292
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
8293
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8294
+ }
8295
+ const result = cloneScore(score);
8296
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
8297
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
8298
+ const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
8299
+ if (!startResult) {
8300
+ return failure([operationError("NOTE_NOT_FOUND", `Start note index ${options.startNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8301
+ }
8302
+ const endResult = findNoteByIndex(endMeasure, options.endNoteIndex);
8303
+ if (!endResult) {
8304
+ return failure([operationError("NOTE_NOT_FOUND", `End note index ${options.endNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8305
+ }
8306
+ const startNote = startResult.note;
8307
+ const endNote = endResult.note;
8308
+ if (!startNote.pitch || !endNote.pitch) {
8309
+ return failure([operationError("TIE_INVALID_TARGET", "Cannot tie notes without pitch", { partIndex: options.partIndex })]);
8310
+ }
8311
+ if (!pitchesEqual2(startNote.pitch, endNote.pitch)) {
8312
+ return failure([operationError("TIE_PITCH_MISMATCH", "Tied notes must have the same pitch", { partIndex: options.partIndex }, { startPitch: startNote.pitch, endPitch: endNote.pitch })]);
8313
+ }
8314
+ if (startNote.tie?.type === "start" || startNote.tie?.type === "continue") {
8315
+ return failure([operationError("TIE_ALREADY_EXISTS", "Start note already has a tie start", { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8316
+ }
8317
+ startNote.tie = { type: "start" };
8318
+ if (!startNote.notations) startNote.notations = [];
8319
+ startNote.notations.push({ type: "tied", tiedType: "start" });
8320
+ endNote.tie = { type: "stop" };
8321
+ if (!endNote.notations) endNote.notations = [];
8322
+ endNote.notations.push({ type: "tied", tiedType: "stop" });
8323
+ const validationResult = validate(result, { checkTies: true });
8324
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
8325
+ if (criticalErrors.length > 0) {
8326
+ return failure(criticalErrors);
8327
+ }
8328
+ return success(result, validationResult.warnings);
8329
+ }
8330
+ function removeTie(score, options) {
8331
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8332
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8333
+ }
8334
+ const part = score.parts[options.partIndex];
8335
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8336
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8337
+ }
8338
+ const result = cloneScore(score);
8339
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8340
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8341
+ if (!noteResult) {
8342
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8343
+ }
8344
+ const note = noteResult.note;
8345
+ if (!note.tie) {
8346
+ return failure([operationError("TIE_NOT_FOUND", "Note does not have a tie", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8347
+ }
8348
+ delete note.tie;
8349
+ delete note.ties;
8350
+ if (note.notations) {
8351
+ note.notations = note.notations.filter((n) => n.type !== "tied");
8352
+ if (note.notations.length === 0) {
8353
+ delete note.notations;
8354
+ }
8355
+ }
8356
+ return success(result);
8357
+ }
8358
+ function addSlur(score, options) {
8359
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8360
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8361
+ }
8362
+ const part = score.parts[options.partIndex];
8363
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
8364
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8365
+ }
8366
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
8367
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8368
+ }
8369
+ const result = cloneScore(score);
8370
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
8371
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
8372
+ const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
8373
+ if (!startResult) {
8374
+ return failure([operationError("NOTE_NOT_FOUND", `Start note index ${options.startNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8375
+ }
8376
+ const endResult = findNoteByIndex(endMeasure, options.endNoteIndex);
8377
+ if (!endResult) {
8378
+ return failure([operationError("NOTE_NOT_FOUND", `End note index ${options.endNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8379
+ }
8380
+ const startNote = startResult.note;
8381
+ const endNote = endResult.note;
8382
+ const slurNumber = options.number ?? 1;
8383
+ if (startNote.notations?.some((n) => n.type === "slur" && n.slurType === "start" && (n.number ?? 1) === slurNumber)) {
8384
+ return failure([operationError("SLUR_ALREADY_EXISTS", `Slur ${slurNumber} already starts on this note`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8385
+ }
8386
+ if (!startNote.notations) startNote.notations = [];
8387
+ startNote.notations.push({
8388
+ type: "slur",
8389
+ slurType: "start",
8390
+ number: slurNumber,
8391
+ placement: options.placement
8392
+ });
8393
+ if (!endNote.notations) endNote.notations = [];
8394
+ endNote.notations.push({
8395
+ type: "slur",
8396
+ slurType: "stop",
8397
+ number: slurNumber
8398
+ });
8399
+ const validationResult = validate(result, { checkSlurs: true });
8400
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
8401
+ if (criticalErrors.length > 0) {
8402
+ return failure(criticalErrors);
8403
+ }
8404
+ return success(result, validationResult.warnings);
8405
+ }
8406
+ function removeSlur(score, options) {
8407
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8408
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8409
+ }
8410
+ const part = score.parts[options.partIndex];
8411
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8412
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8413
+ }
8414
+ const result = cloneScore(score);
8415
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8416
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8417
+ if (!noteResult) {
8418
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8419
+ }
8420
+ const note = noteResult.note;
8421
+ const slurNumber = options.number ?? 1;
8422
+ if (!note.notations?.some((n) => n.type === "slur" && (n.number ?? 1) === slurNumber)) {
8423
+ return failure([operationError("SLUR_NOT_FOUND", `Slur ${slurNumber} not found on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8424
+ }
8425
+ note.notations = note.notations.filter((n) => !(n.type === "slur" && (n.number ?? 1) === slurNumber));
8426
+ if (note.notations.length === 0) {
8427
+ delete note.notations;
8428
+ }
8429
+ return success(result);
8430
+ }
8431
+ function addArticulation(score, options) {
8432
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8433
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8434
+ }
8435
+ const part = score.parts[options.partIndex];
8436
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8437
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8438
+ }
8439
+ const result = cloneScore(score);
8440
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8441
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8442
+ if (!noteResult) {
8443
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8444
+ }
8445
+ const note = noteResult.note;
8446
+ if (note.notations?.some((n) => n.type === "articulation" && n.articulation === options.articulation)) {
8447
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Articulation ${options.articulation} already exists on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8448
+ }
8449
+ if (!note.notations) note.notations = [];
8450
+ note.notations.push({
8451
+ type: "articulation",
8452
+ articulation: options.articulation,
8453
+ placement: options.placement
8454
+ });
8455
+ return success(result);
8456
+ }
8457
+ function removeArticulation(score, options) {
8458
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8459
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8460
+ }
8461
+ const part = score.parts[options.partIndex];
8462
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8463
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8464
+ }
8465
+ const result = cloneScore(score);
8466
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8467
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8468
+ if (!noteResult) {
8469
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8470
+ }
8471
+ const note = noteResult.note;
8472
+ if (!note.notations?.some((n) => n.type === "articulation" && n.articulation === options.articulation)) {
8473
+ return failure([operationError("ARTICULATION_NOT_FOUND", `Articulation ${options.articulation} not found on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8474
+ }
8475
+ note.notations = note.notations.filter((n) => !(n.type === "articulation" && n.articulation === options.articulation));
8476
+ if (note.notations.length === 0) {
8477
+ delete note.notations;
8478
+ }
8479
+ return success(result);
8480
+ }
8481
+ function getInsertPositionForDirection(measure, targetPosition) {
8482
+ let position = 0;
8483
+ let insertIndex = 0;
8484
+ for (let i = 0; i < measure.entries.length; i++) {
8485
+ const entry = measure.entries[i];
8486
+ if (position >= targetPosition) {
8487
+ return insertIndex;
8488
+ }
8489
+ if (entry.type === "note" && !entry.chord) {
8490
+ position += entry.duration;
8491
+ } else if (entry.type === "backup") {
8492
+ position -= entry.duration;
8493
+ } else if (entry.type === "forward") {
8494
+ position += entry.duration;
8495
+ }
8496
+ insertIndex = i + 1;
8497
+ }
8498
+ return insertIndex;
8499
+ }
8500
+ function addDynamics(score, options) {
8501
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8502
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8503
+ }
8504
+ const part = score.parts[options.partIndex];
8505
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8506
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8507
+ }
8508
+ if (options.position < 0) {
8509
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8510
+ }
8511
+ const result = cloneScore(score);
8512
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8513
+ const directionEntry = {
8514
+ _id: generateId(),
8515
+ type: "direction",
8516
+ directionTypes: [{
8517
+ kind: "dynamics",
8518
+ value: options.dynamics
8519
+ }],
8520
+ placement: options.placement ?? "below",
8521
+ staff: options.staff
8522
+ };
8523
+ const insertIndex = getInsertPositionForDirection(measure, options.position);
8524
+ measure.entries.splice(insertIndex, 0, directionEntry);
8525
+ return success(result);
8526
+ }
8527
+ function removeDynamics(score, options) {
8528
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8529
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8530
+ }
8531
+ const part = score.parts[options.partIndex];
8532
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8533
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8534
+ }
8535
+ const result = cloneScore(score);
8536
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8537
+ let directionCount = 0;
8538
+ let targetIndex = -1;
8539
+ for (let i = 0; i < measure.entries.length; i++) {
8540
+ const entry = measure.entries[i];
8541
+ if (entry.type === "direction") {
8542
+ const hasDynamics = entry.directionTypes.some((dt) => dt.kind === "dynamics");
8543
+ if (hasDynamics) {
8544
+ if (directionCount === options.directionIndex) {
8545
+ targetIndex = i;
8546
+ break;
8547
+ }
8548
+ directionCount++;
8549
+ }
8550
+ }
8551
+ }
8552
+ if (targetIndex === -1) {
8553
+ return failure([operationError("DYNAMICS_NOT_FOUND", `Dynamics direction index ${options.directionIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8554
+ }
8555
+ measure.entries.splice(targetIndex, 1);
8556
+ return success(result);
8557
+ }
8558
+ function modifyDynamics(score, options) {
8559
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8560
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8561
+ }
8562
+ const part = score.parts[options.partIndex];
8563
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8564
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8565
+ }
8566
+ const result = cloneScore(score);
8567
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8568
+ let dynamicsCount = 0;
8569
+ let targetIndex = -1;
8570
+ for (let i = 0; i < measure.entries.length; i++) {
8571
+ const entry = measure.entries[i];
8572
+ if (entry.type === "direction") {
8573
+ const hasDynamics = entry.directionTypes.some((dt) => dt.kind === "dynamics");
8574
+ if (hasDynamics) {
8575
+ if (dynamicsCount === options.directionIndex) {
8576
+ targetIndex = i;
8577
+ break;
8578
+ }
8579
+ dynamicsCount++;
8580
+ }
8581
+ }
8582
+ }
8583
+ if (targetIndex === -1) {
8584
+ return failure([operationError("DYNAMICS_NOT_FOUND", `Dynamics direction index ${options.directionIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8585
+ }
8586
+ const direction = measure.entries[targetIndex];
8587
+ const dynamicsType = direction.directionTypes.find((dt) => dt.kind === "dynamics");
8588
+ if (dynamicsType && dynamicsType.kind === "dynamics") {
8589
+ dynamicsType.value = options.dynamics;
8590
+ }
8591
+ if (options.placement !== void 0) {
8592
+ direction.placement = options.placement;
8593
+ }
8594
+ return success(result);
8595
+ }
8596
+ function insertClefChange(score, options) {
8597
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8598
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8599
+ }
8600
+ const part = score.parts[options.partIndex];
8601
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8602
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8603
+ }
8604
+ if (options.position < 0) {
8605
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8606
+ }
8607
+ const validSigns = ["G", "F", "C", "percussion", "TAB"];
8608
+ if (!validSigns.includes(options.clef.sign)) {
8609
+ return failure([operationError("INVALID_CLEF", `Invalid clef sign: ${options.clef.sign}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8610
+ }
8611
+ const result = cloneScore(score);
8612
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8613
+ if (options.position === 0) {
8614
+ if (!measure.attributes) {
8615
+ measure.attributes = {};
8616
+ }
8617
+ const staff = options.clef.staff ?? 1;
8618
+ if (!measure.attributes.clef) {
8619
+ measure.attributes.clef = [];
8620
+ }
8621
+ const existingIndex = measure.attributes.clef.findIndex((c) => (c.staff ?? 1) === staff);
8622
+ if (existingIndex >= 0) {
8623
+ measure.attributes.clef[existingIndex] = options.clef;
8624
+ } else {
8625
+ measure.attributes.clef.push(options.clef);
8626
+ }
8627
+ } else {
8628
+ const attributesEntry = {
8629
+ _id: generateId(),
8630
+ type: "attributes",
8631
+ attributes: {
8632
+ clef: [options.clef]
8633
+ }
8634
+ };
8635
+ const insertIndex = getInsertPositionForDirection(measure, options.position);
8636
+ measure.entries.splice(insertIndex, 0, attributesEntry);
8637
+ }
8638
+ const validationResult = validate(result, { checkStaffStructure: true });
8639
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
8640
+ if (criticalErrors.length > 0) {
8641
+ return failure(criticalErrors);
8642
+ }
8643
+ return success(result, validationResult.warnings);
8644
+ }
8645
+ var addNote = (score, options) => {
8646
+ const result = insertNote(score, {
8647
+ partIndex: options.partIndex,
8648
+ measureIndex: options.measureIndex,
8649
+ voice: options.voice,
8650
+ staff: options.staff,
8651
+ position: options.position,
8652
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
8653
+ duration: options.note.duration,
8654
+ noteType: options.note.noteType,
8655
+ dots: options.note.dots
8656
+ });
8657
+ return result.success ? result.data : score;
8658
+ };
8659
+ var deleteNote = (score, options) => {
8660
+ const result = removeNote(score, options);
8661
+ return result.success ? result.data : score;
8662
+ };
8663
+ var addChordNote = (score, options) => {
8664
+ const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
8665
+ return result.success ? result.data : score;
8666
+ };
8667
+ var modifyNotePitch = (score, options) => {
8668
+ const result = setNotePitch(score, options);
8669
+ return result.success ? result.data : score;
8670
+ };
8671
+ var modifyNoteDuration = (score, options) => {
8672
+ const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
8673
+ return result.success ? result.data : score;
8674
+ };
8675
+ var addNoteChecked = (score, options) => {
8676
+ return insertNote(score, {
8677
+ partIndex: options.partIndex,
8678
+ measureIndex: options.measureIndex,
8679
+ voice: options.voice,
8680
+ staff: options.staff,
8681
+ position: options.position,
8682
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
8683
+ duration: options.note.duration,
8684
+ noteType: options.note.noteType,
8685
+ dots: options.note.dots
8686
+ });
8687
+ };
8688
+ var deleteNoteChecked = removeNote;
8689
+ var addChordNoteChecked = (score, options) => {
8690
+ return addChord(score, { ...options, noteIndex: options.afterNoteIndex });
8691
+ };
8692
+ var modifyNotePitchChecked = setNotePitch;
8693
+ var modifyNoteDurationChecked = (score, options) => {
8694
+ return changeNoteDuration(score, { ...options, newDuration: options.duration });
8695
+ };
8696
+ var transposeChecked = transpose;
8697
+ function createTuplet(score, options) {
8698
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8699
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8700
+ }
8701
+ const part = score.parts[options.partIndex];
8702
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8703
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8704
+ }
8705
+ if (options.noteCount < 2) {
8706
+ return failure([operationError("INVALID_DURATION", "Tuplet must contain at least 2 notes", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8707
+ }
8708
+ if (options.actualNotes < 2 || options.normalNotes < 1) {
8709
+ return failure([operationError("INVALID_DURATION", "Invalid tuplet ratio", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8710
+ }
8711
+ const result = cloneScore(score);
8712
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8713
+ const notes = [];
8714
+ let noteCount = 0;
8715
+ for (let i = 0; i < measure.entries.length; i++) {
8716
+ const entry = measure.entries[i];
8717
+ if (entry.type === "note" && !entry.rest && !entry.chord) {
8718
+ if (noteCount >= options.startNoteIndex && noteCount < options.startNoteIndex + options.noteCount) {
8719
+ notes.push({ note: entry, entryIndex: i });
8720
+ }
8721
+ noteCount++;
8722
+ }
8723
+ }
8724
+ if (notes.length !== options.noteCount) {
8725
+ return failure([operationError("NOTE_NOT_FOUND", `Could not find ${options.noteCount} notes starting at index ${options.startNoteIndex}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8726
+ }
8727
+ const voice = notes[0].note.voice;
8728
+ const staff = notes[0].note.staff;
8729
+ if (!notes.every((n) => n.note.voice === voice)) {
8730
+ return failure([operationError("NOTE_CONFLICT", "All notes in a tuplet must be in the same voice", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice })]);
8731
+ }
8732
+ if (!notes.every((n) => n.note.staff === staff)) {
8733
+ return failure([operationError("NOTE_CONFLICT", "All notes in a tuplet must be on the same staff", { partIndex: options.partIndex, measureIndex: options.measureIndex, staff })]);
8734
+ }
8735
+ const tupletNumber = 1;
8736
+ for (let i = 0; i < notes.length; i++) {
8737
+ const { note } = notes[i];
8738
+ note.timeModification = {
8739
+ actualNotes: options.actualNotes,
8740
+ normalNotes: options.normalNotes
8741
+ };
8742
+ if (!note.notations) note.notations = [];
8743
+ if (i === 0) {
8744
+ note.notations.push({
8745
+ type: "tuplet",
8746
+ tupletType: "start",
8747
+ number: tupletNumber,
8748
+ bracket: options.bracket ?? true,
8749
+ showNumber: options.showNumber ?? "actual",
8750
+ tupletActual: { tupletNumber: options.actualNotes },
8751
+ tupletNormal: { tupletNumber: options.normalNotes }
8752
+ });
8753
+ } else if (i === notes.length - 1) {
8754
+ note.notations.push({
8755
+ type: "tuplet",
8756
+ tupletType: "stop",
8757
+ number: tupletNumber
8758
+ });
8759
+ }
8760
+ }
8761
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8762
+ const errors = validateMeasureLocal(measure, context, {
8763
+ checkTuplets: true,
8764
+ checkMeasureDuration: true
8765
+ });
8766
+ const criticalErrors = errors.filter((e) => e.level === "error");
8767
+ if (criticalErrors.length > 0) {
8768
+ return failure(criticalErrors);
8769
+ }
8770
+ return success(result, errors.filter((e) => e.level !== "error"));
8771
+ }
8772
+ function removeTuplet(score, options) {
8773
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8774
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8775
+ }
8776
+ const part = score.parts[options.partIndex];
8777
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8778
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8779
+ }
8780
+ const result = cloneScore(score);
8781
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8782
+ let noteCount = 0;
8783
+ let targetNote = null;
8784
+ let targetEntryIndex = -1;
8785
+ for (let i = 0; i < measure.entries.length; i++) {
8786
+ const entry = measure.entries[i];
8787
+ if (entry.type === "note" && !entry.rest) {
8788
+ if (noteCount === options.noteIndex) {
8789
+ targetNote = entry;
8790
+ targetEntryIndex = i;
8791
+ break;
8792
+ }
8793
+ noteCount++;
8794
+ }
8795
+ }
8796
+ if (!targetNote || targetEntryIndex === -1) {
8797
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8798
+ }
8799
+ if (!targetNote.timeModification) {
8800
+ return failure([operationError("NOTE_NOT_FOUND", "Note is not part of a tuplet", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8801
+ }
8802
+ const voice = targetNote.voice;
8803
+ const staff = targetNote.staff;
8804
+ const actualNotes = targetNote.timeModification.actualNotes;
8805
+ const normalNotes = targetNote.timeModification.normalNotes;
8806
+ const tupletNotes = [];
8807
+ let inTuplet = false;
8808
+ let currentTupletNumber;
8809
+ for (const entry of measure.entries) {
8810
+ if (entry.type !== "note" || entry.rest) continue;
8811
+ if (entry.voice !== voice || entry.staff !== staff) continue;
8812
+ const hasSameTimeModification = entry.timeModification?.actualNotes === actualNotes && entry.timeModification?.normalNotes === normalNotes;
8813
+ const tupletStart = entry.notations?.find(
8814
+ (n) => n.type === "tuplet" && n.tupletType === "start"
8815
+ );
8816
+ const tupletStop = entry.notations?.find(
8817
+ (n) => n.type === "tuplet" && n.tupletType === "stop" && (currentTupletNumber === void 0 || n.number === currentTupletNumber)
8818
+ );
8819
+ if (tupletStart && tupletStart.type === "tuplet") {
8820
+ inTuplet = true;
8821
+ currentTupletNumber = tupletStart.number;
8822
+ }
8823
+ if (inTuplet && hasSameTimeModification) {
8824
+ tupletNotes.push(entry);
8825
+ }
8826
+ if (tupletStop && inTuplet) {
8827
+ if (tupletNotes.includes(targetNote)) {
8828
+ break;
8829
+ } else {
8830
+ tupletNotes.length = 0;
8831
+ inTuplet = false;
8832
+ currentTupletNumber = void 0;
8833
+ }
8834
+ }
8835
+ }
8836
+ if (tupletNotes.length === 0) {
8837
+ tupletNotes.push(targetNote);
8838
+ }
8839
+ for (const note of tupletNotes) {
8840
+ delete note.timeModification;
8841
+ if (note.notations) {
8842
+ note.notations = note.notations.filter((n) => n.type !== "tuplet");
8843
+ if (note.notations.length === 0) {
8844
+ delete note.notations;
8845
+ }
8846
+ }
8847
+ }
8848
+ return success(result);
8849
+ }
8850
+ function addBeam(score, options) {
8851
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8852
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8853
+ }
8854
+ const part = score.parts[options.partIndex];
8855
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8856
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8857
+ }
8858
+ if (options.noteCount < 2) {
8859
+ return failure([operationError("INVALID_DURATION", "Beam must contain at least 2 notes", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8860
+ }
8861
+ const result = cloneScore(score);
8862
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8863
+ const beamLevel = options.beamLevel ?? 1;
8864
+ const notes = [];
8865
+ let noteCount = 0;
8866
+ for (const entry of measure.entries) {
8867
+ if (entry.type === "note" && !entry.rest && !entry.chord) {
8868
+ if (noteCount >= options.startNoteIndex && noteCount < options.startNoteIndex + options.noteCount) {
8869
+ notes.push(entry);
8870
+ }
8871
+ noteCount++;
8872
+ }
8873
+ }
8874
+ if (notes.length !== options.noteCount) {
8875
+ return failure([operationError("NOTE_NOT_FOUND", `Could not find ${options.noteCount} notes starting at index ${options.startNoteIndex}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8876
+ }
8877
+ const voice = notes[0].voice;
8878
+ if (!notes.every((n) => n.voice === voice)) {
8879
+ return failure([operationError("NOTE_CONFLICT", "All beamed notes must be in the same voice", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice })]);
8880
+ }
8881
+ for (let i = 0; i < notes.length; i++) {
8882
+ const note = notes[i];
8883
+ if (!note.beam) {
8884
+ note.beam = [];
8885
+ }
8886
+ note.beam = note.beam.filter((b) => b.number !== beamLevel);
8887
+ let beamType;
8888
+ if (i === 0) {
8889
+ beamType = "begin";
8890
+ } else if (i === notes.length - 1) {
8891
+ beamType = "end";
8892
+ } else {
8893
+ beamType = "continue";
8894
+ }
8895
+ note.beam.push({
8896
+ number: beamLevel,
8897
+ type: beamType
8898
+ });
8899
+ }
8900
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8901
+ const errors = validateMeasureLocal(measure, context, { checkBeams: true });
8902
+ const criticalErrors = errors.filter((e) => e.level === "error");
8903
+ if (criticalErrors.length > 0) {
8904
+ return failure(criticalErrors);
8905
+ }
8906
+ return success(result, errors.filter((e) => e.level !== "error"));
8907
+ }
8908
+ function removeBeam(score, options) {
8909
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8910
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8911
+ }
8912
+ const part = score.parts[options.partIndex];
8913
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8914
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8915
+ }
8916
+ const result = cloneScore(score);
8917
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8918
+ let noteCount = 0;
8919
+ let targetNote = null;
8920
+ for (const entry of measure.entries) {
8921
+ if (entry.type === "note" && !entry.rest) {
8922
+ if (noteCount === options.noteIndex) {
8923
+ targetNote = entry;
8924
+ break;
8925
+ }
8926
+ noteCount++;
8927
+ }
8928
+ }
8929
+ if (!targetNote) {
8930
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8931
+ }
8932
+ if (!targetNote.beam || targetNote.beam.length === 0) {
8933
+ return failure([operationError("NOTE_NOT_FOUND", "Note is not part of a beam group", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8934
+ }
8935
+ const voice = targetNote.voice;
8936
+ const staff = targetNote.staff;
8937
+ const beamNotes = [];
8938
+ let inBeam = false;
8939
+ const targetBeamLevel = options.beamLevel ?? targetNote.beam[0]?.number ?? 1;
8940
+ for (const entry of measure.entries) {
8941
+ if (entry.type !== "note" || entry.rest) continue;
8942
+ if (entry.voice !== voice || entry.staff !== staff) continue;
8943
+ const beamInfo = entry.beam?.find((b) => b.number === targetBeamLevel);
8944
+ if (!beamInfo) {
8945
+ if (inBeam) {
8946
+ break;
8947
+ }
8948
+ continue;
8949
+ }
8950
+ if (beamInfo.type === "begin") {
8951
+ inBeam = true;
8952
+ beamNotes.push(entry);
8953
+ } else if (beamInfo.type === "continue") {
8954
+ if (inBeam) beamNotes.push(entry);
8955
+ } else if (beamInfo.type === "end") {
8956
+ beamNotes.push(entry);
8957
+ if (beamNotes.includes(targetNote)) {
8958
+ break;
8959
+ } else {
8960
+ beamNotes.length = 0;
8961
+ inBeam = false;
8962
+ }
8963
+ }
8964
+ }
8965
+ if (beamNotes.length === 0) {
8966
+ beamNotes.push(targetNote);
8967
+ }
8968
+ for (const note of beamNotes) {
8969
+ if (note.beam) {
8970
+ if (options.beamLevel !== void 0) {
8971
+ note.beam = note.beam.filter((b) => b.number !== options.beamLevel);
8972
+ } else {
8973
+ note.beam = [];
8974
+ }
8975
+ if (note.beam.length === 0) {
8976
+ delete note.beam;
8977
+ }
8978
+ }
8979
+ }
8980
+ return success(result);
8981
+ }
8982
+ function autoBeam(score, options) {
8983
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8984
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8985
+ }
8986
+ const part = score.parts[options.partIndex];
8987
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8988
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8989
+ }
8990
+ const result = cloneScore(score);
8991
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8992
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8993
+ const divisions = context.divisions;
8994
+ const time = context.time ?? { beats: "4", beatType: 4 };
8995
+ const beatDuration = 4 / time.beatType * divisions;
8996
+ for (const entry of measure.entries) {
8997
+ if (entry.type === "note") {
8998
+ delete entry.beam;
8999
+ }
9000
+ }
9001
+ const notesByVoice = /* @__PURE__ */ new Map();
9002
+ let position = 0;
9003
+ for (const entry of measure.entries) {
9004
+ if (entry.type === "note") {
9005
+ if (!entry.chord && !entry.rest) {
9006
+ const voice = entry.voice;
9007
+ if (options.voice === void 0 || voice === options.voice) {
9008
+ if (!notesByVoice.has(voice)) {
9009
+ notesByVoice.set(voice, []);
9010
+ }
9011
+ notesByVoice.get(voice).push({ note: entry, position });
9012
+ }
9013
+ }
9014
+ if (!entry.chord) {
9015
+ position += entry.duration;
9016
+ }
9017
+ } else if (entry.type === "backup") {
9018
+ position -= entry.duration;
9019
+ } else if (entry.type === "forward") {
9020
+ position += entry.duration;
9021
+ }
9022
+ }
9023
+ for (const [, notes] of notesByVoice) {
9024
+ const beatGroups = [];
9025
+ let currentBeat = -1;
9026
+ let currentGroup = [];
9027
+ for (const { note, position: notePos } of notes) {
9028
+ if (note.duration > beatDuration / 2) {
9029
+ if (currentGroup.length >= 2) {
9030
+ beatGroups.push(currentGroup);
9031
+ }
9032
+ currentGroup = [];
9033
+ currentBeat = -1;
9034
+ continue;
9035
+ }
9036
+ const beat = Math.floor(notePos / beatDuration);
9037
+ if (options.groupByBeat !== false && beat !== currentBeat) {
9038
+ if (currentGroup.length >= 2) {
9039
+ beatGroups.push(currentGroup);
9040
+ }
9041
+ currentGroup = [{ note, position: notePos }];
9042
+ currentBeat = beat;
9043
+ } else {
9044
+ currentGroup.push({ note, position: notePos });
9045
+ }
9046
+ }
9047
+ if (currentGroup.length >= 2) {
9048
+ beatGroups.push(currentGroup);
9049
+ }
9050
+ for (const group of beatGroups) {
9051
+ for (let i = 0; i < group.length; i++) {
9052
+ const { note } = group[i];
9053
+ if (!note.beam) {
9054
+ note.beam = [];
9055
+ }
9056
+ let beamType;
9057
+ if (i === 0) {
9058
+ beamType = "begin";
9059
+ } else if (i === group.length - 1) {
9060
+ beamType = "end";
9061
+ } else {
9062
+ beamType = "continue";
9063
+ }
9064
+ note.beam.push({
9065
+ number: 1,
9066
+ type: beamType
9067
+ });
9068
+ }
9069
+ }
9070
+ }
9071
+ const errors = validateMeasureLocal(measure, context, { checkBeams: true });
9072
+ const criticalErrors = errors.filter((e) => e.level === "error");
9073
+ if (criticalErrors.length > 0) {
9074
+ return failure(criticalErrors);
9075
+ }
9076
+ return success(result, errors.filter((e) => e.level !== "error"));
9077
+ }
9078
+ function copyNotes(score, options) {
9079
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9080
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9081
+ }
9082
+ const part = score.parts[options.partIndex];
9083
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9084
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9085
+ }
9086
+ if (options.startPosition >= options.endPosition) {
9087
+ return failure([operationError("INVALID_POSITION", "Start position must be less than end position", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9088
+ }
9089
+ const measure = part.measures[options.measureIndex];
9090
+ const copiedNotes = [];
9091
+ let position = 0;
9092
+ for (const entry of measure.entries) {
9093
+ if (entry.type === "note") {
9094
+ if (entry.voice === options.voice && (options.staff === void 0 || (entry.staff ?? 1) === options.staff)) {
9095
+ if (!entry.chord) {
9096
+ const noteEnd = position + entry.duration;
9097
+ if (position < options.endPosition && noteEnd > options.startPosition) {
9098
+ const clonedNote = cloneNoteWithNewId(entry);
9099
+ if (clonedNote.tie) {
9100
+ }
9101
+ copiedNotes.push({
9102
+ relativePosition: position - options.startPosition,
9103
+ note: clonedNote
9104
+ });
9105
+ }
9106
+ position += entry.duration;
9107
+ } else {
9108
+ if (copiedNotes.length > 0) {
9109
+ const lastCopied = copiedNotes[copiedNotes.length - 1];
9110
+ if (lastCopied.note.voice === entry.voice && (options.staff === void 0 || (lastCopied.note.staff ?? 1) === (entry.staff ?? 1))) {
9111
+ const clonedNote = cloneNoteWithNewId(entry);
9112
+ copiedNotes.push({
9113
+ relativePosition: lastCopied.relativePosition,
9114
+ note: clonedNote
9115
+ });
9116
+ }
9117
+ }
9118
+ }
9119
+ } else if (!entry.chord) {
9120
+ position += entry.duration;
9121
+ }
9122
+ } else if (entry.type === "backup") {
9123
+ position -= entry.duration;
9124
+ } else if (entry.type === "forward") {
9125
+ position += entry.duration;
9126
+ }
9127
+ }
9128
+ if (copiedNotes.length === 0) {
9129
+ return failure([operationError("NOTE_NOT_FOUND", "No notes found in the specified range", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice })]);
9130
+ }
9131
+ const selection = {
9132
+ source: {
9133
+ partIndex: options.partIndex,
9134
+ measureIndex: options.measureIndex,
9135
+ startPosition: options.startPosition,
9136
+ endPosition: options.endPosition,
9137
+ voice: options.voice,
9138
+ staff: options.staff
9139
+ },
9140
+ notes: copiedNotes,
9141
+ duration: options.endPosition - options.startPosition
9142
+ };
9143
+ return success(selection);
9144
+ }
9145
+ function pasteNotes(score, options) {
9146
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9147
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9148
+ }
9149
+ const part = score.parts[options.partIndex];
9150
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9151
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9152
+ }
9153
+ if (options.position < 0) {
9154
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9155
+ }
9156
+ const result = cloneScore(score);
9157
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9158
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
9159
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
9160
+ const targetVoice = options.voice ?? options.selection.source.voice;
9161
+ const targetStaff = options.staff ?? options.selection.source.staff;
9162
+ const pasteEnd = options.position + options.selection.duration;
9163
+ if (pasteEnd > measureDuration) {
9164
+ return failure([operationError(
9165
+ "EXCEEDS_MEASURE",
9166
+ `Paste would exceed measure duration (ends at ${pasteEnd}, measure is ${measureDuration})`,
9167
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
9168
+ { pasteEnd, measureDuration }
9169
+ )]);
9170
+ }
9171
+ const voiceEntries = getVoiceEntries(measure, targetVoice, targetStaff);
9172
+ if (options.overwrite !== false) {
9173
+ const entriesToKeep = voiceEntries.filter((e) => {
9174
+ if (e.entry.type !== "note") return true;
9175
+ const note = e.entry;
9176
+ if (note.rest) return true;
9177
+ return e.endPosition <= options.position || e.position >= pasteEnd;
9178
+ });
9179
+ const newEntries = [];
9180
+ for (const { position, entry } of entriesToKeep) {
9181
+ if (entry.type === "note") {
9182
+ newEntries.push({ position, entry });
9183
+ }
9184
+ }
9185
+ for (const { relativePosition, note } of options.selection.notes) {
9186
+ const pastePosition = options.position + Math.max(0, relativePosition);
9187
+ const newNote = cloneNoteWithNewId(note);
9188
+ newNote.voice = targetVoice;
9189
+ if (targetStaff !== void 0) {
9190
+ newNote.staff = targetStaff;
9191
+ }
9192
+ delete newNote.tie;
9193
+ delete newNote.ties;
9194
+ if (newNote.notations) {
9195
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
9196
+ if (newNote.notations.length === 0) {
9197
+ delete newNote.notations;
9198
+ }
9199
+ }
9200
+ newEntries.push({ position: pastePosition, entry: newNote });
9201
+ }
9202
+ measure.entries = rebuildMeasureWithVoice(
9203
+ measure,
9204
+ targetVoice,
9205
+ newEntries,
9206
+ measureDuration,
9207
+ targetStaff
9208
+ );
9209
+ } else {
9210
+ const { hasNotes: hasNotes2, conflictingNotes } = hasNotesInRange(voiceEntries, options.position, pasteEnd);
9211
+ if (hasNotes2) {
9212
+ return failure([operationError(
9213
+ "NOTE_CONFLICT",
9214
+ `Paste range ${options.position}-${pasteEnd} conflicts with existing notes`,
9215
+ { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: targetVoice },
9216
+ { conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
9217
+ )]);
9218
+ }
9219
+ const existingNotes = voiceEntries.filter((e) => e.entry.type === "note").map((e) => ({ position: e.position, entry: e.entry }));
9220
+ for (const { relativePosition, note } of options.selection.notes) {
9221
+ const pastePosition = options.position + Math.max(0, relativePosition);
9222
+ const newNote = cloneNoteWithNewId(note);
9223
+ newNote.voice = targetVoice;
9224
+ if (targetStaff !== void 0) {
9225
+ newNote.staff = targetStaff;
9226
+ }
9227
+ delete newNote.tie;
9228
+ delete newNote.ties;
9229
+ if (newNote.notations) {
9230
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
9231
+ if (newNote.notations.length === 0) {
9232
+ delete newNote.notations;
9233
+ }
9234
+ }
9235
+ existingNotes.push({ position: pastePosition, entry: newNote });
9236
+ }
9237
+ measure.entries = rebuildMeasureWithVoice(
9238
+ measure,
9239
+ targetVoice,
9240
+ existingNotes,
9241
+ measureDuration,
9242
+ targetStaff
9243
+ );
9244
+ }
9245
+ const errors = validateMeasureLocal(measure, context, {
9246
+ checkMeasureDuration: true,
9247
+ checkPosition: true,
9248
+ checkVoiceStaff: true
9249
+ });
9250
+ const criticalErrors = errors.filter((e) => e.level === "error");
9251
+ if (criticalErrors.length > 0) {
9252
+ return failure(criticalErrors);
9253
+ }
9254
+ return success(result, errors.filter((e) => e.level !== "error"));
9255
+ }
9256
+ function cutNotes(score, options) {
9257
+ const copyResult = copyNotes(score, options);
9258
+ if (!copyResult.success) {
9259
+ return failure(copyResult.errors);
9260
+ }
9261
+ const selection = copyResult.data;
9262
+ const result = cloneScore(score);
9263
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9264
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
9265
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
9266
+ const voiceEntries = getVoiceEntries(measure, options.voice, options.staff);
9267
+ const entriesToKeep = voiceEntries.filter((e) => {
9268
+ if (e.entry.type !== "note") return true;
9269
+ const note = e.entry;
9270
+ if (note.rest) return true;
9271
+ return e.endPosition <= options.startPosition || e.position >= options.endPosition;
9272
+ });
9273
+ const newEntries = [];
9274
+ for (const { position, entry } of entriesToKeep) {
9275
+ if (entry.type === "note") {
9276
+ newEntries.push({ position, entry });
9277
+ }
9278
+ }
9279
+ measure.entries = rebuildMeasureWithVoice(
9280
+ measure,
9281
+ options.voice,
9282
+ newEntries,
9283
+ measureDuration,
9284
+ options.staff
9285
+ );
9286
+ const errors = validateMeasureLocal(measure, context, {
9287
+ checkMeasureDuration: true,
9288
+ checkPosition: true,
9289
+ checkVoiceStaff: true
9290
+ });
9291
+ const criticalErrors = errors.filter((e) => e.level === "error");
9292
+ if (criticalErrors.length > 0) {
9293
+ return failure(criticalErrors);
9294
+ }
9295
+ return success(
9296
+ { score: result, selection },
9297
+ errors.filter((e) => e.level !== "error")
9298
+ );
9299
+ }
9300
+ function copyNotesMultiMeasure(score, options) {
9301
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9302
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9303
+ }
9304
+ const part = score.parts[options.partIndex];
9305
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
9306
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
9307
+ }
9308
+ if (options.endMeasureIndex < options.startMeasureIndex || options.endMeasureIndex >= part.measures.length) {
9309
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
9310
+ }
9311
+ const selection = {
9312
+ source: {
9313
+ partIndex: options.partIndex,
9314
+ startMeasureIndex: options.startMeasureIndex,
9315
+ endMeasureIndex: options.endMeasureIndex,
9316
+ voice: options.voice,
9317
+ staff: options.staff
9318
+ },
9319
+ measures: []
9320
+ };
9321
+ for (let measureIndex = options.startMeasureIndex; measureIndex <= options.endMeasureIndex; measureIndex++) {
9322
+ const measure = part.measures[measureIndex];
9323
+ const measureOffset = measureIndex - options.startMeasureIndex;
9324
+ const copiedNotes = [];
9325
+ let position = 0;
9326
+ for (const entry of measure.entries) {
9327
+ if (entry.type === "note") {
9328
+ if (entry.voice === options.voice && (options.staff === void 0 || (entry.staff ?? 1) === options.staff)) {
9329
+ if (!entry.chord && !entry.rest) {
9330
+ const clonedNote = cloneNoteWithNewId(entry);
9331
+ copiedNotes.push({
9332
+ relativePosition: position,
9333
+ note: clonedNote
9334
+ });
9335
+ position += entry.duration;
9336
+ } else if (entry.chord && copiedNotes.length > 0) {
9337
+ const clonedNote = cloneNoteWithNewId(entry);
9338
+ copiedNotes.push({
9339
+ relativePosition: copiedNotes[copiedNotes.length - 1].relativePosition,
9340
+ note: clonedNote
9341
+ });
9342
+ } else if (!entry.chord) {
9343
+ position += entry.duration;
9344
+ }
9345
+ } else if (!entry.chord) {
9346
+ position += entry.duration;
9347
+ }
9348
+ } else if (entry.type === "backup") {
9349
+ position -= entry.duration;
9350
+ } else if (entry.type === "forward") {
9351
+ position += entry.duration;
9352
+ }
9353
+ }
9354
+ if (copiedNotes.length > 0) {
9355
+ selection.measures.push({
9356
+ measureOffset,
9357
+ notes: copiedNotes
9358
+ });
9359
+ }
9360
+ }
9361
+ if (selection.measures.length === 0) {
9362
+ return failure([operationError("NOTE_NOT_FOUND", "No notes found in the specified range", { partIndex: options.partIndex, voice: options.voice })]);
9363
+ }
9364
+ return success(selection);
9365
+ }
9366
+ function pasteNotesMultiMeasure(score, options) {
9367
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9368
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9369
+ }
9370
+ const part = score.parts[options.partIndex];
9371
+ const measureCount = options.selection.measures.length > 0 ? options.selection.measures[options.selection.measures.length - 1].measureOffset + 1 : 0;
9372
+ if (options.startMeasureIndex + measureCount > part.measures.length) {
9373
+ return failure([operationError("MEASURE_NOT_FOUND", `Not enough measures to paste (need ${measureCount})`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
9374
+ }
9375
+ let result = cloneScore(score);
9376
+ const targetVoice = options.voice ?? options.selection.source.voice;
9377
+ const targetStaff = options.staff ?? options.selection.source.staff;
9378
+ for (const measureData of options.selection.measures) {
9379
+ const measureIndex = options.startMeasureIndex + measureData.measureOffset;
9380
+ const measure = result.parts[options.partIndex].measures[measureIndex];
9381
+ const context = getMeasureContext(result, options.partIndex, measureIndex);
9382
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
9383
+ const voiceEntries = getVoiceEntries(measure, targetVoice, targetStaff);
9384
+ let entriesToKeep;
9385
+ if (options.overwrite !== false) {
9386
+ entriesToKeep = voiceEntries.filter((e) => e.entry.type === "note" && e.entry.rest).map((e) => ({ position: e.position, entry: e.entry }));
9387
+ } else {
9388
+ entriesToKeep = voiceEntries.filter((e) => e.entry.type === "note").map((e) => ({ position: e.position, entry: e.entry }));
9389
+ }
9390
+ for (const { relativePosition, note } of measureData.notes) {
9391
+ const newNote = cloneNoteWithNewId(note);
9392
+ newNote.voice = targetVoice;
9393
+ if (targetStaff !== void 0) {
9394
+ newNote.staff = targetStaff;
9395
+ }
9396
+ delete newNote.tie;
9397
+ delete newNote.ties;
9398
+ if (newNote.notations) {
9399
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
9400
+ if (newNote.notations.length === 0) {
9401
+ delete newNote.notations;
9402
+ }
9403
+ }
9404
+ entriesToKeep.push({ position: relativePosition, entry: newNote });
9405
+ }
9406
+ measure.entries = rebuildMeasureWithVoice(
9407
+ measure,
9408
+ targetVoice,
9409
+ entriesToKeep,
9410
+ measureDuration,
9411
+ targetStaff
9412
+ );
9413
+ const errors = validateMeasureLocal(measure, context, {
9414
+ checkMeasureDuration: true,
9415
+ checkPosition: true,
9416
+ checkVoiceStaff: true
9417
+ });
9418
+ const criticalErrors = errors.filter((e) => e.level === "error");
9419
+ if (criticalErrors.length > 0) {
9420
+ return failure(criticalErrors);
9421
+ }
9422
+ }
9423
+ return success(result);
9424
+ }
9425
+ function addTempo(score, options) {
9426
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9427
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9428
+ }
9429
+ const part = score.parts[options.partIndex];
9430
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9431
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9432
+ }
9433
+ if (options.bpm <= 0) {
9434
+ return failure([operationError("INVALID_DURATION", "BPM must be positive", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9435
+ }
9436
+ const result = cloneScore(score);
9437
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9438
+ const directionTypes = [];
9439
+ directionTypes.push({
9440
+ kind: "metronome",
9441
+ beatUnit: options.beatUnit ?? "quarter",
9442
+ beatUnitDot: options.beatUnitDot,
9443
+ perMinute: options.bpm
9444
+ });
9445
+ if (options.text) {
9446
+ directionTypes.push({
9447
+ kind: "words",
9448
+ text: options.text,
9449
+ fontWeight: "bold"
9450
+ });
9451
+ }
9452
+ const direction = {
9453
+ _id: generateId(),
9454
+ type: "direction",
9455
+ directionTypes,
9456
+ placement: options.placement ?? "above",
9457
+ sound: { tempo: options.bpm }
9458
+ };
9459
+ insertDirectionAtPosition(measure, direction, options.position);
9460
+ return success(result);
9461
+ }
9462
+ function removeTempo(score, options) {
9463
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9464
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9465
+ }
9466
+ const part = score.parts[options.partIndex];
9467
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9468
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9469
+ }
9470
+ const result = cloneScore(score);
9471
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9472
+ const tempoDirectionIndices = [];
9473
+ for (let i = 0; i < measure.entries.length; i++) {
9474
+ const entry = measure.entries[i];
9475
+ if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "metronome")) {
9476
+ tempoDirectionIndices.push(i);
9477
+ }
9478
+ }
9479
+ if (tempoDirectionIndices.length === 0) {
9480
+ return failure([operationError("TEMPO_NOT_FOUND", "No tempo marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9481
+ }
9482
+ const targetIndex = options.directionIndex ?? 0;
9483
+ if (targetIndex < 0 || targetIndex >= tempoDirectionIndices.length) {
9484
+ return failure([operationError("TEMPO_NOT_FOUND", `Tempo direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9485
+ }
9486
+ measure.entries.splice(tempoDirectionIndices[targetIndex], 1);
9487
+ return success(result);
9488
+ }
9489
+ function modifyTempo(score, options) {
9490
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9491
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9492
+ }
9493
+ const part = score.parts[options.partIndex];
9494
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9495
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9496
+ }
9497
+ const result = cloneScore(score);
9498
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9499
+ const tempoDirectionIndices = [];
9500
+ for (let i = 0; i < measure.entries.length; i++) {
9501
+ const entry = measure.entries[i];
9502
+ if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "metronome")) {
9503
+ tempoDirectionIndices.push(i);
9504
+ }
9505
+ }
9506
+ if (tempoDirectionIndices.length === 0) {
9507
+ return failure([operationError("TEMPO_NOT_FOUND", "No tempo marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9508
+ }
9509
+ const targetIndex = options.directionIndex ?? 0;
9510
+ if (targetIndex < 0 || targetIndex >= tempoDirectionIndices.length) {
9511
+ return failure([operationError("TEMPO_NOT_FOUND", `Tempo direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9512
+ }
9513
+ const direction = measure.entries[tempoDirectionIndices[targetIndex]];
9514
+ const metronome = direction.directionTypes.find((dt) => dt.kind === "metronome");
9515
+ if (metronome && metronome.kind === "metronome") {
9516
+ if (options.bpm !== void 0) {
9517
+ metronome.perMinute = options.bpm;
9518
+ }
9519
+ if (options.beatUnit !== void 0) {
9520
+ metronome.beatUnit = options.beatUnit;
9521
+ }
9522
+ if (options.beatUnitDot !== void 0) {
9523
+ metronome.beatUnitDot = options.beatUnitDot;
9524
+ }
9525
+ }
9526
+ if (options.text !== void 0) {
9527
+ const wordsIndex = direction.directionTypes.findIndex((dt) => dt.kind === "words");
9528
+ if (wordsIndex >= 0) {
9529
+ const words = direction.directionTypes[wordsIndex];
9530
+ if (words.kind === "words") {
9531
+ words.text = options.text;
9532
+ }
9533
+ } else if (options.text) {
9534
+ direction.directionTypes.push({
9535
+ kind: "words",
9536
+ text: options.text,
9537
+ fontWeight: "bold"
9538
+ });
9539
+ }
9540
+ }
9541
+ if (options.bpm !== void 0 && direction.sound) {
9542
+ direction.sound.tempo = options.bpm;
9543
+ }
9544
+ if (options.placement !== void 0) {
9545
+ direction.placement = options.placement;
9546
+ }
9547
+ return success(result);
9548
+ }
9549
+ function addWedge(score, options) {
9550
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9551
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9552
+ }
9553
+ const part = score.parts[options.partIndex];
9554
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
9555
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
9556
+ }
9557
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
9558
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
9559
+ }
9560
+ if (options.endMeasureIndex < options.startMeasureIndex || options.endMeasureIndex === options.startMeasureIndex && options.endPosition <= options.startPosition) {
9561
+ return failure([operationError("INVALID_RANGE", "End position must be after start position", { partIndex: options.partIndex })]);
9562
+ }
9563
+ const result = cloneScore(score);
9564
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
9565
+ const startDirection = {
9566
+ _id: generateId(),
9567
+ type: "direction",
9568
+ directionTypes: [{
9569
+ kind: "wedge",
9570
+ type: options.type
9571
+ }],
9572
+ placement: options.placement ?? "below",
9573
+ staff: options.staff
9574
+ };
9575
+ insertDirectionAtPosition(startMeasure, startDirection, options.startPosition);
9576
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
9577
+ const endDirection = {
9578
+ _id: generateId(),
9579
+ type: "direction",
9580
+ directionTypes: [{
9581
+ kind: "wedge",
9582
+ type: "stop"
9583
+ }],
9584
+ placement: options.placement ?? "below",
9585
+ staff: options.staff
9586
+ };
9587
+ insertDirectionAtPosition(endMeasure, endDirection, options.endPosition);
9588
+ return success(result);
9589
+ }
9590
+ function removeWedge(score, options) {
9591
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9592
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9593
+ }
9594
+ const part = score.parts[options.partIndex];
9595
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9596
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9597
+ }
9598
+ const result = cloneScore(score);
9599
+ const wedgeStarts = [];
9600
+ for (let mi = options.measureIndex; mi < result.parts[options.partIndex].measures.length; mi++) {
9601
+ const measure = result.parts[options.partIndex].measures[mi];
9602
+ for (let ei = 0; ei < measure.entries.length; ei++) {
9603
+ const entry = measure.entries[ei];
9604
+ if (entry.type === "direction") {
9605
+ const wedgeType = entry.directionTypes.find((dt) => dt.kind === "wedge");
9606
+ if (wedgeType && wedgeType.kind === "wedge" && (wedgeType.type === "crescendo" || wedgeType.type === "diminuendo")) {
9607
+ wedgeStarts.push({ measureIndex: mi, entryIndex: ei });
9608
+ }
9609
+ }
9610
+ }
9611
+ if (mi === options.measureIndex && wedgeStarts.length > 0) break;
9612
+ }
9613
+ if (wedgeStarts.length === 0) {
9614
+ return failure([operationError("WEDGE_NOT_FOUND", "No wedge found starting in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9615
+ }
9616
+ const targetIndex = options.directionIndex ?? 0;
9617
+ if (targetIndex >= wedgeStarts.length) {
9618
+ return failure([operationError("WEDGE_NOT_FOUND", `Wedge direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9619
+ }
9620
+ const startInfo = wedgeStarts[targetIndex];
9621
+ const startMeasure = result.parts[options.partIndex].measures[startInfo.measureIndex];
9622
+ startMeasure.entries.splice(startInfo.entryIndex, 1);
9623
+ for (let mi = startInfo.measureIndex; mi < result.parts[options.partIndex].measures.length; mi++) {
9624
+ const measure = result.parts[options.partIndex].measures[mi];
9625
+ for (let ei = 0; ei < measure.entries.length; ei++) {
9626
+ const entry = measure.entries[ei];
9627
+ if (entry.type === "direction") {
9628
+ const wedgeType = entry.directionTypes.find((dt) => dt.kind === "wedge" && dt.type === "stop");
9629
+ if (wedgeType) {
9630
+ measure.entries.splice(ei, 1);
9631
+ return success(result);
9632
+ }
9633
+ }
9634
+ }
9635
+ }
9636
+ return success(result);
9637
+ }
9638
+ function addFermata(score, options) {
9639
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9640
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9641
+ }
9642
+ const part = score.parts[options.partIndex];
9643
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9644
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9645
+ }
9646
+ const result = cloneScore(score);
9647
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9648
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9649
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9650
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9651
+ }
9652
+ const note = notes[options.noteIndex];
9653
+ if (note.notations?.some((n) => n.type === "fermata")) {
9654
+ return failure([operationError("FERMATA_ALREADY_EXISTS", "Note already has a fermata", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9655
+ }
9656
+ if (!note.notations) {
9657
+ note.notations = [];
9658
+ }
9659
+ const fermataNotation = {
9660
+ type: "fermata",
9661
+ shape: options.shape ?? "normal",
9662
+ fermataType: options.fermataType ?? "upright",
9663
+ placement: options.placement ?? "above"
9664
+ };
9665
+ note.notations.push(fermataNotation);
9666
+ return success(result);
9667
+ }
9668
+ function removeFermata(score, options) {
9669
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9670
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9671
+ }
9672
+ const part = score.parts[options.partIndex];
9673
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9674
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9675
+ }
9676
+ const result = cloneScore(score);
9677
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9678
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9679
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9680
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9681
+ }
9682
+ const note = notes[options.noteIndex];
9683
+ const fermataIndex = note.notations?.findIndex((n) => n.type === "fermata");
9684
+ if (fermataIndex === void 0 || fermataIndex === -1) {
9685
+ return failure([operationError("FERMATA_NOT_FOUND", "Note does not have a fermata", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9686
+ }
9687
+ note.notations.splice(fermataIndex, 1);
9688
+ if (note.notations.length === 0) {
9689
+ delete note.notations;
9690
+ }
9691
+ return success(result);
9692
+ }
9693
+ function addOrnament(score, options) {
9694
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9695
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9696
+ }
9697
+ const part = score.parts[options.partIndex];
9698
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9699
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9700
+ }
9701
+ const result = cloneScore(score);
9702
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9703
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9704
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9705
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9706
+ }
9707
+ const note = notes[options.noteIndex];
9708
+ if (note.notations?.some((n) => n.type === "ornament" && n.ornament === options.ornament)) {
9709
+ return failure([operationError("ORNAMENT_ALREADY_EXISTS", `Note already has ornament: ${options.ornament}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9710
+ }
9711
+ if (!note.notations) {
9712
+ note.notations = [];
9713
+ }
9714
+ const ornamentNotation = {
9715
+ type: "ornament",
9716
+ ornament: options.ornament,
9717
+ placement: options.placement,
9718
+ accidentalMark: options.accidentalMark
9719
+ };
9720
+ note.notations.push(ornamentNotation);
9721
+ return success(result);
9722
+ }
9723
+ function removeOrnament(score, options) {
9724
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9725
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9726
+ }
9727
+ const part = score.parts[options.partIndex];
9728
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9729
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9730
+ }
9731
+ const result = cloneScore(score);
9732
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9733
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9734
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9735
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9736
+ }
9737
+ const note = notes[options.noteIndex];
9738
+ const ornamentIndex = options.ornament ? note.notations?.findIndex((n) => n.type === "ornament" && n.ornament === options.ornament) : note.notations?.findIndex((n) => n.type === "ornament");
9739
+ if (ornamentIndex === void 0 || ornamentIndex === -1) {
9740
+ return failure([operationError("ORNAMENT_NOT_FOUND", "Note does not have the specified ornament", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9741
+ }
9742
+ note.notations.splice(ornamentIndex, 1);
9743
+ if (note.notations.length === 0) {
9744
+ delete note.notations;
9745
+ }
9746
+ return success(result);
9747
+ }
9748
+ function addPedal(score, options) {
9749
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9750
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9751
+ }
9752
+ const part = score.parts[options.partIndex];
9753
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9754
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9755
+ }
9756
+ const result = cloneScore(score);
9757
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9758
+ const direction = {
9759
+ _id: generateId(),
9760
+ type: "direction",
9761
+ directionTypes: [{
9762
+ kind: "pedal",
9763
+ type: options.pedalType,
9764
+ line: options.line
9765
+ }],
9766
+ placement: options.placement ?? "below"
9767
+ };
9768
+ insertDirectionAtPosition(measure, direction, options.position);
9769
+ return success(result);
9770
+ }
9771
+ function removePedal(score, options) {
9772
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9773
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9774
+ }
9775
+ const part = score.parts[options.partIndex];
9776
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9777
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9778
+ }
9779
+ const result = cloneScore(score);
9780
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9781
+ const pedalIndices = [];
9782
+ for (let i = 0; i < measure.entries.length; i++) {
9783
+ const entry = measure.entries[i];
9784
+ if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "pedal")) {
9785
+ pedalIndices.push(i);
9786
+ }
9787
+ }
9788
+ if (pedalIndices.length === 0) {
9789
+ return failure([operationError("PEDAL_NOT_FOUND", "No pedal marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9790
+ }
9791
+ const targetIndex = options.directionIndex ?? 0;
9792
+ if (targetIndex < 0 || targetIndex >= pedalIndices.length) {
9793
+ return failure([operationError("PEDAL_NOT_FOUND", `Pedal direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9794
+ }
9795
+ measure.entries.splice(pedalIndices[targetIndex], 1);
9796
+ return success(result);
9797
+ }
9798
+ function addTextDirection(score, options) {
9799
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9800
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9801
+ }
9802
+ const part = score.parts[options.partIndex];
9803
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9804
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9805
+ }
9806
+ if (!options.text.trim()) {
9807
+ return failure([operationError("INVALID_TEXT", "Text cannot be empty", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9808
+ }
9809
+ const result = cloneScore(score);
9810
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9811
+ const direction = {
9812
+ _id: generateId(),
9813
+ type: "direction",
9814
+ directionTypes: [{
9815
+ kind: "words",
9816
+ text: options.text,
9817
+ fontStyle: options.fontStyle,
9818
+ fontWeight: options.fontWeight
9819
+ }],
9820
+ placement: options.placement ?? "above"
9821
+ };
9822
+ insertDirectionAtPosition(measure, direction, options.position);
9823
+ return success(result);
9824
+ }
9825
+ function addRehearsalMark(score, options) {
9826
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9827
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9828
+ }
9829
+ const part = score.parts[options.partIndex];
9830
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9831
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9832
+ }
9833
+ const result = cloneScore(score);
9834
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9835
+ const direction = {
9836
+ _id: generateId(),
9837
+ type: "direction",
9838
+ directionTypes: [{
9839
+ kind: "rehearsal",
9840
+ text: options.text,
9841
+ enclosure: options.enclosure ?? "square"
9842
+ }],
9843
+ placement: "above"
9844
+ };
9845
+ insertDirectionAtPosition(measure, direction, 0);
9846
+ return success(result);
9847
+ }
9848
+ function insertDirectionAtPosition(measure, direction, position) {
9849
+ let currentPosition = 0;
9850
+ let insertIndex = 0;
9851
+ for (let i = 0; i < measure.entries.length; i++) {
9852
+ const entry = measure.entries[i];
9853
+ if (currentPosition >= position) {
9854
+ insertIndex = i;
9855
+ break;
9856
+ }
9857
+ if (entry.type === "note" && !entry.chord) {
9858
+ currentPosition += entry.duration;
9859
+ } else if (entry.type === "forward") {
9860
+ currentPosition += entry.duration;
9861
+ } else if (entry.type === "backup") {
9862
+ currentPosition -= entry.duration;
9863
+ }
9864
+ insertIndex = i + 1;
9865
+ }
9866
+ measure.entries.splice(insertIndex, 0, direction);
9867
+ }
9868
+ function addRepeatBarline(score, options) {
9869
+ const { partIndex, measureIndex, direction, times } = options;
9870
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9871
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9872
+ }
9873
+ const part = score.parts[partIndex];
9874
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9875
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9876
+ }
9877
+ const result = cloneScore(score);
9878
+ const location = direction === "forward" ? "left" : "right";
9879
+ const barStyle = direction === "forward" ? "heavy-light" : "light-heavy";
9880
+ for (const p of result.parts) {
9881
+ if (measureIndex >= p.measures.length) continue;
9882
+ const measure = p.measures[measureIndex];
9883
+ if (!measure.barlines) {
9884
+ measure.barlines = [];
9885
+ }
9886
+ const existingIndex = measure.barlines.findIndex((b) => b.location === location && b.repeat);
9887
+ if (existingIndex >= 0) {
9888
+ return failure([operationError("REPEAT_ALREADY_EXISTS", `Repeat barline already exists at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9889
+ }
9890
+ const nonRepeatIndex = measure.barlines.findIndex((b) => b.location === location && !b.repeat);
9891
+ if (nonRepeatIndex >= 0) {
9892
+ measure.barlines.splice(nonRepeatIndex, 1);
9893
+ }
9894
+ measure.barlines.push({
9895
+ _id: generateId(),
9896
+ location,
9897
+ barStyle,
9898
+ repeat: {
9899
+ direction,
9900
+ times
9901
+ }
9902
+ });
9903
+ }
9904
+ return success(result);
9905
+ }
9906
+ function removeRepeatBarline(score, options) {
9907
+ const { partIndex, measureIndex, location } = options;
9908
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9909
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9910
+ }
9911
+ const part = score.parts[partIndex];
9912
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9913
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9914
+ }
9915
+ const measure = part.measures[measureIndex];
9916
+ if (!measure.barlines) {
9917
+ return failure([operationError("REPEAT_NOT_FOUND", `No repeat barline found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9918
+ }
9919
+ const existingIndex = measure.barlines.findIndex((b) => b.location === location && b.repeat);
9920
+ if (existingIndex < 0) {
9921
+ return failure([operationError("REPEAT_NOT_FOUND", `No repeat barline found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9922
+ }
9923
+ const result = cloneScore(score);
9924
+ for (const p of result.parts) {
9925
+ if (measureIndex >= p.measures.length) continue;
9926
+ const m = p.measures[measureIndex];
9927
+ if (m.barlines) {
9928
+ const idx = m.barlines.findIndex((b) => b.location === location && b.repeat);
9929
+ if (idx >= 0) {
9930
+ m.barlines.splice(idx, 1);
9931
+ }
9932
+ if (m.barlines.length === 0) {
9933
+ delete m.barlines;
9934
+ }
9935
+ }
9936
+ }
9937
+ return success(result);
9938
+ }
9939
+ function addEnding(score, options) {
9940
+ const { partIndex, measureIndex, number, type } = options;
9941
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9942
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9943
+ }
9944
+ const part = score.parts[partIndex];
9945
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9946
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9947
+ }
9948
+ const result = cloneScore(score);
9949
+ const location = type === "start" ? "left" : "right";
9950
+ for (const p of result.parts) {
9951
+ if (measureIndex >= p.measures.length) continue;
9952
+ const measure = p.measures[measureIndex];
9953
+ if (!measure.barlines) {
9954
+ measure.barlines = [];
9955
+ }
9956
+ let barline = measure.barlines.find((b) => b.location === location);
9957
+ if (!barline) {
9958
+ barline = { _id: generateId(), location };
9959
+ measure.barlines.push(barline);
9960
+ }
9961
+ if (barline.ending) {
9962
+ return failure([operationError("ENDING_ALREADY_EXISTS", `Ending already exists at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9963
+ }
9964
+ barline.ending = { number, type };
9965
+ }
9966
+ return success(result);
9967
+ }
9968
+ function removeEnding(score, options) {
9969
+ const { partIndex, measureIndex, location } = options;
9970
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9971
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9972
+ }
9973
+ const part = score.parts[partIndex];
9974
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9975
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9976
+ }
9977
+ const measure = part.measures[measureIndex];
9978
+ const barline = measure.barlines?.find((b) => b.location === location && b.ending);
9979
+ if (!barline) {
9980
+ return failure([operationError("ENDING_NOT_FOUND", `No ending found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9981
+ }
9982
+ const result = cloneScore(score);
9983
+ for (const p of result.parts) {
9984
+ if (measureIndex >= p.measures.length) continue;
9985
+ const m = p.measures[measureIndex];
9986
+ if (m.barlines) {
9987
+ const bl = m.barlines.find((b) => b.location === location);
9988
+ if (bl) {
9989
+ delete bl.ending;
9990
+ if (!bl.barStyle && !bl.repeat && !bl.ending) {
9991
+ const idx = m.barlines.indexOf(bl);
9992
+ m.barlines.splice(idx, 1);
9993
+ }
9994
+ }
9995
+ if (m.barlines.length === 0) {
9996
+ delete m.barlines;
9997
+ }
9998
+ }
9999
+ }
10000
+ return success(result);
10001
+ }
10002
+ function changeBarline(score, options) {
10003
+ const { partIndex, measureIndex, location, barStyle } = options;
10004
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10005
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10006
+ }
10007
+ const part = score.parts[partIndex];
10008
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10009
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10010
+ }
10011
+ const result = cloneScore(score);
10012
+ for (const p of result.parts) {
10013
+ if (measureIndex >= p.measures.length) continue;
10014
+ const measure = p.measures[measureIndex];
10015
+ if (!measure.barlines) {
10016
+ measure.barlines = [];
10017
+ }
10018
+ let barline = measure.barlines.find((b) => b.location === location);
10019
+ if (!barline) {
10020
+ barline = { _id: generateId(), location };
10021
+ measure.barlines.push(barline);
10022
+ }
10023
+ barline.barStyle = barStyle;
10024
+ }
10025
+ return success(result);
10026
+ }
10027
+ function addSegno(score, options) {
10028
+ const { partIndex, measureIndex, position = 0 } = options;
10029
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10030
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10031
+ }
10032
+ const part = score.parts[partIndex];
10033
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10034
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10035
+ }
10036
+ const result = cloneScore(score);
10037
+ const measure = result.parts[partIndex].measures[measureIndex];
10038
+ const direction = {
10039
+ _id: generateId(),
10040
+ type: "direction",
10041
+ directionTypes: [{ kind: "segno" }],
10042
+ placement: "above"
10043
+ };
10044
+ insertDirectionAtPosition(measure, direction, position);
10045
+ return success(result);
10046
+ }
10047
+ function addCoda(score, options) {
10048
+ const { partIndex, measureIndex, position = 0 } = options;
10049
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10050
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10051
+ }
10052
+ const part = score.parts[partIndex];
10053
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10054
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10055
+ }
10056
+ const result = cloneScore(score);
10057
+ const measure = result.parts[partIndex].measures[measureIndex];
10058
+ const direction = {
10059
+ _id: generateId(),
10060
+ type: "direction",
10061
+ directionTypes: [{ kind: "coda" }],
10062
+ placement: "above"
10063
+ };
10064
+ insertDirectionAtPosition(measure, direction, position);
10065
+ return success(result);
10066
+ }
10067
+ function addDaCapo(score, options) {
10068
+ const { partIndex, measureIndex, position } = options;
10069
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10070
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10071
+ }
10072
+ const part = score.parts[partIndex];
10073
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10074
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10075
+ }
10076
+ const result = cloneScore(score);
10077
+ const measure = result.parts[partIndex].measures[measureIndex];
10078
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
10079
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
10080
+ const insertPos = position ?? measureDuration;
10081
+ const direction = {
10082
+ _id: generateId(),
10083
+ type: "direction",
10084
+ directionTypes: [{ kind: "words", text: "D.C." }],
10085
+ placement: "above"
10086
+ };
10087
+ insertDirectionAtPosition(measure, direction, insertPos);
10088
+ const sound = {
10089
+ _id: generateId(),
10090
+ type: "sound",
10091
+ dacapo: true
10092
+ };
10093
+ measure.entries.push(sound);
10094
+ return success(result);
10095
+ }
10096
+ function addDalSegno(score, options) {
10097
+ const { partIndex, measureIndex, position } = options;
10098
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10099
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10100
+ }
10101
+ const part = score.parts[partIndex];
10102
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10103
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10104
+ }
10105
+ const result = cloneScore(score);
10106
+ const measure = result.parts[partIndex].measures[measureIndex];
10107
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
10108
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
10109
+ const insertPos = position ?? measureDuration;
10110
+ const direction = {
10111
+ _id: generateId(),
10112
+ type: "direction",
10113
+ directionTypes: [{ kind: "words", text: "D.S." }],
10114
+ placement: "above"
10115
+ };
10116
+ insertDirectionAtPosition(measure, direction, insertPos);
10117
+ const sound = {
10118
+ _id: generateId(),
10119
+ type: "sound",
10120
+ dalsegno: "segno"
10121
+ };
10122
+ measure.entries.push(sound);
10123
+ return success(result);
10124
+ }
10125
+ function addFine(score, options) {
10126
+ const { partIndex, measureIndex, position } = options;
10127
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10128
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10129
+ }
10130
+ const part = score.parts[partIndex];
10131
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10132
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10133
+ }
10134
+ const result = cloneScore(score);
10135
+ const measure = result.parts[partIndex].measures[measureIndex];
10136
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
10137
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
10138
+ const insertPos = position ?? measureDuration;
10139
+ const direction = {
10140
+ _id: generateId(),
10141
+ type: "direction",
10142
+ directionTypes: [{ kind: "words", text: "Fine" }],
10143
+ placement: "above"
10144
+ };
10145
+ insertDirectionAtPosition(measure, direction, insertPos);
10146
+ const sound = {
10147
+ _id: generateId(),
10148
+ type: "sound",
10149
+ fine: true
10150
+ };
10151
+ measure.entries.push(sound);
10152
+ return success(result);
10153
+ }
10154
+ function addToCoda(score, options) {
10155
+ const { partIndex, measureIndex, position } = options;
10156
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10157
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10158
+ }
10159
+ const part = score.parts[partIndex];
10160
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10161
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10162
+ }
10163
+ const result = cloneScore(score);
10164
+ const measure = result.parts[partIndex].measures[measureIndex];
10165
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
10166
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
10167
+ const insertPos = position ?? measureDuration;
10168
+ const direction = {
10169
+ _id: generateId(),
10170
+ type: "direction",
10171
+ directionTypes: [{ kind: "words", text: "To Coda" }],
10172
+ placement: "above"
10173
+ };
10174
+ insertDirectionAtPosition(measure, direction, insertPos);
10175
+ const sound = {
10176
+ _id: generateId(),
10177
+ type: "sound",
10178
+ tocoda: "coda"
10179
+ };
10180
+ measure.entries.push(sound);
10181
+ return success(result);
10182
+ }
10183
+ function addGraceNote(score, options) {
10184
+ const { partIndex, measureIndex, targetNoteIndex, pitch, noteType = "eighth", slash = true, voice, staff } = options;
10185
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10186
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10187
+ }
10188
+ const part = score.parts[partIndex];
10189
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10190
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10191
+ }
10192
+ const measure = part.measures[measureIndex];
10193
+ let noteCount = 0;
10194
+ let targetEntryIndex = -1;
10195
+ let targetNote = null;
10196
+ for (let i = 0; i < measure.entries.length; i++) {
10197
+ const entry = measure.entries[i];
10198
+ if (entry.type === "note" && !entry.chord) {
10199
+ if (noteCount === targetNoteIndex) {
10200
+ targetEntryIndex = i;
10201
+ targetNote = entry;
10202
+ break;
10203
+ }
10204
+ noteCount++;
10205
+ }
10206
+ }
10207
+ if (targetEntryIndex < 0 || !targetNote) {
10208
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${targetNoteIndex} not found`, { partIndex, measureIndex })]);
10209
+ }
10210
+ const result = cloneScore(score);
10211
+ const resultMeasure = result.parts[partIndex].measures[measureIndex];
10212
+ const graceNote = {
10213
+ _id: generateId(),
10214
+ type: "note",
10215
+ pitch,
10216
+ duration: 0,
10217
+ // Grace notes have no duration
10218
+ voice: voice ?? targetNote.voice,
10219
+ staff: staff ?? targetNote.staff,
10220
+ noteType,
10221
+ grace: {
10222
+ slash
10223
+ }
10224
+ };
10225
+ resultMeasure.entries.splice(targetEntryIndex, 0, graceNote);
10226
+ return success(result);
10227
+ }
10228
+ function removeGraceNote(score, options) {
10229
+ const { partIndex, measureIndex, graceNoteIndex } = options;
10230
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10231
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10232
+ }
10233
+ const part = score.parts[partIndex];
10234
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10235
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10236
+ }
10237
+ const measure = part.measures[measureIndex];
10238
+ let graceCount = 0;
10239
+ let targetEntryIndex = -1;
10240
+ for (let i = 0; i < measure.entries.length; i++) {
10241
+ const entry = measure.entries[i];
10242
+ if (entry.type === "note" && entry.grace) {
10243
+ if (graceCount === graceNoteIndex) {
10244
+ targetEntryIndex = i;
10245
+ break;
10246
+ }
10247
+ graceCount++;
10248
+ }
10249
+ }
10250
+ if (targetEntryIndex < 0) {
10251
+ return failure([operationError("GRACE_NOTE_NOT_FOUND", `Grace note at index ${graceNoteIndex} not found`, { partIndex, measureIndex })]);
10252
+ }
10253
+ const result = cloneScore(score);
10254
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
10255
+ return success(result);
10256
+ }
10257
+ function convertToGrace(score, options) {
10258
+ const { partIndex, measureIndex, noteIndex, slash = true } = options;
10259
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10260
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10261
+ }
10262
+ const part = score.parts[partIndex];
10263
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10264
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10265
+ }
10266
+ const measure = part.measures[measureIndex];
10267
+ let noteCount = 0;
10268
+ let targetEntryIndex = -1;
10269
+ for (let i = 0; i < measure.entries.length; i++) {
10270
+ const entry = measure.entries[i];
10271
+ if (entry.type === "note" && !entry.chord) {
10272
+ if (noteCount === noteIndex) {
10273
+ targetEntryIndex = i;
10274
+ break;
10275
+ }
10276
+ noteCount++;
10277
+ }
10278
+ }
10279
+ if (targetEntryIndex < 0) {
10280
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10281
+ }
10282
+ const targetEntry = measure.entries[targetEntryIndex];
10283
+ if (targetEntry.type !== "note") {
10284
+ return failure([operationError("NOTE_NOT_FOUND", `Entry at index is not a note`, { partIndex, measureIndex })]);
10285
+ }
10286
+ if (targetEntry.grace) {
10287
+ return failure([operationError("INVALID_GRACE_NOTE", `Note is already a grace note`, { partIndex, measureIndex })]);
10288
+ }
10289
+ const result = cloneScore(score);
10290
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10291
+ resultNote.grace = { slash };
10292
+ resultNote.duration = 0;
10293
+ return success(result);
10294
+ }
10295
+ function addLyric(score, options) {
10296
+ const { partIndex, measureIndex, noteIndex, text, syllabic = "single", verse = 1, extend = false } = options;
10297
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10298
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10299
+ }
10300
+ const part = score.parts[partIndex];
10301
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10302
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10303
+ }
10304
+ const measure = part.measures[measureIndex];
10305
+ let noteCount = 0;
10306
+ let targetEntryIndex = -1;
10307
+ for (let i = 0; i < measure.entries.length; i++) {
10308
+ const entry = measure.entries[i];
10309
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10310
+ if (noteCount === noteIndex) {
10311
+ targetEntryIndex = i;
10312
+ break;
10313
+ }
10314
+ noteCount++;
10315
+ }
10316
+ }
10317
+ if (targetEntryIndex < 0) {
10318
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10319
+ }
10320
+ const targetEntry = measure.entries[targetEntryIndex];
10321
+ if (targetEntry.type !== "note") {
10322
+ return failure([operationError("NOTE_NOT_FOUND", `Entry is not a note`, { partIndex, measureIndex })]);
10323
+ }
10324
+ if (targetEntry.lyrics?.some((l) => l.number === verse)) {
10325
+ return failure([operationError("LYRIC_ALREADY_EXISTS", `Lyric for verse ${verse} already exists on this note`, { partIndex, measureIndex })]);
10326
+ }
10327
+ const result = cloneScore(score);
10328
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10329
+ if (!resultNote.lyrics) {
10330
+ resultNote.lyrics = [];
10331
+ }
10332
+ const lyric = {
10333
+ number: verse,
10334
+ syllabic,
10335
+ text,
10336
+ extend
10337
+ };
10338
+ resultNote.lyrics.push(lyric);
10339
+ return success(result);
10340
+ }
10341
+ function removeLyric(score, options) {
10342
+ const { partIndex, measureIndex, noteIndex, verse } = options;
10343
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10344
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10345
+ }
10346
+ const part = score.parts[partIndex];
10347
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10348
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10349
+ }
10350
+ const measure = part.measures[measureIndex];
10351
+ let noteCount = 0;
10352
+ let targetEntryIndex = -1;
10353
+ for (let i = 0; i < measure.entries.length; i++) {
10354
+ const entry = measure.entries[i];
10355
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10356
+ if (noteCount === noteIndex) {
10357
+ targetEntryIndex = i;
10358
+ break;
10359
+ }
10360
+ noteCount++;
10361
+ }
10362
+ }
10363
+ if (targetEntryIndex < 0) {
10364
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10365
+ }
10366
+ const targetEntry = measure.entries[targetEntryIndex];
10367
+ if (targetEntry.type !== "note" || !targetEntry.lyrics || targetEntry.lyrics.length === 0) {
10368
+ return failure([operationError("LYRIC_NOT_FOUND", `No lyrics found on note`, { partIndex, measureIndex })]);
10369
+ }
10370
+ if (verse !== void 0) {
10371
+ const lyricIndex = targetEntry.lyrics.findIndex((l) => l.number === verse);
10372
+ if (lyricIndex < 0) {
10373
+ return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
10374
+ }
10375
+ }
10376
+ const result = cloneScore(score);
10377
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10378
+ if (verse !== void 0) {
10379
+ resultNote.lyrics = resultNote.lyrics.filter((l) => l.number !== verse);
10380
+ } else {
10381
+ delete resultNote.lyrics;
10382
+ }
10383
+ if (resultNote.lyrics && resultNote.lyrics.length === 0) {
10384
+ delete resultNote.lyrics;
10385
+ }
10386
+ return success(result);
10387
+ }
10388
+ function updateLyric(score, options) {
10389
+ const { partIndex, measureIndex, noteIndex, verse = 1, text, syllabic, extend } = options;
10390
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10391
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10392
+ }
10393
+ const part = score.parts[partIndex];
10394
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10395
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10396
+ }
10397
+ const measure = part.measures[measureIndex];
10398
+ let noteCount = 0;
10399
+ let targetEntryIndex = -1;
10400
+ for (let i = 0; i < measure.entries.length; i++) {
10401
+ const entry = measure.entries[i];
10402
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10403
+ if (noteCount === noteIndex) {
10404
+ targetEntryIndex = i;
10405
+ break;
10406
+ }
10407
+ noteCount++;
10408
+ }
10409
+ }
10410
+ if (targetEntryIndex < 0) {
10411
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10412
+ }
10413
+ const targetEntry = measure.entries[targetEntryIndex];
10414
+ if (targetEntry.type !== "note" || !targetEntry.lyrics) {
10415
+ return failure([operationError("LYRIC_NOT_FOUND", `No lyrics found on note`, { partIndex, measureIndex })]);
10416
+ }
10417
+ const lyricIndex = targetEntry.lyrics.findIndex((l) => l.number === verse);
10418
+ if (lyricIndex < 0) {
10419
+ return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
10420
+ }
10421
+ const result = cloneScore(score);
10422
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10423
+ const lyric = resultNote.lyrics[lyricIndex];
10424
+ if (text !== void 0) {
10425
+ lyric.text = text;
10426
+ }
10427
+ if (syllabic !== void 0) {
10428
+ lyric.syllabic = syllabic;
10429
+ }
10430
+ if (extend !== void 0) {
10431
+ lyric.extend = extend;
10432
+ }
10433
+ return success(result);
10434
+ }
10435
+ function addHarmony(score, options) {
10436
+ const { partIndex, measureIndex, position, root, kind, kindText, bass, degrees, staff, placement = "above" } = options;
10437
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10438
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10439
+ }
10440
+ const part = score.parts[partIndex];
10441
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10442
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10443
+ }
10444
+ const validSteps = ["A", "B", "C", "D", "E", "F", "G"];
10445
+ if (!validSteps.includes(root.step.toUpperCase())) {
10446
+ return failure([operationError("INVALID_HARMONY", `Invalid root step: ${root.step}`, { partIndex, measureIndex })]);
10447
+ }
10448
+ if (bass && !validSteps.includes(bass.step.toUpperCase())) {
10449
+ return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
10450
+ }
10451
+ const result = cloneScore(score);
10452
+ const measure = result.parts[partIndex].measures[measureIndex];
10453
+ const harmony = {
10454
+ _id: generateId(),
10455
+ type: "harmony",
10456
+ root: {
10457
+ rootStep: root.step.toUpperCase(),
10458
+ rootAlter: root.alter
10459
+ },
10460
+ kind,
10461
+ kindText,
10462
+ bass: bass ? {
10463
+ bassStep: bass.step.toUpperCase(),
10464
+ bassAlter: bass.alter
10465
+ } : void 0,
10466
+ degrees: degrees?.map((d) => ({
10467
+ degreeValue: d.value,
10468
+ degreeAlter: d.alter,
10469
+ degreeType: d.type
10470
+ })),
10471
+ staff,
10472
+ placement
10473
+ };
10474
+ let currentPosition = 0;
10475
+ let insertIndex = 0;
10476
+ for (let i = 0; i < measure.entries.length; i++) {
10477
+ const entry = measure.entries[i];
10478
+ if (currentPosition >= position) {
10479
+ insertIndex = i;
10480
+ break;
10481
+ }
10482
+ if (entry.type === "note" && !entry.chord) {
10483
+ currentPosition += entry.duration;
10484
+ } else if (entry.type === "forward") {
10485
+ currentPosition += entry.duration;
10486
+ } else if (entry.type === "backup") {
10487
+ currentPosition -= entry.duration;
10488
+ }
10489
+ insertIndex = i + 1;
10490
+ }
10491
+ measure.entries.splice(insertIndex, 0, harmony);
10492
+ return success(result);
10493
+ }
10494
+ function removeHarmony(score, options) {
10495
+ const { partIndex, measureIndex, harmonyIndex } = options;
10496
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10497
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10498
+ }
10499
+ const part = score.parts[partIndex];
10500
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10501
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10502
+ }
10503
+ const measure = part.measures[measureIndex];
10504
+ let harmonyCount = 0;
10505
+ let targetEntryIndex = -1;
10506
+ for (let i = 0; i < measure.entries.length; i++) {
10507
+ const entry = measure.entries[i];
10508
+ if (entry.type === "harmony") {
10509
+ if (harmonyCount === harmonyIndex) {
10510
+ targetEntryIndex = i;
10511
+ break;
10512
+ }
10513
+ harmonyCount++;
10514
+ }
10515
+ }
10516
+ if (targetEntryIndex < 0) {
10517
+ return failure([operationError("HARMONY_NOT_FOUND", `Harmony at index ${harmonyIndex} not found`, { partIndex, measureIndex })]);
10518
+ }
10519
+ const result = cloneScore(score);
10520
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
10521
+ return success(result);
10522
+ }
10523
+ function updateHarmony(score, options) {
10524
+ const { partIndex, measureIndex, harmonyIndex, root, kind, kindText, bass, degrees } = options;
10525
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10526
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10527
+ }
10528
+ const part = score.parts[partIndex];
10529
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10530
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10531
+ }
10532
+ const measure = part.measures[measureIndex];
10533
+ let harmonyCount = 0;
10534
+ let targetEntryIndex = -1;
10535
+ for (let i = 0; i < measure.entries.length; i++) {
10536
+ const entry = measure.entries[i];
10537
+ if (entry.type === "harmony") {
10538
+ if (harmonyCount === harmonyIndex) {
10539
+ targetEntryIndex = i;
10540
+ break;
10541
+ }
10542
+ harmonyCount++;
10543
+ }
10544
+ }
10545
+ if (targetEntryIndex < 0) {
10546
+ return failure([operationError("HARMONY_NOT_FOUND", `Harmony at index ${harmonyIndex} not found`, { partIndex, measureIndex })]);
10547
+ }
10548
+ const validSteps = ["A", "B", "C", "D", "E", "F", "G"];
10549
+ if (root && !validSteps.includes(root.step.toUpperCase())) {
10550
+ return failure([operationError("INVALID_HARMONY", `Invalid root step: ${root.step}`, { partIndex, measureIndex })]);
10551
+ }
10552
+ if (bass && !validSteps.includes(bass.step.toUpperCase())) {
10553
+ return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
10554
+ }
10555
+ const result = cloneScore(score);
10556
+ const harmony = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10557
+ if (root) {
10558
+ harmony.root = {
10559
+ rootStep: root.step.toUpperCase(),
10560
+ rootAlter: root.alter
10561
+ };
10562
+ }
10563
+ if (kind !== void 0) {
10564
+ harmony.kind = kind;
10565
+ }
10566
+ if (kindText !== void 0) {
10567
+ harmony.kindText = kindText;
10568
+ }
10569
+ if (bass !== void 0) {
10570
+ if (bass === null) {
10571
+ delete harmony.bass;
10572
+ } else {
10573
+ harmony.bass = {
10574
+ bassStep: bass.step.toUpperCase(),
10575
+ bassAlter: bass.alter
10576
+ };
10577
+ }
10578
+ }
10579
+ if (degrees !== void 0) {
10580
+ if (degrees === null) {
10581
+ delete harmony.degrees;
10582
+ } else {
10583
+ harmony.degrees = degrees.map((d) => ({
10584
+ degreeValue: d.value,
10585
+ degreeAlter: d.alter,
10586
+ degreeType: d.type
10587
+ }));
10588
+ }
10589
+ }
10590
+ return success(result);
10591
+ }
10592
+ function addFingering(score, options) {
10593
+ const { partIndex, measureIndex, noteIndex, fingering, substitution = false, alternate = false, placement } = options;
10594
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10595
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10596
+ }
10597
+ const part = score.parts[partIndex];
10598
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10599
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10600
+ }
10601
+ const measure = part.measures[measureIndex];
10602
+ let noteCount = 0;
10603
+ let targetEntryIndex = -1;
10604
+ for (let i = 0; i < measure.entries.length; i++) {
10605
+ const entry = measure.entries[i];
10606
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10607
+ if (noteCount === noteIndex) {
10608
+ targetEntryIndex = i;
10609
+ break;
10610
+ }
10611
+ noteCount++;
10612
+ }
10613
+ }
10614
+ if (targetEntryIndex < 0) {
10615
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10616
+ }
10617
+ const result = cloneScore(score);
10618
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10619
+ if (!resultNote.notations) {
10620
+ resultNote.notations = [];
10621
+ }
10622
+ resultNote.notations.push({
10623
+ type: "technical",
10624
+ technical: "fingering",
10625
+ fingering,
10626
+ fingeringSubstitution: substitution || void 0,
10627
+ fingeringAlternate: alternate || void 0,
10628
+ placement
10629
+ });
10630
+ return success(result);
10631
+ }
10632
+ function removeFingering(score, options) {
10633
+ const { partIndex, measureIndex, noteIndex } = options;
10634
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10635
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10636
+ }
10637
+ const part = score.parts[partIndex];
10638
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10639
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10640
+ }
10641
+ const measure = part.measures[measureIndex];
10642
+ let noteCount = 0;
10643
+ let targetEntryIndex = -1;
10644
+ for (let i = 0; i < measure.entries.length; i++) {
10645
+ const entry = measure.entries[i];
10646
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10647
+ if (noteCount === noteIndex) {
10648
+ targetEntryIndex = i;
10649
+ break;
10650
+ }
10651
+ noteCount++;
10652
+ }
10653
+ }
10654
+ if (targetEntryIndex < 0) {
10655
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10656
+ }
10657
+ const targetEntry = measure.entries[targetEntryIndex];
10658
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10659
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10660
+ }
10661
+ const fingeringIndex = targetEntry.notations.findIndex(
10662
+ (n) => n.type === "technical" && n.technical === "fingering"
10663
+ );
10664
+ if (fingeringIndex < 0) {
10665
+ return failure([operationError("NOTE_NOT_FOUND", `No fingering found on note`, { partIndex, measureIndex })]);
10666
+ }
10667
+ const result = cloneScore(score);
10668
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10669
+ resultNote.notations.splice(fingeringIndex, 1);
10670
+ if (resultNote.notations.length === 0) {
10671
+ delete resultNote.notations;
10672
+ }
10673
+ return success(result);
10674
+ }
10675
+ function addBowing(score, options) {
10676
+ const { partIndex, measureIndex, noteIndex, bowingType, placement } = options;
10677
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10678
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10679
+ }
10680
+ const part = score.parts[partIndex];
10681
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10682
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10683
+ }
10684
+ const measure = part.measures[measureIndex];
10685
+ let noteCount = 0;
10686
+ let targetEntryIndex = -1;
10687
+ for (let i = 0; i < measure.entries.length; i++) {
10688
+ const entry = measure.entries[i];
10689
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10690
+ if (noteCount === noteIndex) {
10691
+ targetEntryIndex = i;
10692
+ break;
10693
+ }
10694
+ noteCount++;
10695
+ }
10696
+ }
10697
+ if (targetEntryIndex < 0) {
10698
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10699
+ }
10700
+ const result = cloneScore(score);
10701
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10702
+ if (!resultNote.notations) {
10703
+ resultNote.notations = [];
10704
+ }
10705
+ resultNote.notations.push({
10706
+ type: "technical",
10707
+ technical: bowingType,
10708
+ placement
10709
+ });
10710
+ return success(result);
10711
+ }
10712
+ function removeBowing(score, options) {
10713
+ const { partIndex, measureIndex, noteIndex, bowingType } = options;
10714
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10715
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10716
+ }
10717
+ const part = score.parts[partIndex];
10718
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10719
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10720
+ }
10721
+ const measure = part.measures[measureIndex];
10722
+ let noteCount = 0;
10723
+ let targetEntryIndex = -1;
10724
+ for (let i = 0; i < measure.entries.length; i++) {
10725
+ const entry = measure.entries[i];
10726
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10727
+ if (noteCount === noteIndex) {
10728
+ targetEntryIndex = i;
10729
+ break;
10730
+ }
10731
+ noteCount++;
10732
+ }
10733
+ }
10734
+ if (targetEntryIndex < 0) {
10735
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10736
+ }
10737
+ const targetEntry = measure.entries[targetEntryIndex];
10738
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10739
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10740
+ }
10741
+ const bowingIndex = targetEntry.notations.findIndex((n) => {
10742
+ if (n.type !== "technical") return false;
10743
+ if (bowingType) return n.technical === bowingType;
10744
+ return n.technical === "up-bow" || n.technical === "down-bow";
10745
+ });
10746
+ if (bowingIndex < 0) {
10747
+ return failure([operationError("NOTE_NOT_FOUND", `No bowing found on note`, { partIndex, measureIndex })]);
10748
+ }
10749
+ const result = cloneScore(score);
10750
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10751
+ resultNote.notations.splice(bowingIndex, 1);
10752
+ if (resultNote.notations.length === 0) {
10753
+ delete resultNote.notations;
10754
+ }
10755
+ return success(result);
10756
+ }
10757
+ function addStringNumber(score, options) {
10758
+ const { partIndex, measureIndex, noteIndex, stringNumber, placement } = options;
10759
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10760
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10761
+ }
10762
+ const part = score.parts[partIndex];
10763
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10764
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10765
+ }
10766
+ if (stringNumber < 1) {
10767
+ return failure([operationError("INVALID_POSITION", `String number must be positive`, { partIndex, measureIndex })]);
10768
+ }
10769
+ const measure = part.measures[measureIndex];
10770
+ let noteCount = 0;
10771
+ let targetEntryIndex = -1;
10772
+ for (let i = 0; i < measure.entries.length; i++) {
10773
+ const entry = measure.entries[i];
10774
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10775
+ if (noteCount === noteIndex) {
10776
+ targetEntryIndex = i;
10777
+ break;
10778
+ }
10779
+ noteCount++;
10780
+ }
10781
+ }
10782
+ if (targetEntryIndex < 0) {
10783
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10784
+ }
10785
+ const result = cloneScore(score);
10786
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10787
+ if (!resultNote.notations) {
10788
+ resultNote.notations = [];
10789
+ }
10790
+ resultNote.notations.push({
10791
+ type: "technical",
10792
+ technical: "string",
10793
+ string: stringNumber,
10794
+ placement
10795
+ });
10796
+ return success(result);
10797
+ }
10798
+ function removeStringNumber(score, options) {
10799
+ const { partIndex, measureIndex, noteIndex } = options;
10800
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10801
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10802
+ }
10803
+ const part = score.parts[partIndex];
10804
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10805
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10806
+ }
10807
+ const measure = part.measures[measureIndex];
10808
+ let noteCount = 0;
10809
+ let targetEntryIndex = -1;
10810
+ for (let i = 0; i < measure.entries.length; i++) {
10811
+ const entry = measure.entries[i];
10812
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10813
+ if (noteCount === noteIndex) {
10814
+ targetEntryIndex = i;
10815
+ break;
10816
+ }
10817
+ noteCount++;
10818
+ }
10819
+ }
10820
+ if (targetEntryIndex < 0) {
10821
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10822
+ }
10823
+ const targetEntry = measure.entries[targetEntryIndex];
10824
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10825
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10826
+ }
10827
+ const stringIndex = targetEntry.notations.findIndex(
10828
+ (n) => n.type === "technical" && n.technical === "string"
10829
+ );
10830
+ if (stringIndex < 0) {
10831
+ return failure([operationError("NOTE_NOT_FOUND", `No string number found on note`, { partIndex, measureIndex })]);
10832
+ }
10833
+ const result = cloneScore(score);
10834
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10835
+ resultNote.notations.splice(stringIndex, 1);
10836
+ if (resultNote.notations.length === 0) {
10837
+ delete resultNote.notations;
10838
+ }
10839
+ return success(result);
10840
+ }
10841
+ function addOctaveShift(score, options) {
10842
+ const { partIndex, measureIndex, position, shiftType, size = 8 } = options;
10843
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10844
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10845
+ }
10846
+ const part = score.parts[partIndex];
10847
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10848
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10849
+ }
10850
+ const result = cloneScore(score);
10851
+ const measure = result.parts[partIndex].measures[measureIndex];
10852
+ const direction = {
10853
+ _id: generateId(),
10854
+ type: "direction",
10855
+ directionTypes: [{
10856
+ kind: "octave-shift",
10857
+ type: shiftType,
10858
+ size
10859
+ }],
10860
+ placement: shiftType === "down" ? "above" : "below"
10861
+ };
10862
+ insertDirectionAtPosition(measure, direction, position);
10863
+ return success(result);
10864
+ }
10865
+ function stopOctaveShift(score, options) {
10866
+ const { partIndex, measureIndex, position, size = 8 } = options;
10867
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10868
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10869
+ }
10870
+ const part = score.parts[partIndex];
10871
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10872
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10873
+ }
10874
+ const result = cloneScore(score);
10875
+ const measure = result.parts[partIndex].measures[measureIndex];
10876
+ const direction = {
10877
+ _id: generateId(),
10878
+ type: "direction",
10879
+ directionTypes: [{
10880
+ kind: "octave-shift",
10881
+ type: "stop",
10882
+ size
10883
+ }]
10884
+ };
10885
+ insertDirectionAtPosition(measure, direction, position);
10886
+ return success(result);
10887
+ }
10888
+ function removeOctaveShift(score, options) {
10889
+ const { partIndex, measureIndex, octaveShiftIndex = 0 } = options;
10890
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10891
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10892
+ }
10893
+ const part = score.parts[partIndex];
10894
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10895
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10896
+ }
10897
+ const measure = part.measures[measureIndex];
10898
+ let shiftCount = 0;
10899
+ let targetEntryIndex = -1;
10900
+ for (let i = 0; i < measure.entries.length; i++) {
10901
+ const entry = measure.entries[i];
10902
+ if (entry.type === "direction") {
10903
+ const hasOctaveShift = entry.directionTypes.some((dt) => dt.kind === "octave-shift");
10904
+ if (hasOctaveShift) {
10905
+ if (shiftCount === octaveShiftIndex) {
10906
+ targetEntryIndex = i;
10907
+ break;
10908
+ }
10909
+ shiftCount++;
10910
+ }
10911
+ }
10912
+ }
10913
+ if (targetEntryIndex < 0) {
10914
+ return failure([operationError("NOTE_NOT_FOUND", `Octave shift at index ${octaveShiftIndex} not found`, { partIndex, measureIndex })]);
10915
+ }
10916
+ const result = cloneScore(score);
10917
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
10918
+ return success(result);
10919
+ }
10920
+ function addBreathMark(score, options) {
10921
+ const { partIndex, measureIndex, noteIndex, placement = "above" } = options;
10922
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10923
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10924
+ }
10925
+ const part = score.parts[partIndex];
10926
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10927
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10928
+ }
10929
+ const measure = part.measures[measureIndex];
10930
+ let noteCount = 0;
10931
+ let targetEntryIndex = -1;
10932
+ for (let i = 0; i < measure.entries.length; i++) {
10933
+ const entry = measure.entries[i];
10934
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10935
+ if (noteCount === noteIndex) {
10936
+ targetEntryIndex = i;
10937
+ break;
10938
+ }
10939
+ noteCount++;
10940
+ }
10941
+ }
10942
+ if (targetEntryIndex < 0) {
10943
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10944
+ }
10945
+ const result = cloneScore(score);
10946
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10947
+ if (!resultNote.notations) {
10948
+ resultNote.notations = [];
10949
+ }
10950
+ const existingBreathMark = resultNote.notations.find(
10951
+ (n) => n.type === "articulation" && n.articulation === "breath-mark"
10952
+ );
10953
+ if (existingBreathMark) {
10954
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Breath mark already exists on note`, { partIndex, measureIndex })]);
10955
+ }
10956
+ resultNote.notations.push({
10957
+ type: "articulation",
10958
+ articulation: "breath-mark",
10959
+ placement
10960
+ });
10961
+ return success(result);
10962
+ }
10963
+ function removeBreathMark(score, options) {
10964
+ const { partIndex, measureIndex, noteIndex } = options;
10965
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10966
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10967
+ }
10968
+ const part = score.parts[partIndex];
10969
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10970
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10971
+ }
10972
+ const measure = part.measures[measureIndex];
10973
+ let noteCount = 0;
10974
+ let targetEntryIndex = -1;
10975
+ for (let i = 0; i < measure.entries.length; i++) {
10976
+ const entry = measure.entries[i];
10977
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10978
+ if (noteCount === noteIndex) {
10979
+ targetEntryIndex = i;
10980
+ break;
10981
+ }
10982
+ noteCount++;
10983
+ }
10984
+ }
10985
+ if (targetEntryIndex < 0) {
10986
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10987
+ }
10988
+ const targetEntry = measure.entries[targetEntryIndex];
10989
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10990
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10991
+ }
10992
+ const breathMarkIndex = targetEntry.notations.findIndex(
10993
+ (n) => n.type === "articulation" && n.articulation === "breath-mark"
10994
+ );
10995
+ if (breathMarkIndex < 0) {
10996
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No breath mark found on note`, { partIndex, measureIndex })]);
10997
+ }
10998
+ const result = cloneScore(score);
10999
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
11000
+ resultNote.notations.splice(breathMarkIndex, 1);
11001
+ if (resultNote.notations.length === 0) {
11002
+ delete resultNote.notations;
11003
+ }
11004
+ return success(result);
11005
+ }
11006
+ function addCaesura(score, options) {
11007
+ const { partIndex, measureIndex, noteIndex, placement = "above" } = options;
11008
+ if (partIndex < 0 || partIndex >= score.parts.length) {
11009
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
11010
+ }
11011
+ const part = score.parts[partIndex];
11012
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
11013
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
11014
+ }
11015
+ const measure = part.measures[measureIndex];
11016
+ let noteCount = 0;
11017
+ let targetEntryIndex = -1;
11018
+ for (let i = 0; i < measure.entries.length; i++) {
11019
+ const entry = measure.entries[i];
11020
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
11021
+ if (noteCount === noteIndex) {
11022
+ targetEntryIndex = i;
11023
+ break;
11024
+ }
11025
+ noteCount++;
11026
+ }
11027
+ }
11028
+ if (targetEntryIndex < 0) {
11029
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
11030
+ }
11031
+ const result = cloneScore(score);
11032
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
11033
+ if (!resultNote.notations) {
11034
+ resultNote.notations = [];
11035
+ }
11036
+ const existingCaesura = resultNote.notations.find(
11037
+ (n) => n.type === "articulation" && n.articulation === "caesura"
11038
+ );
11039
+ if (existingCaesura) {
11040
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Caesura already exists on note`, { partIndex, measureIndex })]);
11041
+ }
11042
+ resultNote.notations.push({
11043
+ type: "articulation",
11044
+ articulation: "caesura",
11045
+ placement
11046
+ });
11047
+ return success(result);
11048
+ }
11049
+ function removeCaesura(score, options) {
11050
+ const { partIndex, measureIndex, noteIndex } = options;
11051
+ if (partIndex < 0 || partIndex >= score.parts.length) {
11052
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
11053
+ }
11054
+ const part = score.parts[partIndex];
11055
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
11056
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
11057
+ }
11058
+ const measure = part.measures[measureIndex];
11059
+ let noteCount = 0;
11060
+ let targetEntryIndex = -1;
11061
+ for (let i = 0; i < measure.entries.length; i++) {
11062
+ const entry = measure.entries[i];
11063
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
11064
+ if (noteCount === noteIndex) {
11065
+ targetEntryIndex = i;
11066
+ break;
11067
+ }
11068
+ noteCount++;
11069
+ }
11070
+ }
11071
+ if (targetEntryIndex < 0) {
11072
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
11073
+ }
11074
+ const targetEntry = measure.entries[targetEntryIndex];
11075
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
11076
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
11077
+ }
11078
+ const caesuraIndex = targetEntry.notations.findIndex(
11079
+ (n) => n.type === "articulation" && n.articulation === "caesura"
11080
+ );
11081
+ if (caesuraIndex < 0) {
11082
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No caesura found on note`, { partIndex, measureIndex })]);
11083
+ }
11084
+ const result = cloneScore(score);
11085
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
11086
+ resultNote.notations.splice(caesuraIndex, 1);
11087
+ if (resultNote.notations.length === 0) {
11088
+ delete resultNote.notations;
11089
+ }
11090
+ return success(result);
11091
+ }
11092
+ var addText = addTextDirection;
11093
+ var setBeaming = autoBeam;
11094
+ var addChordSymbol = addHarmony;
11095
+ var removeChordSymbol = removeHarmony;
11096
+ var updateChordSymbol = updateHarmony;
11097
+ var changeClef = insertClefChange;
11098
+ var setBarline = changeBarline;
11099
+ var addRepeat = addRepeatBarline;
11100
+ var removeRepeat = removeRepeatBarline;
7702
11101
 
7703
11102
  // src/file.ts
7704
11103
  var import_promises = require("fs/promises");
@@ -7831,18 +11230,66 @@ function getPartNameMap(score) {
7831
11230
  STEPS,
7832
11231
  STEP_SEMITONES,
7833
11232
  ValidationException,
11233
+ addArticulation,
11234
+ addBeam,
11235
+ addBowing,
11236
+ addBreathMark,
11237
+ addCaesura,
11238
+ addChord,
7834
11239
  addChordNote,
11240
+ addChordNoteChecked,
11241
+ addChordSymbol,
11242
+ addCoda,
11243
+ addDaCapo,
11244
+ addDalSegno,
11245
+ addDynamics,
11246
+ addEnding,
11247
+ addFermata,
11248
+ addFine,
11249
+ addFingering,
11250
+ addGraceNote,
11251
+ addHarmony,
11252
+ addLyric,
7835
11253
  addNote,
11254
+ addNoteChecked,
11255
+ addOctaveShift,
11256
+ addOrnament,
11257
+ addPart,
11258
+ addPedal,
11259
+ addRehearsalMark,
11260
+ addRepeat,
11261
+ addRepeatBarline,
11262
+ addSegno,
11263
+ addSlur,
11264
+ addStringNumber,
11265
+ addTempo,
11266
+ addText,
11267
+ addTextDirection,
11268
+ addTie,
11269
+ addToCoda,
11270
+ addVoice,
11271
+ addWedge,
7836
11272
  assertMeasureValid,
7837
11273
  assertValid,
11274
+ autoBeam,
7838
11275
  buildVoiceToStaffMap,
7839
11276
  buildVoiceToStaffMapForPart,
11277
+ changeBarline,
11278
+ changeClef,
7840
11279
  changeKey,
11280
+ changeNoteDuration,
7841
11281
  changeTime,
11282
+ convertToGrace,
11283
+ copyNotes,
11284
+ copyNotesMultiMeasure,
7842
11285
  countNotes,
11286
+ createTuplet,
11287
+ cutNotes,
7843
11288
  decodeBuffer,
7844
11289
  deleteMeasure,
7845
11290
  deleteNote,
11291
+ deleteNoteChecked,
11292
+ duplicatePart,
7846
11293
  exportMidi,
7847
11294
  findBarlines,
7848
11295
  findDirectionsByType,
@@ -7936,7 +11383,9 @@ function getPartNameMap(score) {
7936
11383
  hasTieStop,
7937
11384
  hasTuplet,
7938
11385
  inferStaff,
11386
+ insertClefChange,
7939
11387
  insertMeasure,
11388
+ insertNote,
7940
11389
  isChordNote,
7941
11390
  isCompressed,
7942
11391
  isCueNote,
@@ -7949,19 +11398,65 @@ function getPartNameMap(score) {
7949
11398
  isValid,
7950
11399
  iterateEntries,
7951
11400
  iterateNotes,
11401
+ lowerAccidental,
7952
11402
  measureRoundtrip,
11403
+ modifyDynamics,
7953
11404
  modifyNoteDuration,
11405
+ modifyNoteDurationChecked,
7954
11406
  modifyNotePitch,
11407
+ modifyNotePitchChecked,
11408
+ modifyTempo,
11409
+ moveNoteToStaff,
7955
11410
  parse,
7956
11411
  parseAuto,
7957
11412
  parseCompressed,
7958
11413
  parseFile,
11414
+ pasteNotes,
11415
+ pasteNotesMultiMeasure,
7959
11416
  pitchToSemitone,
11417
+ raiseAccidental,
11418
+ removeArticulation,
11419
+ removeBeam,
11420
+ removeBowing,
11421
+ removeBreathMark,
11422
+ removeCaesura,
11423
+ removeChordSymbol,
11424
+ removeDynamics,
11425
+ removeEnding,
11426
+ removeFermata,
11427
+ removeFingering,
11428
+ removeGraceNote,
11429
+ removeHarmony,
11430
+ removeLyric,
11431
+ removeNote,
11432
+ removeOctaveShift,
11433
+ removeOrnament,
11434
+ removePart,
11435
+ removePedal,
11436
+ removeRepeat,
11437
+ removeRepeatBarline,
11438
+ removeSlur,
11439
+ removeStringNumber,
11440
+ removeTempo,
11441
+ removeTie,
11442
+ removeTuplet,
11443
+ removeWedge,
7960
11444
  scoresEqual,
7961
11445
  serialize,
7962
11446
  serializeCompressed,
7963
11447
  serializeToFile,
11448
+ setBarline,
11449
+ setBeaming,
11450
+ setNotePitch,
11451
+ setNotePitchBySemitone,
11452
+ setStaves,
11453
+ shiftNotePitch,
11454
+ stopOctaveShift,
7964
11455
  transpose,
11456
+ transposeChecked,
11457
+ updateChordSymbol,
11458
+ updateHarmony,
11459
+ updateLyric,
7965
11460
  validate,
7966
11461
  validateBackupForward,
7967
11462
  validateBeams,