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.mjs CHANGED
@@ -3552,8 +3552,10 @@ function serializeSystemLayout(layout, indent) {
3552
3552
  }
3553
3553
  function serializeCredit(credit, indent) {
3554
3554
  const lines = [];
3555
- const pageAttr = credit.page !== void 0 ? ` page="${credit.page}"` : "";
3556
- lines.push(`${indent}<credit${pageAttr}>`);
3555
+ let attrs = "";
3556
+ if (credit._id) attrs += ` id="${escapeXml(credit._id)}"`;
3557
+ if (credit.page !== void 0) attrs += ` page="${credit.page}"`;
3558
+ lines.push(`${indent}<credit${attrs}>`);
3557
3559
  if (credit.creditType) {
3558
3560
  for (const ct of credit.creditType) {
3559
3561
  lines.push(`${indent}${indent}<credit-type>${escapeXml(ct)}</credit-type>`);
@@ -3561,19 +3563,19 @@ function serializeCredit(credit, indent) {
3561
3563
  }
3562
3564
  if (credit.creditWords) {
3563
3565
  for (const cw of credit.creditWords) {
3564
- let attrs = "";
3565
- if (cw.defaultX !== void 0) attrs += ` default-x="${cw.defaultX}"`;
3566
- if (cw.defaultY !== void 0) attrs += ` default-y="${cw.defaultY}"`;
3567
- if (cw.fontSize) attrs += ` font-size="${escapeXml(cw.fontSize)}"`;
3568
- if (cw.fontWeight) attrs += ` font-weight="${escapeXml(cw.fontWeight)}"`;
3569
- if (cw.fontStyle) attrs += ` font-style="${escapeXml(cw.fontStyle)}"`;
3570
- if (cw.justify) attrs += ` justify="${escapeXml(cw.justify)}"`;
3571
- if (cw.halign) attrs += ` halign="${escapeXml(cw.halign)}"`;
3572
- if (cw.valign) attrs += ` valign="${escapeXml(cw.valign)}"`;
3573
- if (cw.letterSpacing) attrs += ` letter-spacing="${escapeXml(cw.letterSpacing)}"`;
3574
- if (cw.xmlLang) attrs += ` xml:lang="${escapeXml(cw.xmlLang)}"`;
3575
- if (cw.xmlSpace) attrs += ` xml:space="${escapeXml(cw.xmlSpace)}"`;
3576
- lines.push(`${indent}${indent}<credit-words${attrs}>${escapeXml(cw.text)}</credit-words>`);
3566
+ let attrs2 = "";
3567
+ if (cw.defaultX !== void 0) attrs2 += ` default-x="${cw.defaultX}"`;
3568
+ if (cw.defaultY !== void 0) attrs2 += ` default-y="${cw.defaultY}"`;
3569
+ if (cw.fontSize) attrs2 += ` font-size="${escapeXml(cw.fontSize)}"`;
3570
+ if (cw.fontWeight) attrs2 += ` font-weight="${escapeXml(cw.fontWeight)}"`;
3571
+ if (cw.fontStyle) attrs2 += ` font-style="${escapeXml(cw.fontStyle)}"`;
3572
+ if (cw.justify) attrs2 += ` justify="${escapeXml(cw.justify)}"`;
3573
+ if (cw.halign) attrs2 += ` halign="${escapeXml(cw.halign)}"`;
3574
+ if (cw.valign) attrs2 += ` valign="${escapeXml(cw.valign)}"`;
3575
+ if (cw.letterSpacing) attrs2 += ` letter-spacing="${escapeXml(cw.letterSpacing)}"`;
3576
+ if (cw.xmlLang) attrs2 += ` xml:lang="${escapeXml(cw.xmlLang)}"`;
3577
+ if (cw.xmlSpace) attrs2 += ` xml:space="${escapeXml(cw.xmlSpace)}"`;
3578
+ lines.push(`${indent}${indent}<credit-words${attrs2}>${escapeXml(cw.text)}</credit-words>`);
3577
3579
  }
3578
3580
  }
3579
3581
  lines.push(`${indent}</credit>`);
@@ -3693,6 +3695,7 @@ function serializePartGroup(group, indent) {
3693
3695
  const lines = [];
3694
3696
  let attrs = ` type="${group.groupType}"`;
3695
3697
  if (group.number !== void 0) attrs += ` number="${group.number}"`;
3698
+ if (group._id) attrs += ` id="${escapeXml(group._id)}"`;
3696
3699
  lines.push(`${indent}<part-group${attrs}>`);
3697
3700
  if (group.groupName) {
3698
3701
  lines.push(`${indent} <group-name>${escapeXml(group.groupName)}</group-name>`);
@@ -3733,6 +3736,7 @@ function serializePart(part, indent) {
3733
3736
  function serializeMeasure(measure, indent) {
3734
3737
  const lines = [];
3735
3738
  let attrs = ` number="${measure.number}"`;
3739
+ if (measure._id) attrs += ` id="${escapeXml(measure._id)}"`;
3736
3740
  if (measure.width !== void 0) attrs += ` width="${measure.width}"`;
3737
3741
  if (measure.implicit) attrs += ` implicit="yes"`;
3738
3742
  lines.push(`${indent}<measure${attrs}>`);
@@ -3807,9 +3811,10 @@ function serializePrint(print, indent) {
3807
3811
  lines.push(`${indent}</print>`);
3808
3812
  return lines;
3809
3813
  }
3810
- function serializeAttributes(attrs, indent) {
3814
+ function serializeAttributes(attrs, indent, id) {
3811
3815
  const lines = [];
3812
- lines.push(`${indent}<attributes>`);
3816
+ const idAttr = id ? ` id="${escapeXml(id)}"` : "";
3817
+ lines.push(`${indent}<attributes${idAttr}>`);
3813
3818
  if (attrs.divisions !== void 0) {
3814
3819
  lines.push(`${indent} <divisions>${attrs.divisions}</divisions>`);
3815
3820
  }
@@ -3957,7 +3962,7 @@ function serializeEntry(entry, indent) {
3957
3962
  case "sound":
3958
3963
  return serializeSound(entry, indent);
3959
3964
  case "attributes":
3960
- return serializeAttributes(entry.attributes, indent);
3965
+ return serializeAttributes(entry.attributes, indent, entry._id);
3961
3966
  default:
3962
3967
  return [];
3963
3968
  }
@@ -3965,6 +3970,7 @@ function serializeEntry(entry, indent) {
3965
3970
  function serializeNote(note, indent) {
3966
3971
  const lines = [];
3967
3972
  const noteAttrs = buildAttrs({
3973
+ "id": note._id,
3968
3974
  "default-x": note.defaultX,
3969
3975
  "default-y": note.defaultY,
3970
3976
  "relative-x": note.relativeX,
@@ -4447,7 +4453,8 @@ function serializeBackup(backup, indent) {
4447
4453
  }
4448
4454
  function serializeForward(forward, indent) {
4449
4455
  const lines = [];
4450
- lines.push(`${indent}<forward>`);
4456
+ const idAttr = forward._id ? ` id="${escapeXml(forward._id)}"` : "";
4457
+ lines.push(`${indent}<forward${idAttr}>`);
4451
4458
  lines.push(`${indent} <duration>${forward.duration}</duration>`);
4452
4459
  if (forward.voice !== void 0) {
4453
4460
  lines.push(`${indent} <voice>${forward.voice}</voice>`);
@@ -4461,6 +4468,7 @@ function serializeForward(forward, indent) {
4461
4468
  function serializeDirection(direction, indent) {
4462
4469
  const lines = [];
4463
4470
  let attrs = "";
4471
+ if (direction._id) attrs += ` id="${escapeXml(direction._id)}"`;
4464
4472
  if (direction.placement) attrs += ` placement="${direction.placement}"`;
4465
4473
  if (direction.directive) attrs += ' directive="yes"';
4466
4474
  if (direction.system) attrs += ` system="${direction.system}"`;
@@ -4722,7 +4730,9 @@ function serializeDirectionType(dirType, indent) {
4722
4730
  }
4723
4731
  function serializeBarline(barline, indent) {
4724
4732
  const lines = [];
4725
- lines.push(`${indent}<barline location="${barline.location}">`);
4733
+ let attrs = ` location="${barline.location}"`;
4734
+ if (barline._id) attrs += ` id="${escapeXml(barline._id)}"`;
4735
+ lines.push(`${indent}<barline${attrs}>`);
4726
4736
  if (barline.barStyle) {
4727
4737
  lines.push(`${indent} <bar-style>${barline.barStyle}</bar-style>`);
4728
4738
  }
@@ -4815,6 +4825,7 @@ function serializeMeasureStyle(ms, indent) {
4815
4825
  function serializeHarmony(harmony, indent) {
4816
4826
  const lines = [];
4817
4827
  const attrs = buildAttrs({
4828
+ id: harmony._id,
4818
4829
  placement: harmony.placement,
4819
4830
  "print-frame": harmony.printFrame,
4820
4831
  "default-y": harmony.defaultY,
@@ -4892,6 +4903,7 @@ function serializeHarmony(harmony, indent) {
4892
4903
  function serializeFiguredBass(fb, indent) {
4893
4904
  const lines = [];
4894
4905
  let attrs = "";
4906
+ if (fb._id) attrs += ` id="${escapeXml(fb._id)}"`;
4895
4907
  if (fb.parentheses) attrs += ' parentheses="yes"';
4896
4908
  lines.push(`${indent}<figured-bass${attrs}>`);
4897
4909
  for (const fig of fb.figures) {
@@ -4923,6 +4935,7 @@ function serializeFiguredBass(fb, indent) {
4923
4935
  function serializeSound(sound, indent) {
4924
4936
  const lines = [];
4925
4937
  const attrs = [];
4938
+ if (sound._id) attrs.push(`id="${escapeXml(sound._id)}"`);
4926
4939
  if (sound.tempo !== void 0) attrs.push(`tempo="${sound.tempo}"`);
4927
4940
  if (sound.dynamics !== void 0) attrs.push(`dynamics="${sound.dynamics}"`);
4928
4941
  if (sound.dacapo) attrs.push('dacapo="yes"');
@@ -4954,7 +4967,7 @@ function serializeSound(sound, indent) {
4954
4967
  }
4955
4968
  lines.push(`${indent} </swing>`);
4956
4969
  lines.push(`${indent}</sound>`);
4957
- } else if (attrs.length === 0) {
4970
+ } else if (attrs.length === 0 && !sound._id) {
4958
4971
  lines.push(`${indent}<sound/>`);
4959
4972
  } else {
4960
4973
  lines.push(`${indent}<sound${attrStr}/>`);
@@ -5340,9 +5353,128 @@ var STEP_SEMITONES = {
5340
5353
  "A": 9,
5341
5354
  "B": 11
5342
5355
  };
5356
+ var SHARP_ORDER = ["F", "C", "G", "D", "A", "E", "B"];
5357
+ var FLAT_ORDER = ["B", "E", "A", "D", "G", "C", "F"];
5343
5358
  function pitchToSemitone(pitch) {
5344
5359
  return pitch.octave * 12 + STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0);
5345
5360
  }
5361
+ function getAlterForStepInKey(step, key) {
5362
+ const fifths = key.fifths;
5363
+ if (fifths > 0) {
5364
+ const sharps = SHARP_ORDER.slice(0, fifths);
5365
+ return sharps.includes(step) ? 1 : 0;
5366
+ } else if (fifths < 0) {
5367
+ const flats = FLAT_ORDER.slice(0, -fifths);
5368
+ return flats.includes(step) ? -1 : 0;
5369
+ }
5370
+ return 0;
5371
+ }
5372
+ function getAlteredStepsInKey(key) {
5373
+ const alterations = /* @__PURE__ */ new Map();
5374
+ const fifths = key.fifths;
5375
+ if (fifths > 0) {
5376
+ SHARP_ORDER.slice(0, fifths).forEach((step) => alterations.set(step, 1));
5377
+ } else if (fifths < 0) {
5378
+ FLAT_ORDER.slice(0, -fifths).forEach((step) => alterations.set(step, -1));
5379
+ }
5380
+ return alterations;
5381
+ }
5382
+ function getAccidentalsInMeasure(measure, upToPosition, voice) {
5383
+ const accidentals = /* @__PURE__ */ new Map();
5384
+ let position = 0;
5385
+ for (const entry of measure.entries) {
5386
+ if (position >= upToPosition) break;
5387
+ if (entry.type === "note") {
5388
+ if (voice === void 0 || entry.voice === voice) {
5389
+ if (entry.pitch && entry.accidental) {
5390
+ const key = `${entry.pitch.step}${entry.pitch.octave}`;
5391
+ accidentals.set(key, entry.pitch.alter ?? 0);
5392
+ }
5393
+ }
5394
+ if (!entry.chord) {
5395
+ position += entry.duration;
5396
+ }
5397
+ } else if (entry.type === "backup") {
5398
+ position -= entry.duration;
5399
+ } else if (entry.type === "forward") {
5400
+ position += entry.duration;
5401
+ }
5402
+ }
5403
+ return accidentals;
5404
+ }
5405
+ function semitoneToKeyAwarePitch(semitone, key, options) {
5406
+ const octave = Math.floor(semitone / 12);
5407
+ const pitchClass = (semitone % 12 + 12) % 12;
5408
+ const keyPreferSharp = key.fifths >= 0;
5409
+ const preferSharp = options?.preferSharp ?? keyPreferSharp;
5410
+ for (const step of STEPS) {
5411
+ const stepSemitone = STEP_SEMITONES[step];
5412
+ if (stepSemitone === pitchClass) {
5413
+ return { step, octave };
5414
+ }
5415
+ }
5416
+ const keyAlterations = getAlteredStepsInKey(key);
5417
+ for (const step of STEPS) {
5418
+ const stepSemitone = STEP_SEMITONES[step];
5419
+ const keyAlter = keyAlterations.get(step) ?? 0;
5420
+ if ((stepSemitone + keyAlter) % 12 === pitchClass) {
5421
+ return { step, octave, alter: keyAlter };
5422
+ }
5423
+ }
5424
+ if (preferSharp) {
5425
+ for (const step of STEPS) {
5426
+ const stepSemitone = STEP_SEMITONES[step];
5427
+ const diff = (pitchClass - stepSemitone + 12) % 12;
5428
+ if (diff === 1) {
5429
+ return { step, octave, alter: 1 };
5430
+ }
5431
+ }
5432
+ for (const step of STEPS) {
5433
+ const stepSemitone = STEP_SEMITONES[step];
5434
+ const diff = (pitchClass - stepSemitone + 12) % 12;
5435
+ if (diff === 2) {
5436
+ return { step, octave, alter: 2 };
5437
+ }
5438
+ }
5439
+ } else {
5440
+ for (const step of STEPS) {
5441
+ const stepSemitone = STEP_SEMITONES[step];
5442
+ const diff = (stepSemitone - pitchClass + 12) % 12;
5443
+ if (diff === 1) {
5444
+ return { step, octave, alter: -1 };
5445
+ }
5446
+ }
5447
+ for (const step of STEPS) {
5448
+ const stepSemitone = STEP_SEMITONES[step];
5449
+ const diff = (stepSemitone - pitchClass + 12) % 12;
5450
+ if (diff === 2) {
5451
+ return { step, octave, alter: -2 };
5452
+ }
5453
+ }
5454
+ }
5455
+ return { step: "C", octave, alter: pitchClass };
5456
+ }
5457
+ function determineAccidental(pitch, key, accidentalsInMeasure) {
5458
+ const noteKey = `${pitch.step}${pitch.octave}`;
5459
+ const alter = pitch.alter ?? 0;
5460
+ const keyAlter = getAlterForStepInKey(pitch.step, key);
5461
+ const previousAlter = accidentalsInMeasure.get(noteKey);
5462
+ if (previousAlter !== void 0) {
5463
+ if (alter === previousAlter) {
5464
+ return void 0;
5465
+ }
5466
+ } else {
5467
+ if (alter === keyAlter) {
5468
+ return void 0;
5469
+ }
5470
+ }
5471
+ if (alter === 0) return "natural";
5472
+ if (alter === 1) return "sharp";
5473
+ if (alter === -1) return "flat";
5474
+ if (alter === 2) return "double-sharp";
5475
+ if (alter === -2) return "double-flat";
5476
+ return void 0;
5477
+ }
5346
5478
  function createPositionState() {
5347
5479
  return { position: 0, lastNonChordPosition: 0 };
5348
5480
  }
@@ -6950,6 +7082,34 @@ function operationError(code, message, location = {}, details) {
6950
7082
  function cloneScore(score) {
6951
7083
  return JSON.parse(JSON.stringify(score));
6952
7084
  }
7085
+ function cloneNoteWithNewId(note) {
7086
+ const cloned = JSON.parse(JSON.stringify(note));
7087
+ cloned._id = generateId();
7088
+ return cloned;
7089
+ }
7090
+ function cloneEntryWithNewId(entry) {
7091
+ const cloned = JSON.parse(JSON.stringify(entry));
7092
+ cloned._id = generateId();
7093
+ return cloned;
7094
+ }
7095
+ function cloneMeasureWithNewIds(measure) {
7096
+ const cloned = JSON.parse(JSON.stringify(measure));
7097
+ cloned._id = generateId();
7098
+ cloned.entries = cloned.entries.map((entry) => cloneEntryWithNewId(entry));
7099
+ if (cloned.barlines) {
7100
+ cloned.barlines = cloned.barlines.map((barline) => ({
7101
+ ...barline,
7102
+ _id: generateId()
7103
+ }));
7104
+ }
7105
+ return cloned;
7106
+ }
7107
+ function clonePartWithNewIds(part) {
7108
+ const cloned = JSON.parse(JSON.stringify(part));
7109
+ cloned._id = generateId();
7110
+ cloned.measures = cloned.measures.map((measure) => cloneMeasureWithNewIds(measure));
7111
+ return cloned;
7112
+ }
6953
7113
  function getMeasureDuration(divisions, time) {
6954
7114
  const beats = parseInt(time.beats, 10);
6955
7115
  if (isNaN(beats)) return divisions * 4;
@@ -7391,6 +7551,189 @@ function setNotePitch(score, options) {
7391
7551
  }
7392
7552
  return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7393
7553
  }
7554
+ function setNotePitchBySemitone(score, options) {
7555
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7556
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7557
+ }
7558
+ const part = score.parts[options.partIndex];
7559
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7560
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7561
+ }
7562
+ const result = cloneScore(score);
7563
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7564
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
7565
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
7566
+ const keySignature = attrs.key ?? { fifths: 0 };
7567
+ let noteCount = 0;
7568
+ for (const entry of measure.entries) {
7569
+ if (entry.type === "note" && !entry.rest) {
7570
+ if (noteCount === options.noteIndex) {
7571
+ const notePosition = getAbsolutePositionForNote(entry, measure);
7572
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
7573
+ const newPitch = semitoneToKeyAwarePitch(options.semitone, keySignature, {
7574
+ preferSharp: options.preferSharp
7575
+ });
7576
+ const accidental = determineAccidental(newPitch, keySignature, accidentalsInMeasure);
7577
+ entry.pitch = newPitch;
7578
+ if (accidental) {
7579
+ entry.accidental = { value: accidental };
7580
+ } else {
7581
+ delete entry.accidental;
7582
+ }
7583
+ return success(result);
7584
+ }
7585
+ noteCount++;
7586
+ }
7587
+ }
7588
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7589
+ }
7590
+ function shiftNotePitch(score, options) {
7591
+ if (options.semitones === 0) {
7592
+ return success(score);
7593
+ }
7594
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7595
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7596
+ }
7597
+ const part = score.parts[options.partIndex];
7598
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7599
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7600
+ }
7601
+ const measure = part.measures[options.measureIndex];
7602
+ let noteCount = 0;
7603
+ let currentSemitone = null;
7604
+ for (const entry of measure.entries) {
7605
+ if (entry.type === "note" && !entry.rest) {
7606
+ if (noteCount === options.noteIndex) {
7607
+ if (!entry.pitch) {
7608
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7609
+ }
7610
+ currentSemitone = pitchToSemitone(entry.pitch);
7611
+ break;
7612
+ }
7613
+ noteCount++;
7614
+ }
7615
+ }
7616
+ if (currentSemitone === null) {
7617
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7618
+ }
7619
+ return setNotePitchBySemitone(score, {
7620
+ partIndex: options.partIndex,
7621
+ measureIndex: options.measureIndex,
7622
+ noteIndex: options.noteIndex,
7623
+ semitone: currentSemitone + options.semitones,
7624
+ preferSharp: options.preferSharp
7625
+ });
7626
+ }
7627
+ function raiseAccidental(score, options) {
7628
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7629
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7630
+ }
7631
+ const part = score.parts[options.partIndex];
7632
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7633
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7634
+ }
7635
+ const result = cloneScore(score);
7636
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7637
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
7638
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
7639
+ const keySignature = attrs.key ?? { fifths: 0 };
7640
+ let noteCount = 0;
7641
+ for (const entry of measure.entries) {
7642
+ if (entry.type === "note" && !entry.rest) {
7643
+ if (noteCount === options.noteIndex) {
7644
+ if (!entry.pitch) {
7645
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7646
+ }
7647
+ const currentAlter = entry.pitch.alter ?? 0;
7648
+ const newAlter = currentAlter + 1;
7649
+ if (newAlter > 2) {
7650
+ return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot raise accidental beyond double-sharp (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7651
+ }
7652
+ entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
7653
+ const notePosition = getAbsolutePositionForNote(entry, measure);
7654
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
7655
+ const accidental = determineAccidental(entry.pitch, keySignature, accidentalsInMeasure);
7656
+ if (accidental) {
7657
+ entry.accidental = { value: accidental };
7658
+ } else {
7659
+ delete entry.accidental;
7660
+ }
7661
+ return success(result);
7662
+ }
7663
+ noteCount++;
7664
+ }
7665
+ }
7666
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7667
+ }
7668
+ function lowerAccidental(score, options) {
7669
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7670
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7671
+ }
7672
+ const part = score.parts[options.partIndex];
7673
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7674
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7675
+ }
7676
+ const result = cloneScore(score);
7677
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7678
+ const measureNumber = measure.number ?? String(options.measureIndex + 1);
7679
+ const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
7680
+ const keySignature = attrs.key ?? { fifths: 0 };
7681
+ let noteCount = 0;
7682
+ for (const entry of measure.entries) {
7683
+ if (entry.type === "note" && !entry.rest) {
7684
+ if (noteCount === options.noteIndex) {
7685
+ if (!entry.pitch) {
7686
+ return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7687
+ }
7688
+ const currentAlter = entry.pitch.alter ?? 0;
7689
+ const newAlter = currentAlter - 1;
7690
+ if (newAlter < -2) {
7691
+ return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot lower accidental beyond double-flat (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7692
+ }
7693
+ entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
7694
+ const notePosition = getAbsolutePositionForNote(entry, measure);
7695
+ const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
7696
+ const accidental = determineAccidental(entry.pitch, keySignature, accidentalsInMeasure);
7697
+ if (accidental) {
7698
+ entry.accidental = { value: accidental };
7699
+ } else {
7700
+ delete entry.accidental;
7701
+ }
7702
+ return success(result);
7703
+ }
7704
+ noteCount++;
7705
+ }
7706
+ }
7707
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7708
+ }
7709
+ function addVoice(score, options) {
7710
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7711
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7712
+ }
7713
+ const part = score.parts[options.partIndex];
7714
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7715
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7716
+ }
7717
+ const result = cloneScore(score);
7718
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7719
+ const existingVoiceEntries = getVoiceEntries(measure, options.voice, options.staff);
7720
+ if (existingVoiceEntries.length > 0) {
7721
+ return failure([operationError(
7722
+ "NOTE_CONFLICT",
7723
+ `Voice ${options.voice} already exists in this measure`,
7724
+ { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice }
7725
+ )]);
7726
+ }
7727
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
7728
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
7729
+ const rest = createRest(measureDuration, options.voice, options.staff);
7730
+ const currentEnd = getMeasureEndPosition(measure);
7731
+ if (currentEnd > 0) {
7732
+ measure.entries.push({ _id: generateId(), type: "backup", duration: currentEnd });
7733
+ }
7734
+ measure.entries.push(rest);
7735
+ return success(result);
7736
+ }
7394
7737
  function transposePitch(pitch, semitones) {
7395
7738
  const currentSemitone = STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0) + pitch.octave * 12;
7396
7739
  const targetSemitone = currentSemitone + semitones;
@@ -7430,6 +7773,163 @@ function transpose(score, semitones) {
7430
7773
  }
7431
7774
  return success(result);
7432
7775
  }
7776
+ function addPart(score, options) {
7777
+ if (score.parts.find((p) => p.id === options.id)) {
7778
+ return failure([operationError("DUPLICATE_PART_ID", `Part ID "${options.id}" already exists`, { partId: options.id })]);
7779
+ }
7780
+ const result = cloneScore(score);
7781
+ const insertIndex = options.insertIndex ?? result.parts.length;
7782
+ const partInfo = {
7783
+ _id: generateId(),
7784
+ type: "score-part",
7785
+ id: options.id,
7786
+ name: options.name,
7787
+ abbreviation: options.abbreviation
7788
+ };
7789
+ let partListInsertIndex = result.partList.length;
7790
+ let partCount = 0;
7791
+ for (let i = 0; i < result.partList.length; i++) {
7792
+ if (result.partList[i].type === "score-part") {
7793
+ if (partCount === insertIndex) {
7794
+ partListInsertIndex = i;
7795
+ break;
7796
+ }
7797
+ partCount++;
7798
+ }
7799
+ }
7800
+ result.partList.splice(partListInsertIndex, 0, partInfo);
7801
+ const measureCount = result.parts.length > 0 ? result.parts[0].measures.length : 1;
7802
+ const newPart = { _id: generateId(), id: options.id, measures: [] };
7803
+ for (let i = 0; i < measureCount; i++) {
7804
+ const measureNumber = result.parts.length > 0 ? result.parts[0].measures[i]?.number ?? String(i + 1) : String(i + 1);
7805
+ const measure = { _id: generateId(), number: measureNumber, entries: [] };
7806
+ if (i === 0) {
7807
+ measure.attributes = {
7808
+ divisions: options.divisions ?? 4,
7809
+ time: options.time ?? { beats: "4", beatType: 4 },
7810
+ key: options.key ?? { fifths: 0 },
7811
+ clef: options.clef ? [options.clef] : [{ sign: "G", line: 2 }]
7812
+ };
7813
+ }
7814
+ newPart.measures.push(measure);
7815
+ }
7816
+ result.parts.splice(insertIndex, 0, newPart);
7817
+ const validationResult = validate(result, { checkPartReferences: true, checkPartStructure: true });
7818
+ if (!validationResult.valid) {
7819
+ return failure(validationResult.errors);
7820
+ }
7821
+ return success(result, validationResult.warnings);
7822
+ }
7823
+ function removePart(score, partId) {
7824
+ const partIndex = score.parts.findIndex((p) => p.id === partId);
7825
+ if (partIndex === -1) {
7826
+ return failure([operationError("PART_NOT_FOUND", `Part "${partId}" not found`, { partId })]);
7827
+ }
7828
+ if (score.parts.length <= 1) {
7829
+ return failure([operationError("PART_NOT_FOUND", "Cannot remove the only remaining part", { partId })]);
7830
+ }
7831
+ const result = cloneScore(score);
7832
+ result.parts.splice(partIndex, 1);
7833
+ const partListIndex = result.partList.findIndex((e) => e.type === "score-part" && e.id === partId);
7834
+ if (partListIndex !== -1) {
7835
+ result.partList.splice(partListIndex, 1);
7836
+ }
7837
+ return success(result);
7838
+ }
7839
+ function duplicatePart(score, options) {
7840
+ const sourceIndex = score.parts.findIndex((p) => p.id === options.sourcePartId);
7841
+ if (sourceIndex === -1) {
7842
+ return failure([operationError("PART_NOT_FOUND", `Source part "${options.sourcePartId}" not found`, { partId: options.sourcePartId })]);
7843
+ }
7844
+ if (score.parts.find((p) => p.id === options.newPartId)) {
7845
+ return failure([operationError("DUPLICATE_PART_ID", `Part ID "${options.newPartId}" already exists`, { partId: options.newPartId })]);
7846
+ }
7847
+ const result = cloneScore(score);
7848
+ const sourcePart = result.parts[sourceIndex];
7849
+ const newPart = clonePartWithNewIds(sourcePart);
7850
+ newPart.id = options.newPartId;
7851
+ const sourcePartInfo = result.partList.find((e) => e.type === "score-part" && e.id === options.sourcePartId);
7852
+ const newPartInfo = {
7853
+ _id: generateId(),
7854
+ type: "score-part",
7855
+ id: options.newPartId,
7856
+ name: options.newPartName ?? sourcePartInfo?.name,
7857
+ abbreviation: sourcePartInfo?.abbreviation
7858
+ };
7859
+ result.parts.splice(sourceIndex + 1, 0, newPart);
7860
+ const partListSourceIndex = result.partList.findIndex((e) => e.type === "score-part" && e.id === options.sourcePartId);
7861
+ if (partListSourceIndex !== -1) {
7862
+ result.partList.splice(partListSourceIndex + 1, 0, newPartInfo);
7863
+ } else {
7864
+ result.partList.push(newPartInfo);
7865
+ }
7866
+ return success(result);
7867
+ }
7868
+ function setStaves(score, options) {
7869
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7870
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7871
+ }
7872
+ if (options.staves < 1) {
7873
+ return failure([operationError("INVALID_STAFF", `Staves count must be at least 1`, { partIndex: options.partIndex })]);
7874
+ }
7875
+ const result = cloneScore(score);
7876
+ const part = result.parts[options.partIndex];
7877
+ const fromMeasureIndex = options.fromMeasure ?? 0;
7878
+ const measure = part.measures[fromMeasureIndex];
7879
+ if (!measure) {
7880
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${fromMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: fromMeasureIndex })]);
7881
+ }
7882
+ if (!measure.attributes) {
7883
+ measure.attributes = {};
7884
+ }
7885
+ measure.attributes.staves = options.staves;
7886
+ if (options.clefs) {
7887
+ measure.attributes.clef = options.clefs;
7888
+ } else {
7889
+ const existingClefs = measure.attributes.clef ?? [];
7890
+ const newClefs = [...existingClefs];
7891
+ for (let staff = existingClefs.length + 1; staff <= options.staves; staff++) {
7892
+ newClefs.push(staff === 2 ? { sign: "F", line: 4, staff } : { sign: "G", line: 2, staff });
7893
+ }
7894
+ measure.attributes.clef = newClefs;
7895
+ }
7896
+ const validationResult = validate(result, { checkVoiceStaff: true, checkStaffStructure: true });
7897
+ if (!validationResult.valid) {
7898
+ return failure(validationResult.errors);
7899
+ }
7900
+ return success(result, validationResult.warnings);
7901
+ }
7902
+ function moveNoteToStaff(score, options) {
7903
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7904
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7905
+ }
7906
+ const part = score.parts[options.partIndex];
7907
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7908
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7909
+ }
7910
+ if (options.targetStaff < 1) {
7911
+ return failure([operationError("INVALID_STAFF", `Target staff must be at least 1`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7912
+ }
7913
+ const result = cloneScore(score);
7914
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7915
+ let noteCount = 0;
7916
+ for (const entry of measure.entries) {
7917
+ if (entry.type === "note" && !entry.rest) {
7918
+ if (noteCount === options.noteIndex) {
7919
+ entry.staff = options.targetStaff;
7920
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
7921
+ const errors = validateMeasureLocal(measure, context, { checkVoiceStaff: true });
7922
+ const criticalErrors = errors.filter((e) => e.level === "error");
7923
+ if (criticalErrors.length > 0) {
7924
+ return failure(criticalErrors);
7925
+ }
7926
+ return success(result, errors.filter((e) => e.level !== "error"));
7927
+ }
7928
+ noteCount++;
7929
+ }
7930
+ }
7931
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7932
+ }
7433
7933
  function changeKey(score, key, options) {
7434
7934
  const result = cloneScore(score);
7435
7935
  const targetMeasure = String(options.fromMeasure);
@@ -7494,36 +7994,2839 @@ function deleteMeasure(score, measureNumber) {
7494
7994
  }
7495
7995
  return result;
7496
7996
  }
7497
- var addNote = (score, options) => {
7498
- const result = insertNote(score, {
7499
- partIndex: options.partIndex,
7500
- measureIndex: options.measureIndex,
7501
- voice: options.voice,
7502
- staff: options.staff,
7503
- position: options.position,
7504
- pitch: options.note.pitch ?? { step: "C", octave: 4 },
7505
- duration: options.note.duration,
7506
- noteType: options.note.noteType,
7507
- dots: options.note.dots
7508
- });
7509
- return result.success ? result.data : score;
7510
- };
7511
- var deleteNote = (score, options) => {
7512
- const result = removeNote(score, options);
7513
- return result.success ? result.data : score;
7514
- };
7515
- var addChordNote = (score, options) => {
7516
- const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
7517
- return result.success ? result.data : score;
7518
- };
7519
- var modifyNotePitch = (score, options) => {
7520
- const result = setNotePitch(score, options);
7521
- return result.success ? result.data : score;
7522
- };
7523
- var modifyNoteDuration = (score, options) => {
7524
- const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
7525
- return result.success ? result.data : score;
7526
- };
7997
+ function findNoteByIndex(measure, noteIndex) {
7998
+ let noteCount = 0;
7999
+ for (let i = 0; i < measure.entries.length; i++) {
8000
+ const entry = measure.entries[i];
8001
+ if (entry.type === "note" && !entry.rest) {
8002
+ if (noteCount === noteIndex) {
8003
+ return { note: entry, entryIndex: i };
8004
+ }
8005
+ noteCount++;
8006
+ }
8007
+ }
8008
+ return null;
8009
+ }
8010
+ function pitchesEqual2(p1, p2) {
8011
+ return p1.step === p2.step && p1.octave === p2.octave && (p1.alter ?? 0) === (p2.alter ?? 0);
8012
+ }
8013
+ function addTie(score, options) {
8014
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8015
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8016
+ }
8017
+ const part = score.parts[options.partIndex];
8018
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
8019
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8020
+ }
8021
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
8022
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8023
+ }
8024
+ const result = cloneScore(score);
8025
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
8026
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
8027
+ const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
8028
+ if (!startResult) {
8029
+ return failure([operationError("NOTE_NOT_FOUND", `Start note index ${options.startNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8030
+ }
8031
+ const endResult = findNoteByIndex(endMeasure, options.endNoteIndex);
8032
+ if (!endResult) {
8033
+ return failure([operationError("NOTE_NOT_FOUND", `End note index ${options.endNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8034
+ }
8035
+ const startNote = startResult.note;
8036
+ const endNote = endResult.note;
8037
+ if (!startNote.pitch || !endNote.pitch) {
8038
+ return failure([operationError("TIE_INVALID_TARGET", "Cannot tie notes without pitch", { partIndex: options.partIndex })]);
8039
+ }
8040
+ if (!pitchesEqual2(startNote.pitch, endNote.pitch)) {
8041
+ return failure([operationError("TIE_PITCH_MISMATCH", "Tied notes must have the same pitch", { partIndex: options.partIndex }, { startPitch: startNote.pitch, endPitch: endNote.pitch })]);
8042
+ }
8043
+ if (startNote.tie?.type === "start" || startNote.tie?.type === "continue") {
8044
+ return failure([operationError("TIE_ALREADY_EXISTS", "Start note already has a tie start", { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8045
+ }
8046
+ startNote.tie = { type: "start" };
8047
+ if (!startNote.notations) startNote.notations = [];
8048
+ startNote.notations.push({ type: "tied", tiedType: "start" });
8049
+ endNote.tie = { type: "stop" };
8050
+ if (!endNote.notations) endNote.notations = [];
8051
+ endNote.notations.push({ type: "tied", tiedType: "stop" });
8052
+ const validationResult = validate(result, { checkTies: true });
8053
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
8054
+ if (criticalErrors.length > 0) {
8055
+ return failure(criticalErrors);
8056
+ }
8057
+ return success(result, validationResult.warnings);
8058
+ }
8059
+ function removeTie(score, options) {
8060
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8061
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8062
+ }
8063
+ const part = score.parts[options.partIndex];
8064
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8065
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8066
+ }
8067
+ const result = cloneScore(score);
8068
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8069
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8070
+ if (!noteResult) {
8071
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8072
+ }
8073
+ const note = noteResult.note;
8074
+ if (!note.tie) {
8075
+ return failure([operationError("TIE_NOT_FOUND", "Note does not have a tie", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8076
+ }
8077
+ delete note.tie;
8078
+ delete note.ties;
8079
+ if (note.notations) {
8080
+ note.notations = note.notations.filter((n) => n.type !== "tied");
8081
+ if (note.notations.length === 0) {
8082
+ delete note.notations;
8083
+ }
8084
+ }
8085
+ return success(result);
8086
+ }
8087
+ function addSlur(score, options) {
8088
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8089
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8090
+ }
8091
+ const part = score.parts[options.partIndex];
8092
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
8093
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8094
+ }
8095
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
8096
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8097
+ }
8098
+ const result = cloneScore(score);
8099
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
8100
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
8101
+ const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
8102
+ if (!startResult) {
8103
+ return failure([operationError("NOTE_NOT_FOUND", `Start note index ${options.startNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8104
+ }
8105
+ const endResult = findNoteByIndex(endMeasure, options.endNoteIndex);
8106
+ if (!endResult) {
8107
+ return failure([operationError("NOTE_NOT_FOUND", `End note index ${options.endNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
8108
+ }
8109
+ const startNote = startResult.note;
8110
+ const endNote = endResult.note;
8111
+ const slurNumber = options.number ?? 1;
8112
+ if (startNote.notations?.some((n) => n.type === "slur" && n.slurType === "start" && (n.number ?? 1) === slurNumber)) {
8113
+ return failure([operationError("SLUR_ALREADY_EXISTS", `Slur ${slurNumber} already starts on this note`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
8114
+ }
8115
+ if (!startNote.notations) startNote.notations = [];
8116
+ startNote.notations.push({
8117
+ type: "slur",
8118
+ slurType: "start",
8119
+ number: slurNumber,
8120
+ placement: options.placement
8121
+ });
8122
+ if (!endNote.notations) endNote.notations = [];
8123
+ endNote.notations.push({
8124
+ type: "slur",
8125
+ slurType: "stop",
8126
+ number: slurNumber
8127
+ });
8128
+ const validationResult = validate(result, { checkSlurs: true });
8129
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
8130
+ if (criticalErrors.length > 0) {
8131
+ return failure(criticalErrors);
8132
+ }
8133
+ return success(result, validationResult.warnings);
8134
+ }
8135
+ function removeSlur(score, options) {
8136
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8137
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8138
+ }
8139
+ const part = score.parts[options.partIndex];
8140
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8141
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8142
+ }
8143
+ const result = cloneScore(score);
8144
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8145
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8146
+ if (!noteResult) {
8147
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8148
+ }
8149
+ const note = noteResult.note;
8150
+ const slurNumber = options.number ?? 1;
8151
+ if (!note.notations?.some((n) => n.type === "slur" && (n.number ?? 1) === slurNumber)) {
8152
+ return failure([operationError("SLUR_NOT_FOUND", `Slur ${slurNumber} not found on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8153
+ }
8154
+ note.notations = note.notations.filter((n) => !(n.type === "slur" && (n.number ?? 1) === slurNumber));
8155
+ if (note.notations.length === 0) {
8156
+ delete note.notations;
8157
+ }
8158
+ return success(result);
8159
+ }
8160
+ function addArticulation(score, options) {
8161
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8162
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8163
+ }
8164
+ const part = score.parts[options.partIndex];
8165
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8166
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8167
+ }
8168
+ const result = cloneScore(score);
8169
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8170
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8171
+ if (!noteResult) {
8172
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8173
+ }
8174
+ const note = noteResult.note;
8175
+ if (note.notations?.some((n) => n.type === "articulation" && n.articulation === options.articulation)) {
8176
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Articulation ${options.articulation} already exists on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8177
+ }
8178
+ if (!note.notations) note.notations = [];
8179
+ note.notations.push({
8180
+ type: "articulation",
8181
+ articulation: options.articulation,
8182
+ placement: options.placement
8183
+ });
8184
+ return success(result);
8185
+ }
8186
+ function removeArticulation(score, options) {
8187
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8188
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8189
+ }
8190
+ const part = score.parts[options.partIndex];
8191
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8192
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8193
+ }
8194
+ const result = cloneScore(score);
8195
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8196
+ const noteResult = findNoteByIndex(measure, options.noteIndex);
8197
+ if (!noteResult) {
8198
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8199
+ }
8200
+ const note = noteResult.note;
8201
+ if (!note.notations?.some((n) => n.type === "articulation" && n.articulation === options.articulation)) {
8202
+ return failure([operationError("ARTICULATION_NOT_FOUND", `Articulation ${options.articulation} not found on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8203
+ }
8204
+ note.notations = note.notations.filter((n) => !(n.type === "articulation" && n.articulation === options.articulation));
8205
+ if (note.notations.length === 0) {
8206
+ delete note.notations;
8207
+ }
8208
+ return success(result);
8209
+ }
8210
+ function getInsertPositionForDirection(measure, targetPosition) {
8211
+ let position = 0;
8212
+ let insertIndex = 0;
8213
+ for (let i = 0; i < measure.entries.length; i++) {
8214
+ const entry = measure.entries[i];
8215
+ if (position >= targetPosition) {
8216
+ return insertIndex;
8217
+ }
8218
+ if (entry.type === "note" && !entry.chord) {
8219
+ position += entry.duration;
8220
+ } else if (entry.type === "backup") {
8221
+ position -= entry.duration;
8222
+ } else if (entry.type === "forward") {
8223
+ position += entry.duration;
8224
+ }
8225
+ insertIndex = i + 1;
8226
+ }
8227
+ return insertIndex;
8228
+ }
8229
+ function addDynamics(score, options) {
8230
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8231
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8232
+ }
8233
+ const part = score.parts[options.partIndex];
8234
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8235
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8236
+ }
8237
+ if (options.position < 0) {
8238
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8239
+ }
8240
+ const result = cloneScore(score);
8241
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8242
+ const directionEntry = {
8243
+ _id: generateId(),
8244
+ type: "direction",
8245
+ directionTypes: [{
8246
+ kind: "dynamics",
8247
+ value: options.dynamics
8248
+ }],
8249
+ placement: options.placement ?? "below",
8250
+ staff: options.staff
8251
+ };
8252
+ const insertIndex = getInsertPositionForDirection(measure, options.position);
8253
+ measure.entries.splice(insertIndex, 0, directionEntry);
8254
+ return success(result);
8255
+ }
8256
+ function removeDynamics(score, options) {
8257
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8258
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8259
+ }
8260
+ const part = score.parts[options.partIndex];
8261
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8262
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8263
+ }
8264
+ const result = cloneScore(score);
8265
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8266
+ let directionCount = 0;
8267
+ let targetIndex = -1;
8268
+ for (let i = 0; i < measure.entries.length; i++) {
8269
+ const entry = measure.entries[i];
8270
+ if (entry.type === "direction") {
8271
+ const hasDynamics = entry.directionTypes.some((dt) => dt.kind === "dynamics");
8272
+ if (hasDynamics) {
8273
+ if (directionCount === options.directionIndex) {
8274
+ targetIndex = i;
8275
+ break;
8276
+ }
8277
+ directionCount++;
8278
+ }
8279
+ }
8280
+ }
8281
+ if (targetIndex === -1) {
8282
+ return failure([operationError("DYNAMICS_NOT_FOUND", `Dynamics direction index ${options.directionIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8283
+ }
8284
+ measure.entries.splice(targetIndex, 1);
8285
+ return success(result);
8286
+ }
8287
+ function modifyDynamics(score, options) {
8288
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8289
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8290
+ }
8291
+ const part = score.parts[options.partIndex];
8292
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8293
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8294
+ }
8295
+ const result = cloneScore(score);
8296
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8297
+ let dynamicsCount = 0;
8298
+ let targetIndex = -1;
8299
+ for (let i = 0; i < measure.entries.length; i++) {
8300
+ const entry = measure.entries[i];
8301
+ if (entry.type === "direction") {
8302
+ const hasDynamics = entry.directionTypes.some((dt) => dt.kind === "dynamics");
8303
+ if (hasDynamics) {
8304
+ if (dynamicsCount === options.directionIndex) {
8305
+ targetIndex = i;
8306
+ break;
8307
+ }
8308
+ dynamicsCount++;
8309
+ }
8310
+ }
8311
+ }
8312
+ if (targetIndex === -1) {
8313
+ return failure([operationError("DYNAMICS_NOT_FOUND", `Dynamics direction index ${options.directionIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8314
+ }
8315
+ const direction = measure.entries[targetIndex];
8316
+ const dynamicsType = direction.directionTypes.find((dt) => dt.kind === "dynamics");
8317
+ if (dynamicsType && dynamicsType.kind === "dynamics") {
8318
+ dynamicsType.value = options.dynamics;
8319
+ }
8320
+ if (options.placement !== void 0) {
8321
+ direction.placement = options.placement;
8322
+ }
8323
+ return success(result);
8324
+ }
8325
+ function insertClefChange(score, options) {
8326
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8327
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8328
+ }
8329
+ const part = score.parts[options.partIndex];
8330
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8331
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8332
+ }
8333
+ if (options.position < 0) {
8334
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8335
+ }
8336
+ const validSigns = ["G", "F", "C", "percussion", "TAB"];
8337
+ if (!validSigns.includes(options.clef.sign)) {
8338
+ return failure([operationError("INVALID_CLEF", `Invalid clef sign: ${options.clef.sign}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8339
+ }
8340
+ const result = cloneScore(score);
8341
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8342
+ if (options.position === 0) {
8343
+ if (!measure.attributes) {
8344
+ measure.attributes = {};
8345
+ }
8346
+ const staff = options.clef.staff ?? 1;
8347
+ if (!measure.attributes.clef) {
8348
+ measure.attributes.clef = [];
8349
+ }
8350
+ const existingIndex = measure.attributes.clef.findIndex((c) => (c.staff ?? 1) === staff);
8351
+ if (existingIndex >= 0) {
8352
+ measure.attributes.clef[existingIndex] = options.clef;
8353
+ } else {
8354
+ measure.attributes.clef.push(options.clef);
8355
+ }
8356
+ } else {
8357
+ const attributesEntry = {
8358
+ _id: generateId(),
8359
+ type: "attributes",
8360
+ attributes: {
8361
+ clef: [options.clef]
8362
+ }
8363
+ };
8364
+ const insertIndex = getInsertPositionForDirection(measure, options.position);
8365
+ measure.entries.splice(insertIndex, 0, attributesEntry);
8366
+ }
8367
+ const validationResult = validate(result, { checkStaffStructure: true });
8368
+ const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
8369
+ if (criticalErrors.length > 0) {
8370
+ return failure(criticalErrors);
8371
+ }
8372
+ return success(result, validationResult.warnings);
8373
+ }
8374
+ var addNote = (score, options) => {
8375
+ const result = insertNote(score, {
8376
+ partIndex: options.partIndex,
8377
+ measureIndex: options.measureIndex,
8378
+ voice: options.voice,
8379
+ staff: options.staff,
8380
+ position: options.position,
8381
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
8382
+ duration: options.note.duration,
8383
+ noteType: options.note.noteType,
8384
+ dots: options.note.dots
8385
+ });
8386
+ return result.success ? result.data : score;
8387
+ };
8388
+ var deleteNote = (score, options) => {
8389
+ const result = removeNote(score, options);
8390
+ return result.success ? result.data : score;
8391
+ };
8392
+ var addChordNote = (score, options) => {
8393
+ const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
8394
+ return result.success ? result.data : score;
8395
+ };
8396
+ var modifyNotePitch = (score, options) => {
8397
+ const result = setNotePitch(score, options);
8398
+ return result.success ? result.data : score;
8399
+ };
8400
+ var modifyNoteDuration = (score, options) => {
8401
+ const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
8402
+ return result.success ? result.data : score;
8403
+ };
8404
+ var addNoteChecked = (score, options) => {
8405
+ return insertNote(score, {
8406
+ partIndex: options.partIndex,
8407
+ measureIndex: options.measureIndex,
8408
+ voice: options.voice,
8409
+ staff: options.staff,
8410
+ position: options.position,
8411
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
8412
+ duration: options.note.duration,
8413
+ noteType: options.note.noteType,
8414
+ dots: options.note.dots
8415
+ });
8416
+ };
8417
+ var deleteNoteChecked = removeNote;
8418
+ var addChordNoteChecked = (score, options) => {
8419
+ return addChord(score, { ...options, noteIndex: options.afterNoteIndex });
8420
+ };
8421
+ var modifyNotePitchChecked = setNotePitch;
8422
+ var modifyNoteDurationChecked = (score, options) => {
8423
+ return changeNoteDuration(score, { ...options, newDuration: options.duration });
8424
+ };
8425
+ var transposeChecked = transpose;
8426
+ function createTuplet(score, options) {
8427
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8428
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8429
+ }
8430
+ const part = score.parts[options.partIndex];
8431
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8432
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8433
+ }
8434
+ if (options.noteCount < 2) {
8435
+ return failure([operationError("INVALID_DURATION", "Tuplet must contain at least 2 notes", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8436
+ }
8437
+ if (options.actualNotes < 2 || options.normalNotes < 1) {
8438
+ return failure([operationError("INVALID_DURATION", "Invalid tuplet ratio", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8439
+ }
8440
+ const result = cloneScore(score);
8441
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8442
+ const notes = [];
8443
+ let noteCount = 0;
8444
+ for (let i = 0; i < measure.entries.length; i++) {
8445
+ const entry = measure.entries[i];
8446
+ if (entry.type === "note" && !entry.rest && !entry.chord) {
8447
+ if (noteCount >= options.startNoteIndex && noteCount < options.startNoteIndex + options.noteCount) {
8448
+ notes.push({ note: entry, entryIndex: i });
8449
+ }
8450
+ noteCount++;
8451
+ }
8452
+ }
8453
+ if (notes.length !== options.noteCount) {
8454
+ return failure([operationError("NOTE_NOT_FOUND", `Could not find ${options.noteCount} notes starting at index ${options.startNoteIndex}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8455
+ }
8456
+ const voice = notes[0].note.voice;
8457
+ const staff = notes[0].note.staff;
8458
+ if (!notes.every((n) => n.note.voice === voice)) {
8459
+ return failure([operationError("NOTE_CONFLICT", "All notes in a tuplet must be in the same voice", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice })]);
8460
+ }
8461
+ if (!notes.every((n) => n.note.staff === staff)) {
8462
+ return failure([operationError("NOTE_CONFLICT", "All notes in a tuplet must be on the same staff", { partIndex: options.partIndex, measureIndex: options.measureIndex, staff })]);
8463
+ }
8464
+ const tupletNumber = 1;
8465
+ for (let i = 0; i < notes.length; i++) {
8466
+ const { note } = notes[i];
8467
+ note.timeModification = {
8468
+ actualNotes: options.actualNotes,
8469
+ normalNotes: options.normalNotes
8470
+ };
8471
+ if (!note.notations) note.notations = [];
8472
+ if (i === 0) {
8473
+ note.notations.push({
8474
+ type: "tuplet",
8475
+ tupletType: "start",
8476
+ number: tupletNumber,
8477
+ bracket: options.bracket ?? true,
8478
+ showNumber: options.showNumber ?? "actual",
8479
+ tupletActual: { tupletNumber: options.actualNotes },
8480
+ tupletNormal: { tupletNumber: options.normalNotes }
8481
+ });
8482
+ } else if (i === notes.length - 1) {
8483
+ note.notations.push({
8484
+ type: "tuplet",
8485
+ tupletType: "stop",
8486
+ number: tupletNumber
8487
+ });
8488
+ }
8489
+ }
8490
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8491
+ const errors = validateMeasureLocal(measure, context, {
8492
+ checkTuplets: true,
8493
+ checkMeasureDuration: true
8494
+ });
8495
+ const criticalErrors = errors.filter((e) => e.level === "error");
8496
+ if (criticalErrors.length > 0) {
8497
+ return failure(criticalErrors);
8498
+ }
8499
+ return success(result, errors.filter((e) => e.level !== "error"));
8500
+ }
8501
+ function removeTuplet(score, options) {
8502
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8503
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8504
+ }
8505
+ const part = score.parts[options.partIndex];
8506
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8507
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8508
+ }
8509
+ const result = cloneScore(score);
8510
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8511
+ let noteCount = 0;
8512
+ let targetNote = null;
8513
+ let targetEntryIndex = -1;
8514
+ for (let i = 0; i < measure.entries.length; i++) {
8515
+ const entry = measure.entries[i];
8516
+ if (entry.type === "note" && !entry.rest) {
8517
+ if (noteCount === options.noteIndex) {
8518
+ targetNote = entry;
8519
+ targetEntryIndex = i;
8520
+ break;
8521
+ }
8522
+ noteCount++;
8523
+ }
8524
+ }
8525
+ if (!targetNote || targetEntryIndex === -1) {
8526
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8527
+ }
8528
+ if (!targetNote.timeModification) {
8529
+ return failure([operationError("NOTE_NOT_FOUND", "Note is not part of a tuplet", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8530
+ }
8531
+ const voice = targetNote.voice;
8532
+ const staff = targetNote.staff;
8533
+ const actualNotes = targetNote.timeModification.actualNotes;
8534
+ const normalNotes = targetNote.timeModification.normalNotes;
8535
+ const tupletNotes = [];
8536
+ let inTuplet = false;
8537
+ let currentTupletNumber;
8538
+ for (const entry of measure.entries) {
8539
+ if (entry.type !== "note" || entry.rest) continue;
8540
+ if (entry.voice !== voice || entry.staff !== staff) continue;
8541
+ const hasSameTimeModification = entry.timeModification?.actualNotes === actualNotes && entry.timeModification?.normalNotes === normalNotes;
8542
+ const tupletStart = entry.notations?.find(
8543
+ (n) => n.type === "tuplet" && n.tupletType === "start"
8544
+ );
8545
+ const tupletStop = entry.notations?.find(
8546
+ (n) => n.type === "tuplet" && n.tupletType === "stop" && (currentTupletNumber === void 0 || n.number === currentTupletNumber)
8547
+ );
8548
+ if (tupletStart && tupletStart.type === "tuplet") {
8549
+ inTuplet = true;
8550
+ currentTupletNumber = tupletStart.number;
8551
+ }
8552
+ if (inTuplet && hasSameTimeModification) {
8553
+ tupletNotes.push(entry);
8554
+ }
8555
+ if (tupletStop && inTuplet) {
8556
+ if (tupletNotes.includes(targetNote)) {
8557
+ break;
8558
+ } else {
8559
+ tupletNotes.length = 0;
8560
+ inTuplet = false;
8561
+ currentTupletNumber = void 0;
8562
+ }
8563
+ }
8564
+ }
8565
+ if (tupletNotes.length === 0) {
8566
+ tupletNotes.push(targetNote);
8567
+ }
8568
+ for (const note of tupletNotes) {
8569
+ delete note.timeModification;
8570
+ if (note.notations) {
8571
+ note.notations = note.notations.filter((n) => n.type !== "tuplet");
8572
+ if (note.notations.length === 0) {
8573
+ delete note.notations;
8574
+ }
8575
+ }
8576
+ }
8577
+ return success(result);
8578
+ }
8579
+ function addBeam(score, options) {
8580
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8581
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8582
+ }
8583
+ const part = score.parts[options.partIndex];
8584
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8585
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8586
+ }
8587
+ if (options.noteCount < 2) {
8588
+ return failure([operationError("INVALID_DURATION", "Beam must contain at least 2 notes", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8589
+ }
8590
+ const result = cloneScore(score);
8591
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8592
+ const beamLevel = options.beamLevel ?? 1;
8593
+ const notes = [];
8594
+ let noteCount = 0;
8595
+ for (const entry of measure.entries) {
8596
+ if (entry.type === "note" && !entry.rest && !entry.chord) {
8597
+ if (noteCount >= options.startNoteIndex && noteCount < options.startNoteIndex + options.noteCount) {
8598
+ notes.push(entry);
8599
+ }
8600
+ noteCount++;
8601
+ }
8602
+ }
8603
+ if (notes.length !== options.noteCount) {
8604
+ return failure([operationError("NOTE_NOT_FOUND", `Could not find ${options.noteCount} notes starting at index ${options.startNoteIndex}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8605
+ }
8606
+ const voice = notes[0].voice;
8607
+ if (!notes.every((n) => n.voice === voice)) {
8608
+ return failure([operationError("NOTE_CONFLICT", "All beamed notes must be in the same voice", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice })]);
8609
+ }
8610
+ for (let i = 0; i < notes.length; i++) {
8611
+ const note = notes[i];
8612
+ if (!note.beam) {
8613
+ note.beam = [];
8614
+ }
8615
+ note.beam = note.beam.filter((b) => b.number !== beamLevel);
8616
+ let beamType;
8617
+ if (i === 0) {
8618
+ beamType = "begin";
8619
+ } else if (i === notes.length - 1) {
8620
+ beamType = "end";
8621
+ } else {
8622
+ beamType = "continue";
8623
+ }
8624
+ note.beam.push({
8625
+ number: beamLevel,
8626
+ type: beamType
8627
+ });
8628
+ }
8629
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8630
+ const errors = validateMeasureLocal(measure, context, { checkBeams: true });
8631
+ const criticalErrors = errors.filter((e) => e.level === "error");
8632
+ if (criticalErrors.length > 0) {
8633
+ return failure(criticalErrors);
8634
+ }
8635
+ return success(result, errors.filter((e) => e.level !== "error"));
8636
+ }
8637
+ function removeBeam(score, options) {
8638
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8639
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8640
+ }
8641
+ const part = score.parts[options.partIndex];
8642
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8643
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8644
+ }
8645
+ const result = cloneScore(score);
8646
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8647
+ let noteCount = 0;
8648
+ let targetNote = null;
8649
+ for (const entry of measure.entries) {
8650
+ if (entry.type === "note" && !entry.rest) {
8651
+ if (noteCount === options.noteIndex) {
8652
+ targetNote = entry;
8653
+ break;
8654
+ }
8655
+ noteCount++;
8656
+ }
8657
+ }
8658
+ if (!targetNote) {
8659
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8660
+ }
8661
+ if (!targetNote.beam || targetNote.beam.length === 0) {
8662
+ return failure([operationError("NOTE_NOT_FOUND", "Note is not part of a beam group", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8663
+ }
8664
+ const voice = targetNote.voice;
8665
+ const staff = targetNote.staff;
8666
+ const beamNotes = [];
8667
+ let inBeam = false;
8668
+ const targetBeamLevel = options.beamLevel ?? targetNote.beam[0]?.number ?? 1;
8669
+ for (const entry of measure.entries) {
8670
+ if (entry.type !== "note" || entry.rest) continue;
8671
+ if (entry.voice !== voice || entry.staff !== staff) continue;
8672
+ const beamInfo = entry.beam?.find((b) => b.number === targetBeamLevel);
8673
+ if (!beamInfo) {
8674
+ if (inBeam) {
8675
+ break;
8676
+ }
8677
+ continue;
8678
+ }
8679
+ if (beamInfo.type === "begin") {
8680
+ inBeam = true;
8681
+ beamNotes.push(entry);
8682
+ } else if (beamInfo.type === "continue") {
8683
+ if (inBeam) beamNotes.push(entry);
8684
+ } else if (beamInfo.type === "end") {
8685
+ beamNotes.push(entry);
8686
+ if (beamNotes.includes(targetNote)) {
8687
+ break;
8688
+ } else {
8689
+ beamNotes.length = 0;
8690
+ inBeam = false;
8691
+ }
8692
+ }
8693
+ }
8694
+ if (beamNotes.length === 0) {
8695
+ beamNotes.push(targetNote);
8696
+ }
8697
+ for (const note of beamNotes) {
8698
+ if (note.beam) {
8699
+ if (options.beamLevel !== void 0) {
8700
+ note.beam = note.beam.filter((b) => b.number !== options.beamLevel);
8701
+ } else {
8702
+ note.beam = [];
8703
+ }
8704
+ if (note.beam.length === 0) {
8705
+ delete note.beam;
8706
+ }
8707
+ }
8708
+ }
8709
+ return success(result);
8710
+ }
8711
+ function autoBeam(score, options) {
8712
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8713
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8714
+ }
8715
+ const part = score.parts[options.partIndex];
8716
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8717
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8718
+ }
8719
+ const result = cloneScore(score);
8720
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8721
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8722
+ const divisions = context.divisions;
8723
+ const time = context.time ?? { beats: "4", beatType: 4 };
8724
+ const beatDuration = 4 / time.beatType * divisions;
8725
+ for (const entry of measure.entries) {
8726
+ if (entry.type === "note") {
8727
+ delete entry.beam;
8728
+ }
8729
+ }
8730
+ const notesByVoice = /* @__PURE__ */ new Map();
8731
+ let position = 0;
8732
+ for (const entry of measure.entries) {
8733
+ if (entry.type === "note") {
8734
+ if (!entry.chord && !entry.rest) {
8735
+ const voice = entry.voice;
8736
+ if (options.voice === void 0 || voice === options.voice) {
8737
+ if (!notesByVoice.has(voice)) {
8738
+ notesByVoice.set(voice, []);
8739
+ }
8740
+ notesByVoice.get(voice).push({ note: entry, position });
8741
+ }
8742
+ }
8743
+ if (!entry.chord) {
8744
+ position += entry.duration;
8745
+ }
8746
+ } else if (entry.type === "backup") {
8747
+ position -= entry.duration;
8748
+ } else if (entry.type === "forward") {
8749
+ position += entry.duration;
8750
+ }
8751
+ }
8752
+ for (const [, notes] of notesByVoice) {
8753
+ const beatGroups = [];
8754
+ let currentBeat = -1;
8755
+ let currentGroup = [];
8756
+ for (const { note, position: notePos } of notes) {
8757
+ if (note.duration > beatDuration / 2) {
8758
+ if (currentGroup.length >= 2) {
8759
+ beatGroups.push(currentGroup);
8760
+ }
8761
+ currentGroup = [];
8762
+ currentBeat = -1;
8763
+ continue;
8764
+ }
8765
+ const beat = Math.floor(notePos / beatDuration);
8766
+ if (options.groupByBeat !== false && beat !== currentBeat) {
8767
+ if (currentGroup.length >= 2) {
8768
+ beatGroups.push(currentGroup);
8769
+ }
8770
+ currentGroup = [{ note, position: notePos }];
8771
+ currentBeat = beat;
8772
+ } else {
8773
+ currentGroup.push({ note, position: notePos });
8774
+ }
8775
+ }
8776
+ if (currentGroup.length >= 2) {
8777
+ beatGroups.push(currentGroup);
8778
+ }
8779
+ for (const group of beatGroups) {
8780
+ for (let i = 0; i < group.length; i++) {
8781
+ const { note } = group[i];
8782
+ if (!note.beam) {
8783
+ note.beam = [];
8784
+ }
8785
+ let beamType;
8786
+ if (i === 0) {
8787
+ beamType = "begin";
8788
+ } else if (i === group.length - 1) {
8789
+ beamType = "end";
8790
+ } else {
8791
+ beamType = "continue";
8792
+ }
8793
+ note.beam.push({
8794
+ number: 1,
8795
+ type: beamType
8796
+ });
8797
+ }
8798
+ }
8799
+ }
8800
+ const errors = validateMeasureLocal(measure, context, { checkBeams: true });
8801
+ const criticalErrors = errors.filter((e) => e.level === "error");
8802
+ if (criticalErrors.length > 0) {
8803
+ return failure(criticalErrors);
8804
+ }
8805
+ return success(result, errors.filter((e) => e.level !== "error"));
8806
+ }
8807
+ function copyNotes(score, options) {
8808
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8809
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8810
+ }
8811
+ const part = score.parts[options.partIndex];
8812
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8813
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8814
+ }
8815
+ if (options.startPosition >= options.endPosition) {
8816
+ return failure([operationError("INVALID_POSITION", "Start position must be less than end position", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8817
+ }
8818
+ const measure = part.measures[options.measureIndex];
8819
+ const copiedNotes = [];
8820
+ let position = 0;
8821
+ for (const entry of measure.entries) {
8822
+ if (entry.type === "note") {
8823
+ if (entry.voice === options.voice && (options.staff === void 0 || (entry.staff ?? 1) === options.staff)) {
8824
+ if (!entry.chord) {
8825
+ const noteEnd = position + entry.duration;
8826
+ if (position < options.endPosition && noteEnd > options.startPosition) {
8827
+ const clonedNote = cloneNoteWithNewId(entry);
8828
+ if (clonedNote.tie) {
8829
+ }
8830
+ copiedNotes.push({
8831
+ relativePosition: position - options.startPosition,
8832
+ note: clonedNote
8833
+ });
8834
+ }
8835
+ position += entry.duration;
8836
+ } else {
8837
+ if (copiedNotes.length > 0) {
8838
+ const lastCopied = copiedNotes[copiedNotes.length - 1];
8839
+ if (lastCopied.note.voice === entry.voice && (options.staff === void 0 || (lastCopied.note.staff ?? 1) === (entry.staff ?? 1))) {
8840
+ const clonedNote = cloneNoteWithNewId(entry);
8841
+ copiedNotes.push({
8842
+ relativePosition: lastCopied.relativePosition,
8843
+ note: clonedNote
8844
+ });
8845
+ }
8846
+ }
8847
+ }
8848
+ } else if (!entry.chord) {
8849
+ position += entry.duration;
8850
+ }
8851
+ } else if (entry.type === "backup") {
8852
+ position -= entry.duration;
8853
+ } else if (entry.type === "forward") {
8854
+ position += entry.duration;
8855
+ }
8856
+ }
8857
+ if (copiedNotes.length === 0) {
8858
+ return failure([operationError("NOTE_NOT_FOUND", "No notes found in the specified range", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice })]);
8859
+ }
8860
+ const selection = {
8861
+ source: {
8862
+ partIndex: options.partIndex,
8863
+ measureIndex: options.measureIndex,
8864
+ startPosition: options.startPosition,
8865
+ endPosition: options.endPosition,
8866
+ voice: options.voice,
8867
+ staff: options.staff
8868
+ },
8869
+ notes: copiedNotes,
8870
+ duration: options.endPosition - options.startPosition
8871
+ };
8872
+ return success(selection);
8873
+ }
8874
+ function pasteNotes(score, options) {
8875
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
8876
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
8877
+ }
8878
+ const part = score.parts[options.partIndex];
8879
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
8880
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8881
+ }
8882
+ if (options.position < 0) {
8883
+ return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
8884
+ }
8885
+ const result = cloneScore(score);
8886
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8887
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8888
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
8889
+ const targetVoice = options.voice ?? options.selection.source.voice;
8890
+ const targetStaff = options.staff ?? options.selection.source.staff;
8891
+ const pasteEnd = options.position + options.selection.duration;
8892
+ if (pasteEnd > measureDuration) {
8893
+ return failure([operationError(
8894
+ "EXCEEDS_MEASURE",
8895
+ `Paste would exceed measure duration (ends at ${pasteEnd}, measure is ${measureDuration})`,
8896
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
8897
+ { pasteEnd, measureDuration }
8898
+ )]);
8899
+ }
8900
+ const voiceEntries = getVoiceEntries(measure, targetVoice, targetStaff);
8901
+ if (options.overwrite !== false) {
8902
+ const entriesToKeep = voiceEntries.filter((e) => {
8903
+ if (e.entry.type !== "note") return true;
8904
+ const note = e.entry;
8905
+ if (note.rest) return true;
8906
+ return e.endPosition <= options.position || e.position >= pasteEnd;
8907
+ });
8908
+ const newEntries = [];
8909
+ for (const { position, entry } of entriesToKeep) {
8910
+ if (entry.type === "note") {
8911
+ newEntries.push({ position, entry });
8912
+ }
8913
+ }
8914
+ for (const { relativePosition, note } of options.selection.notes) {
8915
+ const pastePosition = options.position + Math.max(0, relativePosition);
8916
+ const newNote = cloneNoteWithNewId(note);
8917
+ newNote.voice = targetVoice;
8918
+ if (targetStaff !== void 0) {
8919
+ newNote.staff = targetStaff;
8920
+ }
8921
+ delete newNote.tie;
8922
+ delete newNote.ties;
8923
+ if (newNote.notations) {
8924
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
8925
+ if (newNote.notations.length === 0) {
8926
+ delete newNote.notations;
8927
+ }
8928
+ }
8929
+ newEntries.push({ position: pastePosition, entry: newNote });
8930
+ }
8931
+ measure.entries = rebuildMeasureWithVoice(
8932
+ measure,
8933
+ targetVoice,
8934
+ newEntries,
8935
+ measureDuration,
8936
+ targetStaff
8937
+ );
8938
+ } else {
8939
+ const { hasNotes: hasNotes2, conflictingNotes } = hasNotesInRange(voiceEntries, options.position, pasteEnd);
8940
+ if (hasNotes2) {
8941
+ return failure([operationError(
8942
+ "NOTE_CONFLICT",
8943
+ `Paste range ${options.position}-${pasteEnd} conflicts with existing notes`,
8944
+ { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: targetVoice },
8945
+ { conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
8946
+ )]);
8947
+ }
8948
+ const existingNotes = voiceEntries.filter((e) => e.entry.type === "note").map((e) => ({ position: e.position, entry: e.entry }));
8949
+ for (const { relativePosition, note } of options.selection.notes) {
8950
+ const pastePosition = options.position + Math.max(0, relativePosition);
8951
+ const newNote = cloneNoteWithNewId(note);
8952
+ newNote.voice = targetVoice;
8953
+ if (targetStaff !== void 0) {
8954
+ newNote.staff = targetStaff;
8955
+ }
8956
+ delete newNote.tie;
8957
+ delete newNote.ties;
8958
+ if (newNote.notations) {
8959
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
8960
+ if (newNote.notations.length === 0) {
8961
+ delete newNote.notations;
8962
+ }
8963
+ }
8964
+ existingNotes.push({ position: pastePosition, entry: newNote });
8965
+ }
8966
+ measure.entries = rebuildMeasureWithVoice(
8967
+ measure,
8968
+ targetVoice,
8969
+ existingNotes,
8970
+ measureDuration,
8971
+ targetStaff
8972
+ );
8973
+ }
8974
+ const errors = validateMeasureLocal(measure, context, {
8975
+ checkMeasureDuration: true,
8976
+ checkPosition: true,
8977
+ checkVoiceStaff: true
8978
+ });
8979
+ const criticalErrors = errors.filter((e) => e.level === "error");
8980
+ if (criticalErrors.length > 0) {
8981
+ return failure(criticalErrors);
8982
+ }
8983
+ return success(result, errors.filter((e) => e.level !== "error"));
8984
+ }
8985
+ function cutNotes(score, options) {
8986
+ const copyResult = copyNotes(score, options);
8987
+ if (!copyResult.success) {
8988
+ return failure(copyResult.errors);
8989
+ }
8990
+ const selection = copyResult.data;
8991
+ const result = cloneScore(score);
8992
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
8993
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
8994
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
8995
+ const voiceEntries = getVoiceEntries(measure, options.voice, options.staff);
8996
+ const entriesToKeep = voiceEntries.filter((e) => {
8997
+ if (e.entry.type !== "note") return true;
8998
+ const note = e.entry;
8999
+ if (note.rest) return true;
9000
+ return e.endPosition <= options.startPosition || e.position >= options.endPosition;
9001
+ });
9002
+ const newEntries = [];
9003
+ for (const { position, entry } of entriesToKeep) {
9004
+ if (entry.type === "note") {
9005
+ newEntries.push({ position, entry });
9006
+ }
9007
+ }
9008
+ measure.entries = rebuildMeasureWithVoice(
9009
+ measure,
9010
+ options.voice,
9011
+ newEntries,
9012
+ measureDuration,
9013
+ options.staff
9014
+ );
9015
+ const errors = validateMeasureLocal(measure, context, {
9016
+ checkMeasureDuration: true,
9017
+ checkPosition: true,
9018
+ checkVoiceStaff: true
9019
+ });
9020
+ const criticalErrors = errors.filter((e) => e.level === "error");
9021
+ if (criticalErrors.length > 0) {
9022
+ return failure(criticalErrors);
9023
+ }
9024
+ return success(
9025
+ { score: result, selection },
9026
+ errors.filter((e) => e.level !== "error")
9027
+ );
9028
+ }
9029
+ function copyNotesMultiMeasure(score, options) {
9030
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9031
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9032
+ }
9033
+ const part = score.parts[options.partIndex];
9034
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
9035
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
9036
+ }
9037
+ if (options.endMeasureIndex < options.startMeasureIndex || options.endMeasureIndex >= part.measures.length) {
9038
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
9039
+ }
9040
+ const selection = {
9041
+ source: {
9042
+ partIndex: options.partIndex,
9043
+ startMeasureIndex: options.startMeasureIndex,
9044
+ endMeasureIndex: options.endMeasureIndex,
9045
+ voice: options.voice,
9046
+ staff: options.staff
9047
+ },
9048
+ measures: []
9049
+ };
9050
+ for (let measureIndex = options.startMeasureIndex; measureIndex <= options.endMeasureIndex; measureIndex++) {
9051
+ const measure = part.measures[measureIndex];
9052
+ const measureOffset = measureIndex - options.startMeasureIndex;
9053
+ const copiedNotes = [];
9054
+ let position = 0;
9055
+ for (const entry of measure.entries) {
9056
+ if (entry.type === "note") {
9057
+ if (entry.voice === options.voice && (options.staff === void 0 || (entry.staff ?? 1) === options.staff)) {
9058
+ if (!entry.chord && !entry.rest) {
9059
+ const clonedNote = cloneNoteWithNewId(entry);
9060
+ copiedNotes.push({
9061
+ relativePosition: position,
9062
+ note: clonedNote
9063
+ });
9064
+ position += entry.duration;
9065
+ } else if (entry.chord && copiedNotes.length > 0) {
9066
+ const clonedNote = cloneNoteWithNewId(entry);
9067
+ copiedNotes.push({
9068
+ relativePosition: copiedNotes[copiedNotes.length - 1].relativePosition,
9069
+ note: clonedNote
9070
+ });
9071
+ } else if (!entry.chord) {
9072
+ position += entry.duration;
9073
+ }
9074
+ } else if (!entry.chord) {
9075
+ position += entry.duration;
9076
+ }
9077
+ } else if (entry.type === "backup") {
9078
+ position -= entry.duration;
9079
+ } else if (entry.type === "forward") {
9080
+ position += entry.duration;
9081
+ }
9082
+ }
9083
+ if (copiedNotes.length > 0) {
9084
+ selection.measures.push({
9085
+ measureOffset,
9086
+ notes: copiedNotes
9087
+ });
9088
+ }
9089
+ }
9090
+ if (selection.measures.length === 0) {
9091
+ return failure([operationError("NOTE_NOT_FOUND", "No notes found in the specified range", { partIndex: options.partIndex, voice: options.voice })]);
9092
+ }
9093
+ return success(selection);
9094
+ }
9095
+ function pasteNotesMultiMeasure(score, options) {
9096
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9097
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9098
+ }
9099
+ const part = score.parts[options.partIndex];
9100
+ const measureCount = options.selection.measures.length > 0 ? options.selection.measures[options.selection.measures.length - 1].measureOffset + 1 : 0;
9101
+ if (options.startMeasureIndex + measureCount > part.measures.length) {
9102
+ return failure([operationError("MEASURE_NOT_FOUND", `Not enough measures to paste (need ${measureCount})`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
9103
+ }
9104
+ let result = cloneScore(score);
9105
+ const targetVoice = options.voice ?? options.selection.source.voice;
9106
+ const targetStaff = options.staff ?? options.selection.source.staff;
9107
+ for (const measureData of options.selection.measures) {
9108
+ const measureIndex = options.startMeasureIndex + measureData.measureOffset;
9109
+ const measure = result.parts[options.partIndex].measures[measureIndex];
9110
+ const context = getMeasureContext(result, options.partIndex, measureIndex);
9111
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
9112
+ const voiceEntries = getVoiceEntries(measure, targetVoice, targetStaff);
9113
+ let entriesToKeep;
9114
+ if (options.overwrite !== false) {
9115
+ entriesToKeep = voiceEntries.filter((e) => e.entry.type === "note" && e.entry.rest).map((e) => ({ position: e.position, entry: e.entry }));
9116
+ } else {
9117
+ entriesToKeep = voiceEntries.filter((e) => e.entry.type === "note").map((e) => ({ position: e.position, entry: e.entry }));
9118
+ }
9119
+ for (const { relativePosition, note } of measureData.notes) {
9120
+ const newNote = cloneNoteWithNewId(note);
9121
+ newNote.voice = targetVoice;
9122
+ if (targetStaff !== void 0) {
9123
+ newNote.staff = targetStaff;
9124
+ }
9125
+ delete newNote.tie;
9126
+ delete newNote.ties;
9127
+ if (newNote.notations) {
9128
+ newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
9129
+ if (newNote.notations.length === 0) {
9130
+ delete newNote.notations;
9131
+ }
9132
+ }
9133
+ entriesToKeep.push({ position: relativePosition, entry: newNote });
9134
+ }
9135
+ measure.entries = rebuildMeasureWithVoice(
9136
+ measure,
9137
+ targetVoice,
9138
+ entriesToKeep,
9139
+ measureDuration,
9140
+ targetStaff
9141
+ );
9142
+ const errors = validateMeasureLocal(measure, context, {
9143
+ checkMeasureDuration: true,
9144
+ checkPosition: true,
9145
+ checkVoiceStaff: true
9146
+ });
9147
+ const criticalErrors = errors.filter((e) => e.level === "error");
9148
+ if (criticalErrors.length > 0) {
9149
+ return failure(criticalErrors);
9150
+ }
9151
+ }
9152
+ return success(result);
9153
+ }
9154
+ function addTempo(score, options) {
9155
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9156
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9157
+ }
9158
+ const part = score.parts[options.partIndex];
9159
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9160
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9161
+ }
9162
+ if (options.bpm <= 0) {
9163
+ return failure([operationError("INVALID_DURATION", "BPM must be positive", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9164
+ }
9165
+ const result = cloneScore(score);
9166
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9167
+ const directionTypes = [];
9168
+ directionTypes.push({
9169
+ kind: "metronome",
9170
+ beatUnit: options.beatUnit ?? "quarter",
9171
+ beatUnitDot: options.beatUnitDot,
9172
+ perMinute: options.bpm
9173
+ });
9174
+ if (options.text) {
9175
+ directionTypes.push({
9176
+ kind: "words",
9177
+ text: options.text,
9178
+ fontWeight: "bold"
9179
+ });
9180
+ }
9181
+ const direction = {
9182
+ _id: generateId(),
9183
+ type: "direction",
9184
+ directionTypes,
9185
+ placement: options.placement ?? "above",
9186
+ sound: { tempo: options.bpm }
9187
+ };
9188
+ insertDirectionAtPosition(measure, direction, options.position);
9189
+ return success(result);
9190
+ }
9191
+ function removeTempo(score, options) {
9192
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9193
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9194
+ }
9195
+ const part = score.parts[options.partIndex];
9196
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9197
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9198
+ }
9199
+ const result = cloneScore(score);
9200
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9201
+ const tempoDirectionIndices = [];
9202
+ for (let i = 0; i < measure.entries.length; i++) {
9203
+ const entry = measure.entries[i];
9204
+ if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "metronome")) {
9205
+ tempoDirectionIndices.push(i);
9206
+ }
9207
+ }
9208
+ if (tempoDirectionIndices.length === 0) {
9209
+ return failure([operationError("TEMPO_NOT_FOUND", "No tempo marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9210
+ }
9211
+ const targetIndex = options.directionIndex ?? 0;
9212
+ if (targetIndex < 0 || targetIndex >= tempoDirectionIndices.length) {
9213
+ return failure([operationError("TEMPO_NOT_FOUND", `Tempo direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9214
+ }
9215
+ measure.entries.splice(tempoDirectionIndices[targetIndex], 1);
9216
+ return success(result);
9217
+ }
9218
+ function modifyTempo(score, options) {
9219
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9220
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9221
+ }
9222
+ const part = score.parts[options.partIndex];
9223
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9224
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9225
+ }
9226
+ const result = cloneScore(score);
9227
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9228
+ const tempoDirectionIndices = [];
9229
+ for (let i = 0; i < measure.entries.length; i++) {
9230
+ const entry = measure.entries[i];
9231
+ if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "metronome")) {
9232
+ tempoDirectionIndices.push(i);
9233
+ }
9234
+ }
9235
+ if (tempoDirectionIndices.length === 0) {
9236
+ return failure([operationError("TEMPO_NOT_FOUND", "No tempo marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9237
+ }
9238
+ const targetIndex = options.directionIndex ?? 0;
9239
+ if (targetIndex < 0 || targetIndex >= tempoDirectionIndices.length) {
9240
+ return failure([operationError("TEMPO_NOT_FOUND", `Tempo direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9241
+ }
9242
+ const direction = measure.entries[tempoDirectionIndices[targetIndex]];
9243
+ const metronome = direction.directionTypes.find((dt) => dt.kind === "metronome");
9244
+ if (metronome && metronome.kind === "metronome") {
9245
+ if (options.bpm !== void 0) {
9246
+ metronome.perMinute = options.bpm;
9247
+ }
9248
+ if (options.beatUnit !== void 0) {
9249
+ metronome.beatUnit = options.beatUnit;
9250
+ }
9251
+ if (options.beatUnitDot !== void 0) {
9252
+ metronome.beatUnitDot = options.beatUnitDot;
9253
+ }
9254
+ }
9255
+ if (options.text !== void 0) {
9256
+ const wordsIndex = direction.directionTypes.findIndex((dt) => dt.kind === "words");
9257
+ if (wordsIndex >= 0) {
9258
+ const words = direction.directionTypes[wordsIndex];
9259
+ if (words.kind === "words") {
9260
+ words.text = options.text;
9261
+ }
9262
+ } else if (options.text) {
9263
+ direction.directionTypes.push({
9264
+ kind: "words",
9265
+ text: options.text,
9266
+ fontWeight: "bold"
9267
+ });
9268
+ }
9269
+ }
9270
+ if (options.bpm !== void 0 && direction.sound) {
9271
+ direction.sound.tempo = options.bpm;
9272
+ }
9273
+ if (options.placement !== void 0) {
9274
+ direction.placement = options.placement;
9275
+ }
9276
+ return success(result);
9277
+ }
9278
+ function addWedge(score, options) {
9279
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9280
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9281
+ }
9282
+ const part = score.parts[options.partIndex];
9283
+ if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
9284
+ return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
9285
+ }
9286
+ if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
9287
+ return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
9288
+ }
9289
+ if (options.endMeasureIndex < options.startMeasureIndex || options.endMeasureIndex === options.startMeasureIndex && options.endPosition <= options.startPosition) {
9290
+ return failure([operationError("INVALID_RANGE", "End position must be after start position", { partIndex: options.partIndex })]);
9291
+ }
9292
+ const result = cloneScore(score);
9293
+ const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
9294
+ const startDirection = {
9295
+ _id: generateId(),
9296
+ type: "direction",
9297
+ directionTypes: [{
9298
+ kind: "wedge",
9299
+ type: options.type
9300
+ }],
9301
+ placement: options.placement ?? "below",
9302
+ staff: options.staff
9303
+ };
9304
+ insertDirectionAtPosition(startMeasure, startDirection, options.startPosition);
9305
+ const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
9306
+ const endDirection = {
9307
+ _id: generateId(),
9308
+ type: "direction",
9309
+ directionTypes: [{
9310
+ kind: "wedge",
9311
+ type: "stop"
9312
+ }],
9313
+ placement: options.placement ?? "below",
9314
+ staff: options.staff
9315
+ };
9316
+ insertDirectionAtPosition(endMeasure, endDirection, options.endPosition);
9317
+ return success(result);
9318
+ }
9319
+ function removeWedge(score, options) {
9320
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9321
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9322
+ }
9323
+ const part = score.parts[options.partIndex];
9324
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9325
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9326
+ }
9327
+ const result = cloneScore(score);
9328
+ const wedgeStarts = [];
9329
+ for (let mi = options.measureIndex; mi < result.parts[options.partIndex].measures.length; mi++) {
9330
+ const measure = result.parts[options.partIndex].measures[mi];
9331
+ for (let ei = 0; ei < measure.entries.length; ei++) {
9332
+ const entry = measure.entries[ei];
9333
+ if (entry.type === "direction") {
9334
+ const wedgeType = entry.directionTypes.find((dt) => dt.kind === "wedge");
9335
+ if (wedgeType && wedgeType.kind === "wedge" && (wedgeType.type === "crescendo" || wedgeType.type === "diminuendo")) {
9336
+ wedgeStarts.push({ measureIndex: mi, entryIndex: ei });
9337
+ }
9338
+ }
9339
+ }
9340
+ if (mi === options.measureIndex && wedgeStarts.length > 0) break;
9341
+ }
9342
+ if (wedgeStarts.length === 0) {
9343
+ return failure([operationError("WEDGE_NOT_FOUND", "No wedge found starting in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9344
+ }
9345
+ const targetIndex = options.directionIndex ?? 0;
9346
+ if (targetIndex >= wedgeStarts.length) {
9347
+ return failure([operationError("WEDGE_NOT_FOUND", `Wedge direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9348
+ }
9349
+ const startInfo = wedgeStarts[targetIndex];
9350
+ const startMeasure = result.parts[options.partIndex].measures[startInfo.measureIndex];
9351
+ startMeasure.entries.splice(startInfo.entryIndex, 1);
9352
+ for (let mi = startInfo.measureIndex; mi < result.parts[options.partIndex].measures.length; mi++) {
9353
+ const measure = result.parts[options.partIndex].measures[mi];
9354
+ for (let ei = 0; ei < measure.entries.length; ei++) {
9355
+ const entry = measure.entries[ei];
9356
+ if (entry.type === "direction") {
9357
+ const wedgeType = entry.directionTypes.find((dt) => dt.kind === "wedge" && dt.type === "stop");
9358
+ if (wedgeType) {
9359
+ measure.entries.splice(ei, 1);
9360
+ return success(result);
9361
+ }
9362
+ }
9363
+ }
9364
+ }
9365
+ return success(result);
9366
+ }
9367
+ function addFermata(score, options) {
9368
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9369
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9370
+ }
9371
+ const part = score.parts[options.partIndex];
9372
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9373
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9374
+ }
9375
+ const result = cloneScore(score);
9376
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9377
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9378
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9379
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9380
+ }
9381
+ const note = notes[options.noteIndex];
9382
+ if (note.notations?.some((n) => n.type === "fermata")) {
9383
+ return failure([operationError("FERMATA_ALREADY_EXISTS", "Note already has a fermata", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9384
+ }
9385
+ if (!note.notations) {
9386
+ note.notations = [];
9387
+ }
9388
+ const fermataNotation = {
9389
+ type: "fermata",
9390
+ shape: options.shape ?? "normal",
9391
+ fermataType: options.fermataType ?? "upright",
9392
+ placement: options.placement ?? "above"
9393
+ };
9394
+ note.notations.push(fermataNotation);
9395
+ return success(result);
9396
+ }
9397
+ function removeFermata(score, options) {
9398
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9399
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9400
+ }
9401
+ const part = score.parts[options.partIndex];
9402
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9403
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9404
+ }
9405
+ const result = cloneScore(score);
9406
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9407
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9408
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9409
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9410
+ }
9411
+ const note = notes[options.noteIndex];
9412
+ const fermataIndex = note.notations?.findIndex((n) => n.type === "fermata");
9413
+ if (fermataIndex === void 0 || fermataIndex === -1) {
9414
+ return failure([operationError("FERMATA_NOT_FOUND", "Note does not have a fermata", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9415
+ }
9416
+ note.notations.splice(fermataIndex, 1);
9417
+ if (note.notations.length === 0) {
9418
+ delete note.notations;
9419
+ }
9420
+ return success(result);
9421
+ }
9422
+ function addOrnament(score, options) {
9423
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9424
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9425
+ }
9426
+ const part = score.parts[options.partIndex];
9427
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9428
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9429
+ }
9430
+ const result = cloneScore(score);
9431
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9432
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9433
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9434
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9435
+ }
9436
+ const note = notes[options.noteIndex];
9437
+ if (note.notations?.some((n) => n.type === "ornament" && n.ornament === options.ornament)) {
9438
+ return failure([operationError("ORNAMENT_ALREADY_EXISTS", `Note already has ornament: ${options.ornament}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9439
+ }
9440
+ if (!note.notations) {
9441
+ note.notations = [];
9442
+ }
9443
+ const ornamentNotation = {
9444
+ type: "ornament",
9445
+ ornament: options.ornament,
9446
+ placement: options.placement,
9447
+ accidentalMark: options.accidentalMark
9448
+ };
9449
+ note.notations.push(ornamentNotation);
9450
+ return success(result);
9451
+ }
9452
+ function removeOrnament(score, options) {
9453
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9454
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9455
+ }
9456
+ const part = score.parts[options.partIndex];
9457
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9458
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9459
+ }
9460
+ const result = cloneScore(score);
9461
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9462
+ const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
9463
+ if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
9464
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9465
+ }
9466
+ const note = notes[options.noteIndex];
9467
+ const ornamentIndex = options.ornament ? note.notations?.findIndex((n) => n.type === "ornament" && n.ornament === options.ornament) : note.notations?.findIndex((n) => n.type === "ornament");
9468
+ if (ornamentIndex === void 0 || ornamentIndex === -1) {
9469
+ return failure([operationError("ORNAMENT_NOT_FOUND", "Note does not have the specified ornament", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9470
+ }
9471
+ note.notations.splice(ornamentIndex, 1);
9472
+ if (note.notations.length === 0) {
9473
+ delete note.notations;
9474
+ }
9475
+ return success(result);
9476
+ }
9477
+ function addPedal(score, options) {
9478
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9479
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9480
+ }
9481
+ const part = score.parts[options.partIndex];
9482
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9483
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9484
+ }
9485
+ const result = cloneScore(score);
9486
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9487
+ const direction = {
9488
+ _id: generateId(),
9489
+ type: "direction",
9490
+ directionTypes: [{
9491
+ kind: "pedal",
9492
+ type: options.pedalType,
9493
+ line: options.line
9494
+ }],
9495
+ placement: options.placement ?? "below"
9496
+ };
9497
+ insertDirectionAtPosition(measure, direction, options.position);
9498
+ return success(result);
9499
+ }
9500
+ function removePedal(score, options) {
9501
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9502
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9503
+ }
9504
+ const part = score.parts[options.partIndex];
9505
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9506
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9507
+ }
9508
+ const result = cloneScore(score);
9509
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9510
+ const pedalIndices = [];
9511
+ for (let i = 0; i < measure.entries.length; i++) {
9512
+ const entry = measure.entries[i];
9513
+ if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "pedal")) {
9514
+ pedalIndices.push(i);
9515
+ }
9516
+ }
9517
+ if (pedalIndices.length === 0) {
9518
+ return failure([operationError("PEDAL_NOT_FOUND", "No pedal marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9519
+ }
9520
+ const targetIndex = options.directionIndex ?? 0;
9521
+ if (targetIndex < 0 || targetIndex >= pedalIndices.length) {
9522
+ return failure([operationError("PEDAL_NOT_FOUND", `Pedal direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9523
+ }
9524
+ measure.entries.splice(pedalIndices[targetIndex], 1);
9525
+ return success(result);
9526
+ }
9527
+ function addTextDirection(score, options) {
9528
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9529
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9530
+ }
9531
+ const part = score.parts[options.partIndex];
9532
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9533
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9534
+ }
9535
+ if (!options.text.trim()) {
9536
+ return failure([operationError("INVALID_TEXT", "Text cannot be empty", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9537
+ }
9538
+ const result = cloneScore(score);
9539
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9540
+ const direction = {
9541
+ _id: generateId(),
9542
+ type: "direction",
9543
+ directionTypes: [{
9544
+ kind: "words",
9545
+ text: options.text,
9546
+ fontStyle: options.fontStyle,
9547
+ fontWeight: options.fontWeight
9548
+ }],
9549
+ placement: options.placement ?? "above"
9550
+ };
9551
+ insertDirectionAtPosition(measure, direction, options.position);
9552
+ return success(result);
9553
+ }
9554
+ function addRehearsalMark(score, options) {
9555
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
9556
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
9557
+ }
9558
+ const part = score.parts[options.partIndex];
9559
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
9560
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
9561
+ }
9562
+ const result = cloneScore(score);
9563
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
9564
+ const direction = {
9565
+ _id: generateId(),
9566
+ type: "direction",
9567
+ directionTypes: [{
9568
+ kind: "rehearsal",
9569
+ text: options.text,
9570
+ enclosure: options.enclosure ?? "square"
9571
+ }],
9572
+ placement: "above"
9573
+ };
9574
+ insertDirectionAtPosition(measure, direction, 0);
9575
+ return success(result);
9576
+ }
9577
+ function insertDirectionAtPosition(measure, direction, position) {
9578
+ let currentPosition = 0;
9579
+ let insertIndex = 0;
9580
+ for (let i = 0; i < measure.entries.length; i++) {
9581
+ const entry = measure.entries[i];
9582
+ if (currentPosition >= position) {
9583
+ insertIndex = i;
9584
+ break;
9585
+ }
9586
+ if (entry.type === "note" && !entry.chord) {
9587
+ currentPosition += entry.duration;
9588
+ } else if (entry.type === "forward") {
9589
+ currentPosition += entry.duration;
9590
+ } else if (entry.type === "backup") {
9591
+ currentPosition -= entry.duration;
9592
+ }
9593
+ insertIndex = i + 1;
9594
+ }
9595
+ measure.entries.splice(insertIndex, 0, direction);
9596
+ }
9597
+ function addRepeatBarline(score, options) {
9598
+ const { partIndex, measureIndex, direction, times } = options;
9599
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9600
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9601
+ }
9602
+ const part = score.parts[partIndex];
9603
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9604
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9605
+ }
9606
+ const result = cloneScore(score);
9607
+ const location = direction === "forward" ? "left" : "right";
9608
+ const barStyle = direction === "forward" ? "heavy-light" : "light-heavy";
9609
+ for (const p of result.parts) {
9610
+ if (measureIndex >= p.measures.length) continue;
9611
+ const measure = p.measures[measureIndex];
9612
+ if (!measure.barlines) {
9613
+ measure.barlines = [];
9614
+ }
9615
+ const existingIndex = measure.barlines.findIndex((b) => b.location === location && b.repeat);
9616
+ if (existingIndex >= 0) {
9617
+ return failure([operationError("REPEAT_ALREADY_EXISTS", `Repeat barline already exists at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9618
+ }
9619
+ const nonRepeatIndex = measure.barlines.findIndex((b) => b.location === location && !b.repeat);
9620
+ if (nonRepeatIndex >= 0) {
9621
+ measure.barlines.splice(nonRepeatIndex, 1);
9622
+ }
9623
+ measure.barlines.push({
9624
+ _id: generateId(),
9625
+ location,
9626
+ barStyle,
9627
+ repeat: {
9628
+ direction,
9629
+ times
9630
+ }
9631
+ });
9632
+ }
9633
+ return success(result);
9634
+ }
9635
+ function removeRepeatBarline(score, options) {
9636
+ const { partIndex, measureIndex, location } = options;
9637
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9638
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9639
+ }
9640
+ const part = score.parts[partIndex];
9641
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9642
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9643
+ }
9644
+ const measure = part.measures[measureIndex];
9645
+ if (!measure.barlines) {
9646
+ return failure([operationError("REPEAT_NOT_FOUND", `No repeat barline found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9647
+ }
9648
+ const existingIndex = measure.barlines.findIndex((b) => b.location === location && b.repeat);
9649
+ if (existingIndex < 0) {
9650
+ return failure([operationError("REPEAT_NOT_FOUND", `No repeat barline found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9651
+ }
9652
+ const result = cloneScore(score);
9653
+ for (const p of result.parts) {
9654
+ if (measureIndex >= p.measures.length) continue;
9655
+ const m = p.measures[measureIndex];
9656
+ if (m.barlines) {
9657
+ const idx = m.barlines.findIndex((b) => b.location === location && b.repeat);
9658
+ if (idx >= 0) {
9659
+ m.barlines.splice(idx, 1);
9660
+ }
9661
+ if (m.barlines.length === 0) {
9662
+ delete m.barlines;
9663
+ }
9664
+ }
9665
+ }
9666
+ return success(result);
9667
+ }
9668
+ function addEnding(score, options) {
9669
+ const { partIndex, measureIndex, number, type } = options;
9670
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9671
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9672
+ }
9673
+ const part = score.parts[partIndex];
9674
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9675
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9676
+ }
9677
+ const result = cloneScore(score);
9678
+ const location = type === "start" ? "left" : "right";
9679
+ for (const p of result.parts) {
9680
+ if (measureIndex >= p.measures.length) continue;
9681
+ const measure = p.measures[measureIndex];
9682
+ if (!measure.barlines) {
9683
+ measure.barlines = [];
9684
+ }
9685
+ let barline = measure.barlines.find((b) => b.location === location);
9686
+ if (!barline) {
9687
+ barline = { _id: generateId(), location };
9688
+ measure.barlines.push(barline);
9689
+ }
9690
+ if (barline.ending) {
9691
+ return failure([operationError("ENDING_ALREADY_EXISTS", `Ending already exists at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9692
+ }
9693
+ barline.ending = { number, type };
9694
+ }
9695
+ return success(result);
9696
+ }
9697
+ function removeEnding(score, options) {
9698
+ const { partIndex, measureIndex, location } = options;
9699
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9700
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9701
+ }
9702
+ const part = score.parts[partIndex];
9703
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9704
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9705
+ }
9706
+ const measure = part.measures[measureIndex];
9707
+ const barline = measure.barlines?.find((b) => b.location === location && b.ending);
9708
+ if (!barline) {
9709
+ return failure([operationError("ENDING_NOT_FOUND", `No ending found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
9710
+ }
9711
+ const result = cloneScore(score);
9712
+ for (const p of result.parts) {
9713
+ if (measureIndex >= p.measures.length) continue;
9714
+ const m = p.measures[measureIndex];
9715
+ if (m.barlines) {
9716
+ const bl = m.barlines.find((b) => b.location === location);
9717
+ if (bl) {
9718
+ delete bl.ending;
9719
+ if (!bl.barStyle && !bl.repeat && !bl.ending) {
9720
+ const idx = m.barlines.indexOf(bl);
9721
+ m.barlines.splice(idx, 1);
9722
+ }
9723
+ }
9724
+ if (m.barlines.length === 0) {
9725
+ delete m.barlines;
9726
+ }
9727
+ }
9728
+ }
9729
+ return success(result);
9730
+ }
9731
+ function changeBarline(score, options) {
9732
+ const { partIndex, measureIndex, location, barStyle } = options;
9733
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9734
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9735
+ }
9736
+ const part = score.parts[partIndex];
9737
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9738
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9739
+ }
9740
+ const result = cloneScore(score);
9741
+ for (const p of result.parts) {
9742
+ if (measureIndex >= p.measures.length) continue;
9743
+ const measure = p.measures[measureIndex];
9744
+ if (!measure.barlines) {
9745
+ measure.barlines = [];
9746
+ }
9747
+ let barline = measure.barlines.find((b) => b.location === location);
9748
+ if (!barline) {
9749
+ barline = { _id: generateId(), location };
9750
+ measure.barlines.push(barline);
9751
+ }
9752
+ barline.barStyle = barStyle;
9753
+ }
9754
+ return success(result);
9755
+ }
9756
+ function addSegno(score, options) {
9757
+ const { partIndex, measureIndex, position = 0 } = options;
9758
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9759
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9760
+ }
9761
+ const part = score.parts[partIndex];
9762
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9763
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9764
+ }
9765
+ const result = cloneScore(score);
9766
+ const measure = result.parts[partIndex].measures[measureIndex];
9767
+ const direction = {
9768
+ _id: generateId(),
9769
+ type: "direction",
9770
+ directionTypes: [{ kind: "segno" }],
9771
+ placement: "above"
9772
+ };
9773
+ insertDirectionAtPosition(measure, direction, position);
9774
+ return success(result);
9775
+ }
9776
+ function addCoda(score, options) {
9777
+ const { partIndex, measureIndex, position = 0 } = options;
9778
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9779
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9780
+ }
9781
+ const part = score.parts[partIndex];
9782
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9783
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9784
+ }
9785
+ const result = cloneScore(score);
9786
+ const measure = result.parts[partIndex].measures[measureIndex];
9787
+ const direction = {
9788
+ _id: generateId(),
9789
+ type: "direction",
9790
+ directionTypes: [{ kind: "coda" }],
9791
+ placement: "above"
9792
+ };
9793
+ insertDirectionAtPosition(measure, direction, position);
9794
+ return success(result);
9795
+ }
9796
+ function addDaCapo(score, options) {
9797
+ const { partIndex, measureIndex, position } = options;
9798
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9799
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9800
+ }
9801
+ const part = score.parts[partIndex];
9802
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9803
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9804
+ }
9805
+ const result = cloneScore(score);
9806
+ const measure = result.parts[partIndex].measures[measureIndex];
9807
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
9808
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
9809
+ const insertPos = position ?? measureDuration;
9810
+ const direction = {
9811
+ _id: generateId(),
9812
+ type: "direction",
9813
+ directionTypes: [{ kind: "words", text: "D.C." }],
9814
+ placement: "above"
9815
+ };
9816
+ insertDirectionAtPosition(measure, direction, insertPos);
9817
+ const sound = {
9818
+ _id: generateId(),
9819
+ type: "sound",
9820
+ dacapo: true
9821
+ };
9822
+ measure.entries.push(sound);
9823
+ return success(result);
9824
+ }
9825
+ function addDalSegno(score, options) {
9826
+ const { partIndex, measureIndex, position } = options;
9827
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9828
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9829
+ }
9830
+ const part = score.parts[partIndex];
9831
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9832
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9833
+ }
9834
+ const result = cloneScore(score);
9835
+ const measure = result.parts[partIndex].measures[measureIndex];
9836
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
9837
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
9838
+ const insertPos = position ?? measureDuration;
9839
+ const direction = {
9840
+ _id: generateId(),
9841
+ type: "direction",
9842
+ directionTypes: [{ kind: "words", text: "D.S." }],
9843
+ placement: "above"
9844
+ };
9845
+ insertDirectionAtPosition(measure, direction, insertPos);
9846
+ const sound = {
9847
+ _id: generateId(),
9848
+ type: "sound",
9849
+ dalsegno: "segno"
9850
+ };
9851
+ measure.entries.push(sound);
9852
+ return success(result);
9853
+ }
9854
+ function addFine(score, options) {
9855
+ const { partIndex, measureIndex, position } = options;
9856
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9857
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9858
+ }
9859
+ const part = score.parts[partIndex];
9860
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9861
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9862
+ }
9863
+ const result = cloneScore(score);
9864
+ const measure = result.parts[partIndex].measures[measureIndex];
9865
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
9866
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
9867
+ const insertPos = position ?? measureDuration;
9868
+ const direction = {
9869
+ _id: generateId(),
9870
+ type: "direction",
9871
+ directionTypes: [{ kind: "words", text: "Fine" }],
9872
+ placement: "above"
9873
+ };
9874
+ insertDirectionAtPosition(measure, direction, insertPos);
9875
+ const sound = {
9876
+ _id: generateId(),
9877
+ type: "sound",
9878
+ fine: true
9879
+ };
9880
+ measure.entries.push(sound);
9881
+ return success(result);
9882
+ }
9883
+ function addToCoda(score, options) {
9884
+ const { partIndex, measureIndex, position } = options;
9885
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9886
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9887
+ }
9888
+ const part = score.parts[partIndex];
9889
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9890
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9891
+ }
9892
+ const result = cloneScore(score);
9893
+ const measure = result.parts[partIndex].measures[measureIndex];
9894
+ const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
9895
+ const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
9896
+ const insertPos = position ?? measureDuration;
9897
+ const direction = {
9898
+ _id: generateId(),
9899
+ type: "direction",
9900
+ directionTypes: [{ kind: "words", text: "To Coda" }],
9901
+ placement: "above"
9902
+ };
9903
+ insertDirectionAtPosition(measure, direction, insertPos);
9904
+ const sound = {
9905
+ _id: generateId(),
9906
+ type: "sound",
9907
+ tocoda: "coda"
9908
+ };
9909
+ measure.entries.push(sound);
9910
+ return success(result);
9911
+ }
9912
+ function addGraceNote(score, options) {
9913
+ const { partIndex, measureIndex, targetNoteIndex, pitch, noteType = "eighth", slash = true, voice, staff } = options;
9914
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9915
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9916
+ }
9917
+ const part = score.parts[partIndex];
9918
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9919
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9920
+ }
9921
+ const measure = part.measures[measureIndex];
9922
+ let noteCount = 0;
9923
+ let targetEntryIndex = -1;
9924
+ let targetNote = null;
9925
+ for (let i = 0; i < measure.entries.length; i++) {
9926
+ const entry = measure.entries[i];
9927
+ if (entry.type === "note" && !entry.chord) {
9928
+ if (noteCount === targetNoteIndex) {
9929
+ targetEntryIndex = i;
9930
+ targetNote = entry;
9931
+ break;
9932
+ }
9933
+ noteCount++;
9934
+ }
9935
+ }
9936
+ if (targetEntryIndex < 0 || !targetNote) {
9937
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${targetNoteIndex} not found`, { partIndex, measureIndex })]);
9938
+ }
9939
+ const result = cloneScore(score);
9940
+ const resultMeasure = result.parts[partIndex].measures[measureIndex];
9941
+ const graceNote = {
9942
+ _id: generateId(),
9943
+ type: "note",
9944
+ pitch,
9945
+ duration: 0,
9946
+ // Grace notes have no duration
9947
+ voice: voice ?? targetNote.voice,
9948
+ staff: staff ?? targetNote.staff,
9949
+ noteType,
9950
+ grace: {
9951
+ slash
9952
+ }
9953
+ };
9954
+ resultMeasure.entries.splice(targetEntryIndex, 0, graceNote);
9955
+ return success(result);
9956
+ }
9957
+ function removeGraceNote(score, options) {
9958
+ const { partIndex, measureIndex, graceNoteIndex } = options;
9959
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9960
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9961
+ }
9962
+ const part = score.parts[partIndex];
9963
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9964
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9965
+ }
9966
+ const measure = part.measures[measureIndex];
9967
+ let graceCount = 0;
9968
+ let targetEntryIndex = -1;
9969
+ for (let i = 0; i < measure.entries.length; i++) {
9970
+ const entry = measure.entries[i];
9971
+ if (entry.type === "note" && entry.grace) {
9972
+ if (graceCount === graceNoteIndex) {
9973
+ targetEntryIndex = i;
9974
+ break;
9975
+ }
9976
+ graceCount++;
9977
+ }
9978
+ }
9979
+ if (targetEntryIndex < 0) {
9980
+ return failure([operationError("GRACE_NOTE_NOT_FOUND", `Grace note at index ${graceNoteIndex} not found`, { partIndex, measureIndex })]);
9981
+ }
9982
+ const result = cloneScore(score);
9983
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
9984
+ return success(result);
9985
+ }
9986
+ function convertToGrace(score, options) {
9987
+ const { partIndex, measureIndex, noteIndex, slash = true } = options;
9988
+ if (partIndex < 0 || partIndex >= score.parts.length) {
9989
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
9990
+ }
9991
+ const part = score.parts[partIndex];
9992
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
9993
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
9994
+ }
9995
+ const measure = part.measures[measureIndex];
9996
+ let noteCount = 0;
9997
+ let targetEntryIndex = -1;
9998
+ for (let i = 0; i < measure.entries.length; i++) {
9999
+ const entry = measure.entries[i];
10000
+ if (entry.type === "note" && !entry.chord) {
10001
+ if (noteCount === noteIndex) {
10002
+ targetEntryIndex = i;
10003
+ break;
10004
+ }
10005
+ noteCount++;
10006
+ }
10007
+ }
10008
+ if (targetEntryIndex < 0) {
10009
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10010
+ }
10011
+ const targetEntry = measure.entries[targetEntryIndex];
10012
+ if (targetEntry.type !== "note") {
10013
+ return failure([operationError("NOTE_NOT_FOUND", `Entry at index is not a note`, { partIndex, measureIndex })]);
10014
+ }
10015
+ if (targetEntry.grace) {
10016
+ return failure([operationError("INVALID_GRACE_NOTE", `Note is already a grace note`, { partIndex, measureIndex })]);
10017
+ }
10018
+ const result = cloneScore(score);
10019
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10020
+ resultNote.grace = { slash };
10021
+ resultNote.duration = 0;
10022
+ return success(result);
10023
+ }
10024
+ function addLyric(score, options) {
10025
+ const { partIndex, measureIndex, noteIndex, text, syllabic = "single", verse = 1, extend = false } = options;
10026
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10027
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10028
+ }
10029
+ const part = score.parts[partIndex];
10030
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10031
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10032
+ }
10033
+ const measure = part.measures[measureIndex];
10034
+ let noteCount = 0;
10035
+ let targetEntryIndex = -1;
10036
+ for (let i = 0; i < measure.entries.length; i++) {
10037
+ const entry = measure.entries[i];
10038
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10039
+ if (noteCount === noteIndex) {
10040
+ targetEntryIndex = i;
10041
+ break;
10042
+ }
10043
+ noteCount++;
10044
+ }
10045
+ }
10046
+ if (targetEntryIndex < 0) {
10047
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10048
+ }
10049
+ const targetEntry = measure.entries[targetEntryIndex];
10050
+ if (targetEntry.type !== "note") {
10051
+ return failure([operationError("NOTE_NOT_FOUND", `Entry is not a note`, { partIndex, measureIndex })]);
10052
+ }
10053
+ if (targetEntry.lyrics?.some((l) => l.number === verse)) {
10054
+ return failure([operationError("LYRIC_ALREADY_EXISTS", `Lyric for verse ${verse} already exists on this note`, { partIndex, measureIndex })]);
10055
+ }
10056
+ const result = cloneScore(score);
10057
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10058
+ if (!resultNote.lyrics) {
10059
+ resultNote.lyrics = [];
10060
+ }
10061
+ const lyric = {
10062
+ number: verse,
10063
+ syllabic,
10064
+ text,
10065
+ extend
10066
+ };
10067
+ resultNote.lyrics.push(lyric);
10068
+ return success(result);
10069
+ }
10070
+ function removeLyric(score, options) {
10071
+ const { partIndex, measureIndex, noteIndex, verse } = options;
10072
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10073
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10074
+ }
10075
+ const part = score.parts[partIndex];
10076
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10077
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10078
+ }
10079
+ const measure = part.measures[measureIndex];
10080
+ let noteCount = 0;
10081
+ let targetEntryIndex = -1;
10082
+ for (let i = 0; i < measure.entries.length; i++) {
10083
+ const entry = measure.entries[i];
10084
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10085
+ if (noteCount === noteIndex) {
10086
+ targetEntryIndex = i;
10087
+ break;
10088
+ }
10089
+ noteCount++;
10090
+ }
10091
+ }
10092
+ if (targetEntryIndex < 0) {
10093
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10094
+ }
10095
+ const targetEntry = measure.entries[targetEntryIndex];
10096
+ if (targetEntry.type !== "note" || !targetEntry.lyrics || targetEntry.lyrics.length === 0) {
10097
+ return failure([operationError("LYRIC_NOT_FOUND", `No lyrics found on note`, { partIndex, measureIndex })]);
10098
+ }
10099
+ if (verse !== void 0) {
10100
+ const lyricIndex = targetEntry.lyrics.findIndex((l) => l.number === verse);
10101
+ if (lyricIndex < 0) {
10102
+ return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
10103
+ }
10104
+ }
10105
+ const result = cloneScore(score);
10106
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10107
+ if (verse !== void 0) {
10108
+ resultNote.lyrics = resultNote.lyrics.filter((l) => l.number !== verse);
10109
+ } else {
10110
+ delete resultNote.lyrics;
10111
+ }
10112
+ if (resultNote.lyrics && resultNote.lyrics.length === 0) {
10113
+ delete resultNote.lyrics;
10114
+ }
10115
+ return success(result);
10116
+ }
10117
+ function updateLyric(score, options) {
10118
+ const { partIndex, measureIndex, noteIndex, verse = 1, text, syllabic, extend } = options;
10119
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10120
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10121
+ }
10122
+ const part = score.parts[partIndex];
10123
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10124
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10125
+ }
10126
+ const measure = part.measures[measureIndex];
10127
+ let noteCount = 0;
10128
+ let targetEntryIndex = -1;
10129
+ for (let i = 0; i < measure.entries.length; i++) {
10130
+ const entry = measure.entries[i];
10131
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10132
+ if (noteCount === noteIndex) {
10133
+ targetEntryIndex = i;
10134
+ break;
10135
+ }
10136
+ noteCount++;
10137
+ }
10138
+ }
10139
+ if (targetEntryIndex < 0) {
10140
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10141
+ }
10142
+ const targetEntry = measure.entries[targetEntryIndex];
10143
+ if (targetEntry.type !== "note" || !targetEntry.lyrics) {
10144
+ return failure([operationError("LYRIC_NOT_FOUND", `No lyrics found on note`, { partIndex, measureIndex })]);
10145
+ }
10146
+ const lyricIndex = targetEntry.lyrics.findIndex((l) => l.number === verse);
10147
+ if (lyricIndex < 0) {
10148
+ return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
10149
+ }
10150
+ const result = cloneScore(score);
10151
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10152
+ const lyric = resultNote.lyrics[lyricIndex];
10153
+ if (text !== void 0) {
10154
+ lyric.text = text;
10155
+ }
10156
+ if (syllabic !== void 0) {
10157
+ lyric.syllabic = syllabic;
10158
+ }
10159
+ if (extend !== void 0) {
10160
+ lyric.extend = extend;
10161
+ }
10162
+ return success(result);
10163
+ }
10164
+ function addHarmony(score, options) {
10165
+ const { partIndex, measureIndex, position, root, kind, kindText, bass, degrees, staff, placement = "above" } = options;
10166
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10167
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10168
+ }
10169
+ const part = score.parts[partIndex];
10170
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10171
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10172
+ }
10173
+ const validSteps = ["A", "B", "C", "D", "E", "F", "G"];
10174
+ if (!validSteps.includes(root.step.toUpperCase())) {
10175
+ return failure([operationError("INVALID_HARMONY", `Invalid root step: ${root.step}`, { partIndex, measureIndex })]);
10176
+ }
10177
+ if (bass && !validSteps.includes(bass.step.toUpperCase())) {
10178
+ return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
10179
+ }
10180
+ const result = cloneScore(score);
10181
+ const measure = result.parts[partIndex].measures[measureIndex];
10182
+ const harmony = {
10183
+ _id: generateId(),
10184
+ type: "harmony",
10185
+ root: {
10186
+ rootStep: root.step.toUpperCase(),
10187
+ rootAlter: root.alter
10188
+ },
10189
+ kind,
10190
+ kindText,
10191
+ bass: bass ? {
10192
+ bassStep: bass.step.toUpperCase(),
10193
+ bassAlter: bass.alter
10194
+ } : void 0,
10195
+ degrees: degrees?.map((d) => ({
10196
+ degreeValue: d.value,
10197
+ degreeAlter: d.alter,
10198
+ degreeType: d.type
10199
+ })),
10200
+ staff,
10201
+ placement
10202
+ };
10203
+ let currentPosition = 0;
10204
+ let insertIndex = 0;
10205
+ for (let i = 0; i < measure.entries.length; i++) {
10206
+ const entry = measure.entries[i];
10207
+ if (currentPosition >= position) {
10208
+ insertIndex = i;
10209
+ break;
10210
+ }
10211
+ if (entry.type === "note" && !entry.chord) {
10212
+ currentPosition += entry.duration;
10213
+ } else if (entry.type === "forward") {
10214
+ currentPosition += entry.duration;
10215
+ } else if (entry.type === "backup") {
10216
+ currentPosition -= entry.duration;
10217
+ }
10218
+ insertIndex = i + 1;
10219
+ }
10220
+ measure.entries.splice(insertIndex, 0, harmony);
10221
+ return success(result);
10222
+ }
10223
+ function removeHarmony(score, options) {
10224
+ const { partIndex, measureIndex, harmonyIndex } = options;
10225
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10226
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10227
+ }
10228
+ const part = score.parts[partIndex];
10229
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10230
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10231
+ }
10232
+ const measure = part.measures[measureIndex];
10233
+ let harmonyCount = 0;
10234
+ let targetEntryIndex = -1;
10235
+ for (let i = 0; i < measure.entries.length; i++) {
10236
+ const entry = measure.entries[i];
10237
+ if (entry.type === "harmony") {
10238
+ if (harmonyCount === harmonyIndex) {
10239
+ targetEntryIndex = i;
10240
+ break;
10241
+ }
10242
+ harmonyCount++;
10243
+ }
10244
+ }
10245
+ if (targetEntryIndex < 0) {
10246
+ return failure([operationError("HARMONY_NOT_FOUND", `Harmony at index ${harmonyIndex} not found`, { partIndex, measureIndex })]);
10247
+ }
10248
+ const result = cloneScore(score);
10249
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
10250
+ return success(result);
10251
+ }
10252
+ function updateHarmony(score, options) {
10253
+ const { partIndex, measureIndex, harmonyIndex, root, kind, kindText, bass, degrees } = options;
10254
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10255
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10256
+ }
10257
+ const part = score.parts[partIndex];
10258
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10259
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10260
+ }
10261
+ const measure = part.measures[measureIndex];
10262
+ let harmonyCount = 0;
10263
+ let targetEntryIndex = -1;
10264
+ for (let i = 0; i < measure.entries.length; i++) {
10265
+ const entry = measure.entries[i];
10266
+ if (entry.type === "harmony") {
10267
+ if (harmonyCount === harmonyIndex) {
10268
+ targetEntryIndex = i;
10269
+ break;
10270
+ }
10271
+ harmonyCount++;
10272
+ }
10273
+ }
10274
+ if (targetEntryIndex < 0) {
10275
+ return failure([operationError("HARMONY_NOT_FOUND", `Harmony at index ${harmonyIndex} not found`, { partIndex, measureIndex })]);
10276
+ }
10277
+ const validSteps = ["A", "B", "C", "D", "E", "F", "G"];
10278
+ if (root && !validSteps.includes(root.step.toUpperCase())) {
10279
+ return failure([operationError("INVALID_HARMONY", `Invalid root step: ${root.step}`, { partIndex, measureIndex })]);
10280
+ }
10281
+ if (bass && !validSteps.includes(bass.step.toUpperCase())) {
10282
+ return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
10283
+ }
10284
+ const result = cloneScore(score);
10285
+ const harmony = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10286
+ if (root) {
10287
+ harmony.root = {
10288
+ rootStep: root.step.toUpperCase(),
10289
+ rootAlter: root.alter
10290
+ };
10291
+ }
10292
+ if (kind !== void 0) {
10293
+ harmony.kind = kind;
10294
+ }
10295
+ if (kindText !== void 0) {
10296
+ harmony.kindText = kindText;
10297
+ }
10298
+ if (bass !== void 0) {
10299
+ if (bass === null) {
10300
+ delete harmony.bass;
10301
+ } else {
10302
+ harmony.bass = {
10303
+ bassStep: bass.step.toUpperCase(),
10304
+ bassAlter: bass.alter
10305
+ };
10306
+ }
10307
+ }
10308
+ if (degrees !== void 0) {
10309
+ if (degrees === null) {
10310
+ delete harmony.degrees;
10311
+ } else {
10312
+ harmony.degrees = degrees.map((d) => ({
10313
+ degreeValue: d.value,
10314
+ degreeAlter: d.alter,
10315
+ degreeType: d.type
10316
+ }));
10317
+ }
10318
+ }
10319
+ return success(result);
10320
+ }
10321
+ function addFingering(score, options) {
10322
+ const { partIndex, measureIndex, noteIndex, fingering, substitution = false, alternate = false, placement } = options;
10323
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10324
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10325
+ }
10326
+ const part = score.parts[partIndex];
10327
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10328
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10329
+ }
10330
+ const measure = part.measures[measureIndex];
10331
+ let noteCount = 0;
10332
+ let targetEntryIndex = -1;
10333
+ for (let i = 0; i < measure.entries.length; i++) {
10334
+ const entry = measure.entries[i];
10335
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10336
+ if (noteCount === noteIndex) {
10337
+ targetEntryIndex = i;
10338
+ break;
10339
+ }
10340
+ noteCount++;
10341
+ }
10342
+ }
10343
+ if (targetEntryIndex < 0) {
10344
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10345
+ }
10346
+ const result = cloneScore(score);
10347
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10348
+ if (!resultNote.notations) {
10349
+ resultNote.notations = [];
10350
+ }
10351
+ resultNote.notations.push({
10352
+ type: "technical",
10353
+ technical: "fingering",
10354
+ fingering,
10355
+ fingeringSubstitution: substitution || void 0,
10356
+ fingeringAlternate: alternate || void 0,
10357
+ placement
10358
+ });
10359
+ return success(result);
10360
+ }
10361
+ function removeFingering(score, options) {
10362
+ const { partIndex, measureIndex, noteIndex } = options;
10363
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10364
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10365
+ }
10366
+ const part = score.parts[partIndex];
10367
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10368
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10369
+ }
10370
+ const measure = part.measures[measureIndex];
10371
+ let noteCount = 0;
10372
+ let targetEntryIndex = -1;
10373
+ for (let i = 0; i < measure.entries.length; i++) {
10374
+ const entry = measure.entries[i];
10375
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10376
+ if (noteCount === noteIndex) {
10377
+ targetEntryIndex = i;
10378
+ break;
10379
+ }
10380
+ noteCount++;
10381
+ }
10382
+ }
10383
+ if (targetEntryIndex < 0) {
10384
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10385
+ }
10386
+ const targetEntry = measure.entries[targetEntryIndex];
10387
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10388
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10389
+ }
10390
+ const fingeringIndex = targetEntry.notations.findIndex(
10391
+ (n) => n.type === "technical" && n.technical === "fingering"
10392
+ );
10393
+ if (fingeringIndex < 0) {
10394
+ return failure([operationError("NOTE_NOT_FOUND", `No fingering found on note`, { partIndex, measureIndex })]);
10395
+ }
10396
+ const result = cloneScore(score);
10397
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10398
+ resultNote.notations.splice(fingeringIndex, 1);
10399
+ if (resultNote.notations.length === 0) {
10400
+ delete resultNote.notations;
10401
+ }
10402
+ return success(result);
10403
+ }
10404
+ function addBowing(score, options) {
10405
+ const { partIndex, measureIndex, noteIndex, bowingType, placement } = options;
10406
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10407
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10408
+ }
10409
+ const part = score.parts[partIndex];
10410
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10411
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10412
+ }
10413
+ const measure = part.measures[measureIndex];
10414
+ let noteCount = 0;
10415
+ let targetEntryIndex = -1;
10416
+ for (let i = 0; i < measure.entries.length; i++) {
10417
+ const entry = measure.entries[i];
10418
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10419
+ if (noteCount === noteIndex) {
10420
+ targetEntryIndex = i;
10421
+ break;
10422
+ }
10423
+ noteCount++;
10424
+ }
10425
+ }
10426
+ if (targetEntryIndex < 0) {
10427
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10428
+ }
10429
+ const result = cloneScore(score);
10430
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10431
+ if (!resultNote.notations) {
10432
+ resultNote.notations = [];
10433
+ }
10434
+ resultNote.notations.push({
10435
+ type: "technical",
10436
+ technical: bowingType,
10437
+ placement
10438
+ });
10439
+ return success(result);
10440
+ }
10441
+ function removeBowing(score, options) {
10442
+ const { partIndex, measureIndex, noteIndex, bowingType } = options;
10443
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10444
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10445
+ }
10446
+ const part = score.parts[partIndex];
10447
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10448
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10449
+ }
10450
+ const measure = part.measures[measureIndex];
10451
+ let noteCount = 0;
10452
+ let targetEntryIndex = -1;
10453
+ for (let i = 0; i < measure.entries.length; i++) {
10454
+ const entry = measure.entries[i];
10455
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10456
+ if (noteCount === noteIndex) {
10457
+ targetEntryIndex = i;
10458
+ break;
10459
+ }
10460
+ noteCount++;
10461
+ }
10462
+ }
10463
+ if (targetEntryIndex < 0) {
10464
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10465
+ }
10466
+ const targetEntry = measure.entries[targetEntryIndex];
10467
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10468
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10469
+ }
10470
+ const bowingIndex = targetEntry.notations.findIndex((n) => {
10471
+ if (n.type !== "technical") return false;
10472
+ if (bowingType) return n.technical === bowingType;
10473
+ return n.technical === "up-bow" || n.technical === "down-bow";
10474
+ });
10475
+ if (bowingIndex < 0) {
10476
+ return failure([operationError("NOTE_NOT_FOUND", `No bowing found on note`, { partIndex, measureIndex })]);
10477
+ }
10478
+ const result = cloneScore(score);
10479
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10480
+ resultNote.notations.splice(bowingIndex, 1);
10481
+ if (resultNote.notations.length === 0) {
10482
+ delete resultNote.notations;
10483
+ }
10484
+ return success(result);
10485
+ }
10486
+ function addStringNumber(score, options) {
10487
+ const { partIndex, measureIndex, noteIndex, stringNumber, placement } = options;
10488
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10489
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10490
+ }
10491
+ const part = score.parts[partIndex];
10492
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10493
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10494
+ }
10495
+ if (stringNumber < 1) {
10496
+ return failure([operationError("INVALID_POSITION", `String number must be positive`, { partIndex, measureIndex })]);
10497
+ }
10498
+ const measure = part.measures[measureIndex];
10499
+ let noteCount = 0;
10500
+ let targetEntryIndex = -1;
10501
+ for (let i = 0; i < measure.entries.length; i++) {
10502
+ const entry = measure.entries[i];
10503
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10504
+ if (noteCount === noteIndex) {
10505
+ targetEntryIndex = i;
10506
+ break;
10507
+ }
10508
+ noteCount++;
10509
+ }
10510
+ }
10511
+ if (targetEntryIndex < 0) {
10512
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10513
+ }
10514
+ const result = cloneScore(score);
10515
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10516
+ if (!resultNote.notations) {
10517
+ resultNote.notations = [];
10518
+ }
10519
+ resultNote.notations.push({
10520
+ type: "technical",
10521
+ technical: "string",
10522
+ string: stringNumber,
10523
+ placement
10524
+ });
10525
+ return success(result);
10526
+ }
10527
+ function removeStringNumber(score, options) {
10528
+ const { partIndex, measureIndex, noteIndex } = options;
10529
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10530
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10531
+ }
10532
+ const part = score.parts[partIndex];
10533
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10534
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10535
+ }
10536
+ const measure = part.measures[measureIndex];
10537
+ let noteCount = 0;
10538
+ let targetEntryIndex = -1;
10539
+ for (let i = 0; i < measure.entries.length; i++) {
10540
+ const entry = measure.entries[i];
10541
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10542
+ if (noteCount === noteIndex) {
10543
+ targetEntryIndex = i;
10544
+ break;
10545
+ }
10546
+ noteCount++;
10547
+ }
10548
+ }
10549
+ if (targetEntryIndex < 0) {
10550
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10551
+ }
10552
+ const targetEntry = measure.entries[targetEntryIndex];
10553
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10554
+ return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10555
+ }
10556
+ const stringIndex = targetEntry.notations.findIndex(
10557
+ (n) => n.type === "technical" && n.technical === "string"
10558
+ );
10559
+ if (stringIndex < 0) {
10560
+ return failure([operationError("NOTE_NOT_FOUND", `No string number found on note`, { partIndex, measureIndex })]);
10561
+ }
10562
+ const result = cloneScore(score);
10563
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10564
+ resultNote.notations.splice(stringIndex, 1);
10565
+ if (resultNote.notations.length === 0) {
10566
+ delete resultNote.notations;
10567
+ }
10568
+ return success(result);
10569
+ }
10570
+ function addOctaveShift(score, options) {
10571
+ const { partIndex, measureIndex, position, shiftType, size = 8 } = options;
10572
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10573
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10574
+ }
10575
+ const part = score.parts[partIndex];
10576
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10577
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10578
+ }
10579
+ const result = cloneScore(score);
10580
+ const measure = result.parts[partIndex].measures[measureIndex];
10581
+ const direction = {
10582
+ _id: generateId(),
10583
+ type: "direction",
10584
+ directionTypes: [{
10585
+ kind: "octave-shift",
10586
+ type: shiftType,
10587
+ size
10588
+ }],
10589
+ placement: shiftType === "down" ? "above" : "below"
10590
+ };
10591
+ insertDirectionAtPosition(measure, direction, position);
10592
+ return success(result);
10593
+ }
10594
+ function stopOctaveShift(score, options) {
10595
+ const { partIndex, measureIndex, position, size = 8 } = options;
10596
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10597
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10598
+ }
10599
+ const part = score.parts[partIndex];
10600
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10601
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10602
+ }
10603
+ const result = cloneScore(score);
10604
+ const measure = result.parts[partIndex].measures[measureIndex];
10605
+ const direction = {
10606
+ _id: generateId(),
10607
+ type: "direction",
10608
+ directionTypes: [{
10609
+ kind: "octave-shift",
10610
+ type: "stop",
10611
+ size
10612
+ }]
10613
+ };
10614
+ insertDirectionAtPosition(measure, direction, position);
10615
+ return success(result);
10616
+ }
10617
+ function removeOctaveShift(score, options) {
10618
+ const { partIndex, measureIndex, octaveShiftIndex = 0 } = options;
10619
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10620
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10621
+ }
10622
+ const part = score.parts[partIndex];
10623
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10624
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10625
+ }
10626
+ const measure = part.measures[measureIndex];
10627
+ let shiftCount = 0;
10628
+ let targetEntryIndex = -1;
10629
+ for (let i = 0; i < measure.entries.length; i++) {
10630
+ const entry = measure.entries[i];
10631
+ if (entry.type === "direction") {
10632
+ const hasOctaveShift = entry.directionTypes.some((dt) => dt.kind === "octave-shift");
10633
+ if (hasOctaveShift) {
10634
+ if (shiftCount === octaveShiftIndex) {
10635
+ targetEntryIndex = i;
10636
+ break;
10637
+ }
10638
+ shiftCount++;
10639
+ }
10640
+ }
10641
+ }
10642
+ if (targetEntryIndex < 0) {
10643
+ return failure([operationError("NOTE_NOT_FOUND", `Octave shift at index ${octaveShiftIndex} not found`, { partIndex, measureIndex })]);
10644
+ }
10645
+ const result = cloneScore(score);
10646
+ result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
10647
+ return success(result);
10648
+ }
10649
+ function addBreathMark(score, options) {
10650
+ const { partIndex, measureIndex, noteIndex, placement = "above" } = options;
10651
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10652
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10653
+ }
10654
+ const part = score.parts[partIndex];
10655
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10656
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10657
+ }
10658
+ const measure = part.measures[measureIndex];
10659
+ let noteCount = 0;
10660
+ let targetEntryIndex = -1;
10661
+ for (let i = 0; i < measure.entries.length; i++) {
10662
+ const entry = measure.entries[i];
10663
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10664
+ if (noteCount === noteIndex) {
10665
+ targetEntryIndex = i;
10666
+ break;
10667
+ }
10668
+ noteCount++;
10669
+ }
10670
+ }
10671
+ if (targetEntryIndex < 0) {
10672
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10673
+ }
10674
+ const result = cloneScore(score);
10675
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10676
+ if (!resultNote.notations) {
10677
+ resultNote.notations = [];
10678
+ }
10679
+ const existingBreathMark = resultNote.notations.find(
10680
+ (n) => n.type === "articulation" && n.articulation === "breath-mark"
10681
+ );
10682
+ if (existingBreathMark) {
10683
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Breath mark already exists on note`, { partIndex, measureIndex })]);
10684
+ }
10685
+ resultNote.notations.push({
10686
+ type: "articulation",
10687
+ articulation: "breath-mark",
10688
+ placement
10689
+ });
10690
+ return success(result);
10691
+ }
10692
+ function removeBreathMark(score, options) {
10693
+ const { partIndex, measureIndex, noteIndex } = options;
10694
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10695
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10696
+ }
10697
+ const part = score.parts[partIndex];
10698
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10699
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10700
+ }
10701
+ const measure = part.measures[measureIndex];
10702
+ let noteCount = 0;
10703
+ let targetEntryIndex = -1;
10704
+ for (let i = 0; i < measure.entries.length; i++) {
10705
+ const entry = measure.entries[i];
10706
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10707
+ if (noteCount === noteIndex) {
10708
+ targetEntryIndex = i;
10709
+ break;
10710
+ }
10711
+ noteCount++;
10712
+ }
10713
+ }
10714
+ if (targetEntryIndex < 0) {
10715
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10716
+ }
10717
+ const targetEntry = measure.entries[targetEntryIndex];
10718
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10719
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10720
+ }
10721
+ const breathMarkIndex = targetEntry.notations.findIndex(
10722
+ (n) => n.type === "articulation" && n.articulation === "breath-mark"
10723
+ );
10724
+ if (breathMarkIndex < 0) {
10725
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No breath mark found on note`, { partIndex, measureIndex })]);
10726
+ }
10727
+ const result = cloneScore(score);
10728
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10729
+ resultNote.notations.splice(breathMarkIndex, 1);
10730
+ if (resultNote.notations.length === 0) {
10731
+ delete resultNote.notations;
10732
+ }
10733
+ return success(result);
10734
+ }
10735
+ function addCaesura(score, options) {
10736
+ const { partIndex, measureIndex, noteIndex, placement = "above" } = options;
10737
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10738
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10739
+ }
10740
+ const part = score.parts[partIndex];
10741
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10742
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10743
+ }
10744
+ const measure = part.measures[measureIndex];
10745
+ let noteCount = 0;
10746
+ let targetEntryIndex = -1;
10747
+ for (let i = 0; i < measure.entries.length; i++) {
10748
+ const entry = measure.entries[i];
10749
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10750
+ if (noteCount === noteIndex) {
10751
+ targetEntryIndex = i;
10752
+ break;
10753
+ }
10754
+ noteCount++;
10755
+ }
10756
+ }
10757
+ if (targetEntryIndex < 0) {
10758
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10759
+ }
10760
+ const result = cloneScore(score);
10761
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10762
+ if (!resultNote.notations) {
10763
+ resultNote.notations = [];
10764
+ }
10765
+ const existingCaesura = resultNote.notations.find(
10766
+ (n) => n.type === "articulation" && n.articulation === "caesura"
10767
+ );
10768
+ if (existingCaesura) {
10769
+ return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Caesura already exists on note`, { partIndex, measureIndex })]);
10770
+ }
10771
+ resultNote.notations.push({
10772
+ type: "articulation",
10773
+ articulation: "caesura",
10774
+ placement
10775
+ });
10776
+ return success(result);
10777
+ }
10778
+ function removeCaesura(score, options) {
10779
+ const { partIndex, measureIndex, noteIndex } = options;
10780
+ if (partIndex < 0 || partIndex >= score.parts.length) {
10781
+ return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
10782
+ }
10783
+ const part = score.parts[partIndex];
10784
+ if (measureIndex < 0 || measureIndex >= part.measures.length) {
10785
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
10786
+ }
10787
+ const measure = part.measures[measureIndex];
10788
+ let noteCount = 0;
10789
+ let targetEntryIndex = -1;
10790
+ for (let i = 0; i < measure.entries.length; i++) {
10791
+ const entry = measure.entries[i];
10792
+ if (entry.type === "note" && !entry.chord && !entry.rest) {
10793
+ if (noteCount === noteIndex) {
10794
+ targetEntryIndex = i;
10795
+ break;
10796
+ }
10797
+ noteCount++;
10798
+ }
10799
+ }
10800
+ if (targetEntryIndex < 0) {
10801
+ return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
10802
+ }
10803
+ const targetEntry = measure.entries[targetEntryIndex];
10804
+ if (targetEntry.type !== "note" || !targetEntry.notations) {
10805
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
10806
+ }
10807
+ const caesuraIndex = targetEntry.notations.findIndex(
10808
+ (n) => n.type === "articulation" && n.articulation === "caesura"
10809
+ );
10810
+ if (caesuraIndex < 0) {
10811
+ return failure([operationError("ARTICULATION_NOT_FOUND", `No caesura found on note`, { partIndex, measureIndex })]);
10812
+ }
10813
+ const result = cloneScore(score);
10814
+ const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
10815
+ resultNote.notations.splice(caesuraIndex, 1);
10816
+ if (resultNote.notations.length === 0) {
10817
+ delete resultNote.notations;
10818
+ }
10819
+ return success(result);
10820
+ }
10821
+ var addText = addTextDirection;
10822
+ var setBeaming = autoBeam;
10823
+ var addChordSymbol = addHarmony;
10824
+ var removeChordSymbol = removeHarmony;
10825
+ var updateChordSymbol = updateHarmony;
10826
+ var changeClef = insertClefChange;
10827
+ var setBarline = changeBarline;
10828
+ var addRepeat = addRepeatBarline;
10829
+ var removeRepeat = removeRepeatBarline;
7527
10830
 
7528
10831
  // src/file.ts
7529
10832
  import { readFile, writeFile } from "fs/promises";
@@ -7655,18 +10958,66 @@ export {
7655
10958
  STEPS,
7656
10959
  STEP_SEMITONES,
7657
10960
  ValidationException,
10961
+ addArticulation,
10962
+ addBeam,
10963
+ addBowing,
10964
+ addBreathMark,
10965
+ addCaesura,
10966
+ addChord,
7658
10967
  addChordNote,
10968
+ addChordNoteChecked,
10969
+ addChordSymbol,
10970
+ addCoda,
10971
+ addDaCapo,
10972
+ addDalSegno,
10973
+ addDynamics,
10974
+ addEnding,
10975
+ addFermata,
10976
+ addFine,
10977
+ addFingering,
10978
+ addGraceNote,
10979
+ addHarmony,
10980
+ addLyric,
7659
10981
  addNote,
10982
+ addNoteChecked,
10983
+ addOctaveShift,
10984
+ addOrnament,
10985
+ addPart,
10986
+ addPedal,
10987
+ addRehearsalMark,
10988
+ addRepeat,
10989
+ addRepeatBarline,
10990
+ addSegno,
10991
+ addSlur,
10992
+ addStringNumber,
10993
+ addTempo,
10994
+ addText,
10995
+ addTextDirection,
10996
+ addTie,
10997
+ addToCoda,
10998
+ addVoice,
10999
+ addWedge,
7660
11000
  assertMeasureValid,
7661
11001
  assertValid,
11002
+ autoBeam,
7662
11003
  buildVoiceToStaffMap,
7663
11004
  buildVoiceToStaffMapForPart,
11005
+ changeBarline,
11006
+ changeClef,
7664
11007
  changeKey,
11008
+ changeNoteDuration,
7665
11009
  changeTime,
11010
+ convertToGrace,
11011
+ copyNotes,
11012
+ copyNotesMultiMeasure,
7666
11013
  countNotes,
11014
+ createTuplet,
11015
+ cutNotes,
7667
11016
  decodeBuffer,
7668
11017
  deleteMeasure,
7669
11018
  deleteNote,
11019
+ deleteNoteChecked,
11020
+ duplicatePart,
7670
11021
  exportMidi,
7671
11022
  findBarlines,
7672
11023
  findDirectionsByType,
@@ -7760,7 +11111,9 @@ export {
7760
11111
  hasTieStop,
7761
11112
  hasTuplet,
7762
11113
  inferStaff,
11114
+ insertClefChange,
7763
11115
  insertMeasure,
11116
+ insertNote,
7764
11117
  isChordNote,
7765
11118
  isCompressed,
7766
11119
  isCueNote,
@@ -7773,19 +11126,65 @@ export {
7773
11126
  isValid,
7774
11127
  iterateEntries,
7775
11128
  iterateNotes,
11129
+ lowerAccidental,
7776
11130
  measureRoundtrip,
11131
+ modifyDynamics,
7777
11132
  modifyNoteDuration,
11133
+ modifyNoteDurationChecked,
7778
11134
  modifyNotePitch,
11135
+ modifyNotePitchChecked,
11136
+ modifyTempo,
11137
+ moveNoteToStaff,
7779
11138
  parse,
7780
11139
  parseAuto,
7781
11140
  parseCompressed,
7782
11141
  parseFile,
11142
+ pasteNotes,
11143
+ pasteNotesMultiMeasure,
7783
11144
  pitchToSemitone,
11145
+ raiseAccidental,
11146
+ removeArticulation,
11147
+ removeBeam,
11148
+ removeBowing,
11149
+ removeBreathMark,
11150
+ removeCaesura,
11151
+ removeChordSymbol,
11152
+ removeDynamics,
11153
+ removeEnding,
11154
+ removeFermata,
11155
+ removeFingering,
11156
+ removeGraceNote,
11157
+ removeHarmony,
11158
+ removeLyric,
11159
+ removeNote,
11160
+ removeOctaveShift,
11161
+ removeOrnament,
11162
+ removePart,
11163
+ removePedal,
11164
+ removeRepeat,
11165
+ removeRepeatBarline,
11166
+ removeSlur,
11167
+ removeStringNumber,
11168
+ removeTempo,
11169
+ removeTie,
11170
+ removeTuplet,
11171
+ removeWedge,
7784
11172
  scoresEqual,
7785
11173
  serialize,
7786
11174
  serializeCompressed,
7787
11175
  serializeToFile,
11176
+ setBarline,
11177
+ setBeaming,
11178
+ setNotePitch,
11179
+ setNotePitchBySemitone,
11180
+ setStaves,
11181
+ shiftNotePitch,
11182
+ stopOctaveShift,
7788
11183
  transpose,
11184
+ transposeChecked,
11185
+ updateChordSymbol,
11186
+ updateHarmony,
11187
+ updateLyric,
7789
11188
  validate,
7790
11189
  validateBackupForward,
7791
11190
  validateBeams,