@viritura/score-engine 0.1.1-next.29 → 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.29", 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 "861ac5447e15b8f748d78775a893a6bda2f11d6d";
2218
+ return "90b89f13680a7855b46725024057cc9919dfbc20";
2219
2219
  }
2220
2220
  function bundledAssetBase() {
2221
2221
  let moduleUrl = import.meta.url;
@@ -16244,19 +16244,575 @@ init_define_import_meta_env();
16244
16244
 
16245
16245
  // packages/midi/src/repeatExpansion.ts
16246
16246
  init_define_import_meta_env();
16247
- function expandMeasureOrder(globalMeasures, hints) {
16248
- if (globalMeasures.length === 0) return [];
16249
- let order = expandRepeats(globalMeasures);
16250
- 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);
16251
16262
  }
16252
- function detectToCodaMeasureIndex(score) {
16253
- for (let part of score.parts)
16254
- for (let i = 0; i < part.measures.length; i++) {
16255
- let expressions = part.measures[i]?.expressions;
16256
- if (!(!expressions || expressions.length === 0) && expressions.some((expr) => /\bto\s*coda\b/i.test(plainTextContent(expr.text))))
16257
- 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;
16258
16383
  }
16259
- 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);
16260
16816
  }
16261
16817
  function appendVoltaPass(order, endings, pass, startIdx, endIdx) {
16262
16818
  for (let j = startIdx; j <= endIdx; j++) {
@@ -16269,7 +16825,12 @@ function expandRepeats(globalMeasures) {
16269
16825
  for (; i < len; ) {
16270
16826
  let gm = globalMeasures[i];
16271
16827
  if (gm.repeatEnd) {
16272
- 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;
16273
16834
  for (let j = i; j >= 0; j--)
16274
16835
  if (globalMeasures[j].repeatStart) {
16275
16836
  repeatStartIdx = j;
@@ -16299,45 +16860,6 @@ function expandRepeats(globalMeasures) {
16299
16860
  }
16300
16861
  return order;
16301
16862
  }
16302
- function appendRange(order, start, endInclusive) {
16303
- for (let measure = start; measure <= endInclusive; measure++) order.push(measure);
16304
- }
16305
- function appendAlCodaReplay(result, replayStart, codaIdx, measureCount, toCodaMeasureIdx) {
16306
- let replayEnd = toCodaMeasureIdx !== void 0 && toCodaMeasureIdx >= replayStart && toCodaMeasureIdx < codaIdx ? toCodaMeasureIdx : codaIdx - 1;
16307
- appendRange(result, replayStart, replayEnd), appendRange(result, codaIdx, measureCount - 1);
16308
- }
16309
- function expandJumps(globalMeasures, initialOrder, toCodaMeasureIdx) {
16310
- let segnoIdx = -1, fineIdx = -1, codaIdx = -1;
16311
- for (let m = 0; m < globalMeasures.length; m++) {
16312
- let gm = globalMeasures[m];
16313
- gm.segno && (segnoIdx = m), gm.fine && (fineIdx = m), gm.coda && (codaIdx = m);
16314
- }
16315
- let jumpOrderPos = -1, jumpMeasureIdx = -1;
16316
- for (let p = 0; p < initialOrder.length; p++) {
16317
- let mi = initialOrder[p];
16318
- if (globalMeasures[mi].jump) {
16319
- jumpOrderPos = p, jumpMeasureIdx = mi;
16320
- break;
16321
- }
16322
- }
16323
- if (jumpOrderPos === -1) return [...initialOrder];
16324
- let jump = globalMeasures[jumpMeasureIdx].jump, result = initialOrder.slice(0, jumpOrderPos + 1);
16325
- switch (jump.type) {
16326
- case "dsAlFine":
16327
- segnoIdx >= 0 && fineIdx >= 0 && appendRange(result, segnoIdx, fineIdx);
16328
- break;
16329
- case "dsAlCoda":
16330
- segnoIdx >= 0 && codaIdx >= 0 && appendAlCodaReplay(result, segnoIdx, codaIdx, globalMeasures.length, toCodaMeasureIdx);
16331
- break;
16332
- case "dcAlCoda":
16333
- codaIdx >= 0 && appendAlCodaReplay(result, 0, codaIdx, globalMeasures.length, toCodaMeasureIdx);
16334
- break;
16335
- case "segno":
16336
- segnoIdx >= 0 && appendRange(result, segnoIdx, globalMeasures.length - 1);
16337
- break;
16338
- }
16339
- return result;
16340
- }
16341
16863
 
16342
16864
  // packages/midi/src/measureRepeats.ts
16343
16865
  init_define_import_meta_env();
@@ -16676,7 +17198,7 @@ function fermataMultiplier(fermata) {
16676
17198
  function caesuraBeats(style) {
16677
17199
  return style === "short" ? CAESURA_SHORT_BEATS : CAESURA_BEATS;
16678
17200
  }
16679
- function durationBeats2(d) {
17201
+ function durationBeats3(d) {
16680
17202
  let base = DURATION_BEATS[d.base] ?? 1;
16681
17203
  return d.dots ? base * (2 - Math.pow(2, -d.dots)) : base;
16682
17204
  }
@@ -16694,16 +17216,16 @@ function collectFromContent(content, startBeat, ratio, spans, caesuraByBeat) {
16694
17216
  let cursor = startBeat;
16695
17217
  for (let item of content)
16696
17218
  if (item.type === "event") {
16697
- let dur = durationBeats2(item.duration) * ratio, endBeat = cursor + dur;
17219
+ let dur = durationBeats3(item.duration) * ratio, endBeat = cursor + dur;
16698
17220
  if (item.fermata && item.fermata.duration !== "none") {
16699
17221
  let mult = fermataMultiplier(item.fermata);
16700
17222
  dur > 0 && mult > 1 && spans.push({ onset: cursor, end: endBeat, duration: dur, multiplier: mult });
16701
17223
  }
16702
17224
  item.markings?.caesura && mergeMax(caesuraByBeat, endBeat, caesuraBeats(item.markings.caesura.style)), cursor = endBeat;
16703
17225
  } else if (item.type === "tuplet") {
16704
- 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;
16705
17227
  collectFromContent(item.content, cursor, childRatio, spans, caesuraByBeat), cursor += outerBeats * ratio;
16706
- } 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);
16707
17229
  return cursor - startBeat;
16708
17230
  }
16709
17231
  function collectPartMeasureHolds(partMeasure, originalMeasureIndex, staffMeterTable, spans, caesuraByBeat) {
@@ -16738,94 +17260,25 @@ function buildHoldSchedule(score, measureOrder, globalMeasures) {
16738
17260
  let origIdx = measureOrder[expandedIdx], fermataSpans = [], caesuraByBeat = /* @__PURE__ */ new Map();
16739
17261
  for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
16740
17262
  let part = score.parts[partIndex];
16741
- collectPartMeasureHolds(
16742
- part.measures[origIdx],
16743
- origIdx,
16744
- staffMeterTables[partIndex],
16745
- fermataSpans,
16746
- caesuraByBeat
16747
- );
16748
- }
16749
- let globalCaesura = globalMeasures[origIdx]?.caesura;
16750
- if (globalCaesura) {
16751
- let mBeats = measureBeats2(resolveTimeSig(globalMeasures, origIdx));
16752
- mergeMax(caesuraByBeat, mBeats, caesuraBeats(globalCaesura.style));
16753
- }
16754
- let holds = groupFermataSpans(fermataSpans);
16755
- for (let [atBeat, extraBeats] of caesuraByBeat)
16756
- holds.push({ atBeat, extraBeats, kind: "caesura" });
16757
- holds.sort((a, b) => a.atBeat - b.atBeat), schedule.push(holds);
16758
- }
16759
- return schedule;
16760
- }
16761
-
16762
- // packages/midi/src/cadenzaTiming.ts
16763
- init_define_import_meta_env();
16764
- function contentBeats(content) {
16765
- let beats = 0;
16766
- for (let item of content)
16767
- switch (item.type) {
16768
- case "event":
16769
- beats += durationBeats3(item.duration.base, item.duration.dots);
16770
- break;
16771
- case "space":
16772
- beats += item.duration[0] / item.duration[1] * 4;
16773
- break;
16774
- case "tuplet":
16775
- case "tremolo":
16776
- beats += durationBeats3(item.outer.duration.base, item.outer.duration.dots) * item.outer.multiple;
16777
- break;
16778
- case "grace":
16779
- break;
16780
- }
16781
- return beats;
16782
- }
16783
- function durationBeats3(base, dots) {
16784
- let beats = DURATION_BEATS[base];
16785
- return dots ? beats * (2 - Math.pow(2, -dots)) : beats;
16786
- }
16787
- function writtenMeasureBeats(score, measureIndex, minimumBeats) {
16788
- return score.parts.reduce((maxBeats, part) => {
16789
- let measure = part.measures[measureIndex];
16790
- return measure ? Math.max(
16791
- maxBeats,
16792
- ...measure.sequences.filter((sequence) => !sequence.fullMeasure).map((sequence) => contentBeats(sequence.content))
16793
- ) : maxBeats;
16794
- }, minimumBeats);
16795
- }
16796
- function playbackGlobalMeasures(score, measureOrder) {
16797
- let activeBeats = 4;
16798
- return measureOrder.map((measureIndex) => {
16799
- let measure = score.global.measures[measureIndex];
16800
- if (measure.time && (activeBeats = measure.time.count * 4 / measure.time.unit), measureIndex === 0 && measure.number === 0) {
16801
- let pickupBeats = writtenMeasureBeats(score, measureIndex, 0);
16802
- if (pickupBeats > 0)
16803
- return {
16804
- ...measure,
16805
- __playbackNominalTime: measure.time ? { ...measure.time } : void 0,
16806
- time: {
16807
- ...measure.time,
16808
- count: pickupBeats,
16809
- unit: 4
16810
- }
16811
- };
16812
- }
16813
- if (measure.time?.display !== "senzaMisura") return measure;
16814
- let cadenzaBeats = writtenMeasureBeats(score, measureIndex, activeBeats);
16815
- return {
16816
- ...measure,
16817
- time: {
16818
- ...measure.time,
16819
- count: cadenzaBeats,
16820
- unit: 4
16821
- }
16822
- };
16823
- });
16824
- }
16825
- function suppressCadenzaFermataHolds(measures, holds) {
16826
- return holds.map(
16827
- (measureHolds, index) => measures[index]?.time?.display === "senzaMisura" ? measureHolds.filter((hold) => hold.kind !== "fermata") : [...measureHolds]
16828
- );
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;
16829
17282
  }
16830
17283
 
16831
17284
  // packages/midi/src/legato.ts
@@ -17001,388 +17454,139 @@ function applyMeasureTechniques(expressions, ctx, state, caps, ratioForStaff = (
17001
17454
  });
17002
17455
  } else if (mark.action.kind === "mute" && muteFamily) {
17003
17456
  if (mark.action.muted === muted) continue;
17004
- muted = mark.action.muted;
17005
- let curve = muted ? MUTE_CURVES[muteFamily] : OPEN_CURVE;
17006
- emitCc(ctx, time, CC_BRIGHTNESS, curve.brightness), emitCc(ctx, time, CC_RESONANCE, curve.resonance);
17007
- }
17008
- }
17009
- return { program, muted, ...instrumentKey === void 0 ? {} : { instrumentKey } };
17010
- }
17011
-
17012
- // packages/midi/src/instrumentPlayback/index.ts
17013
- init_define_import_meta_env();
17014
- function instrumentPart(part, instrument) {
17015
- return instrument ? {
17016
- ...part,
17017
- name: instrument.name ?? part.name,
17018
- _x: {
17019
- ...part._x,
17020
- viritura: {
17021
- ...part._x?.viritura,
17022
- instrumentId: instrument.instrumentId,
17023
- midiProgram: instrument.midiProgram,
17024
- initialInstrument: void 0,
17025
- instruments: void 0
17026
- }
17027
- }
17028
- } : part;
17029
- }
17030
- function playbackInstruments(part) {
17031
- let initial = initialInstrumentState(part);
17032
- if (initial.instrumentKey !== void 0 && !initial.instrument)
17033
- throw new Error(`Unknown initial instrument "${initial.instrumentKey}" in "${part.name}".`);
17034
- let result = [{ key: "initial", part: instrumentPart(part, initial.instrument) }], seen = /* @__PURE__ */ new Set();
17035
- for (let located of listInstrumentChanges(part)) {
17036
- let [numerator, denominator] = located.position;
17037
- if (denominator <= 0 || numerator < 0) throw new Error(`Invalid instrument-change position in "${part.name}".`);
17038
- let key = located.change.instrument;
17039
- if (!(key === void 0 || seen.has(key))) {
17040
- if (!located.state.instrument) throw new Error(`Unknown instrument "${key}" in "${part.name}".`);
17041
- seen.add(key), result.push({ key: `instrument:${key}`, part: instrumentPart(part, located.state.instrument) });
17042
- }
17043
- }
17044
- return result;
17045
- }
17046
- function playbackInstrumentKeyAt(part, measureIndex, beatOffset = 0) {
17047
- let key = "initial";
17048
- for (let located of listInstrumentChanges(part)) {
17049
- if (located.measureIndex > measureIndex || located.measureIndex === measureIndex && located.position[0] / located.position[1] * 4 > beatOffset) break;
17050
- located.change.instrument !== void 0 && (key = `instrument:${located.change.instrument}`);
17051
- }
17052
- return key;
17053
- }
17054
- function boundariesForPart(part, timing, programs, initialProgram) {
17055
- 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) => {
17056
- let program = programs?.get(key) ?? (key === "initial" ? initialProgram : void 0) ?? definitions.get(key)?._x?.viritura?.midiProgram;
17057
- boundaries.push({
17058
- beat,
17059
- time: timing.model.timeAtBeat(beat),
17060
- key,
17061
- program: typeof program == "number" && Number.isInteger(program) && program >= 0 && program <= 127 ? program : void 0
17062
- });
17063
- };
17064
- return timing.measureOrder.forEach((measureIndex, expandedIndex) => {
17065
- let start = timing.measureStartBeats[expandedIndex];
17066
- add(start, playbackInstrumentKeyAt(part, measureIndex));
17067
- for (let located of changes) {
17068
- if (located.measureIndex !== measureIndex || located.position[0] === 0) continue;
17069
- let [numerator, denominator] = located.position;
17070
- if (denominator <= 0 || numerator < 0) throw new Error(`Invalid instrument-change position in "${part.name}".`);
17071
- add(start + numerator / denominator * 4, `instrument:${located.change.instrument}`);
17072
- }
17073
- }), boundaries;
17074
- }
17075
- function applyInstrumentPlayback(score, events, timing, programs, initialPrograms) {
17076
- let diagnostics = [];
17077
- return score.parts.forEach((part, partIndex) => {
17078
- if (playbackInstruments(part).length === 1) return;
17079
- 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] : []));
17080
- lanes.size === 0 && lanes.add(`part:${partIndex}`);
17081
- let missingPrograms = [
17082
- ...new Set(boundaries.filter((boundary) => boundary.program === void 0).map((boundary) => boundary.key))
17083
- ];
17084
- missingPrograms.length > 0 && diagnostics.push({
17085
- code: "instrument-program-unavailable",
17086
- message: `Physical MIDI cannot select ${missingPrograms.join(", ")} in "${part.name}"; provide resolved instrumentPrograms or a MIDI program fallback.`,
17087
- playbackLaneIds: [...lanes]
17088
- });
17089
- for (let event of [...partEvents]) {
17090
- if (event.playbackLaneId) continue;
17091
- let [first, ...others] = lanes;
17092
- event.playbackLaneId = first;
17093
- for (let lane of others) {
17094
- let scoped = { ...event, playbackLaneId: lane };
17095
- events.push(scoped), partEvents.push(scoped);
17096
- }
17097
- }
17098
- let attackDelays = /* @__PURE__ */ new Map(), attackKey = (event) => JSON.stringify([event.playbackLaneId, event.channel, event.midiNote, event.scoreBeat, event.scoreEventId]);
17099
- for (let event of partEvents) {
17100
- if (event.type === "controlChange" && event.cc !== 74 && event.cc !== 71) continue;
17101
- let beat = event.scoreBeat ?? timing.model.beatAtTime(event.time), instrument = "initial", instrumentStart = 0;
17102
- for (let boundary of boundaries) {
17103
- if (boundary.beat > beat + 1e-9) break;
17104
- instrument = boundary.key, instrumentStart = boundary.time;
17105
- }
17106
- if (event.type === "noteOn") {
17107
- let delay = Math.max(0, instrumentStart - event.time);
17108
- attackDelays.set(attackKey(event), delay), event.time = Math.max(event.time, instrumentStart);
17109
- }
17110
- event.playbackInstrumentKey = instrument;
17111
- }
17112
- for (let event of partEvents)
17113
- event.type === "noteOff" && (event.time += attackDelays.get(attackKey(event)) ?? 0);
17114
- for (let lane of lanes) {
17115
- let previous;
17116
- for (let boundary of boundaries)
17117
- boundary.key !== previous && (previous = boundary.key, events.push({
17118
- type: "programChange",
17119
- time: boundary.time,
17120
- scoreBeat: boundary.beat,
17121
- midiNote: 0,
17122
- velocity: 0,
17123
- partIndex,
17124
- playbackLaneId: lane,
17125
- channel: partEvents.find((event) => event.playbackLaneId === lane)?.channel ?? 0,
17126
- playbackInstrumentKey: boundary.key,
17127
- instrumentChange: !0,
17128
- program: boundary.program
17129
- }));
17130
- }
17131
- }), diagnostics;
17132
- }
17133
-
17134
- // packages/midi/src/tempoMap.ts
17135
- init_define_import_meta_env();
17136
-
17137
- // packages/midi/src/tempoModel.ts
17138
- init_define_import_meta_env();
17139
- var bpmToSpb = (bpm) => 60 / bpm;
17140
- function integrateTime(b0, b, bpm0, slope) {
17141
- let db = b - b0;
17142
- if (Math.abs(slope) < 1e-6) return bpmToSpb(bpm0) * db;
17143
- let bpmB = bpm0 + slope * db;
17144
- return 60 / slope * Math.log(bpmB / bpm0);
17145
- }
17146
- function inverseBeat(dt, bpm0, slope) {
17147
- return Math.abs(slope) < 1e-6 ? dt * bpm0 / 60 : (bpm0 * Math.exp(dt * slope / 60) - bpm0) / slope;
17148
- }
17149
- var TempoModel = class _TempoModel {
17150
- knots;
17151
- constructor(knots) {
17152
- this.knots = knots;
17153
- }
17154
- /**
17155
- * Build a model from tempo regions and point insertions.
17156
- *
17157
- * `regions` need not be pre-sorted or gapless; they are sorted by `startBeat`
17158
- * and gaps are filled by extending the previous tempo (a held constant). The
17159
- * first region's start tempo applies from beat 0. `insertions` are point gaps
17160
- * (caesuras) keyed by beat.
17161
- */
17162
- static build(regions, insertions = []) {
17163
- let sorted = [...regions].sort((a, b) => a.startBeat - b.startBeat), insByBeat = /* @__PURE__ */ new Map();
17164
- for (let ins of insertions)
17165
- ins.seconds > 0 && insByBeat.set(ins.beat, (insByBeat.get(ins.beat) ?? 0) + ins.seconds);
17166
- let boundarySet = /* @__PURE__ */ new Set([0]);
17167
- for (let r of sorted) boundarySet.add(r.startBeat);
17168
- for (let ins of insByBeat.keys()) boundarySet.add(ins);
17169
- let boundaries = [...boundarySet].sort((a, b) => a - b), tempoAt = (beat) => {
17170
- let active;
17171
- for (let r of sorted)
17172
- if (r.startBeat <= beat + 1e-9) active = r;
17173
- else break;
17174
- if (!active) {
17175
- let first = sorted[0];
17176
- return { bpm: first ? first.startBpm : 120, slope: 0 };
17177
- }
17178
- let span = active.endBeat - active.startBeat, slope = span > 0 && Number.isFinite(span) ? (active.endBpm - active.startBpm) / span : 0;
17179
- return { bpm: active.startBpm + slope * (beat - active.startBeat), slope };
17180
- }, knots = [], time = 0, prevBeat = boundaries[0], prev = tempoAt(prevBeat), ins0 = insByBeat.get(prevBeat) ?? 0;
17181
- time += ins0, knots.push({ beat: prevBeat, time, bpm: prev.bpm, slope: prev.slope, insertBefore: ins0 });
17182
- for (let i = 1; i < boundaries.length; i++) {
17183
- let beat = boundaries[i];
17184
- time += integrateTime(prevBeat, beat, prev.bpm, prev.slope);
17185
- let ins = insByBeat.get(beat) ?? 0;
17186
- time += ins;
17187
- let t = tempoAt(beat);
17188
- knots.push({ beat, time, bpm: t.bpm, slope: t.slope, insertBefore: ins }), prevBeat = beat, prev = t;
17189
- }
17190
- return new _TempoModel(knots);
17191
- }
17192
- /** Index of the last knot whose beat ≤ `beat`. */
17193
- knotIndexForBeat(beat) {
17194
- let k = this.knots, lo = 0, hi = k.length - 1, ans = 0;
17195
- for (; lo <= hi; ) {
17196
- let mid = lo + hi >> 1;
17197
- k[mid].beat <= beat + 1e-9 ? (ans = mid, lo = mid + 1) : hi = mid - 1;
17198
- }
17199
- return ans;
17200
- }
17201
- /** Index of the last knot whose time ≤ `time`. */
17202
- knotIndexForTime(time) {
17203
- let k = this.knots, lo = 0, hi = k.length - 1, ans = 0;
17204
- for (; lo <= hi; ) {
17205
- let mid = lo + hi >> 1;
17206
- k[mid].time <= time + 1e-9 ? (ans = mid, lo = mid + 1) : hi = mid - 1;
17207
- }
17208
- return ans;
17209
- }
17210
- /**
17211
- * Absolute time (seconds) at a global quarter-note beat.
17212
- *
17213
- * `endpoint` selects how a point insertion EXACTLY at `beat` is treated:
17214
- * - `"onset"` (default): time AFTER the insertion — what an event STARTING at
17215
- * `beat` wants (it begins once the gap is over).
17216
- * - `"release"`: time BEFORE the insertion — what a note ENDING at `beat`
17217
- * wants (a caesura/gap at the release point isn't part of the note).
17218
- */
17219
- timeAtBeat(beat, endpoint = "onset") {
17220
- let i = this.knotIndexForBeat(beat), knot = this.knots[i], base = knot.time + integrateTime(knot.beat, beat, knot.bpm, knot.slope);
17221
- return endpoint === "release" && Math.abs(knot.beat - beat) < 1e-9 ? base - knot.insertBefore : base;
17222
- }
17223
- /**
17224
- * Seconds elapsed over `[beat, beat + beats)` — a note/event DURATION. The end
17225
- * uses release semantics so a point insertion (caesura) exactly at the release
17226
- * beat is not absorbed into the note's sounding length.
17227
- */
17228
- secondsForBeats(beat, beats) {
17229
- return this.timeAtBeat(beat + beats, "release") - this.timeAtBeat(beat);
17230
- }
17231
- /**
17232
- * Global quarter-note beat at an absolute time (inverse of {@link timeAtBeat}).
17233
- * Within a point-insertion gap, returns the insertion's beat (position is
17234
- * frozen during a caesura).
17235
- */
17236
- beatAtTime(time) {
17237
- let i = this.knotIndexForTime(time), knot = this.knots[i], next = this.knots[i + 1];
17238
- if (next) {
17239
- let musicEnd = next.time - next.insertBefore;
17240
- 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);
17241
17460
  }
17242
- return knot.beat + inverseBeat(time - knot.time, knot.bpm, knot.slope);
17243
- }
17244
- /** Seconds-per-quarter-note beat at a global beat. */
17245
- spbAtBeat(beat) {
17246
- let i = this.knotIndexForBeat(beat), knot = this.knots[i], bpmHere = knot.bpm + knot.slope * (beat - knot.beat);
17247
- return bpmToSpb(bpmHere);
17248
- }
17249
- /** Quarter-note BPM at a global beat. */
17250
- bpmAtBeat(beat) {
17251
- return 60 / this.spbAtBeat(beat);
17252
- }
17253
- /**
17254
- * Generate click times (seconds) for a metronome / studio click track over
17255
- * `[startBeat, endBeat)`. `clickBeats` are the beat positions to click,
17256
- * expressed as offsets within each "grid step" — but for the common case
17257
- * callers pass a flat list of global beat positions and we just map them.
17258
- * Returns the absolute time of every beat in `beats`.
17259
- */
17260
- clickTimes(beats) {
17261
- return beats.map((b) => this.timeAtBeat(b));
17262
17461
  }
17263
- };
17264
-
17265
- // packages/midi/src/tempoMap.ts
17266
- function playbackNominalTime(gm) {
17267
- return gm.__playbackNominalTime;
17462
+ return { program, muted, ...instrumentKey === void 0 ? {} : { instrumentKey } };
17268
17463
  }
17269
- var DEFAULT_BPM = 120, DEFAULT_TIME = { count: 4, unit: 4 };
17270
- function measureBeatsFromTime(ts) {
17271
- 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;
17272
17482
  }
17273
- function tempoNoteBeats(base, dots) {
17274
- let baseBeats = DURATION_BEATS[base] ?? 1;
17275
- 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;
17276
17498
  }
17277
- function effectiveQpm(bpm, tempoBase, tempoDots) {
17278
- 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;
17279
17506
  }
17280
- function buildTempoMap(globalMeasures, holds) {
17281
- let tempoMap = [], measureStartTimes = [], currentQpm = DEFAULT_BPM, currentSpq = 60 / currentQpm, currentTime = { count: DEFAULT_TIME.count, unit: DEFAULT_TIME.unit }, absoluteTime = 0, hasInitialTempo = !1;
17282
- for (let m = 0; m < globalMeasures.length; m++) {
17283
- let gm = globalMeasures[m], nominalTime = playbackNominalTime(gm) ?? gm.time;
17284
- nominalTime && (currentTime = { count: nominalTime.count, unit: nominalTime.unit });
17285
- let mBeats = measureBeatsFromTime(gm.time ?? currentTime);
17286
- measureStartTimes.push(absoluteTime);
17287
- let measureStartSpq = currentSpq, changes = [];
17288
- if (gm.tempos && gm.tempos.length > 0) {
17289
- for (let tempo of gm.tempos) {
17290
- let beatOffset = tempo.location ? tempo.location.fraction[0] / tempo.location.fraction[1] * mBeats : 0, qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);
17291
- changes.push({ beatOffset, qpm });
17292
- }
17293
- 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}`);
17294
17525
  }
17295
- let beatCursor = 0;
17296
- for (let change of changes)
17297
- change.beatOffset > beatCursor && (absoluteTime += (change.beatOffset - beatCursor) * currentSpq, beatCursor = change.beatOffset), tempoMap.push({
17298
- measureIndex: m,
17299
- beatInMeasure: change.beatOffset,
17300
- timeSeconds: absoluteTime,
17301
- bpm: change.qpm
17302
- }), m === 0 && change.beatOffset === 0 && (hasInitialTempo = !0), currentQpm = change.qpm, currentSpq = 60 / currentQpm;
17303
- mBeats > beatCursor && (absoluteTime += (mBeats - beatCursor) * currentSpq);
17304
- let measureHolds = holds?.[m];
17305
- if (measureHolds)
17306
- for (let hold of measureHolds)
17307
- absoluteTime += hold.extraBeats * measureStartSpq;
17308
- }
17309
- return hasInitialTempo || tempoMap.unshift({
17310
- measureIndex: 0,
17311
- beatInMeasure: 0,
17312
- timeSeconds: 0,
17313
- bpm: DEFAULT_BPM
17314
- }), { tempoMap, measureStartTimes };
17526
+ }), boundaries;
17315
17527
  }
17316
- function buildTempoModel(globalMeasures, holds) {
17317
- let regions = [], measureStartBeats = [], cumBeats = 0, currentTime = { count: DEFAULT_TIME.count, unit: DEFAULT_TIME.unit }, gradualDecls = [];
17318
- for (let m = 0; m < globalMeasures.length; m++) {
17319
- let gm = globalMeasures[m], nominalTime = playbackNominalTime(gm) ?? gm.time;
17320
- nominalTime && (currentTime = { count: nominalTime.count, unit: nominalTime.unit });
17321
- let mBeats = measureBeatsFromTime(gm.time ?? currentTime);
17322
- if (measureStartBeats.push(cumBeats), gm.tempos && gm.tempos.length > 0) {
17323
- let sorted = [...gm.tempos].sort((a, b) => tempoBeat(a, mBeats) - tempoBeat(b, mBeats));
17324
- for (let tempo of sorted) {
17325
- let startBeat = cumBeats + tempoBeat(tempo, mBeats), qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);
17326
- 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);
17327
17549
  }
17328
17550
  }
17329
- gm.gradualTempo && gradualDecls.push({
17330
- declIdx: m,
17331
- gt: gm.gradualTempo,
17332
- startBeat: cumBeats + posBeats(gm.gradualTempo.position.fraction)
17333
- }), cumBeats += mBeats;
17334
- }
17335
- 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) {
17336
- let idToIdx = buildMeasureIdToIndex(globalMeasures), constModel = TempoModel.build(regions);
17337
- for (let { declIdx, gt, startBeat } of gradualDecls) {
17338
- let endBeat = resolveGradualEndBeat(gt.end, declIdx, globalMeasures, measureStartBeats, idToIdx);
17339
- if (endBeat === void 0 || endBeat <= startBeat + 1e-9) continue;
17340
- let startBpm = gt.startBpm ?? constModel.bpmAtBeat(startBeat);
17341
- 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;
17342
17564
  }
17343
- }
17344
- regions.sort((a, b) => a.startBeat - b.startBeat);
17345
- for (let i = 0; i < regions.length - 1; i++)
17346
- regions[i].endBeat = regions[i + 1].startBeat;
17347
- let regionsModel = TempoModel.build(regions), insertions = [];
17348
- if (holds)
17349
- for (let m = 0; m < holds.length; m++) {
17350
- let measureHolds = holds[m];
17351
- if (!measureHolds || measureHolds.length === 0) continue;
17352
- let startBeat = measureStartBeats[m], spb = regionsModel.spbAtBeat(startBeat);
17353
- for (let hold of measureHolds)
17354
- 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
+ }));
17355
17583
  }
17356
- let model = insertions.length > 0 ? TempoModel.build(regions, insertions) : regionsModel, measureStartTimes = measureStartBeats.map((b) => model.timeAtBeat(b));
17357
- return { model, measureStartTimes, measureStartBeats, totalBeats: cumBeats };
17358
- }
17359
- function posBeats(frac) {
17360
- return !frac || frac[1] === 0 ? 0 : frac[0] / frac[1] * 4;
17361
- }
17362
- function buildMeasureIdToIndex(globalMeasures) {
17363
- let m = /* @__PURE__ */ new Map();
17364
- return globalMeasures.forEach((gm, i) => {
17365
- gm.id && !m.has(gm.id) && m.set(gm.id, i);
17366
- }), m;
17367
- }
17368
- function resolveGradualEndBeat(end, fromIdx, globalMeasures, measureStartBeats, idToIdx) {
17369
- let idx = idToIdx.get(end.measure);
17370
- if (idx === void 0) {
17371
- let parsed = Number.parseInt(end.measure, 10);
17372
- !Number.isNaN(parsed) && parsed >= 0 && parsed < globalMeasures.length && (idx = parsed);
17373
- }
17374
- idx === void 0 && (idx = fromIdx);
17375
- let start = measureStartBeats[idx];
17376
- if (start !== void 0)
17377
- return start + posBeats(end.position.fraction);
17378
- }
17379
- function tempoBeat(tempo, mBeats) {
17380
- return tempo.location ? tempo.location.fraction[0] / tempo.location.fraction[1] * mBeats : 0;
17584
+ }), diagnostics;
17381
17585
  }
17382
17586
 
17383
17587
  // packages/midi/src/dynamicsEnvelope.ts
17384
17588
  init_define_import_meta_env();
17385
- 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 = {
17386
17590
  n: { velocity: 1, cc11: 0 },
17387
17591
  pppppp: { velocity: 22, cc11: 11 },
17388
17592
  ppppp: { velocity: 28, cc11: 17 },
@@ -17449,7 +17653,7 @@ function stepRung(axes, dir) {
17449
17653
  function sampleAnchorsAxes(anchors, t) {
17450
17654
  let active = DEFAULT_DYNAMIC;
17451
17655
  for (let a of anchors) {
17452
- if (a.time > t + EPS) break;
17656
+ if (a.time > t + EPS2) break;
17453
17657
  active = { velocity: a.velocity, cc11: a.cc11 };
17454
17658
  }
17455
17659
  return active;
@@ -17465,24 +17669,24 @@ function gradualTextDirection(text) {
17465
17669
  if (/^cresc(?:endo)?\.?$/.test(normalized)) return 1;
17466
17670
  if (/^(?:dim(?:inuendo)?|decresc(?:endo)?)\.?$/.test(normalized)) return -1;
17467
17671
  }
17468
- function collectMeasureDynamicGroups(pm, originalMeasureIndex, measureStartBeat, model, expandedIdx, groups, staffMeterTable) {
17672
+ function collectMeasureDynamicGroups(pm, originalMeasureIndex, measureStartBeat, model, expandedIdx, groups, staffMeterTable, span) {
17469
17673
  for (let group of pm.dynamics ?? []) {
17470
- let ratio = staffMeterRatioAt(staffMeterTable, originalMeasureIndex, group.staff);
17471
- groups.push({
17674
+ let ratio = staffMeterRatioAt(staffMeterTable, originalMeasureIndex, group.staff), beat = fractionToBeats2(group.position.fraction) * ratio;
17675
+ beatInPlayedSpan(beat, span) && groups.push({
17472
17676
  group,
17473
- time: model.timeAtBeat(measureStartBeat + fractionToBeats2(group.position.fraction) * ratio),
17677
+ time: model.timeAtBeat(measureStartBeat + beat),
17474
17678
  expandedIdx
17475
17679
  });
17476
17680
  }
17477
17681
  }
17478
- function collectMeasureGradualTexts(pm, originalMeasureIndex, measureStartBeat, model, expandedIdx, gradualTexts, staffMeterTable) {
17682
+ function collectMeasureGradualTexts(pm, originalMeasureIndex, measureStartBeat, model, expandedIdx, gradualTexts, staffMeterTable, span) {
17479
17683
  for (let index = 0; index < (pm.expressions?.length ?? 0); index++) {
17480
17684
  let expression = pm.expressions[index], dir = gradualTextDirection(plainTextContent(expression.text));
17481
- 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({
17482
17688
  id: `text-gradual-${expandedIdx}-${index}`,
17483
- startTime: model.timeAtBeat(
17484
- measureStartBeat + fractionToBeats2(expression.position.fraction) * staffMeterRatioAt(staffMeterTable, originalMeasureIndex, expression.staff)
17485
- ),
17689
+ startTime: model.timeAtBeat(measureStartBeat + beat),
17486
17690
  dir,
17487
17691
  expandedIdx
17488
17692
  });
@@ -17505,7 +17709,7 @@ function resolveEndMeasureLocation(endMeasureId, fromExpandedIdx, measureOrder,
17505
17709
  }
17506
17710
  function explicitAnchorAt(anchors, time) {
17507
17711
  for (let a of anchors)
17508
- 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 };
17509
17713
  }
17510
17714
  function measureHasSoundingNotes(part, measureIndex) {
17511
17715
  return measureIndex === void 0 ? !1 : (part.measures[measureIndex]?.sequences ?? []).some(
@@ -17526,15 +17730,15 @@ function resolveRamps(timed, anchors) {
17526
17730
  startsAfterSilence,
17527
17731
  measureStartTime
17528
17732
  } of ordered) {
17529
- if (endTime <= startTime + EPS) continue;
17530
- let chained = prev !== void 0 && Math.abs(prev.endTime - startTime) <= EPS, currentMeasureAnchor = anchors.some(
17531
- (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
17532
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 };
17533
17737
  ramps.push(ramp), prev = ramp;
17534
17738
  }
17535
17739
  return ramps;
17536
17740
  }
17537
- function collectExpandedDynamics(part, measureOrder, measureStartBeats, model, staffMeterTable, timedGroups, gradualTexts) {
17741
+ function collectExpandedDynamics(part, measureOrder, measureStartBeats, model, staffMeterTable, timedGroups, gradualTexts, playedSpans) {
17538
17742
  for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {
17539
17743
  let originalMeasureIndex = measureOrder[expandedIdx], measure = part.measures[originalMeasureIndex];
17540
17744
  if (!measure) continue;
@@ -17546,7 +17750,8 @@ function collectExpandedDynamics(part, measureOrder, measureStartBeats, model, s
17546
17750
  model,
17547
17751
  expandedIdx,
17548
17752
  timedGroups,
17549
- staffMeterTable
17753
+ staffMeterTable,
17754
+ playedSpans?.[expandedIdx]
17550
17755
  ), collectMeasureGradualTexts(
17551
17756
  measure,
17552
17757
  originalMeasureIndex,
@@ -17554,13 +17759,23 @@ function collectExpandedDynamics(part, measureOrder, measureStartBeats, model, s
17554
17759
  model,
17555
17760
  expandedIdx,
17556
17761
  gradualTexts,
17557
- staffMeterTable
17762
+ staffMeterTable,
17763
+ playedSpans?.[expandedIdx]
17558
17764
  );
17559
17765
  }
17560
17766
  }
17561
- function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, globalMeasures, impliedAnchors = [], resolvedStaffMeters) {
17767
+ function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, globalMeasures, impliedAnchors = [], resolvedStaffMeters, playedSpans) {
17562
17768
  let anchors = [], attacks = [], timedGroups = [], gradualTexts = [], idToOrig = buildMeasureIdToOrig(globalMeasures), staffMeterTable = resolvedStaffMeters ?? resolvePartStaffMeterTable(part, globalMeasures);
17563
- 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
+ );
17564
17779
  let typeRank = { immediate: 0, relative: 1, accent: 2, gradual: 3 };
17565
17780
  timedGroups.sort(
17566
17781
  (a, b) => a.time - b.time || typeRank[a.group.type] - typeRank[b.group.type] || a.group.id.localeCompare(b.group.id)
@@ -17570,7 +17785,7 @@ function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, glo
17570
17785
  let body = axesOf(group.value);
17571
17786
  anchors.push({ time, velocity: body.velocity, cc11: body.cc11 });
17572
17787
  } else if (group.type === "relative") {
17573
- 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);
17574
17789
  anchors.push({ time, velocity: next.velocity, cc11: next.cc11 });
17575
17790
  } else if (group.type === "accent" && (attacks.push({ time, attackVelocity: axesOf(group.value).velocity }), group.residualValue !== void 0)) {
17576
17791
  let body = axesOf(group.residualValue);
@@ -17579,9 +17794,9 @@ function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, glo
17579
17794
  for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {
17580
17795
  let measureIndex = measureOrder[expandedIdx];
17581
17796
  for (let implied of impliedAnchors) {
17582
- if (implied.measureIndex !== measureIndex) continue;
17797
+ if (implied.measureIndex !== measureIndex || !beatInPlayedSpan(fractionToBeats2(implied.position), playedSpans?.[expandedIdx])) continue;
17583
17798
  let time = model.timeAtBeat(measureStartBeats[expandedIdx] + fractionToBeats2(implied.position));
17584
- if (anchors.some((anchor) => Math.abs(anchor.time - time) <= EPS)) continue;
17799
+ if (anchors.some((anchor) => Math.abs(anchor.time - time) <= EPS2)) continue;
17585
17800
  let axes = axesOf(implied.value);
17586
17801
  anchors.push({ time, velocity: axes.velocity, cc11: axes.cc11 });
17587
17802
  }
@@ -17616,8 +17831,8 @@ function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, glo
17616
17831
  });
17617
17832
  }
17618
17833
  for (let text of gradualTexts) {
17619
- if (timed.some((hairpin) => Math.abs(hairpin.startTime - text.startTime) <= EPS)) continue;
17620
- 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);
17621
17836
  endpoint && timed.push({
17622
17837
  groupId: text.id,
17623
17838
  startTime: text.startTime,
@@ -17632,7 +17847,7 @@ function buildDynamicsEnvelope(part, measureOrder, measureStartBeats, model, glo
17632
17847
  }
17633
17848
  function rampAt(env, t) {
17634
17849
  for (let r of env.ramps)
17635
- if (t >= r.startTime - EPS && t <= r.endTime + EPS) return r;
17850
+ if (t >= r.startTime - EPS2 && t <= r.endTime + EPS2) return r;
17636
17851
  }
17637
17852
  function sampleDynamics(env, t) {
17638
17853
  let r = rampAt(env, t);
@@ -17644,7 +17859,7 @@ function sampleDynamics(env, t) {
17644
17859
  }
17645
17860
  function attackAt(env, t) {
17646
17861
  for (let c of env.attacks)
17647
- if (Math.abs(c.time - t) <= EPS) return c;
17862
+ if (Math.abs(c.time - t) <= EPS2) return c;
17648
17863
  }
17649
17864
  function noteVelocityAt(env, t) {
17650
17865
  let c = attackAt(env, t);
@@ -17652,7 +17867,7 @@ function noteVelocityAt(env, t) {
17652
17867
  }
17653
17868
  function rampQuantizationEvents(ramp) {
17654
17869
  let start = ramp.start.cc11, end = ramp.end.cc11;
17655
- if (start === end || ramp.endTime <= ramp.startTime + EPS)
17870
+ if (start === end || ramp.endTime <= ramp.startTime + EPS2)
17656
17871
  return [
17657
17872
  { time: ramp.startTime, value: start },
17658
17873
  { time: ramp.endTime, value: end }
@@ -17675,13 +17890,13 @@ function cc11Events(env) {
17675
17890
  let out = [];
17676
17891
  for (let e of raw) {
17677
17892
  let prev = out[out.length - 1];
17678
- if (prev && Math.abs(prev.time - e.time) <= EPS) {
17893
+ if (prev && Math.abs(prev.time - e.time) <= EPS2) {
17679
17894
  out[out.length - 1] = e;
17680
17895
  continue;
17681
17896
  }
17682
17897
  prev && prev.value === e.value || out.push(e);
17683
17898
  }
17684
- 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;
17685
17900
  }
17686
17901
  function emitDynamicsCc11(env, partIndex, channel, out, playbackLaneId2) {
17687
17902
  for (let ev of cc11Events(env))
@@ -17824,7 +18039,7 @@ function collectRampDiagnostics(lanes) {
17824
18039
  }
17825
18040
  return diagnostics;
17826
18041
  }
17827
- 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) {
17828
18043
  let scoped = part.measures.some(
17829
18044
  (measure) => measure.dynamics?.some((group) => group.staff !== void 0 || group.voice !== void 0)
17830
18045
  ), scopes = collectLaneScopes(part), selectedScopes = scoped ? scopes : [scopes[0]], profile = selectDynamicResponseProfile(gmProgram), lanes = /* @__PURE__ */ new Map();
@@ -17837,7 +18052,8 @@ function compileDynamicProgram(part, partIndex, measureOrder, measureStartBeats,
17837
18052
  model,
17838
18053
  globalMeasures,
17839
18054
  impliedAnchors,
17840
- staffMeterTable
18055
+ staffMeterTable,
18056
+ playedSpans
17841
18057
  ),
17842
18058
  profile
17843
18059
  );
@@ -17892,13 +18108,13 @@ function generateTimeline(inputScore, options) {
17892
18108
  let score = expandScoreMeasureRepeats(inputScore), impliedSectionDynamics = analyzeImpliedSectionDynamics(score), globalMeasures = score.global.measures;
17893
18109
  if (globalMeasures.length === 0)
17894
18110
  return emptyTimeline();
17895
- 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);
17896
18112
  if (measureOrder.length === 0)
17897
18113
  return emptyTimeline();
17898
18114
  let expandedGlobal = playbackGlobalMeasures(score, measureOrder), holdSchedule = suppressCadenzaFermataHolds(
17899
18115
  expandedGlobal,
17900
18116
  buildHoldSchedule(score, measureOrder, globalMeasures)
17901
- ), { 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);
17902
18118
  for (let partIdx = 0; partIdx < score.parts.length; partIdx++)
17903
18119
  processPart(
17904
18120
  score,
@@ -17931,8 +18147,7 @@ function generateTimeline(inputScore, options) {
17931
18147
  let duration = model.timeAtBeat(tempoBuild.totalBeats), measureTimeSignatures = measureOrder.map((origIdx) => {
17932
18148
  let ts = resolveActiveTime(globalMeasures, origIdx);
17933
18149
  return { count: ts.count, unit: ts.unit };
17934
- });
17935
- return {
18150
+ }), result = {
17936
18151
  events: allEvents,
17937
18152
  duration,
17938
18153
  tempoMap,
@@ -17944,6 +18159,7 @@ function generateTimeline(inputScore, options) {
17944
18159
  measureTimeSignatures,
17945
18160
  diagnostics
17946
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 };
17947
18163
  }
17948
18164
  function assignPhysicalMidiChannels(events, score) {
17949
18165
  let melodicChannels = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15], lanesByPart = /* @__PURE__ */ new Map();
@@ -18039,12 +18255,14 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
18039
18255
  globalMeasures,
18040
18256
  gmProgram,
18041
18257
  impliedDynamics,
18042
- staffMeterTable
18258
+ staffMeterTable,
18259
+ timing.playedSpans
18043
18260
  );
18044
18261
  for (let lane of dynamicProgram.lanes.values())
18045
18262
  emitDynamicsCc11(lane.envelope, partIdx, channel, allEvents, lane.id);
18046
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;
18047
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());
18048
18266
  let origMeasureIdx = measureOrder[expandedIdx], partMeasure = part.measures[origMeasureIdx];
18049
18267
  if (!partMeasure) continue;
18050
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;
@@ -18054,6 +18272,8 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
18054
18272
  spanEndBeat: h.spanEndBeat ?? h.atBeat,
18055
18273
  insertBeat: h.atBeat
18056
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,
18057
18277
  tempoMap,
18058
18278
  model,
18059
18279
  measureStartBeat,
@@ -18084,7 +18304,12 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
18084
18304
  fermataGroups
18085
18305
  };
18086
18306
  (techniqueEnabled || instruments.size > 1) && (techniqueState = applyMeasureTechniques(
18087
- 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
+ ),
18088
18313
  ctx,
18089
18314
  techniqueState,
18090
18315
  {
@@ -18185,6 +18410,7 @@ function tryProcessSpecialEvent(ctx, event, measureStartTime, beatOffset, baseVe
18185
18410
  function processNoteEvent(ctx, event, measureStartTime, beatOffset, baseVelocity, tupletRatio) {
18186
18411
  ctx.gmProgram = ctx.instrumentAtBeatOffset?.(beatOffset * ctx.staffMeterRatio).program ?? ctx.gmProgram;
18187
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;
18188
18414
  if (isRest(event))
18189
18415
  return flushVoiceLegato(ctx.pendingLegato, ctx.voiceKey, ctx.out), beats;
18190
18416
  resolveVoiceLegato({
@@ -18215,7 +18441,7 @@ function processNoteEvent(ctx, event, measureStartTime, beatOffset, baseVelocity
18215
18441
  for (let note of event.notes ?? []) {
18216
18442
  let midiNote = noteToMidi(note);
18217
18443
  if (midiNote < 0 || midiNote > 127) continue;
18218
- 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;
18219
18445
  isTiedTo ? extendOrFinalizeTie(ctx, midiNote, startTime, durationSec, durationScale, hasTieOut) : emitFreshNote(
18220
18446
  ctx,
18221
18447
  midiNote,
@@ -18493,19 +18719,22 @@ function tryProcessTrill(ctx, event, measureStartTime, beatOffset, baseVelocity,
18493
18719
  function processGrace(ctx, grace, measureStartTime, beatOffset, tupletRatio) {
18494
18720
  let events = grace.content;
18495
18721
  if (events.length === 0) return 0;
18496
- let graceBeats = GRACE_NOTE_BEATS * tupletRatio, totalGraceBeats = events.length * graceBeats;
18497
- 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) {
18498
18727
  let cursor2 = beatOffset;
18499
18728
  for (let evt of events) {
18500
18729
  let ratio = graceBeats / durationBeats4(evt.duration);
18501
- processScopedNoteEvent(ctx, evt, measureStartTime, cursor2, ratio), cursor2 += graceBeats;
18730
+ processScopedNoteEvent(graceCtx, evt, measureStartTime, cursor2, ratio), cursor2 += graceBeats;
18502
18731
  }
18503
18732
  return totalGraceBeats;
18504
18733
  }
18505
- let cursor = beatOffset - totalGraceBeats;
18734
+ let cursor = Math.max(beatOffset - totalGraceBeats, (ctx.graceBoundaryBeat ?? -1 / 0) / ctx.staffMeterRatio);
18506
18735
  for (let evt of events) {
18507
18736
  let ratio = graceBeats / durationBeats4(evt.duration);
18508
- processScopedNoteEvent(ctx, evt, measureStartTime, cursor, ratio), cursor += graceBeats;
18737
+ processScopedNoteEvent(graceCtx, evt, measureStartTime, cursor, ratio), cursor += graceBeats;
18509
18738
  }
18510
18739
  return 0;
18511
18740
  }