ts-audio
Version:
`ts-audio` is an agnostic and easy-to-use library to work with the `AudioContext` API and create Playlists.
234 lines (208 loc) • 6.83 kB
text/typescript
import { AudioCtx } from './AudioCtx'
import { defaultStates } from './states'
import { EventEmitter } from '../EventEmitter'
import { EventHandler } from '../EventHandler'
import { decodeAudioData } from './decodeAudioData'
import { initializeSource } from './initializeSource'
import { getBuffer, preloadFile } from './utils'
/**
* Configuration options for creating an Audio instance.
*/
type AudioProp = {
file: string
volume?: number
autoPlay?: boolean
loop?: boolean
preload?: boolean
}
/**
* Valid event types that can be emitted by the Audio instance.
*/
type AudioEvent = 'ready' | 'start' | 'state' | 'end'
/**
* If `AudioContext` is initialized before a user gesture on the page, its
* state becomes `suspended` by default. Once `AudioContext.state` is `suspended`,
* the only way to start it after a user gesture is executing the `resume` method.
*/
const start = (audioCtx: AudioContext, source: AudioBufferSourceNode) =>
audioCtx.state === 'suspended' ? audioCtx.resume().then(() => source.start(0)) : source.start(0)
/**
* Audio player class that provides control over a single audio file.
* Implements the AudioType interface for managing audio playback, volume, and events.
*/
export class AudioClass {
/** @private Path or URL to the audio file */
private _file: AudioProp['file']
/** @private Initial volume level set during construction */
private _initialVolume: number
/** @private Flag indicating if audio should play automatically */
private _autoPlay: boolean
/** @private Initial loop state set during construction */
private _initialLoop: boolean
/** @private Web Audio API context */
private _audioCtx: AudioContext
/** @private Internal state management object */
private _states: typeof defaultStates
/** @private Event emitter for handling audio events */
private _emitter: EventEmitter
/** @private Event handler for managing event subscriptions */
private _eventHandler: EventHandler
/**
* Creates an instance of Audio player.
*
* @param {AudioProp} config - The audio configuration object
* @param {string} config.file - Path or URL to the audio file
* @param {number} [config.volume=1] - Initial volume level (0 to 1)
* @param {boolean} [config.autoPlay=false] - Whether to start playing automatically
* @param {boolean} [config.loop=false] - Whether to loop the audio
* @param {boolean} [config.preload=false] - Whether to preload the audio file
*/
constructor({ file, volume = 1, autoPlay = false, loop = false, preload = false }: AudioProp) {
this._file = file
this._initialVolume = volume
this._autoPlay = autoPlay
this._initialLoop = loop
this._audioCtx = AudioCtx()
this._states = { ...defaultStates }
this._emitter = new EventEmitter()
this._eventHandler = new EventHandler(this._emitter, this._audioCtx)
if (preload) {
preloadFile(file)
}
}
/**
* Fetches and decodes the audio buffer for the given source node.
* @private
* @param {AudioBufferSourceNode} source - The audio source node to load buffer into
*/
private curryGetBuffer(source: AudioBufferSourceNode): void {
this._states.isDecoded = false
getBuffer(this._file)
.then((arrayBuffer) => {
decodeAudioData({
audioCtx: this._audioCtx,
source,
arrayBuffer,
autoPlay: this._autoPlay,
loop: this._initialLoop,
states: this._states,
emitter: this._emitter,
})
})
.catch(console.error)
}
/**
* Starts or resumes audio playback.
* If playback hasn't started, initializes audio source and begins playback.
* If playback was paused, resumes from the current position.
*/
public play(): void {
if (this._states.hasStarted) {
this._audioCtx.resume()
this._states.isPlaying = true
return
}
initializeSource({
audioCtx: this._audioCtx,
volume: this._initialVolume,
emitter: this._emitter,
states: this._states,
})
const { source } = this._states
if (source) {
this.curryGetBuffer(source)
if (this._states.isDecoded) {
start(this._audioCtx, source)
} else {
this._emitter.listener('decoded', () => start(this._audioCtx, source))
}
this._states.hasStarted = true
this._states.isPlaying = true
this._emitter.emit('start', { data: null })
}
}
/**
* Pauses audio playback by suspending the audio context.
*/
public pause(): void {
this._audioCtx.suspend()
this._states.isPlaying = false
}
/**
* Toggles between play and pause states.
*/
public toggle(): void {
this._states.isPlaying ? this.pause() : this.play()
}
/**
* Stops audio playback completely.
* Different from pause as it resets the playback position.
*/
public stop(): void {
if (this._states.hasStarted) {
this._states.source?.stop(0)
this._states.isPlaying = false
}
}
/**
* Subscribes to audio events.
* @param {AudioEvent} eventType - Type of event to listen for
* @param {Function} callback - Function to call when event occurs
*/
public on(eventType: AudioEvent, callback: <T>(param: { [data: string]: T }) => void): void {
this._eventHandler[eventType]?.(callback)
}
/**
* Gets the current volume level.
* @returns {number} Current volume value between 0 and 1
*/
public get volume(): number {
return this._states.gainNode?.gain.value ?? 0
}
/**
* Sets the audio volume level.
* @param {number} newVolume - New volume value between 0 and 1
*/
public set volume(newVolume: number) {
if (this._states.gainNode) {
this._states.gainNode.gain.value = newVolume
}
}
/**
* Gets the current loop state.
* @returns {boolean} Whether audio is set to loop
*/
public get loop(): boolean {
return this._states.source?.loop ?? false
}
/**
* Sets the loop state.
* @param {boolean} newLoop - Whether audio should loop
*/
public set loop(newLoop: boolean) {
if (this._states.source) {
this._states.source.loop = newLoop
}
}
/**
* Gets the current state of the audio context.
* @returns {AudioContextState} Current state of the audio context
*/
public get state(): AudioContextState {
return this._audioCtx.state
}
/**
* Gets the current AudioContext instance.
* @returns {AudioContext} The current AudioContext instance
*/
public get audioCtx(): AudioContext {
return this._audioCtx
}
}
/**
* Factory function to create a new Audio instance.
*
* @param {AudioPropType} props - The audio configuration properties
* @returns {AudioType} A new Audio instance
*/
export default (props: AudioProp): AudioClass => new AudioClass(props)