Stereo Field
stereo-fieldViewersThe 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.
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-fieldPulls in
First time? Add the registry to your components.json once:
{
"registries": {
"@syntho-ui": "https://engine.syntho.app/r/{name}.json"
}
}Usage
Controlled or uncontrolled, whichever suits the surrounding state.
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
| Prop | Type | Default | Description |
|---|---|---|---|
| pan | number | 0 | Cursor 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. |
| width | number | 0 | Width 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 / side | number | — | Precomputed 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 / analyserRight | AnalyserNode | null | — | A live pair. Read-only — the component never connects, disconnects or reconfigures a node, and never builds an AudioContext. |
| getStereoData | (left: Float32Array, right: Float32Array) => boolean | void | — | Fill 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 / showMidSide | boolean | true | Which 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. |
| color | string | — | Override 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'. |
| height | number | 64 | Height 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
sliderandswitchroles with livearia-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.