# Binaural Panner

> A binaural panner device: a direction, a distance, a head's size and a mix, a panel with the head seen from above, a Web Audio engine and tools for agents.

```tsx
"use client";

import { useState } from "react";
import {
  type BinauralPannerState,
  binauralPannerDevice,
} from "@/components/audio-ui/devices/binaural-panner/device";
import { createBinauralPannerEngine } from "@/components/audio-ui/devices/binaural-panner/engine";
import { BinauralPannerPanel } from "@/components/audio-ui/devices/binaural-panner/panel";
import { useDeviceLoop } from "@/hooks/use-device-loop";
import { useMasterBus } from "@/hooks/use-master-bus";

const MASTER_VOLUME = 0.7;

/**
 * A binaural panner you can plug in: its state, a panel with the head seen
 * from above and an engine that plays it, each its own file and none knowing
 * the others. Put headphones on, play the loop and drag the sound around.
 */
export function BlockBinauralPanner() {
  const [state, setState] = useState<BinauralPannerState>(() =>
    binauralPannerDevice.defaults()
  );
  const master = useMasterBus(MASTER_VOLUME);
  useDeviceLoop({
    create: createBinauralPannerEngine,
    destination: master,
    state,
  });
  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <BinauralPannerPanel onValueChange={setState} value={state} />
    </div>
  );
}
```

Places a sound all around the head, for headphones. A pan only sets a level between two speakers; this one makes the cues the ear uses: the far ear hears the sound a fraction of a millisecond later, and darker, in the head's shadow. Drag the sound around the head, behind it, beside it, farther out. It is a [device](/docs/devices): a state, a panel, an engine and tools, each its own file.

## Installation

**CLI**

```bash
npx shadcn@latest add @audio-ui/binaural-panner
```

It installs the device's state, its engine and its panel to `components/audio-ui/devices/binaural-panner/`. The demo above, which wires them to a loop, is `@audio-ui/block-binaural-panner`.

**Manual**

**1. Install the packages:**

```bash
npm install @audio-ui/react @audio-ui/core
```

**2. Add the pieces you want. All of them, or only the state and the panel if your engine is elsewhere:**

```bash
npx shadcn@latest add @audio-ui/binaural-panner-device @audio-ui/binaural-panner-panel @audio-ui/binaural-panner-engine
```

**3. Copy and paste the demo that wires them, if you want it.**

```tsx title="components/examples/block-binaural-panner.tsx"
"use client";

import { useState } from "react";
import {
  type BinauralPannerState,
  binauralPannerDevice,
} from "@/components/audio-ui/devices/binaural-panner/device";
import { createBinauralPannerEngine } from "@/components/audio-ui/devices/binaural-panner/engine";
import { BinauralPannerPanel } from "@/components/audio-ui/devices/binaural-panner/panel";
import { useDeviceLoop } from "@/hooks/use-device-loop";
import { useMasterBus } from "@/hooks/use-master-bus";

const MASTER_VOLUME = 0.7;

/**
 * A binaural panner you can plug in: its state, a panel with the head seen
 * from above and an engine that plays it, each its own file and none knowing
 * the others. Put headphones on, play the loop and drag the sound around.
 */
export function BlockBinauralPanner() {
  const [state, setState] = useState<BinauralPannerState>(() =>
    binauralPannerDevice.defaults()
  );
  const master = useMasterBus(MASTER_VOLUME);
  useDeviceLoop({
    create: createBinauralPannerEngine,
    destination: master,
    state,
  });
  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <BinauralPannerPanel onValueChange={setState} value={state} />
    </div>
  );
}
```

**Agent**

Give your agent the skill, and it picks the component, installs it and wires it to your audio:

```bash
npx skills add ouestlabs/audio-ui
```

Then ask it:

```text
Add the binaural panner from audio/ui to my project and wire it to my audio code.
```

To let the agent read the docs live, connect the [MCP server](/docs/mcp). More in [Agent Skill](/docs/agents).

## Controls

| Control | Range | Default | What it does |
| --- | --- | --- | --- |
| On | On or off | On | Turns the device on. Off, the signal passes through unchanged. |
| Direction | -180° to 180° | 0° | Where the sound comes from, in degrees: 0 in front, 90 to the right, -90 to the left, 180 behind. Drag the handle around the head to set it. |
| Distance | 0.50 m to 10.00 m | 1.00 m | How far the sound is: twice as far is half as loud. Drag the handle out or in to set it. |
| Head | 5.00 cm to 12.00 cm | 8.75 cm | The radius of the head the cues are made for: a bigger head delays and shades the far ear more. |
| Mix | 0 % to 100 % | 100 % | The balance of the placed sound and the sound as it was. |

The cues are of direction around the head, not of height, and a sound behind sounds much like one in front: the model has no ears' folds to tell them apart.

## Panel

The panel shows the head seen from above, in front up, and a handle for the sound: its angle is the direction and how far out it is the distance. `binauralPoint(state)` is where the handle sits, and `binauralPlace(point)` the direction and distance of a point.

```tsx
import { BinauralPannerPanel } from "@/components/audio-ui/devices/binaural-panner/panel";
import { binauralPannerDevice } from "@/components/audio-ui/devices/binaural-panner/device";

const [state, setState] = useState(() => binauralPannerDevice.defaults());

<BinauralPannerPanel onValueChange={setState} value={state} />;
```

## Engine

Create it, connect it and update it as for any device: see [The engine](/docs/devices#the-engine).

The panner runs on the audio thread: the binaural panner of `@audio/spatial-binaural`, the structural model of Brown and Duda, in an `AudioWorklet` loaded from a blob (`BINAURAL_PANNER_WORKLET` is its source), 128 frames at a time. It folds the sound to mono, delays the far ear as a round head would, and shades it with a one-pole filter, smoothing a change of direction so it does not click. Its delay line is made for the largest head, so the head can change while it plays, and a direction is sent as the angle nearest the last, so a turn across the back goes the short way round. A worklet cannot import, so the device carries the kernel as published (`BINAURAL_PANNER_KERNELS`, in its `kernels.ts`). Until it is ready, and in a context with no worklets, the sound passes clean: `ready` resolves once it is in the graph.
