# Sound readouts

> The row of a sound's facts under its waveform, as parts: its level in the measure you pick, its sample rate and its channels, each a menu that says what to convert it to.

```tsx
import { useCallback, useState } from "react";
import {
  SoundChannels,
  type SoundLayout,
  SoundLevel,
  SoundRate,
  SoundReadout,
  SoundReadouts,
} from "@/components/audio-ui/sound-readouts";

const LAYOUT_CHANNELS: Record<SoundLayout, number> = {
  "5.1": 6,
  "7.1": 8,
  mono: 1,
  stereo: 2,
};

const LEVELS = { loudness: -19.5, peak: -2.1, rms: -19.4 };

/** A sound's facts as parts: its level in the measure you pick, its rate and its channels, each a menu that says what to convert it to. */
export function SoundReadoutsReadoutsDemo() {
  const [sampleRate, setSampleRate] = useState(48_000);
  const [channels, setChannels] = useState(2);
  const mix = useCallback(
    (layout: SoundLayout) => setChannels(LAYOUT_CHANNELS[layout]),
    []
  );
  return (
    <SoundReadouts>
      <SoundLevel levels={LEVELS} />
      <SoundReadout>{LEVELS.peak} dBFS</SoundReadout>
      <SoundRate onResample={setSampleRate} sampleRate={sampleRate} />
      <SoundChannels channels={channels} onMix={mix} />
    </SoundReadouts>
  );
}
```

## Installation

**CLI**

```bash
npx shadcn@latest add @audio-ui/sound-readouts
```

**Manual**

**1. Install the following dependencies:**

```bash
npm install @audio-ui/react
```

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

```tsx title="components/audio-ui/sound-readouts.tsx"
import { formatLevel, formatSampleRate } from "@audio-ui/engine/format";
import { type ComponentProps, useCallback, useState } from "react";
import { cn } from "@/lib/utils";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuShortcut,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

/** The layouts a sound can be mixed to, each with what it is. */
export const LAYOUTS = [
  { channels: 1, id: "mono", note: "One channel, sides summed" },
  { channels: 2, id: "stereo", note: "Left and right" },
  { channels: 6, id: "5.1", note: "Surround with LFE" },
  { channels: 8, id: "7.1", note: "Surround, sides and back" },
] as const;

export type SoundLayout = (typeof LAYOUTS)[number]["id"];

/** The rates a sound can be converted to, each with what it is used for. */
export const RATES = [
  { note: "Telephone, walkie-talkie", rate: 8000 },
  { note: "Low-quality PCM", rate: 11_025 },
  { note: "VoIP, wideband speech", rate: 16_000 },
  { note: "Low-quality MPEG", rate: 22_050 },
  { note: "Audio CD, MP3", rate: 44_100 },
  { note: "Video, professional audio", rate: 48_000 },
  { note: "Pro recording for CD", rate: 88_200 },
  { note: "DVD-Audio, Blu-ray", rate: 96_000 },
  { note: "HDCD, CD production", rate: 176_400 },
  { note: "Pro video, Blu-ray", rate: 192_000 },
  { note: "DXD, SACD editing", rate: 352_800 },
  { note: "Highest in common software", rate: 384_000 },
] as const;

/** The measures of a sound's level, in the order the menu lists them. */
export const LEVELS = [
  { id: "peak", label: "Peak", unit: "dBFS" },
  { id: "rms", label: "RMS", unit: "dBFS" },
  { id: "loudness", label: "Loudness", unit: "LUFS" },
] as const;

export type SoundLevelId = (typeof LEVELS)[number]["id"];

/** What the three level measures read, each in its own unit. */
export type SoundLevels = Record<SoundLevelId, number>;

const TRIGGER =
  "rounded-sm px-1 outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 data-popup-open:bg-muted data-popup-open:text-foreground";

/** The row of a sound's facts under its waveform: its parts say its level, its rate and its channels, each a button that opens its menu. */
function SoundReadouts({ className, ...props }: ComponentProps<"div">) {
  return (
    <div
      className={cn(
        "flex items-center gap-1 font-mono text-[11px] text-muted-foreground tabular-nums",
        className
      )}
      data-slot="sound-readouts"
      {...props}
    />
  );
}

/** A plain readout in the row, not a menu: the peak beside a level that is. */
function SoundReadout({ className, ...props }: ComponentProps<"span">) {
  return (
    <span
      className={cn("px-1", className)}
      data-slot="sound-readout"
      {...props}
    />
  );
}

/** The level of the sound: one measure shown, the others one click away, each with its value. Uncontrolled, it starts on `defaultMeasure`; give `measure` and `onMeasureChange` to hold it yourself. */
function SoundLevel({
  defaultMeasure = "loudness",
  levels,
  measure,
  onMeasureChange,
}: {
  defaultMeasure?: SoundLevelId;
  levels: SoundLevels;
  measure?: SoundLevelId;
  onMeasureChange?: (measure: SoundLevelId) => void;
}) {
  const [own, setOwn] = useState<SoundLevelId>(defaultMeasure);
  const shown = measure ?? own;
  const current = LEVELS.find((level) => level.id === shown) ?? LEVELS[2];
  const choose = useCallback(
    (value: string) => {
      setOwn(value as SoundLevelId);
      onMeasureChange?.(value as SoundLevelId);
    },
    [onMeasureChange]
  );
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        aria-label="Level measure"
        className={TRIGGER}
        data-slot="sound-level"
        title="Level measure"
      >
        {formatLevel(levels[current.id], current.unit)}
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end" className="w-64" side="top">
        <DropdownMenuRadioGroup onValueChange={choose} value={shown}>
          {LEVELS.map((level) => (
            <DropdownMenuRadioItem key={level.id} value={level.id}>
              <span className="whitespace-nowrap">{level.label}</span>
              <DropdownMenuShortcut className="tracking-normal">
                {formatLevel(levels[level.id], level.unit)}
              </DropdownMenuShortcut>
            </DropdownMenuRadioItem>
          ))}
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

/** The sample rate of the sound, and the menu of the rates to resample it to. */
function SoundRate({
  onResample,
  sampleRate,
}: {
  onResample: (rate: number) => void;
  sampleRate: number;
}) {
  const choose = useCallback(
    (value: string) => onResample(Number(value)),
    [onResample]
  );
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        aria-label="Sample rate"
        className={TRIGGER}
        data-slot="sound-rate"
        title="Resample"
      >
        {formatSampleRate(sampleRate)}
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end" className="w-80" side="top">
        <DropdownMenuGroup>
          <DropdownMenuLabel>Resample to</DropdownMenuLabel>
        </DropdownMenuGroup>
        <DropdownMenuRadioGroup
          onValueChange={choose}
          value={String(sampleRate)}
        >
          {RATES.map((each) => (
            <DropdownMenuRadioItem key={each.rate} value={String(each.rate)}>
              <span className="whitespace-nowrap">
                {formatSampleRate(each.rate)}
              </span>
              <DropdownMenuShortcut className="tracking-normal">
                {each.note}
              </DropdownMenuShortcut>
            </DropdownMenuRadioItem>
          ))}
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

/** The channels of the sound, and the menu of the layouts to mix it to. */
function SoundChannels({
  channels,
  onMix,
}: {
  channels: number;
  onMix: (layout: SoundLayout) => void;
}) {
  const choose = useCallback(
    (value: string) => onMix(value as SoundLayout),
    [onMix]
  );
  const current = LAYOUTS.find((layout) => layout.channels === channels);
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        aria-label="Channels"
        className={TRIGGER}
        data-slot="sound-channels"
        title="Mix to"
      >
        {current?.id ?? `${channels} channels`}
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end" className="w-72" side="top">
        <DropdownMenuGroup>
          <DropdownMenuLabel>Mix to</DropdownMenuLabel>
        </DropdownMenuGroup>
        <DropdownMenuRadioGroup
          onValueChange={choose}
          value={current?.id ?? ""}
        >
          {LAYOUTS.map((layout) => (
            <DropdownMenuRadioItem key={layout.id} value={layout.id}>
              <span className="whitespace-nowrap">{layout.id}</span>
              <DropdownMenuShortcut className="tracking-normal">
                {layout.note}
              </DropdownMenuShortcut>
            </DropdownMenuRadioItem>
          ))}
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

export { SoundChannels, SoundLevel, SoundRate, SoundReadout, SoundReadouts };
```

