diff --git a/playground/faust/faust-worklet-processor.js b/playground/faust/faust-worklet-processor.js index b3ce032..b3a4865 100644 --- a/playground/faust/faust-worklet-processor.js +++ b/playground/faust/faust-worklet-processor.js @@ -174,6 +174,13 @@ class FaustWorkletProcessor extends AudioWorkletProcessor { } } +// Explicitly attach to globalThis so subsequent addModule() scripts can see +// it. Class declarations at the top of a classic script are lexically scoped +// to that script's evaluation context and do NOT propagate across separate +// addModule() calls; an explicit property assignment is required for the +// cross-script reference to resolve. +globalThis.FaustWorkletProcessor = FaustWorkletProcessor; + // Note: registerProcessor() is called by each concrete engine file, not here, // because each engine has its own processor name. // Subclass files should end with: diff --git a/playground/js/a-app.js b/playground/js/a-app.js index 9cffb08..7206609 100644 --- a/playground/js/a-app.js +++ b/playground/js/a-app.js @@ -2392,6 +2392,12 @@ function routeOutputs(outputs) { // Visualizer always gets every frame (it's local, no buffer) synthVisualizer.setParams(overridden); + // Modular matrix UI: mirror live MLP outputs into visible matrix cells + // (the UI throttles its own DOM writes internally) + if (modularUI && activeEngine?.id === 'modular') { + modularUI.updateLive(overridden); + } + // Engine param updates: throttle + dead-zone filter if (activeEngine && activeEngine.running) { const now = performance.now(); diff --git a/playground/js/ui/modular-ui.js b/playground/js/ui/modular-ui.js index d624d12..d5040ee 100644 --- a/playground/js/ui/modular-ui.js +++ b/playground/js/ui/modular-ui.js @@ -754,12 +754,60 @@ export function initModularUI({ getEngine, onStateChange } = {}) { // However, wireDock was wired at init time — any new dock-click delegation // works because it's attached to #dock via event delegation. + // Live-update throttle: the MLP can push ~60 Hz of new output vectors, but + // rewriting ~120 visible DOM cells that often is wasteful. Cap to ~20 fps. + const LIVE_UPDATE_INTERVAL_MS = 50; + let _lastLiveUpdate = 0; + + /** + * Called from the inference loop with the MLP output vector (normalized + * [0,1]). Updates visible matrix cell DOM to reflect live values without + * touching the engine (engine.setParam is already being called on the + * same tick by routeOutputs). + */ + function updateLive(outputs) { + if (!isVisible()) return; + if (!outputs || outputs.length === 0) return; + const engine = getEngine?.(); + if (!engine || engine.id !== 'modular') return; + if (matrixIndexCache.size === 0) return; + + const now = performance.now(); + if (now - _lastLiveUpdate < LIVE_UPDATE_INTERVAL_MS) return; + _lastLiveUpdate = now; + + const meta = engine.paramMeta; + const adsrN = uiState.adsrCount; + const lfoN = uiState.lfoCount; + const nVisibleSources = adsrN + lfoN; + + for (const [key, idx] of matrixIndexCache.entries()) { + // Only update cells whose source is currently visible. + const barIdx = key.indexOf('|'); + const s = +key.slice(0, barIdx); + if (s >= 16) { + // LFO: sources 16..47 map to LFO slots 0..31 + if ((s - 16) >= lfoN) continue; + } else if (s >= adsrN) { + continue; + } + const m = meta[idx]; + if (!m) continue; + const norm = outputs[idx]; + if (norm == null) continue; + const raw = m.min + norm * (m.max - m.min); + cellValues.set(key, raw); + updateCellDOM(+key.slice(0, barIdx), +key.slice(barIdx + 1), raw); + } + } + return { teardown, refresh, show, hide, isVisible, + updateLive, /** Exposed for testability. */ _debug: { getCell,