@dawcore/transport 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,12 +23,12 @@ __export(index_exports, {
23
23
  ClipPlayer: () => ClipPlayer,
24
24
  Clock: () => Clock,
25
25
  MasterNode: () => MasterNode,
26
+ MeterMap: () => MeterMap,
26
27
  MetronomePlayer: () => MetronomePlayer,
27
28
  NativePlayoutAdapter: () => NativePlayoutAdapter,
28
29
  SampleTimeline: () => SampleTimeline,
29
30
  Scheduler: () => Scheduler,
30
31
  TempoMap: () => TempoMap,
31
- TickTimeline: () => TickTimeline,
32
32
  Timer: () => Timer,
33
33
  TrackNode: () => TrackNode,
34
34
  Transport: () => Transport
@@ -209,34 +209,6 @@ var SampleTimeline = class {
209
209
  }
210
210
  };
211
211
 
212
- // src/timeline/tick-timeline.ts
213
- var TickTimeline = class {
214
- constructor(ppqn = 960) {
215
- this._ppqn = ppqn;
216
- }
217
- get ppqn() {
218
- return this._ppqn;
219
- }
220
- ticksPerBeat() {
221
- return this._ppqn;
222
- }
223
- ticksPerBar(beatsPerBar) {
224
- return this._ppqn * beatsPerBar;
225
- }
226
- toPosition(ticks, beatsPerBar) {
227
- const ticksPerBar = this.ticksPerBar(beatsPerBar);
228
- const bar = Math.floor(ticks / ticksPerBar) + 1;
229
- const remaining = ticks % ticksPerBar;
230
- const beat = Math.floor(remaining / this._ppqn) + 1;
231
- const tick = remaining % this._ppqn;
232
- return { bar, beat, tick };
233
- }
234
- fromPosition(bar, beat, tick, beatsPerBar) {
235
- const ticksPerBar = this.ticksPerBar(beatsPerBar);
236
- return (bar - 1) * ticksPerBar + (beat - 1) * this._ppqn + tick;
237
- }
238
- };
239
-
240
212
  // src/timeline/tempo-map.ts
241
213
  var TempoMap = class {
242
214
  constructor(ppqn = 960, initialBpm = 120) {
@@ -289,6 +261,10 @@ var TempoMap = class {
289
261
  secondsToBeats(seconds) {
290
262
  return this.secondsToTicks(seconds) / this._ppqn;
291
263
  }
264
+ clearTempos() {
265
+ const first = this._entries[0];
266
+ this._entries = [{ tick: 0, bpm: first.bpm, secondsAtTick: 0 }];
267
+ }
292
268
  _ticksToSecondsInternal(ticks) {
293
269
  const entry = this._entryAt(ticks);
294
270
  const ticksIntoSegment = ticks - entry.tick;
@@ -321,6 +297,179 @@ var TempoMap = class {
321
297
  }
322
298
  };
323
299
 
300
+ // src/timeline/meter-map.ts
301
+ function isPowerOf2(n) {
302
+ return n > 0 && (n & n - 1) === 0;
303
+ }
304
+ var MeterMap = class {
305
+ constructor(ppqn, numerator = 4, denominator = 4) {
306
+ this._ppqn = ppqn;
307
+ this._entries = [{ tick: 0, numerator, denominator, barAtTick: 0 }];
308
+ }
309
+ get ppqn() {
310
+ return this._ppqn;
311
+ }
312
+ getMeter(atTick = 0) {
313
+ const entry = this._entryAt(atTick);
314
+ return { numerator: entry.numerator, denominator: entry.denominator };
315
+ }
316
+ setMeter(numerator, denominator, atTick = 0) {
317
+ this._validateMeter(numerator, denominator);
318
+ if (atTick < 0) {
319
+ throw new Error("[waveform-playlist] MeterMap: atTick must be non-negative, got " + atTick);
320
+ }
321
+ if (atTick === 0) {
322
+ this._entries[0] = { ...this._entries[0], numerator, denominator };
323
+ this._resnapDownstreamEntries(0);
324
+ this._recomputeCache(0);
325
+ return;
326
+ }
327
+ const snapped = this._snapToBarBoundary(atTick);
328
+ if (snapped !== atTick) {
329
+ console.warn(
330
+ "[waveform-playlist] MeterMap.setMeter: tick " + atTick + " is not on a bar boundary, snapped to " + snapped
331
+ );
332
+ }
333
+ let i = this._entries.length - 1;
334
+ while (i > 0 && this._entries[i].tick > snapped) i--;
335
+ if (this._entries[i].tick === snapped) {
336
+ this._entries[i] = { ...this._entries[i], numerator, denominator };
337
+ } else {
338
+ const barAtTick = this._computeBarAtTick(snapped);
339
+ this._entries.splice(i + 1, 0, { tick: snapped, numerator, denominator, barAtTick });
340
+ i = i + 1;
341
+ }
342
+ this._resnapDownstreamEntries(i);
343
+ this._recomputeCache(i);
344
+ }
345
+ removeMeter(atTick) {
346
+ if (atTick === 0) {
347
+ throw new Error("[waveform-playlist] MeterMap: cannot remove meter at tick 0");
348
+ }
349
+ const idx = this._entries.findIndex((e) => e.tick === atTick);
350
+ if (idx > 0) {
351
+ this._entries.splice(idx, 1);
352
+ this._recomputeCache(idx);
353
+ } else if (idx === -1) {
354
+ console.warn("[waveform-playlist] MeterMap.removeMeter: no entry at tick " + atTick);
355
+ }
356
+ }
357
+ clearMeters() {
358
+ const first = this._entries[0];
359
+ this._entries = [{ ...first, barAtTick: 0 }];
360
+ }
361
+ ticksPerBeat(atTick = 0) {
362
+ const entry = this._entryAt(atTick);
363
+ return this._ppqn * (4 / entry.denominator);
364
+ }
365
+ ticksPerBar(atTick = 0) {
366
+ const entry = this._entryAt(atTick);
367
+ return entry.numerator * this._ppqn * (4 / entry.denominator);
368
+ }
369
+ barToTick(bar) {
370
+ if (bar < 1) {
371
+ throw new Error("[waveform-playlist] MeterMap: bar must be >= 1, got " + bar);
372
+ }
373
+ const targetBar = bar - 1;
374
+ for (let i = 0; i < this._entries.length; i++) {
375
+ const nextBar = i < this._entries.length - 1 ? this._entries[i + 1].barAtTick : Infinity;
376
+ if (targetBar < nextBar) {
377
+ const barsInto = targetBar - this._entries[i].barAtTick;
378
+ const tpb = this._ticksPerBarForEntry(this._entries[i]);
379
+ return this._entries[i].tick + barsInto * tpb;
380
+ }
381
+ }
382
+ return 0;
383
+ }
384
+ tickToBar(tick) {
385
+ const entry = this._entryAt(tick);
386
+ const ticksInto = tick - entry.tick;
387
+ const tpb = this._ticksPerBarForEntry(entry);
388
+ return entry.barAtTick + Math.floor(ticksInto / tpb) + 1;
389
+ }
390
+ isBarBoundary(tick) {
391
+ const entry = this._entryAt(tick);
392
+ const ticksInto = tick - entry.tick;
393
+ const tpb = this._ticksPerBarForEntry(entry);
394
+ return ticksInto % tpb === 0;
395
+ }
396
+ /** Internal: get the full entry at a tick (for MetronomePlayer beat grid anchoring) */
397
+ getEntryAt(tick) {
398
+ return this._entryAt(tick);
399
+ }
400
+ _entryAt(tick) {
401
+ let lo = 0;
402
+ let hi = this._entries.length - 1;
403
+ while (lo < hi) {
404
+ const mid = lo + hi + 1 >> 1;
405
+ if (this._entries[mid].tick <= tick) {
406
+ lo = mid;
407
+ } else {
408
+ hi = mid - 1;
409
+ }
410
+ }
411
+ return this._entries[lo];
412
+ }
413
+ _ticksPerBarForEntry(entry) {
414
+ return entry.numerator * this._ppqn * (4 / entry.denominator);
415
+ }
416
+ _snapToBarBoundary(atTick) {
417
+ const entry = this._entryAt(atTick);
418
+ const tpb = this._ticksPerBarForEntry(entry);
419
+ const ticksInto = atTick - entry.tick;
420
+ if (ticksInto % tpb === 0) return atTick;
421
+ return entry.tick + Math.ceil(ticksInto / tpb) * tpb;
422
+ }
423
+ _computeBarAtTick(tick) {
424
+ const entry = this._entryAt(tick);
425
+ const ticksInto = tick - entry.tick;
426
+ const tpb = this._ticksPerBarForEntry(entry);
427
+ return entry.barAtTick + ticksInto / tpb;
428
+ }
429
+ _recomputeCache(fromIndex) {
430
+ for (let i = Math.max(1, fromIndex); i < this._entries.length; i++) {
431
+ const prev = this._entries[i - 1];
432
+ const tickDelta = this._entries[i].tick - prev.tick;
433
+ const tpb = this._ticksPerBarForEntry(prev);
434
+ this._entries[i] = {
435
+ ...this._entries[i],
436
+ barAtTick: prev.barAtTick + tickDelta / tpb
437
+ };
438
+ }
439
+ }
440
+ /**
441
+ * After changing a meter entry, re-snap downstream entries to bar boundaries
442
+ * of their preceding meter so barAtTick stays integer.
443
+ */
444
+ _resnapDownstreamEntries(fromIndex) {
445
+ for (let i = Math.max(1, fromIndex + 1); i < this._entries.length; i++) {
446
+ const prev = this._entries[i - 1];
447
+ const tpb = this._ticksPerBarForEntry(prev);
448
+ const tick = this._entries[i].tick;
449
+ const ticksIntoPrev = tick - prev.tick;
450
+ if (ticksIntoPrev % tpb !== 0) {
451
+ const snapped = prev.tick + Math.ceil(ticksIntoPrev / tpb) * tpb;
452
+ console.warn(
453
+ "[waveform-playlist] MeterMap: meter change moved entry from tick " + tick + " to " + snapped + " (bar boundary alignment)"
454
+ );
455
+ this._entries[i] = { ...this._entries[i], tick: snapped };
456
+ }
457
+ }
458
+ }
459
+ _validateMeter(numerator, denominator) {
460
+ if (!Number.isInteger(numerator) || numerator < 1 || numerator > 32) {
461
+ throw new Error(
462
+ "[waveform-playlist] MeterMap: numerator must be an integer 1-32, got " + numerator
463
+ );
464
+ }
465
+ if (!isPowerOf2(denominator) || denominator > 32) {
466
+ throw new Error(
467
+ "[waveform-playlist] MeterMap: denominator must be a power of 2 (1-32), got " + denominator
468
+ );
469
+ }
470
+ }
471
+ };
472
+
324
473
  // src/audio/master-node.ts
325
474
  var MasterNode = class {
326
475
  constructor(audioContext) {
@@ -587,15 +736,14 @@ var ClipPlayer = class {
587
736
 
588
737
  // src/audio/metronome-player.ts
589
738
  var MetronomePlayer = class {
590
- constructor(audioContext, tempoMap, tickTimeline, destination, toAudioTime) {
739
+ constructor(audioContext, tempoMap, meterMap, destination, toAudioTime) {
591
740
  this._enabled = false;
592
- this._beatsPerBar = 4;
593
741
  this._accentBuffer = null;
594
742
  this._normalBuffer = null;
595
743
  this._activeSources = /* @__PURE__ */ new Set();
596
744
  this._audioContext = audioContext;
597
745
  this._tempoMap = tempoMap;
598
- this._tickTimeline = tickTimeline;
746
+ this._meterMap = meterMap;
599
747
  this._destination = destination;
600
748
  this._toAudioTime = toAudioTime;
601
749
  }
@@ -605,9 +753,6 @@ var MetronomePlayer = class {
605
753
  this.silence();
606
754
  }
607
755
  }
608
- setBeatsPerBar(beats) {
609
- this._beatsPerBar = beats;
610
- }
611
756
  setClickSounds(accent, normal) {
612
757
  this._accentBuffer = accent;
613
758
  this._normalBuffer = normal;
@@ -617,19 +762,27 @@ var MetronomePlayer = class {
617
762
  return [];
618
763
  }
619
764
  const events = [];
620
- const ppqn = this._tickTimeline.ppqn;
621
765
  const fromTicks = this._tempoMap.secondsToTicks(fromTime);
622
766
  const toTicks = this._tempoMap.secondsToTicks(toTime);
623
- const firstBeatTick = Math.ceil(fromTicks / ppqn) * ppqn;
624
- for (let tick = firstBeatTick; tick < toTicks; tick += ppqn) {
767
+ let entry = this._meterMap.getEntryAt(fromTicks);
768
+ let beatSize = this._meterMap.ticksPerBeat(fromTicks);
769
+ const tickIntoSection = fromTicks - entry.tick;
770
+ let tick = entry.tick + Math.ceil(tickIntoSection / beatSize) * beatSize;
771
+ while (tick < toTicks) {
772
+ const currentEntry = this._meterMap.getEntryAt(tick);
773
+ if (currentEntry.tick !== entry.tick) {
774
+ entry = currentEntry;
775
+ beatSize = this._meterMap.ticksPerBeat(tick);
776
+ }
777
+ const isAccent = this._meterMap.isBarBoundary(tick);
625
778
  const transportTime = this._tempoMap.ticksToSeconds(tick);
626
- const ticksPerBar = this._tickTimeline.ticksPerBar(this._beatsPerBar);
627
- const isAccent = tick % ticksPerBar === 0;
628
779
  events.push({
629
780
  transportTime,
630
781
  isAccent,
631
782
  buffer: isAccent ? this._accentBuffer : this._normalBuffer
632
783
  });
784
+ beatSize = this._meterMap.ticksPerBeat(tick);
785
+ tick += beatSize;
633
786
  }
634
787
  return events;
635
788
  }
@@ -690,9 +843,10 @@ var Transport = class _Transport {
690
843
  const sampleRate = options.sampleRate ?? audioContext.sampleRate;
691
844
  const ppqn = options.ppqn ?? 960;
692
845
  const tempo = options.tempo ?? 120;
693
- const beatsPerBar = options.beatsPerBar ?? 4;
846
+ const numerator = options.numerator ?? 4;
847
+ const denominator = options.denominator ?? 4;
694
848
  const lookahead = options.schedulerLookahead ?? 0.2;
695
- _Transport._validateOptions(sampleRate, ppqn, tempo, beatsPerBar, lookahead);
849
+ _Transport._validateOptions(sampleRate, ppqn, tempo, numerator, denominator, lookahead);
696
850
  this._clock = new Clock(audioContext);
697
851
  this._scheduler = new Scheduler({
698
852
  lookahead,
@@ -701,9 +855,9 @@ var Transport = class _Transport {
701
855
  }
702
856
  });
703
857
  this._sampleTimeline = new SampleTimeline(sampleRate);
704
- this._tickTimeline = new TickTimeline(ppqn);
858
+ this._meterMap = new MeterMap(ppqn, numerator, denominator);
705
859
  this._tempoMap = new TempoMap(ppqn, tempo);
706
- this._initAudioGraph(audioContext, beatsPerBar);
860
+ this._initAudioGraph(audioContext);
707
861
  this._timer = new Timer(() => {
708
862
  const time = this._clock.getTime();
709
863
  if (this._endTime !== void 0 && time >= this._endTime) {
@@ -895,15 +1049,46 @@ var Transport = class _Transport {
895
1049
  this._emit("loop");
896
1050
  }
897
1051
  // --- Tempo ---
898
- setTempo(bpm) {
899
- this._tempoMap.setTempo(bpm);
1052
+ setTempo(bpm, atTick) {
1053
+ this._tempoMap.setTempo(bpm, atTick);
900
1054
  this._emit("tempochange");
901
1055
  }
902
- getTempo() {
903
- return this._tempoMap.getTempo();
1056
+ getTempo(atTick) {
1057
+ return this._tempoMap.getTempo(atTick);
1058
+ }
1059
+ // --- Meter ---
1060
+ setMeter(numerator, denominator, atTick) {
1061
+ this._meterMap.setMeter(numerator, denominator, atTick);
1062
+ this._emit("meterchange");
1063
+ }
1064
+ getMeter(atTick) {
1065
+ return this._meterMap.getMeter(atTick);
1066
+ }
1067
+ removeMeter(atTick) {
1068
+ this._meterMap.removeMeter(atTick);
1069
+ this._emit("meterchange");
1070
+ }
1071
+ clearMeters() {
1072
+ this._meterMap.clearMeters();
1073
+ this._emit("meterchange");
1074
+ }
1075
+ clearTempos() {
1076
+ this._tempoMap.clearTempos();
1077
+ this._emit("tempochange");
904
1078
  }
905
- setBeatsPerBar(beats) {
906
- this._metronomePlayer.setBeatsPerBar(beats);
1079
+ barToTick(bar) {
1080
+ return this._meterMap.barToTick(bar);
1081
+ }
1082
+ tickToBar(tick) {
1083
+ return this._meterMap.tickToBar(tick);
1084
+ }
1085
+ /** Convert transport time (seconds) to tick position, using the tempo map. */
1086
+ timeToTick(seconds) {
1087
+ return this._tempoMap.secondsToTicks(seconds);
1088
+ }
1089
+ /** Convert tick position to transport time (seconds), using the tempo map. */
1090
+ tickToTime(tick) {
1091
+ return this._tempoMap.ticksToSeconds(tick);
907
1092
  }
908
1093
  // --- Metronome ---
909
1094
  setMetronomeEnabled(enabled) {
@@ -950,7 +1135,7 @@ var Transport = class _Transport {
950
1135
  this._listeners.clear();
951
1136
  }
952
1137
  // --- Private ---
953
- static _validateOptions(sampleRate, ppqn, tempo, beatsPerBar, lookahead) {
1138
+ static _validateOptions(sampleRate, ppqn, tempo, numerator, denominator, lookahead) {
954
1139
  if (sampleRate <= 0) {
955
1140
  throw new Error(
956
1141
  "[waveform-playlist] Transport: sampleRate must be positive, got " + sampleRate
@@ -964,9 +1149,14 @@ var Transport = class _Transport {
964
1149
  if (tempo <= 0) {
965
1150
  throw new Error("[waveform-playlist] Transport: tempo must be positive, got " + tempo);
966
1151
  }
967
- if (beatsPerBar <= 0 || !Number.isInteger(beatsPerBar)) {
1152
+ if (!Number.isInteger(numerator) || numerator < 1 || numerator > 32) {
1153
+ throw new Error(
1154
+ "[waveform-playlist] Transport: numerator must be an integer 1-32, got " + numerator
1155
+ );
1156
+ }
1157
+ if (denominator <= 0 || (denominator & denominator - 1) !== 0 || denominator > 32) {
968
1158
  throw new Error(
969
- "[waveform-playlist] Transport: beatsPerBar must be a positive integer, got " + beatsPerBar
1159
+ "[waveform-playlist] Transport: denominator must be a power of 2 (1-32), got " + denominator
970
1160
  );
971
1161
  }
972
1162
  if (lookahead <= 0) {
@@ -975,7 +1165,7 @@ var Transport = class _Transport {
975
1165
  );
976
1166
  }
977
1167
  }
978
- _initAudioGraph(audioContext, beatsPerBar) {
1168
+ _initAudioGraph(audioContext) {
979
1169
  this._masterNode = new MasterNode(audioContext);
980
1170
  this._masterNode.output.connect(audioContext.destination);
981
1171
  const toAudioTime = (transportTime) => this._clock.toAudioTime(transportTime);
@@ -983,11 +1173,10 @@ var Transport = class _Transport {
983
1173
  this._metronomePlayer = new MetronomePlayer(
984
1174
  audioContext,
985
1175
  this._tempoMap,
986
- this._tickTimeline,
1176
+ this._meterMap,
987
1177
  this._masterNode.input,
988
1178
  toAudioTime
989
1179
  );
990
- this._metronomePlayer.setBeatsPerBar(beatsPerBar);
991
1180
  this._scheduler.addListener(this._clipPlayer);
992
1181
  this._scheduler.addListener(this._metronomePlayer);
993
1182
  }
@@ -1094,12 +1283,12 @@ var NativePlayoutAdapter = class {
1094
1283
  ClipPlayer,
1095
1284
  Clock,
1096
1285
  MasterNode,
1286
+ MeterMap,
1097
1287
  MetronomePlayer,
1098
1288
  NativePlayoutAdapter,
1099
1289
  SampleTimeline,
1100
1290
  Scheduler,
1101
1291
  TempoMap,
1102
- TickTimeline,
1103
1292
  Timer,
1104
1293
  TrackNode,
1105
1294
  Transport