useAudio

useAudio provides sound effects, music, and volume control for game logic. It reads from the AudioManager which is automatically set up by Game.

tsx
import { useAudio } from "@carverjs/core/hooks";

Quick Start #

tsx
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 #

PropertyTypeDescription
play(name, options?)(string, PlaySoundOptions?) => SoundHandle | nullPlay a registered sound. Returns a handle to control the instance
stop(name)(string) => voidStop all instances of a named sound
pause(name)(string) => voidPause all instances of a named sound
resume(name)(string) => voidResume paused instances of a named sound
isPlaying(name)(string) => booleanCheck if any instance is playing
playMusic(src, options?)(string | string[], MusicOptions?) => voidPlay a music track with crossfade
stopMusic(fadeOut?)(number?) => voidStop music with optional fade-out (seconds)
pauseMusic()() => voidPause the music
resumeMusic()() => voidResume the music
isMusicPlaying()() => booleanCheck if music is playing
setVolume(channel, vol)(AudioChannel, number) => voidSet channel volume (0–1)
getVolume(channel)(AudioChannel) => numberGet channel volume
setMute(channel, muted)(AudioChannel, boolean) => voidMute/unmute a channel
isMuted(channel)(AudioChannel) => booleanCheck if a channel is muted
setMasterMute(muted)(boolean) => voidMute/unmute all audio
preload(name)(string) => Promise<void>Preload a registered sound
preloadAll(names)(string[]) => Promise<void>Preload multiple sounds
isUnlockedbooleanWhether the AudioContext has been unlocked (user has interacted)
isReadybooleanWhether the audio system is fully ready

Options #

tsx
const audio = useAudio({
  sounds: {
    jump: { src: "/sounds/jump.mp3" },
    coin: { src: "/sounds/coin.ogg", volume: 0.8 },
  },
  enabled: true,
});
OptionTypeDefaultDescription
soundsSoundMap{}Map of sound names to definitions. Registered on mount, unregistered on unmount
enabledbooleantrueToggle all audio from this hook on/off

Sound Definition #

Each sound in the sounds map is a SoundDefinition:

FieldTypeDefaultDescription
srcstring | string[]Required. URL(s) of the audio file. Array enables format negotiation
channelAudioChannel"sfx"Volume channel: "sfx", "music", "ui", "ambient", "voice"
volumenumber1Base volume (0–1)
ratenumber1Playback rate (0.5 = half speed, 2 = double)
loopbooleanfalseLoop playback
maxInstancesnumber5Maximum simultaneous instances. Oldest is stolen when exceeded
preloadbooleantruePreload the audio buffer on registration
spritesAudioSpriteMapAudio sprite regions (see Audio Sprites)
cooldownnumberMinimum seconds between play calls. Rapid calls within cooldown are dropped

Format Negotiation #

Provide multiple formats to maximize browser compatibility:

tsx
sounds: {
  explosion: {
    src: ["/sounds/explosion.webm", "/sounds/explosion.ogg", "/sounds/explosion.mp3"],
  },
}

The system picks the first format the browser supports.


Playing Sounds #

Basic #

tsx
const { play } = useAudio({ sounds: { hit: { src: "/sounds/hit.mp3" } } });

play("hit"); // Fire and forget

With Options #

tsx
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:

tsx
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:

tsx
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.

OptionTypeDefaultDescription
refRefObject<Object3D>Required. The emitter's Object3D
panningModel"HRTF" | "equalpower""HRTF"Panning algorithm
distanceModel"linear" | "inverse" | "exponential""inverse"Volume rolloff model
refDistancenumber1Distance at which volume starts decreasing
maxDistancenumber100Maximum audible distance
rolloffFactornumber1Rolloff speed
coneInnerAnglenumber360Full-volume cone angle (degrees)
coneOuterAnglenumber360Outer cone angle (degrees)
coneOuterGainnumber0Volume outside outer cone (0–1)
trackPositionbooleantrueUpdate position every frame

Music #

Play Music #

tsx
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:

tsx
// Playing track A...
playMusic("/music/boss.mp3", {
  crossfade: { duration: 3 }, // 3-second crossfade
});
// Track A fades out, boss music fades in
OptionTypeDefaultDescription
volumenumber1Music volume (0–1)
loopbooleantrueLoop the track
crossfade.durationnumber2Crossfade duration in seconds

Stop / Pause / Resume #

tsx
stopMusic(2);     // Fade out over 2 seconds
pauseMusic();     // Pause (preserves position)
resumeMusic();    // Resume from paused position

Audio Sprites #

Pack multiple sounds into a single file to reduce HTTP requests:

tsx
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" });
FieldTypeDefaultDescription
startnumberStart offset in seconds
durationnumberDuration in seconds
loopbooleanfalseLoop this sprite region

Volume Channels #

Six independent volume channels, all routed through master:

text
[Source] → [Instance Gain] → [Channel Gain] → [Master Gain] → Output
ChannelPurpose
"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 #

tsx
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:

  1. On first click/tap/keypress, the AudioContext is unlocked

  2. isUnlocked becomes true (triggers React re-render)

  3. Queued music and looping sounds begin playing

  4. One-shot SFX requested before unlock are silently dropped (stale sounds are worse than none)

tsx
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.