@dawcore/transport 0.0.3 → 0.0.4
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/README.md +33 -1
- package/dist/index.d.mts +47 -4
- package/dist/index.d.ts +47 -4
- package/dist/index.js +185 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +185 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -215,28 +215,43 @@ var SampleTimeline = class {
|
|
|
215
215
|
};
|
|
216
216
|
|
|
217
217
|
// src/timeline/tempo-map.ts
|
|
218
|
+
var CURVE_EPSILON = 1e-15;
|
|
219
|
+
var CURVE_SUBDIVISIONS = 64;
|
|
220
|
+
function curveNormalizedAt(x, slope) {
|
|
221
|
+
if (slope > 0.499999 && slope < 0.500001) return x;
|
|
222
|
+
const p = Math.max(CURVE_EPSILON, Math.min(1 - CURVE_EPSILON, slope));
|
|
223
|
+
return p * p / (1 - p * 2) * (Math.pow((1 - p) / p, 2 * x) - 1);
|
|
224
|
+
}
|
|
218
225
|
var TempoMap = class {
|
|
219
226
|
constructor(ppqn = 960, initialBpm = 120) {
|
|
220
227
|
this._ppqn = ppqn;
|
|
221
|
-
this._entries = [{ tick: 0, bpm: initialBpm, secondsAtTick: 0 }];
|
|
228
|
+
this._entries = [{ tick: 0, bpm: initialBpm, interpolation: "step", secondsAtTick: 0 }];
|
|
222
229
|
}
|
|
223
230
|
getTempo(atTick = 0) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
return this._getTempoAt(atTick);
|
|
232
|
+
}
|
|
233
|
+
setTempo(bpm, atTick = 0, options) {
|
|
234
|
+
const interpolation = options?.interpolation ?? "step";
|
|
235
|
+
if (typeof interpolation === "object" && interpolation.type === "curve") {
|
|
236
|
+
const s = interpolation.slope;
|
|
237
|
+
if (!Number.isFinite(s) || s <= 0 || s >= 1) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
"[waveform-playlist] TempoMap: curve slope must be between 0 and 1 (exclusive), got " + s
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
228
243
|
if (atTick === 0) {
|
|
229
|
-
this._entries[0] = { ...this._entries[0], bpm };
|
|
244
|
+
this._entries[0] = { ...this._entries[0], bpm, interpolation: "step" };
|
|
230
245
|
this._recomputeCache(0);
|
|
231
246
|
return;
|
|
232
247
|
}
|
|
233
248
|
let i = this._entries.length - 1;
|
|
234
249
|
while (i > 0 && this._entries[i].tick > atTick) i--;
|
|
235
250
|
if (this._entries[i].tick === atTick) {
|
|
236
|
-
this._entries[i] = { ...this._entries[i], bpm };
|
|
251
|
+
this._entries[i] = { ...this._entries[i], bpm, interpolation };
|
|
237
252
|
} else {
|
|
238
253
|
const secondsAtTick = this._ticksToSecondsInternal(atTick);
|
|
239
|
-
this._entries.splice(i + 1, 0, { tick: atTick, bpm, secondsAtTick });
|
|
254
|
+
this._entries.splice(i + 1, 0, { tick: atTick, bpm, interpolation, secondsAtTick });
|
|
240
255
|
i = i + 1;
|
|
241
256
|
}
|
|
242
257
|
this._recomputeCache(i);
|
|
@@ -257,6 +272,28 @@ var TempoMap = class {
|
|
|
257
272
|
}
|
|
258
273
|
const entry = this._entries[lo];
|
|
259
274
|
const secondsIntoSegment = seconds - entry.secondsAtTick;
|
|
275
|
+
const nextEntry = lo < this._entries.length - 1 ? this._entries[lo + 1] : null;
|
|
276
|
+
if (nextEntry && nextEntry.interpolation === "linear") {
|
|
277
|
+
return Math.round(
|
|
278
|
+
entry.tick + this._secondsToTicksLinear(
|
|
279
|
+
secondsIntoSegment,
|
|
280
|
+
entry.bpm,
|
|
281
|
+
nextEntry.bpm,
|
|
282
|
+
nextEntry.tick - entry.tick
|
|
283
|
+
)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (nextEntry && typeof nextEntry.interpolation === "object") {
|
|
287
|
+
return Math.round(
|
|
288
|
+
entry.tick + this._secondsToTicksCurve(
|
|
289
|
+
secondsIntoSegment,
|
|
290
|
+
entry.bpm,
|
|
291
|
+
nextEntry.bpm,
|
|
292
|
+
nextEntry.tick - entry.tick,
|
|
293
|
+
nextEntry.interpolation.slope
|
|
294
|
+
)
|
|
295
|
+
);
|
|
296
|
+
}
|
|
260
297
|
const ticksPerSecond = entry.bpm / 60 * this._ppqn;
|
|
261
298
|
return Math.round(entry.tick + secondsIntoSegment * ticksPerSecond);
|
|
262
299
|
}
|
|
@@ -268,15 +305,122 @@ var TempoMap = class {
|
|
|
268
305
|
}
|
|
269
306
|
clearTempos() {
|
|
270
307
|
const first = this._entries[0];
|
|
271
|
-
this._entries = [{ tick: 0, bpm: first.bpm, secondsAtTick: 0 }];
|
|
308
|
+
this._entries = [{ tick: 0, bpm: first.bpm, interpolation: "step", secondsAtTick: 0 }];
|
|
309
|
+
}
|
|
310
|
+
/** Get the interpolated BPM at a tick position */
|
|
311
|
+
_getTempoAt(atTick) {
|
|
312
|
+
const entryIndex = this._entryIndexAt(atTick);
|
|
313
|
+
const entry = this._entries[entryIndex];
|
|
314
|
+
const nextEntry = entryIndex < this._entries.length - 1 ? this._entries[entryIndex + 1] : null;
|
|
315
|
+
if (nextEntry && nextEntry.interpolation !== "step") {
|
|
316
|
+
const segmentTicks = nextEntry.tick - entry.tick;
|
|
317
|
+
const ticksInto = atTick - entry.tick;
|
|
318
|
+
if (segmentTicks > 0) {
|
|
319
|
+
const progress = ticksInto / segmentTicks;
|
|
320
|
+
if (nextEntry.interpolation === "linear") {
|
|
321
|
+
return entry.bpm + (nextEntry.bpm - entry.bpm) * progress;
|
|
322
|
+
}
|
|
323
|
+
const t = curveNormalizedAt(progress, nextEntry.interpolation.slope);
|
|
324
|
+
return entry.bpm + (nextEntry.bpm - entry.bpm) * t;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return entry.bpm;
|
|
272
328
|
}
|
|
273
329
|
_ticksToSecondsInternal(ticks) {
|
|
274
|
-
const
|
|
330
|
+
const entryIndex = this._entryIndexAt(ticks);
|
|
331
|
+
const entry = this._entries[entryIndex];
|
|
275
332
|
const ticksIntoSegment = ticks - entry.tick;
|
|
333
|
+
const nextEntry = entryIndex < this._entries.length - 1 ? this._entries[entryIndex + 1] : null;
|
|
334
|
+
if (nextEntry && nextEntry.interpolation === "linear") {
|
|
335
|
+
const segmentTicks = nextEntry.tick - entry.tick;
|
|
336
|
+
return entry.secondsAtTick + this._ticksToSecondsLinear(ticksIntoSegment, entry.bpm, nextEntry.bpm, segmentTicks);
|
|
337
|
+
}
|
|
338
|
+
if (nextEntry && typeof nextEntry.interpolation === "object") {
|
|
339
|
+
const segmentTicks = nextEntry.tick - entry.tick;
|
|
340
|
+
return entry.secondsAtTick + this._ticksToSecondsCurve(
|
|
341
|
+
ticksIntoSegment,
|
|
342
|
+
entry.bpm,
|
|
343
|
+
nextEntry.bpm,
|
|
344
|
+
segmentTicks,
|
|
345
|
+
nextEntry.interpolation.slope
|
|
346
|
+
);
|
|
347
|
+
}
|
|
276
348
|
const secondsPerTick = 60 / (entry.bpm * this._ppqn);
|
|
277
349
|
return entry.secondsAtTick + ticksIntoSegment * secondsPerTick;
|
|
278
350
|
}
|
|
279
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Exact integration for a linear BPM ramp using the logarithmic formula.
|
|
353
|
+
* For bpm(t) = bpm0 + r*t where r = (bpm1-bpm0)/T:
|
|
354
|
+
* seconds = (T * 60) / (ppqn * (bpm1-bpm0)) * ln(bpmAtTick / bpm0)
|
|
355
|
+
*/
|
|
356
|
+
_ticksToSecondsLinear(ticks, bpm0, bpm1, totalSegmentTicks) {
|
|
357
|
+
if (totalSegmentTicks === 0) return 0;
|
|
358
|
+
const bpmAtTick = bpm0 + (bpm1 - bpm0) * (ticks / totalSegmentTicks);
|
|
359
|
+
if (Math.abs(bpm1 - bpm0) < 1e-10) {
|
|
360
|
+
return ticks * 60 / (bpm0 * this._ppqn);
|
|
361
|
+
}
|
|
362
|
+
const deltaBpm = bpm1 - bpm0;
|
|
363
|
+
return totalSegmentTicks * 60 / (this._ppqn * deltaBpm) * Math.log(bpmAtTick / bpm0);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Inverse of _ticksToSecondsLinear: given seconds, return ticks.
|
|
367
|
+
* Closed-form via exponential: bpmAtTick = bpm0 * exp(seconds * deltaBpm * ppqn / (60 * T))
|
|
368
|
+
* then ticks = (bpmAtTick - bpm0) * T / deltaBpm
|
|
369
|
+
*
|
|
370
|
+
* Note: exp(log(x)) has ~1 ULP floating-point error, so round-trips depend on
|
|
371
|
+
* Math.round() in the caller (secondsToTicks). This is sufficient for all tested
|
|
372
|
+
* BPM ranges (10–300 BPM) but is not algebraically exact like the previous
|
|
373
|
+
* trapezoidal/quadratic approach was.
|
|
374
|
+
*/
|
|
375
|
+
_secondsToTicksLinear(seconds, bpm0, bpm1, totalSegmentTicks) {
|
|
376
|
+
if (totalSegmentTicks === 0 || seconds === 0) return 0;
|
|
377
|
+
if (Math.abs(bpm1 - bpm0) < 1e-10) {
|
|
378
|
+
return seconds * bpm0 * this._ppqn / 60;
|
|
379
|
+
}
|
|
380
|
+
const deltaBpm = bpm1 - bpm0;
|
|
381
|
+
const bpmAtTick = bpm0 * Math.exp(seconds * deltaBpm * this._ppqn / (60 * totalSegmentTicks));
|
|
382
|
+
return (bpmAtTick - bpm0) / deltaBpm * totalSegmentTicks;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Subdivided trapezoidal integration for a Möbius-Ease tempo curve.
|
|
386
|
+
* The BPM at progress p is: bpm0 + curveNormalizedAt(p, slope) * (bpm1 - bpm0).
|
|
387
|
+
* We subdivide into CURVE_SUBDIVISIONS intervals and apply trapezoidal rule.
|
|
388
|
+
*/
|
|
389
|
+
_ticksToSecondsCurve(ticks, bpm0, bpm1, totalSegmentTicks, slope) {
|
|
390
|
+
if (totalSegmentTicks === 0 || ticks === 0) return 0;
|
|
391
|
+
const n = CURVE_SUBDIVISIONS;
|
|
392
|
+
const dt = ticks / n;
|
|
393
|
+
let seconds = 0;
|
|
394
|
+
let prevBpm = bpm0;
|
|
395
|
+
for (let i = 1; i <= n; i++) {
|
|
396
|
+
const progress = dt * i / totalSegmentTicks;
|
|
397
|
+
const curBpm = bpm0 + curveNormalizedAt(progress, slope) * (bpm1 - bpm0);
|
|
398
|
+
seconds += dt * 60 / this._ppqn * (1 / prevBpm + 1 / curBpm) / 2;
|
|
399
|
+
prevBpm = curBpm;
|
|
400
|
+
}
|
|
401
|
+
return seconds;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Inverse of _ticksToSecondsCurve: given seconds into a curved segment,
|
|
405
|
+
* return ticks. Uses binary search since there's no closed-form inverse.
|
|
406
|
+
*/
|
|
407
|
+
_secondsToTicksCurve(seconds, bpm0, bpm1, totalSegmentTicks, slope) {
|
|
408
|
+
if (totalSegmentTicks === 0 || seconds === 0) return 0;
|
|
409
|
+
const iterations = Math.min(40, Math.max(1, Math.ceil(Math.log2(2 * totalSegmentTicks))));
|
|
410
|
+
let lo = 0;
|
|
411
|
+
let hi = totalSegmentTicks;
|
|
412
|
+
for (let i = 0; i < iterations; i++) {
|
|
413
|
+
const mid = (lo + hi) / 2;
|
|
414
|
+
const midSeconds = this._ticksToSecondsCurve(mid, bpm0, bpm1, totalSegmentTicks, slope);
|
|
415
|
+
if (midSeconds < seconds) {
|
|
416
|
+
lo = mid;
|
|
417
|
+
} else {
|
|
418
|
+
hi = mid;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return (lo + hi) / 2;
|
|
422
|
+
}
|
|
423
|
+
_entryIndexAt(tick) {
|
|
280
424
|
let lo = 0;
|
|
281
425
|
let hi = this._entries.length - 1;
|
|
282
426
|
while (lo < hi) {
|
|
@@ -287,16 +431,31 @@ var TempoMap = class {
|
|
|
287
431
|
hi = mid - 1;
|
|
288
432
|
}
|
|
289
433
|
}
|
|
290
|
-
return
|
|
434
|
+
return lo;
|
|
291
435
|
}
|
|
292
436
|
_recomputeCache(fromIndex) {
|
|
293
437
|
for (let i = Math.max(1, fromIndex); i < this._entries.length; i++) {
|
|
294
438
|
const prev = this._entries[i - 1];
|
|
295
439
|
const tickDelta = this._entries[i].tick - prev.tick;
|
|
296
|
-
const
|
|
440
|
+
const entry = this._entries[i];
|
|
441
|
+
let segmentSeconds;
|
|
442
|
+
if (entry.interpolation === "linear") {
|
|
443
|
+
segmentSeconds = this._ticksToSecondsLinear(tickDelta, prev.bpm, entry.bpm, tickDelta);
|
|
444
|
+
} else if (typeof entry.interpolation === "object") {
|
|
445
|
+
segmentSeconds = this._ticksToSecondsCurve(
|
|
446
|
+
tickDelta,
|
|
447
|
+
prev.bpm,
|
|
448
|
+
entry.bpm,
|
|
449
|
+
tickDelta,
|
|
450
|
+
entry.interpolation.slope
|
|
451
|
+
);
|
|
452
|
+
} else {
|
|
453
|
+
const secondsPerTick = 60 / (prev.bpm * this._ppqn);
|
|
454
|
+
segmentSeconds = tickDelta * secondsPerTick;
|
|
455
|
+
}
|
|
297
456
|
this._entries[i] = {
|
|
298
|
-
...
|
|
299
|
-
secondsAtTick: prev.secondsAtTick +
|
|
457
|
+
...entry,
|
|
458
|
+
secondsAtTick: prev.secondsAtTick + segmentSeconds
|
|
300
459
|
};
|
|
301
460
|
}
|
|
302
461
|
}
|
|
@@ -863,6 +1022,7 @@ var Transport = class _Transport {
|
|
|
863
1022
|
this._mutedTrackIds = /* @__PURE__ */ new Set();
|
|
864
1023
|
this._playing = false;
|
|
865
1024
|
this._loopEnabled = false;
|
|
1025
|
+
this._loopStartTick = 0;
|
|
866
1026
|
this._loopStartSeconds = 0;
|
|
867
1027
|
this._listeners = /* @__PURE__ */ new Map();
|
|
868
1028
|
this._audioContext = audioContext;
|
|
@@ -1080,6 +1240,7 @@ var Transport = class _Transport {
|
|
|
1080
1240
|
return;
|
|
1081
1241
|
}
|
|
1082
1242
|
this._loopEnabled = enabled;
|
|
1243
|
+
this._loopStartTick = startTick;
|
|
1083
1244
|
this._loopStartSeconds = this._tempoMap.ticksToSeconds(startTick);
|
|
1084
1245
|
this._scheduler.setLoop(enabled, startTick, endTick);
|
|
1085
1246
|
this._clipPlayer.setLoop(enabled, startTick, endTick);
|
|
@@ -1108,14 +1269,18 @@ var Transport = class _Transport {
|
|
|
1108
1269
|
const startTick = this._sampleTimeline.samplesToTicks(startSample);
|
|
1109
1270
|
const endTick = this._sampleTimeline.samplesToTicks(endSample);
|
|
1110
1271
|
this._loopEnabled = enabled;
|
|
1272
|
+
this._loopStartTick = startTick;
|
|
1111
1273
|
this._loopStartSeconds = this._tempoMap.ticksToSeconds(startTick);
|
|
1112
1274
|
this._clipPlayer.setLoopSamples(enabled, startSample, endSample);
|
|
1113
1275
|
this._scheduler.setLoop(enabled, startTick, endTick);
|
|
1114
1276
|
this._emit("loop");
|
|
1115
1277
|
}
|
|
1116
1278
|
// --- Tempo ---
|
|
1117
|
-
setTempo(bpm, atTick) {
|
|
1118
|
-
this._tempoMap.setTempo(bpm, atTick);
|
|
1279
|
+
setTempo(bpm, atTick, options) {
|
|
1280
|
+
this._tempoMap.setTempo(bpm, atTick, options);
|
|
1281
|
+
if (this._loopEnabled) {
|
|
1282
|
+
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1283
|
+
}
|
|
1119
1284
|
this._emit("tempochange");
|
|
1120
1285
|
}
|
|
1121
1286
|
getTempo(atTick) {
|
|
@@ -1139,6 +1304,9 @@ var Transport = class _Transport {
|
|
|
1139
1304
|
}
|
|
1140
1305
|
clearTempos() {
|
|
1141
1306
|
this._tempoMap.clearTempos();
|
|
1307
|
+
if (this._loopEnabled) {
|
|
1308
|
+
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1309
|
+
}
|
|
1142
1310
|
this._emit("tempochange");
|
|
1143
1311
|
}
|
|
1144
1312
|
barToTick(bar) {
|