@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.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 fadeInDurationSamples = clip.fadeIn ? clip.fadeIn.duration ?? 0 : 0;
767
- const fadeOutDurationSamples = clip.fadeOut ? clip.fadeOut.duration ?? 0 : 0;
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 && clip.startSample + durationSamples > this._loopEndSamples) {
770
- durationSamples = this._loopEndSamples - clip.startSample;
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 sampleRate = this._sampleTimeline.sampleRate;
797
- const offsetSeconds = event.offsetSamples / sampleRate;
798
- const durationSeconds = event.durationSamples / sampleRate;
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
- let fadeIn = event.fadeInDurationSamples / sampleRate;
812
- let fadeOut = event.fadeOutDurationSamples / sampleRate;
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 clipEndSample = clip.startSample + clip.durationSamples;
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 durationSamples = clipEndSample - newSample;
857
- if (this._loopEnabled && newSample + durationSamples > this._loopEndSamples) {
858
- durationSamples = this._loopEndSamples - newSample;
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 (durationSamples <= 0) continue;
861
- const fadeOutDurationSamples = clip.fadeOut ? clip.fadeOut.duration ?? 0 : 0;
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(