fix(playground): modular worklet base-class loading + live matrix display
Two fixes for Modular mode: 1. faust-worklet-processor.js: explicitly attach FaustWorkletProcessor to globalThis. 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, so the base class was invisible to subclass processors when they loaded in AudioWorklet- GlobalScope. Symptom: "FaustWorkletProcessor is not defined" at the extends clause of modular-subtractive-processor.js. 2. modular-ui.js + a-app.js: wire live matrix cell updates. Phase C wired matrix cells for writes (click cycles, precise editor) but not for reads — the MLP-driven values never propagated to the DOM. modular-ui now exposes updateLive(outputs), called from routeOutputs on every inference tick. Throttled to ~20 fps internally to avoid DOM thrashing. Only visible sources (within adsrCount/lfoCount) are updated; muted slots stay dark.
This commit is contained in:
parent
0de64463fd
commit
e6938ebb8b
3 changed files with 61 additions and 0 deletions
|
|
@ -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,
|
// Note: registerProcessor() is called by each concrete engine file, not here,
|
||||||
// because each engine has its own processor name.
|
// because each engine has its own processor name.
|
||||||
// Subclass files should end with:
|
// Subclass files should end with:
|
||||||
|
|
|
||||||
|
|
@ -2392,6 +2392,12 @@ function routeOutputs(outputs) {
|
||||||
// Visualizer always gets every frame (it's local, no buffer)
|
// Visualizer always gets every frame (it's local, no buffer)
|
||||||
synthVisualizer.setParams(overridden);
|
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
|
// Engine param updates: throttle + dead-zone filter
|
||||||
if (activeEngine && activeEngine.running) {
|
if (activeEngine && activeEngine.running) {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
|
|
|
||||||
|
|
@ -754,12 +754,60 @@ export function initModularUI({ getEngine, onStateChange } = {}) {
|
||||||
// However, wireDock was wired at init time — any new dock-click delegation
|
// However, wireDock was wired at init time — any new dock-click delegation
|
||||||
// works because it's attached to #dock via event 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 {
|
return {
|
||||||
teardown,
|
teardown,
|
||||||
refresh,
|
refresh,
|
||||||
show,
|
show,
|
||||||
hide,
|
hide,
|
||||||
isVisible,
|
isVisible,
|
||||||
|
updateLive,
|
||||||
/** Exposed for testability. */
|
/** Exposed for testability. */
|
||||||
_debug: {
|
_debug: {
|
||||||
getCell,
|
getCell,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue