@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.mjs CHANGED
@@ -172,34 +172,6 @@ var SampleTimeline = class {
172
172
  }
173
173
  };
174
174
 
175
- // src/timeline/tick-timeline.ts
176
- var TickTimeline = class {
177
- constructor(ppqn = 960) {
178
- this._ppqn = ppqn;
179
- }
180
- get ppqn() {
181
- return this._ppqn;
182
- }
183
- ticksPerBeat() {
184
- return this._ppqn;
185
- }
186
- ticksPerBar(beatsPerBar) {
187
- return this._ppqn * beatsPerBar;
188
- }
189
- toPosition(ticks, beatsPerBar) {
190
- const ticksPerBar = this.ticksPerBar(beatsPerBar);
191
- const bar = Math.floor(ticks / ticksPerBar) + 1;
192
- const remaining = ticks % ticksPerBar;
193
- const beat = Math.floor(remaining / this._ppqn) + 1;
194
- const tick = remaining % this._ppqn;
195
- return { bar, beat, tick };
196
- }
197
- fromPosition(bar, beat, tick, beatsPerBar) {
198
- const ticksPerBar = this.ticksPerBar(beatsPerBar);
199
- return (bar - 1) * ticksPerBar + (beat - 1) * this._ppqn + tick;
200
- }
201
- };
202
-
203
175
  // src/timeline/tempo-map.ts
