@dawcore/transport 0.0.11 → 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 +41 -10
- package/dist/index.d.mts +45 -2
- package/dist/index.d.ts +45 -2
- package/dist/index.js +96 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +96 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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)`
|
|
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
|
|
58
|
+
// Use as daw-editor's playout adapter
|
|
59
59
|
const editor = document.querySelector('daw-editor');
|
|
60
|
-
editor.
|
|
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](
|
|
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](
|
|
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
|
@@ -147,12 +147,21 @@ declare class TempoMap {
|
|
|
147
147
|
* in secondsToTicks) — reject it at the boundary instead. */
|
|
148
148
|
private static _validateBpm;
|
|
149
149
|
getTempo(atTick?: Tick): number;
|
|
150
|
+
/** Number of tempo entries in the map. Always >= 1 — the tick-0 entry is
|
|
151
|
+
* permanent. Used by Transport.setTempo to detect multi-entry maps. */
|
|
152
|
+
get entryCount(): number;
|
|
150
153
|
setTempo(bpm: number, atTick?: Tick, options?: SetTempoOptions): void;
|
|
151
154
|
ticksToSeconds(ticks: Tick): number;
|
|
152
155
|
secondsToTicks(seconds: number): Tick;
|
|
153
156
|
beatsToSeconds(beats: number): number;
|
|
154
157
|
secondsToBeats(seconds: number): number;
|
|
155
158
|
clearTempos(): void;
|
|
159
|
+
/** Remove the tempo entry at exactly `atTick`, if one exists. The tick-0
|
|
160
|
+
* entry is permanent (a map must always have a tempo) — removing it is a
|
|
161
|
+
* no-op, matching removeMeter's treatment of the initial meter. The
|
|
162
|
+
* seconds cache is recomputed from the removal point, the same partial
|
|
163
|
+
* update setTempo uses. */
|
|
164
|
+
removeTempo(atTick: Tick): void;
|
|
156
165
|
/** Get the interpolated BPM at a tick position */
|
|
157
166
|
private _getTempoAt;
|
|
158
167
|
private _ticksToSecondsInternal;
|
|
@@ -272,9 +281,22 @@ declare class MeterMap {
|
|
|
272
281
|
|
|
273
282
|
declare class MasterNode {
|
|
274
283
|
private _gainNode;
|
|
284
|
+
private _destination;
|
|
285
|
+
private _effectsInput;
|
|
275
286
|
constructor(audioContext: AudioContext);
|
|
276
287
|
get input(): AudioNode;
|
|
277
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;
|
|
278
300
|
setVolume(value: number): void;
|
|
279
301
|
dispose(): void;
|
|
280
302
|
}
|
|
@@ -384,6 +406,9 @@ interface TransportEvents {
|
|
|
384
406
|
pause: () => void;
|
|
385
407
|
stop: () => void;
|
|
386
408
|
loop: () => void;
|
|
409
|
+
seek: (event: {
|
|
410
|
+
seconds: number;
|
|
411
|
+
}) => void;
|
|
387
412
|
tempochange: (event: TempoChangeEventData) => void;
|
|
388
413
|
meterchange: (event: MeterChangeEventData) => void;
|
|
389
414
|
countIn: (event: CountInEventData) => void;
|
|
@@ -448,13 +473,21 @@ declare class Transport {
|
|
|
448
473
|
setLoopSeconds(enabled: boolean, startSec: number, endSec: number): void;
|
|
449
474
|
/** Convenience — sets loop in samples */
|
|
450
475
|
setLoopSamples(enabled: boolean, startSample: Sample, endSample: Sample): void;
|
|
451
|
-
|
|
476
|
+
/** Returns true when the tempo was applied, false when a defaulted (no
|
|
477
|
+
* atTick) write was refused because the tempo map has multiple entries —
|
|
478
|
+
* pass an explicit atTick to modify a multi-entry map (#407). */
|
|
479
|
+
setTempo(bpm: number, atTick?: Tick, options?: SetTempoOptions): boolean;
|
|
452
480
|
getTempo(atTick?: Tick): number;
|
|
453
481
|
setMeter(numerator: number, denominator: number, atTick?: Tick): void;
|
|
454
482
|
getMeter(atTick?: Tick): MeterSignature;
|
|
455
483
|
removeMeter(atTick: Tick): void;
|
|
456
484
|
clearMeters(): void;
|
|
457
485
|
clearTempos(): void;
|
|
486
|
+
/** Remove the tempo entry at exactly `atTick` (tick 0 is permanent — a
|
|
487
|
+
* no-op, like removeMeter's initial entry). Mirrors setTempo's loop-cache
|
|
488
|
+
* invalidation and event; the emitted bpm is the tempo now in force at
|
|
489
|
+
* the removed position. */
|
|
490
|
+
removeTempo(atTick: Tick): void;
|
|
458
491
|
barToTick(bar: number): Tick;
|
|
459
492
|
tickToBar(tick: Tick): number;
|
|
460
493
|
/** Convert transport time (seconds) to tick position, using the tempo map. */
|
|
@@ -475,6 +508,16 @@ declare class Transport {
|
|
|
475
508
|
/** The master output AudioNode. Connect your own nodes (analyzers, recorders, etc.)
|
|
476
509
|
* in parallel. The transport already routes this to audioContext.destination. */
|
|
477
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;
|
|
478
521
|
on<K extends TransportEventType>(event: K, cb: TransportEvents[K]): void;
|
|
479
522
|
off<K extends TransportEventType>(event: K, cb: TransportEvents[K]): void;
|
|
480
523
|
dispose(): void;
|
|
@@ -519,7 +562,7 @@ declare class NativePlayoutAdapter implements PlayoutAdapter {
|
|
|
519
562
|
setCountInMode(mode: CountInMode): void;
|
|
520
563
|
setRecording(recording: boolean): void;
|
|
521
564
|
isCountingIn(): boolean;
|
|
522
|
-
setTempo(bpm: number, atTick?: number):
|
|
565
|
+
setTempo(bpm: number, atTick?: number): boolean;
|
|
523
566
|
setMeter(numerator: number, denominator: number, atTick?: number): void;
|
|
524
567
|
ticksToSeconds(tick: number): number;
|
|
525
568
|
secondsToTicks(seconds: number): number;
|
package/dist/index.d.ts
CHANGED
|
@@ -147,12 +147,21 @@ declare class TempoMap {
|
|
|
147
147
|
* in secondsToTicks) — reject it at the boundary instead. */
|
|
148
148
|
private static _validateBpm;
|
|
149
149
|
getTempo(atTick?: Tick): number;
|
|
150
|
+
/** Number of tempo entries in the map. Always >= 1 — the tick-0 entry is
|
|
151
|
+
* permanent. Used by Transport.setTempo to detect multi-entry maps. */
|
|
152
|
+
get entryCount(): number;
|
|
150
153
|
setTempo(bpm: number, atTick?: Tick, options?: SetTempoOptions): void;
|
|
151
154
|
ticksToSeconds(ticks: Tick): number;
|
|
152
155
|
secondsToTicks(seconds: number): Tick;
|
|
153
156
|
beatsToSeconds(beats: number): number;
|
|
154
157
|
secondsToBeats(seconds: number): number;
|
|
155
158
|
clearTempos(): void;
|
|
159
|
+
/** Remove the tempo entry at exactly `atTick`, if one exists. The tick-0
|
|
160
|
+
* entry is permanent (a map must always have a tempo) — removing it is a
|
|
161
|
+
* no-op, matching removeMeter's treatment of the initial meter. The
|
|
162
|
+
* seconds cache is recomputed from the removal point, the same partial
|
|
163
|
+
* update setTempo uses. */
|
|
164
|
+
removeTempo(atTick: Tick): void;
|
|
156
165
|
/** Get the interpolated BPM at a tick position */
|
|
157
166
|
private _getTempoAt;
|
|
158
167
|
private _ticksToSecondsInternal;
|
|
@@ -272,9 +281,22 @@ declare class MeterMap {
|
|
|
272
281
|
|
|
273
282
|
declare class MasterNode {
|
|
274
283
|
private _gainNode;
|
|
284
|
+
private _destination;
|
|
285
|
+
private _effectsInput;
|
|
275
286
|
constructor(audioContext: AudioContext);
|
|
276
287
|
get input(): AudioNode;
|
|
277
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;
|
|
278
300
|
setVolume(value: number): void;
|
|
279
301
|
dispose(): void;
|
|
280
302
|
}
|
|
@@ -384,6 +406,9 @@ interface TransportEvents {
|
|
|
384
406
|
pause: () => void;
|
|
385
407
|
stop: () => void;
|
|
386
408
|
loop: () => void;
|
|
409
|
+
seek: (event: {
|
|
410
|
+
seconds: number;
|
|
411
|
+
}) => void;
|
|
387
412
|
tempochange: (event: TempoChangeEventData) => void;
|
|
388
413
|
meterchange: (event: MeterChangeEventData) => void;
|
|
389
414
|
countIn: (event: CountInEventData) => void;
|
|
@@ -448,13 +473,21 @@ declare class Transport {
|
|
|
448
473
|
setLoopSeconds(enabled: boolean, startSec: number, endSec: number): void;
|
|
449
474
|
/** Convenience — sets loop in samples */
|
|
450
475
|
setLoopSamples(enabled: boolean, startSample: Sample, endSample: Sample): void;
|
|
451
|
-
|
|
476
|
+
/** Returns true when the tempo was applied, false when a defaulted (no
|
|
477
|
+
* atTick) write was refused because the tempo map has multiple entries —
|
|
478
|
+
* pass an explicit atTick to modify a multi-entry map (#407). */
|
|
479
|
+
setTempo(bpm: number, atTick?: Tick, options?: SetTempoOptions): boolean;
|
|
452
480
|
getTempo(atTick?: Tick): number;
|
|
453
481
|
setMeter(numerator: number, denominator: number, atTick?: Tick): void;
|
|
454
482
|
getMeter(atTick?: Tick): MeterSignature;
|
|
455
483
|
removeMeter(atTick: Tick): void;
|
|
456
484
|
clearMeters(): void;
|
|
457
485
|
clearTempos(): void;
|
|
486
|
+
/** Remove the tempo entry at exactly `atTick` (tick 0 is permanent — a
|
|
487
|
+
* no-op, like removeMeter's initial entry). Mirrors setTempo's loop-cache
|
|
488
|
+
* invalidation and event; the emitted bpm is the tempo now in force at
|
|
489
|
+
* the removed position. */
|
|
490
|
+
removeTempo(atTick: Tick): void;
|
|
458
491
|
barToTick(bar: number): Tick;
|
|
459
492
|
tickToBar(tick: Tick): number;
|
|
460
493
|
/** Convert transport time (seconds) to tick position, using the tempo map. */
|
|
@@ -475,6 +508,16 @@ declare class Transport {
|
|
|
475
508
|
/** The master output AudioNode. Connect your own nodes (analyzers, recorders, etc.)
|
|
476
509
|
* in parallel. The transport already routes this to audioContext.destination. */
|
|
477
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;
|
|
478
521
|
on<K extends TransportEventType>(event: K, cb: TransportEvents[K]): void;
|
|
479
522
|
off<K extends TransportEventType>(event: K, cb: TransportEvents[K]): void;
|
|
480
523
|
dispose(): void;
|
|
@@ -519,7 +562,7 @@ declare class NativePlayoutAdapter implements PlayoutAdapter {
|
|
|
519
562
|
setCountInMode(mode: CountInMode): void;
|
|
520
563
|
setRecording(recording: boolean): void;
|
|
521
564
|
isCountingIn(): boolean;
|
|
522
|
-
setTempo(bpm: number, atTick?: number):
|
|
565
|
+
setTempo(bpm: number, atTick?: number): boolean;
|
|
523
566
|
setMeter(numerator: number, denominator: number, atTick?: number): void;
|
|
524
567
|
ticksToSeconds(tick: number): number;
|
|
525
568
|
secondsToTicks(seconds: number): number;
|
package/dist/index.js
CHANGED
|
@@ -249,7 +249,7 @@ var SampleTimeline = class {
|
|
|
249
249
|
"[waveform-playlist] SampleTimeline: tempoMap not set \u2014 call setTempoMap() first"
|
|
250
250
|
);
|
|
251
251
|
}
|
|
252
|
-
return this._tempoMap.secondsToTicks(samples / this._sampleRate);
|
|
252
|
+
return Math.round(this._tempoMap.secondsToTicks(samples / this._sampleRate));
|
|
253
253
|
}
|
|
254
254
|
};
|
|
255
255
|
|
|
@@ -280,6 +280,11 @@ var TempoMap = class _TempoMap {
|
|
|
280
280
|
getTempo(atTick = 0) {
|
|
281
281
|
return this._getTempoAt(atTick);
|
|
282
282
|
}
|
|
283
|
+
/** Number of tempo entries in the map. Always >= 1 — the tick-0 entry is
|
|
284
|
+
* permanent. Used by Transport.setTempo to detect multi-entry maps. */
|
|
285
|
+
get entryCount() {
|
|
286
|
+
return this._entries.length;
|
|
287
|
+
}
|
|
283
288
|
setTempo(bpm, atTick = 0, options) {
|
|
284
289
|
_TempoMap._validateBpm(bpm);
|
|
285
290
|
const interpolation = options?.interpolation ?? "step";
|
|
@@ -358,6 +363,18 @@ var TempoMap = class _TempoMap {
|
|
|
358
363
|
const first = this._entries[0];
|
|
359
364
|
this._entries = [{ tick: 0, bpm: first.bpm, interpolation: "step", secondsAtTick: 0 }];
|
|
360
365
|
}
|
|
366
|
+
/** Remove the tempo entry at exactly `atTick`, if one exists. The tick-0
|
|
367
|
+
* entry is permanent (a map must always have a tempo) — removing it is a
|
|
368
|
+
* no-op, matching removeMeter's treatment of the initial meter. The
|
|
369
|
+
* seconds cache is recomputed from the removal point, the same partial
|
|
370
|
+
* update setTempo uses. */
|
|
371
|
+
removeTempo(atTick) {
|
|
372
|
+
if (atTick === 0) return;
|
|
373
|
+
const i = this._entries.findIndex((e) => e.tick === atTick);
|
|
374
|
+
if (i === -1) return;
|
|
375
|
+
this._entries = [...this._entries.slice(0, i), ...this._entries.slice(i + 1)];
|
|
376
|
+
this._recomputeCache(i);
|
|
377
|
+
}
|
|
361
378
|
/** Get the interpolated BPM at a tick position */
|
|
362
379
|
_getTempoAt(atTick) {
|
|
363
380
|
const entryIndex = this._entryIndexAt(atTick);
|
|
@@ -683,12 +700,19 @@ var MeterMap = class {
|
|
|
683
700
|
"[waveform-playlist] MeterMap: denominator must be a power of 2 (1-32), got " + denominator
|
|
684
701
|
);
|
|
685
702
|
}
|
|
703
|
+
if (!Number.isInteger(this._ppqn * 4 / denominator)) {
|
|
704
|
+
throw new Error(
|
|
705
|
+
"[waveform-playlist] MeterMap: ppqn (" + this._ppqn + ") * 4 is not divisible by denominator (" + denominator + ") \u2014 bar boundaries would fall on fractional ticks"
|
|
706
|
+
);
|
|
707
|
+
}
|
|
686
708
|
}
|
|
687
709
|
};
|
|
688
710
|
|
|
689
711
|
// src/audio/master-node.ts
|
|
690
712
|
var MasterNode = class {
|
|
691
713
|
constructor(audioContext) {
|
|
714
|
+
this._destination = null;
|
|
715
|
+
this._effectsInput = null;
|
|
692
716
|
this._gainNode = audioContext.createGain();
|
|
693
717
|
}
|
|
694
718
|
get input() {
|
|
@@ -697,6 +721,37 @@ var MasterNode = class {
|
|
|
697
721
|
get output() {
|
|
698
722
|
return this._gainNode;
|
|
699
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
|
+
}
|
|
700
755
|
setVolume(value) {
|
|
701
756
|
this._gainNode.gain.value = value;
|
|
702
757
|
}
|
|
@@ -706,6 +761,8 @@ var MasterNode = class {
|
|
|
706
761
|
} catch (err) {
|
|
707
762
|
console.warn("[waveform-playlist] MasterNode.dispose: error disconnecting: " + String(err));
|
|
708
763
|
}
|
|
764
|
+
this._destination = null;
|
|
765
|
+
this._effectsInput = null;
|
|
709
766
|
}
|
|
710
767
|
};
|
|
711
768
|
|
|
@@ -1356,6 +1413,7 @@ var _Transport = class _Transport {
|
|
|
1356
1413
|
this._clipPlayer.onPositionJump(seekTick);
|
|
1357
1414
|
this._timer.start();
|
|
1358
1415
|
}
|
|
1416
|
+
this._emit("seek", { seconds: time });
|
|
1359
1417
|
}
|
|
1360
1418
|
getCurrentTime() {
|
|
1361
1419
|
if (this._countingIn) {
|
|
@@ -1526,12 +1584,22 @@ var _Transport = class _Transport {
|
|
|
1526
1584
|
this._emit("loop");
|
|
1527
1585
|
}
|
|
1528
1586
|
// --- Tempo ---
|
|
1587
|
+
/** Returns true when the tempo was applied, false when a defaulted (no
|
|
1588
|
+
* atTick) write was refused because the tempo map has multiple entries —
|
|
1589
|
+
* pass an explicit atTick to modify a multi-entry map (#407). */
|
|
1529
1590
|
setTempo(bpm, atTick, options) {
|
|
1591
|
+
if (atTick === void 0 && this._tempoMap.entryCount > 1) {
|
|
1592
|
+
console.warn(
|
|
1593
|
+
"[waveform-playlist] Transport.setTempo: refusing defaulted tick-0 write of " + bpm + " BPM \u2014 the tempo map has " + this._tempoMap.entryCount + " entries. Pass an explicit atTick to modify a multi-entry tempo map."
|
|
1594
|
+
);
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1530
1597
|
this._tempoMap.setTempo(bpm, atTick, options);
|
|
1531
1598
|
if (this._loopEnabled) {
|
|
1532
1599
|
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1533
1600
|
}
|
|
1534
1601
|
this._emit("tempochange", { bpm, atTick: atTick ?? 0 });
|
|
1602
|
+
return true;
|
|
1535
1603
|
}
|
|
1536
1604
|
getTempo(atTick) {
|
|
1537
1605
|
return this._tempoMap.getTempo(atTick);
|
|
@@ -1569,6 +1637,17 @@ var _Transport = class _Transport {
|
|
|
1569
1637
|
}
|
|
1570
1638
|
this._emit("tempochange", { bpm: this._tempoMap.getTempo(), atTick: 0 });
|
|
1571
1639
|
}
|
|
1640
|
+
/** Remove the tempo entry at exactly `atTick` (tick 0 is permanent — a
|
|
1641
|
+
* no-op, like removeMeter's initial entry). Mirrors setTempo's loop-cache
|
|
1642
|
+
* invalidation and event; the emitted bpm is the tempo now in force at
|
|
1643
|
+
* the removed position. */
|
|
1644
|
+
removeTempo(atTick) {
|
|
1645
|
+
this._tempoMap.removeTempo(atTick);
|
|
1646
|
+
if (this._loopEnabled) {
|
|
1647
|
+
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1648
|
+
}
|
|
1649
|
+
this._emit("tempochange", { bpm: this._tempoMap.getTempo(atTick), atTick });
|
|
1650
|
+
}
|
|
1572
1651
|
barToTick(bar) {
|
|
1573
1652
|
return this._meterMap.barToTick(bar);
|
|
1574
1653
|
}
|
|
@@ -1644,6 +1723,20 @@ var _Transport = class _Transport {
|
|
|
1644
1723
|
get masterOutputNode() {
|
|
1645
1724
|
return this._masterNode.output;
|
|
1646
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
|
+
}
|
|
1647
1740
|
// --- Events ---
|
|
1648
1741
|
on(event, cb) {
|
|
1649
1742
|
if (!this._listeners.has(event)) {
|
|
@@ -1697,7 +1790,7 @@ var _Transport = class _Transport {
|
|
|
1697
1790
|
}
|
|
1698
1791
|
_initAudioGraph(audioContext) {
|
|
1699
1792
|
this._masterNode = new MasterNode(audioContext);
|
|
1700
|
-
this._masterNode.
|
|
1793
|
+
this._masterNode.connectOutput(audioContext.destination);
|
|
1701
1794
|
const toAudioTime = (transportTime) => this._clock.toAudioTime(transportTime);
|
|
1702
1795
|
this._clipPlayer = new ClipPlayer(
|
|
1703
1796
|
audioContext,
|
|
@@ -1941,7 +2034,7 @@ var NativePlayoutAdapter = class {
|
|
|
1941
2034
|
return this._transport.isCountingIn();
|
|
1942
2035
|
}
|
|
1943
2036
|
setTempo(bpm, atTick) {
|
|
1944
|
-
this._transport.setTempo(bpm, atTick !== void 0 ? atTick : void 0);
|
|
2037
|
+
return this._transport.setTempo(bpm, atTick !== void 0 ? atTick : void 0);
|
|
1945
2038
|
}
|
|
1946
2039
|
setMeter(numerator, denominator, atTick) {
|
|
1947
2040
|
this._transport.setMeter(
|