@waveform-playlist/media-element-playout 10.1.2 → 10.3.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/dist/index.d.mts CHANGED
@@ -12,8 +12,10 @@ interface MediaElementTrackOptions {
12
12
  name?: string;
13
13
  /** Initial volume (0.0 to 1.0) */
14
14
  volume?: number;
15
- /** Initial playback rate (0.5 to 2.0, pitch preserved) */
15
+ /** Initial playback rate (0.5 to 2.0) */
16
16
  playbackRate?: number;
17
+ /** Whether to preserve pitch when changing playback rate (default: true) */
18
+ preservesPitch?: boolean;
17
19
  /**
18
20
  * AudioContext for Web Audio routing.
19
21
  * When provided, audio is routed through Web Audio nodes for fades and effects:
@@ -158,6 +160,9 @@ interface MediaElementPlayoutOptions {
158
160
  masterVolume?: number;
159
161
  /** Initial playback rate (0.5 to 2.0) */
160
162
  playbackRate?: number;
163
+ /** Whether to preserve pitch when changing playback rate (default: true).
164
+ * Set to false when using an external pitch processor like SoundTouch. */
165
+ preservesPitch?: boolean;
161
166
  }
162
167
  /**
163
168
  * Single-track playout engine using HTMLAudioElement.
@@ -180,6 +185,7 @@ declare class MediaElementPlayout {
180
185
  private track;
181
186
  private _masterVolume;
182
187
  private _playbackRate;
188
+ private _preservesPitch;
183
189
  private _isPlaying;
184
190
  private onPlaybackCompleteCallback?;
185
191
  constructor(options?: MediaElementPlayoutOptions);
package/dist/index.d.ts CHANGED
@@ -12,8 +12,10 @@ interface MediaElementTrackOptions {
12
12
  name?: string;
13
13
  /** Initial volume (0.0 to 1.0) */
14
14
  volume?: number;
15
- /** Initial playback rate (0.5 to 2.0, pitch preserved) */
15
+ /** Initial playback rate (0.5 to 2.0) */
16
16
  playbackRate?: number;
17
+ /** Whether to preserve pitch when changing playback rate (default: true) */
18
+ preservesPitch?: boolean;
17
19
  /**
18
20
  * AudioContext for Web Audio routing.
19
21
  * When provided, audio is routed through Web Audio nodes for fades and effects:
@@ -158,6 +160,9 @@ interface MediaElementPlayoutOptions {
158
160
  masterVolume?: number;
159
161
  /** Initial playback rate (0.5 to 2.0) */
160
162
  playbackRate?: number;
163
+ /** Whether to preserve pitch when changing playback rate (default: true).
164
+ * Set to false when using an external pitch processor like SoundTouch. */
165
+ preservesPitch?: boolean;
161
166
  }