**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 sound readouts 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 {
  SoundChannels,
  SoundLevel,
  SoundRate,
  SoundReadout,
  SoundReadouts,
} from "@/components/audio-ui/sound-readouts";

<SoundReadouts>
  <SoundLevel levels={{ loudness: -19.5, peak: -2.1, rms: -19.4 }} />
  <SoundReadout>-2.1 dBFS</SoundReadout>
  <SoundRate onResample={resample} sampleRate={48_000} />
  <SoundChannels channels={2} onMix={mix} />
</SoundReadouts>;
```

The parts show; they convert nothing. `SoundRate` calls `onResample` with the rate picked, from 8 kHz to 384 kHz, and `SoundChannels` calls `onMix` with `mono`, `stereo`, `5.1` or `7.1`: convert the sound your own way, or with `resampleChannels` and `mixChannels` from `@audio-ui/engine/files`. `SoundLevel` keeps the measure it shows (Peak, RMS or Loudness) itself, or take it over with `measure` and `onMeasureChange`.

The [audio editor](/docs/components/base/audio-editor) puts these parts under its waveform and replaces the sound with the converted one, as a step of undo.

## API reference

### SoundReadouts

The row. A `<div>`, with its own props.

### SoundLevel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `levels` | `{ peak: number; rms: number; loudness: number }` |  | What each measure reads, in dB: the peak and the RMS in dBFS, the loudness in LUFS. |
| `defaultMeasure` | `"peak" \| "rms" \| "loudness"` | `"loudness"` | The measure shown at first. |
| `measure` | `"peak" \| "rms" \| "loudness"` |  | The measure shown, when you hold it yourself. |
| `onMeasureChange` | `(measure) => void` |  | Called with the measure picked. |

### SoundRate

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `sampleRate` | `number` |  | The sound's rate, in Hz. |
| `onResample` | `(rate: number) => void` |  | Called with the rate picked. |

### SoundChannels

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `channels` | `number` |  | How many channels the sound has. |
| `onMix` | `(layout: "mono" \| "stereo" \| "5.1" \| "7.1") => void` |  | Called with the layout picked. |

### SoundReadout

A plain value in the row, such as the peak beside a level that is a menu. A `<span>`.
