# LCD

> A small screen, as on a synth, that holds a reading under a caption, in light and dark alike.

```tsx
"use client";

import { useState } from "react";
import { Lcd } from "@/components/audio-ui/lcd";
import { ParameterField } from "@/components/audio-ui/parameter-field";
import { ValueDisplay } from "@/components/audio-ui/value-display";

export function LcdDemo() {
  const [cutoff, setCutoff] = useState(1200);
  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <Lcd className="w-full" label="Filter cutoff">
        <ValueDisplay kind="hz" size="lg" value={cutoff} />
      </Lcd>
      <div className="flex gap-2">
        <Lcd className="flex-1" label="Preset" size="sm">
          012 Warm Pad
        </Lcd>
        <Lcd className="flex-1" label="Tempo" size="sm">
          <ValueDisplay kind="bpm" size="sm" value={120} />
        </Lcd>
      </div>
      <ParameterField
        label="Cutoff"
        largeStep={1000}
        max={20_000}
        min={20}
        onValueChange={setCutoff}
        step={10}
        unit="Hz"
        value={cutoff}
      />
    </div>
  );
}
```

A small screen, as on a synth or a drum machine: the theme's foreground is its glass and its background the lit figures, so it reads as a screen in light and dark alike. It holds a reading, a `ValueDisplay` or a preset's name, under an optional caption. Its corners follow the style.

## Installation

**CLI**

```bash
npx shadcn@latest add @audio-ui/lcd
```

**Manual**

**1. Install the following dependencies:**

```bash
npm install class-variance-authority
```

**2. Copy and paste the following code into your project.**

```tsx title="components/audio-ui/lcd.tsx"
import { cva, type VariantProps } from "class-variance-authority";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";

const lcdVariants = cva("rounded-lg bg-foreground font-mono text-background tabular-nums shadow-[inset_0_1px_3px_rgb(0_0_0/0.45)] ring-1 ring-foreground/20 ring-inset inline-flex min-w-0 flex-col justify-center", {
  defaultVariants: { size: "default" },
  variants: {
    /** "sm" for a single line in a dense panel. */
    size: {
      default: "gap-0.5 px-3 py-2 text-sm",
      sm: "gap-0 px-2 py-1 text-xs",
    },
  },
});

export interface LcdProps extends VariantProps<typeof lcdVariants> {
  children: ReactNode;
  className?: string;
  /** A small caption over the reading: the parameter's or the preset's name. */
  label?: ReactNode;
  /** Announces changes politely, for a reading that matters as it changes. @default false */
  live?: boolean;
}

/**
 * A small screen, as on a synth or a drum machine: the theme's foreground as
 * its glass and its background as the lit figures, so it reads as a screen
 * in light and dark alike. Holds a reading (a `ValueDisplay`, a preset's
 * name) under an optional caption.
 */
export function Lcd({
  children,
  className,
  label,
  live = false,
  size,
}: LcdProps) {
  return (
    <div
      aria-live={live ? "polite" : undefined}
      className={cn(lcdVariants({ size }), className)}
      data-size={size ?? "default"}
      data-slot="lcd"
    >
      {label ? (
        <span className="text-[0.625rem] uppercase tracking-wider opacity-60 truncate" data-slot="lcd-label">
          {label}
        </span>
      ) : null}
      <span className="truncate" data-slot="lcd-reading">
        {children}
      </span>
    </div>
  );
}
```

**3. Update the import paths to match your project setup.**

**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 lcd 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).

## Usage

```tsx
import { Lcd } from "@/components/audio-ui/lcd";

<Lcd label="Preset" size="sm">
  012 Warm Pad
</Lcd>
```

For a value, put a [Value Display](/docs/components/base/value-display) in it, as the preview does: `<ValueDisplay kind="hz" size="lg" value={cutoff} />` under the caption "Filter cutoff".

Give it `live` when its reading matters as it changes: a screen readers announce politely.

## Together

A synth's panel, with the screen showing the value last touched: Power plays an arpeggio at the tempo, through the waveform, the filter's cutoff and resonance, the gain and the transpose, into the master fader.

