Navigate

Search topics across all sections

GitHub
Animation & Physics

Spatial Audio

Close your eyes in a concert hall. The drummer is on your left, the guitarist on your right. Walk closer to the guitarist and they get louder. Turn your head and the balance shifts. That is positional audio -- sound that exists in 3D space, not just your speakers. In WebGL, Three.js gives you the tools to place sounds in your scene so they behave just like real-world sound sources.

You add a PositionalAudio component to your scene and set autoplay. Nothing plays. No errors in the console. You refresh, try different browsers, check your volume. The file is loading fine. After an hour of debugging, you discover the issue: browsers block autoplay audio until the user interacts with the page.

terminal
DOMException: play() request was interrupted.
The AudioContext was not allowed to start.
Autoplay policy: https://developer.chrome.com/blog/autoplay
User gesture required before AudioContext.resume().

Real-world

Imagine standing in the middle of a concert hall. There are four musicians around you -- drums to the left, guitar to the right, vocals straight ahead, bass behind you.

As you walk toward the guitarist, their sound gets louder while the drums behind you fade. Turn your head to the right and the guitar moves to center while the drums shift to your right ear. This is exactly how positional audio works in 3D.

Three.js models this with two concepts: an AudioListener (your ears, attached to the camera) and PositionalAudio sources (the musicians, placed at specific coordinates). The Web Audio API handles the math -- distance falloff, stereo panning, and Doppler effects.

How Spatial Audio Works

AudioListener on camera

Your virtual ears -- moves with the camera

PositionalAudio on object

A sound source placed at a 3D position

Distance calculated

How far is the listener from the source?

Volume falls off

Inverse distance model reduces volume with distance

Stereo panning

Left/right balance based on relative position

Visualizing Spatial Audio

Since we cannot play actual audio in this demo, we are visualizing the concept. Each colored sphere is a sound source (drums, guitar, vocals, bass). The white sphere with ears is the listener. Drag the listener position with the controls and watch how pulse rings change -- closer sources pulse faster and brighter, just like they would sound louder. The connecting lines fade based on distance falloff.

Building It Step by Step

Step 1 -- Set up the AudioListener

The AudioListener is your virtual ears. Attach it to the camera so it moves and rotates with the viewer. There should be exactly one listener per scene.

AudioSetup.tsxTSX
import { useThree } from "@react-three/fiber"
import * as THREE from "three"

function AudioSetup() {
  const { camera } = useThree()
  const [listener] = useState(() => new THREE.AudioListener())

  useEffect(() => {
    camera.add(listener)
    return () => { camera.remove(listener) }
  }, [camera, listener])

  // Store listener in context or pass to children
  return null
}

Step 2 -- Create a positional audio source

Place a PositionalAudio as a child of any mesh. The sound will emanate from that mesh's position. The refDistance property controls where the falloff begins.

Speaker.tsxTSX
import { PositionalAudio } from "@react-three/drei"

function Speaker({ position }) {
  return (
    <mesh position={position}>
      <sphereGeometry args={[0.3]} />
      <meshStandardMaterial color="tomato" />

      {/* Audio emanates from this mesh's position */}
      <PositionalAudio
        url="/sounds/drums.mp3"
        distance={5}       // refDistance: full volume within 5 units
        loop
      />
    </mesh>
  )
}

distance (refDistance) is the radius of full volume. Beyond this distance, volume drops off using an inverse-distance model by default.

Step 3 -- Configure distance falloff

Three.js supports multiple distance models for how sound fades with distance. The most common is inverse (realistic) but you can also use linear (predictable) or exponential (dramatic).

DistanceModels.tsxTSX
// The three distance models:

// Inverse (default, most realistic):
// volume = refDistance / (refDistance + rolloff * (distance - refDistance))
audio.setDistanceModel('inverse')
audio.setRefDistance(5)       // full volume within 5 units
audio.setRolloffFactor(1)    // how quickly it fades

// Linear (predictable, reaches zero):
// volume = 1 - rolloff * (distance - refDistance) / (maxDistance - refDistance)
audio.setDistanceModel('linear')
audio.setMaxDistance(20)     // silence beyond 20 units

// Exponential (dramatic falloff):
// volume = (distance / refDistance) ^ -rolloff
audio.setDistanceModel('exponential')

What you just learned

AudioListener attaches to the camera and represents the player's ears

PositionalAudio sources are placed at 3D coordinates and their volume varies with distance

refDistance defines the radius of full volume -- beyond it, sound fades

Three distance models exist: inverse (realistic), linear (predictable), exponential (dramatic)

Browsers require a user gesture before audio can play -- always gate audio behind a click

Insight

Spatial audio is not just for games. Virtual tours use it to make rooms feel lived-in (a ticking clock in the study, rain on the windows). Product configurators use it for satisfying click sounds when selecting options. Art installations use it to guide attention -- placing a subtle sound draws the viewer toward a specific area of the scene.

Think about it...

You have a sound source with refDistance=5 using the inverse distance model. The listener is at distance 15. Roughly how loud is the sound compared to being at distance 5?

Try These Challenges

Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.

Try This!

Beginner

Move listener to [0,0] — center stage

Try This!

Beginner

Set falloff to 1 — tiny range

Try This!

Beginner

Move listener to edge — distant sounds

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Trying to autoplay audio without user interaction
Browsers block audio until the user clicks or taps
Don't do this
Scene.tsxTSX
function Scene() {
  // This will be blocked by the browser!
  return (
    <PositionalAudio
      url="/music.mp3"
      autoplay
      distance={5}
    />
  )
}
All modern browsers require a user gesture (click, tap, key press) before playing audio. This is a security feature to prevent websites from blasting sound unexpectedly. Gate your audio behind a 'Start' button.
2
Setting refDistance too small
Audio drops to zero almost immediately
Don't do this
SoundSource.tsxTSX
// refDistance = 0.1 means audio is only
// audible within 0.1 units of the source
<PositionalAudio
  url="/music.mp3"
  distance={0.1}
/>
refDistance is the distance at which the volume is at 100%. Beyond that, it falls off. If refDistance is tiny, the listener has to be almost on top of the source to hear anything. Set it to match your scene scale.
3
Forgetting to attach audio to the camera
AudioListener is not connected, so positional audio has no reference point
Don't do this
AudioSetup.tsxTSX
// No AudioListener attached to camera
<Canvas>
  <PositionalAudio url="/sound.mp3" />
</Canvas>
Positional audio calculates volume and panning based on the listener's position, which is typically attached to the camera. Without an AudioListener on the camera, Three.js has no reference point for spatial calculations.

Best Practices

Gate audio behind user interaction

Always require a click or tap before starting audio. Use a "Start Experience" button or begin audio on the first user interaction with the scene.

Match refDistance to scene scale

If your scene units are meters, set refDistance in meters. A room might use refDistance=3, while an outdoor scene might use refDistance=20. Test by walking the camera around.

Limit concurrent audio sources

The Web Audio API can handle many sources, but each one consumes CPU for spatial processing. Keep active sources under ~10 and pause distant ones that the listener cannot hear.

Use compressed audio formats

Load .mp3 or .ogg instead of .wav. Compressed formats are 10-20x smaller, loading faster and using less memory. The quality difference is imperceptible for most spatial audio use cases.