@dawcore/transport 0.0.4 → 0.0.6

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/README.md CHANGED
@@ -7,7 +7,8 @@ Native Web Audio transport for multi-track audio scheduling, looping, tempo, and
7
7
  - **Native Web Audio** — No Tone.js, no `standardized-audio-context`. Direct `AudioContext` with full `sampleRate` and `latencyHint` control.
8
8
  - **Sliding window scheduler** — Schedules audio 200ms ahead via `requestAnimationFrame` for glitch-free playback.
9
9
  - **Dual timeline** — Sample-absolute positions for audio clips, PPQN tick positions for metronome/MIDI.
10
- - **Built-in metronome** — Beat-grid click scheduling with accent on beat 1. Just another scheduler listener.
10
+ - **Built-in metronome** — Beat-grid click scheduling with accent on beat 1. Default synthesized click sounds out of the box.
11
+ - **Count-in (pre-roll)** — Configurable bars of click sounds before playback begins. Beat-by-beat events for UI countdown.
11
12
  - **Per-track signal chain** — Native GainNode (volume) → StereoPannerNode → GainNode (mute) → effects hook → master output.
12
13
  - **Effects plugin hook** — `connectTrackOutput(trackId, node)` inserts any `AudioNode` chain (Tone.js effects, WAM plugins, native nodes).
13
14
  - **Type-safe coordinates** — Branded `Tick` and `Sample` types prevent accidentally passing seconds where ticks or samples are expected. Zero runtime cost.
@@ -71,9 +72,31 @@ const transport = new Transport(audioContext, {
71
72
  denominator: 4,
72
73
  });
73
74
 
75
+ // Default click sounds are built in — just enable and play
74
76
  transport.setMetronomeEnabled(true);
75
- transport.setMetronomeClickSounds(accentBuffer, normalBuffer);
76
77
  transport.play();
