Skip to main content

Data Layer (react_ui/src/data/)

State management, C++↔JS bridge, and real-time metering.

Files

FilePurpose
bridge.tsC++↔JS communication layer. StateStorage for Zustand persistence, addStateListener for C++ events, juceBridge for native function calls.
meters.tsReal-time metering: useMeterStore (Zustand), raw rtBuffer, ballistic smoothing engine, subscribeRtBuffer API.
store.tsPrimary Zustand stores (useTransportStore, useMixerStore, etc.) with C++ persistence.
plugins.tsPlugin scanning and management state.
sliderDrag.tsShared slider drag utilities.

Real-Time Metering Pipeline

C++ (30Hz)  →  bridge.ts  →  rtBuffer (mutable)  →  rAF ballistic smoothing  →  displayBuffer  →  DOM
                (no log)       (raw data)              (60Hz, instant attack,      (smoothed)       (GPU transforms)
                                                        ~300ms release decay)

Three-Layer Architecture

  1. rtBuffer — Raw mutable object, written by the C++ event handler. No subscriptions, no notifications. Just a data landing zone.
  2. displayBuffer — Smoothed values computed by the requestAnimationFrame loop. Applies ballistic smoothing so meters look fluid even with 30Hz data input. Subscribers receive this.
  3. useMeterStore (Zustand) — Throttled to ~10Hz for the few React components that still use reactive selectors. Most components use subscribeRtBuffer instead.

Ballistic Smoothing Constants

ElementRelease FactorEffective DecayRationale
Level meters0.88~300msMatches professional PPM meter ballistics
Spectrum bars0.82~200msSlightly faster for responsive spectrum display
Stereo/phase0.85~250msBidirectional smoothing (not attack/release)
CPU0.95~600msSlow-moving data, avoid visual noise
Attack is always instant — when the new value exceeds the current smoothed value, it snaps immediately.

API

// Subscribe to smoothed display-rate data (~60Hz via rAF)
const unsub = subscribeRtBuffer((buf) => {
  // Direct DOM updates here — no React re-renders
  el.style.transform = `scaleY(${buf.master.left / 100})`;
});

// Get the raw (unsmoothed) latest data
const raw = getRtBuffer();

Rendering Best Practices

All meter components use direct DOM manipulation via subscribeRtBuffer (no React re-renders):
✅ Do❌ Don’t
style.transform = 'scaleY(...)'style.height = '...'
style.transform = 'scaleX(...)'style.width = '...'
style.transform = 'translateX(...)'style.left = '...'
will-change: transformCSS transition-* on high-freq elements
subscribeRtBuffer()useMeterStore(selector) for meters
Why transforms? height/width/left trigger browser layout recalculation (~0.5ms per property). transform is GPU-composited — the browser sends a matrix to the GPU with no layout cost.

Event Flow

C++ PlaybackInfo (30Hz timer)
  → callAsync(emitEventIfBrowserIsVisible("rtFrame", json))
  → bridge.ts addStateListener("rtFrame", ...)
      (pushLog skipped for rtFrame — would saturate 200-entry buffer)
  → JSON.parse → write to rtBuffer
  → markNewData() — ensures rAF loop is running
  → rAF loop applies ballistic smoothing → displayBuffer
  → subscribeRtBuffer callbacks → direct DOM updates