# Mono synth

> A monophonic analog synth device: a saw, square or triangle oscillator, a sub oscillator and noise into a resonant filter, an envelope, a glide and a vibrato, with eight presets, a panel with a keyboard, a Web Audio engine and tools for agents.

```tsx
"use client";

import { noteToFrequency } from "@audio-ui/core/pitch";
import { useCallback, useEffect, useRef, useState } from "react";
import { MonoSynthPanel } from "@/components/audio-ui/devices/mono-synth/panel";
import { useMasterBus } from "@/hooks/use-master-bus";
import { useMidiInput } from "@/hooks/use-midi-input";
import {
  type MonoSynthNote,
  type MonoSynthState,
  monoSynthDevice,
} from "@/components/audio-ui/devices/mono-synth/device";
import {
  createMonoSynthEngine,
  type MonoSynthEngine,
} from "@/components/audio-ui/devices/mono-synth/engine";
import { webAudio } from "@/lib/web-audio";

/** The note the panel's keyboard holds: one at a time, under its own name. */
const PANEL_NOTE = "panel";

/**
 * A monophonic analog synth you can plug in: its state, a panel that draws
 * and plays it, and a Web Audio engine that makes the sound, each its own
 * file. The engine is the whole synth: every note goes to it, and a note
 * played while another is held glides to it. Play the keyboard, or connect a
 * MIDI keyboard and play legato lines on it.
 */
export function BlockMonoSynth() {
  const [state, setState] = useState<MonoSynthState>(() =>
    monoSynthDevice.defaults()
  );
  const master = useMasterBus(1);
  const [engine, setEngine] = useState<MonoSynthEngine | null>(null);
  const stateRef = useRef(state);
  stateRef.current = state;

  useEffect(() => {
    const ctx = webAudio.getContext();
    if (!(ctx && master)) {
      return;
    }
    const made = createMonoSynthEngine(ctx, stateRef.current);
    made.output.connect(master);
    setEngine(made);
    return () => {
      made.dispose();
      setEngine(null);
    };
  }, [master]);

  // The engine follows the state, whoever changed it: a control or a call.
  useEffect(() => {
    engine?.update(state);
  }, [engine, state]);

  const playKey = useCallback(
    (note: MonoSynthNote) => engine?.noteOn(note, { id: PANEL_NOTE }),
    [engine]
  );
  const releaseKey = useCallback(
    () => engine?.noteOff({ id: PANEL_NOTE }),
    [engine]
  );
  const midi = useMidiInput({
    onNoteOff: (pitch: number) => engine?.noteOff({ id: `midi-${pitch}` }),
    onNoteOn: (pitch: number, velocity: number) =>
      engine?.noteOn(
        { frequency: noteToFrequency(pitch), velocity },
        { id: `midi-${pitch}` }
      ),
  });

  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <MonoSynthPanel
        activeNotes={midi.activeNotes}
        onNoteOff={releaseKey}
        onNoteOn={playKey}
        onValueChange={setState}
        value={state}
      />
    </div>
  );
}
```