78
+
79
+ // Override with custom click sounds
80
+ transport.setMetronomeClickSounds(accentBuffer, normalBuffer);
81
+ ```
82
+
83
+ ### Count-In
84
+
85
+ ```typescript
86
+ transport.setCountIn(true);
87
+ transport.setCountInBars(1); // 1–8 bars, default 1
88
+ transport.setCountInMode('always'); // 'always' | 'recording-only' (default)
89
+
90
+ // Beat-by-beat events for UI countdown
91
+ transport.on('countIn', ({ beat, totalBeats }) => {
92
+ console.log(beat + ' / ' + totalBeats); // "1 / 4", "2 / 4", ...
93
+ });
94
+
95
+ transport.on('countInEnd', () => {
96
+ console.log('Playback starting');
97
+ });
98
+
99
+ transport.play(); // Plays count-in clicks, then starts playback
77
100
  ```
78
101
 
79
102
  ### Mixed Meter
@@ -155,6 +178,8 @@ new Transport(audioContext: AudioContext, options?: TransportOptions)
155
178
  | `numerator` | `4` | Beats per bar (time signature numerator) |
156
179
  | `denominator` | `4` | Beat unit (time signature denominator) |
157
180
  | `schedulerLookahead` | `0.2` | How far ahead to schedule (seconds) |
181
+ | `accentFrequency` | `1000` | Default accent click frequency (Hz) |
182
+ | `normalFrequency` | `800` | Default normal click frequency (Hz) |
158
183
 
159
184
  **Playback:**
160
185
  - `play(startTime?, endTime?)` — Start or resume playback
@@ -192,7 +217,14 @@ new Transport(audioContext: AudioContext, options?: TransportOptions)
192
217
 
193
218
  **Metronome:**
194
219
  - `setMetronomeEnabled(enabled)`
195
- - `setMetronomeClickSounds(accent, normal)`
220
+ - `setMetronomeClickSounds(accent, normal)` — overrides default synthesized sounds
221
+
222
+ **Count-In:**
223
+ - `setCountIn(enabled)` — enable/disable count-in
224
+ - `setCountInBars(bars)` — number of bars (1–8, default 1)
225
+ - `setCountInMode(mode)` — `'recording-only'` (default) or `'always'`
226
+ - `setRecording(recording)` — consumer signals recording state (for `'recording-only'` mode)
227
+ - `isCountingIn()` — whether count-in is active
196
228
 
197
229
  **Effects:**
198
230
  - `connectTrackOutput(trackId, node)` — Insert effects chain
@@ -200,7 +232,10 @@ new Transport(audioContext: AudioContext, options?: TransportOptions)
200
232
 
201
233
  **Events:**
202
234
  - `on(event, callback)` / `off(event, callback)`
203
- - Events: `play`, `pause`, `stop`, `loop`, `tempochange`
235
+ - Events: `play`, `pause`, `stop`, `loop`, `tempochange`, `meterchange`, `countIn`, `countInEnd`
236
+ - `tempochange` payload: `{ bpm: number, atTick: Tick }`
237
+ - `meterchange` payload: `{ numerator: number, denominator: number, atTick: Tick }`
238
+ - `countIn` payload: `{ beat: number, totalBeats: number }`
204
239
 
205
240
  **Cleanup:**
206
241
  - `dispose()` — Stop playback, disconnect all nodes, remove listeners
@@ -219,6 +254,10 @@ Implements `PlayoutAdapter` from `@waveform-playlist/engine`. All methods delega
219
254
 
220
255
  See [TRANSPORT.md](./TRANSPORT.md) for the full architecture guide.
221
256
 
257
+ ## How It Works
258
+
259
+ See [EDUCATIONAL.md](./EDUCATIONAL.md) for an in-depth explanation of the math and timing models behind audio transport systems.
260
+
222
261
  ## License
223
262
 
224
263
  MIT
package/dist/index.d.mts CHANGED
@@ -40,6 +40,10 @@ interface TransportOptions {
40
40
  denominator?: number;
41
41
  /** How far ahead to schedule audio, in seconds. Default: 0.2 */
42
42
  schedulerLookahead?: number;
43
+ /** Accent click frequency in Hz. Default: 1000 */
44
+ accentFrequency?: number;
45
+ /** Normal click frequency in Hz. Default: 800 */
46
+ normalFrequency?: number;
43
47
  }
44
48
  /** Public return type for getMeter() */
45
49
  interface MeterSignature {
@@ -84,6 +88,33 @@ interface TransportPosition {
84
88
  * Not branded Tick — a remainder within a beat, not an absolute position. */
85
89
  subTick: number;
86
90
  }
91
+ /** Count-in mode: 'always' plays count-in on every play(), 'recording-only' only when recording. */
92
+ type CountInMode = 'always' | 'recording-only';
93
+ /** Payload emitted with the 'countIn' transport event.
94
+ * Example: beat=1, totalBeats=4 means "first of four beats". */
95
+ interface CountInEventData {
96
+ /** Current beat number (1-indexed) */
97
+ beat: number;
98
+ /** Total number of beats in the count-in */
99
+ totalBeats: number;
100
+ }
101
+ /** Payload emitted with the 'tempochange' transport event. */
102
+ interface TempoChangeEventData {
103
+ /** The new BPM value */
104
+ bpm: number;
105
+ /** Tick position where the tempo was set */
106
+ atTick: Tick;
107
+ }
108
+ /** Payload emitted with the 'meterchange' transport event.
109
+ * After removeMeter/clearMeters, reflects the resulting meter at that position. */
110
+ interface MeterChangeEventData {
111
+ /** Time signature numerator */
112
+ numerator: number;
113
+ /** Time signature denominator */
114
+ denominator: number;
115
+ /** Tick position where the meter was set or removed */
116
+ atTick: Tick;
117
+ }
87
118
 
88
119
  declare class Clock {
89
120
  private _audioContext;
@@ -329,13 +360,29 @@ declare class MetronomePlayer implements SchedulerListener<MetronomeEvent> {
329
360
  silence(): void;
330
361
  }
331
362
 
363
+ interface CountInEvent extends SchedulerEvent {
364
+ isAccent: boolean;
365
+ buffer: AudioBuffer;
366
+ beat: number;
367
+ totalBeats: number;
368
+ }
369
+
370
+ interface ClickSoundOptions {
371
+ /** Frequency for accent click (beat 1). Default: 1000 Hz */
372
+ accentFrequency?: number;
373
+ /** Frequency for normal click (other beats). Default: 800 Hz */
374
+ normalFrequency?: number;
375
+ }
376
+
332
377
  interface TransportEvents {
333
378
  play: () => void;
334
379
  pause: () => void;
335
380
  stop: () => void;
336
381
  loop: () => void;
337
- tempochange: () => void;
338
- meterchange: () => void;
382
+ tempochange: (event: TempoChangeEventData) => void;
383
+ meterchange: (event: MeterChangeEventData) => void;
384
+ countIn: (event: CountInEventData) => void;
385
+ countInEnd: () => void;
339
386
  }
340
387
  type TransportEventType = keyof TransportEvents;
341
388
  declare class Transport {
@@ -359,6 +406,19 @@ declare class Transport {
359
406
  private _loopStartTick;
360
407
  private _loopStartSeconds;
361
408
  private _listeners;
409
+ private _countInEnabled;
410
+ private _countInBars;
411
+ private _countInMode;
412
+ private _recording;
413
+ private _countingIn;
414
+ private _countInStartPosition;
415
+ private _countInDuration;
416
+ private _countInPlayer;
417
+ private _countInScheduler;
418
+ private _accentBuffer;
419
+ private _normalBuffer;
420
+ private _schedulerLookahead;
421
+ private _ppqn;
362
422
  constructor(audioContext: AudioContext, options?: TransportOptions);
363
423
  get audioContext(): AudioContext;
364
424
  play(startTime?: number, endTime?: number): void;
@@ -397,6 +457,13 @@ declare class Transport {
397
457
  tickToTime(tick: Tick): number;
398
458
  setMetronomeEnabled(enabled: boolean): void;
399
459
  setMetronomeClickSounds(accent: AudioBuffer, normal: AudioBuffer): void;
460
+ static readonly MIN_COUNT_IN_BARS = 1;
461
+ static readonly MAX_COUNT_IN_BARS = 8;
462
+ setCountIn(enabled: boolean): void;
463
+ setCountInBars(bars: number): void;
464
+ setCountInMode(mode: CountInMode): void;
465
+ setRecording(recording: boolean): void;
466
+ isCountingIn(): boolean;
400
467
  connectTrackOutput(trackId: string, node: AudioNode): void;
401
468
  disconnectTrackOutput(trackId: string): void;
402
469
  on<K extends TransportEventType>(event: K, cb: TransportEvents[K]): void;
@@ -404,6 +471,11 @@ declare class Transport {
404
471
  dispose(): void;
405
472
  private static _validateOptions;
406
473
  private _initAudioGraph;
474
+ private _initCountIn;
475
+ private _shouldCountIn;
476
+ private _startCountIn;
477
+ private _finishCountIn;
478
+ private _cancelCountIn;
407
479
  private _silenceAll;
408
480
  private _applyMuteState;
409
481
  private _emit;
@@ -431,7 +503,12 @@ declare class NativePlayoutAdapter implements PlayoutAdapter {
431
503
  setTrackSolo(trackId: string, soloed: boolean): void;
432
504
  setTrackPan(trackId: string, pan: number): void;
433
505
  setLoop(enabled: boolean, start: number, end: number): void;
506
+ setCountIn(enabled: boolean): void;
507
+ setCountInBars(bars: number): void;
508
+ setCountInMode(mode: CountInMode): void;
509
+ setRecording(recording: boolean): void;
510
+ isCountingIn(): boolean;
434
511
  dispose(): void;
435
512
  }
436
513
 
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 };
514
+ export { type ClickSoundOptions, type ClipEvent, ClipPlayer, Clock, type CountInEvent, type CountInEventData, type CountInMode, MasterNode, type MeterChangeEventData, type MeterEntry, MeterMap, type MeterSignature, type MetronomeEvent, MetronomePlayer, NativePlayoutAdapter, type Sample, SampleTimeline, Scheduler, type SchedulerEvent, type SchedulerListener, type SchedulerOptions, type SetTempoOptions, type TempoChangeEventData, type TempoEntry, type TempoInterpolation, TempoMap, type Tick, Timer, TrackNode, Transport, type TransportEvents, type TransportOptions, type TransportPosition };
package/dist/index.d.ts CHANGED
@@ -40,6 +40,10 @@ interface TransportOptions {
40
40
  denominator?: number;
41
41
  /** How far ahead to schedule audio, in seconds. Default: 0.2 */
42
42
  schedulerLookahead?: number;
43
+ /** Accent click frequency in Hz. Default: 1000 */
44
+ accentFrequency?: number;
45
+ /** Normal click frequency in Hz. Default: 800 */
46
+ normalFrequency?: number;
43
47
  }
44
48
  /** Public return type for getMeter() */
45
49
  interface MeterSignature {
@@ -84,6 +88,33 @@ interface TransportPosition {
84
88
  * Not branded Tick — a remainder within a beat, not an absolute position. */
85
89
  subTick: number;
86
90
  }
91
+ /** Count-in mode: 'always' plays count-in on every play(), 'recording-only' only when recording. */
92
+ type CountInMode = 'always' | 'recording-only';
93
+ /** Payload emitted with the 'countIn' transport event.
94
+ * Example: beat=1, totalBeats=4 means "first of four beats". */
95
+ interface CountInEventData {
96
+ /** Current beat number (1-indexed) */
97
+ beat: number;
98
+ /** Total number of beats in the count-in */
99
+ totalBeats: number;
100
+ }
101
+ /** Payload emitted with the 'tempochange' transport event. */
102
+ interface TempoChangeEventData {
103
+ /** The new BPM value */
104
+ bpm: number;
105
+ /** Tick position where the tempo was set */
106
+ atTick: Tick;
107
+ }
108
+ /** Payload emitted with the 'meterchange' transport event.
109
+ * After removeMeter/clearMeters, reflects the resulting meter at that position. */
110
+ interface MeterChangeEventData {
111
+ /** Time signature numerator */
112
+ numerator: number;
113
+ /** Time signature denominator */
114
+ denominator: number;
115
+ /** Tick position where the meter was set or removed */
116
+ atTick: Tick;
117
+ }
87
118
 
88
119
  declare class Clock {
89
120
  private _audioContext;
@@ -329,13 +360,29 @@ declare class MetronomePlayer implements SchedulerListener<MetronomeEvent> {
329
360
  silence(): void;
330
361
  }
331
362
 
363
+ interface CountInEvent extends SchedulerEvent {
364
+ isAccent: boolean;
365
+ buffer: AudioBuffer;
366
+ beat: number;
367
+ totalBeats: number;
368
+ }
369
+
370
+ interface ClickSoundOptions {
371
+ /** Frequency for accent click (beat 1). Default: 1000 Hz */
372
+ accentFrequency?: number;
373
+ /** Frequency for normal click (other beats). Default: 800 Hz */
374
+ normalFrequency?: number;
375
+ }
376
+
332
377
  interface TransportEvents {
333
378
  play: () => void;
334
379
  pause: () => void;
335
380
  stop: () => void;
336
381
  loop: () => void;
337
- tempochange: () => void;
338
- meterchange: () => void;
382
+ tempochange: (event: TempoChangeEventData) => void;
383
+ meterchange: (event: MeterChangeEventData) => void;
384
+ countIn: (event: CountInEventData) => void;
385
+ countInEnd: () => void;
339
386
  }
340
387
  type TransportEventType = keyof TransportEvents;
341
388
  declare class Transport {
@@ -359,6 +406,19 @@ declare class Transport {
359
406
  private _loopStartTick;
360
407
  private _loopStartSeconds;
361
408
  private _listeners;
409
+ private _countInEnabled;
410
+ private _countInBars;
411
+ private _countInMode;
412
+ private _recording;
413
+ private _countingIn;
414
+ private _countInStartPosition;
415
+ private _countInDuration;
416
+ private _countInPlayer;
417
+ private _countInScheduler;
418
+ private _accentBuffer;
419
+ private _normalBuffer;
420
+ private _schedulerLookahead;
421
+ private _ppqn;
362
422
  constructor(audioContext: AudioContext, options?: TransportOptions);
363
423
  get audioContext(): AudioContext;
364
424
  play(startTime?: number, endTime?: number): void;
@@ -397,6 +457,13 @@ declare class Transport {
397
457
  tickToTime(tick: Tick): number;
398
458
  setMetronomeEnabled(enabled: boolean): void;
399
459
  setMetronomeClickSounds(accent: AudioBuffer, normal: AudioBuffer): void;
460
+ static readonly MIN_COUNT_IN_BARS = 1;
461
+ static readonly MAX_COUNT_IN_BARS = 8;
462
+ setCountIn(enabled: boolean): void;
463
+ setCountInBars(bars: number): void;
464
+ setCountInMode(mode: CountInMode): void;
465
+ setRecording(recording: boolean): void;
466
+ isCountingIn(): boolean;
400
467
  connectTrackOutput(trackId: string, node: AudioNode): void;
401
468
  disconnectTrackOutput(trackId: string): void;
402
469
  on<K extends TransportEventType>(event: K, cb: TransportEvents[K]): void;
@@ -404,6 +471,11 @@ declare class Transport {
404
471
  dispose(): void;
405
472
  private static _validateOptions;
406
473
  private _initAudioGraph;
474
+ private _initCountIn;
475
+ private _shouldCountIn;
476
+ private _startCountIn;
477
+ private _finishCountIn;
478
+ private _cancelCountIn;
407
479
  private _silenceAll;
408
480
  private _applyMuteState;
409
481
  private _emit;
@@ -431,7 +503,12 @@ declare class NativePlayoutAdapter implements PlayoutAdapter {
431
503
  setTrackSolo(trackId: string, soloed: boolean): void;
432
504
  setTrackPan(trackId: string, pan: number): void;
433
505
  setLoop(enabled: boolean, start: number, end: number): void;
506
+ setCountIn(enabled: boolean): void;
507
+ setCountInBars(bars: number): void;
508
+ setCountInMode(mode: CountInMode): void;
509
+ setRecording(recording: boolean): void;
510
+ isCountingIn(): boolean;
434
511
  dispose(): void;
435
512
  }
436
513
 
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 };
514
+ export { type ClickSoundOptions, type ClipEvent, ClipPlayer, Clock, type CountInEvent, type CountInEventData, type CountInMode, MasterNode, type MeterChangeEventData, type MeterEntry, MeterMap, type MeterSignature, type MetronomeEvent, MetronomePlayer, NativePlayoutAdapter, type Sample, SampleTimeline, Scheduler, type SchedulerEvent, type SchedulerListener, type SchedulerOptions, type SetTempoOptions, type TempoChangeEventData, type TempoEntry, type TempoInterpolation, TempoMap, type Tick, Timer, TrackNode, Transport, type TransportEvents, type TransportOptions, type TransportPosition };