162
167
  /**
163
168
  * Single-track playout engine using HTMLAudioElement.
@@ -180,6 +185,7 @@ declare class MediaElementPlayout {
180
185
  private track;
181
186
  private _masterVolume;
182
187
  private _playbackRate;
188
+ private _preservesPitch;
183
189
  private _isPlaying;
184
190
  private onPlaybackCompleteCallback?;
185
191
  constructor(options?: MediaElementPlayoutOptions);
package/dist/index.js CHANGED
@@ -63,13 +63,14 @@ var MediaElementTrack = class {
63
63
  }
64
64
  this.audioElement.preload = "auto";
65
65
  this.audioElement.playbackRate = this._playbackRate;
66
+ const shouldPreservePitch = options.preservesPitch ?? true;
66
67
  const audio = this.audioElement;
67
68
  if ("preservesPitch" in this.audioElement) {
68
- audio.preservesPitch = true;
69
+ audio.preservesPitch = shouldPreservePitch;
69
70
  } else if ("mozPreservesPitch" in this.audioElement) {
70
- audio.mozPreservesPitch = true;
71
+ audio.mozPreservesPitch = shouldPreservePitch;
71
72
  } else if ("webkitPreservesPitch" in this.audioElement) {
72
- audio.webkitPreservesPitch = true;
73
+ audio.webkitPreservesPitch = shouldPreservePitch;
73
74
  }
74
75
  if (options.audioContext) {
75
76
  this._audioContext = options.audioContext;
@@ -374,6 +375,7 @@ var MediaElementPlayout = class {
374
375
  this._isPlaying = false;
375
376
  this._masterVolume = options.masterVolume ?? 1;
376
377
  this._playbackRate = options.playbackRate ?? 1;
378
+ this._preservesPitch = options.preservesPitch ?? true;
377
379
  }
378
380
  /**
379
381
  * Initialize the playout engine.
@@ -397,7 +399,8 @@ var MediaElementPlayout = class {
397
399
  this.track = new MediaElementTrack({
398
400
  ...options,
399
401
  volume: this._masterVolume * (options.volume ?? 1),
400
- playbackRate: this._playbackRate
402
+ playbackRate: this._playbackRate,
403
+ preservesPitch: this._preservesPitch
401
404
  });
402
405
  this.track.setOnStopCallback(() => {
403
406
  this._isPlaying = false;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/MediaElementTrack.ts","../src/MediaElementPlayout.ts","../src/types.ts"],"sourcesContent":["export { MediaElementPlayout } from './MediaElementPlayout';\nexport { MediaElementTrack } from './MediaElementTrack';\n\nexport type { MediaElementPlayoutOptions } from './MediaElementPlayout';\nexport type { MediaElementTrackOptions, FadeConfig } from './MediaElementTrack';\n\n// Common interface types\nexport type { PlayoutEngine, PlaybackRateEngine } from './types';\nexport { supportsPlaybackRate } from './types';\n","import type { WaveformDataObject, FadeConfig } from '@waveform-playlist/core';\nimport { applyFadeIn, applyFadeOut, generateCurve } from '@waveform-playlist/core';\n\nexport type { FadeConfig } from '@waveform-playlist/core';\n\n/**\n * Extended HTMLAudioElement with vendor-prefixed preservesPitch properties.\n * `preservesPitch` is standard; the `moz` and `webkit` prefixes are for older browsers.\n */\ninterface VendorPrefixedPitch {\n preservesPitch?: boolean;\n mozPreservesPitch?: boolean;\n webkitPreservesPitch?: boolean;\n}\n\nexport interface MediaElementTrackOptions {\n /** The audio source - can be a URL, Blob URL, or HTMLAudioElement */\n source: string | HTMLAudioElement;\n /** Pre-computed waveform data for visualization (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":[]}
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) */\n playbackRate?: number;\n /** Whether to preserve pitch when changing playback rate (default: true) */\n preservesPitch?: boolean;\n /**\n * AudioContext for Web Audio routing.\n * When provided, audio is routed through Web Audio nodes for fades and effects:\n * HTMLAudioElement → MediaElementSourceNode → fadeGain → volumeGain → destination\n *\n * Without this, playback uses HTMLAudioElement directly (no fades or effects).\n *\n * Note: createMediaElementSource() can only be called once per element.\n * Once routed, HTMLAudioElement.volume no longer works — volume is controlled\n * via the Web Audio GainNode instead.\n */\n audioContext?: AudioContext;\n /** Fade in configuration (requires audioContext) */\n fadeIn?: FadeConfig;\n /** Fade out configuration (requires audioContext) */\n fadeOut?: FadeConfig;\n}\n\n/**\n * Single-track playback using HTMLAudioElement.\n *\n * Benefits over AudioBuffer/Tone.js:\n * - Pitch-preserving playback rate (0.5x - 2.0x) via browser's built-in algorithm\n * - No AudioBuffer decoding required (uses pre-computed peaks for visualization)\n * - Simpler, lighter-weight for single-track use cases\n *\n * When an AudioContext is provided:\n * - Audio routes through Web Audio graph for fades and effects\n * - Volume controlled via GainNode (HTMLAudioElement.volume is bypassed)\n * - Output node exposed for connecting external effects chains\n */\nexport class MediaElementTrack {\n private audioElement: HTMLAudioElement;\n private ownsElement: boolean; // Whether we created the element (need to clean up)\n private _peaks: WaveformDataObject;\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 // Set pitch preservation (default: true).\n // When false, the browser won't apply its own pitch correction — useful\n // when an external processor like SoundTouch handles pitch compensation.\n // Vendor-prefixed properties are non-standard; cast once for type safety.\n const shouldPreservePitch = options.preservesPitch ?? true;\n const audio = this.audioElement as unknown as VendorPrefixedPitch;\n if ('preservesPitch' in this.audioElement) {\n audio.preservesPitch = shouldPreservePitch;\n } else if ('mozPreservesPitch' in this.audioElement) {\n audio.mozPreservesPitch = shouldPreservePitch;\n } else if ('webkitPreservesPitch' in this.audioElement) {\n audio.webkitPreservesPitch = shouldPreservePitch;\n }\n\n // Set up Web Audio routing if AudioContext provided\n if (options.audioContext) {\n this._audioContext = options.audioContext;\n try {\n this._sourceNode = options.audioContext.createMediaElementSource(this.audioElement);\n } catch (err) {\n throw new Error(\n '[waveform-playlist] MediaElementTrack: createMediaElementSource() failed. ' +\n 'This can happen if the audio element is already connected to another AudioContext. ' +\n 'Each audio element can only have one MediaElementSourceNode. ' +\n 'Original error: ' +\n String(err)\n );\n }\n this._fadeGain = options.audioContext.createGain();\n this._volumeGain = options.audioContext.createGain();\n this._volumeGain.gain.value = this._volume;\n\n this._sourceNode.connect(this._fadeGain);\n this._fadeGain.connect(this._volumeGain);\n this._volumeGain.connect(options.audioContext.destination);\n\n // With Web Audio routing, HTMLAudioElement.volume is bypassed.\n // Set it to 1 so it doesn't attenuate the signal before the source node.\n this.audioElement.volume = 1;\n } else {\n // Without Web Audio, use HTMLAudioElement.volume directly\n this.audioElement.volume = this._volume;\n }\n\n // Set up event listeners\n this.audioElement.addEventListener('ended', this.handleEnded);\n this.audioElement.addEventListener('timeupdate', this.handleTimeUpdate);\n }\n\n private handleEnded = () => {\n this._cancelFades();\n if (this.onStopCallback) {\n this.onStopCallback();\n }\n };\n\n private handleTimeUpdate = () => {\n if (this.onTimeUpdateCallback) {\n this.onTimeUpdateCallback(this.audioElement.currentTime);\n }\n };\n\n /**\n * Schedule fade automation on the fade GainNode.\n * Called at the start of each play() — fades are relative to the playback offset.\n */\n private _scheduleFades(offset: number): void {\n if (!this._fadeGain || !this._audioContext) return;\n\n const fadeGain = this._fadeGain.gain;\n const now = this._audioContext.currentTime;\n const totalDuration = this.duration;\n\n // Reset fade gain\n fadeGain.cancelScheduledValues(0);\n fadeGain.setValueAtTime(1, now);\n\n // Fade in\n if (this._fadeIn && this._fadeIn.duration > 0) {\n const fadeInEnd = this._fadeIn.duration;\n if (offset < fadeInEnd) {\n const remainingFade = fadeInEnd - offset;\n const fadeType = this._fadeIn.type ?? 'linear';\n if (offset === 0) {\n // Full fade from beginning\n applyFadeIn(fadeGain, now, remainingFade, fadeType, 0, 1);\n } else {\n // Partial fade — slice the original curve to preserve shape\n const curve = generateCurve(fadeType, 1000, true);\n const startIndex = Math.round((offset / this._fadeIn.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingFade);\n }\n }\n }\n\n // Fade out\n if (this._fadeOut && this._fadeOut.duration > 0) {\n const fadeOutStart = totalDuration - this._fadeOut.duration;\n if (offset < totalDuration && fadeOutStart < totalDuration) {\n if (offset > fadeOutStart) {\n // Already past the fade-out start — slice original curve to preserve shape\n const elapsed = offset - fadeOutStart;\n const fadeType = this._fadeOut.type ?? 'linear';\n const curve = generateCurve(fadeType, 1000, false);\n const startIndex = Math.round((elapsed / this._fadeOut.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n const remainingDuration = this._fadeOut.duration - elapsed;\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingDuration);\n } else {\n // Schedule full fade-out at the right time\n const delayUntilFadeOut = fadeOutStart - offset;\n applyFadeOut(\n fadeGain,\n now + delayUntilFadeOut,\n this._fadeOut.duration,\n this._fadeOut.type ?? 'linear',\n 1,\n 0\n );\n }\n }\n }\n }\n\n /**\n * Cancel any scheduled fade automation.\n */\n private _cancelFades(): void {\n if (this._fadeGain) {\n this._fadeGain.gain.cancelScheduledValues(0);\n this._fadeGain.gain.value = 1;\n }\n }\n\n /**\n * Start playback from a specific time.\n * Resumes the AudioContext first if suspended, then schedules fades\n * (fades depend on audioContext.currentTime being non-zero).\n */\n play(offset: number = 0): void {\n const startPlayback = () => {\n this._scheduleFades(offset);\n this.audioElement.currentTime = offset;\n this.audioElement.play().catch((err) => {\n console.warn('[waveform-playlist] MediaElementTrack: play() failed: ' + String(err));\n });\n };\n\n // Resume AudioContext if suspended (browser autoplay policy).\n // Must await resume before scheduling fades — audioContext.currentTime\n // is 0 while suspended, which would schedule all fades in the past.\n if (this._audioContext && this._audioContext.state === 'suspended') {\n this._audioContext\n .resume()\n .then(startPlayback)\n .catch((err) => {\n console.warn(\n '[waveform-playlist] MediaElementTrack: AudioContext.resume() failed: ' + String(err)\n );\n });\n } else {\n startPlayback();\n }\n }\n\n /**\n * Pause playback\n */\n pause(): void {\n this._cancelFades();\n this.audioElement.pause();\n }\n\n /**\n * Stop playback and reset to beginning\n */\n stop(): void {\n this._cancelFades();\n this.audioElement.pause();\n this.audioElement.currentTime = 0;\n }\n\n /**\n * Seek to a specific time\n */\n seekTo(time: number): void {\n this.audioElement.currentTime = Math.max(0, Math.min(time, this.duration));\n }\n\n /**\n * Set volume (0.0 to 1.0)\n */\n setVolume(volume: number): void {\n this._volume = Math.max(0, Math.min(1, volume));\n if (this._volumeGain) {\n this._volumeGain.gain.value = this._volume;\n } else {\n this.audioElement.volume = this._volume;\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved)\n */\n setPlaybackRate(rate: number): void {\n const clampedRate = Math.max(0.5, Math.min(2.0, rate));\n this._playbackRate = clampedRate;\n this.audioElement.playbackRate = clampedRate;\n }\n\n /**\n * Set muted state\n */\n setMuted(muted: boolean): void {\n this.audioElement.muted = muted;\n }\n\n /**\n * Set fade in configuration\n */\n setFadeIn(fadeIn: FadeConfig | undefined): void {\n this._fadeIn = fadeIn;\n }\n\n /**\n * Set fade out configuration\n */\n setFadeOut(fadeOut: FadeConfig | undefined): void {\n this._fadeOut = fadeOut;\n }\n\n /**\n * Set callback for when playback ends\n */\n setOnStopCallback(callback: () => void): void {\n this.onStopCallback = callback;\n }\n\n /**\n * Set callback for time updates\n */\n setOnTimeUpdateCallback(callback: (time: number) => void): void {\n this.onTimeUpdateCallback = callback;\n }\n\n /**\n * Connect the output to a different destination (for effects chains).\n * Disconnects from the current destination first.\n *\n * @param destination - The AudioNode to connect to\n */\n connectOutput(destination: AudioNode): void {\n if (!this._volumeGain) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: connectOutput() requires audioContext. ' +\n 'Pass audioContext in constructor options.'\n );\n return;\n }\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before connectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(destination);\n }\n\n /**\n * Disconnect the output and reconnect to the default AudioContext destination.\n */\n disconnectOutput(): void {\n if (!this._volumeGain || !this._audioContext) return;\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before disconnectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(this._audioContext.destination);\n }\n\n /**\n * Clean up resources\n */\n dispose(): void {\n this.audioElement.removeEventListener('ended', this.handleEnded);\n this.audioElement.removeEventListener('timeupdate', this.handleTimeUpdate);\n this._cancelFades();\n this.audioElement.pause();\n\n if (this._sourceNode) {\n try {\n this._sourceNode.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: sourceNode disconnect failed: ' + String(err)\n );\n }\n }\n if (this._fadeGain) {\n try {\n this._fadeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: fadeGain disconnect failed: ' + String(err)\n );\n }\n }\n if (this._volumeGain) {\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: volumeGain disconnect failed: ' + String(err)\n );\n }\n }\n\n if (this.ownsElement) {\n this.audioElement.src = '';\n this.audioElement.load(); // Release resources\n }\n }\n\n // Getters\n get id(): string {\n return this._id;\n }\n\n get name(): string {\n return this._name;\n }\n\n get peaks(): WaveformDataObject {\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 /** Whether to preserve pitch when changing playback rate (default: true).\n * Set to false when using an external pitch processor like SoundTouch. */\n preservesPitch?: boolean;\n}\n\n/**\n * Single-track playout engine using HTMLAudioElement.\n *\n * This is a lightweight alternative to TonePlayout for single-track use cases\n * that need pitch-preserving playback rate control.\n *\n * Key features:\n * - Pitch-preserving playback rate (0.5x - 2.0x)\n * - Uses pre-computed peaks (no AudioBuffer required)\n * - Simpler API for single-track playback\n *\n * Limitations:\n * - Single track only - will warn if multiple tracks added\n * - No multi-track mixing\n *\n * For multi-track editing, use TonePlayout from @waveform-playlist/playout instead.\n */\nexport class MediaElementPlayout {\n private track: MediaElementTrack | null = null;\n private _masterVolume: number;\n private _playbackRate: number;\n private _preservesPitch: boolean;\n private _isPlaying: boolean = false;\n private onPlaybackCompleteCallback?: () => void;\n\n constructor(options: MediaElementPlayoutOptions = {}) {\n this._masterVolume = options.masterVolume ?? 1;\n this._playbackRate = options.playbackRate ?? 1;\n this._preservesPitch = options.preservesPitch ?? true;\n }\n\n /**\n * Initialize the playout engine.\n * For MediaElementPlayout this is a no-op — HTMLAudioElement doesn't require\n * explicit initialization. When an AudioContext is provided for fades/effects,\n * it resumes automatically on first play via MediaElementTrack.\n */\n async init(): Promise<void> {\n // No initialization needed — audio element handles autoplay policy automatically\n }\n\n /**\n * Add a track to the playout.\n * Note: Only one track is supported. Adding a second track will dispose the first.\n */\n addTrack(options: MediaElementTrackOptions): MediaElementTrack {\n if (this.track) {\n console.warn(\n 'MediaElementPlayout: Only one track is supported. ' +\n 'Disposing previous track. For multi-track, use TonePlayout.'\n );\n this.track.dispose();\n }\n\n this.track = new MediaElementTrack({\n ...options,\n volume: this._masterVolume * (options.volume ?? 1),\n playbackRate: this._playbackRate,\n preservesPitch: this._preservesPitch,\n });\n\n // Set up stop callback\n this.track.setOnStopCallback(() => {\n this._isPlaying = false;\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n });\n\n return this.track;\n }\n\n /**\n * Remove a track by ID.\n */\n removeTrack(trackId: string): void {\n if (this.track && this.track.id === trackId) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n /**\n * Get a track by ID.\n */\n getTrack(trackId: string): MediaElementTrack | undefined {\n if (this.track && this.track.id === trackId) {\n return this.track;\n }\n return undefined;\n }\n\n /**\n * Start playback.\n * @param _when - Ignored (HTMLAudioElement doesn't support scheduled start)\n * @param offset - Start position in seconds\n * @param duration - Duration to play in seconds (optional)\n */\n play(_when?: number, offset?: number, duration?: number): void {\n if (!this.track) {\n console.warn('MediaElementPlayout: No track to play');\n return;\n }\n\n const startPosition = offset ?? 0;\n this._isPlaying = true;\n\n this.track.play(startPosition);\n\n // If duration is specified, schedule stop\n if (duration !== undefined) {\n const adjustedDuration = duration / this._playbackRate;\n setTimeout(() => {\n if (this._isPlaying) {\n this.pause();\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n }\n }, adjustedDuration * 1000);\n }\n }\n\n /**\n * Pause playback.\n */\n pause(): void {\n if (this.track) {\n this.track.pause();\n }\n this._isPlaying = false;\n }\n\n /**\n * Stop playback and reset to start.\n */\n stop(): void {\n if (this.track) {\n this.track.stop();\n }\n this._isPlaying = false;\n }\n\n /**\n * Seek to a specific time.\n */\n seekTo(time: number): void {\n if (this.track) {\n this.track.seekTo(time);\n }\n }\n\n /**\n * Get current playback time.\n */\n getCurrentTime(): number {\n if (this.track) {\n return this.track.currentTime;\n }\n return 0;\n }\n\n /**\n * Set master volume.\n */\n setMasterVolume(volume: number): void {\n this._masterVolume = Math.max(0, Math.min(1, volume));\n if (this.track) {\n this.track.setVolume(this._masterVolume);\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved).\n */\n setPlaybackRate(rate: number): void {\n this._playbackRate = Math.max(0.5, Math.min(2.0, rate));\n if (this.track) {\n this.track.setPlaybackRate(this._playbackRate);\n }\n }\n\n /**\n * Set mute state for a track.\n */\n setMute(trackId: string, muted: boolean): void {\n const track = this.getTrack(trackId);\n if (track) {\n track.setMuted(muted);\n }\n }\n\n /**\n * Set solo state for a track.\n * Note: With single track, solo is effectively the same as unmute.\n */\n setSolo(_trackId: string, _soloed: boolean): void {\n // No-op for single track - solo doesn't make sense\n console.warn('MediaElementPlayout: Solo is not applicable for single-track playback');\n }\n\n /**\n * Set callback for when playback completes.\n */\n setOnPlaybackComplete(callback: () => void): void {\n this.onPlaybackCompleteCallback = callback;\n }\n\n /**\n * Clean up resources.\n */\n dispose(): void {\n if (this.track) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n // Getters\n get isPlaying(): boolean {\n return this._isPlaying;\n }\n\n get masterVolume(): number {\n return this._masterVolume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get duration(): number {\n return this.track?.duration ?? 0;\n }\n\n get sampleRate(): number {\n // HTMLAudioElement doesn't expose sample rate directly\n // Return a common default - peaks will have the actual sample rate\n return this.track?.peaks.sample_rate ?? 44100;\n }\n\n /**\n * Get the volume GainNode output for connecting external effects chains.\n * Returns null if no AudioContext was provided to the track.\n *\n * Usage: disconnect from default destination, connect to effect input,\n * then connect effect output to audioContext.destination.\n */\n get outputNode(): GainNode | null {\n return this.track?.outputNode ?? null;\n }\n}\n","/**\n * Common interface for playout engines.\n *\n * Both TonePlayout and MediaElementPlayout implement this interface,\n * allowing the browser package to work with either engine.\n */\nexport interface PlayoutEngine {\n // Lifecycle\n init(): Promise<void>;\n dispose(): void;\n\n // Playback\n play(when?: number, offset?: number, duration?: number): void;\n pause(): void;\n stop(): void;\n seekTo(time: number): void;\n getCurrentTime(): number;\n\n // Volume\n setMasterVolume(volume: number): void;\n\n // Track controls (optional - not all engines support all features)\n setMute?(trackId: string, muted: boolean): void;\n setSolo?(trackId: string, soloed: boolean): void;\n\n // Callbacks\n setOnPlaybackComplete(callback: () => void): void;\n\n // State\n readonly isPlaying: boolean;\n readonly duration: number;\n readonly sampleRate: number;\n}\n\n/**\n * Extended interface for engines that support playback rate.\n */\nexport interface PlaybackRateEngine extends PlayoutEngine {\n setPlaybackRate(rate: number): void;\n readonly playbackRate: number;\n}\n\n/**\n * Type guard to check if an engine supports playback rate.\n */\nexport function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine {\n return (\n 'setPlaybackRate' in engine &&\n typeof (engine as Record<string, unknown>).setPlaybackRate === 'function'\n );\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -35,13 +35,14 @@ var MediaElementTrack = class {
35
35
  }
