← Sequencer results

Sequencer

muse spark 1.2 · basic

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 29,391 bytes · SHA-256 4da99c72ca28
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>TR-16 — Step Sequencer</title>
<style>
  :root{
    --bg:#0a0c10;
    --panel:#12161f;
    --panel2:#181e2b;
    --grid:#1e2535;
    --accent:#ff3b30;
    --accent2:#00e5ff;
    --accent3:#ffe600;
    --text:#e6eaf2;
    --muted:#6b758a;
    --cell:#1a2130;
    --cell-on:#ff3b30;
    --cell-accent:#ff6b3b;
    --border:#252e42;
  }
  *{box-sizing:border-box}
  body{
    margin:0;background:radial-gradient(1200px 600px at 50% -10%,#1a2540 0%,var(--bg) 60%);
    color:var(--text);font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    min-height:100vh;overflow-x:hidden;
  }
  .wrap{max-width:1060px;margin:0 auto;padding:18px 16px 40px}
  /* header */
  .top{
    background:linear-gradient(180deg,#1b2233,#131722);
    border:1px solid var(--border);border-radius:16px;
    padding:14px 16px;display:flex;flex-wrap:wrap;gap:14px;align-items:center;
    box-shadow:0 10px 30px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.06);
  }
  .brand{display:flex;align-items:center;gap:12px}
  .logo{
    width:44px;height:44px;border-radius:10px;background:#0f1320;border:1px solid #2a344e;
    display:grid;place-items:center;font-weight:900;letter-spacing:.5px;color:var(--accent);
    box-shadow:0 0 18px rgba(255,59,48,.35);
  }
  .brand h1{margin:0;font-size:18px;letter-spacing:.12em;line-height:1}
  .brand p{margin:2px 0 0;font-size:10px;letter-spacing:.18em;color:var(--muted);text-transform:uppercase}
  .transport{display:flex;align-items:center;gap:10px;margin-left:auto;flex-wrap:wrap}
  .btn{
    appearance:none;border:1px solid #2a344e;background:#0f1320;color:var(--text);
    padding:10px 18px;border-radius:10px;font-weight:800;letter-spacing:.08em;
    cursor:pointer;display:inline-flex;align-items:center;gap:8px;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.06),0 4px 12px rgba(0,0,0,.3);
    transition:.15s;user-select:none
  }
  .btn:active{transform:translateY(1px)}
  .btn.play{background:var(--accent);border-color:#ff5a4f;color:white;box-shadow:0 6px 18px rgba(255,59,48,.45)}
  .btn.play.playing{background:#111;color:var(--text);box-shadow:none}
  .btn.ghost{background:#151a28}
  .btn.small{padding:7px 10px;font-size:12px;border-radius:8px}
  /* knobs */
  .controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
  .ctrl{
    background:#0f1320;border:1px solid #252e42;border-radius:12px;padding:10px 12px;
    min-width:140px;display:flex;flex-direction:column;gap:6px
  }
  .ctrl label{font-size:9px;letter-spacing:.16em;color:var(--muted);text-transform:uppercase;display:flex;justify-content:space-between}
  .ctrl label b{color:var(--accent2);font-size:12px}
  .ctrl label b.bpm{color:var(--accent3);font-size:13px;text-shadow:0 0 10px rgba(255,230,0,.5)}
  input[type=range]{-webkit-appearance:none;appearance:none;height:4px;background:#232c42;border-radius:99px;outline:none}
  input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:var(--text);border:2px solid #0f1320;box-shadow:0 2px 8px rgba(0,0,0,.5);cursor:pointer}
  .led{
    display:inline-block;width:8px;height:8px;border-radius:50%;background:#1a2a1a;box-shadow:0 0 0 rgba(0,255,100,0);
    transition:.1s
  }
  .led.on{background:#00ff88;box-shadow:0 0 10px #00ff88}
  /* main panel */
  .machine{
    margin-top:14px;background:linear-gradient(180deg,var(--panel2),var(--panel));
    border:1px solid var(--border);border-radius:16px;overflow:hidden;
    box-shadow:0 20px 50px rgba(0,0,0,.6)
  }
  .machine-head{
    display:flex;gap:8px;padding:12px 14px;border-bottom:1px solid var(--border);
    align-items:center;flex-wrap:wrap;background:#131722
  }
  .machine-head .hint{font-size:10px;color:var(--muted);letter-spacing:.08em}
  .machine-head .hint span{color:var(--text)}
  .grid-wrap{padding:14px;overflow-x:auto}
  .grid{
    display:grid;grid-template-columns:160px repeat(16,1fr);gap:6px;min-width:860px
  }
  .grid-header{display:contents}
  .grid-header div{font-size:9px;letter-spacing:.12em;color:var(--muted);text-align:center;padding:4px 0}
  .grid-header div.beat{color:var(--accent3)}
  .row{display:contents}
  .track-label{
    background:#0f1320;border:1px solid #252e42;border-radius:10px;
    display:flex;align-items:center;gap:8px;padding:8px 10px;
  }
  .track-label.muted{opacity:.45}
  .icon{
    width:28px;height:28px;border-radius:8px;display:grid;place-items:center;
    font-size:13px;font-weight:900;flex-shrink:0
  }
  .t-kick .icon{background:#ff3b30;color:white}
  .t-snare .icon{background:#00e5ff;color:#002a33}
  .t-hatC .icon{background:#ffe600;color:#332d00}
  .t-hatO .icon{background:#a78bfa;color:white}
  .t-clap .icon{background:#ff8a00;color:white}
  .t-tom .icon{background:#00ff88;color:#00331a}
  .track-meta{line-height:1}
  .track-meta strong{font-size:11px;letter-spacing:.06em;display:block}
  .track-meta span{font-size:9px;color:var(--muted);letter-spacing:.08em;text-transform:uppercase}
  .track-ctrl{display:flex;gap:4px;margin-left:auto;align-items:center}
  .mbtn{
    width:28px;height:18px;border-radius:6px;border:1px solid #2a344e;
    background:#1a2130;color:var(--muted);font-size:8px;font-weight:800;letter-spacing:.06em;cursor:pointer
  }
  .mbtn.active{background:var(--accent);color:white;border-color:#ff5a4f;box-shadow:0 0 8px rgba(255,59,48,.6)}
  .mbtn.solo.active{background:var(--accent3);color:#111;border-color:var(--accent3)}
  .vslider{width:54px;height:4px}
  .pad{
    aspect-ratio:1;min-height:42px;border-radius:9px;
    background:var(--cell);border:1px solid #252e42;
    position:relative;cursor:pointer;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.05), inset 0 -6px 12px rgba(0,0,0,.25);
    transition:.08s;
    display:grid;place-items:center;
  }
  .pad:hover{filter:brightness(1.15)}
  .pad.on{background:radial-gradient(120% 120% at 30% 20%,#ff6a5a,#ff3b30 55%,#a81c14);border-color:#ff5a4f;box-shadow:0 0 14px rgba(255,59,48,.7), inset 0 1px 0 rgba(255,255,255,.3)}
  .pad.on.accent{background:radial-gradient(120% 120% at 30% 20%,#fff2a0,#ffcc00 35%,#ff6b00 70%);border-color:#ffcc00;box-shadow:0 0 16px rgba(255,204,0,.8), inset 0 1px 0 rgba(255,255,255,.6);transform:scale(1.03)}
  .pad.on.accent::after{content:"•";position:absolute;top:2px;right:5px;font-size:14px;color:rgba(0,0,0,.6);line-height:1}
  .pad.playhead{outline:2px solid var(--accent2);outline-offset:1px;z-index:1;box-shadow:0 0 14px rgba(0,229,255,.6)}
  .pad.beat-sep{border-right:2px solid rgba(255,255,255,.08)}
  .pad::before{
    content:"";width:36%;height:36%;border-radius:50%;background:rgba(255,255,255,.07);
    position:absolute;opacity:0;transition:.1s
  }
  .pad.on::before{opacity:1;background:rgba(255,255,255,.18)}
  .footer{display:flex;gap:10px;flex-wrap:wrap;padding:12px 14px;border-top:1px solid var(--border);background:#0f1320;align-items:center}
  .foot-label{font-size:9px;letter-spacing:.14em;color:var(--muted);text-transform:uppercase}
  .sep{width:1px;height:24px;background:var(--border)}
  @media(max-width:900px){
    .grid{grid-template-columns:140px repeat(16, minmax(36px,1fr));min-width:740px}
    .wrap{padding:10px}
  }
</style>
</head>
<body>
<div class="wrap">
  <div class="top">
    <div class="brand">
      <div class="logo">TR-16</div>
      <div>
        <h1>DRUM COMPUTER</h1>
        <p>16-Step ● 6 Voice ● Analog Modelled</p>
      </div>
    </div>
    <div class="transport">
      <button id="playBtn" class="btn play"><span id="playIcon">▶</span> <span id="playText">PLAY</span> <i class="led" id="playLed"></i></button>
      <div class="controls">
        <div class="ctrl" style="min-width:150px">
          <label>BPM <b class="bpm" id="bpmVal">120</b></label>
          <input id="bpm" type="range" min="60" max="200" value="120">
        </div>
        <div class="ctrl" style="min-width:150px">
          <label>SWING <b id="swingVal">0%</b></label>
          <input id="swing" type="range" min="0" max="60" value="0">
        </div>
        <div class="ctrl" style="min-width:130px">
          <label>MASTER <b id="volVal">85%</b></label>
          <input id="masterVol" type="range" min="0" max="100" value="85">
        </div>
      </div>
    </div>
  </div>

  <div class="machine">
    <div class="machine-head">
      <span class="hint"><span style="color:var(--accent)">●</span> CLICK to toggle · DRAG to paint · <span>RIGHT-CLICK</span> accent · M = mute · S = solo</span>
      <div style="margin-left:auto;display:flex;gap:8px">
        <button id="clearBtn" class="btn small ghost">CLEAR</button>
        <button id="randBtn" class="btn small ghost" style="border-color:var(--accent2);color:var(--accent2)">RANDOMIZE</button>
      </div>
    </div>
    <div class="grid-wrap">
      <div id="grid" class="grid"></div>
    </div>
    <div class="footer">
      <span class="foot-label">Current Step</span>
      <span id="stepReadout" style="background:#0a0c10;border:1px solid #252e42;padding:4px 10px;border-radius:8px;font-weight:800;color:var(--accent3);min-width:44px;text-align:center">--</span>
      <div class="sep"></div>
      <span class="foot-label">Lookahead 25ms / 100ms</span>
      <span class="foot-label" style="margin-left:auto;opacity:.7">SAMPLE-ACCURATE SCHEDULER • Web Audio Clock</span>
    </div>
  </div>
</div>

<script>
/* ==========================================================
   TUNABLE CONSTANTS
   ========================================================== */
const BPM_MIN=60, BPM_MAX=200;
const LOOKAHEAD_MS=25;          // scheduler interval
const SCHEDULE_AHEAD_SEC=0.10;  // how far ahead to schedule
const SWING_MAX=0.60;
const DEFAULT_BPM=120;
const DEFAULT_SWING=0.08; // 8% for groove
const MASTER_DEFAULT=0.85;

/* Instrument patch params */
const PATCH={
  kick:{base:50, peak:150, decay:0.45},
  snare:{tone:180, noiseDecay:0.18},
  hatClosed:{decay:0.08, hp:7000},
  hatOpen:{decay:0.35, hp:7000},
  clap:{bursts:4, spread:0.03},
  tom:{base:90, peak:180, decay:0.45}
};

/* ==========================================================
   STATE
   ========================================================== */
const TRACKS=[
  {id:'kick', name:'KICK', short:'K'},
  {id:'snare', name:'SNARE', short:'S'},
  {id:'hatC', name:'CLOSED HAT', short:'CH'},
  {id:'hatO', name:'OPEN HAT', short:'OH'},
  {id:'clap', name:'CLAP', short:'CP'},
  {id:'tom', name:'TOM', short:'T'}
];
let pattern = []; // [track][step] 0=off 1=on 2=accent
let trackVols=[0.9,0.85,0.7,0.7,0.8,0.8];
let trackMutes=[false,false,false,false,false,false];
let trackSolos=[false,false,false,false,false,false];

let bpm=DEFAULT_BPM;
let swing=DEFAULT_SWING;
let masterGainVal=MASTER_DEFAULT;

let audioCtx=null, masterGain=null;
let isPlaying=false;
let currentStep=0; // absolute step counter
let nextNoteTime=0;
let schedulerId=null;
let queued=[]; // {step,time}
let dragMode=null, isDragging=false;

/* ==========================================================
   PERSISTENCE
   ========================================================== */
const LS_KEY='tr16_v2';
function save(){
  localStorage.setItem(LS_KEY, JSON.stringify({pattern,bpm,swing,trackVols,trackMutes,trackSolos,masterGainVal}));
}
function load(){
  try{
    const d=JSON.parse(localStorage.getItem(LS_KEY));
    if(d){
      if(d.pattern) pattern=d.pattern;
      if(d.bpm) bpm=d.bpm;
      if(d.swing!==undefined) swing=d.swing;
      if(d.trackVols) trackVols=d.trackVols;
      if(d.trackMutes) trackMutes=d.trackMutes;
      if(d.trackSolos) trackSolos=d.trackSolos;
      if(d.masterGainVal!==undefined) masterGainVal=d.masterGainVal;
      return true;
    }
  }catch(e){}
  return false;
}
function defaultPattern(){
  // groovy house / hiphop hybrid
  pattern = TRACKS.map(()=>Array(16).fill(0));
  // kick 0,4,8,12 with ghost on 10
  [0,4,8,12].forEach(s=>pattern[0][s]=1);
  pattern[0][10]=1;
  // snare 4,12 accent, ghost 11
  pattern[1][4]=2; pattern[1][12]=2; pattern[1][11]=1;
  // closed hat 2,6,10,14 + every 2nd 16th
  [2,6,10,14].forEach(s=>pattern[2][s]=1);
  [1,5,9,13].forEach(s=>pattern[2][s]=0);
  // extra hats
  pattern[2][0]=1; pattern[2][4]=1; pattern[2][8]=1; pattern[2][12]=1;
  // open hat on off-beats
  pattern[3][6]=1; pattern[3][14]=1;
  // clap on 4,12
  pattern[4][4]=1; pattern[4][12]=1;
  // tom fills
  pattern[5][7]=1; pattern[5][15]=2;
  // accents
  pattern[2][2]=2; pattern[2][14]=2;
}

/* ==========================================================
   AUDIO ENGINE
   ========================================================== */
function ensureAudio(){
  if(!audioCtx){
    audioCtx=new (window.AudioContext||window.webkitAudioContext)();
    masterGain=audioCtx.createGain();
    masterGain.gain.value=masterGainVal;
    masterGain.connect(audioCtx.destination);
    // pre-create noise buffer
    noiseBuffer=createNoiseBuffer();
  }
  if(audioCtx.state==='suspended') audioCtx.resume();
}
let noiseBuffer=null;
function createNoiseBuffer(){
  const len=audioCtx.sampleRate*1;
  const buf=audioCtx.createBuffer(1,len,audioCtx.sampleRate);
  const ch=buf.getChannelData(0);
  for(let i=0;i<len;i++) ch[i]=Math.random()*2-1;
  return buf;
}

function isTrackAudible(t){
  if(trackMutes[t]) return false;
  if(trackSolos.some(s=>s)) return trackSolos[t];
  return true;
}

function playSound(trackIdx,time,vel){
  if(!isTrackAudible(trackIdx)) return;
  const isAccent=vel===2;
  const gainMul=isAccent?1.35:0.85;
  const vol=trackVols[trackIdx]*gainMul;
  switch(TRACKS[trackIdx].id){
    case 'kick': playKick(time,vol,isAccent); break;
    case 'snare': playSnare(time,vol,isAccent); break;
    case 'hatC': playHat(time,vol,false,isAccent); break;
    case 'hatO': playHat(time,vol,true,isAccent); break;
    case 'clap': playClap(time,vol,isAccent); break;
    case 'tom': playTom(time,vol,isAccent); break;
  }
}
function playKick(t,vol,accent){
  const osc=audioCtx.createOscillator();
  const g=audioCtx.createGain();
  osc.type='sine';
  osc.frequency.setValueAtTime(PATCH.kick.peak,t);
  osc.frequency.exponentialRampToValueAtTime(PATCH.kick.base,t+0.08);
  g.gain.setValueAtTime(0,t);
  g.gain.linearRampToValueAtTime(vol*1.0,t+0.003);
  g.gain.exponentialRampToValueAtTime(0.001,t+PATCH.kick.decay);
  osc.connect(g).connect(masterGain);
  osc.start(t); osc.stop(t+PATCH.kick.decay+0.05);
}
function playSnare(t,vol,accent){
  // tonal body
  const osc=audioCtx.createOscillator();
  const og=audioCtx.createGain();
  osc.type='sine'; osc.frequency.setValueAtTime(PATCH.snare.tone,t);
  osc.frequency.exponentialRampToValueAtTime(60,t+0.12);
  og.gain.setValueAtTime(0,t);
  og.gain.linearRampToValueAtTime(vol*0.6,t+0.005);
  og.gain.exponentialRampToValueAtTime(0.001,t+0.18);
  osc.connect(og).connect(masterGain);
  osc.start(t); osc.stop(t+0.2);
  // noise
  const src=audioCtx.createBufferSource(); src.buffer=noiseBuffer;
  const bp=audioCtx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=accent?1800:1300; bp.Q.value=0.7;
  const hp=audioCtx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=900;
  const ng=audioCtx.createGain();
  ng.gain.setValueAtTime(0,t);
  ng.gain.linearRampToValueAtTime(vol*0.9,t+0.002);
  ng.gain.exponentialRampToValueAtTime(0.001,t+PATCH.snare.noiseDecay*(accent?1.4:1));
  src.connect(hp).connect(bp).connect(ng).connect(masterGain);
  src.start(t); src.stop(t+0.3);
}
function playHat(t,vol,open,accent){
  const src=audioCtx.createBufferSource(); src.buffer=noiseBuffer;
  const hp=audioCtx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=PATCH.hatClosed.hp;
  const bp=audioCtx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=9500; bp.Q.value=0.6;
  const g=audioCtx.createGain();
  const decay=open?PATCH.hatOpen.decay:PATCH.hatClosed.decay;
  const v=open?0.45:0.38;
  g.gain.setValueAtTime(0,t);
  g.gain.linearRampToValueAtTime(vol*v*(accent?1.3:1),t+0.001);
  g.gain.exponentialRampToValueAtTime(0.001,t+decay);
  src.connect(hp).connect(bp).connect(g).connect(masterGain);
  src.start(t); src.stop(t+decay+0.05);
  // metallic click for closed
  if(!open){
    const o=audioCtx.createOscillator(); o.type='square'; o.frequency.value=1200;
    const og=audioCtx.createGain();
    og.gain.setValueAtTime(0,t); og.gain.linearRampToValueAtTime(vol*0.08,t+0.001); og.gain.exponentialRampToValueAtTime(0.001,t+0.02);
    o.connect(og).connect(masterGain);
    o.start(t); o.stop(t+0.03);
  }
}
function playClap(t,vol,accent){
  const bursts=PATCH.clap.bursts;
  for(let i=0;i<bursts;i++){
    const bt=t+i*0.028;
    const src=audioCtx.createBufferSource(); src.buffer=noiseBuffer;
    const bp=audioCtx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=1200+Math.random()*400; bp.Q.value=0.9;
    const g=audioCtx.createGain();
    const env = i===bursts-1?0.18:0.04;
    g.gain.setValueAtTime(0,bt);
    g.gain.linearRampToValueAtTime(vol*0.7*(accent?1.2:1)*(1-i*0.12),bt+0.002);
    g.gain.exponentialRampToValueAtTime(0.001,bt+env);
    src.connect(bp).connect(g).connect(masterGain);
    src.start(bt); src.stop(bt+0.2);
  }
}
function playTom(t,vol,accent){
  const osc=audioCtx.createOscillator();
  const g=audioCtx.createGain();
  osc.type='sine';
  const base=accent?100:PATCH.tom.base;
  osc.frequency.setValueAtTime(PATCH.tom.peak*(accent?1.15:1),t);
  osc.frequency.exponentialRampToValueAtTime(base,t+0.14);
  g.gain.setValueAtTime(0,t);
  g.gain.linearRampToValueAtTime(vol*0.9,t+0.004);
  g.gain.exponentialRampToValueAtTime(0.001,t+PATCH.tom.decay);
  // slight overdrive for accent
  if(accent){
    const hp=audioCtx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=30;
    osc.connect(hp).connect(g).connect(masterGain);
  }else{
    osc.connect(g).connect(masterGain);
  }
  osc.start(t); osc.stop(t+0.5);
}

/* ==========================================================
   SCHEDULER — lookahead pattern, absolute step counter
   ========================================================== */
function secondsPerStep(){
  return 60/bpm/4; // 16th
}
function scheduler(){
  if(!isPlaying || !audioCtx) return;
  // schedule while next note is within lookahead window
  while(nextNoteTime < audioCtx.currentTime + SCHEDULE_AHEAD_SEC){
    scheduleStep(currentStep, nextNoteTime);
    // compute duration to next step with swing applied (paired swing)
    const stepInPair = currentStep % 2;
    let dur = secondsPerStep();
    if(swing>0){
      if(stepInPair===0) dur = dur * (1+swing);
      else dur = dur * (1-swing);
    }
    nextNoteTime += dur;
    currentStep = (currentStep+1)%16;
    // expose for self-check
    window.__lastSchedule={step:currentStep,time:nextNoteTime};
  }
}
function scheduleStep(step,time){
  queued.push({step,time});
  // keep queue small
  if(queued.length>32) queued.shift();
  window.__scheduledTimes=window.__scheduledTimes||[];
  window.__scheduledTimes.push({step,time});
  if(window.__scheduledTimes.length>64) window.__scheduledTimes.shift();

  for(let tr=0;tr<TRACKS.length;tr++){
    const vel=pattern[tr][step];
    if(vel>0) playSound(tr,time,vel);
  }
}
window.__scheduledTimes=[];
window.__lastSchedule=null;

/* ==========================================================
   PLAYHEAD — driven from audio clock via rAF
   ========================================================== */
let rafId=null;
function updatePlayhead(){
  rafId=requestAnimationFrame(updatePlayhead);
  if(!isPlaying || !audioCtx || queued.length===0) return;
  const now=audioCtx.currentTime;
  // find current step: latest queued step whose time <= now and next time > now
  // prune old
  while(queued.length>1 && queued[1].time <= now){
    queued.shift();
  }
  let cur = queued[0];
  // if before first scheduled time, show last? handle
  let displayStep = cur?cur.step:0;
  // if now is before cur.time, the audible step is previous
  if(cur && now < cur.time){
    displayStep = (cur.step+15)%16;
  } else {
    // cur is playing
    displayStep = cur.step;
  }
  highlightStep(displayStep);
  document.getElementById('stepReadout').textContent = String(displayStep+1).padStart(2,'0');
  document.getElementById('playLed').classList.toggle('on', true);
}
function highlightStep(idx){
  document.querySelectorAll('.pad').forEach(p=>p.classList.remove('playhead'));
  document.querySelectorAll(`.pad[data-step="${idx}"]`).forEach(p=>p.classList.add('playhead'));
}

/* ==========================================================
   TRANSPORT
   ========================================================== */
function startPlayback(){
  ensureAudio();
  if(isPlaying) return;
  isPlaying=true;
  currentStep=0;
  nextNoteTime=audioCtx.currentTime+0.06;
  queued=[];
  schedulerId=setInterval(scheduler, LOOKAHEAD_MS);
  // start rAF
  if(!rafId) updatePlayhead();
  document.getElementById('playText').textContent='STOP';
  document.getElementById('playIcon').textContent='■';
  document.getElementById('playBtn').classList.add('playing');
  document.getElementById('playLed').classList.add('on');
}
function stopPlayback(){
  isPlaying=false;
  clearInterval(schedulerId); schedulerId=null;
  queued=[];
  document.querySelectorAll('.pad').forEach(p=>p.classList.remove('playhead'));
  document.getElementById('stepReadout').textContent='--';
  document.getElementById('playText').textContent='PLAY';
  document.getElementById('playIcon').textContent='▶';
  document.getElementById('playBtn').classList.remove('playing');
  document.getElementById('playLed').classList.remove('on');
  // immediate silence: ramp master down/up quickly to kill lingering?
}

/* ==========================================================
   UI BUILD
   ========================================================== */
function buildGrid(){
  const grid=document.getElementById('grid');
  grid.innerHTML='';
  // header numbers
  const hdr=document.createElement('div'); hdr.className='grid-header';
  const empty=document.createElement('div'); empty.textContent='VOICE'; empty.style.textAlign='left'; empty.style.paddingLeft='10px';
  hdr.appendChild(empty);
  for(let s=0;s<16;s++){
    const d=document.createElement('div');
    d.textContent=s+1;
    if(s%4===0) d.classList.add('beat');
    hdr.appendChild(d);
  }
  grid.appendChild(hdr);
  // rows
  TRACKS.forEach((tr,ti)=>{
    const label=document.createElement('div');
    label.className='track-label t-'+tr.id + (trackMutes[ti]?' muted':'');
    label.innerHTML=`
      <div class="icon">${tr.short}</div>
      <div class="track-meta"><strong>${tr.name}</strong><span>${tr.id}</span></div>
      <div class="track-ctrl">
        <button class="mbtn ${trackMutes[ti]?'active':''}" data-mute="${ti}">M</button>
        <button class="mbtn solo ${trackSolos[ti]?'active':''}" data-solo="${ti}">S</button>
        <input class="vslider" type="range" min="0" max="100" value="${Math.round(trackVols[ti]*100)}" data-vol="${ti}">
      </div>
    `;
    grid.appendChild(label);
    for(let s=0;s<16;s++){
      const pad=document.createElement('div');
      pad.className='pad'+(pattern[ti][s] ? ' on'+(pattern[ti][s]===2?' accent':'') : '')+(s%4===3?' beat-sep':'');
      pad.dataset.track=ti; pad.dataset.step=s;
      if(pattern[ti][s]===2) pad.title='Accent';
      // mouse
      pad.addEventListener('mousedown', e=>{
        e.preventDefault();
        isDragging=true;
        const cur=pattern[ti][s];
        // left click cycle 0->1->2->0, right click accent logic but we cycle
        if(e.button===2){
          pattern[ti][s]= cur===2?0:2;
          if(cur===0) pattern[ti][s]=2;
          if(cur===1) pattern[ti][s]=2;
          if(cur===2) pattern[ti][s]=0;
        }else{
          // toggle: if off -> on, on->accent, accent->off
          if(cur===0){pattern[ti][s]=1; dragMode=1}
          else if(cur===1){pattern[ti][s]=2; dragMode=2}
          else {pattern[ti][s]=0; dragMode=0}
          // for drag, we set dragMode to the new value
          if(dragMode===null) dragMode=pattern[ti][s];
        }
        renderPads(); save();
      });
      pad.addEventListener('mouseenter', ()=>{
        if(!isDragging) return;
        if(dragMode!==null) pattern[ti][s]=dragMode;
        renderPads(); save();
      });
      pad.addEventListener('contextmenu', e=>e.preventDefault());
      // touch
      pad.addEventListener('touchstart', e=>{
        e.preventDefault();
        const cur=pattern[ti][s];
        pattern[ti][s]=cur===0?1:cur===1?2:0;
        renderPads(); save();
      },{passive:false});
      grid.appendChild(pad);
    }
  });
  // mute/solo/vol listeners
  grid.querySelectorAll('[data-mute]').forEach(b=>{
    b.addEventListener('click',()=>{
      const i=+b.dataset.mute;
      trackMutes[i]=!trackMutes[i];
      b.classList.toggle('active',trackMutes[i]);
      b.closest('.track-label').classList.toggle('muted',trackMutes[i] && !trackSolos.some(s=>s));
      save();
    })
  });
  grid.querySelectorAll('[data-solo]').forEach(b=>{
    b.addEventListener('click',()=>{
      const i=+b.dataset.solo;
      trackSolos[i]=!trackSolos[i];
      b.classList.toggle('active',trackSolos[i]);
      // update muted appearance
      document.querySelectorAll('.track-label').forEach((el,idx)=>{
        const audible=isTrackAudible(idx);
        el.classList.toggle('muted',!audible);
      });
      save();
    })
  });
  grid.querySelectorAll('[data-vol]').forEach(sl=>{
    sl.addEventListener('input',()=>{
      const i=+sl.dataset.vol;
      trackVols[i]=sl.value/100;
      save();
    })
  });
}
function renderPads(){
  document.querySelectorAll('.pad').forEach(p=>{
    const tr=+p.dataset.track, st=+p.dataset.step;
    const v=pattern[tr][st];
    p.className='pad'+(v?' on'+(v===2?' accent':''):'')+(st%4===3?' beat-sep':'');
  });
}

/* ==========================================================
   CONTROLS
   ========================================================== */
function initControls(){
  const bpmSl=document.getElementById('bpm');
  const swingSl=document.getElementById('swing');
  const masterSl=document.getElementById('masterVol');
  bpmSl.value=bpm; swingSl.value=Math.round(swing*100); masterSl.value=Math.round(masterGainVal*100);
  document.getElementById('bpmVal').textContent=bpm;
  document.getElementById('swingVal').textContent=Math.round(swing*100)+'%';
  document.getElementById('volVal').textContent=Math.round(masterGainVal*100)+'%';

  bpmSl.addEventListener('input',()=>{
    bpm=parseInt(bpmSl.value);
    document.getElementById('bpmVal').textContent=bpm;
    save();
  });
  swingSl.addEventListener('input',()=>{
    swing=parseInt(swingSl.value)/100;
    document.getElementById('swingVal').textContent=swingSl.value+'%';
    save();
  });
  masterSl.addEventListener('input',()=>{
    masterGainVal=parseInt(masterSl.value)/100;
    document.getElementById('volVal').textContent=masterSl.value+'%';
    if(masterGain) masterGain.gain.value=masterGainVal;
    save();
  });
  document.getElementById('playBtn').addEventListener('click',()=>{
    if(isPlaying) stopPlayback(); else startPlayback();
  });
  document.getElementById('clearBtn').addEventListener('click',()=>{
    pattern=TRACKS.map(()=>Array(16).fill(0));
    renderPads(); save();
  });
  document.getElementById('randBtn').addEventListener('click',()=>{
    pattern=TRACKS.map((_,ti)=>Array.from({length:16},()=>{
      const r=Math.random();
      if(ti===0) return r>0.68?(Math.random()>0.8?2:1):0;
      if(ti===1) return r>0.78?(Math.random()>0.7?2:1):0;
      if(ti===2) return r>0.5?(Math.random()>0.85?2:1):0;
      return r>0.82?(Math.random()>0.8?2:1):0;
    }));
    renderPads(); save();
  });
  window.addEventListener('mouseup',()=>{isDragging=false; dragMode=null});
  window.addEventListener('keydown',e=>{
    if(e.code==='Space'){e.preventDefault(); if(isPlaying) stopPlayback(); else startPlayback();}
  });
}

/* ==========================================================
   INIT
   ========================================================== */
const loaded=load();
if(!loaded || !pattern || pattern.length!==6) defaultPattern();
if(!pattern || pattern[0].length!==16) defaultPattern();
buildGrid();
initControls();
renderPads();
// ensure UI reflects loaded values
document.getElementById('bpm').value=bpm;
document.getElementById('bpmVal').textContent=bpm;
document.getElementById('swing').value=Math.round(swing*100);
document.getElementById('swingVal').textContent=Math.round(swing*100)+'%';
document.getElementById('masterVol').value=Math.round(masterGainVal*100);
document.getElementById('volVal').textContent=Math.round(masterGainVal*100)+'%';
updatePlayhead();

// expose for self-check
window.__getScheduledDeltaStats=()=>{
  const t=window.__scheduledTimes;
  if(t.length<2) return null;
  const deltas=[];
  for(let i=1;i<t.length;i++) deltas.push(t[i].time-t[i-1].time);
  const avg=deltas.reduce((a,b)=>a+b,0)/deltas.length;
  const max=Math.max(...deltas), min=Math.min(...deltas);
  return {avg,min,max,deltas: deltas.slice(-8)};
};
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"41952cec-61dd-42fa-b607-c5f2fd2999c0","tokensIn":1115,"tokensOut":10393,"tokensTotal":11508,"cost":0.0454397,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:29:07.472Z"} -->