204
176
  var TempoMap = class {
205
177
  constructor(ppqn = 960, initialBpm = 120) {
@@ -252,6 +224,10 @@ var TempoMap = class {
252
224
  secondsToBeats(seconds) {
253
225
  return this.secondsToTicks(seconds) / this._ppqn;
254
226
  }
227
+ clearTempos() {
228
+ const first = this._entries[0];
229
+ this._entries = [{ tick: 0, bpm: first.bpm, secondsAtTick: 0 }];
230
+ }
255
231
  _ticksToSecondsInternal(ticks) {
256
232
  const entry = this._entryAt(ticks);
257
233
  const ticksIntoSegment = ticks - entry.tick;
@@ -284,6 +260,179 @@ var TempoMap = class {
284
260
  }
285
261
  };
286
262
 
263
+ // src/timeline/meter-map.ts
264
+ function isPowerOf2(n) {
265
+ return n > 0 && (n & n - 1) === 0;
266
+ }
267
+ var MeterMap = class {
268
+ constructor(ppqn, numerator = 4, denominator = 4) {
269
+ this._ppqn = ppqn;
270
+ this._entries = [{ tick: 0, numerator, denominator, barAtTick: 0 }];
271
+ }
272
+ get ppqn() {
273
+ return this._ppqn;
274
+ }
275
+ getMeter(atTick = 0) {
276
+ const entry = this._entryAt(atTick);
277
+ return { numerator: entry.numerator, denominator: entry.denominator };
278
+ }
279
+ setMeter(numerator, denominator, atTick = 0) {
280
+ this._validateMeter(numerator, denominator);
281
+ if (atTick < 0) {
282
+ throw new Error("[waveform-playlist] MeterMap: atTick must be non-negative, got " + atTick);
283
+ }
284
+ if (atTick === 0) {
285
+ this._entries[0] = { ...this._entries[0], numerator, denominator };
286
+ this._resnapDownstreamEntries(0);
287
+ this._recomputeCache(0);
288
+ return;
289
+ }
290
+ const snapped = this._snapToBarBoundary(atTick);
291
+ if (snapped !== atTick) {
292
+ console.warn(
293
+ "[waveform-playlist] MeterMap.setMeter: tick " + atTick + " is not on a bar boundary, snapped to " + snapped
294
+ );
295
+ }
296
+ let i = this._entries.length - 1;
297
+ while (i > 0 && this._entries[i].tick > snapped) i--;
298
+ if (this._entries[i].tick === snapped) {
299
+ this._entries[i] = { ...this._entries[i], numerator, denominator };
300
+ } else {
301
+ const barAtTick = this._computeBarAtTick(snapped);
302
+ this._entries.splice(i + 1, 0, { tick: snapped, numerator, denominator, barAtTick });
303
+ i = i + 1;
304
+ }
305
+ this._resnapDownstreamEntries(i);
306
+ this._recomputeCache(i);
307
+ }
308
+ removeMeter(atTick) {
309
+ if (atTick === 0) {
310
+ throw new Error("[waveform-playlist] MeterMap: cannot remove meter at tick 0");
311
+ }
312
+ const idx = this._entries.findIndex((e) => e.tick === atTick);
313
+ if (idx > 0) {
314
+ this._entries.splice(idx, 1);
315
+ this._recomputeCache(idx);
316
+ } else if (idx === -1) {
317
+ console.warn("[waveform-playlist] MeterMap.removeMeter: no entry at tick " + atTick);
318
+ }
319
+ }
320
+ clearMeters() {
321
+ const first = this._entries[0];
322
+ this._entries = [{ ...first, barAtTick: 0 }];
323
+ }
324
+ ticksPerBeat(atTick = 0) {
325
+ const entry = this._entryAt(atTick);
326
+ return this._ppqn * (4 / entry.denominator);
327
+ }
328
+ ticksPerBar(atTick = 0) {
329
+ const entry = this._entryAt(atTick);
330
+ return entry.numerator * this._ppqn * (4 / entry.denominator);
331
+ }
332
+ barToTick(bar) {
333
+ if (bar < 1) {
334
+ throw new Error("[waveform-playlist] MeterMap: bar must be >= 1, got " + bar);
335
+ }
336
+ const targetBar = bar - 1;
337
+ for (let i = 0; i < this._entries.length; i++) {
338
+ const nextBar = i < this._entries.length - 1 ? this._entries[i + 1].barAtTick : Infinity;
339
+ if (targetBar < nextBar) {
340
+ const barsInto = targetBar - this._entries[i].barAtTick;
341
+ const tpb = this._ticksPerBarForEntry(this._entries[i]);
342
+ return this._entries[i].tick + barsInto * tpb;
343
+ }
344
+ }
345
+ return 0;
346
+ }
347
+ tickToBar(tick) {
348
+ const entry = this._entryAt(tick);
349
+ const ticksInto = tick - entry.tick;
350
+ const tpb = this._ticksPerBarForEntry(entry);
351
+ return entry.barAtTick + Math.floor(ticksInto / tpb) + 1;
352
+ }
353
+ isBarBoundary(tick) {
354
+ const entry = this._entryAt(tick);
355
+ const ticksInto = tick - entry.tick;
356
+ const tpb = this._ticksPerBarForEntry(entry);
357
+ return ticksInto % tpb === 0;
358
+ }
359
+ /** Internal: get the full entry at a tick (for MetronomePlayer beat grid anchoring) */
360
+ getEntryAt(tick) {
361
+ return this._entryAt(tick);
362
+ }
363
+ _entryAt(tick) {
364
+ let lo = 0;
365
+ let hi = this._entries.length - 1;
366
+ while (lo < hi) {
367
+ const mid = lo + hi + 1 >> 1;
368
+ if (this._entries[mid].tick <= tick) {
369
+ lo = mid;
370
+ } else {
371
+ hi = mid - 1;
372
+ }
373
+ }
374
+ return this._entries[lo];
375
+ }
376
+ _ticksPerBarForEntry(entry) {
377
+ return entry.numerator * this._ppqn * (4 / entry.denominator);
378
+ }
379
+ _snapToBarBoundary(atTick) {
380
+ const entry = this._entryAt(atTick);
381
+ const tpb = this._ticksPerBarForEntry(entry);
382
+ const ticksInto = atTick - entry.tick;
383
+ if (ticksInto % tpb === 0) return atTick;
384
+ return entry.tick + Math.ceil(ticksInto / tpb) * tpb;
385
+ }
386
+ _computeBarAtTick(tick) {
387
+ const entry = this._entryAt(tick);
388
+ const ticksInto = tick - entry.tick;
389
+ const tpb = this._ticksPerBarForEntry(entry);
390
+ return entry.barAtTick + ticksInto / tpb;
391
+ }
392
+ _recomputeCache(fromIndex) {
393
+ for (let i = Math.max(1, fromIndex); i < this._entries.length; i++) {
394
+ const prev = this._entries[i - 1];
395
+ const tickDelta = this._entries[i].tick - prev.tick;
396
+ const tpb = this._ticksPerBarForEntry(prev);
397
+ this._entries[i] = {
398
+ ...this._entries[i],
399
+ barAtTick: prev.barAtTick + tickDelta / tpb
400
+ };
401
+ }
402
+ }
403
+ /**
404
+ * After changing a meter entry, re-snap downstream entries to bar boundaries
405
+ * of their preceding meter so barAtTick stays integer.
406
+ */
407
+ _resnapDownstreamEntries(fromIndex) {
408
+ for (let i = Math.max(1, fromIndex + 1); i < this._entries.length; i++) {
409
+ const prev = this._entries[i - 1];
410
+ const tpb = this._ticksPerBarForEntry(prev);
411
+ const tick = this._entries[i].tick;
412
+ const ticksIntoPrev = tick - prev.tick;
413
+ if (ticksIntoPrev % tpb !== 0) {
414
+ const snapped = prev.tick + Math.ceil(ticksIntoPrev / tpb) * tpb;
415
+ console.warn(
416
+ "[waveform-playlist] MeterMap: meter change moved entry from tick " + tick + " to " + snapped + " (bar boundary alignment)"
417
+ );
418
+ this._entries[i] = { ...this._entries[i], tick: snapped };
419
+ }
420
+ }
421
+ }
422
+ _validateMeter(numerator, denominator) {
423
+ if (!Number.isInteger(numerator) || numerator < 1 || numerator > 32) {
424
+ throw new Error(
425
+ "[waveform-playlist] MeterMap: numerator must be an integer 1-32, got " + numerator
426
+ );
427
+ }
428
+ if (!isPowerOf2(denominator) || denominator > 32) {
429
+ throw new Error(
430
+ "[waveform-playlist] MeterMap: denominator must be a power of 2 (1-32), got " + denominator
431
+ );
432
+ }
433
+ }
434
+ };
435
+
287
436
  // src/audio/master-node.ts
288
437
  var MasterNode = class {
289
438
  constructor(audioContext) {
@@ -550,15 +699,14 @@ var ClipPlayer = class {
550
699
 
551
700
  // src/audio/metronome-player.ts
552
701
  var MetronomePlayer = class {
553
- constructor(audioContext, tempoMap, tickTimeline, destination, toAudioTime) {
702
+ constructor(audioContext, tempoMap, meterMap, destination, toAudioTime) {
554
703
  this._enabled = false;
555
- this._beatsPerBar = 4;
556
704
  this._accentBuffer = null;
557
705
  this._normalBuffer = null;
558
706
  this._activeSources = /* @__PURE__ */ new Set();
559
707
  this._audioContext = audioContext;
560
708
  this._tempoMap = tempoMap;
561
- this._tickTimeline = tickTimeline;
709
+ this._meterMap = meterMap;
562
710
  this._destination = destination;
563
711
  this._toAudioTime = toAudioTime;
564
712
  }
@@ -568,9 +716,6 @@ var MetronomePlayer = class {
568
716
  this.silence();
569
717
  }
570
718
  }
571
- setBeatsPerBar(beats) {
572
- this._beatsPerBar = beats;
573
- }
574
719
  setClickSounds(accent, normal) {
575
720
  this._accentBuffer = accent;
576
721
  this._normalBuffer = normal;
@@ -580,19 +725,27 @@ var MetronomePlayer = class {
580
725
  return [];
581
726
  }
582
727
  const events = [];
583
- const ppqn = this._tickTimeline.ppqn;
584
728
  const fromTicks = this._tempoMap.secondsToTicks(fromTime);
585
729
  const toTicks = this._tempoMap.secondsToTicks(toTime);
586
- const firstBeatTick = Math.ceil(fromTicks / ppqn) * ppqn;
587
- for (let tick = firstBeatTick; tick < toTicks; tick += ppqn) {
730
+ let entry = this._meterMap.getEntryAt(fromTicks);
731
+ let beatSize = this._meterMap.ticksPerBeat(fromTicks);
732
+ const tickIntoSection = fromTicks - entry.tick;
733
+ let tick = entry.tick + Math.ceil(tickIntoSection / beatSize) * beatSize;
734
+ while (tick < toTicks) {
735
+ const currentEntry = this._meterMap.getEntryAt(tick);
736
+ if (currentEntry.tick !== entry.tick) {
737
+ entry = currentEntry;
738
+ beatSize = this._meterMap.ticksPerBeat(tick);
739
+ }
740
+ const isAccent = this._meterMap.isBarBoundary(tick);
588
741
  const transportTime = this._tempoMap.ticksToSeconds(tick);
589
- const ticksPerBar = this._tickTimeline.ticksPerBar(this._beatsPerBar);
590
- const isAccent = tick % ticksPerBar === 0;
591
742
  events.push({
592
743
  transportTime,
593
744
  isAccent,
594
745
  buffer: isAccent ? this._accentBuffer : this._normalBuffer
595
746
  });
747
+ beatSize = this._meterMap.ticksPerBeat(tick);
748
+ tick += beatSize;
596
749
  }
597
750
  return events;
598
751
  }
@@ -653,9 +806,10 @@ var Transport = class _Transport {
653
806
  const sampleRate = options.sampleRate ?? audioContext.sampleRate;
654
807
  const ppqn = options.ppqn ?? 960;
655
808
  const tempo = options.tempo ?? 120;
656
- const beatsPerBar = options.beatsPerBar ?? 4;
809
+ const numerator = options.numerator ?? 4;
810
+ const denominator = options.denominator ?? 4;
657
811
  const lookahead = options.schedulerLookahead ?? 0.2;
658
- _Transport._validateOptions(sampleRate, ppqn, tempo, beatsPerBar, lookahead);
812
+ _Transport._validateOptions(sampleRate, ppqn, tempo, numerator, denominator, lookahead);
659
813
  this._clock = new Clock(audioContext);
660
814
  this._scheduler = new Scheduler({
661
815
  lookahead,
@@ -664,9 +818,9 @@ var Transport = class _Transport {
664
818
  }
665
819
  });
666
820
  this._sampleTimeline = new SampleTimeline(sampleRate);
667
- this._tickTimeline = new TickTimeline(ppqn);
821
+ this._meterMap = new MeterMap(ppqn, numerator, denominator);
668
822
  this._tempoMap = new TempoMap(ppqn, tempo);
669
- this._initAudioGraph(audioContext, beatsPerBar);
823
+ this._initAudioGraph(audioContext);
670
824
  this._timer = new Timer(() => {
671
825
  const time = this._clock.getTime();
672
826
  if (this._endTime !== void 0 && time >= this._endTime) {
@@ -858,15 +1012,46 @@ var Transport = class _Transport {
858
1012
  this._emit("loop");
859
1013
  }
860
1014
  // --- Tempo ---
861
- setTempo(bpm) {
862
- this._tempoMap.setTempo(bpm);
1015
+ setTempo(bpm, atTick) {
1016
+ this._tempoMap.setTempo(bpm, atTick);
863
1017
  this._emit("tempochange");
864
1018
  }
865
- getTempo() {
866
- return this._tempoMap.getTempo();
1019
+ getTempo(atTick) {
1020
+ return this._tempoMap.getTempo(atTick);
1021
+ }
1022
+ // --- Meter ---
1023
+ setMeter(numerator, denominator, atTick) {
1024
+ this._meterMap.setMeter(numerator, denominator, atTick);
1025
+ this._emit("meterchange");
1026
+ }
1027
+ getMeter(atTick) {
1028
+ return this._meterMap.getMeter(atTick);
1029
+ }
1030
+ removeMeter(atTick) {
1031
+ this._meterMap.removeMeter(atTick);
1032
+ this._emit("meterchange");
1033
+ }
1034
+ clearMeters() {
1035
+ this._meterMap.clearMeters();
1036
+ this._emit("meterchange");
1037
+ }
1038
+ clearTempos() {
1039
+ this._tempoMap.clearTempos();
1040
+ this._emit("tempochange");
867
1041
  }
868
- setBeatsPerBar(beats) {
869
- this._metronomePlayer.setBeatsPerBar(beats);
1042
+ barToTick(bar) {
1043
+ return this._meterMap.barToTick(bar);
1044
+ }
1045
+ tickToBar(tick) {
1046
+ return this._meterMap.tickToBar(tick);
1047
+ }
1048
+ /** Convert transport time (seconds) to tick position, using the tempo map. */
1049
+ timeToTick(seconds) {
1050
+ return this._tempoMap.secondsToTicks(seconds);
1051
+ }
1052
+ /** Convert tick position to transport time (seconds), using the tempo map. */
1053
+ tickToTime(tick) {
1054
+ return this._tempoMap.ticksToSeconds(tick);
870
1055
  }
871
1056
  // --- Metronome ---
872
1057
  setMetronomeEnabled(enabled) {
@@ -913,7 +1098,7 @@ var Transport = class _Transport {
913
1098
  this._listeners.clear();
914
1099
  }
915
1100
  // --- Private ---
916
- static _validateOptions(sampleRate, ppqn, tempo, beatsPerBar, lookahead) {
1101
+ static _validateOptions(sampleRate, ppqn, tempo, numerator, denominator, lookahead) {
917
1102
  if (sampleRate <= 0) {
918
1103
  throw new Error(
919
1104
  "[waveform-playlist] Transport: sampleRate must be positive, got " + sampleRate
@@ -927,9 +1112,14 @@ var Transport = class _Transport {
927
1112
  if (tempo <= 0) {
928
1113
  throw new Error("[waveform-playlist] Transport: tempo must be positive, got " + tempo);
929
1114
  }
930
- if (beatsPerBar <= 0 || !Number.isInteger(beatsPerBar)) {
1115
+ if (!Number.isInteger(numerator) || numerator < 1 || numerator > 32) {
1116
+ throw new Error(
1117
+ "[waveform-playlist] Transport: numerator must be an integer 1-32, got " + numerator
1118
+ );
1119
+ }
1120
+ if (denominator <= 0 || (denominator & denominator - 1) !== 0 || denominator > 32) {
931
1121
  throw new Error(
932
- "[waveform-playlist] Transport: beatsPerBar must be a positive integer, got " + beatsPerBar
1122
+ "[waveform-playlist] Transport: denominator must be a power of 2 (1-32), got " + denominator
933
1123
  );
934
1124
  }
935
1125
  if (lookahead <= 0) {
@@ -938,7 +1128,7 @@ var Transport = class _Transport {
938
1128
  );
939
1129
  }
940
1130
  }
941
- _initAudioGraph(audioContext, beatsPerBar) {
1131
+ _initAudioGraph(audioContext) {
942
1132
  this._masterNode = new MasterNode(audioContext);
943
1133
  this._masterNode.output.connect(audioContext.destination);
944
1134
  const toAudioTime = (transportTime) => this._clock.toAudioTime(transportTime);
@@ -946,11 +1136,10 @@ var Transport = class _Transport {
946
1136
  this._metronomePlayer = new MetronomePlayer(
947
1137
  audioContext,
948
1138
  this._tempoMap,
949
- this._tickTimeline,
1139
+ this._meterMap,
950
1140
  this._masterNode.input,
951
1141
  toAudioTime
952
1142
  );
953
- this._metronomePlayer.setBeatsPerBar(beatsPerBar);
954
1143
  this._scheduler.addListener(this._clipPlayer);
955
1144
  this._scheduler.addListener(this._metronomePlayer);
956
1145
  }
@@ -1056,12 +1245,12 @@ export {
1056
1245
  ClipPlayer,
1057
1246
  Clock,
1058
1247
  MasterNode,
1248
+ MeterMap,
1059
1249
  MetronomePlayer,
1060
1250
  NativePlayoutAdapter,
1061
1251
  SampleTimeline,
1062
1252
  Scheduler,
1063
1253
  TempoMap,
1064
- TickTimeline,
1065
1254
  Timer,
1066
1255
  TrackNode,
1067
1256
  Transport