# Rotary

> A rotary speaker device: a slow, fast or stop speed, a crossover, a depth, the horn's and the drum's ramps, a width and a mix, a panel that draws its two bands, a Web Audio engine and tools for agents.

```tsx
"use client";

import { useState } from "react";
import {
  type RotaryState,
  rotaryDevice,
} from "@/components/audio-ui/devices/rotary/device";
import { createRotaryEngine } from "@/components/audio-ui/devices/rotary/engine";
import { RotaryPanel } from "@/components/audio-ui/devices/rotary/panel";
import { useDeviceLoop } from "@/hooks/use-device-loop";
import { useMasterBus } from "@/hooks/use-master-bus";

const MASTER_VOLUME = 0.7;

/**
 * A rotary speaker you can plug in: its state, a panel that draws its two
 * bands and an engine that plays it, each its own file and none knowing the
 * others. Play the loop and switch to Fast: the rotors wind up.
 */
export function BlockRotary() {
  const [state, setState] = useState<RotaryState>(() =>
    rotaryDevice.defaults()
  );
  const master = useMasterBus(MASTER_VOLUME);
  useDeviceLoop({
    create: createRotaryEngine,
    destination: master,
    state,
  });
  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <RotaryPanel onValueChange={setState} value={state} />
    </div>
  );
}
```

Spins the sound in a rotary speaker, the cabinet of an organ: the highs come from a horn and the lows from a drum, both turning, so the sound sweeps its pitch and its level past the microphones. Switch from Slow to Fast and the rotors wind up, the light horn first, the heavy drum after it. 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/rotary
```

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

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

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

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

import { useState } from "react";
import {
  type RotaryState,
  rotaryDevice,
} from "@/components/audio-ui/devices/rotary/device";
import { createRotaryEngine } from "@/components/audio-ui/devices/rotary/engine";
import { RotaryPanel } from "@/components/audio-ui/devices/rotary/panel";
import { useDeviceLoop } from "@/hooks/use-device-loop";
import { useMasterBus } from "@/hooks/use-master-bus";

const MASTER_VOLUME = 0.7;

/**
 * A rotary speaker you can plug in: its state, a panel that draws its two
 * bands and an engine that plays it, each its own file and none knowing the
 * others. Play the loop and switch to Fast: the rotors wind up.
 */
export function BlockRotary() {
  const [state, setState] = useState<RotaryState>(() =>
    rotaryDevice.defaults()
  );
  const master = useMasterBus(MASTER_VOLUME);
  useDeviceLoop({
    create: createRotaryEngine,
    destination: master,
    state,
  });
  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <RotaryPanel 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 rotary 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. |
| Speed | Slow, Fast, Stop | Slow | How fast the rotors turn: Slow, the horn at 0.8 turns a second and the drum at 0.7; Fast, the horn at 6.7 and the drum at 5.9; or Stop. They wind to the new speed, they never jump. |
| Crossover | 200 Hz to 3 kHz | 800 Hz | Where the sound splits between the drum, under it, and the horn, over it. |
| Depth | 0 % to 100 % | 100 % | How far the rotors sweep the pitch and the level. At 0 the sound only splits. |
| Horn ramp | 0.05 s to 3.00 s | 0.60 s | How long the horn takes to reach a new speed. |
| Drum ramp | 0.05 s to 6.00 s | 2.50 s | How long the drum takes, heavier and slower than the horn. |
| Width | 0° to 180° | 90° | The angle between the two microphones around the cabinet: 0 is mono, 180 the widest. |
| Mix | 0 % to 100 % | 100 % | The balance of the cabinet and the clean sound, in mono. |

## Panel

The panel draws the drum's band, dashed, and the horn's, split at the crossover, over its speed and its knobs. `rotaryBands(state)` is what it draws.

```tsx
import { RotaryPanel } from "@/components/audio-ui/devices/rotary/panel";
import { rotaryDevice } from "@/components/audio-ui/devices/rotary/device";

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

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

## Engine

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

The cabinet runs on the audio thread: the rotary of `@audio/effect-rotary` in an `AudioWorklet` loaded from a blob (`ROTARY_WORKLET` is its source), 128 frames at a time. It splits the sound with a fourth-order Linkwitz-Riley crossover, which sums back to flat, then each rotor turns its band through a delay that sweeps its pitch and a gain that sweeps its level, heard by two microphones. The cabinet is mono: it takes both channels together and gives the two microphones. A worklet cannot import, so the device carries the rotary and the modules it is built from as published, the crossover of `@audio/eq-crossover`, the Butterworth design of `digital-filter` and the sections of `@audio/biquad` (`ROTARY_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.
