@dawcore/transport 0.0.12 → 0.0.13

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
@@ -10,7 +10,7 @@ Native Web Audio transport for multi-track audio scheduling, looping, tempo, and
10
10
  - **Built-in metronome** — Beat-grid click scheduling with accent on beat 1. Default synthesized click sounds out of the box.
11
11
  - **Count-in (pre-roll)** — Configurable bars of click sounds before playback begins. Beat-by-beat events for UI countdown.
12
12
  - **Per-track signal chain** — Native GainNode (volume) → StereoPannerNode → GainNode (mute) → effects hook → master output.
13
- - **Effects plugin hook** — `connectTrackOutput(trackId, node)` inserts any `AudioNode` chain (Tone.js effects, WAM plugins, native nodes).
13
+ - **Effects plugin hook** — `connectTrackOutput(trackId, node)` and `connectMasterOutput(node)` insert any `AudioNode` chain (Tone.js effects, WAM plugins, native nodes) per-track or on the master bus.
14
14
  - **Type-safe coordinates** — Branded `Tick` and `Sample` types prevent accidentally passing seconds where ticks or samples are expected. Zero runtime cost.
15
15
  - **PlayoutAdapter bridge** — `NativePlayoutAdapter` implements the `PlayoutAdapter` interface from `@waveform-playlist/engine`.
16
16
 
@@ -55,9 +55,12 @@ import { NativePlayoutAdapter } from '@dawcore/transport';
55
55
  const audioContext = new AudioContext({ sampleRate: 48000 });
56
56
  const adapter = new NativePlayoutAdapter(audioContext);
57
57
 
58
- // Use as daw-editor's adapter factory
58
+ // Use as daw-editor's playout adapter
59
59
  const editor = document.querySelector('daw-editor');
60
- editor.adapterFactory = () => new NativePlayoutAdapter(audioContext);
60
+ editor.adapter = adapter;
61
+
62
+ // Transport-specific APIs stay available on your adapter reference
63
+ adapter.transport.setMetronomeEnabled(true);
61
64
  ```
62
65
 
63
66
  ### Metronome
@@ -156,6 +159,17 @@ transport.connectTrackOutput('vocals', reverb);
156
159
 
157
160
  // Remove effects — restores direct routing to master
158
161
  transport.disconnectTrackOutput('vocals');
162
+
163
+ // Master bus effects — inserted between master gain and destination
164
+ const compressor = audioContext.createDynamicsCompressor();
165
+ compressor.connect(audioContext.destination);
166
+
167
+ transport.connectMasterOutput(compressor);
168
+
169
+ // Remove master effects — restores direct routing to destination.
170
+ // Parallel taps on transport.masterOutputNode (analyzers, recorders)
171
+ // are unaffected by connect/disconnect.
172
+ transport.disconnectMasterOutput();
159
173
  ```
160
174
 
161
175
  ## API
@@ -205,7 +219,8 @@ new Transport(audioContext: AudioContext, options?: TransportOptions)
205
219
  - `setLoopSamples(enabled, startSample: Sample, endSample: Sample)` — Set loop region in samples (convenience)
206
220
 
207
221
  **Tempo & Meter:**
208
- - `setTempo(bpm, atTick?, options?)` / `getTempo(atTick?: Tick)` — options: `{ interpolation: 'step' | 'linear' | { type: 'curve', slope } }`
222
+ - `setTempo(bpm, atTick?, options?)` / `getTempo(atTick?: Tick)` — options: `{ interpolation: 'step' | 'linear' | { type: 'curve', slope } }`. Returns `boolean`: a defaulted `atTick` is the single-BPM convenience path and is refused (with a warning) when the tempo map has more than one entry — pass an explicit `atTick` to modify a tempo curve.
223
+ - `removeTempo(atTick: Tick)` — remove the tempo entry at a tick (the tick-0 entry cannot be removed)
209
224
  - `clearTempos()` — remove all tempo entries
210
225
  - `setMeter(numerator, denominator, atTick?: Tick)` / `getMeter(atTick?: Tick)`
211
226
  - `removeMeter(atTick: Tick)` / `clearMeters()`
@@ -224,12 +239,16 @@ new Transport(audioContext: AudioContext, options?: TransportOptions)
224
239
  - `isCountingIn()` — whether count-in is active
225
240
 
226
241
  **Effects:**
