Skip to content

Make a Spatial-Audio Pitch Sketch: Should the Sound Stay With the Scene or the Listener?

Advertising

Make a Spatial-Audio Pitch Sketch: Should the Sound Stay With the Scene or the Listener?

Turn your head. If a place sound stays exactly where it was in your ears, you have not built a spatial sound. You have built a stereo one.

That test costs nothing and settles more arguments than any authoring tool's preset browser — once its verdict is scoped. It convicts a sound meant to belong to the scene, not one deliberately attached to the listener: a head-locked cue passes it by design, which is why the taxonomy below sorts the attachments before it judges any of them. It also reframes the job. A spatial sketch isn't "make the sound move left to right." It's a claim about what the sound is attached to, and a decision about whether the attachment survives the listener turning around.

So the sketch you bring to the pitch should do three things: name the attachment, declare the coordinates, and compare one turn against one walk. Here's one of those, built small enough to finish.

Name what the sound is attached to

Three attachments matter, and they're distinguished by a single question: what stays the same when the listener turns?

A kettle on a counter is attached to a place. Turn your head and the kettle's bearing changes; the kettle does not care. A phone in the courier's hand is attached to an object, which moves on its own schedule regardless of where you're looking. A voice explaining the offer is attached to the listener, or to nothing at all — turn your head and it stays in front of you, unchanged, which is exactly the point of it.

Most pitches reach for spatialization because one sound needs to behave like the first case. The mistake is applying it to the third. Explanatory narration that drifts out to the side as the viewer turns is a located sound doing a job that location actively harms: the words get harder to follow at precisely the moment they're carrying the argument. If the sentence has to be understood, don't spend it on spatial implication.

The object case is worth knowing about even when your sketch doesn't need it, because the code shape is identical. The panner's coordinates get written from the object's transform instead of the room's, and everything downstream — the comparison, the fallback, the text description — is unchanged.

Separate source motion from listener motion

Pick a convention and write it down where the next person will find it. This sketch uses metres, +Y up, and a listener who starts at the origin facing −Z. Ear height 1.6 m. The kettle sits at (−1.2, 1.0, −2.0): a metre and a bit to the left, two metres ahead, sitting at counter height below the ear line.

MDN's page on Web Audio spatialization is worth reading for the object model before you write any of this. It treats the AudioListener and the PannerNode as separate objects, each with its own position and orientation, and its example wires a source through a panner and starts playback from a user action, with suspended-context handling and a note that browsers differ (page reviewed 20 September 2026; not executed).

That separation is the whole lesson. The listener's position and orientation are your viewer's head. The panner's position is the source's place in the world. Neither knows about the other; the relationship between them is what the listener perceives.

Which gives you the quietest way to waste a week: the camera and the listener are two consumers of one state. If you drive the visible camera from a controller object and forget to write the same numbers into the listener, nothing errors. The scene renders. The sound plays. The listener is nailed to the origin forever, so the sound behaves as if you never moved, and your comparison shows nothing at all. Build the state object first, then feed both from it in the same frame.

And notice that turning and walking are not the same test. A turn about your own vertical axis changes bearing and nothing else. A walk changes bearing, distance and elevation at once. If your only listener control is a turn, you have tested a third of the relationship and the easiest third at that.

Compare scene attachment with listener attachment

Here's the sketch. It's invented for this article: the kettle is fictional, and no chime has been rendered, played or listened to while writing it. What follows is geometry and code, not measurement.

const ctx = new AudioContext();

// Declared scene: metres, +Y up, listener starts at the origin facing -Z.
const EAR    = 1.6;
const KETTLE = { x: -1.2, y: 1.0, z: -2.0 };   // world space, scene-fixed route

// The kettle's opening offset, expressed in the listener's own frame:
// 1.2 m left, 2.0 m ahead, 0.6 m below the ears.
const LOCAL  = { right: -1.2, ahead: 2.0, up: -0.6 };

// One object drives the camera and the listener. Never let them drift apart.
const state = { x: 0, y: EAR, z: 0, yaw: 0 };   // yaw in radians, + = turn right

function applyListener(s) {
  const l = ctx.listener;
  l.positionX.value = s.x;
  l.positionY.value = s.y;
  l.positionZ.value = s.z;
  l.forwardX.value =  Math.sin(s.yaw);
  l.forwardY.value =  0;
  l.forwardZ.value = -Math.cos(s.yaw);
  l.upX.value = 0; l.upY.value = 1; l.upZ.value = 0;
}

// The only difference between the two routes.
function positionFor(route, s) {
  if (route === 'scene') return KETTLE;

  // 'attached': re-express the same local offset every frame.
  const fwd   = { x:  Math.sin(s.yaw), z: -Math.cos(s.yaw) };
  const right = { x:  Math.cos(s.yaw), z:  Math.sin(s.yaw) };
  return {
    x: s.x + LOCAL.right * right.x + LOCAL.ahead * fwd.x,
    y: s.y + LOCAL.up,
    z: s.z + LOCAL.right * right.z + LOCAL.ahead * fwd.z,
  };
}

