musicxml-io 0.1.0 → 0.2.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.
package/dist/index.mjs CHANGED
@@ -378,8 +378,10 @@ function parsePartList(elements) {
378
378
  const sound = getElementText(instContent, "instrument-sound");
379
379
  if (sound) inst.sound = sound;
380
380
  if (hasElement(instContent, "solo")) inst.solo = true;
381
- const ens = getElementText(instContent, "ensemble");
382
- if (ens) inst.ensemble = parseInt(ens, 10);
381
+ if (hasElement(instContent, "ensemble")) {
382
+ const ensText = getElementText(instContent, "ensemble");
383
+ inst.ensemble = ensText ? parseInt(ensText, 10) : 0;
384
+ }
383
385
  instruments.push(inst);
384
386
  }
385
387
  }
@@ -562,6 +564,8 @@ function parseAttributes(elements) {
562
564
  if (divisions !== void 0) attrs.divisions = divisions;
563
565
  const staves = getElementTextAsInt(elements, "staves");
564
566
  if (staves !== void 0) attrs.staves = staves;
567
+ const instruments = getElementTextAsInt(elements, "instruments");
568
+ if (instruments !== void 0) attrs.instruments = instruments;
565
569
  const time = getElementContent(elements, "time");
566
570
  if (time) {
567
571
  attrs.time = parseTimeSignature(time, elements);
@@ -658,7 +662,7 @@ function parseKeySignature(elements) {
658
662
  const key = {
659
663
  fifths: parseInt(fifths || "0", 10)
660
664
  };
661
- const validModes = ["major", "minor", "dorian", "phrygian", "lydian", "mixolydian", "aeolian", "ionian", "locrian"];
665
+ const validModes = ["major", "minor", "dorian", "phrygian", "lydian", "mixolydian", "aeolian", "ionian", "locrian", "none"];
662
666
  if (mode && validModes.includes(mode)) {
663
667
  key.mode = mode;
664
668
  }
@@ -689,6 +693,12 @@ function parseClef(elements, attrs) {
689
693
  if (octaveChange !== void 0) {
690
694
  clef.clefOctaveChange = octaveChange;
691
695
  }
696
+ if (attrs["print-object"] === "no") {
697
+ clef.printObject = false;
698
+ }
699
+ if (attrs["after-barline"] === "yes") {
700
+ clef.afterBarline = true;
701
+ }
692
702
  return clef;
693
703
  }
694
704
  function parseTranspose(elements) {
@@ -898,6 +908,7 @@ function parseNotations(elements, notationsIndex = 0) {
898
908
  slurType: attrs["type"] || "start",
899
909
  number: attrs["number"] ? parseInt(attrs["number"], 10) : void 0,
900
910
  lineType: attrs["line-type"],
911
+ orientation: attrs["orientation"],
901
912
  defaultX: attrs["default-x"] ? parseFloat(attrs["default-x"]) : void 0,
902
913
  defaultY: attrs["default-y"] ? parseFloat(attrs["default-y"]) : void 0,
903
914
  bezierX: attrs["bezier-x"] ? parseFloat(attrs["bezier-x"]) : void 0,
@@ -1410,6 +1421,9 @@ function parseDirection(elements, attrs) {
1410
1421
  direction.sound = {};
1411
1422
  if (soundAttrs["tempo"]) direction.sound.tempo = parseFloat(soundAttrs["tempo"]);
1412
1423
  if (soundAttrs["dynamics"]) direction.sound.dynamics = parseFloat(soundAttrs["dynamics"]);
1424
+ if (soundAttrs["damper-pedal"]) direction.sound.damperPedal = soundAttrs["damper-pedal"];
1425
+ if (soundAttrs["soft-pedal"]) direction.sound.softPedal = soundAttrs["soft-pedal"];
1426
+ if (soundAttrs["sostenuto-pedal"]) direction.sound.sostenutoPedal = soundAttrs["sostenuto-pedal"];
1413
1427
  for (const soundEl of soundContent) {
1414
1428
  if (soundEl["midi-instrument"]) {
1415
1429
  const midiAttrs = getAttributes(soundEl);
@@ -1570,6 +1584,8 @@ function parseDirectionType(elements) {
1570
1584
  if (bracketAttrs["number"]) result.number = parseInt(bracketAttrs["number"], 10);
1571
1585
  if (bracketAttrs["line-end"]) result.lineEnd = bracketAttrs["line-end"];
1572
1586
  if (bracketAttrs["line-type"]) result.lineType = bracketAttrs["line-type"];
1587
+ if (bracketAttrs["default-y"]) result.defaultY = parseFloat(bracketAttrs["default-y"]);
1588
+ if (bracketAttrs["relative-x"]) result.relativeX = parseFloat(bracketAttrs["relative-x"]);
1573
1589
  return result;
1574
1590
  }
1575
1591
  }
@@ -1869,6 +1885,8 @@ var VALID_BAR_STYLES = /* @__PURE__ */ new Set([
1869
1885
  "light-heavy",
1870
1886
  "heavy-light",
1871
1887
  "heavy-heavy",
1888
+ "tick",
1889
+ "short",
1872
1890
  "none"
1873
1891
  ]);
1874
1892
  function isValidNoteType(value) {
@@ -2138,6 +2156,9 @@ function parseSound(elements, attrs) {
2138
2156
  if (attrs["tocoda"]) sound.tocoda = attrs["tocoda"];
2139
2157
  if (attrs["fine"] === "yes") sound.fine = true;
2140
2158
  if (attrs["forward-repeat"] === "yes") sound.forwardRepeat = true;
2159
+ if (attrs["damper-pedal"]) sound.damperPedal = attrs["damper-pedal"];
2160
+ if (attrs["soft-pedal"]) sound.softPedal = attrs["soft-pedal"];
2161
+ if (attrs["sostenuto-pedal"]) sound.sostenutoPedal = attrs["sostenuto-pedal"];
2141
2162
  for (const el of elements) {
2142
2163
  if (el["swing"]) {
2143
2164
  const swingContent = el["swing"];
@@ -2234,6 +2255,8 @@ function parseAuto(data) {
2234
2255
  var DEFAULT_OPTIONS = {
2235
2256
  checkDivisions: true,
2236
2257
  checkMeasureDuration: true,
2258
+ checkMeasureFullness: false,
2259
+ // Piano Roll semantics - opt-in
2237
2260
  checkPosition: true,
2238
2261
  checkTies: true,
2239
2262
  checkBeams: true,
@@ -2290,6 +2313,14 @@ function validate(score, options = {}) {
2290
2313
  opts.durationTolerance
2291
2314
  ));
2292
2315
  }
2316
+ if (opts.checkMeasureFullness && currentTime) {
2317
+ allErrors.push(...validateMeasureFullness(
2318
+ measure,
2319
+ currentDivisions,
2320
+ currentTime,
2321
+ location
2322
+ ));
2323
+ }
2293
2324
  if (opts.checkPosition) {
2294
2325
  allErrors.push(...validateBackupForward(measure, location));
2295
2326
  }
@@ -2438,6 +2469,92 @@ function calculateVoiceDurations(measure) {
2438
2469
  }
2439
2470
  return voiceDurations;
2440
2471
  }
2472
+ function validateMeasureFullness(measure, divisions, time, location) {
2473
+ const errors = [];
2474
+ if (time.senzaMisura) {
2475
+ return errors;
2476
+ }
2477
+ const beats = parseInt(time.beats, 10);
2478
+ if (isNaN(beats)) {
2479
+ return errors;
2480
+ }
2481
+ const expectedDuration = beats / time.beatType * 4 * divisions;
2482
+ const voiceCoverage = /* @__PURE__ */ new Map();
2483
+ let currentPosition = 0;
2484
+ for (const entry of measure.entries) {
2485
+ if (entry.type === "note") {
2486
+ const staff = entry.staff ?? 1;
2487
+ const voice = entry.voice;
2488
+ const key = `${staff}-${voice}`;
2489
+ if (!entry.chord) {
2490
+ if (!voiceCoverage.has(key)) {
2491
+ voiceCoverage.set(key, { segments: [] });
2492
+ }
2493
+ voiceCoverage.get(key).segments.push({
2494
+ start: currentPosition,
2495
+ end: currentPosition + entry.duration
2496
+ });
2497
+ currentPosition += entry.duration;
2498
+ }
2499
+ } else if (entry.type === "backup") {
2500
+ currentPosition -= entry.duration;
2501
+ } else if (entry.type === "forward") {
2502
+ const staff = entry.staff ?? 1;
2503
+ const voice = entry.voice ?? 1;
2504
+ const key = `${staff}-${voice}`;
2505
+ if (!voiceCoverage.has(key)) {
2506
+ voiceCoverage.set(key, { segments: [] });
2507
+ }
2508
+ voiceCoverage.get(key).segments.push({
2509
+ start: currentPosition,
2510
+ end: currentPosition + entry.duration
2511
+ });
2512
+ currentPosition += entry.duration;
2513
+ }
2514
+ }
2515
+ for (const [voiceKey, { segments }] of voiceCoverage.entries()) {
2516
+ const [staff, voice] = voiceKey.split("-").map(Number);
2517
+ const sorted = [...segments].sort((a, b) => a.start - b.start);
2518
+ let lastEnd = 0;
2519
+ const gaps = [];
2520
+ for (const seg of sorted) {
2521
+ if (seg.start > lastEnd) {
2522
+ gaps.push({ start: lastEnd, end: seg.start });
2523
+ }
2524
+ lastEnd = Math.max(lastEnd, seg.end);
2525
+ }
2526
+ if (lastEnd < expectedDuration) {
2527
+ gaps.push({ start: lastEnd, end: expectedDuration });
2528
+ }
2529
+ for (const gap of gaps) {
2530
+ errors.push({
2531
+ code: "VOICE_GAP",
2532
+ level: "warning",
2533
+ message: `Voice ${voice} (staff ${staff}) has gap from position ${gap.start} to ${gap.end}`,
2534
+ location: { ...location, voice, staff },
2535
+ details: {
2536
+ gapStart: gap.start,
2537
+ gapEnd: gap.end,
2538
+ gapDuration: gap.end - gap.start
2539
+ }
2540
+ });
2541
+ }
2542
+ if (lastEnd < expectedDuration) {
2543
+ errors.push({
2544
+ code: "VOICE_INCOMPLETE",
2545
+ level: "warning",
2546
+ message: `Voice ${voice} (staff ${staff}) ends at ${lastEnd}, expected ${expectedDuration}`,
2547
+ location: { ...location, voice, staff },
2548
+ details: {
2549
+ actualEnd: lastEnd,
2550
+ expectedDuration,
2551
+ missing: expectedDuration - lastEnd
2552
+ }
2553
+ });
2554
+ }
2555
+ }
2556
+ return errors;
2557
+ }
2441
2558
  function validateBackupForward(measure, location) {
2442
2559
  const errors = [];
2443
2560
  let position = 0;
@@ -2904,6 +3021,8 @@ function validateStaffStructure(part, partIndex) {
2904
3021
  }
2905
3022
  var DEFAULT_LOCAL_OPTIONS = {
2906
3023
  checkMeasureDuration: true,
3024
+ checkMeasureFullness: false,
3025
+ // Piano Roll semantics - opt-in
2907
3026
  checkPosition: true,
2908
3027
  checkBeams: true,
2909
3028
  checkTuplets: true,
@@ -2928,6 +3047,14 @@ function validateMeasureLocal(measure, context, options = {}) {
2928
3047
  opts.durationTolerance
2929
3048
  ));
2930
3049
  }
3050
+ if (opts.checkMeasureFullness && context.time) {
3051
+ errors.push(...validateMeasureFullness(
3052
+ measure,
3053
+ context.divisions,
3054
+ context.time,
3055
+ location
3056
+ ));
3057
+ }
2931
3058
  if (opts.checkPosition) {
2932
3059
  errors.push(...validateBackupForward(measure, location));
2933
3060
  }
@@ -3494,7 +3621,11 @@ function serializeScorePart(part, indent) {
3494
3621
  lines.push(`${indent} <solo/>`);
3495
3622
  }
3496
3623
  if (inst.ensemble !== void 0) {
3497
- lines.push(`${indent} <ensemble>${inst.ensemble}</ensemble>`);
3624
+ if (inst.ensemble === 0) {
3625
+ lines.push(`${indent} <ensemble/>`);
3626
+ } else {
3627
+ lines.push(`${indent} <ensemble>${inst.ensemble}</ensemble>`);
3628
+ }
3498
3629
  }
3499
3630
  lines.push(`${indent} </score-instrument>`);
3500
3631
  }
@@ -3674,6 +3805,9 @@ function serializeAttributes(attrs, indent) {
3674
3805
  if (attrs.staves !== void 0) {
3675
3806
  lines.push(`${indent} <staves>${attrs.staves}</staves>`);
3676
3807
  }
3808
+ if (attrs.instruments !== void 0) {
3809
+ lines.push(`${indent} <instruments>${attrs.instruments}</instruments>`);
3810
+ }
3677
3811
  if (attrs.clef) {
3678
3812
  for (const clef of attrs.clef) {
3679
3813
  lines.push(...serializeClef(clef, indent + " "));
@@ -3762,8 +3896,10 @@ function serializeTime(time, indent) {
3762
3896
  }
3763
3897
  function serializeClef(clef, indent) {
3764
3898
  const lines = [];
3765
- const numberAttr = clef.staff ? ` number="${clef.staff}"` : "";
3766
- lines.push(`${indent}<clef${numberAttr}>`);
3899
+ let attrs = clef.staff ? ` number="${clef.staff}"` : "";
3900
+ if (clef.printObject === false) attrs += ' print-object="no"';
3901
+ if (clef.afterBarline) attrs += ' after-barline="yes"';
3902
+ lines.push(`${indent}<clef${attrs}>`);
3767
3903
  lines.push(`${indent} <sign>${clef.sign}</sign>`);
3768
3904
  lines.push(`${indent} <line>${clef.line}</line>`);
3769
3905
  if (clef.clefOctaveChange !== void 0) {
@@ -4012,6 +4148,7 @@ function serializeNotationsGroup(notations, indent) {
4012
4148
  if (notation.number !== void 0) attrs += ` number="${notation.number}"`;
4013
4149
  attrs += ` type="${notation.slurType}"`;
4014
4150
  if (notation.lineType) attrs += ` line-type="${notation.lineType}"`;
4151
+ if (notation.orientation) attrs += ` orientation="${notation.orientation}"`;
4015
4152
  if (notation.defaultX !== void 0) attrs += ` default-x="${notation.defaultX}"`;
4016
4153
  if (notation.defaultY !== void 0) attrs += ` default-y="${notation.defaultY}"`;
4017
4154
  if (notation.bezierX !== void 0) attrs += ` bezier-x="${notation.bezierX}"`;
@@ -4325,6 +4462,15 @@ function serializeDirection(direction, indent) {
4325
4462
  if (direction.sound.dynamics !== void 0) {
4326
4463
  attrs2.push(`dynamics="${direction.sound.dynamics}"`);
4327
4464
  }
4465
+ if (direction.sound.damperPedal) {
4466
+ attrs2.push(`damper-pedal="${direction.sound.damperPedal}"`);
4467
+ }
4468
+ if (direction.sound.softPedal) {
4469
+ attrs2.push(`soft-pedal="${direction.sound.softPedal}"`);
4470
+ }
4471
+ if (direction.sound.sostenutoPedal) {
4472
+ attrs2.push(`sostenuto-pedal="${direction.sound.sostenutoPedal}"`);
4473
+ }
4328
4474
  const attrStr = attrs2.length > 0 ? ` ${attrs2.join(" ")}` : "";
4329
4475
  if (direction.sound.midiInstrument) {
4330
4476
  lines.push(`${indent} <sound${attrStr}>`);
@@ -4429,6 +4575,8 @@ function serializeDirectionType(dirType, indent) {
4429
4575
  if (dirType.number !== void 0) bracketAttrs += ` number="${dirType.number}"`;
4430
4576
  if (dirType.lineEnd) bracketAttrs += ` line-end="${dirType.lineEnd}"`;
4431
4577
  if (dirType.lineType) bracketAttrs += ` line-type="${dirType.lineType}"`;
4578
+ if (dirType.defaultY !== void 0) bracketAttrs += ` default-y="${dirType.defaultY}"`;
4579
+ if (dirType.relativeX !== void 0) bracketAttrs += ` relative-x="${dirType.relativeX}"`;
4432
4580
  lines.push(`${indent} <bracket${bracketAttrs}/>`);
4433
4581
  break;
4434
4582
  }
@@ -4763,6 +4911,9 @@ function serializeSound(sound, indent) {
4763
4911
  if (sound.tocoda) attrs.push(`tocoda="${escapeXml(sound.tocoda)}"`);
4764
4912
  if (sound.fine) attrs.push('fine="yes"');
4765
4913
  if (sound.forwardRepeat) attrs.push('forward-repeat="yes"');
4914
+ if (sound.damperPedal) attrs.push(`damper-pedal="${sound.damperPedal === true ? "yes" : sound.damperPedal}"`);
4915
+ if (sound.softPedal) attrs.push(`soft-pedal="${sound.softPedal === true ? "yes" : sound.softPedal}"`);
4916
+ if (sound.sostenutoPedal) attrs.push(`sostenuto-pedal="${sound.sostenutoPedal === true ? "yes" : sound.sostenutoPedal}"`);
4766
4917
  const attrStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
4767
4918
  if (sound.swing) {
4768
4919
  lines.push(`${indent}<sound${attrStr}>`);
@@ -5355,178 +5506,1381 @@ function getNormalizedDuration(note, options) {
5355
5506
  const currentDivisions = options.currentDivisions ?? 1;
5356
5507
  return note.duration * options.baseDivisions / currentDivisions;
5357
5508
  }
5358
-
5359
- // src/query/index.ts
5360
- function getMeasure(score, options) {
5361
- const part = score.parts[options.part];
5362
- if (!part) return void 0;
5363
- const targetMeasure = String(options.measure);
5364
- return part.measures.find((m) => m.number === targetMeasure);
5365
- }
5366
- function getMeasureByIndex(score, options) {
5367
- const part = score.parts[options.part];
5368
- if (!part) return void 0;
5369
- return part.measures[options.measureIndex];
5370
- }
5371
- function getMeasureCount(score) {
5372
- if (score.parts.length === 0) return 0;
5373
- return score.parts[0].measures.length;
5374
- }
5375
- function getDivisions(score, options) {
5376
- const part = score.parts[options.part];
5377
- if (!part) return 1;
5378
- const targetMeasure = parseInt(String(options.measure), 10);
5379
- if (isNaN(targetMeasure)) return 1;
5380
- for (let i = 0; i < part.measures.length; i++) {
5381
- const m = part.measures[i];
5382
- const mNum = parseInt(m.number, 10);
5383
- if (!isNaN(mNum) && mNum > targetMeasure) break;
5384
- if (m.attributes?.divisions !== void 0) {
5509
+ function getEntriesForStaff(measure, staff) {
5510
+ return measure.entries.filter((entry) => {
5511
+ if (entry.type === "note") {
5512
+ return (entry.staff ?? 1) === staff;
5385
5513
  }
5386
- }
5387
- let divisions = 1;
5388
- for (const m of part.measures) {
5389
- const mNum = parseInt(m.number, 10);
5390
- if (!isNaN(mNum) && mNum > targetMeasure) break;
5391
- if (m.attributes?.divisions !== void 0) {
5392
- divisions = m.attributes.divisions;
5514
+ if (entry.type === "forward") {
5515
+ return (entry.staff ?? 1) === staff;
5393
5516
  }
5394
- }
5395
- return divisions;
5517
+ if (entry.type === "direction") {
5518
+ return (entry.staff ?? 1) === staff;
5519
+ }
5520
+ if (entry.type === "backup") {
5521
+ return false;
5522
+ }
5523
+ return false;
5524
+ });
5396
5525
  }
5397
- function getAttributesAtMeasure(score, options) {
5398
- const part = score.parts[options.part];
5399
- if (!part) return {};
5400
- const targetMeasure = parseInt(String(options.measure), 10);
5401
- const result = {};
5402
- for (const m of part.measures) {
5403
- const mNum = parseInt(m.number, 10);
5404
- if (!isNaN(targetMeasure) && !isNaN(mNum) && mNum > targetMeasure) break;
5405
- if (m.attributes) {
5406
- if (m.attributes.divisions !== void 0) result.divisions = m.attributes.divisions;
5407
- if (m.attributes.time !== void 0) result.time = m.attributes.time;
5408
- if (m.attributes.key !== void 0) result.key = m.attributes.key;
5409
- if (m.attributes.clef !== void 0) result.clef = m.attributes.clef;
5410
- if (m.attributes.staves !== void 0) result.staves = m.attributes.staves;
5411
- if (m.attributes.transpose !== void 0) result.transpose = m.attributes.transpose;
5526
+ function buildVoiceToStaffMap(measure) {
5527
+ const map = /* @__PURE__ */ new Map();
5528
+ for (const entry of measure.entries) {
5529
+ if (entry.type === "note" && entry.staff !== void 0) {
5530
+ const voice = entry.voice;
5531
+ const staff = entry.staff;
5532
+ if (!map.has(voice)) {
5533
+ map.set(voice, staff);
5534
+ }
5412
5535
  }
5413
5536
  }
5414
- return result;
5537
+ return {
5538
+ get: (voice) => map.get(voice),
5539
+ has: (voice) => map.has(voice),
5540
+ entries: () => map.entries(),
5541
+ size: map.size
5542
+ };
5415
5543
  }
5416
- function findNotes(score, filter) {
5417
- const results = [];
5418
- for (const part of score.parts) {
5419
- for (const measure of part.measures) {
5420
- for (const entry of measure.entries) {
5421
- if (entry.type !== "note") continue;
5422
- if (filter.pitchRange && entry.pitch) {
5423
- const noteValue = pitchToSemitone(entry.pitch);
5424
- if (filter.pitchRange.min) {
5425
- const minValue = pitchToSemitone(filter.pitchRange.min);
5426
- if (noteValue < minValue) continue;
5427
- }
5428
- if (filter.pitchRange.max) {
5429
- const maxValue = pitchToSemitone(filter.pitchRange.max);
5430
- if (noteValue > maxValue) continue;
5431
- }
5432
- }
5433
- if (filter.voice !== void 0 && entry.voice !== filter.voice) continue;
5434
- if (filter.staff !== void 0 && (entry.staff ?? 1) !== filter.staff) continue;
5435
- if (filter.noteType !== void 0 && entry.noteType !== filter.noteType) continue;
5436
- if (filter.hasTie !== void 0) {
5437
- const hasTie = entry.tie !== void 0;
5438
- if (filter.hasTie !== hasTie) continue;
5544
+ function buildVoiceToStaffMapForPart(part) {
5545
+ const map = /* @__PURE__ */ new Map();
5546
+ for (const measure of part.measures) {
5547
+ for (const entry of measure.entries) {
5548
+ if (entry.type === "note" && entry.staff !== void 0) {
5549
+ const voice = entry.voice;
5550
+ const staff = entry.staff;
5551
+ if (!map.has(voice)) {
5552
+ map.set(voice, staff);
5439
5553
  }
5440
- results.push(entry);
5441
5554
  }
5442
5555
  }
5443
5556
  }
5444
- return results;
5557
+ return {
5558
+ get: (voice) => map.get(voice),
5559
+ has: (voice) => map.has(voice),
5560
+ entries: () => map.entries(),
5561
+ size: map.size
5562
+ };
5445
5563
  }
5446
- function getDuration(score) {
5447
- if (score.parts.length === 0) return 0;
5448
- const part = score.parts[0];
5449
- let totalDuration = 0;
5450
- let divisions = 1;
5451
- for (const measure of part.measures) {
5452
- if (measure.attributes?.divisions !== void 0) {
5453
- divisions = measure.attributes.divisions;
5454
- }
5455
- let measureDuration = 0;
5564
+ function inferStaff(entry, voiceToStaffMap) {
5565
+ if (entry.staff !== void 0) {
5566
+ return entry.staff;
5567
+ }
5568
+ const inferredStaff = voiceToStaffMap.get(entry.voice);
5569
+ if (inferredStaff !== void 0) {
5570
+ return inferredStaff;
5571
+ }
5572
+ return 1;
5573
+ }
5574
+ function getEffectiveStaff(entry, measure) {
5575
+ if (entry.staff !== void 0) {
5576
+ return entry.staff;
5577
+ }
5578
+ const map = buildVoiceToStaffMap(measure);
5579
+ return inferStaff(entry, map);
5580
+ }
5581
+ function getClefForStaff(score, options) {
5582
+ const part = score.parts[options.partIndex];
5583
+ if (!part) return void 0;
5584
+ for (let i = options.measureIndex; i >= 0; i--) {
5585
+ const measure = part.measures[i];
5456
5586
  for (const entry of measure.entries) {
5457
- if (entry.type === "note" && !entry.chord) {
5458
- measureDuration = Math.max(measureDuration, entry.duration);
5587
+ if (entry.type === "attributes" && entry.attributes.clef) {
5588
+ for (const clef of entry.attributes.clef) {
5589
+ if ((clef.staff ?? 1) === options.staff) {
5590
+ return clef;
5591
+ }
5592
+ }
5459
5593
  }
5460
5594
  }
5461
- const attrs = getAttributesAtMeasure(score, { part: 0, measure: measure.number });
5462
- if (attrs.time) {
5463
- const beats = parseInt(attrs.time.beats, 10) || 4;
5464
- const expectedDuration = beats / attrs.time.beatType * 4 * divisions;
5465
- measureDuration = Math.max(measureDuration, expectedDuration);
5595
+ if (measure.attributes?.clef) {
5596
+ for (const clef of measure.attributes.clef) {
5597
+ if ((clef.staff ?? 1) === options.staff) {
5598
+ return clef;
5599
+ }
5600
+ }
5466
5601
  }
5467
- totalDuration += measureDuration;
5468
5602
  }
5469
- return totalDuration;
5470
- }
5471
- function getPartById(score, id) {
5472
- return score.parts.find((p) => p.id === id);
5473
- }
5474
- function getPartIndex(score, id) {
5475
- return score.parts.findIndex((p) => p.id === id);
5603
+ return void 0;
5476
5604
  }
5477
- function hasMultipleStaves(score, partIndex = 0) {
5478
- const part = score.parts[partIndex];
5479
- if (!part) return false;
5480
- for (const measure of part.measures) {
5481
- if (measure.attributes?.staves !== void 0 && measure.attributes.staves > 1) {
5482
- return true;
5605
+ function getVoicesForStaff(measure, staff) {
5606
+ const voices = /* @__PURE__ */ new Set();
5607
+ for (const entry of measure.entries) {
5608
+ if (entry.type === "note") {
5609
+ const entryStaff = entry.staff ?? 1;
5610
+ if (entryStaff === staff) {
5611
+ voices.add(entry.voice);
5612
+ }
5483
5613
  }
5484
5614
  }
5485
- return false;
5615
+ return Array.from(voices).sort((a, b) => a - b);
5486
5616
  }
5487
- function getStaveCount(score, partIndex = 0) {
5617
+ function getStaffRange(score, partIndex) {
5488
5618
  const part = score.parts[partIndex];
5489
- if (!part) return 1;
5619
+ if (!part) return { min: 1, max: 1 };
5620
+ let min = 1;
5621
+ let max = 1;
5490
5622
  for (const measure of part.measures) {
5491
5623
  if (measure.attributes?.staves !== void 0) {
5492
- return measure.attributes.staves;
5624
+ max = Math.max(max, measure.attributes.staves);
5625
+ }
5626
+ for (const entry of measure.entries) {
5627
+ if (entry.type === "note" && entry.staff !== void 0) {
5628
+ max = Math.max(max, entry.staff);
5629
+ }
5493
5630
  }
5494
5631
  }
5495
- return 1;
5496
- }
5497
- function measureRoundtrip(original, exported) {
5498
- const notesOriginal = countNotes(original);
5499
- const notesExported = countNotes(exported);
5500
- const notesPreserved = Math.min(notesOriginal, notesExported);
5501
- const measuresOriginal = getMeasureCount(original);
5502
- const measuresExported = getMeasureCount(exported);
5503
- const measuresPreserved = Math.min(measuresOriginal, measuresExported);
5504
- const partsOriginal = original.parts.length;
5505
- const partsExported = exported.parts.length;
5506
- const partsPreserved = Math.min(partsOriginal, partsExported);
5507
- const preservationRate = notesOriginal > 0 ? notesPreserved / notesOriginal : 1;
5508
- return {
5509
- notesOriginal,
5510
- notesPreserved,
5511
- measuresOriginal,
5512
- measuresPreserved,
5513
- partsOriginal,
5514
- partsPreserved,
5515
- preservationRate
5516
- };
5632
+ return { min, max };
5517
5633
  }
5518
- function countNotes(score) {
5519
- let count = 0;
5520
- for (const part of score.parts) {
5521
- for (const measure of part.measures) {
5522
- for (const entry of measure.entries) {
5523
- if (entry.type === "note") {
5524
- count++;
5634
+ function getEntriesAtPosition(measure, position, options) {
5635
+ const result = [];
5636
+ const state = createPositionState();
5637
+ for (const entry of measure.entries) {
5638
+ const currentPosition = entry.type === "note" && entry.chord ? state.lastNonChordPosition : state.position;
5639
+ if (currentPosition === position) {
5640
+ if (entry.type === "note") {
5641
+ if (options?.staff !== void 0 && (entry.staff ?? 1) !== options.staff) {
5642
+ updatePositionForEntry(state, entry);
5643
+ continue;
5644
+ }
5645
+ if (options?.voice !== void 0 && entry.voice !== options.voice) {
5646
+ updatePositionForEntry(state, entry);
5647
+ continue;
5648
+ }
5649
+ if (options?.includeChordNotes === false && entry.chord) {
5650
+ updatePositionForEntry(state, entry);
5651
+ continue;
5525
5652
  }
5526
5653
  }
5654
+ result.push(entry);
5527
5655
  }
5656
+ updatePositionForEntry(state, entry);
5528
5657
  }
5529
- return count;
5658
+ return result;
5659
+ }
5660
+ function getNotesAtPosition(measure, position, options) {
5661
+ return getEntriesAtPosition(measure, position, options).filter(
5662
+ (entry) => entry.type === "note"
5663
+ );
5664
+ }
5665
+ function getEntriesInRange(measure, range, options) {
5666
+ const result = [];
5667
+ const state = createPositionState();
5668
+ for (const entry of measure.entries) {
5669
+ const currentPosition = entry.type === "note" && entry.chord ? state.lastNonChordPosition : state.position;
5670
+ if (currentPosition >= range.start && currentPosition < range.end) {
5671
+ if (entry.type === "note") {
5672
+ if (options?.staff !== void 0 && (entry.staff ?? 1) !== options.staff) {
5673
+ updatePositionForEntry(state, entry);
5674
+ continue;
5675
+ }
5676
+ if (options?.voice !== void 0 && entry.voice !== options.voice) {
5677
+ updatePositionForEntry(state, entry);
5678
+ continue;
5679
+ }
5680
+ if (options?.includeChordNotes === false && entry.chord) {
5681
+ updatePositionForEntry(state, entry);
5682
+ continue;
5683
+ }
5684
+ }
5685
+ result.push(entry);
5686
+ }
5687
+ updatePositionForEntry(state, entry);
5688
+ }
5689
+ return result;
5690
+ }
5691
+ function getNotesInRange(measure, range, options) {
5692
+ return getEntriesInRange(measure, range, options).filter(
5693
+ (entry) => entry.type === "note"
5694
+ );
5695
+ }
5696
+ function getVerticalSlice(score, options) {
5697
+ const parts = /* @__PURE__ */ new Map();
5698
+ for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
5699
+ const part = score.parts[partIndex];
5700
+ const measure = part.measures[options.measureIndex];
5701
+ if (!measure) continue;
5702
+ const notes = getNotesAtPosition(measure, options.position);
5703
+ if (notes.length > 0) {
5704
+ parts.set(partIndex, notes);
5705
+ }
5706
+ }
5707
+ return {
5708
+ measureIndex: options.measureIndex,
5709
+ position: options.position,
5710
+ parts
5711
+ };
5712
+ }
5713
+ function getVoiceLine(score, options) {
5714
+ const part = score.parts[options.partIndex];
5715
+ if (!part) {
5716
+ return { partIndex: options.partIndex, voice: options.voice, staff: options.staff, notes: [] };
5717
+ }
5718
+ const notes = [];
5719
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
5720
+ const measure = part.measures[measureIndex];
5721
+ const state = createPositionState();
5722
+ for (const entry of measure.entries) {
5723
+ if (entry.type === "note") {
5724
+ const entryStaff = entry.staff ?? 1;
5725
+ const matchesVoice = entry.voice === options.voice;
5726
+ const matchesStaff = options.staff === void 0 || entryStaff === options.staff;
5727
+ if (matchesVoice && matchesStaff) {
5728
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
5729
+ notes.push({
5730
+ note: entry,
5731
+ part,
5732
+ partIndex: options.partIndex,
5733
+ measure,
5734
+ measureIndex,
5735
+ position
5736
+ });
5737
+ }
5738
+ }
5739
+ updatePositionForEntry(state, entry);
5740
+ }
5741
+ }
5742
+ return {
5743
+ partIndex: options.partIndex,
5744
+ voice: options.voice,
5745
+ staff: options.staff,
5746
+ notes
5747
+ };
5748
+ }
5749
+ function getVoiceLineInRange(score, options) {
5750
+ const part = score.parts[options.partIndex];
5751
+ if (!part) {
5752
+ return { partIndex: options.partIndex, voice: options.voice, staff: options.staff, notes: [] };
5753
+ }
5754
+ const notes = [];
5755
+ for (let measureIndex = options.startMeasure; measureIndex <= options.endMeasure && measureIndex < part.measures.length; measureIndex++) {
5756
+ const measure = part.measures[measureIndex];
5757
+ const state = createPositionState();
5758
+ for (const entry of measure.entries) {
5759
+ if (entry.type === "note") {
5760
+ const entryStaff = entry.staff ?? 1;
5761
+ const matchesVoice = entry.voice === options.voice;
5762
+ const matchesStaff = options.staff === void 0 || entryStaff === options.staff;
5763
+ if (matchesVoice && matchesStaff) {
5764
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
5765
+ notes.push({
5766
+ note: entry,
5767
+ part,
5768
+ partIndex: options.partIndex,
5769
+ measure,
5770
+ measureIndex,
5771
+ position
5772
+ });
5773
+ }
5774
+ }
5775
+ updatePositionForEntry(state, entry);
5776
+ }
5777
+ }
5778
+ return {
5779
+ partIndex: options.partIndex,
5780
+ voice: options.voice,
5781
+ staff: options.staff,
5782
+ notes
5783
+ };
5784
+ }
5785
+ function* iterateEntries(score) {
5786
+ for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
5787
+ const part = score.parts[partIndex];
5788
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
5789
+ const measure = part.measures[measureIndex];
5790
+ const state = createPositionState();
5791
+ for (const entry of measure.entries) {
5792
+ const position = entry.type === "note" && entry.chord ? state.lastNonChordPosition : state.position;
5793
+ yield {
5794
+ entry,
5795
+ part,
5796
+ partIndex,
5797
+ measure,
5798
+ measureIndex,
5799
+ position
5800
+ };
5801
+ updatePositionForEntry(state, entry);
5802
+ }
5803
+ }
5804
+ }
5805
+ }
5806
+ function getNextNote(score, context) {
5807
+ const part = score.parts[context.partIndex];
5808
+ if (!part) return null;
5809
+ let foundCurrent = false;
5810
+ for (let measureIndex = context.measureIndex; measureIndex < part.measures.length; measureIndex++) {
5811
+ const measure = part.measures[measureIndex];
5812
+ const state = createPositionState();
5813
+ for (const entry of measure.entries) {
5814
+ if (entry.type === "note") {
5815
+ const entryPosition = entry.chord ? state.lastNonChordPosition : state.position;
5816
+ if (measureIndex === context.measureIndex && entry === context.note) {
5817
+ foundCurrent = true;
5818
+ updatePositionForEntry(state, entry);
5819
+ continue;
5820
+ }
5821
+ if (foundCurrent && entry.voice === context.note.voice && !entry.chord) {
5822
+ if (context.note.staff !== void 0) {
5823
+ if ((entry.staff ?? 1) !== context.note.staff) {
5824
+ updatePositionForEntry(state, entry);
5825
+ continue;
5826
+ }
5827
+ }
5828
+ return {
5829
+ note: entry,
5830
+ part,
5831
+ partIndex: context.partIndex,
5832
+ measure,
5833
+ measureIndex,
5834
+ position: entryPosition
5835
+ };
5836
+ }
5837
+ }
5838
+ updatePositionForEntry(state, entry);
5839
+ }
5840
+ }
5841
+ return null;
5842
+ }
5843
+ function getPrevNote(score, context) {
5844
+ const part = score.parts[context.partIndex];
5845
+ if (!part) return null;
5846
+ let lastCandidate = null;
5847
+ for (let measureIndex = 0; measureIndex <= context.measureIndex; measureIndex++) {
5848
+ const measure = part.measures[measureIndex];
5849
+ const state = createPositionState();
5850
+ for (const entry of measure.entries) {
5851
+ if (entry.type === "note") {
5852
+ const entryPosition = entry.chord ? state.lastNonChordPosition : state.position;
5853
+ if (measureIndex === context.measureIndex && entry === context.note) {
5854
+ return lastCandidate;
5855
+ }
5856
+ if (entry.voice === context.note.voice && !entry.chord) {
5857
+ if (context.note.staff !== void 0) {
5858
+ if ((entry.staff ?? 1) !== context.note.staff) {
5859
+ updatePositionForEntry(state, entry);
5860
+ continue;
5861
+ }
5862
+ }
5863
+ lastCandidate = {
5864
+ note: entry,
5865
+ part,
5866
+ partIndex: context.partIndex,
5867
+ measure,
5868
+ measureIndex,
5869
+ position: entryPosition
5870
+ };
5871
+ }
5872
+ }
5873
+ updatePositionForEntry(state, entry);
5874
+ }
5875
+ }
5876
+ return null;
5877
+ }
5878
+ function getAdjacentNotes(score, context) {
5879
+ return {
5880
+ prev: getPrevNote(score, context),
5881
+ next: getNextNote(score, context)
5882
+ };
5883
+ }
5884
+ function getDirections(score, options) {
5885
+ const results = [];
5886
+ const startPart = options?.partIndex ?? 0;
5887
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
5888
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
5889
+ const part = score.parts[partIndex];
5890
+ if (!part) continue;
5891
+ const startMeasure = options?.measureIndex ?? 0;
5892
+ const endMeasure = options?.measureIndex !== void 0 ? options.measureIndex + 1 : part.measures.length;
5893
+ for (let measureIndex = startMeasure; measureIndex < endMeasure; measureIndex++) {
5894
+ const measure = part.measures[measureIndex];
5895
+ if (!measure) continue;
5896
+ const state = createPositionState();
5897
+ for (const entry of measure.entries) {
5898
+ if (entry.type === "direction") {
5899
+ results.push({
5900
+ direction: entry,
5901
+ part,
5902
+ partIndex,
5903
+ measure,
5904
+ measureIndex,
5905
+ position: state.position
5906
+ });
5907
+ }
5908
+ updatePositionForEntry(state, entry);
5909
+ }
5910
+ }
5911
+ }
5912
+ return results;
5913
+ }
5914
+ function getDirectionsAtPosition(measure, position) {
5915
+ const results = [];
5916
+ const state = createPositionState();
5917
+ for (const entry of measure.entries) {
5918
+ if (entry.type === "direction" && state.position === position) {
5919
+ results.push(entry);
5920
+ }
5921
+ updatePositionForEntry(state, entry);
5922
+ }
5923
+ return results;
5924
+ }
5925
+ function findDirectionsByType(score, kind) {
5926
+ const allDirections = getDirections(score);
5927
+ return allDirections.filter(
5928
+ (d) => d.direction.directionTypes.some((dt) => dt.kind === kind)
5929
+ );
5930
+ }
5931
+ function getDynamics(score, options) {
5932
+ const results = [];
5933
+ const startPart = options?.partIndex ?? 0;
5934
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
5935
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
5936
+ const part = score.parts[partIndex];
5937
+ if (!part) continue;
5938
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
5939
+ const measure = part.measures[measureIndex];
5940
+ const state = createPositionState();
5941
+ for (const entry of measure.entries) {
5942
+ if (entry.type === "direction") {
5943
+ for (const dirType of entry.directionTypes) {
5944
+ if (dirType.kind === "dynamics") {
5945
+ results.push({
5946
+ dynamic: dirType.value,
5947
+ direction: entry,
5948
+ part,
5949
+ partIndex,
5950
+ measure,
5951
+ measureIndex,
5952
+ position: state.position
5953
+ });
5954
+ }
5955
+ }
5956
+ }
5957
+ updatePositionForEntry(state, entry);
5958
+ }
5959
+ }
5960
+ }
5961
+ return results;
5962
+ }
5963
+ function getTempoMarkings(score) {
5964
+ const results = [];
5965
+ for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
5966
+ const part = score.parts[partIndex];
5967
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
5968
+ const measure = part.measures[measureIndex];
5969
+ const state = createPositionState();
5970
+ for (const entry of measure.entries) {
5971
+ if (entry.type === "direction") {
5972
+ for (const dirType of entry.directionTypes) {
5973
+ if (dirType.kind === "metronome") {
5974
+ results.push({
5975
+ beatUnit: dirType.beatUnit,
5976
+ perMinute: dirType.perMinute,
5977
+ beatUnitDot: dirType.beatUnitDot,
5978
+ direction: entry,
5979
+ partIndex,
5980
+ measureIndex,
5981
+ position: state.position
5982
+ });
5983
+ }
5984
+ }
5985
+ }
5986
+ updatePositionForEntry(state, entry);
5987
+ }
5988
+ }
5989
+ }
5990
+ return results;
5991
+ }
5992
+ function getPedalMarkings(score, options) {
5993
+ const results = [];
5994
+ const startPart = options?.partIndex ?? 0;
5995
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
5996
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
5997
+ const part = score.parts[partIndex];
5998
+ if (!part) continue;
5999
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6000
+ const measure = part.measures[measureIndex];
6001
+ const state = createPositionState();
6002
+ for (const entry of measure.entries) {
6003
+ if (entry.type === "direction") {
6004
+ for (const dirType of entry.directionTypes) {
6005
+ if (dirType.kind === "pedal") {
6006
+ results.push({
6007
+ pedalType: dirType.type,
6008
+ direction: entry,
6009
+ partIndex,
6010
+ measureIndex,
6011
+ position: state.position
6012
+ });
6013
+ }
6014
+ }
6015
+ }
6016
+ updatePositionForEntry(state, entry);
6017
+ }
6018
+ }
6019
+ }
6020
+ return results;
6021
+ }
6022
+ function getWedges(score, options) {
6023
+ const results = [];
6024
+ const startPart = options?.partIndex ?? 0;
6025
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6026
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6027
+ const part = score.parts[partIndex];
6028
+ if (!part) continue;
6029
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6030
+ const measure = part.measures[measureIndex];
6031
+ const state = createPositionState();
6032
+ for (const entry of measure.entries) {
6033
+ if (entry.type === "direction") {
6034
+ for (const dirType of entry.directionTypes) {
6035
+ if (dirType.kind === "wedge") {
6036
+ results.push({
6037
+ wedgeType: dirType.type,
6038
+ direction: entry,
6039
+ partIndex,
6040
+ measureIndex,
6041
+ position: state.position
6042
+ });
6043
+ }
6044
+ }
6045
+ }
6046
+ updatePositionForEntry(state, entry);
6047
+ }
6048
+ }
6049
+ }
6050
+ return results;
6051
+ }
6052
+ function getOctaveShifts(score, options) {
6053
+ const results = [];
6054
+ const startPart = options?.partIndex ?? 0;
6055
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6056
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6057
+ const part = score.parts[partIndex];
6058
+ if (!part) continue;
6059
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6060
+ const measure = part.measures[measureIndex];
6061
+ const state = createPositionState();
6062
+ for (const entry of measure.entries) {
6063
+ if (entry.type === "direction") {
6064
+ for (const dirType of entry.directionTypes) {
6065
+ if (dirType.kind === "octave-shift") {
6066
+ results.push({
6067
+ shiftType: dirType.type,
6068
+ size: dirType.size,
6069
+ direction: entry,
6070
+ partIndex,
6071
+ measureIndex,
6072
+ position: state.position
6073
+ });
6074
+ }
6075
+ }
6076
+ }
6077
+ updatePositionForEntry(state, entry);
6078
+ }
6079
+ }
6080
+ }
6081
+ return results;
6082
+ }
6083
+ function getTiedNoteGroups(score, options) {
6084
+ const results = [];
6085
+ const startPart = options?.partIndex ?? 0;
6086
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6087
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6088
+ const part = score.parts[partIndex];
6089
+ if (!part) continue;
6090
+ const pendingTies = /* @__PURE__ */ new Map();
6091
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6092
+ const measure = part.measures[measureIndex];
6093
+ const state = createPositionState();
6094
+ for (const entry of measure.entries) {
6095
+ if (entry.type === "note" && entry.pitch) {
6096
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6097
+ const pitchKey = `${entry.pitch.step}${entry.pitch.octave}-${entry.voice}`;
6098
+ const context = {
6099
+ note: entry,
6100
+ part,
6101
+ partIndex,
6102
+ measure,
6103
+ measureIndex,
6104
+ position
6105
+ };
6106
+ const hasTieStart = entry.tie?.type === "start" || entry.ties?.some((t) => t.type === "start") || entry.notations?.some((n) => n.type === "tied" && n.tiedType === "start");
6107
+ const hasTieStop = entry.tie?.type === "stop" || entry.ties?.some((t) => t.type === "stop") || entry.notations?.some((n) => n.type === "tied" && n.tiedType === "stop");
6108
+ if (hasTieStop && pendingTies.has(pitchKey)) {
6109
+ const group = pendingTies.get(pitchKey);
6110
+ group.push(context);
6111
+ if (!hasTieStart) {
6112
+ const totalDuration = group.reduce((sum, nc) => sum + nc.note.duration, 0);
6113
+ results.push({ notes: group, totalDuration });
6114
+ pendingTies.delete(pitchKey);
6115
+ }
6116
+ } else if (hasTieStart) {
6117
+ pendingTies.set(pitchKey, [context]);
6118
+ }
6119
+ }
6120
+ updatePositionForEntry(state, entry);
6121
+ }
6122
+ }
6123
+ }
6124
+ return results;
6125
+ }
6126
+ function getSlurSpans(score, options) {
6127
+ const results = [];
6128
+ const startPart = options?.partIndex ?? 0;
6129
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6130
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6131
+ const part = score.parts[partIndex];
6132
+ if (!part) continue;
6133
+ const pendingSlurs = /* @__PURE__ */ new Map();
6134
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6135
+ const measure = part.measures[measureIndex];
6136
+ const state = createPositionState();
6137
+ for (const entry of measure.entries) {
6138
+ if (entry.type === "note") {
6139
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6140
+ const context = {
6141
+ note: entry,
6142
+ part,
6143
+ partIndex,
6144
+ measure,
6145
+ measureIndex,
6146
+ position
6147
+ };
6148
+ if (entry.notations) {
6149
+ for (const notation of entry.notations) {
6150
+ if (notation.type === "slur") {
6151
+ const slurNumber = notation.number ?? 1;
6152
+ if (notation.slurType === "start") {
6153
+ pendingSlurs.set(slurNumber, { startNote: context, notes: [context] });
6154
+ } else if (notation.slurType === "stop" && pendingSlurs.has(slurNumber)) {
6155
+ const pending = pendingSlurs.get(slurNumber);
6156
+ pending.notes.push(context);
6157
+ results.push({
6158
+ number: slurNumber,
6159
+ startNote: pending.startNote,
6160
+ endNote: context,
6161
+ notes: pending.notes
6162
+ });
6163
+ pendingSlurs.delete(slurNumber);
6164
+ } else if (notation.slurType === "continue" && pendingSlurs.has(slurNumber)) {
6165
+ pendingSlurs.get(slurNumber).notes.push(context);
6166
+ }
6167
+ }
6168
+ }
6169
+ }
6170
+ for (const [, pending] of pendingSlurs) {
6171
+ const lastNote = pending.notes[pending.notes.length - 1];
6172
+ if (lastNote !== context) {
6173
+ pending.notes.push(context);
6174
+ }
6175
+ }
6176
+ }
6177
+ updatePositionForEntry(state, entry);
6178
+ }
6179
+ }
6180
+ }
6181
+ return results;
6182
+ }
6183
+ function getTupletGroups(score, options) {
6184
+ const results = [];
6185
+ const startPart = options?.partIndex ?? 0;
6186
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6187
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6188
+ const part = score.parts[partIndex];
6189
+ if (!part) continue;
6190
+ const pendingTuplets = /* @__PURE__ */ new Map();
6191
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6192
+ const measure = part.measures[measureIndex];
6193
+ const state = createPositionState();
6194
+ for (const entry of measure.entries) {
6195
+ if (entry.type === "note") {
6196
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6197
+ const context = {
6198
+ note: entry,
6199
+ part,
6200
+ partIndex,
6201
+ measure,
6202
+ measureIndex,
6203
+ position
6204
+ };
6205
+ if (entry.notations) {
6206
+ for (const notation of entry.notations) {
6207
+ if (notation.type === "tuplet") {
6208
+ const tupletNumber = notation.number ?? 1;
6209
+ if (notation.tupletType === "start") {
6210
+ const actualNotes = entry.timeModification?.actualNotes ?? 3;
6211
+ const normalNotes = entry.timeModification?.normalNotes ?? 2;
6212
+ pendingTuplets.set(tupletNumber, {
6213
+ notes: [context],
6214
+ actualNotes,
6215
+ normalNotes
6216
+ });
6217
+ } else if (notation.tupletType === "stop" && pendingTuplets.has(tupletNumber)) {
6218
+ const pending = pendingTuplets.get(tupletNumber);
6219
+ pending.notes.push(context);
6220
+ results.push({
6221
+ number: tupletNumber,
6222
+ notes: pending.notes,
6223
+ actualNotes: pending.actualNotes,
6224
+ normalNotes: pending.normalNotes
6225
+ });
6226
+ pendingTuplets.delete(tupletNumber);
6227
+ }
6228
+ }
6229
+ }
6230
+ }
6231
+ if (entry.timeModification && !entry.notations?.some((n) => n.type === "tuplet")) {
6232
+ for (const [, pending] of pendingTuplets) {
6233
+ if (entry.timeModification.actualNotes === pending.actualNotes && entry.timeModification.normalNotes === pending.normalNotes) {
6234
+ pending.notes.push(context);
6235
+ }
6236
+ }
6237
+ }
6238
+ }
6239
+ updatePositionForEntry(state, entry);
6240
+ }
6241
+ }
6242
+ }
6243
+ return results;
6244
+ }
6245
+ function getBeamGroups(measure) {
6246
+ const results = [];
6247
+ const state = createPositionState();
6248
+ const pendingBeams = /* @__PURE__ */ new Map();
6249
+ for (const entry of measure.entries) {
6250
+ if (entry.type === "note" && entry.beam) {
6251
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6252
+ const context = {
6253
+ note: entry,
6254
+ part: {},
6255
+ // Will be set by caller if needed
6256
+ partIndex: 0,
6257
+ measure,
6258
+ measureIndex: 0,
6259
+ position
6260
+ };
6261
+ for (const beam of entry.beam) {
6262
+ const beamNumber = beam.number;
6263
+ if (beam.type === "begin") {
6264
+ pendingBeams.set(beamNumber, [context]);
6265
+ } else if (beam.type === "continue" && pendingBeams.has(beamNumber)) {
6266
+ pendingBeams.get(beamNumber).push(context);
6267
+ } else if (beam.type === "end" && pendingBeams.has(beamNumber)) {
6268
+ const group = pendingBeams.get(beamNumber);
6269
+ group.push(context);
6270
+ results.push({
6271
+ notes: group,
6272
+ beamLevel: beamNumber
6273
+ });
6274
+ pendingBeams.delete(beamNumber);
6275
+ }
6276
+ }
6277
+ }
6278
+ updatePositionForEntry(state, entry);
6279
+ }
6280
+ return results;
6281
+ }
6282
+ function findNotesWithNotation(score, notationType, options) {
6283
+ const results = [];
6284
+ const startPart = options?.partIndex ?? 0;
6285
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6286
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6287
+ const part = score.parts[partIndex];
6288
+ if (!part) continue;
6289
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6290
+ const measure = part.measures[measureIndex];
6291
+ const state = createPositionState();
6292
+ for (const entry of measure.entries) {
6293
+ if (entry.type === "note") {
6294
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6295
+ if (entry.notations?.some((n) => n.type === notationType)) {
6296
+ results.push({
6297
+ note: entry,
6298
+ part,
6299
+ partIndex,
6300
+ measure,
6301
+ measureIndex,
6302
+ position
6303
+ });
6304
+ }
6305
+ }
6306
+ updatePositionForEntry(state, entry);
6307
+ }
6308
+ }
6309
+ }
6310
+ return results;
6311
+ }
6312
+ function getHarmonies(score, options) {
6313
+ const results = [];
6314
+ const startPart = options?.partIndex ?? 0;
6315
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6316
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6317
+ const part = score.parts[partIndex];
6318
+ if (!part) continue;
6319
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6320
+ const measure = part.measures[measureIndex];
6321
+ const state = createPositionState();
6322
+ for (const entry of measure.entries) {
6323
+ if (entry.type === "harmony") {
6324
+ results.push({
6325
+ harmony: entry,
6326
+ part,
6327
+ partIndex,
6328
+ measure,
6329
+ measureIndex,
6330
+ position: state.position
6331
+ });
6332
+ }
6333
+ updatePositionForEntry(state, entry);
6334
+ }
6335
+ }
6336
+ }
6337
+ return results;
6338
+ }
6339
+ function getHarmonyAtPosition(measure, position) {
6340
+ const state = createPositionState();
6341
+ let lastHarmony;
6342
+ for (const entry of measure.entries) {
6343
+ if (entry.type === "harmony") {
6344
+ if (state.position <= position) {
6345
+ lastHarmony = entry;
6346
+ }
6347
+ }
6348
+ updatePositionForEntry(state, entry);
6349
+ if (state.position > position && lastHarmony) {
6350
+ break;
6351
+ }
6352
+ }
6353
+ return lastHarmony;
6354
+ }
6355
+ function getChordProgression(score, options) {
6356
+ const harmonies = getHarmonies(score, options);
6357
+ return harmonies.map((h) => {
6358
+ const rootAlter = h.harmony.root.rootAlter ?? 0;
6359
+ const rootAccidental = rootAlter > 0 ? "#".repeat(rootAlter) : "b".repeat(-rootAlter);
6360
+ const root = h.harmony.root.rootStep + rootAccidental;
6361
+ let bass;
6362
+ if (h.harmony.bass) {
6363
+ const bassAlter = h.harmony.bass.bassAlter ?? 0;
6364
+ const bassAccidental = bassAlter > 0 ? "#".repeat(bassAlter) : "b".repeat(-bassAlter);
6365
+ bass = h.harmony.bass.bassStep + bassAccidental;
6366
+ }
6367
+ return {
6368
+ root,
6369
+ kind: h.harmony.kind,
6370
+ bass,
6371
+ measureIndex: h.measureIndex,
6372
+ position: h.position
6373
+ };
6374
+ });
6375
+ }
6376
+ function getLyrics(score, options) {
6377
+ const results = [];
6378
+ const startPart = options?.partIndex ?? 0;
6379
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6380
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6381
+ const part = score.parts[partIndex];
6382
+ if (!part) continue;
6383
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6384
+ const measure = part.measures[measureIndex];
6385
+ const state = createPositionState();
6386
+ for (const entry of measure.entries) {
6387
+ if (entry.type === "note" && entry.lyrics) {
6388
+ const position = entry.chord ? state.lastNonChordPosition : state.position;
6389
+ for (const lyric of entry.lyrics) {
6390
+ const verse = lyric.number ?? 1;
6391
+ if (options?.verse !== void 0 && verse !== options.verse) {
6392
+ continue;
6393
+ }
6394
+ results.push({
6395
+ lyric,
6396
+ note: entry,
6397
+ part,
6398
+ partIndex,
6399
+ measure,
6400
+ measureIndex,
6401
+ position,
6402
+ verse
6403
+ });
6404
+ }
6405
+ }
6406
+ updatePositionForEntry(state, entry);
6407
+ }
6408
+ }
6409
+ }
6410
+ return results;
6411
+ }
6412
+ function getLyricText(score, options) {
6413
+ const lyrics = getLyrics(score, options);
6414
+ const verseMap = /* @__PURE__ */ new Map();
6415
+ for (const lyric of lyrics) {
6416
+ if (!verseMap.has(lyric.verse)) {
6417
+ verseMap.set(lyric.verse, []);
6418
+ }
6419
+ verseMap.get(lyric.verse).push(lyric);
6420
+ }
6421
+ const results = [];
6422
+ for (const [verse, verseLyrics] of verseMap) {
6423
+ const syllables = [];
6424
+ const textParts = [];
6425
+ for (const lyric of verseLyrics) {
6426
+ const text = lyric.lyric.text;
6427
+ syllables.push({
6428
+ text,
6429
+ position: lyric.position,
6430
+ measureIndex: lyric.measureIndex
6431
+ });
6432
+ const syllabic = lyric.lyric.syllabic;
6433
+ if (syllabic === "begin" || syllabic === "middle") {
6434
+ textParts.push(text + "-");
6435
+ } else if (syllabic === "end") {
6436
+ textParts.push(text + " ");
6437
+ } else {
6438
+ textParts.push(text + " ");
6439
+ }
6440
+ }
6441
+ results.push({
6442
+ verse,
6443
+ text: textParts.join("").trim(),
6444
+ syllables
6445
+ });
6446
+ }
6447
+ return results.sort((a, b) => a.verse - b.verse);
6448
+ }
6449
+ function getVerseCount(score, options) {
6450
+ const verses = /* @__PURE__ */ new Set();
6451
+ const startPart = options?.partIndex ?? 0;
6452
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6453
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6454
+ const part = score.parts[partIndex];
6455
+ if (!part) continue;
6456
+ for (const measure of part.measures) {
6457
+ for (const entry of measure.entries) {
6458
+ if (entry.type === "note" && entry.lyrics) {
6459
+ for (const lyric of entry.lyrics) {
6460
+ verses.add(lyric.number ?? 1);
6461
+ }
6462
+ }
6463
+ }
6464
+ }
6465
+ }
6466
+ return verses.size;
6467
+ }
6468
+ function getRepeatStructure(score, options) {
6469
+ const results = [];
6470
+ const partIndex = options?.partIndex ?? 0;
6471
+ const part = score.parts[partIndex];
6472
+ if (!part) return results;
6473
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6474
+ const measure = part.measures[measureIndex];
6475
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6476
+ if (measure.barlines) {
6477
+ for (const barline of measure.barlines) {
6478
+ if (barline.repeat) {
6479
+ results.push({
6480
+ type: barline.repeat.direction,
6481
+ times: barline.repeat.times,
6482
+ measureIndex,
6483
+ measureNumber
6484
+ });
6485
+ }
6486
+ }
6487
+ }
6488
+ }
6489
+ return results;
6490
+ }
6491
+ function findBarlines(score, options) {
6492
+ const results = [];
6493
+ const startPart = options?.partIndex ?? 0;
6494
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6495
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6496
+ const part = score.parts[partIndex];
6497
+ if (!part) continue;
6498
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6499
+ const measure = part.measures[measureIndex];
6500
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6501
+ if (measure.barlines) {
6502
+ for (const barline of measure.barlines) {
6503
+ if (options?.style !== void 0 && barline.barStyle !== options.style) {
6504
+ continue;
6505
+ }
6506
+ if (options?.repeat !== void 0) {
6507
+ const hasRepeat = barline.repeat !== void 0;
6508
+ if (hasRepeat !== options.repeat) {
6509
+ continue;
6510
+ }
6511
+ }
6512
+ results.push({
6513
+ barline,
6514
+ partIndex,
6515
+ measureIndex,
6516
+ measureNumber
6517
+ });
6518
+ }
6519
+ }
6520
+ }
6521
+ }
6522
+ return results;
6523
+ }
6524
+ function getEndings(score, options) {
6525
+ const results = [];
6526
+ const startPart = options?.partIndex ?? 0;
6527
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6528
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6529
+ const part = score.parts[partIndex];
6530
+ if (!part) continue;
6531
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6532
+ const measure = part.measures[measureIndex];
6533
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6534
+ if (measure.barlines) {
6535
+ for (const barline of measure.barlines) {
6536
+ if (barline.ending) {
6537
+ results.push({
6538
+ number: barline.ending.number,
6539
+ type: barline.ending.type,
6540
+ partIndex,
6541
+ measureIndex,
6542
+ measureNumber
6543
+ });
6544
+ }
6545
+ }
6546
+ }
6547
+ }
6548
+ }
6549
+ return results;
6550
+ }
6551
+ function getKeyChanges(score, options) {
6552
+ const results = [];
6553
+ const startPart = options?.partIndex ?? 0;
6554
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6555
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6556
+ const part = score.parts[partIndex];
6557
+ if (!part) continue;
6558
+ let lastKey;
6559
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6560
+ const measure = part.measures[measureIndex];
6561
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6562
+ const state = createPositionState();
6563
+ if (measure.attributes?.key) {
6564
+ const key = measure.attributes.key;
6565
+ if (!lastKey || lastKey.fifths !== key.fifths || lastKey.mode !== key.mode) {
6566
+ results.push({
6567
+ key,
6568
+ partIndex,
6569
+ measureIndex,
6570
+ measureNumber,
6571
+ position: 0
6572
+ });
6573
+ lastKey = key;
6574
+ }
6575
+ }
6576
+ for (const entry of measure.entries) {
6577
+ if (entry.type === "attributes" && entry.attributes.key) {
6578
+ const key = entry.attributes.key;
6579
+ if (!lastKey || lastKey.fifths !== key.fifths || lastKey.mode !== key.mode) {
6580
+ results.push({
6581
+ key,
6582
+ partIndex,
6583
+ measureIndex,
6584
+ measureNumber,
6585
+ position: state.position
6586
+ });
6587
+ lastKey = key;
6588
+ }
6589
+ }
6590
+ updatePositionForEntry(state, entry);
6591
+ }
6592
+ }
6593
+ }
6594
+ return results;
6595
+ }
6596
+ function getTimeChanges(score, options) {
6597
+ const results = [];
6598
+ const startPart = options?.partIndex ?? 0;
6599
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6600
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6601
+ const part = score.parts[partIndex];
6602
+ if (!part) continue;
6603
+ let lastTime;
6604
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6605
+ const measure = part.measures[measureIndex];
6606
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6607
+ if (measure.attributes?.time) {
6608
+ const time = measure.attributes.time;
6609
+ if (!lastTime || lastTime.beats !== time.beats || lastTime.beatType !== time.beatType) {
6610
+ results.push({
6611
+ time,
6612
+ partIndex,
6613
+ measureIndex,
6614
+ measureNumber
6615
+ });
6616
+ lastTime = time;
6617
+ }
6618
+ }
6619
+ for (const entry of measure.entries) {
6620
+ if (entry.type === "attributes" && entry.attributes.time) {
6621
+ const time = entry.attributes.time;
6622
+ if (!lastTime || lastTime.beats !== time.beats || lastTime.beatType !== time.beatType) {
6623
+ results.push({
6624
+ time,
6625
+ partIndex,
6626
+ measureIndex,
6627
+ measureNumber
6628
+ });
6629
+ lastTime = time;
6630
+ }
6631
+ }
6632
+ }
6633
+ }
6634
+ }
6635
+ return results;
6636
+ }
6637
+ function getClefChanges(score, options) {
6638
+ const results = [];
6639
+ const startPart = options?.partIndex ?? 0;
6640
+ const endPart = options?.partIndex !== void 0 ? options.partIndex + 1 : score.parts.length;
6641
+ for (let partIndex = startPart; partIndex < endPart; partIndex++) {
6642
+ const part = score.parts[partIndex];
6643
+ if (!part) continue;
6644
+ const lastClefs = /* @__PURE__ */ new Map();
6645
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
6646
+ const measure = part.measures[measureIndex];
6647
+ const measureNumber = measure.number ?? String(measureIndex + 1);
6648
+ const state = createPositionState();
6649
+ if (measure.attributes?.clef) {
6650
+ for (const clef of measure.attributes.clef) {
6651
+ const staff = clef.staff ?? 1;
6652
+ if (options?.staff !== void 0 && staff !== options.staff) {
6653
+ continue;
6654
+ }
6655
+ const lastClef = lastClefs.get(staff);
6656
+ if (!lastClef || lastClef.sign !== clef.sign || lastClef.line !== clef.line || lastClef.clefOctaveChange !== clef.clefOctaveChange) {
6657
+ results.push({
6658
+ clef,
6659
+ staff,
6660
+ partIndex,
6661
+ measureIndex,
6662
+ measureNumber,
6663
+ position: 0
6664
+ });
6665
+ lastClefs.set(staff, clef);
6666
+ }
6667
+ }
6668
+ }
6669
+ for (const entry of measure.entries) {
6670
+ if (entry.type === "attributes" && entry.attributes.clef) {
6671
+ for (const clef of entry.attributes.clef) {
6672
+ const staff = clef.staff ?? 1;
6673
+ if (options?.staff !== void 0 && staff !== options.staff) {
6674
+ continue;
6675
+ }
6676
+ const lastClef = lastClefs.get(staff);
6677
+ if (!lastClef || lastClef.sign !== clef.sign || lastClef.line !== clef.line || lastClef.clefOctaveChange !== clef.clefOctaveChange) {
6678
+ results.push({
6679
+ clef,
6680
+ staff,
6681
+ partIndex,
6682
+ measureIndex,
6683
+ measureNumber,
6684
+ position: state.position
6685
+ });
6686
+ lastClefs.set(staff, clef);
6687
+ }
6688
+ }
6689
+ }
6690
+ updatePositionForEntry(state, entry);
6691
+ }
6692
+ }
6693
+ }
6694
+ return results;
6695
+ }
6696
+ function getStructuralChanges(score, options) {
6697
+ return {
6698
+ keyChanges: getKeyChanges(score, options),
6699
+ timeChanges: getTimeChanges(score, options),
6700
+ clefChanges: getClefChanges(score, options)
6701
+ };
6702
+ }
6703
+ function getPartByIndex(score, index) {
6704
+ return score.parts[index];
6705
+ }
6706
+ function getPartCount(score) {
6707
+ return score.parts.length;
6708
+ }
6709
+ function getPartIds(score) {
6710
+ return score.parts.map((part) => part.id);
6711
+ }
6712
+
6713
+ // src/query/index.ts
6714
+ function getMeasure(score, options) {
6715
+ const part = score.parts[options.part];
6716
+ if (!part) return void 0;
6717
+ const targetMeasure = String(options.measure);
6718
+ return part.measures.find((m) => m.number === targetMeasure);
6719
+ }
6720
+ function getMeasureByIndex(score, options) {
6721
+ const part = score.parts[options.part];
6722
+ if (!part) return void 0;
6723
+ return part.measures[options.measureIndex];
6724
+ }
6725
+ function getMeasureCount(score) {
6726
+ if (score.parts.length === 0) return 0;
6727
+ return score.parts[0].measures.length;
6728
+ }
6729
+ function getDivisions(score, options) {
6730
+ const part = score.parts[options.part];
6731
+ if (!part) return 1;
6732
+ const targetMeasure = parseInt(String(options.measure), 10);
6733
+ if (isNaN(targetMeasure)) return 1;
6734
+ for (let i = 0; i < part.measures.length; i++) {
6735
+ const m = part.measures[i];
6736
+ const mNum = parseInt(m.number, 10);
6737
+ if (!isNaN(mNum) && mNum > targetMeasure) break;
6738
+ if (m.attributes?.divisions !== void 0) {
6739
+ }
6740
+ }
6741
+ let divisions = 1;
6742
+ for (const m of part.measures) {
6743
+ const mNum = parseInt(m.number, 10);
6744
+ if (!isNaN(mNum) && mNum > targetMeasure) break;
6745
+ if (m.attributes?.divisions !== void 0) {
6746
+ divisions = m.attributes.divisions;
6747
+ }
6748
+ }
6749
+ return divisions;
6750
+ }
6751
+ function getAttributesAtMeasure(score, options) {
6752
+ const part = score.parts[options.part];
6753
+ if (!part) return {};
6754
+ const targetMeasure = parseInt(String(options.measure), 10);
6755
+ const result = {};
6756
+ for (const m of part.measures) {
6757
+ const mNum = parseInt(m.number, 10);
6758
+ if (!isNaN(targetMeasure) && !isNaN(mNum) && mNum > targetMeasure) break;
6759
+ if (m.attributes) {
6760
+ if (m.attributes.divisions !== void 0) result.divisions = m.attributes.divisions;
6761
+ if (m.attributes.time !== void 0) result.time = m.attributes.time;
6762
+ if (m.attributes.key !== void 0) result.key = m.attributes.key;
6763
+ if (m.attributes.clef !== void 0) result.clef = m.attributes.clef;
6764
+ if (m.attributes.staves !== void 0) result.staves = m.attributes.staves;
6765
+ if (m.attributes.transpose !== void 0) result.transpose = m.attributes.transpose;
6766
+ }
6767
+ }
6768
+ return result;
6769
+ }
6770
+ function findNotes(score, filter) {
6771
+ const results = [];
6772
+ for (const part of score.parts) {
6773
+ for (const measure of part.measures) {
6774
+ for (const entry of measure.entries) {
6775
+ if (entry.type !== "note") continue;
6776
+ if (filter.pitchRange && entry.pitch) {
6777
+ const noteValue = pitchToSemitone(entry.pitch);
6778
+ if (filter.pitchRange.min) {
6779
+ const minValue = pitchToSemitone(filter.pitchRange.min);
6780
+ if (noteValue < minValue) continue;
6781
+ }
6782
+ if (filter.pitchRange.max) {
6783
+ const maxValue = pitchToSemitone(filter.pitchRange.max);
6784
+ if (noteValue > maxValue) continue;
6785
+ }
6786
+ }
6787
+ if (filter.voice !== void 0 && entry.voice !== filter.voice) continue;
6788
+ if (filter.staff !== void 0 && (entry.staff ?? 1) !== filter.staff) continue;
6789
+ if (filter.noteType !== void 0 && entry.noteType !== filter.noteType) continue;
6790
+ if (filter.hasTie !== void 0) {
6791
+ const hasTie = entry.tie !== void 0;
6792
+ if (filter.hasTie !== hasTie) continue;
6793
+ }
6794
+ results.push(entry);
6795
+ }
6796
+ }
6797
+ }
6798
+ return results;
6799
+ }
6800
+ function getDuration(score) {
6801
+ if (score.parts.length === 0) return 0;
6802
+ const part = score.parts[0];
6803
+ let totalDuration = 0;
6804
+ let divisions = 1;
6805
+ for (const measure of part.measures) {
6806
+ if (measure.attributes?.divisions !== void 0) {
6807
+ divisions = measure.attributes.divisions;
6808
+ }
6809
+ let measureDuration = 0;
6810
+ for (const entry of measure.entries) {
6811
+ if (entry.type === "note" && !entry.chord) {
6812
+ measureDuration = Math.max(measureDuration, entry.duration);
6813
+ }
6814
+ }
6815
+ const attrs = getAttributesAtMeasure(score, { part: 0, measure: measure.number });
6816
+ if (attrs.time) {
6817
+ const beats = parseInt(attrs.time.beats, 10) || 4;
6818
+ const expectedDuration = beats / attrs.time.beatType * 4 * divisions;
6819
+ measureDuration = Math.max(measureDuration, expectedDuration);
6820
+ }
6821
+ totalDuration += measureDuration;
6822
+ }
6823
+ return totalDuration;
6824
+ }
6825
+ function getPartById(score, id) {
6826
+ return score.parts.find((p) => p.id === id);
6827
+ }
6828
+ function getPartIndex(score, id) {
6829
+ return score.parts.findIndex((p) => p.id === id);
6830
+ }
6831
+ function hasMultipleStaves(score, partIndex = 0) {
6832
+ const part = score.parts[partIndex];
6833
+ if (!part) return false;
6834
+ for (const measure of part.measures) {
6835
+ if (measure.attributes?.staves !== void 0 && measure.attributes.staves > 1) {
6836
+ return true;
6837
+ }
6838
+ }
6839
+ return false;
6840
+ }
6841
+ function getStaveCount(score, partIndex = 0) {
6842
+ const part = score.parts[partIndex];
6843
+ if (!part) return 1;
6844
+ for (const measure of part.measures) {
6845
+ if (measure.attributes?.staves !== void 0) {
6846
+ return measure.attributes.staves;
6847
+ }
6848
+ }
6849
+ return 1;
6850
+ }
6851
+ function measureRoundtrip(original, exported) {
6852
+ const notesOriginal = countNotes(original);
6853
+ const notesExported = countNotes(exported);
6854
+ const notesPreserved = Math.min(notesOriginal, notesExported);
6855
+ const measuresOriginal = getMeasureCount(original);
6856
+ const measuresExported = getMeasureCount(exported);
6857
+ const measuresPreserved = Math.min(measuresOriginal, measuresExported);
6858
+ const partsOriginal = original.parts.length;
6859
+ const partsExported = exported.parts.length;
6860
+ const partsPreserved = Math.min(partsOriginal, partsExported);
6861
+ const preservationRate = notesOriginal > 0 ? notesPreserved / notesOriginal : 1;
6862
+ return {
6863
+ notesOriginal,
6864
+ notesPreserved,
6865
+ measuresOriginal,
6866
+ measuresPreserved,
6867
+ partsOriginal,
6868
+ partsPreserved,
6869
+ preservationRate
6870
+ };
6871
+ }
6872
+ function countNotes(score) {
6873
+ let count = 0;
6874
+ for (const part of score.parts) {
6875
+ for (const measure of part.measures) {
6876
+ for (const entry of measure.entries) {
6877
+ if (entry.type === "note") {
6878
+ count++;
6879
+ }
6880
+ }
6881
+ }
6882
+ }
6883
+ return count;
5530
6884
  }
5531
6885
  function scoresEqual(a, b) {
5532
6886
  if (a.parts.length !== b.parts.length) return false;
@@ -5559,9 +6913,461 @@ function pitchesEqual(a, b) {
5559
6913
  }
5560
6914
 
5561
6915
  // src/operations/index.ts
6916
+ function success(data, warnings) {
6917
+ return { success: true, data, warnings };
6918
+ }
6919
+ function failure(errors) {
6920
+ return { success: false, errors };
6921
+ }
6922
+ function operationError(code, message, location = {}, details) {
6923
+ return {
6924
+ code,
6925
+ level: "error",
6926
+ message,
6927
+ location,
6928
+ details
6929
+ };
6930
+ }
5562
6931
  function cloneScore(score) {
5563
6932
  return JSON.parse(JSON.stringify(score));
5564
6933
  }
6934
+ function getMeasureDuration(divisions, time) {
6935
+ const beats = parseInt(time.beats, 10);
6936
+ if (isNaN(beats)) return divisions * 4;
6937
+ return beats / time.beatType * 4 * divisions;
6938
+ }
6939
+ function getVoiceEntries(measure, voice, staff) {
6940
+ const result = [];
6941
+ let position = 0;
6942
+ for (let i = 0; i < measure.entries.length; i++) {
6943
+ const entry = measure.entries[i];
6944
+ if (entry.type === "note") {
6945
+ const noteStaff = entry.staff ?? 1;
6946
+ if (entry.voice === voice && (staff === void 0 || noteStaff === staff)) {
6947
+ if (!entry.chord) {
6948
+ result.push({
6949
+ entry,
6950
+ entryIndex: i,
6951
+ position,
6952
+ endPosition: position + entry.duration
6953
+ });
6954
+ position += entry.duration;
6955
+ } else {
6956
+ if (result.length > 0) {
6957
+ const prev = result[result.length - 1];
6958
+ result.push({
6959
+ entry,
6960
+ entryIndex: i,
6961
+ position: prev.position,
6962
+ endPosition: prev.endPosition
6963
+ });
6964
+ }
6965
+ }
6966
+ } else if (!entry.chord) {
6967
+ position += entry.duration;
6968
+ }
6969
+ } else if (entry.type === "backup") {
6970
+ position -= entry.duration;
6971
+ } else if (entry.type === "forward") {
6972
+ if (entry.voice === voice) {
6973
+ result.push({
6974
+ entry,
6975
+ entryIndex: i,
6976
+ position,
6977
+ endPosition: position + entry.duration
6978
+ });
6979
+ }
6980
+ position += entry.duration;
6981
+ }
6982
+ }
6983
+ return result;
6984
+ }
6985
+ function hasNotesInRange(voiceEntries, startPos, endPos) {
6986
+ const conflicting = voiceEntries.filter((e) => {
6987
+ if (e.entry.type !== "note") return false;
6988
+ const note = e.entry;
6989
+ if (note.rest) return false;
6990
+ return e.position < endPos && e.endPosition > startPos;
6991
+ });
6992
+ return { hasNotes: conflicting.length > 0, conflictingNotes: conflicting };
6993
+ }
6994
+ function createRest(duration, voice, staff) {
6995
+ return {
6996
+ type: "note",
6997
+ rest: { displayStep: void 0, displayOctave: void 0 },
6998
+ duration,
6999
+ voice,
7000
+ staff
7001
+ };
7002
+ }
7003
+ function rebuildMeasureWithVoice(measure, voice, newEntries, measureDuration, staff) {
7004
+ const otherEntries = [];
7005
+ let position = 0;
7006
+ for (const entry of measure.entries) {
7007
+ if (entry.type === "note") {
7008
+ if (entry.voice !== voice || staff !== void 0 && (entry.staff ?? 1) !== staff) {
7009
+ if (!entry.chord) {
7010
+ otherEntries.push({ position, entry });
7011
+ position += entry.duration;
7012
+ } else {
7013
+ otherEntries.push({ position, entry });
7014
+ }
7015
+ } else if (!entry.chord) {
7016
+ position += entry.duration;
7017
+ }
7018
+ } else if (entry.type === "backup") {
7019
+ position -= entry.duration;
7020
+ } else if (entry.type === "forward") {
7021
+ if (entry.voice !== voice) {
7022
+ otherEntries.push({ position, entry });
7023
+ }
7024
+ position += entry.duration;
7025
+ } else {
7026
+ otherEntries.push({ position, entry });
7027
+ }
7028
+ }
7029
+ const filledNewEntries = [];
7030
+ let currentPos = 0;
7031
+ const sortedNew = [...newEntries].sort((a, b) => a.position - b.position);
7032
+ for (const { position: notePos, entry } of sortedNew) {
7033
+ if (notePos > currentPos) {
7034
+ filledNewEntries.push({
7035
+ position: currentPos,
7036
+ entry: createRest(notePos - currentPos, voice, staff)
7037
+ });
7038
+ }
7039
+ filledNewEntries.push({ position: notePos, entry });
7040
+ if (!entry.chord) {
7041
+ currentPos = notePos + entry.duration;
7042
+ }
7043
+ }
7044
+ if (currentPos < measureDuration) {
7045
+ filledNewEntries.push({
7046
+ position: currentPos,
7047
+ entry: createRest(measureDuration - currentPos, voice, staff)
7048
+ });
7049
+ }
7050
+ const allEntries = [...otherEntries, ...filledNewEntries];
7051
+ allEntries.sort((a, b) => a.position - b.position);
7052
+ const result = [];
7053
+ let currentPosition = 0;
7054
+ for (const { position: targetPos, entry } of allEntries) {
7055
+ const diff = targetPos - currentPosition;
7056
+ if (diff < 0) {
7057
+ result.push({ type: "backup", duration: -diff });
7058
+ currentPosition = targetPos;
7059
+ } else if (diff > 0) {
7060
+ result.push({
7061
+ type: "forward",
7062
+ duration: diff,
7063
+ voice: entry.type === "note" ? entry.voice : 1,
7064
+ staff: entry.type === "note" ? entry.staff : void 0
7065
+ });
7066
+ currentPosition = targetPos;
7067
+ }
7068
+ result.push(entry);
7069
+ if (entry.type === "note" && !entry.chord) {
7070
+ currentPosition += entry.duration;
7071
+ } else if (entry.type === "forward") {
7072
+ currentPosition += entry.duration;
7073
+ }
7074
+ }
7075
+ return result;
7076
+ }
7077
+ function insertNote(score, options) {
7078
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7079
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7080
+ }
7081
+ const part = score.parts[options.partIndex];
7082
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7083
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7084
+ }
7085
+ if (options.duration <= 0) {
7086
+ return failure([operationError("INVALID_DURATION", `Duration must be positive`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7087
+ }
7088
+ if (options.position < 0) {
7089
+ return failure([operationError("INVALID_POSITION", `Position cannot be negative`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7090
+ }
7091
+ const result = cloneScore(score);
7092
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7093
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
7094
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
7095
+ const noteEnd = options.position + options.duration;
7096
+ if (noteEnd > measureDuration) {
7097
+ return failure([operationError(
7098
+ "EXCEEDS_MEASURE",
7099
+ `Note ending at ${noteEnd} exceeds measure duration ${measureDuration}`,
7100
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
7101
+ { noteEnd, measureDuration }
7102
+ )]);
7103
+ }
7104
+ const voiceEntries = getVoiceEntries(measure, options.voice, options.staff);
7105
+ const { hasNotes: hasNotes2, conflictingNotes } = hasNotesInRange(voiceEntries, options.position, noteEnd);
7106
+ if (hasNotes2) {
7107
+ return failure([operationError(
7108
+ "NOTE_CONFLICT",
7109
+ `Position ${options.position}-${noteEnd} conflicts with existing note(s)`,
7110
+ { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice },
7111
+ { conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
7112
+ )]);
7113
+ }
7114
+ const newNote = {
7115
+ type: "note",
7116
+ pitch: options.pitch,
7117
+ duration: options.duration,
7118
+ voice: options.voice,
7119
+ staff: options.staff,
7120
+ noteType: options.noteType,
7121
+ dots: options.dots
7122
+ };
7123
+ const existingNotes = voiceEntries.filter((e) => {
7124
+ if (e.entry.type !== "note") return true;
7125
+ const note = e.entry;
7126
+ if (note.rest) {
7127
+ return !(e.position < noteEnd && e.endPosition > options.position);
7128
+ }
7129
+ return true;
7130
+ }).map((e) => ({ position: e.position, entry: e.entry }));
7131
+ existingNotes.push({ position: options.position, entry: newNote });
7132
+ measure.entries = rebuildMeasureWithVoice(
7133
+ measure,
7134
+ options.voice,
7135
+ existingNotes,
7136
+ measureDuration,
7137
+ options.staff
7138
+ );
7139
+ const errors = validateMeasureLocal(measure, context, {
7140
+ checkMeasureDuration: true,
7141
+ checkPosition: true,
7142
+ checkVoiceStaff: true
7143
+ });
7144
+ const criticalErrors = errors.filter((e) => e.level === "error");
7145
+ if (criticalErrors.length > 0) {
7146
+ return failure(criticalErrors);
7147
+ }
7148
+ return success(result, errors.filter((e) => e.level !== "error"));
7149
+ }
7150
+ function removeNote(score, options) {
7151
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7152
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7153
+ }
7154
+ const part = score.parts[options.partIndex];
7155
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7156
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7157
+ }
7158
+ const result = cloneScore(score);
7159
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7160
+ let noteCount = 0;
7161
+ let targetEntry = null;
7162
+ let targetIndex = -1;
7163
+ for (let i2 = 0; i2 < measure.entries.length; i2++) {
7164
+ const entry = measure.entries[i2];
7165
+ if (entry.type === "note" && !entry.rest) {
7166
+ if (noteCount === options.noteIndex) {
7167
+ targetEntry = entry;
7168
+ targetIndex = i2;
7169
+ break;
7170
+ }
7171
+ noteCount++;
7172
+ }
7173
+ }
7174
+ if (!targetEntry || targetIndex === -1) {
7175
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7176
+ }
7177
+ measure.entries[targetIndex] = createRest(
7178
+ targetEntry.duration,
7179
+ targetEntry.voice,
7180
+ targetEntry.staff
7181
+ );
7182
+ let i = targetIndex + 1;
7183
+ while (i < measure.entries.length) {
7184
+ const entry = measure.entries[i];
7185
+ if (entry.type === "note" && entry.chord) {
7186
+ measure.entries.splice(i, 1);
7187
+ } else {
7188
+ break;
7189
+ }
7190
+ }
7191
+ return success(result);
7192
+ }
7193
+ function addChord(score, options) {
7194
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7195
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7196
+ }
7197
+ const part = score.parts[options.partIndex];
7198
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7199
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7200
+ }
7201
+ const result = cloneScore(score);
7202
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7203
+ let noteCount = 0;
7204
+ let targetEntry = null;
7205
+ let targetIndex = -1;
7206
+ for (let i = 0; i < measure.entries.length; i++) {
7207
+ const entry = measure.entries[i];
7208
+ if (entry.type === "note" && !entry.rest && !entry.chord) {
7209
+ if (noteCount === options.noteIndex) {
7210
+ targetEntry = entry;
7211
+ targetIndex = i;
7212
+ break;
7213
+ }
7214
+ noteCount++;
7215
+ }
7216
+ }
7217
+ if (!targetEntry || targetIndex === -1) {
7218
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7219
+ }
7220
+ const chordNote = {
7221
+ type: "note",
7222
+ pitch: options.pitch,
7223
+ duration: targetEntry.duration,
7224
+ voice: targetEntry.voice,
7225
+ staff: targetEntry.staff,
7226
+ chord: true,
7227
+ noteType: targetEntry.noteType,
7228
+ dots: targetEntry.dots
7229
+ };
7230
+ let insertIndex = targetIndex + 1;
7231
+ while (insertIndex < measure.entries.length) {
7232
+ const entry = measure.entries[insertIndex];
7233
+ if (entry.type === "note" && entry.chord) {
7234
+ insertIndex++;
7235
+ } else {
7236
+ break;
7237
+ }
7238
+ }
7239
+ measure.entries.splice(insertIndex, 0, chordNote);
7240
+ return success(result);
7241
+ }
7242
+ function changeNoteDuration(score, options) {
7243
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7244
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7245
+ }
7246
+ const part = score.parts[options.partIndex];
7247
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7248
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7249
+ }
7250
+ if (options.newDuration <= 0) {
7251
+ return failure([operationError("INVALID_DURATION", `Duration must be positive`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7252
+ }
7253
+ const result = cloneScore(score);
7254
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7255
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
7256
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
7257
+ let noteCount = 0;
7258
+ let targetEntry = null;
7259
+ let targetPosition = 0;
7260
+ let position = 0;
7261
+ for (const entry of measure.entries) {
7262
+ if (entry.type === "note") {
7263
+ if (!entry.rest && !entry.chord) {
7264
+ if (noteCount === options.noteIndex) {
7265
+ targetEntry = entry;
7266
+ targetPosition = position;
7267
+ break;
7268
+ }
7269
+ noteCount++;
7270
+ }
7271
+ if (!entry.chord) {
7272
+ position += entry.duration;
7273
+ }
7274
+ } else if (entry.type === "backup") {
7275
+ position -= entry.duration;
7276
+ } else if (entry.type === "forward") {
7277
+ position += entry.duration;
7278
+ }
7279
+ }
7280
+ if (!targetEntry) {
7281
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7282
+ }
7283
+ const oldDuration = targetEntry.duration;
7284
+ const newEnd = targetPosition + options.newDuration;
7285
+ if (newEnd > measureDuration) {
7286
+ return failure([operationError(
7287
+ "EXCEEDS_MEASURE",
7288
+ `New duration would exceed measure (ends at ${newEnd}, measure is ${measureDuration})`,
7289
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
7290
+ { newEnd, measureDuration }
7291
+ )]);
7292
+ }
7293
+ const voiceEntries = getVoiceEntries(measure, targetEntry.voice, targetEntry.staff);
7294
+ if (options.newDuration > oldDuration) {
7295
+ const { hasNotes: hasNotes2, conflictingNotes } = hasNotesInRange(
7296
+ voiceEntries.filter((e) => e.position !== targetPosition),
7297
+ // Exclude current note
7298
+ targetPosition + oldDuration,
7299
+ newEnd
7300
+ );
7301
+ if (hasNotes2) {
7302
+ return failure([operationError(
7303
+ "NOTE_CONFLICT",
7304
+ `Cannot extend note: conflicts with existing note(s)`,
7305
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
7306
+ { conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
7307
+ )]);
7308
+ }
7309
+ }
7310
+ targetEntry.duration = options.newDuration;
7311
+ if (options.noteType !== void 0) {
7312
+ targetEntry.noteType = options.noteType;
7313
+ }
7314
+ if (options.dots !== void 0) {
7315
+ targetEntry.dots = options.dots;
7316
+ }
7317
+ const existingNotes = voiceEntries.filter((e) => {
7318
+ if (e.position === targetPosition) return true;
7319
+ const note = e.entry;
7320
+ if (note.rest) {
7321
+ if (options.newDuration > oldDuration) {
7322
+ return !(e.position >= targetPosition + oldDuration && e.position < newEnd);
7323
+ }
7324
+ }
7325
+ return true;
7326
+ }).map((e) => ({ position: e.position, entry: e.entry }));
7327
+ const modifiedIdx = existingNotes.findIndex((e) => e.position === targetPosition);
7328
+ if (modifiedIdx >= 0) {
7329
+ existingNotes[modifiedIdx].entry = targetEntry;
7330
+ }
7331
+ measure.entries = rebuildMeasureWithVoice(
7332
+ measure,
7333
+ targetEntry.voice,
7334
+ existingNotes,
7335
+ measureDuration,
7336
+ targetEntry.staff
7337
+ );
7338
+ const errors = validateMeasureLocal(measure, context, {
7339
+ checkMeasureDuration: true,
7340
+ checkPosition: true,
7341
+ checkVoiceStaff: true
7342
+ });
7343
+ const criticalErrors = errors.filter((e) => e.level === "error");
7344
+ if (criticalErrors.length > 0) {
7345
+ return failure(criticalErrors);
7346
+ }
7347
+ return success(result, errors.filter((e) => e.level !== "error"));
7348
+ }
7349
+ function setNotePitch(score, options) {
7350
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
7351
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
7352
+ }
7353
+ const part = score.parts[options.partIndex];
7354
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
7355
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7356
+ }
7357
+ const result = cloneScore(score);
7358
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
7359
+ let noteCount = 0;
7360
+ for (const entry of measure.entries) {
7361
+ if (entry.type === "note" && !entry.rest) {
7362
+ if (noteCount === options.noteIndex) {
7363
+ entry.pitch = options.pitch;
7364
+ return success(result);
7365
+ }
7366
+ noteCount++;
7367
+ }
7368
+ }
7369
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
7370
+ }
5565
7371
  function transposePitch(pitch, semitones) {
5566
7372
  const currentSemitone = STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0) + pitch.octave * 12;
5567
7373
  const targetSemitone = currentSemitone + semitones;
@@ -5588,7 +7394,7 @@ function transposePitch(pitch, semitones) {
5588
7394
  };
5589
7395
  }
5590
7396
  function transpose(score, semitones) {
5591
- if (semitones === 0) return score;
7397
+ if (semitones === 0) return success(score);
5592
7398
  const result = cloneScore(score);
5593
7399
  for (const part of result.parts) {
5594
7400
  for (const measure of part.measures) {
@@ -5599,59 +7405,7 @@ function transpose(score, semitones) {
5599
7405
  }
5600
7406
  }
5601
7407
  }
5602
- return result;
5603
- }
5604
- function addNote(score, options) {
5605
- const result = cloneScore(score);
5606
- const part = result.parts[options.partIndex];
5607
- if (!part) return result;
5608
- const measure = part.measures[options.measureIndex];
5609
- if (!measure) return result;
5610
- const newNote = {
5611
- type: "note",
5612
- voice: options.voice,
5613
- staff: options.staff,
5614
- ...options.note
5615
- };
5616
- const currentPosition = getMeasureEndPosition(measure);
5617
- const positionDiff = options.position - currentPosition;
5618
- if (positionDiff < 0) {
5619
- measure.entries.push({
5620
- type: "backup",
5621
- duration: -positionDiff
5622
- });
5623
- } else if (positionDiff > 0) {
5624
- measure.entries.push({
5625
- type: "forward",
5626
- duration: positionDiff,
5627
- voice: options.voice,
5628
- staff: options.staff
5629
- });
5630
- }
5631
- measure.entries.push(newNote);
5632
- return result;
5633
- }
5634
- function deleteNote(score, options) {
5635
- const result = cloneScore(score);
5636
- const part = result.parts[options.partIndex];
5637
- if (!part) return result;
5638
- const measure = part.measures[options.measureIndex];
5639
- if (!measure) return result;
5640
- let noteCount = 0;
5641
- let entryIndex = -1;
5642
- for (let i = 0; i < measure.entries.length; i++) {
5643
- if (measure.entries[i].type === "note") {
5644
- if (noteCount === options.noteIndex) {
5645
- entryIndex = i;
5646
- break;
5647
- }
5648
- noteCount++;
5649
- }
5650
- }
5651
- if (entryIndex !== -1) {
5652
- measure.entries.splice(entryIndex, 1);
5653
- }
5654
- return result;
7408
+ return success(result);
5655
7409
  }
5656
7410
  function changeKey(score, key, options) {
5657
7411
  const result = cloneScore(score);
@@ -5659,9 +7413,7 @@ function changeKey(score, key, options) {
5659
7413
  for (const part of result.parts) {
5660
7414
  for (const measure of part.measures) {
5661
7415
  if (measure.number === targetMeasure) {
5662
- if (!measure.attributes) {
5663
- measure.attributes = {};
5664
- }
7416
+ if (!measure.attributes) measure.attributes = {};
5665
7417
  measure.attributes.key = key;
5666
7418
  }
5667
7419
  }
@@ -5674,9 +7426,7 @@ function changeTime(score, time, options) {
5674
7426
  for (const part of result.parts) {
5675
7427
  for (const measure of part.measures) {
5676
7428
  if (measure.number === targetMeasure) {
5677
- if (!measure.attributes) {
5678
- measure.attributes = {};
5679
- }
7429
+ if (!measure.attributes) measure.attributes = {};
5680
7430
  measure.attributes.time = time;
5681
7431
  }
5682
7432
  }
@@ -5691,15 +7441,9 @@ function insertMeasure(score, options) {
5691
7441
  if (insertIndex === -1) continue;
5692
7442
  const numericPart = parseInt(targetMeasure, 10);
5693
7443
  const newMeasureNumber = String(isNaN(numericPart) ? insertIndex + 2 : numericPart + 1);
5694
- const newMeasure = {
5695
- number: newMeasureNumber,
5696
- entries: []
5697
- };
5698
- if (options.copyAttributes) {
5699
- const sourceMeasure = part.measures[insertIndex];
5700
- if (sourceMeasure.attributes) {
5701
- newMeasure.attributes = { ...sourceMeasure.attributes };
5702
- }
7444
+ const newMeasure = { number: newMeasureNumber, entries: [] };
7445
+ if (options.copyAttributes && part.measures[insertIndex].attributes) {
7446
+ newMeasure.attributes = { ...part.measures[insertIndex].attributes };
5703
7447
  }
5704
7448
  part.measures.splice(insertIndex + 1, 0, newMeasure);
5705
7449
  for (let i = insertIndex + 2; i < part.measures.length; i++) {
@@ -5727,95 +7471,36 @@ function deleteMeasure(score, measureNumber) {
5727
7471
  }
5728
7472
  return result;
5729
7473
  }
5730
- function setDivisions(score, options) {
5731
- const result = cloneScore(score);
5732
- const part = result.parts[options.partIndex];
5733
- if (!part) return result;
5734
- const measure = part.measures[options.measureIndex];
5735
- if (!measure) return result;
5736
- if (!measure.attributes) {
5737
- measure.attributes = {};
5738
- }
5739
- measure.attributes.divisions = options.divisions;
5740
- return result;
5741
- }
5742
- function addChordNote(score, options) {
5743
- const result = cloneScore(score);
5744
- const part = result.parts[options.partIndex];
5745
- if (!part) return result;
5746
- const measure = part.measures[options.measureIndex];
5747
- if (!measure) return result;
5748
- let noteCount = 0;
5749
- let entryIndex = -1;
5750
- let targetNote = null;
5751
- for (let i = 0; i < measure.entries.length; i++) {
5752
- const entry = measure.entries[i];
5753
- if (entry.type === "note") {
5754
- if (noteCount === options.afterNoteIndex) {
5755
- entryIndex = i;
5756
- targetNote = entry;
5757
- break;
5758
- }
5759
- noteCount++;
5760
- }
5761
- }
5762
- if (entryIndex !== -1 && targetNote) {
5763
- const chordNote = {
5764
- type: "note",
5765
- pitch: options.pitch,
5766
- duration: targetNote.duration,
5767
- voice: targetNote.voice,
5768
- staff: targetNote.staff,
5769
- chord: true,
5770
- noteType: targetNote.noteType,
5771
- dots: targetNote.dots
5772
- };
5773
- measure.entries.splice(entryIndex + 1, 0, chordNote);
5774
- }
5775
- return result;
5776
- }
5777
- function modifyNotePitch(score, options) {
5778
- const result = cloneScore(score);
5779
- const part = result.parts[options.partIndex];
5780
- if (!part) return result;
5781
- const measure = part.measures[options.measureIndex];
5782
- if (!measure) return result;
5783
- let noteCount = 0;
5784
- for (const entry of measure.entries) {
5785
- if (entry.type === "note") {
5786
- if (noteCount === options.noteIndex) {
5787
- entry.pitch = options.pitch;
5788
- break;
5789
- }
5790
- noteCount++;
5791
- }
5792
- }
5793
- return result;
5794
- }
5795
- function modifyNoteDuration(score, options) {
5796
- const result = cloneScore(score);
5797
- const part = result.parts[options.partIndex];
5798
- if (!part) return result;
5799
- const measure = part.measures[options.measureIndex];
5800
- if (!measure) return result;
5801
- let noteCount = 0;
5802
- for (const entry of measure.entries) {
5803
- if (entry.type === "note") {
5804
- if (noteCount === options.noteIndex) {
5805
- entry.duration = options.duration;
5806
- if (options.noteType !== void 0) {
5807
- entry.noteType = options.noteType;
5808
- }
5809
- if (options.dots !== void 0) {
5810
- entry.dots = options.dots;
5811
- }
5812
- break;
5813
- }
5814
- noteCount++;
5815
- }
5816
- }
5817
- return result;
5818
- }
7474
+ var addNote = (score, options) => {
7475
+ const result = insertNote(score, {
7476
+ partIndex: options.partIndex,
7477
+ measureIndex: options.measureIndex,
7478
+ voice: options.voice,
7479
+ staff: options.staff,
7480
+ position: options.position,
7481
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
7482
+ duration: options.note.duration,
7483
+ noteType: options.note.noteType,
7484
+ dots: options.note.dots
7485
+ });
7486
+ return result.success ? result.data : score;
7487
+ };
7488
+ var deleteNote = (score, options) => {
7489
+ const result = removeNote(score, options);
7490
+ return result.success ? result.data : score;
7491
+ };
7492
+ var addChordNote = (score, options) => {
7493
+ const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
7494
+ return result.success ? result.data : score;
7495
+ };
7496
+ var modifyNotePitch = (score, options) => {
7497
+ const result = setNotePitch(score, options);
7498
+ return result.success ? result.data : score;
7499
+ };
7500
+ var modifyNoteDuration = (score, options) => {
7501
+ const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
7502
+ return result.success ? result.data : score;
7503
+ };
5819
7504
 
5820
7505
  // src/file.ts
5821
7506
  import { readFile, writeFile } from "fs/promises";
@@ -5860,6 +7545,8 @@ export {
5860
7545
  addNote,
5861
7546
  assertMeasureValid,
5862
7547
  assertValid,
7548
+ buildVoiceToStaffMap,
7549
+ buildVoiceToStaffMapForPart,
5863
7550
  changeKey,
5864
7551
  changeTime,
5865
7552
  countNotes,
@@ -5867,36 +7554,82 @@ export {
5867
7554
  deleteMeasure,
5868
7555
  deleteNote,
5869
7556
  exportMidi,
7557
+ findBarlines,
7558
+ findDirectionsByType,
5870
7559
  findNotes,
7560
+ findNotesWithNotation,
5871
7561
  formatLocation,
5872
7562
  getAbsolutePosition,
7563
+ getAdjacentNotes,
5873
7564
  getAllNotes,
5874
7565
  getAttributesAtMeasure,
7566
+ getBeamGroups,
7567
+ getChordProgression,
5875
7568
  getChords,
7569
+ getClefChanges,
7570
+ getClefForStaff,
7571
+ getDirections,
7572
+ getDirectionsAtPosition,
5876
7573
  getDivisions,
5877
7574
  getDuration,
7575
+ getDynamics,
7576
+ getEffectiveStaff,
7577
+ getEndings,
7578
+ getEntriesAtPosition,
7579
+ getEntriesForStaff,
7580
+ getEntriesInRange,
7581
+ getHarmonies,
7582
+ getHarmonyAtPosition,
7583
+ getKeyChanges,
7584
+ getLyricText,
7585
+ getLyrics,
5878
7586
  getMeasure,
5879
7587
  getMeasureByIndex,
5880
7588
  getMeasureContext,
5881
7589
  getMeasureCount,
5882
7590
  getMeasureEndPosition,
7591
+ getNextNote,
5883
7592
  getNormalizedDuration,
5884
7593
  getNormalizedPosition,
7594
+ getNotesAtPosition,
5885
7595
  getNotesForStaff,
5886
7596
  getNotesForVoice,
7597
+ getNotesInRange,
7598
+ getOctaveShifts,
5887
7599
  getPartById,
7600
+ getPartByIndex,
7601
+ getPartCount,
7602
+ getPartIds,
5888
7603
  getPartIndex,
7604
+ getPedalMarkings,
7605
+ getPrevNote,
7606
+ getRepeatStructure,
7607
+ getSlurSpans,
7608
+ getStaffRange,
5889
7609
  getStaveCount,
5890
7610
  getStaves,
7611
+ getStructuralChanges,
7612
+ getTempoMarkings,
7613
+ getTiedNoteGroups,
7614
+ getTimeChanges,
7615
+ getTupletGroups,
7616
+ getVerseCount,
7617
+ getVerticalSlice,
7618
+ getVoiceLine,
7619
+ getVoiceLineInRange,
5891
7620
  getVoices,
7621
+ getVoicesForStaff,
7622
+ getWedges,
5892
7623
  groupByStaff,
5893
7624
  groupByVoice,
5894
7625
  hasMultipleStaves,
5895
7626
  hasNotes,
7627
+ inferStaff,
5896
7628
  insertMeasure,
5897
7629
  isCompressed,
5898
7630
  isRestMeasure,
5899
7631
  isValid,
7632
+ iterateEntries,
5900
7633
  iterateNotes,
5901
7634
  measureRoundtrip,
5902
7635
  modifyNoteDuration,
@@ -5910,7 +7643,6 @@ export {
5910
7643
  serialize,
5911
7644
  serializeCompressed,
5912
7645
  serializeToFile,
5913
- setDivisions,
5914
7646
  transpose,
5915
7647
  validate,
5916
7648
  validateBackupForward,
@@ -5929,4 +7661,3 @@ export {
5929
7661
  validateVoiceStaff,
5930
7662
  withAbsolutePositions
5931
7663
  };
5932
- //# sourceMappingURL=index.mjs.map