Z zvuk
Concept

Spatializer

Stereo pan or full 3D positional audio. PannerNode wrapped, HRTF on by default.

TL;DR

Pass spatializer in play() to insert a StereoPannerNode (2D) or PannerNode (3D HRTF) between the voice and its bus. For positions that change over time — e.g. a moving NPC — construct a Spatializer manually so you can hold its reference and update it per frame.

Mental model

2D — pan: -1 ↔ 1 L R -1 0 +1 voice @ 0.43 3D — position: [x, y, z] · listener at origin listener A B C
2D pan is cheap (StereoPannerNode). 3D uses PannerNode in HRTF mode — one node per voice.

API surface

Spatializer + SpatialOptions ts
interface SpatialOptions {
  pan?: number;                       // [-1, 1] — 2D
  position?: [number, number, number]; // x, y, z  — 3D
}

class Spatializer {
  setPan(pan: number): void;          // 2D only
  setPosition(x: number, y: number, z: number): void;  // 3D only
  connectInto(dest: AudioNode): AudioNode;
  dispose(): void;
}

Live demo

Drag the puck to pan a looping sound. Best with headphones.

Recipes

2D pan from play()

ts ts
engine.sound('footstep').play({
  spatializer: { pan: -0.6 },              // [-1, 1] stereo
});

3D position from play()

ts ts
engine.sound('footstep').play({
  spatializer: { position: [x, y, z] },    // world-space coords
});

Dynamic position — moving source

ts ts
import { Spatializer } from 'zvuk';

const sp = new Spatializer(engine.context, { position: [0, 0, 0] });
sp.connectInto(engine.bus('sfx').input);
sp.setPosition(playerX, playerY, playerZ);    // each frame

// when done:
sp.dispose();

Pitfalls

Don't 3D-spatialize music.
Music wants to feel "wide", not "located." HRTF on a stereo music bus is a noticeable downgrade. Pan it manually if you want a side-bias.
Don't construct a Spatializer per frame.
HRTF nodes are cheap-ish but not free. Hold one per emitter and setPosition it.

Related

  • Voice — the spatialized voice.
  • Bus — what spatializers route into.