Then the graph, with the two routes and the flat fallback sharing one output:

const master = ctx.createGain();
master.connect(ctx.destination);

function playChime(buffer, route) {
  const src = ctx.createBufferSource();
  src.buffer = buffer;

  if (route === 'flat') {
    src.connect(master);                 // no bearing, no distance, no height
  } else {
    const p = ctx.createPanner();
    p.panningModel  = 'HRTF';
    p.distanceModel = 'inverse';
    p.refDistance   = 1;
    p.rolloffFactor = 1;
    const pos = positionFor(route, state);
    p.positionX.value = pos.x;
    p.positionY.value = pos.y;
    p.positionZ.value = pos.z;
    src.connect(p).connect(master);
  }
  src.start();
}

// Browsers won't start audio without a gesture, and the context may be born
// suspended, so resume it from the same click that plays the cue.
document.querySelector('#play').addEventListener('click', async () => {
  if (ctx.state === 'suspended') await ctx.resume();
  playChime(buffer, document.querySelector('input[name=route]:checked').value);
});

// Turn, walk, reset. Each one writes `state`, then re-applies the listener in
// the same frame the camera reads `state` from.
function turn(degrees) {
  state.yaw += degrees * Math.PI / 180;
  applyListener(state);
}

function walk(metres) {
  state.x += Math.sin(state.yaw) * metres;
  state.z -= Math.cos(state.yaw) * metres;
  applyListener(state);
}

function reset() {
  Object.assign(state, { x: 0, y: EAR, z: 0, yaw: 0 });
  // The route lives in the radio group, not in `state`, so reset it too:
  // otherwise the comparison restarts from whichever route was last chosen.
  document.querySelector('input[name=route][value=scene]').checked = true;
  applyListener(state);
}

applyListener(state);   // once at startup: the default listener sits at (0,0,0)

Two controls, matching the two listener behaviours: turn (±60° in place) and walk (1.5 m along the current heading). Both write state and re-apply the listener, so the state object and the listener can't drift apart. Plus a reset that returns yaw to zero, position to the origin, and the route selector to scene-fixed, so every comparison starts from the same state.

That startup call matters more than it looks. Until something writes to it, the listener stays where Web Audio leaves it — at (0, 0, 0), below ear height — so the numbers below wouldn't hold on the first play. Skipping it reproduces the same "nailed to the origin" bug, just more quietly, before a single turn.

Now the numbers. At rest the kettle is about 31° left of your nose, 2.41 m away, 14° below the ear line.

start after turning 60° right after walking 1.5 m forward
bearing to kettle 31° left 91° left 67° left
distance 2.41 m 2.41 m 1.43 m
elevation 14° below ear 14° below 25° below
level (declared model) unchanged ≈ 4.5 dB louder

Read the first column of changes: turning moves the kettle from front-left to a degree past abeam — essentially straight out from your left ear. Distance and height don't budge, because you rotated on the spot. That's the purest look at bearing you'll get.

Walking is messier and more honest. The kettle swings out to 67° left, closes to 1.43 m, and drops further below your line of sight because you got closer to something below eye level. Three quantities move at once. That's why the walk belongs in the sketch even though the turn is easier to reason about.

Now the listener-attached route with the same buffer, the same gain, the same panner settings. All four numbers in every column are unchanged, because the cue was defined relative to the listener in the first place. Turning right swings the room past you and the chime comes along, 31° left, 2.41 m, 14° down, forever.

The useful way to see that: for a pure turn, the gap between the two routes is exactly the angle you turned. Rotate 60°, and the scene-fixed cue moves 60° while the attached one moves zero — a 60° disagreement you can prove on paper, as long as the source isn't straight overhead where bearing stops meaning much. Walking has no such tidy identity. You have to measure it, and now you can.

Here's where the attached route stops being merely abstract. After the 60° turn, the attached chime's virtual position is about 2.3 m to the right of the kettle, at almost exactly the kettle's depth. After the 1.5 m walk, it sits 1.5 m directly beyond the kettle — past the counter, somewhere in the wall. The cue hasn't gotten worse in the abstract. It has stopped being a claim about the room. If there's nothing on screen to contradict it, nobody will notice. Put a kettle in the frame and the disagreement is the entire experience.

One fairness note: the sketch leaves the panner's orientation alone, so no directional cone is in play. If you later give the chime a cone to make it feel like it fires forward, set it identically in both routes. Change two things at once and you've stopped comparing.

Make playback and the fallback part of the sketch

Two practicalities keep the demo from dying in the meeting.

The first is the gesture. Audio won't start without a user action, and the context can begin suspended; resuming it inside the click that also plays the cue is what makes the demo survive a laptop that just woke up. Fix the panning and distance models before you compare anything — between routes, only the coordinates should differ.

