# Lo-fi

> A lo-fi device: wow, flutter, hiss, crackle, a high-pass and a low-pass and a tape drive, a panel that draws the band it leaves, a Web Audio engine and tools for agents.

```tsx
"use client";

import { useState } from "react";
import {
  type LofiState,
  lofiDevice,
} from "@/components/audio-ui/devices/lofi/device";
import { createLofiEngine } from "@/components/audio-ui/devices/lofi/engine";
import { LofiPanel } from "@/components/audio-ui/devices/lofi/panel";
import { useDeviceLoop } from "@/hooks/use-device-loop";
import { useMasterBus } from "@/hooks/use-master-bus";

const MASTER_VOLUME = 0.7;

/**
 * A lo-fi you can plug in: its state, a panel that draws the band it leaves
 * and an engine that plays it, each its own file and none knowing the others.
 * Play the loop and turn up the wow and the crackle: it ages like a worn tape.
 */
export function BlockLofi() {
  const [state, setState] = useState<LofiState>(() => lofiDevice.defaults());
  const master = useMasterBus(MASTER_VOLUME);
  useDeviceLoop({
    create: createLofiEngine,
    destination: master,
    state,
  });
  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <LofiPanel onValueChange={setState} value={state} />
    </div>
  );
}
```

Ages the sound like a worn tape or a record. Its pitch drifts and wobbles, a hiss and the crackle of dust ride under it, its band narrows and a tape drive saturates 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/lofi
```

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

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

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

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

import { useState } from "react";
import {
  type LofiState,
  lofiDevice,
} from "@/components/audio-ui/devices/lofi/device";
import { createLofiEngine } from "@/components/audio-ui/devices/lofi/engine";
import { LofiPanel } from "@/components/audio-ui/devices/lofi/panel";
import { useDeviceLoop } from "@/hooks/use-device-loop";
import { useMasterBus } from "@/hooks/use-master-bus";

const MASTER_VOLUME = 0.7;

/**
 * A lo-fi you can plug in: its state, a panel that draws the band it leaves
 * and an engine that plays it, each its own file and none knowing the others.
 * Play the loop and turn up the wow and the crackle: it ages like a worn tape.
 */
export function BlockLofi() {
  const [state, setState] = useState<LofiState>(() => lofiDevice.defaults());
  const master = useMasterBus(MASTER_VOLUME);
  useDeviceLoop({
    create: createLofiEngine,
    destination: master,
    state,
  });
  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <LofiPanel 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 lofi 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. |
| Wow | 0 % to 100 % | 30 % | A slow drift of the pitch, about 0.7 times a second, as a tape stretching on its reels. |
| Flutter | 0 % to 100 % | 20 % | A fast wobble of the pitch, about 7 times a second, as a capstan that does not turn true. |
| Hiss | 0 % to 100 % | 10 % | A hiss under the sound, as a tape's or a record's surface. |
| Crackle | 0 % to 100 % | 10 % | How often dust crackles, and how loud. |
| Low-pass | 500 Hz to 18 kHz | 6 kHz | Where the highs fall away, 18 dB an octave, as a worn tape loses its top. |
| High-pass | 20 Hz to 500 Hz | 60 Hz | Where the lows fall away, as a small speaker or a record's rumble filter. |
| Drive | 0 % to 100 % | 30 % | How hard the sound goes into the tape's saturation, its level kept. |

Each control at 0 takes its part out. There is no mix: the drift and the wobble delay the sound by a couple of milliseconds, and mixing it with the clean sound would comb it.

## Panel

The panel draws the band the high-pass and the low-pass leave. `lofiCurve(state)` is the curve it draws: the kernel's three one-pole low-passes in a row and its one-pole high-pass.

```tsx
import { LofiPanel } from "@/components/audio-ui/devices/lofi/panel";
import { lofiDevice } from "@/components/audio-ui/devices/lofi/device";

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

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

## Engine

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

The lo-fi is the kernel of `@audio/effect-lofi` in an `AudioWorklet` loaded from a blob (`LOFI_WORKLET` is its source), 128 frames at a time, each channel with a noise of its own. A worklet cannot import, so the device carries the kernel as published (`LOFI_KERNELS`, in its `kernels.ts`). Until it is ready, and when it is off, the sound passes clean, and `engine.ready` resolves to `false` in a context that cannot run worklets. Its hiss and crackle play on through silence, as a record's do.
