@researchbunny/react-use-audio-player
Version:
React hook for building custom audio playback controls
1 lines • 59.2 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/AudioPlayerProvider.tsx","../src/useAudioPlayer.ts","../src/players/howl/cache.ts","../src/players/state.ts","../src/players/howl/store.ts","../src/worklets/stream-processor.ts","../src/players/pcm/store.ts"],"sourcesContent":["export * from \"./AudioPlayerProvider\"\nexport * from \"./useAudioPlayer\"\nexport * from \"./types\"","import { type ComponentProps, createContext, useContext } from \"react\"\nimport { type AudioPlayer, useAudioPlayer } from \"./useAudioPlayer\"\n\nexport const context = createContext<AudioPlayer | null>(null)\n\nexport const useAudioPlayerContext = () => {\n const ctx = useContext(context)\n if (ctx === null) {\n throw new Error(\n \"useAudioPlayerContext must be used within an AudioPlayerProvider\"\n )\n }\n\n return ctx\n}\n\ntype Props = Omit<ComponentProps<typeof context.Provider>, \"value\">\n\nexport function AudioPlayerProvider({ children }: Props) {\n const player = useAudioPlayer()\n\n return <context.Provider value={player}>{children}</context.Provider>\n}\n","import React, { useCallback, useEffect, useRef, useSyncExternalStore } from \"react\"\nimport type { Howl } from \"howler\"\n\nimport { HowlStore } from \"./players/howl/store\"\nimport defaultState from \"./players/state\"\nimport { PcmStreamStore } from \"./players/pcm/store\"\nimport type { AudioControls, AudioLoadOptions, Snapshot, Store } from \"./types\"\nimport { UAParser } from \"ua-parser-js\"\n\n// Define separate types for PCM and Howl options\ntype PcmLoadOptions = {\n src: string;\n autoplay?: boolean;\n loop?: boolean;\n initialVolume?: number;\n initialMute?: boolean;\n onload?: () => void;\n onplay?: () => void;\n onend?: () => void;\n onpause?: () => void;\n onstop?: () => void;\n};\n\ntype HowlLoadOptions = {\n src: string;\n format?: string;\n html5?: boolean;\n autoplay?: boolean;\n loop?: boolean;\n initialVolume?: number;\n initialMute?: boolean;\n initialRate?: number;\n onload?: () => void;\n onplay?: () => void;\n onend?: () => void;\n onpause?: () => void;\n onstop?: () => void;\n};\n\n// Helper function to create PCM options\nfunction createPcmOptions(\n src: string,\n options?: AudioLoadOptions\n): PcmLoadOptions { // Using 'any' to allow both string and ArrayBuffer src types\n return {\n src: src, // Pass the source directly, whether it's a string URL or ArrayBuffer\n autoplay: options?.autoplay,\n loop: options?.loop,\n initialVolume: options?.initialVolume,\n initialMute: options?.initialMute,\n onload: options?.onload,\n onplay: options?.onplay,\n onend: options?.onend,\n onpause: options?.onpause,\n onstop: options?.onstop\n };\n}\n\n// Helper function to create Howl options\nfunction createHowlOptions(\n src: string,\n options?: AudioLoadOptions\n): HowlLoadOptions {\n return {\n src,\n format: options?.format,\n html5: options?.html5,\n autoplay: options?.autoplay,\n loop: options?.loop,\n initialVolume: options?.initialVolume,\n initialMute: options?.initialMute,\n initialRate: options?.initialRate,\n // event callbacks\n onload: options?.onload,\n onplay: options?.onplay,\n onend: options?.onend,\n onpause: options?.onpause,\n onstop: options?.onstop\n };\n}\n\n/**\n * Returns true if the audio player needs to use a PCM player.\n * @param options - Audio load options that may indicate PCM stream processing\n * @returns Boolean indicating whether a PCM player is needed\n */\nfunction needsPcmPlayer(options?: AudioLoadOptions): boolean {\n const isWebKit = new UAParser().getEngine().name === \"WebKit\";\n\n return !!(options?.isPCMStream) && isWebKit;\n}\n\n// Helper function to load audio based on source type\nfunction loadAudio(\n howlerRef: React.MutableRefObject<Store>,\n pcmAudioRef: React.MutableRefObject<Store>,\n src: string,\n options?: AudioLoadOptions\n): void {\n if (needsPcmPlayer(options)) {\n // Destroy the howlerRef if it exists\n if (howlerRef.current) {\n console.log(\"Destroying Howler instance for PCM stream\");\n howlerRef.current.destroy();\n }\n // Destroy the pcmAudioRef if it exists\n if (pcmAudioRef.current) {\n console.log(\"Destroying PCM instance for Howl stream\");\n pcmAudioRef.current.destroy();\n }\n const pcmOptions = createPcmOptions(src, options);\n (pcmAudioRef.current as PcmStreamStore).load(pcmOptions);\n } else {\n // Destroy the howlerRef if it exists\n if (howlerRef.current) {\n console.log(\"Destroying Howler instance for PCM stream\");\n howlerRef.current.destroy();\n }\n // Destroy the pcmAudioRef if it exists\n if (pcmAudioRef.current) {\n console.log(\"Destroying PCM instance for Howl stream\");\n pcmAudioRef.current.destroy();\n }\n\n const howlOptions = createHowlOptions(src, options);\n (howlerRef.current as HowlStore).load(howlOptions);\n }\n}\n\nexport type AudioPlayer = AudioControls &\n Snapshot & {\n /** A reference to the underlying player object.\n * For regular audio, this is a Howl object.\n * */\n player: Howl | null\n src: string | null\n /** A way to explicitly load an audio resource */\n load: (...args: [string, AudioLoadOptions | undefined]) => void\n /** Removes event listeners, resets state and unloads the internal player object */\n cleanup: () => void,\n canSeek: () => boolean,\n canLoop: () => boolean,\n canChangeRate: () => boolean,\n }\n\nexport function useAudioPlayer(): AudioPlayer\n\n/**\n */\nexport function useAudioPlayer() {\n const [isPcmPlayer, setIsPcmPlayer] = React.useState(false);\n\n // Choose the appropriate store based on the options\n const pcmAudioRef = useRef<Store>(new PcmStreamStore()) as React.MutableRefObject<Store>;\n const howlerRef = useRef<Store>(new HowlStore()) as React.MutableRefObject<Store>;\n\n // need to bind functions back to the howl since they will be called from the context of React\n const pcmAudioState = useSyncExternalStore(\n pcmAudioRef.current.subscribe.bind(pcmAudioRef.current),\n pcmAudioRef.current.getSnapshot.bind(pcmAudioRef.current),\n () => defaultState\n );\n const howlerState = useSyncExternalStore(\n howlerRef.current.subscribe.bind(howlerRef.current),\n howlerRef.current.getSnapshot.bind(howlerRef.current),\n () => defaultState\n );\n\n useEffect(() => {\n // cleans up the sound when hook unmounts\n return () => {\n if (pcmAudioRef.current) {\n pcmAudioRef.current.destroy()\n }\n if (howlerRef.current) {\n howlerRef.current.destroy()\n }\n }\n }, []);\n\n const load: AudioPlayer[\"load\"] = useCallback((src, options) => {\n if (needsPcmPlayer(options)) {\n setIsPcmPlayer(true);\n }\n\n loadAudio(howlerRef, pcmAudioRef, src, options);\n }, [])\n\n const state = isPcmPlayer ? pcmAudioState : howlerState;\n const audioRef = isPcmPlayer ? pcmAudioRef : howlerRef;\n\n return {\n ...state,\n player: audioRef.current instanceof HowlStore ? audioRef.current.howl : null,\n src: audioRef.current.src,\n load: load,\n // AudioControls interface\n play: audioRef.current.play.bind(audioRef.current),\n pause: audioRef.current.pause.bind(audioRef.current),\n togglePlayPause: audioRef.current.togglePlayPause.bind(\n audioRef.current\n ),\n stop: audioRef.current.stop.bind(audioRef.current),\n setVolume: audioRef.current.setVolume.bind(audioRef.current),\n fade: audioRef.current.fade.bind(audioRef.current),\n mute: audioRef.current.mute.bind(audioRef.current),\n unmute: audioRef.current.unmute.bind(audioRef.current),\n toggleMute: audioRef.current.toggleMute.bind(audioRef.current),\n setRate: audioRef.current.setRate.bind(audioRef.current),\n seek: audioRef.current.seek.bind(audioRef.current),\n loopOn: audioRef.current.loopOn.bind(audioRef.current),\n loopOff: audioRef.current.loopOff.bind(audioRef.current),\n toggleLoop: audioRef.current.toggleLoop.bind(audioRef.current),\n getPosition: audioRef.current.getPosition.bind(audioRef.current),\n cleanup: audioRef.current.destroy.bind(audioRef.current),\n canSeek: () => audioRef.current instanceof HowlStore ? true : false,\n canLoop: () => audioRef.current instanceof HowlStore ? true : false,\n canChangeRate: () => audioRef.current instanceof HowlStore ? true : false,\n }\n}\n","import { Howl, type HowlOptions as BaseHowlOptions } from \"howler\"\n\nexport type HowlOptions = BaseHowlOptions & {\n src: string // override src property to only be a single string\n}\n\n/**\n * A cache that tracks all the instances of AudioSources created by the library\n * An instance is cached based on the src attribute it was created with\n *\n * This prevents duplicate instances of audio being created in certain edge cases\n * React StrictMode being one such scenario\n */\nclass HowlCache {\n private _cache: Map<string, Howl> = new Map()\n\n public create(options: HowlOptions): Howl {\n const key = options.src\n if (this._cache.has(key)) {\n return this._cache.get(key)!\n }\n\n const howl = new Howl(options)\n this._cache.set(key, howl)\n return howl\n }\n\n public set(key: string, howl: Howl) {\n this._cache.set(key, howl)\n }\n\n public get(key: string) {\n return this._cache.get(key)\n }\n\n public clear(key: string) {\n this._cache.delete(key)\n }\n\n public destroy(key: string) {\n const howl = this.get(key)\n if (howl) {\n howl.unload()\n this.clear(key)\n }\n }\n\n public reset() {\n this._cache.values().forEach((audio) => audio.unload())\n this._cache.clear()\n }\n}\n\nconst howlCache = new HowlCache()\n\nexport default howlCache\n","import type { Snapshot } from \"../types\";\n\n// Default state of the audio player\nexport default {\n isUnloaded: true,\n isLoading: false,\n isReady: false,\n isLooping: false,\n isPlaying: false,\n isStopped: false,\n isPaused: false,\n duration: 0,\n rate: 1,\n volume: 1,\n isMuted: false,\n error: undefined\n} as Snapshot;","import type { Howl } from \"howler\"\nimport howlCache from \"./cache\"\nimport type { Snapshot, Subscriber, Store } from \"../../types\"\nimport defaultState from \"../state\"\n\ntype CreateOptions = Parameters<typeof howlCache.create>[0]\n\nexport class HowlStore implements Store {\n public howl: Howl | null\n public src: string | null\n\n private subscriptions: Set<Subscriber>\n private snapshot: Snapshot\n\n /**\n * Merges changes to the AudioSnapshot with the instnace variable and invokes all subscriber callbacks\n */\n private updateSnapshot(update: Partial<Snapshot>) {\n this.snapshot = {\n ...this.snapshot,\n ...update\n }\n\n this.subscriptions.forEach((cb) => cb())\n }\n\n /**\n * Initiates a snapshot update from a Howl instance\n */\n private updateSnapshotFromHowlState(howl: Howl) {\n this.updateSnapshot({\n ...this.getSnapshotFromHowl(howl)\n })\n }\n\n /**\n * Initializes the store with a new instance of a Howl\n * - creates a new Howl instance\n * - updates the AudioSnapshot instance\n * - sets up Howl event listeners to synchronize AudioSnapshot\n */\n private initHowl(options: CreateOptions) {\n const newHowl = howlCache.create(options)\n this.src = options.src\n this.howl = newHowl\n\n this.updateSnapshot({\n ...this.getSnapshotFromHowl(newHowl),\n // reset error on creation of new Howl\n error: undefined\n })\n\n /*\n Howler places the Howl in a \"loading\" state when HTML5 audio is seeked.\n This is likely done in case the user agent needs to buffer more of the resource.\n However, it may be a bug in Howler that the load event is not emitted following this state change.\n This leaves this hook hanging in an \"isLoading\" state.\n As a temporary (hopefully) workaround we can hijack the HTML5 Audio element from Howler\n and trigger a state update once the Audio element has buffered.\n */\n if (newHowl._html5 && newHowl._sounds[0]?._node) {\n const htmlAudioNode = newHowl._sounds[0]?._node\n htmlAudioNode.addEventListener(\"canplaythrough\", () => {\n this.updateSnapshotFromHowlState(newHowl)\n })\n }\n\n // Howl event listeners and state mutations\n newHowl.on(\"load\", () => this.updateSnapshotFromHowlState(newHowl))\n newHowl.on(\"play\", () => this.updateSnapshotFromHowlState(newHowl))\n newHowl.on(\"end\", () => this.updateSnapshotFromHowlState(newHowl))\n newHowl.on(\"pause\", () => this.updateSnapshotFromHowlState(newHowl))\n newHowl.on(\"stop\", () => this.updateSnapshotFromHowlState(newHowl))\n newHowl.on(\"mute\", () => this.updateSnapshotFromHowlState(newHowl))\n newHowl.on(\"volume\", () => this.updateSnapshotFromHowlState(newHowl))\n newHowl.on(\"rate\", () => this.updateSnapshotFromHowlState(newHowl))\n newHowl.on(\"seek\", () => this.updateSnapshotFromHowlState(newHowl))\n newHowl.on(\"fade\", () => this.updateSnapshotFromHowlState(newHowl))\n\n newHowl.on(\"loaderror\", (_: number, errorCode: unknown) => {\n console.error(`Howl load error: ${errorCode}`)\n this.updateSnapshotFromHowlState(newHowl)\n this.updateSnapshot({\n error: \"Failed to load audio source\"\n })\n })\n\n newHowl.on(\"playerror\", (_: number, errorCode: unknown) => {\n console.error(`Howl playback error: ${errorCode}`)\n this.updateSnapshotFromHowlState(newHowl)\n this.updateSnapshot({\n error: \"Failed to play audio source\"\n })\n })\n }\n\n private getSnapshotFromHowl(howl: Howl): Snapshot {\n if (howl.state() === \"unloaded\") {\n return defaultState\n }\n\n const howlState = howl.state()\n const isPlaying = howl.playing()\n const muteReturn = howl.mute()\n return {\n isUnloaded: howlState === \"unloaded\",\n isLoading: howlState === \"loading\",\n isReady: howlState === \"loaded\",\n isLooping: howl.loop(),\n isPlaying,\n isStopped: !isPlaying && howl.seek() === 0,\n isPaused: !isPlaying && howl.seek() > 0,\n duration: howl.duration(),\n rate: howl.rate(),\n volume: howl.volume(),\n // the Howl#mute method sometimes returns the Howl (i.e. this) instead of the boolean\n isMuted: typeof muteReturn === \"object\" ? false : muteReturn\n }\n }\n\n constructor(options?: CreateOptions) {\n this.howl = null\n this.src = null\n\n this.subscriptions = new Set()\n this.snapshot = defaultState\n\n if (options !== undefined) {\n this.initHowl(options)\n }\n }\n\n public load(options: CreateOptions) {\n if (this.howl !== null) {\n this.destroy()\n }\n\n this.initHowl(options)\n }\n\n public destroy() {\n if (this.src && this.howl) {\n // guarantees that event listeners can no longer be called\n this.howl.off(\"load\")\n this.howl.off(\"play\")\n this.howl.off(\"end\")\n this.howl.off(\"pause\")\n this.howl.off(\"stop\")\n this.howl.off(\"mute\")\n this.howl.off(\"volume\")\n this.howl.off(\"rate\")\n this.howl.off(\"seek\")\n this.howl.off(\"fade\")\n this.howl.off(\"loaderror\")\n this.howl.off(\"playerror\")\n\n howlCache.destroy(this.src)\n\n this.src = null\n this.howl = null\n }\n }\n\n public subscribe(cb: Subscriber) {\n this.subscriptions.add(cb)\n return () => this.subscriptions.delete(cb)\n }\n\n public getSnapshot() {\n return this.snapshot\n }\n\n public play() {\n if (this.howl) {\n // prevents the Howl from spinning up a new \"Sound\" from the loaded audio resource\n if (this.howl.playing()) {\n return\n }\n\n this.howl.play()\n }\n }\n\n public pause() {\n if (this.howl) {\n this.howl.pause()\n }\n }\n\n public togglePlayPause() {\n if (this.snapshot.isPlaying) {\n this.pause()\n } else {\n this.play()\n }\n }\n\n public stop() {\n if (this.howl) {\n this.howl.stop()\n }\n }\n\n public setVolume(vol: number) {\n if (this.howl) {\n this.howl.volume(vol)\n }\n }\n\n public setRate(rate: number) {\n if (this.howl) {\n this.howl.rate(rate)\n }\n }\n\n public loopOn() {\n if (this.howl) {\n this.howl.loop(true)\n // there is no loop even to listen for on Howl so calling the sync operation manually\n this.updateSnapshotFromHowlState(this.howl)\n }\n }\n\n public loopOff() {\n if (this.howl) {\n this.howl.loop(false)\n // there is no loop even to listen for on Howl so calling the sync operation manually\n this.updateSnapshotFromHowlState(this.howl)\n }\n }\n\n public toggleLoop() {\n if (this.snapshot.isLooping) {\n this.loopOff()\n } else {\n this.loopOn()\n }\n }\n\n public mute() {\n if (this.howl) {\n this.howl.mute(true)\n }\n }\n\n public unmute() {\n if (this.howl) {\n this.howl.mute(false)\n }\n }\n\n public toggleMute() {\n if (this.snapshot.isMuted) {\n this.unmute()\n } else {\n this.mute()\n }\n }\n\n public seek(seconds: number) {\n // if audio resource is being streamed then it's duration will be Infinity\n if (this.howl && this.snapshot.duration !== Infinity) {\n this.howl.seek(seconds)\n }\n }\n\n public getPosition(): number {\n if (this.howl) {\n return this.howl.seek()\n }\n\n return 0\n }\n\n public fade(startVolume: number, endVolume: number, durationMs: number) {\n if (this.howl) {\n this.howl.fade(startVolume, endVolume, durationMs)\n }\n }\n}\n","export const StreamProcessorName = \"audio-player\";\nconst StreamProcessorWorklet = `\n\"use strict\";\n\n/* ---------- AudioPlayerProcessor ---------- */\nclass AudioPlayerProcessor extends AudioWorkletProcessor {\n constructor() {\n super();\n this.initBufferStore();\n this.state = \"idle\"; // idle | playing | paused\n this.currentTime = 0;\n this.lastEmit = 0;\n this.totalSamplesEmitted = 0;\n\n /* ---- messages from main thread ---- */\n this.port.onmessage = (ev) => {\n const { audioData, play, pause, clear } = ev.data;\n\n if (audioData) {\n this.writeData(audioData);\n }\n\n if (play && this.state !== \"playing\") {\n this.state = this.outputBuffers.length ? \"playing\" : \"idle\";\n this.port.postMessage({ state: this.state });\n }\n\n if (pause && this.state === \"playing\") {\n this.state = \"paused\";\n this.port.postMessage({ state: \"paused\" });\n }\n\n if (clear) {\n this.initBufferStore();\n this.currentTime = 0;\n this.state = \"idle\";\n this.port.postMessage({ state: \"idle\", currentTime: 0 });\n }\n };\n }\n\n /**\n * Initializes the buffer store for audio data.\n * This sets up the initial buffer length and prepares the output buffers.\n */\n initBufferStore() {\n this.bufferLength = 128;\n this.outputBuffers = [];\n this.writeBuffer = new Float32Array(this.bufferLength);\n this.writeOffset = 0;\n }\n\n /**\n * Writes audio data to the output buffers.\n * @param {ArrayBuffer} audioData - The audio data to write.\n */\n writeData(audioData) {\n const int16Data = new Int16Array(audioData);\n const floatData = new Float32Array(int16Data.length);\n\n // Convert from Int16 to Float32 (-1.0 to 1.0)\n for (let i = 0; i < floatData.length; i++) {\n floatData[i] = int16Data[i] / 0x8000; // Convert Int16 to Float32\n }\n\n for (let i = 0; i < floatData.length; i++) {\n this.writeBuffer[this.writeOffset++] = floatData[i];\n if (this.writeOffset >= this.bufferLength) {\n this.writeBuffer = new Float32Array(this.bufferLength);\n this.outputBuffers.push(this.writeBuffer);\n this.writeOffset = 0;\n }\n }\n }\n\n /**\n * Read audio data from the output buffers.\n * @param {number} length - The number of samples to read.\n * @returns {Float32Array} - The read audio data.\n */\n readData(length) {\n if (this.outputBuffers.length === 0) {\n throw new Error(\"No audio data available to read.\");\n }\n let output = new Float32Array(length);\n const buffer = this.outputBuffers.shift();\n for (let sampleNum = 0; sampleNum < length; sampleNum++) {\n output[sampleNum] = buffer[sampleNum] || 0;\n }\n\n return output;\n }\n\n process(_ins, outs) {\n const chan = outs[0][0];\n if (this.state !== \"playing\") return true;\n\n if (this.outputBuffers.length) {\n const samples = this.readData(chan.length);\n chan.set(samples);\n this.currentTime += samples.length / sampleRate;\n this.totalSamplesEmitted += samples.length;\n\n if (this.state === \"idle\") {\n this.state = \"playing\";\n this.port.postMessage({ state: \"playing\", currentTime: this.currentTime });\n } else if (this.outputBuffers.length === 0) {\n let a = 1;\n // this.state = \"idle\";\n // this.currentTime = 0;\n // this.port.postMessage({ state: \"idle\", currentTime: 0 });\n } else if (this.currentTime - this.lastEmit > 0.5) {\n this.lastEmit = this.currentTime;\n this.port.postMessage({ currentTime: this.currentTime });\n }\n }\n\n return true;\n }\n}\n\nregisterProcessor(\"${StreamProcessorName}\", AudioPlayerProcessor);\n`\n\nconst script = new Blob([StreamProcessorWorklet], {\n type: 'application/javascript',\n});\nconst src = URL.createObjectURL(script);\nexport const StreamProcessorSrc = src;","import { StreamProcessorName, StreamProcessorSrc } from \"../../worklets/stream-processor\";\nimport type { Snapshot, Subscriber, Store } from \"../../types\";\nimport defaultState from \"../state\";\n\ntype CreateOptions = {\n src: string;\n autoplay?: boolean;\n loop?: boolean;\n initialVolume?: number;\n initialMute?: boolean;\n onload?: () => void;\n onplay?: () => void;\n onend?: () => void;\n onpause?: () => void;\n onstop?: () => void;\n};\n\n/**\n * PCM Player implementation using AudioWorklet\n * Provides a similar interface to Howl for playing PCM audio data\n */\nexport class PcmStreamStore implements Store {\n public src: string | null;\n private audioContext: AudioContext | null;\n private workletNode: AudioWorkletNode | null;\n private gainNode: GainNode | null;\n private subscriptions: Set<Subscriber>;\n private snapshot: Snapshot;\n private position: number;\n private duration: number;\n private sampleRate: number;\n private currentTime: number;\n private reader: ReadableStreamDefaultReader<Uint8Array<ArrayBufferLike>> | null = null;\n private abortController: AbortController | null = null;\n\n /**\n * Merges changes to the Snapshot with the instance variable and invokes all subscriber callbacks\n */\n private updateSnapshot(update: Partial<Snapshot>) {\n this.snapshot = {\n ...this.snapshot,\n ...update\n };\n\n this.subscriptions.forEach((cb) => cb());\n }\n\n constructor(options?: CreateOptions) {\n this.src = null;\n this.audioContext = null;\n this.workletNode = null;\n this.gainNode = null;\n this.sampleRate = 24000; // Default sample rate, will be updated from WAV header\n this.position = 0;\n this.duration = 0;\n this.currentTime = 0;\n this.subscriptions = new Set();\n this.snapshot = defaultState;\n this.reader = null;\n\n if (options !== undefined) {\n this.initPlayer(options);\n }\n }\n\n /**\n * Initialize the PCM player with the provided options\n * @param options The options for creating the PCM player\n */\n private async initPlayer(options: CreateOptions) {\n this.src = options.src;\n this.abortController = new AbortController();\n \n // Update state to loading\n this.updateSnapshot({\n isUnloaded: false,\n isLoading: false,\n isReady: false,\n error: undefined\n });\n\n try {\n // Fetch the audio data\n const response = await fetch(options.src, {\n signal: this.abortController.signal\n });\n \n if (!response.ok) {\n throw new Error(`HTTP error! Status: ${response.status}`);\n }\n \n // Check if we can get a ReadableStream from the response body\n if (!response.body) {\n throw new Error('Response body is not available as a readable stream');\n }\n \n // Create a reader for the stream\n this.reader = response.body.getReader();\n\n // Process the stream\n const processStream = async (processSrc: string) => {\n let isFirstChunk = true;\n let leftOverBuffer: ArrayBuffer | null = null;\n try {\n while (processSrc === this.src && this.reader) { // this needs to be fixed.\n const { done, value } = await this.reader.read();\n\n // Proceed only if there is data\n if (value) {\n let pcmData: ArrayBuffer;\n\n // Process the chunk\n if (isFirstChunk) {\n isFirstChunk = false; \n // Extract WAV header (first 44 bytes)\n if (value.length >= 44) {\n // Use a copy of the buffer to ensure it's an ArrayBuffer\n let wavHeader = value.buffer.slice(0, 44) as ArrayBuffer;\n \n // Parse WAV header to get sample rate\n const dataView = new DataView(wavHeader);\n\n // Sample rate is at offset 24\n this.sampleRate = dataView.getUint32(24, true) as unknown as number;\n\n // Extract PCM data (after header)\n pcmData = value.buffer.slice(44) as ArrayBuffer;\n\n // Initialize audio context and worklet\n await this.initAudioContext(options);\n\n // Ensure that pcm data has a even byte length. Save leftover buffer if odd\n if (pcmData.byteLength % 2 !== 0) {\n leftOverBuffer = pcmData.slice(pcmData.byteLength - 1);\n pcmData = pcmData.slice(0, pcmData.byteLength - 1);\n }\n\n //Update duration\n this.duration += pcmData.byteLength / (2 * this.sampleRate); // 2 bytes per sample for PCM16\n\n // Send PCM data to worklet. pcmData will be undefined after this call\n this.sendPcmDataToWorklet(pcmData);\n\n // Update state to ready\n this.updateSnapshot({\n isReady: true,\n duration: this.duration,\n });\n \n // Autoplay if specified\n if (options.autoplay) {\n this.play();\n }\n } else {\n throw new Error('Invalid WAV header: chunk size too small');\n }\n } else {\n // Process subsequent chunks\n pcmData = value.buffer as ArrayBuffer;\n\n // Check if we have a leftover buffer from the previous chunk and merge it with the current pcmData\n if (leftOverBuffer) {\n const mergedData = new Uint8Array(1 + pcmData.byteLength);\n mergedData.set(new Uint8Array(leftOverBuffer), 0);\n mergedData.set(new Uint8Array(pcmData), leftOverBuffer.byteLength);\n pcmData = mergedData.buffer;\n leftOverBuffer = null; // Reset leftover buffer\n }\n\n // Ensure that pcm data has a even byte length. Save leftover buffer if odd\n if (pcmData.byteLength % 2 !== 0) {\n leftOverBuffer = pcmData.slice(pcmData.byteLength - 1);\n pcmData = pcmData.slice(0, pcmData.byteLength - 1);\n }\n\n //Update duration\n this.duration += pcmData.byteLength / (2 * this.sampleRate); // 2 bytes per sample for PCM16 \n \n // Send PCM data to worklet. pcmData will be undefined after this call\n this.sendPcmDataToWorklet(pcmData);\n\n // Update state to ready\n this.updateSnapshot({\n duration: this.duration,\n });\n }\n }\n\n // If this is the last chunk, update state\n // and stop processing\n if (done) {\n // this.updateSnapshot({\n // isLoading: false,\n // });\n\n // Call onload callback if provided\n if (options.onload) {\n options.onload();\n }\n\n // Exit the loop\n break;\n }\n }\n } catch (error) {\n this.updateSnapshot({\n error: `Error processing stream: ${error}`\n });\n }\n };\n\n // Start processing the stream\n await processStream(options.src);\n } catch (error) {\n this.updateSnapshot({\n isLoading: false,\n isReady: false,\n error: `Failed to load PCM data from URL: ${error}`\n });\n }\n }\n\n /**\n * Initialize the audio context and worklet\n */\n private async initAudioContext(options: CreateOptions): Promise<void> {\n if (this.audioContext) {\n return;\n }\n\n try {\n // Create audio context\n this.audioContext = new AudioContext({\n sampleRate: this.sampleRate,\n latencyHint: 'interactive'\n });\n\n // Load audio worklet\n await this.audioContext.audioWorklet.addModule(StreamProcessorSrc);\n\n // Create worklet node\n this.workletNode = new AudioWorkletNode(this.audioContext, StreamProcessorName);\n\n // Create gain node for volume control\n this.gainNode = this.audioContext.createGain();\n this.gainNode.gain.value = this.snapshot.isMuted ? 0 : this.snapshot.volume;\n\n // Connect nodes\n this.workletNode.connect(this.gainNode);\n this.gainNode.connect(this.audioContext.destination);\n\n // Set up message handling from worklet\n this.workletNode.port.onmessage = (event) => {\n const { state, currentTime } = event.data;\n\n if (currentTime !== undefined) {\n this.currentTime = currentTime;\n this.position = Math.round(this.currentTime);\n }\n\n if (state) {\n switch (state) {\n case 'playing':\n this.updateSnapshot({\n isPlaying: true,\n isPaused: false,\n isStopped: false\n });\n if (options.onplay) {\n options.onplay();\n }\n break;\n case 'paused':\n this.updateSnapshot({\n isPlaying: false,\n isPaused: true,\n isStopped: false\n });\n if (options.onpause) {\n options.onpause();\n }\n break;\n case 'idle':\n this.updateSnapshot({\n isPlaying: false,\n isPaused: false,\n isStopped: true\n });\n if (options.onstop) {\n options.onstop();\n }\n break;\n }\n }\n };\n } catch (error) {\n this.updateSnapshot({\n error: `Failed to initialize audio context: ${error}`\n });\n }\n }\n\n /**\n * Send PCM data to the audio worklet\n */\n private sendPcmDataToWorklet(data: ArrayBuffer) {\n if (!this.workletNode) {\n return;\n }\n\n if (data.byteLength % 2 !== 0) {\n throw new Error('PCM data must have an even byte length (16-bit samples)');\n }\n\n // Send to worklet\n this.workletNode.port.postMessage({ audioData: data }, [data]);\n }\n\n /**\n * Load PCM data from a URL\n * The URL should provide audio bytes with a WAV header (first 44 bytes) followed by raw PCM16 bit data\n */\n public async load(options: CreateOptions) {\n // Clean up previous instance if any\n await this.destroy();\n\n this.initPlayer(options);\n }\n\n /**\n * Clean up resources\n */\n public async destroy() {\n // 1) abort the fetch\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n\n // 2) cancel the reader\n if (this.reader) {\n try { await this.reader.cancel(); } catch {/*ignore*/}\n this.reader = null;\n }\n\n\n // Stop playback if playing\n if (this.snapshot.isPlaying) {\n this.stop();\n }\n\n // Clean up audio nodes\n if (this.workletNode) {\n this.workletNode.disconnect();\n this.workletNode = null;\n }\n\n if (this.gainNode) {\n this.gainNode.disconnect();\n this.gainNode = null;\n }\n\n // Close audio context\n if (this.audioContext) {\n this.audioContext.close();\n this.audioContext = null;\n }\n\n // Reset state\n this.duration = 0;\n this.currentTime = 0;\n this.src = null;\n this.sampleRate = 24000;\n this.updateSnapshot(defaultState);\n }\n\n /**\n * Subscribe to state changes\n */\n public subscribe(cb: Subscriber) {\n this.subscriptions.add(cb);\n return () => this.subscriptions.delete(cb);\n }\n\n /**\n * Get current state snapshot\n */\n public getSnapshot() {\n return this.snapshot;\n }\n\n /**\n * Begin or resume playback\n */\n public async play() {\n if (!this.audioContext || !this.workletNode) {\n return;\n }\n\n try {\n // Resume audio context if suspended\n if (this.audioContext.state === 'suspended') {\n await this.audioContext.resume();\n }\n\n if (!this.snapshot.isPlaying) {\n this.workletNode.port.postMessage({ play: true });\n }\n } catch (error) {\n this.updateSnapshot({\n error: `Failed to play PCM audio: ${error}`\n });\n }\n }\n\n /**\n * Pause playback\n */\n public pause() {\n if (!this.workletNode || !this.snapshot.isPlaying) {\n return;\n }\n\n try {\n // Send pause command to worklet\n this.workletNode.port.postMessage({ pause: true });\n } catch (error) {\n this.updateSnapshot({\n error: `Failed to pause PCM audio: ${error}`\n });\n }\n }\n\n /**\n * Toggle between play and pause\n */\n public togglePlayPause() {\n if (this.snapshot.isPlaying) {\n this.pause();\n } else {\n this.play();\n }\n }\n\n /**\n * Stop playback and reset position\n */\n public stop() {\n if (!this.workletNode) {\n return;\n }\n\n try {\n // Send clear command to worklet\n this.workletNode.port.postMessage({ clear: true });\n \n // Reset position\n this.currentTime = 0;\n this.position = 0;\n \n } catch (error) {\n this.updateSnapshot({\n error: `Failed to stop PCM audio: ${error}`\n });\n }\n }\n\n /**\n * Set volume (0-1)\n */\n public setVolume(vol: number) {\n if (this.gainNode) {\n const volume = Math.max(0, Math.min(1, vol));\n this.gainNode.gain.value = this.snapshot.isMuted ? 0 : volume;\n this.updateSnapshot({\n volume\n });\n }\n }\n\n /**\n * Set playback rate\n */\n public setRate(_: number) {\n // Note: The worklet doesn't support rate changes directly\n // A proper implementation would require resampling the audio data\n throw new Error('Setting playback rate is not supported in this implementation');\n }\n\n /**\n * Enable looping\n */\n public loopOn() {\n throw new Error('Looping playback is not supported in this implementation');\n }\n\n /**\n * Disable looping\n */\n public loopOff() {\n throw new Error('Looping playback is not supported in this implementation');\n }\n\n /**\n * Toggle looping\n */\n public toggleLoop() {\n if (this.snapshot.isLooping) {\n this.loopOff()\n } else {\n this.loopOn()\n }\n }\n\n /**\n * Mute audio\n */\n public mute() {\n if (this.gainNode) {\n this.gainNode.gain.value = 0;\n this.updateSnapshot({\n isMuted: true\n });\n }\n }\n\n /**\n * Unmute audio\n */\n public unmute() {\n if (this.gainNode) {\n this.gainNode.gain.value = this.snapshot.volume;\n this.updateSnapshot({\n isMuted: false\n });\n }\n }\n\n /**\n * Toggle mute\n */\n public toggleMute() {\n if (this.snapshot.isMuted) {\n this.unmute();\n } else {\n this.mute();\n }\n }\n\n /**\n * Seek to position in seconds\n * Note: This is a basic implementation that doesn't support precise seeking\n */\n public seek(_: number) {\n // Seeking is not fully supported in this implementation\n throw Error('Precise seeking is not fully supported in PCM player');\n }\n\n /**\n * Get current position in seconds\n */\n public getPosition(): number {\n return this.position;\n }\n\n /**\n * Fade volume from startVolume to endVolume over durationMs\n */\n public fade(startVolume: number, endVolume: number, durationMs: number) {\n if (!this.gainNode || !this.audioContext) {\n return;\n }\n \n const startVol = Math.max(0, Math.min(1, startVolume));\n const endVol = Math.max(0, Math.min(1, endVolume));\n \n // Set initial volume\n this.gainNode.gain.setValueAtTime(startVol, this.audioContext.currentTime);\n \n // Linear ramp to target volume\n this.gainNode.gain.linearRampToValueAtTime(\n endVol,\n this.audioContext.currentTime + (durationMs / 1000)\n );\n \n // Update state after fade completes\n setTimeout(() => {\n this.updateSnapshot({\n volume: endVol\n });\n }, durationMs);\n }\n}\n"],"mappings":"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