ez-web-audio
Version:
Making the Web Audio API super EZ since 2024.
529 lines • 19.7 kB
TypeScript
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