# Sound Lane

> A lane's clips drawn on the GPU in the view you pick: the waveform, the waveform by band, or the spectrogram on one of four frequency axes with its axis and the reading under the pointer.

## Installation

**CLI**

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

**Manual**

**1. Install the following dependencies:**

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

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

```tsx title="components/audio-ui/sound-lane.tsx"
import type { SpectrogramScale } from "@audio-ui/gl/spectrogram";
import type { WaveformClip, WaveformLayout } from "@audio-ui/gl/waveform";
import { createSource } from "@audio-ui/react";
import { type ComponentProps, useState } from "react";
import {
  SpectrogramAxis,
  type SpectrogramBand,
  SpectrogramLane,
} from "@/components/audio-ui/clip-spectrogram";
import { WaveformLane } from "@/components/audio-ui/waveform-lane";

/** How a sound lane draws its clips: the waveform, the waveform by band, or the spectrogram on one of its frequency axes. */
export type SoundView = "bands" | "wave" | SpectrogramScale;

/** The views a sound lane draws, labelled for a menu: the waveforms first, then the spectrogram on each axis. */
export const SOUND_VIEWS: readonly {
  label: string;
  note: string;
  value: SoundView;
}[] = [
  { label: "Waveform", note: "Level over time", value: "wave" },
  { label: "Waveform, by band", note: "Lows, mids, highs", value: "bands" },
  { label: "Spectrogram", note: "Octaves", value: "log" },
  { label: "Spectrogram, mel", note: "Pitch scale", value: "mel" },
  { label: "Spectrogram, ERB", note: "Hearing filters", value: "erb" },
  { label: "Spectrogram, hertz", note: "Linear", value: "lin" },
];

/** Whether a string is one of the views, as a menu's value comes back. */
export const isSoundView = (value: string): value is SoundView =>
  SOUND_VIEWS.some((each) => each.value === value);

/** Whether a view draws the spectrogram. */
export const isSpectrogramView = (view: SoundView): view is SpectrogramScale =>
  view !== "wave" && view !== "bands";

/**
 * A lane's clips drawn on the GPU in the view you pick: their waveforms
 * (`WaveformLane`), the waveforms by band, or their spectrograms
 * (`SpectrogramLane`) with the frequency axis down the lane's left edge and
 * the frequency and level under the pointer. Put it in the lane's box, `zoom`
 * being the lane's pixels a second. Clips without `bands` are drawn plain in
 * the band view. It keeps the frequencies shown itself unless given `band`, and
 * needs WebGL 2: `hasWebGL2` of `@audio-ui/gl/lane` says whether to draw this
 * way at all.
 */
function SoundLane({
  band,
  className,
  clips,
  layout = "auto",
  view,
  zoom,
  ...props
}: Omit<ComponentProps<"canvas">, "ref"> & {
  /** The frequencies a spectrogram shows, as shares of its axis: give one to keep it outside, as it is kept across views. */
  band?: SpectrogramBand;
  clips: readonly WaveformClip[];
  /** How a recording's channels are laid out in the waveform: a row each once the rows are tall enough to read (`auto`), always (`split`) or never (`mix`). @default "auto" */
  layout?: WaveformLayout;
  view: SoundView;
  /** The lane's pixels a second. */
  zoom: number;
}) {
  const [shown] = useState(() =>
    createSource<readonly [number, number]>([0, 1])
  );
  // The frequency under the pointer, from the picture to the axis.
  const [pointer] = useState(() => createSource<number | null>(null));
  const [first] = clips;
  if (isSpectrogramView(view)) {
    return (
      <>
        {first ? (
          <SpectrogramAxis
            band={band ?? shown}
            pointer={pointer}
            sampleRate={first.sampleRate}
            scale={view}
          />
        ) : null}
        <SpectrogramLane
          band={band ?? shown}
          className={className}
          clips={clips}
          pointerHertz={pointer}
          scale={view}
          zoom={zoom}
          {...props}
        />
      </>
    );
  }
  return (
    <WaveformLane
      className={className}
      clips={clips}
      layout={layout}
      look={view === "bands" ? "bands" : "plain"}
      zoom={zoom}
      {...props}
    />
  );
}

export { SoundLane };
```

**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 lane 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

`SoundLane` draws a lane's clips in one of the ways a sound is looked at, the view you pick: its waveform ([Waveform Lane](/docs/components/base/waveform-lane)), its waveform by band, the lows warm and the highs cool, or its spectrogram ([Spectrogram Lane](/docs/components/base/clip-spectrogram)) on octaves, mel, ERB or hertz, with the frequency axis down the lane's left edge and the frequency and level under the pointer. The audio editor's View menu and the clip panel's both draw through it. It keeps the frequencies a spectrogram shows across views, and the pointer's frequency goes from the picture to the axis on its own.

```tsx
import { SOUND_VIEWS, SoundLane, type SoundView } from "@/components/audio-ui/sound-lane";

const [view, setView] = useState<SoundView>("wave");

<SoundLane
  className="h-full text-foreground"
  clips={clips} // WaveformClip[]: see Waveform Lane
  view={view} // "wave", "bands", "log", "mel", "erb" or "lin"
  zoom={pixelsPerSecond}
/>;
```

`SOUND_VIEWS` labels each view for a menu, with a note on what it shows; `isSoundView` checks a menu's value. A clip without `bands` is drawn plain in the band view.

| Prop | Type | |
| --- | --- | --- |
| `clips` | `WaveformClip[]` | The clips to draw, as the waveform lane takes them; the spectrogram's axis reads the first one's `sampleRate`. |
| `view` | `SoundView` | `"wave"`, `"bands"`, or the spectrogram on `"log"`, `"mel"`, `"erb"` or `"lin"`. |
| `zoom` | `number` | The lane's pixels a second. |
| `layout` | `"auto" \| "split" \| "mix"` | How a recording's channels are laid out in the waveform. Default `"auto"`: a row a channel once the rows are tall enough to read. |
| `band` | `SpectrogramBand` | The frequencies a spectrogram shows, as shares of its axis. Leave it out for the lane to keep its own. |

A recording streamed from a file is drawn without being read whole when its `samples` give `leaves` for the waveform and `spectra` for the spectrogram (see [`@audio-ui/gl/waveform`](/docs/gl/waveform#streamed-recordings) and [`@audio-ui/gl/spectrogram`](/docs/gl/spectrogram#streamed-recordings)). It needs WebGL 2 and draws nothing without it: `hasWebGL2` of `@audio-ui/gl/lane` says whether to draw this way at all.
