@viritura/score-engine 0.1.1-next.28 → 0.1.1-next.31

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.js CHANGED
@@ -2213,9 +2213,9 @@ function interpolateBeatAnchors(bounds, beat, endX) {
2213
2213
 
2214
2214
  // packages/score-engine/src/buildInfo.ts
2215
2215
  init_define_import_meta_env();
2216
- var PACKAGE_VERSION = "0.1.1-next.28", BUNDLE_WORKER_FILE = "score-engine.worker.js";
2216
+ var PACKAGE_VERSION = "0.1.1-next.31", BUNDLE_WORKER_FILE = "score-engine.worker.js";
2217
2217
  function buildCommit() {
2218
- return "29a9ddc177a7341276fc760a1004f0dd17bfcbb2";
2218
+ return "90b89f13680a7855b46725024057cc9919dfbc20";
2219
2219
  }
2220
2220
  function bundledAssetBase() {
2221
2221
  let moduleUrl = import.meta.url;
@@ -15714,6 +15714,8 @@ var KEYS = {
15714
15714
  "ending",
15715
15715
  "tempos",
15716
15716
  "segno",
15717
+ "coda",
15718
+ "caesura",
15717
15719
  "fine",
15718
15720
  "jump",
15719
15721
  "_x"
@@ -15777,7 +15779,7 @@ var KEYS = {
15777
15779
  eventLyricLine: /* @__PURE__ */ new Set(["type", "text", "extender"]),
15778
15780
  eventLyricLineExtender: /* @__PURE__ */ new Set(["show", "target", "id"]),
15779
15781
  rest: /* @__PURE__ */ new Set(["staffPosition"]),
15780
- note: /* @__PURE__ */ new Set(["pitch", "id", "ties", "accidentalDisplay", "written"]),
15782
+ note: /* @__PURE__ */ new Set(["pitch", "id", "ties", "accidentalDisplay", "written", "markings"]),
15781
15783
  pitch: /* @__PURE__ */ new Set(["step", "octave", "alter"]),
15782
15784
  tie: /* @__PURE__ */ new Set(["target", "targetType", "side", "lv"]),
15783
15785
  accidentalDisplay: /* @__PURE__ */ new Set(["show", "force", "enclosure"]),
@@ -15797,6 +15799,7 @@ var KEYS = {
15797
15799
  "stress",
15798
15800
  "unstress",
15799
15801
  "breath",
15802
+ "caesura",
15800
15803
  "bowDirection",
15801
15804
  "_x"
15802
15805
  ]),
@@ -16109,6 +16112,8 @@ function scanGlobalMeasure(gm, p, dx) {
16109
16112
  });
16110
16113
  let segno = gm.segno;
16111
16114
  segno && emitUnknown(segno, KEYS.segno, p + ptr("segno"), dx);
16115
+ let coda = gm.coda;
16116
+ coda && emitUnknown(coda, KEYS.segno, p + ptr("coda"), dx);
16112
16117
  let fine = gm.fine;
16113
16118
  fine && emitUnknown(fine, KEYS.fine, p + ptr("fine"), dx);
16114
16119
  let jump = gm.jump;
@@ -16239,19 +16244,575 @@ init_define_import_meta_env();
16239
16244
 
16240
16245
  // packages/midi/src/repeatExpansion.ts
16241
16246
  init_define_import_meta_env();
16242
- function expandMeasureOrder(globalMeasures, hints) {
16243
- if (globalMeasures.length === 0) return [];
16244
- let order = expandRepeats(globalMeasures);
16245
- return expandJumps(globalMeasures, order, hints?.toCodaMeasureIndex);
16247
+
16248
+ // packages/midi/src/cadenzaTiming.ts
16249
+ init_define_import_meta_env();
16250
+
16251
+ // packages/midi/src/tempoMap.ts
16252
+ init_define_import_meta_env();
16253
+
16254
+ // packages/midi/src/tempoModel.ts
16255
+ init_define_import_meta_env();
16256
+ var bpmToSpb = (bpm) => 60 / bpm;
16257
+ function integrateTime(b0, b, bpm0, slope) {
16258
+ let db = b - b0;
16259
+ if (Math.abs(slope) < 1e-6) return bpmToSpb(bpm0) * db;
16260
+ let bpmB = bpm0 + slope * db;
16261
+ return 60 / slope * Math.log(bpmB / bpm0);
16246
16262
  }
16247
- function detectToCodaMeasureIndex(score) {
16248
- for (let part of score.parts)
16249
- for (let i = 0; i < part.measures.length; i++) {
16250
- let expressions = part.measures[i]?.expressions;
16251
- if (!(!expressions || expressions.length === 0) && expressions.some((expr) => /\bto\s*coda\b/i.test(plainTextContent(expr.text))))
16252
- return i;
16263
+ function inverseBeat(dt, bpm0, slope) {
16264
+ return Math.abs(slope) < 1e-6 ? dt * bpm0 / 60 : (bpm0 * Math.exp(dt * slope / 60) - bpm0) / slope;
16265
+ }
16266
+ var TempoModel = class _TempoModel {
16267
+ knots;
16268
+ constructor(knots) {
16269
+ this.knots = knots;
16270
+ }
16271
+ /** Realize selected source-beat intervals on one continuous performance axis. */
16272
+ static concatenate(source, spans) {
16273
+ let regions = [], insertions = [], outputBeat = 0;
16274
+ for (let span of spans) {
16275
+ let boundaries = [
16276
+ span.startBeat,
16277
+ ...source.knots.filter((knot) => knot.beat > span.startBeat && knot.beat < span.endBeat).map((knot) => knot.beat),
16278
+ span.endBeat
16279
+ ];
16280
+ for (let index = 0; index < boundaries.length - 1; index++) {
16281
+ let start = boundaries[index], end = boundaries[index + 1], knot = source.knots[source.knotIndexForBeat(start)], bpm = knot.bpm + knot.slope * (start - knot.beat);
16282
+ regions.push({
16283
+ startBeat: outputBeat + start - span.startBeat,
16284
+ endBeat: outputBeat + end - span.startBeat,
16285
+ startBpm: bpm,
16286
+ endBpm: bpm + knot.slope * (end - start)
16287
+ });
16288
+ }
16289
+ for (let knot of source.knots)
16290
+ knot.beat > span.startBeat && knot.beat <= span.endBeat && knot.insertBefore > 0 && insertions.push({ beat: outputBeat + knot.beat - span.startBeat, seconds: knot.insertBefore });
16291
+ outputBeat += span.endBeat - span.startBeat;
16292
+ }
16293
+ let last = regions.at(-1);
16294
+ return last && regions.push({ startBeat: outputBeat, endBeat: 1 / 0, startBpm: last.endBpm, endBpm: last.endBpm }), _TempoModel.build(regions, insertions);
16295
+ }
16296
+ /**
16297
+ * Build a model from tempo regions and point insertions.
16298
+ *
16299
+ * `regions` need not be pre-sorted or gapless; they are sorted by `startBeat`
16300
+ * and gaps are filled by extending the previous tempo (a held constant). The
16301
+ * first region's start tempo applies from beat 0. `insertions` are point gaps
16302
+ * (caesuras) keyed by beat.
16303
+ */
16304
+ static build(regions, insertions = []) {
16305
+ let sorted = [...regions].sort((a, b) => a.startBeat - b.startBeat), insByBeat = /* @__PURE__ */ new Map();
16306
+ for (let ins of insertions)
16307
+ ins.seconds > 0 && insByBeat.set(ins.beat, (insByBeat.get(ins.beat) ?? 0) + ins.seconds);
16308
+ let boundarySet = /* @__PURE__ */ new Set([0]);
16309
+ for (let r of sorted) boundarySet.add(r.startBeat);
16310
+ for (let ins of insByBeat.keys()) boundarySet.add(ins);
16311
+ let boundaries = [...boundarySet].sort((a, b) => a - b), tempoAt = (beat) => {
16312
+ let active;
16313
+ for (let r of sorted)
16314
+ if (r.startBeat <= beat + 1e-9) active = r;
16315
+ else break;
16316
+ if (!active) {
16317
+ let first = sorted[0];
16318
+ return { bpm: first ? first.startBpm : 120, slope: 0 };
16319
+ }
16320
+ let span = active.endBeat - active.startBeat, slope = span > 0 && Number.isFinite(span) ? (active.endBpm - active.startBpm) / span : 0;
16321
+ return { bpm: active.startBpm + slope * (beat - active.startBeat), slope };
16322
+ }, knots = [], time = 0, prevBeat = boundaries[0], prev = tempoAt(prevBeat), ins0 = insByBeat.get(prevBeat) ?? 0;
16323
+ time += ins0, knots.push({ beat: prevBeat, time, bpm: prev.bpm, slope: prev.slope, insertBefore: ins0 });
16324
+ for (let i = 1; i < boundaries.length; i++) {
16325
+ let beat = boundaries[i];
16326
+ time += integrateTime(prevBeat, beat, prev.bpm, prev.slope);
16327
+ let ins = insByBeat.get(beat) ?? 0;
16328
+ time += ins;
16329
+ let t = tempoAt(beat);
16330
+ knots.push({ beat, time, bpm: t.bpm, slope: t.slope, insertBefore: ins }), prevBeat = beat, prev = t;
16331
+ }
16332
+ return new _TempoModel(knots);
16333
+ }
16334
+ /** Index of the last knot whose beat ≤ `beat`. */
16335
+ knotIndexForBeat(beat) {
16336
+ let k = this.knots, lo = 0, hi = k.length - 1, ans = 0;
16337
+ for (; lo <= hi; ) {
16338
+ let mid = lo + hi >> 1;
16339
+ k[mid].beat <= beat + 1e-9 ? (ans = mid, lo = mid + 1) : hi = mid - 1;
16340
+ }
16341
+ return ans;
16342
+ }
16343
+ /** Index of the last knot whose time ≤ `time`. */
16344
+ knotIndexForTime(time) {
16345
+ let k = this.knots, lo = 0, hi = k.length - 1, ans = 0;
16346
+ for (; lo <= hi; ) {
16347
+ let mid = lo + hi >> 1;
16348
+ k[mid].time <= time + 1e-9 ? (ans = mid, lo = mid + 1) : hi = mid - 1;
16349
+ }
16350
+ return ans;
16351
+ }
16352
+ /**
16353
+ * Absolute time (seconds) at a global quarter-note beat.
16354
+ *
16355
+ * `endpoint` selects how a point insertion EXACTLY at `beat` is treated:
16356
+ * - `"onset"` (default): time AFTER the insertion — what an event STARTING at
16357
+ * `beat` wants (it begins once the gap is over).
16358
+ * - `"release"`: time BEFORE the insertion — what a note ENDING at `beat`
16359
+ * wants (a caesura/gap at the release point isn't part of the note).
16360
+ */
16361
+ timeAtBeat(beat, endpoint = "onset") {
16362
+ let i = this.knotIndexForBeat(beat), knot = this.knots[i], base = knot.time + integrateTime(knot.beat, beat, knot.bpm, knot.slope);
16363
+ return endpoint === "release" && Math.abs(knot.beat - beat) < 1e-9 ? base - knot.insertBefore : base;
16364
+ }
16365
+ /**
16366
+ * Seconds elapsed over `[beat, beat + beats)` — a note/event DURATION. The end
16367
+ * uses release semantics so a point insertion (caesura) exactly at the release
16368
+ * beat is not absorbed into the note's sounding length.
16369
+ */
16370
+ secondsForBeats(beat, beats) {
16371
+ return this.timeAtBeat(beat + beats, "release") - this.timeAtBeat(beat);
16372
+ }
16373
+ /**
16374
+ * Global quarter-note beat at an absolute time (inverse of {@link timeAtBeat}).
16375
+ * Within a point-insertion gap, returns the insertion's beat (position is
16376
+ * frozen during a caesura).
16377
+ */
16378
+ beatAtTime(time) {
16379
+ let i = this.knotIndexForTime(time), knot = this.knots[i], next = this.knots[i + 1];
16380
+ if (next) {
16381
+ let musicEnd = next.time - next.insertBefore;
16382
+ if (time >= musicEnd - 1e-9) return next.beat;
16253
16383
  }
16254
- return -1;
16384
+ return knot.beat + inverseBeat(time - knot.time, knot.bpm, knot.slope);
16385
+ }
16386
+ /** Seconds-per-quarter-note beat at a global beat. */
16387
+ spbAtBeat(beat) {
16388
+ let i = this.knotIndexForBeat(beat), knot = this.knots[i], bpmHere = knot.bpm + knot.slope * (beat - knot.beat);
16389
+ return bpmToSpb(bpmHere);
16390
+ }
16391
+ /** Quarter-note BPM at a global beat. */
16392
+ bpmAtBeat(beat) {
16393
+ return 60 / this.spbAtBeat(beat);
16394
+ }
16395
+ /**
16396
+ * Generate click times (seconds) for a metronome / studio click track over
16397
+ * `[startBeat, endBeat)`. `clickBeats` are the beat positions to click,
16398
+ * expressed as offsets within each "grid step" — but for the common case
16399
+ * callers pass a flat list of global beat positions and we just map them.
16400
+ * Returns the absolute time of every beat in `beats`.
16401
+ */
16402
+ clickTimes(beats) {
16403
+ return beats.map((b) => this.timeAtBeat(b));
16404
+ }
16405
+ };
16406
+
16407
+ // packages/midi/src/tempoMap.ts
16408
+ function playbackNominalTime(gm) {
16409
+ return gm.__playbackNominalTime;
16410
+ }
16411
+ var DEFAULT_BPM = 120, DEFAULT_TIME = { count: 4, unit: 4 };
16412
+ function measureBeatsFromTime(ts) {
16413
+ return ts.count * 4 / ts.unit;
16414
+ }
16415
+ function tempoNoteBeats(base, dots) {
16416
+ let baseBeats = DURATION_BEATS[base] ?? 1;
16417
+ return dots ? baseBeats * (2 - Math.pow(2, -dots)) : baseBeats;
16418
+ }
16419
+ function effectiveQpm(bpm, tempoBase, tempoDots) {
16420
+ return bpm * tempoNoteBeats(tempoBase, tempoDots);
16421
+ }
16422
+ function buildTempoMap(globalMeasures, holds) {
16423
+ let tempoMap = [], measureStartTimes = [], currentQpm = DEFAULT_BPM, currentSpq = 60 / currentQpm, currentTime = { count: DEFAULT_TIME.count, unit: DEFAULT_TIME.unit }, absoluteTime = 0, hasInitialTempo = !1;
16424
+ for (let m = 0; m < globalMeasures.length; m++) {
16425
+ let gm = globalMeasures[m], nominalTime = playbackNominalTime(gm) ?? gm.time;
16426
+ nominalTime && (currentTime = { count: nominalTime.count, unit: nominalTime.unit });
16427
+ let mBeats = measureBeatsFromTime(gm.time ?? currentTime);
16428
+ measureStartTimes.push(absoluteTime);
16429
+ let measureStartSpq = currentSpq, changes = [];
16430
+ if (gm.tempos && gm.tempos.length > 0) {
16431
+ for (let tempo of gm.tempos) {
16432
+ let beatOffset = tempo.location ? tempo.location.fraction[0] / tempo.location.fraction[1] * mBeats : 0, qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);
16433
+ changes.push({ beatOffset, qpm });
16434
+ }
16435
+ changes.sort((a, b) => a.beatOffset - b.beatOffset);
16436
+ }
16437
+ let beatCursor = 0;
16438
+ for (let change of changes)
16439
+ change.beatOffset > beatCursor && (absoluteTime += (change.beatOffset - beatCursor) * currentSpq, beatCursor = change.beatOffset), tempoMap.push({
16440
+ measureIndex: m,
16441
+ beatInMeasure: change.beatOffset,
16442
+ timeSeconds: absoluteTime,
16443
+ bpm: change.qpm
16444
+ }), m === 0 && change.beatOffset === 0 && (hasInitialTempo = !0), currentQpm = change.qpm, currentSpq = 60 / currentQpm;
16445
+ mBeats > beatCursor && (absoluteTime += (mBeats - beatCursor) * currentSpq);
16446
+ let measureHolds = holds?.[m];
16447
+ if (measureHolds)
16448
+ for (let hold of measureHolds)
16449
+ absoluteTime += hold.extraBeats * measureStartSpq;
16450
+ }
16451
+ return hasInitialTempo || tempoMap.unshift({
16452
+ measureIndex: 0,
16453
+ beatInMeasure: 0,
16454
+ timeSeconds: 0,
16455
+ bpm: DEFAULT_BPM
16456
+ }), { tempoMap, measureStartTimes };
16457
+ }
16458
+ function buildTempoModel(globalMeasures, holds) {
16459
+ let regions = [], measureStartBeats = [], cumBeats = 0, currentTime = { count: DEFAULT_TIME.count, unit: DEFAULT_TIME.unit }, gradualDecls = [];
16460
+ for (let m = 0; m < globalMeasures.length; m++) {
16461
+ let gm = globalMeasures[m], nominalTime = playbackNominalTime(gm) ?? gm.time;
16462
+ nominalTime && (currentTime = { count: nominalTime.count, unit: nominalTime.unit });
16463
+ let mBeats = measureBeatsFromTime(gm.time ?? currentTime);
16464
+ if (measureStartBeats.push(cumBeats), gm.tempos && gm.tempos.length > 0) {
16465
+ let sorted = [...gm.tempos].sort((a, b) => tempoBeat(a, mBeats) - tempoBeat(b, mBeats));
16466
+ for (let tempo of sorted) {
16467
+ let startBeat = cumBeats + tempoBeat(tempo, mBeats), qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);
16468
+ regions.push({ startBeat, endBeat: 1 / 0, startBpm: qpm, endBpm: qpm });
16469
+ }
16470
+ }
16471
+ gm.gradualTempo && gradualDecls.push({
16472
+ declIdx: m,
16473
+ gt: gm.gradualTempo,
16474
+ startBeat: cumBeats + posBeats(gm.gradualTempo.position.fraction)
16475
+ }), cumBeats += mBeats;
16476
+ }
16477
+ if ((regions.length === 0 || regions.every((r) => r.startBeat > 1e-9)) && regions.unshift({ startBeat: 0, endBeat: 1 / 0, startBpm: DEFAULT_BPM, endBpm: DEFAULT_BPM }), gradualDecls.length > 0) {
16478
+ let idToIdx = buildMeasureIdToIndex(globalMeasures), constModel = TempoModel.build(regions);
16479
+ for (let { declIdx, gt, startBeat } of gradualDecls) {
16480
+ let endBeat = resolveGradualEndBeat(gt.end, declIdx, globalMeasures, measureStartBeats, idToIdx);
16481
+ if (endBeat === void 0 || endBeat <= startBeat + 1e-9) continue;
16482
+ let startBpm = gt.startBpm ?? constModel.bpmAtBeat(startBeat);
16483
+ regions.push({ startBeat, endBeat, startBpm, endBpm: gt.endBpm }), regions.some((r) => Math.abs(r.startBeat - endBeat) < 1e-9) || regions.push({ startBeat: endBeat, endBeat: 1 / 0, startBpm: gt.endBpm, endBpm: gt.endBpm });
16484
+ }
16485
+ }
16486
+ regions.sort((a, b) => a.startBeat - b.startBeat);
16487
+ for (let i = 0; i < regions.length - 1; i++)
16488
+ regions[i].endBeat = regions[i + 1].startBeat;
16489
+ let regionsModel = TempoModel.build(regions), insertions = [];
16490
+ if (holds)
16491
+ for (let m = 0; m < holds.length; m++) {
16492
+ let measureHolds = holds[m];
16493
+ if (!measureHolds || measureHolds.length === 0) continue;
16494
+ let startBeat = measureStartBeats[m], spb = regionsModel.spbAtBeat(startBeat);
16495
+ for (let hold of measureHolds)
16496
+ insertions.push({ beat: startBeat + hold.atBeat, seconds: hold.extraBeats * spb });
16497
+ }
16498
+ let model = insertions.length > 0 ? TempoModel.build(regions, insertions) : regionsModel, measureStartTimes = measureStartBeats.map((b) => model.timeAtBeat(b));
16499
+ return { model, measureStartTimes, measureStartBeats, totalBeats: cumBeats };
16500
+ }
16501
+ function posBeats(frac) {
16502
+ return !frac || frac[1] === 0 ? 0 : frac[0] / frac[1] * 4;
16503
+ }
16504
+ function buildMeasureIdToIndex(globalMeasures) {
16505
+ let m = /* @__PURE__ */ new Map();
16506
+ return globalMeasures.forEach((gm, i) => {
16507
+ gm.id && !m.has(gm.id) && m.set(gm.id, i);
16508
+ }), m;
16509
+ }
16510
+ function resolveGradualEndBeat(end, fromIdx, globalMeasures, measureStartBeats, idToIdx) {
16511
+ let idx = idToIdx.get(end.measure);
16512
+ if (idx === void 0) {
16513
+ let parsed = Number.parseInt(end.measure, 10);
16514
+ !Number.isNaN(parsed) && parsed >= 0 && parsed < globalMeasures.length && (idx = parsed);
16515
+ }
16516
+ idx === void 0 && (idx = fromIdx);
16517
+ let start = measureStartBeats[idx];
16518
+ if (start !== void 0)
16519
+ return start + posBeats(end.position.fraction);
16520
+ }
16521
+ function tempoBeat(tempo, mBeats) {
16522
+ return tempo.location ? tempo.location.fraction[0] / tempo.location.fraction[1] * mBeats : 0;
16523
+ }
16524
+
16525
+ // packages/midi/src/cadenzaTiming.ts
16526
+ function contentBeats(content) {
16527
+ let beats = 0;
16528
+ for (let item of content)
16529
+ switch (item.type) {
16530
+ case "event":
16531
+ beats += durationBeats2(item.duration.base, item.duration.dots);
16532
+ break;
16533
+ case "space":
16534
+ beats += item.duration[0] / item.duration[1] * 4;
16535
+ break;
16536
+ case "tuplet":
16537
+ case "tremolo":
16538
+ beats += durationBeats2(item.outer.duration.base, item.outer.duration.dots) * item.outer.multiple;
16539
+ break;
16540
+ case "grace":
16541
+ break;
16542
+ }
16543
+ return beats;
16544
+ }
16545
+ function durationBeats2(base, dots) {
16546
+ let beats = DURATION_BEATS[base];
16547
+ return dots ? beats * (2 - Math.pow(2, -dots)) : beats;
16548
+ }
16549
+ function writtenMeasureBeats(score, measureIndex, minimumBeats) {
16550
+ return score.parts.reduce((maxBeats, part) => {
16551
+ let measure = part.measures[measureIndex];
16552
+ return measure ? Math.max(
16553
+ maxBeats,
16554
+ ...measure.sequences.filter((sequence) => !sequence.fullMeasure).map((sequence) => contentBeats(sequence.content))
16555
+ ) : maxBeats;
16556
+ }, minimumBeats);
16557
+ }
16558
+ function playbackGlobalMeasures(score, measureOrder) {
16559
+ let activeBeats = 4, activeTime = { count: 4, unit: 4 }, activeTempo = {
16560
+ bpm: DEFAULT_BPM,
16561
+ value: { base: "quarter" }
16562
+ }, resolved = score.global.measures.map((original, measureIndex) => {
16563
+ original.time && (activeTime = original.time);
16564
+ let measure = {
16565
+ ...original,
16566
+ time: { ...activeTime },
16567
+ tempos: original.tempos?.some((tempo) => !tempo.location || tempo.location.fraction[0] === 0) ? original.tempos : [{ ...activeTempo, location: { fraction: [0, 1] } }, ...original.tempos ?? []]
16568
+ };
16569
+ if (original.tempos?.length && (activeTempo = [...original.tempos].sort(
16570
+ (a, b) => (a.location?.fraction[0] ?? 0) / (a.location?.fraction[1] ?? 1) - (b.location?.fraction[0] ?? 0) / (b.location?.fraction[1] ?? 1)
16571
+ ).at(-1)), measure.time && (activeBeats = measure.time.count * 4 / measure.time.unit), measureIndex === 0 && measure.number === 0) {
16572
+ let pickupBeats = writtenMeasureBeats(score, measureIndex, 0);
16573
+ if (pickupBeats > 0)
16574
+ return {
16575
+ ...measure,
16576
+ __playbackNominalTime: measure.time ? { ...measure.time } : void 0,
16577
+ time: {
16578
+ ...measure.time,
16579
+ count: pickupBeats,
16580
+ unit: 4
16581
+ }
16582
+ };
16583
+ }
16584
+ if (measure.time?.display !== "senzaMisura") return measure;
16585
+ let cadenzaBeats = writtenMeasureBeats(score, measureIndex, activeBeats);
16586
+ return {
16587
+ ...measure,
16588
+ time: {
16589
+ ...measure.time,
16590
+ count: cadenzaBeats,
16591
+ unit: 4
16592
+ }
16593
+ };
16594
+ });
16595
+ return measureOrder.map((index) => resolved[index]);
16596
+ }
16597
+ function suppressCadenzaFermataHolds(measures, holds) {
16598
+ return holds.map(
16599
+ (measureHolds, index) => measures[index]?.time?.display === "senzaMisura" ? measureHolds.filter((hold) => hold.kind !== "fermata") : [...measureHolds]
16600
+ );
16601
+ }
16602
+
16603
+ // packages/midi/src/navigation/index.ts
16604
+ init_define_import_meta_env();
16605
+
16606
+ // packages/midi/src/navigation/itinerary.ts
16607
+ init_define_import_meta_env();
16608
+ function beatInPlayedSpan(beat, span) {
16609
+ return span === void 0 || beat >= span.startBeat - 1e-9 && (beat < span.endBeat - 1e-9 || span.endBeat === span.measureBeats && Math.abs(beat - span.endBeat) < 1e-9);
16610
+ }
16611
+ function playedSpansContinue(span, next) {
16612
+ return next !== void 0 && span.endBeat === span.measureBeats && next.startBeat === 0 && next.measureIndex === span.measureIndex + 1;
16613
+ }
16614
+ var NavigationError = class extends Error {
16615
+ constructor(message, pointers) {
16616
+ super(`${message} (${pointers.join(", ")})`);
16617
+ this.pointers = pointers;
16618
+ this.name = "NavigationError";
16619
+ }
16620
+ pointers;
16621
+ }, EPS = 1e-9, MAX_SPANS = 1e5;
16622
+ function locationBeat(position, length, pointer) {
16623
+ let [numerator, denominator] = position.fraction, beat = numerator * 4 / denominator;
16624
+ if (!Number.isFinite(beat) || denominator <= 0 || beat < 0 || beat > length + EPS)
16625
+ throw new NavigationError("Navigation location is outside its measure", [pointer]);
16626
+ return Math.min(beat, length);
16627
+ }
16628
+ function resolveDestinations(measures, measureLengths) {
16629
+ let destinations = (kind) => measures.flatMap((measure, measureIndex) => {
16630
+ let marker = measure[kind];
16631
+ return marker ? [
16632
+ {
16633
+ measureIndex,
16634
+ beat: locationBeat(
16635
+ marker.location,
16636
+ measureLengths[measureIndex],
16637
+ `/global/measures/${measureIndex}/${kind}/location`
16638
+ )
16639
+ }
16640
+ ] : [];
16641
+ }), markers = { segno: destinations("segno"), coda: destinations("coda"), fine: destinations("fine") }, required = (kind, source) => {
16642
+ let found = markers[kind];
16643
+ if (found.length !== 1)
16644
+ throw new NavigationError(`Navigation requires exactly one ${kind} destination`, [
16645
+ `/global/measures/${source}/jump`,
16646
+ ...found.map(({ measureIndex }) => `/global/measures/${measureIndex}/${kind}`)
16647
+ ]);
16648
+ return found[0];
16649
+ };
16650
+ for (let index = 0; index < measures.length; index++) {
16651
+ let jump = measures[index]?.jump;
16652
+ if (!jump) continue;
16653
+ let type = jump.type;
16654
+ if (locationBeat(jump.location, measureLengths[index], `/global/measures/${index}/jump/location`), ["segno", "dsAlFine", "dsAlCoda"].includes(type) && required("segno", index), ["dcAlFine", "dsAlFine"].includes(type) && required("fine", index), ["dcAlCoda", "dsAlCoda", "toCoda"].includes(type) && required("coda", index), ["dcAlCoda", "dsAlCoda"].includes(type) && !measures.some((measure) => String(measure.jump?.type) === "toCoda"))
16655
+ throw new NavigationError("Al Coda requires an explicit To Coda instruction", [`/global/measures/${index}/jump`]);
16656
+ if (!["dc", "dcAlCoda", "dcAlFine", "segno", "dsAlFine", "dsAlCoda", "toCoda"].includes(type))
16657
+ throw new NavigationError(`Unsupported jump type: ${type}`, [`/global/measures/${index}/jump/type`]);
16658
+ }
16659
+ return { markers, required };
16660
+ }
16661
+ function nextInstruction(measure, cursor, length, state, fine, taken, initialOrder) {
16662
+ let index = cursor.measureIndex, jump = measure.jump, type = jump ? String(jump.type) : "", jumpBeat = jump ? locationBeat(jump.location, length, `/global/measures/${index}/jump/location`) : 1 / 0, fineBeat = state.fineArmed && fine?.measureIndex === index ? fine.beat : 1 / 0, jumpActive = jump !== void 0 && !taken.has(index) && jumpBeat >= cursor.beat - EPS && (type !== "toCoda" || state.codaArmed) && (state.replay || initialOrder.lastIndexOf(index) === state.orderIndex), stop = fineBeat >= cursor.beat - EPS ? Math.min(length, fineBeat) : length, endBeat = jumpActive ? Math.min(stop, jumpBeat) : stop;
16663
+ return {
16664
+ type,
16665
+ jumpActive,
16666
+ jumpBeat,
16667
+ fineReached: fineBeat >= cursor.beat - EPS && fineBeat <= endBeat + EPS,
16668
+ endBeat
16669
+ };
16670
+ }
16671
+ function compileNavigation(measures, initialOrder, measureLengths) {
16672
+ let { markers, required } = resolveDestinations(measures, measureLengths), result = [], taken = /* @__PURE__ */ new Set(), state = { fineArmed: !1, codaArmed: !1, replay: !1, orderIndex: 0 }, cursor = { measureIndex: initialOrder[0] ?? 0, beat: 0 }, transitions = 0;
16673
+ for (; cursor.measureIndex < measures.length && (state.replay || state.orderIndex < initialOrder.length); ) {
16674
+ if (++transitions > MAX_SPANS)
16675
+ throw new NavigationError("Navigation exceeds the safe playback traversal limit", ["/global/measures"]);
16676
+ let index = cursor.measureIndex, measure = measures[index], length = measureLengths[index], { type, jumpActive, jumpBeat, fineReached, endBeat } = nextInstruction(
16677
+ measure,
16678
+ cursor,
16679
+ length,
16680
+ state,
16681
+ markers.fine[0],
16682
+ taken,
16683
+ initialOrder
16684
+ );
16685
+ if (endBeat > cursor.beat + EPS && result.push({ measureIndex: index, startBeat: cursor.beat, endBeat, measureBeats: length }), state.fineArmed && fineReached) {
16686
+ state.fineArmed = !1;
16687
+ break;
16688
+ }
16689
+ if (jumpActive && jumpBeat <= endBeat + EPS) {
16690
+ taken.add(index), state.replay = !0, type === "toCoda" ? (state.codaArmed = !1, cursor = required("coda", index)) : (state.fineArmed = type === "dcAlFine" || type === "dsAlFine", state.codaArmed = type === "dcAlCoda" || type === "dsAlCoda", cursor = ["segno", "dsAlFine", "dsAlCoda"].includes(type) ? required("segno", index) : { measureIndex: 0, beat: 0 });
16691
+ continue;
16692
+ }
16693
+ state.orderIndex++, cursor = { measureIndex: state.replay ? index + 1 : initialOrder[state.orderIndex] ?? measures.length, beat: 0 };
16694
+ }
16695
+ if (state.fineArmed || state.codaArmed)
16696
+ throw new NavigationError("The armed Fine or To Coda instruction is not reachable on replay", ["/global/measures"]);
16697
+ return result;
16698
+ }
16699
+
16700
+ // packages/midi/src/navigation/realization.ts
16701
+ init_define_import_meta_env();
16702
+ function authoredOnsetInSpan(event, start, end) {
16703
+ return event.type === "noteOn" && event.scoreBeat !== void 0 && event.scoreBeat >= start - 1e-9 && event.scoreBeat < end - 1e-9;
16704
+ }
16705
+ function continuousEndBeat(spans, index, startBeat) {
16706
+ let end = startBeat + spans[index].endBeat;
16707
+ for (let nextIndex = index; playedSpansContinue(spans[nextIndex], spans[nextIndex + 1]); nextIndex++)
16708
+ end += spans[nextIndex].measureBeats - spans[nextIndex].endBeat + spans[nextIndex + 1].endBeat;
16709
+ return end;
16710
+ }
16711
+ function spanTempoMap(timeline, index, span, model, outputBeat) {
16712
+ let entries = [
16713
+ {
16714
+ measureIndex: index,
16715
+ beatInMeasure: 0,
16716
+ timeSeconds: model.timeAtBeat(outputBeat),
16717
+ bpm: model.bpmAtBeat(outputBeat)
16718
+ }
16719
+ ];
16720
+ for (let entry of timeline.tempoMap) {
16721
+ if (entry.measureIndex !== index || entry.beatInMeasure <= span.startBeat + 1e-9 || entry.beatInMeasure >= span.endBeat - 1e-9)
16722
+ continue;
16723
+ let beatInMeasure = entry.beatInMeasure - span.startBeat;
16724
+ entries.push({
16725
+ ...entry,
16726
+ beatInMeasure,
16727
+ timeSeconds: model.timeAtBeat(outputBeat + beatInMeasure)
16728
+ });
16729
+ }
16730
+ return entries;
16731
+ }
16732
+ function realizeNavigation(timeline, spans) {
16733
+ let sourceSpans = spans.map((span, index) => ({
16734
+ startBeat: timeline.measureStartBeats[index] + span.startBeat,
16735
+ endBeat: timeline.measureStartBeats[index] + span.endBeat
16736
+ })), model = TempoModel.concatenate(timeline.model, sourceSpans), events = [], measureStartBeats = [], tempoMap = [], outputBeat = 0, activeNotes = /* @__PURE__ */ new Map();
16737
+ for (let index = 0; index < spans.length; index++) {
16738
+ let span = spans[index], source = sourceSpans[index], startTime = timeline.model.timeAtBeat(source.startBeat), endTime = timeline.model.timeAtBeat(source.endBeat), controllers = /* @__PURE__ */ new Map(), segmentStart = events.length;
16739
+ measureStartBeats.push(outputBeat), tempoMap.push(...spanTempoMap(timeline, index, span, model, outputBeat));
16740
+ let translate = (time) => model.timeAtBeat(outputBeat) + Math.min(endTime - startTime, Math.max(0, time - startTime)), noteKey = (event) => `${event.partIndex}:${event.playbackLaneId ?? event.channel}:${event.midiNote}:${event.drumKitProgram ?? ""}`;
16741
+ for (let event of timeline.events) {
16742
+ let authoredOnset = authoredOnsetInSpan(event, source.startBeat, source.endBeat);
16743
+ if (event.time < startTime - 1e-9 && !authoredOnset) {
16744
+ (event.type === "controlChange" || event.type === "programChange") && controllers.set(
16745
+ `${event.partIndex}:${event.playbackLaneId ?? event.channel}:${event.type}:${event.cc ?? ""}`,
16746
+ event
16747
+ );
16748
+ continue;
16749
+ }
16750
+ if (!(event.time > endTime + 1e-9 && !authoredOnset))
16751
+ if (event.type === "noteOn") {
16752
+ let beat = event.scoreBeat ?? timeline.model.beatAtTime(event.time);
16753
+ if (beat < source.startBeat - 1e-9 || beat >= source.endBeat - 1e-9) continue;
16754
+ let output = {
16755
+ ...event,
16756
+ time: translate(event.time),
16757
+ scoreBeat: outputBeat + beat - source.startBeat,
16758
+ scoreDurationBeats: event.scoreDurationBeats === void 0 ? void 0 : Math.min(
16759
+ event.scoreDurationBeats,
16760
+ continuousEndBeat(spans, index, timeline.measureStartBeats[index]) - beat
16761
+ )
16762
+ };
16763
+ events.push(output);
16764
+ let key = noteKey(event), pending = activeNotes.get(key) ?? [];
16765
+ pending.push(output), activeNotes.set(key, pending);
16766
+ } else if (event.type === "noteOff") {
16767
+ let pending = activeNotes.get(noteKey(event));
16768
+ if (!pending?.length) continue;
16769
+ pending.shift(), events.push({ ...event, time: translate(event.time) });
16770
+ } else event.time < endTime - 1e-9 && events.push({ ...event, time: translate(event.time) });
16771
+ }
16772
+ events.splice(
16773
+ segmentStart,
16774
+ 0,
16775
+ ...Array.from(controllers.values(), (event) => ({ ...event, time: model.timeAtBeat(outputBeat) }))
16776
+ );
16777
+ let next = spans[index + 1];
16778
+ if (!playedSpansContinue(span, next))
16779
+ for (let pending of activeNotes.values()) {
16780
+ for (let onset of pending)
16781
+ events.push({
16782
+ ...onset,
16783
+ type: "noteOff",
16784
+ velocity: 0,
16785
+ time: model.timeAtBeat(outputBeat + span.endBeat - span.startBeat, "release")
16786
+ });
16787
+ pending.length = 0;
16788
+ }
16789
+ outputBeat += span.endBeat - span.startBeat;
16790
+ }
16791
+ let rank = { noteOff: 0, programChange: 1, controlChange: 2, noteOn: 3 };
16792
+ return events.sort((a, b) => a.time - b.time || rank[a.type] - rank[b.type]), {
16793
+ ...timeline,
16794
+ events,
16795
+ model,
16796
+ tempoMap,
16797
+ totalBeats: outputBeat,
16798
+ duration: model.timeAtBeat(outputBeat),
16799
+ measureStartBeats,
16800
+ measureStartTimes: measureStartBeats.map((beat) => model.timeAtBeat(beat)),
16801
+ playedSpans: [...spans]
16802
+ };
16803
+ }
16804
+
16805
+ // packages/midi/src/repeatExpansion.ts
16806
+ function expandPlaybackItinerary(measures, lengths) {
16807
+ let activeBeats = 4, measureLengths = lengths ?? measures.map((measure) => (measure.time && (activeBeats = measure.time.count * 4 / measure.time.unit), activeBeats));
16808
+ return compileNavigation(measures, expandRepeats(measures), measureLengths);
16809
+ }
16810
+ function scorePlaybackItinerary(score) {
16811
+ let indices = score.global.measures.map((_, index) => index), lengths = playbackGlobalMeasures(score, indices).map((measure) => {
16812
+ let time = measure.time;
16813
+ return time ? time.count * 4 / time.unit : 4;
16814
+ });
16815
+ return expandPlaybackItinerary(score.global.measures, lengths);
16255
16816
  }
16256
16817
  function appendVoltaPass(order, endings, pass, startIdx, endIdx) {
16257
16818
  for (let j = startIdx; j <= endIdx; j++) {
@@ -16264,7 +16825,12 @@ function expandRepeats(globalMeasures) {
16264
16825
  for (; i < len; ) {
16265
16826
  let gm = globalMeasures[i];
16266
16827
  if (gm.repeatEnd) {
16267
- let repeatTimes = gm.repeatEnd.times ?? 2, repeatStartIdx = 0;
16828
+ let repeatTimes = gm.repeatEnd.times ?? 2;
16829
+ if (!Number.isInteger(repeatTimes) || repeatTimes < 1 || repeatTimes > 1e3)
16830
+ throw new NavigationError("Repeat count must be an integer between 1 and 1000", [
16831
+ `/global/measures/${i}/repeatEnd/times`
16832
+ ]);
16833
+ let repeatStartIdx = 0;
16268
16834
  for (let j = i; j >= 0; j--)
16269
16835
  if (globalMeasures[j].repeatStart) {
16270
16836
  repeatStartIdx = j;
@@ -16294,45 +16860,6 @@ function expandRepeats(globalMeasures) {
16294
16860
  }
16295
16861
  return order;
16296
16862
  }
16297
- function appendRange(order, start, endInclusive) {
16298
- for (let measure = start; measure <= endInclusive; measure++) order.push(measure);
16299
- }
16300
- function appendAlCodaReplay(result, replayStart, codaIdx, measureCount, toCodaMeasureIdx) {
16301
- let replayEnd = toCodaMeasureIdx !== void 0 && toCodaMeasureIdx >= replayStart && toCodaMeasureIdx < codaIdx ? toCodaMeasureIdx : codaIdx - 1;
16302
- appendRange(result, replayStart, replayEnd), appendRange(result, codaIdx, measureCount - 1);
16303
- }
16304
- function expandJumps(globalMeasures, initialOrder, toCodaMeasureIdx) {
16305
- let segnoIdx = -1, fineIdx = -1, codaIdx = -1;
16306
- for (let m = 0; m < globalMeasures.length; m++) {
16307
- let gm = globalMeasures[m];
16308
- gm.segno && (segnoIdx = m), gm.fine && (fineIdx = m), gm.coda && (codaIdx = m);
16309
- }
16310
- let jumpOrderPos = -1, jumpMeasureIdx = -1;
16311
- for (let p = 0; p < initialOrder.length; p++) {
16312
- let mi = initialOrder[p];
16313
- if (globalMeasures[mi].jump) {
16314
- jumpOrderPos = p, jumpMeasureIdx = mi;
16315
- break;
16316
- }
16317
- }
16318
- if (jumpOrderPos === -1) return [...initialOrder];
16319
- let jump = globalMeasures[jumpMeasureIdx].jump, result = initialOrder.slice(0, jumpOrderPos + 1);
16320
- switch (jump.type) {
16321
- case "dsAlFine":
16322
- segnoIdx >= 0 && fineIdx >= 0 && appendRange(result, segnoIdx, fineIdx);
16323
- break;
16324
- case "dsAlCoda":
16325
- segnoIdx >= 0 && codaIdx >= 0 && appendAlCodaReplay(result, segnoIdx, codaIdx, globalMeasures.length, toCodaMeasureIdx);
16326
- break;
16327
- case "dcAlCoda":
16328
- codaIdx >= 0 && appendAlCodaReplay(result, 0, codaIdx, globalMeasures.length, toCodaMeasureIdx);
16329
- break;
16330
- case "segno":
16331
- segnoIdx >= 0 && appendRange(result, segnoIdx, globalMeasures.length - 1);
16332
- break;
16333
- }
16334
- return result;
16335
- }
16336
16863
 
16337
16864
  // packages/midi/src/measureRepeats.ts
16338
16865
  init_define_import_meta_env();
@@ -16671,7 +17198,7 @@ function fermataMultiplier(fermata) {
16671
17198
  function caesuraBeats(style) {
16672
17199
  return style === "short" ? CAESURA_SHORT_BEATS : CAESURA_BEATS;
16673
17200
  }
16674
- function durationBeats2(d) {
17201
+ function durationBeats3(d) {
16675
17202
  let base = DURATION_BEATS[d.base] ?? 1;
16676
17203
  return d.dots ? base * (2 - Math.pow(2, -d.dots)) : base;
16677
17204
  }
@@ -16689,16 +17216,16 @@ function collectFromContent(content, startBeat, ratio, spans, caesuraByBeat) {
16689
17216
  let cursor = startBeat;
16690
17217
  for (let item of content)
16691
17218
  if (item.type === "event") {
16692
- let dur = durationBeats2(item.duration) * ratio, endBeat = cursor + dur;
17219
+ let dur = durationBeats3(item.duration) * ratio, endBeat = cursor + dur;
16693
17220
  if (item.fermata && item.fermata.duration !== "none") {
16694
17221
  let mult = fermataMultiplier(item.fermata);
16695
17222
  dur > 0 && mult > 1 && spans.push({ onset: cursor, end: endBeat, duration: dur, multiplier: mult });
16696
17223
  }
16697
17224
  item.markings?.caesura && mergeMax(caesuraByBeat, endBeat, caesuraBeats(item.markings.caesura.style)), cursor = endBeat;
16698
17225
  } else if (item.type === "tuplet") {
16699
- let outerBeats = durationBeats2(item.outer.duration) * item.outer.multiple, innerBeats = durationBeats2(item.inner.duration) * item.inner.multiple, childRatio = innerBeats > 0 ? outerBeats / innerBeats * ratio : ratio;
17226
+ let outerBeats = durationBeats3(item.outer.duration) * item.outer.multiple, innerBeats = durationBeats3(item.inner.duration) * item.inner.multiple, childRatio = innerBeats > 0 ? outerBeats / innerBeats * ratio : ratio;
16700
17227
  collectFromContent(item.content, cursor, childRatio, spans, caesuraByBeat), cursor += outerBeats * ratio;
16701
- } else item.type === "tremolo" ? cursor += durationBeats2(item.outer.duration) * item.outer.multiple * ratio : item.type === "space" && (cursor += item.duration[0] / item.duration[1] * 4 * ratio);
17228
+ } else item.type === "tremolo" ? cursor += durationBeats3(item.outer.duration) * item.outer.multiple * ratio : item.type === "space" && (cursor += item.duration[0] / item.duration[1] * 4 * ratio);
16702
17229
  return cursor - startBeat;
16703
17230
  }
16704
17231
  function collectPartMeasureHolds(partMeasure, originalMeasureIndex, staffMeterTable, spans, caesuraByBeat) {
@@ -16733,94 +17260,25 @@ function buildHoldSchedule(score, measureOrder, globalMeasures) {
16733
17260
  let origIdx = measureOrder[expandedIdx], fermataSpans = [], caesuraByBeat = /* @__PURE__ */ new Map();
16734
17261
  for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
16735
17262
  let part = score.parts[partIndex];
16736
- collectPartMeasureHolds(
16737
- part.measures[origIdx],
16738
- origIdx,
16739
- staffMeterTables[partIndex],
16740
- fermataSpans,
16741
- caesuraByBeat
16742
- );
16743
- }
16744
- let globalCaesura = globalMeasures[origIdx]?.caesura;
16745
- if (globalCaesura) {
16746
- let mBeats = measureBeats2(resolveTimeSig(globalMeasures, origIdx));
16747
- mergeMax(caesuraByBeat, mBeats, caesuraBeats(globalCaesura.style));
16748
- }
16749
- let holds = groupFermataSpans(fermataSpans);
16750
- for (let [atBeat, extraBeats] of caesuraByBeat)
16751
- holds.push({ atBeat, extraBeats, kind: "caesura" });
16752
- holds.sort((a, b) => a.atBeat - b.atBeat), schedule.push(holds);
16753
- }
16754
- return schedule;
16755
- }
16756
-
16757
- // packages/midi/src/cadenzaTiming.ts
16758
- init_define_import_meta_env();
16759
- function contentBeats(content) {
16760
- let beats = 0;
16761
- for (let item of content)
16762
- switch (item.type) {
16763
- case "event":
16764
- beats += durationBeats3(item.duration.base, item.duration.dots);
16765
- break;
16766
- case "space":
16767
- beats += item.duration[0] / item.duration[1] * 4;
16768
- break;
16769
- case "tuplet":
16770
- case "tremolo":
16771
- beats += durationBeats3(item.outer.duration.base, item.outer.duration.dots) * item.outer.multiple;
16772
- break;
16773
- case "grace":
16774
- break;
16775
- }
16776
- return beats;
16777
- }
16778
- function durationBeats3(base, dots) {
16779
- let beats = DURATION_BEATS[base];
16780
- return dots ? beats * (2 - Math.pow(2, -dots)) : beats;
16781
- }
16782
- function writtenMeasureBeats(score, measureIndex, minimumBeats) {
16783
- return score.parts.reduce((maxBeats, part) => {
16784
- let measure = part.measures[measureIndex];
16785
- return measure ? Math.max(
16786
- maxBeats,
16787
- ...measure.sequences.filter((sequence) => !sequence.fullMeasure).map((sequence) => contentBeats(sequence.content))
16788
- ) : maxBeats;
16789
- }, minimumBeats);
16790
- }
16791
- function playbackGlobalMeasures(score, measureOrder) {
16792
- let activeBeats = 4;
16793
- return measureOrder.map((measureIndex) => {
16794
- let measure = score.global.measures[measureIndex];
16795
- if (measure.time && (activeBeats = measure.time.count * 4 / measure.time.unit), measureIndex === 0 && measure.number === 0) {
16796
- let pickupBeats = writtenMeasureBeats(score, measureIndex, 0);
16797
- if (pickupBeats > 0)
16798
- return {
16799
- ...measure,
16800
- __playbackNominalTime: measure.time ? { ...measure.time } : void 0,
16801
- time: {
16802
- ...measure.time,
16803
- count: pickupBeats,
16804
- unit: 4
16805
- }
16806
- };
16807
- }
16808
- if (measure.time?.display !== "senzaMisura") return measure;
16809
- let cadenzaBeats = writtenMeasureBeats(score, measureIndex, activeBeats);
16810
- return {
16811
- ...measure,
16812
- time: {
16813
- ...measure.time,
16814
- count: cadenzaBeats,
16815
- unit: 4
16816
- }
16817
- };
16818
- });
16819
- }
16820
- function suppressCadenzaFermataHolds(measures, holds) {
16821
- return holds.map(
16822
- (measureHolds, index) => measures[index]?.time?.display === "senzaMisura" ? measureHolds.filter((hold) => hold.kind !== "fermata") : [...measureHolds]
16823
- );
17263
+ collectPartMeasureHolds(
17264
+ part.measures[origIdx],
17265
+ origIdx,
17266
+ staffMeterTables[partIndex],
17267
+ fermataSpans,
17268
+ caesuraByBeat
17269
+ );
17270
+ }
17271
+ let globalCaesura = globalMeasures[origIdx]?.caesura;
17272
+ if (globalCaesura) {
17273
+ let mBeats = measureBeats2(resolveTimeSig(globalMeasures, origIdx));
17274
+ mergeMax(caesuraByBeat, mBeats, caesuraBeats(globalCaesura.style));
17275
+ }
17276
+ let holds = groupFermataSpans(fermataSpans);
17277
+ for (let [atBeat, extraBeats] of caesuraByBeat)
17278
+ holds.push({ atBeat, extraBeats, kind: "caesura" });
17279
+ holds.sort((a, b) => a.atBeat - b.atBeat), schedule.push(holds);
17280
+ }
17281
+ return schedule;
16824
17282
  }
16825
17283
 
16826
17284
  // packages/midi/src/legato.ts
@@ -16996,388 +17454,139 @@ function applyMeasureTechniques(expressions, ctx, state, caps, ratioForStaff = (
16996
17454
  });
16997
17455
  } else if (mark.action.kind === "mute" && muteFamily) {
16998
17456
  if (mark.action.muted === muted) continue;
16999
- muted = mark.action.muted;
17000
- let curve = muted ? MUTE_CURVES[muteFamily] : OPEN_CURVE;
17001
- emitCc(ctx, time, CC_BRIGHTNESS, curve.brightness), emitCc(ctx, time, CC_RESONANCE, curve.resonance);
17002
- }
17003
- }
17004
- return { program, muted, ...instrumentKey === void 0 ? {} : { instrumentKey } };
17005
- }
17006
-
17007
- // packages/midi/src/instrumentPlayback/index.ts
17008
- init_define_import_meta_env();
17009
- function instrumentPart(part, instrument) {
17010
- return instrument ? {
17011
- ...part,
17012
- name: instrument.name ?? part.name,
17013
- _x: {
17014
- ...part._x,
17015
- viritura: {
17016
- ...part._x?.viritura,
17017
- instrumentId: instrument.instrumentId,
17018
- midiProgram: instrument.midiProgram,
17019
- initialInstrument: void 0,
17020
- instruments: void 0
17021
- }
17022
- }
17023
- } : part;
17024
- }
17025
- function playbackInstruments(part) {
17026
- let initial = initialInstrumentState(part);
17027
- if (initial.instrumentKey !== void 0 && !initial.instrument)
17028
- throw new Error(`Unknown initial instrument "${initial.instrumentKey}" in "${part.name}".`);
17029
- let result = [{ key: "initial", part: instrumentPart(part, initial.instrument) }], seen = /* @__PURE__ */ new Set();
17030
- for (let located of listInstrumentChanges(part)) {
17031
- let [numerator, denominator] = located.position;
17032
- if (denominator <= 0 || numerator < 0) throw new Error(`Invalid instrument-change position in "${part.name}".`);
17033
- let key = located.change.instrument;
17034
- if (!(key === void 0 || seen.has(key))) {
17035
- if (!located.state.instrument) throw new Error(`Unknown instrument "${key}" in "${part.name}".`);
17036
- seen.add(key), result.push({ key: `instrument:${key}`, part: instrumentPart(part, located.state.instrument) });
17037
- }
17038
- }
17039
- return result;
17040
- }
17041
- function playbackInstrumentKeyAt(part, measureIndex, beatOffset = 0) {
17042
- let key = "initial";
17043
- for (let located of listInstrumentChanges(part)) {
17044
- if (located.measureIndex > measureIndex || located.measureIndex === measureIndex && located.position[0] / located.position[1] * 4 > beatOffset) break;
17045
- located.change.instrument !== void 0 && (key = `instrument:${located.change.instrument}`);
17046
- }
17047
- return key;
17048
- }
17049
- function boundariesForPart(part, timing, programs, initialProgram) {
17050
- let instruments = playbackInstruments(part), definitions = new Map(instruments.map((instrument) => [instrument.key, instrument.part])), changes = listInstrumentChanges(part).filter((located) => located.change.instrument !== void 0), boundaries = [], add = (beat, key) => {
17051
- let program = programs?.get(key) ?? (key === "initial" ? initialProgram : void 0) ?? definitions.get(key)?._x?.viritura?.midiProgram;
17052
- boundaries.push({
17053
- beat,
17054
- time: timing.model.timeAtBeat(beat),
17055
- key,
17056
- program: typeof program == "number" && Number.isInteger(program) && program >= 0 && program <= 127 ? program : void 0
17057
- });
17058
- };
17059
- return timing.measureOrder.forEach((measureIndex, expandedIndex) => {
17060
- let start = timing.measureStartBeats[expandedIndex];
17061
- add(start, playbackInstrumentKeyAt(part, measureIndex));
17062
- for (let located of changes) {
17063
- if (located.measureIndex !== measureIndex || located.position[0] === 0) continue;
17064
- let [numerator, denominator] = located.position;
17065
- if (denominator <= 0 || numerator < 0) throw new Error(`Invalid instrument-change position in "${part.name}".`);
17066
- add(start + numerator / denominator * 4, `instrument:${located.change.instrument}`);
17067
- }
17068
- }), boundaries;
17069
- }
17070
- function applyInstrumentPlayback(score, events, timing, programs, initialPrograms) {
17071
- let diagnostics = [];
17072
- return score.parts.forEach((part, partIndex) => {
17073
- if (playbackInstruments(part).length === 1) return;
17074
- let boundaries = boundariesForPart(part, timing, programs?.get(partIndex), initialPrograms?.[partIndex]), partEvents = events.filter((event) => event.partIndex === partIndex), lanes = new Set(partEvents.flatMap((event) => event.playbackLaneId ? [event.playbackLaneId] : []));
17075
- lanes.size === 0 && lanes.add(`part:${partIndex}`);
17076
- let missingPrograms = [
17077
- ...new Set(boundaries.filter((boundary) => boundary.program === void 0).map((boundary) => boundary.key))
17078
- ];
17079
- missingPrograms.length > 0 && diagnostics.push({
17080
- code: "instrument-program-unavailable",
17081
- message: `Physical MIDI cannot select ${missingPrograms.join(", ")} in "${part.name}"; provide resolved instrumentPrograms or a MIDI program fallback.`,
17082
- playbackLaneIds: [...lanes]
17083
- });
17084
- for (let event of [...partEvents]) {
17085
- if (event.playbackLaneId) continue;
17086
- let [first, ...others] = lanes;
17087
- event.playbackLaneId = first;
17088
- for (let lane of others) {
17089
- let scoped = { ...event, playbackLaneId: lane };
17090
- events.push(scoped), partEvents.push(scoped);
17091
- }
17092
- }
17093
- let attackDelays = /* @__PURE__ */ new Map(), attackKey = (event) => JSON.stringify([event.playbackLaneId, event.channel, event.midiNote, event.scoreBeat, event.scoreEventId]);
17094
- for (let event of partEvents) {
17095
- if (event.type === "controlChange" && event.cc !== 74 && event.cc !== 71) continue;
17096
- let beat = event.scoreBeat ?? timing.model.beatAtTime(event.time), instrument = "initial", instrumentStart = 0;
17097
- for (let boundary of boundaries) {
17098
- if (boundary.beat > beat + 1e-9) break;
17099
- instrument = boundary.key, instrumentStart = boundary.time;
17100
- }
17101
- if (event.type === "noteOn") {
17102
- let delay = Math.max(0, instrumentStart - event.time);
17103
- attackDelays.set(attackKey(event), delay), event.time = Math.max(event.time, instrumentStart);
17104
- }
17105
- event.playbackInstrumentKey = instrument;
17106
- }
17107
- for (let event of partEvents)
17108
- event.type === "noteOff" && (event.time += attackDelays.get(attackKey(event)) ?? 0);
17109
- for (let lane of lanes) {
17110
- let previous;
17111
- for (let boundary of boundaries)
17112
- boundary.key !== previous && (previous = boundary.key, events.push({
17113
- type: "programChange",
17114
- time: boundary.time,
17115
- scoreBeat: boundary.beat,
17116
- midiNote: 0,
17117
- velocity: 0,
17118
- partIndex,
17119
- playbackLaneId: lane,
17120
- channel: partEvents.find((event) => event.playbackLaneId === lane)?.channel ?? 0,
17121
- playbackInstrumentKey: boundary.key,
17122
- instrumentChange: !0,
17123
- program: boundary.program
17124
- }));
17125
- }
17126
- }), diagnostics;
17127
- }
17128
-
17129
- // packages/midi/src/tempoMap.ts
17130
- init_define_import_meta_env();
17131
-
17132
- // packages/midi/src/tempoModel.ts
17133
- init_define_import_meta_env();
17134
- var bpmToSpb = (bpm) => 60 / bpm;
17135
- function integrateTime(b0, b, bpm0, slope) {
17136
- let db = b - b0;
17137
- if (Math.abs(slope) < 1e-6) return bpmToSpb(bpm0) * db;
17138
- let bpmB = bpm0 + slope * db;
17139
- return 60 / slope * Math.log(bpmB / bpm0);
17140
- }
17141
- function inverseBeat(dt, bpm0, slope) {
17142
- return Math.abs(slope) < 1e-6 ? dt * bpm0 / 60 : (bpm0 * Math.exp(dt * slope / 60) - bpm0) / slope;
17143
- }
17144
- var TempoModel = class _TempoModel {
17145
- knots;
17146
- constructor(knots) {
17147
- this.knots = knots;
17148
- }
17149
- /**
17150
- * Build a model from tempo regions and point insertions.
17151
- *
17152
- * `regions` need not be pre-sorted or gapless; they are sorted by `startBeat`
17153
- * and gaps are filled by extending the previous tempo (a held constant). The
17154
- * first region's start tempo applies from beat 0. `insertions` are point gaps
17155
- * (caesuras) keyed by beat.
17156
- */
17157
- static build(regions, insertions = []) {
17158
- let sorted = [...regions].sort((a, b) => a.startBeat - b.startBeat), insByBeat = /* @__PURE__ */ new Map();
17159
- for (let ins of insertions)
17160
- ins.seconds > 0 && insByBeat.set(ins.beat, (insByBeat.get(ins.beat) ?? 0) + ins.seconds);
17161
- let boundarySet = /* @__PURE__ */ new Set([0]);
17162
- for (let r of sorted) boundarySet.add(r.startBeat);
17163
- for (let ins of insByBeat.keys()) boundarySet.add(ins);
17164
- let boundaries = [...boundarySet].sort((a, b) => a - b), tempoAt = (beat) => {
17165
- let active;
17166
- for (let r of sorted)
17167
- if (r.startBeat <= beat + 1e-9) active = r;
17168
- else break;
17169
- if (!active) {
17170
- let first = sorted[0];
17171
- return { bpm: first ? first.startBpm : 120, slope: 0 };
17172
- }
17173
- let span = active.endBeat - active.startBeat, slope = span > 0 && Number.isFinite(span) ? (active.endBpm - active.startBpm) / span : 0;
17174
- return { bpm: active.startBpm + slope * (beat - active.startBeat), slope };
17175
- }, knots = [], time = 0, prevBeat = boundaries[0], prev = tempoAt(prevBeat), ins0 = insByBeat.get(prevBeat) ?? 0;
17176
- time += ins0, knots.push({ beat: prevBeat, time, bpm: prev.bpm, slope: prev.slope, insertBefore: ins0 });
17177
- for (let i = 1; i < boundaries.length; i++) {
17178
- let beat = boundaries[i];
17179
- time += integrateTime(prevBeat, beat, prev.bpm, prev.slope);
17180
- let ins = insByBeat.get(beat) ?? 0;
17181
- time += ins;
17182
- let t = tempoAt(beat);
17183
- knots.push({ beat, time, bpm: t.bpm, slope: t.slope, insertBefore: ins }), prevBeat = beat, prev = t;
17184
- }
17185
- return new _TempoModel(knots);
17186
- }
17187
- /** Index of the last knot whose beat ≤ `beat`. */
17188
- knotIndexForBeat(beat) {
17189
- let k = this.knots, lo = 0, hi = k.length - 1, ans = 0;
17190
- for (; lo <= hi; ) {
17191
- let mid = lo + hi >> 1;
17192
- k[mid].beat <= beat + 1e-9 ? (ans = mid, lo = mid + 1) : hi = mid - 1;
17193
- }
17194
- return ans;
17195
- }
17196
- /** Index of the last knot whose time ≤ `time`. */
17197
- knotIndexForTime(time) {
17198
- let k = this.knots, lo = 0, hi = k.length - 1, ans = 0;
17199
- for (; lo <= hi; ) {
17200
- let mid = lo + hi >> 1;
17201
- k[mid].time <= time + 1e-9 ? (ans = mid, lo = mid + 1) : hi = mid - 1;
17202
- }
17203
- return ans;
17204
- }
17205
- /**
17206
- * Absolute time (seconds) at a global quarter-note beat.
17207
- *
17208
- * `endpoint` selects how a point insertion EXACTLY at `beat` is treated:
17209
- * - `"onset"` (default): time AFTER the insertion — what an event STARTING at
17210
- * `beat` wants (it begins once the gap is over).
17211
- * - `"release"`: time BEFORE the insertion — what a note ENDING at `beat`
17212
- * wants (a caesura/gap at the release point isn't part of the note).
17213
- */
17214
- timeAtBeat(beat, endpoint = "onset") {
17215
- let i = this.knotIndexForBeat(beat), knot = this.knots[i], base = knot.time + integrateTime(knot.beat, beat, knot.bpm, knot.slope);
17216
- return endpoint === "release" && Math.abs(knot.beat - beat) < 1e-9 ? base - knot.insertBefore : base;
17217
- }
17218
- /**
17219
- * Seconds elapsed over `[beat, beat + beats)` — a note/event DURATION. The end
17220
- * uses release semantics so a point insertion (caesura) exactly at the release
17221
- * beat is not absorbed into the note's sounding length.
17222
- */
17223
- secondsForBeats(beat, beats) {
17224
- return this.timeAtBeat(beat + beats, "release") - this.timeAtBeat(beat);
17225
- }
17226
- /**
17227
- * Global quarter-note beat at an absolute time (inverse of {@link timeAtBeat}).
17228
- * Within a point-insertion gap, returns the insertion's beat (position is
17229
- * frozen during a caesura).
17230
- */
17231
- beatAtTime(time) {
17232
- let i = this.knotIndexForTime(time), knot = this.knots[i], next = this.knots[i + 1];
17233
- if (next) {
17234
- let musicEnd = next.time - next.insertBefore;
17235
- if (time >= musicEnd - 1e-9) return next.beat;
17457
+ muted = mark.action.muted;
17458
+ let curve = muted ? MUTE_CURVES[muteFamily] : OPEN_CURVE;
17459
+ emitCc(ctx, time, CC_BRIGHTNESS, curve.brightness), emitCc(ctx, time, CC_RESONANCE, curve.resonance);
17236
17460
  }
17237
- return knot.beat + inverseBeat(time - knot.time, knot.bpm, knot.slope);
17238
- }
17239
- /** Seconds-per-quarter-note beat at a global beat. */
17240
- spbAtBeat(beat) {
17241
- let i = this.knotIndexForBeat(beat), knot = this.knots[i], bpmHere = knot.bpm + knot.slope * (beat - knot.beat);
17242
- return bpmToSpb(bpmHere);
17243
- }
17244
- /** Quarter-note BPM at a global beat. */
17245
- bpmAtBeat(beat) {
17246
- return 60 / this.spbAtBeat(beat);
17247
- }
17248
- /**
17249
- * Generate click times (seconds) for a metronome / studio click track over
17250
- * `[startBeat, endBeat)`. `clickBeats` are the beat positions to click,
17251
- * expressed as offsets within each "grid step" — but for the common case
17252
- * callers pass a flat list of global beat positions and we just map them.
17253
- * Returns the absolute time of every beat in `beats`.
17254
- */
17255
- clickTimes(beats) {
17256
- return beats.map((b) => this.timeAtBeat(b));
17257
17461
  }
17258
- };
17259
-
17260
- // packages/midi/src/tempoMap.ts
17261
- function playbackNominalTime(gm) {
17262
- return gm.__playbackNominalTime;
17462
+ return { program, muted, ...instrumentKey === void 0 ? {} : { instrumentKey } };
17263
17463
  }
17264
- var DEFAULT_BPM = 120, DEFAULT_TIME = { count: 4, unit: 4 };
17265
- function measureBeatsFromTime(ts) {
17266
- return ts.count * 4 / ts.unit;
17464
+
17465
+ // packages/midi/src/instrumentPlayback/index.ts
17466
+ init_define_import_meta_env();
17467
+ function instrumentPart(part, instrument) {
17468
+ return instrument ? {
17469
+ ...part,
17470
+ name: instrument.name ?? part.name,
17471
+ _x: {
17472
+ ...part._x,
17473
+ viritura: {
17474
+ ...part._x?.viritura,
17475
+ instrumentId: instrument.instrumentId,
17476
+ midiProgram: instrument.midiProgram,
17477
+ initialInstrument: void 0,
17478
+ instruments: void 0
17479
+ }
17480
+ }
17481
+ } : part;
17267
17482
  }
17268
- function tempoNoteBeats(base, dots) {
17269
- let baseBeats = DURATION_BEATS[base] ?? 1;
17270
- return dots ? baseBeats * (2 - Math.pow(2, -dots)) : baseBeats;
17483
+ function playbackInstruments(part) {
17484
+ let initial = initialInstrumentState(part);
17485
+ if (initial.instrumentKey !== void 0 && !initial.instrument)
17486
+ throw new Error(`Unknown initial instrument "${initial.instrumentKey}" in "${part.name}".`);
17487
+ let result = [{ key: "initial", part: instrumentPart(part, initial.instrument) }], seen = /* @__PURE__ */ new Set();
17488
+ for (let located of listInstrumentChanges(part)) {
17489
+ let [numerator, denominator] = located.position;
17490
+ if (denominator <= 0 || numerator < 0) throw new Error(`Invalid instrument-change position in "${part.name}".`);
17491
+ let key = located.change.instrument;
17492
+ if (!(key === void 0 || seen.has(key))) {
17493
+ if (!located.state.instrument) throw new Error(`Unknown instrument "${key}" in "${part.name}".`);
17494
+ seen.add(key), result.push({ key: `instrument:${key}`, part: instrumentPart(part, located.state.instrument) });
17495
+ }
17496
+ }
17497
+ return result;
17271
17498
  }
17272
- function effectiveQpm(bpm, tempoBase, tempoDots) {
17273
- return bpm * tempoNoteBeats(tempoBase, tempoDots);
17499
+ function playbackInstrumentKeyAt(part, measureIndex, beatOffset = 0) {
17500
+ let key = "initial";
17501
+ for (let located of listInstrumentChanges(part)) {
17502
+ if (located.measureIndex > measureIndex || located.measureIndex === measureIndex && located.position[0] / located.position[1] * 4 > beatOffset) break;
17503
+ located.change.instrument !== void 0 && (key = `instrument:${located.change.instrument}`);
17504
+ }
17505
+ return key;
17274
17506
  }
17275
- function buildTempoMap(globalMeasures, holds) {
17276
- let tempoMap = [], measureStartTimes = [], currentQpm = DEFAULT_BPM, currentSpq = 60 / currentQpm, currentTime = { count: DEFAULT_TIME.count, unit: DEFAULT_TIME.unit }, absoluteTime = 0, hasInitialTempo = !1;
17277
- for (let m = 0; m < globalMeasures.length; m++) {
17278
- let gm = globalMeasures[m], nominalTime = playbackNominalTime(gm) ?? gm.time;
17279
- nominalTime && (currentTime = { count: nominalTime.count, unit: nominalTime.unit });
17280
- let mBeats = measureBeatsFromTime(gm.time ?? currentTime);
17281
- measureStartTimes.push(absoluteTime);
17282
- let measureStartSpq = currentSpq, changes = [];
17283
- if (gm.tempos && gm.tempos.length > 0) {
17284
- for (let tempo of gm.tempos) {
17285
- let beatOffset = tempo.location ? tempo.location.fraction[0] / tempo.location.fraction[1] * mBeats : 0, qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);
17286
- changes.push({ beatOffset, qpm });
17287
- }
17288
- changes.sort((a, b) => a.beatOffset - b.beatOffset);
17507
+ function boundariesForPart(part, timing, programs, initialProgram) {
17508
+ let instruments = playbackInstruments(part), definitions = new Map(instruments.map((instrument) => [instrument.key, instrument.part])), changes = listInstrumentChanges(part).filter((located) => located.change.instrument !== void 0), boundaries = [], add = (beat, key) => {
17509
+ let program = programs?.get(key) ?? (key === "initial" ? initialProgram : void 0) ?? definitions.get(key)?._x?.viritura?.midiProgram;
17510
+ boundaries.push({
17511
+ beat,
17512
+ time: timing.model.timeAtBeat(beat),
17513
+ key,
17514
+ program: typeof program == "number" && Number.isInteger(program) && program >= 0 && program <= 127 ? program : void 0
17515
+ });
17516
+ };
17517
+ return timing.measureOrder.forEach((measureIndex, expandedIndex) => {
17518
+ let start = timing.measureStartBeats[expandedIndex];
17519
+ add(start, playbackInstrumentKeyAt(part, measureIndex));
17520
+ for (let located of changes) {
17521
+ if (located.measureIndex !== measureIndex || located.position[0] === 0) continue;
17522
+ let [numerator, denominator] = located.position;
17523
+ if (denominator <= 0 || numerator < 0) throw new Error(`Invalid instrument-change position in "${part.name}".`);
17524
+ add(start + numerator / denominator * 4, `instrument:${located.change.instrument}`);
17289
17525
  }
17290
- let beatCursor = 0;
17291
- for (let change of changes)
17292
- change.beatOffset > beatCursor && (absoluteTime += (change.beatOffset - beatCursor) * currentSpq, beatCursor = change.beatOffset), tempoMap.push({
17293
- measureIndex: m,
17294
- beatInMeasure: change.beatOffset,
17295
- timeSeconds: absoluteTime,
17296
- bpm: change.qpm
17297
- }), m === 0 && change.beatOffset === 0 && (hasInitialTempo = !0), currentQpm = change.qpm, currentSpq = 60 / currentQpm;
17298
- mBeats > beatCursor && (absoluteTime += (mBeats - beatCursor) * currentSpq);
17299
- let measureHolds = holds?.[m];
17300
- if (measureHolds)
17301
- for (let hold of measureHolds)
17302
- absoluteTime += hold.extraBeats * measureStartSpq;
17303
- }
17304
- return hasInitialTempo || tempoMap.unshift({
17305
- measureIndex: 0,
17306
- beatInMeasure: 0,
17307
- timeSeconds: 0,
17308
- bpm: DEFAULT_BPM
17309
- }), { tempoMap, measureStartTimes };
17526
+ }), boundaries;
17310
17527
  }
17311
- function buildTempoModel(globalMeasures, holds) {
17312
- let regions = [], measureStartBeats = [], cumBeats = 0, currentTime = { count: DEFAULT_TIME.count, unit: DEFAULT_TIME.unit }, gradualDecls = [];
17313
- for (let m = 0; m < globalMeasures.length; m++) {
17314
- let gm = globalMeasures[m], nominalTime = playbackNominalTime(gm) ?? gm.time;
17315
- nominalTime && (currentTime = { count: nominalTime.count, unit: nominalTime.unit });
17316
- let mBeats = measureBeatsFromTime(gm.time ?? currentTime);
17317
- if (measureStartBeats.push(cumBeats), gm.tempos && gm.tempos.length > 0) {
17318
- let sorted = [...gm.tempos].sort((a, b) => tempoBeat(a, mBeats) - tempoBeat(b, mBeats));
17319
- for (let tempo of sorted) {
17320
- let startBeat = cumBeats + tempoBeat(tempo, mBeats), qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);
17321
- regions.push({ startBeat, endBeat: 1 / 0, startBpm: qpm, endBpm: qpm });
17528
+ function applyInstrumentPlayback(score, events, timing, programs, initialPrograms) {
17529
+ let diagnostics = [];
17530
+ return score.parts.forEach((part, partIndex) => {
17531
+ if (playbackInstruments(part).length === 1) return;
17532
+ let boundaries = boundariesForPart(part, timing, programs?.get(partIndex), initialPrograms?.[partIndex]), partEvents = events.filter((event) => event.partIndex === partIndex), lanes = new Set(partEvents.flatMap((event) => event.playbackLaneId ? [event.playbackLaneId] : []));
17533
+ lanes.size === 0 && lanes.add(`part:${partIndex}`);
17534
+ let missingPrograms = [
17535
+ ...new Set(boundaries.filter((boundary) => boundary.program === void 0).map((boundary) => boundary.key))
17536
+ ];
17537
+ missingPrograms.length > 0 && diagnostics.push({
17538
+ code: "instrument-program-unavailable",
17539
+ message: `Physical MIDI cannot select ${missingPrograms.join(", ")} in "${part.name}"; provide resolved instrumentPrograms or a MIDI program fallback.`,
17540
+ playbackLaneIds: [...lanes]
17541
+ });
17542
+ for (let event of [...partEvents]) {
17543
+ if (event.playbackLaneId) continue;
17544
+ let [first, ...others] = lanes;
17545
+ event.playbackLaneId = first;
17546
+ for (let lane of others) {
17547
+ let scoped = { ...event, playbackLaneId: lane };
17548
+ events.push(scoped), partEvents.push(scoped);
17322
17549
  }
17323
17550
  }
17324
- gm.gradualTempo && gradualDecls.push({
17325
- declIdx: m,
17326
- gt: gm.gradualTempo,
17327
- startBeat: cumBeats + posBeats(gm.gradualTempo.position.fraction)
17328
- }), cumBeats += mBeats;
17329
- }
17330
- if ((regions.length === 0 || regions.every((r) => r.startBeat > 1e-9)) && regions.unshift({ startBeat: 0, endBeat: 1 / 0, startBpm: DEFAULT_BPM, endBpm: DEFAULT_BPM }), gradualDecls.length > 0) {
17331
- let idToIdx = buildMeasureIdToIndex(globalMeasures), constModel = TempoModel.build(regions);
17332
- for (let { declIdx, gt, startBeat } of gradualDecls) {
17333
- let endBeat = resolveGradualEndBeat(gt.end, declIdx, globalMeasures, measureStartBeats, idToIdx);
17334
- if (endBeat === void 0 || endBeat <= startBeat + 1e-9) continue;
17335
- let startBpm = gt.startBpm ?? constModel.bpmAtBeat(startBeat);
17336
- regions.push({ startBeat, endBeat, startBpm, endBpm: gt.endBpm }), regions.some((r) => Math.abs(r.startBeat - endBeat) < 1e-9) || regions.push({ startBeat: endBeat, endBeat: 1 / 0, startBpm: gt.endBpm, endBpm: gt.endBpm });
17551
+ let attackDelays = /* @__PURE__ */ new Map(), attackKey = (event) => JSON.stringify([event.playbackLaneId, event.channel, event.midiNote, event.scoreBeat, event.scoreEventId]);
17552
+ for (let event of partEvents) {
17553
+ if (event.type === "controlChange" && event.cc !== 74 && event.cc !== 71) continue;
17554
+ let beat = event.scoreBeat ?? timing.model.beatAtTime(event.time), instrument = "initial", instrumentStart = 0;
17555
+ for (let boundary of boundaries) {
17556
+ if (boundary.beat > beat + 1e-9) break;
17557
+ instrument = boundary.key, instrumentStart = boundary.time;
17558
+ }
17559
+ if (event.type === "noteOn") {
17560
+ let delay = Math.max(0, instrumentStart - event.time);
17561
+ attackDelays.set(attackKey(event), delay), event.time = Math.max(event.time, instrumentStart);
17562
+ }
17563
+ event.playbackInstrumentKey = instrument;
17337
17564
  }
17338
- }
17339
- regions.sort((a, b) => a.startBeat - b.startBeat);
17340
- for (let i = 0; i < regions.length - 1; i++)
17341
- regions[i].endBeat = regions[i + 1].startBeat;
17342
- let regionsModel = TempoModel.build(regions), insertions = [];
17343
- if (holds)
17344
- for (let m = 0; m < holds.length; m++) {
17345
- let measureHolds = holds[m];
17346
- if (!measureHolds || measureHolds.length === 0) continue;
17347
- let startBeat = measureStartBeats[m], spb = regionsModel.spbAtBeat(startBeat);
17348
- for (let hold of measureHolds)
17349
- insertions.push({ beat: startBeat + hold.atBeat, seconds: hold.extraBeats * spb });
17565
+ for (let event of partEvents)
17566
+ event.type === "noteOff" && (event.time += attackDelays.get(attackKey(event)) ?? 0);
17567
+ for (let lane of lanes) {
17568
+ let previous;
17569
+ for (let boundary of boundaries)
17570
+ boundary.key !== previous && (previous = boundary.key, events.push({
17571
+ type: "programChange",
17572
+ time: boundary.time,
17573
+ scoreBeat: boundary.beat,
17574
+ midiNote: 0,
17575
+ velocity: 0,
17576
+ partIndex,
17577
+ playbackLaneId: lane,
17578
+ channel: partEvents.find((event) => event.playbackLaneId === lane)?.channel ?? 0,
17579
+ playbackInstrumentKey: boundary.key,
17580
+ instrumentChange: !0,
17581
+ program: boundary.program
17582
+ }));
17350
17583
  }
17351
- let model = insertions.length > 0 ? TempoModel.build(regions, insertions) : regionsModel, measureStartTimes = measureStartBeats.map((b) => model.timeAtBeat(b));
17352
- return { model, measureStartTimes, measureStartBeats, totalBeats: cumBeats };
17353
- }
17354
- function posBeats(frac) {
17355
- return !frac || frac[1] === 0 ? 0 : frac[0] / frac[1] * 4;
17356
- }
17357
- function buildMeasureIdToIndex(globalMeasures) {
17358
- let m = /* @__PURE__ */ new Map();
17359
- return globalMeasures.forEach((gm, i) => {
17360
- gm.id && !m.has(gm.id) && m.set(gm.id, i);
17361
- }), m;
17362
- }
17363
- function resolveGradualEndBeat(end, fromIdx, globalMeasures, measureStartBeats, idToIdx) {
17364
- let idx = idToIdx.get(end.measure);
17365
- if (idx === void 0) {
17366
- let parsed = Number.parseInt(end.measure, 10);
17367
- !Number.isNaN(parsed) && parsed >= 0 && parsed < globalMeasures.length && (idx = parsed);
17368
- }
17369
- idx === void 0 && (idx = fromIdx);
17370
- let start = measureStartBeats[idx];
17371
- if (start !== void 0)
17372
- return start + posBeats(end.position.fraction);
17373
- }
17374
- function tempoBeat(tempo, mBeats) {
17375
- return tempo.location ? tempo.location.fraction[0] / tempo.location.fraction[1] * mBeats : 0;
17584
+ }), diagnostics;
17376
17585
  }
17377
17586
 
17378
17587
  // packages/midi/src/dynamicsEnvelope.ts
17379
17588
  init_define_import_meta_env();
17380
- var CC_EXPRESSION = 11, EPS = 1e-6, DEFAULT_DYNAMIC = { velocity: 84, cc11: 100 }, DYNAMIC_AXES = {
17589
+ var CC_EXPRESSION = 11, EPS2 = 1e-6, DEFAULT_DYNAMIC = { velocity: 84, cc11: 100 }, DYNAMIC_AXES = {
17381
17590
  n: { velocity: 1, cc11: 0 },
17382
17591
  pppppp: { velocity: 22, cc11: 11 },
17383
17592
  ppppp: { velocity: 28, cc11: 17 },
@@ -17444,7 +17653,7 @@ function stepRung(axes, dir) {
17444
17653
  function sampleAnchorsAxes(anchors, t) {
17445
17654
  let active = DEFAULT_DYNAMIC;
17446
17655
  for (let a of anchors) {
17447
- if (a.time > t + EPS) break;
17656
+ if (a.time > t + EPS2) break;
17448
17657
  active = { velocity: a.velocity, cc11: a.cc11 };
17449
17658
  }
17450
17659
  return active;
@@ -17460,24 +17669,24 @@ function gradualTextDirection(text) {
17460
17669
  if (/^cresc(?:endo)?\.?$/.test(normalized)) return 1;
17461
17670
  if (/^(?:dim(?:inuendo)?|decresc(?:endo)?)\.?$/.test(normalized)) return -1;
17462
17671
  }
17463
- function collectMeasureDynamicGroups(pm, originalMeasureIndex, measureStartBeat, model, expandedIdx, groups, staffMeterTable) {
17672
+ function collectMeasureDynamicGroups(pm, originalMeasureIndex, measureStartBeat, model, expandedIdx, groups, staffMeterTable, span) {
17464
17673
  for (let group of pm.dynamics ?? []) {
17465
- let ratio = staffMeterRatioAt(staffMeterTable, originalMeasureIndex, group.staff);
17466
- groups.push({
17674
+ let ratio = staffMeterRatioAt(staffMeterTable, originalMeasureIndex, group.staff), beat = fractionToBeats2(group.position.fraction) * ratio;
17675
+ beatInPlayedSpan(beat, span) && groups.push({
17467
17676
  group,
17468
- time: model.timeAtBeat(measureStartBeat + fractionToBeats2(group.position.fraction) * ratio),
17677
+ time: model.timeAtBeat(measureStartBeat + beat),
17469
17678
  expandedIdx
17470
17679
  });
17471
17680
  }
17472
17681
  }
17473
- function collectMeasureGradualTexts(pm, originalMeasureIndex, measureStartBeat, model, expandedIdx, gradualTexts, staffMeterTable) {
17682
+ function collectMeasureGradualTexts(pm, originalMeasureIndex, measureStartBeat, model, expandedIdx, gradualTexts, staffMeterTable, span) {
17474
17683
  for (let index = 0; index < (pm.expressions?.length ?? 0); index++) {
17475
17684
  let expression = pm.expressions[index], dir = gradualTextDirection(plainTextContent(expression.text));
17476
- dir && gradualTexts.push({
17685
+ if (!dir) continue;
17686
+ let beat = fractionToBeats2(expression.position.fraction) * staffMeterRatioAt(staffMeterTable, originalMeasureIndex, expression.staff);
17687
+ beatInPlayedSpan(beat, span) && gradualTexts.push({
17477
17688
  id: `text-gradual-${expandedIdx}-${index}`,
17478
- startTime: model.timeAtBeat(
17479
- measureStartBeat + fractionToBeats2(expression.position.fraction) * staffMeterRatioAt(staffMeterTable, originalMeasureIndex, expression.staff)
17480
- ),
17689
+ startTime: model.timeAtBeat(measureStartBeat + beat),
17481
17690
  dir,
17482
17691
  expandedIdx
17483
17692
  });
@@ -17500,7 +17709,7 @@ function resolveEndMeasureLocation(endMeasureId, fromExpandedIdx, measureOrder,
17500
17709
  }
17501
17710
  function explicitAnchorAt(anchors, time) {
17502
17711
  for (let a of anchors)
17503
- if (Math.abs(a.time - time) <= EPS) return { velocity: a.velocity, cc11: a.cc11 };
17712
+ if (Math.abs(a.time - time) <= EPS2) return { velocity: a.velocity, cc11: a.cc11 };
17504
17713
  }
17505
17714
  function measureHasSoundingNotes(part, measureIndex) {
17506
17715
  return measureIndex === void 0 ? !1 : (part.measures[measureIndex]?.sequences ?? []).some(
@@ -17521,15 +17730,15 @@ function resolveRamps(timed, anchors) {
17521
17730
  startsAfterSilence,
17522
17731
  measureStartTime
17523
17732
  } of ordered) {
17524
- if (endTime <= startTime + EPS) continue;
17525
- let chained = prev !== void 0 && Math.abs(prev.endTime - startTime) <= EPS, currentMeasureAnchor = anchors.some(
17526
- (anchor) => anchor.time >= measureStartTime - EPS && anchor.time <= startTime + EPS
17733
+ if (endTime <= startTime + EPS2) continue;
17734
+ let chained = prev !== void 0 && Math.abs(prev.endTime - startTime) <= EPS2, currentMeasureAnchor = anchors.some(
17735
+ (anchor) => anchor.time >= measureStartTime - EPS2 && anchor.time <= startTime + EPS2
17527
17736
  ), start = authoredStart ?? explicitAnchorAt(anchors, startTime) ?? (startsAfterSilence && !currentMeasureAnchor ? axesOf("p") : chained ? prev.end : sampleAnchorsAxes(anchors, startTime)), end = explicitAnchorAt(anchors, endTime) ?? stepRung(start, dir), ramp = { groupId, startTime, endTime, start, end };
17528
17737
  ramps.push(ramp), prev = ramp;
17529
17738
  }
17530
17739
  return ramps;
17531
17740
  }
17532
- function collectExpandedDynamics(part, measureOrder, measureStartBeats, model, staffMeterTable, timedGroups, gradualTexts) {
17741
+ function collectExpandedDynamics(part, measureOrder, measureStartBeats, model, staffMeterTable, timedGroups, gradualTexts, playedSpans) {
17533
17742
  for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {
17534
17743
  let originalMeasureIndex = measureOrder[expandedIdx], measure = part.measures[originalMeasureIndex];
17535
17744
  if (!measure) continue;
@@ -17541,7 +17750,8 @@ function collectExpandedDynamics(part, measureOrder, measureStartBeats, model, s
17541
17750
  model,
17542
17751
  expandedIdx,
17543
17752
  timedGroups,
17544
- staffMeterTable
17753
+ staffMeterTable,
17754
+ playedSpans?.[expandedIdx]
17545
17755
  ), collectMeasureGradualTexts(
17546
17756
  measure,
17547
17757
  originalMeasureIndex,
@@ -17549,13 +17759,23 @@ function collectExpandedDynamics(part, measureOrder, measureStartBeats, model, s
17549
17759
  model,
17550
17760
  expandedIdx,
17551
17761
  gradualTexts,
17552
- staffMeterTable
17762
+ staffMeterTable,
17763
+ playedSpans?.[expandedIdx]
17553
17764
  );
17554
17765
  }
17555
17766
  }
17556
- function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, globalMeasures, impliedAnchors = [], resolvedStaffMeters) {
17767
+ function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, globalMeasures, impliedAnchors = [], resolvedStaffMeters, playedSpans) {
17557
17768
  let anchors = [], attacks = [], timedGroups = [], gradualTexts = [], idToOrig = buildMeasureIdToOrig(globalMeasures), staffMeterTable = resolvedStaffMeters ?? resolvePartStaffMeterTable(part, globalMeasures);
17558
- collectExpandedDynamics(part, measureOrder, measureStartBeats, model, staffMeterTable, timedGroups, gradualTexts);
17769
+ collectExpandedDynamics(
17770
+ part,
17771
+ measureOrder,
17772
+ measureStartBeats,
17773
+ model,
17774
+ staffMeterTable,
17775
+ timedGroups,
17776
+ gradualTexts,
17777
+ playedSpans
17778
+ );
17559
17779
  let typeRank = { immediate: 0, relative: 1, accent: 2, gradual: 3 };
17560
17780
  timedGroups.sort(
17561
17781
  (a, b) => a.time - b.time || typeRank[a.group.type] - typeRank[b.group.type] || a.group.id.localeCompare(b.group.id)
@@ -17565,7 +17785,7 @@ function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, glo
17565
17785
  let body = axesOf(group.value);
17566
17786
  anchors.push({ time, velocity: body.velocity, cc11: body.cc11 });
17567
17787
  } else if (group.type === "relative") {
17568
- let current = sampleAnchorsAxes(anchors, time - EPS), next = stepRung(current, group.relativeValue === "louder" ? 1 : -1);
17788
+ let current = sampleAnchorsAxes(anchors, time - EPS2), next = stepRung(current, group.relativeValue === "louder" ? 1 : -1);
17569
17789
  anchors.push({ time, velocity: next.velocity, cc11: next.cc11 });
17570
17790
  } else if (group.type === "accent" && (attacks.push({ time, attackVelocity: axesOf(group.value).velocity }), group.residualValue !== void 0)) {
17571
17791
  let body = axesOf(group.residualValue);
@@ -17574,9 +17794,9 @@ function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, glo
17574
17794
  for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {
17575
17795
  let measureIndex = measureOrder[expandedIdx];
17576
17796
  for (let implied of impliedAnchors) {
17577
- if (implied.measureIndex !== measureIndex) continue;
17797
+ if (implied.measureIndex !== measureIndex || !beatInPlayedSpan(fractionToBeats2(implied.position), playedSpans?.[expandedIdx])) continue;
17578
17798
  let time = model.timeAtBeat(measureStartBeats[expandedIdx] + fractionToBeats2(implied.position));
17579
- if (anchors.some((anchor) => Math.abs(anchor.time - time) <= EPS)) continue;
17799
+ if (anchors.some((anchor) => Math.abs(anchor.time - time) <= EPS2)) continue;
17580
17800
  let axes = axesOf(implied.value);
17581
17801
  anchors.push({ time, velocity: axes.velocity, cc11: axes.cc11 });
17582
17802
  }
@@ -17611,8 +17831,8 @@ function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, glo
17611
17831
  });
17612
17832
  }
17613
17833
  for (let text of gradualTexts) {
17614
- if (timed.some((hairpin) => Math.abs(hairpin.startTime - text.startTime) <= EPS)) continue;
17615
- let endpoint = anchors.find((anchor) => anchor.time > text.startTime + EPS);
17834
+ if (timed.some((hairpin) => Math.abs(hairpin.startTime - text.startTime) <= EPS2)) continue;
17835
+ let endpoint = anchors.find((anchor) => anchor.time > text.startTime + EPS2);
17616
17836
  endpoint && timed.push({
17617
17837
  groupId: text.id,
17618
17838
  startTime: text.startTime,
@@ -17627,7 +17847,7 @@ function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, glo
17627
17847
  }
17628
17848
  function rampAt(env, t) {
17629
17849
  for (let r of env.ramps)
17630
- if (t >= r.startTime - EPS && t <= r.endTime + EPS) return r;
17850
+ if (t >= r.startTime - EPS2 && t <= r.endTime + EPS2) return r;
17631
17851
  }
17632
17852
  function sampleDynamics(env, t) {
17633
17853
  let r = rampAt(env, t);
@@ -17639,7 +17859,7 @@ function sampleDynamics(env, t) {
17639
17859
  }
17640
17860
  function attackAt(env, t) {
17641
17861
  for (let c of env.attacks)
17642
- if (Math.abs(c.time - t) <= EPS) return c;
17862
+ if (Math.abs(c.time - t) <= EPS2) return c;
17643
17863
  }
17644
17864
  function noteVelocityAt(env, t) {
17645
17865
  let c = attackAt(env, t);
@@ -17647,7 +17867,7 @@ function noteVelocityAt(env, t) {
17647
17867
  }
17648
17868
  function rampQuantizationEvents(ramp) {
17649
17869
  let start = ramp.start.cc11, end = ramp.end.cc11;
17650
- if (start === end || ramp.endTime <= ramp.startTime + EPS)
17870
+ if (start === end || ramp.endTime <= ramp.startTime + EPS2)
17651
17871
  return [
17652
17872
  { time: ramp.startTime, value: start },
17653
17873
  { time: ramp.endTime, value: end }
@@ -17670,13 +17890,13 @@ function cc11Events(env) {
17670
17890
  let out = [];
17671
17891
  for (let e of raw) {
17672
17892
  let prev = out[out.length - 1];
17673
- if (prev && Math.abs(prev.time - e.time) <= EPS) {
17893
+ if (prev && Math.abs(prev.time - e.time) <= EPS2) {
17674
17894
  out[out.length - 1] = e;
17675
17895
  continue;
17676
17896
  }
17677
17897
  prev && prev.value === e.value || out.push(e);
17678
17898
  }
17679
- return (out.length === 0 || out[0].time > EPS) && out.unshift({ time: 0, value: DEFAULT_DYNAMIC.cc11 }), out;
17899
+ return (out.length === 0 || out[0].time > EPS2) && out.unshift({ time: 0, value: DEFAULT_DYNAMIC.cc11 }), out;
17680
17900
  }
17681
17901
  function emitDynamicsCc11(env, partIndex, channel, out, playbackLaneId2) {
17682
17902
  for (let ev of cc11Events(env))
@@ -17819,7 +18039,7 @@ function collectRampDiagnostics(lanes) {
17819
18039
  }
17820
18040
  return diagnostics;
17821
18041
  }
17822
- function compileDynamicProgram(part, partIndex, measureOrder, measureStartBeats, model, globalMeasures, gmProgram = -1, impliedAnchors = [], staffMeterTable) {
18042
+ function compileDynamicProgram(part, partIndex, measureOrder, measureStartBeats, model, globalMeasures, gmProgram = -1, impliedAnchors = [], staffMeterTable, playedSpans) {
17823
18043
  let scoped = part.measures.some(
17824
18044
  (measure) => measure.dynamics?.some((group) => group.staff !== void 0 || group.voice !== void 0)
17825
18045
  ), scopes = collectLaneScopes(part), selectedScopes = scoped ? scopes : [scopes[0]], profile = selectDynamicResponseProfile(gmProgram), lanes = /* @__PURE__ */ new Map();
@@ -17832,7 +18052,8 @@ function compileDynamicProgram(part, partIndex, measureOrder, measureStartBeats,
17832
18052
  model,
17833
18053
  globalMeasures,
17834
18054
  impliedAnchors,
17835
- staffMeterTable
18055
+ staffMeterTable,
18056
+ playedSpans
17836
18057
  ),
17837
18058
  profile
17838
18059
  );
@@ -17887,13 +18108,13 @@ function generateTimeline(inputScore, options) {
17887
18108
  let score = expandScoreMeasureRepeats(inputScore), impliedSectionDynamics = analyzeImpliedSectionDynamics(score), globalMeasures = score.global.measures;
17888
18109
  if (globalMeasures.length === 0)
17889
18110
  return emptyTimeline();
17890
- let chords = compileChordPlayback(score, options?.includeGlobalChords), toCodaMeasureIndex = detectToCodaMeasureIndex(score), measureOrder = options?.expandRepeats === !1 ? globalMeasures.map((_, index) => index) : expandMeasureOrder(globalMeasures, { toCodaMeasureIndex });
18111
+ let chords = compileChordPlayback(score, options?.includeGlobalChords), spans = options?.expandRepeats === !1 ? void 0 : scorePlaybackItinerary(score), measureOrder = options?.expandRepeats === !1 ? globalMeasures.map((_, index) => index) : spans.map((span) => span.measureIndex);
17891
18112
  if (measureOrder.length === 0)
17892
18113
  return emptyTimeline();
17893
18114
  let expandedGlobal = playbackGlobalMeasures(score, measureOrder), holdSchedule = suppressCadenzaFermataHolds(
17894
18115
  expandedGlobal,
17895
18116
  buildHoldSchedule(score, measureOrder, globalMeasures)
17896
- ), { tempoMap } = buildTempoMap(expandedGlobal, holdSchedule), tempoBuild = buildTempoModel(expandedGlobal, holdSchedule), { model, measureStartTimes, measureStartBeats } = tempoBuild, timing = { tempoMap, measureStartTimes, model, measureStartBeats }, allEvents = [], partChannels = allocatePartChannels(score.parts);
18117
+ ), { tempoMap } = buildTempoMap(expandedGlobal, holdSchedule), tempoBuild = buildTempoModel(expandedGlobal, holdSchedule), { model, measureStartTimes, measureStartBeats } = tempoBuild, timing = { tempoMap, measureStartTimes, model, measureStartBeats, playedSpans: spans }, allEvents = [], partChannels = allocatePartChannels(score.parts);
17897
18118
  for (let partIdx = 0; partIdx < score.parts.length; partIdx++)
17898
18119
  processPart(
17899
18120
  score,
@@ -17926,8 +18147,7 @@ function generateTimeline(inputScore, options) {
17926
18147
  let duration = model.timeAtBeat(tempoBuild.totalBeats), measureTimeSignatures = measureOrder.map((origIdx) => {
17927
18148
  let ts = resolveActiveTime(globalMeasures, origIdx);
17928
18149
  return { count: ts.count, unit: ts.unit };
17929
- });
17930
- return {
18150
+ }), result = {
17931
18151
  events: allEvents,
17932
18152
  duration,
17933
18153
  tempoMap,
@@ -17939,6 +18159,7 @@ function generateTimeline(inputScore, options) {
17939
18159
  measureTimeSignatures,
17940
18160
  diagnostics
17941
18161
  };
18162
+ return spans && (globalMeasures.some((measure) => measure.jump !== void 0) || spans.some((span) => span.startBeat > 0 || span.endBeat < span.measureBeats)) ? realizeNavigation(result, spans) : { ...result, playedSpans: spans };
17942
18163
  }
17943
18164
  function assignPhysicalMidiChannels(events, score) {
17944
18165
  let melodicChannels = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15], lanesByPart = /* @__PURE__ */ new Map();
@@ -18034,12 +18255,14 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
18034
18255
  globalMeasures,
18035
18256
  gmProgram,
18036
18257
  impliedDynamics,
18037
- staffMeterTable
18258
+ staffMeterTable,
18259
+ timing.playedSpans
18038
18260
  );
18039
18261
  for (let lane of dynamicProgram.lanes.values())
18040
18262
  emitDynamicsCc11(lane.envelope, partIdx, channel, allEvents, lane.id);
18041
18263
  let techniqueState = { program: gmProgram, muted: !1, instrumentKey: "initial" }, instruments = new Map(playbackInstruments(part).map(({ key, part: instrumentPart2 }) => [key, instrumentPart2])), programForInstrument = (key) => key === "initial" ? gmProgram : options?.instrumentPrograms?.get(partIdx)?.get(key) ?? instruments.get(key)?._x?.viritura?.midiProgram ?? -1;
18042
18264
  for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {
18265
+ expandedIdx > 0 && timing.playedSpans && !playedSpansContinue(timing.playedSpans[expandedIdx - 1], timing.playedSpans[expandedIdx]) && (allEvents.push(...pendingTieOffs.values(), ...Array.from(pendingLegato.values()).flat()), pendingTieOffs.clear(), pendingLegato.clear());
18043
18266
  let origMeasureIdx = measureOrder[expandedIdx], partMeasure = part.measures[origMeasureIdx];
18044
18267
  if (!partMeasure) continue;
18045
18268
  let measureStartTime = measureStartTimes[expandedIdx], measureStartBeat = measureStartBeats[expandedIdx], instrumentKey = playbackInstrumentKeyAt(part, origMeasureIdx), activeProgram = programForInstrument(instrumentKey), bowCapable = ARCO_CAPABLE_PROGRAMS.has(activeProgram), muteFamily = muteFamilyForProgram(activeProgram), techniqueEnabled = bowCapable || muteFamily !== null;
@@ -18049,6 +18272,8 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
18049
18272
  spanEndBeat: h.spanEndBeat ?? h.atBeat,
18050
18273
  insertBeat: h.atBeat
18051
18274
  })), ctx = {
18275
+ playedSpan: timing.playedSpans?.[expandedIdx],
18276
+ graceBoundaryBeat: timing.playedSpans && (expandedIdx === 0 || !playedSpansContinue(timing.playedSpans[expandedIdx - 1], timing.playedSpans[expandedIdx])) ? timing.playedSpans[expandedIdx].startBeat : void 0,
18052
18277
  tempoMap,
18053
18278
  model,
18054
18279
  measureStartBeat,
@@ -18079,7 +18304,12 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
18079
18304
  fermataGroups
18080
18305
  };
18081
18306
  (techniqueEnabled || instruments.size > 1) && (techniqueState = applyMeasureTechniques(
18082
- partMeasure.expressions,
18307
+ partMeasure.expressions?.filter(
18308
+ (expression) => beatInPlayedSpan(
18309
+ fractionToBeats(expression.position.fraction) * staffMeterRatioAt(staffMeterTable, origMeasureIdx, expression.staff),
18310
+ timing.playedSpans?.[expandedIdx]
18311
+ )
18312
+ ),
18083
18313
  ctx,
18084
18314
  techniqueState,
18085
18315
  {
@@ -18180,6 +18410,7 @@ function tryProcessSpecialEvent(ctx, event, measureStartTime, beatOffset, baseVe
18180
18410
  function processNoteEvent(ctx, event, measureStartTime, beatOffset, baseVelocity, tupletRatio) {
18181
18411
  ctx.gmProgram = ctx.instrumentAtBeatOffset?.(beatOffset * ctx.staffMeterRatio).program ?? ctx.gmProgram;
18182
18412
  let { partIndex, channel, tieTargets, out, timeSig, kitMidiMap, kitAltProgramMap } = ctx, beats = durationBeats4(event.duration) * tupletRatio;
18413
+ if (!beatInPlayedSpan(beatOffset * ctx.staffMeterRatio, ctx.playedSpan)) return beats;
18183
18414
  if (isRest(event))
18184
18415
  return flushVoiceLegato(ctx.pendingLegato, ctx.voiceKey, ctx.out), beats;
18185
18416
  resolveVoiceLegato({
@@ -18210,7 +18441,7 @@ function processNoteEvent(ctx, event, measureStartTime, beatOffset, baseVelocity
18210
18441
  for (let note of event.notes ?? []) {
18211
18442
  let midiNote = noteToMidi(note);
18212
18443
  if (midiNote < 0 || midiNote > 127) continue;
18213
- let noteId = note.id ?? "", isTiedTo = tieTargets.has(noteId), hasTieOut = note.ties?.some((t) => t.target) ?? !1;
18444
+ let noteId = note.id ?? "", isTiedTo = tieTargets.has(noteId) && ctx.pendingTieOffs.has(`${midiNote}:${partIndex}:${ctx.voiceKey}`), hasTieOut = note.ties?.some((t) => t.target) ?? !1;
18214
18445
  isTiedTo ? extendOrFinalizeTie(ctx, midiNote, startTime, durationSec, durationScale, hasTieOut) : emitFreshNote(
18215
18446
  ctx,
18216
18447
  midiNote,
@@ -18488,19 +18719,22 @@ function tryProcessTrill(ctx, event, measureStartTime, beatOffset, baseVelocity,
18488
18719
  function processGrace(ctx, grace, measureStartTime, beatOffset, tupletRatio) {
18489
18720
  let events = grace.content;
18490
18721
  if (events.length === 0) return 0;
18491
- let graceBeats = GRACE_NOTE_BEATS * tupletRatio, totalGraceBeats = events.length * graceBeats;
18492
- if (grace.graceType === "stealFollowing") {
18722
+ let graceBeats = GRACE_NOTE_BEATS * tupletRatio, totalGraceBeats = events.length * graceBeats, following = grace.graceType === "stealFollowing", principalBeat = beatOffset + (following ? totalGraceBeats : 0);
18723
+ if (!beatInPlayedSpan(principalBeat * ctx.staffMeterRatio, ctx.playedSpan))
18724
+ return following ? totalGraceBeats : 0;
18725
+ let graceCtx = { ...ctx, playedSpan: void 0 };
18726
+ if (following) {
18493
18727
  let cursor2 = beatOffset;
18494
18728
  for (let evt of events) {
18495
18729
  let ratio = graceBeats / durationBeats4(evt.duration);
18496
- processScopedNoteEvent(ctx, evt, measureStartTime, cursor2, ratio), cursor2 += graceBeats;
18730
+ processScopedNoteEvent(graceCtx, evt, measureStartTime, cursor2, ratio), cursor2 += graceBeats;
18497
18731
  }
18498
18732
  return totalGraceBeats;
18499
18733
  }
18500
- let cursor = beatOffset - totalGraceBeats;
18734
+ let cursor = Math.max(beatOffset - totalGraceBeats, (ctx.graceBoundaryBeat ?? -1 / 0) / ctx.staffMeterRatio);
18501
18735
  for (let evt of events) {
18502
18736
  let ratio = graceBeats / durationBeats4(evt.duration);
18503
- processScopedNoteEvent(ctx, evt, measureStartTime, cursor, ratio), cursor += graceBeats;
18737
+ processScopedNoteEvent(graceCtx, evt, measureStartTime, cursor, ratio), cursor += graceBeats;
18504
18738
  }
18505
18739
  return 0;
18506
18740
  }