← Sequencer results

Sequencer

GLM 5.3 Flash · 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 23,979 bytes · SHA-256 20462da9d665
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DR-909 · Step Sequencer</title>
<style>
  /* ============ STYLE ============ */
  :root{
    --bg:#0c0d10; --panel:#17181d; --panel2:#1e2027; --line:#2a2d36;
    --txt:#c8ccd6; --dim:#6b7080;
    --amber:#ffb347; --red:#ff5a4e; --green:#4ade80; --cyan:#53d8ff;
    --cell:#23252e; --cell-on:#3a3f52;
  }
  *{box-sizing:border-box; margin:0; padding:0; user-select:none; -webkit-user-select:none;}
  body{
    background:radial-gradient(1200px 700px at 50% -10%, #1a1c24, 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:linear-gradient(180deg,#1b1d24,#131419);
    border:1px solid #000; border-radius:14px; padding:18px 20px 22px;
    box-shadow:0 30px 60px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.06);
    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:#e8ebf2; font-weight:800;}
  .brand .led{width:9px;height:9px;border-radius:50%;background:#3a1010;align-self:center;}
  .brand .led.on{background:var(--red); box-shadow:0 0 8px var(--red);}
  .brand small{color:var(--dim); letter-spacing:2px;}

  /* transport */
  .transport{display:flex; flex-wrap:wrap; gap:14px; align-items:center;
    background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:12px 14px; margin-bottom:14px;}
  button{font:inherit; color:var(--txt); background:var(--panel2); border:1px solid var(--line);
    border-radius:8px; padding:8px 14px; cursor:pointer; transition:filter .1s, box-shadow .1s;}
  button:hover{filter:brightness(1.2);}
  button:active{transform:translateY(1px);}
  #playBtn{min-width:92px; font-weight:700; letter-spacing:1px;}
  #playBtn.playing{background:#3d1418; color:var(--red); border-color:#6b2020; box-shadow:0 0 12px rgba(255,90,78,.35);}
  .ctl{display:flex; flex-direction:column; gap:4px; min-width:150px; flex:1;}
  .ctl label{font-size:10px; letter-spacing:2px; color:var(--dim); text-transform:uppercase;
    display:flex; justify-content:space-between;}
  .ctl label b{color:var(--amber); font-variant-numeric:tabular-nums; letter-spacing:1px;}
  input[type=range]{-webkit-appearance:none; appearance:none; height:4px; border-radius:2px;
    background:#33363f; outline:none; cursor:pointer;}
  input[type=range]::-webkit-slider-thumb{-webkit-appearance:none; width:14px; height:20px; border-radius:3px;
    background:linear-gradient(180deg,#4a4e5c,#2c2f39); border:1px solid #000; box-shadow:0 0 6px rgba(0,0,0,.8);}
  #bpmVal{font-size:22px; font-weight:800; color:var(--amber); text-shadow:0 0 10px rgba(255,179,71,.5);
    font-variant-numeric:tabular-nums; min-width:74px; text-align:right;}

  /* sequencer */
  .grid-wrap{background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:12px 14px;}
  .row{display:grid; grid-template-columns:96px repeat(16, 1fr); gap:5px; align-items:center; margin-bottom:6px;}
  .row:last-child{margin-bottom:0;}
  .row .trackname{display:flex; align-items:center; gap:7px; font-size:12px; font-weight:600; letter-spacing:1px;}
  .trackname .dot{width:10px; height:10px; border-radius:3px; flex:none;}
  .trackname .tbtns{display:flex; gap:3px; margin-left:auto;}
  .tbtns button{padding:2px 6px; font-size:10px; border-radius:4px; letter-spacing:1px;}
  .tbtns .mute.active{background:#4a1418; color:var(--red); border-color:#7a2222;}
  .tvol{width:52px; height:4px;}
  .ruler{display:grid; grid-template-columns:96px repeat(16,1fr); gap:5px; margin-bottom:8px;}
  .ruler span{font-size:9px; color:var(--dim); text-align:center; letter-spacing:1px;}
  .cell{
    aspect-ratio:1/.82; border-radius:5px; background:var(--cell);
    border:1px solid rgba(0,0,0,.6); box-shadow:inset 0 1px 0 rgba(255,255,255,.05);
    cursor:pointer; position:relative; transition:background .05s;
  }
  .cell.beat{margin-left:6px;}
  .row .cell:first-of-type{margin-left:0;}
  .cell.vel1{background:var(--cell-on); box-shadow:inset 0 0 6px rgba(120,140,255,.25);}
  .cell.vel2{background:#5b628a; box-shadow:0 0 10px rgba(130,150,255,.55), inset 0 0 8px rgba(180,190,255,.5);}
  .cell.ph{outline:2px solid rgba(255,255,255,.55); outline-offset:-2px; filter:brightness(1.5);}
  .cell.flash{filter:brightness(2.2);}
  .row.muted .cell{opacity:.28;}
  .row.muted .trackname{opacity:.45;}
  .foot{margin-top:12px; display:flex; gap:10px; justify-content:flex-end;}
  .hint{color:var(--dim); font-size:11px; margin-top:10px; letter-spacing:.5px;}
</style>
</head>
<body>
<div class="machine">
  <div class="brand"><div class="led" id="powerLed"></div><h1>DR-909</h1><small>STEP SEQUENCER</small>
    <span id="bpmVal">120</span></div>

  <div class="transport">
    <button id="playBtn">▶ PLAY</button>
    <div class="ctl"><label>Tempo <b id="bpmLabel">120 BPM</b></label>
      <input type="range" id="bpm" min="60" max="200" value="120" step="1"></div>
    <div class="ctl"><label>Swing <b id="swingLabel">0%</b></label>
      <input type="range" id="swing" min="0" max="60" value="0" step="1"></div>
    <div class="ctl"><label>Master <b id="volLabel">80%</b></label>
      <input type="range" id="masterVol" min="0" max="100" value="80" step="1"></div>
  </div>

  <div class="grid-wrap">
    <div class="ruler" id="ruler"></div>
    <div id="grid"></div>
    <div class="hint">Click: toggle · Right-click: accent · Drag: paint · Steps grouped in beats of 4</div>
    <div class="foot">
      <button id="clearBtn">CLEAR</button>
      <button id="randomBtn">RANDOM</button>
    </div>
  </div>
</div>

<script>
/* ================================================================
   TUNABLE CONSTANTS
================================================================ */
const CONST = {
  LOOKAHEAD_MS: 25,          // scheduler timer interval
  SCHEDULE_AHEAD_S: 0.12,    // how far ahead we schedule on the audio clock
  SCHEDULE_AHEAD_HIDDEN_S: 1.2, // extra lookahead when tab is throttled/backgrounded
  BPM_MIN: 60, BPM_MAX: 200, BPM_DEFAULT: 120,
  SWING_MAX: 60,             // percent of a 16th step
  STEPS: 16, TRACKS: 6,
  VEL: { OFF:0, NORM:1, ACCENT:2 },
  VEL_GAIN: { 1: 0.75, 2: 1.0 },
  // instrument patch params
  KICK:  { f0:150, f1:50, pitchDecay:0.09, ampDecay:0.42, gain:1.0 },
  SNARE: { bodyF:185, noiseDecay:0.18, bodyDecay:0.12, hpNorm:1200, hpAccent:2600, gain:0.85 },
  HAT:   { hp:7500, decayClosed:0.05, decayOpen:0.35, gain:0.5 },
  CLAP:  { bp:1400, q:1.4, bursts:[0, 0.011, 0.022], tailDecay:0.16, gain:0.8 },
  TOM:   { f:110, fDrop:0.85, decay:0.32, lpBase:900, lpAccent:3200, gain:0.8 },
  TRACK_COLORS: ['#ff5a4e','#ffb347','#53d8ff','#4ade80','#c084fc','#f472b6'],
  LS_KEY: 'dr909.pattern.v1'
};

/* ================================================================
   STATE
================================================================ */
// grid[track][step] : 0 off, 1 normal, 2 accent
const TRACK_NAMES = ['KICK','SNARE','HAT C','HAT O','CLAP','TOM'];
const state = {
  grid: Array.from({length: CONST.TRACKS}, () => new Array(CONST.STEPS).fill(0)),
  trackVol: [1,1,1,1,1,1],
  mute: [false,false,false,false,false,false],
  bpm: CONST.BPM_DEFAULT,
  swing: 0,
  master: 0.8,
  playing: false,
};

/* ================================================================
   AUDIO ENGINE
================================================================ */
let ctx = null, masterGain = null, comp = null, noiseBuf = null;
const trackGains = [];

function ensureAudio(){
  if (ctx) { if (ctx.state === 'suspended') ctx.resume(); return; }
  ctx = new (window.AudioContext || window.webkitAudioContext)();
  comp = ctx.createDynamicsCompressor();
  comp.threshold.value = -10; comp.ratio.value = 4; comp.attack.value = 0.003;
  masterGain = ctx.createGain();
  masterGain.gain.value = state.master;
  masterGain.connect(comp); comp.connect(ctx.destination);
  for (let i = 0; i < CONST.TRACKS; i++){
    const g = ctx.createGain();
    g.gain.value = state.mute[i] ? 0 : state.trackVol[i];
    g.connect(masterGain);
    trackGains.push(g);
  }
  // shared 2s white-noise buffer
  const len = ctx.sampleRate * 2;
  noiseBuf = ctx.createBuffer(1, len, ctx.sampleRate);
  const d = noiseBuf.getChannelData(0);
  for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
}

/* ---------------- Instruments (all scheduled at time t) ---------------- */
function noiseSrc(t, dur){
  const s = ctx.createBufferSource();
  s.buffer = noiseBuf; s.loop = true;
  s.start(t); s.stop(t + dur + 0.05);
  return s;
}
function env(t, peak, decay){
  const g = ctx.createGain();
  g.gain.setValueAtTime(0.0001, t);
  g.gain.exponentialRampToValueAtTime(Math.max(peak, 0.0001), t + 0.002);
  g.gain.exponentialRampToValueAtTime(0.0001, t + decay);
  return g;
}

function playKick(t, vel){
  const p = CONST.KICK, out = trackGains[0];
  const o = ctx.createOscillator(); o.type = 'sine';
  o.frequency.setValueAtTime(p.f0, t);
  o.frequency.exponentialRampToValueAtTime(p.f1, t + p.pitchDecay);
  const g = env(t, p.gain * CONST.VEL_GAIN[vel] * (vel === 2 ? 1.1 : 1), p.ampDecay * (vel === 2 ? 1.15 : 1));
  o.connect(g); g.connect(out);
  o.start(t); o.stop(t + p.ampDecay + 0.1);
}
function playSnare(t, vel){
  const p = CONST.SNARE, out = trackGains[1], vg = CONST.VEL_GAIN[vel];
  // noise snap through highpass (accent = brighter)
  const n = noiseSrc(t, p.noiseDecay + 0.1);
  const hp = ctx.createBiquadFilter(); hp.type = 'highpass';
  hp.frequency.value = vel === 2 ? p.hpAccent : p.hpNorm;
  const ng = env(t, 0.9 * vg, p.noiseDecay * (vel === 2 ? 1.2 : 1));
  n.connect(hp); hp.connect(ng); ng.connect(out);
  // tonal body
  const o = ctx.createOscillator(); o.type = 'triangle';
  o.frequency.setValueAtTime(p.bodyF, t);
  o.frequency.exponentialRampToValueAtTime(p.bodyF * 0.6, t + p.bodyDecay);
  const og = env(t, 0.7 * vg, p.bodyDecay);
  o.connect(og); og.connect(out);
  o.start(t); o.stop(t + p.bodyDecay + 0.1);
}
function playHat(t, vel, open){
  const p = CONST.HAT, out = open ? trackGains[3] : trackGains[2];
  const dur = open ? p.decayOpen : p.decayClosed;
  const n = noiseSrc(t, dur + 0.1);
  const hp = ctx.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = p.hp;
  const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 10000; bp.Q.value = 0.8;
  const g = env(t, p.gain * CONST.VEL_GAIN[vel], dur);
  n.connect(hp); hp.connect(bp); bp.connect(g); g.connect(out);
}
function playClap(t, vel){
  const p = CONST.CLAP, out = trackGains[4], vg = CONST.VEL_GAIN[vel];
  for (const off of p.bursts){
    const n = noiseSrc(t + off, 0.05);
    const bp = ctx.createBiquadFilter(); bp.type = 'bandpass';
    bp.frequency.value = p.bp; bp.Q.value = p.q;
    const g = env(t + off, 0.8 * vg, 0.035);
    n.connect(bp); bp.connect(g); g.connect(out);
  }
  // tail
  const n = noiseSrc(t + 0.022, p.tailDecay + 0.1);
  const bp = ctx.createBiquadFilter(); bp.type = 'bandpass';
  bp.frequency.value = p.bp; bp.Q.value = p.q;
  const g = env(t + 0.022, 0.6 * vg, p.tailDecay);
  n.connect(bp); bp.connect(g); g.connect(out);
}
function playTom(t, vel){
  const p = CONST.TOM, out = trackGains[5], vg = CONST.VEL_GAIN[vel];
  const f = p.f * (vel === 2 ? 1.0 : 0.94);
  const o = ctx.createOscillator(); o.type = 'sawtooth';
  o.frequency.setValueAtTime(f, t);
  o.frequency.exponentialRampToValueAtTime(f * p.fDrop, t + p.decay);
  const sub = ctx.createOscillator(); sub.type = 'sine';
  sub.frequency.setValueAtTime(f / 2, t);
  const lp = ctx.createBiquadFilter(); lp.type = 'lowpass';
  lp.frequency.setValueAtTime(vel === 2 ? p.lpAccent : p.lpBase, t);
  lp.frequency.exponentialRampToValueAtTime(250, t + p.decay);
  const g = env(t, p.gain * vg, p.decay);
  o.connect(lp); sub.connect(lp); lp.connect(g); g.connect(out);
  o.start(t); sub.start(t);
  o.stop(t + p.decay + 0.1); sub.stop(t + p.decay + 0.1);
}
const INSTRUMENTS = [playKick, playSnare, (t,v)=>playHat(t,v,false), (t,v)=>playHat(t,v,true), playClap, playTom];

/* ================================================================
   SCHEDULER — lookahead pattern on the Web Audio clock
================================================================ */
let currentStep = 0;        // absolute next step index (0..15)
let nextNoteTime = 0;       // audio-clock time of next step boundary (straight grid)
let timerID = null;
const drawQueue = [];       // {step, time, trackFlashes:[...]} for the rAF playhead
// exposed for testing: window.__schedLog = [{step, t, boundaryT}, ...]
window.__schedLog = [];

function stepDuration(){ return (60 / state.bpm) / 4; } // 16th note

function swingOffset(step){
  // off-beat 8th notes (steps 2,6,10,14) delayed by swing% of a 16th step
  return (step % 4 === 2) ? (state.swing / 100) * stepDuration() : 0;
}

function scheduleStep(step, t){
  const flashes = [];
  for (let tr = 0; tr < CONST.TRACKS; tr++){
    const v = state.grid[tr][step];
    if (v !== CONST.VEL.OFF && !state.mute[tr]){
      INSTRUMENTS[tr](t, v);
      flashes.push(tr);
    }
  }
  drawQueue.push({ step, time: t, flashes });
  window.__schedLog.push({ step, t, boundaryT: t - swingOffset(step) });
  if (window.__schedLog.length > 400) window.__schedLog.splice(0, 200);
}

function nextNote(){
  const sd = stepDuration();
  scheduleStep(currentStep, nextNoteTime + swingOffset(currentStep));
  nextNoteTime += sd;                 // straight grid — never accumulates drift
  currentStep = (currentStep + 1) % CONST.STEPS;
}

function scheduler(){
  if (!state.playing) return;
  const ahead = document.hidden ? CONST.SCHEDULE_AHEAD_HIDDEN_S : CONST.SCHEDULE_AHEAD_S;
  while (nextNoteTime < ctx.currentTime + ahead) nextNote();
}

function startPlayback(){
  ensureAudio();
  currentStep = 0;
  nextNoteTime = ctx.currentTime + 0.06; // restart cleanly on step 1
  state.playing = true;
  timerID = setInterval(scheduler, CONST.LOOKAHEAD_MS);
  scheduler();
  updatePlayUI(true);
}
function stopPlayback(){
  state.playing = false;
  clearInterval(timerID); timerID = null;
  drawQueue.length = 0;
  clearPlayhead();
  updatePlayUI(false);
}

/* ---------------- Playhead (rAF, driven by audio clock) ---------------- */
const cellEls = []; // cellEls[track][step]
let lastStepEl = null;
function drawLoop(){
  if (state.playing && ctx){
    const now = ctx.currentTime;
    while (drawQueue.length && drawQueue[0].time <= now){
      const ev = drawQueue.shift();
      setPlayhead(ev.step);
      flashCells(ev.flashes, ev.step);
    }
  }
  requestAnimationFrame(drawLoop);
}
function setPlayhead(step){
  if (lastStepEl !== null) for (const row of cellEls) row[lastStepEl].classList.remove('ph');
  for (const row of cellEls) row[step].classList.add('ph');
  lastStepEl = step;
}
function clearPlayhead(){
  if (lastStepEl !== null) for (const row of cellEls) row[lastStepEl].classList.remove('ph');
  lastStepEl = null;
}
function flashCells(tracks, step){
  for (const tr of tracks){
    const el = cellEls[tr][step];
    el.classList.add('flash');
    setTimeout(() => el.classList.remove('flash'), 90);
  }
}
requestAnimationFrame(drawLoop);

/* ================================================================
   UI — grid, controls, painting
================================================================ */
const gridEl = document.getElementById('grid');
const rulerEl = document.getElementById('ruler');

function buildUI(){
  // ruler
  for (let s = 0; s < CONST.STEPS; s++)
    rulerEl.appendChild(Object.assign(document.createElement('span'),
      { textContent: s % 4 === 0 ? String(s / 4 + 1) : '·' }));
  // rows
  for (let tr = 0; tr < CONST.TRACKS; tr++){
    const row = document.createElement('div'); row.className = 'row';
    const name = document.createElement('div'); name.className = 'trackname';
    const dot = document.createElement('div'); dot.className = 'dot';
    dot.style.background = CONST.TRACK_COLORS[tr];
    const label = document.createElement('span'); label.textContent = TRACK_NAMES[tr];
    const btns = document.createElement('div'); btns.className = 'tbtns';
    const muteBtn = document.createElement('button'); muteBtn.textContent = 'M';
    muteBtn.className = 'mute'; muteBtn.title = 'Mute';
    muteBtn.onclick = () => {
      state.mute[tr] = !state.mute[tr];
      muteBtn.classList.toggle('active', state.mute[tr]);
      row.classList.toggle('muted', state.mute[tr]);
      if (trackGains[tr]) trackGains[tr].gain.setTargetAtTime(state.mute[tr] ? 0 : state.trackVol[tr], ctx.currentTime, 0.01);
      save();
    };
    const vol = document.createElement('input');
    vol.type = 'range'; vol.className = 'tvol'; vol.min = 0; vol.max = 100; vol.value = 100;
    vol.title = 'Track volume';
    vol.oninput = () => {
      state.trackVol[tr] = vol.value / 100;
      if (trackGains[tr] && !state.mute[tr]) trackGains[tr].gain.setTargetAtTime(state.trackVol[tr], ctx.currentTime, 0.01);
      save();
    };
    btns.append(muteBtn, vol);
    name.append(dot, label, btns);
    row.appendChild(name);
    cellEls[tr] = [];
    for (let s = 0; s < CONST.STEPS; s++){
      const c = document.createElement('div');
      c.className = 'cell' + (s % 4 === 0 && s !== 0 ? ' beat' : '');
      c.dataset.tr = tr; c.dataset.s = s;
      c.addEventListener('pointerdown', e => onCellDown(e, tr, s));
      c.addEventListener('pointerenter', e => { if (painting) onCellDown(e, tr, s, true); });
      c.addEventListener('contextmenu', e => e.preventDefault());
      row.appendChild(c);
      cellEls[tr].push(c);
    }
    gridEl.appendChild(row);
  }
}

// painting: left = toggle off<->normal, right = accent; drag paints the chosen value
let painting = false, paintVal = 0, paintBtn = 0;
function onCellDown(e, tr, s, isEnter){
  if (isEnter){ state.grid[tr][s] = paintVal; refreshCell(tr, s); save(); return; }
  e.preventDefault();
  painting = true; paintBtn = e.button;
  const cur = state.grid[tr][s];
  if (e.button === 2){
    paintVal = (cur === 2) ? CONST.VEL.OFF : CONST.VEL.ACCENT;
  } else {
    paintVal = (cur === CONST.VEL.OFF) ? CONST.VEL.NORM : CONST.VEL.OFF;
  }
  state.grid[tr][s] = paintVal;
  refreshCell(tr, s); save();
  if (ctx) ensureAudio(); // resume on gesture
}
window.addEventListener('pointerup', () => painting = false);

function refreshCell(tr, s){
  const c = cellEls[tr][s], v = state.grid[tr][s];
  c.classList.toggle('vel1', v === 1);
  c.classList.toggle('vel2', v === 2);
}
function refreshAllCells(){
  for (let tr = 0; tr < CONST.TRACKS; tr++) for (let s = 0; s < CONST.STEPS; s++) refreshCell(tr, s);
}

/* ---------------- Transport controls ---------------- */
const playBtn = document.getElementById('playBtn');
const powerLed = document.getElementById('powerLed');
function updatePlayUI(on){
  playBtn.textContent = on ? '■ STOP' : '▶ PLAY';
  playBtn.classList.toggle('playing', on);
  powerLed.classList.toggle('on', on);
}
playBtn.onclick = () => state.playing ? stopPlayback() : startPlayback();

const bpmSlider = document.getElementById('bpm');
const bpmLabel = document.getElementById('bpmLabel');
const bpmVal = document.getElementById('bpmVal');
bpmSlider.oninput = () => {
  state.bpm = +bpmSlider.value;
  bpmLabel.textContent = state.bpm + ' BPM';
  bpmVal.textContent = state.bpm;
  save();
};

const swingSlider = document.getElementById('swing');
const swingLabel = document.getElementById('swingLabel');
swingSlider.oninput = () => {
  state.swing = +swingSlider.value;
  swingLabel.textContent = state.swing + '%';
  save();
};

const masterSlider = document.getElementById('masterVol');
const volLabel = document.getElementById('volLabel');
masterSlider.oninput = () => {
  state.master = masterSlider.value / 100;
  volLabel.textContent = masterSlider.value + '%';
  if (masterGain) masterGain.gain.setTargetAtTime(state.master, ctx.currentTime, 0.01);
  save();
};

document.getElementById('clearBtn').onclick = () => {
  state.grid = Array.from({length: CONST.TRACKS}, () => new Array(CONST.STEPS).fill(0));
  refreshAllCells(); save();
};
document.getElementById('randomBtn').onclick = () => {
  // musical-ish randomization
  const prob = [ [0,.9],[1,.25],[2,.7],[3,.12],[4,.3],[5,.2] ];
  const g = Array.from({length: CONST.TRACKS}, () => new Array(CONST.STEPS).fill(0));
  for (let tr = 0; tr < CONST.TRACKS; tr++)
    for (let s = 0; s < CONST.STEPS; s++){
      let p = prob[tr][1];
      if (tr === 0) p = (s % 4 === 0) ? 0.85 : 0.08;      // kick anchors beats
      if (tr === 1) p = (s === 4 || s === 12) ? 0.8 : 0.05; // snare backbeat
      if (tr === 4) p = (s === 4 || s === 12) ? 0.5 : 0.03;
      if (Math.random() < p) g[tr][s] = Math.random() < 0.3 ? 2 : 1;
    }
  state.grid = g; refreshAllCells(); save();
};

/* ================================================================
   PERSISTENCE
================================================================ */
let saveTimer = null;
function save(){
  clearTimeout(saveTimer);
  saveTimer = setTimeout(() => {
    try {
      localStorage.setItem(CONST.LS_KEY, JSON.stringify({
        grid: state.grid, trackVol: state.trackVol, mute: state.mute,
        bpm: state.bpm, swing: state.swing, master: state.master
      }));
    } catch(e){}
  }, 150);
}
function load(){
  try {
    const raw = localStorage.getItem(CONST.LS_KEY);
    if (!raw) return false;
    const d = JSON.parse(raw);
    if (!d || !Array.isArray(d.grid)) return false;
    state.grid = d.grid.map(r => Array.from({length: CONST.STEPS}, (_, i) => r[i] | 0));
    while (state.grid.length < CONST.TRACKS) state.grid.push(new Array(CONST.STEPS).fill(0));
    state.trackVol = d.trackVol || state.trackVol;
    state.mute = d.mute || state.mute;
    state.bpm = Math.min(CONST.BPM_MAX, Math.max(CONST.BPM_MIN, d.bpm || CONST.BPM_DEFAULT));
    state.swing = Math.min(CONST.SWING_MAX, Math.max(0, d.swing || 0));
    state.master = d.master ?? state.master;
    return true;
  } catch(e){ return false; }
}

/* ================================================================
   DEFAULT PATTERN
================================================================ */
function loadDefaultPattern(){
  const P = (row, steps, acc = []) => {
    steps.forEach(s => state.grid[row][s] = acc.includes(s) ? 2 : 1);
  };
  P(0, [0,4,8,12], [0]);            // kick four-on-floor, accent 1
  P(1, [12]);                        // snare layering with clap at 12
  P(2, [2,6,10,14], [2,6,10,14]);    // closed hat offbeats, accented
  P(3, [14]);                        // open hat at end of bar 2
  P(4, [4,12]);                      // clap backbeat
  P(5, [7,15], [15]);                // tom stab pickups
}

/* ================================================================
   INIT
================================================================ */
buildUI();
if (!load()) loadDefaultPattern();
refreshAllCells();
bpmSlider.value = state.bpm; bpmLabel.textContent = state.bpm + ' BPM'; bpmVal.textContent = state.bpm;
swingSlider.value = state.swing; swingLabel.textContent = state.swing + '%';
masterSlider.value = Math.round(state.master * 100); volLabel.textContent = Math.round(state.master * 100) + '%';
cellEls.forEach((row, tr) => {
  if (state.mute[tr]){
    row[0].closest('.row').classList.add('muted');
    row[0].closest('.trackname').querySelector('.mute').classList.add('active');
  }
});
window.addEventListener('beforeunload', () => { clearTimeout(saveTimer); save(); });
</script>
</body>
</html>
<!-- agent-meta {"model":"z-ai/glm-5.3-flash","provider":"openrouter","persona":"full","sessionId":"7b9b7754-4b54-49c3-b212-3a4447ecadc8","tokensIn":100138,"tokensOut":10413,"tokensTotal":110551,"cost":0.005835839999999999,"turns":7,"toolCalls":6,"failedToolCalls":1,"timestamp":"2026-08-28T04:16:44.777Z"} -->