# Stutter

> A stutter device: an interval and a slice in note values, a decay and a mix, a panel that draws one interval, a Web Audio engine and tools for agents.

```tsx
"use client";

import { useState } from "react";
import {
  type StutterState,
  stutterDevice,
} from "@/components/audio-ui/devices/stutter/device";
import { createStutterEngine } from "@/components/audio-ui/devices/stutter/engine";
import { StutterPanel } from "@/components/audio-ui/devices/stutter/panel";
import { useDeviceLoop } from "@/hooks/use-device-loop";
import { useMasterBus } from "@/hooks/use-master-bus";

const MASTER_VOLUME = 0.7;

/**
 * A stutter you can plug in: its state, a panel that draws one interval and
 * an engine that plays it, each its own file and none knowing the others. Play
 * the loop and switch it on: it catches a slice and repeats it.
 */
export function BlockStutter() {
  const [state, setState] = useState<StutterState>(() =>
    stutterDevice.defaults()
  );
  const master = useMasterBus(MASTER_VOLUME);
  useDeviceLoop({
    create: createStutterEngine,
    destination: master,
    state,
  });
  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <StutterPanel onValueChange={setState} value={state} />
    </div>
  );
}
```

Repeats the sound. At the start of every interval it catches a slice, then plays it again and again until the interval ends, each repeat quieter by the decay. The lengths are note values of the host's tempo, so the repeats fall in time. Turning it on catches a slice at once, a stutter to play live. 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/stutter
```

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

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

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

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

import { useState } from "react";
import {
  type StutterState,
  stutterDevice,
} from "@/components/audio-ui/devices/stutter/device";
import { createStutterEngine } from "@/components/audio-ui/devices/stutter/engine";
import { StutterPanel } from "@/components/audio-ui/devices/stutter/panel";
import { useDeviceLoop } from "@/hooks/use-device-loop";
import { useMasterBus } from "@/hooks/use-master-bus";

const MASTER_VOLUME = 0.7;

/**
 * A stutter you can plug in: its state, a panel that draws one interval and
 * an engine that plays it, each its own file and none knowing the others. Play
 * the loop and switch it on: it catches a slice and repeats it.
 */
export function BlockStutter() {
  const [state, setState] = useState<StutterState>(() =>
    stutterDevice.defaults()
  );
  const master = useMasterBus(MASTER_VOLUME);
  useDeviceLoop({
    create: createStutterEngine,
    destination: master,
    state,
  });
  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <StutterPanel 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 stutter 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. |
| Interval | 1/32 triplet, 1/32, 1/32 dotted, 1/16 triplet, 1/16, 1/16 dotted, 1/8 triplet, 1/8, 1/8 dotted, 1/4 triplet, 1/4, 1/4 dotted, 1/2 triplet, 1/2, 1/2 dotted, 1/1 triplet, 1/1, 1/1 dotted | 1/4 | How long a cycle lasts: the slice is caught at its start, and repeated to its end. |
| Slice | 1/32 triplet, 1/32, 1/32 dotted, 1/16 triplet, 1/16, 1/16 dotted, 1/8 triplet, 1/8, 1/8 dotted, 1/4 triplet, 1/4, 1/4 dotted, 1/2 triplet, 1/2, 1/2 dotted, 1/1 triplet, 1/1, 1/1 dotted | 1/16 | How long the slice is, never longer than the interval: shorter repeats it faster. |
| Decay | 0 % to 100 % | 0 % | How much quieter each repeat is than the one before. At 0 they all play at full level. |
| Mix | 0 % to 100 % | 100 % | The balance of the repeats and the sound as it plays. |

## Panel

The panel draws one interval, in beats: the slice it catches, the sound as it plays, then each repeat at the level the decay leaves it. `stutterCycle(state)` is what it draws, and `stutterSettings(state, tempo)` the lengths in seconds.

```tsx
import { StutterPanel } from "@/components/audio-ui/devices/stutter/panel";
import { stutterDevice } from "@/components/audio-ui/devices/stutter/device";

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

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

## Engine

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

The stutter runs on the audio thread: the stutter of `@audio/effect-stutter` in an `AudioWorklet` loaded from a blob (`STUTTER_WORKLET` is its source), 128 frames at a time, with short fades at the edges of each repeat so it does not click. Its cycle runs from when it starts, not from the bar: the device knows the tempo, not where the transport is. Turning it on starts a fresh cycle, catching a slice at once, and new lengths, from a control or a change of tempo, wait for them to rest, then fade in on a fresh cycle. A worklet cannot import, so the device carries the kernel as published (`STUTTER_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.