```tsx
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { Fader } from "@/components/audio-ui/fader";
import { Knob } from "@/components/audio-ui/knob";
import { Lcd } from "@/components/audio-ui/lcd";
import { Led } from "@/components/audio-ui/led";
import { ParameterField } from "@/components/audio-ui/parameter-field";
import { RockerSwitch } from "@/components/audio-ui/rocker-switch";
import { ValueDisplay } from "@/components/audio-ui/value-display";
import { WaveformSelect } from "@/components/audio-ui/waveform-select";
import { useMasterBus } from "@/hooks/use-master-bus";
import { webAudio } from "@/lib/web-audio";

const WAVEFORMS = [
  { label: "Sine", value: "sine" },
  { label: "Triangle", value: "triangle" },
  { label: "Sawtooth", value: "sawtooth" },
  { label: "Square", value: "square" },
] as const;

/** The arpeggio, in semitones from C3: a minor seventh up and down. */
const STEPS = [0, 3, 7, 10, 12, 10, 7, 3] as const;
const ROOT = 48;
const SIGNED: Intl.NumberFormatOptions = { signDisplay: "exceptZero" };
/** How far ahead notes are scheduled, and how often the scheduler wakes, in seconds. */
const LOOKAHEAD = 0.1;
const TICK_MS = 25;

const hz = (note: number) => 440 * 2 ** ((note - 69) / 12);

/** Which value the screen shows: the last one touched. */
type Shown = "cutoff" | "gain" | "resonance" | "tempo" | "transpose";

/**
 * A small synth's panel, every control of the audio set in one place: Power
 * plays an arpeggio at the tempo, through the oscillator's waveform, a
 * low-pass filter (cutoff and resonance), the gain and the transpose, into the
 * master fader. The screen shows the value last touched; the LED lights on
 * each beat.
 */
export function BlockSynthPanel() {
  const [power, setPower] = useState(false);
  const [bpm, setBpm] = useState(120);
  const [gain, setGain] = useState(-6);
  const [transpose, setTranspose] = useState(0);
  const [waveform, setWaveform] = useState<string>("sawtooth");
  const [cutoff, setCutoff] = useState(1800);
  const [resonance, setResonance] = useState(4);
  const [volume, setVolume] = useState(0.7);
  const [shown, setShown] = useState<Shown>("cutoff");
  const [beat, setBeat] = useState(false);
  const master = useMasterBus(volume);

  // What the scheduler reads as it goes, without restarting it.
  const live = useRef({ bpm, transpose, waveform });
  live.current = { bpm, transpose, waveform };
  const chain = useRef<{ filter: BiquadFilterNode; level: GainNode } | null>(
    null
  );

  // The filter and the gain stage, made once the master bus is there.
  useEffect(() => {
    const ctx = webAudio.getContext();
    if (!(ctx && master)) {
      return;
    }
    const filter = ctx.createBiquadFilter();
    const level = ctx.createGain();
    filter.type = "lowpass";
    filter.connect(level).connect(master);
    chain.current = { filter, level };
    return () => {
      level.disconnect();
      filter.disconnect();
      chain.current = null;
    };
  }, [master]);

  useEffect(() => {
    const ctx = webAudio.getContext();
    const nodes = chain.current;
    if (!(ctx && nodes)) {
      return;
    }
    nodes.filter.frequency.setTargetAtTime(cutoff, ctx.currentTime, 0.01);
    nodes.filter.Q.setTargetAtTime(resonance, ctx.currentTime, 0.01);
    nodes.level.gain.setTargetAtTime(10 ** (gain / 20), ctx.currentTime, 0.01);
  }, [cutoff, gain, resonance]);

  // The arpeggio, sixteenth notes at the tempo, scheduled a little ahead on the audio clock.
  useEffect(() => {
    const ctx = webAudio.getContext();
    if (!(power && ctx && chain.current)) {
      return;
    }
    if (ctx.state !== "running") {
      ctx.resume();
    }
    let step = 0;
    let next = ctx.currentTime + 0.05;
    const playNote = (at: number, note: number) => {
      const nodes = chain.current;
      if (!nodes) {
        return;
      }
      const length = 60 / live.current.bpm / 4;
      const oscillator = ctx.createOscillator();
      const envelope = ctx.createGain();
      oscillator.type = live.current.waveform as OscillatorType;
      oscillator.frequency.value = hz(note);
      envelope.gain.setValueAtTime(0, at);
      envelope.gain.linearRampToValueAtTime(0.25, at + 0.005);
      envelope.gain.setTargetAtTime(0, at + length * 0.6, 0.03);
      oscillator.connect(envelope).connect(nodes.filter);
      oscillator.start(at);
      oscillator.stop(at + length + 0.2);
    };
    const timer = setInterval(() => {
      while (next < ctx.currentTime + LOOKAHEAD) {
        const index = step % STEPS.length;
        playNote(next, ROOT + (STEPS[index] ?? 0) + live.current.transpose);
        if (index % 4 === 0) {
          const delay = Math.max(0, (next - ctx.currentTime) * 1000);
          setTimeout(() => setBeat(true), delay);
          setTimeout(() => setBeat(false), delay + 90);
        }
        next += 60 / live.current.bpm / 4;
        step += 1;
      }
    }, TICK_MS);
    return () => {
      clearInterval(timer);
      setBeat(false);
    };
  }, [power]);

  const showTempo = useCallback((value: number) => {
    setBpm(value);
    setShown("tempo");
  }, []);
  const showGain = useCallback((value: number) => {
    setGain(value);
    setShown("gain");
  }, []);
  const showTranspose = useCallback((value: number) => {
    setTranspose(value);
    setShown("transpose");
  }, []);
  const showCutoff = useCallback((value: number) => {
    setCutoff(value);
    setShown("cutoff");
  }, []);
  const showResonance = useCallback((value: number) => {
    setResonance(value);
    setShown("resonance");
  }, []);
  const setMaster = useCallback(
    (value: number | readonly number[]) =>
      setVolume(Array.isArray(value) ? (value[0] ?? 0) : Number(value)),
    []
  );

  const reading = {
    cutoff: <ValueDisplay kind="hz" size="lg" value={cutoff} />,
    gain: <ValueDisplay kind="db" size="lg" value={gain} />,
    resonance: (
      <span className="font-medium text-2xl">Q {resonance.toFixed(1)}</span>
    ),
    tempo: <ValueDisplay kind="bpm" size="lg" value={bpm} />,
    transpose: <ValueDisplay kind="semitones" size="lg" value={transpose} />,
  }[shown];

  return (
    <div className="flex w-full max-w-2xl flex-wrap items-stretch gap-4 rounded-xl border bg-card p-4 text-card-foreground">
      <div className="flex flex-col items-center gap-2">
        <Led label="Beat" on={beat && power} />
        <RockerSwitch
          aria-label="Power"
          checked={power}
          onCheckedChange={setPower}
        />
        <span className="text-muted-foreground text-xs uppercase tracking-wide">
          Power
        </span>
      </div>
      <div className="flex min-w-56 flex-1 flex-col gap-2">
        <Lcd className="w-full" label={shown} live>
          {reading}
        </Lcd>
        <ParameterField
          label="Tempo"
          max={240}
          min={40}
          onValueChange={showTempo}
          precision={2}
          size="sm"
          unit="BPM"
          value={bpm}
        />
        <ParameterField
          format={SIGNED}
          label="Gain"
          max={6}
          min={-36}
          onValueChange={showGain}
          precision={1}
          size="sm"
          step={0.1}
          unit="dB"
          value={gain}
        />
        <ParameterField
          format={SIGNED}
          label="Transpose"
          max={24}
          min={-24}
          onValueChange={showTranspose}
          scrub="vertical"
          size="sm"
          unit="st"
          value={transpose}
        />
      </div>
      <div className="flex flex-col items-center justify-between gap-3">
        <WaveformSelect
          aria-label="Oscillator waveform"
          onValueChange={setWaveform}
          options={WAVEFORMS}
          value={waveform}
        />
        <div className="flex gap-4">
          <div className="flex flex-col items-center gap-1">
            <Knob
              aria-label="Cutoff"
              max={20_000}
              min={40}
              onValueChange={showCutoff}
              scale="log"
              step={1}
              value={cutoff}
              valueLabel="hidden"
            />
            <span className="text-muted-foreground text-xs uppercase tracking-wide">
              Cutoff
            </span>
          </div>
          <div className="flex flex-col items-center gap-1">
            <Knob
              aria-label="Resonance"
              max={20}
              min={0.5}
              onValueChange={showResonance}
              step={0.1}
              value={resonance}
              valueLabel="hidden"
            />
            <span className="text-muted-foreground text-xs uppercase tracking-wide">
              Reso
            </span>
          </div>
        </div>
      </div>
      <div className="flex h-44 flex-col items-center gap-1">
        <Fader
          aria-label="Master volume"
          max={1}
          min={0}
          onValueChange={setMaster}
          step={0.01}
          value={volume}
        />
        <span className="text-muted-foreground text-xs uppercase tracking-wide">
          Master
        </span>
      </div>
    </div>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` |  | The reading. |
| `label` | `ReactNode` |  | A small caption over the reading: the parameter's or the preset's name. |
| `live` | `boolean` | `false` | Announces changes politely, for a reading that matters as it changes. |
| `size` | `"default" \| "sm"` | `"default"` | "sm" for a single line in a dense panel. |
