Merge branch 'worktree-agent-ae813a27'

This commit is contained in:
w1n5t0n 2026-04-03 17:57:05 +01:00
commit 6007ab3853
3 changed files with 77 additions and 13 deletions

View file

@ -18,6 +18,8 @@ import { SYNTH_PRESETS, PRESET_TIERS } from './synth/presets.js';
import { EOCChain } from './eoc/index.js';
import { EOCChainUI, moduleFactory } from './ui/eoc-chain-ui.js';
import { EngineSwitcher } from './ui/engine-switcher.js';
import { AdditiveEngine } from './synth/additive-engine.js';
import { FMEngine } from './synth/fm-engine.js';
// ---- Constants ----
const N_JOY_INPUTS = 2;
@ -761,7 +763,8 @@ function outputCountForMode(mode) {
*/
async function _startEocChain() {
if (_eocInited || !eocChain) return;
const audioCtx = activeEngine._bridge?.audioContext;
// C15Adapter exposes _bridge.audioContext; Faust engines expose _audioCtx
const audioCtx = activeEngine?._bridge?.audioContext ?? activeEngine?._audioCtx;
if (!audioCtx) {
console.warn('[EOC] _startEocChain: no AudioContext yet — skipping');
return;
@ -809,7 +812,11 @@ async function setActiveEngine(engine) {
const outputNode = engine.getOutputNode();
if (outputNode) {
try { outputNode.disconnect(); } catch (_) { /* not yet connected */ }
eocChain.connect(outputNode, engine._bridge.audioContext.destination);
// C15Adapter exposes _bridge.audioContext; Faust engines expose _audioCtx
const rewireCtx = engine._bridge?.audioContext ?? engine._audioCtx;
if (rewireCtx) {
eocChain.connect(outputNode, rewireCtx.destination);
}
}
}
}
@ -933,24 +940,54 @@ async function init() {
displayName: 'Additive',
paramCount: 48,
description: 'Spectral envelope additive synthesis. 64 harmonics shaped by ML.',
comingSoon: true,
},
{
id: 'fm',
displayName: 'FM Matrix',
paramCount: 55,
description: '4-operator FM with continuous routing matrix. Algorithm emerges from exploration.',
comingSoon: true,
},
];
const engineSwitcherEl = document.getElementById('engine-params');
if (engineSwitcherEl) {
EngineSwitcher.init(engineSwitcherEl, ENGINES, async (engineId) => {
if (engineId === activeEngine.id) return;
if (engineId !== 'shaper-feedback') {
showToast(`${engineId} engine coming soon`);
if (engineId === activeEngine?.id) return;
EngineSwitcher.setLoading(engineId, true);
try {
let newEngine;
if (engineId === 'shaper-feedback') {
// Switch back to C15 — reuse the existing instance
newEngine = c15 instanceof C15Adapter ? c15 : new C15Adapter();
} else if (engineId === 'additive') {
newEngine = new AdditiveEngine();
} else if (engineId === 'fm') {
newEngine = new FMEngine();
} else {
showToast(`Unknown engine: ${engineId}`);
return;
}
if (newEngine !== activeEngine) {
// Stop arpeggiator before switching
if (arpeggiator) arpeggiator.stop();
// If audio is already running, init the new engine now
const audioCtxForInit = activeEngine?._audioCtx
?? activeEngine?._bridge?.audioContext
?? null;
if (audioCtxForInit && !newEngine._running) {
await newEngine.init(audioCtxForInit);
}
await setActiveEngine(newEngine);
}
} catch (err) {
console.error('[EngineSwitcher] Failed to switch engine:', err);
showToast(`Failed to load ${engineId}: ${err.message}`);
} finally {
EngineSwitcher.setLoading(engineId, false);
}
}, {
hasTrainingData: () => (imlJoy?.exampleCount ?? 0) > 0 || (imlHand?.exampleCount ?? 0) > 0,
});
@ -1763,7 +1800,7 @@ function routeOutputs(outputs) {
const now = performance.now();
if (now - _lastParamSendTime >= PARAM_SEND_INTERVAL) {
_lastParamSendTime = now;
for (let i = 0; i < overridden.length && i < SYNTH_PARAM_MAP.length; i++) {
for (let i = 0; i < overridden.length && i < N_OUTPUTS; i++) {
const v = overridden[i];
if (Math.abs(v - _lastSentParams[i]) > PARAM_DEAD_ZONE) {
activeEngine.setParam(i, v);
@ -3481,10 +3518,9 @@ async function loadState() {
// Note: don't auto-restore inputMode='hands' — requires camera permission
// Restore engine selection (currently always 'shaper-feedback'; pattern ready for future engines)
if (typeof state.engineId === 'string' && state.engineId !== (activeEngine ? activeEngine.id : null)) {
// Future: if (ENGINES.find(e => e.id === state.engineId && !e.comingSoon)) setActiveEngine(...)
// For now, sync the switcher highlight to whatever is active
// Restore engine selection — sync switcher highlight; deferred engine init
// happens on first audio-start click (Faust engines require a running AudioContext).
if (typeof state.engineId === 'string') {
EngineSwitcher.setActive(activeEngine ? activeEngine.id : 'shaper-feedback');
}

View file

@ -0,0 +1,14 @@
import { FaustEngineBase } from './faust-engine-base.js';
export class AdditiveEngine extends FaustEngineBase {
constructor() {
super({
id: 'additive',
displayName: 'Additive',
wasmUrl: 'faust/additive.wasm',
jsonUrl: 'faust/additive.json',
workletUrl: 'faust/additive-processor.js',
processorName: 'additive-processor',
});
}
}

View file

@ -0,0 +1,14 @@
import { FaustEngineBase } from './faust-engine-base.js';
export class FMEngine extends FaustEngineBase {
constructor() {
super({
id: 'fm',
displayName: 'FM Matrix',
wasmUrl: 'faust/fm-matrix.wasm',
jsonUrl: 'faust/fm-matrix.json',
workletUrl: 'faust/fm-matrix-processor.js',
processorName: 'fm-matrix-processor',
});
}
}