Stereo Field

stereo-fieldViewers

The parameter-side picture of a stereo image, in one panel: an L-R field with a pan cursor, the width parameter drawn as a spread around it, mid/side balance bars, and a correlation meter. The cursor is a reading and not a control — hand it an LFO phase each frame and an auto-panner's motion is drawn by the host that owns the clock, the same way lfo-visualizer takes phase. Below the cursor sits a second, deliberately different mark: the measured energy balance, which is not a pan-knob readback. A mono source panned to 0.5 by the constant-power law balances at 0.414, both numbers are right about different things, and drawing them as one mark would force a choice between lying about the parameter and lying about the signal. The correlation meter's lower half is tinted because the scale is not symmetric in meaning: above zero the channels reinforce on a mono sum, at -1 they cancel and the material disappears the moment anything downstream folds to mono. Pairs with lissajous-scope, which draws the same signal's shape — this one draws its numbers, and it is not a goniometer.

L
R
M
S
MID / SIDE0% side
CORRELATION-1 · 0 · +1+1.00
C
35%
Auto-pan
OFF

Two knobs draw, one measures. Pan and Width are the parameter side — the solid cursor and the band around it are what was asked for, animated here by the demo's own interval the way an auto-panner would animate them from its LFO. The hollow diamond is the signal side: where the energy actually landed, which sits at tan(p·π/4) rather than at p, so at half right the knob says 0.50 and the meter says 0.41 and both are correct. Decorrelate slides the right channel's phase; the meter reads the cosine of it (+1.00 at 0°), and panning does not move it at all, because correlation is invariant to per-channel gain — until the very ends, where one channel goes silent and the meter drops to zero rather than claim a phase relationship with nothing. Past 90° the needle crosses into the red half, and that half is the one that matters: material below zero cancels itself the moment anything downstream folds to mono — a phone speaker, a club's sub feed — so at -1 the sound you are mixing simply is not there. For the shape of the image rather than its numbers, put a lissajous-scope beside this; the two are the same signal seen from opposite ends.

Install

Copies the source into your project. Anything it depends on comes with it.

$ npx shadcn add @syntho-ui/stereo-field

Pulls in

stereo-mathuse-audio-sourcecontrol-theme

First time? Add the registry to your components.json once:

components.json
{
  "registries": {
    "@syntho-ui": "https://engine.syntho.app/r/{name}.json"
  }
}

Usage

Controlled or uncontrolled, whichever suits the surrounding state.

example.tsx
import { StereoField } from "@/components/ui/stereo-field"

// Parameter-side: an auto-panner's cursor, animated by whoever owns the LFO.
<StereoField pan={Math.sin(phase)} width={0.4} />

// Driven readings, when the engine already meters.
<StereoField pan={0} width={0.8} correlation={corr} balance={bal} mid={m} side={s} />

// A live pair — read-only, never connected or reconfigured.
<StereoField analyserLeft={left} analyserRight={right} />

Props

PropTypeDefaultDescription
pannumber0Cursor position, -1 (hard left) to +1 (hard right). A parameter, not a measurement: animate it from an LFO phase and the auto-panner's motion draws itself.
widthnumber0Width parameter, 0 (a point at the cursor) to 1 (the whole field when centred), drawn as a symmetric spread. Clamped at the field's edges, so a wide image panned hard draws asymmetrically — the field ends, and the image does not get to continue past L.
correlation / balance / mid / sidenumberPrecomputed readings, the driven form and the primary one. Resolved per field: anything supplied here wins over any live source, so a panel that knows its own correlation can still borrow the live mid/side.
analyserLeft / analyserRightAnalyserNode | nullA live pair. Read-only — the component never connects, disconnects or reconfigures a node, and never builds an AudioContext.
getStereoData(left: Float32Array, right: Float32Array) => boolean | voidFill both arrays with the current frame and return true. The engine-agnostic form: a worklet or a test harness drives the meter with no Web Audio involved. Returning false is 'no data this frame'.
ballistics{ attack?: number; release?: number }{ attack: 0.35, release: 0.08 }Needle time constants for live readings, in seconds. The default is the inverse of the kit's meter default on purpose — on a correlation meter the move worth catching is the drop toward antiphase, so the fast constant is on the downward travel. Driven values are never smoothed: a driven value is already what the host meant, and smoothing it would lag the knob.
showCorrelation / showMidSidebooleantrueWhich readings are drawn. With both off the panel is the L-R field and the cursor alone, which is all an auto-panner face needs.
labels[string, string]["L", "R"]End captions for the field.
colorstringOverride the accent colour for this instance. The warning tint is not overridable — it is shared with gain-reduction-meter's hot zone so one face cannot have two different reds meaning 'this is the bad one'.
heightnumber64Height of the L-R field in pixels. There is no layout width prop: `width` here is the stereo width parameter, and one name cannot be both — the panel fills its container.

Interaction

  • Drag with pointer capture, so the gesture keeps tracking past the edge of the control.
  • Shift while dragging for a fine pass; release it mid-gesture and coarse tracking resumes.
  • Double-click resets to defaultValue.
  • Arrow keys step normally, Shift+arrow steps finely, PageUp/PageDown jump by ten, Home/End go to the extremes.
  • Proper slider and switch roles with live aria-valuetext, so a screen reader announces the formatted value rather than the raw number.
  • The scroll wheel adjusts the value while the pointer is over the control; stepped ranges move at least one declared step per notch by default.