The second is the fallback, and it's where the interesting mistake lives. Reachable, labeled controls — Scene-fixed, Listener-attached, No spatialization — plus buttons for start, after turning, and after walking, so nobody has to hold a key and pivot on a chair to see the comparison. Freeze the listener at the two end states and fire the single chime from each; a one-shot cue can be compared with no looping stand-in and no extra asset.

Then the part that's easy to get backwards: the flat version is not a neutral safety net. An unpanned cue has no bearing and no distance that respond to the listener. It shares the attached route's invariance exactly — the same "nothing changes when you turn" that we just spent the comparison diagnosing. It differs in the cue (centred rather than 31° left) but not in the structure. So the mono fallback is itself a design position, taken silently, and worth taking deliberately.

Which is why the text description isn't a footnote. You can't choose what the reviewer listens on. One line beside the demo — "Turn right: the chime swings from 31° left of your nose to 91° left; the kettle does not move" — carries the argument to anyone on laptop speakers, in mono, or on a browser that never started the context. The comparison has to survive being read.

What to carry back to the pitch

Pick one attachment and say the invariant out loud: the kettle's chime stays with the room, so turning your head is how you learn where the room's objects are. The narration stays with the listener, because it's a message to the viewer rather than a property of the place. That's two different rules in one 20-second spot, and stating them is more persuasive than any parameter count.

Then say what the sketch doesn't do, before someone else does. The inverse distance model is a formula, not a room — no reflections, no occlusion, no material. HRTF is not your ears. Browser behaviour differs, and MDN's page notes as much. Nothing here has been heard through headphones, speakers, or mono by anyone, including me; the figures above are geometry, not perception, and this chime has no listener report attached to it. Those auditions are still owed.

The reason to build the tiny version anyway is that it converts an argument about taste into an argument about a turn. Either the chime goes with the room or it goes with the head — and once you've written the coordinates down separately, everybody in the room can see which one you picked and why the kettle won.

Frequently asked questions

What test distinguishes spatial audio from stereo, and what are the three attachments?

Turn your head. If a place sound stays exactly where it was in your ears, you have built a stereo sound, not a spatial one. The test is scoped: it convicts a sound meant to belong to the scene, not a head-locked cue deliberately attached to the listener. The three attachments are place, object, and listener, distinguished by what stays the same when the listener turns. A kettle on a counter is attached to a place, a phone in a courier's hand is attached to an object, and a voice explaining the offer is attached to the listener or to nothing at all and stays in front unchanged. If the sentence has to be understood, do not spend it on spatial implication.

Why separate source motion from listener motion, and what bug does that prevent?

The listener's position and orientation are your viewer's head; the panner's position is the source's place in the world. Neither knows about the other, and the relationship between them is what the listener perceives. The camera and listener are two consumers of one state: if you drive the visible camera but forget to write the same numbers into the listener, nothing errors, but the listener is nailed to the origin and the comparison shows nothing. Build the state object first, then feed both from it in the same frame. Also note that turning and walking are not the same test: a turn changes bearing only, while a walk changes bearing, distance, and elevation at once.

What does the example comparison show for scene-fixed versus listener-attached sound?

At rest the kettle is about 31 degrees left of your nose, 2.41 m away, and 14 degrees below the ear line. After turning 60 degrees right, the scene-fixed bearing becomes 91 degrees left, while distance and elevation do not budge; after walking 1.5 m forward, the scene-fixed bearing is 67 degrees left, distance is 1.43 m, elevation is 25 degrees below, and level is about 4.5 dB louder. The listener-attached route leaves all four numbers unchanged in every column. For a pure turn, the gap between the two routes is exactly the angle you turned. The attached chime after the turn sits about 2.3 m to the right of the kettle, and after the walk it sits 1.5 m directly beyond the kettle.

What playback and fallback concerns belong in the sketch?

Audio will not start without a user action, and the context may begin suspended, so resume it from the same click that plays the cue. Fix the panning and distance models before comparing anything; between routes, only the coordinates should differ. The fallback needs reachable, labeled controls for scene-fixed, listener-attached, and no spatialization, plus buttons for start, after turning, and after walking, so nobody has to pivot on a chair. The flat version is not a neutral safety net: it has no bearing or distance that respond to the listener and shares the attached route's invariance exactly, differing only by being centred. A text description beside the demo carries the comparison to anyone on laptop speakers, in mono, or on a browser that never started the context.

What limits does the article state about the sketch?

The sketch is invented: the kettle is fictional, no chime has been rendered, played, or listened to while writing it, and the figures are geometry, not perception. The inverse distance model is a formula, not a room: no reflections, occlusion, or material. HRTF is not your ears, and browser behaviour differs. The sketch leaves the panner's orientation alone, so if you later give the chime a cone, set it identically in both routes. Nothing here has been heard through headphones, speakers, or mono by anyone, including the author; those auditions are still owed.

More in Advertising Browse all articles