fix(modular): matrix cells opt-in + per-group drawer for non-C15 engines
Two fixes driven by user reports: 1. MLP wasn't affecting the sound. Matrix cells are now opt-in to the MLP output vector rather than always-driven. Default modular paramMeta is 32 mod-source params (4 ADSR * A/D/S/R + 8 LFO * rate/morph), down from 512. The default patch's MM_Matrix/s00_d08_amp=1.0 now survives the first inference tick because it's not in paramMeta — ADSR1 stays routed to amp and the MLP drives envelope shape per joystick position. Matrix cells still clickable as direct-DSP knobs in modular-ui: setCell now routes through engine._setRawByLabel(). A later UI pass can add a "expose to MLP" menu entry that calls engine.setExposeMatrixCell(s,d). Also removes the earlier exampleCount-based routing gate; no longer needed now that the default patch is stable. 2. Per-group curve drawer (hover over section labels on the synth visualizer) is now available for all synth engines, not just C15. Refactored showGroupDrawer behind a getSectionView(sectionIndex) helper that returns a uniform view for either C15 (via SYNTH_SECTIONS + groupOverrides) or non-C15 (via nonC15Sections + nonC15GroupCurves + engineParamOverrides). Group-level curve persists across sub-engine swaps by group name, so e.g. tuning the "ADSR 1" curve survives a switch between subtractive and fm without being reset. ModularEngine: adds setExposeMatrixCell(s,d,exposed) + getExposedMatrixCells() + clears exposed cells on sub-engine swap.
This commit is contained in:
parent
70c858016f
commit
4507db00c8
3 changed files with 191 additions and 69 deletions
|
|
@ -310,6 +310,43 @@ function rebuildParamToSection(paramMeta) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic section metadata for non-C15 engines (parallel to SYNTH_SECTIONS
|
||||||
|
* for C15). Indexed by section index (the `si` field in paramToSection).
|
||||||
|
*
|
||||||
|
* nonC15Sections[si] = { name, color, count, startIndex }
|
||||||
|
* nonC15GroupCurves[si] = scalar group-master curve [0,1]
|
||||||
|
*
|
||||||
|
* Group curves persist across engine switches keyed by section NAME, so a
|
||||||
|
* user's "Filter" curve survives a sub-engine swap that keeps the same
|
||||||
|
* group label.
|
||||||
|
*/
|
||||||
|
let nonC15Sections = [];
|
||||||
|
let nonC15GroupCurves = [];
|
||||||
|
const _nonC15GroupCurveMemory = new Map(); // groupName -> curve
|
||||||
|
|
||||||
|
function rebuildNonC15Sections(paramMeta) {
|
||||||
|
nonC15Sections = [];
|
||||||
|
nonC15GroupCurves = [];
|
||||||
|
let currentGroup = null;
|
||||||
|
let si = -1;
|
||||||
|
for (let i = 0; i < paramMeta.length; i++) {
|
||||||
|
const g = paramMeta[i].group ?? 'Other';
|
||||||
|
if (g !== currentGroup) {
|
||||||
|
si++;
|
||||||
|
currentGroup = g;
|
||||||
|
nonC15Sections.push({
|
||||||
|
name: g,
|
||||||
|
color: _colorFromGroup(g),
|
||||||
|
count: 0,
|
||||||
|
startIndex: i,
|
||||||
|
});
|
||||||
|
nonC15GroupCurves.push(_nonC15GroupCurveMemory.get(g) ?? 0.5);
|
||||||
|
}
|
||||||
|
nonC15Sections[si].count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore paramToSection to the static C15 layout (from SYNTH_SECTIONS).
|
* Restore paramToSection to the static C15 layout (from SYNTH_SECTIONS).
|
||||||
*/
|
*/
|
||||||
|
|
@ -1158,6 +1195,7 @@ async function setActiveEngine(engine) {
|
||||||
}
|
}
|
||||||
} else if (engine.paramMeta?.length > 0) {
|
} else if (engine.paramMeta?.length > 0) {
|
||||||
rebuildParamToSection(engine.paramMeta);
|
rebuildParamToSection(engine.paramMeta);
|
||||||
|
rebuildNonC15Sections(engine.paramMeta);
|
||||||
if (synthVisualizer) {
|
if (synthVisualizer) {
|
||||||
synthVisualizer.rebuild(engine.paramMeta);
|
synthVisualizer.rebuild(engine.paramMeta);
|
||||||
}
|
}
|
||||||
|
|
@ -1348,6 +1386,7 @@ async function init() {
|
||||||
}
|
}
|
||||||
if (newEngine.paramMeta?.length > 0) {
|
if (newEngine.paramMeta?.length > 0) {
|
||||||
rebuildParamToSection(newEngine.paramMeta);
|
rebuildParamToSection(newEngine.paramMeta);
|
||||||
|
rebuildNonC15Sections(newEngine.paramMeta);
|
||||||
if (synthVisualizer) synthVisualizer.rebuild(newEngine.paramMeta);
|
if (synthVisualizer) synthVisualizer.rebuild(newEngine.paramMeta);
|
||||||
}
|
}
|
||||||
buildEngineParamOverrides();
|
buildEngineParamOverrides();
|
||||||
|
|
@ -2398,16 +2437,8 @@ function routeOutputs(outputs) {
|
||||||
modularUI.updateLive(overridden);
|
modularUI.updateLive(overridden);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modular mode cold-start gate: with no training examples, an untrained
|
|
||||||
// MLP produces outputs around 0.5 which denormalise to ~0 for matrix
|
|
||||||
// cells (min=-1, max=1) and clobber the default patch's s00_d08_amp=1.0.
|
|
||||||
// Result: silence. Until the user captures at least one example, leave
|
|
||||||
// the worklet running on the default patch we already pushed at init.
|
|
||||||
const skipModularRouting = activeEngine?.id === 'modular' &&
|
|
||||||
(iml?.exampleCount ?? 0) === 0;
|
|
||||||
|
|
||||||
// Engine param updates: throttle + dead-zone filter
|
// Engine param updates: throttle + dead-zone filter
|
||||||
if (activeEngine && activeEngine.running && !skipModularRouting) {
|
if (activeEngine && activeEngine.running) {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
if (now - _lastParamSendTime >= PARAM_SEND_INTERVAL) {
|
if (now - _lastParamSendTime >= PARAM_SEND_INTERVAL) {
|
||||||
_lastParamSendTime = now;
|
_lastParamSendTime = now;
|
||||||
|
|
@ -3855,23 +3886,62 @@ function wireGroupDrawer() {
|
||||||
}, true); // capture phase so it fires before bar interaction
|
}, true); // capture phase so it fires before bar interaction
|
||||||
}
|
}
|
||||||
|
|
||||||
function showGroupDrawer(region) {
|
/**
|
||||||
// Group drawer is C15-specific: indexes into the hardcoded SYNTH_SECTIONS
|
* Uniform view of a "section" (aka param group) for the active engine.
|
||||||
// table and groupOverrides, which only exist for the shaper-feedback engine.
|
* C15 gets the static SYNTH_SECTIONS + groupOverrides + SYNTH_PARAM_MAP view;
|
||||||
// Other engines (additive, fm, modular) have their own param UIs.
|
* non-C15 gets a view derived from paramMeta + engineParamOverrides +
|
||||||
if (activeEngine?.id !== 'shaper-feedback') return;
|
* nonC15Sections/nonC15GroupCurves.
|
||||||
activeDrawerSection = region.index;
|
*
|
||||||
const sec = SYNTH_SECTIONS[region.index];
|
* Returns { name, color, count, startIndex, getCurve, setCurve,
|
||||||
const ov = groupOverrides[region.index];
|
* getParamName, getParamOverride } or null if the section doesn't
|
||||||
|
* exist for the active engine.
|
||||||
|
*/
|
||||||
|
function getSectionView(sectionIndex) {
|
||||||
|
if (activeEngine?.id === 'shaper-feedback') {
|
||||||
|
const sec = SYNTH_SECTIONS[sectionIndex];
|
||||||
|
const ov = groupOverrides[sectionIndex];
|
||||||
|
if (!sec || !ov) return null;
|
||||||
|
let start = 0;
|
||||||
|
for (let i = 0; i < sectionIndex; i++) start += SYNTH_SECTIONS[i].count;
|
||||||
|
return {
|
||||||
|
name: sec.name,
|
||||||
|
color: sec.color,
|
||||||
|
count: sec.count,
|
||||||
|
startIndex: start,
|
||||||
|
getCurve: () => ov.curve,
|
||||||
|
setCurve: (v) => { ov.curve = v; },
|
||||||
|
getParamName: (li) => SYNTH_PARAM_MAP[start + li]?.label ?? `p${start + li}`,
|
||||||
|
getParamOverride: (li) => ov.params[li],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Non-C15 engines
|
||||||
|
const sec = nonC15Sections[sectionIndex];
|
||||||
|
if (!sec || !engineParamOverrides) return null;
|
||||||
|
const start = sec.startIndex;
|
||||||
|
return {
|
||||||
|
name: sec.name,
|
||||||
|
color: sec.color,
|
||||||
|
count: sec.count,
|
||||||
|
startIndex: start,
|
||||||
|
getCurve: () => nonC15GroupCurves[sectionIndex] ?? 0.5,
|
||||||
|
setCurve: (v) => {
|
||||||
|
nonC15GroupCurves[sectionIndex] = v;
|
||||||
|
_nonC15GroupCurveMemory.set(sec.name, v);
|
||||||
|
},
|
||||||
|
getParamName: (li) => activeEngine?.paramMeta?.[start + li]?.name ?? `p${start + li}`,
|
||||||
|
getParamOverride: (li) => engineParamOverrides[start + li],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Find global param start index for this section
|
function showGroupDrawer(region) {
|
||||||
let paramStart = 0;
|
const view = getSectionView(region.index);
|
||||||
for (let i = 0; i < region.index; i++) paramStart += SYNTH_SECTIONS[i].count;
|
if (!view) return;
|
||||||
|
activeDrawerSection = region.index;
|
||||||
|
|
||||||
// Header with section name
|
// Header with section name
|
||||||
const header = $groupDrawer.querySelector('.group-drawer-header');
|
const header = $groupDrawer.querySelector('.group-drawer-header');
|
||||||
header.textContent = sec.name;
|
header.textContent = view.name;
|
||||||
header.style.color = sec.color;
|
header.style.color = view.color;
|
||||||
|
|
||||||
// Body: group curve + per-param rows
|
// Body: group curve + per-param rows
|
||||||
const body = $groupDrawer.querySelector('.group-drawer-body');
|
const body = $groupDrawer.querySelector('.group-drawer-body');
|
||||||
|
|
@ -3892,10 +3962,10 @@ function showGroupDrawer(region) {
|
||||||
|
|
||||||
const curveVal = document.createElement('span');
|
const curveVal = document.createElement('span');
|
||||||
curveVal.className = 'gd-val';
|
curveVal.className = 'gd-val';
|
||||||
curveVal.textContent = ov.curve.toFixed(2);
|
curveVal.textContent = view.getCurve().toFixed(2);
|
||||||
|
|
||||||
function drawGroupCurvePreview() {
|
function drawGroupCurvePreview() {
|
||||||
_drawCurveOnCanvas(curveCanvas, ov.curve, sec.color);
|
_drawCurveOnCanvas(curveCanvas, view.getCurve(), view.color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical drag on group curve — applies relative delta to all param curves
|
// Vertical drag on group curve — applies relative delta to all param curves
|
||||||
|
|
@ -3907,8 +3977,11 @@ function showGroupDrawer(region) {
|
||||||
e.preventDefault(); e.stopPropagation();
|
e.preventDefault(); e.stopPropagation();
|
||||||
dragging = true;
|
dragging = true;
|
||||||
startY = e.clientY;
|
startY = e.clientY;
|
||||||
startGroupCurve = ov.curve;
|
startGroupCurve = view.getCurve();
|
||||||
startParamCurves = ov.params.map(p => p.curve);
|
startParamCurves = [];
|
||||||
|
for (let i = 0; i < view.count; i++) {
|
||||||
|
startParamCurves.push(view.getParamOverride(i)?.curve ?? 0.5);
|
||||||
|
}
|
||||||
curveCanvas.setPointerCapture(e.pointerId);
|
curveCanvas.setPointerCapture(e.pointerId);
|
||||||
});
|
});
|
||||||
curveCanvas.addEventListener('pointermove', (e) => {
|
curveCanvas.addEventListener('pointermove', (e) => {
|
||||||
|
|
@ -3917,11 +3990,12 @@ function showGroupDrawer(region) {
|
||||||
const dy = e.clientY - startY;
|
const dy = e.clientY - startY;
|
||||||
const delta = dy / 80;
|
const delta = dy / 80;
|
||||||
const newGroup = Math.max(0, Math.min(1, startGroupCurve + delta));
|
const newGroup = Math.max(0, Math.min(1, startGroupCurve + delta));
|
||||||
ov.curve = newGroup;
|
view.setCurve(newGroup);
|
||||||
curveVal.textContent = newGroup.toFixed(2);
|
curveVal.textContent = newGroup.toFixed(2);
|
||||||
// Apply same delta to each param, preserving relative offsets
|
// Apply same delta to each param, preserving relative offsets
|
||||||
for (let i = 0; i < ov.params.length; i++) {
|
for (let i = 0; i < view.count; i++) {
|
||||||
ov.params[i].curve = Math.max(0, Math.min(1, startParamCurves[i] + delta));
|
const pov = view.getParamOverride(i);
|
||||||
|
if (pov) pov.curve = Math.max(0, Math.min(1, startParamCurves[i] + delta));
|
||||||
}
|
}
|
||||||
drawGroupCurvePreview();
|
drawGroupCurvePreview();
|
||||||
body.querySelectorAll('.gd-param-curve-canvas').forEach(c => {
|
body.querySelectorAll('.gd-param-curve-canvas').forEach(c => {
|
||||||
|
|
@ -3940,11 +4014,10 @@ function showGroupDrawer(region) {
|
||||||
drawGroupCurvePreview();
|
drawGroupCurvePreview();
|
||||||
|
|
||||||
// -- Per-param rows --
|
// -- Per-param rows --
|
||||||
for (let li = 0; li < sec.count; li++) {
|
for (let li = 0; li < view.count; li++) {
|
||||||
const pi = paramStart + li;
|
const pov = view.getParamOverride(li);
|
||||||
if (pi >= SYNTH_PARAM_MAP.length) break;
|
if (!pov) continue;
|
||||||
const param = SYNTH_PARAM_MAP[pi];
|
const paramName = view.getParamName(li);
|
||||||
const pov = ov.params[li];
|
|
||||||
|
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'gd-param-row';
|
row.className = 'gd-param-row';
|
||||||
|
|
@ -3953,14 +4026,14 @@ function showGroupDrawer(region) {
|
||||||
// Name
|
// Name
|
||||||
const nameSpan = document.createElement('span');
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'gd-param-name';
|
nameSpan.className = 'gd-param-name';
|
||||||
nameSpan.textContent = param.label;
|
nameSpan.textContent = paramName;
|
||||||
|
|
||||||
// Per-param curve canvas (vertically draggable, no slider)
|
// Per-param curve canvas (vertically draggable, no slider)
|
||||||
const pCurveCanvas = document.createElement('canvas');
|
const pCurveCanvas = document.createElement('canvas');
|
||||||
pCurveCanvas.className = 'gd-param-curve-canvas';
|
pCurveCanvas.className = 'gd-param-curve-canvas';
|
||||||
pCurveCanvas.width = 28;
|
pCurveCanvas.width = 28;
|
||||||
pCurveCanvas.height = 28;
|
pCurveCanvas.height = 28;
|
||||||
pCurveCanvas._redraw = () => _drawCurveOnCanvas(pCurveCanvas, pov.curve, sec.color);
|
pCurveCanvas._redraw = () => _drawCurveOnCanvas(pCurveCanvas, pov.curve, view.color);
|
||||||
|
|
||||||
_wireCurveDrag(pCurveCanvas, () => pov.curve, (v) => {
|
_wireCurveDrag(pCurveCanvas, () => pov.curve, (v) => {
|
||||||
pov.curve = v;
|
pov.curve = v;
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,13 @@ export class ModularEngine extends SynthEngine {
|
||||||
// Exposure toggles (Phase C hooks).
|
// Exposure toggles (Phase C hooks).
|
||||||
this._exposedEngineParams = new Set(); // label strings
|
this._exposedEngineParams = new Set(); // label strings
|
||||||
|
|
||||||
|
// Matrix cell exposure: keys are "sXX_dYY" strings. Default-empty —
|
||||||
|
// matrix routing is a "patch" setting (user-configured once) rather
|
||||||
|
// than a "performance" surface (wiggled by the MLP). Users can opt in
|
||||||
|
// individual cells via the modular UI; each opt-in triggers a
|
||||||
|
// paramMeta:change event and an MLP resize.
|
||||||
|
this._exposedMatrixCells = new Set();
|
||||||
|
|
||||||
// Most-recent raw value written to each DSP label (by _setRawByLabel
|
// Most-recent raw value written to each DSP label (by _setRawByLabel
|
||||||
// or setParam). Used by getState() to snapshot current DSP values
|
// or setParam). Used by getState() to snapshot current DSP values
|
||||||
// without round-tripping through the worklet. Cleared on sub-engine
|
// without round-tripping through the worklet. Cleared on sub-engine
|
||||||
|
|
@ -454,6 +461,7 @@ export class ModularEngine extends SynthEngine {
|
||||||
this._subCfg = SUB_ENGINES[id];
|
this._subCfg = SUB_ENGINES[id];
|
||||||
this._faustJson = null;
|
this._faustJson = null;
|
||||||
this._exposedEngineParams.clear();
|
this._exposedEngineParams.clear();
|
||||||
|
this._exposedMatrixCells.clear();
|
||||||
this._lastRawByLabel.clear();
|
this._lastRawByLabel.clear();
|
||||||
|
|
||||||
await this._loadSubEngineJson(id);
|
await this._loadSubEngineJson(id);
|
||||||
|
|
@ -487,6 +495,32 @@ export class ModularEngine extends SynthEngine {
|
||||||
this._emit('paramMeta:change', { engine: this });
|
this._emit('paramMeta:change', { engine: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opt a single matrix cell into (or out of) the MLP-driven paramMeta.
|
||||||
|
* When `exposed` is true, the cell joins the MLP output vector and its
|
||||||
|
* value is driven by inference every tick. When false, it stays at
|
||||||
|
* whatever raw value the worklet currently holds (e.g. from the default
|
||||||
|
* patch or the user's direct edits).
|
||||||
|
*
|
||||||
|
* @param {number} s 0..47 source index
|
||||||
|
* @param {number} d 0..9 destination index
|
||||||
|
* @param {boolean} exposed
|
||||||
|
*/
|
||||||
|
setExposeMatrixCell(s, d, exposed) {
|
||||||
|
const key = `s${String(s).padStart(2, '0')}_d${String(d).padStart(2, '0')}`;
|
||||||
|
const had = this._exposedMatrixCells.has(key);
|
||||||
|
if (exposed && !had) this._exposedMatrixCells.add(key);
|
||||||
|
else if (!exposed && had) this._exposedMatrixCells.delete(key);
|
||||||
|
else return;
|
||||||
|
this._rebuildParamMeta();
|
||||||
|
this._emit('paramMeta:change', { engine: this });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Snapshot of currently-exposed matrix cell keys (e.g. "s00_d08"). */
|
||||||
|
getExposedMatrixCells() {
|
||||||
|
return [...this._exposedMatrixCells];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change how many ADSR / LFO slots appear in paramMeta. Rebuilds paramMeta
|
* Change how many ADSR / LFO slots appear in paramMeta. Rebuilds paramMeta
|
||||||
* and emits 'paramMeta:change' so a-app.js resizes the MLP. Slots past the
|
* and emits 'paramMeta:change' so a-app.js resizes the MLP. Slots past the
|
||||||
|
|
@ -636,11 +670,16 @@ export class ModularEngine extends SynthEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- 2. Matrix cells — dest-major, source-major within each destination -----
|
// ----- 2. Matrix cells — opt-in only (empty by default) -----
|
||||||
|
// Stored as "sXX_dYY" keys. The paramMeta order follows insertion
|
||||||
|
// order, grouped by destination for locality when scanning.
|
||||||
|
if (this._exposedMatrixCells.size > 0) {
|
||||||
for (let d = 0; d < cfg.destCount; d++) {
|
for (let d = 0; d < cfg.destCount; d++) {
|
||||||
const destName = cfg.destNames[d];
|
const destName = cfg.destNames[d];
|
||||||
for (let s = 0; s < 48; s++) {
|
for (let s = 0; s < 48; s++) {
|
||||||
const label = `MM_Matrix/s${String(s).padStart(2, '0')}_d${String(d).padStart(2, '0')}_${destName}`;
|
const cellKey = `s${String(s).padStart(2, '0')}_d${String(d).padStart(2, '0')}`;
|
||||||
|
if (!this._exposedMatrixCells.has(cellKey)) continue;
|
||||||
|
const label = `MM_Matrix/${cellKey}_${destName}`;
|
||||||
const e = this._labelToWalk.get(label);
|
const e = this._labelToWalk.get(label);
|
||||||
if (!e) continue;
|
if (!e) continue;
|
||||||
meta.push(this._makeMetaEntry(e, {
|
meta.push(this._makeMetaEntry(e, {
|
||||||
|
|
@ -650,6 +689,7 @@ export class ModularEngine extends SynthEngine {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----- 3. Opted-in engine sound params -----
|
// ----- 3. Opted-in engine sound params -----
|
||||||
for (const label of this._exposedEngineParams) {
|
for (const label of this._exposedEngineParams) {
|
||||||
|
|
|
||||||
|
|
@ -282,16 +282,17 @@ export function initModularUI({ getEngine, onStateChange } = {}) {
|
||||||
if (!persistToEngine) return;
|
if (!persistToEngine) return;
|
||||||
const engine = getEngine?.();
|
const engine = getEngine?.();
|
||||||
if (!engine || engine.id !== 'modular') return;
|
if (!engine || engine.id !== 'modular') return;
|
||||||
const idx = matrixIndexCache.get(`${s}|${d}`);
|
|
||||||
if (idx == null) return; // not in paramMeta (s out of adsr/lfo counts maybe)
|
// Matrix cells are direct DSP knobs by default — write straight to
|
||||||
const meta = engine.paramMeta[idx];
|
// the worklet by Faust label. (If a cell has been opt'd into the MLP
|
||||||
if (!meta) return;
|
// output vector via setExposeMatrixCell, the MLP will overwrite it on
|
||||||
const range = (meta.max - meta.min) || 1;
|
// the next inference tick; that's fine, this write still feeds the
|
||||||
// v is the raw value in the paramMeta [min, max] space for matrix cells
|
// worklet immediately for tactile feedback.)
|
||||||
// because we set min=-1, max=1 in the DSP — but meta.min/max are raw,
|
const destNames = engine.destNames || [];
|
||||||
// so normalise generically.
|
const destName = destNames[d];
|
||||||
const norm = (v - meta.min) / range;
|
if (!destName) return;
|
||||||
engine.setParam(idx, Math.max(0, Math.min(1, norm)));
|
const label = `MM_Matrix/s${String(s).padStart(2, '0')}_d${String(d).padStart(2, '0')}_${destName}`;
|
||||||
|
engine._setRawByLabel?.(label, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleCell(s, d) {
|
function cycleCell(s, d) {
|
||||||
|
|
@ -336,20 +337,28 @@ export function initModularUI({ getEngine, onStateChange } = {}) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Matrix cells may or may not be in paramMeta (they're opt-in for MLP
|
||||||
|
// control; default is that the matrix is a direct-DSP patch editor).
|
||||||
|
// Build the paramMeta index for cells that ARE exposed, so updateLive
|
||||||
|
// can mirror MLP outputs into them; other cells are direct-edit only.
|
||||||
matrixIndexCache = buildMatrixIndex(engine);
|
matrixIndexCache = buildMatrixIndex(engine);
|
||||||
if (matrixIndexCache.size === 0) {
|
|
||||||
if (refs.matrixEmpty) refs.matrixEmpty.textContent = 'No matrix cells in paramMeta.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (refs.matrixEmpty) refs.matrixEmpty.textContent = '';
|
if (refs.matrixEmpty) refs.matrixEmpty.textContent = '';
|
||||||
|
|
||||||
// Seed current values from paramMeta init (normalized 0..1).
|
// Seed current values from the engine's _lastRawByLabel map (which
|
||||||
const meta = engine.paramMeta;
|
// tracks every write via setParam / _setRawByLabel / default patch),
|
||||||
for (const [key, idx] of matrixIndexCache.entries()) {
|
// falling back to the walk-entry init value for cells the user has
|
||||||
const m = meta[idx];
|
// never touched.
|
||||||
const range = (m.max - m.min) || 1;
|
const destNames = engine.destNames || [];
|
||||||
const raw = m.min + m.init * range;
|
const lastRaw = engine._lastRawByLabel || new Map();
|
||||||
cellValues.set(key, raw);
|
for (let d = 0; d < destNames.length; d++) {
|
||||||
|
const destName = destNames[d];
|
||||||
|
for (let s = 0; s < 48; s++) {
|
||||||
|
const label = `MM_Matrix/s${String(s).padStart(2, '0')}_d${String(d).padStart(2, '0')}_${destName}`;
|
||||||
|
const walk = engine._labelToWalk?.get?.(label);
|
||||||
|
if (!walk) continue;
|
||||||
|
const raw = lastRaw.has(label) ? lastRaw.get(label) : walk.init;
|
||||||
|
cellValues.set(`${s}|${d}`, raw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine visible source range from current counts.
|
// Determine visible source range from current counts.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue