musicxml-io 0.8.2 → 0.9.0

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.
@@ -1095,21 +1095,47 @@ function operationError(code, message, location = {}, details) {
1095
1095
  details
1096
1096
  };
1097
1097
  }
1098
+ function deepClone(value) {
1099
+ if (value === null || typeof value !== "object") return value;
1100
+ if (Array.isArray(value)) {
1101
+ const len = value.length;
1102
+ const out2 = new Array(len);
1103
+ for (let i = 0; i < len; i++) out2[i] = deepClone(value[i]);
1104
+ return out2;
1105
+ }
1106
+ const out = {};
1107
+ const record = value;
1108
+ for (const key in record) {
1109
+ const v = record[key];
1110
+ if (v !== void 0) out[key] = deepClone(v);
1111
+ }
1112
+ return out;
1113
+ }
1098
1114
  function cloneScore(score) {
1099
- return JSON.parse(JSON.stringify(score));
1115
+ return deepClone(score);
1116
+ }
1117
+ function cloneScoreAt(score, partIndex, ...measureIndices) {
1118
+ const parts = score.parts.slice();
1119
+ const part = { ...parts[partIndex] };
1120
+ part.measures = part.measures.slice();
1121
+ for (const measureIndex of new Set(measureIndices)) {
1122
+ part.measures[measureIndex] = deepClone(score.parts[partIndex].measures[measureIndex]);
1123
+ }
1124
+ parts[partIndex] = part;
1125
+ return { ...score, parts };
1100
1126
  }
1101
1127
  function cloneNoteWithNewId(note) {
1102
- const cloned = JSON.parse(JSON.stringify(note));
1128
+ const cloned = deepClone(note);
1103
1129
  cloned._id = generateId();
1104
1130
  return cloned;
1105
1131
  }
1106
1132
  function cloneEntryWithNewId(entry) {
1107
- const cloned = JSON.parse(JSON.stringify(entry));
1133
+ const cloned = deepClone(entry);
1108
1134
  cloned._id = generateId();
1109
1135
  return cloned;
1110
1136
  }
