@waveform-playlist/media-element-playout 12.1.0 → 12.2.0
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 +32 -1
- package/dist/index.d.mts +94 -1
- package/dist/index.d.ts +94 -1
- package/dist/index.js +164 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +164 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ playout.play(0); // Play from beginning
|
|
|
56
56
|
playout.setPlaybackRate(0.75); // Slow down to 75% speed (pitch preserved)
|
|
57
57
|
playout.pause();
|
|
58
58
|
playout.seekTo(30); // Seek to 30 seconds
|
|
59
|
-
playout.
|
|
59
|
+
playout.resume(); // Resume from the current position (does NOT reset to 0)
|
|
60
60
|
|
|
61
61
|
// Clean up
|
|
62
62
|
playout.dispose();
|
|
@@ -79,16 +79,22 @@ class MediaElementPlayout {
|
|
|
79
79
|
|
|
80
80
|
// Track management
|
|
81
81
|
addTrack(options: MediaElementTrackOptions): MediaElementTrack;
|
|
82
|
+
setSource(options: MediaElementTrackOptions): MediaElementTrack; // silent in-place replace
|
|
82
83
|
removeTrack(trackId: string): void;
|
|
83
84
|
getTrack(trackId: string): MediaElementTrack | undefined;
|
|
84
85
|
|
|
85
86
|
// Playback
|
|
86
87
|
play(when?: number, offset?: number, duration?: number): void;
|
|
88
|
+
resume(): void; // resume from current position (no reset to 0)
|
|
87
89
|
pause(): void;
|
|
88
90
|
stop(): void;
|
|
89
91
|
seekTo(time: number): void;
|
|
90
92
|
getCurrentTime(): number;
|
|
91
93
|
|
|
94
|
+
// Lifecycle events (typed via MediaElementTrackEvents)
|
|
95
|
+
on<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
96
|
+
off<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
97
|
+
|
|
92
98
|
// Volume & Rate
|
|
93
99
|
setMasterVolume(volume: number): void;
|
|
94
100
|
setPlaybackRate(rate: number): void; // 0.5 to 2.0, pitch preserved
|
|
@@ -113,6 +119,31 @@ interface MediaElementTrackOptions {
|
|
|
113
119
|
}
|
|
114
120
|
```
|
|
115
121
|
|
|
122
|
+
## Player Mode
|
|
123
|
+
|
|
124
|
+
Beyond the timeline/editor API, three affordances make this engine pleasant to
|
|
125
|
+
reuse as a single-track **player** (podcast/audiobook players, `<daw-player>`):
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// Resume from the current position (play() with no offset resets to 0)
|
|
129
|
+
playout.resume();
|
|
130
|
+
|
|
131
|
+
// Swap to the next source in place — no "Only one track is supported" warning,
|
|
132
|
+
// and any Web Audio routing/effects are preserved across the swap
|
|
133
|
+
playout.setSource({ source: '/audio/episode-2.mp3', name: 'Episode 2' });
|
|
134
|
+
|
|
135
|
+
// Observe media lifecycle without reaching into the audio element
|
|
136
|
+
playout.on('loadedmetadata', () => console.log('duration:', playout.duration));
|
|
137
|
+
playout.on('play', () => updateTransportUI('playing'));
|
|
138
|
+
playout.on('pause', () => updateTransportUI('paused'));
|
|
139
|
+
playout.on('error', (err) => surfaceError(err));
|
|
140
|
+
playout.off('play', handler); // unsubscribe
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`on()` listeners are retained across `setSource()` swaps — register them once.
|
|
144
|
+
The same `on()/off()` and `resume()`/`load()` methods exist on `MediaElementTrack`
|
|
145
|
+
for power users. Event names and payloads are typed via `MediaElementTrackEvents`.
|
|
146
|
+
|
|
116
147
|
## Generating Peaks
|
|
117
148
|
|
|
118
149
|
Use [audiowaveform](https://github.com/bbc/audiowaveform) or [waveform-data.js](https://github.com/bbc/waveform-data.js) to pre-compute peaks:
|
package/dist/index.d.mts
CHANGED
|
@@ -34,6 +34,25 @@ interface MediaElementTrackOptions {
|
|
|
34
34
|
/** Fade out configuration (requires audioContext) */
|
|
35
35
|
fadeOut?: FadeConfig;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Typed event map for MediaElementTrack's emitter. Mirrors the on()/off()
|
|
39
|
+
* pattern used by @waveform-playlist/engine's PlaylistEngine so consumers and
|
|
40
|
+
* the dawcore web-components layer can wire events uniformly across engines.
|
|
41
|
+
*/
|
|
42
|
+
interface MediaElementTrackEvents {
|
|
43
|
+
/** Fired when the element's metadata (duration, dimensions) has loaded. */
|
|
44
|
+
loadedmetadata: () => void;
|
|
45
|
+
/** Fired when native playback starts/resumes. */
|
|
46
|
+
play: () => void;
|
|
47
|
+
/** Fired when native playback pauses (including at end-of-media). */
|
|
48
|
+
pause: () => void;
|
|
49
|
+
/** Fired on a media error; carries the element's MediaError (or null). */
|
|
50
|
+
error: (err: MediaError | null) => void;
|
|
51
|
+
/** Fired when playback reaches the end of the media. */
|
|
52
|
+
ended: () => void;
|
|
53
|
+
/** Fired on each native timeupdate; carries the current time in seconds. */
|
|
54
|
+
timeupdate: (time: number) => void;
|
|
55
|
+
}
|
|
37
56
|
/**
|
|
38
57
|
* Single-track playback using HTMLAudioElement.
|
|
39
58
|
*
|
|
@@ -57,6 +76,7 @@ declare class MediaElementTrack {
|
|
|
57
76
|
private _volume;
|
|
58
77
|
private onStopCallback?;
|
|
59
78
|
private onTimeUpdateCallback?;
|
|
79
|
+
private _listeners;
|
|
60
80
|
private _audioContext;
|
|
61
81
|
private _sourceNode;
|
|
62
82
|
private _fadeGain;
|
|
@@ -66,6 +86,10 @@ declare class MediaElementTrack {
|
|
|
66
86
|
constructor(options: MediaElementTrackOptions);
|
|
67
87
|
private handleEnded;
|
|
68
88
|
private handleTimeUpdate;
|
|
89
|
+
private handleLoadedMetadata;
|
|
90
|
+
private handlePlay;
|
|
91
|
+
private handlePause;
|
|
92
|
+
private handleError;
|
|
69
93
|
/**
|
|
70
94
|
* Schedule fade automation on the fade GainNode.
|
|
71
95
|
* Called at the start of each play() — fades are relative to the playback offset.
|
|
@@ -81,6 +105,29 @@ declare class MediaElementTrack {
|
|
|
81
105
|
* (fades depend on audioContext.currentTime being non-zero).
|
|
82
106
|
*/
|
|
83
107
|
play(offset?: number): void;
|
|
108
|
+
/**
|
|
109
|
+
* Resume playback from the current position without resetting currentTime.
|
|
110
|
+
* Reuses play()'s fade re-scheduling and AudioContext-resume machinery —
|
|
111
|
+
* passing the current position as the offset is a no-op seek that leaves
|
|
112
|
+
* playback where it was.
|
|
113
|
+
*/
|
|
114
|
+
resume(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Swap the audio source in place, reusing the existing <audio> element.
|
|
117
|
+
* Because the MediaElementAudioSourceNode is once-per-element, reusing the
|
|
118
|
+
* element preserves any Web Audio routing/effects across the swap.
|
|
119
|
+
*
|
|
120
|
+
* Only supported when this track owns its element (constructed from a URL
|
|
121
|
+
* string). A borrowed element (constructed from an HTMLAudioElement) warns
|
|
122
|
+
* and no-ops — swapping a consumer-owned element's src is out of contract.
|
|
123
|
+
*
|
|
124
|
+
* Peaks are coupled to the specific audio, so they are replaced (defaulting
|
|
125
|
+
* to null when omitted). Name is a label, so it updates only when provided.
|
|
126
|
+
*/
|
|
127
|
+
load(source: string, opts?: {
|
|
128
|
+
peaks?: WaveformDataObject;
|
|
129
|
+
name?: string;
|
|
130
|
+
}): void;
|
|
84
131
|
/**
|
|
85
132
|
* Pause playback
|
|
86
133
|
*/
|
|
@@ -132,6 +179,16 @@ declare class MediaElementTrack {
|
|
|
132
179
|
* Disconnect the output and reconnect to the default AudioContext destination.
|
|
133
180
|
*/
|
|
134
181
|
disconnectOutput(): void;
|
|
182
|
+
/**
|
|
183
|
+
* Subscribe to a track lifecycle event. Multiple listeners per event are
|
|
184
|
+
* supported. Mirrors PlaylistEngine's on()/off() emitter.
|
|
185
|
+
*/
|
|
186
|
+
on<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
187
|
+
/**
|
|
188
|
+
* Unsubscribe a previously registered listener.
|
|
189
|
+
*/
|
|
190
|
+
off<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
191
|
+
private _emit;
|
|
135
192
|
/**
|
|
136
193
|
* Clean up resources
|
|
137
194
|
*/
|
|
@@ -189,6 +246,8 @@ declare class MediaElementPlayout {
|
|
|
189
246
|
private _preservesPitch;
|
|
190
247
|
private _isPlaying;
|
|
191
248
|
private onPlaybackCompleteCallback?;
|
|
249
|
+
/** Consumer event listeners, retained so they re-attach across track swaps. */
|
|
250
|
+
private _eventListeners;
|
|
192
251
|
constructor(options?: MediaElementPlayoutOptions);
|
|
193
252
|
/**
|
|
194
253
|
* Initialize the playout engine.
|
|
@@ -202,6 +261,16 @@ declare class MediaElementPlayout {
|
|
|
202
261
|
* Note: Only one track is supported. Adding a second track will dispose the first.
|
|
203
262
|
*/
|
|
204
263
|
addTrack(options: MediaElementTrackOptions): MediaElementTrack;
|
|
264
|
+
/**
|
|
265
|
+
* Replace the playout's source (player-mode affordance). The documented
|
|
266
|
+
* single-track replace path — does NOT warn like addTrack().
|
|
267
|
+
*
|
|
268
|
+
* For URL (string) sources with an existing track, swaps in place via
|
|
269
|
+
* track.load(), reusing the element and preserving Web Audio routing. For a
|
|
270
|
+
* provided HTMLAudioElement source, or when there is no track yet, (re)creates
|
|
271
|
+
* the track. Returns the active track.
|
|
272
|
+
*/
|
|
273
|
+
setSource(options: MediaElementTrackOptions): MediaElementTrack;
|
|
205
274
|
/**
|
|
206
275
|
* Remove a track by ID.
|
|
207
276
|
*/
|
|
@@ -217,6 +286,13 @@ declare class MediaElementPlayout {
|
|
|
217
286
|
* @param duration - Duration to play in seconds (optional)
|
|
218
287
|
*/
|
|
219
288
|
play(_when?: number, offset?: number, duration?: number): void;
|
|
289
|
+
/**
|
|
290
|
+
* Resume playback from the current position (player-mode affordance).
|
|
291
|
+
* Unlike play() with no offset (which resets to 0), this keeps currentTime.
|
|
292
|
+
* Delegates to play() with the current position as the offset, so all of
|
|
293
|
+
* play()'s machinery (AudioContext resume, fades, _isPlaying) is reused.
|
|
294
|
+
*/
|
|
295
|
+
resume(): void;
|
|
220
296
|
/**
|
|
221
297
|
* Pause playback.
|
|
222
298
|
*/
|
|
@@ -254,6 +330,23 @@ declare class MediaElementPlayout {
|
|
|
254
330
|
* Set callback for when playback completes.
|
|
255
331
|
*/
|
|
256
332
|
setOnPlaybackComplete(callback: () => void): void;
|
|
333
|
+
/**
|
|
334
|
+
* Subscribe to a lifecycle event (loadedmetadata / play / pause / error /
|
|
335
|
+
* ended / timeupdate) without reaching into track.element. Listeners are
|
|
336
|
+
* retained and re-attached automatically when the source is swapped.
|
|
337
|
+
*/
|
|
338
|
+
on<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
339
|
+
/**
|
|
340
|
+
* Unsubscribe a previously registered lifecycle listener.
|
|
341
|
+
*/
|
|
342
|
+
off<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
343
|
+
/**
|
|
344
|
+
* Attach every registered listener to the current track. Called after a new
|
|
345
|
+
* track is created so subscriptions survive source swaps. The cast is safe:
|
|
346
|
+
* the event→listener correlation was enforced by the typed on() that filled
|
|
347
|
+
* the registry; TS cannot track it through this loop.
|
|
348
|
+
*/
|
|
349
|
+
private _attachListenersToTrack;
|
|
257
350
|
/**
|
|
258
351
|
* Clean up resources.
|
|
259
352
|
*/
|
|
@@ -307,4 +400,4 @@ interface PlaybackRateEngine extends PlayoutEngine {
|
|
|
307
400
|
*/
|
|
308
401
|
declare function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine;
|
|
309
402
|
|
|
310
|
-
export { MediaElementPlayout, type MediaElementPlayoutOptions, MediaElementTrack, type MediaElementTrackOptions, type PlaybackRateEngine, type PlayoutEngine, supportsPlaybackRate };
|
|
403
|
+
export { MediaElementPlayout, type MediaElementPlayoutOptions, MediaElementTrack, type MediaElementTrackEvents, type MediaElementTrackOptions, type PlaybackRateEngine, type PlayoutEngine, supportsPlaybackRate };
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,25 @@ interface MediaElementTrackOptions {
|
|
|
34
34
|
/** Fade out configuration (requires audioContext) */
|
|
35
35
|
fadeOut?: FadeConfig;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Typed event map for MediaElementTrack's emitter. Mirrors the on()/off()
|
|
39
|
+
* pattern used by @waveform-playlist/engine's PlaylistEngine so consumers and
|
|
40
|
+
* the dawcore web-components layer can wire events uniformly across engines.
|
|
41
|
+
*/
|
|
42
|
+
interface MediaElementTrackEvents {
|
|
43
|
+
/** Fired when the element's metadata (duration, dimensions) has loaded. */
|
|
44
|
+
loadedmetadata: () => void;
|
|
45
|
+
/** Fired when native playback starts/resumes. */
|
|
46
|
+
play: () => void;
|
|
47
|
+
/** Fired when native playback pauses (including at end-of-media). */
|
|
48
|
+
pause: () => void;
|
|
49
|
+
/** Fired on a media error; carries the element's MediaError (or null). */
|
|
50
|
+
error: (err: MediaError | null) => void;
|
|
51
|
+
/** Fired when playback reaches the end of the media. */
|
|
52
|
+
ended: () => void;
|
|
53
|
+
/** Fired on each native timeupdate; carries the current time in seconds. */
|
|
54
|
+
timeupdate: (time: number) => void;
|
|
55
|
+
}
|
|
37
56
|
/**
|
|
38
57
|
* Single-track playback using HTMLAudioElement.
|
|
39
58
|
*
|
|
@@ -57,6 +76,7 @@ declare class MediaElementTrack {
|
|
|
57
76
|
private _volume;
|
|
58
77
|
private onStopCallback?;
|
|
59
78
|
private onTimeUpdateCallback?;
|
|
79
|
+
private _listeners;
|
|
60
80
|
private _audioContext;
|
|
61
81
|
private _sourceNode;
|
|
62
82
|
private _fadeGain;
|
|
@@ -66,6 +86,10 @@ declare class MediaElementTrack {
|
|
|
66
86
|
constructor(options: MediaElementTrackOptions);
|
|
67
87
|
private handleEnded;
|
|
68
88
|
private handleTimeUpdate;
|
|
89
|
+
private handleLoadedMetadata;
|
|
90
|
+
private handlePlay;
|
|
91
|
+
private handlePause;
|
|
92
|
+
private handleError;
|
|
69
93
|
/**
|
|
70
94
|
* Schedule fade automation on the fade GainNode.
|
|
71
95
|
* Called at the start of each play() — fades are relative to the playback offset.
|
|
@@ -81,6 +105,29 @@ declare class MediaElementTrack {
|
|
|
81
105
|
* (fades depend on audioContext.currentTime being non-zero).
|
|
82
106
|
*/
|
|
83
107
|
play(offset?: number): void;
|
|
108
|
+
/**
|
|
109
|
+
* Resume playback from the current position without resetting currentTime.
|
|
110
|
+
* Reuses play()'s fade re-scheduling and AudioContext-resume machinery —
|
|
111
|
+
* passing the current position as the offset is a no-op seek that leaves
|
|
112
|
+
* playback where it was.
|
|
113
|
+
*/
|
|
114
|
+
resume(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Swap the audio source in place, reusing the existing <audio> element.
|
|
117
|
+
* Because the MediaElementAudioSourceNode is once-per-element, reusing the
|
|
118
|
+
* element preserves any Web Audio routing/effects across the swap.
|
|
119
|
+
*
|
|
120
|
+
* Only supported when this track owns its element (constructed from a URL
|
|
121
|
+
* string). A borrowed element (constructed from an HTMLAudioElement) warns
|
|
122
|
+
* and no-ops — swapping a consumer-owned element's src is out of contract.
|
|
123
|
+
*
|
|
124
|
+
* Peaks are coupled to the specific audio, so they are replaced (defaulting
|
|
125
|
+
* to null when omitted). Name is a label, so it updates only when provided.
|
|
126
|
+
*/
|
|
127
|
+
load(source: string, opts?: {
|
|
128
|
+
peaks?: WaveformDataObject;
|
|
129
|
+
name?: string;
|
|
130
|
+
}): void;
|
|
84
131
|
/**
|
|
85
132
|
* Pause playback
|
|
86
133
|
*/
|
|
@@ -132,6 +179,16 @@ declare class MediaElementTrack {
|
|
|
132
179
|
* Disconnect the output and reconnect to the default AudioContext destination.
|
|
133
180
|
*/
|
|
134
181
|
disconnectOutput(): void;
|
|
182
|
+
/**
|
|
183
|
+
* Subscribe to a track lifecycle event. Multiple listeners per event are
|
|
184
|
+
* supported. Mirrors PlaylistEngine's on()/off() emitter.
|
|
185
|
+
*/
|
|
186
|
+
on<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
187
|
+
/**
|
|
188
|
+
* Unsubscribe a previously registered listener.
|
|
189
|
+
*/
|
|
190
|
+
off<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
191
|
+
private _emit;
|
|
135
192
|
/**
|
|
136
193
|
* Clean up resources
|
|
137
194
|
*/
|
|
@@ -189,6 +246,8 @@ declare class MediaElementPlayout {
|
|
|
189
246
|
private _preservesPitch;
|
|
190
247
|
private _isPlaying;
|
|
191
248
|
private onPlaybackCompleteCallback?;
|
|
249
|
+
/** Consumer event listeners, retained so they re-attach across track swaps. */
|
|
250
|
+
private _eventListeners;
|
|
192
251
|
constructor(options?: MediaElementPlayoutOptions);
|
|
193
252
|
/**
|
|
194
253
|
* Initialize the playout engine.
|
|
@@ -202,6 +261,16 @@ declare class MediaElementPlayout {
|
|
|
202
261
|
* Note: Only one track is supported. Adding a second track will dispose the first.
|
|
203
262
|
*/
|
|
204
263
|
addTrack(options: MediaElementTrackOptions): MediaElementTrack;
|
|
264
|
+
/**
|
|
265
|
+
* Replace the playout's source (player-mode affordance). The documented
|
|
266
|
+
* single-track replace path — does NOT warn like addTrack().
|
|
267
|
+
*
|
|
268
|
+
* For URL (string) sources with an existing track, swaps in place via
|
|
269
|
+
* track.load(), reusing the element and preserving Web Audio routing. For a
|
|
270
|
+
* provided HTMLAudioElement source, or when there is no track yet, (re)creates
|
|
271
|
+
* the track. Returns the active track.
|
|
272
|
+
*/
|
|
273
|
+
setSource(options: MediaElementTrackOptions): MediaElementTrack;
|
|
205
274
|
/**
|
|
206
275
|
* Remove a track by ID.
|
|
207
276
|
*/
|
|
@@ -217,6 +286,13 @@ declare class MediaElementPlayout {
|
|
|
217
286
|
* @param duration - Duration to play in seconds (optional)
|
|
218
287
|
*/
|
|
219
288
|
play(_when?: number, offset?: number, duration?: number): void;
|
|
289
|
+
/**
|
|
290
|
+
* Resume playback from the current position (player-mode affordance).
|
|
291
|
+
* Unlike play() with no offset (which resets to 0), this keeps currentTime.
|
|
292
|
+
* Delegates to play() with the current position as the offset, so all of
|
|
293
|
+
* play()'s machinery (AudioContext resume, fades, _isPlaying) is reused.
|
|
294
|
+
*/
|
|
295
|
+
resume(): void;
|
|
220
296
|
/**
|
|
221
297
|
* Pause playback.
|
|
222
298
|
*/
|
|
@@ -254,6 +330,23 @@ declare class MediaElementPlayout {
|
|
|
254
330
|
* Set callback for when playback completes.
|
|
255
331
|
*/
|
|
256
332
|
setOnPlaybackComplete(callback: () => void): void;
|
|
333
|
+
/**
|
|
334
|
+
* Subscribe to a lifecycle event (loadedmetadata / play / pause / error /
|
|
335
|
+
* ended / timeupdate) without reaching into track.element. Listeners are
|
|
336
|
+
* retained and re-attached automatically when the source is swapped.
|
|
337
|
+
*/
|
|
338
|
+
on<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
339
|
+
/**
|
|
340
|
+
* Unsubscribe a previously registered lifecycle listener.
|
|
341
|
+
*/
|
|
342
|
+
off<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
343
|
+
/**
|
|
344
|
+
* Attach every registered listener to the current track. Called after a new
|
|
345
|
+
* track is created so subscriptions survive source swaps. The cast is safe:
|
|
346
|
+
* the event→listener correlation was enforced by the typed on() that filled
|
|
347
|
+
* the registry; TS cannot track it through this loop.
|
|
348
|
+
*/
|
|
349
|
+
private _attachListenersToTrack;
|
|
257
350
|
/**
|
|
258
351
|
* Clean up resources.
|
|
259
352
|
*/
|
|
@@ -307,4 +400,4 @@ interface PlaybackRateEngine extends PlayoutEngine {
|
|
|
307
400
|
*/
|
|
308
401
|
declare function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine;
|
|
309
402
|
|
|
310
|
-
export { MediaElementPlayout, type MediaElementPlayoutOptions, MediaElementTrack, type MediaElementTrackOptions, type PlaybackRateEngine, type PlayoutEngine, supportsPlaybackRate };
|
|
403
|
+
export { MediaElementPlayout, type MediaElementPlayoutOptions, MediaElementTrack, type MediaElementTrackEvents, type MediaElementTrackOptions, type PlaybackRateEngine, type PlayoutEngine, supportsPlaybackRate };
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ var import_core = require("@waveform-playlist/core");
|
|
|
31
31
|
var MediaElementTrack = class {
|
|
32
32
|
constructor(options) {
|
|
33
33
|
this._playbackRate = 1;
|
|
34
|
+
this._listeners = /* @__PURE__ */ new Map();
|
|
34
35
|
// Web Audio nodes (only when audioContext is provided)
|
|
35
36
|
this._audioContext = null;
|
|
36
37
|
this._sourceNode = null;
|
|
@@ -41,11 +42,25 @@ var MediaElementTrack = class {
|
|
|
41
42
|
if (this.onStopCallback) {
|
|
42
43
|
this.onStopCallback();
|
|
43
44
|
}
|
|
45
|
+
this._emit("ended");
|
|
44
46
|
};
|
|
45
47
|
this.handleTimeUpdate = () => {
|
|
46
48
|
if (this.onTimeUpdateCallback) {
|
|
47
49
|
this.onTimeUpdateCallback(this.audioElement.currentTime);
|
|
48
50
|
}
|
|
51
|
+
this._emit("timeupdate", this.audioElement.currentTime);
|
|
52
|
+
};
|
|
53
|
+
this.handleLoadedMetadata = () => {
|
|
54
|
+
this._emit("loadedmetadata");
|
|
55
|
+
};
|
|
56
|
+
this.handlePlay = () => {
|
|
57
|
+
this._emit("play");
|
|
58
|
+
};
|
|
59
|
+
this.handlePause = () => {
|
|
60
|
+
this._emit("pause");
|
|
61
|
+
};
|
|
62
|
+
this.handleError = () => {
|
|
63
|
+
this._emit("error", this.audioElement.error);
|
|
49
64
|
};
|
|
50
65
|
this._peaks = options.peaks ?? null;
|
|
51
66
|
this._id = options.id ?? `track-${Date.now()}`;
|
|
@@ -93,6 +108,10 @@ var MediaElementTrack = class {
|
|
|
93
108
|
}
|
|
94
109
|
this.audioElement.addEventListener("ended", this.handleEnded);
|
|
95
110
|
this.audioElement.addEventListener("timeupdate", this.handleTimeUpdate);
|
|
111
|
+
this.audioElement.addEventListener("loadedmetadata", this.handleLoadedMetadata);
|
|
112
|
+
this.audioElement.addEventListener("play", this.handlePlay);
|
|
113
|
+
this.audioElement.addEventListener("pause", this.handlePause);
|
|
114
|
+
this.audioElement.addEventListener("error", this.handleError);
|
|
96
115
|
}
|
|
97
116
|
/**
|
|
98
117
|
* Schedule fade automation on the fade GainNode.
|
|
@@ -179,6 +198,45 @@ var MediaElementTrack = class {
|
|
|
179
198
|
startPlayback();
|
|
180
199
|
}
|
|
181
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Resume playback from the current position without resetting currentTime.
|
|
203
|
+
* Reuses play()'s fade re-scheduling and AudioContext-resume machinery —
|
|
204
|
+
* passing the current position as the offset is a no-op seek that leaves
|
|
205
|
+
* playback where it was.
|
|
206
|
+
*/
|
|
207
|
+
resume() {
|
|
208
|
+
this.play(this.currentTime);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Swap the audio source in place, reusing the existing <audio> element.
|
|
212
|
+
* Because the MediaElementAudioSourceNode is once-per-element, reusing the
|
|
213
|
+
* element preserves any Web Audio routing/effects across the swap.
|
|
214
|
+
*
|
|
215
|
+
* Only supported when this track owns its element (constructed from a URL
|
|
216
|
+
* string). A borrowed element (constructed from an HTMLAudioElement) warns
|
|
217
|
+
* and no-ops — swapping a consumer-owned element's src is out of contract.
|
|
218
|
+
*
|
|
219
|
+
* Peaks are coupled to the specific audio, so they are replaced (defaulting
|
|
220
|
+
* to null when omitted). Name is a label, so it updates only when provided.
|
|
221
|
+
*/
|
|
222
|
+
load(source, opts = {}) {
|
|
223
|
+
if (!this.ownsElement) {
|
|
224
|
+
console.warn(
|
|
225
|
+
"[waveform-playlist] MediaElementTrack: load() is only supported for tracks that own their audio element (constructed from a URL string). A track constructed from an existing HTMLAudioElement cannot swap its source in place."
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this._cancelFades();
|
|
230
|
+
this.audioElement.pause();
|
|
231
|
+
this.audioElement.src = source;
|
|
232
|
+
this.audioElement.load();
|
|
233
|
+
this.audioElement.currentTime = 0;
|
|
234
|
+
this._peaks = opts.peaks ?? null;
|
|
235
|
+
if (opts.name !== void 0) {
|
|
236
|
+
this._name = opts.name;
|
|
237
|
+
}
|
|
238
|
+
this.audioElement.playbackRate = this._playbackRate;
|
|
239
|
+
}
|
|
182
240
|
/**
|
|
183
241
|
* Pause playback
|
|
184
242
|
*/
|
|
@@ -285,12 +343,47 @@ var MediaElementTrack = class {
|
|
|
285
343
|
}
|
|
286
344
|
this._volumeGain.connect(this._audioContext.destination);
|
|
287
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Subscribe to a track lifecycle event. Multiple listeners per event are
|
|
348
|
+
* supported. Mirrors PlaylistEngine's on()/off() emitter.
|
|
349
|
+
*/
|
|
350
|
+
on(event, listener) {
|
|
351
|
+
if (!this._listeners.has(event)) {
|
|
352
|
+
this._listeners.set(event, /* @__PURE__ */ new Set());
|
|
353
|
+
}
|
|
354
|
+
this._listeners.get(event).add(listener);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Unsubscribe a previously registered listener.
|
|
358
|
+
*/
|
|
359
|
+
off(event, listener) {
|
|
360
|
+
this._listeners.get(event)?.delete(listener);
|
|
361
|
+
}
|
|
362
|
+
_emit(event, ...args) {
|
|
363
|
+
const listeners = this._listeners.get(event);
|
|
364
|
+
if (listeners) {
|
|
365
|
+
for (const listener of listeners) {
|
|
366
|
+
try {
|
|
367
|
+
listener(...args);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.warn(
|
|
370
|
+
"[waveform-playlist] MediaElementTrack: error in event listener: " + String(error)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
288
376
|
/**
|
|
289
377
|
* Clean up resources
|
|
290
378
|
*/
|
|
291
379
|
dispose() {
|
|
292
380
|
this.audioElement.removeEventListener("ended", this.handleEnded);
|
|
293
381
|
this.audioElement.removeEventListener("timeupdate", this.handleTimeUpdate);
|
|
382
|
+
this.audioElement.removeEventListener("loadedmetadata", this.handleLoadedMetadata);
|
|
383
|
+
this.audioElement.removeEventListener("play", this.handlePlay);
|
|
384
|
+
this.audioElement.removeEventListener("pause", this.handlePause);
|
|
385
|
+
this.audioElement.removeEventListener("error", this.handleError);
|
|
386
|
+
this._listeners.clear();
|
|
294
387
|
this._cancelFades();
|
|
295
388
|
this.audioElement.pause();
|
|
296
389
|
if (this._sourceNode) {
|
|
@@ -373,6 +466,8 @@ var MediaElementPlayout = class {
|
|
|
373
466
|
constructor(options = {}) {
|
|
374
467
|
this.track = null;
|
|
375
468
|
this._isPlaying = false;
|
|
469
|
+
/** Consumer event listeners, retained so they re-attach across track swaps. */
|
|
470
|
+
this._eventListeners = /* @__PURE__ */ new Map();
|
|
376
471
|
this._masterVolume = options.masterVolume ?? 1;
|
|
377
472
|
this._playbackRate = options.playbackRate ?? 1;
|
|
378
473
|
this._preservesPitch = options.preservesPitch ?? true;
|
|
@@ -408,8 +503,30 @@ var MediaElementPlayout = class {
|
|
|
408
503
|
this.onPlaybackCompleteCallback();
|
|
409
504
|
}
|
|
410
505
|
});
|
|
506
|
+
this._attachListenersToTrack();
|
|
411
507
|
return this.track;
|
|
412
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Replace the playout's source (player-mode affordance). The documented
|
|
511
|
+
* single-track replace path — does NOT warn like addTrack().
|
|
512
|
+
*
|
|
513
|
+
* For URL (string) sources with an existing track, swaps in place via
|
|
514
|
+
* track.load(), reusing the element and preserving Web Audio routing. For a
|
|
515
|
+
* provided HTMLAudioElement source, or when there is no track yet, (re)creates
|
|
516
|
+
* the track. Returns the active track.
|
|
517
|
+
*/
|
|
518
|
+
setSource(options) {
|
|
519
|
+
this._isPlaying = false;
|
|
520
|
+
if (this.track && typeof options.source === "string") {
|
|
521
|
+
this.track.load(options.source, { peaks: options.peaks, name: options.name });
|
|
522
|
+
return this.track;
|
|
523
|
+
}
|
|
524
|
+
if (this.track) {
|
|
525
|
+
this.track.dispose();
|
|
526
|
+
this.track = null;
|
|
527
|
+
}
|
|
528
|
+
return this.addTrack(options);
|
|
529
|
+
}
|
|
413
530
|
/**
|
|
414
531
|
* Remove a track by ID.
|
|
415
532
|
*/
|
|
@@ -454,6 +571,15 @@ var MediaElementPlayout = class {
|
|
|
454
571
|
}, adjustedDuration * 1e3);
|
|
455
572
|
}
|
|
456
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* Resume playback from the current position (player-mode affordance).
|
|
576
|
+
* Unlike play() with no offset (which resets to 0), this keeps currentTime.
|
|
577
|
+
* Delegates to play() with the current position as the offset, so all of
|
|
578
|
+
* play()'s machinery (AudioContext resume, fades, _isPlaying) is reused.
|
|
579
|
+
*/
|
|
580
|
+
resume() {
|
|
581
|
+
this.play(void 0, this.getCurrentTime());
|
|
582
|
+
}
|
|
457
583
|
/**
|
|
458
584
|
* Pause playback.
|
|
459
585
|
*/
|
|
@@ -529,6 +655,43 @@ var MediaElementPlayout = class {
|
|
|
529
655
|
setOnPlaybackComplete(callback) {
|
|
530
656
|
this.onPlaybackCompleteCallback = callback;
|
|
531
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Subscribe to a lifecycle event (loadedmetadata / play / pause / error /
|
|
660
|
+
* ended / timeupdate) without reaching into track.element. Listeners are
|
|
661
|
+
* retained and re-attached automatically when the source is swapped.
|
|
662
|
+
*/
|
|
663
|
+
on(event, listener) {
|
|
664
|
+
if (!this._eventListeners.has(event)) {
|
|
665
|
+
this._eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
666
|
+
}
|
|
667
|
+
this._eventListeners.get(event).add(listener);
|
|
668
|
+
this.track?.on(event, listener);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Unsubscribe a previously registered lifecycle listener.
|
|
672
|
+
*/
|
|
673
|
+
off(event, listener) {
|
|
674
|
+
this._eventListeners.get(event)?.delete(listener);
|
|
675
|
+
this.track?.off(event, listener);
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Attach every registered listener to the current track. Called after a new
|
|
679
|
+
* track is created so subscriptions survive source swaps. The cast is safe:
|
|
680
|
+
* the event→listener correlation was enforced by the typed on() that filled
|
|
681
|
+
* the registry; TS cannot track it through this loop.
|
|
682
|
+
*/
|
|
683
|
+
_attachListenersToTrack() {
|
|
684
|
+
const track = this.track;
|
|
685
|
+
if (!track) return;
|
|
686
|
+
for (const [event, listeners] of this._eventListeners) {
|
|
687
|
+
for (const listener of listeners) {
|
|
688
|
+
track.on(
|
|
689
|
+
event,
|
|
690
|
+
listener
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
532
695
|
/**
|
|
533
696
|
* Clean up resources.
|
|
534
697
|
*/
|
|
@@ -537,6 +700,7 @@ var MediaElementPlayout = class {
|
|
|
537
700
|
this.track.dispose();
|
|
538
701
|
this.track = null;
|
|
539
702
|
}
|
|
703
|
+
this._eventListeners.clear();
|
|
540
704
|
}
|
|
541
705
|
// Getters
|
|
542
706
|
get isPlaying() {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/MediaElementTrack.ts","../src/MediaElementPlayout.ts","../src/types.ts"],"sourcesContent":["export { MediaElementPlayout } from './MediaElementPlayout';\nexport { MediaElementTrack } from './MediaElementTrack';\n\nexport type { MediaElementPlayoutOptions } from './MediaElementPlayout';\nexport type { MediaElementTrackOptions, FadeConfig } from './MediaElementTrack';\n\n// Common interface types\nexport type { PlayoutEngine, PlaybackRateEngine } from './types';\nexport { supportsPlaybackRate } from './types';\n","import type { WaveformDataObject, FadeConfig } from '@waveform-playlist/core';\nimport { applyFadeIn, applyFadeOut, generateCurve } from '@waveform-playlist/core';\n\nexport type { FadeConfig } from '@waveform-playlist/core';\n\n/**\n * Extended HTMLAudioElement with vendor-prefixed preservesPitch properties.\n * `preservesPitch` is standard; the `moz` and `webkit` prefixes are for older browsers.\n */\ninterface VendorPrefixedPitch {\n preservesPitch?: boolean;\n mozPreservesPitch?: boolean;\n webkitPreservesPitch?: boolean;\n}\n\nexport interface MediaElementTrackOptions {\n /** The audio source - can be a URL, Blob URL, or HTMLAudioElement */\n source: string | HTMLAudioElement;\n /** Pre-computed waveform data for visualization. Optional — omit for\n * scrubber-only / headless players that don't render a waveform. */\n peaks?: WaveformDataObject;\n /** Track ID */\n id?: string;\n /** Track name for display */\n name?: string;\n /** Initial volume (0.0 to 1.0) */\n volume?: number;\n /** Initial playback rate (0.5 to 2.0) */\n playbackRate?: number;\n /** Whether to preserve pitch when changing playback rate (default: true) */\n preservesPitch?: boolean;\n /**\n * AudioContext for Web Audio routing.\n * When provided, audio is routed through Web Audio nodes for fades and effects:\n * HTMLAudioElement → MediaElementSourceNode → fadeGain → volumeGain → destination\n *\n * Without this, playback uses HTMLAudioElement directly (no fades or effects).\n *\n * Note: createMediaElementSource() can only be called once per element.\n * Once routed, HTMLAudioElement.volume no longer works — volume is controlled\n * via the Web Audio GainNode instead.\n */\n audioContext?: AudioContext;\n /** Fade in configuration (requires audioContext) */\n fadeIn?: FadeConfig;\n /** Fade out configuration (requires audioContext) */\n fadeOut?: FadeConfig;\n}\n\n/**\n * Single-track playback using HTMLAudioElement.\n *\n * Benefits over AudioBuffer/Tone.js:\n * - Pitch-preserving playback rate (0.5x - 2.0x) via browser's built-in algorithm\n * - No AudioBuffer decoding required (uses pre-computed peaks for visualization)\n * - Simpler, lighter-weight for single-track use cases\n *\n * When an AudioContext is provided:\n * - Audio routes through Web Audio graph for fades and effects\n * - Volume controlled via GainNode (HTMLAudioElement.volume is bypassed)\n * - Output node exposed for connecting external effects chains\n */\nexport class MediaElementTrack {\n private audioElement: HTMLAudioElement;\n private ownsElement: boolean; // Whether we created the element (need to clean up)\n private _peaks: WaveformDataObject | null;\n private _id: string;\n private _name: string;\n private _playbackRate: number = 1;\n private _volume: number;\n private onStopCallback?: () => void;\n private onTimeUpdateCallback?: (time: number) => void;\n\n // Web Audio nodes (only when audioContext is provided)\n private _audioContext: AudioContext | null = null;\n private _sourceNode: MediaElementAudioSourceNode | null = null;\n private _fadeGain: GainNode | null = null;\n private _volumeGain: GainNode | null = null;\n private _fadeIn: FadeConfig | undefined;\n private _fadeOut: FadeConfig | undefined;\n\n constructor(options: MediaElementTrackOptions) {\n this._peaks = options.peaks ?? null;\n this._id = options.id ?? `track-${Date.now()}`;\n this._name = options.name ?? 'Track';\n this._playbackRate = options.playbackRate ?? 1;\n this._volume = options.volume ?? 1;\n this._fadeIn = options.fadeIn;\n this._fadeOut = options.fadeOut;\n\n // Create or use provided audio element\n if (typeof options.source === 'string') {\n this.audioElement = new Audio(options.source);\n this.ownsElement = true;\n } else {\n this.audioElement = options.source;\n this.ownsElement = false;\n }\n\n // Configure audio element\n this.audioElement.preload = 'auto';\n this.audioElement.playbackRate = this._playbackRate;\n\n // Set pitch preservation (default: true).\n // When false, the browser won't apply its own pitch correction — useful\n // when an external processor like SoundTouch handles pitch compensation.\n // Vendor-prefixed properties are non-standard; cast once for type safety.\n const shouldPreservePitch = options.preservesPitch ?? true;\n const audio = this.audioElement as unknown as VendorPrefixedPitch;\n if ('preservesPitch' in this.audioElement) {\n audio.preservesPitch = shouldPreservePitch;\n } else if ('mozPreservesPitch' in this.audioElement) {\n audio.mozPreservesPitch = shouldPreservePitch;\n } else if ('webkitPreservesPitch' in this.audioElement) {\n audio.webkitPreservesPitch = shouldPreservePitch;\n }\n\n // Set up Web Audio routing if AudioContext provided\n if (options.audioContext) {\n this._audioContext = options.audioContext;\n try {\n this._sourceNode = options.audioContext.createMediaElementSource(this.audioElement);\n } catch (err) {\n throw new Error(\n '[waveform-playlist] MediaElementTrack: createMediaElementSource() failed. ' +\n 'This can happen if the audio element is already connected to another AudioContext. ' +\n 'Each audio element can only have one MediaElementSourceNode. ' +\n 'Original error: ' +\n String(err)\n );\n }\n this._fadeGain = options.audioContext.createGain();\n this._volumeGain = options.audioContext.createGain();\n this._volumeGain.gain.value = this._volume;\n\n this._sourceNode.connect(this._fadeGain);\n this._fadeGain.connect(this._volumeGain);\n this._volumeGain.connect(options.audioContext.destination);\n\n // With Web Audio routing, HTMLAudioElement.volume is bypassed.\n // Set it to 1 so it doesn't attenuate the signal before the source node.\n this.audioElement.volume = 1;\n } else {\n // Without Web Audio, use HTMLAudioElement.volume directly\n this.audioElement.volume = this._volume;\n }\n\n // Set up event listeners\n this.audioElement.addEventListener('ended', this.handleEnded);\n this.audioElement.addEventListener('timeupdate', this.handleTimeUpdate);\n }\n\n private handleEnded = () => {\n this._cancelFades();\n if (this.onStopCallback) {\n this.onStopCallback();\n }\n };\n\n private handleTimeUpdate = () => {\n if (this.onTimeUpdateCallback) {\n this.onTimeUpdateCallback(this.audioElement.currentTime);\n }\n };\n\n /**\n * Schedule fade automation on the fade GainNode.\n * Called at the start of each play() — fades are relative to the playback offset.\n */\n private _scheduleFades(offset: number): void {\n if (!this._fadeGain || !this._audioContext) return;\n\n const fadeGain = this._fadeGain.gain;\n const now = this._audioContext.currentTime;\n const totalDuration = this.duration;\n\n // Reset fade gain\n fadeGain.cancelScheduledValues(0);\n fadeGain.setValueAtTime(1, now);\n\n // Fade in\n if (this._fadeIn && this._fadeIn.duration > 0) {\n const fadeInEnd = this._fadeIn.duration;\n if (offset < fadeInEnd) {\n const remainingFade = fadeInEnd - offset;\n const fadeType = this._fadeIn.type ?? 'linear';\n if (offset === 0) {\n // Full fade from beginning\n applyFadeIn(fadeGain, now, remainingFade, fadeType, 0, 1);\n } else {\n // Partial fade — slice the original curve to preserve shape\n const curve = generateCurve(fadeType, 1000, true);\n const startIndex = Math.round((offset / this._fadeIn.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingFade);\n }\n }\n }\n\n // Fade out\n if (this._fadeOut && this._fadeOut.duration > 0) {\n const fadeOutStart = totalDuration - this._fadeOut.duration;\n if (offset < totalDuration && fadeOutStart < totalDuration) {\n if (offset > fadeOutStart) {\n // Already past the fade-out start — slice original curve to preserve shape\n const elapsed = offset - fadeOutStart;\n const fadeType = this._fadeOut.type ?? 'linear';\n const curve = generateCurve(fadeType, 1000, false);\n const startIndex = Math.round((elapsed / this._fadeOut.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n const remainingDuration = this._fadeOut.duration - elapsed;\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingDuration);\n } else {\n // Schedule full fade-out at the right time\n const delayUntilFadeOut = fadeOutStart - offset;\n applyFadeOut(\n fadeGain,\n now + delayUntilFadeOut,\n this._fadeOut.duration,\n this._fadeOut.type ?? 'linear',\n 1,\n 0\n );\n }\n }\n }\n }\n\n /**\n * Cancel any scheduled fade automation.\n */\n private _cancelFades(): void {\n if (this._fadeGain) {\n this._fadeGain.gain.cancelScheduledValues(0);\n this._fadeGain.gain.value = 1;\n }\n }\n\n /**\n * Start playback from a specific time.\n * Resumes the AudioContext first if suspended, then schedules fades\n * (fades depend on audioContext.currentTime being non-zero).\n */\n play(offset: number = 0): void {\n const startPlayback = () => {\n this._scheduleFades(offset);\n this.audioElement.currentTime = offset;\n this.audioElement.play().catch((err) => {\n console.warn('[waveform-playlist] MediaElementTrack: play() failed: ' + String(err));\n });\n };\n\n // Resume AudioContext if suspended (browser autoplay policy).\n // Must await resume before scheduling fades — audioContext.currentTime\n // is 0 while suspended, which would schedule all fades in the past.\n if (this._audioContext && this._audioContext.state === 'suspended') {\n this._audioContext\n .resume()\n .then(startPlayback)\n .catch((err) => {\n console.warn(\n '[waveform-playlist] MediaElementTrack: AudioContext.resume() failed: ' + String(err)\n );\n });\n } else {\n startPlayback();\n }\n }\n\n /**\n * Pause playback\n */\n pause(): void {\n this._cancelFades();\n this.audioElement.pause();\n }\n\n /**\n * Stop playback and reset to beginning\n */\n stop(): void {\n this._cancelFades();\n this.audioElement.pause();\n this.audioElement.currentTime = 0;\n }\n\n /**\n * Seek to a specific time\n */\n seekTo(time: number): void {\n this.audioElement.currentTime = Math.max(0, Math.min(time, this.duration));\n }\n\n /**\n * Set volume (0.0 to 1.0)\n */\n setVolume(volume: number): void {\n this._volume = Math.max(0, Math.min(1, volume));\n if (this._volumeGain) {\n this._volumeGain.gain.value = this._volume;\n } else {\n this.audioElement.volume = this._volume;\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved)\n */\n setPlaybackRate(rate: number): void {\n const clampedRate = Math.max(0.5, Math.min(2.0, rate));\n this._playbackRate = clampedRate;\n this.audioElement.playbackRate = clampedRate;\n }\n\n /**\n * Set muted state\n */\n setMuted(muted: boolean): void {\n this.audioElement.muted = muted;\n }\n\n /**\n * Set fade in configuration\n */\n setFadeIn(fadeIn: FadeConfig | undefined): void {\n this._fadeIn = fadeIn;\n }\n\n /**\n * Set fade out configuration\n */\n setFadeOut(fadeOut: FadeConfig | undefined): void {\n this._fadeOut = fadeOut;\n }\n\n /**\n * Set callback for when playback ends\n */\n setOnStopCallback(callback: () => void): void {\n this.onStopCallback = callback;\n }\n\n /**\n * Set callback for time updates\n */\n setOnTimeUpdateCallback(callback: (time: number) => void): void {\n this.onTimeUpdateCallback = callback;\n }\n\n /**\n * Connect the output to a different destination (for effects chains).\n * Disconnects from the current destination first.\n *\n * @param destination - The AudioNode to connect to\n */\n connectOutput(destination: AudioNode): void {\n if (!this._volumeGain) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: connectOutput() requires audioContext. ' +\n 'Pass audioContext in constructor options.'\n );\n return;\n }\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before connectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(destination);\n }\n\n /**\n * Disconnect the output and reconnect to the default AudioContext destination.\n */\n disconnectOutput(): void {\n if (!this._volumeGain || !this._audioContext) return;\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before disconnectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(this._audioContext.destination);\n }\n\n /**\n * Clean up resources\n */\n dispose(): void {\n this.audioElement.removeEventListener('ended', this.handleEnded);\n this.audioElement.removeEventListener('timeupdate', this.handleTimeUpdate);\n this._cancelFades();\n this.audioElement.pause();\n\n if (this._sourceNode) {\n try {\n this._sourceNode.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: sourceNode disconnect failed: ' + String(err)\n );\n }\n }\n if (this._fadeGain) {\n try {\n this._fadeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: fadeGain disconnect failed: ' + String(err)\n );\n }\n }\n if (this._volumeGain) {\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: volumeGain disconnect failed: ' + String(err)\n );\n }\n }\n\n if (this.ownsElement) {\n this.audioElement.src = '';\n this.audioElement.load(); // Release resources\n }\n }\n\n // Getters\n get id(): string {\n return this._id;\n }\n\n get name(): string {\n return this._name;\n }\n\n get peaks(): WaveformDataObject | null {\n return this._peaks;\n }\n\n get currentTime(): number {\n return this.audioElement.currentTime;\n }\n\n get duration(): number {\n return this.audioElement.duration || this._peaks?.duration || 0;\n }\n\n get isPlaying(): boolean {\n return !this.audioElement.paused && !this.audioElement.ended;\n }\n\n get volume(): number {\n return this._volume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get muted(): boolean {\n return this.audioElement.muted;\n }\n\n /**\n * Get the underlying audio element (for advanced use cases)\n */\n get element(): HTMLAudioElement {\n return this.audioElement;\n }\n\n /**\n * Get the volume GainNode output (for connecting effects chains).\n * Returns null if no AudioContext was provided.\n */\n get outputNode(): GainNode | null {\n return this._volumeGain;\n }\n}\n","import { MediaElementTrack, type MediaElementTrackOptions } from './MediaElementTrack';\n\nexport interface MediaElementPlayoutOptions {\n /** Initial master volume (0.0 to 1.0) */\n masterVolume?: number;\n /** Initial playback rate (0.5 to 2.0) */\n playbackRate?: number;\n /** Whether to preserve pitch when changing playback rate (default: true).\n * Set to false when using an external pitch processor like SoundTouch. */\n preservesPitch?: boolean;\n}\n\n/**\n * Single-track playout engine using HTMLAudioElement.\n *\n * This is a lightweight alternative to TonePlayout for single-track use cases\n * that need pitch-preserving playback rate control.\n *\n * Key features:\n * - Pitch-preserving playback rate (0.5x - 2.0x)\n * - Uses pre-computed peaks (no AudioBuffer required)\n * - Simpler API for single-track playback\n *\n * Limitations:\n * - Single track only - will warn if multiple tracks added\n * - No multi-track mixing\n *\n * For multi-track editing, use TonePlayout from @waveform-playlist/playout instead.\n */\nexport class MediaElementPlayout {\n private track: MediaElementTrack | null = null;\n private _masterVolume: number;\n private _playbackRate: number;\n private _preservesPitch: boolean;\n private _isPlaying: boolean = false;\n private onPlaybackCompleteCallback?: () => void;\n\n constructor(options: MediaElementPlayoutOptions = {}) {\n this._masterVolume = options.masterVolume ?? 1;\n this._playbackRate = options.playbackRate ?? 1;\n this._preservesPitch = options.preservesPitch ?? true;\n }\n\n /**\n * Initialize the playout engine.\n * For MediaElementPlayout this is a no-op — HTMLAudioElement doesn't require\n * explicit initialization. When an AudioContext is provided for fades/effects,\n * it resumes automatically on first play via MediaElementTrack.\n */\n async init(): Promise<void> {\n // No initialization needed — audio element handles autoplay policy automatically\n }\n\n /**\n * Add a track to the playout.\n * Note: Only one track is supported. Adding a second track will dispose the first.\n */\n addTrack(options: MediaElementTrackOptions): MediaElementTrack {\n if (this.track) {\n console.warn(\n 'MediaElementPlayout: Only one track is supported. ' +\n 'Disposing previous track. For multi-track, use TonePlayout.'\n );\n this.track.dispose();\n }\n\n this.track = new MediaElementTrack({\n ...options,\n volume: this._masterVolume * (options.volume ?? 1),\n playbackRate: this._playbackRate,\n preservesPitch: this._preservesPitch,\n });\n\n // Set up stop callback\n this.track.setOnStopCallback(() => {\n this._isPlaying = false;\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n });\n\n return this.track;\n }\n\n /**\n * Remove a track by ID.\n */\n removeTrack(trackId: string): void {\n if (this.track && this.track.id === trackId) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n /**\n * Get a track by ID.\n */\n getTrack(trackId: string): MediaElementTrack | undefined {\n if (this.track && this.track.id === trackId) {\n return this.track;\n }\n return undefined;\n }\n\n /**\n * Start playback.\n * @param _when - Ignored (HTMLAudioElement doesn't support scheduled start)\n * @param offset - Start position in seconds\n * @param duration - Duration to play in seconds (optional)\n */\n play(_when?: number, offset?: number, duration?: number): void {\n if (!this.track) {\n console.warn('MediaElementPlayout: No track to play');\n return;\n }\n\n const startPosition = offset ?? 0;\n this._isPlaying = true;\n\n this.track.play(startPosition);\n\n // If duration is specified, schedule stop\n if (duration !== undefined) {\n const adjustedDuration = duration / this._playbackRate;\n setTimeout(() => {\n if (this._isPlaying) {\n this.pause();\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n }\n }, adjustedDuration * 1000);\n }\n }\n\n /**\n * Pause playback.\n */\n pause(): void {\n if (this.track) {\n this.track.pause();\n }\n this._isPlaying = false;\n }\n\n /**\n * Stop playback and reset to start.\n */\n stop(): void {\n if (this.track) {\n this.track.stop();\n }\n this._isPlaying = false;\n }\n\n /**\n * Seek to a specific time.\n */\n seekTo(time: number): void {\n if (this.track) {\n this.track.seekTo(time);\n }\n }\n\n /**\n * Get current playback time.\n */\n getCurrentTime(): number {\n if (this.track) {\n return this.track.currentTime;\n }\n return 0;\n }\n\n /**\n * Set master volume.\n */\n setMasterVolume(volume: number): void {\n this._masterVolume = Math.max(0, Math.min(1, volume));\n if (this.track) {\n this.track.setVolume(this._masterVolume);\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved).\n */\n setPlaybackRate(rate: number): void {\n this._playbackRate = Math.max(0.5, Math.min(2.0, rate));\n if (this.track) {\n this.track.setPlaybackRate(this._playbackRate);\n }\n }\n\n /**\n * Set mute state for a track.\n */\n setMute(trackId: string, muted: boolean): void {\n const track = this.getTrack(trackId);\n if (track) {\n track.setMuted(muted);\n }\n }\n\n /**\n * Set solo state for a track.\n * Note: With single track, solo is effectively the same as unmute.\n */\n setSolo(_trackId: string, _soloed: boolean): void {\n // No-op for single track - solo doesn't make sense\n console.warn('MediaElementPlayout: Solo is not applicable for single-track playback');\n }\n\n /**\n * Set callback for when playback completes.\n */\n setOnPlaybackComplete(callback: () => void): void {\n this.onPlaybackCompleteCallback = callback;\n }\n\n /**\n * Clean up resources.\n */\n dispose(): void {\n if (this.track) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n // Getters\n get isPlaying(): boolean {\n return this._isPlaying;\n }\n\n get masterVolume(): number {\n return this._masterVolume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get duration(): number {\n return this.track?.duration ?? 0;\n }\n\n get sampleRate(): number {\n // HTMLAudioElement doesn't expose sample rate directly\n // Return a common default - peaks will have the actual sample rate\n return this.track?.peaks?.sample_rate ?? 44100;\n }\n\n /**\n * Get the volume GainNode output for connecting external effects chains.\n * Returns null if no AudioContext was provided to the track.\n *\n * Usage: disconnect from default destination, connect to effect input,\n * then connect effect output to audioContext.destination.\n */\n get outputNode(): GainNode | null {\n return this.track?.outputNode ?? null;\n }\n}\n","/**\n * Common interface for playout engines.\n *\n * Both TonePlayout and MediaElementPlayout implement this interface,\n * allowing the browser package to work with either engine.\n */\nexport interface PlayoutEngine {\n // Lifecycle\n init(): Promise<void>;\n dispose(): void;\n\n // Playback\n play(when?: number, offset?: number, duration?: number): void;\n pause(): void;\n stop(): void;\n seekTo(time: number): void;\n getCurrentTime(): number;\n\n // Volume\n setMasterVolume(volume: number): void;\n\n // Track controls (optional - not all engines support all features)\n setMute?(trackId: string, muted: boolean): void;\n setSolo?(trackId: string, soloed: boolean): void;\n\n // Callbacks\n setOnPlaybackComplete(callback: () => void): void;\n\n // State\n readonly isPlaying: boolean;\n readonly duration: number;\n readonly sampleRate: number;\n}\n\n/**\n * Extended interface for engines that support playback rate.\n */\nexport interface PlaybackRateEngine extends PlayoutEngine {\n setPlaybackRate(rate: number): void;\n readonly playbackRate: number;\n}\n\n/**\n * Type guard to check if an engine supports playback rate.\n */\nexport function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine {\n return (\n 'setPlaybackRate' in engine &&\n typeof (engine as Record<string, unknown>).setPlaybackRate === 'function'\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/MediaElementTrack.ts","../src/MediaElementPlayout.ts","../src/types.ts"],"sourcesContent":["export { MediaElementPlayout } from './MediaElementPlayout';\nexport { MediaElementTrack } from './MediaElementTrack';\n\nexport type { MediaElementPlayoutOptions } from './MediaElementPlayout';\nexport type {\n MediaElementTrackOptions,\n MediaElementTrackEvents,\n FadeConfig,\n} from './MediaElementTrack';\n\n// Common interface types\nexport type { PlayoutEngine, PlaybackRateEngine } from './types';\nexport { supportsPlaybackRate } from './types';\n","import type { WaveformDataObject, FadeConfig } from '@waveform-playlist/core';\nimport { applyFadeIn, applyFadeOut, generateCurve } from '@waveform-playlist/core';\n\nexport type { FadeConfig } from '@waveform-playlist/core';\n\n/**\n * Extended HTMLAudioElement with vendor-prefixed preservesPitch properties.\n * `preservesPitch` is standard; the `moz` and `webkit` prefixes are for older browsers.\n */\ninterface VendorPrefixedPitch {\n preservesPitch?: boolean;\n mozPreservesPitch?: boolean;\n webkitPreservesPitch?: boolean;\n}\n\nexport interface MediaElementTrackOptions {\n /** The audio source - can be a URL, Blob URL, or HTMLAudioElement */\n source: string | HTMLAudioElement;\n /** Pre-computed waveform data for visualization. Optional — omit for\n * scrubber-only / headless players that don't render a waveform. */\n peaks?: WaveformDataObject;\n /** Track ID */\n id?: string;\n /** Track name for display */\n name?: string;\n /** Initial volume (0.0 to 1.0) */\n volume?: number;\n /** Initial playback rate (0.5 to 2.0) */\n playbackRate?: number;\n /** Whether to preserve pitch when changing playback rate (default: true) */\n preservesPitch?: boolean;\n /**\n * AudioContext for Web Audio routing.\n * When provided, audio is routed through Web Audio nodes for fades and effects:\n * HTMLAudioElement → MediaElementSourceNode → fadeGain → volumeGain → destination\n *\n * Without this, playback uses HTMLAudioElement directly (no fades or effects).\n *\n * Note: createMediaElementSource() can only be called once per element.\n * Once routed, HTMLAudioElement.volume no longer works — volume is controlled\n * via the Web Audio GainNode instead.\n */\n audioContext?: AudioContext;\n /** Fade in configuration (requires audioContext) */\n fadeIn?: FadeConfig;\n /** Fade out configuration (requires audioContext) */\n fadeOut?: FadeConfig;\n}\n\n/**\n * Typed event map for MediaElementTrack's emitter. Mirrors the on()/off()\n * pattern used by @waveform-playlist/engine's PlaylistEngine so consumers and\n * the dawcore web-components layer can wire events uniformly across engines.\n */\nexport interface MediaElementTrackEvents {\n /** Fired when the element's metadata (duration, dimensions) has loaded. */\n loadedmetadata: () => void;\n /** Fired when native playback starts/resumes. */\n play: () => void;\n /** Fired when native playback pauses (including at end-of-media). */\n pause: () => void;\n /** Fired on a media error; carries the element's MediaError (or null). */\n error: (err: MediaError | null) => void;\n /** Fired when playback reaches the end of the media. */\n ended: () => void;\n /** Fired on each native timeupdate; carries the current time in seconds. */\n timeupdate: (time: number) => void;\n}\n\n/**\n * Single-track playback using HTMLAudioElement.\n *\n * Benefits over AudioBuffer/Tone.js:\n * - Pitch-preserving playback rate (0.5x - 2.0x) via browser's built-in algorithm\n * - No AudioBuffer decoding required (uses pre-computed peaks for visualization)\n * - Simpler, lighter-weight for single-track use cases\n *\n * When an AudioContext is provided:\n * - Audio routes through Web Audio graph for fades and effects\n * - Volume controlled via GainNode (HTMLAudioElement.volume is bypassed)\n * - Output node exposed for connecting external effects chains\n */\nexport class MediaElementTrack {\n private audioElement: HTMLAudioElement;\n private ownsElement: boolean; // Whether we created the element (need to clean up)\n private _peaks: WaveformDataObject | null;\n private _id: string;\n private _name: string;\n private _playbackRate: number = 1;\n private _volume: number;\n private onStopCallback?: () => void;\n private onTimeUpdateCallback?: (time: number) => void;\n private _listeners: Map<string, Set<MediaElementTrackEvents[keyof MediaElementTrackEvents]>> =\n new Map();\n\n // Web Audio nodes (only when audioContext is provided)\n private _audioContext: AudioContext | null = null;\n private _sourceNode: MediaElementAudioSourceNode | null = null;\n private _fadeGain: GainNode | null = null;\n private _volumeGain: GainNode | null = null;\n private _fadeIn: FadeConfig | undefined;\n private _fadeOut: FadeConfig | undefined;\n\n constructor(options: MediaElementTrackOptions) {\n this._peaks = options.peaks ?? null;\n this._id = options.id ?? `track-${Date.now()}`;\n this._name = options.name ?? 'Track';\n this._playbackRate = options.playbackRate ?? 1;\n this._volume = options.volume ?? 1;\n this._fadeIn = options.fadeIn;\n this._fadeOut = options.fadeOut;\n\n // Create or use provided audio element\n if (typeof options.source === 'string') {\n this.audioElement = new Audio(options.source);\n this.ownsElement = true;\n } else {\n this.audioElement = options.source;\n this.ownsElement = false;\n }\n\n // Configure audio element\n this.audioElement.preload = 'auto';\n this.audioElement.playbackRate = this._playbackRate;\n\n // Set pitch preservation (default: true).\n // When false, the browser won't apply its own pitch correction — useful\n // when an external processor like SoundTouch handles pitch compensation.\n // Vendor-prefixed properties are non-standard; cast once for type safety.\n const shouldPreservePitch = options.preservesPitch ?? true;\n const audio = this.audioElement as unknown as VendorPrefixedPitch;\n if ('preservesPitch' in this.audioElement) {\n audio.preservesPitch = shouldPreservePitch;\n } else if ('mozPreservesPitch' in this.audioElement) {\n audio.mozPreservesPitch = shouldPreservePitch;\n } else if ('webkitPreservesPitch' in this.audioElement) {\n audio.webkitPreservesPitch = shouldPreservePitch;\n }\n\n // Set up Web Audio routing if AudioContext provided\n if (options.audioContext) {\n this._audioContext = options.audioContext;\n try {\n this._sourceNode = options.audioContext.createMediaElementSource(this.audioElement);\n } catch (err) {\n throw new Error(\n '[waveform-playlist] MediaElementTrack: createMediaElementSource() failed. ' +\n 'This can happen if the audio element is already connected to another AudioContext. ' +\n 'Each audio element can only have one MediaElementSourceNode. ' +\n 'Original error: ' +\n String(err)\n );\n }\n this._fadeGain = options.audioContext.createGain();\n this._volumeGain = options.audioContext.createGain();\n this._volumeGain.gain.value = this._volume;\n\n this._sourceNode.connect(this._fadeGain);\n this._fadeGain.connect(this._volumeGain);\n this._volumeGain.connect(options.audioContext.destination);\n\n // With Web Audio routing, HTMLAudioElement.volume is bypassed.\n // Set it to 1 so it doesn't attenuate the signal before the source node.\n this.audioElement.volume = 1;\n } else {\n // Without Web Audio, use HTMLAudioElement.volume directly\n this.audioElement.volume = this._volume;\n }\n\n // Set up event listeners\n this.audioElement.addEventListener('ended', this.handleEnded);\n this.audioElement.addEventListener('timeupdate', this.handleTimeUpdate);\n this.audioElement.addEventListener('loadedmetadata', this.handleLoadedMetadata);\n this.audioElement.addEventListener('play', this.handlePlay);\n this.audioElement.addEventListener('pause', this.handlePause);\n this.audioElement.addEventListener('error', this.handleError);\n }\n\n private handleEnded = () => {\n this._cancelFades();\n if (this.onStopCallback) {\n this.onStopCallback();\n }\n this._emit('ended');\n };\n\n private handleTimeUpdate = () => {\n if (this.onTimeUpdateCallback) {\n this.onTimeUpdateCallback(this.audioElement.currentTime);\n }\n this._emit('timeupdate', this.audioElement.currentTime);\n };\n\n private handleLoadedMetadata = () => {\n this._emit('loadedmetadata');\n };\n\n private handlePlay = () => {\n this._emit('play');\n };\n\n private handlePause = () => {\n this._emit('pause');\n };\n\n private handleError = () => {\n this._emit('error', this.audioElement.error);\n };\n\n /**\n * Schedule fade automation on the fade GainNode.\n * Called at the start of each play() — fades are relative to the playback offset.\n */\n private _scheduleFades(offset: number): void {\n if (!this._fadeGain || !this._audioContext) return;\n\n const fadeGain = this._fadeGain.gain;\n const now = this._audioContext.currentTime;\n const totalDuration = this.duration;\n\n // Reset fade gain\n fadeGain.cancelScheduledValues(0);\n fadeGain.setValueAtTime(1, now);\n\n // Fade in\n if (this._fadeIn && this._fadeIn.duration > 0) {\n const fadeInEnd = this._fadeIn.duration;\n if (offset < fadeInEnd) {\n const remainingFade = fadeInEnd - offset;\n const fadeType = this._fadeIn.type ?? 'linear';\n if (offset === 0) {\n // Full fade from beginning\n applyFadeIn(fadeGain, now, remainingFade, fadeType, 0, 1);\n } else {\n // Partial fade — slice the original curve to preserve shape\n const curve = generateCurve(fadeType, 1000, true);\n const startIndex = Math.round((offset / this._fadeIn.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingFade);\n }\n }\n }\n\n // Fade out\n if (this._fadeOut && this._fadeOut.duration > 0) {\n const fadeOutStart = totalDuration - this._fadeOut.duration;\n if (offset < totalDuration && fadeOutStart < totalDuration) {\n if (offset > fadeOutStart) {\n // Already past the fade-out start — slice original curve to preserve shape\n const elapsed = offset - fadeOutStart;\n const fadeType = this._fadeOut.type ?? 'linear';\n const curve = generateCurve(fadeType, 1000, false);\n const startIndex = Math.round((elapsed / this._fadeOut.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n const remainingDuration = this._fadeOut.duration - elapsed;\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingDuration);\n } else {\n // Schedule full fade-out at the right time\n const delayUntilFadeOut = fadeOutStart - offset;\n applyFadeOut(\n fadeGain,\n now + delayUntilFadeOut,\n this._fadeOut.duration,\n this._fadeOut.type ?? 'linear',\n 1,\n 0\n );\n }\n }\n }\n }\n\n /**\n * Cancel any scheduled fade automation.\n */\n private _cancelFades(): void {\n if (this._fadeGain) {\n this._fadeGain.gain.cancelScheduledValues(0);\n this._fadeGain.gain.value = 1;\n }\n }\n\n /**\n * Start playback from a specific time.\n * Resumes the AudioContext first if suspended, then schedules fades\n * (fades depend on audioContext.currentTime being non-zero).\n */\n play(offset: number = 0): void {\n const startPlayback = () => {\n this._scheduleFades(offset);\n this.audioElement.currentTime = offset;\n this.audioElement.play().catch((err) => {\n console.warn('[waveform-playlist] MediaElementTrack: play() failed: ' + String(err));\n });\n };\n\n // Resume AudioContext if suspended (browser autoplay policy).\n // Must await resume before scheduling fades — audioContext.currentTime\n // is 0 while suspended, which would schedule all fades in the past.\n if (this._audioContext && this._audioContext.state === 'suspended') {\n this._audioContext\n .resume()\n .then(startPlayback)\n .catch((err) => {\n console.warn(\n '[waveform-playlist] MediaElementTrack: AudioContext.resume() failed: ' + String(err)\n );\n });\n } else {\n startPlayback();\n }\n }\n\n /**\n * Resume playback from the current position without resetting currentTime.\n * Reuses play()'s fade re-scheduling and AudioContext-resume machinery —\n * passing the current position as the offset is a no-op seek that leaves\n * playback where it was.\n */\n resume(): void {\n this.play(this.currentTime);\n }\n\n /**\n * Swap the audio source in place, reusing the existing <audio> element.\n * Because the MediaElementAudioSourceNode is once-per-element, reusing the\n * element preserves any Web Audio routing/effects across the swap.\n *\n * Only supported when this track owns its element (constructed from a URL\n * string). A borrowed element (constructed from an HTMLAudioElement) warns\n * and no-ops — swapping a consumer-owned element's src is out of contract.\n *\n * Peaks are coupled to the specific audio, so they are replaced (defaulting\n * to null when omitted). Name is a label, so it updates only when provided.\n */\n load(source: string, opts: { peaks?: WaveformDataObject; name?: string } = {}): void {\n if (!this.ownsElement) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: load() is only supported for tracks that ' +\n 'own their audio element (constructed from a URL string). A track constructed from ' +\n 'an existing HTMLAudioElement cannot swap its source in place.'\n );\n return;\n }\n this._cancelFades();\n this.audioElement.pause();\n this.audioElement.src = source;\n this.audioElement.load();\n this.audioElement.currentTime = 0;\n this._peaks = opts.peaks ?? null;\n if (opts.name !== undefined) {\n this._name = opts.name;\n }\n // The HTML load algorithm resets playbackRate to defaultPlaybackRate (1.0);\n // re-apply the configured rate so it survives the source swap.\n this.audioElement.playbackRate = this._playbackRate;\n }\n\n /**\n * Pause playback\n */\n pause(): void {\n this._cancelFades();\n this.audioElement.pause();\n }\n\n /**\n * Stop playback and reset to beginning\n */\n stop(): void {\n this._cancelFades();\n this.audioElement.pause();\n this.audioElement.currentTime = 0;\n }\n\n /**\n * Seek to a specific time\n */\n seekTo(time: number): void {\n this.audioElement.currentTime = Math.max(0, Math.min(time, this.duration));\n }\n\n /**\n * Set volume (0.0 to 1.0)\n */\n setVolume(volume: number): void {\n this._volume = Math.max(0, Math.min(1, volume));\n if (this._volumeGain) {\n this._volumeGain.gain.value = this._volume;\n } else {\n this.audioElement.volume = this._volume;\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved)\n */\n setPlaybackRate(rate: number): void {\n const clampedRate = Math.max(0.5, Math.min(2.0, rate));\n this._playbackRate = clampedRate;\n this.audioElement.playbackRate = clampedRate;\n }\n\n /**\n * Set muted state\n */\n setMuted(muted: boolean): void {\n this.audioElement.muted = muted;\n }\n\n /**\n * Set fade in configuration\n */\n setFadeIn(fadeIn: FadeConfig | undefined): void {\n this._fadeIn = fadeIn;\n }\n\n /**\n * Set fade out configuration\n */\n setFadeOut(fadeOut: FadeConfig | undefined): void {\n this._fadeOut = fadeOut;\n }\n\n /**\n * Set callback for when playback ends\n */\n setOnStopCallback(callback: () => void): void {\n this.onStopCallback = callback;\n }\n\n /**\n * Set callback for time updates\n */\n setOnTimeUpdateCallback(callback: (time: number) => void): void {\n this.onTimeUpdateCallback = callback;\n }\n\n /**\n * Connect the output to a different destination (for effects chains).\n * Disconnects from the current destination first.\n *\n * @param destination - The AudioNode to connect to\n */\n connectOutput(destination: AudioNode): void {\n if (!this._volumeGain) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: connectOutput() requires audioContext. ' +\n 'Pass audioContext in constructor options.'\n );\n return;\n }\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before connectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(destination);\n }\n\n /**\n * Disconnect the output and reconnect to the default AudioContext destination.\n */\n disconnectOutput(): void {\n if (!this._volumeGain || !this._audioContext) return;\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before disconnectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(this._audioContext.destination);\n }\n\n /**\n * Subscribe to a track lifecycle event. Multiple listeners per event are\n * supported. Mirrors PlaylistEngine's on()/off() emitter.\n */\n on<K extends keyof MediaElementTrackEvents>(\n event: K,\n listener: MediaElementTrackEvents[K]\n ): void {\n if (!this._listeners.has(event)) {\n this._listeners.set(event, new Set());\n }\n this._listeners.get(event)!.add(listener);\n }\n\n /**\n * Unsubscribe a previously registered listener.\n */\n off<K extends keyof MediaElementTrackEvents>(\n event: K,\n listener: MediaElementTrackEvents[K]\n ): void {\n this._listeners.get(event)?.delete(listener);\n }\n\n private _emit(event: string, ...args: unknown[]): void {\n const listeners = this._listeners.get(event);\n if (listeners) {\n for (const listener of listeners) {\n try {\n (listener as (...a: unknown[]) => void)(...args);\n } catch (error) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: error in event listener: ' + String(error)\n );\n }\n }\n }\n }\n\n /**\n * Clean up resources\n */\n dispose(): void {\n this.audioElement.removeEventListener('ended', this.handleEnded);\n this.audioElement.removeEventListener('timeupdate', this.handleTimeUpdate);\n this.audioElement.removeEventListener('loadedmetadata', this.handleLoadedMetadata);\n this.audioElement.removeEventListener('play', this.handlePlay);\n this.audioElement.removeEventListener('pause', this.handlePause);\n this.audioElement.removeEventListener('error', this.handleError);\n this._listeners.clear();\n this._cancelFades();\n this.audioElement.pause();\n\n if (this._sourceNode) {\n try {\n this._sourceNode.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: sourceNode disconnect failed: ' + String(err)\n );\n }\n }\n if (this._fadeGain) {\n try {\n this._fadeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: fadeGain disconnect failed: ' + String(err)\n );\n }\n }\n if (this._volumeGain) {\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: volumeGain disconnect failed: ' + String(err)\n );\n }\n }\n\n if (this.ownsElement) {\n this.audioElement.src = '';\n this.audioElement.load(); // Release resources\n }\n }\n\n // Getters\n get id(): string {\n return this._id;\n }\n\n get name(): string {\n return this._name;\n }\n\n get peaks(): WaveformDataObject | null {\n return this._peaks;\n }\n\n get currentTime(): number {\n return this.audioElement.currentTime;\n }\n\n get duration(): number {\n return this.audioElement.duration || this._peaks?.duration || 0;\n }\n\n get isPlaying(): boolean {\n return !this.audioElement.paused && !this.audioElement.ended;\n }\n\n get volume(): number {\n return this._volume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get muted(): boolean {\n return this.audioElement.muted;\n }\n\n /**\n * Get the underlying audio element (for advanced use cases)\n */\n get element(): HTMLAudioElement {\n return this.audioElement;\n }\n\n /**\n * Get the volume GainNode output (for connecting effects chains).\n * Returns null if no AudioContext was provided.\n */\n get outputNode(): GainNode | null {\n return this._volumeGain;\n }\n}\n","import {\n MediaElementTrack,\n type MediaElementTrackOptions,\n type MediaElementTrackEvents,\n} from './MediaElementTrack';\n\nexport interface MediaElementPlayoutOptions {\n /** Initial master volume (0.0 to 1.0) */\n masterVolume?: number;\n /** Initial playback rate (0.5 to 2.0) */\n playbackRate?: number;\n /** Whether to preserve pitch when changing playback rate (default: true).\n * Set to false when using an external pitch processor like SoundTouch. */\n preservesPitch?: boolean;\n}\n\n/**\n * Single-track playout engine using HTMLAudioElement.\n *\n * This is a lightweight alternative to TonePlayout for single-track use cases\n * that need pitch-preserving playback rate control.\n *\n * Key features:\n * - Pitch-preserving playback rate (0.5x - 2.0x)\n * - Uses pre-computed peaks (no AudioBuffer required)\n * - Simpler API for single-track playback\n *\n * Limitations:\n * - Single track only - will warn if multiple tracks added\n * - No multi-track mixing\n *\n * For multi-track editing, use TonePlayout from @waveform-playlist/playout instead.\n */\nexport class MediaElementPlayout {\n private track: MediaElementTrack | null = null;\n private _masterVolume: number;\n private _playbackRate: number;\n private _preservesPitch: boolean;\n private _isPlaying: boolean = false;\n private onPlaybackCompleteCallback?: () => void;\n /** Consumer event listeners, retained so they re-attach across track swaps. */\n private _eventListeners: Map<\n string,\n Set<MediaElementTrackEvents[keyof MediaElementTrackEvents]>\n > = new Map();\n\n constructor(options: MediaElementPlayoutOptions = {}) {\n this._masterVolume = options.masterVolume ?? 1;\n this._playbackRate = options.playbackRate ?? 1;\n this._preservesPitch = options.preservesPitch ?? true;\n }\n\n /**\n * Initialize the playout engine.\n * For MediaElementPlayout this is a no-op — HTMLAudioElement doesn't require\n * explicit initialization. When an AudioContext is provided for fades/effects,\n * it resumes automatically on first play via MediaElementTrack.\n */\n async init(): Promise<void> {\n // No initialization needed — audio element handles autoplay policy automatically\n }\n\n /**\n * Add a track to the playout.\n * Note: Only one track is supported. Adding a second track will dispose the first.\n */\n addTrack(options: MediaElementTrackOptions): MediaElementTrack {\n if (this.track) {\n console.warn(\n 'MediaElementPlayout: Only one track is supported. ' +\n 'Disposing previous track. For multi-track, use TonePlayout.'\n );\n this.track.dispose();\n }\n\n this.track = new MediaElementTrack({\n ...options,\n volume: this._masterVolume * (options.volume ?? 1),\n playbackRate: this._playbackRate,\n preservesPitch: this._preservesPitch,\n });\n\n // Set up stop callback\n this.track.setOnStopCallback(() => {\n this._isPlaying = false;\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n });\n\n // Re-attach any consumer listeners to the newly created track (covers the\n // first track and the addTrack-replace path).\n this._attachListenersToTrack();\n\n return this.track;\n }\n\n /**\n * Replace the playout's source (player-mode affordance). The documented\n * single-track replace path — does NOT warn like addTrack().\n *\n * For URL (string) sources with an existing track, swaps in place via\n * track.load(), reusing the element and preserving Web Audio routing. For a\n * provided HTMLAudioElement source, or when there is no track yet, (re)creates\n * the track. Returns the active track.\n */\n setSource(options: MediaElementTrackOptions): MediaElementTrack {\n // A source swap stops current playback (in-place load() pauses; recreate disposes).\n this._isPlaying = false;\n if (this.track && typeof options.source === 'string') {\n this.track.load(options.source, { peaks: options.peaks, name: options.name });\n return this.track;\n }\n // First source, or swapping to a provided element: (re)create the track.\n // Dispose silently — this is the documented single-track replace path, not\n // the multi-track misuse that addTrack() warns about.\n if (this.track) {\n this.track.dispose();\n this.track = null;\n }\n return this.addTrack(options);\n }\n\n /**\n * Remove a track by ID.\n */\n removeTrack(trackId: string): void {\n if (this.track && this.track.id === trackId) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n /**\n * Get a track by ID.\n */\n getTrack(trackId: string): MediaElementTrack | undefined {\n if (this.track && this.track.id === trackId) {\n return this.track;\n }\n return undefined;\n }\n\n /**\n * Start playback.\n * @param _when - Ignored (HTMLAudioElement doesn't support scheduled start)\n * @param offset - Start position in seconds\n * @param duration - Duration to play in seconds (optional)\n */\n play(_when?: number, offset?: number, duration?: number): void {\n if (!this.track) {\n console.warn('MediaElementPlayout: No track to play');\n return;\n }\n\n const startPosition = offset ?? 0;\n this._isPlaying = true;\n\n this.track.play(startPosition);\n\n // If duration is specified, schedule stop\n if (duration !== undefined) {\n const adjustedDuration = duration / this._playbackRate;\n setTimeout(() => {\n if (this._isPlaying) {\n this.pause();\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n }\n }, adjustedDuration * 1000);\n }\n }\n\n /**\n * Resume playback from the current position (player-mode affordance).\n * Unlike play() with no offset (which resets to 0), this keeps currentTime.\n * Delegates to play() with the current position as the offset, so all of\n * play()'s machinery (AudioContext resume, fades, _isPlaying) is reused.\n */\n resume(): void {\n this.play(undefined, this.getCurrentTime());\n }\n\n /**\n * Pause playback.\n */\n pause(): void {\n if (this.track) {\n this.track.pause();\n }\n this._isPlaying = false;\n }\n\n /**\n * Stop playback and reset to start.\n */\n stop(): void {\n if (this.track) {\n this.track.stop();\n }\n this._isPlaying = false;\n }\n\n /**\n * Seek to a specific time.\n */\n seekTo(time: number): void {\n if (this.track) {\n this.track.seekTo(time);\n }\n }\n\n /**\n * Get current playback time.\n */\n getCurrentTime(): number {\n if (this.track) {\n return this.track.currentTime;\n }\n return 0;\n }\n\n /**\n * Set master volume.\n */\n setMasterVolume(volume: number): void {\n this._masterVolume = Math.max(0, Math.min(1, volume));\n if (this.track) {\n this.track.setVolume(this._masterVolume);\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved).\n */\n setPlaybackRate(rate: number): void {\n this._playbackRate = Math.max(0.5, Math.min(2.0, rate));\n if (this.track) {\n this.track.setPlaybackRate(this._playbackRate);\n }\n }\n\n /**\n * Set mute state for a track.\n */\n setMute(trackId: string, muted: boolean): void {\n const track = this.getTrack(trackId);\n if (track) {\n track.setMuted(muted);\n }\n }\n\n /**\n * Set solo state for a track.\n * Note: With single track, solo is effectively the same as unmute.\n */\n setSolo(_trackId: string, _soloed: boolean): void {\n // No-op for single track - solo doesn't make sense\n console.warn('MediaElementPlayout: Solo is not applicable for single-track playback');\n }\n\n /**\n * Set callback for when playback completes.\n */\n setOnPlaybackComplete(callback: () => void): void {\n this.onPlaybackCompleteCallback = callback;\n }\n\n /**\n * Subscribe to a lifecycle event (loadedmetadata / play / pause / error /\n * ended / timeupdate) without reaching into track.element. Listeners are\n * retained and re-attached automatically when the source is swapped.\n */\n on<K extends keyof MediaElementTrackEvents>(\n event: K,\n listener: MediaElementTrackEvents[K]\n ): void {\n if (!this._eventListeners.has(event)) {\n this._eventListeners.set(event, new Set());\n }\n this._eventListeners.get(event)!.add(listener);\n this.track?.on(event, listener);\n }\n\n /**\n * Unsubscribe a previously registered lifecycle listener.\n */\n off<K extends keyof MediaElementTrackEvents>(\n event: K,\n listener: MediaElementTrackEvents[K]\n ): void {\n this._eventListeners.get(event)?.delete(listener);\n this.track?.off(event, listener);\n }\n\n /**\n * Attach every registered listener to the current track. Called after a new\n * track is created so subscriptions survive source swaps. The cast is safe:\n * the event→listener correlation was enforced by the typed on() that filled\n * the registry; TS cannot track it through this loop.\n */\n private _attachListenersToTrack(): void {\n const track = this.track;\n if (!track) return;\n for (const [event, listeners] of this._eventListeners) {\n for (const listener of listeners) {\n track.on(\n event as keyof MediaElementTrackEvents,\n listener as MediaElementTrackEvents[keyof MediaElementTrackEvents]\n );\n }\n }\n }\n\n /**\n * Clean up resources.\n */\n dispose(): void {\n if (this.track) {\n this.track.dispose();\n this.track = null;\n }\n this._eventListeners.clear();\n }\n\n // Getters\n get isPlaying(): boolean {\n return this._isPlaying;\n }\n\n get masterVolume(): number {\n return this._masterVolume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get duration(): number {\n return this.track?.duration ?? 0;\n }\n\n get sampleRate(): number {\n // HTMLAudioElement doesn't expose sample rate directly\n // Return a common default - peaks will have the actual sample rate\n return this.track?.peaks?.sample_rate ?? 44100;\n }\n\n /**\n * Get the volume GainNode output for connecting external effects chains.\n * Returns null if no AudioContext was provided to the track.\n *\n * Usage: disconnect from default destination, connect to effect input,\n * then connect effect output to audioContext.destination.\n */\n get outputNode(): GainNode | null {\n return this.track?.outputNode ?? null;\n }\n}\n","/**\n * Common interface for playout engines.\n *\n * Both TonePlayout and MediaElementPlayout implement this interface,\n * allowing the browser package to work with either engine.\n */\nexport interface PlayoutEngine {\n // Lifecycle\n init(): Promise<void>;\n dispose(): void;\n\n // Playback\n play(when?: number, offset?: number, duration?: number): void;\n pause(): void;\n stop(): void;\n seekTo(time: number): void;\n getCurrentTime(): number;\n\n // Volume\n setMasterVolume(volume: number): void;\n\n // Track controls (optional - not all engines support all features)\n setMute?(trackId: string, muted: boolean): void;\n setSolo?(trackId: string, soloed: boolean): void;\n\n // Callbacks\n setOnPlaybackComplete(callback: () => void): void;\n\n // State\n readonly isPlaying: boolean;\n readonly duration: number;\n readonly sampleRate: number;\n}\n\n/**\n * Extended interface for engines that support playback rate.\n */\nexport interface PlaybackRateEngine extends PlayoutEngine {\n setPlaybackRate(rate: number): void;\n readonly playbackRate: number;\n}\n\n/**\n * Type guard to check if an engine supports playback rate.\n */\nexport function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine {\n return (\n 'setPlaybackRate' in engine &&\n typeof (engine as Record<string, unknown>).setPlaybackRate === 'function'\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { applyFadeIn, applyFadeOut, generateCurve } from "@waveform-playlist/cor
|
|
|
3
3
|
var MediaElementTrack = class {
|
|
4
4
|
constructor(options) {
|
|
5
5
|
this._playbackRate = 1;
|
|
6
|
+
this._listeners = /* @__PURE__ */ new Map();
|
|
6
7
|
// Web Audio nodes (only when audioContext is provided)
|
|
7
8
|
this._audioContext = null;
|
|
8
9
|
this._sourceNode = null;
|
|
@@ -13,11 +14,25 @@ var MediaElementTrack = class {
|
|
|
13
14
|
if (this.onStopCallback) {
|
|
14
15
|
this.onStopCallback();
|
|
15
16
|
}
|
|
17
|
+
this._emit("ended");
|
|
16
18
|
};
|
|
17
19
|
this.handleTimeUpdate = () => {
|
|
18
20
|
if (this.onTimeUpdateCallback) {
|
|
19
21
|
this.onTimeUpdateCallback(this.audioElement.currentTime);
|
|
20
22
|
}
|
|
23
|
+
this._emit("timeupdate", this.audioElement.currentTime);
|
|
24
|
+
};
|
|
25
|
+
this.handleLoadedMetadata = () => {
|
|
26
|
+
this._emit("loadedmetadata");
|
|
27
|
+
};
|
|
28
|
+
this.handlePlay = () => {
|
|
29
|
+
this._emit("play");
|
|
30
|
+
};
|
|
31
|
+
this.handlePause = () => {
|
|
32
|
+
this._emit("pause");
|
|
33
|
+
};
|
|
34
|
+
this.handleError = () => {
|
|
35
|
+
this._emit("error", this.audioElement.error);
|
|
21
36
|
};
|
|
22
37
|
this._peaks = options.peaks ?? null;
|
|
23
38
|
this._id = options.id ?? `track-${Date.now()}`;
|
|
@@ -65,6 +80,10 @@ var MediaElementTrack = class {
|
|
|
65
80
|
}
|
|
66
81
|
this.audioElement.addEventListener("ended", this.handleEnded);
|
|
67
82
|
this.audioElement.addEventListener("timeupdate", this.handleTimeUpdate);
|
|
83
|
+
this.audioElement.addEventListener("loadedmetadata", this.handleLoadedMetadata);
|
|
84
|
+
this.audioElement.addEventListener("play", this.handlePlay);
|
|
85
|
+
this.audioElement.addEventListener("pause", this.handlePause);
|
|
86
|
+
this.audioElement.addEventListener("error", this.handleError);
|
|
68
87
|
}
|
|
69
88
|
/**
|
|
70
89
|
* Schedule fade automation on the fade GainNode.
|
|
@@ -151,6 +170,45 @@ var MediaElementTrack = class {
|
|
|
151
170
|
startPlayback();
|
|
152
171
|
}
|
|
153
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Resume playback from the current position without resetting currentTime.
|
|
175
|
+
* Reuses play()'s fade re-scheduling and AudioContext-resume machinery —
|
|
176
|
+
* passing the current position as the offset is a no-op seek that leaves
|
|
177
|
+
* playback where it was.
|
|
178
|
+
*/
|
|
179
|
+
resume() {
|
|
180
|
+
this.play(this.currentTime);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Swap the audio source in place, reusing the existing <audio> element.
|
|
184
|
+
* Because the MediaElementAudioSourceNode is once-per-element, reusing the
|
|
185
|
+
* element preserves any Web Audio routing/effects across the swap.
|
|
186
|
+
*
|
|
187
|
+
* Only supported when this track owns its element (constructed from a URL
|
|
188
|
+
* string). A borrowed element (constructed from an HTMLAudioElement) warns
|
|
189
|
+
* and no-ops — swapping a consumer-owned element's src is out of contract.
|
|
190
|
+
*
|
|
191
|
+
* Peaks are coupled to the specific audio, so they are replaced (defaulting
|
|
192
|
+
* to null when omitted). Name is a label, so it updates only when provided.
|
|
193
|
+
*/
|
|
194
|
+
load(source, opts = {}) {
|
|
195
|
+
if (!this.ownsElement) {
|
|
196
|
+
console.warn(
|
|
197
|
+
"[waveform-playlist] MediaElementTrack: load() is only supported for tracks that own their audio element (constructed from a URL string). A track constructed from an existing HTMLAudioElement cannot swap its source in place."
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this._cancelFades();
|
|
202
|
+
this.audioElement.pause();
|
|
203
|
+
this.audioElement.src = source;
|
|
204
|
+
this.audioElement.load();
|
|
205
|
+
this.audioElement.currentTime = 0;
|
|
206
|
+
this._peaks = opts.peaks ?? null;
|
|
207
|
+
if (opts.name !== void 0) {
|
|
208
|
+
this._name = opts.name;
|
|
209
|
+
}
|
|
210
|
+
this.audioElement.playbackRate = this._playbackRate;
|
|
211
|
+
}
|
|
154
212
|
/**
|
|
155
213
|
* Pause playback
|
|
156
214
|
*/
|
|
@@ -257,12 +315,47 @@ var MediaElementTrack = class {
|
|
|
257
315
|
}
|
|
258
316
|
this._volumeGain.connect(this._audioContext.destination);
|
|
259
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Subscribe to a track lifecycle event. Multiple listeners per event are
|
|
320
|
+
* supported. Mirrors PlaylistEngine's on()/off() emitter.
|
|
321
|
+
*/
|
|
322
|
+
on(event, listener) {
|
|
323
|
+
if (!this._listeners.has(event)) {
|
|
324
|
+
this._listeners.set(event, /* @__PURE__ */ new Set());
|
|
325
|
+
}
|
|
326
|
+
this._listeners.get(event).add(listener);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Unsubscribe a previously registered listener.
|
|
330
|
+
*/
|
|
331
|
+
off(event, listener) {
|
|
332
|
+
this._listeners.get(event)?.delete(listener);
|
|
333
|
+
}
|
|
334
|
+
_emit(event, ...args) {
|
|
335
|
+
const listeners = this._listeners.get(event);
|
|
336
|
+
if (listeners) {
|
|
337
|
+
for (const listener of listeners) {
|
|
338
|
+
try {
|
|
339
|
+
listener(...args);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.warn(
|
|
342
|
+
"[waveform-playlist] MediaElementTrack: error in event listener: " + String(error)
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
260
348
|
/**
|
|
261
349
|
* Clean up resources
|
|
262
350
|
*/
|
|
263
351
|
dispose() {
|
|
264
352
|
this.audioElement.removeEventListener("ended", this.handleEnded);
|
|
265
353
|
this.audioElement.removeEventListener("timeupdate", this.handleTimeUpdate);
|
|
354
|
+
this.audioElement.removeEventListener("loadedmetadata", this.handleLoadedMetadata);
|
|
355
|
+
this.audioElement.removeEventListener("play", this.handlePlay);
|
|
356
|
+
this.audioElement.removeEventListener("pause", this.handlePause);
|
|
357
|
+
this.audioElement.removeEventListener("error", this.handleError);
|
|
358
|
+
this._listeners.clear();
|
|
266
359
|
this._cancelFades();
|
|
267
360
|
this.audioElement.pause();
|
|
268
361
|
if (this._sourceNode) {
|
|
@@ -345,6 +438,8 @@ var MediaElementPlayout = class {
|
|
|
345
438
|
constructor(options = {}) {
|
|
346
439
|
this.track = null;
|
|
347
440
|
this._isPlaying = false;
|
|
441
|
+
/** Consumer event listeners, retained so they re-attach across track swaps. */
|
|
442
|
+
this._eventListeners = /* @__PURE__ */ new Map();
|
|
348
443
|
this._masterVolume = options.masterVolume ?? 1;
|
|
349
444
|
this._playbackRate = options.playbackRate ?? 1;
|
|
350
445
|
this._preservesPitch = options.preservesPitch ?? true;
|
|
@@ -380,8 +475,30 @@ var MediaElementPlayout = class {
|
|
|
380
475
|
this.onPlaybackCompleteCallback();
|
|
381
476
|
}
|
|
382
477
|
});
|
|
478
|
+
this._attachListenersToTrack();
|
|
383
479
|
return this.track;
|
|
384
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Replace the playout's source (player-mode affordance). The documented
|
|
483
|
+
* single-track replace path — does NOT warn like addTrack().
|
|
484
|
+
*
|
|
485
|
+
* For URL (string) sources with an existing track, swaps in place via
|
|
486
|
+
* track.load(), reusing the element and preserving Web Audio routing. For a
|
|
487
|
+
* provided HTMLAudioElement source, or when there is no track yet, (re)creates
|
|
488
|
+
* the track. Returns the active track.
|
|
489
|
+
*/
|
|
490
|
+
setSource(options) {
|
|
491
|
+
this._isPlaying = false;
|
|
492
|
+
if (this.track && typeof options.source === "string") {
|
|
493
|
+
this.track.load(options.source, { peaks: options.peaks, name: options.name });
|
|
494
|
+
return this.track;
|
|
495
|
+
}
|
|
496
|
+
if (this.track) {
|
|
497
|
+
this.track.dispose();
|
|
498
|
+
this.track = null;
|
|
499
|
+
}
|
|
500
|
+
return this.addTrack(options);
|
|
501
|
+
}
|
|
385
502
|
/**
|
|
386
503
|
* Remove a track by ID.
|
|
387
504
|
*/
|
|
@@ -426,6 +543,15 @@ var MediaElementPlayout = class {
|
|
|
426
543
|
}, adjustedDuration * 1e3);
|
|
427
544
|
}
|
|
428
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Resume playback from the current position (player-mode affordance).
|
|
548
|
+
* Unlike play() with no offset (which resets to 0), this keeps currentTime.
|
|
549
|
+
* Delegates to play() with the current position as the offset, so all of
|
|
550
|
+
* play()'s machinery (AudioContext resume, fades, _isPlaying) is reused.
|
|
551
|
+
*/
|
|
552
|
+
resume() {
|
|
553
|
+
this.play(void 0, this.getCurrentTime());
|
|
554
|
+
}
|
|
429
555
|
/**
|
|
430
556
|
* Pause playback.
|
|
431
557
|
*/
|
|
@@ -501,6 +627,43 @@ var MediaElementPlayout = class {
|
|
|
501
627
|
setOnPlaybackComplete(callback) {
|
|
502
628
|
this.onPlaybackCompleteCallback = callback;
|
|
503
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* Subscribe to a lifecycle event (loadedmetadata / play / pause / error /
|
|
632
|
+
* ended / timeupdate) without reaching into track.element. Listeners are
|
|
633
|
+
* retained and re-attached automatically when the source is swapped.
|
|
634
|
+
*/
|
|
635
|
+
on(event, listener) {
|
|
636
|
+
if (!this._eventListeners.has(event)) {
|
|
637
|
+
this._eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
638
|
+
}
|
|
639
|
+
this._eventListeners.get(event).add(listener);
|
|
640
|
+
this.track?.on(event, listener);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Unsubscribe a previously registered lifecycle listener.
|
|
644
|
+
*/
|
|
645
|
+
off(event, listener) {
|
|
646
|
+
this._eventListeners.get(event)?.delete(listener);
|
|
647
|
+
this.track?.off(event, listener);
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Attach every registered listener to the current track. Called after a new
|
|
651
|
+
* track is created so subscriptions survive source swaps. The cast is safe:
|
|
652
|
+
* the event→listener correlation was enforced by the typed on() that filled
|
|
653
|
+
* the registry; TS cannot track it through this loop.
|
|
654
|
+
*/
|
|
655
|
+
_attachListenersToTrack() {
|
|
656
|
+
const track = this.track;
|
|
657
|
+
if (!track) return;
|
|
658
|
+
for (const [event, listeners] of this._eventListeners) {
|
|
659
|
+
for (const listener of listeners) {
|
|
660
|
+
track.on(
|
|
661
|
+
event,
|
|
662
|
+
listener
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
504
667
|
/**
|
|
505
668
|
* Clean up resources.
|
|
506
669
|
*/
|
|
@@ -509,6 +672,7 @@ var MediaElementPlayout = class {
|
|
|
509
672
|
this.track.dispose();
|
|
510
673
|
this.track = null;
|
|
511
674
|
}
|
|
675
|
+
this._eventListeners.clear();
|
|
512
676
|
}
|
|
513
677
|
// Getters
|
|
514
678
|
get isPlaying() {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/MediaElementTrack.ts","../src/MediaElementPlayout.ts","../src/types.ts"],"sourcesContent":["import type { WaveformDataObject, FadeConfig } from '@waveform-playlist/core';\nimport { applyFadeIn, applyFadeOut, generateCurve } from '@waveform-playlist/core';\n\nexport type { FadeConfig } from '@waveform-playlist/core';\n\n/**\n * Extended HTMLAudioElement with vendor-prefixed preservesPitch properties.\n * `preservesPitch` is standard; the `moz` and `webkit` prefixes are for older browsers.\n */\ninterface VendorPrefixedPitch {\n preservesPitch?: boolean;\n mozPreservesPitch?: boolean;\n webkitPreservesPitch?: boolean;\n}\n\nexport interface MediaElementTrackOptions {\n /** The audio source - can be a URL, Blob URL, or HTMLAudioElement */\n source: string | HTMLAudioElement;\n /** Pre-computed waveform data for visualization. Optional — omit for\n * scrubber-only / headless players that don't render a waveform. */\n peaks?: WaveformDataObject;\n /** Track ID */\n id?: string;\n /** Track name for display */\n name?: string;\n /** Initial volume (0.0 to 1.0) */\n volume?: number;\n /** Initial playback rate (0.5 to 2.0) */\n playbackRate?: number;\n /** Whether to preserve pitch when changing playback rate (default: true) */\n preservesPitch?: boolean;\n /**\n * AudioContext for Web Audio routing.\n * When provided, audio is routed through Web Audio nodes for fades and effects:\n * HTMLAudioElement → MediaElementSourceNode → fadeGain → volumeGain → destination\n *\n * Without this, playback uses HTMLAudioElement directly (no fades or effects).\n *\n * Note: createMediaElementSource() can only be called once per element.\n * Once routed, HTMLAudioElement.volume no longer works — volume is controlled\n * via the Web Audio GainNode instead.\n */\n audioContext?: AudioContext;\n /** Fade in configuration (requires audioContext) */\n fadeIn?: FadeConfig;\n /** Fade out configuration (requires audioContext) */\n fadeOut?: FadeConfig;\n}\n\n/**\n * Single-track playback using HTMLAudioElement.\n *\n * Benefits over AudioBuffer/Tone.js:\n * - Pitch-preserving playback rate (0.5x - 2.0x) via browser's built-in algorithm\n * - No AudioBuffer decoding required (uses pre-computed peaks for visualization)\n * - Simpler, lighter-weight for single-track use cases\n *\n * When an AudioContext is provided:\n * - Audio routes through Web Audio graph for fades and effects\n * - Volume controlled via GainNode (HTMLAudioElement.volume is bypassed)\n * - Output node exposed for connecting external effects chains\n */\nexport class MediaElementTrack {\n private audioElement: HTMLAudioElement;\n private ownsElement: boolean; // Whether we created the element (need to clean up)\n private _peaks: WaveformDataObject | null;\n private _id: string;\n private _name: string;\n private _playbackRate: number = 1;\n private _volume: number;\n private onStopCallback?: () => void;\n private onTimeUpdateCallback?: (time: number) => void;\n\n // Web Audio nodes (only when audioContext is provided)\n private _audioContext: AudioContext | null = null;\n private _sourceNode: MediaElementAudioSourceNode | null = null;\n private _fadeGain: GainNode | null = null;\n private _volumeGain: GainNode | null = null;\n private _fadeIn: FadeConfig | undefined;\n private _fadeOut: FadeConfig | undefined;\n\n constructor(options: MediaElementTrackOptions) {\n this._peaks = options.peaks ?? null;\n this._id = options.id ?? `track-${Date.now()}`;\n this._name = options.name ?? 'Track';\n this._playbackRate = options.playbackRate ?? 1;\n this._volume = options.volume ?? 1;\n this._fadeIn = options.fadeIn;\n this._fadeOut = options.fadeOut;\n\n // Create or use provided audio element\n if (typeof options.source === 'string') {\n this.audioElement = new Audio(options.source);\n this.ownsElement = true;\n } else {\n this.audioElement = options.source;\n this.ownsElement = false;\n }\n\n // Configure audio element\n this.audioElement.preload = 'auto';\n this.audioElement.playbackRate = this._playbackRate;\n\n // Set pitch preservation (default: true).\n // When false, the browser won't apply its own pitch correction — useful\n // when an external processor like SoundTouch handles pitch compensation.\n // Vendor-prefixed properties are non-standard; cast once for type safety.\n const shouldPreservePitch = options.preservesPitch ?? true;\n const audio = this.audioElement as unknown as VendorPrefixedPitch;\n if ('preservesPitch' in this.audioElement) {\n audio.preservesPitch = shouldPreservePitch;\n } else if ('mozPreservesPitch' in this.audioElement) {\n audio.mozPreservesPitch = shouldPreservePitch;\n } else if ('webkitPreservesPitch' in this.audioElement) {\n audio.webkitPreservesPitch = shouldPreservePitch;\n }\n\n // Set up Web Audio routing if AudioContext provided\n if (options.audioContext) {\n this._audioContext = options.audioContext;\n try {\n this._sourceNode = options.audioContext.createMediaElementSource(this.audioElement);\n } catch (err) {\n throw new Error(\n '[waveform-playlist] MediaElementTrack: createMediaElementSource() failed. ' +\n 'This can happen if the audio element is already connected to another AudioContext. ' +\n 'Each audio element can only have one MediaElementSourceNode. ' +\n 'Original error: ' +\n String(err)\n );\n }\n this._fadeGain = options.audioContext.createGain();\n this._volumeGain = options.audioContext.createGain();\n this._volumeGain.gain.value = this._volume;\n\n this._sourceNode.connect(this._fadeGain);\n this._fadeGain.connect(this._volumeGain);\n this._volumeGain.connect(options.audioContext.destination);\n\n // With Web Audio routing, HTMLAudioElement.volume is bypassed.\n // Set it to 1 so it doesn't attenuate the signal before the source node.\n this.audioElement.volume = 1;\n } else {\n // Without Web Audio, use HTMLAudioElement.volume directly\n this.audioElement.volume = this._volume;\n }\n\n // Set up event listeners\n this.audioElement.addEventListener('ended', this.handleEnded);\n this.audioElement.addEventListener('timeupdate', this.handleTimeUpdate);\n }\n\n private handleEnded = () => {\n this._cancelFades();\n if (this.onStopCallback) {\n this.onStopCallback();\n }\n };\n\n private handleTimeUpdate = () => {\n if (this.onTimeUpdateCallback) {\n this.onTimeUpdateCallback(this.audioElement.currentTime);\n }\n };\n\n /**\n * Schedule fade automation on the fade GainNode.\n * Called at the start of each play() — fades are relative to the playback offset.\n */\n private _scheduleFades(offset: number): void {\n if (!this._fadeGain || !this._audioContext) return;\n\n const fadeGain = this._fadeGain.gain;\n const now = this._audioContext.currentTime;\n const totalDuration = this.duration;\n\n // Reset fade gain\n fadeGain.cancelScheduledValues(0);\n fadeGain.setValueAtTime(1, now);\n\n // Fade in\n if (this._fadeIn && this._fadeIn.duration > 0) {\n const fadeInEnd = this._fadeIn.duration;\n if (offset < fadeInEnd) {\n const remainingFade = fadeInEnd - offset;\n const fadeType = this._fadeIn.type ?? 'linear';\n if (offset === 0) {\n // Full fade from beginning\n applyFadeIn(fadeGain, now, remainingFade, fadeType, 0, 1);\n } else {\n // Partial fade — slice the original curve to preserve shape\n const curve = generateCurve(fadeType, 1000, true);\n const startIndex = Math.round((offset / this._fadeIn.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingFade);\n }\n }\n }\n\n // Fade out\n if (this._fadeOut && this._fadeOut.duration > 0) {\n const fadeOutStart = totalDuration - this._fadeOut.duration;\n if (offset < totalDuration && fadeOutStart < totalDuration) {\n if (offset > fadeOutStart) {\n // Already past the fade-out start — slice original curve to preserve shape\n const elapsed = offset - fadeOutStart;\n const fadeType = this._fadeOut.type ?? 'linear';\n const curve = generateCurve(fadeType, 1000, false);\n const startIndex = Math.round((elapsed / this._fadeOut.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n const remainingDuration = this._fadeOut.duration - elapsed;\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingDuration);\n } else {\n // Schedule full fade-out at the right time\n const delayUntilFadeOut = fadeOutStart - offset;\n applyFadeOut(\n fadeGain,\n now + delayUntilFadeOut,\n this._fadeOut.duration,\n this._fadeOut.type ?? 'linear',\n 1,\n 0\n );\n }\n }\n }\n }\n\n /**\n * Cancel any scheduled fade automation.\n */\n private _cancelFades(): void {\n if (this._fadeGain) {\n this._fadeGain.gain.cancelScheduledValues(0);\n this._fadeGain.gain.value = 1;\n }\n }\n\n /**\n * Start playback from a specific time.\n * Resumes the AudioContext first if suspended, then schedules fades\n * (fades depend on audioContext.currentTime being non-zero).\n */\n play(offset: number = 0): void {\n const startPlayback = () => {\n this._scheduleFades(offset);\n this.audioElement.currentTime = offset;\n this.audioElement.play().catch((err) => {\n console.warn('[waveform-playlist] MediaElementTrack: play() failed: ' + String(err));\n });\n };\n\n // Resume AudioContext if suspended (browser autoplay policy).\n // Must await resume before scheduling fades — audioContext.currentTime\n // is 0 while suspended, which would schedule all fades in the past.\n if (this._audioContext && this._audioContext.state === 'suspended') {\n this._audioContext\n .resume()\n .then(startPlayback)\n .catch((err) => {\n console.warn(\n '[waveform-playlist] MediaElementTrack: AudioContext.resume() failed: ' + String(err)\n );\n });\n } else {\n startPlayback();\n }\n }\n\n /**\n * Pause playback\n */\n pause(): void {\n this._cancelFades();\n this.audioElement.pause();\n }\n\n /**\n * Stop playback and reset to beginning\n */\n stop(): void {\n this._cancelFades();\n this.audioElement.pause();\n this.audioElement.currentTime = 0;\n }\n\n /**\n * Seek to a specific time\n */\n seekTo(time: number): void {\n this.audioElement.currentTime = Math.max(0, Math.min(time, this.duration));\n }\n\n /**\n * Set volume (0.0 to 1.0)\n */\n setVolume(volume: number): void {\n this._volume = Math.max(0, Math.min(1, volume));\n if (this._volumeGain) {\n this._volumeGain.gain.value = this._volume;\n } else {\n this.audioElement.volume = this._volume;\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved)\n */\n setPlaybackRate(rate: number): void {\n const clampedRate = Math.max(0.5, Math.min(2.0, rate));\n this._playbackRate = clampedRate;\n this.audioElement.playbackRate = clampedRate;\n }\n\n /**\n * Set muted state\n */\n setMuted(muted: boolean): void {\n this.audioElement.muted = muted;\n }\n\n /**\n * Set fade in configuration\n */\n setFadeIn(fadeIn: FadeConfig | undefined): void {\n this._fadeIn = fadeIn;\n }\n\n /**\n * Set fade out configuration\n */\n setFadeOut(fadeOut: FadeConfig | undefined): void {\n this._fadeOut = fadeOut;\n }\n\n /**\n * Set callback for when playback ends\n */\n setOnStopCallback(callback: () => void): void {\n this.onStopCallback = callback;\n }\n\n /**\n * Set callback for time updates\n */\n setOnTimeUpdateCallback(callback: (time: number) => void): void {\n this.onTimeUpdateCallback = callback;\n }\n\n /**\n * Connect the output to a different destination (for effects chains).\n * Disconnects from the current destination first.\n *\n * @param destination - The AudioNode to connect to\n */\n connectOutput(destination: AudioNode): void {\n if (!this._volumeGain) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: connectOutput() requires audioContext. ' +\n 'Pass audioContext in constructor options.'\n );\n return;\n }\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before connectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(destination);\n }\n\n /**\n * Disconnect the output and reconnect to the default AudioContext destination.\n */\n disconnectOutput(): void {\n if (!this._volumeGain || !this._audioContext) return;\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before disconnectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(this._audioContext.destination);\n }\n\n /**\n * Clean up resources\n */\n dispose(): void {\n this.audioElement.removeEventListener('ended', this.handleEnded);\n this.audioElement.removeEventListener('timeupdate', this.handleTimeUpdate);\n this._cancelFades();\n this.audioElement.pause();\n\n if (this._sourceNode) {\n try {\n this._sourceNode.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: sourceNode disconnect failed: ' + String(err)\n );\n }\n }\n if (this._fadeGain) {\n try {\n this._fadeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: fadeGain disconnect failed: ' + String(err)\n );\n }\n }\n if (this._volumeGain) {\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: volumeGain disconnect failed: ' + String(err)\n );\n }\n }\n\n if (this.ownsElement) {\n this.audioElement.src = '';\n this.audioElement.load(); // Release resources\n }\n }\n\n // Getters\n get id(): string {\n return this._id;\n }\n\n get name(): string {\n return this._name;\n }\n\n get peaks(): WaveformDataObject | null {\n return this._peaks;\n }\n\n get currentTime(): number {\n return this.audioElement.currentTime;\n }\n\n get duration(): number {\n return this.audioElement.duration || this._peaks?.duration || 0;\n }\n\n get isPlaying(): boolean {\n return !this.audioElement.paused && !this.audioElement.ended;\n }\n\n get volume(): number {\n return this._volume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get muted(): boolean {\n return this.audioElement.muted;\n }\n\n /**\n * Get the underlying audio element (for advanced use cases)\n */\n get element(): HTMLAudioElement {\n return this.audioElement;\n }\n\n /**\n * Get the volume GainNode output (for connecting effects chains).\n * Returns null if no AudioContext was provided.\n */\n get outputNode(): GainNode | null {\n return this._volumeGain;\n }\n}\n","import { MediaElementTrack, type MediaElementTrackOptions } from './MediaElementTrack';\n\nexport interface MediaElementPlayoutOptions {\n /** Initial master volume (0.0 to 1.0) */\n masterVolume?: number;\n /** Initial playback rate (0.5 to 2.0) */\n playbackRate?: number;\n /** Whether to preserve pitch when changing playback rate (default: true).\n * Set to false when using an external pitch processor like SoundTouch. */\n preservesPitch?: boolean;\n}\n\n/**\n * Single-track playout engine using HTMLAudioElement.\n *\n * This is a lightweight alternative to TonePlayout for single-track use cases\n * that need pitch-preserving playback rate control.\n *\n * Key features:\n * - Pitch-preserving playback rate (0.5x - 2.0x)\n * - Uses pre-computed peaks (no AudioBuffer required)\n * - Simpler API for single-track playback\n *\n * Limitations:\n * - Single track only - will warn if multiple tracks added\n * - No multi-track mixing\n *\n * For multi-track editing, use TonePlayout from @waveform-playlist/playout instead.\n */\nexport class MediaElementPlayout {\n private track: MediaElementTrack | null = null;\n private _masterVolume: number;\n private _playbackRate: number;\n private _preservesPitch: boolean;\n private _isPlaying: boolean = false;\n private onPlaybackCompleteCallback?: () => void;\n\n constructor(options: MediaElementPlayoutOptions = {}) {\n this._masterVolume = options.masterVolume ?? 1;\n this._playbackRate = options.playbackRate ?? 1;\n this._preservesPitch = options.preservesPitch ?? true;\n }\n\n /**\n * Initialize the playout engine.\n * For MediaElementPlayout this is a no-op — HTMLAudioElement doesn't require\n * explicit initialization. When an AudioContext is provided for fades/effects,\n * it resumes automatically on first play via MediaElementTrack.\n */\n async init(): Promise<void> {\n // No initialization needed — audio element handles autoplay policy automatically\n }\n\n /**\n * Add a track to the playout.\n * Note: Only one track is supported. Adding a second track will dispose the first.\n */\n addTrack(options: MediaElementTrackOptions): MediaElementTrack {\n if (this.track) {\n console.warn(\n 'MediaElementPlayout: Only one track is supported. ' +\n 'Disposing previous track. For multi-track, use TonePlayout.'\n );\n this.track.dispose();\n }\n\n this.track = new MediaElementTrack({\n ...options,\n volume: this._masterVolume * (options.volume ?? 1),\n playbackRate: this._playbackRate,\n preservesPitch: this._preservesPitch,\n });\n\n // Set up stop callback\n this.track.setOnStopCallback(() => {\n this._isPlaying = false;\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n });\n\n return this.track;\n }\n\n /**\n * Remove a track by ID.\n */\n removeTrack(trackId: string): void {\n if (this.track && this.track.id === trackId) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n /**\n * Get a track by ID.\n */\n getTrack(trackId: string): MediaElementTrack | undefined {\n if (this.track && this.track.id === trackId) {\n return this.track;\n }\n return undefined;\n }\n\n /**\n * Start playback.\n * @param _when - Ignored (HTMLAudioElement doesn't support scheduled start)\n * @param offset - Start position in seconds\n * @param duration - Duration to play in seconds (optional)\n */\n play(_when?: number, offset?: number, duration?: number): void {\n if (!this.track) {\n console.warn('MediaElementPlayout: No track to play');\n return;\n }\n\n const startPosition = offset ?? 0;\n this._isPlaying = true;\n\n this.track.play(startPosition);\n\n // If duration is specified, schedule stop\n if (duration !== undefined) {\n const adjustedDuration = duration / this._playbackRate;\n setTimeout(() => {\n if (this._isPlaying) {\n this.pause();\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n }\n }, adjustedDuration * 1000);\n }\n }\n\n /**\n * Pause playback.\n */\n pause(): void {\n if (this.track) {\n this.track.pause();\n }\n this._isPlaying = false;\n }\n\n /**\n * Stop playback and reset to start.\n */\n stop(): void {\n if (this.track) {\n this.track.stop();\n }\n this._isPlaying = false;\n }\n\n /**\n * Seek to a specific time.\n */\n seekTo(time: number): void {\n if (this.track) {\n this.track.seekTo(time);\n }\n }\n\n /**\n * Get current playback time.\n */\n getCurrentTime(): number {\n if (this.track) {\n return this.track.currentTime;\n }\n return 0;\n }\n\n /**\n * Set master volume.\n */\n setMasterVolume(volume: number): void {\n this._masterVolume = Math.max(0, Math.min(1, volume));\n if (this.track) {\n this.track.setVolume(this._masterVolume);\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved).\n */\n setPlaybackRate(rate: number): void {\n this._playbackRate = Math.max(0.5, Math.min(2.0, rate));\n if (this.track) {\n this.track.setPlaybackRate(this._playbackRate);\n }\n }\n\n /**\n * Set mute state for a track.\n */\n setMute(trackId: string, muted: boolean): void {\n const track = this.getTrack(trackId);\n if (track) {\n track.setMuted(muted);\n }\n }\n\n /**\n * Set solo state for a track.\n * Note: With single track, solo is effectively the same as unmute.\n */\n setSolo(_trackId: string, _soloed: boolean): void {\n // No-op for single track - solo doesn't make sense\n console.warn('MediaElementPlayout: Solo is not applicable for single-track playback');\n }\n\n /**\n * Set callback for when playback completes.\n */\n setOnPlaybackComplete(callback: () => void): void {\n this.onPlaybackCompleteCallback = callback;\n }\n\n /**\n * Clean up resources.\n */\n dispose(): void {\n if (this.track) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n // Getters\n get isPlaying(): boolean {\n return this._isPlaying;\n }\n\n get masterVolume(): number {\n return this._masterVolume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get duration(): number {\n return this.track?.duration ?? 0;\n }\n\n get sampleRate(): number {\n // HTMLAudioElement doesn't expose sample rate directly\n // Return a common default - peaks will have the actual sample rate\n return this.track?.peaks?.sample_rate ?? 44100;\n }\n\n /**\n * Get the volume GainNode output for connecting external effects chains.\n * Returns null if no AudioContext was provided to the track.\n *\n * Usage: disconnect from default destination, connect to effect input,\n * then connect effect output to audioContext.destination.\n */\n get outputNode(): GainNode | null {\n return this.track?.outputNode ?? null;\n }\n}\n","/**\n * Common interface for playout engines.\n *\n * Both TonePlayout and MediaElementPlayout implement this interface,\n * allowing the browser package to work with either engine.\n */\nexport interface PlayoutEngine {\n // Lifecycle\n init(): Promise<void>;\n dispose(): void;\n\n // Playback\n play(when?: number, offset?: number, duration?: number): void;\n pause(): void;\n stop(): void;\n seekTo(time: number): void;\n getCurrentTime(): number;\n\n // Volume\n setMasterVolume(volume: number): void;\n\n // Track controls (optional - not all engines support all features)\n setMute?(trackId: string, muted: boolean): void;\n setSolo?(trackId: string, soloed: boolean): void;\n\n // Callbacks\n setOnPlaybackComplete(callback: () => void): void;\n\n // State\n readonly isPlaying: boolean;\n readonly duration: number;\n readonly sampleRate: number;\n}\n\n/**\n * Extended interface for engines that support playback rate.\n */\nexport interface PlaybackRateEngine extends PlayoutEngine {\n setPlaybackRate(rate: number): void;\n readonly playbackRate: number;\n}\n\n/**\n * Type guard to check if an engine supports playback rate.\n */\nexport function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine {\n return (\n 'setPlaybackRate' in engine &&\n typeof (engine as Record<string, unknown>).setPlaybackRate === 'function'\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/MediaElementTrack.ts","../src/MediaElementPlayout.ts","../src/types.ts"],"sourcesContent":["import type { WaveformDataObject, FadeConfig } from '@waveform-playlist/core';\nimport { applyFadeIn, applyFadeOut, generateCurve } from '@waveform-playlist/core';\n\nexport type { FadeConfig } from '@waveform-playlist/core';\n\n/**\n * Extended HTMLAudioElement with vendor-prefixed preservesPitch properties.\n * `preservesPitch` is standard; the `moz` and `webkit` prefixes are for older browsers.\n */\ninterface VendorPrefixedPitch {\n preservesPitch?: boolean;\n mozPreservesPitch?: boolean;\n webkitPreservesPitch?: boolean;\n}\n\nexport interface MediaElementTrackOptions {\n /** The audio source - can be a URL, Blob URL, or HTMLAudioElement */\n source: string | HTMLAudioElement;\n /** Pre-computed waveform data for visualization. Optional — omit for\n * scrubber-only / headless players that don't render a waveform. */\n peaks?: WaveformDataObject;\n /** Track ID */\n id?: string;\n /** Track name for display */\n name?: string;\n /** Initial volume (0.0 to 1.0) */\n volume?: number;\n /** Initial playback rate (0.5 to 2.0) */\n playbackRate?: number;\n /** Whether to preserve pitch when changing playback rate (default: true) */\n preservesPitch?: boolean;\n /**\n * AudioContext for Web Audio routing.\n * When provided, audio is routed through Web Audio nodes for fades and effects:\n * HTMLAudioElement → MediaElementSourceNode → fadeGain → volumeGain → destination\n *\n * Without this, playback uses HTMLAudioElement directly (no fades or effects).\n *\n * Note: createMediaElementSource() can only be called once per element.\n * Once routed, HTMLAudioElement.volume no longer works — volume is controlled\n * via the Web Audio GainNode instead.\n */\n audioContext?: AudioContext;\n /** Fade in configuration (requires audioContext) */\n fadeIn?: FadeConfig;\n /** Fade out configuration (requires audioContext) */\n fadeOut?: FadeConfig;\n}\n\n/**\n * Typed event map for MediaElementTrack's emitter. Mirrors the on()/off()\n * pattern used by @waveform-playlist/engine's PlaylistEngine so consumers and\n * the dawcore web-components layer can wire events uniformly across engines.\n */\nexport interface MediaElementTrackEvents {\n /** Fired when the element's metadata (duration, dimensions) has loaded. */\n loadedmetadata: () => void;\n /** Fired when native playback starts/resumes. */\n play: () => void;\n /** Fired when native playback pauses (including at end-of-media). */\n pause: () => void;\n /** Fired on a media error; carries the element's MediaError (or null). */\n error: (err: MediaError | null) => void;\n /** Fired when playback reaches the end of the media. */\n ended: () => void;\n /** Fired on each native timeupdate; carries the current time in seconds. */\n timeupdate: (time: number) => void;\n}\n\n/**\n * Single-track playback using HTMLAudioElement.\n *\n * Benefits over AudioBuffer/Tone.js:\n * - Pitch-preserving playback rate (0.5x - 2.0x) via browser's built-in algorithm\n * - No AudioBuffer decoding required (uses pre-computed peaks for visualization)\n * - Simpler, lighter-weight for single-track use cases\n *\n * When an AudioContext is provided:\n * - Audio routes through Web Audio graph for fades and effects\n * - Volume controlled via GainNode (HTMLAudioElement.volume is bypassed)\n * - Output node exposed for connecting external effects chains\n */\nexport class MediaElementTrack {\n private audioElement: HTMLAudioElement;\n private ownsElement: boolean; // Whether we created the element (need to clean up)\n private _peaks: WaveformDataObject | null;\n private _id: string;\n private _name: string;\n private _playbackRate: number = 1;\n private _volume: number;\n private onStopCallback?: () => void;\n private onTimeUpdateCallback?: (time: number) => void;\n private _listeners: Map<string, Set<MediaElementTrackEvents[keyof MediaElementTrackEvents]>> =\n new Map();\n\n // Web Audio nodes (only when audioContext is provided)\n private _audioContext: AudioContext | null = null;\n private _sourceNode: MediaElementAudioSourceNode | null = null;\n private _fadeGain: GainNode | null = null;\n private _volumeGain: GainNode | null = null;\n private _fadeIn: FadeConfig | undefined;\n private _fadeOut: FadeConfig | undefined;\n\n constructor(options: MediaElementTrackOptions) {\n this._peaks = options.peaks ?? null;\n this._id = options.id ?? `track-${Date.now()}`;\n this._name = options.name ?? 'Track';\n this._playbackRate = options.playbackRate ?? 1;\n this._volume = options.volume ?? 1;\n this._fadeIn = options.fadeIn;\n this._fadeOut = options.fadeOut;\n\n // Create or use provided audio element\n if (typeof options.source === 'string') {\n this.audioElement = new Audio(options.source);\n this.ownsElement = true;\n } else {\n this.audioElement = options.source;\n this.ownsElement = false;\n }\n\n // Configure audio element\n this.audioElement.preload = 'auto';\n this.audioElement.playbackRate = this._playbackRate;\n\n // Set pitch preservation (default: true).\n // When false, the browser won't apply its own pitch correction — useful\n // when an external processor like SoundTouch handles pitch compensation.\n // Vendor-prefixed properties are non-standard; cast once for type safety.\n const shouldPreservePitch = options.preservesPitch ?? true;\n const audio = this.audioElement as unknown as VendorPrefixedPitch;\n if ('preservesPitch' in this.audioElement) {\n audio.preservesPitch = shouldPreservePitch;\n } else if ('mozPreservesPitch' in this.audioElement) {\n audio.mozPreservesPitch = shouldPreservePitch;\n } else if ('webkitPreservesPitch' in this.audioElement) {\n audio.webkitPreservesPitch = shouldPreservePitch;\n }\n\n // Set up Web Audio routing if AudioContext provided\n if (options.audioContext) {\n this._audioContext = options.audioContext;\n try {\n this._sourceNode = options.audioContext.createMediaElementSource(this.audioElement);\n } catch (err) {\n throw new Error(\n '[waveform-playlist] MediaElementTrack: createMediaElementSource() failed. ' +\n 'This can happen if the audio element is already connected to another AudioContext. ' +\n 'Each audio element can only have one MediaElementSourceNode. ' +\n 'Original error: ' +\n String(err)\n );\n }\n this._fadeGain = options.audioContext.createGain();\n this._volumeGain = options.audioContext.createGain();\n this._volumeGain.gain.value = this._volume;\n\n this._sourceNode.connect(this._fadeGain);\n this._fadeGain.connect(this._volumeGain);\n this._volumeGain.connect(options.audioContext.destination);\n\n // With Web Audio routing, HTMLAudioElement.volume is bypassed.\n // Set it to 1 so it doesn't attenuate the signal before the source node.\n this.audioElement.volume = 1;\n } else {\n // Without Web Audio, use HTMLAudioElement.volume directly\n this.audioElement.volume = this._volume;\n }\n\n // Set up event listeners\n this.audioElement.addEventListener('ended', this.handleEnded);\n this.audioElement.addEventListener('timeupdate', this.handleTimeUpdate);\n this.audioElement.addEventListener('loadedmetadata', this.handleLoadedMetadata);\n this.audioElement.addEventListener('play', this.handlePlay);\n this.audioElement.addEventListener('pause', this.handlePause);\n this.audioElement.addEventListener('error', this.handleError);\n }\n\n private handleEnded = () => {\n this._cancelFades();\n if (this.onStopCallback) {\n this.onStopCallback();\n }\n this._emit('ended');\n };\n\n private handleTimeUpdate = () => {\n if (this.onTimeUpdateCallback) {\n this.onTimeUpdateCallback(this.audioElement.currentTime);\n }\n this._emit('timeupdate', this.audioElement.currentTime);\n };\n\n private handleLoadedMetadata = () => {\n this._emit('loadedmetadata');\n };\n\n private handlePlay = () => {\n this._emit('play');\n };\n\n private handlePause = () => {\n this._emit('pause');\n };\n\n private handleError = () => {\n this._emit('error', this.audioElement.error);\n };\n\n /**\n * Schedule fade automation on the fade GainNode.\n * Called at the start of each play() — fades are relative to the playback offset.\n */\n private _scheduleFades(offset: number): void {\n if (!this._fadeGain || !this._audioContext) return;\n\n const fadeGain = this._fadeGain.gain;\n const now = this._audioContext.currentTime;\n const totalDuration = this.duration;\n\n // Reset fade gain\n fadeGain.cancelScheduledValues(0);\n fadeGain.setValueAtTime(1, now);\n\n // Fade in\n if (this._fadeIn && this._fadeIn.duration > 0) {\n const fadeInEnd = this._fadeIn.duration;\n if (offset < fadeInEnd) {\n const remainingFade = fadeInEnd - offset;\n const fadeType = this._fadeIn.type ?? 'linear';\n if (offset === 0) {\n // Full fade from beginning\n applyFadeIn(fadeGain, now, remainingFade, fadeType, 0, 1);\n } else {\n // Partial fade — slice the original curve to preserve shape\n const curve = generateCurve(fadeType, 1000, true);\n const startIndex = Math.round((offset / this._fadeIn.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingFade);\n }\n }\n }\n\n // Fade out\n if (this._fadeOut && this._fadeOut.duration > 0) {\n const fadeOutStart = totalDuration - this._fadeOut.duration;\n if (offset < totalDuration && fadeOutStart < totalDuration) {\n if (offset > fadeOutStart) {\n // Already past the fade-out start — slice original curve to preserve shape\n const elapsed = offset - fadeOutStart;\n const fadeType = this._fadeOut.type ?? 'linear';\n const curve = generateCurve(fadeType, 1000, false);\n const startIndex = Math.round((elapsed / this._fadeOut.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n const remainingDuration = this._fadeOut.duration - elapsed;\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingDuration);\n } else {\n // Schedule full fade-out at the right time\n const delayUntilFadeOut = fadeOutStart - offset;\n applyFadeOut(\n fadeGain,\n now + delayUntilFadeOut,\n this._fadeOut.duration,\n this._fadeOut.type ?? 'linear',\n 1,\n 0\n );\n }\n }\n }\n }\n\n /**\n * Cancel any scheduled fade automation.\n */\n private _cancelFades(): void {\n if (this._fadeGain) {\n this._fadeGain.gain.cancelScheduledValues(0);\n this._fadeGain.gain.value = 1;\n }\n }\n\n /**\n * Start playback from a specific time.\n * Resumes the AudioContext first if suspended, then schedules fades\n * (fades depend on audioContext.currentTime being non-zero).\n */\n play(offset: number = 0): void {\n const startPlayback = () => {\n this._scheduleFades(offset);\n this.audioElement.currentTime = offset;\n this.audioElement.play().catch((err) => {\n console.warn('[waveform-playlist] MediaElementTrack: play() failed: ' + String(err));\n });\n };\n\n // Resume AudioContext if suspended (browser autoplay policy).\n // Must await resume before scheduling fades — audioContext.currentTime\n // is 0 while suspended, which would schedule all fades in the past.\n if (this._audioContext && this._audioContext.state === 'suspended') {\n this._audioContext\n .resume()\n .then(startPlayback)\n .catch((err) => {\n console.warn(\n '[waveform-playlist] MediaElementTrack: AudioContext.resume() failed: ' + String(err)\n );\n });\n } else {\n startPlayback();\n }\n }\n\n /**\n * Resume playback from the current position without resetting currentTime.\n * Reuses play()'s fade re-scheduling and AudioContext-resume machinery —\n * passing the current position as the offset is a no-op seek that leaves\n * playback where it was.\n */\n resume(): void {\n this.play(this.currentTime);\n }\n\n /**\n * Swap the audio source in place, reusing the existing <audio> element.\n * Because the MediaElementAudioSourceNode is once-per-element, reusing the\n * element preserves any Web Audio routing/effects across the swap.\n *\n * Only supported when this track owns its element (constructed from a URL\n * string). A borrowed element (constructed from an HTMLAudioElement) warns\n * and no-ops — swapping a consumer-owned element's src is out of contract.\n *\n * Peaks are coupled to the specific audio, so they are replaced (defaulting\n * to null when omitted). Name is a label, so it updates only when provided.\n */\n load(source: string, opts: { peaks?: WaveformDataObject; name?: string } = {}): void {\n if (!this.ownsElement) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: load() is only supported for tracks that ' +\n 'own their audio element (constructed from a URL string). A track constructed from ' +\n 'an existing HTMLAudioElement cannot swap its source in place.'\n );\n return;\n }\n this._cancelFades();\n this.audioElement.pause();\n this.audioElement.src = source;\n this.audioElement.load();\n this.audioElement.currentTime = 0;\n this._peaks = opts.peaks ?? null;\n if (opts.name !== undefined) {\n this._name = opts.name;\n }\n // The HTML load algorithm resets playbackRate to defaultPlaybackRate (1.0);\n // re-apply the configured rate so it survives the source swap.\n this.audioElement.playbackRate = this._playbackRate;\n }\n\n /**\n * Pause playback\n */\n pause(): void {\n this._cancelFades();\n this.audioElement.pause();\n }\n\n /**\n * Stop playback and reset to beginning\n */\n stop(): void {\n this._cancelFades();\n this.audioElement.pause();\n this.audioElement.currentTime = 0;\n }\n\n /**\n * Seek to a specific time\n */\n seekTo(time: number): void {\n this.audioElement.currentTime = Math.max(0, Math.min(time, this.duration));\n }\n\n /**\n * Set volume (0.0 to 1.0)\n */\n setVolume(volume: number): void {\n this._volume = Math.max(0, Math.min(1, volume));\n if (this._volumeGain) {\n this._volumeGain.gain.value = this._volume;\n } else {\n this.audioElement.volume = this._volume;\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved)\n */\n setPlaybackRate(rate: number): void {\n const clampedRate = Math.max(0.5, Math.min(2.0, rate));\n this._playbackRate = clampedRate;\n this.audioElement.playbackRate = clampedRate;\n }\n\n /**\n * Set muted state\n */\n setMuted(muted: boolean): void {\n this.audioElement.muted = muted;\n }\n\n /**\n * Set fade in configuration\n */\n setFadeIn(fadeIn: FadeConfig | undefined): void {\n this._fadeIn = fadeIn;\n }\n\n /**\n * Set fade out configuration\n */\n setFadeOut(fadeOut: FadeConfig | undefined): void {\n this._fadeOut = fadeOut;\n }\n\n /**\n * Set callback for when playback ends\n */\n setOnStopCallback(callback: () => void): void {\n this.onStopCallback = callback;\n }\n\n /**\n * Set callback for time updates\n */\n setOnTimeUpdateCallback(callback: (time: number) => void): void {\n this.onTimeUpdateCallback = callback;\n }\n\n /**\n * Connect the output to a different destination (for effects chains).\n * Disconnects from the current destination first.\n *\n * @param destination - The AudioNode to connect to\n */\n connectOutput(destination: AudioNode): void {\n if (!this._volumeGain) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: connectOutput() requires audioContext. ' +\n 'Pass audioContext in constructor options.'\n );\n return;\n }\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before connectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(destination);\n }\n\n /**\n * Disconnect the output and reconnect to the default AudioContext destination.\n */\n disconnectOutput(): void {\n if (!this._volumeGain || !this._audioContext) return;\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before disconnectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(this._audioContext.destination);\n }\n\n /**\n * Subscribe to a track lifecycle event. Multiple listeners per event are\n * supported. Mirrors PlaylistEngine's on()/off() emitter.\n */\n on<K extends keyof MediaElementTrackEvents>(\n event: K,\n listener: MediaElementTrackEvents[K]\n ): void {\n if (!this._listeners.has(event)) {\n this._listeners.set(event, new Set());\n }\n this._listeners.get(event)!.add(listener);\n }\n\n /**\n * Unsubscribe a previously registered listener.\n */\n off<K extends keyof MediaElementTrackEvents>(\n event: K,\n listener: MediaElementTrackEvents[K]\n ): void {\n this._listeners.get(event)?.delete(listener);\n }\n\n private _emit(event: string, ...args: unknown[]): void {\n const listeners = this._listeners.get(event);\n if (listeners) {\n for (const listener of listeners) {\n try {\n (listener as (...a: unknown[]) => void)(...args);\n } catch (error) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: error in event listener: ' + String(error)\n );\n }\n }\n }\n }\n\n /**\n * Clean up resources\n */\n dispose(): void {\n this.audioElement.removeEventListener('ended', this.handleEnded);\n this.audioElement.removeEventListener('timeupdate', this.handleTimeUpdate);\n this.audioElement.removeEventListener('loadedmetadata', this.handleLoadedMetadata);\n this.audioElement.removeEventListener('play', this.handlePlay);\n this.audioElement.removeEventListener('pause', this.handlePause);\n this.audioElement.removeEventListener('error', this.handleError);\n this._listeners.clear();\n this._cancelFades();\n this.audioElement.pause();\n\n if (this._sourceNode) {\n try {\n this._sourceNode.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: sourceNode disconnect failed: ' + String(err)\n );\n }\n }\n if (this._fadeGain) {\n try {\n this._fadeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: fadeGain disconnect failed: ' + String(err)\n );\n }\n }\n if (this._volumeGain) {\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: volumeGain disconnect failed: ' + String(err)\n );\n }\n }\n\n if (this.ownsElement) {\n this.audioElement.src = '';\n this.audioElement.load(); // Release resources\n }\n }\n\n // Getters\n get id(): string {\n return this._id;\n }\n\n get name(): string {\n return this._name;\n }\n\n get peaks(): WaveformDataObject | null {\n return this._peaks;\n }\n\n get currentTime(): number {\n return this.audioElement.currentTime;\n }\n\n get duration(): number {\n return this.audioElement.duration || this._peaks?.duration || 0;\n }\n\n get isPlaying(): boolean {\n return !this.audioElement.paused && !this.audioElement.ended;\n }\n\n get volume(): number {\n return this._volume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get muted(): boolean {\n return this.audioElement.muted;\n }\n\n /**\n * Get the underlying audio element (for advanced use cases)\n */\n get element(): HTMLAudioElement {\n return this.audioElement;\n }\n\n /**\n * Get the volume GainNode output (for connecting effects chains).\n * Returns null if no AudioContext was provided.\n */\n get outputNode(): GainNode | null {\n return this._volumeGain;\n }\n}\n","import {\n MediaElementTrack,\n type MediaElementTrackOptions,\n type MediaElementTrackEvents,\n} from './MediaElementTrack';\n\nexport interface MediaElementPlayoutOptions {\n /** Initial master volume (0.0 to 1.0) */\n masterVolume?: number;\n /** Initial playback rate (0.5 to 2.0) */\n playbackRate?: number;\n /** Whether to preserve pitch when changing playback rate (default: true).\n * Set to false when using an external pitch processor like SoundTouch. */\n preservesPitch?: boolean;\n}\n\n/**\n * Single-track playout engine using HTMLAudioElement.\n *\n * This is a lightweight alternative to TonePlayout for single-track use cases\n * that need pitch-preserving playback rate control.\n *\n * Key features:\n * - Pitch-preserving playback rate (0.5x - 2.0x)\n * - Uses pre-computed peaks (no AudioBuffer required)\n * - Simpler API for single-track playback\n *\n * Limitations:\n * - Single track only - will warn if multiple tracks added\n * - No multi-track mixing\n *\n * For multi-track editing, use TonePlayout from @waveform-playlist/playout instead.\n */\nexport class MediaElementPlayout {\n private track: MediaElementTrack | null = null;\n private _masterVolume: number;\n private _playbackRate: number;\n private _preservesPitch: boolean;\n private _isPlaying: boolean = false;\n private onPlaybackCompleteCallback?: () => void;\n /** Consumer event listeners, retained so they re-attach across track swaps. */\n private _eventListeners: Map<\n string,\n Set<MediaElementTrackEvents[keyof MediaElementTrackEvents]>\n > = new Map();\n\n constructor(options: MediaElementPlayoutOptions = {}) {\n this._masterVolume = options.masterVolume ?? 1;\n this._playbackRate = options.playbackRate ?? 1;\n this._preservesPitch = options.preservesPitch ?? true;\n }\n\n /**\n * Initialize the playout engine.\n * For MediaElementPlayout this is a no-op — HTMLAudioElement doesn't require\n * explicit initialization. When an AudioContext is provided for fades/effects,\n * it resumes automatically on first play via MediaElementTrack.\n */\n async init(): Promise<void> {\n // No initialization needed — audio element handles autoplay policy automatically\n }\n\n /**\n * Add a track to the playout.\n * Note: Only one track is supported. Adding a second track will dispose the first.\n */\n addTrack(options: MediaElementTrackOptions): MediaElementTrack {\n if (this.track) {\n console.warn(\n 'MediaElementPlayout: Only one track is supported. ' +\n 'Disposing previous track. For multi-track, use TonePlayout.'\n );\n this.track.dispose();\n }\n\n this.track = new MediaElementTrack({\n ...options,\n volume: this._masterVolume * (options.volume ?? 1),\n playbackRate: this._playbackRate,\n preservesPitch: this._preservesPitch,\n });\n\n // Set up stop callback\n this.track.setOnStopCallback(() => {\n this._isPlaying = false;\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n });\n\n // Re-attach any consumer listeners to the newly created track (covers the\n // first track and the addTrack-replace path).\n this._attachListenersToTrack();\n\n return this.track;\n }\n\n /**\n * Replace the playout's source (player-mode affordance). The documented\n * single-track replace path — does NOT warn like addTrack().\n *\n * For URL (string) sources with an existing track, swaps in place via\n * track.load(), reusing the element and preserving Web Audio routing. For a\n * provided HTMLAudioElement source, or when there is no track yet, (re)creates\n * the track. Returns the active track.\n */\n setSource(options: MediaElementTrackOptions): MediaElementTrack {\n // A source swap stops current playback (in-place load() pauses; recreate disposes).\n this._isPlaying = false;\n if (this.track && typeof options.source === 'string') {\n this.track.load(options.source, { peaks: options.peaks, name: options.name });\n return this.track;\n }\n // First source, or swapping to a provided element: (re)create the track.\n // Dispose silently — this is the documented single-track replace path, not\n // the multi-track misuse that addTrack() warns about.\n if (this.track) {\n this.track.dispose();\n this.track = null;\n }\n return this.addTrack(options);\n }\n\n /**\n * Remove a track by ID.\n */\n removeTrack(trackId: string): void {\n if (this.track && this.track.id === trackId) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n /**\n * Get a track by ID.\n */\n getTrack(trackId: string): MediaElementTrack | undefined {\n if (this.track && this.track.id === trackId) {\n return this.track;\n }\n return undefined;\n }\n\n /**\n * Start playback.\n * @param _when - Ignored (HTMLAudioElement doesn't support scheduled start)\n * @param offset - Start position in seconds\n * @param duration - Duration to play in seconds (optional)\n */\n play(_when?: number, offset?: number, duration?: number): void {\n if (!this.track) {\n console.warn('MediaElementPlayout: No track to play');\n return;\n }\n\n const startPosition = offset ?? 0;\n this._isPlaying = true;\n\n this.track.play(startPosition);\n\n // If duration is specified, schedule stop\n if (duration !== undefined) {\n const adjustedDuration = duration / this._playbackRate;\n setTimeout(() => {\n if (this._isPlaying) {\n this.pause();\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n }\n }, adjustedDuration * 1000);\n }\n }\n\n /**\n * Resume playback from the current position (player-mode affordance).\n * Unlike play() with no offset (which resets to 0), this keeps currentTime.\n * Delegates to play() with the current position as the offset, so all of\n * play()'s machinery (AudioContext resume, fades, _isPlaying) is reused.\n */\n resume(): void {\n this.play(undefined, this.getCurrentTime());\n }\n\n /**\n * Pause playback.\n */\n pause(): void {\n if (this.track) {\n this.track.pause();\n }\n this._isPlaying = false;\n }\n\n /**\n * Stop playback and reset to start.\n */\n stop(): void {\n if (this.track) {\n this.track.stop();\n }\n this._isPlaying = false;\n }\n\n /**\n * Seek to a specific time.\n */\n seekTo(time: number): void {\n if (this.track) {\n this.track.seekTo(time);\n }\n }\n\n /**\n * Get current playback time.\n */\n getCurrentTime(): number {\n if (this.track) {\n return this.track.currentTime;\n }\n return 0;\n }\n\n /**\n * Set master volume.\n */\n setMasterVolume(volume: number): void {\n this._masterVolume = Math.max(0, Math.min(1, volume));\n if (this.track) {\n this.track.setVolume(this._masterVolume);\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved).\n */\n setPlaybackRate(rate: number): void {\n this._playbackRate = Math.max(0.5, Math.min(2.0, rate));\n if (this.track) {\n this.track.setPlaybackRate(this._playbackRate);\n }\n }\n\n /**\n * Set mute state for a track.\n */\n setMute(trackId: string, muted: boolean): void {\n const track = this.getTrack(trackId);\n if (track) {\n track.setMuted(muted);\n }\n }\n\n /**\n * Set solo state for a track.\n * Note: With single track, solo is effectively the same as unmute.\n */\n setSolo(_trackId: string, _soloed: boolean): void {\n // No-op for single track - solo doesn't make sense\n console.warn('MediaElementPlayout: Solo is not applicable for single-track playback');\n }\n\n /**\n * Set callback for when playback completes.\n */\n setOnPlaybackComplete(callback: () => void): void {\n this.onPlaybackCompleteCallback = callback;\n }\n\n /**\n * Subscribe to a lifecycle event (loadedmetadata / play / pause / error /\n * ended / timeupdate) without reaching into track.element. Listeners are\n * retained and re-attached automatically when the source is swapped.\n */\n on<K extends keyof MediaElementTrackEvents>(\n event: K,\n listener: MediaElementTrackEvents[K]\n ): void {\n if (!this._eventListeners.has(event)) {\n this._eventListeners.set(event, new Set());\n }\n this._eventListeners.get(event)!.add(listener);\n this.track?.on(event, listener);\n }\n\n /**\n * Unsubscribe a previously registered lifecycle listener.\n */\n off<K extends keyof MediaElementTrackEvents>(\n event: K,\n listener: MediaElementTrackEvents[K]\n ): void {\n this._eventListeners.get(event)?.delete(listener);\n this.track?.off(event, listener);\n }\n\n /**\n * Attach every registered listener to the current track. Called after a new\n * track is created so subscriptions survive source swaps. The cast is safe:\n * the event→listener correlation was enforced by the typed on() that filled\n * the registry; TS cannot track it through this loop.\n */\n private _attachListenersToTrack(): void {\n const track = this.track;\n if (!track) return;\n for (const [event, listeners] of this._eventListeners) {\n for (const listener of listeners) {\n track.on(\n event as keyof MediaElementTrackEvents,\n listener as MediaElementTrackEvents[keyof MediaElementTrackEvents]\n );\n }\n }\n }\n\n /**\n * Clean up resources.\n */\n dispose(): void {\n if (this.track) {\n this.track.dispose();\n this.track = null;\n }\n this._eventListeners.clear();\n }\n\n // Getters\n get isPlaying(): boolean {\n return this._isPlaying;\n }\n\n get masterVolume(): number {\n return this._masterVolume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get duration(): number {\n return this.track?.duration ?? 0;\n }\n\n get sampleRate(): number {\n // HTMLAudioElement doesn't expose sample rate directly\n // Return a common default - peaks will have the actual sample rate\n return this.track?.peaks?.sample_rate ?? 44100;\n }\n\n /**\n * Get the volume GainNode output for connecting external effects chains.\n * Returns null if no AudioContext was provided to the track.\n *\n * Usage: disconnect from default destination, connect to effect input,\n * then connect effect output to audioContext.destination.\n */\n get outputNode(): GainNode | null {\n return this.track?.outputNode ?? null;\n }\n}\n","/**\n * Common interface for playout engines.\n *\n * Both TonePlayout and MediaElementPlayout implement this interface,\n * allowing the browser package to work with either engine.\n */\nexport interface PlayoutEngine {\n // Lifecycle\n init(): Promise<void>;\n dispose(): void;\n\n // Playback\n play(when?: number, offset?: number, duration?: number): void;\n pause(): void;\n stop(): void;\n seekTo(time: number): void;\n getCurrentTime(): number;\n\n // Volume\n setMasterVolume(volume: number): void;\n\n // Track controls (optional - not all engines support all features)\n setMute?(trackId: string, muted: boolean): void;\n setSolo?(trackId: string, soloed: boolean): void;\n\n // Callbacks\n setOnPlaybackComplete(callback: () => void): void;\n\n // State\n readonly isPlaying: boolean;\n readonly duration: number;\n readonly sampleRate: number;\n}\n\n/**\n * Extended interface for engines that support playback rate.\n */\nexport interface PlaybackRateEngine extends PlayoutEngine {\n setPlaybackRate(rate: number): void;\n readonly playbackRate: number;\n}\n\n/**\n * Type guard to check if an engine supports playback rate.\n */\nexport function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine {\n return (\n 'setPlaybackRate' in engine &&\n typeof (engine as Record<string, unknown>).setPlaybackRate === 'function'\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waveform-playlist/media-element-playout",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.2.0",
|
|
4
4
|
"description": "HTMLMediaElement-based playout engine for waveform-playlist with pitch-preserving playback rate",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"vitest": "^3.2.6"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@waveform-playlist/core": "12.
|
|
45
|
+
"@waveform-playlist/core": "12.4.0"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsup",
|