@dawcore/transport 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +29 -7
- package/dist/index.d.ts +29 -7
- package/dist/index.js +89 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +89 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -8
package/dist/index.mjs
CHANGED
|
@@ -212,7 +212,7 @@ var SampleTimeline = class {
|
|
|
212
212
|
"[waveform-playlist] SampleTimeline: tempoMap not set \u2014 call setTempoMap() first"
|
|
213
213
|
);
|
|
214
214
|
}
|
|
215
|
-
return this._tempoMap.secondsToTicks(samples / this._sampleRate);
|
|
215
|
+
return Math.round(this._tempoMap.secondsToTicks(samples / this._sampleRate));
|
|
216
216
|
}
|
|
217
217
|
};
|
|
218
218
|
|
|
@@ -224,15 +224,32 @@ function curveNormalizedAt(x, slope) {
|
|
|
224
224
|
const p = Math.max(CURVE_EPSILON, Math.min(1 - CURVE_EPSILON, slope));
|
|
225
225
|
return p * p / (1 - p * 2) * (Math.pow((1 - p) / p, 2 * x) - 1);
|
|
226
226
|
}
|
|
227
|
-
var TempoMap = class {
|
|
227
|
+
var TempoMap = class _TempoMap {
|
|
228
228
|
constructor(ppqn = 960, initialBpm = 120) {
|
|
229
|
+
_TempoMap._validateBpm(initialBpm);
|
|
229
230
|
this._ppqn = ppqn;
|
|
230
231
|
this._entries = [{ tick: 0, bpm: initialBpm, interpolation: "step", secondsAtTick: 0 }];
|
|
231
232
|
}
|
|
233
|
+
/** A non-finite or non-positive BPM silently corrupts the secondsAtTick
|
|
234
|
+
* cache (Infinity, or non-monotonic values that break the binary search
|
|
235
|
+
* in secondsToTicks) — reject it at the boundary instead. */
|
|
236
|
+
static _validateBpm(bpm) {
|
|
237
|
+
if (!Number.isFinite(bpm) || bpm <= 0) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
"[waveform-playlist] TempoMap: bpm must be a finite positive number, got " + bpm
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
232
243
|
getTempo(atTick = 0) {
|
|
233
244
|
return this._getTempoAt(atTick);
|
|
234
245
|
}
|
|
246
|
+
/** Number of tempo entries in the map. Always >= 1 — the tick-0 entry is
|
|
247
|
+
* permanent. Used by Transport.setTempo to detect multi-entry maps. */
|
|
248
|
+
get entryCount() {
|
|
249
|
+
return this._entries.length;
|
|
250
|
+
}
|
|
235
251
|
setTempo(bpm, atTick = 0, options) {
|
|
252
|
+
_TempoMap._validateBpm(bpm);
|
|
236
253
|
const interpolation = options?.interpolation ?? "step";
|
|
237
254
|
if (typeof interpolation === "object" && interpolation.type === "curve") {
|
|
238
255
|
const s = interpolation.slope;
|
|
@@ -309,6 +326,18 @@ var TempoMap = class {
|
|
|
309
326
|
const first = this._entries[0];
|
|
310
327
|
this._entries = [{ tick: 0, bpm: first.bpm, interpolation: "step", secondsAtTick: 0 }];
|
|
311
328
|
}
|
|
329
|
+
/** Remove the tempo entry at exactly `atTick`, if one exists. The tick-0
|
|
330
|
+
* entry is permanent (a map must always have a tempo) — removing it is a
|
|
331
|
+
* no-op, matching removeMeter's treatment of the initial meter. The
|
|
332
|
+
* seconds cache is recomputed from the removal point, the same partial
|
|
333
|
+
* update setTempo uses. */
|
|
334
|
+
removeTempo(atTick) {
|
|
335
|
+
if (atTick === 0) return;
|
|
336
|
+
const i = this._entries.findIndex((e) => e.tick === atTick);
|
|
337
|
+
if (i === -1) return;
|
|
338
|
+
this._entries = [...this._entries.slice(0, i), ...this._entries.slice(i + 1)];
|
|
339
|
+
this._recomputeCache(i);
|
|
340
|
+
}
|
|
312
341
|
/** Get the interpolated BPM at a tick position */
|
|
313
342
|
_getTempoAt(atTick) {
|
|
314
343
|
const entryIndex = this._entryIndexAt(atTick);
|
|
@@ -470,6 +499,7 @@ function isPowerOf2(n) {
|
|
|
470
499
|
var MeterMap = class {
|
|
471
500
|
constructor(ppqn, numerator = 4, denominator = 4) {
|
|
472
501
|
this._ppqn = ppqn;
|
|
502
|
+
this._validateMeter(numerator, denominator);
|
|
473
503
|
this._entries = [{ tick: 0, numerator, denominator, barAtTick: 0 }];
|
|
474
504
|
}
|
|
475
505
|
get ppqn() {
|
|
@@ -633,6 +663,11 @@ var MeterMap = class {
|
|
|
633
663
|
"[waveform-playlist] MeterMap: denominator must be a power of 2 (1-32), got " + denominator
|
|
634
664
|
);
|
|
635
665
|
}
|
|
666
|
+
if (!Number.isInteger(this._ppqn * 4 / denominator)) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
"[waveform-playlist] MeterMap: ppqn (" + this._ppqn + ") * 4 is not divisible by denominator (" + denominator + ") \u2014 bar boundaries would fall on fractional ticks"
|
|
669
|
+
);
|
|
670
|
+
}
|
|
636
671
|
}
|
|
637
672
|
};
|
|
638
673
|
|
|
@@ -763,11 +798,19 @@ var ClipPlayer = class {
|
|
|
763
798
|
const clipTick = clip.startTick !== void 0 ? clip.startTick : this._sampleTimeline.samplesToTicks(clip.startSample);
|
|
764
799
|
if (clipTick < fromTick) continue;
|
|
765
800
|
if (clipTick >= toTick) continue;
|
|
766
|
-
const
|
|
767
|
-
const
|
|
801
|
+
const timelineRate = this._sampleTimeline.sampleRate;
|
|
802
|
+
const bufferRate = clip.audioBuffer.sampleRate;
|
|
803
|
+
const fadeInDurationSamples = clip.fadeIn ? Math.round((clip.fadeIn.duration ?? 0) * timelineRate) : 0;
|
|
804
|
+
const fadeOutDurationSamples = clip.fadeOut ? Math.round((clip.fadeOut.duration ?? 0) * timelineRate) : 0;
|
|
768
805
|
let durationSamples = clip.durationSamples;
|
|
769
|
-
if (this._loopEnabled
|
|
770
|
-
|
|
806
|
+
if (this._loopEnabled) {
|
|
807
|
+
const durationTimelineSamples = Math.round(
|
|
808
|
+
clip.durationSamples / bufferRate * timelineRate
|
|
809
|
+
);
|
|
810
|
+
const allowedTimelineSamples = this._loopEndSamples - clip.startSample;
|
|
811
|
+
if (durationTimelineSamples > allowedTimelineSamples) {
|
|
812
|
+
durationSamples = Math.round(allowedTimelineSamples / timelineRate * bufferRate);
|
|
813
|
+
}
|
|
771
814
|
}
|
|
772
815
|
events.push({
|
|
773
816
|
trackId,
|
|
@@ -793,9 +836,9 @@ var ClipPlayer = class {
|
|
|
793
836
|
);
|
|
794
837
|
return;
|
|
795
838
|
}
|
|
796
|
-
const
|
|
797
|
-
const offsetSeconds = event.offsetSamples /
|
|
798
|
-
const durationSeconds = event.durationSamples /
|
|
839
|
+
const bufferRate = event.audioBuffer.sampleRate;
|
|
840
|
+
const offsetSeconds = event.offsetSamples / bufferRate;
|
|
841
|
+
const durationSeconds = event.durationSamples / bufferRate;
|
|
799
842
|
if (offsetSeconds >= event.audioBuffer.duration) {
|
|
800
843
|
console.warn(
|
|
801
844
|
"[waveform-playlist] ClipPlayer.consume: offset (" + offsetSeconds + "s) exceeds audioBuffer.duration (" + event.audioBuffer.duration + 's) for clipId "' + event.clipId + '" \u2014 clip will not play'
|
|
@@ -808,8 +851,9 @@ var ClipPlayer = class {
|
|
|
808
851
|
const when = this._toAudioTime(transportSeconds);
|
|
809
852
|
const gainNode = this._audioContext.createGain();
|
|
810
853
|
gainNode.gain.value = event.gain;
|
|
811
|
-
|
|
812
|
-
let
|
|
854
|
+
const timelineRate = this._sampleTimeline.sampleRate;
|
|
855
|
+
let fadeIn = event.fadeInDurationSamples / timelineRate;
|
|
856
|
+
let fadeOut = event.fadeOutDurationSamples / timelineRate;
|
|
813
857
|
if (fadeIn + fadeOut > durationSeconds) {
|
|
814
858
|
const ratio = durationSeconds / (fadeIn + fadeOut);
|
|
815
859
|
fadeIn *= ratio;
|
|
@@ -849,16 +893,21 @@ var ClipPlayer = class {
|
|
|
849
893
|
if (!clip.audioBuffer) continue;
|
|
850
894
|
const clipTick = clip.startTick !== void 0 ? clip.startTick : this._sampleTimeline.samplesToTicks(clip.startSample);
|
|
851
895
|
if (clipTick >= newTick) continue;
|
|
852
|
-
const
|
|
896
|
+
const timelineRate = this._sampleTimeline.sampleRate;
|
|
897
|
+
const bufferRate = clip.audioBuffer.sampleRate;
|
|
898
|
+
const bufToTimeline = (n) => Math.round(n / bufferRate * timelineRate);
|
|
899
|
+
const timelineToBuf = (n) => Math.round(n / timelineRate * bufferRate);
|
|
900
|
+
const clipEndSample = clip.startSample + bufToTimeline(clip.durationSamples);
|
|
853
901
|
if (clipEndSample <= newSample) continue;
|
|
854
902
|
const offsetIntoClipSamples = newSample - clip.startSample;
|
|
855
|
-
const offsetSamples = clip.offsetSamples + offsetIntoClipSamples;
|
|
856
|
-
let
|
|
857
|
-
if (this._loopEnabled && newSample +
|
|
858
|
-
|
|
903
|
+
const offsetSamples = clip.offsetSamples + timelineToBuf(offsetIntoClipSamples);
|
|
904
|
+
let remainingTimelineSamples = clipEndSample - newSample;
|
|
905
|
+
if (this._loopEnabled && newSample + remainingTimelineSamples > this._loopEndSamples) {
|
|
906
|
+
remainingTimelineSamples = this._loopEndSamples - newSample;
|
|
859
907
|
}
|
|
860
|
-
if (
|
|
861
|
-
const
|
|
908
|
+
if (remainingTimelineSamples <= 0) continue;
|
|
909
|
+
const durationSamples = timelineToBuf(remainingTimelineSamples);
|
|
910
|
+
const fadeOutDurationSamples = clip.fadeOut ? Math.round((clip.fadeOut.duration ?? 0) * timelineRate) : 0;
|
|
862
911
|
this.consume({
|
|
863
912
|
trackId,
|
|
864
913
|
clipId: clip.id,
|
|
@@ -1462,12 +1511,22 @@ var _Transport = class _Transport {
|
|
|
1462
1511
|
this._emit("loop");
|
|
1463
1512
|
}
|
|
1464
1513
|
// --- Tempo ---
|
|
1514
|
+
/** Returns true when the tempo was applied, false when a defaulted (no
|
|
1515
|
+
* atTick) write was refused because the tempo map has multiple entries —
|
|
1516
|
+
* pass an explicit atTick to modify a multi-entry map (#407). */
|
|
1465
1517
|
setTempo(bpm, atTick, options) {
|
|
1518
|
+
if (atTick === void 0 && this._tempoMap.entryCount > 1) {
|
|
1519
|
+
console.warn(
|
|
1520
|
+
"[waveform-playlist] Transport.setTempo: refusing defaulted tick-0 write of " + bpm + " BPM \u2014 the tempo map has " + this._tempoMap.entryCount + " entries. Pass an explicit atTick to modify a multi-entry tempo map."
|
|
1521
|
+
);
|
|
1522
|
+
return false;
|
|
1523
|
+
}
|
|
1466
1524
|
this._tempoMap.setTempo(bpm, atTick, options);
|
|
1467
1525
|
if (this._loopEnabled) {
|
|
1468
1526
|
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1469
1527
|
}
|
|
1470
1528
|
this._emit("tempochange", { bpm, atTick: atTick ?? 0 });
|
|
1529
|
+
return true;
|
|
1471
1530
|
}
|
|
1472
1531
|
getTempo(atTick) {
|
|
1473
1532
|
return this._tempoMap.getTempo(atTick);
|
|
@@ -1505,6 +1564,17 @@ var _Transport = class _Transport {
|
|
|
1505
1564
|
}
|
|
1506
1565
|
this._emit("tempochange", { bpm: this._tempoMap.getTempo(), atTick: 0 });
|
|
1507
1566
|
}
|
|
1567
|
+
/** Remove the tempo entry at exactly `atTick` (tick 0 is permanent — a
|
|
1568
|
+
* no-op, like removeMeter's initial entry). Mirrors setTempo's loop-cache
|
|
1569
|
+
* invalidation and event; the emitted bpm is the tempo now in force at
|
|
1570
|
+
* the removed position. */
|
|
1571
|
+
removeTempo(atTick) {
|
|
1572
|
+
this._tempoMap.removeTempo(atTick);
|
|
1573
|
+
if (this._loopEnabled) {
|
|
1574
|
+
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1575
|
+
}
|
|
1576
|
+
this._emit("tempochange", { bpm: this._tempoMap.getTempo(atTick), atTick });
|
|
1577
|
+
}
|
|
1508
1578
|
barToTick(bar) {
|
|
1509
1579
|
return this._meterMap.barToTick(bar);
|
|
1510
1580
|
}
|
|
@@ -1877,7 +1947,7 @@ var NativePlayoutAdapter = class {
|
|
|
1877
1947
|
return this._transport.isCountingIn();
|
|
1878
1948
|
}
|
|
1879
1949
|
setTempo(bpm, atTick) {
|
|
1880
|
-
this._transport.setTempo(bpm, atTick !== void 0 ? atTick : void 0);
|
|
1950
|
+
return this._transport.setTempo(bpm, atTick !== void 0 ? atTick : void 0);
|
|
1881
1951
|
}
|
|
1882
1952
|
setMeter(numerator, denominator, atTick) {
|
|
1883
1953
|
this._transport.setMeter(
|