@viritura/score-engine 0.2.1-next.40 → 0.2.1-next.42

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
@@ -358,11 +358,41 @@ init_define_import_meta_env();
358
358
 
359
359
  // packages/core/src/operations/chordSymbols/modifiers.ts
360
360
  init_define_import_meta_env();
361
+ function parseAlteredDegree(text) {
362
+ let match = /^([b#♭♯])(13|11|9|5)/u.exec(text);
363
+ if (match)
364
+ return {
365
+ degree: Number(match[2]),
366
+ alter: match[1] === "b" || match[1] === "\u266D" ? -1 : 1,
367
+ length: match[0].length
368
+ };
369
+ }
370
+ function finishModifiers(result, inGroup, needsDegree, afterDegree, afterComma) {
371
+ if (!(inGroup || needsDegree || !afterDegree || afterComma))
372
+ return {
373
+ added: [...new Set(result.added)].sort((a, b) => a - b),
374
+ omitted: [...new Set(result.omitted)].sort((a, b) => a - b),
375
+ ...result.altered ? {
376
+ altered: result.altered.filter(
377
+ (item, index, items) => items.findIndex((other) => other.degree === item.degree && other.alter === item.alter) === index
378
+ ).sort((a, b) => a.degree - b.degree || a.alter - b.alter)
379
+ } : {}
380
+ };
381
+ }
361
382
  function parseModifiers(text) {
362
383
  let result = { added: [], omitted: [] }, rest = text.trim().toLowerCase();
363
384
  if (!rest) return result;
364
385
  let operation, needsDegree = !1, afterDegree = !1, inGroup = !1, afterComma = !1;
365
386
  for (; rest; ) {
387
+ let altered = parseAlteredDegree(rest);
388
+ if (altered) {
389
+ if (needsDegree) return;
390
+ (result.altered ??= []).push({
391
+ degree: altered.degree,
392
+ alter: altered.alter
393
+ }), operation = void 0, afterDegree = !0, afterComma = !1, rest = rest.slice(altered.length).trimStart();
394
+ continue;
395
+ }
366
396
  let marker = /^(add|omit|no)/.exec(rest)?.[0];
367
397
  if (marker) {
368
398
  if (needsDegree) return;
@@ -386,14 +416,16 @@ function parseModifiers(text) {
386
416
  return;
387
417
  rest = rest.slice(1).trimStart();
388
418
  }
389
- if (!(inGroup || needsDegree || !afterDegree || afterComma))
390
- return {
391
- added: [...new Set(result.added)].sort((a, b) => a - b),
392
- omitted: [...new Set(result.omitted)].sort((a, b) => a - b)
393
- };
419
+ return finishModifiers(result, inGroup, needsDegree, afterDegree, afterComma);
394
420
  }
395
421
  var ADDED_INTERVALS = { 2: 2, 4: 5, 6: 9, 7: 10, 9: 2, 11: 5, 13: 9 };
396
422
  function applyModifiers(degrees, modifiers) {
423
+ let altered = /* @__PURE__ */ new Map();
424
+ for (let { degree, alter } of modifiers?.altered ?? []) {
425
+ let natural = degree === 5 ? 7 : ADDED_INTERVALS[degree];
426
+ altered.set(degree, [...altered.get(degree) ?? [], natural + alter]);
427
+ }
428
+ for (let [degree, intervals] of altered) degrees.set(degree, intervals);
397
429
  for (let degree of modifiers?.added ?? [])
398
430
  degrees.set(degree, [...degrees.get(degree) ?? [], ADDED_INTERVALS[degree]]);
399
431
  for (let degree of modifiers?.omitted ?? []) degrees.delete(degree);
@@ -582,11 +614,13 @@ function parseBaseQuality(text) {
582
614
  return extension === void 0 ? { quality } : { quality, extension };
583
615
  }
584
616
  function parseQuality(text) {
585
- let boundary = text.search(/add|omit|no|\(/i);
617
+ let complete = parseBaseQuality(text);
618
+ if (complete) return complete;
619
+ let boundary = text.search(/add|omit|no|\(|[b#♭♯](?:13|11|9|5)/iu);
586
620
  if (boundary < 0) return parseBaseQuality(text);
587
621
  let base = parseBaseQuality(text.slice(0, boundary).trim()), modifiers = parseModifiers(text.slice(boundary));
588
622
  if (!(!base || !modifiers))
589
- return base.quality === "major" && base.extension === void 0 && modifiers.added.length === 1 && modifiers.added[0] === 6 && modifiers.omitted.length === 0 ? { quality: "major", extension: 6 } : { ...base, modifiers };
623
+ return base.quality === "major" && base.extension === void 0 && modifiers.added.length === 1 && modifiers.added[0] === 6 && modifiers.omitted.length === 0 && !modifiers.altered?.length ? { quality: "major", extension: 6 } : { ...base, modifiers };
590
624
  }
591
625
  function parseChordSymbolText(input, position) {
592
626
  let parts = splitChordSymbolText(input);
@@ -602,9 +636,9 @@ function parseChordSymbolText(input, position) {
602
636
 
603
637
  // packages/core/src/operations/lyricExtenders.ts
604
638
  init_define_import_meta_env();
605
- function durationBeats(duration) {
606
- let base = DURATION_BEATS[duration.base], dots = 1;
607
- for (let index = 0; index < (duration.dots ?? 0); index++) dots += 0.5 ** (index + 1);
639
+ function durationBeats(duration2) {
640
+ let base = DURATION_BEATS[duration2.base], dots = 1;
641
+ for (let index = 0; index < (duration2.dots ?? 0); index++) dots += 0.5 ** (index + 1);
608
642
  return base * dots;
609
643
  }
610
644
  function quantityBeats(quantity) {
@@ -614,7 +648,7 @@ function walkTimedContent(content, startBeat, scale, partIndex, sequenceIndex, v
614
648
  let cursor = startBeat;
615
649
  for (let item of content) {
616
650
  if (item.type === "event") {
617
- let duration = durationBeats(item.duration) * scale;
651
+ let duration2 = durationBeats(item.duration) * scale;
618
652
  output.push({
619
653
  event: item,
620
654
  partIndex,
@@ -623,10 +657,10 @@ function walkTimedContent(content, startBeat, scale, partIndex, sequenceIndex, v
623
657
  staff,
624
658
  measureIndex,
625
659
  startBeat: cursor,
626
- durationBeats: duration,
660
+ durationBeats: duration2,
627
661
  rest: isRest(item),
628
662
  ordinal: ordinal.value++
629
- }), cursor += duration;
663
+ }), cursor += duration2;
630
664
  continue;
631
665
  }
632
666
  if (item.type === "space") {
@@ -2231,9 +2265,9 @@ function interpolateBeatAnchors(bounds, beat, endX) {
2231
2265
 
2232
2266
  // packages/score-engine/src/buildInfo.ts
2233
2267
  init_define_import_meta_env();
2234
- var PACKAGE_VERSION = "0.2.1-next.40", BUNDLE_WORKER_FILE = "score-engine.worker.js";
2268
+ var PACKAGE_VERSION = "0.2.1-next.42", BUNDLE_WORKER_FILE = "score-engine.worker.js";
2235
2269
  function buildCommit() {
2236
- return "aa2b59d2a29c0334fa5cf09255eae831c99f366d";
2270
+ return "addcef3a89be5d1d76a3f918b2aacbdc0dd7c30a";
2237
2271
  }
2238
2272
  function bundledAssetBase() {
2239
2273
  let moduleUrl = import.meta.url;
@@ -16659,8 +16693,8 @@ function parseVideoMedia(raw) {
16659
16693
  typeof contentHash == "string" && contentHash.length > 0 && (media.contentHash = contentHash);
16660
16694
  let demoSourceId = raw?.demoSourceId;
16661
16695
  typeof demoSourceId == "string" && demoSourceId.length > 0 && (media.demoSourceId = demoSourceId);
16662
- let duration = raw?.durationSeconds;
16663
- return typeof duration == "number" && Number.isFinite(duration) && duration > 0 && (media.durationSeconds = duration), media;
16696
+ let duration2 = raw?.durationSeconds;
16697
+ return typeof duration2 == "number" && Number.isFinite(duration2) && duration2 > 0 && (media.durationSeconds = duration2), media;
16664
16698
  }
16665
16699
  function parseRootVideoSync(rootX) {
16666
16700
  let raw = rootX?.viritura?.videoSync;
@@ -17153,7 +17187,7 @@ function buildTempoMap(globalMeasures, holds) {
17153
17187
  if (gm.tempos && gm.tempos.length > 0) {
17154
17188
  for (let tempo of gm.tempos) {
17155
17189
  if (tempo.bpm === void 0) continue;
17156
- let beatOffset = tempo.location ? tempo.location.fraction[0] / tempo.location.fraction[1] * mBeats : 0, qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);
17190
+ let beatOffset = tempo.location ? posBeats(tempo.location.fraction) : 0, qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);
17157
17191
  changes.push({ beatOffset, qpm });
17158
17192
  }
17159
17193
  changes.sort((a, b) => a.beatOffset - b.beatOffset);
@@ -17186,10 +17220,10 @@ function buildTempoModel(globalMeasures, holds) {
17186
17220
  nominalTime && (currentTime = { count: nominalTime.count, unit: nominalTime.unit });
17187
17221
  let mBeats = measureBeatsFromTime(gm.time ?? currentTime);
17188
17222
  if (measureStartBeats.push(cumBeats), gm.tempos && gm.tempos.length > 0) {
17189
- let sorted = [...gm.tempos].sort((a, b) => tempoBeat(a, mBeats) - tempoBeat(b, mBeats));
17223
+ let sorted = [...gm.tempos].sort((a, b) => tempoBeat(a) - tempoBeat(b));
17190
17224
  for (let tempo of sorted) {
17191
17225
  if (tempo.bpm === void 0) continue;
17192
- let startBeat = cumBeats + tempoBeat(tempo, mBeats), qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);
17226
+ let startBeat = cumBeats + tempoBeat(tempo), qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);
17193
17227
  regions.push({ startBeat, endBeat: 1 / 0, startBpm: qpm, endBpm: qpm });
17194
17228
  }
17195
17229
  }
@@ -17243,8 +17277,8 @@ function resolveGradualEndBeat(end, fromIdx, globalMeasures, measureStartBeats,
17243
17277
  if (start !== void 0)
17244
17278
  return start + posBeats(end.position.fraction);
17245
17279
  }
17246
- function tempoBeat(tempo, mBeats) {
17247
- return tempo.location ? tempo.location.fraction[0] / tempo.location.fraction[1] * mBeats : 0;
17280
+ function tempoBeat(tempo) {
17281
+ return tempo.location ? posBeats(tempo.location.fraction) : 0;
17248
17282
  }
17249
17283
 
17250
17284
  // packages/midi/src/cadenzaTiming.ts
@@ -17284,14 +17318,14 @@ function playbackGlobalMeasures(score, measureOrder) {
17284
17318
  let activeBeats = 4, activeTime = { count: 4, unit: 4 }, activeTempo = {
17285
17319
  bpm: DEFAULT_BPM,
17286
17320
  value: { base: "quarter" }
17287
- }, resolved = score.global.measures.map((original, measureIndex) => {
17288
- original.time && (activeTime = original.time);
17321
+ }, inheritedTempos = [], resolved = score.global.measures.map((original, measureIndex) => {
17322
+ inheritedTempos.push(activeTempo), original.time && (activeTime = original.time);
17289
17323
  let measure = {
17290
17324
  ...original,
17291
17325
  time: { ...activeTime },
17292
- tempos: original.tempos?.some((tempo) => !tempo.location || tempo.location.fraction[0] === 0) ? original.tempos : [{ ...activeTempo, location: { fraction: [0, 1] } }, ...original.tempos ?? []]
17293
- };
17294
- if (original.tempos?.length && (activeTempo = [...original.tempos].sort(
17326
+ tempos: original.tempos
17327
+ }, metronomicTempos = original.tempos?.filter((tempo) => tempo.bpm !== void 0);
17328
+ if (metronomicTempos?.length && (activeTempo = [...metronomicTempos].sort(
17295
17329
  (a, b) => (a.location?.fraction[0] ?? 0) / (a.location?.fraction[1] ?? 1) - (b.location?.fraction[0] ?? 0) / (b.location?.fraction[1] ?? 1)
17296
17330
  ).at(-1)), measure.time && (activeBeats = measure.time.count * 4 / measure.time.unit), measureIndex === 0 && measure.number === 0) {
17297
17331
  let pickupBeats = writtenMeasureBeats(score, measureIndex, 0);
@@ -17317,7 +17351,13 @@ function playbackGlobalMeasures(score, measureOrder) {
17317
17351
  }
17318
17352
  };
17319
17353
  });
17320
- return measureOrder.map((index) => resolved[index]);
17354
+ return measureOrder.map((index, expandedIndex) => {
17355
+ let measure = resolved[index];
17356
+ return expandedIndex === 0 || index === measureOrder[expandedIndex - 1] + 1 || measure.tempos?.some((tempo) => tempo.bpm !== void 0 && (!tempo.location || tempo.location.fraction[0] === 0)) ? measure : {
17357
+ ...measure,
17358
+ tempos: [{ ...inheritedTempos[index], location: { fraction: [0, 1] } }, ...measure.tempos ?? []]
17359
+ };
17360
+ });
17321
17361
  }
17322
17362
  function suppressCadenzaFermataHolds(measures, holds) {
17323
17363
  return holds.map(
@@ -17433,6 +17473,17 @@ function compileNavigation(measures, initialOrder, measureLengths) {
17433
17473
 
17434
17474
  // packages/midi/src/navigation/realization.ts
17435
17475
  init_define_import_meta_env();
17476
+ function navigationNoteKey(event) {
17477
+ return `${event.partIndex}:${event.playbackLaneId ?? event.channel}:${event.midiNote}:${event.drumKitProgram ?? ""}`;
17478
+ }
17479
+ function releaseUncontinuedNotes(activeNotes, continuations, time, out) {
17480
+ let continued = new Set(continuations.map(navigationNoteKey));
17481
+ for (let [key, pending] of activeNotes)
17482
+ if (!continued.has(key)) {
17483
+ for (let onset of pending) out.push({ ...onset, type: "noteOff", velocity: 0, time });
17484
+ pending.length = 0;
17485
+ }
17486
+ }
17436
17487
  function authoredOnsetInSpan(event, start, end) {
17437
17488
  return event.type === "noteOn" && event.scoreBeat !== void 0 && event.scoreBeat >= start - 1e-9 && event.scoreBeat < end - 1e-9;
17438
17489
  }
@@ -17463,7 +17514,7 @@ function spanTempoMap(timeline, index, span, model, outputBeat) {
17463
17514
  }
17464
17515
  return entries;
17465
17516
  }
17466
- function realizeNavigation(timeline, spans) {
17517
+ function realizeNavigation(timeline, spans, crossJumpContinuations = /* @__PURE__ */ new Map()) {
17467
17518
  let sourceSpans = spans.map((span, index) => ({
17468
17519
  startBeat: timeline.measureStartBeats[index] + span.startBeat,
17469
17520
  endBeat: timeline.measureStartBeats[index] + span.endBeat
@@ -17471,7 +17522,7 @@ function realizeNavigation(timeline, spans) {
17471
17522
  for (let index = 0; index < spans.length; index++) {
17472
17523
  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;
17473
17524
  measureStartBeats.push(outputBeat), tempoMap.push(...spanTempoMap(timeline, index, span, model, outputBeat));
17474
- 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 ?? ""}`;
17525
+ let translate = (time) => model.timeAtBeat(outputBeat) + Math.min(endTime - startTime, Math.max(0, time - startTime));
17475
17526
  for (let event of timeline.events) {
17476
17527
  let authoredOnset = authoredOnsetInSpan(event, source.startBeat, source.endBeat);
17477
17528
  if (event.time < startTime - 1e-9 && !authoredOnset) {
@@ -17495,10 +17546,10 @@ function realizeNavigation(timeline, spans) {
17495
17546
  )
17496
17547
  };
17497
17548
  events.push(output);
17498
- let key = noteKey(event), pending = activeNotes.get(key) ?? [];
17549
+ let key = navigationNoteKey(event), pending = activeNotes.get(key) ?? [];
17499
17550
  pending.push(output), activeNotes.set(key, pending);
17500
17551
  } else if (event.type === "noteOff") {
17501
- let pending = activeNotes.get(noteKey(event));
17552
+ let pending = activeNotes.get(navigationNoteKey(event));
17502
17553
  if (!pending?.length) continue;
17503
17554
  pending.shift(), events.push({ ...event, time: translate(event.time) });
17504
17555
  } else event.time < endTime - 1e-9 && events.push({ ...event, time: translate(event.time) });
@@ -17509,18 +17560,12 @@ function realizeNavigation(timeline, spans) {
17509
17560
  ...Array.from(controllers.values(), (event) => ({ ...event, time: model.timeAtBeat(outputBeat) }))
17510
17561
  );
17511
17562
  let next = spans[index + 1];
17512
- if (!playedSpansContinue(span, next))
17513
- for (let pending of activeNotes.values()) {
17514
- for (let onset of pending)
17515
- events.push({
17516
- ...onset,
17517
- type: "noteOff",
17518
- velocity: 0,
17519
- time: model.timeAtBeat(outputBeat + span.endBeat - span.startBeat, "release")
17520
- });
17521
- pending.length = 0;
17522
- }
17523
- outputBeat += span.endBeat - span.startBeat;
17563
+ playedSpansContinue(span, next) || releaseUncontinuedNotes(
17564
+ activeNotes,
17565
+ crossJumpContinuations.get(index + 1) ?? [],
17566
+ model.timeAtBeat(outputBeat + span.endBeat - span.startBeat, "release"),
17567
+ events
17568
+ ), outputBeat += span.endBeat - span.startBeat;
17524
17569
  }
17525
17570
  let rank = { noteOff: 0, programChange: 1, controlChange: 2, noteOn: 3 };
17526
17571
  return events.sort((a, b) => a.time - b.time || rank[a.type] - rank[b.type]), {
@@ -17687,9 +17732,9 @@ function compileChordPlayback(score, includeGlobalChords = !0) {
17687
17732
  function realizeChordPlayback(program, measureOrder, timing) {
17688
17733
  let notes = [], pitches = [], startBeat = 0, occurrence = 0, finish = (endBeat) => {
17689
17734
  if (endBeat <= startBeat) return;
17690
- let startTime = timing.model.timeAtBeat(startBeat), duration = timing.model.timeAtBeat(endBeat) - startTime;
17735
+ let startTime = timing.model.timeAtBeat(startBeat), duration2 = timing.model.timeAtBeat(endBeat) - startTime;
17691
17736
  for (let pitch of pitches)
17692
- notes.push({ id: `${CHORDS_PART_ID}:${occurrence}:${pitch}`, pitch, startTime, duration });
17737
+ notes.push({ id: `${CHORDS_PART_ID}:${occurrence}:${pitch}`, pitch, startTime, duration: duration2 });
17693
17738
  occurrence++;
17694
17739
  };
17695
17740
  for (let expandedIndex = 0; expandedIndex < measureOrder.length; expandedIndex++) {
@@ -17758,9 +17803,9 @@ var WHOLE_DURATIONS = {
17758
17803
  "512th": 1 / 512,
17759
17804
  "1024th": 1 / 1024
17760
17805
  };
17761
- function durationValue(duration) {
17762
- let base = WHOLE_DURATIONS[duration.base] ?? 0, value = base;
17763
- for (let dot = 0; dot < (duration.dots ?? 0); dot++) value += base / 2 ** (dot + 1);
17806
+ function durationValue(duration2) {
17807
+ let base = WHOLE_DURATIONS[duration2.base] ?? 0, value = base;
17808
+ for (let dot = 0; dot < (duration2.dots ?? 0); dot++) value += base / 2 ** (dot + 1);
17764
17809
  return value;
17765
17810
  }
17766
17811
  function contentInfo(content, scale = 1) {
@@ -17769,8 +17814,8 @@ function contentInfo(content, scale = 1) {
17769
17814
  if (item.type === "event") {
17770
17815
  let sounding = !!item.notes?.length || !!item.kitNotes?.length;
17771
17816
  sounding && (active = !0, onset ??= cursor);
17772
- let duration = durationValue(item.duration) * scale;
17773
- return cursor += duration, {
17817
+ let duration2 = durationValue(item.duration) * scale;
17818
+ return cursor += duration2, {
17774
17819
  duration: item.duration,
17775
17820
  sounding
17776
17821
  };
@@ -17780,13 +17825,13 @@ function contentInfo(content, scale = 1) {
17780
17825
  return nested.active && (active = !0, onset ??= cursor + nested.onset), cursor += nested.duration, { type: item.type, inner: item.inner, outer: item.outer, content: nested.signature };
17781
17826
  }
17782
17827
  if (item.type === "tremolo") {
17783
- let duration = durationValue(item.outer.duration) * item.outer.multiple * scale;
17784
- return item.content.some((event) => !!event.notes?.length || !!event.kitNotes?.length) && (active = !0, onset ??= cursor), cursor += duration, { type: item.type, outer: item.outer, content: item.content.map((event) => event.duration) };
17828
+ let duration2 = durationValue(item.outer.duration) * item.outer.multiple * scale;
17829
+ return item.content.some((event) => !!event.notes?.length || !!event.kitNotes?.length) && (active = !0, onset ??= cursor), cursor += duration2, { type: item.type, outer: item.outer, content: item.content.map((event) => event.duration) };
17785
17830
  }
17786
17831
  if (item.type === "grace") return { type: item.type, content: item.content.map((event) => event.duration) };
17787
17832
  if (item.type === "space") {
17788
- let duration = (item.duration[1] === 0 ? 0 : item.duration[0] / item.duration[1]) * scale;
17789
- return cursor += duration, { type: item.type, duration: item.duration };
17833
+ let duration2 = (item.duration[1] === 0 ? 0 : item.duration[0] / item.duration[1]) * scale;
17834
+ return cursor += duration2, { type: item.type, duration: item.duration };
17790
17835
  }
17791
17836
  return {};
17792
17837
  }), active, onset, duration: cursor };
@@ -18128,8 +18173,8 @@ function resolveVoiceLegato(a) {
18128
18173
  if (!bucket || bucket.length === 0) return;
18129
18174
  let onset = Math.max(
18130
18175
  0,
18131
- a.model.timeAtBeat(a.measureStartBeat + a.beatOffset) + timingHumanize(a.measureStartTime, a.beatOffset, a.partIndex)
18132
- ), nextDur = a.model.secondsForBeats(a.measureStartBeat + a.beatOffset, a.beats);
18176
+ (a.soundingOnset ?? a.model.timeAtBeat(a.measureStartBeat + a.beatOffset)) + timingHumanize(a.measureStartTime, a.beatOffset, a.partIndex)
18177
+ ), nextDur = a.soundingDuration ?? a.model.secondsForBeats(a.measureStartBeat + a.beatOffset, a.beats);
18133
18178
  resolveLegato(bucket, a.out, onset, nextDur, a.notes);
18134
18179
  }
18135
18180
 
@@ -18196,6 +18241,44 @@ function applyMeasureTechniques(expressions, ctx, state, caps, ratioForStaff = (
18196
18241
  return { program, muted, ...instrumentKey === void 0 ? {} : { instrumentKey } };
18197
18242
  }
18198
18243
 
18244
+ // packages/midi/src/tiePlayback/index.ts
18245
+ init_define_import_meta_env();
18246
+ function tiePlaybackKey(pitch, partIndex, voice) {
18247
+ return `${pitch}:${partIndex}:${voice}`;
18248
+ }
18249
+ function recordTieTargets(state, key, note) {
18250
+ let targets = note.ties?.filter((tie) => tie.lv !== !0 && tie.target !== void 0) ?? [];
18251
+ targets.length ? state.set(key, targets) : state.delete(key);
18252
+ }
18253
+ function tieContinuesTo(state, key, note) {
18254
+ return note.id !== void 0 && (state.get(key)?.some((tie) => tie.target === note.id) ?? !1);
18255
+ }
18256
+ function notesAtBeat(content, target, notes, offset = 0, ratio = 1) {
18257
+ let beat = offset;
18258
+ for (let item of content) {
18259
+ if (item.type === "event")
18260
+ Math.abs(beat - target) < 1e-9 && notes.push(...item.notes ?? []), beat += durationBeats4(item.duration) * ratio;
18261
+ else if (item.type === "tuplet") {
18262
+ let factor = durationBeats4(item.outer.duration) * item.outer.multiple / (durationBeats4(item.inner.duration) * item.inner.multiple);
18263
+ beat = notesAtBeat(item.content, target, notes, beat, ratio * factor);
18264
+ } else item.type === "space" ? beat += item.duration[0] / item.duration[1] * 4 * ratio : item.type === "tremolo" && (Math.abs(beat - target) < 1e-9 && notes.push(...item.content[0]?.notes ?? []), beat += durationBeats4(item.outer.duration) * item.outer.multiple * ratio);
18265
+ if (beat > target + 1e-9) break;
18266
+ }
18267
+ return beat;
18268
+ }
18269
+ function crossJumpContinuationKeys(state, measure, startBeat, partIndex, ratioForStaff) {
18270
+ let keys = /* @__PURE__ */ new Set();
18271
+ for (let [index, sequence] of (measure?.sequences ?? []).entries()) {
18272
+ let notes = [];
18273
+ notesAtBeat(sequence.content, startBeat / ratioForStaff(sequence.staff ?? 1), notes);
18274
+ for (let note of notes) {
18275
+ let key = tiePlaybackKey(pitchToMidi(note.pitch), partIndex, sequence.voice ?? `v${index}`);
18276
+ note.id !== void 0 && state.get(key)?.some((tie) => tie.targetType === "crossJump" && tie.target === note.id) && keys.add(key);
18277
+ }
18278
+ }
18279
+ return keys;
18280
+ }
18281
+
18199
18282
  // packages/midi/src/instrumentPlayback/index.ts
18200
18283
  init_define_import_meta_env();
18201
18284
  function instrumentPart(part, instrument) {
@@ -18808,6 +18891,107 @@ function laneForScope(program, partIndex, staff, voice) {
18808
18891
  return program.lanes.get(id) ?? program.lanes.values().next().value;
18809
18892
  }
18810
18893
 
18894
+ // packages/midi/src/rhythmRealization/index.ts
18895
+ init_define_import_meta_env();
18896
+ var EPS3 = 1e-9;
18897
+ function duration(d) {
18898
+ return DURATION_BEATS[d.base] * (d.dots ? 2 - 2 ** -d.dots : 1);
18899
+ }
18900
+ function itemBeats(item) {
18901
+ switch (item.type) {
18902
+ case "event":
18903
+ return duration(item.duration);
18904
+ case "space":
18905
+ return item.duration[0] * 4 / item.duration[1];
18906
+ case "tuplet":
18907
+ case "tremolo":
18908
+ return duration(item.outer.duration) * item.outer.multiple;
18909
+ case "grace":
18910
+ return 0;
18911
+ }
18912
+ }
18913
+ function eligible(item, sequence) {
18914
+ return item?.type === "event" && !item.duration.dots && (item.staff === void 0 || item.staff === (sequence.staff ?? 1)) && item.markings?.tremolo === void 0 && item.markings?.trill === void 0;
18915
+ }
18916
+ function swingAt(anchors, beat) {
18917
+ let result = { mode: "straight" };
18918
+ for (let anchor of anchors) {
18919
+ if (anchor.beat > beat + EPS3) break;
18920
+ result = anchor.swing;
18921
+ }
18922
+ return result;
18923
+ }
18924
+ function containsGrace(content) {
18925
+ return content.some((item) => item.type === "grace" || item.type === "tuplet" && containsGrace(item.content));
18926
+ }
18927
+ function compileSequence(sequence, anchors, metricOffset, measureBeats3, holdBeats) {
18928
+ let timings = /* @__PURE__ */ new Map();
18929
+ if (sequence.fullMeasure || containsGrace(sequence.content)) return timings;
18930
+ let cursor = 0;
18931
+ for (let index = 0; index < sequence.content.length; index++) {
18932
+ let first = sequence.content[index], second = sequence.content[index + 1], swing2 = swingAt(anchors, cursor), beats = itemBeats(first), pairEnd = cursor + 2 * beats;
18933
+ if (swing2.mode === "swing" && eligible(first, sequence) && eligible(second, sequence) && first.duration.base === swing2.subdivision && second.duration.base === swing2.subdivision && Math.abs((cursor + metricOffset) / (2 * beats) - Math.round((cursor + metricOffset) / (2 * beats))) < EPS3 && pairEnd <= measureBeats3 + EPS3 && !holdBeats.some((beat) => beat > cursor + EPS3 && beat < pairEnd - EPS3) && !anchors.some((anchor) => anchor.beat > cursor + EPS3 && anchor.beat < pairEnd - EPS3)) {
18934
+ let split = cursor + 2 * beats * swing2.ratio[0] / (swing2.ratio[0] + swing2.ratio[1]);
18935
+ timings.set(first, { start: cursor, end: split, pairStart: cursor, pairEnd }), timings.set(second, { start: split, end: pairEnd, pairStart: cursor, pairEnd });
18936
+ }
18937
+ cursor += beats;
18938
+ }
18939
+ return timings;
18940
+ }
18941
+ function compileRhythmProgram(score) {
18942
+ let indices = score.global.measures.map((_, index) => index), playbackMeasures = playbackGlobalMeasures(score, indices), holds = buildHoldSchedule(score, indices, score.global.measures), inherited = { mode: "straight" }, anchorsByMeasure = score.global.measures.map((measure) => {
18943
+ let anchors = [{ beat: -1 / 0, swing: inherited }], ordered = (measure.tempos ?? []).map((tempo, index) => ({
18944
+ tempo,
18945
+ index,
18946
+ beat: (tempo.location?.fraction[0] ?? 0) * 4 / (tempo.location?.fraction[1] ?? 1)
18947
+ })).sort((a, b) => a.beat - b.beat || a.index - b.index);
18948
+ for (let { tempo, beat } of ordered)
18949
+ if (tempo.swing) {
18950
+ if (tempo.swing.mode === "swing" && !tempo.swing.ratio.every((value) => Number.isSafeInteger(value) && value > 0))
18951
+ throw new Error("Swing ratios must be positive safe integers.");
18952
+ inherited = tempo.swing, anchors.push({ beat, swing: inherited });
18953
+ }
18954
+ return anchors;
18955
+ }), sequences = /* @__PURE__ */ new Map(), diagnostics = [];
18956
+ return score.parts.forEach((part, partIndex) => {
18957
+ let staffMeters = resolvePartStaffMeterTable(part, score.global.measures);
18958
+ part.measures.forEach((measure, measureIndex) => {
18959
+ let anchors = anchorsByMeasure[measureIndex];
18960
+ if (!anchors) return;
18961
+ let time = resolveActiveTime(score.global.measures, measureIndex), playbackTime = playbackMeasures[measureIndex].time, measureBeats3 = playbackTime.count * 4 / playbackTime.unit, metricOffset = measureIndex === 0 && score.global.measures[0].number === 0 ? time.count * 4 / time.unit - measureBeats3 : 0, diagnosed = /* @__PURE__ */ new Set(), hasSwing = [
18962
+ 0,
18963
+ ...anchors.map((anchor) => anchor.beat).filter((beat) => beat > 0 && beat < measureBeats3)
18964
+ ].some((beat) => swingAt(anchors, beat).mode === "swing");
18965
+ for (let sequence of measure.sequences) {
18966
+ let staff = sequence.staff ?? 1;
18967
+ if (staffMeters[measureIndex]?.has(staff)) {
18968
+ !diagnosed.has(staff) && hasSwing && (diagnostics.push({
18969
+ code: "swing-staff-meter-unsupported",
18970
+ source: { partIndex, measureIndex, staff },
18971
+ message: `Swing left literal in part ${partIndex}, measure ${measureIndex}, staff ${staff}: local staff meters are not supported.`,
18972
+ playbackLaneIds: []
18973
+ }), diagnosed.add(staff));
18974
+ continue;
18975
+ }
18976
+ time.display !== "senzaMisura" && sequences.set(
18977
+ sequence,
18978
+ compileSequence(
18979
+ sequence,
18980
+ anchors,
18981
+ metricOffset,
18982
+ measureBeats3,
18983
+ (holds[measureIndex] ?? []).map((hold) => hold.atBeat)
18984
+ )
18985
+ );
18986
+ }
18987
+ });
18988
+ }), { sequences, diagnostics };
18989
+ }
18990
+ function realizeRhythmEvent(timings, event, start, beats, span) {
18991
+ let timing = timings?.get(event);
18992
+ return !timing || span && (timing.pairStart < span.startBeat - EPS3 || timing.pairEnd > span.endBeat + EPS3) ? { start, beats } : { start: timing.start, beats: timing.end - timing.start };
18993
+ }
18994
+
18811
18995
  // packages/midi/src/timeline.ts
18812
18996
  var GM_DRUM_CHANNEL = 9;
18813
18997
  function noteToMidi(note) {
@@ -18848,7 +19032,15 @@ function generateTimeline(inputScore, options) {
18848
19032
  let expandedGlobal = playbackGlobalMeasures(score, measureOrder), holdSchedule = suppressCadenzaFermataHolds(
18849
19033
  expandedGlobal,
18850
19034
  buildHoldSchedule(score, measureOrder, globalMeasures)
18851
- ), { tempoMap } = buildTempoMap(expandedGlobal, holdSchedule), tempoBuild = buildTempoModel(expandedGlobal, holdSchedule), { model, measureStartTimes, measureStartBeats } = tempoBuild, timing = { tempoMap, measureStartTimes, model, measureStartBeats, playedSpans: spans }, allEvents = [], partChannels = allocatePartChannels(score.parts);
19035
+ ), { tempoMap } = buildTempoMap(expandedGlobal, holdSchedule), tempoBuild = buildTempoModel(expandedGlobal, holdSchedule), { model, measureStartTimes, measureStartBeats } = tempoBuild, rhythm = compileRhythmProgram(score), crossJumpContinuations = /* @__PURE__ */ new Map(), timing = {
19036
+ tempoMap,
19037
+ measureStartTimes,
19038
+ model,
19039
+ measureStartBeats,
19040
+ playedSpans: spans,
19041
+ rhythm,
19042
+ crossJumpContinuations
19043
+ }, allEvents = [], partChannels = allocatePartChannels(score.parts);
18852
19044
  for (let partIdx = 0; partIdx < score.parts.length; partIdx++)
18853
19045
  processPart(
18854
19046
  score,
@@ -18876,14 +19068,19 @@ function generateTimeline(inputScore, options) {
18876
19068
  { model, measureStartBeats, measureOrder },
18877
19069
  options?.instrumentPrograms,
18878
19070
  options?.partPrograms
18879
- ), diagnostics = [...assignPhysicalMidiChannels(allEvents, score), ...instrumentDiagnostics];
19071
+ ), diagnostics = [
19072
+ ...assignPhysicalMidiChannels(allEvents, score),
19073
+ ...instrumentDiagnostics,
19074
+ ...rhythm.diagnostics
19075
+ ];
19076
+ for (let diagnostic of diagnostics) options?.onDiagnostic?.(diagnostic);
18880
19077
  allEvents.sort(compareEvents);
18881
- let duration = model.timeAtBeat(tempoBuild.totalBeats), measureTimeSignatures = measureOrder.map((origIdx) => {
19078
+ let duration2 = model.timeAtBeat(tempoBuild.totalBeats), measureTimeSignatures = measureOrder.map((origIdx) => {
18882
19079
  let ts = resolveActiveTime(globalMeasures, origIdx);
18883
19080
  return { count: ts.count, unit: ts.unit };
18884
19081
  }), result = {
18885
19082
  events: allEvents,
18886
- duration,
19083
+ duration: duration2,
18887
19084
  tempoMap,
18888
19085
  measureStartTimes,
18889
19086
  model,
@@ -18893,7 +19090,7 @@ function generateTimeline(inputScore, options) {
18893
19090
  measureTimeSignatures,
18894
19091
  diagnostics
18895
19092
  };
18896
- 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 };
19093
+ return spans && (globalMeasures.some((measure) => measure.jump !== void 0) || spans.some((span) => span.startBeat > 0 || span.endBeat < span.measureBeats)) ? realizeNavigation(result, spans, crossJumpContinuations) : { ...result, playedSpans: spans };
18897
19094
  }
18898
19095
  function assignPhysicalMidiChannels(events, score) {
18899
19096
  let melodicChannels = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15], lanesByPart = /* @__PURE__ */ new Map();
@@ -18924,24 +19121,6 @@ function assignPhysicalMidiChannels(events, score) {
18924
19121
  event.channel = channelByLane.get(event.playbackLaneId ?? `part:${event.partIndex}`) ?? event.channel;
18925
19122
  return diagnostics;
18926
19123
  }
18927
- function collectNoteTieTargets(notes, targets) {
18928
- if (notes) {
18929
- for (let note of notes)
18930
- if (note.ties)
18931
- for (let tie of note.ties)
18932
- tie.target && targets.add(tie.target);
18933
- }
18934
- }
18935
- function collectTieTargets(content, targets) {
18936
- for (let item of content)
18937
- if (item.type === "event")
18938
- collectNoteTieTargets(item.notes, targets);
18939
- else if (item.type === "tuplet")
18940
- collectTieTargets(item.content, targets);
18941
- else if (item.type === "grace")
18942
- for (let evt of item.content)
18943
- collectNoteTieTargets(evt.notes, targets);
18944
- }
18945
19124
  function allocatePartChannels(parts) {
18946
19125
  let channels = [], melodicCounter = 0;
18947
19126
  for (let part of parts)
@@ -18969,18 +19148,17 @@ function buildKitAltProgramMap(part) {
18969
19148
  typeof comp.drumKit == "number" && (map ??= /* @__PURE__ */ new Map()).set(id, comp.drumKit);
18970
19149
  return map;
18971
19150
  }
18972
- function collectPartTieTargets(part, measureOrder) {
18973
- let tieTargets = /* @__PURE__ */ new Set();
18974
- for (let expandedIdx of measureOrder) {
18975
- let partMeasure = part.measures[expandedIdx];
18976
- if (partMeasure)
18977
- for (let seq of partMeasure.sequences)
18978
- collectTieTargets(seq.content, tieTargets);
19151
+ function retainCrossJumpNoteOffs(pending, targets, continuing, cutTime, continued, out) {
19152
+ for (let [key, noteOff] of pending) {
19153
+ if (continuing.has(key) && noteOff.time >= cutTime - 1e-7) {
19154
+ continued.push(noteOff);
19155
+ continue;
19156
+ }
19157
+ noteOff.time = Math.min(noteOff.time, cutTime), out.push(noteOff), pending.delete(key), targets.delete(key);
18979
19158
  }
18980
- return tieTargets;
18981
19159
  }
18982
19160
  function processPart(score, partIdx, options, partChannels, timing, measureOrder, globalMeasures, holdSchedule, impliedDynamics, allEvents) {
18983
- let { tempoMap, measureStartTimes, model, measureStartBeats } = timing, part = score.parts[partIdx], channel = partChannels[partIdx], gmProgram = options?.partPrograms?.[partIdx] ?? -1, kitMidiMap = buildKitMidiMap(score, part), kitAltProgramMap = buildKitAltProgramMap(part), tieTargets = collectPartTieTargets(part, measureOrder), pendingTieOffs = /* @__PURE__ */ new Map(), legatoOutIds = collectPartLegatoOut(part), pendingLegato = /* @__PURE__ */ new Map(), staffMeterTable = resolvePartStaffMeterTable(part, globalMeasures), dynamicProgram = compileDynamicProgram(
19161
+ let { tempoMap, measureStartTimes, model, measureStartBeats } = timing, part = score.parts[partIdx], channel = partChannels[partIdx], gmProgram = options?.partPrograms?.[partIdx] ?? -1, kitMidiMap = buildKitMidiMap(score, part), kitAltProgramMap = buildKitAltProgramMap(part), pendingTieTargets = /* @__PURE__ */ new Map(), pendingTieOffs = /* @__PURE__ */ new Map(), legatoOutIds = collectPartLegatoOut(part), pendingLegato = /* @__PURE__ */ new Map(), staffMeterTable = resolvePartStaffMeterTable(part, globalMeasures), dynamicProgram = compileDynamicProgram(
18984
19162
  part,
18985
19163
  partIdx,
18986
19164
  measureOrder,
@@ -18996,8 +19174,19 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
18996
19174
  emitDynamicsCc11(lane.envelope, partIdx, channel, allEvents, lane.id);
18997
19175
  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;
18998
19176
  for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {
18999
- 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());
19000
19177
  let origMeasureIdx = measureOrder[expandedIdx], partMeasure = part.measures[origMeasureIdx];
19178
+ if (expandedIdx > 0 && timing.playedSpans && !playedSpansContinue(timing.playedSpans[expandedIdx - 1], timing.playedSpans[expandedIdx])) {
19179
+ let continuing = crossJumpContinuationKeys(
19180
+ pendingTieTargets,
19181
+ partMeasure,
19182
+ timing.playedSpans[expandedIdx].startBeat,
19183
+ partIdx,
19184
+ (staff) => staffMeterRatioAt(staffMeterTable, origMeasureIdx, staff)
19185
+ ), cutTime = model.timeAtBeat(
19186
+ measureStartBeats[expandedIdx - 1] + timing.playedSpans[expandedIdx - 1].endBeat
19187
+ ), continued = timing.crossJumpContinuations.get(expandedIdx) ?? [];
19188
+ retainCrossJumpNoteOffs(pendingTieOffs, pendingTieTargets, continuing, cutTime, continued, allEvents), timing.crossJumpContinuations.set(expandedIdx, continued), allEvents.push(...Array.from(pendingLegato.values()).flat()), pendingLegato.clear();
19189
+ }
19001
19190
  if (!partMeasure) continue;
19002
19191
  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;
19003
19192
  instrumentKey !== techniqueState.instrumentKey && (techniqueState = { program: activeProgram, muted: !1, instrumentKey });
@@ -19013,7 +19202,7 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
19013
19202
  measureStartBeat,
19014
19203
  partIndex: partIdx,
19015
19204
  channel,
19016
- tieTargets,
19205
+ pendingTieTargets,
19017
19206
  pendingTieOffs,
19018
19207
  legatoOutIds,
19019
19208
  pendingLegato,
@@ -19056,7 +19245,7 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
19056
19245
  let lane = laneForSequence(dynamicProgram, partIdx, seq, seqIdx);
19057
19246
  ctx.sequenceStaff = seq.staff ?? 1, ctx.sequenceVoice = lane.voice, ctx.dynamicsEnvelope = lane.envelope, ctx.playbackLaneId = lane.id;
19058
19247
  let effectiveStaffMeter = staffMeterTable[origMeasureIdx]?.get(ctx.sequenceStaff);
19059
- ctx.timeSig = effectiveStaffMeter ? effectiveStaffMeter.timeSignature : activeTime, ctx.staffMeterRatio = staffMeterRatioAt(staffMeterTable, origMeasureIdx, ctx.sequenceStaff);
19248
+ ctx.timeSig = effectiveStaffMeter ? effectiveStaffMeter.timeSignature : activeTime, ctx.staffMeterRatio = staffMeterRatioAt(staffMeterTable, origMeasureIdx, ctx.sequenceStaff), ctx.rhythm = timing.rhythm.sequences.get(seq);
19060
19249
  let firstNewEvent = allEvents.length;
19061
19250
  processSequence(ctx, seq, measureStartTime, measureHolds, measureSpq), stampPlaybackLane(allEvents, firstNewEvent, lane.id), stampPendingLane(ctx, lane.id);
19062
19251
  });
@@ -19093,7 +19282,11 @@ function processScopedNoteEvent(ctx, event, measureStartTime, beatOffset, tuplet
19093
19282
  event,
19094
19283
  measureStartTime,
19095
19284
  beatOffset,
19096
- dynamicVelocityAt(ctx, measureStartTime, beatOffset),
19285
+ dynamicVelocityAt(
19286
+ ctx,
19287
+ measureStartTime,
19288
+ realizeRhythmEvent(ctx.rhythm, event, beatOffset, durationBeats4(event.duration) * tupletRatio, ctx.playedSpan).start
19289
+ ),
19097
19290
  tupletRatio
19098
19291
  );
19099
19292
  return stampPlaybackLane(ctx.out, firstNewEvent, lane.id), stampPendingLane(ctx, lane.id), ctx.dynamicsEnvelope = previousEnvelope, ctx.playbackLaneId = previousLaneId, beats;
@@ -19120,13 +19313,13 @@ function processContentItem(ctx, item, measureStartTime, beatOffset, tupletRatio
19120
19313
  return 0;
19121
19314
  }
19122
19315
  }
19123
- function resolveFermataDuration(ctx, event, naturalDurationSec, beatOffset) {
19316
+ function resolveFermataDuration(ctx, event, naturalDurationSec, beatOffset, soundingBeatOffset = beatOffset) {
19124
19317
  if (event.fermata && event.fermata.duration !== "none" && ctx.fermataGroups.length > 0) {
19125
19318
  let globalBeatOffset = beatOffset * ctx.staffMeterRatio, group = ctx.fermataGroups.find(
19126
19319
  (g) => globalBeatOffset >= g.startBeat - 1e-9 && globalBeatOffset < g.spanEndBeat - 1e-9
19127
19320
  );
19128
19321
  if (group) {
19129
- let startTime = eventTime(ctx, beatOffset), resumeTime = ctx.model.timeAtBeat(ctx.measureStartBeat + group.insertBeat);
19322
+ let startTime = eventTime(ctx, soundingBeatOffset), resumeTime = ctx.model.timeAtBeat(ctx.measureStartBeat + group.insertBeat);
19130
19323
  return { durationSec: Math.max(naturalDurationSec, resumeTime - startTime), durationScale: 1, held: !0 };
19131
19324
  }
19132
19325
  }
@@ -19143,10 +19336,11 @@ function tryProcessSpecialEvent(ctx, event, measureStartTime, beatOffset, baseVe
19143
19336
  }
19144
19337
  function processNoteEvent(ctx, event, measureStartTime, beatOffset, baseVelocity, tupletRatio) {
19145
19338
  ctx.gmProgram = ctx.instrumentAtBeatOffset?.(beatOffset * ctx.staffMeterRatio).program ?? ctx.gmProgram;
19146
- let { partIndex, channel, tieTargets, out, timeSig, kitMidiMap, kitAltProgramMap } = ctx, beats = durationBeats4(event.duration) * tupletRatio;
19339
+ let { partIndex, channel, out, timeSig, kitMidiMap, kitAltProgramMap } = ctx, beats = durationBeats4(event.duration) * tupletRatio;
19147
19340
  if (!beatInPlayedSpan(beatOffset * ctx.staffMeterRatio, ctx.playedSpan)) return beats;
19148
19341
  if (isRest(event))
19149
19342
  return flushVoiceLegato(ctx.pendingLegato, ctx.voiceKey, ctx.out), beats;
19343
+ let realized = realizeRhythmEvent(ctx.rhythm, event, beatOffset, beats, ctx.playedSpan), startTime = eventTime(ctx, realized.start), naturalDurationSec = eventSeconds(ctx, realized.start, realized.beats);
19150
19344
  resolveVoiceLegato({
19151
19345
  map: ctx.pendingLegato,
19152
19346
  voiceKey: ctx.voiceKey,
@@ -19157,15 +19351,17 @@ function processNoteEvent(ctx, event, measureStartTime, beatOffset, baseVelocity
19157
19351
  measureStartTime,
19158
19352
  beatOffset,
19159
19353
  beats,
19160
- notes: event.notes ?? []
19354
+ notes: event.notes ?? [],
19355
+ soundingOnset: startTime,
19356
+ soundingDuration: naturalDurationSec
19161
19357
  });
19162
19358
  let specialBeats = tryProcessSpecialEvent(ctx, event, measureStartTime, beatOffset, baseVelocity, beats);
19163
19359
  if (specialBeats !== null) return specialBeats;
19164
- let startTime = eventTime(ctx, beatOffset), timingOffset = timingHumanize(measureStartTime, beatOffset, partIndex), naturalDurationSec = eventSeconds(ctx, beatOffset, beats), {
19360
+ let timingOffset = timingHumanize(measureStartTime, beatOffset, partIndex), {
19165
19361
  durationSec,
19166
19362
  durationScale,
19167
19363
  held: fermataHeld
19168
- } = resolveFermataDuration(ctx, event, naturalDurationSec, beatOffset), velocity = Math.min(
19364
+ } = resolveFermataDuration(ctx, event, naturalDurationSec, beatOffset, realized.start), velocity = Math.min(
19169
19365
  127,
19170
19366
  Math.max(
19171
19367
  1,
@@ -19175,23 +19371,29 @@ function processNoteEvent(ctx, event, measureStartTime, beatOffset, baseVelocity
19175
19371
  for (let note of event.notes ?? []) {
19176
19372
  let midiNote = noteToMidi(note);
19177
19373
  if (midiNote < 0 || midiNote > 127) continue;
19178
- let noteId = note.id ?? "", isTiedTo = tieTargets.has(noteId) && ctx.pendingTieOffs.has(`${midiNote}:${partIndex}:${ctx.voiceKey}`), hasTieOut = note.ties?.some((t) => t.target) ?? !1;
19179
- isTiedTo ? extendOrFinalizeTie(ctx, midiNote, startTime, durationSec, durationScale, hasTieOut) : emitFreshNote(
19180
- ctx,
19181
- midiNote,
19182
- startTime,
19183
- timingOffset,
19184
- durationSec,
19185
- durationScale,
19186
- velocity,
19187
- hasTieOut,
19188
- legatoOut,
19189
- {
19190
- scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,
19191
- scoreDurationBeats: beats * ctx.staffMeterRatio,
19192
- scoreEventId: event.id
19193
- }
19194
- );
19374
+ let key = tiePlaybackKey(midiNote, partIndex, ctx.voiceKey), isTiedTo = tieContinuesTo(ctx.pendingTieTargets, key, note) && ctx.pendingTieOffs.has(key), hasTieOut = note.ties?.some((t) => t.lv !== !0 && t.target) ?? !1;
19375
+ if (isTiedTo)
19376
+ extendOrFinalizeTie(ctx, midiNote, startTime, durationSec, durationScale, hasTieOut);
19377
+ else {
19378
+ let previous = ctx.pendingTieOffs.get(key);
19379
+ previous && (out.push(previous), ctx.pendingTieOffs.delete(key)), emitFreshNote(
19380
+ ctx,
19381
+ midiNote,
19382
+ startTime,
19383
+ timingOffset,
19384
+ durationSec,
19385
+ durationScale,
19386
+ velocity,
19387
+ hasTieOut,
19388
+ legatoOut,
19389
+ {
19390
+ scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,
19391
+ scoreDurationBeats: beats * ctx.staffMeterRatio,
19392
+ scoreEventId: event.id
19393
+ }
19394
+ );
19395
+ }
19396
+ recordTieTargets(ctx.pendingTieTargets, key, note);
19195
19397
  }
19196
19398
  if (kitMidiMap) {
19197
19399
  let jitteredStart = Math.max(0, startTime + timingOffset);
@@ -19225,7 +19427,7 @@ function processNoteEvent(ctx, event, measureStartTime, beatOffset, baseVelocity
19225
19427
  return beats;
19226
19428
  }
19227
19429
  function extendOrFinalizeTie(ctx, midiNote, startTime, durationSec, durationScale, hasTieOut) {
19228
- let { partIndex, channel, pendingTieOffs, out } = ctx, tieKey = `${midiNote}:${partIndex}:${ctx.voiceKey}`, pending = pendingTieOffs.get(tieKey);
19430
+ let { partIndex, channel, pendingTieOffs, out } = ctx, tieKey = tiePlaybackKey(midiNote, partIndex, ctx.voiceKey), pending = pendingTieOffs.get(tieKey);
19229
19431
  pending && (hasTieOut ? pending.time = startTime + durationSec : (pending.time = startTime + durationSec * durationScale, out.push(pending), pendingTieOffs.delete(tieKey))), hasTieOut && !pendingTieOffs.has(tieKey) && pendingTieOffs.set(tieKey, {
19230
19432
  type: "noteOff",
19231
19433
  time: startTime + durationSec,
@@ -19246,7 +19448,7 @@ function emitFreshNote(ctx, midiNote, startTime, timingOffset, durationSec, dura
19246
19448
  channel,
19247
19449
  ...metadata
19248
19450
  }), hasTieOut) {
19249
- let tieKey = `${midiNote}:${partIndex}:${ctx.voiceKey}`;
19451
+ let tieKey = tiePlaybackKey(midiNote, partIndex, ctx.voiceKey);
19250
19452
  pendingTieOffs.set(tieKey, {
19251
19453
  type: "noteOff",
19252
19454
  time: startTime + durationSec,