diff --git a/README.md b/README.md index f997f83..0f31a6c 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Each instrument gets its own wavelength colour and a **live waveform of its own - **Live layer cards** (`d1`–`d16`) — each shows its code, a plain-English explanation, per-layer knobs, and a **live waveform of that channel's own audio** - **Per-channel oscilloscope** — every card draws a real, phase-locked waveform tapped from its own voice; plus a master L/R meter and a **wavelength-coloured spectrum** (low freq red → high freq blue) +- **Mixer view** with all 12 channels as strips: a volume fader, pan, reverb + delay sends, mute/solo, a meter, and a live mini-scope each - **Step sequencer** with **per-step velocity** (scroll a pad), **drag-to-paint** + right-click erase, swing, and a playhead **locked to the audio** so each lit step flashes exactly when you hear it - **Pattern slots (A/B/C/D)** you can chain into a song that advances each bar - **Drag-and-drop sample browser** — drag a sound onto a channel to swap it; drag channels to reorder diff --git a/mcp/dashboard-mixer.d.ts b/mcp/dashboard-mixer.d.ts new file mode 100644 index 0000000..9c5d9db --- /dev/null +++ b/mcp/dashboard-mixer.d.ts @@ -0,0 +1,17 @@ +// Ambient types for dashboard-mixer.js (window.AbxMixer), the mixer module. The browser loads it +// as a classic + diff --git a/mcp/dashboard.js b/mcp/dashboard.js index 9ca67cc..6af8b1f 100644 --- a/mcp/dashboard.js +++ b/mcp/dashboard.js @@ -194,6 +194,7 @@ if(el.type==="range") lastInput=Date.now(); // suppress card re-render while dragging any slider if(window.AbxSeq && AbxSeq.handleInput(e)) return; // seqname / seqvol / seqSwing if(el.type!=="range")return; + if(window.AbxMixer && AbxMixer.handleInput(e)) return; // mixer fader / pan / FX1 / FX2 (range + data-slot, no data-param) if(el.dataset.tempo!==undefined){ document.getElementById("bpm").textContent=el.value; pendingTempo=+el.value; return; } if(el.dataset.slot){ var vb=document.getElementById("v-"+el.dataset.slot+"-"+el.dataset.param); if(vb)vb.textContent=el.value; pending[el.dataset.slot+"|"+el.dataset.param]={slot:el.dataset.slot,param:el.dataset.param,value:+el.value}; } @@ -210,6 +211,7 @@ if(!b)return; if(window.AbxSeq && AbxSeq.handleClick(b)) return; // steps / seqadd / seqclear / row mute+clear / pattern / song if(window.AbxCurves && AbxCurves.handleClick(b)) return; // curves panel / curve toggle+preset + if(window.AbxMixer && AbxMixer.handleClick(b)) return; // mixer drawer toggle (strip M/S use data-cmd, shared with cards) if(b.dataset.act==="run"){ runCode(); return; } if(b.dataset.act==="boot"){ var o=document.getElementById("consoleOut"); o.textContent="booting the sound engine… (~30-40s) — watch the status light top-left"; o.className=""; send({cmd:"boot"}).then(function(){poll();}); return; } if(b.dataset.act==="reset"){ if(confirm("Reboot the engine? Wipes everything, fresh start (~30s).")) cmd("reset"); return; } @@ -318,6 +320,7 @@ } grid.innerHTML=h; if(window.AbxCurves) AbxCurves.maybeRerender(keys); + if(window.AbxMixer) AbxMixer.maybeRerender(); } function poll(){ fetch("/state").then(function(r){return r.json();}).then(render).catch(function(){ document.getElementById("engstate").textContent="disconnected"; document.getElementById("engdot").className="dot error"; }); } diff --git a/mcp/run-tests.mjs b/mcp/run-tests.mjs index 96e29a2..a33e80e 100644 --- a/mcp/run-tests.mjs +++ b/mcp/run-tests.mjs @@ -21,7 +21,7 @@ import { dirname, join } from "node:path"; import { run } from "node:test"; import { spec } from "node:test/reporters"; -const FLOOR = 19; // current test count on this branch (track 7 + dsp 12) +const FLOOR = 31; // current test count: track 7 + dsp 12 + mixer 12 (raised with the mixer tests) const distDir = join(dirname(fileURLToPath(import.meta.url)), "dist"); let files = []; diff --git a/mcp/src/mixer.test.ts b/mcp/src/mixer.test.ts new file mode 100644 index 0000000..ab4294f --- /dev/null +++ b/mcp/src/mixer.test.ts @@ -0,0 +1,79 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +// Single source: the browser loads dashboard-mixer.js (window.AbxMixer) as a classic