A monophonic analog synth, for basses, leads and acid lines. An oscillator (a saw, a square of any width or a triangle), a square sub oscillator an octave down and noise are mixed into a resonant low-pass filter, which an envelope opens along with the level. It plays one note at a time: a note played while another is held glides to it. Start from one of its eight presets (Init Saw, Warm Pad, Acid Bass, Pluck, Brass, Sub Bass, Lead, Wind), then shape 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/mono-synth
```

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

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

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

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

import { noteToFrequency } from "@audio-ui/core/pitch";
import { useCallback, useEffect, useRef, useState } from "react";
import { MonoSynthPanel } from "@/components/audio-ui/devices/mono-synth/panel";
import { useMasterBus } from "@/hooks/use-master-bus";
import { useMidiInput } from "@/hooks/use-midi-input";
import {
  type MonoSynthNote,
  type MonoSynthState,
  monoSynthDevice,
} from "@/components/audio-ui/devices/mono-synth/device";
import {
  createMonoSynthEngine,
  type MonoSynthEngine,
} from "@/components/audio-ui/devices/mono-synth/engine";
import { webAudio } from "@/lib/web-audio";

/** The note the panel's keyboard holds: one at a time, under its own name. */
const PANEL_NOTE = "panel";

/**
 * A monophonic analog synth you can plug in: its state, a panel that draws
 * and plays it, and a Web Audio engine that makes the sound, each its own
 * file. The engine is the whole synth: every note goes to it, and a note
 * played while another is held glides to it. Play the keyboard, or connect a
 * MIDI keyboard and play legato lines on it.
 */
export function BlockMonoSynth() {
  const [state, setState] = useState<MonoSynthState>(() =>
    monoSynthDevice.defaults()
  );
  const master = useMasterBus(1);
  const [engine, setEngine] = useState<MonoSynthEngine | null>(null);
  const stateRef = useRef(state);
  stateRef.current = state;

  useEffect(() => {
    const ctx = webAudio.getContext();
    if (!(ctx && master)) {
      return;
    }
    const made = createMonoSynthEngine(ctx, stateRef.current);
    made.output.connect(master);
    setEngine(made);
    return () => {
      made.dispose();
      setEngine(null);
    };
  }, [master]);

  // The engine follows the state, whoever changed it: a control or a call.
  useEffect(() => {
    engine?.update(state);
  }, [engine, state]);

  const playKey = useCallback(
    (note: MonoSynthNote) => engine?.noteOn(note, { id: PANEL_NOTE }),
    [engine]
  );
  const releaseKey = useCallback(
    () => engine?.noteOff({ id: PANEL_NOTE }),
    [engine]
  );
  const midi = useMidiInput({
    onNoteOff: (pitch: number) => engine?.noteOff({ id: `midi-${pitch}` }),
    onNoteOn: (pitch: number, velocity: number) =>
      engine?.noteOn(
        { frequency: noteToFrequency(pitch), velocity },
        { id: `midi-${pitch}` }
      ),
  });

  return (
    <div className="rounded-xl h-full w-full min-w-0">
      <MonoSynthPanel
        activeNotes={midi.activeNotes}
        onNoteOff={releaseKey}
        onNoteOn={playKey}
        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 mono synth 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

Every number reads 0 to 10, as a panel's sliders do, and shows the value it sets in the unit the sound takes.

| Control | Range | Default | What it does |
| --- | --- | --- | --- |
| Wave | Saw, Square, Triangle | Saw | The oscillator's shape: a saw, a square or a triangle. |
| Range | 32', 16', 8', 4' | 8' | The octave the keyboard plays: 8' plays the pitch as it is, 16' and 32' one and two octaves down, 4' one up. |
| Tune | -50 ct to +50 ct | 0 ct | A fine tune, half a semitone either way. |
| Oscillator | 0 to 10 | 8 | The level of the oscillator. |
| Pulse width | 50% to 5% | 50% | The square's width: even at 0, a thin pulse at 10. It shapes the square only. |
| Sub | 0 to 10 | 4 | The level of the square an octave under the oscillator. |
| Noise | 0 to 10 | 1 | The level of the white noise. |
| Cutoff | 40 Hz to 12 kHz | 1.2 kHz | Where the low-pass filter closes, from 40 Hz to 12 kHz. |
| Resonance | 0 dB to 12 dB | 3.6 dB | How much the filter rings at its cutoff. |
| Envelope amount | 0 st to 48 st | 24 st | How far the envelope opens the filter, up to four octaves. |
| Attack | 3 ms to 1.80 s | 6 ms | How long a note takes to rise. |
| Decay | 10 ms to 3.00 s | 98 ms | How long it takes to fall to the sustain. |
| Sustain | 0% to 100% | 70% | The level a held note stays at. |
| Release | 10 ms to 5.00 s | 65 ms | How long a note takes to fade once let go. |
| Glide | 0 ms to 600 ms | 120 ms | How long the pitch takes to slide to a note played while another is held. |
| Vibrato rate | 0.2 Hz to 20 Hz | 0.8 Hz | The vibrato's speed. |
| Vibrato depth | 0 ct to 100 ct | 0 ct | How far the vibrato moves the pitch. |
| Volume | -∞ dB to -3 dB | -14.1 dB | The level of the output. |

## Panel

The panel loads a preset from the menu in its title bar, beside the oscillator's wave and the range, lays the other controls out by what they do (oscillators, filter, envelope, pitch, output) and has a keyboard of two octaves that plays the synth. `monoSynthPreset(name)` gives a preset's whole state, and `MONO_SYNTH_PRESETS` lists them. With `activeNotes`, the notes held now, such as on a MIDI keyboard, light up their keys. The panel sends notes out and does not make the sound, so it plays through any engine.

```tsx
import { MonoSynthPanel } from "@/components/audio-ui/devices/mono-synth/panel";
import { monoSynthDevice } from "@/components/audio-ui/devices/mono-synth/device";

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

<MonoSynthPanel 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.noteOn({ frequency: 110, velocity: 0.8 }, { id: "a2" }); // the envelope opens
engine.noteOn({ frequency: 165 }, { id: "e3" }); // held: glides to it
engine.noteOff({ id: "e3" }); // glides back to A2, still held
engine.noteOff({ id: "a2" }); // the last one: fades over the release
```

The engine is the whole synth: give it every note, each with its own `id`, rather than one engine a note. The first note opens the amplifier and the filter along the envelope. A note played while one is held glides the pitch to it over the glide and leaves the envelope as it is; let go of it, and the pitch glides back to the note still held. Once every note is let go, both fall back over the release from where they had got to, so a short note never clicks. A `noteOff` with no `id` lets every note go. Notes take a time of the audio clock, `{ at, id }`, for a host that schedules ahead, and the [arrangement](/docs/components/base/arrangement)'s engine gives a MIDI track's notes to the one engine. Every control moves with a ramp, a held note included. An agent plays the synth with the `play` tool.
