Sequencer
ox alpha · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 22,050 bytes · SHA-256 88ac58996fa6
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OX-909 Step Sequencer</title>
<style>
:root{
--bg:#0b0d10; --panel:#14181d; --panel2:#1a1f26; --line:#262c34;
--led:#ff3b2f; --amber:#ffb02e;
--txt:#cfd6de; --dim:#6b7480;
--kick:#ff5a4e; --snare:#ffb02e; --chat:#ffd23f; --ohat:#7ee081; --clap:#5ec8ff; --tom:#c07eff;
}
*{box-sizing:border-box; margin:0; padding:0; user-select:none;}
body{background:var(--bg); color:var(--txt); font-family:"Segoe UI",system-ui,sans-serif;
min-height:100vh; display:flex; align-items:center; justify-content:center; padding:16px;}
.machine{background:var(--panel); border:1px solid var(--line); border-radius:14px;
box-shadow:0 20px 60px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.04);
padding:18px 20px 22px; max-width:1060px; width:100%;}
.brand{display:flex; align-items:baseline; gap:12px; margin-bottom:14px;}
.brand h1{font-size:20px; letter-spacing:4px; color:#e8edf2;}
.brand .sub{font-size:11px; color:var(--dim); letter-spacing:2px;}
#powerLed{width:10px;height:10px;border-radius:50%;background:#3a2020;margin-left:auto;align-self:center;}
#powerLed.on{background:var(--led); box-shadow:0 0 8px var(--led),0 0 16px rgba(255,59,47,.5);}
/* ---- transport ---- */
.transport{display:flex; flex-wrap:wrap; align-items:center; gap:18px; background:var(--panel2);
border:1px solid var(--line); border-radius:10px; padding:12px 16px; margin-bottom:14px;}
button{font-family:inherit; cursor:pointer; border:none; border-radius:8px;}
.btn-play{width:64px;height:44px;background:#232a33;color:#9fe8a0;font-size:15px;font-weight:700;
letter-spacing:1px;border:1px solid #303a46;}
.btn-play.playing{color:#ff6b5e;background:#33232a;box-shadow:inset 0 0 12px rgba(255,60,40,.25);}
.btn-sm{background:#232a33;color:var(--txt);border:1px solid #303a46;padding:9px 14px;font-size:12px;letter-spacing:1px;}
.btn-sm:hover{background:#2b333e;}
.ctrl{display:flex;flex-direction:column;gap:4px;}
.ctrl label{font-size:10px;letter-spacing:2px;color:var(--dim);}
.val{font-family:ui-monospace,Consolas,monospace;color:var(--amber);font-size:13px;text-shadow:0 0 6px rgba(255,176,46,.5);}
input[type=range]{-webkit-appearance:none;width:130px;height:4px;background:#2c343f;border-radius:2px;outline:none;}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;
background:#dfe6ee;border:2px solid #10141a;cursor:pointer;}
/* ---- grid ---- */
.grid-wrap{overflow-x:auto;}
.grid{display:grid; grid-template-columns:110px repeat(16,1fr); gap:5px; min-width:820px;}
.rowlabel{display:flex;align-items:center;gap:8px;font-size:12px;letter-spacing:1px;color:var(--txt);
padding-right:6px;}
.rowlabel .dot{width:10px;height:10px;border-radius:3px;flex:none;}
.rowlabel.muted{opacity:.35;}
.corner{font-size:10px;color:var(--dim);letter-spacing:1px;display:flex;align-items:flex-end;padding-bottom:2px;}
.stepnum{text-align:center;font-size:10px;color:var(--dim);font-family:ui-monospace,monospace;padding-bottom:2px;}
.stepnum.beat{color:var(--txt);}
.stepnum.ph{color:var(--amber);text-shadow:0 0 6px rgba(255,176,46,.7);}
.cell{height:42px;border-radius:7px;background:#1d232b;border:1px solid #262d37;cursor:pointer;
position:relative; transition:background .05s;}
.cell.beat{margin-left:2px;}
.cell.on{border-color:transparent;}
.cell::after{content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
width:55%;height:55%;border-radius:5px;background:rgba(255,255,255,.04);}
.cell.acc::after{width:74%;height:74%;}
.cell.ph{box-shadow:0 0 0 2px rgba(255,176,46,.85), 0 0 10px rgba(255,176,46,.45);}
.cell.hit{filter:brightness(1.9);}
/* ---- mixer ---- */
.mixer{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-top:16px;}
.strip{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 8px;
display:flex;flex-direction:column;align-items:center;gap:6px;}
.strip.muted{opacity:.45;}
.strip .name{font-size:11px;letter-spacing:1px;display:flex;align-items:center;gap:6px;}
.strip input[type=range]{width:100%;}
.mbtn{padding:5px 12px;font-size:10px;letter-spacing:1px;background:#232a33;color:var(--txt);border:1px solid #303a46;}
.mbtn.active{background:#3a2020;color:#ff6b5e;border-color:#5a2c28;}
</style>
</head>
<body>
<div class="machine">
<div class="brand"><h1>OX-909</h1><span class="sub">RHYTHM COMPOSER · 16 STEP</span><div id="powerLed"></div></div>
<div class="transport">
<button class="btn-play" id="playBtn">▶ PLAY</button>
<div class="ctrl"><label>TEMPO</label><input type="range" id="bpm" min="60" max="200" value="120"><span class="val" id="bpmVal">120 BPM</span></div>
<div class="ctrl"><label>SWING</label><input type="range" id="swing" min="0" max="60" value="0"><span class="val" id="swingVal">0%</span></div>
<div class="ctrl"><label>MASTER</label><input type="range" id="master" min="0" max="100" value="80"><span class="val" id="masterVal">80%</span></div>
<button class="btn-sm" id="clearBtn">CLEAR</button>
<button class="btn-sm" id="randBtn">RANDOM</button>
</div>
<div class="grid-wrap"><div class="grid" id="grid"></div></div>
<div class="mixer" id="mixer"></div>
</div>
<script>
"use strict";
/* ============================================================
TUNABLE CONSTANTS
============================================================ */
const CFG = {
LOOKAHEAD_MS: 25, // scheduler timer interval
SCHEDULE_AHEAD: 0.12, // seconds scheduled ahead of audio clock
STEPS: 16,
BPM_MIN: 60, BPM_MAX: 200,
// instrument patches
kick: { fStart:150, fEnd:50, pitchDecay:0.055, ampDecay:0.42, gain:1.0 },
snare:{ noiseDecay:0.20, bodyF:180, bodyDecay:0.11, gain:0.75 },
chh: { decay:0.05, gain:0.42 },
ohat: { decay:0.32, gain:0.38 },
clap: { bursts:[0,0.012,0.024,0.038], decay:0.13, bpF:1100, gain:0.65 },
tom: { f:196, decay:0.30, gain:0.7 }, // G3, musical vs kick
};
const TRACKS = [
{id:'kick', name:'KICK', color:'var(--kick)'},
{id:'snare',name:'SNARE', color:'var(--snare)'},
{id:'chh', name:'CH HAT',color:'var(--chat)'},
{id:'ohat', name:'OP HAT',color:'var(--ohat)'},
{id:'clap', name:'CLAP', color:'var(--clap)'},
{id:'tom', name:'TOM', color:'var(--tom)'},
];
// velocities: 0 = off, 1 = normal, 2 = accent
const VEL_GAIN = [0, 0.72, 1.0];
/* ============================================================
STATE + PERSISTENCE
============================================================ */
const LS_KEY = 'ox909_pattern_v1';
let state = {
bpm: 120, swing: 0, master: 0.8,
pattern: {}, // trackId -> array of 16 (0/1/2)
vol: {}, mute: {},
};
TRACKS.forEach(t=>{
state.pattern[t.id] = new Array(16).fill(0);
state.vol[t.id] = 0.8; state.mute[t.id] = false;
});
function saveState(){
try{ localStorage.setItem(LS_KEY, JSON.stringify(state)); }catch(e){}
}
function loadState(){
try{
const raw = localStorage.getItem(LS_KEY);
if(!raw) return false;
const s = JSON.parse(raw);
if(!s || !s.pattern) return false;
state.bpm = s.bpm ?? state.bpm;
state.swing = s.swing ?? 0;
state.master = s.master ?? 0.8;
TRACKS.forEach(t=>{
if(Array.isArray(s.pattern[t.id]) && s.pattern[t.id].length===16)
state.pattern[t.id] = s.pattern[t.id].map(v=>v?1:0);
if(typeof s.vol?.[t.id]==='number') state.vol[t.id]=s.vol[t.id];
state.mute[t.id] = !!s.mute?.[t.id];
});
return true;
}catch(e){ return false; }
}
/* default groove */
function loadDefaultPattern(){
const P = {
kick : [2,0,0,0, 0,0,1,0, 0,0,1,0, 0,0,0,0],
snare: [0,0,0,0, 2,0,0,0, 0,0,0,0, 2,0,1,0],
chh : [1,0,1,0, 1,0,1,2, 1,0,1,0, 1,0,2,0],
ohat : [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,1],
clap : [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,2],
tom : [0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0],
};
Object.assign(state.pattern, JSON.parse(JSON.stringify(P)));
}
/* ============================================================
AUDIO ENGINE
============================================================ */
let actx = null, masterGain = null, noiseBuf = null;
function initAudio(){
if(actx) return;
actx = new (window.AudioContext||window.webkitAudioContext)();
masterGain = actx.createGain();
masterGain.gain.value = state.master;
masterGain.connect(actx.destination);
// 2s white noise buffer
const len = actx.sampleRate * 2;
noiseBuf = actx.createBuffer(1, len, actx.sampleRate);
const d = noiseBuf.getChannelData(0);
for(let i=0;i<len;i++) d[i] = Math.random()*2-1;
}
function trackOut(id, t){
const g = actx.createGain();
g.gain.value = state.mute[id] ? 0 : state.vol[id];
g.connect(masterGain);
return g;
}
function noiseSrc(t){ const s = actx.createBufferSource(); s.buffer = noiseBuf; return s; }
const SYNTH = {
kick(t, vel){
const p = CFG.kick, out = trackOut('kick', t);
const o = actx.createOscillator(), g = actx.createGain();
o.type='sine';
o.frequency.setValueAtTime(p.fStart, t);
o.frequency.exponentialRampToValueAtTime(p.fEnd, t+p.pitchDecay);
g.gain.setValueAtTime(0.0001, t);
g.gain.linearRampToValueAtTime(p.gain*VEL_GAIN[vel], t+0.002);
g.gain.exponentialRampToValueAtTime(0.0001, t+p.ampDecay);
o.connect(g).connect(out);
o.start(t); o.stop(t+p.ampDecay+0.05);
},
snare(t, vel){
const p = CFG.snare, acc = vel===2, out = trackOut('snare', t);
// noise
const n = noiseSrc(), ng = actx.createGain(), hp = actx.createBiquadFilter();
hp.type='highpass'; hp.frequency.value = acc ? 1800 : 1200;
ng.gain.setValueAtTime(0.0001,t);
ng.gain.linearRampToValueAtTime(p.gain*VEL_GAIN[vel], t+0.001);
ng.gain.exponentialRampToValueAtTime(0.0001, t+p.noiseDecay*(acc?1.15:1));
n.connect(hp).connect(ng).connect(out);
n.start(t); n.stop(t+p.noiseDecay+0.05);
// tonal body
const o = actx.createOscillator(), og = actx.createGain();
o.type='triangle'; o.frequency.setValueAtTime(p.bodyF,t);
o.frequency.exponentialRampToValueAtTime(p.bodyF*0.6, t+p.bodyDecay);
og.gain.setValueAtTime(0.0001,t);
og.gain.linearRampToValueAtTime(0.5*VEL_GAIN[vel], t+0.001);
og.gain.exponentialRampToValueAtTime(0.0001, t+p.bodyDecay);
o.connect(og).connect(out);
o.start(t); o.stop(t+p.bodyDecay+0.05);
},
hat(t, vel, open){
const p = open?CFG.ohat:CFG.chh, out = trackOut(open?'ohat':'chh', t);
const n = noiseSrc(), hp = actx.createBiquadFilter(), g = actx.createGain();
hp.type='highpass'; hp.frequency.value = 7500;
const dec = p.decay * (vel===2?1.2:1);
g.gain.setValueAtTime(0.0001,t);
g.gain.linearRampToValueAtTime(p.gain*VEL_GAIN[vel], t+0.001);
g.gain.exponentialRampToValueAtTime(0.0001, t+dec);
n.connect(hp).connect(g).connect(out);
n.start(t); n.stop(t+dec+0.05);
},
clap(t, vel){
const p = CFG.clap, out = trackOut('clap', t);
p.bursts.forEach((off,i)=>{
const n = noiseSrc(), bp = actx.createBiquadFilter(), g = actx.createGain();
bp.type='bandpass'; bp.frequency.value=p.bpF; bp.Q.value=1.4;
const dec = i===p.bursts.length-1 ? p.decay : 0.028;
g.gain.setValueAtTime(0.0001, t+off);
g.gain.linearRampToValueAtTime(p.gain*VEL_GAIN[vel]*(i===p.bursts.length-1?1:0.7), t+off+0.001);
g.gain.exponentialRampToValueAtTime(0.0001, t+off+dec);
n.connect(bp).connect(g).connect(out);
n.start(t+off); n.stop(t+off+dec+0.05);
});
},
tom(t, vel){
const p = CFG.tom, out = trackOut('tom', t);
const o = actx.createOscillator(), g = actx.createGain();
o.type='triangle';
o.frequency.setValueAtTime(p.f*(vel===2?1.06:1), t);
o.frequency.exponentialRampToValueAtTime(p.f*0.7, t+p.decay);
g.gain.setValueAtTime(0.0001,t);
g.gain.linearRampToValueAtTime(p.gain*VEL_GAIN[vel], t+0.002);
g.gain.exponentialRampToValueAtTime(0.0001, t+p.decay);
o.connect(g).connect(out);
o.start(t); o.stop(t+p.decay+0.05);
},
};
function trigger(id, t, vel){
switch(id){
case 'kick': SYNTH.kick(t,vel); break;
case 'snare': SYNTH.snare(t,vel); break;
case 'chh': SYNTH.hat(t,vel,false); break;
case 'ohat': SYNTH.hat(t,vel,true); break;
case 'clap': SYNTH.clap(t,vel); break;
case 'tom': SYNTH.tom(t,vel); break;
}
}
/* ============================================================
SCHEDULER — lookahead pattern on the audio clock
============================================================ */
let playing = false;
let nextStepTime = 0; // absolute audio time of the next step to schedule
let stepCounter = 0; // absolute step index (never reset except on stop)
let timerID = null;
window.__schedLog = []; // instrumentation for verification
function stepDuration(){ return 60 / state.bpm / 4; } // 16th notes
// swing: off-beat 8ths (odd 16ths within a beat pair) delayed by swing fraction of a step
function swingOffset(step){
return (step % 2 === 1) ? stepDuration() * (state.swing/100) : 0;
}
function scheduler(){
while(nextStepTime < actx.currentTime + CFG.SCHEDULE_AHEAD){
scheduleStep(stepCounter, nextStepTime);
nextStepTime += stepDuration(); // derived from absolute counter & current bpm
stepCounter++;
}
}
function scheduleStep(absStep, time){
const s = absStep % 16;
const t = time + swingOffset(s);
window.__schedLog.push({step: s, time: t});
if(window.__schedLog.length > 200) window.__schedLog.shift();
TRACKS.forEach(tr=>{
const v = state.pattern[tr.id][s];
if(v>0 && !state.mute[tr.id]) trigger(tr.id, t, v);
});
// playhead event queue for the visual layer
drawQueue.push({step:s, time:t});
}
function startPlayback(){
initAudio();
if(actx.state==='suspended') actx.resume();
playing = true;
stepCounter = 0;
nextStepTime = actx.currentTime + 0.06; // restart cleanly on step 1
window.__schedLog.length = 0;
timerID = setInterval(scheduler, CFG.LOOKAHEAD_MS);
scheduler();
uiPlay(true);
}
function stopPlayback(){
playing = false;
clearInterval(timerID); timerID=null;
drawQueue.length = 0;
clearPlayhead();
uiPlay(false);
}
/* ============================================================
VISUAL PLAYHEAD — driven from the audio clock via rAF
============================================================ */
let drawQueue = [];
let curPhStep = -1;
function playheadLoop(){
if(!playing) return;
const now = actx.currentTime;
let step = curPhStep;
while(drawQueue.length && drawQueue[0].time <= now){
step = drawQueue.shift().step;
}
if(step !== curPhStep){
setPlayhead(curPhStep, step);
flashColumn(step);
curPhStep = step;
}
requestAnimationFrame(playheadLoop);
}
function setPlayhead(oldS, newS){
document.querySelectorAll(`.cell[data-step="${oldS}"]`).forEach(c=>c.classList.remove('ph'));
document.querySelectorAll(`.cell[data-step="${newS}"]`).forEach(c=>c.classList.add('ph'));
document.querySelectorAll('.stepnum').forEach((el,i)=>el.classList.toggle('ph', i===newS));
}
function clearPlayhead(){
document.querySelectorAll('.cell.ph').forEach(c=>c.classList.remove('ph'));
document.querySelectorAll('.stepnum.ph').forEach(el=>el.classList.remove('ph'));
curPhStep = -1;
}
function flashColumn(s){
document.querySelectorAll(`.cell[data-step="${s}"].on`).forEach(c=>{
c.classList.add('hit');
setTimeout(()=>c.classList.remove('hit'), 70);
});
}
/* ============================================================
UI BUILD
============================================================ */
const gridEl = document.getElementById('grid');
function buildGrid(){
gridEl.innerHTML='';
gridEl.appendChild(Object.assign(document.createElement('div'),{className:'corner',textContent:''}));
for(let s=0;s<16;s++){
const d=document.createElement('div');
d.className='stepnum'+(s%4===0?' beat':''); d.textContent=s+1;
gridEl.appendChild(d);
}
TRACKS.forEach(tr=>{
const lab=document.createElement('div');
lab.className='rowlabel'+(state.mute[tr.id]?' muted':'');
lab.dataset.track=tr.id;
lab.innerHTML=`<span class="dot" style="background:${tr.color}"></span>${tr.name}`;
gridEl.appendChild(lab);
for(let s=0;s<16;s++){
const c=document.createElement('div');
c.className='cell'+(s%4===0 && s>0?' beat':'');
c.dataset.track=tr.id; c.dataset.step=s;
paintCell(c, state.pattern[tr.id][s], tr.color);
gridEl.appendChild(c);
}
});
}
function paintCell(c, v, color){
c.classList.toggle('on', v>0);
c.classList.toggle('acc', v===2);
c.style.background = v>0 ? color : '';
c.style.opacity = v===2 ? 1 : (v===1 ? 0.62 : '');
c.style.boxShadow = v>0 ? `0 0 ${v===2?10:5}px ${color}55` : '';
}
/* painting: click cycles off->norm->acc->off; drag paints first-click value */
let painting=false, paintVal=0;
gridEl.addEventListener('pointerdown', e=>{
const c=e.target.closest('.cell'); if(!c) return;
e.preventDefault();
const tr=c.dataset.track, s=+c.dataset.step;
if(e.button===2){ // right click = cycle accent
state.pattern[tr][s] = state.pattern[tr][s]===2 ? 0 : 2;
}else{
const cur = state.pattern[tr][s];
const next = (cur+1)%3;
state.pattern[tr][s]=next;
painting=true; paintVal=next;
}
refreshCell(tr,s); saveState();
});
gridEl.addEventListener('pointerover', e=>{
if(!painting) return;
const c=e.target.closest('.cell'); if(!c) return;
const tr=c.dataset.track, s=+c.dataset.step;
if(state.pattern[tr][s]!==paintVal){
state.pattern[tr][s]=paintVal; refreshCell(tr,s); saveState();
}
});
window.addEventListener('pointerup', ()=>painting=false);
gridEl.addEventListener('contextmenu', e=>e.preventDefault());
function refreshCell(tr,s){
const c=gridEl.querySelector(`.cell[data-track="${tr}"][data-step="${s}"]`);
if(c) paintCell(c, state.pattern[tr][s], TRACKS.find(t=>t.id===tr).color);
}
/* mixer strips */
const mixerEl = document.getElementById('mixer');
function buildMixer(){
mixerEl.innerHTML='';
TRACKS.forEach(tr=>{
const st=document.createElement('div');
st.className='strip'+(state.mute[tr.id]?' muted':'');
st.innerHTML=`
<div class="name"><span class="dot" style="width:8px;height:8px;border-radius:2px;background:${tr.color};display:inline-block"></span>${tr.name}</div>
<input type="range" min="0" max="100" value="${Math.round(state.vol[tr.id]*100)}" data-vol="${tr.id}">
<button class="mbtn${state.mute[tr.id]?' active':''}" data-mute="${tr.id}">${state.mute[tr.id]?'MUTED':'MUTE'}</button>`;
mixerEl.appendChild(st);
});
}
mixerEl.addEventListener('input', e=>{
const id=e.target.dataset.vol; if(!id) return;
state.vol[id]=e.target.value/100; saveState();
});
mixerEl.addEventListener('click', e=>{
const b=e.target.closest('[data-mute]'); if(!b) return;
const id=b.dataset.mute;
state.mute[id]=!state.mute[id];
b.classList.toggle('active', state.mute[id]);
b.textContent = state.mute[id]?'MUTED':'MUTE';
b.parentElement.classList.toggle('muted', state.mute[id]);
document.querySelector(`.rowlabel[data-track="${id}"]`).classList.toggle('muted', state.mute[id]);
saveState();
});
/* transport controls */
const $ = id=>document.getElementById(id);
$('playBtn').addEventListener('click', ()=> playing ? stopPlayback() : startPlayback());
function uiPlay(on){
$('playBtn').classList.toggle('playing', on);
$('playBtn').textContent = on ? '■ STOP' : '▶ PLAY';
$('powerLed').classList.toggle('on', on);
if(on) requestAnimationFrame(playheadLoop);
}
$('bpm').addEventListener('input', e=>{
state.bpm=+e.target.value;
$('bpmVal').textContent = state.bpm+' BPM';
saveState();
});
$('swing').addEventListener('input', e=>{
state.swing=+e.target.value;
$('swingVal').textContent = state.swing+'%';
saveState();
});
$('master').addEventListener('input', e=>{
state.master=e.target.value/100;
$('masterVal').textContent = e.target.value+'%';
if(masterGain) masterGain.gain.setTargetAtTime(state.master, actx.currentTime, 0.01);
saveState();
});
$('clearBtn').addEventListener('click', ()=>{
TRACKS.forEach(t=>state.pattern[t.id].fill(0));
redrawAllCells(); saveState();
});
$('randBtn').addEventListener('click', ()=>{
const dens={kick:.35,snare:.2,chh:.55,ohat:.1,clap:.12,tom:.15};
TRACKS.forEach(t=>{
for(let s=0;s<16;s++){
let on = Math.random()<dens[t.id];
if(t.id==='kick' && s%4===0) on = Math.random()<0.8;
if(t.id==='snare' && (s===4||s===12)) on = true;
state.pattern[t.id][s] = on ? (Math.random()<0.25?2:1) : 0;
}
state.pattern[t.id][0] = t.id==='kick'?2:(state.pattern[t.id][0]||0);
});
redrawAllCells(); saveState();
});
function redrawAllCells(){
TRACKS.forEach(tr=>{
for(let s=0;s<16;s++) refreshCell(tr.id,s);
document.querySelector(`.rowlabel[data-track="${tr.id}"]`).classList.toggle('muted', state.mute[tr.id]);
});
}
/* ============================================================
INIT
============================================================ */
if(!loadState()) loadDefaultPattern();
$('bpm').value=state.bpm; $('bpmVal').textContent=state.bpm+' BPM';
$('swing').value=state.swing; $('swingVal').textContent=state.swing+'%';
$('master').value=Math.round(state.master*100); $('masterVal').textContent=Math.round(state.master*100)+'%';
buildGrid(); buildMixer();
// resume audio context on first gesture anywhere
document.addEventListener('pointerdown', ()=>{
initAudio();
if(actx.state==='suspended') actx.resume();
}, {once:false});
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"a43afaa0-2fce-40b8-8f8c-28ccee484864","tokensIn":295507,"tokensOut":9535,"tokensTotal":305042,"cost":0,"turns":19,"toolCalls":18,"failedToolCalls":0,"timestamp":"2026-08-21T19:58:42.856Z"} -->