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._bypassOut = null; // GainNode: output exit point
|
||||||
this._bypassDry = null; // GainNode: direct input→output path when bypassed
|
this._bypassDry = null; // GainNode: direct input→output path when bypassed
|
||||||
this._initialized = false;
|
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]
|
* @param {number} normalizedValue — [0, 1]
|
||||||
*/
|
*/
|
||||||
setParam(index, normalizedValue) { // eslint-disable-line no-unused-vars
|
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 ---
|
// --- Audio graph ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the master gain node. Connect this to a compressor or destination.
|
* Return the limiter node — the last AudioNode before destination.
|
||||||
* Only available after init() completes.
|
* 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() {
|
getOutputNode() {
|
||||||
return this._bridge.masterGain;
|
return this._bridge.limiterNode ?? this._bridge.masterGain;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- C15-specific passthrough (for MIDIInput and volume controls) ---
|
// --- C15-specific passthrough (for MIDIInput and volume controls) ---
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,14 @@ export class C15Bridge {
|
||||||
/** SharedArrayBuffer for the ring buffer — available after start() */
|
/** SharedArrayBuffer for the ring buffer — available after start() */
|
||||||
get sharedBuffer() { return this._sab; }
|
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) {
|
_status(msg) {
|
||||||
console.log('[C15]', msg);
|
console.log('[C15]', msg);
|
||||||
this._onStatusChange?.(msg);
|
this._onStatusChange?.(msg);
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,12 @@ export const EOCChainUI = {
|
||||||
});
|
});
|
||||||
row.appendChild(removeBtn);
|
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 ---
|
// --- HTML5 Drag-and-drop ---
|
||||||
row.addEventListener('dragstart', (e) => {
|
row.addEventListener('dragstart', (e) => {
|
||||||
this._dragId = mod.id;
|
this._dragId = mod.id;
|
||||||
|
|
@ -283,6 +289,59 @@ export const EOCChainUI = {
|
||||||
return row;
|
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
|
// Add-module bar
|
||||||
_buildAddBar() {
|
_buildAddBar() {
|
||||||
const bar = document.createElement('div');
|
const bar = document.createElement('div');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue