DSP rack

Three grip faders (EQ tilt, compression, saturation): a tick-marked track and a rounded grip that follows the hand 1:1, with a unity tick on the bipolar EQ.

Show code
"use client";

import {
  useEffect,
  useRef,
  useState,
  type KeyboardEvent as ReactKeyboardEvent,
  type PointerEvent as ReactPointerEvent,
} from "react";
import type { DspParams } from "@/lib/openkit/audio-engine";
import { cn } from "@/lib/utils";

// Live DSP: EQ tilt, compression and saturation as three grip faders. Each
// fader is a label and readout over a tick-marked track with a rounded grip.
// The grip follows the pointer 1:1 and keyboard steps snap: no spring, no
// lag, no overshoot. Audio and readout update on the same frame as the hand.
// EQ is bipolar (warm ← flat → bright), so its centre tick marks unity.

interface SliderDef {
  key: keyof DspParams;
  label: string;
  /** Centre is the neutral value (marked with a taller tick). */
  bipolar?: boolean;
  /** Human-readable readout for the current 0..1 value. */
  format: (v: number) => string;
}

const SLIDERS: SliderDef[] = [
  {
    key: "eq",
    label: "EQ",
    bipolar: true,
    format: (v) => {
      const tilt = Math.round((v - 0.5) * 200);
      if (tilt === 0) return "Flat";
      return tilt > 0 ? `+${tilt} bright` : `${-tilt} warm`;
    },
  },
  { key: "comp", label: "Compression", format: (v) => `${Math.round(v * 100)}%` },
  { key: "sat", label: "Saturation", format: (v) => `${Math.round(v * 100)}%` },
];

const DEFAULTS: DspParams = { eq: 0.5, comp: 0, sat: 0 };
const STEP = 0.05;
const BIG_STEP = 0.2;
const TICKS = 17; // odd, so the bipolar centre lands on a tick
const GRIP_W = 14;
const INSET = 8; // grip travel stays clear of the track's rounded ends

export function DspRack({
  onChange,
  initial,
  controls,
}: {
  onChange: (params: Partial<DspParams>) => void;
  /** Starting values (0..1); EQ 0.5 is flat. */
  initial?: Partial<DspParams>;
  /** Which faders to show, in order. Defaults to all three. */
  controls?: readonly (keyof DspParams)[];
}) {
  const sliders = controls
    ? controls.flatMap((key) => SLIDERS.filter((def) => def.key === key))
    : SLIDERS;
  return (
    <div className="flex w-full flex-col gap-4">
      {sliders.map((def) => (
        <Fader
          key={def.key}
          def={def}
          initial={initial?.[def.key] ?? DEFAULTS[def.key]}
          onChange={(v) => onChange({ [def.key]: v })}
        />
      ))}
    </div>
  );
}

function Fader({
  def,
  initial,
  onChange,
}: {
  def: SliderDef;
  initial: number;
  onChange: (value: number) => void;
}) {
  const trackRef = useRef<HTMLDivElement>(null);
  const [value, setValue] = useState(initial);
  const [dragging, setDragging] = useState(false);
  const [travel, setTravel] = useState(0);

  // Grip travel in px, so the grip moves with transform (no layout per frame).
  useEffect(() => {
    const el = trackRef.current;
    if (!el) return;
    const ro = new ResizeObserver(([entry]) => {
      setTravel(Math.max(0, entry.contentRect.width - INSET * 2 - GRIP_W));
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const setTo = (next: number) => {
    const clamped = Math.round(Math.min(1, Math.max(0, next)) * 100) / 100;
    if (clamped === value) return;
    setValue(clamped);
    onChange(clamped);
  };

  const fromClientX = (clientX: number) => {
    const el = trackRef.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    setTo((clientX - rect.left - INSET - GRIP_W / 2) / (rect.width - INSET * 2 - GRIP_W));
  };

  const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
    if (e.pointerType === "mouse" && e.button !== 0) return;
    e.currentTarget.setPointerCapture(e.pointerId);
    setDragging(true);
    fromClientX(e.clientX);
  };

  const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
    if (dragging) fromClientX(e.clientX);
  };

  const endDrag = (e: ReactPointerEvent<HTMLDivElement>) => {
    setDragging(false);
    e.currentTarget.releasePointerCapture?.(e.pointerId);
  };

  const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {
    const step = e.shiftKey ? BIG_STEP : STEP;
    const keys: Record<string, number> = {
      ArrowRight: value + step,
      ArrowUp: value + step,
      ArrowLeft: value - step,
      ArrowDown: value - step,
      PageUp: value + BIG_STEP,
      PageDown: value - BIG_STEP,
      Home: 0,
      End: 1,
    };
    if (!(e.key in keys)) return;
    e.preventDefault();
    setTo(keys[e.key]);
  };

  const centre = (TICKS - 1) / 2;

  return (
    <div className="flex flex-col gap-2">
      <div className="flex items-baseline justify-between px-0.5">
        <span className="text-sm text-foreground">{def.label}</span>
        <span className="font-mono text-xs tabular-nums text-muted-foreground">{def.format(value)}</span>
      </div>
      <div
        ref={trackRef}
        role="slider"
        tabIndex={0}
        aria-label={def.label}
        aria-valuemin={0}
        aria-valuemax={100}
        aria-valuenow={Math.round(value * 100)}
        aria-valuetext={def.format(value)}
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={endDrag}
        onPointerCancel={endDrag}
        onKeyDown={onKeyDown}
        onDoubleClick={() => setTo(DEFAULTS[def.key])}
        data-dragging={dragging ? "" : undefined}
        className={cn(
          "group relative h-9 w-full cursor-ew-resize touch-none select-none rounded-xl bg-muted outline-none",
          "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
        )}
      >
        {/* Ticks: evenly spaced across the grip's travel. */}
        <span
          aria-hidden
          className="pointer-events-none absolute inset-y-0 flex items-center justify-between"
          style={{ left: INSET + GRIP_W / 2 - 1, right: INSET + GRIP_W / 2 - 1 }}
        >
          {Array.from({ length: TICKS }, (_, i) => (
            <span
              key={i}
              className={cn(
                "w-0.5 rounded-full",
                def.bipolar && i === centre ? "h-4 bg-muted-foreground/70" : "h-2.5 bg-muted-foreground/30",
              )}
            />
          ))}
        </span>
        {/* Grip: a rounded bar with a hairline edge; brightens while held. */}
        <span
          aria-hidden
          className={cn(
            "pointer-events-none absolute top-1.5 bottom-1.5 left-0 rounded-[5px] shadow-[inset_0_0_0_1px_rgb(255_255_255/0.25),0_1px_2px_rgb(0_0_0/0.35)]",
            "bg-muted-foreground transition-colors duration-150 group-hover:bg-foreground/80 group-data-[dragging]:bg-foreground",
          )}
          style={{ width: GRIP_W, transform: `translateX(${INSET + value * travel}px)` }}
        />
      </div>
    </div>
  );
}