UNPKG

ez-web-audio

Version:

Making the Web Audio API super EZ since 2024.

529 lines 19.7 kB
import { TimeObject } from './utils/create-time-object'; import { Playable } from './interfaces/playable'; import { Connectable, Connection } from './interfaces/connectable'; import { ControlType, ParamController, RampType, RatioType } from './controllers/base-param-controller'; import { SoundEventMap } from './events/event-types'; import { Effect } from './effects'; import { Analyzer } from './analyzer'; /** * Configuration options for BaseSound and its subclasses. */ export interface BaseSoundOptions { /** * Optional name for identifying this sound instance. * Useful for debugging and when managing multiple sounds. */ name?: string; /** * Custom setTimeout implementation. * * By default, an AudioContext-aware setTimeout is used that compensates * for browser throttling. Override this if you need different timing behavior. * * @param fn - Function to call after delay * @param delayMillis - Delay in milliseconds * @returns Timeout ID for cancellation */ setTimeout?: (fn: () => void, delayMillis: number) => number; } /** * Abstract base class for all playable audio sources. * * BaseSound provides the core audio infrastructure that Sound, Track, and Oscillator * build upon. It handles: * - Audio node routing (gain, panner, effect chain, destination) * - Playback control (play, stop, timing methods) * - Parameter control via fluent API (update, onPlaySet, onPlayRamp) * - Event system for lifecycle events (play, stop, end) * - Effect chain management (addEffect, removeEffect) * - Analyzer attachment for visualization * * @example * ```typescript * // Inherited by Sound, Track, Oscillator * const sound = await createSound('click.mp3') * * // Immediate parameter update * sound.update('gain').to(0.5).from('ratio') * * // Schedule parameter for next play * sound.onPlaySet('gain').to(0).endingAt(1, 'exponential') // fade in * * // Ramp parameter during playback * sound.onPlayRamp('gain').from(1).to(0).in(2) // fade out over 2s * * // Add effects * const filter = createFilterEffect(ctx, 'lowpass', { frequency: 1000 }) * sound.addEffect(filter) * * // Listen for events * sound.on('play', () => console.log('Started')) * sound.on('end', () => console.log('Finished')) * ``` */ export declare abstract class BaseSound extends EventTarget implements Connectable, Playable { audioContext: AudioContext; protected _isPlaying: boolean; gainNode: GainNode; protected pannerNode: StereoPannerNode; protected setTimeout: (fn: () => void, delayMillis: number) => number; protected startedPlayingAt: number; /** * @property effects * An array of Effect instances that form the persistent effect chain. * Effects are wired once and persist across multiple play() calls - only the source reconnects. * * Use addEffect() and removeEffect() to manage the effect chain. * Chain order: source -> effectChainInput -> [effects] -> gainNode -> pannerNode -> destination */ protected effects: Effect[]; /** * @property effectChainInput * The entry point for the effect chain. The audio source connects to this node, * which then routes through any effects before reaching gain/panner/destination. */ protected effectChainInput: GainNode; /** * @property _destination * The final destination node for audio output. Defaults to audioContext.destination. * Can be changed with setDestination() to route audio elsewhere (e.g., for sub-mixing). */ protected _destination: AudioNode; /** * @property _analyzer * Optional Analyzer attached to the end of the signal chain for visualization. * Audio flows through the analyzer (passthrough) before reaching destination. */ protected _analyzer: Analyzer | null; /** * @property connections * An array of connections that will be placed in between the `audioSourceNode` (where the audio comes from) and the gain/panner nodes. * * This is useful for adding effects to a sound. For example, to add a reverb effect, you can create a `ConvolverNode` and add it to this array. * * The `audioSourceNode` is mandatory and always first, and the gain/panner nodes are mandatory and always last, but the nodes in between can be in any order. * * You can use the `addConnection` and `removeConnection` methods to add and remove connections from this array, or you can set/mutate the array directly. * * The `wireConnections` method is called automatically when the sound is played, and it will connect all the nodes in this array in the correct order. * * @deprecated Use addEffect() instead for the new persistent effect chain system. * * @example * const sound = new Oscillator(audioContext, { type: 'sine', frequency: 440 }) * const convolverNode = audioContext.createConvolver() * sound.connections = [convolverNode] * sound.play() * */ connections: Connection[]; /** * @property startOffset * * See Web Audio API documentation for this one, as it is just passed into the `start` method of the `audioSourceNode`. * * This is useful for starting a sound at a specific offset from the beginning of the sound. Manipulation of this value is used * extensively in the `Track` class to allow for starting the track at specific positions. * * @default 0 * @see https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode/start */ startOffset: number; protected abstract controller: ParamController; protected abstract wireConnections(): void; protected abstract setup(): void; /** * @property audioSourceNode * * The audio source node that this sound is using. This is the first node in the chain and is the node that actually provides audio. */ abstract audioSourceNode: OscillatorNode | AudioBufferSourceNode; /** * @property duration * * The duration of this sound. This is used to schedule the stop method to be called after the sound has finished playing. Not all * `Sound` types have a useful `duration`, such as `Oscillator` */ abstract duration: TimeObject; /** * @property name * * A name for this sound. Optional. Useful for identification of a given sound and debugging. */ name: string; /** * @property debug * * Per-sound debug override. Set to true to enable debug logging for this sound only, * or false to disable logging even when global debug is enabled. * * @default undefined (follows global debug mode) * * @example * sound.debug = true // enable debug for this sound * sound.debug = false // silence this sound even when global debug is on */ debug?: boolean; constructor(audioContext: AudioContext, opts?: BaseSoundOptions); /** * Wires the effect chain from effectChainInput through all non-bypassed effects * to gainNode -> pannerNode -> [analyzer] -> destination. * * If an analyzer is attached, audio flows through it before reaching destination. * The analyzer is a passthrough node that also provides visualization data. * * Called when effects are added/removed/reordered, destination changes, or analyzer changes. * NOT called on every play() - the chain persists. * * @private */ private wireEffectChain; /** * Add an effect to the effect chain. * Effects persist across multiple play() calls. * * @param effect - The Effect instance to add * @param position - Optional index to insert at (defaults to end of chain) * @returns this for chaining * * @example * const filter = createFilterEffect(audioContext, 'lowpass', { frequency: 1000 }) * sound.addEffect(filter) */ addEffect(effect: Effect, position?: number): this; /** * Remove an effect from the effect chain. * * @param effect - The Effect instance to remove * @returns this for chaining * * @example * sound.removeEffect(filter) */ removeEffect(effect: Effect): this; /** * Get a readonly copy of the current effects array. * * @returns Shallow copy of the effects array */ getEffects(): readonly Effect[]; /** * Set a custom destination for audio output instead of audioContext.destination. * Useful for routing to sub-mixes, analyzers, or other processing chains. * * @param node - The AudioNode to route output to * @returns this for chaining * * @example * const analyzer = audioContext.createAnalyser() * analyzer.connect(audioContext.destination) * sound.setDestination(analyzer) */ setDestination(node: AudioNode): this; /** * Re-wire the effect chain. Call this after toggling effect.bypass * to update the audio routing. */ rewireEffects(): void; /** * Attach an analyzer to this sound for visualization. * The analyzer is inserted at the end of the signal chain (after effects and panner, * before destination), showing the fully processed signal. * * The analyzer is a passthrough node - audio flows through it unchanged while * providing frequency and waveform data for visualization. * * @param analyzer - The Analyzer instance to attach, or null to detach * @returns this for chaining * * @example * const analyzer = createAnalyzer(audioContext, { fftSize: 2048 }) * sound.setAnalyzer(analyzer) * * function draw() { * const freqData = analyzer.getFrequencyData() * // Draw frequency bars * requestAnimationFrame(draw) * } */ setAnalyzer(analyzer: Analyzer | null): this; /** * Get the currently attached analyzer, if any. * * @returns The attached Analyzer instance, or null if none attached */ getAnalyzer(): Analyzer | null; /** * Add a typed event listener for sound lifecycle events. * Overloaded to provide type safety for known event types while remaining * compatible with EventTarget. * * @param type - The event type ('play', 'stop', 'end', etc.) * @param listener - The event handler function * @param options - Standard addEventListener options */ addEventListener<K extends keyof SoundEventMap>(type: K, listener: (event: SoundEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void; addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void; /** * Remove a typed event listener for sound lifecycle events. * Overloaded to provide type safety for known event types while remaining * compatible with EventTarget. * * @param type - The event type ('play', 'stop', 'end', etc.) * @param listener - The event handler function to remove * @param options - Standard removeEventListener options */ removeEventListener<K extends keyof SoundEventMap>(type: K, listener: (event: SoundEventMap[K]) => void, options?: boolean | EventListenerOptions): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void; /** * Emit a typed event with the given detail. * @protected * @param type - The event type to emit * @param detail - The event detail object */ protected emit<K extends keyof SoundEventMap>(type: K, detail: SoundEventMap[K]['detail']): void; /** * Subscribe to one or more events. Supports chaining. * * @example * ```typescript * sound.on('play', handlePlay).on('stop', handleStop); * sound.on(['play', 'stop'], handleBoth); * ``` * * @param type - The event type(s) to subscribe to * @param listener - The event handler function * @returns this for chaining */ on<K extends keyof SoundEventMap>(type: K | K[], listener: (event: SoundEventMap[K]) => void): this; /** * Subscribe to an event once. Handler is removed after first invocation. * * @example * ```typescript * sound.once('end', () => console.log('Playback finished')); * ``` * * @param type - The event type to subscribe to * @param listener - The event handler function * @returns this for chaining */ once<K extends keyof SoundEventMap>(type: K, listener: (event: SoundEventMap[K]) => void): this; /** * Unsubscribe from an event. * * Note: Due to native EventTarget limitations, you must provide the same * listener function reference that was used when subscribing. To remove * listeners, store the function reference when adding it. * * @example * ```typescript * const handler = (e) => console.log(e.detail); * sound.on('play', handler); * // later... * sound.off('play', handler); * ``` * * @param type - The event type to unsubscribe from * @param listener - The event handler function to remove * @returns this for chaining */ off<K extends keyof SoundEventMap>(type: K, listener: (event: SoundEventMap[K]) => void): this; addConnection(connection: Connection): this; removeConnection(name: string): this; getConnection(name: string): Connection | undefined; getNodeFrom<T extends AudioNode | StereoPannerNode>(connectionName: string): T | undefined; /** * Update an audio parameter immediately. * * Returns a fluent builder for setting the parameter value. Use `.to(value)` * to set the value, then `.from(unit)` for unit interpretation. * * @param type - The parameter to update ('gain' or 'pan') * @returns Fluent builder for setting the value * * @example * ```typescript * // Set gain to 50% * sound.update('gain').to(0.5).from('ratio') * * // Set pan to left * sound.update('pan').to(-1).from('ratio') * ``` */ update(type: ControlType): { to: (value: number) => { from: (method: RatioType) => void; }; }; /** * Set the pan position immediately. * * Convenience method for `update('pan').to(value).from('ratio')`. * * @param value - Pan position from -1 (left) to 1 (right), 0 is center * @returns this for chaining * * @example * ```typescript * sound.changePanTo(-1) // Hard left * sound.changePanTo(0) // Center * sound.changePanTo(1) // Hard right * ``` */ changePanTo(value: number): this; /** * Set the gain (volume) immediately. * * Convenience method for `update('gain').to(value).from('ratio')`. * * @param value - Gain from 0 (silent) to 1 (full volume) * @returns this for chaining * * @example * ```typescript * sound.changeGainTo(0.5) // Half volume * sound.changeGainTo(0) // Muted * sound.changeGainTo(1) // Full volume * ``` */ changeGainTo(value: number): this; /** * Schedule a parameter value to be set when play() is called. * * Use this for fade-ins, fade-outs, or precise parameter timing. * The value is applied relative to when play() is called. * * @param type - The parameter to control ('gain' or 'pan') * @returns Fluent builder for setting value and timing * * @example * ```typescript * // Fade in: start at 0, ramp to 1 over 0.5 seconds * sound.onPlaySet('gain').to(0).at(0) * sound.onPlaySet('gain').to(1).endingAt(0.5, 'linear') * sound.play() * * // Start panned left, move to center over 2 seconds * sound.onPlaySet('pan').to(-1).at(0) * sound.onPlaySet('pan').to(0).endingAt(2, 'linear') * sound.play() * ``` */ onPlaySet(type: ControlType): { to: (value: number) => { at: (time: number) => void; endingAt: (time: number, rampType?: RampType) => void; }; }; /** * Schedule a parameter ramp when play() is called. * * Use this for smooth transitions like vibrato, tremolo, or automation. * * @param type - The parameter to ramp ('gain' or 'pan') * @param rampType - Type of ramp curve ('linear' or 'exponential') * @returns Fluent builder for setting start value, end value, and duration * * @example * ```typescript * // Fade out over 2 seconds * sound.onPlayRamp('gain', 'linear').from(1).to(0).in(2) * sound.play() * * // Pan sweep from left to right over 4 seconds * sound.onPlayRamp('pan', 'linear').from(-1).to(1).in(4) * sound.play() * ``` */ onPlayRamp(type: ControlType, rampType?: RampType): { from: (startValue: number) => { to: (endValue: number) => { in: (endTime: number) => void; }; }; }; play(): Promise<void>; playIn(when: number): void; playFor(duration: number): void; /** * Play after a delay, then stop after a duration. * * Combines playIn() and stopIn() for precise timed playback. * * @param playIn - Seconds from now until playback starts * @param stopAfter - Seconds of playback before stopping (from play start) * * @example * ```typescript * // Start in 1 second, play for 3 seconds * sound.playInAndStopAfter(1, 3) * ``` */ playInAndStopAfter(playIn: number, stopAfter: number): void; /** * Play the audio source at a specific time. * * This is the underlying method for all play variants. Time is measured in seconds * from when the AudioContext was created (audioContext.currentTime). * * @param time - The AudioContext time when playback should start * * @example * ```typescript * // Play immediately * sound.playAt(audioContext.currentTime) * * // Play in 2 seconds * sound.playAt(audioContext.currentTime + 2) * * // Sync multiple sounds * const startTime = audioContext.currentTime + 0.1 * sound1.playAt(startTime) * sound2.playAt(startTime) * ``` */ playAt(time: number): Promise<void>; /** * Hook method called after playback starts. * Override in subclasses to add behavior that runs for all play variants. * @protected */ protected _onPlaybackStarted(): void; /** * Stop the audio source after a delay. * * @param seconds - Seconds from now until playback stops * * @example * ```typescript * sound.play() * // Stop after 5 seconds * sound.stopIn(5) * ``` */ stopIn(seconds: number): Promise<void>; /** * Stop the audio source at a specific time. * * This is the underlying method for all stop variants. Time is measured in seconds * from when the AudioContext was created (audioContext.currentTime). * * @param time - The AudioContext time when playback should stop * * @example * ```typescript * // Stop immediately * sound.stopAt(audioContext.currentTime) * * // Stop in 5 seconds * sound.stopAt(audioContext.currentTime + 5) * ``` */ stopAt(time: number): Promise<void>; stop(): Promise<void>; get isPlaying(): boolean; get percentGain(): number; protected later(fn: () => void): void; } //# sourceMappingURL=base-sound.d.ts.map