musicxml-io 0.2.9 → 0.2.12

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