@waveform-playlist/media-element-playout 10.0.0 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/dist/index.d.mts +76 -8
- package/dist/index.d.ts +76 -8
- package/dist/index.js +215 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +215 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
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
|
-
*
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
28
|
-
* -
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
28
|
-
* -
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCA,kBAAyD;AA0DlD,IAAM,oBAAN,MAAwB;AAAA,EAmB7B,YAAY,SAAmC;AAb/C,SAAQ,gBAAwB;AAMhC;AAAA,SAAQ,gBAAqC;AAC7C,SAAQ,cAAkD;AAC1D,SAAQ,YAA6B;AACrC,SAAQ,cAA+B;AAwEvC,SAAQ,cAAc,MAAM;AAC1B,WAAK,aAAa;AAClB,UAAI,KAAK,gBAAgB;AACvB,aAAK,eAAe;AAAA,MACtB;AAAA,IACF;AAEA,SAAQ,mBAAmB,MAAM;AAC/B,UAAI,KAAK,sBAAsB;AAC7B,aAAK,qBAAqB,KAAK,aAAa,WAAW;AAAA,MACzD;AAAA,IACF;AA9EE,SAAK,SAAS,QAAQ;AACtB,SAAK,MAAM,QAAQ,MAAM,SAAS,KAAK,IAAI,CAAC;AAC5C,SAAK,QAAQ,QAAQ,QAAQ;AAC7B,SAAK,gBAAgB,QAAQ,gBAAgB;AAC7C,SAAK,UAAU,QAAQ,UAAU;AACjC,SAAK,UAAU,QAAQ;AACvB,SAAK,WAAW,QAAQ;AAGxB,QAAI,OAAO,QAAQ,WAAW,UAAU;AACtC,WAAK,eAAe,IAAI,MAAM,QAAQ,MAAM;AAC5C,WAAK,cAAc;AAAA,IACrB,OAAO;AACL,WAAK,eAAe,QAAQ;AAC5B,WAAK,cAAc;AAAA,IACrB;AAGA,SAAK,aAAa,UAAU;AAC5B,SAAK,aAAa,eAAe,KAAK;AAItC,UAAM,QAAQ,KAAK;AACnB,QAAI,oBAAoB,KAAK,cAAc;AACzC,YAAM,iBAAiB;AAAA,IACzB,WAAW,uBAAuB,KAAK,cAAc;AACnD,YAAM,oBAAoB;AAAA,IAC5B,WAAW,0BAA0B,KAAK,cAAc;AACtD,YAAM,uBAAuB;AAAA,IAC/B;AAGA,QAAI,QAAQ,cAAc;AACxB,WAAK,gBAAgB,QAAQ;AAC7B,UAAI;AACF,aAAK,cAAc,QAAQ,aAAa,yBAAyB,KAAK,YAAY;AAAA,MACpF,SAAS,KAAK;AACZ,cAAM,IAAI;AAAA,UACR,+OAIE,OAAO,GAAG;AAAA,QACd;AAAA,MACF;AACA,WAAK,YAAY,QAAQ,aAAa,WAAW;AACjD,WAAK,cAAc,QAAQ,aAAa,WAAW;AACnD,WAAK,YAAY,KAAK,QAAQ,KAAK;AAEnC,WAAK,YAAY,QAAQ,KAAK,SAAS;AACvC,WAAK,UAAU,QAAQ,KAAK,WAAW;AACvC,WAAK,YAAY,QAAQ,QAAQ,aAAa,WAAW;AAIzD,WAAK,aAAa,SAAS;AAAA,IAC7B,OAAO;AAEL,WAAK,aAAa,SAAS,KAAK;AAAA,IAClC;AAGA,SAAK,aAAa,iBAAiB,SAAS,KAAK,WAAW;AAC5D,SAAK,aAAa,iBAAiB,cAAc,KAAK,gBAAgB;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBQ,eAAe,QAAsB;AAC3C,QAAI,CAAC,KAAK,aAAa,CAAC,KAAK,cAAe;AAE5C,UAAM,WAAW,KAAK,UAAU;AAChC,UAAM,MAAM,KAAK,cAAc;AAC/B,UAAM,gBAAgB,KAAK;AAG3B,aAAS,sBAAsB,CAAC;AAChC,aAAS,eAAe,GAAG,GAAG;AAG9B,QAAI,KAAK,WAAW,KAAK,QAAQ,WAAW,GAAG;AAC7C,YAAM,YAAY,KAAK,QAAQ;AAC/B,UAAI,SAAS,WAAW;AACtB,cAAM,gBAAgB,YAAY;AAClC,cAAM,WAAW,KAAK,QAAQ,QAAQ;AACtC,YAAI,WAAW,GAAG;AAEhB,uCAAY,UAAU,KAAK,eAAe,UAAU,GAAG,CAAC;AAAA,QAC1D,OAAO;AAEL,gBAAM,YAAQ,2BAAc,UAAU,KAAM,IAAI;AAChD,gBAAM,aAAa,KAAK,MAAO,SAAS,KAAK,QAAQ,YAAa,MAAM,SAAS,EAAE;AACnF,gBAAM,SAAS,MAAM,MAAM,UAAU;AACrC,mBAAS,eAAe,OAAO,CAAC,GAAG,GAAG;AACtC,mBAAS,oBAAoB,QAAQ,KAAK,aAAa;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAGA,QAAI,KAAK,YAAY,KAAK,SAAS,WAAW,GAAG;AAC/C,YAAM,eAAe,gBAAgB,KAAK,SAAS;AACnD,UAAI,SAAS,iBAAiB,eAAe,eAAe;AAC1D,YAAI,SAAS,cAAc;AAEzB,gBAAM,UAAU,SAAS;AACzB,gBAAM,WAAW,KAAK,SAAS,QAAQ;AACvC,gBAAM,YAAQ,2BAAc,UAAU,KAAM,KAAK;AACjD,gBAAM,aAAa,KAAK,MAAO,UAAU,KAAK,SAAS,YAAa,MAAM,SAAS,EAAE;AACrF,gBAAM,SAAS,MAAM,MAAM,UAAU;AACrC,gBAAM,oBAAoB,KAAK,SAAS,WAAW;AACnD,mBAAS,eAAe,OAAO,CAAC,GAAG,GAAG;AACtC,mBAAS,oBAAoB,QAAQ,KAAK,iBAAiB;AAAA,QAC7D,OAAO;AAEL,gBAAM,oBAAoB,eAAe;AACzC;AAAA,YACE;AAAA,YACA,MAAM;AAAA,YACN,KAAK,SAAS;AAAA,YACd,KAAK,SAAS,QAAQ;AAAA,YACtB;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAqB;AAC3B,QAAI,KAAK,WAAW;AAClB,WAAK,UAAU,KAAK,sBAAsB,CAAC;AAC3C,WAAK,UAAU,KAAK,QAAQ;AAAA,IAC9B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,KAAK,SAAiB,GAAS;AAC7B,UAAM,gBAAgB,MAAM;AAC1B,WAAK,eAAe,MAAM;AAC1B,WAAK,aAAa,cAAc;AAChC,WAAK,aAAa,KAAK,EAAE,MAAM,CAAC,QAAQ;AACtC,gBAAQ,KAAK,2DAA2D,OAAO,GAAG,CAAC;AAAA,MACrF,CAAC;AAAA,IACH;AAKA,QAAI,KAAK,iBAAiB,KAAK,cAAc,UAAU,aAAa;AAClE,WAAK,cACF,OAAO,EACP,KAAK,aAAa,EAClB,MAAM,CAAC,QAAQ;AACd,gBAAQ;AAAA,UACN,0EAA0E,OAAO,GAAG;AAAA,QACtF;AAAA,MACF,CAAC;AAAA,IACL,OAAO;AACL,oBAAc;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,aAAa;AAClB,SAAK,aAAa,MAAM;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACX,SAAK,aAAa;AAClB,SAAK,aAAa,MAAM;AACxB,SAAK,aAAa,cAAc;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,MAAoB;AACzB,SAAK,aAAa,cAAc,KAAK,IAAI,GAAG,KAAK,IAAI,MAAM,KAAK,QAAQ,CAAC;AAAA,EAC3E;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,QAAsB;AAC9B,SAAK,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,CAAC;AAC9C,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,KAAK,QAAQ,KAAK;AAAA,IACrC,OAAO;AACL,WAAK,aAAa,SAAS,KAAK;AAAA,IAClC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,MAAoB;AAClC,UAAM,cAAc,KAAK,IAAI,KAAK,KAAK,IAAI,GAAK,IAAI,CAAC;AACrD,SAAK,gBAAgB;AACrB,SAAK,aAAa,eAAe;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,OAAsB;AAC7B,SAAK,aAAa,QAAQ;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,QAAsC;AAC9C,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,SAAuC;AAChD,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAkB,UAA4B;AAC5C,SAAK,iBAAiB;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,wBAAwB,UAAwC;AAC9D,SAAK,uBAAuB;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,cAAc,aAA8B;AAC1C,QAAI,CAAC,KAAK,aAAa;AACrB,cAAQ;AAAA,QACN;AAAA,MAEF;AACA;AAAA,IACF;AACA,QAAI;AACF,WAAK,YAAY,WAAW;AAAA,IAC9B,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,oFACE,OAAO,GAAG;AAAA,MACd;AAAA,IACF;AACA,SAAK,YAAY,QAAQ,WAAW;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAyB;AACvB,QAAI,CAAC,KAAK,eAAe,CAAC,KAAK,cAAe;AAC9C,QAAI;AACF,WAAK,YAAY,WAAW;AAAA,IAC9B,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,uFACE,OAAO,GAAG;AAAA,MACd;AAAA,IACF;AACA,SAAK,YAAY,QAAQ,KAAK,cAAc,WAAW;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACd,SAAK,aAAa,oBAAoB,SAAS,KAAK,WAAW;AAC/D,SAAK,aAAa,oBAAoB,cAAc,KAAK,gBAAgB;AACzE,SAAK,aAAa;AAClB,SAAK,aAAa,MAAM;AAExB,QAAI,KAAK,aAAa;AACpB,UAAI;AACF,aAAK,YAAY,WAAW;AAAA,MAC9B,SAAS,KAAK;AACZ,gBAAQ;AAAA,UACN,0EAA0E,OAAO,GAAG;AAAA,QACtF;AAAA,MACF;AAAA,IACF;AACA,QAAI,KAAK,WAAW;AAClB,UAAI;AACF,aAAK,UAAU,WAAW;AAAA,MAC5B,SAAS,KAAK;AACZ,gBAAQ;AAAA,UACN,wEAAwE,OAAO,GAAG;AAAA,QACpF;AAAA,MACF;AAAA,IACF;AACA,QAAI,KAAK,aAAa;AACpB,UAAI;AACF,aAAK,YAAY,WAAW;AAAA,MAC9B,SAAS,KAAK;AACZ,gBAAQ;AAAA,UACN,0EAA0E,OAAO,GAAG;AAAA,QACtF;AAAA,MACF;AAAA,IACF;AAEA,QAAI,KAAK,aAAa;AACpB,WAAK,aAAa,MAAM;AACxB,WAAK,aAAa,KAAK;AAAA,IACzB;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,KAAa;AACf,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,OAAe;AACjB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,QAA4B;AAC9B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,cAAsB;AACxB,WAAO,KAAK,aAAa;AAAA,EAC3B;AAAA,EAEA,IAAI,WAAmB;AACrB,WAAO,KAAK,aAAa,YAAY,KAAK,OAAO;AAAA,EACnD;AAAA,EAEA,IAAI,YAAqB;AACvB,WAAO,CAAC,KAAK,aAAa,UAAU,CAAC,KAAK,aAAa;AAAA,EACzD;AAAA,EAEA,IAAI,SAAiB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,eAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,QAAiB;AACnB,WAAO,KAAK,aAAa;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,UAA4B;AAC9B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,aAA8B;AAChC,WAAO,KAAK;AAAA,EACd;AACF;;;ACtcO,IAAM,sBAAN,MAA0B;AAAA,EAO/B,YAAY,UAAsC,CAAC,GAAG;AANtD,SAAQ,QAAkC;AAG1C,SAAQ,aAAsB;AAI5B,SAAK,gBAAgB,QAAQ,gBAAgB;AAC7C,SAAK,gBAAgB,QAAQ,gBAAgB;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,OAAsB;AAAA,EAE5B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,SAAsD;AAC7D,QAAI,KAAK,OAAO;AACd,cAAQ;AAAA,QACN;AAAA,MAEF;AACA,WAAK,MAAM,QAAQ;AAAA,IACrB;AAEA,SAAK,QAAQ,IAAI,kBAAkB;AAAA,MACjC,GAAG;AAAA,MACH,QAAQ,KAAK,iBAAiB,QAAQ,UAAU;AAAA,MAChD,cAAc,KAAK;AAAA,IACrB,CAAC;AAGD,SAAK,MAAM,kBAAkB,MAAM;AACjC,WAAK,aAAa;AAClB,UAAI,KAAK,4BAA4B;AACnC,aAAK,2BAA2B;AAAA,MAClC;AAAA,IACF,CAAC;AAED,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,SAAuB;AACjC,QAAI,KAAK,SAAS,KAAK,MAAM,OAAO,SAAS;AAC3C,WAAK,MAAM,QAAQ;AACnB,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,SAAgD;AACvD,QAAI,KAAK,SAAS,KAAK,MAAM,OAAO,SAAS;AAC3C,aAAO,KAAK;AAAA,IACd;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,KAAK,OAAgB,QAAiB,UAAyB;AAC7D,QAAI,CAAC,KAAK,OAAO;AACf,cAAQ,KAAK,uCAAuC;AACpD;AAAA,IACF;AAEA,UAAM,gBAAgB,UAAU;AAChC,SAAK,aAAa;AAElB,SAAK,MAAM,KAAK,aAAa;AAG7B,QAAI,aAAa,QAAW;AAC1B,YAAM,mBAAmB,WAAW,KAAK;AACzC,iBAAW,MAAM;AACf,YAAI,KAAK,YAAY;AACnB,eAAK,MAAM;AACX,cAAI,KAAK,4BAA4B;AACnC,iBAAK,2BAA2B;AAAA,UAClC;AAAA,QACF;AAAA,MACF,GAAG,mBAAmB,GAAI;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,QAAI,KAAK,OAAO;AACd,WAAK,MAAM,MAAM;AAAA,IACnB;AACA,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACX,QAAI,KAAK,OAAO;AACd,WAAK,MAAM,KAAK;AAAA,IAClB;AACA,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,MAAoB;AACzB,QAAI,KAAK,OAAO;AACd,WAAK,MAAM,OAAO,IAAI;AAAA,IACxB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAyB;AACvB,QAAI,KAAK,OAAO;AACd,aAAO,KAAK,MAAM;AAAA,IACpB;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,QAAsB;AACpC,SAAK,gBAAgB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,CAAC;AACpD,QAAI,KAAK,OAAO;AACd,WAAK,MAAM,UAAU,KAAK,aAAa;AAAA,IACzC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,MAAoB;AAClC,SAAK,gBAAgB,KAAK,IAAI,KAAK,KAAK,IAAI,GAAK,IAAI,CAAC;AACtD,QAAI,KAAK,OAAO;AACd,WAAK,MAAM,gBAAgB,KAAK,aAAa;AAAA,IAC/C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ,SAAiB,OAAsB;AAC7C,UAAM,QAAQ,KAAK,SAAS,OAAO;AACnC,QAAI,OAAO;AACT,YAAM,SAAS,KAAK;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAQ,UAAkB,SAAwB;AAEhD,YAAQ,KAAK,uEAAuE;AAAA,EACtF;AAAA;AAAA;AAAA;AAAA,EAKA,sBAAsB,UAA4B;AAChD,SAAK,6BAA6B;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACd,QAAI,KAAK,OAAO;AACd,WAAK,MAAM,QAAQ;AACnB,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,YAAqB;AACvB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,eAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,eAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,WAAmB;AACrB,WAAO,KAAK,OAAO,YAAY;AAAA,EACjC;AAAA,EAEA,IAAI,aAAqB;AAGvB,WAAO,KAAK,OAAO,MAAM,eAAe;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,IAAI,aAA8B;AAChC,WAAO,KAAK,OAAO,cAAc;AAAA,EACnC;AACF;;;ACpNO,SAAS,qBAAqB,QAAqD;AACxF,SACE,qBAAqB,UACrB,OAAQ,OAAmC,oBAAoB;AAEnE;","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
|
-
*
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/MediaElementTrack.ts","../src/MediaElementPlayout.ts","../src/types.ts"],"sourcesContent":["import type { WaveformDataObject } 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.
|
|
3
|
+
"version": "10.1.0",
|
|
4
4
|
"description": "HTMLMediaElement-based playout engine for waveform-playlist with pitch-preserving playback rate",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -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": "
|
|
44
|
+
"@waveform-playlist/core": "10.1.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"dev": "tsup --watch",
|
|
49
|
+
"typecheck": "tsc --noEmit"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|