@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/CHANGELOG.md +17 -0
- package/dist/index.js +783 -554
- package/dist/index.js.map +4 -4
- package/package.json +3 -3
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.
|
|
2216
|
+
var PACKAGE_VERSION = "0.1.1-next.31", BUNDLE_WORKER_FILE = "score-engine.worker.js";
|
|
2217
2217
|
function buildCommit() {
|
|
2218
|
-
return "
|
|
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
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
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
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
|
|
16257
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 +=
|
|
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
|
-
|
|
17270
|
-
|
|
17271
|
-
|
|
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
|
|
17274
|
-
let
|
|
17275
|
-
|
|
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
|
|
17278
|
-
|
|
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
|
|
17281
|
-
let
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
17285
|
-
|
|
17286
|
-
|
|
17287
|
-
|
|
17288
|
-
|
|
17289
|
-
|
|
17290
|
-
|
|
17291
|
-
|
|
17292
|
-
|
|
17293
|
-
|
|
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
|
-
|
|
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
|
|
17317
|
-
let
|
|
17318
|
-
|
|
17319
|
-
|
|
17320
|
-
|
|
17321
|
-
|
|
17322
|
-
|
|
17323
|
-
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
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
|
-
|
|
17330
|
-
|
|
17331
|
-
|
|
17332
|
-
|
|
17333
|
-
|
|
17334
|
-
|
|
17335
|
-
|
|
17336
|
-
|
|
17337
|
-
|
|
17338
|
-
|
|
17339
|
-
|
|
17340
|
-
|
|
17341
|
-
|
|
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
|
-
|
|
17345
|
-
|
|
17346
|
-
|
|
17347
|
-
|
|
17348
|
-
|
|
17349
|
-
|
|
17350
|
-
|
|
17351
|
-
|
|
17352
|
-
|
|
17353
|
-
|
|
17354
|
-
|
|
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
|
-
|
|
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,
|
|
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 +
|
|
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 +
|
|
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
|
|
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) <=
|
|
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 +
|
|
17530
|
-
let chained = prev !== void 0 && Math.abs(prev.endTime - startTime) <=
|
|
17531
|
-
(anchor) => anchor.time >= measureStartTime -
|
|
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(
|
|
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 -
|
|
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) <=
|
|
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) <=
|
|
17620
|
-
let endpoint = anchors.find((anchor) => anchor.time > text.startTime +
|
|
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 -
|
|
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) <=
|
|
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 +
|
|
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) <=
|
|
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 >
|
|
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),
|
|
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 (
|
|
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(
|
|
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(
|
|
18737
|
+
processScopedNoteEvent(graceCtx, evt, measureStartTime, cursor, ratio), cursor += graceBeats;
|
|
18509
18738
|
}
|
|
18510
18739
|
return 0;
|
|
18511
18740
|
}
|