useAudio
useAudio provides sound effects, music, and volume control for game logic. It reads from the AudioManager which is automatically set up by Game.
import { useAudio } from "@carverjs/core/hooks";Quick Start #
import { useGameLoop, useInput, useAudio } from "@carverjs/core/hooks";
function Player() {
const { isActionJustPressed } = useInput({ actions: { jump: ["Space"] } });
const { play } = useAudio({
sounds: {
jump: { src: "/sounds/jump.mp3" },
land: { src: ["/sounds/land.webm", "/sounds/land.mp3"] },
},
});
useGameLoop(() => {
if (isActionJustPressed("jump")) play("jump");
});
return <Actor type="primitive" shape="box" color="blue" />;
}Return Value #
| Property | Type | Description |
|---|---|---|
play(name, options?) | (string, PlaySoundOptions?) => SoundHandle | null | Play a registered sound. Returns a handle to control the instance |
stop(name) | (string) => void | Stop all instances of a named sound |
pause(name) | (string) => void | Pause all instances of a named sound |
resume(name) | (string) => void | Resume paused instances of a named sound |
isPlaying(name) | (string) => boolean | Check if any instance is playing |
playMusic(src, options?) | (string | string[], MusicOptions?) => void | Play a music track with crossfade |
stopMusic(fadeOut?) | (number?) => void | Stop music with optional fade-out (seconds) |
pauseMusic() | () => void | Pause the music |
resumeMusic() | () => void | Resume the music |
isMusicPlaying() | () => boolean | Check if music is playing |
setVolume(channel, vol) | (AudioChannel, number) => void | Set channel volume (0–1) |
getVolume(channel) | (AudioChannel) => number | Get channel volume |
setMute(channel, muted) | (AudioChannel, boolean) => void | Mute/unmute a channel |
isMuted(channel) | (AudioChannel) => boolean | Check if a channel is muted |
setMasterMute(muted) | (boolean) => void | Mute/unmute all audio |
preload(name) | (string) => Promise<void> | Preload a registered sound |
preloadAll(names) | (string[]) => Promise<void> | Preload multiple sounds |
isUnlocked | boolean | Whether the AudioContext has been unlocked (user has interacted) |
isReady | boolean | Whether the audio system is fully ready |
Options #
const audio = useAudio({
sounds: {
jump: { src: "/sounds/jump.mp3" },
coin: { src: "/sounds/coin.ogg", volume: 0.8 },
},
enabled: true,
});| Option | Type | Default | Description |
|---|---|---|---|
sounds | SoundMap | {} | Map of sound names to definitions. Registered on mount, unregistered on unmount |
enabled | boolean | true | Toggle all audio from this hook on/off |
Sound Definition #
Each sound in the sounds map is a SoundDefinition:
| Field | Type | Default | Description |
|---|---|---|---|
src | string | string[] | — | Required. URL(s) of the audio file. Array enables format negotiation |
channel | AudioChannel | "sfx" | Volume channel: "sfx", "music", "ui", "ambient", "voice" |
volume | number | 1 | Base volume (0–1) |
rate | number | 1 | Playback rate (0.5 = half speed, 2 = double) |
loop | boolean | false | Loop playback |
maxInstances | number | 5 | Maximum simultaneous instances. Oldest is stolen when exceeded |
preload | boolean | true | Preload the audio buffer on registration |
sprites | AudioSpriteMap | — | Audio sprite regions (see Audio Sprites) |
cooldown | number | — | Minimum seconds between play calls. Rapid calls within cooldown are dropped |
Format Negotiation #
Provide multiple formats to maximize browser compatibility:
sounds: {
explosion: {
src: ["/sounds/explosion.webm", "/sounds/explosion.ogg", "/sounds/explosion.mp3"],
},
}The system picks the first format the browser supports.
Playing Sounds #
Basic #
const { play } = useAudio({ sounds: { hit: { src: "/sounds/hit.mp3" } } });
play("hit"); // Fire and forgetWith Options #
const handle = play("hit", {
volume: 0.5, // Override volume
rate: 1.2, // Speed up
loop: true, // Loop this instance
fadeIn: 0.3, // Fade in over 0.3 seconds
delay: 0.5, // Wait 0.5 seconds before playing
onEnd: () => {}, // Called when playback finishes (non-looping)
});Sound Handle #
play() returns a SoundHandle for controlling the instance:
const handle = play("engine", { loop: true });
// Later...
handle?.setVolume(0.3);
handle?.setRate(1.5);
handle?.fade(1, 0, 2); // Fade from 1 to 0 over 2 seconds
handle?.pause();
handle?.resume();
handle?.stop();Spatial Audio (3D) #
Attach a sound to a Three.js object for positional audio:
function Enemy() {
const ref = useRef<Group>(null);
const { play } = useAudio({
sounds: { growl: { src: "/sounds/growl.ogg", loop: true } },
});
useEffect(() => {
play("growl", {
spatial: {
ref,
panningModel: "HRTF", // Realistic 3D audio (default)
distanceModel: "inverse", // Volume rolloff model (default)
refDistance: 2, // Full volume within 2 units
maxDistance: 50, // Silent beyond 50 units
rolloffFactor: 1, // Rolloff speed
},
});
}, [play]);
return <Actor ref={ref} type="model" src="/models/enemy.glb" />;
}The sound's position updates every frame from the Object3D ref. The listener follows the active camera by default — override with AudioListener.
| Option | Type | Default | Description |
|---|---|---|---|
ref | RefObject<Object3D> | — | Required. The emitter's Object3D |
panningModel | "HRTF" | "equalpower" | "HRTF" | Panning algorithm |
distanceModel | "linear" | "inverse" | "exponential" | "inverse" | Volume rolloff model |
refDistance | number | 1 | Distance at which volume starts decreasing |
maxDistance | number | 100 | Maximum audible distance |
rolloffFactor | number | 1 | Rolloff speed |
coneInnerAngle | number | 360 | Full-volume cone angle (degrees) |
coneOuterAngle | number | 360 | Outer cone angle (degrees) |
coneOuterGain | number | 0 | Volume outside outer cone (0–1) |
trackPosition | boolean | true | Update position every frame |
Music #
Play Music #
const { playMusic, isUnlocked } = useAudio();
useEffect(() => {
if (isUnlocked) {
playMusic("/music/level1.mp3", { loop: true });
}
}, [isUnlocked, playMusic]);Crossfade Between Tracks #
When playMusic is called while music is already playing, the old track fades out and the new one fades in:
// Playing track A...
playMusic("/music/boss.mp3", {
crossfade: { duration: 3 }, // 3-second crossfade
});
// Track A fades out, boss music fades in| Option | Type | Default | Description |
|---|---|---|---|
volume | number | 1 | Music volume (0–1) |
loop | boolean | true | Loop the track |
crossfade.duration | number | 2 | Crossfade duration in seconds |
Stop / Pause / Resume #
stopMusic(2); // Fade out over 2 seconds
pauseMusic(); // Pause (preserves position)
resumeMusic(); // Resume from paused positionAudio Sprites #
Pack multiple sounds into a single file to reduce HTTP requests:
const { play } = useAudio({
sounds: {
footsteps: {
src: "/sounds/footsteps.ogg",
sprites: {
left: { start: 0, duration: 0.3 },
right: { start: 0.4, duration: 0.3 },
run: { start: 0.8, duration: 0.2, loop: true },
},
},
},
});
play("footsteps", { sprite: "left" });
play("footsteps", { sprite: "run" });| Field | Type | Default | Description |
|---|---|---|---|
start | number | — | Start offset in seconds |
duration | number | — | Duration in seconds |
loop | boolean | false | Loop this sprite region |
Volume Channels #
Six independent volume channels, all routed through master:
[Source] → [Instance Gain] → [Channel Gain] → [Master Gain] → Output| Channel | Purpose |
|---|---|
"master" | Global output — affects everything |
"sfx" | Sound effects (default) |
"music" | Background music |
"ui" | UI sounds (clicks, hovers) |
"ambient" | Environmental audio (wind, rain) |
"voice" | Character dialogue |
Settings Menu Example #
function SettingsMenu() {
const { setVolume, getVolume, setMute, isMuted } = useAudio();
return (
<div>
<label>
Master Volume
<input
type="range"
min={0} max={1} step={0.01}
defaultValue={getVolume("master")}
onChange={(e) => setVolume("master", parseFloat(e.target.value))}
/>
</label>
<label>
<input
type="checkbox"
checked={isMuted("sfx")}
onChange={(e) => setMute("sfx", e.target.checked)}
/>
Mute SFX
</label>
</div>
);
}AudioContext Unlock #
Browsers require a user gesture before audio can play. The system handles this automatically:
On first click/tap/keypress, the
AudioContextis unlockedisUnlockedbecomestrue(triggers React re-render)Queued music and looping sounds begin playing
One-shot SFX requested before unlock are silently dropped (stale sounds are worse than none)
const { isUnlocked, playMusic } = useAudio();
// Music starts as soon as the user interacts
useEffect(() => {
if (isUnlocked) playMusic("/music/bg.mp3");
}, [isUnlocked, playMusic]);Game Phase Integration #
Audio automatically pauses when the game phase is "paused" or "gameover", and resumes when "playing". No manual handling needed.
Type Definitions #
See Types for UseAudioOptions, UseAudioReturn, SoundDefinition, SoundMap, PlaySoundOptions, SoundHandle, SoundState, AudioChannel, AudioFormat, SpatialAudioOptions, PanningModel, DistanceModel, CrossfadeOptions, MusicOptions, AudioSpriteRegion, AudioSpriteMap, and AudioListenerConfig.