227
- - `connectTrackOutput(trackId, node)` — Insert effects chain
228
- - `disconnectTrackOutput(trackId)` — Remove effects chain
242
+ - `connectTrackOutput(trackId, node)` — Insert per-track effects chain
243
+ - `disconnectTrackOutput(trackId)` — Remove per-track effects chain
244
+ - `connectMasterOutput(node)` — Insert master bus effects chain
245
+ - `disconnectMasterOutput()` — Remove master bus effects chain
246
+ - `masterOutputNode` (getter) — Master gain node, for parallel taps (analyzers, recorders) that should survive chain connect/disconnect
229
247
 
230
248
  **Events:**
231
249
  - `on(event, callback)` / `off(event, callback)`
232
- - Events: `play`, `pause`, `stop`, `loop`, `tempochange`, `meterchange`, `countIn`, `countInEnd`
250
+ - Events: `play`, `pause`, `stop`, `seek`, `loop`, `tempochange`, `meterchange`, `countIn`, `countInEnd`
251
+ - `seek` payload: `{ seconds: number }`
233
252
  - `tempochange` payload: `{ bpm: number, atTick: Tick }`
234
253
  - `meterchange` payload: `{ numerator: number, denominator: number, atTick: Tick }`
235
254
  - `countIn` payload: `{ beat: number, totalBeats: number }`
@@ -245,15 +264,27 @@ new NativePlayoutAdapter(audioContext: AudioContext, options?: TransportOptions)
245
264
 
246
265
  Implements `PlayoutAdapter` from `@waveform-playlist/engine`. All methods delegate to the internal `Transport` instance.
247
266
 
