@dawcore/transport 0.0.2 → 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/dist/index.d.ts CHANGED
@@ -1,17 +1,29 @@
1
1
  import { ClipTrack } from '@waveform-playlist/core';
2
2
  import { PlayoutAdapter } from '@waveform-playlist/engine';
3
3
 
4
+ /** Branded type for tick positions — prevents accidentally passing seconds where ticks are expected. */
5
+ declare const __tick: unique symbol;
6
+ type Tick = number & {
7
+ readonly [__tick]: never;
8
+ };
9
+ /** Branded type for sample counts — prevents accidentally passing seconds where samples are expected. */
10
+ declare const __sample: unique symbol;
11
+ type Sample = number & {
12
+ readonly [__sample]: never;
13
+ };
4
14
  interface SchedulerEvent {
5
- /** Transport time (elapsed seconds from timeline start) when this event should be realized */
6
- transportTime: number;
15
+ /** Tick position (integer) on the timeline */
16
+ tick: Tick;
7
17
  }
8
18
  interface SchedulerListener<T extends SchedulerEvent> {
9
- /** Generate events in the time window [fromTime, toTime) */
10
- generate(fromTime: number, toTime: number): T[];
19
+ /** Generate events in the tick window [fromTick, toTick) */
20
+ generate(fromTick: Tick, toTick: Tick): T[];
11
21
  /** Realize an event (create audio nodes, start sources) */
12
22
  consume(event: T): void;
13
- /** Position jumped (loop/seek) — stop active sources, re-schedule */
14
- onPositionJump(newTime: number): void;
23
+ /** Position jumped (loop/seek) — listeners may stop and re-schedule as appropriate
24
+ * (ClipPlayer stops sources and creates mid-clip restarts; MetronomePlayer is a no-op
25
+ * since clicks are short one-shots that finish naturally) */
26
+ onPositionJump(newTick: Tick): void;
15
27
  /** Stop all active audio immediately */
16
28
  silence(): void;
17
29
  }
