← Sequencer results

Sequencer

ox alpha · full

Initial view of Sequencer

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"} -->