@waveform-playlist/media-element-playout 10.0.0 → 10.1.1

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Naomi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { WaveformDataObject } from '@waveform-playlist/core';
1
+ import { WaveformDataObject, FadeConfig } from '@waveform-playlist/core';
2
+ export { FadeConfig } from '@waveform-playlist/core';
2
3
 
3
4
  interface MediaElementTrackOptions {
4
5
  /** The audio source - can be a URL, Blob URL, or HTMLAudioElement */
@@ -13,6 +14,22 @@ interface MediaElementTrackOptions {
13
14
  volume?: number;
14
15
  /** Initial playback rate (0.5 to 2.0, pitch preserved) */
15
16
  playbackRate?: number;
17
+ /**
18
+ * AudioContext for Web Audio routing.
19
+ * When provided, audio is routed through Web Audio nodes for fades and effects:
20
+ * HTMLAudioElement → MediaElementSourceNode → fadeGain → volumeGain → destination
21
+ *
22
+ * Without this, playback uses HTMLAudioElement directly (no fades or effects).
23
+ *
24
+ * Note: createMediaElementSource() can only be called once per element.
25
+ * Once routed, HTMLAudioElement.volume no longer works — volume is controlled
26
+ * via the Web Audio GainNode instead.
27
+ */
28
+ audioContext?: AudioContext;
29
+ /** Fade in configuration (requires audioContext) */
30
+ fadeIn?: FadeConfig;
31
+ /** Fade out configuration (requires audioContext) */
32
+ fadeOut?: FadeConfig;
16
33
  }
17
34
  /**
18
35
  * Single-track playback using HTMLAudioElement.
@@ -22,10 +39,10 @@ interface MediaElementTrackOptions {
22
39
  * - No AudioBuffer decoding required (uses pre-computed peaks for visualization)
23
40
  * - Simpler, lighter-weight for single-track use cases
24
41
  *
25
- * Limitations:
26
- * - Single track only (no multi-track mixing)
27
- * - No clip-level effects or fades (track-level volume only)
28
- * - Relies on browser's time-stretching quality
42
+ * When an AudioContext is provided:
43
+ * - Audio routes through Web Audio graph for fades and effects
44
+ * - Volume controlled via GainNode (HTMLAudioElement.volume is bypassed)
45
+ * - Output node exposed for connecting external effects chains
29
46
  */
30
47
  declare class MediaElementTrack {
31
48
  private audioElement;
@@ -34,13 +51,31 @@ declare class MediaElementTrack {
34
51
  private _id;
35
52
  private _name;
36
53
  private _playbackRate;
54
+ private _volume;
37
55
  private onStopCallback?;
38
56
  private onTimeUpdateCallback?;
57
+ private _audioContext;
58
+ private _sourceNode;
59
+ private _fadeGain;
60
+ private _volumeGain;
61
+ private _fadeIn;
62
+ private _fadeOut;
39
63
  constructor(options: MediaElementTrackOptions);
40
64
  private handleEnded;
41
65
  private handleTimeUpdate;
42
66
  /**
43
- * Start playback from a specific time
67
+ * Schedule fade automation on the fade GainNode.
68
+ * Called at the start of each play() — fades are relative to the playback offset.
69
+ */
70
+ private _scheduleFades;
71
+ /**
72
+ * Cancel any scheduled fade automation.
73
+ */
74
+ private _cancelFades;
75
+ /**
76
+ * Start playback from a specific time.
77
+ * Resumes the AudioContext first if suspended, then schedules fades
78
+ * (fades depend on audioContext.currentTime being non-zero).
44
79
  */
45
80
  play(offset?: number): void;
46
81
  /**
@@ -67,6 +102,14 @@ declare class MediaElementTrack {
67
102
  * Set muted state
68
103
  */
69
104
  setMuted(muted: boolean): void;
105
+ /**
106
+ * Set fade in configuration
107
+ */
108
+ setFadeIn(fadeIn: FadeConfig | undefined): void;
109
+ /**
110
+ * Set fade out configuration
111
+ */
112
+ setFadeOut(fadeOut: FadeConfig | undefined): void;
70
113
  /**
71
114
  * Set callback for when playback ends
72
115
  */
@@ -75,6 +118,17 @@ declare class MediaElementTrack {
75
118
  * Set callback for time updates
76
119
  */
77
120
  setOnTimeUpdateCallback(callback: (time: number) => void): void;
121
+ /**
122
+ * Connect the output to a different destination (for effects chains).
123
+ * Disconnects from the current destination first.
124
+ *
125
+ * @param destination - The AudioNode to connect to
126
+ */
127
+ connectOutput(destination: AudioNode): void;
128
+ /**
129
+ * Disconnect the output and reconnect to the default AudioContext destination.
130
+ */
131
+ disconnectOutput(): void;
78
132
  /**
79
133
  * Clean up resources
80
134
  */
@@ -92,6 +146,11 @@ declare class MediaElementTrack {
92
146
  * Get the underlying audio element (for advanced use cases)
93
147
  */
94
148
  get element(): HTMLAudioElement;
149
+ /**
150
+ * Get the volume GainNode output (for connecting effects chains).
151
+ * Returns null if no AudioContext was provided.
152
+ */
153
+ get outputNode(): GainNode | null;
95
154
  }
96
155
 
97
156
  interface MediaElementPlayoutOptions {
@@ -113,7 +172,6 @@ interface MediaElementPlayoutOptions {
113
172
  *
114
173
  * Limitations:
115
174
  * - Single track only - will warn if multiple tracks added
116
- * - No clip-level effects or crossfades
117
175
  * - No multi-track mixing
118
176
  *
119
177
  * For multi-track editing, use TonePlayout from @waveform-playlist/playout instead.
@@ -127,7 +185,9 @@ declare class MediaElementPlayout {
127
185
  constructor(options?: MediaElementPlayoutOptions);
128
186
  /**
129
187
  * Initialize the playout engine.
130
- * For MediaElementPlayout this is a no-op (no AudioContext to start).
188
+ * For MediaElementPlayout this is a no-op HTMLAudioElement doesn't require
189
+ * explicit initialization. When an AudioContext is provided for fades/effects,
190
+ * it resumes automatically on first play via MediaElementTrack.
131
191
  */
132
192
  init(): Promise<void>;
133
193
  /**
@@ -196,6 +256,14 @@ declare class MediaElementPlayout {
196
256
  get playbackRate(): number;
197
257
  get duration(): number;
198
258
  get sampleRate(): number;
259
+ /**
260
+ * Get the volume GainNode output for connecting external effects chains.
261
+ * Returns null if no AudioContext was provided to the track.
262
+ *
263
+ * Usage: disconnect from default destination, connect to effect input,
264
+ * then connect effect output to audioContext.destination.
265
+ */
266
+ get outputNode(): GainNode | null;
199
267
  }
200
268
 
201
269
  /**
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { WaveformDataObject } from '@waveform-playlist/core';
1
+ import { WaveformDataObject, FadeConfig } from '@waveform-playlist/core';
2
+ export { FadeConfig } from '@waveform-playlist/core';
2
3
 
3
4
  interface MediaElementTrackOptions {
4
5
  /** The audio source - can be a URL, Blob URL, or HTMLAudioElement */
@@ -13,6 +14,22 @@ interface MediaElementTrackOptions {
13
14
  volume?: number;
14
15
  /** Initial playback rate (0.5 to 2.0, pitch preserved) */
15
16
  playbackRate?: number;
17
+ /**
18
+ * AudioContext for Web Audio routing.
19
+ * When provided, audio is routed through Web Audio nodes for fades and effects:
20
+ * HTMLAudioElement → MediaElementSourceNode → fadeGain → volumeGain → destination
21
+ *
22
+ * Without this, playback uses HTMLAudioElement directly (no fades or effects).
23
+ *
24
+ * Note: createMediaElementSource() can only be called once per element.
25
+ * Once routed, HTMLAudioElement.volume no longer works — volume is controlled
26
+ * via the Web Audio GainNode instead.
27
+ */
28
+ audioContext?: AudioContext;
29
+ /** Fade in configuration (requires audioContext) */
30
+ fadeIn?: FadeConfig;
31
+ /** Fade out configuration (requires audioContext) */
32
+ fadeOut?: FadeConfig;
16
33
  }
17
34
  /**
18
35
  * Single-track playback using HTMLAudioElement.
@@ -22,10 +39,10 @@ interface MediaElementTrackOptions {
22
39
  * - No AudioBuffer decoding required (uses pre-computed peaks for visualization)
23
40
  * - Simpler, lighter-weight for single-track use cases
24
41
  *
25
- * Limitations:
26
- * - Single track only (no multi-track mixing)
27
- * - No clip-level effects or fades (track-level volume only)
28
- * - Relies on browser's time-stretching quality
42
+ * When an AudioContext is provided:
43
+ * - Audio routes through Web Audio graph for fades and effects
44
+ * - Volume controlled via GainNode (HTMLAudioElement.volume is bypassed)
45
+ * - Output node exposed for connecting external effects chains
29
46
  */
30
47
  declare class MediaElementTrack {
31
48
  private audioElement;
@@ -34,13 +51,31 @@ declare class MediaElementTrack {
34
51
  private _id;
35
52
  private _name;
36
53
  private _playbackRate;
54
+ private _volume;
37
55
  private onStopCallback?;
38
56
  private onTimeUpdateCallback?;
57
+ private _audioContext;
58
+ private _sourceNode;
59
+ private _fadeGain;
60
+ private _volumeGain;
61
+ private _fadeIn;
62
+ private _fadeOut;
39
63
  constructor(options: MediaElementTrackOptions);
40
64
  private handleEnded;
41
65
  private handleTimeUpdate;
42
66
  /**
43
- * Start playback from a specific time
67
+ * Schedule fade automation on the fade GainNode.
68
+ * Called at the start of each play() — fades are relative to the playback offset.
69
+ */
70
+ private _scheduleFades;
71
+ /**
72
+ * Cancel any scheduled fade automation.
73
+ */
74
+ private _cancelFades;
75
+ /**
76
+ * Start playback from a specific time.
77
+ * Resumes the AudioContext first if suspended, then schedules fades
78
+ * (fades depend on audioContext.currentTime being non-zero).
44
79
  */
45
80
  play(offset?: number): void;
46
81
  /**
@@ -67,6 +102,14 @@ declare class MediaElementTrack {
67
102
  * Set muted state
68
103
  */
69
104
  setMuted(muted: boolean): void;
105
+ /**
106
+ * Set fade in configuration
107
+ */
108
+ setFadeIn(fadeIn: FadeConfig | undefined): void;
109
+ /**
110
+ * Set fade out configuration
111
+ */
112
+ setFadeOut(fadeOut: FadeConfig | undefined): void;
70
113
  /**
71
114
  * Set callback for when playback ends
72
115
  */
@@ -75,6 +118,17 @@ declare class MediaElementTrack {
75
118
  * Set callback for time updates
76
119
  */
77
120
  setOnTimeUpdateCallback(callback: (time: number) => void): void;
121
+ /**
122
+ * Connect the output to a different destination (for effects chains).
123
+ * Disconnects from the current destination first.
124
+ *
125
+ * @param destination - The AudioNode to connect to
126
+ */
127
+ connectOutput(destination: AudioNode): void;
128
+ /**
129
+ * Disconnect the output and reconnect to the default AudioContext destination.
130
+ */
131
+ disconnectOutput(): void;
78
132
  /**
79
133
  * Clean up resources
80
134
  */
@@ -92,6 +146,11 @@ declare class MediaElementTrack {
92
146
  * Get the underlying audio element (for advanced use cases)
93
147
  */
94
148
  get element(): HTMLAudioElement;
149
+ /**
150
+ * Get the volume GainNode output (for connecting effects chains).
151
+ * Returns null if no AudioContext was provided.
152
+ */
153
+ get outputNode(): GainNode | null;
95
154
  }
96
155
 
97
156
  interface MediaElementPlayoutOptions {
@@ -113,7 +172,6 @@ interface MediaElementPlayoutOptions {
113
172
  *
114
173
  * Limitations:
115
174
  * - Single track only - will warn if multiple tracks added
116
- * - No clip-level effects or crossfades
117
175
  * - No multi-track mixing
118
176
  *
119
177
  * For multi-track editing, use TonePlayout from @waveform-playlist/playout instead.
@@ -127,7 +185,9 @@ declare class MediaElementPlayout {
127
185
  constructor(options?: MediaElementPlayoutOptions);
128
186
  /**
129
187
  * Initialize the playout engine.
130
- * For MediaElementPlayout this is a no-op (no AudioContext to start).
188
+ * For MediaElementPlayout this is a no-op HTMLAudioElement doesn't require
189
+ * explicit initialization. When an AudioContext is provided for fades/effects,
190
+ * it resumes automatically on first play via MediaElementTrack.
131
191
  */
132
192
  init(): Promise<void>;
133
193
  /**
@@ -196,6 +256,14 @@ declare class MediaElementPlayout {
196
256
  get playbackRate(): number;
197
257
  get duration(): number;
198
258
  get sampleRate(): number;
259
+ /**
260
+ * Get the volume GainNode output for connecting external effects chains.
261
+ * Returns null if no AudioContext was provided to the track.
262
+ *
263
+ * Usage: disconnect from default destination, connect to effect input,
264
+ * then connect effect output to audioContext.destination.
265
+ */
266
+ get outputNode(): GainNode | null;
199
267
  }
200
268
 
201
269
  /**
package/dist/index.js CHANGED
@@ -27,10 +27,17 @@ __export(index_exports, {
27
27
  module.exports = __toCommonJS(index_exports);
28
28
 
29
29
  // src/MediaElementTrack.ts
30
+ var import_core = require("@waveform-playlist/core");
30
31
  var MediaElementTrack = class {
31
32
  constructor(options) {
32
33
  this._playbackRate = 1;
34
+ // Web Audio nodes (only when audioContext is provided)
35
+ this._audioContext = null;
36
+ this._sourceNode = null;
37
+ this._fadeGain = null;
38
+ this._volumeGain = null;
33
39
  this.handleEnded = () => {
40
+ this._cancelFades();
34
41
  if (this.onStopCallback) {
35
42
  this.onStopCallback();
36
43
  }
@@ -44,6 +51,9 @@ var MediaElementTrack = class {
44
51
  this._id = options.id ?? `track-${Date.now()}`;
45
52
  this._name = options.name ?? "Track";
46
53
  this._playbackRate = options.playbackRate ?? 1;
54
+ this._volume = options.volume ?? 1;
55
+ this._fadeIn = options.fadeIn;
56
+ this._fadeOut = options.fadeOut;
47
57
  if (typeof options.source === "string") {
48
58
  this.audioElement = new Audio(options.source);
49
59
  this.ownsElement = true;
@@ -52,7 +62,6 @@ var MediaElementTrack = class {
52
62
  this.ownsElement = false;
53
63
  }
54
64
  this.audioElement.preload = "auto";
55
- this.audioElement.volume = options.volume ?? 1;
56
65
  this.audioElement.playbackRate = this._playbackRate;
57
66
  const audio = this.audioElement;
58
67
  if ("preservesPitch" in this.audioElement) {
@@ -62,28 +71,125 @@ var MediaElementTrack = class {
62
71
  } else if ("webkitPreservesPitch" in this.audioElement) {
63
72
  audio.webkitPreservesPitch = true;
64
73
  }
74
+ if (options.audioContext) {
75
+ this._audioContext = options.audioContext;
76
+ try {
77
+ this._sourceNode = options.audioContext.createMediaElementSource(this.audioElement);
78
+ } catch (err) {
79
+ throw new Error(
80
+ "[waveform-playlist] MediaElementTrack: createMediaElementSource() failed. This can happen if the audio element is already connected to another AudioContext. Each audio element can only have one MediaElementSourceNode. Original error: " + String(err)
81
+ );
82
+ }
83
+ this._fadeGain = options.audioContext.createGain();
84
+ this._volumeGain = options.audioContext.createGain();
85
+ this._volumeGain.gain.value = this._volume;
86
+ this._sourceNode.connect(this._fadeGain);
87
+ this._fadeGain.connect(this._volumeGain);
88
+ this._volumeGain.connect(options.audioContext.destination);
89
+ this.audioElement.volume = 1;
90
+ } else {
91
+ this.audioElement.volume = this._volume;
92
+ }
65
93
  this.audioElement.addEventListener("ended", this.handleEnded);
66
94
  this.audioElement.addEventListener("timeupdate", this.handleTimeUpdate);
67
95
  }
68
96
  /**
69
- * Start playback from a specific time
97
+ * Schedule fade automation on the fade GainNode.
98
+ * Called at the start of each play() — fades are relative to the playback offset.
99
+ */
100
+ _scheduleFades(offset) {
101
+ if (!this._fadeGain || !this._audioContext) return;
102
+ const fadeGain = this._fadeGain.gain;
103
+ const now = this._audioContext.currentTime;
104
+ const totalDuration = this.duration;
105
+ fadeGain.cancelScheduledValues(0);
106
+ fadeGain.setValueAtTime(1, now);
107
+ if (this._fadeIn && this._fadeIn.duration > 0) {
108
+ const fadeInEnd = this._fadeIn.duration;
109
+ if (offset < fadeInEnd) {
110
+ const remainingFade = fadeInEnd - offset;
111
+ const fadeType = this._fadeIn.type ?? "linear";
112
+ if (offset === 0) {
113
+ (0, import_core.applyFadeIn)(fadeGain, now, remainingFade, fadeType, 0, 1);
114
+ } else {
115
+ const curve = (0, import_core.generateCurve)(fadeType, 1e3, true);
116
+ const startIndex = Math.round(offset / this._fadeIn.duration * (curve.length - 1));
117
+ const sliced = curve.slice(startIndex);
118
+ fadeGain.setValueAtTime(sliced[0], now);
119
+ fadeGain.setValueCurveAtTime(sliced, now, remainingFade);
120
+ }
121
+ }
122
+ }
123
+ if (this._fadeOut && this._fadeOut.duration > 0) {
124
+ const fadeOutStart = totalDuration - this._fadeOut.duration;
125
+ if (offset < totalDuration && fadeOutStart < totalDuration) {
126
+ if (offset > fadeOutStart) {
127
+ const elapsed = offset - fadeOutStart;
128
+ const fadeType = this._fadeOut.type ?? "linear";
129
+ const curve = (0, import_core.generateCurve)(fadeType, 1e3, false);
130
+ const startIndex = Math.round(elapsed / this._fadeOut.duration * (curve.length - 1));
131
+ const sliced = curve.slice(startIndex);
132
+ const remainingDuration = this._fadeOut.duration - elapsed;
133
+ fadeGain.setValueAtTime(sliced[0], now);
134
+ fadeGain.setValueCurveAtTime(sliced, now, remainingDuration);
135
+ } else {
136
+ const delayUntilFadeOut = fadeOutStart - offset;
137
+ (0, import_core.applyFadeOut)(
138
+ fadeGain,
139
+ now + delayUntilFadeOut,
140
+ this._fadeOut.duration,
141
+ this._fadeOut.type ?? "linear",
142
+ 1,
143
+ 0
144
+ );
145
+ }
146
+ }
147
+ }
148
+ }
149
+ /**
150
+ * Cancel any scheduled fade automation.
151
+ */
152
+ _cancelFades() {
153
+ if (this._fadeGain) {
154
+ this._fadeGain.gain.cancelScheduledValues(0);
155
+ this._fadeGain.gain.value = 1;
156
+ }
157
+ }
158
+ /**
159
+ * Start playback from a specific time.
160
+ * Resumes the AudioContext first if suspended, then schedules fades
161
+ * (fades depend on audioContext.currentTime being non-zero).
70
162
  */
71
163
  play(offset = 0) {
72
- this.audioElement.currentTime = offset;
73
- this.audioElement.play().catch((err) => {
74
- console.warn("MediaElementTrack: play() failed:", err);
75
- });
164
+ const startPlayback = () => {
165
+ this._scheduleFades(offset);
166
+ this.audioElement.currentTime = offset;
167
+ this.audioElement.play().catch((err) => {
168
+ console.warn("[waveform-playlist] MediaElementTrack: play() failed: " + String(err));
169
+ });
170
+ };
171
+ if (this._audioContext && this._audioContext.state === "suspended") {
172
+ this._audioContext.resume().then(startPlayback).catch((err) => {
173
+ console.warn(
174
+ "[waveform-playlist] MediaElementTrack: AudioContext.resume() failed: " + String(err)
175
+ );
176
+ });
177
+ } else {
178
+ startPlayback();
179
+ }
76
180
  }
77
181
  /**
78
182
  * Pause playback
79
183
  */
80
184
  pause() {
185
+ this._cancelFades();
81
186
  this.audioElement.pause();
82
187
  }
83
188
  /**
84
189
  * Stop playback and reset to beginning
85
190
  */
86
191
  stop() {
192
+ this._cancelFades();
87
193
  this.audioElement.pause();
88
194
  this.audioElement.currentTime = 0;
89
195
  }
@@ -97,7 +203,12 @@ var MediaElementTrack = class {
97
203
  * Set volume (0.0 to 1.0)
98
204
  */
99
205
  setVolume(volume) {
100
- this.audioElement.volume = Math.max(0, Math.min(1, volume));
206
+ this._volume = Math.max(0, Math.min(1, volume));
207
+ if (this._volumeGain) {
208
+ this._volumeGain.gain.value = this._volume;
209
+ } else {
210
+ this.audioElement.volume = this._volume;
211
+ }
101
212
  }
102
213
  /**
103
214
  * Set playback rate (0.5 to 2.0, pitch preserved)
@@ -113,6 +224,18 @@ var MediaElementTrack = class {
113
224
  setMuted(muted) {
114
225
  this.audioElement.muted = muted;
115
226
  }
227
+ /**
228
+ * Set fade in configuration
229
+ */
230
+ setFadeIn(fadeIn) {
231
+ this._fadeIn = fadeIn;
232
+ }
233
+ /**
234
+ * Set fade out configuration
235
+ */
236
+ setFadeOut(fadeOut) {
237
+ this._fadeOut = fadeOut;
238
+ }
116
239
  /**
117
240
  * Set callback for when playback ends
118
241
  */
@@ -125,13 +248,77 @@ var MediaElementTrack = class {
125
248
  setOnTimeUpdateCallback(callback) {
126
249
  this.onTimeUpdateCallback = callback;
127
250
  }
251
+ /**
252
+ * Connect the output to a different destination (for effects chains).
253
+ * Disconnects from the current destination first.
254
+ *
255
+ * @param destination - The AudioNode to connect to
256
+ */
257
+ connectOutput(destination) {
258
+ if (!this._volumeGain) {
259
+ console.warn(
260
+ "[waveform-playlist] MediaElementTrack: connectOutput() requires audioContext. Pass audioContext in constructor options."
261
+ );
262
+ return;
263
+ }
264
+ try {
265
+ this._volumeGain.disconnect();
266
+ } catch (err) {
267
+ console.warn(
268
+ "[waveform-playlist] MediaElementTrack: disconnect before connectOutput failed: " + String(err)
269
+ );
270
+ }
271
+ this._volumeGain.connect(destination);
272
+ }
273
+ /**
274
+ * Disconnect the output and reconnect to the default AudioContext destination.
275
+ */
276
+ disconnectOutput() {
277
+ if (!this._volumeGain || !this._audioContext) return;
278
+ try {
279
+ this._volumeGain.disconnect();
280
+ } catch (err) {
281
+ console.warn(
282
+ "[waveform-playlist] MediaElementTrack: disconnect before disconnectOutput failed: " + String(err)
283
+ );
284
+ }
285
+ this._volumeGain.connect(this._audioContext.destination);
286
+ }
128
287
  /**
129
288
  * Clean up resources
130
289
  */
131
290
  dispose() {
132
291
  this.audioElement.removeEventListener("ended", this.handleEnded);
133
292
  this.audioElement.removeEventListener("timeupdate", this.handleTimeUpdate);
293
+ this._cancelFades();
134
294
  this.audioElement.pause();
295
+ if (this._sourceNode) {
296
+ try {
297
+ this._sourceNode.disconnect();
298
+ } catch (err) {
299
+ console.warn(
300
+ "[waveform-playlist] MediaElementTrack: sourceNode disconnect failed: " + String(err)
301
+ );
302
+ }
303
+ }
304
+ if (this._fadeGain) {
305
+ try {
306
+ this._fadeGain.disconnect();
307
+ } catch (err) {
308
+ console.warn(
309
+ "[waveform-playlist] MediaElementTrack: fadeGain disconnect failed: " + String(err)
310
+ );
311
+ }
312
+ }
313
+ if (this._volumeGain) {
314
+ try {
315
+ this._volumeGain.disconnect();
316
+ } catch (err) {
317
+ console.warn(
318
+ "[waveform-playlist] MediaElementTrack: volumeGain disconnect failed: " + String(err)
319
+ );
320
+ }
321
+ }
135
322
  if (this.ownsElement) {
136
323
  this.audioElement.src = "";
137
324
  this.audioElement.load();
@@ -157,7 +344,7 @@ var MediaElementTrack = class {
157
344
  return !this.audioElement.paused && !this.audioElement.ended;
158
345
  }
159
346
  get volume() {
160
- return this.audioElement.volume;
347
+ return this._volume;
161
348
  }
162
349
  get playbackRate() {
163
350
  return this._playbackRate;
@@ -171,6 +358,13 @@ var MediaElementTrack = class {
171
358
  get element() {
172
359
  return this.audioElement;
173
360
  }
361
+ /**
362
+ * Get the volume GainNode output (for connecting effects chains).
363
+ * Returns null if no AudioContext was provided.
364
+ */
365
+ get outputNode() {
366
+ return this._volumeGain;
367
+ }
174
368
  };
175
369
 
176
370
  // src/MediaElementPlayout.ts
@@ -183,7 +377,9 @@ var MediaElementPlayout = class {
183
377
  }
184
378
  /**
185
379
  * Initialize the playout engine.
186
- * For MediaElementPlayout this is a no-op (no AudioContext to start).
380
+ * For MediaElementPlayout this is a no-op HTMLAudioElement doesn't require
381
+ * explicit initialization. When an AudioContext is provided for fades/effects,
382
+ * it resumes automatically on first play via MediaElementTrack.
187
383
  */
188
384
  async init() {
189
385
  }
@@ -355,6 +551,16 @@ var MediaElementPlayout = class {
355
551
  get sampleRate() {
356
552
  return this.track?.peaks.sample_rate ?? 44100;
357
553
  }
554
+ /**
555
+ * Get the volume GainNode output for connecting external effects chains.
556
+ * Returns null if no AudioContext was provided to the track.
557
+ *
558
+ * Usage: disconnect from default destination, connect to effect input,
559
+ * then connect effect output to audioContext.destination.
560
+ */
561
+ get outputNode() {
562
+ return this.track?.outputNode ?? null;
563
+ }
358
564
  };
359
565
 
360
566
  // src/types.ts
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 } from './MediaElementTrack';\n\n// Common interface types\nexport type { PlayoutEngine, PlaybackRateEngine } from './types';\nexport { supportsPlaybackRate } from './types';\n","import type { WaveformDataObject } 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 (required - no AudioBuffer decoding) */\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, pitch preserved) */\n playbackRate?: number;\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 * Limitations:\n * - Single track only (no multi-track mixing)\n * - No clip-level effects or fades (track-level volume only)\n * - Relies on browser's time-stretching quality\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;\n private _id: string;\n private _name: string;\n private _playbackRate: number = 1;\n private onStopCallback?: () => void;\n private onTimeUpdateCallback?: (time: number) => void;\n\n constructor(options: MediaElementTrackOptions) {\n this._peaks = options.peaks;\n this._id = options.id ?? `track-${Date.now()}`;\n this._name = options.name ?? 'Track';\n this._playbackRate = options.playbackRate ?? 1;\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.volume = options.volume ?? 1;\n this.audioElement.playbackRate = this._playbackRate;\n\n // Preserve pitch when changing playback rate (default in modern browsers)\n // Some older browsers may not support this, but it's the default behavior\n // Vendor-prefixed properties are non-standard; cast once for type safety.\n const audio = this.audioElement as unknown as VendorPrefixedPitch;\n if ('preservesPitch' in this.audioElement) {\n audio.preservesPitch = true;\n } else if ('mozPreservesPitch' in this.audioElement) {\n // Firefox prefix\n audio.mozPreservesPitch = true;\n } else if ('webkitPreservesPitch' in this.audioElement) {\n // Safari prefix\n audio.webkitPreservesPitch = true;\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 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 * Start playback from a specific time\n */\n play(offset: number = 0): void {\n this.audioElement.currentTime = offset;\n this.audioElement.play().catch((err) => {\n console.warn('MediaElementTrack: play() failed:', err);\n });\n }\n\n /**\n * Pause playback\n */\n pause(): void {\n this.audioElement.pause();\n }\n\n /**\n * Stop playback and reset to beginning\n */\n stop(): void {\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.audioElement.volume = Math.max(0, Math.min(1, volume));\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved)\n */\n setPlaybackRate(rate: number): void {\n // Clamp to reasonable range for pitch preservation quality\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 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 * Clean up resources\n */\n dispose(): void {\n this.audioElement.removeEventListener('ended', this.handleEnded);\n this.audioElement.removeEventListener('timeupdate', this.handleTimeUpdate);\n this.audioElement.pause();\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 {\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;\n }\n\n get isPlaying(): boolean {\n return !this.audioElement.paused && !this.audioElement.ended;\n }\n\n get volume(): number {\n return this.audioElement.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","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}\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 clip-level effects or crossfades\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 _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 }\n\n /**\n * Initialize the playout engine.\n * For MediaElementPlayout this is a no-op (no AudioContext to start).\n */\n async init(): Promise<void> {\n // No initialization needed for HTMLAudioElement\n // AudioContext requires user gesture, but audio element just works\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 });\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 * 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 { 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 (required - no AudioBuffer decoding) */\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, pitch preserved) */\n playbackRate?: number;\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;\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;\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 // Preserve pitch when changing playback rate (default in modern browsers)\n // Vendor-prefixed properties are non-standard; cast once for type safety.\n const audio = this.audioElement as unknown as VendorPrefixedPitch;\n if ('preservesPitch' in this.audioElement) {\n audio.preservesPitch = true;\n } else if ('mozPreservesPitch' in this.audioElement) {\n audio.mozPreservesPitch = true;\n } else if ('webkitPreservesPitch' in this.audioElement) {\n audio.webkitPreservesPitch = true;\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 {\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;\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}\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 _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 }\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 });\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":[]}
package/dist/index.mjs CHANGED
@@ -1,8 +1,15 @@
1
1
  // src/MediaElementTrack.ts
2
+ import { applyFadeIn, applyFadeOut, generateCurve } from "@waveform-playlist/core";
2
3
  var MediaElementTrack = class {
3
4
  constructor(options) {
4
5
  this._playbackRate = 1;
6
+ // Web Audio nodes (only when audioContext is provided)
7
+ this._audioContext = null;
8
+ this._sourceNode = null;
9
+ this._fadeGain = null;
10
+ this._volumeGain = null;
5
11
  this.handleEnded = () => {
12
+ this._cancelFades();
6
13
  if (this.onStopCallback) {
7
14
  this.onStopCallback();
8
15
  }
@@ -16,6 +23,9 @@ var MediaElementTrack = class {
16
23
  this._id = options.id ?? `track-${Date.now()}`;
17
24
  this._name = options.name ?? "Track";
18
25
  this._playbackRate = options.playbackRate ?? 1;
26
+ this._volume = options.volume ?? 1;
27
+ this._fadeIn = options.fadeIn;
28
+ this._fadeOut = options.fadeOut;
19
29
  if (typeof options.source === "string") {
20
30
  this.audioElement = new Audio(options.source);
21
31
  this.ownsElement = true;
@@ -24,7 +34,6 @@ var MediaElementTrack = class {
24
34
  this.ownsElement = false;
25
35
  }
26
36
  this.audioElement.preload = "auto";
27
- this.audioElement.volume = options.volume ?? 1;
28
37
  this.audioElement.playbackRate = this._playbackRate;
29
38
  const audio = this.audioElement;
30
39
  if ("preservesPitch" in this.audioElement) {
@@ -34,28 +43,125 @@ var MediaElementTrack = class {
34
43
  } else if ("webkitPreservesPitch" in this.audioElement) {
35
44
  audio.webkitPreservesPitch = true;
36
45
  }
46
+ if (options.audioContext) {
47
+ this._audioContext = options.audioContext;
48
+ try {
49
+ this._sourceNode = options.audioContext.createMediaElementSource(this.audioElement);
50
+ } catch (err) {
51
+ throw new Error(
52
+ "[waveform-playlist] MediaElementTrack: createMediaElementSource() failed. This can happen if the audio element is already connected to another AudioContext. Each audio element can only have one MediaElementSourceNode. Original error: " + String(err)
53
+ );
54
+ }
55
+ this._fadeGain = options.audioContext.createGain();
56
+ this._volumeGain = options.audioContext.createGain();
57
+ this._volumeGain.gain.value = this._volume;
58
+ this._sourceNode.connect(this._fadeGain);
59
+ this._fadeGain.connect(this._volumeGain);
60
+ this._volumeGain.connect(options.audioContext.destination);
61
+ this.audioElement.volume = 1;
62
+ } else {
63
+ this.audioElement.volume = this._volume;
64
+ }
37
65
  this.audioElement.addEventListener("ended", this.handleEnded);
38
66
  this.audioElement.addEventListener("timeupdate", this.handleTimeUpdate);
39
67
  }
40
68
  /**
41
- * Start playback from a specific time
69
+ * Schedule fade automation on the fade GainNode.
70
+ * Called at the start of each play() — fades are relative to the playback offset.
71
+ */
72
+ _scheduleFades(offset) {
73
+ if (!this._fadeGain || !this._audioContext) return;
74
+ const fadeGain = this._fadeGain.gain;
75
+ const now = this._audioContext.currentTime;
76
+ const totalDuration = this.duration;
77
+ fadeGain.cancelScheduledValues(0);
78
+ fadeGain.setValueAtTime(1, now);
79
+ if (this._fadeIn && this._fadeIn.duration > 0) {
80
+ const fadeInEnd = this._fadeIn.duration;
81
+ if (offset < fadeInEnd) {
82
+ const remainingFade = fadeInEnd - offset;
83
+ const fadeType = this._fadeIn.type ?? "linear";
84
+ if (offset === 0) {
85
+ applyFadeIn(fadeGain, now, remainingFade, fadeType, 0, 1);
86
+ } else {
87
+ const curve = generateCurve(fadeType, 1e3, true);
88
+ const startIndex = Math.round(offset / this._fadeIn.duration * (curve.length - 1));
89
+ const sliced = curve.slice(startIndex);
90
+ fadeGain.setValueAtTime(sliced[0], now);
91
+ fadeGain.setValueCurveAtTime(sliced, now, remainingFade);
92
+ }
93
+ }
94
+ }
95
+ if (this._fadeOut && this._fadeOut.duration > 0) {
96
+ const fadeOutStart = totalDuration - this._fadeOut.duration;
97
+ if (offset < totalDuration && fadeOutStart < totalDuration) {
98
+ if (offset > fadeOutStart) {
99
+ const elapsed = offset - fadeOutStart;
100
+ const fadeType = this._fadeOut.type ?? "linear";
101
+ const curve = generateCurve(fadeType, 1e3, false);
102
+ const startIndex = Math.round(elapsed / this._fadeOut.duration * (curve.length - 1));
103
+ const sliced = curve.slice(startIndex);
104
+ const remainingDuration = this._fadeOut.duration - elapsed;
105
+ fadeGain.setValueAtTime(sliced[0], now);
106
+ fadeGain.setValueCurveAtTime(sliced, now, remainingDuration);
107
+ } else {
108
+ const delayUntilFadeOut = fadeOutStart - offset;
109
+ applyFadeOut(
110
+ fadeGain,
111
+ now + delayUntilFadeOut,
112
+ this._fadeOut.duration,
113
+ this._fadeOut.type ?? "linear",
114
+ 1,
115
+ 0
116
+ );
117
+ }
118
+ }
119
+ }
120
+ }
121
+ /**
122
+ * Cancel any scheduled fade automation.
123
+ */
124
+ _cancelFades() {
125
+ if (this._fadeGain) {
126
+ this._fadeGain.gain.cancelScheduledValues(0);
127
+ this._fadeGain.gain.value = 1;
128
+ }
129
+ }
130
+ /**
131
+ * Start playback from a specific time.
132
+ * Resumes the AudioContext first if suspended, then schedules fades
133
+ * (fades depend on audioContext.currentTime being non-zero).
42
134
  */
43
135
  play(offset = 0) {
44
- this.audioElement.currentTime = offset;
45
- this.audioElement.play().catch((err) => {
46
- console.warn("MediaElementTrack: play() failed:", err);
47
- });
136
+ const startPlayback = () => {
137
+ this._scheduleFades(offset);
138
+ this.audioElement.currentTime = offset;
139
+ this.audioElement.play().catch((err) => {
140
+ console.warn("[waveform-playlist] MediaElementTrack: play() failed: " + String(err));
141
+ });
142
+ };
143
+ if (this._audioContext && this._audioContext.state === "suspended") {
144
+ this._audioContext.resume().then(startPlayback).catch((err) => {
145
+ console.warn(
146
+ "[waveform-playlist] MediaElementTrack: AudioContext.resume() failed: " + String(err)
147
+ );
148
+ });
149
+ } else {
150
+ startPlayback();
151
+ }
48
152
  }
49
153
  /**
50
154
  * Pause playback
51
155
  */
52
156
  pause() {
157
+ this._cancelFades();
53
158
  this.audioElement.pause();
54
159
  }
55
160
  /**
56
161
  * Stop playback and reset to beginning
57
162
  */
58
163
  stop() {
164
+ this._cancelFades();
59
165
  this.audioElement.pause();
60
166
  this.audioElement.currentTime = 0;
61
167
  }
@@ -69,7 +175,12 @@ var MediaElementTrack = class {
69
175
  * Set volume (0.0 to 1.0)
70
176
  */
71
177
  setVolume(volume) {
72
- this.audioElement.volume = Math.max(0, Math.min(1, volume));
178
+ this._volume = Math.max(0, Math.min(1, volume));
179
+ if (this._volumeGain) {
180
+ this._volumeGain.gain.value = this._volume;
181
+ } else {
182
+ this.audioElement.volume = this._volume;
183
+ }
73
184
  }
74
185
  /**
75
186
  * Set playback rate (0.5 to 2.0, pitch preserved)
@@ -85,6 +196,18 @@ var MediaElementTrack = class {
85
196
  setMuted(muted) {
86
197
  this.audioElement.muted = muted;
87
198
  }
199
+ /**
200
+ * Set fade in configuration
201
+ */
202
+ setFadeIn(fadeIn) {
203
+ this._fadeIn = fadeIn;
204
+ }
205
+ /**
206
+ * Set fade out configuration
207
+ */
208
+ setFadeOut(fadeOut) {
209
+ this._fadeOut = fadeOut;
210
+ }
88
211
  /**
89
212
  * Set callback for when playback ends
90
213
  */
@@ -97,13 +220,77 @@ var MediaElementTrack = class {
97
220
  setOnTimeUpdateCallback(callback) {
98
221
  this.onTimeUpdateCallback = callback;
99
222
  }
223
+ /**
224
+ * Connect the output to a different destination (for effects chains).
225
+ * Disconnects from the current destination first.
226
+ *
227
+ * @param destination - The AudioNode to connect to
228
+ */
229
+ connectOutput(destination) {
230
+ if (!this._volumeGain) {
231
+ console.warn(
232
+ "[waveform-playlist] MediaElementTrack: connectOutput() requires audioContext. Pass audioContext in constructor options."
233
+ );
234
+ return;
235
+ }
236
+ try {
237
+ this._volumeGain.disconnect();
238
+ } catch (err) {
239
+ console.warn(
240
+ "[waveform-playlist] MediaElementTrack: disconnect before connectOutput failed: " + String(err)
241
+ );
242
+ }
243
+ this._volumeGain.connect(destination);
244
+ }
245
+ /**
246
+ * Disconnect the output and reconnect to the default AudioContext destination.
247
+ */
248
+ disconnectOutput() {
249
+ if (!this._volumeGain || !this._audioContext) return;
250
+ try {
251
+ this._volumeGain.disconnect();
252
+ } catch (err) {
253
+ console.warn(
254
+ "[waveform-playlist] MediaElementTrack: disconnect before disconnectOutput failed: " + String(err)
255
+ );
256
+ }
257
+ this._volumeGain.connect(this._audioContext.destination);
258
+ }
100
259
  /**
101
260
  * Clean up resources
102
261
  */
103
262
  dispose() {
104
263
  this.audioElement.removeEventListener("ended", this.handleEnded);
105
264
  this.audioElement.removeEventListener("timeupdate", this.handleTimeUpdate);
265
+ this._cancelFades();
106
266
  this.audioElement.pause();
267
+ if (this._sourceNode) {
268
+ try {
269
+ this._sourceNode.disconnect();
270
+ } catch (err) {
271
+ console.warn(
272
+ "[waveform-playlist] MediaElementTrack: sourceNode disconnect failed: " + String(err)
273
+ );
274
+ }
275
+ }
276
+ if (this._fadeGain) {
277
+ try {
278
+ this._fadeGain.disconnect();
279
+ } catch (err) {
280
+ console.warn(
281
+ "[waveform-playlist] MediaElementTrack: fadeGain disconnect failed: " + String(err)
282
+ );
283
+ }
284
+ }
285
+ if (this._volumeGain) {
286
+ try {
287
+ this._volumeGain.disconnect();
288
+ } catch (err) {
289
+ console.warn(
290
+ "[waveform-playlist] MediaElementTrack: volumeGain disconnect failed: " + String(err)
291
+ );
292
+ }
293
+ }
107
294
  if (this.ownsElement) {
108
295
  this.audioElement.src = "";
109
296
  this.audioElement.load();
@@ -129,7 +316,7 @@ var MediaElementTrack = class {
129
316
  return !this.audioElement.paused && !this.audioElement.ended;
130
317
  }
131
318
  get volume() {
132
- return this.audioElement.volume;
319
+ return this._volume;
133
320
  }
134
321
  get playbackRate() {
135
322
  return this._playbackRate;
@@ -143,6 +330,13 @@ var MediaElementTrack = class {
143
330
  get element() {
144
331
  return this.audioElement;
145
332
  }
333
+ /**
334
+ * Get the volume GainNode output (for connecting effects chains).
335
+ * Returns null if no AudioContext was provided.
336
+ */
337
+ get outputNode() {
338
+ return this._volumeGain;
339
+ }
146
340
  };
147
341
 
148
342
  // src/MediaElementPlayout.ts
@@ -155,7 +349,9 @@ var MediaElementPlayout = class {
155
349
  }
156
350
  /**
157
351
  * Initialize the playout engine.
158
- * For MediaElementPlayout this is a no-op (no AudioContext to start).
352
+ * For MediaElementPlayout this is a no-op HTMLAudioElement doesn't require
353
+ * explicit initialization. When an AudioContext is provided for fades/effects,
354
+ * it resumes automatically on first play via MediaElementTrack.
159
355
  */
160
356
  async init() {
161
357
  }
@@ -327,6 +523,16 @@ var MediaElementPlayout = class {
327
523
  get sampleRate() {
328
524
  return this.track?.peaks.sample_rate ?? 44100;
329
525
  }
526
+ /**
527
+ * Get the volume GainNode output for connecting external effects chains.
528
+ * Returns null if no AudioContext was provided to the track.
529
+ *
530
+ * Usage: disconnect from default destination, connect to effect input,
531
+ * then connect effect output to audioContext.destination.
532
+ */
533
+ get outputNode() {
534
+ return this.track?.outputNode ?? null;
535
+ }
330
536
  };
331
537
 
332
538
  // src/types.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/MediaElementTrack.ts","../src/MediaElementPlayout.ts","../src/types.ts"],"sourcesContent":["import type { WaveformDataObject } 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 (required - no AudioBuffer decoding) */\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, pitch preserved) */\n playbackRate?: number;\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 * Limitations:\n * - Single track only (no multi-track mixing)\n * - No clip-level effects or fades (track-level volume only)\n * - Relies on browser's time-stretching quality\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;\n private _id: string;\n private _name: string;\n private _playbackRate: number = 1;\n private onStopCallback?: () => void;\n private onTimeUpdateCallback?: (time: number) => void;\n\n constructor(options: MediaElementTrackOptions) {\n this._peaks = options.peaks;\n this._id = options.id ?? `track-${Date.now()}`;\n this._name = options.name ?? 'Track';\n this._playbackRate = options.playbackRate ?? 1;\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.volume = options.volume ?? 1;\n this.audioElement.playbackRate = this._playbackRate;\n\n // Preserve pitch when changing playback rate (default in modern browsers)\n // Some older browsers may not support this, but it's the default behavior\n // Vendor-prefixed properties are non-standard; cast once for type safety.\n const audio = this.audioElement as unknown as VendorPrefixedPitch;\n if ('preservesPitch' in this.audioElement) {\n audio.preservesPitch = true;\n } else if ('mozPreservesPitch' in this.audioElement) {\n // Firefox prefix\n audio.mozPreservesPitch = true;\n } else if ('webkitPreservesPitch' in this.audioElement) {\n // Safari prefix\n audio.webkitPreservesPitch = true;\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 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 * Start playback from a specific time\n */\n play(offset: number = 0): void {\n this.audioElement.currentTime = offset;\n this.audioElement.play().catch((err) => {\n console.warn('MediaElementTrack: play() failed:', err);\n });\n }\n\n /**\n * Pause playback\n */\n pause(): void {\n this.audioElement.pause();\n }\n\n /**\n * Stop playback and reset to beginning\n */\n stop(): void {\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.audioElement.volume = Math.max(0, Math.min(1, volume));\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved)\n */\n setPlaybackRate(rate: number): void {\n // Clamp to reasonable range for pitch preservation quality\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 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 * Clean up resources\n */\n dispose(): void {\n this.audioElement.removeEventListener('ended', this.handleEnded);\n this.audioElement.removeEventListener('timeupdate', this.handleTimeUpdate);\n this.audioElement.pause();\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 {\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;\n }\n\n get isPlaying(): boolean {\n return !this.audioElement.paused && !this.audioElement.ended;\n }\n\n get volume(): number {\n return this.audioElement.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","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}\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 clip-level effects or crossfades\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 _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 }\n\n /**\n * Initialize the playout engine.\n * For MediaElementPlayout this is a no-op (no AudioContext to start).\n */\n async init(): Promise<void> {\n // No initialization needed for HTMLAudioElement\n // AudioContext requires user gesture, but audio element just works\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 });\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 * 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 (required - no AudioBuffer decoding) */\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, pitch preserved) */\n playbackRate?: number;\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;\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;\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 // Preserve pitch when changing playback rate (default in modern browsers)\n // Vendor-prefixed properties are non-standard; cast once for type safety.\n const audio = this.audioElement as unknown as VendorPrefixedPitch;\n if ('preservesPitch' in this.audioElement) {\n audio.preservesPitch = true;\n } else if ('mozPreservesPitch' in this.audioElement) {\n audio.mozPreservesPitch = true;\n } else if ('webkitPreservesPitch' in this.audioElement) {\n audio.webkitPreservesPitch = true;\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 {\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;\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}\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 _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 }\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 });\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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waveform-playlist/media-element-playout",
3
- "version": "10.0.0",
3
+ "version": "10.1.1",
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",
@@ -13,11 +13,6 @@
13
13
  }
14
14
  },
15
15
  "sideEffects": false,
16
- "scripts": {
17
- "build": "tsup",
18
- "dev": "tsup --watch",
19
- "typecheck": "tsc --noEmit"
20
- },
21
16
  "keywords": [
22
17
  "waveform",
23
18
  "audio",
@@ -46,6 +41,11 @@
46
41
  "typescript": "^5.3.3"
47
42
  },
48
43
  "dependencies": {
49
- "@waveform-playlist/core": "workspace:*"
44
+ "@waveform-playlist/core": "10.1.1"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup",
48
+ "dev": "tsup --watch",
49
+ "typecheck": "tsc --noEmit"
50
50
  }
51
- }
51
+ }