feat(playground): EOC chain wired into audio graph, bypass mode manual sliders (meml-xp3)
- Wire EOCChain into audio graph after c15.start() (both start-btn and quick-play paths); guarded by _eocInited flag so init happens once per AudioContext lifetime - getOutputNode() on C15Adapter now returns limiter (last node before destination) so EOC inserts correctly between limiter and destination - Expose limiterNode getter on C15Bridge - Add getCurrentParamValue(i) to EOCModule base class, backed by _paramValues array that setParam() writes to - Add _buildParamSliders() to EOCChainUI: collapsible ▶ params toggle per module row, one range slider per paramMeta entry, calls module.setParam() on input; labelled "manual" when nispsMode=bypass - eoc:change listener in a-app.js logs paramCount (hook point for future modes) - saveState()/loadState() persist eocModules (id, enabled, params) and eocNispsMode; restore re-adds modules via moduleFactory and re-applies values - CSS: eoc-params-section, eoc-params-toggle, eoc-param-row/label/slider; eoc-module-row gains flex-wrap to accommodate params section below controls
This commit is contained in:
parent
f1b6e1aba0
commit
25caf726d8
4 changed files with 93 additions and 5 deletions
|
|
@ -33,6 +33,8 @@ export class EOCModule {
|
|||
this._bypassOut = null; // GainNode: output exit point
|
||||
this._bypassDry = null; // GainNode: direct input→output path when bypassed
|
||||
this._initialized = false;
|
||||
// Stored normalized param values — set via setParam(), read by getCurrentParamValue()
|
||||
this._paramValues = [];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -184,7 +186,24 @@ export class EOCModule {
|
|||
* @param {number} normalizedValue — [0, 1]
|
||||
*/
|
||||
setParam(index, normalizedValue) { // eslint-disable-line no-unused-vars
|
||||
// default no-op — override in subclass
|
||||
// Store the value so getCurrentParamValue() can read it back
|
||||
this._paramValues[index] = normalizedValue;
|
||||
// default no-op — override in subclass for actual audio effect
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last normalized value set for a parameter.
|
||||
* Returns the param's init value (from paramMeta) if never explicitly set.
|
||||
*
|
||||
* @param {number} index — 0-based index into paramMeta
|
||||
* @returns {number} normalized value [0, 1]
|
||||
*/
|
||||
getCurrentParamValue(index) {
|
||||
if (this._paramValues[index] !== undefined) {
|
||||
return this._paramValues[index];
|
||||
}
|
||||
const meta = this.paramMeta[index];
|
||||
return meta ? (meta.init ?? 0) : 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -144,13 +144,15 @@ export class C15Adapter extends SynthEngine {
|
|||
// --- Audio graph ---
|
||||
|
||||
/**
|
||||
* Return the master gain node. Connect this to a compressor or destination.
|
||||
* Only available after init() completes.
|
||||
* Return the limiter node — the last AudioNode before destination.
|
||||
* This is the correct insertion point for post-processing (e.g. EOCChain).
|
||||
* Falls back to masterGain before start() is called.
|
||||
* Only useful after init() completes.
|
||||
*
|
||||
* @returns {GainNode}
|
||||
* @returns {AudioNode}
|
||||
*/
|
||||
getOutputNode() {
|
||||
return this._bridge.masterGain;
|
||||
return this._bridge.limiterNode ?? this._bridge.masterGain;
|
||||
}
|
||||
|
||||
// --- C15-specific passthrough (for MIDIInput and volume controls) ---
|
||||
|
|
|
|||
|
|
@ -80,6 +80,14 @@ export class C15Bridge {
|
|||
/** SharedArrayBuffer for the ring buffer — available after start() */
|
||||
get sharedBuffer() { return this._sab; }
|
||||
|
||||
/**
|
||||
* The limiter node — the last AudioNode before destination.
|
||||
* Use this as the input to any post-processing chain (e.g. EOCChain).
|
||||
* Only available after start().
|
||||
* @returns {DynamicsCompressorNode|null}
|
||||
*/
|
||||
get limiterNode() { return this.limiter ?? null; }
|
||||
|
||||
_status(msg) {
|
||||
console.log('[C15]', msg);
|
||||
this._onStatusChange?.(msg);
|
||||
|
|
|
|||
|
|
@ -224,6 +224,12 @@ export const EOCChainUI = {
|
|||
});
|
||||
row.appendChild(removeBtn);
|
||||
|
||||
// --- Manual param sliders (shown in bypass mode, or when module has params) ---
|
||||
if (mod.paramCount > 0) {
|
||||
const paramsSection = this._buildParamSliders(mod);
|
||||
row.appendChild(paramsSection);
|
||||
}
|
||||
|
||||
// --- HTML5 Drag-and-drop ---
|
||||
row.addEventListener('dragstart', (e) => {
|
||||
this._dragId = mod.id;
|
||||
|
|
@ -283,6 +289,59 @@ export const EOCChainUI = {
|
|||
return row;
|
||||
},
|
||||
|
||||
// Per-module param sliders (collapsible, shown only when paramCount > 0)
|
||||
_buildParamSliders(mod) {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'eoc-params-section';
|
||||
|
||||
// Toggle button (chevron + label)
|
||||
const toggleBtn = document.createElement('button');
|
||||
toggleBtn.className = 'eoc-params-toggle';
|
||||
const isManual = this._chain.nispsMode === 'bypass';
|
||||
toggleBtn.textContent = isManual ? '▶ params (manual)' : '▶ params';
|
||||
toggleBtn.title = 'Expand to show parameter sliders';
|
||||
|
||||
const sliderList = document.createElement('div');
|
||||
sliderList.className = 'eoc-params-list eoc-params-collapsed';
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const collapsed = sliderList.classList.toggle('eoc-params-collapsed');
|
||||
toggleBtn.textContent = collapsed
|
||||
? (isManual ? '▶ params (manual)' : '▶ params')
|
||||
: (isManual ? '▼ params (manual)' : '▼ params');
|
||||
});
|
||||
|
||||
// One slider per param
|
||||
mod.paramMeta.forEach((meta, i) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'eoc-param-row';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = 'eoc-param-label';
|
||||
label.textContent = meta.name ?? meta.id ?? `Param ${i}`;
|
||||
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
slider.min = '0';
|
||||
slider.max = '1';
|
||||
slider.step = '0.001';
|
||||
slider.value = String(mod.getCurrentParamValue(i));
|
||||
slider.className = 'eoc-param-slider';
|
||||
|
||||
slider.addEventListener('input', () => {
|
||||
mod.setParam(i, parseFloat(slider.value));
|
||||
});
|
||||
|
||||
row.appendChild(label);
|
||||
row.appendChild(slider);
|
||||
sliderList.appendChild(row);
|
||||
});
|
||||
|
||||
section.appendChild(toggleBtn);
|
||||
section.appendChild(sliderList);
|
||||
return section;
|
||||
},
|
||||
|
||||
// Add-module bar
|
||||
_buildAddBar() {
|
||||
const bar = document.createElement('div');
|
||||
|
|
|
|||
Loading…
Reference in a new issue