1111
1137
  function cloneMeasureWithNewIds(measure) {
1112
- const cloned = JSON.parse(JSON.stringify(measure));
1138
+ const cloned = deepClone(measure);
1113
1139
  cloned._id = generateId();
1114
1140
  cloned.entries = cloned.entries.map((entry) => cloneEntryWithNewId(entry));
1115
1141
  if (cloned.barlines) {
@@ -1121,7 +1147,7 @@ function cloneMeasureWithNewIds(measure) {
1121
1147
  return cloned;
1122
1148
  }
1123
1149
  function clonePartWithNewIds(part) {
1124
- const cloned = JSON.parse(JSON.stringify(part));
1150
+ const cloned = deepClone(part);
1125
1151
  cloned._id = generateId();
1126
1152
  cloned.measures = cloned.measures.map((measure) => cloneMeasureWithNewIds(measure));
1127
1153
  return cloned;
@@ -1288,7 +1314,7 @@ function insertNote(score, options) {
1288
1314
  if (options.position < 0) {
1289
1315
  return failure([operationError("INVALID_POSITION", `Position cannot be negative`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1290
1316
  }
1291
- const result = cloneScore(score);
1317
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
1292
1318
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1293
1319
  const context = getMeasureContext(result, options.partIndex, options.measureIndex);
1294
1320
  const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
@@ -1356,7 +1382,7 @@ function removeNote(score, options) {
1356
1382
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1357
1383
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1358
1384
  }
1359
- const result = cloneScore(score);
1385
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
1360
1386
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1361
1387
  let noteCount = 0;
1362
1388
  let targetEntry = null;
@@ -1399,7 +1425,7 @@ function addChord(score, options) {
1399
1425
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1400
1426
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1401
1427
  }
1402
- const result = cloneScore(score);
1428
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
1403
1429
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1404
1430
  let noteCount = 0;
1405
1431
  let targetEntry = null;
@@ -1452,7 +1478,7 @@ function changeNoteDuration(score, options) {
1452
1478
  if (options.newDuration <= 0) {
1453
1479
  return failure([operationError("INVALID_DURATION", `Duration must be positive`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1454
1480
  }
1455
- const result = cloneScore(score);
1481
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
1456
1482
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1457
1483
  const context = getMeasureContext(result, options.partIndex, options.measureIndex);
1458
1484
  const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
@@ -1556,7 +1582,7 @@ function setNotePitch(score, options) {
1556
1582
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1557
1583
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1558
1584
  }
1559
- const result = cloneScore(score);
1585
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
1560
1586
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1561
1587
  let noteCount = 0;
1562
1588
  for (const entry of measure.entries) {
@@ -1578,7 +1604,7 @@ function setNotePitchBySemitone(score, options) {
1578
1604
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1579
1605
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1580
1606
  }
1581
- const result = cloneScore(score);
1607
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
1582
1608
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1583
1609
  const measureNumber = measure.number ?? String(options.measureIndex + 1);
1584
1610
  const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
@@ -1651,7 +1677,7 @@ function raiseAccidental(score, options) {
1651
1677
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1652
1678
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1653
1679
  }
1654
- const result = cloneScore(score);
1680
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
1655
1681
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1656
1682
  const measureNumber = measure.number ?? String(options.measureIndex + 1);
1657
1683
  const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
@@ -1692,7 +1718,7 @@ function lowerAccidental(score, options) {
1692
1718
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1693
1719
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1694
1720
  }
1695
- const result = cloneScore(score);
1721
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
1696
1722
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1697
1723
  const measureNumber = measure.number ?? String(options.measureIndex + 1);
1698
1724
  const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
@@ -1733,7 +1759,7 @@ function addVoice(score, options) {
1733
1759
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1734
1760
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1735
1761
  }
1736
- const result = cloneScore(score);
1762
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
1737
1763
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1738
1764
  const existingVoiceEntries = getVoiceEntries(measure, options.voice, options.staff);
1739
1765
  if (existingVoiceEntries.length > 0) {
@@ -1930,7 +1956,7 @@ function moveNoteToStaff(score, options) {
1930
1956
  if (options.targetStaff < 1) {
1931
1957
  return failure([operationError("INVALID_STAFF", `Target staff must be at least 1`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1932
1958
  }
1933
- const result = cloneScore(score);
1959
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
1934
1960
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1935
1961
  let noteCount = 0;
1936
1962
  for (const entry of measure.entries) {
@@ -2041,7 +2067,7 @@ function addTie(score, options) {
2041
2067
  if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
2042
2068
  return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
2043
2069
  }
2044
- const result = cloneScore(score);
2070
+ const result = cloneScoreAt(score, options.partIndex, options.startMeasureIndex, options.endMeasureIndex);
2045
2071
  const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
2046
2072
  const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
2047
2073
  const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
@@ -2084,7 +2110,7 @@ function removeTie(score, options) {
2084
2110
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2085
2111
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2086
2112
  }
2087
- const result = cloneScore(score);
2113
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2088
2114
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2089
2115
  const noteResult = findNoteByIndex(measure, options.noteIndex);
2090
2116
  if (!noteResult) {
@@ -2115,7 +2141,7 @@ function addSlur(score, options) {
2115
2141
  if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
2116
2142
  return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
2117
2143
  }
2118
- const result = cloneScore(score);
2144
+ const result = cloneScoreAt(score, options.partIndex, options.startMeasureIndex, options.endMeasureIndex);
2119
2145
  const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
2120
2146
  const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
2121
2147
  const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
@@ -2160,7 +2186,7 @@ function removeSlur(score, options) {
2160
2186
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2161
2187
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2162
2188
  }
2163
- const result = cloneScore(score);
2189
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2164
2190
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2165
2191
  const noteResult = findNoteByIndex(measure, options.noteIndex);
2166
2192
  if (!noteResult) {
@@ -2185,7 +2211,7 @@ function addArticulation(score, options) {
2185
2211
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2186
2212
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2187
2213
  }
2188
- const result = cloneScore(score);
2214
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2189
2215
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2190
2216
  const noteResult = findNoteByIndex(measure, options.noteIndex);
2191
2217
  if (!noteResult) {
@@ -2211,7 +2237,7 @@ function removeArticulation(score, options) {
2211
2237
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2212
2238
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2213
2239
  }
2214
- const result = cloneScore(score);
2240
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2215
2241
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2216
2242
  const noteResult = findNoteByIndex(measure, options.noteIndex);
2217
2243
  if (!noteResult) {
@@ -2257,7 +2283,7 @@ function addDynamics(score, options) {
2257
2283
  if (options.position < 0) {
2258
2284
  return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2259
2285
  }
2260
- const result = cloneScore(score);
2286
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2261
2287
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2262
2288
  const directionEntry = {
2263
2289
  _id: generateId(),
@@ -2281,7 +2307,7 @@ function removeDynamics(score, options) {
2281
2307
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2282
2308
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2283
2309
  }
2284
- const result = cloneScore(score);
2310
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2285
2311
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2286
2312
  let directionCount = 0;
2287
2313
  let targetIndex = -1;
@@ -2312,7 +2338,7 @@ function modifyDynamics(score, options) {
2312
2338
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2313
2339
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2314
2340
  }
2315
- const result = cloneScore(score);
2341
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2316
2342
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2317
2343
  let dynamicsCount = 0;
2318
2344
  let targetIndex = -1;
@@ -2457,7 +2483,7 @@ function createTuplet(score, options) {
2457
2483
  if (options.actualNotes < 2 || options.normalNotes < 1) {
2458
2484
  return failure([operationError("INVALID_DURATION", "Invalid tuplet ratio", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2459
2485
  }
2460
- const result = cloneScore(score);
2486
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2461
2487
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2462
2488
  const notes = [];
2463
2489
  let noteCount = 0;
@@ -2526,7 +2552,7 @@ function removeTuplet(score, options) {
2526
2552
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2527
2553
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2528
2554
  }
2529
- const result = cloneScore(score);
2555
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2530
2556
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2531
2557
  let noteCount = 0;
2532
2558
  let targetNote = null;
@@ -2607,7 +2633,7 @@ function addBeam(score, options) {
2607
2633
  if (options.noteCount < 2) {
2608
2634
  return failure([operationError("INVALID_DURATION", "Beam must contain at least 2 notes", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2609
2635
  }
2610
- const result = cloneScore(score);
2636
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2611
2637
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2612
2638
  const beamLevel = options.beamLevel ?? 1;
2613
2639
  const notes = [];
@@ -2662,7 +2688,7 @@ function removeBeam(score, options) {
2662
2688
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2663
2689
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2664
2690
  }
2665
- const result = cloneScore(score);
2691
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2666
2692
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2667
2693
  let noteCount = 0;
2668
2694
  let targetNote = null;
@@ -2736,7 +2762,7 @@ function autoBeam(score, options) {
2736
2762
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
2737
2763
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2738
2764
  }
2739
- const result = cloneScore(score);
2765
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2740
2766
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2741
2767
  const context = getMeasureContext(result, options.partIndex, options.measureIndex);
2742
2768
  const divisions = context.divisions;
@@ -2902,7 +2928,7 @@ function pasteNotes(score, options) {
2902
2928
  if (options.position < 0) {
2903
2929
  return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
2904
2930
  }
2905
- const result = cloneScore(score);
2931
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
2906
2932
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
2907
2933
  const context = getMeasureContext(result, options.partIndex, options.measureIndex);
2908
2934
  const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
@@ -3008,7 +3034,7 @@ function cutNotes(score, options) {
3008
3034
  return failure(copyResult.errors);
3009
3035
  }
3010
3036
  const selection = copyResult.data;
3011
- const result = cloneScore(score);
3037
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3012
3038
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3013
3039
  const context = getMeasureContext(result, options.partIndex, options.measureIndex);
3014
3040
  const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
@@ -3182,7 +3208,7 @@ function addTempo(score, options) {
3182
3208
  if (options.bpm <= 0) {
3183
3209
  return failure([operationError("INVALID_DURATION", "BPM must be positive", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3184
3210
  }
3185
- const result = cloneScore(score);
3211
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3186
3212
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3187
3213
  const directionTypes = [];
3188
3214
  directionTypes.push({
@@ -3216,7 +3242,7 @@ function removeTempo(score, options) {
3216
3242
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3217
3243
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3218
3244
  }
3219
- const result = cloneScore(score);
3245
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3220
3246
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3221
3247
  const tempoDirectionIndices = [];
3222
3248
  for (let i = 0; i < measure.entries.length; i++) {
@@ -3243,7 +3269,7 @@ function modifyTempo(score, options) {
3243
3269
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3244
3270
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3245
3271
  }
3246
- const result = cloneScore(score);
3272
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3247
3273
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3248
3274
  const tempoDirectionIndices = [];
3249
3275
  for (let i = 0; i < measure.entries.length; i++) {
@@ -3392,7 +3418,7 @@ function addFermata(score, options) {
3392
3418
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3393
3419
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3394
3420
  }
3395
- const result = cloneScore(score);
3421
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3396
3422
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3397
3423
  const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
3398
3424
  if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
@@ -3422,7 +3448,7 @@ function removeFermata(score, options) {
3422
3448
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3423
3449
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3424
3450
  }
3425
- const result = cloneScore(score);
3451
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3426
3452
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3427
3453
  const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
3428
3454
  if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
@@ -3447,7 +3473,7 @@ function addOrnament(score, options) {
3447
3473
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3448
3474
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3449
3475
  }
3450
- const result = cloneScore(score);
3476
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3451
3477
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3452
3478
  const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
3453
3479
  if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
@@ -3477,7 +3503,7 @@ function removeOrnament(score, options) {
3477
3503
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3478
3504
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3479
3505
  }
3480
- const result = cloneScore(score);
3506
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3481
3507
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3482
3508
  const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
3483
3509
  if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
@@ -3502,7 +3528,7 @@ function addPedal(score, options) {
3502
3528
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3503
3529
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3504
3530
  }
3505
- const result = cloneScore(score);
3531
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3506
3532
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3507
3533
  const direction = {
3508
3534
  _id: generateId(),
@@ -3525,7 +3551,7 @@ function removePedal(score, options) {
3525
3551
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3526
3552
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3527
3553
  }
3528
- const result = cloneScore(score);
3554
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3529
3555
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3530
3556
  const pedalIndices = [];
3531
3557
  for (let i = 0; i < measure.entries.length; i++) {
@@ -3555,7 +3581,7 @@ function addTextDirection(score, options) {
3555
3581
  if (!options.text.trim()) {
3556
3582
  return failure([operationError("INVALID_TEXT", "Text cannot be empty", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3557
3583
  }
3558
- const result = cloneScore(score);
3584
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3559
3585
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3560
3586
  const direction = {
3561
3587
  _id: generateId(),
@@ -3579,7 +3605,7 @@ function addRehearsalMark(score, options) {
3579
3605
  if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
3580
3606
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
3581
3607
  }
3582
- const result = cloneScore(score);
3608
+ const result = cloneScoreAt(score, options.partIndex, options.measureIndex);
3583
3609
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
3584
3610
  const direction = {
3585
3611
  _id: generateId(),
@@ -3782,7 +3808,7 @@ function addSegno(score, options) {
3782
3808
  if (measureIndex < 0 || measureIndex >= part.measures.length) {
3783
3809
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3784
3810
  }
3785
- const result = cloneScore(score);
3811
+ const result = cloneScoreAt(score, partIndex, measureIndex);
3786
3812
  const measure = result.parts[partIndex].measures[measureIndex];
3787
3813
  const direction = {
3788
3814
  _id: generateId(),
@@ -3802,7 +3828,7 @@ function addCoda(score, options) {
3802
3828
  if (measureIndex < 0 || measureIndex >= part.measures.length) {
3803
3829
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3804
3830
  }
3805
- const result = cloneScore(score);
3831
+ const result = cloneScoreAt(score, partIndex, measureIndex);
3806
3832
  const measure = result.parts[partIndex].measures[measureIndex];
3807
3833
  const direction = {
3808
3834
  _id: generateId(),
@@ -3822,7 +3848,7 @@ function addDaCapo(score, options) {
3822
3848
  if (measureIndex < 0 || measureIndex >= part.measures.length) {
3823
3849
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3824
3850
  }
3825
- const result = cloneScore(score);
3851
+ const result = cloneScoreAt(score, partIndex, measureIndex);
3826
3852
  const measure = result.parts[partIndex].measures[measureIndex];
3827
3853
  const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
3828
3854
  const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
@@ -3851,7 +3877,7 @@ function addDalSegno(score, options) {
3851
3877
  if (measureIndex < 0 || measureIndex >= part.measures.length) {
3852
3878
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3853
3879
  }
3854
- const result = cloneScore(score);
3880
+ const result = cloneScoreAt(score, partIndex, measureIndex);
3855
3881
  const measure = result.parts[partIndex].measures[measureIndex];
3856
3882
  const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
3857
3883
  const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
@@ -3880,7 +3906,7 @@ function addFine(score, options) {
3880
3906
  if (measureIndex < 0 || measureIndex >= part.measures.length) {
3881
3907
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3882
3908
  }
3883
- const result = cloneScore(score);
3909
+ const result = cloneScoreAt(score, partIndex, measureIndex);
3884
3910
  const measure = result.parts[partIndex].measures[measureIndex];
3885
3911
  const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
3886
3912
  const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
@@ -3909,7 +3935,7 @@ function addToCoda(score, options) {
3909
3935
  if (measureIndex < 0 || measureIndex >= part.measures.length) {
3910
3936
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
3911
3937
  }
3912
- const result = cloneScore(score);
3938
+ const result = cloneScoreAt(score, partIndex, measureIndex);
3913
3939
  const measure = result.parts[partIndex].measures[measureIndex];
3914
3940
  const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
3915
3941
  const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
@@ -3956,7 +3982,7 @@ function addGraceNote(score, options) {
3956
3982
  if (targetEntryIndex < 0 || !targetNote) {
3957
3983
  return failure([operationError("NOTE_NOT_FOUND", `Note at index ${targetNoteIndex} not found`, { partIndex, measureIndex })]);
3958
3984
  }
3959
- const result = cloneScore(score);
3985
+ const result = cloneScoreAt(score, partIndex, measureIndex);
3960
3986
  const resultMeasure = result.parts[partIndex].measures[measureIndex];
3961
3987
  const graceNote = {
3962
3988
  _id: generateId(),
@@ -3999,7 +4025,7 @@ function removeGraceNote(score, options) {
3999
4025
  if (targetEntryIndex < 0) {
4000
4026
  return failure([operationError("GRACE_NOTE_NOT_FOUND", `Grace note at index ${graceNoteIndex} not found`, { partIndex, measureIndex })]);
4001
4027
  }
4002
- const result = cloneScore(score);
4028
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4003
4029
  result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
4004
4030
  return success(result);
4005
4031
  }
@@ -4035,7 +4061,7 @@ function convertToGrace(score, options) {
4035
4061
  if (targetEntry.grace) {
4036
4062
  return failure([operationError("INVALID_GRACE_NOTE", `Note is already a grace note`, { partIndex, measureIndex })]);
4037
4063
  }
4038
- const result = cloneScore(score);
4064
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4039
4065
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4040
4066
  resultNote.grace = { slash };
4041
4067
  resultNote.duration = 0;
@@ -4073,7 +4099,7 @@ function addLyric(score, options) {
4073
4099
  if (targetEntry.lyrics?.some((l) => l.number === verse)) {
4074
4100
  return failure([operationError("LYRIC_ALREADY_EXISTS", `Lyric for verse ${verse} already exists on this note`, { partIndex, measureIndex })]);
4075
4101
  }
4076
- const result = cloneScore(score);
4102
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4077
4103
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4078
4104
  if (!resultNote.lyrics) {
4079
4105
  resultNote.lyrics = [];
@@ -4122,7 +4148,7 @@ function removeLyric(score, options) {
4122
4148
  return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
4123
4149
  }
4124
4150
  }
4125
- const result = cloneScore(score);
4151
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4126
4152
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4127
4153
  if (verse !== void 0) {
4128
4154
  resultNote.lyrics = resultNote.lyrics.filter((l) => l.number !== verse);
@@ -4167,7 +4193,7 @@ function updateLyric(score, options) {
4167
4193
  if (lyricIndex < 0) {
4168
4194
  return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
4169
4195
  }
4170
- const result = cloneScore(score);
4196
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4171
4197
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4172
4198
  const lyric = resultNote.lyrics[lyricIndex];
4173
4199
  if (text !== void 0) {
@@ -4197,7 +4223,7 @@ function addHarmony(score, options) {
4197
4223
  if (bass && !validSteps.includes(bass.step.toUpperCase())) {
4198
4224
  return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
4199
4225
  }
4200
- const result = cloneScore(score);
4226
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4201
4227
  const measure = result.parts[partIndex].measures[measureIndex];
4202
4228
  const harmony = {
4203
4229
  _id: generateId(),
@@ -4265,7 +4291,7 @@ function removeHarmony(score, options) {
4265
4291
  if (targetEntryIndex < 0) {
4266
4292
  return failure([operationError("HARMONY_NOT_FOUND", `Harmony at index ${harmonyIndex} not found`, { partIndex, measureIndex })]);
4267
4293
  }
4268
- const result = cloneScore(score);
4294
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4269
4295
  result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
4270
4296
  return success(result);
4271
4297
  }
@@ -4301,7 +4327,7 @@ function updateHarmony(score, options) {
4301
4327
  if (bass && !validSteps.includes(bass.step.toUpperCase())) {
4302
4328
  return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
4303
4329
  }
4304
- const result = cloneScore(score);
4330
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4305
4331
  const harmony = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4306
4332
  if (root) {
4307
4333
  harmony.root = {
@@ -4363,7 +4389,7 @@ function addFingering(score, options) {
4363
4389
  if (targetEntryIndex < 0) {
4364
4390
  return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4365
4391
  }
4366
- const result = cloneScore(score);
4392
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4367
4393
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4368
4394
  if (!resultNote.notations) {
4369
4395
  resultNote.notations = [];
@@ -4413,7 +4439,7 @@ function removeFingering(score, options) {
4413
4439
  if (fingeringIndex < 0) {
4414
4440
  return failure([operationError("NOTE_NOT_FOUND", `No fingering found on note`, { partIndex, measureIndex })]);
4415
4441
  }
4416
- const result = cloneScore(score);
4442
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4417
4443
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4418
4444
  resultNote.notations.splice(fingeringIndex, 1);
4419
4445
  if (resultNote.notations.length === 0) {
@@ -4446,7 +4472,7 @@ function addBowing(score, options) {
4446
4472
  if (targetEntryIndex < 0) {
4447
4473
  return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4448
4474
  }
4449
- const result = cloneScore(score);
4475
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4450
4476
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4451
4477
  if (!resultNote.notations) {
4452
4478
  resultNote.notations = [];
@@ -4495,7 +4521,7 @@ function removeBowing(score, options) {
4495
4521
  if (bowingIndex < 0) {
4496
4522
  return failure([operationError("NOTE_NOT_FOUND", `No bowing found on note`, { partIndex, measureIndex })]);
4497
4523
  }
4498
- const result = cloneScore(score);
4524
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4499
4525
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4500
4526
  resultNote.notations.splice(bowingIndex, 1);
4501
4527
  if (resultNote.notations.length === 0) {
@@ -4531,7 +4557,7 @@ function addStringNumber(score, options) {
4531
4557
  if (targetEntryIndex < 0) {
4532
4558
  return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4533
4559
  }
4534
- const result = cloneScore(score);
4560
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4535
4561
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4536
4562
  if (!resultNote.notations) {
4537
4563
  resultNote.notations = [];
@@ -4579,7 +4605,7 @@ function removeStringNumber(score, options) {
4579
4605
  if (stringIndex < 0) {
4580
4606
  return failure([operationError("NOTE_NOT_FOUND", `No string number found on note`, { partIndex, measureIndex })]);
4581
4607
  }
4582
- const result = cloneScore(score);
4608
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4583
4609
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4584
4610
  resultNote.notations.splice(stringIndex, 1);
4585
4611
  if (resultNote.notations.length === 0) {
@@ -4596,7 +4622,7 @@ function addOctaveShift(score, options) {
4596
4622
  if (measureIndex < 0 || measureIndex >= part.measures.length) {
4597
4623
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4598
4624
  }
4599
- const result = cloneScore(score);
4625
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4600
4626
  const measure = result.parts[partIndex].measures[measureIndex];
4601
4627
  const direction = {
4602
4628
  _id: generateId(),
@@ -4620,7 +4646,7 @@ function stopOctaveShift(score, options) {
4620
4646
  if (measureIndex < 0 || measureIndex >= part.measures.length) {
4621
4647
  return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
4622
4648
  }
4623
- const result = cloneScore(score);
4649
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4624
4650
  const measure = result.parts[partIndex].measures[measureIndex];
4625
4651
  const direction = {
4626
4652
  _id: generateId(),
@@ -4662,7 +4688,7 @@ function removeOctaveShift(score, options) {
4662
4688
  if (targetEntryIndex < 0) {
4663
4689
  return failure([operationError("NOTE_NOT_FOUND", `Octave shift at index ${octaveShiftIndex} not found`, { partIndex, measureIndex })]);
4664
4690
  }
4665
- const result = cloneScore(score);
4691
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4666
4692
  result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
4667
4693
  return success(result);
4668
4694
  }
@@ -4691,7 +4717,7 @@ function addBreathMark(score, options) {
4691
4717
  if (targetEntryIndex < 0) {
4692
4718
  return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4693
4719
  }
4694
- const result = cloneScore(score);
4720
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4695
4721
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4696
4722
  if (!resultNote.notations) {
4697
4723
  resultNote.notations = [];
@@ -4744,7 +4770,7 @@ function removeBreathMark(score, options) {
4744
4770
  if (breathMarkIndex < 0) {
4745
4771
  return failure([operationError("ARTICULATION_NOT_FOUND", `No breath mark found on note`, { partIndex, measureIndex })]);
4746
4772
  }
4747
- const result = cloneScore(score);
4773
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4748
4774
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4749
4775
  resultNote.notations.splice(breathMarkIndex, 1);
4750
4776
  if (resultNote.notations.length === 0) {
@@ -4777,7 +4803,7 @@ function addCaesura(score, options) {
4777
4803
  if (targetEntryIndex < 0) {
4778
4804
  return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
4779
4805
  }
4780
- const result = cloneScore(score);
4806
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4781
4807
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4782
4808
  if (!resultNote.notations) {
4783
4809
  resultNote.notations = [];
@@ -4830,7 +4856,7 @@ function removeCaesura(score, options) {
4830
4856
  if (caesuraIndex < 0) {
4831
4857
  return failure([operationError("ARTICULATION_NOT_FOUND", `No caesura found on note`, { partIndex, measureIndex })]);
4832
4858
  }
4833
- const result = cloneScore(score);
4859
+ const result = cloneScoreAt(score, partIndex, measureIndex);
4834
4860
  const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
4835
4861
  resultNote.notations.splice(caesuraIndex, 1);
4836
4862
  if (resultNote.notations.length === 0) {