36
36
  this.audioElement.preload = "auto";
37
37
  this.audioElement.playbackRate = this._playbackRate;
38
+ const shouldPreservePitch = options.preservesPitch ?? true;
38
39
  const audio = this.audioElement;
39
40
  if ("preservesPitch" in this.audioElement) {
40
- audio.preservesPitch = true;
41
+ audio.preservesPitch = shouldPreservePitch;
41
42
  } else if ("mozPreservesPitch" in this.audioElement) {
42
- audio.mozPreservesPitch = true;
43
+ audio.mozPreservesPitch = shouldPreservePitch;
43
44
  } else if ("webkitPreservesPitch" in this.audioElement) {
44
- audio.webkitPreservesPitch = true;
45
+ audio.webkitPreservesPitch = shouldPreservePitch;
45
46
  }
46
47
  if (options.audioContext) {
47
48
  this._audioContext = options.audioContext;
@@ -346,6 +347,7 @@ var MediaElementPlayout = class {
346
347
  this._isPlaying = false;
347
348
  this._masterVolume = options.masterVolume ?? 1;
348
349
  this._playbackRate = options.playbackRate ?? 1;
350
+ this._preservesPitch = options.preservesPitch ?? true;
349
351
  }
350
352
  /**
351
353
  * Initialize the playout engine.
@@ -369,7 +371,8 @@ var MediaElementPlayout = class {
369
371
  this.track = new MediaElementTrack({
370
372
  ...options,
371
373
  volume: this._masterVolume * (options.volume ?? 1),
372
- playbackRate: this._playbackRate
374
+ playbackRate: this._playbackRate,
375
+ preservesPitch: this._preservesPitch
373
376
  });
374
377
  this.track.setOnStopCallback(() => {
375
378
  this._isPlaying = false;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/MediaElementTrack.ts","../src/MediaElementPlayout.ts","../src/types.ts"],"sourcesContent":["import type { WaveformDataObject, FadeConfig } from '@waveform-playlist/core';\nimport { applyFadeIn, applyFadeOut, generateCurve } from '@waveform-playlist/core';\n\nexport type { FadeConfig } from '@waveform-playlist/core';\n\n/**\n * Extended HTMLAudioElement with vendor-prefixed preservesPitch properties.\n * `preservesPitch` is standard; the `moz` and `webkit` prefixes are for older browsers.\n */\ninterface VendorPrefixedPitch {\n preservesPitch?: boolean;\n mozPreservesPitch?: boolean;\n webkitPreservesPitch?: boolean;\n}\n\nexport interface MediaElementTrackOptions {\n /** The audio source - can be a URL, Blob URL, or HTMLAudioElement */\n source: string | HTMLAudioElement;\n /** Pre-computed waveform data for visualization (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":[]}
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) */\n playbackRate?: number;\n /** Whether to preserve pitch when changing playback rate (default: true) */\n preservesPitch?: boolean;\n /**\n * AudioContext for Web Audio routing.\n * When provided, audio is routed through Web Audio nodes for fades and effects:\n * HTMLAudioElement → MediaElementSourceNode → fadeGain → volumeGain → destination\n *\n * Without this, playback uses HTMLAudioElement directly (no fades or effects).\n *\n * Note: createMediaElementSource() can only be called once per element.\n * Once routed, HTMLAudioElement.volume no longer works — volume is controlled\n * via the Web Audio GainNode instead.\n */\n audioContext?: AudioContext;\n /** Fade in configuration (requires audioContext) */\n fadeIn?: FadeConfig;\n /** Fade out configuration (requires audioContext) */\n fadeOut?: FadeConfig;\n}\n\n/**\n * Single-track playback using HTMLAudioElement.\n *\n * Benefits over AudioBuffer/Tone.js:\n * - Pitch-preserving playback rate (0.5x - 2.0x) via browser's built-in algorithm\n * - No AudioBuffer decoding required (uses pre-computed peaks for visualization)\n * - Simpler, lighter-weight for single-track use cases\n *\n * When an AudioContext is provided:\n * - Audio routes through Web Audio graph for fades and effects\n * - Volume controlled via GainNode (HTMLAudioElement.volume is bypassed)\n * - Output node exposed for connecting external effects chains\n */\nexport class MediaElementTrack {\n private audioElement: HTMLAudioElement;\n private ownsElement: boolean; // Whether we created the element (need to clean up)\n private _peaks: WaveformDataObject;\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 // Set pitch preservation (default: true).\n // When false, the browser won't apply its own pitch correction — useful\n // when an external processor like SoundTouch handles pitch compensation.\n // Vendor-prefixed properties are non-standard; cast once for type safety.\n const shouldPreservePitch = options.preservesPitch ?? true;\n const audio = this.audioElement as unknown as VendorPrefixedPitch;\n if ('preservesPitch' in this.audioElement) {\n audio.preservesPitch = shouldPreservePitch;\n } else if ('mozPreservesPitch' in this.audioElement) {\n audio.mozPreservesPitch = shouldPreservePitch;\n } else if ('webkitPreservesPitch' in this.audioElement) {\n audio.webkitPreservesPitch = shouldPreservePitch;\n }\n\n // Set up Web Audio routing if AudioContext provided\n if (options.audioContext) {\n this._audioContext = options.audioContext;\n try {\n this._sourceNode = options.audioContext.createMediaElementSource(this.audioElement);\n } catch (err) {\n throw new Error(\n '[waveform-playlist] MediaElementTrack: createMediaElementSource() failed. ' +\n 'This can happen if the audio element is already connected to another AudioContext. ' +\n 'Each audio element can only have one MediaElementSourceNode. ' +\n 'Original error: ' +\n String(err)\n );\n }\n this._fadeGain = options.audioContext.createGain();\n this._volumeGain = options.audioContext.createGain();\n this._volumeGain.gain.value = this._volume;\n\n this._sourceNode.connect(this._fadeGain);\n this._fadeGain.connect(this._volumeGain);\n this._volumeGain.connect(options.audioContext.destination);\n\n // With Web Audio routing, HTMLAudioElement.volume is bypassed.\n // Set it to 1 so it doesn't attenuate the signal before the source node.\n this.audioElement.volume = 1;\n } else {\n // Without Web Audio, use HTMLAudioElement.volume directly\n this.audioElement.volume = this._volume;\n }\n\n // Set up event listeners\n this.audioElement.addEventListener('ended', this.handleEnded);\n this.audioElement.addEventListener('timeupdate', this.handleTimeUpdate);\n }\n\n private handleEnded = () => {\n this._cancelFades();\n if (this.onStopCallback) {\n this.onStopCallback();\n }\n };\n\n private handleTimeUpdate = () => {\n if (this.onTimeUpdateCallback) {\n this.onTimeUpdateCallback(this.audioElement.currentTime);\n }\n };\n\n /**\n * Schedule fade automation on the fade GainNode.\n * Called at the start of each play() — fades are relative to the playback offset.\n */\n private _scheduleFades(offset: number): void {\n if (!this._fadeGain || !this._audioContext) return;\n\n const fadeGain = this._fadeGain.gain;\n const now = this._audioContext.currentTime;\n const totalDuration = this.duration;\n\n // Reset fade gain\n fadeGain.cancelScheduledValues(0);\n fadeGain.setValueAtTime(1, now);\n\n // Fade in\n if (this._fadeIn && this._fadeIn.duration > 0) {\n const fadeInEnd = this._fadeIn.duration;\n if (offset < fadeInEnd) {\n const remainingFade = fadeInEnd - offset;\n const fadeType = this._fadeIn.type ?? 'linear';\n if (offset === 0) {\n // Full fade from beginning\n applyFadeIn(fadeGain, now, remainingFade, fadeType, 0, 1);\n } else {\n // Partial fade — slice the original curve to preserve shape\n const curve = generateCurve(fadeType, 1000, true);\n const startIndex = Math.round((offset / this._fadeIn.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingFade);\n }\n }\n }\n\n // Fade out\n if (this._fadeOut && this._fadeOut.duration > 0) {\n const fadeOutStart = totalDuration - this._fadeOut.duration;\n if (offset < totalDuration && fadeOutStart < totalDuration) {\n if (offset > fadeOutStart) {\n // Already past the fade-out start — slice original curve to preserve shape\n const elapsed = offset - fadeOutStart;\n const fadeType = this._fadeOut.type ?? 'linear';\n const curve = generateCurve(fadeType, 1000, false);\n const startIndex = Math.round((elapsed / this._fadeOut.duration) * (curve.length - 1));\n const sliced = curve.slice(startIndex);\n const remainingDuration = this._fadeOut.duration - elapsed;\n fadeGain.setValueAtTime(sliced[0], now);\n fadeGain.setValueCurveAtTime(sliced, now, remainingDuration);\n } else {\n // Schedule full fade-out at the right time\n const delayUntilFadeOut = fadeOutStart - offset;\n applyFadeOut(\n fadeGain,\n now + delayUntilFadeOut,\n this._fadeOut.duration,\n this._fadeOut.type ?? 'linear',\n 1,\n 0\n );\n }\n }\n }\n }\n\n /**\n * Cancel any scheduled fade automation.\n */\n private _cancelFades(): void {\n if (this._fadeGain) {\n this._fadeGain.gain.cancelScheduledValues(0);\n this._fadeGain.gain.value = 1;\n }\n }\n\n /**\n * Start playback from a specific time.\n * Resumes the AudioContext first if suspended, then schedules fades\n * (fades depend on audioContext.currentTime being non-zero).\n */\n play(offset: number = 0): void {\n const startPlayback = () => {\n this._scheduleFades(offset);\n this.audioElement.currentTime = offset;\n this.audioElement.play().catch((err) => {\n console.warn('[waveform-playlist] MediaElementTrack: play() failed: ' + String(err));\n });\n };\n\n // Resume AudioContext if suspended (browser autoplay policy).\n // Must await resume before scheduling fades — audioContext.currentTime\n // is 0 while suspended, which would schedule all fades in the past.\n if (this._audioContext && this._audioContext.state === 'suspended') {\n this._audioContext\n .resume()\n .then(startPlayback)\n .catch((err) => {\n console.warn(\n '[waveform-playlist] MediaElementTrack: AudioContext.resume() failed: ' + String(err)\n );\n });\n } else {\n startPlayback();\n }\n }\n\n /**\n * Pause playback\n */\n pause(): void {\n this._cancelFades();\n this.audioElement.pause();\n }\n\n /**\n * Stop playback and reset to beginning\n */\n stop(): void {\n this._cancelFades();\n this.audioElement.pause();\n this.audioElement.currentTime = 0;\n }\n\n /**\n * Seek to a specific time\n */\n seekTo(time: number): void {\n this.audioElement.currentTime = Math.max(0, Math.min(time, this.duration));\n }\n\n /**\n * Set volume (0.0 to 1.0)\n */\n setVolume(volume: number): void {\n this._volume = Math.max(0, Math.min(1, volume));\n if (this._volumeGain) {\n this._volumeGain.gain.value = this._volume;\n } else {\n this.audioElement.volume = this._volume;\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved)\n */\n setPlaybackRate(rate: number): void {\n const clampedRate = Math.max(0.5, Math.min(2.0, rate));\n this._playbackRate = clampedRate;\n this.audioElement.playbackRate = clampedRate;\n }\n\n /**\n * Set muted state\n */\n setMuted(muted: boolean): void {\n this.audioElement.muted = muted;\n }\n\n /**\n * Set fade in configuration\n */\n setFadeIn(fadeIn: FadeConfig | undefined): void {\n this._fadeIn = fadeIn;\n }\n\n /**\n * Set fade out configuration\n */\n setFadeOut(fadeOut: FadeConfig | undefined): void {\n this._fadeOut = fadeOut;\n }\n\n /**\n * Set callback for when playback ends\n */\n setOnStopCallback(callback: () => void): void {\n this.onStopCallback = callback;\n }\n\n /**\n * Set callback for time updates\n */\n setOnTimeUpdateCallback(callback: (time: number) => void): void {\n this.onTimeUpdateCallback = callback;\n }\n\n /**\n * Connect the output to a different destination (for effects chains).\n * Disconnects from the current destination first.\n *\n * @param destination - The AudioNode to connect to\n */\n connectOutput(destination: AudioNode): void {\n if (!this._volumeGain) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: connectOutput() requires audioContext. ' +\n 'Pass audioContext in constructor options.'\n );\n return;\n }\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before connectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(destination);\n }\n\n /**\n * Disconnect the output and reconnect to the default AudioContext destination.\n */\n disconnectOutput(): void {\n if (!this._volumeGain || !this._audioContext) return;\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: disconnect before disconnectOutput failed: ' +\n String(err)\n );\n }\n this._volumeGain.connect(this._audioContext.destination);\n }\n\n /**\n * Clean up resources\n */\n dispose(): void {\n this.audioElement.removeEventListener('ended', this.handleEnded);\n this.audioElement.removeEventListener('timeupdate', this.handleTimeUpdate);\n this._cancelFades();\n this.audioElement.pause();\n\n if (this._sourceNode) {\n try {\n this._sourceNode.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: sourceNode disconnect failed: ' + String(err)\n );\n }\n }\n if (this._fadeGain) {\n try {\n this._fadeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: fadeGain disconnect failed: ' + String(err)\n );\n }\n }\n if (this._volumeGain) {\n try {\n this._volumeGain.disconnect();\n } catch (err) {\n console.warn(\n '[waveform-playlist] MediaElementTrack: volumeGain disconnect failed: ' + String(err)\n );\n }\n }\n\n if (this.ownsElement) {\n this.audioElement.src = '';\n this.audioElement.load(); // Release resources\n }\n }\n\n // Getters\n get id(): string {\n return this._id;\n }\n\n get name(): string {\n return this._name;\n }\n\n get peaks(): WaveformDataObject {\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 /** Whether to preserve pitch when changing playback rate (default: true).\n * Set to false when using an external pitch processor like SoundTouch. */\n preservesPitch?: boolean;\n}\n\n/**\n * Single-track playout engine using HTMLAudioElement.\n *\n * This is a lightweight alternative to TonePlayout for single-track use cases\n * that need pitch-preserving playback rate control.\n *\n * Key features:\n * - Pitch-preserving playback rate (0.5x - 2.0x)\n * - Uses pre-computed peaks (no AudioBuffer required)\n * - Simpler API for single-track playback\n *\n * Limitations:\n * - Single track only - will warn if multiple tracks added\n * - No multi-track mixing\n *\n * For multi-track editing, use TonePlayout from @waveform-playlist/playout instead.\n */\nexport class MediaElementPlayout {\n private track: MediaElementTrack | null = null;\n private _masterVolume: number;\n private _playbackRate: number;\n private _preservesPitch: boolean;\n private _isPlaying: boolean = false;\n private onPlaybackCompleteCallback?: () => void;\n\n constructor(options: MediaElementPlayoutOptions = {}) {\n this._masterVolume = options.masterVolume ?? 1;\n this._playbackRate = options.playbackRate ?? 1;\n this._preservesPitch = options.preservesPitch ?? true;\n }\n\n /**\n * Initialize the playout engine.\n * For MediaElementPlayout this is a no-op — HTMLAudioElement doesn't require\n * explicit initialization. When an AudioContext is provided for fades/effects,\n * it resumes automatically on first play via MediaElementTrack.\n */\n async init(): Promise<void> {\n // No initialization needed — audio element handles autoplay policy automatically\n }\n\n /**\n * Add a track to the playout.\n * Note: Only one track is supported. Adding a second track will dispose the first.\n */\n addTrack(options: MediaElementTrackOptions): MediaElementTrack {\n if (this.track) {\n console.warn(\n 'MediaElementPlayout: Only one track is supported. ' +\n 'Disposing previous track. For multi-track, use TonePlayout.'\n );\n this.track.dispose();\n }\n\n this.track = new MediaElementTrack({\n ...options,\n volume: this._masterVolume * (options.volume ?? 1),\n playbackRate: this._playbackRate,\n preservesPitch: this._preservesPitch,\n });\n\n // Set up stop callback\n this.track.setOnStopCallback(() => {\n this._isPlaying = false;\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n });\n\n return this.track;\n }\n\n /**\n * Remove a track by ID.\n */\n removeTrack(trackId: string): void {\n if (this.track && this.track.id === trackId) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n /**\n * Get a track by ID.\n */\n getTrack(trackId: string): MediaElementTrack | undefined {\n if (this.track && this.track.id === trackId) {\n return this.track;\n }\n return undefined;\n }\n\n /**\n * Start playback.\n * @param _when - Ignored (HTMLAudioElement doesn't support scheduled start)\n * @param offset - Start position in seconds\n * @param duration - Duration to play in seconds (optional)\n */\n play(_when?: number, offset?: number, duration?: number): void {\n if (!this.track) {\n console.warn('MediaElementPlayout: No track to play');\n return;\n }\n\n const startPosition = offset ?? 0;\n this._isPlaying = true;\n\n this.track.play(startPosition);\n\n // If duration is specified, schedule stop\n if (duration !== undefined) {\n const adjustedDuration = duration / this._playbackRate;\n setTimeout(() => {\n if (this._isPlaying) {\n this.pause();\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n }\n }, adjustedDuration * 1000);\n }\n }\n\n /**\n * Pause playback.\n */\n pause(): void {\n if (this.track) {\n this.track.pause();\n }\n this._isPlaying = false;\n }\n\n /**\n * Stop playback and reset to start.\n */\n stop(): void {\n if (this.track) {\n this.track.stop();\n }\n this._isPlaying = false;\n }\n\n /**\n * Seek to a specific time.\n */\n seekTo(time: number): void {\n if (this.track) {\n this.track.seekTo(time);\n }\n }\n\n /**\n * Get current playback time.\n */\n getCurrentTime(): number {\n if (this.track) {\n return this.track.currentTime;\n }\n return 0;\n }\n\n /**\n * Set master volume.\n */\n setMasterVolume(volume: number): void {\n this._masterVolume = Math.max(0, Math.min(1, volume));\n if (this.track) {\n this.track.setVolume(this._masterVolume);\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved).\n */\n setPlaybackRate(rate: number): void {\n this._playbackRate = Math.max(0.5, Math.min(2.0, rate));\n if (this.track) {\n this.track.setPlaybackRate(this._playbackRate);\n }\n }\n\n /**\n * Set mute state for a track.\n */\n setMute(trackId: string, muted: boolean): void {\n const track = this.getTrack(trackId);\n if (track) {\n track.setMuted(muted);\n }\n }\n\n /**\n * Set solo state for a track.\n * Note: With single track, solo is effectively the same as unmute.\n */\n setSolo(_trackId: string, _soloed: boolean): void {\n // No-op for single track - solo doesn't make sense\n console.warn('MediaElementPlayout: Solo is not applicable for single-track playback');\n }\n\n /**\n * Set callback for when playback completes.\n */\n setOnPlaybackComplete(callback: () => void): void {\n this.onPlaybackCompleteCallback = callback;\n }\n\n /**\n * Clean up resources.\n */\n dispose(): void {\n if (this.track) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n // Getters\n get isPlaying(): boolean {\n return this._isPlaying;\n }\n\n get masterVolume(): number {\n return this._masterVolume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get duration(): number {\n return this.track?.duration ?? 0;\n }\n\n get sampleRate(): number {\n // HTMLAudioElement doesn't expose sample rate directly\n // Return a common default - peaks will have the actual sample rate\n return this.track?.peaks.sample_rate ?? 44100;\n }\n\n /**\n * Get the volume GainNode output for connecting external effects chains.\n * Returns null if no AudioContext was provided to the track.\n *\n * Usage: disconnect from default destination, connect to effect input,\n * then connect effect output to audioContext.destination.\n */\n get outputNode(): GainNode | null {\n return this.track?.outputNode ?? null;\n }\n}\n","/**\n * Common interface for playout engines.\n *\n * Both TonePlayout and MediaElementPlayout implement this interface,\n * allowing the browser package to work with either engine.\n */\nexport interface PlayoutEngine {\n // Lifecycle\n init(): Promise<void>;\n dispose(): void;\n\n // Playback\n play(when?: number, offset?: number, duration?: number): void;\n pause(): void;\n stop(): void;\n seekTo(time: number): void;\n getCurrentTime(): number;\n\n // Volume\n setMasterVolume(volume: number): void;\n\n // Track controls (optional - not all engines support all features)\n setMute?(trackId: string, muted: boolean): void;\n setSolo?(trackId: string, soloed: boolean): void;\n\n // Callbacks\n setOnPlaybackComplete(callback: () => void): void;\n\n // State\n readonly isPlaying: boolean;\n readonly duration: number;\n readonly sampleRate: number;\n}\n\n/**\n * Extended interface for engines that support playback rate.\n */\nexport interface PlaybackRateEngine extends PlayoutEngine {\n setPlaybackRate(rate: number): void;\n readonly playbackRate: number;\n}\n\n/**\n * Type guard to check if an engine supports playback rate.\n */\nexport function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine {\n return (\n 'setPlaybackRate' in engine &&\n typeof (engine as Record<string, unknown>).setPlaybackRate === 'function'\n );\n}\n"],"mappings":";AACA,SAAS,aAAa,cAAc,qBAAqB;AA4DlD,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;AA2EvC,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;AAjFE,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;AAMtC,UAAM,sBAAsB,QAAQ,kBAAkB;AACtD,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,sBAAY,UAAU,KAAK,eAAe,UAAU,GAAG,CAAC;AAAA,QAC1D,OAAO;AAEL,gBAAM,QAAQ,cAAc,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,QAAQ,cAAc,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;;;ACxcO,IAAM,sBAAN,MAA0B;AAAA,EAQ/B,YAAY,UAAsC,CAAC,GAAG;AAPtD,SAAQ,QAAkC;AAI1C,SAAQ,aAAsB;AAI5B,SAAK,gBAAgB,QAAQ,gBAAgB;AAC7C,SAAK,gBAAgB,QAAQ,gBAAgB;AAC7C,SAAK,kBAAkB,QAAQ,kBAAkB;AAAA,EACnD;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,MACnB,gBAAgB,KAAK;AAAA,IACvB,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;;;AC1NO,SAAS,qBAAqB,QAAqD;AACxF,SACE,qBAAqB,UACrB,OAAQ,OAAmC,oBAAoB;AAEnE;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waveform-playlist/media-element-playout",
3
- "version": "10.1.2",
3
+ "version": "10.3.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",
@@ -41,7 +41,7 @@
41
41
  "typescript": "^5.3.3"
42
42
  },
43
43
  "dependencies": {
44
- "@waveform-playlist/core": "10.1.2"
44
+ "@waveform-playlist/core": "10.3.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsup",