musicxml-io 0.7.0 → 0.7.2

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.
@@ -8,7 +8,7 @@ import {
8
8
  getMeasureEndPosition,
9
9
  pitchToSemitone,
10
10
  semitoneToKeyAwarePitch
11
- } from "./chunk-CCSOG7HU.mjs";
11
+ } from "./chunk-VQUFSGB5.mjs";
12
12
 
13
13
  // src/id.ts
14
14
  import { customAlphabet } from "nanoid";
@@ -8,7 +8,7 @@
8
8
 
9
9
 
10
10
 
11
- var _chunkCJZK2DGVjs = require('./chunk-CJZK2DGV.js');
11
+ var _chunkCHMV7XWYjs = require('./chunk-CHMV7XWY.js');
12
12
 
13
13
  // src/id.ts
14
14
  var _nanoid = require('nanoid');
@@ -1562,18 +1562,18 @@ function setNotePitchBySemitone(score, options) {
1562
1562
  const result = cloneScore(score);
1563
1563
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1564
1564
  const measureNumber = _nullishCoalesce(measure.number, () => ( String(options.measureIndex + 1)));
1565
- const attrs = _chunkCJZK2DGVjs.getAttributesAtMeasure.call(void 0, result, { part: options.partIndex, measure: measureNumber });
1565
+ const attrs = _chunkCHMV7XWYjs.getAttributesAtMeasure.call(void 0, result, { part: options.partIndex, measure: measureNumber });
1566
1566
  const keySignature = _nullishCoalesce(attrs.key, () => ( { fifths: 0 }));
1567
1567
  let noteCount = 0;
1568
1568
  for (const entry of measure.entries) {
1569
1569
  if (entry.type === "note" && !entry.rest) {
1570
1570
  if (noteCount === options.noteIndex) {
1571
- const notePosition = _chunkCJZK2DGVjs.getAbsolutePositionForNote.call(void 0, entry, measure);
1572
- const accidentalsInMeasure = _chunkCJZK2DGVjs.getAccidentalsInMeasure.call(void 0, measure, notePosition, entry.voice);
1573
- const newPitch = _chunkCJZK2DGVjs.semitoneToKeyAwarePitch.call(void 0, options.semitone, keySignature, {
1571
+ const notePosition = _chunkCHMV7XWYjs.getAbsolutePositionForNote.call(void 0, entry, measure);
1572
+ const accidentalsInMeasure = _chunkCHMV7XWYjs.getAccidentalsInMeasure.call(void 0, measure, notePosition, entry.voice);
1573
+ const newPitch = _chunkCHMV7XWYjs.semitoneToKeyAwarePitch.call(void 0, options.semitone, keySignature, {
1574
1574
  preferSharp: options.preferSharp
1575
1575
  });
1576
- const accidental = _chunkCJZK2DGVjs.determineAccidental.call(void 0, newPitch, keySignature, accidentalsInMeasure);
1576
+ const accidental = _chunkCHMV7XWYjs.determineAccidental.call(void 0, newPitch, keySignature, accidentalsInMeasure);
1577
1577
  entry.pitch = newPitch;
1578
1578
  if (accidental) {
1579
1579
  entry.accidental = { value: accidental };
@@ -1607,7 +1607,7 @@ function shiftNotePitch(score, options) {
1607
1607
  if (!entry.pitch) {
1608
1608
  return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1609
1609
  }
1610
- currentSemitone = _chunkCJZK2DGVjs.pitchToSemitone.call(void 0, entry.pitch);
1610
+ currentSemitone = _chunkCHMV7XWYjs.pitchToSemitone.call(void 0, entry.pitch);
1611
1611
  break;
1612
1612
  }
1613
1613
  noteCount++;
@@ -1635,7 +1635,7 @@ function raiseAccidental(score, options) {
1635
1635
  const result = cloneScore(score);
1636
1636
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1637
1637
  const measureNumber = _nullishCoalesce(measure.number, () => ( String(options.measureIndex + 1)));
1638
- const attrs = _chunkCJZK2DGVjs.getAttributesAtMeasure.call(void 0, result, { part: options.partIndex, measure: measureNumber });
1638
+ const attrs = _chunkCHMV7XWYjs.getAttributesAtMeasure.call(void 0, result, { part: options.partIndex, measure: measureNumber });
1639
1639
  const keySignature = _nullishCoalesce(attrs.key, () => ( { fifths: 0 }));
1640
1640
  let noteCount = 0;
1641
1641
  for (const entry of measure.entries) {
@@ -1650,9 +1650,9 @@ function raiseAccidental(score, options) {
1650
1650
  return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot raise accidental beyond double-sharp (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1651
1651
  }
1652
1652
  entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
1653
- const notePosition = _chunkCJZK2DGVjs.getAbsolutePositionForNote.call(void 0, entry, measure);
1654
- const accidentalsInMeasure = _chunkCJZK2DGVjs.getAccidentalsInMeasure.call(void 0, measure, notePosition, entry.voice);
1655
- const accidental = _chunkCJZK2DGVjs.determineAccidental.call(void 0, entry.pitch, keySignature, accidentalsInMeasure);
1653
+ const notePosition = _chunkCHMV7XWYjs.getAbsolutePositionForNote.call(void 0, entry, measure);
1654
+ const accidentalsInMeasure = _chunkCHMV7XWYjs.getAccidentalsInMeasure.call(void 0, measure, notePosition, entry.voice);
1655
+ const accidental = _chunkCHMV7XWYjs.determineAccidental.call(void 0, entry.pitch, keySignature, accidentalsInMeasure);
1656
1656
  if (accidental) {
1657
1657
  entry.accidental = { value: accidental };
1658
1658
  } else {
@@ -1676,7 +1676,7 @@ function lowerAccidental(score, options) {
1676
1676
  const result = cloneScore(score);
1677
1677
  const measure = result.parts[options.partIndex].measures[options.measureIndex];
1678
1678
  const measureNumber = _nullishCoalesce(measure.number, () => ( String(options.measureIndex + 1)));
1679
- const attrs = _chunkCJZK2DGVjs.getAttributesAtMeasure.call(void 0, result, { part: options.partIndex, measure: measureNumber });
1679
+ const attrs = _chunkCHMV7XWYjs.getAttributesAtMeasure.call(void 0, result, { part: options.partIndex, measure: measureNumber });
1680
1680
  const keySignature = _nullishCoalesce(attrs.key, () => ( { fifths: 0 }));
1681
1681
  let noteCount = 0;
1682
1682
  for (const entry of measure.entries) {
@@ -1691,9 +1691,9 @@ function lowerAccidental(score, options) {
1691
1691
  return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot lower accidental beyond double-flat (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1692
1692
  }
1693
1693
  entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
1694
- const notePosition = _chunkCJZK2DGVjs.getAbsolutePositionForNote.call(void 0, entry, measure);
1695
- const accidentalsInMeasure = _chunkCJZK2DGVjs.getAccidentalsInMeasure.call(void 0, measure, notePosition, entry.voice);
1696
- const accidental = _chunkCJZK2DGVjs.determineAccidental.call(void 0, entry.pitch, keySignature, accidentalsInMeasure);
1694
+ const notePosition = _chunkCHMV7XWYjs.getAbsolutePositionForNote.call(void 0, entry, measure);
1695
+ const accidentalsInMeasure = _chunkCHMV7XWYjs.getAccidentalsInMeasure.call(void 0, measure, notePosition, entry.voice);
1696
+ const accidental = _chunkCHMV7XWYjs.determineAccidental.call(void 0, entry.pitch, keySignature, accidentalsInMeasure);
1697
1697
  if (accidental) {
1698
1698
  entry.accidental = { value: accidental };
1699
1699
  } else {
@@ -1727,7 +1727,7 @@ function addVoice(score, options) {
1727
1727
  const context = getMeasureContext(result, options.partIndex, options.measureIndex);
1728
1728
  const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
1729
1729
  const rest = createRest(measureDuration, options.voice, options.staff);
1730
- const currentEnd = _chunkCJZK2DGVjs.getMeasureEndPosition.call(void 0, measure);
1730
+ const currentEnd = _chunkCHMV7XWYjs.getMeasureEndPosition.call(void 0, measure);
1731
1731
  if (currentEnd > 0) {
1732
1732
  measure.entries.push({ _id: generateId(), type: "backup", duration: currentEnd });
1733
1733
  }
@@ -1735,14 +1735,14 @@ function addVoice(score, options) {
1735
1735
  return success(result);
1736
1736
  }
1737
1737
  function transposePitch(pitch, semitones) {
1738
- const currentSemitone = _chunkCJZK2DGVjs.STEP_SEMITONES[pitch.step] + (_nullishCoalesce(pitch.alter, () => ( 0))) + pitch.octave * 12;
1738
+ const currentSemitone = _chunkCHMV7XWYjs.STEP_SEMITONES[pitch.step] + (_nullishCoalesce(pitch.alter, () => ( 0))) + pitch.octave * 12;
1739
1739
  const targetSemitone = currentSemitone + semitones;
1740
1740
  const targetOctave = Math.floor(targetSemitone / 12);
1741
1741
  const targetPitchClass = (targetSemitone % 12 + 12) % 12;
1742
1742
  let bestStep = "C";
1743
1743
  let bestAlter = 99;
1744
- for (const step of _chunkCJZK2DGVjs.STEPS) {
1745
- const stepSemitone = _chunkCJZK2DGVjs.STEP_SEMITONES[step];
1744
+ for (const step of _chunkCHMV7XWYjs.STEPS) {
1745
+ const stepSemitone = _chunkCHMV7XWYjs.STEP_SEMITONES[step];
1746
1746
  let diff = targetPitchClass - stepSemitone;
1747
1747
  if (diff > 6) diff -= 12;
1748
1748
  if (diff < -6) diff += 12;
@@ -3805,7 +3805,7 @@ function addDaCapo(score, options) {
3805
3805
  }
3806
3806
  const result = cloneScore(score);
3807
3807
  const measure = result.parts[partIndex].measures[measureIndex];
3808
- const attrs = _chunkCJZK2DGVjs.getAttributesAtMeasure.call(void 0, result, { part: partIndex, measure: measureIndex });
3808
+ const attrs = _chunkCHMV7XWYjs.getAttributesAtMeasure.call(void 0, result, { part: partIndex, measure: measureIndex });
3809
3809
  const measureDuration = getMeasureDuration(_nullishCoalesce(attrs.divisions, () => ( 1)), _nullishCoalesce(attrs.time, () => ( { beats: "4", beatType: 4 })));
3810
3810
  const insertPos = _nullishCoalesce(position, () => ( measureDuration));
3811
3811
  const direction = {
@@ -3834,7 +3834,7 @@ function addDalSegno(score, options) {
3834
3834
  }
3835
3835
  const result = cloneScore(score);
3836
3836
  const measure = result.parts[partIndex].measures[measureIndex];
3837
- const attrs = _chunkCJZK2DGVjs.getAttributesAtMeasure.call(void 0, result, { part: partIndex, measure: measureIndex });
3837
+ const attrs = _chunkCHMV7XWYjs.getAttributesAtMeasure.call(void 0, result, { part: partIndex, measure: measureIndex });
3838
3838
  const measureDuration = getMeasureDuration(_nullishCoalesce(attrs.divisions, () => ( 1)), _nullishCoalesce(attrs.time, () => ( { beats: "4", beatType: 4 })));
3839
3839
  const insertPos = _nullishCoalesce(position, () => ( measureDuration));
3840
3840
  const direction = {
@@ -3863,7 +3863,7 @@ function addFine(score, options) {
3863
3863
  }
3864
3864
  const result = cloneScore(score);
3865
3865
  const measure = result.parts[partIndex].measures[measureIndex];
3866
- const attrs = _chunkCJZK2DGVjs.getAttributesAtMeasure.call(void 0, result, { part: partIndex, measure: measureIndex });
3866
+ const attrs = _chunkCHMV7XWYjs.getAttributesAtMeasure.call(void 0, result, { part: partIndex, measure: measureIndex });
3867
3867
  const measureDuration = getMeasureDuration(_nullishCoalesce(attrs.divisions, () => ( 1)), _nullishCoalesce(attrs.time, () => ( { beats: "4", beatType: 4 })));
3868
3868
  const insertPos = _nullishCoalesce(position, () => ( measureDuration));
3869
3869
  const direction = {
@@ -3892,7 +3892,7 @@ function addToCoda(score, options) {
3892
3892
  }
3893
3893
  const result = cloneScore(score);
3894
3894
  const measure = result.parts[partIndex].measures[measureIndex];
3895
- const attrs = _chunkCJZK2DGVjs.getAttributesAtMeasure.call(void 0, result, { part: partIndex, measure: measureIndex });
3895
+ const attrs = _chunkCHMV7XWYjs.getAttributesAtMeasure.call(void 0, result, { part: partIndex, measure: measureIndex });
3896
3896
  const measureDuration = getMeasureDuration(_nullishCoalesce(attrs.divisions, () => ( 1)), _nullishCoalesce(attrs.time, () => ( { beats: "4", beatType: 4 })));
3897
3897
  const insertPos = _nullishCoalesce(position, () => ( measureDuration));
3898
3898
  const direction = {
@@ -509,6 +509,363 @@ function hasPlaybackControls(score, options) {
509
509
  return controls.repeatStarts.length > 0 || controls.repeatEnds.length > 0 || controls.voltas.length > 0 || controls.jumps.length > 0 || controls.segnoIndex !== null || controls.codaIndex !== null;
510
510
  }
511
511
 
512
+ // src/query/playback-timeline.ts
513
+ var DEFAULT_FERMATA_HOLD = 1.75;
514
+ var DEFAULT_CAESURA_SECONDS = 0.4;
515
+ var DEFAULT_BREATH_SECONDS = 0.25;
516
+ var DEFAULT_RAMP_STEPS = 12;
517
+ function metronomeBpm(perMinute) {
518
+ if (perMinute === void 0) return null;
519
+ const bpm = typeof perMinute === "number" ? perMinute : parseFloat(perMinute);
520
+ return isNaN(bpm) ? null : bpm;
521
+ }
522
+ function measureMaxPosition(measure) {
523
+ let position = 0;
524
+ let max = 0;
525
+ for (const entry of measure.entries) {
526
+ if (entry.type === "note") {
527
+ if (!entry.chord && !entry.grace) {
528
+ position += entry.duration;
529
+ if (position > max) max = position;
530
+ }
531
+ } else if (entry.type === "backup") {
532
+ position -= entry.duration;
533
+ } else if (entry.type === "forward") {
534
+ position += entry.duration;
535
+ if (position > max) max = position;
536
+ }
537
+ }
538
+ return max;
539
+ }
540
+ function findTimeSignature(part, measureNumber) {
541
+ const targetMeasure = parseInt(String(measureNumber), 10);
542
+ let time;
543
+ for (const measure of part.measures) {
544
+ const mNum = parseInt(measure.number, 10);
545
+ if (!isNaN(targetMeasure) && !isNaN(mNum) && mNum > targetMeasure) break;
546
+ if (measure.attributes?.time) {
547
+ time = {
548
+ beats: parseInt(measure.attributes.time.beats, 10) || 4,
549
+ beatType: measure.attributes.time.beatType
550
+ };
551
+ }
552
+ }
553
+ return time;
554
+ }
555
+ function measureEndTick(measure, part, divisions, measureStartTick, maxPosition, ticksPerQuarterNote) {
556
+ const actualTicks = Math.round(maxPosition * ticksPerQuarterNote / divisions);
557
+ if (measure.implicit) {
558
+ return measureStartTick + actualTicks;
559
+ }
560
+ const timeAttrs = findTimeSignature(part, measure.number);
561
+ if (timeAttrs) {
562
+ const measureDuration = timeAttrs.beats / timeAttrs.beatType * 4 * divisions;
563
+ const calculatedTicks = Math.round(measureDuration * ticksPerQuarterNote / divisions);
564
+ const ticksToAdd = Math.min(calculatedTicks, actualTicks > 0 ? actualTicks : calculatedTicks);
565
+ return measureStartTick + ticksToAdd;
566
+ }
567
+ return measureStartTick + actualTicks;
568
+ }
569
+ function hasFermata(note) {
570
+ return note.notations?.some((n) => n.type === "fermata") ?? false;
571
+ }
572
+ function pauseArticulation(note) {
573
+ if (!note.notations) return null;
574
+ for (const n of note.notations) {
575
+ if (n.type === "articulation") {
576
+ if (n.articulation === "caesura") return "caesura";
577
+ if (n.articulation === "breath-mark") return "breath";
578
+ }
579
+ }
580
+ return null;
581
+ }
582
+ function tempoRampWord(text) {
583
+ const t = text.trim().toLowerCase().replace(/[.\s]+$/, "");
584
+ if (!t) return null;
585
+ if (/^(rit|riten|rall|allarg|slarg|cala|smorz|morend)/.test(t)) return "rit";
586
+ if (/^(accel|string|affret|incalz|pi[uù]\s*mosso)/.test(t)) return "accel";
587
+ return null;
588
+ }
589
+ function buildGridTimeline(score, ticksPerQuarterNote, sequence, options = {}) {
590
+ const rawTempo = [];
591
+ const measures = [];
592
+ const rampMarkers = [];
593
+ const stretches = [];
594
+ if (score.parts.length === 0) {
595
+ return { tempoEvents: [], measures, totalTicks: 0, expressions: [] };
596
+ }
597
+ const fermataHold = options.fermataHoldMultiplier ?? DEFAULT_FERMATA_HOLD;
598
+ const caesuraSeconds = options.caesuraSeconds ?? DEFAULT_CAESURA_SECONDS;
599
+ const breathSeconds = options.breathSeconds ?? DEFAULT_BREATH_SECONDS;
600
+ const rampSteps = Math.max(1, Math.round(options.tempoRampSteps ?? DEFAULT_RAMP_STEPS));
601
+ const defaultTempo = options.defaultTempo ?? 120;
602
+ const part = score.parts[0];
603
+ let divisions = 1;
604
+ let currentTick = 0;
605
+ let prevailingBpm = defaultTempo;
606
+ const pauseFactor = (windowTicks, bpm, pauseSeconds) => {
607
+ const baseSeconds = windowTicks / ticksPerQuarterNote * (60 / bpm);
608
+ if (baseSeconds <= 0) return 1;
609
+ return (baseSeconds + pauseSeconds) / baseSeconds;
610
+ };
611
+ for (const { measureIndex, repeatIteration } of sequence) {
612
+ const measure = part.measures[measureIndex];
613
+ if (!measure) continue;
614
+ if (measure.attributes?.divisions) {
615
+ divisions = measure.attributes.divisions;
616
+ }
617
+ const measureStartTick = currentTick;
618
+ let position = 0;
619
+ let chordBasePosition = 0;
620
+ const tickAt = (pos) => measureStartTick + Math.round(pos * ticksPerQuarterNote / divisions);
621
+ for (const entry of measure.entries) {
622
+ if (entry.type === "direction") {
623
+ for (const dirType of entry.directionTypes) {
624
+ if (dirType.kind === "metronome") {
625
+ const bpm = metronomeBpm(dirType.perMinute);
626
+ if (bpm !== null) {
627
+ rawTempo.push({ tick: tickAt(position), bpm });
628
+ prevailingBpm = bpm;
629
+ }
630
+ } else if (dirType.kind === "words") {
631
+ const kind = tempoRampWord(dirType.text);
632
+ if (kind) rampMarkers.push({ tick: tickAt(position), kind });
633
+ }
634
+ }
635
+ if (entry.sound?.tempo) {
636
+ rawTempo.push({ tick: tickAt(position), bpm: entry.sound.tempo });
637
+ prevailingBpm = entry.sound.tempo;
638
+ }
639
+ } else if (entry.type === "sound") {
640
+ if (entry.tempo) {
641
+ rawTempo.push({ tick: tickAt(position), bpm: entry.tempo });
642
+ prevailingBpm = entry.tempo;
643
+ }
644
+ } else if (entry.type === "note") {
645
+ const notePos = entry.chord ? chordBasePosition : position;
646
+ const noteStartTick = tickAt(notePos);
647
+ const noteEndTick = tickAt(notePos + entry.duration);
648
+ if (noteEndTick > noteStartTick) {
649
+ if (fermataHold !== 1 && hasFermata(entry)) {
650
+ stretches.push({
651
+ startTick: noteStartTick,
652
+ endTick: noteEndTick,
653
+ factor: fermataHold,
654
+ type: "fermata"
655
+ });
656
+ }
657
+ const pause = pauseArticulation(entry);
658
+ if (pause) {
659
+ const seconds = pause === "caesura" ? caesuraSeconds : breathSeconds;
660
+ if (seconds > 0) {
661
+ const window = Math.min(noteEndTick - noteStartTick, ticksPerQuarterNote);
662
+ stretches.push({
663
+ startTick: noteEndTick - window,
664
+ endTick: noteEndTick,
665
+ factor: pauseFactor(window, prevailingBpm, seconds),
666
+ type: pause
667
+ });
668
+ }
669
+ }
670
+ }
671
+ if (!entry.chord) {
672
+ chordBasePosition = position;
673
+ position += entry.duration;
674
+ }
675
+ } else if (entry.type === "backup") {
676
+ position -= entry.duration;
677
+ } else if (entry.type === "forward") {
678
+ position += entry.duration;
679
+ }
680
+ }
681
+ const endTick = measureEndTick(
682
+ measure,
683
+ part,
684
+ divisions,
685
+ measureStartTick,
686
+ measureMaxPosition(measure),
687
+ ticksPerQuarterNote
688
+ );
689
+ measures.push({
690
+ measureIndex,
691
+ repeatIteration,
692
+ measureNumber: measure.number,
693
+ startTick: measureStartTick,
694
+ endTick
695
+ });
696
+ currentTick = endTick;
697
+ }
698
+ const { tempoEvents, expressions } = bakeTempoMap(
699
+ rawTempo,
700
+ rampMarkers,
701
+ stretches,
702
+ defaultTempo,
703
+ rampSteps
704
+ );
705
+ return { tempoEvents, measures, totalTicks: currentTick, expressions };
706
+ }
707
+ function bakeTempoMap(rawTempo, rampMarkers, stretches, defaultTempo, rampSteps) {
708
+ const sortedRaw = [...rawTempo].sort((a, b) => a.tick - b.tick);
709
+ const startBpm = sortedRaw.length > 0 && sortedRaw[0].tick === 0 ? sortedRaw[0].bpm : defaultTempo;
710
+ const baseChanges = [{ tick: 0, bpm: startBpm }];
711
+ let lastBpm = startBpm;
712
+ for (const ev of sortedRaw) {
713
+ if (ev.tick === 0) continue;
714
+ if (ev.bpm === lastBpm) continue;
715
+ if (baseChanges[baseChanges.length - 1].tick === ev.tick) {
716
+ baseChanges[baseChanges.length - 1].bpm = ev.bpm;
717
+ } else {
718
+ baseChanges.push({ tick: ev.tick, bpm: ev.bpm });
719
+ }
720
+ lastBpm = ev.bpm;
721
+ }
722
+ const baseBpmAt = (tick) => {
723
+ let i = baseChanges.length - 1;
724
+ while (i > 0 && baseChanges[i].tick > tick) i--;
725
+ return baseChanges[i].bpm;
726
+ };
727
+ const rampExpressions = [];
728
+ const rampPoints = [];
729
+ for (const marker of rampMarkers) {
730
+ const from = baseBpmAt(marker.tick);
731
+ const next = baseChanges.find((c) => c.tick > marker.tick && c.bpm !== from);
732
+ if (!next) continue;
733
+ const span = next.tick - marker.tick;
734
+ if (span <= 0) continue;
735
+ for (let k = 1; k < rampSteps; k++) {
736
+ const tick = Math.round(marker.tick + span * k / rampSteps);
737
+ if (tick <= marker.tick || tick >= next.tick) continue;
738
+ const bpm = from + (next.bpm - from) * k / rampSteps;
739
+ rampPoints.push({ tick, bpm });
740
+ }
741
+ rampExpressions.push({ type: marker.kind, startTick: marker.tick, endTick: next.tick });
742
+ }
743
+ const baseTicks = new Set(baseChanges.map((c) => c.tick));
744
+ const stepList = [...baseChanges];
745
+ for (const p of rampPoints) {
746
+ if (!baseTicks.has(p.tick)) stepList.push(p);
747
+ }
748
+ stepList.sort((a, b) => a.tick - b.tick);
749
+ const stepBpmAt = (tick) => {
750
+ let i = stepList.length - 1;
751
+ while (i > 0 && stepList[i].tick > tick) i--;
752
+ return stepList[i].bpm;
753
+ };
754
+ const boundaries = new Set(stepList.map((s) => s.tick));
755
+ for (const s of stretches) {
756
+ boundaries.add(s.startTick);
757
+ boundaries.add(s.endTick);
758
+ }
759
+ const sortedBoundaries = [...boundaries].sort((a, b) => a - b);
760
+ const tempoEvents = [];
761
+ let emittedBpm = null;
762
+ for (const tick of sortedBoundaries) {
763
+ let bpm = stepBpmAt(tick);
764
+ for (const s of stretches) {
765
+ if (tick >= s.startTick && tick < s.endTick) bpm /= s.factor;
766
+ }
767
+ if (emittedBpm === null || bpm !== emittedBpm) {
768
+ tempoEvents.push({ tick, bpm });
769
+ emittedBpm = bpm;
770
+ }
771
+ }
772
+ const expressions = [
773
+ ...stretches.map((s) => ({ type: s.type, startTick: s.startTick, endTick: s.endTick })),
774
+ ...rampExpressions
775
+ ].sort((a, b) => a.startTick - b.startTick || a.endTick - b.endTick);
776
+ return { tempoEvents, expressions };
777
+ }
778
+ function bpmToUsPerQuarter(bpm) {
779
+ return Math.round(6e7 / bpm);
780
+ }
781
+ function makeTickToSec(tempoEvents, defaultTempo, ticksPerQuarterNote) {
782
+ const sorted = [...tempoEvents].sort((a, b) => a.tick - b.tick);
783
+ const startBpm = sorted.length > 0 && sorted[0].tick === 0 ? sorted[0].bpm : defaultTempo;
784
+ const changes = [
785
+ { tick: 0, usPerQuarter: bpmToUsPerQuarter(startBpm) }
786
+ ];
787
+ let lastBpm = startBpm;
788
+ for (const ev of sorted) {
789
+ if (ev.tick === 0) continue;
790
+ if (ev.bpm === lastBpm) continue;
791
+ changes.push({ tick: ev.tick, usPerQuarter: bpmToUsPerQuarter(ev.bpm) });
792
+ lastBpm = ev.bpm;
793
+ }
794
+ const secPerTick = (usPerQuarter) => usPerQuarter / 1e6 / ticksPerQuarterNote;
795
+ const cumSec = [0];
796
+ for (let i = 1; i < changes.length; i++) {
797
+ const dTick = changes[i].tick - changes[i - 1].tick;
798
+ cumSec[i] = cumSec[i - 1] + dTick * secPerTick(changes[i - 1].usPerQuarter);
799
+ }
800
+ return (tick) => {
801
+ let i = changes.length - 1;
802
+ while (i > 0 && changes[i].tick > tick) i--;
803
+ return cumSec[i] + (tick - changes[i].tick) * secPerTick(changes[i].usPerQuarter);
804
+ };
805
+ }
806
+ function buildTimingSidecar(grid, defaultTempo, ticksPerQuarterNote) {
807
+ const tickToSec = makeTickToSec(grid.tempoEvents, defaultTempo, ticksPerQuarterNote);
808
+ const breakpoints = [];
809
+ const measureStartTicks = new Set(grid.measures.map((m) => m.startTick));
810
+ for (const m of grid.measures) {
811
+ breakpoints.push({
812
+ midiSec: tickToSec(m.startTick),
813
+ quarterPos: m.startTick / ticksPerQuarterNote,
814
+ measureNumber: m.measureNumber,
815
+ beatInMeasure: 0,
816
+ repeatIteration: m.repeatIteration
817
+ });
818
+ }
819
+ for (const ev of grid.tempoEvents) {
820
+ if (measureStartTicks.has(ev.tick)) continue;
821
+ const m = grid.measures.find((mm) => ev.tick >= mm.startTick && ev.tick < mm.endTick);
822
+ if (!m) continue;
823
+ breakpoints.push({
824
+ midiSec: tickToSec(ev.tick),
825
+ quarterPos: ev.tick / ticksPerQuarterNote,
826
+ measureNumber: m.measureNumber,
827
+ beatInMeasure: (ev.tick - m.startTick) / ticksPerQuarterNote,
828
+ repeatIteration: m.repeatIteration
829
+ });
830
+ }
831
+ const last = grid.measures[grid.measures.length - 1];
832
+ breakpoints.push({
833
+ midiSec: tickToSec(grid.totalTicks),
834
+ quarterPos: grid.totalTicks / ticksPerQuarterNote,
835
+ measureNumber: last ? last.measureNumber : "0",
836
+ beatInMeasure: last ? (grid.totalTicks - last.startTick) / ticksPerQuarterNote : 0,
837
+ repeatIteration: last ? last.repeatIteration : 0
838
+ });
839
+ breakpoints.sort((a, b) => a.midiSec - b.midiSec || a.quarterPos - b.quarterPos);
840
+ const sidecar = {
841
+ version: "1",
842
+ durationSec: tickToSec(grid.totalTicks),
843
+ ticksPerQuarterNote,
844
+ breakpoints
845
+ };
846
+ if (grid.expressions.length > 0) {
847
+ sidecar.expressions = grid.expressions.map((e) => ({
848
+ type: e.type,
849
+ fromMidiSec: tickToSec(e.startTick),
850
+ toMidiSec: tickToSec(e.endTick)
851
+ })).sort((a, b) => a.fromMidiSec - b.fromMidiSec || a.toMidiSec - b.toMidiSec);
852
+ }
853
+ return sidecar;
854
+ }
855
+ function generatePlaybackTimeline(score, options = {}) {
856
+ const ticksPerQuarterNote = options.ticksPerQuarterNote ?? 480;
857
+ const defaultTempo = options.defaultTempo ?? 120;
858
+ const sequence = generatePlaybackSequence(score);
859
+ const grid = buildGridTimeline(score, ticksPerQuarterNote, sequence, {
860
+ defaultTempo,
861
+ fermataHoldMultiplier: options.fermataHoldMultiplier,
862
+ caesuraSeconds: options.caesuraSeconds,
863
+ breathSeconds: options.breathSeconds,
864
+ tempoRampSteps: options.tempoRampSteps
865
+ });
866
+ return buildTimingSidecar(grid, defaultTempo, ticksPerQuarterNote);
867
+ }
868
+
512
869
  // src/query/index.ts
513
870
  function getNotesForVoice(measure, filter) {
514
871
  return measure.entries.filter((entry) => {
@@ -2062,6 +2419,11 @@ export {
2062
2419
  extractPlaybackControls,
2063
2420
  generatePlaybackSequence,
2064
2421
  hasPlaybackControls,
2422
+ measureEndTick,
2423
+ buildGridTimeline,
2424
+ bpmToUsPerQuarter,
2425
+ buildTimingSidecar,
2426
+ generatePlaybackTimeline,
2065
2427
  STEPS,
2066
2428
  STEP_SEMITONES,
2067
2429
  pitchToSemitone,
package/dist/index.d.mts CHANGED
@@ -2,7 +2,8 @@ import { S as Score, P as Pitch, M as Measure, D as DirectionType, a as Directio
2
2
  export { A as Accidental, j as AccidentalInfo, O as AdjacentNotes, a4 as AssembledLyrics, B as BackupEntry, p as Barline, a5 as BarlineWithContext, a0 as BeamGroup, k as BeamInfo, s as Chord, C as Clef, aa as ClefChangeInfo, w as Credit, v as Defaults, Q as DirectionKind, z as DirectionWithContext, R as DynamicWithContext, m as DynamicsValue, a7 as EndingInfo, E as EntryWithContext, F as ForwardEntry, a2 as HarmonyWithContext, a8 as KeyChangeInfo, K as KeySignature, L as Lyric, a3 as LyricWithContext, g as MeasureAttributes, h as MeasureEntry, l as Notation, a1 as NotationType, t as NoteIteratorItem, i as NoteType, y as NoteWithContext, r as NoteWithPosition, Y as OctaveShiftWithContext, f as Part, e as PartGroup, W as PedalWithContext, H as PositionQueryOptions, u as Print, a6 as RepeatInfo, d as ScoreMetadata, _ as SlurSpan, q as StaffGroup, G as StaffRange, ab as StructuralChanges, U as TempoWithContext, T as TieInfo, Z as TiedNoteGroup, a9 as TimeChangeInfo, n as TimeSignature, o as Transpose, $ as TupletGroup, I as VerticalSlice, V as VoiceGroup, J as VoiceLine, x as VoiceToStaffMap, X as WedgeWithContext } from './types-CkeI8vw6.mjs';
3
3
  import { V as ValidateOptions, a as ValidationResult } from './index-8MkN7sbm.mjs';
4
4
  export { ba as AddArticulationOptions, bj as AddBeamOptions, c8 as AddBowingOptions, ch as AddBreathMarkOptions, ck as AddCaesuraOptions, aW as AddChordOptions, c2 as AddChordSymbolOptions, bS as AddCodaOptions, bc as AddDynamicsOptions, bM as AddEndingOptions, bz as AddFermataOptions, c5 as AddFingeringOptions, bU as AddGraceNoteOptions, b$ as AddHarmonyOptions, bX as AddLyricOptions, bT as AddNavigationOptions, cd as AddOctaveShiftOptions, bB as AddOrnamentOptions, b2 as AddPartOptions, bD as AddPedalOptions, bH as AddRehearsalMarkOptions, bI as AddRepeatBarlineOptions, bK as AddRepeatOptions, bR as AddSegnoOptions, b8 as AddSlurOptions, ca as AddStringNumberOptions, bu as AddTempoOptions, bF as AddTextDirectionOptions, bG as AddTextOptions, b6 as AddTieOptions, b1 as AddVoiceOptions, bx as AddWedgeOptions, bl as AutoBeamOptions, bQ as BarStyle, c7 as BowingType, cg as BreathMarkValue, cj as CaesuraValue, bO as ChangeBarlineOptions, bg as ChangeClefOptions, aX as ChangeNoteDurationOptions, bW as ConvertToGraceOptions, br as CopyNotesMultiMeasureOptions, bo as CopyNotesOptions, bh as CreateTupletOptions, bq as CutNotesOptions, b3 as DuplicatePartOptions, b_ as HarmonyKind, bf as InsertClefChangeOptions, aU as InsertNoteOptions, cM as LocalValidateOptions, b0 as LowerAccidentalOptions, cL as MeasureValidationContext, be as ModifyDynamicsOptions, bw as ModifyTempoOptions, b5 as MoveNoteToStaffOptions, bs as MultiMeasureSelection, bn as NoteSelection, cc as OctaveShiftType, aT as OperationErrorCode, aS as OperationResult, bt as PasteNotesMultiMeasureOptions, bp as PasteNotesOptions, a$ as RaiseAccidentalOptions, bb as RemoveArticulationOptions, bk as RemoveBeamOptions, c9 as RemoveBowingOptions, ci as RemoveBreathMarkOptions, cl as RemoveCaesuraOptions, c3 as RemoveChordSymbolOptions, bd as RemoveDynamicsOptions, bN as RemoveEndingOptions, bA as RemoveFermataOptions, c6 as RemoveFingeringOptions, bV as RemoveGraceNoteOptions, c0 as RemoveHarmonyOptions, bY as RemoveLyricOptions, aV as RemoveNoteOptions, cf as RemoveOctaveShiftOptions, bC as RemoveOrnamentOptions, bE as RemovePedalOptions, bJ as RemoveRepeatBarlineOptions, bL as RemoveRepeatOptions, b9 as RemoveSlurOptions, cb as RemoveStringNumberOptions, bv as RemoveTempoOptions, b7 as RemoveTieOptions, bi as RemoveTupletOptions, by as RemoveWedgeOptions, bP as SetBarlineOptions, bm as SetBeamingOptions, aZ as SetNotePitchBySemitoneOptions, aY as SetNotePitchOptions, b4 as SetStavesOptions, a_ as ShiftNotePitchOptions, ce as StopOctaveShiftOptions, c4 as UpdateChordSymbolOptions, c1 as UpdateHarmonyOptions, bZ as UpdateLyricOptions, cH as ValidationError, cJ as ValidationErrorCode, cD as ValidationException, cK as ValidationLevel, cI as ValidationLocation, K as addArticulation, T as addBeam, aH as addBowing, aO as addBreathMark, aQ as addCaesura, b as addChord, j as addChordNote, p as addChordNoteChecked, aC as addChordSymbol, ao as addCoda, ap as addDaCapo, aq as addDalSegno, M as addDynamics, aj as addEnding, a6 as addFermata, ar as addFine, aF as addFingering, at as addGraceNote, az as addHarmony, aw as addLyric, g as addNote, n as addNoteChecked, aL as addOctaveShift, a8 as addOrnament, x as addPart, aa as addPedal, ae as addRehearsalMark, ah as addRepeat, af as addRepeatBarline, an as addSegno, I as addSlur, aJ as addStringNumber, a1 as addTempo, ad as addText, ac as addTextDirection, G as addTie, as as addToCoda, w as addVoice, a4 as addWedge, cG as assertMeasureValid, co as assertValid, W as autoBeam, al as changeBarline, Q as changeClef, C as changeKey, e as changeNoteDuration, D as changeTime, av as convertToGrace, Y as copyNotes, $ as copyNotesMultiMeasure, R as createTuplet, _ as cutNotes, F as deleteMeasure, h as deleteNote, o as deleteNoteChecked, z as duplicatePart, cC as formatLocation, cF as getMeasureContext, P as insertClefChange, E as insertMeasure, i as insertNote, cn as isValid, l as lowerAccidental, O as modifyDynamics, k as modifyNoteDuration, u as modifyNoteDurationChecked, m as modifyNotePitch, q as modifyNotePitchChecked, a3 as modifyTempo, B as moveNoteToStaff, Z as pasteNotes, a0 as pasteNotesMultiMeasure, f as raiseAccidental, L as removeArticulation, U as removeBeam, aI as removeBowing, aP as removeBreathMark, aR as removeCaesura, aD as removeChordSymbol, N as removeDynamics, ak as removeEnding, a7 as removeFermata, aG as removeFingering, au as removeGraceNote, aA as removeHarmony, ax as removeLyric, r as removeNote, aN as removeOctaveShift, a9 as removeOrnament, y as removePart, ab as removePedal, ai as removeRepeat, ag as removeRepeatBarline, J as removeSlur, aK as removeStringNumber, a2 as removeTempo, H as removeTie, S as removeTuplet, a5 as removeWedge, am as setBarline, X as setBeaming, s as setNotePitch, c as setNotePitchBySemitone, A as setStaves, d as shiftNotePitch, aM as stopOctaveShift, t as transpose, v as transposeChecked, aE as updateChordSymbol, aB as updateHarmony, ay as updateLyric, cm as validate, cr as validateBackupForward, ct as validateBeams, cp as validateDivisions, cq as validateMeasureDuration, cE as validateMeasureLocal, cw as validatePartReferences, cx as validatePartStructure, cu as validateSlurs, cB as validateSlursAcrossMeasures, cy as validateStaffStructure, cs as validateTies, cA as validateTiesAcrossMeasures, cv as validateTuplets, cz as validateVoiceStaff } from './index-8MkN7sbm.mjs';
5
- export { FindNotesFilter, NormalizedPositionOptions, PitchRange, PlaybackControls, PlaybackMeasure, RoundtripMetrics, VoiceFilter, buildVoiceToStaffMap, buildVoiceToStaffMapForPart, countNotes, extractPlaybackControls, findBarlines, findDirectionsByType, findNotes, findNotesWithNotation, generatePlaybackSequence, getAbsolutePosition, getAdjacentNotes, getAllNotes, getAttributesAtMeasure, getBeamGroups, getChordProgression, getChords, getClefChanges, getClefForStaff, getDirections, getDirectionsAtPosition, getDivisions, getDuration, getDynamics, getEffectiveStaff, getEndings, getEntriesAtPosition, getEntriesForStaff, getEntriesInRange, getHarmonies, getHarmonyAtPosition, getKeyChanges, getLyricText, getLyrics, getMeasure, getMeasureByIndex, getMeasureCount, getNextNote, getNormalizedDuration, getNormalizedPosition, getNotesAtPosition, getNotesForStaff, getNotesForVoice, getNotesInRange, getOctaveShifts, getPartById, getPartByIndex, getPartCount, getPartIds, getPartIndex, getPedalMarkings, getPrevNote, getRepeatStructure, getSlurSpans, getStaffRange, getStaveCount, getStaves, getStructuralChanges, getTempoMarkings, getTiedNoteGroups, getTimeChanges, getTupletGroups, getVerseCount, getVerticalSlice, getVoiceLine, getVoiceLineInRange, getVoices, getVoicesForStaff, getWedges, groupByStaff, groupByVoice, hasMultipleStaves, hasNotes, hasPlaybackControls, inferStaff, isRestMeasure, iterateEntries, iterateNotes, measureRoundtrip, scoresEqual, withAbsolutePositions } from './query/index.mjs';
5
+ import { ExpressionOptions, TimingSidecar } from './query/index.mjs';
6
+ export { ExpressionHint, ExpressionKind, FindNotesFilter, NormalizedPositionOptions, PitchRange, PlaybackControls, PlaybackMeasure, RoundtripMetrics, TimingBreakpoint, TimingMapOptions, VoiceFilter, buildVoiceToStaffMap, buildVoiceToStaffMapForPart, countNotes, extractPlaybackControls, findBarlines, findDirectionsByType, findNotes, findNotesWithNotation, generatePlaybackSequence, generatePlaybackTimeline, getAbsolutePosition, getAdjacentNotes, getAllNotes, getAttributesAtMeasure, getBeamGroups, getChordProgression, getChords, getClefChanges, getClefForStaff, getDirections, getDirectionsAtPosition, getDivisions, getDuration, getDynamics, getEffectiveStaff, getEndings, getEntriesAtPosition, getEntriesForStaff, getEntriesInRange, getHarmonies, getHarmonyAtPosition, getKeyChanges, getLyricText, getLyrics, getMeasure, getMeasureByIndex, getMeasureCount, getNextNote, getNormalizedDuration, getNormalizedPosition, getNotesAtPosition, getNotesForStaff, getNotesForVoice, getNotesInRange, getOctaveShifts, getPartById, getPartByIndex, getPartCount, getPartIds, getPartIndex, getPedalMarkings, getPrevNote, getRepeatStructure, getSlurSpans, getStaffRange, getStaveCount, getStaves, getStructuralChanges, getTempoMarkings, getTiedNoteGroups, getTimeChanges, getTupletGroups, getVerseCount, getVerticalSlice, getVoiceLine, getVoiceLineInRange, getVoices, getVoicesForStaff, getWedges, groupByStaff, groupByVoice, hasMultipleStaves, hasNotes, hasPlaybackControls, inferStaff, isRestMeasure, iterateEntries, iterateNotes, measureRoundtrip, scoresEqual, withAbsolutePositions } from './query/index.mjs';
6
7
 
7
8
  declare function parse(input: string | Uint8Array): Score;
8
9
 
@@ -75,8 +76,11 @@ declare function serializeCompressed(score: Score, options?: SerializeOptions):
75
76
 
76
77
  /**
77
78
  * MIDI export options
79
+ *
80
+ * Extends {@link ExpressionOptions} so fermata holds and rit./accel. ramps land
81
+ * in the exported tempo map (and the timing sidecar) identically.
78
82
  */
79
- interface MidiExportOptions {
83
+ interface MidiExportOptions extends ExpressionOptions {
80
84
  /** Ticks per quarter note (default: 480) */
81
85
  ticksPerQuarterNote?: number;
82
86
  /** Default tempo in BPM (default: 120) */
@@ -84,6 +88,22 @@ interface MidiExportOptions {
84
88
  /** Default velocity for notes when no dynamics are present (default: 80) */
85
89
  defaultVelocity?: number;
86
90
  }
91
+ /**
92
+ * A single point on the timing sidecar: a correspondence between a time in the
93
+ * generated MIDI and a conceptual musical position (measure + beat).
94
+ *
95
+ * Breakpoints are emitted at every played-measure start and every tempo change
96
+ * (plus a terminal point). Between two consecutive breakpoints the relationship
97
+ * `midiSec ↔ quarterPos` is linear (tempo is piecewise-constant), so a consumer
98
+ * can interpolate any intermediate time exactly.
99
+ */
100
+ /** Result of {@link exportMidiWithTimingMap}. */
101
+ interface MidiWithTimingMap {
102
+ /** The Standard MIDI File data. */
103
+ midi: Uint8Array;
104
+ /** The MIDI-time ↔ musical-position sidecar. */
105
+ sidecar: TimingSidecar;
106
+ }
87
107
  /**
88
108
  * Export a Score to Standard MIDI File format (SMF Type 1)
89
109
  * @param score - The Score to export
@@ -91,6 +111,18 @@ interface MidiExportOptions {
91
111
  * @returns The MIDI file data as Uint8Array
92
112
  */
93
113
  declare function exportMidi(score: Score, options?: MidiExportOptions): Uint8Array;
114
+ /**
115
+ * Export a Score to MIDI together with a timing sidecar that maps the generated
116
+ * MIDI timeline to conceptual musical positions (measure + beat).
117
+ *
118
+ * The MIDI bytes are byte-for-byte identical to {@link exportMidi}; the sidecar
119
+ * is derived from the same internal time computation so the two can never drift.
120
+ *
121
+ * @param score - The Score to export
122
+ * @param options - Export options (must match those used for any aligned audio)
123
+ * @returns The MIDI data and its timing sidecar
124
+ */
125
+ declare function exportMidiWithTimingMap(score: Score, options?: MidiExportOptions): MidiWithTimingMap;
94
126
 
95
127
  /**
96
128
  * ABC Notation Serializer
@@ -346,4 +378,4 @@ declare function getPartNameMap(score: Score): Record<string, string | undefined
346
378
  */
347
379
  declare function generateId(): string;
348
380
 
349
- export { type AbcSerializeOptions, DirectionEntry, DirectionType, type DirectionTypeOfKind, Measure, type MidiExportOptions, NoteEntry, PartInfo, PartListEntry, Pitch, STEPS, STEP_SEMITONES, Score, type SerializeOptions, ValidateOptions, ValidationResult, decodeBuffer, exportMidi, generateId, getAllPartInfos, getDirectionOfKind, getDirectionsOfKind, getMeasureEndPosition, getPartAbbreviation, getPartInfo, getPartName, getPartNameMap, getSoundDamperPedal, getSoundDynamics, getSoundSoftPedal, getSoundSostenutoPedal, getSoundTempo, hasBeam, hasDirectionOfKind, hasLyrics, hasNotations, hasTie, hasTieStart, hasTieStop, hasTuplet, isChordNote, isCompressed, isCueNote, isGraceNote, isPartInfo, isPitchedNote, isRest, isUnpitchedNote, parse, parseAbc, parseAuto, parseCompressed, parseFile, pitchToSemitone, serialize, serializeAbc, serializeCompressed, serializeToFile };
381
+ export { type AbcSerializeOptions, DirectionEntry, DirectionType, type DirectionTypeOfKind, ExpressionOptions, Measure, type MidiExportOptions, type MidiWithTimingMap, NoteEntry, PartInfo, PartListEntry, Pitch, STEPS, STEP_SEMITONES, Score, type SerializeOptions, TimingSidecar, ValidateOptions, ValidationResult, decodeBuffer, exportMidi, exportMidiWithTimingMap, generateId, getAllPartInfos, getDirectionOfKind, getDirectionsOfKind, getMeasureEndPosition, getPartAbbreviation, getPartInfo, getPartName, getPartNameMap, getSoundDamperPedal, getSoundDynamics, getSoundSoftPedal, getSoundSostenutoPedal, getSoundTempo, hasBeam, hasDirectionOfKind, hasLyrics, hasNotations, hasTie, hasTieStart, hasTieStop, hasTuplet, isChordNote, isCompressed, isCueNote, isGraceNote, isPartInfo, isPitchedNote, isRest, isUnpitchedNote, parse, parseAbc, parseAuto, parseCompressed, parseFile, pitchToSemitone, serialize, serializeAbc, serializeCompressed, serializeToFile };