@@ -37,7 +49,7 @@ interface MeterSignature {
37
49
  /** Storage entry for MeterMap */
38
50
  interface MeterEntry {
39
51
  /** Tick position where this meter starts */
40
- tick: number;
52
+ tick: Tick;
41
53
  /** Time signature numerator (e.g., 6 in 6/8) */
42
54
  numerator: number;
43
55
  /** Time signature denominator (e.g., 8 in 6/8) */
@@ -45,11 +57,20 @@ interface MeterEntry {
45
57
  /** Cached cumulative bar count from tick 0 to this entry. Derived — do not set manually. */
46
58
  readonly barAtTick: number;
47
59
  }
60
+ /** How to interpolate tempo from the previous entry to this one.
61
+ * 'step' = instant jump (default). 'linear' = linear ramp.
62
+ * { type: 'curve', slope } = Möbius-Ease curve (slope 0-1 exclusive). */
63
+ type TempoInterpolation = 'step' | 'linear' | {
64
+ type: 'curve';
65
+ slope: number;
66
+ };
48
67
  interface TempoEntry {
49
68
  /** Tick position where this tempo starts */
50
- tick: number;
69
+ tick: Tick;
51
70
  /** Beats per minute */
52
71
  bpm: number;
72
+ /** How to arrive at this BPM from the previous entry */
73
+ readonly interpolation: TempoInterpolation;
53
74
  /** Cached cumulative seconds up to this tick (for O(log n) lookup). Derived — do not set manually. */
54
75
  readonly secondsAtTick: number;
55
76
  }
@@ -58,8 +79,10 @@ interface TransportPosition {
58
79
  bar: number;
59
80
  /** 1-indexed beat within bar */
60
81
  beat: number;
61
- /** Sub-beat tick (0 to ppqn-1) */
62
- tick: number;
82
+ /** Sub-beat tick remainder (0 to ppqn-1). Named subTick to avoid
83
+ * collision with SchedulerEvent.tick (absolute timeline position).
84
+ * Not branded Tick — a remainder within a beat, not an absolute position. */
85
+ subTick: number;
63
86
  }
64
87
 
65
88
  declare class Clock {
@@ -81,10 +104,62 @@ declare class Clock {
81
104
  isRunning(): boolean;
82
105
  }
83
106
 
107
+ interface SetTempoOptions {
108
+ interpolation?: TempoInterpolation;
109
+ }
110
+ declare class TempoMap {
111
+ private _ppqn;
112
+ private _entries;
113
+ constructor(ppqn?: number, initialBpm?: number);
114
+ getTempo(atTick?: Tick): number;
115
+ setTempo(bpm: number, atTick?: Tick, options?: SetTempoOptions): void;
116
+ ticksToSeconds(ticks: Tick): number;
117
+ secondsToTicks(seconds: number): Tick;
118
+ beatsToSeconds(beats: number): number;
119
+ secondsToBeats(seconds: number): number;
120
+ clearTempos(): void;
121
+ /** Get the interpolated BPM at a tick position */
122
+ private _getTempoAt;
123
+ private _ticksToSecondsInternal;
124
+ /**
125
+ * Exact integration for a linear BPM ramp using the logarithmic formula.
126
+ * For bpm(t) = bpm0 + r*t where r = (bpm1-bpm0)/T:
127
+ * seconds = (T * 60) / (ppqn * (bpm1-bpm0)) * ln(bpmAtTick / bpm0)
128
+ */
129
+ private _ticksToSecondsLinear;
130
+ /**
131
+ * Inverse of _ticksToSecondsLinear: given seconds, return ticks.
132
+ * Closed-form via exponential: bpmAtTick = bpm0 * exp(seconds * deltaBpm * ppqn / (60 * T))
133
+ * then ticks = (bpmAtTick - bpm0) * T / deltaBpm
134
+ *
135
+ * Note: exp(log(x)) has ~1 ULP floating-point error, so round-trips depend on
136
+ * Math.round() in the caller (secondsToTicks). This is sufficient for all tested
137
+ * BPM ranges (10–300 BPM) but is not algebraically exact like the previous
138
+ * trapezoidal/quadratic approach was.
139
+ */
140
+ private _secondsToTicksLinear;
141
+ /**
142
+ * Subdivided trapezoidal integration for a Möbius-Ease tempo curve.
143
+ * The BPM at progress p is: bpm0 + curveNormalizedAt(p, slope) * (bpm1 - bpm0).
144
+ * We subdivide into CURVE_SUBDIVISIONS intervals and apply trapezoidal rule.
145
+ */
146
+ private _ticksToSecondsCurve;
147
+ /**
148
+ * Inverse of _ticksToSecondsCurve: given seconds into a curved segment,
149
+ * return ticks. Uses binary search since there's no closed-form inverse.
150
+ */
151
+ private _secondsToTicksCurve;
152
+ private _entryIndexAt;
153
+ private _recomputeCache;
154
+ }
155
+
84
156
  interface SchedulerOptions {
85
157
  lookahead?: number;
86
- /** Called when the scheduler wraps at loopEnd — Transport uses this to seek the clock */
87
- onLoop?: (loopStartTime: number) => void;
158
+ /** Called when the scheduler wraps at loopEnd.
159
+ * Receives loopStart, loopEnd, and the currentTimeSeconds snapshot from
160
+ * advance() so the Transport can compute the correct clock seek target
161
+ * without re-reading the live AudioContext.currentTime. */
162
+ onLoop?: (loopStartSeconds: number, loopEndSeconds: number, currentTimeSeconds: number) => void;
88
163
  }
89
164
  declare class Scheduler<T extends SchedulerEvent> {
90
165
  private _lookahead;
@@ -94,12 +169,18 @@ declare class Scheduler<T extends SchedulerEvent> {
94
169
  private _loopStart;
95
170
  private _loopEnd;
96
171
  private _onLoop;
97
- constructor(options?: SchedulerOptions);
172
+ private _tempoMap;
173
+ constructor(tempoMap: TempoMap, options?: SchedulerOptions);
98
174
  addListener(listener: SchedulerListener<T>): void;
99
175
  removeListener(listener: SchedulerListener<T>): void;
100
- setLoop(enabled: boolean, start: number, end: number): void;
101
- reset(time: number): void;
102
- advance(currentTime: number): void;
176
+ /** Primary API ticks as source of truth */
177
+ setLoop(enabled: boolean, startTick: Tick, endTick: Tick): void;
178
+ /** Convenience — converts seconds to ticks via TempoMap */
179
+ setLoopSeconds(enabled: boolean, startSec: number, endSec: number): void;
180
+ /** Reset scheduling cursor. Takes seconds (from Clock), converts to ticks. */
181
+ reset(timeSeconds: number): void;
182
+ /** Advance the scheduling window. Takes seconds (from Clock), converts to ticks. */
183
+ advance(currentTimeSeconds: number): void;
103
184
  private _generateAndConsume;
104
185
  }
105
186
 
@@ -115,26 +196,14 @@ declare class Timer {
115
196
 
116
197
  declare class SampleTimeline {
117
198
  private _sampleRate;
199
+ private _tempoMap;
118
200
  constructor(sampleRate: number);
119
201
  get sampleRate(): number;
120
- samplesToSeconds(samples: number): number;
121
- secondsToSamples(seconds: number): number;
122
- }
123
-
124
- declare class TempoMap {
125
- private _ppqn;
126
- private _entries;
127
- constructor(ppqn?: number, initialBpm?: number);
128
- getTempo(atTick?: number): number;
129
- setTempo(bpm: number, atTick?: number): void;
130
- ticksToSeconds(ticks: number): number;
131
- secondsToTicks(seconds: number): number;
132
- beatsToSeconds(beats: number): number;
133
- secondsToBeats(seconds: number): number;
134
- clearTempos(): void;
135
- private _ticksToSecondsInternal;
136
- private _entryAt;
137
- private _recomputeCache;
202
+ setTempoMap(tempoMap: TempoMap): void;
203
+ samplesToSeconds(samples: Sample): number;
204
+ secondsToSamples(seconds: number): Sample;
205
+ ticksToSamples(ticks: Tick): Sample;
206
+ samplesToTicks(samples: Sample): Tick;
138
207
  }
139
208
 
140
209
  declare class MeterMap {
@@ -142,17 +211,17 @@ declare class MeterMap {
142
211
  private _entries;
143
212
  constructor(ppqn: number, numerator?: number, denominator?: number);
144
213
  get ppqn(): number;
145
- getMeter(atTick?: number): MeterSignature;
146
- setMeter(numerator: number, denominator: number, atTick?: number): void;
147
- removeMeter(atTick: number): void;
214
+ getMeter(atTick?: Tick): MeterSignature;
215
+ setMeter(numerator: number, denominator: number, atTick?: Tick): void;
216
+ removeMeter(atTick: Tick): void;
148
217
  clearMeters(): void;
149
- ticksPerBeat(atTick?: number): number;
150
- ticksPerBar(atTick?: number): number;
151
- barToTick(bar: number): number;
152
- tickToBar(tick: number): number;
153
- isBarBoundary(tick: number): boolean;
218
+ ticksPerBeat(atTick?: Tick): number;
219
+ ticksPerBar(atTick?: Tick): number;
220
+ barToTick(bar: number): Tick;
221
+ tickToBar(tick: Tick): number;
222
+ isBarBoundary(tick: Tick): boolean;
154
223
  /** Internal: get the full entry at a tick (for MetronomePlayer beat grid anchoring) */
155
- getEntryAt(tick: number): MeterEntry;
224
+ getEntryAt(tick: Tick): MeterEntry;
156
225
  private _entryAt;
157
226
  private _ticksPerBarForEntry;
158
227
  private _snapToBarBoundary;
@@ -199,33 +268,40 @@ interface ClipEvent extends SchedulerEvent {
199
268
  trackId: string;
200
269
  clipId: string;
201
270
  audioBuffer: AudioBuffer;
202
- /** Offset into the audioBuffer (seconds) */
203
- offset: number;
204
- /** Duration to play (seconds) */
205
- duration: number;
271
+ /** Clip position on timeline (integer samples) */
272
+ startSample: Sample;
273
+ /** Offset into audioBuffer (integer samples) */
274
+ offsetSamples: Sample;
275
+ /** Duration to play (integer samples) */
276
+ durationSamples: Sample;
206
277
  /** Clip gain multiplier */
207
278
  gain: number;
208
- /** Fade in duration in seconds */
209
- fadeInDuration: number;
210
- /** Fade out duration in seconds */
211
- fadeOutDuration: number;
279
+ /** Fade in duration (integer samples) */
280
+ fadeInDurationSamples: Sample;
281
+ /** Fade out duration (integer samples) */
282
+ fadeOutDurationSamples: Sample;
212
283
  }
213
284
  declare class ClipPlayer implements SchedulerListener<ClipEvent> {
214
285
  private _audioContext;
215
286
  private _sampleTimeline;
287
+ private _tempoMap;
216
288
  private _toAudioTime;
217
289
  private _tracks;
218
290
  private _trackNodes;
219
291
  private _activeSources;
220
292
  private _loopEnabled;
221
- private _loopEnd;
222
- constructor(audioContext: AudioContext, sampleTimeline: SampleTimeline, toAudioTime: (transportTime: number) => number);
293
+ private _loopEndSamples;
294
+ constructor(audioContext: AudioContext, sampleTimeline: SampleTimeline, tempoMap: TempoMap, toAudioTime: (transportTime: number) => number);
223
295
  setTracks(tracks: ClipTrack[], trackNodes: Map<string, TrackNode>): void;
224
- setLoop(enabled: boolean, _start: number, end: number): void;
296
+ /** Set loop region using ticks. startTick is unused — loop clamping only needs
297
+ * the end boundary; mid-clip restart at loopStart is handled by onPositionJump. */
298
+ setLoop(enabled: boolean, _startTick: Tick, endTick: Tick): void;
299
+ /** Set loop region using samples directly */
300
+ setLoopSamples(enabled: boolean, _startSample: Sample, endSample: Sample): void;
225
301
  updateTrack(trackId: string, track: ClipTrack): void;
226
- generate(fromTime: number, toTime: number): ClipEvent[];
302
+ generate(fromTick: Tick, toTick: Tick): ClipEvent[];
227
303
  consume(event: ClipEvent): void;
228
- onPositionJump(newTime: number): void;
304
+ onPositionJump(newTick: Tick): void;
229
305
  silence(): void;
230
306
  private _silenceTrack;
231
307
  }
@@ -247,9 +323,9 @@ declare class MetronomePlayer implements SchedulerListener<MetronomeEvent> {
247
323
  constructor(audioContext: AudioContext, tempoMap: TempoMap, meterMap: MeterMap, destination: AudioNode, toAudioTime: (transportTime: number) => number);
248
324
  setEnabled(enabled: boolean): void;
249
325
  setClickSounds(accent: AudioBuffer, normal: AudioBuffer): void;
250
- generate(fromTime: number, toTime: number): MetronomeEvent[];
326
+ generate(fromTick: Tick, toTick: Tick): MetronomeEvent[];
251
327
  consume(event: MetronomeEvent): void;
252
- onPositionJump(_newTime: number): void;
328
+ onPositionJump(_newTick: Tick): void;
253
329
  silence(): void;
254
330
  }
255
331
 
@@ -279,6 +355,9 @@ declare class Transport {
279
355
  private _mutedTrackIds;
280
356
  private _playing;
281
357
  private _endTime;
358
+ private _loopEnabled;
359
+ private _loopStartTick;
360
+ private _loopStartSeconds;
282
361
  private _listeners;
283
362
  constructor(audioContext: AudioContext, options?: TransportOptions);
284
363
  get audioContext(): AudioContext;
@@ -297,20 +376,25 @@ declare class Transport {
297
376
  setTrackMute(trackId: string, muted: boolean): void;
298
377
  setTrackSolo(trackId: string, soloed: boolean): void;
299
378
  setMasterVolume(volume: number): void;
300
- setLoop(enabled: boolean, start: number, end: number): void;
301
- setTempo(bpm: number, atTick?: number): void;
302
- getTempo(atTick?: number): number;
303
- setMeter(numerator: number, denominator: number, atTick?: number): void;
304
- getMeter(atTick?: number): MeterSignature;
305
- removeMeter(atTick: number): void;
379
+ /** Primary loop API ticks as source of truth */
380
+ setLoop(enabled: boolean, startTick: Tick, endTick: Tick): void;
381
+ /** Convenience — converts seconds to ticks */
382
+ setLoopSeconds(enabled: boolean, startSec: number, endSec: number): void;
383
+ /** Convenience — sets loop in samples */
384
+ setLoopSamples(enabled: boolean, startSample: Sample, endSample: Sample): void;
385
+ setTempo(bpm: number, atTick?: Tick, options?: SetTempoOptions): void;
386
+ getTempo(atTick?: Tick): number;
387
+ setMeter(numerator: number, denominator: number, atTick?: Tick): void;
388
+ getMeter(atTick?: Tick): MeterSignature;
389
+ removeMeter(atTick: Tick): void;
306
390
  clearMeters(): void;
307
391
  clearTempos(): void;
308
- barToTick(bar: number): number;
309
- tickToBar(tick: number): number;
392
+ barToTick(bar: number): Tick;
393
+ tickToBar(tick: Tick): number;
310
394
  /** Convert transport time (seconds) to tick position, using the tempo map. */
311
- timeToTick(seconds: number): number;
395
+ timeToTick(seconds: number): Tick;
312
396
  /** Convert tick position to transport time (seconds), using the tempo map. */
313
- tickToTime(tick: number): number;
397
+ tickToTime(tick: Tick): number;
314
398
  setMetronomeEnabled(enabled: boolean): void;
315
399
  setMetronomeClickSounds(accent: AudioBuffer, normal: AudioBuffer): void;
316
400
  connectTrackOutput(trackId: string, node: AudioNode): void;
@@ -350,4 +434,4 @@ declare class NativePlayoutAdapter implements PlayoutAdapter {
350
434
  dispose(): void;
351
435
  }
352
436
 
353
- export { type ClipEvent, ClipPlayer, Clock, MasterNode, type MeterEntry, MeterMap, type MeterSignature, type MetronomeEvent, MetronomePlayer, NativePlayoutAdapter, SampleTimeline, Scheduler, type SchedulerEvent, type SchedulerListener, type SchedulerOptions, type TempoEntry, TempoMap, Timer, TrackNode, Transport, type TransportEvents, type TransportOptions, type TransportPosition };
437
+ export { type ClipEvent, ClipPlayer, Clock, MasterNode, type MeterEntry, MeterMap, type MeterSignature, type MetronomeEvent, MetronomePlayer, NativePlayoutAdapter, type Sample, SampleTimeline, Scheduler, type SchedulerEvent, type SchedulerListener, type SchedulerOptions, type SetTempoOptions, type TempoEntry, type TempoInterpolation, TempoMap, type Tick, Timer, TrackNode, Transport, type TransportEvents, type TransportOptions, type TransportPosition };