# Expander

> An expander device: downward or upward, threshold, ratio, knee, range, attack and release, a panel that draws its transfer curve, a Web Audio engine and tools for agents.

```tsx
"use client";

import { useState } from "react";
import {
  type ExpanderState,
  expanderDevice,
} from "@/components/audio-ui/devices/expander/device";
import { createExpanderEngine } from "@/components/audio-ui/devices/expander/engine";
import { ExpanderPanel } from "@/components/audio-ui/devices/expander/panel";
import { useDeviceLoop } from "@/hooks/use-device-loop";
import { useMasterBus } from "@/hooks/use-master-bus";

const MASTER_VOLUME = 0.7;

/**
 * An expander you can plug in: its state, a panel that draws its curve and
 * an engine that plays it, each its own file and none knowing the others. Play
 * the loop: downward, what falls under the threshold is turned down; upward,
 * the peaks are lifted.
 */
export function BlockExpander() {
  const [state, setState] = useState<ExpanderState>(() =>
    expanderDevice.defaults()
  );
  const master = useMasterBus(MASTER_VOLUME);
  useDeviceLoop({
    create: createExpanderEngine,
    destination: master,
    state,
  });
  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <ExpanderPanel onValueChange={setState} value={state} />
    </div>
  );
}
```

Widens the dynamics. Downward, it turns down what falls under the threshold, softer than a gate: the room between the words, the bleed between the hits. Upward, it lifts what rises over it, giving back the peaks a limiter flattened. It plays the expander of [audio.js](https://audio.js.org). 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/expander
```

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

**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/expander-device @audio-ui/expander-panel @audio-ui/expander-engine
```

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

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

import { useState } from "react";
import {
  type ExpanderState,
  expanderDevice,
} from "@/components/audio-ui/devices/expander/device";
import { createExpanderEngine } from "@/components/audio-ui/devices/expander/engine";
import { ExpanderPanel } from "@/components/audio-ui/devices/expander/panel";
import { useDeviceLoop } from "@/hooks/use-device-loop";
import { useMasterBus } from "@/hooks/use-master-bus";

const MASTER_VOLUME = 0.7;

/**
 * An expander you can plug in: its state, a panel that draws its curve and
 * an engine that plays it, each its own file and none knowing the others. Play
 * the loop: downward, what falls under the threshold is turned down; upward,
 * the peaks are lifted.
 */
export function BlockExpander() {
  const [state, setState] = useState<ExpanderState>(() =>
    expanderDevice.defaults()
  );
  const master = useMasterBus(MASTER_VOLUME);
  useDeviceLoop({
    create: createExpanderEngine,
    destination: master,
    state,
  });
  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <ExpanderPanel 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 expander 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. |
| Mode | Downward, Upward | Downward | Downward turns down what falls under the threshold. Upward lifts what rises over it. It is picked in the title bar. |
| Threshold | -80 dB to 0 dB | -30 dB | The level the expander turns on. Drag the handle on the plot across to set it. |
| Ratio | 1:1 to 1:20 | 1:2 | How many dB the level moves for each dB past the threshold: at 1:2, 10 dB under the threshold come out 20 dB under it. |
| Knee | 0 dB to 24 dB | 6 dB | How gradually it starts to work around the threshold. 0 is a hard knee. |
| Range | 0 dB to 90 dB | 40 dB | The most it turns the sound down, or lifts it. |
| Attack | 0.1 ms to 100 ms | 5 ms | How fast it follows the level as it rises. |
| Release | 1 ms to 1000 ms | 50 ms | How fast it follows the level as it falls. |

## Panel

The panel draws the transfer curve against the straight line of a clean sound, with a handle on the threshold, through the package's own gain curves.

```tsx
import { ExpanderPanel } from "@/components/audio-ui/devices/expander/panel";
import { expanderDevice } from "@/components/audio-ui/devices/expander/device";

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

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

## Engine

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

```ts
engine.reduction; // dB being taken off, read per frame
```

The browser has no expander node. The engine plays the processor of `@audio/dynamics-expander`'s manifest as published, in an `AudioWorklet` fed only while it plays: off, or until the worklet is ready, the sound goes round it. The mode, the threshold, the ratio, the knee and the range change as it plays. A new attack or release builds it again: at most every 50 ms, the new one first playing the last 100 ms of sound, then taking over in a 30 ms fade. `engine.reduction` reads what it takes off, so it reads 0 upward.

## How close the curve is

The curve is the static curve, the level once it has settled. Measured on a steady tone at 100 Hz and at 1 kHz, the output's peaks sit up to 1.2 dB under it near and past the threshold, and on it elsewhere.

Upward has no ceiling but the range: at the defaults, a tone at -6 dB comes out at +17 dB. Follow it with a [limiter](/docs/components/base/limiter), or keep the range low.