248
- - `adapter.transport` — Direct access to the `Transport` for tempo, metronome, and effects APIs
267
+ - `adapter.transport` — Direct access to the `Transport` for tempo, metronome, count-in, and effects APIs
268
+ - `adapter.ppqn` — Tick resolution, read by the engine on construction
269
+ - `adapter.masterOutputNode` — Master gain node for parallel taps (analyzers, recorders)
270
+ - `adapter.init()` — Resumes a suspended AudioContext and waits for the hardware pipeline to warm up (Safari needs this before clips scheduled at time 0 play on time)
271
+ - `setTempo(bpm, atTick?)`, `setMeter(numerator, denominator, atTick?)`, `ticksToSeconds(tick)`, `secondsToTicks(seconds)` — tempo/meter surface the engine uses for tick-based timeline math
272
+
273
+ ## Examples
274
+
275
+ [`examples/dawcore-native/`](https://github.com/naomiaro/waveform-playlist/tree/main/examples/dawcore-native) pairs this transport with the `@dawcore/components` editor: metronome, tempo automation, mixed meter, beat-map grids, effects, and recording pages (`pnpm example:dawcore-native`).
249
276
 
250
277
  ## Architecture
251
278
 
252
- See [TRANSPORT.md](./TRANSPORT.md) for the full architecture guide.
279
+ See [TRANSPORT.md](https://github.com/naomiaro/waveform-playlist/blob/main/packages/transport/TRANSPORT.md) for the full architecture guide.
253
280
 
254
281
  ## How It Works
255
282
 
256
- See [EDUCATIONAL.md](./EDUCATIONAL.md) for an in-depth explanation of the math and timing models behind audio transport systems.
283
+ See [EDUCATIONAL.md](https://github.com/naomiaro/waveform-playlist/blob/main/packages/transport/EDUCATIONAL.md) for an in-depth explanation of the math and timing models behind audio transport systems.
284
+
285
+ ## Documentation
286
+
287
+ Full guides at [naomiaro.github.io/waveform-playlist](https://naomiaro.github.io/waveform-playlist/).
257
288
 
258
289
  ## License
259
290
 
package/dist/index.d.mts CHANGED
@@ -281,9 +281,22 @@ declare class MeterMap {
281
281
 
282
282
  declare class MasterNode {
283
283
  private _gainNode;
284
+ private _destination;
285
+ private _effectsInput;
284
286
  constructor(audioContext: AudioContext);
285
287
  get input(): AudioNode;
286
288
  get output(): AudioNode;
289
+ /** Connect the master output to its final destination (audioContext.destination) */
290
+ connectOutput(destination: AudioNode): void;
291
+ /**
292
+ * Insert an effects chain between the master gain and the destination.
293
+ * Only the destination edge is severed (targeted disconnect), so parallel
294
+ * taps on the master output (analyzers, recorders) keep working.
295
+ * The caller is responsible for routing the chain's output onward.
296
+ */
297
+ connectEffects(effectsInput: AudioNode): void;
298
+ /** Remove the effects chain and restore direct routing to the destination */
299
+ disconnectEffects(): void;
287
300
  setVolume(value: number): void;
288
301
  dispose(): void;
289
302
  }
@@ -393,6 +406,9 @@ interface TransportEvents {
393
406
  pause: () => void;
394
407
  stop: () => void;
395
408
  loop: () => void;
409
+ seek: (event: {
410
+ seconds: number;
411
+ }) => void;
396
412
  tempochange: (event: TempoChangeEventData) => void;
397
413
  meterchange: (event: MeterChangeEventData) => void;
398
414
  countIn: (event: CountInEventData) => void;
@@ -492,6 +508,16 @@ declare class Transport {
492
508
  /** The master output AudioNode. Connect your own nodes (analyzers, recorders, etc.)
493
509
  * in parallel. The transport already routes this to audioContext.destination. */
494
510
  get masterOutputNode(): AudioNode;
511
+ /**
512
+ * Insert an effects chain on the master bus, between the master gain and
513
+ * audioContext.destination. Accepts any AudioNode chain (Tone.js effects,
514
+ * WAM plugins, native nodes). The caller is responsible for connecting the
515
+ * chain's output to audioContext.destination. Calling again replaces the
516
+ * previous chain. Parallel taps on `masterOutputNode` are unaffected.
517
+ */
518
+ connectMasterOutput(node: AudioNode): void;
519
+ /** Remove the master effects chain and restore direct routing to audioContext.destination. */
520
+ disconnectMasterOutput(): void;
495
521
  on<K extends TransportEventType>(event: K, cb: TransportEvents[K]): void;
496
522
  off<K extends TransportEventType>(event: K, cb: TransportEvents[K]): void;
497
523
  dispose(): void;
package/dist/index.d.ts CHANGED
@@ -281,9 +281,22 @@ declare class MeterMap {
281
281
 
282
282
  declare class MasterNode {
283
283
  private _gainNode;
284
+ private _destination;
285
+ private _effectsInput;
284
286
  constructor(audioContext: AudioContext);
285
287
  get input(): AudioNode;
286
288
  get output(): AudioNode;
289
+ /** Connect the master output to its final destination (audioContext.destination) */
290
+ connectOutput(destination: AudioNode): void;
291
+ /**
292
+ * Insert an effects chain between the master gain and the destination.
293
+ * Only the destination edge is severed (targeted disconnect), so parallel
294
+ * taps on the master output (analyzers, recorders) keep working.
295
+ * The caller is responsible for routing the chain's output onward.
296
+ */
297
+ connectEffects(effectsInput: AudioNode): void;
298
+ /** Remove the effects chain and restore direct routing to the destination */
299
+ disconnectEffects(): void;
287
300
  setVolume(value: number): void;
288
301
  dispose(): void;
289
302
  }
@@ -393,6 +406,9 @@ interface TransportEvents {
393
406
  pause: () => void;
394
407
  stop: () => void;
395
408
  loop: () => void;
409
+ seek: (event: {
410
+ seconds: number;
411
+ }) => void;
396
412
  tempochange: (event: TempoChangeEventData) => void;
397
413
  meterchange: (event: MeterChangeEventData) => void;
398
414
  countIn: (event: CountInEventData) => void;
@@ -492,6 +508,16 @@ declare class Transport {
492
508
  /** The master output AudioNode. Connect your own nodes (analyzers, recorders, etc.)
493
509
  * in parallel. The transport already routes this to audioContext.destination. */
494
510
  get masterOutputNode(): AudioNode;
511
+ /**
512
+ * Insert an effects chain on the master bus, between the master gain and
513
+ * audioContext.destination. Accepts any AudioNode chain (Tone.js effects,
514
+ * WAM plugins, native nodes). The caller is responsible for connecting the
515
+ * chain's output to audioContext.destination. Calling again replaces the
516
+ * previous chain. Parallel taps on `masterOutputNode` are unaffected.
517
+ */
518
+ connectMasterOutput(node: AudioNode): void;
519
+ /** Remove the master effects chain and restore direct routing to audioContext.destination. */
520
+ disconnectMasterOutput(): void;
495
521
  on<K extends TransportEventType>(event: K, cb: TransportEvents[K]): void;
496
522
  off<K extends TransportEventType>(event: K, cb: TransportEvents[K]): void;
497
523
  dispose(): void;
package/dist/index.js CHANGED
@@ -711,6 +711,8 @@ var MeterMap = class {
711
711
  // src/audio/master-node.ts
712
712
  var MasterNode = class {
713
713
  constructor(audioContext) {
714
+ this._destination = null;
715
+ this._effectsInput = null;
714
716
  this._gainNode = audioContext.createGain();
715
717
  }
716
718
  get input() {
@@ -719,6 +721,37 @@ var MasterNode = class {
719
721
  get output() {
720
722
  return this._gainNode;
721
723
  }
724
+ /** Connect the master output to its final destination (audioContext.destination) */
725
+ connectOutput(destination) {
726
+ this._destination = destination;
727
+ this._gainNode.connect(destination);
728
+ }
729
+ /**
730
+ * Insert an effects chain between the master gain and the destination.
731
+ * Only the destination edge is severed (targeted disconnect), so parallel
732
+ * taps on the master output (analyzers, recorders) keep working.
733
+ * The caller is responsible for routing the chain's output onward.
734
+ */
735
+ connectEffects(effectsInput) {
736
+ if (this._effectsInput) {
737
+ this._gainNode.disconnect(this._effectsInput);
738
+ } else if (this._destination) {
739
+ this._gainNode.disconnect(this._destination);
740
+ }
741
+ this._gainNode.connect(effectsInput);
742
+ this._effectsInput = effectsInput;
743
+ }
744
+ /** Remove the effects chain and restore direct routing to the destination */
745
+ disconnectEffects() {
746
+ if (!this._effectsInput) {
747
+ return;
748
+ }
749
+ this._gainNode.disconnect(this._effectsInput);
750
+ if (this._destination) {
751
+ this._gainNode.connect(this._destination);
752
+ }
753
+ this._effectsInput = null;
754
+ }
722
755
  setVolume(value) {
723
756
  this._gainNode.gain.value = value;
724
757
  }
@@ -728,6 +761,8 @@ var MasterNode = class {
728
761
  } catch (err) {
729
762
  console.warn("[waveform-playlist] MasterNode.dispose: error disconnecting: " + String(err));
730
763
  }
764
+ this._destination = null;
765
+ this._effectsInput = null;
731
766
  }
732
767
  };
733
768
 
@@ -1378,6 +1413,7 @@ var _Transport = class _Transport {
1378
1413
  this._clipPlayer.onPositionJump(seekTick);
1379
1414
  this._timer.start();
1380
1415
  }
1416
+ this._emit("seek", { seconds: time });
1381
1417
  }
1382
1418
  getCurrentTime() {
1383
1419
  if (this._countingIn) {
@@ -1687,6 +1723,20 @@ var _Transport = class _Transport {
1687
1723
  get masterOutputNode() {
1688
1724
  return this._masterNode.output;
1689
1725
  }
1726
+ /**
1727
+ * Insert an effects chain on the master bus, between the master gain and
1728
+ * audioContext.destination. Accepts any AudioNode chain (Tone.js effects,
1729
+ * WAM plugins, native nodes). The caller is responsible for connecting the
1730
+ * chain's output to audioContext.destination. Calling again replaces the
1731
+ * previous chain. Parallel taps on `masterOutputNode` are unaffected.
1732
+ */
1733
+ connectMasterOutput(node) {
1734
+ this._masterNode.connectEffects(node);
1735
+ }
1736
+ /** Remove the master effects chain and restore direct routing to audioContext.destination. */
1737
+ disconnectMasterOutput() {
1738
+ this._masterNode.disconnectEffects();
1739
+ }
1690
1740
  // --- Events ---
1691
1741
  on(event, cb) {
1692
1742
  if (!this._listeners.has(event)) {
@@ -1740,7 +1790,7 @@ var _Transport = class _Transport {
1740
1790
  }
1741
1791
  _initAudioGraph(audioContext) {
1742
1792
  this._masterNode = new MasterNode(audioContext);
1743
- this._masterNode.output.connect(audioContext.destination);
1793
+ this._masterNode.connectOutput(audioContext.destination);
1744
1794
  const toAudioTime = (transportTime) => this._clock.toAudioTime(transportTime);
1745
1795
  this._clipPlayer = new ClipPlayer(
1746
1796
  audioContext,