SoundA sound effect for games: one decoded AudioBuffer, played as many lightweight SoundInstances.
Unlike an AudioClip, a sound has no audio nodes of its own. Every instance is a single AudioBufferSourceNode (plus a GainNode when it does not have a spatial source to itself), which is released as soon as it has finished. This makes Sound the right choice for anything that is played often and from many places: gunshots, impacts, footsteps, explosions, collisions and ambience loops.
Per sound you can limit how many instances play at the same time (maxInstances and steal), skip starts that follow each other too quickly (minInterval), and add a random pitch and volume variation so repeated sounds do not sound mechanical. See SoundOptions.
A sound can be played in three ways:
| Call | Use it for |
|---|---|
| sound.play(bus) | Sounds that are not positioned: UI, music stingers, the player's own sounds. |
| source.play(sound) | Sounds of an object that has its own source, such as an NPC or a vehicle. The sound moves with the source. |
| renderer.playAt(sound, position) | Fire-and-forget sounds at a position that do not belong to an object: explosions, impacts, collisions, ambience of objects. |
import { Sound } from "@fluex/fluexgl-dsp";
const gunshot = await Sound.load(audioDevice, "/sfx/gunshot.wav", {
maxInstances: 6,
pitchVariation: 0.5
});
gunshot.play(effectsBus); // the player's own gun
renderer.playAt(gunshot, { x: 120, y: 0, z: -40 }); // another player's gun
new Sound(source: AudioBuffer | AudioSourceData, options?: Partial<SoundOptions>): Sound;
Creates a sound from an already decoded buffer, for example the result of loadAudioSource(). To load a file, Sound.load() is usually more convenient.
source: AudioBuffer | AudioSourceData - The decoded audio.options?: Partial<SoundOptions> - Limits and variation of this sound. Every field is optional.Sound.load(target: BaseAudioContext | { context: BaseAudioContext }, url: string, options?: Partial<SoundOptions>): Promise<Sound>Fetches and decodes an audio file with the given AudioContext or AudioDevice. The decoded buffer is cached per context and url, so loading the same url again (also while the first load is still running) does not fetch or decode it again. A failed load is not cached, so it can be retried.
Decoding happens on the context the sound will be played on, so the buffer already has the right sample rate. loadAudioSource() creates a temporary AudioContext for every file, which is noticeably slower when loading hundreds of sound effects.
target: BaseAudioContext | AudioDevice - The context to decode with.url: string - Url of the audio file.options?: Partial<SoundOptions> - label defaults to the url.Promise<Sound>ERROR:FLUEXGL-DSP@0003 (ErrorCodes.PATH_TO_FILE_NOT_FOUND) - The file could not be fetched or decoded. The promise rejects.id: stringA unique id, automatically generated when constructing the sound. Should NOT be changed.
buffer: AudioBufferThe decoded audio, shared by every instance.
options: SoundOptionsThe resolved SoundOptions. Can be changed at runtime; changes apply to instances started afterwards.
play(target: Channel | Master, options?: Partial<SoundPlayOptions>): SoundInstance | nullPlays the sound unpositioned on a bus Channel or a Master. The instance gets its own GainNode for its volume and fade-out.
target: Channel | Master - Where the sound is played.options?: Partial<SoundPlayOptions> - Volume, pitch, loop, offset and start time of this instance.SoundInstance | null - null when the start was skipped because of minInterval or maxInstances (with steal: "none", or "quietest" when the new instance is the quietest).ERROR:FLUEXGL-DSP@0014 (ErrorCodes.CHANNEL_NOT_INITIALIZED) - The target has no context or input, for example because it was disposed.stopAll(fadeTime?: number): voidStops every playing instance of this sound.
fadeTime?: number - Fade-out in seconds. Defaults to 0.02.voidcreateInstance(target: SoundInstanceTarget): SoundInstance | nullStarts an instance into the given destination, after checking minInterval and maxInstances. Used internally by play(), SpatialAudioSource.play() and SpatialAudioRenderer.playAt(); prefer those. See SoundInstanceTarget.
SoundInstance | nullremove(instance: SoundInstance): voidRemoves an instance from the list of playing instances, so it no longer counts towards maxInstances. Called by the instance itself when it stops or ends; you do not need to call this.
get duration(): numberDuration of the buffer in seconds.
get playing(): numberNumber of instances currently playing. Instances that are fading out after stop() no longer count.
When play() is called while maxInstances instances are already playing, steal decides what happens:
| steal | Behavior |
|---|---|
| "oldest" (default) | The instance that started first is stopped with a 20 ms fade, and the new one starts. |
| "quietest" | The quietest instance is stopped, but only when the new instance is louder than it. Otherwise the new instance is skipped. For spatial instances, loudness includes the distance attenuation. |
| "none" | The new instance is skipped. |
minInterval is checked first: a start within minInterval seconds after the previous start of the same sound is skipped. Use it for sounds that can be triggered many times in the same frame, such as collisions.
This class does not emit any events. Use instance.onEnded() to know when an instance has finished.
const sounds = {
gunshot: await Sound.load(audioDevice, "/sfx/gunshot.wav", { maxInstances: 6, pitchVariation: 0.5 }),
impact: await Sound.load(audioDevice, "/sfx/impact.wav", { minInterval: 0.03, volumeVariation: 0.3 }),
explosion: await Sound.load(audioDevice, "/sfx/explosion.wav", { maxInstances: 4, steal: "quietest" }),
click: await Sound.load(audioDevice, "/sfx/click.wav", { maxInstances: 2 })
};
Or load them in parallel:
const [gunshot, impact] = await Promise.all([
Sound.load(audioDevice, "/sfx/gunshot.wav", { maxInstances: 6 }),
Sound.load(audioDevice, "/sfx/impact.wav", { minInterval: 0.03 })
]);
const ui = audioDevice.createChannel("UI");
ui.send(audioDevice.getMasterChannel());
button.addEventListener("click", () => sounds.click.play(ui));
sounds.explosion.play(effectsBus)?.onEnded(() => {
console.log("explosion has finished");
});
See Example 06: One-shot sounds and Example 12: Game audio architecture for complete setups.