← Sequencer results

Sequencer

muse spark 1.2 contributor · 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 39,134 bytes · SHA-256 53bbe312a69f
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DRM-16 — Step Sequencer</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Space+Grotesk:wght@500;700&display=swap');
  :root{
    --bg:#080a0f;
    --panel:#13151d;
    --panel2:#1a1d2a;
    --border:#232636;
    --grid:#1e2133;
    --text:#e8eaf0;
    --muted:#6b708a;
    --accent:#ff3b30;
    --accent2:#ff7a00;
    --kick:#ff3b30;
    --snare:#ffcc00;
    --ch:#00e5ff;
    --oh:#00e5ff;
    --clap:#a855f7;
    --tom:#22c55e;
    --led:#ff3b30;
    --led-dim:#3a1a1a;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  body{
    background:var(--bg);
    color:var(--text);
    font-family:'Space Grotesk',system-ui,sans-serif;
    min-height:100vh;
    overflow-x:hidden;
    background:
      radial-gradient(1200px 600px at 20% -10%, #1a1a3a 0%, transparent 60%),
      radial-gradient(900px 500px at 90% 0%, #2a1a1a 0%, transparent 60%),
      var(--bg);
  }
  /* header */
  .topbar{
    max-width:1100px;margin:0 auto;
    padding:18px 20px 10px;
    display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;
  }
  .brand{display:flex;align-items:center;gap:14px}
  .logo{
    width:44px;height:44px;background:linear-gradient(135deg,#ff3b30,#ff7a00);
    border-radius:10px;display:grid;place-items:center;font-weight:800;font-size:18px;letter-spacing:-1px;
    box-shadow:0 4px 20px rgba(255,59,48,.4), inset 0 1px 0 rgba(255,255,255,.3);
  }
  .brand h1{font-size:22px;letter-spacing:1.5px;font-weight:700;line-height:1}
  .brand h1 span{color:var(--muted);font-weight:500;font-size:12px;letter-spacing:3px;display:block;margin-top:2px}
  .status{
    display:flex;align-items:center;gap:10px;
    background:var(--panel);border:1px solid var(--border);border-radius:999px;padding:6px 14px;
  }
  .led{width:9px;height:9px;border-radius:50%;background:var(--led-dim);box-shadow:0 0 0 4px rgba(255,59,48,.08);transition:.2s}
  .led.on{background:var(--led);box-shadow:0 0 12px var(--led),0 0 0 4px rgba(255,59,48,.15)}
  .status span{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:1px;color:var(--muted)}
  .status b{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text)}

  /* main container */
  .machine{
    max-width:1100px;margin:10px auto 30px;
    background:linear-gradient(180deg,#151821 0%, #0f111a 100%);
    border:1px solid var(--border);
    border-radius:20px;
    box-shadow:0 20px 60px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.06);
    overflow:hidden;
  }
  /* transport */
  .transport{
    display:flex;align-items:center;gap:14px;flex-wrap:wrap;
    padding:18px 20px;
    background:rgba(255,255,255,.02);
    border-bottom:1px solid var(--border);
  }
  .play-btn{
    width:64px;height:64px;border-radius:16px;border:none;cursor:pointer;
    background:linear-gradient(135deg,#ff3b30,#ff5420);
    color:white;font-size:22px;display:grid;place-items:center;
    box-shadow:0 8px 24px rgba(255,59,48,.45), inset 0 1px 0 rgba(255,255,255,.3);
    transition:.15s;
  }
  .play-btn:active{transform:scale(.96)}
  .play-btn.playing{background:linear-gradient(135deg,#1a1a1a,#2a2a2a);border:1px solid #333;box-shadow:inset 0 1px 0 rgba(255,255,255,.08)}
  .stop-btn{
    width:44px;height:44px;border-radius:12px;border:1px solid var(--border);
    background:var(--panel2);color:var(--muted);cursor:pointer;display:grid;place-items:center;font-size:14px;
  }
  .stop-btn:hover{color:var(--text);border-color:#3a3d55}
  .readout{
    background:#05070a;border:1px solid #1e2133;border-radius:12px;padding:10px 14px;min-width:110px;
    box-shadow:inset 0 2px 8px rgba(0,0,0,.6);
  }
  .readout label{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:1.5px;color:var(--muted);display:block}
  .readout .val{font-family:'JetBrains Mono',monospace;font-size:28px;font-weight:700;letter-spacing:-1px;line-height:1;margin-top:2px}
  .readout .val small{font-size:11px;color:var(--muted);font-weight:400;letter-spacing:1px}
  .sliders{flex:1;display:flex;gap:16px;flex-wrap:wrap;align-items:center}
  .ctrl{flex:1;min-width:140px;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:10px 12px}
  .ctrl-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}
  .ctrl-head label{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:1.2px;color:var(--muted)}
  .ctrl-head output{font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:700;background:#0b0d14;padding:2px 6px;border-radius:6px;border:1px solid #232636}
  input[type=range]{width:100%;-webkit-appearance:none;height:4px;background:#232636;border-radius:999px;outline:none}
  input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:white;box-shadow:0 2px 8px rgba(0,0,0,.4);cursor:pointer;border:2px solid #ff3b30}
  .tempo-ctrl output{color:#ff3b30;border-color:rgba(255,59,48,.3)}
  .swing-ctrl output{color:#00e5ff;border-color:rgba(0,229,255,.3)}
  .vol-ctrl output{color:#22c55e;border-color:rgba(34,197,94,.3)}
  .actions{display:flex;gap:8px;flex-wrap:wrap}
  .btn{
    font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.8px;
    padding:10px 14px;border-radius:10px;border:1px solid var(--border);
    background:var(--panel2);color:var(--text);cursor:pointer;transition:.15s;
  }
  .btn:hover{border-color:#3a3d55;transform:translateY(-1px)}
  .btn.ghost{color:var(--muted)}
  .btn.primary{background:linear-gradient(135deg,#ff3b30,#ff7a00);border-color:transparent;color:white;box-shadow:0 4px 16px rgba(255,59,48,.3)}

  /* sequencer */
  .seq-wrap{padding:16px 16px 8px;overflow-x:auto}
  .seq{
    min-width:720px;
    display:grid;
    grid-template-columns:140px repeat(16,1fr);
    gap:6px;
    align-items:center;
  }
  .seq-head{
    display:contents;
  }
  .seq-head .corner{height:28px}
  .step-num{
    text-align:center;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--muted);
    padding:4px 0;position:relative;
  }
  .step-num b{color:var(--text);font-size:11px}
  .step-num::after{content:'';position:absolute;bottom:-6px;left:50%;width:2px;height:6px;background:#232636;transform:translateX(-50%);border-radius:1px}
  .step-num.beat::after{background:#ff3b30;height:8px}
  .playhead-track{
    grid-column:2 / span 16;
    height:4px;background:#0b0d14;border-radius:999px;position:relative;overflow:hidden;
    border:1px solid #1e2133;
  }
  .playhead{
    position:absolute;top:0;bottom:0;width:calc(100% / 16);
    background:linear-gradient(90deg,transparent,#ff3b30,transparent);
    opacity:0;transition:opacity .15s;
    box-shadow:0 0 12px #ff3b30;
  }
  .playhead.on{opacity:1}
  /* track row */
  .track-label{
    display:flex;align-items:center;gap:10px;
    background:var(--panel);border:1px solid var(--border);border-radius:12px;
    padding:8px 10px;height:56px;
  }
  .track-label.muted{opacity:.45}
  .track-icon{
    width:36px;height:36px;border-radius:9px;display:grid;place-items:center;font-size:14px;flex-shrink:0;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.15);
  }
  .track-info{flex:1;min-width:0}
  .track-name{font-size:12px;font-weight:700;letter-spacing:.5px;line-height:1}
  .track-sub{font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted);letter-spacing:.8px}
  .track-controls{display:flex;align-items:center;gap:6px}
  .mute-btn,.solo-btn{
    width:28px;height:22px;border-radius:6px;border:1px solid var(--border);
    background:#0b0d14;color:var(--muted);font-family:'JetBrains Mono',monospace;font-size:9px;cursor:pointer;
  }
  .mute-btn.active{background:#ff3b30;color:white;border-color:#ff3b30;box-shadow:0 2px 10px rgba(255,59,48,.4)}
  .solo-btn.active{background:#ffcc00;color:#111;border-color:#ffcc00}
  .vol-mini{width:54px}
  .vol-mini input{height:3px}
  .vol-mini input::-webkit-slider-thumb{width:12px;height:12px;border-width:1.5px}
  /* cells */
  .cell{
    height:56px;border-radius:10px;
    background:linear-gradient(180deg,#1a1d2a,#13151d);
    border:1px solid #232636;
    cursor:pointer;position:relative;overflow:hidden;
    transition:.12s;user-select:none;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.04);
  }
  .cell:hover{border-color:#3a3d55;transform:translateY(-1px)}
  .cell.on{
    background:linear-gradient(180deg,#2a2d3f,#1e2133);
    border-color:rgba(255,255,255,.12);
    box-shadow:0 4px 16px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.08);
  }
  .cell.on::before{
    content:'';position:absolute;inset:0;border-radius:10px;opacity:.9;
  }
  .cell.kick.on::before{background:radial-gradient(120px 80px at 50% 30%, rgba(255,59,48,.9), rgba(255,59,48,.15) 70%, transparent 75%)}
  .cell.snare.on::before{background:radial-gradient(120px 80px at 50% 30%, rgba(255,204,0,.85), rgba(255,204,0,.12) 70%, transparent 75%)}
  .cell.ch.on::before{background:radial-gradient(120px 80px at 50% 30%, rgba(0,229,255,.85), rgba(0,229,255,.12) 70%, transparent 75%)}
  .cell.oh.on::before{background:radial-gradient(120px 80px at 50% 30%, rgba(0,229,255,.55), rgba(0,229,255,.08) 70%, transparent 75%)}
  .cell.clap.on::before{background:radial-gradient(120px 80px at 50% 30%, rgba(168,85,247,.85), rgba(168,85,247,.12) 70%, transparent 75%)}
  .cell.tom.on::before{background:radial-gradient(120px 80px at 50% 30%, rgba(34,197,94,.85), rgba(34,197,94,.12) 70%, transparent 75%)}
  .cell.on::after{
    content:'';position:absolute;left:50%;top:50%;width:8px;height:8px;transform:translate(-50%,-50%);
    background:white;border-radius:50%;box-shadow:0 0 10px white, 0 0 20px currentColor;opacity:.95;
  }
  .cell.accent::after{width:14px;height:14px;box-shadow:0 0 14px white,0 0 28px currentColor}
  .cell.accent{border-color:rgba(255,255,255,.22);box-shadow:0 6px 20px rgba(0,0,0,.4), 0 0 16px rgba(255,255,255,.08)}
  .cell.playing{
    outline:2px solid #ff3b30;outline-offset:1px;z-index:2;
    box-shadow:0 0 18px rgba(255,59,48,.6);
  }
  .cell.beat-sep{border-right:2px solid rgba(255,59,48,.18)}
  /* bottom */
  .foot{
    display:flex;gap:12px;flex-wrap:wrap;align-items:center;justify-content:space-between;
    padding:12px 16px 16px;border-top:1px solid var(--border);margin-top:10px;
    font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--muted);
  }
  .foot kbd{background:#0b0d14;border:1px solid #232636;border-bottom-width:2px;padding:2px 6px;border-radius:5px;color:var(--text);font-size:10px}
  .legend{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
  .dot{width:10px;height:10px;border-radius:50%;background:var(--grid);border:1px solid #232636}
  .dot.on{background:white;box-shadow:0 0 8px white}
  .dot.accent{width:14px;height:14px}
  @media(max-width:860px){
    .seq{grid-template-columns:120px repeat(16,1fr)}
    .transport{padding:14px}
    .readout .val{font-size:24px}
  }
</style>
</head>
<body>
  <div class="topbar">
    <div class="brand">
      <div class="logo">◉</div>
      <h1>DRM-16 <span>STEP SEQUENCER • 16 STEPS • 6 VOICES</span></h1>
    </div>
    <div class="status">
      <div class="led" id="led"></div>
      <span id="statusText">STOPPED</span>
      <b id="stepReadout">—</b>
    </div>
  </div>

  <div class="machine">
    <div class="transport">
      <button class="play-btn" id="playBtn" aria-label="Play">▶</button>
      <button class="stop-btn" id="stopBtn" aria-label="Stop">■</button>

      <div class="readout">
        <label>BPM</label>
        <div class="val" id="bpmVal">128 <small>BPM</small></div>
      </div>

      <div class="sliders">
        <div class="ctrl tempo-ctrl">
          <div class="ctrl-head"><label>TEMPO</label><output id="bpmOut">128</output></div>
          <input type="range" id="bpmSlider" min="60" max="200" value="128">
        </div>
        <div class="ctrl swing-ctrl">
          <div class="ctrl-head"><label>SWING</label><output id="swingOut">0%</output></div>
          <input type="range" id="swingSlider" min="0" max="60" value="0">
        </div>
        <div class="ctrl vol-ctrl">
          <div class="ctrl-head"><label>MASTER</label><output id="masterOut">85%</output></div>
          <input type="range" id="masterSlider" min="0" max="100" value="85">
        </div>
      </div>

      <div class="actions">
        <button class="btn ghost" id="clearBtn">CLEAR</button>
        <button class="btn ghost" id="randomBtn">RANDOM</button>
        <button class="btn primary" id="saveBtn">SAVE</button>
      </div>
    </div>

    <div class="seq-wrap">
      <div class="seq" id="seq"></div>
    </div>

    <div class="foot">
      <div>Click to toggle • Drag to paint • <kbd>Right-click</kbd> for accent • <kbd>Space</kbd> Play/Stop</div>
      <div class="legend">
        <span style="display:flex;align-items:center;gap:6px"><span class="dot"></span> off</span>
        <span style="display:flex;align-items:center;gap:6px"><span class="dot on"></span> on</span>
        <span style="display:flex;align-items:center;gap:6px"><span class="dot accent on"></span> accent</span>
        <span style="margin-left:8px;opacity:.6">Pattern auto-saves to localStorage</span>
      </div>
    </div>
  </div>

<script>
// ============================================================
// TUNABLE CONSTANTS
// ============================================================
const CONFIG = {
  BPM_MIN: 60,
  BPM_MAX: 200,
  DEFAULT_BPM: 128,
  DEFAULT_SWING: 16, // %
  DEFAULT_MASTER: 85,
  LOOKAHEAD_MS: 25,          // scheduler interval
  SCHEDULE_AHEAD_S: 0.12,    // how far ahead to schedule
  STEPS: 16,
  TRACKS: 6,
  // instrument volumes (0-1)
  TRACK_DEFAULT_VOL: [0.95, 0.85, 0.7, 0.65, 0.8, 0.8],
};

// Track definitions
const TRACKS = [
  { id:'kick',  name:'KICK',  sub:'BD • 50Hz',  color:'#ff3b30', icon:'●' },
  { id:'snare', name:'SNARE', sub:'SD • 180Hz', color:'#ffcc00', icon:'◎' },
  { id:'ch',    name:'HI-HAT',sub:'CH • CLOSED',color:'#00e5ff', icon:'◐' },
  { id:'oh',    name:'OPEN HAT',sub:'OH • OPEN',color:'#00e5ff', icon:'◑' },
  { id:'clap',  name:'CLAP',  sub:'CP • BURST', color:'#a855f7', icon:'▦' },
  { id:'tom',   name:'TOM',   sub:'LT • 90Hz',  color:'#22c55e', icon:'⬢' },
];

// Default groovy pattern (house)
const DEFAULT_PATTERN = [
  // kick: 4 on floor
  [2,0,0,0, 2,0,0,0, 2,0,0,0, 2,0,0,1],
  // snare: 5 and 13 with accents
  [0,0,0,0, 2,0,0,0, 0,0,0,0, 2,0,1,0],
  // ch: off-beats
  [0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,1],
  // oh: occasional
  [0,0,0,0, 0,0,0,1, 0,0,0,0, 0,0,0,0],
  // clap: on 5
  [0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0],
  // tom: fill
  [0,0,0,0, 0,0,0,0, 0,0,1,0, 0,1,0,0],
];

// ============================================================
// STATE
// ============================================================
let pattern = JSON.parse(JSON.stringify(DEFAULT_PATTERN));
let trackVol = [...CONFIG.TRACK_DEFAULT_VOL];
let trackMute = [false,false,false,false,false,false];
let trackSolo = [false,false,false,false,false,false];
let tempo = CONFIG.DEFAULT_BPM;
let swing = CONFIG.DEFAULT_SWING; // 0-60
let masterVol = CONFIG.DEFAULT_MASTER/100;

let audioCtx = null;
let masterGain = null;
let isPlaying = false;
let currentStep = 0;          // scheduler step index
let nextNoteTime = 0;         // audio time for next step (base, without swing)
let timerID = null;
let scheduledQueue = [];      // {step, time} for playhead
window.scheduledQueue = scheduledQueue;
window.schedulerLog = [];     // for probe verification

// drag painting
let isDragging = false;
let dragMode = 1; // 1 = paint on, 0 = erase
let dragStartVal = 0;

// ============================================================
// AUDIO CONTEXT
// ============================================================
function ensureAudio(){
  if(!audioCtx){
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    masterGain = audioCtx.createGain();
    masterGain.gain.value = masterVol;
    masterGain.connect(audioCtx.destination);
    window.audioCtx = audioCtx; // expose for probe
  }
  if(audioCtx.state === 'suspended') audioCtx.resume();
  return audioCtx;
}

// ============================================================
// INSTRUMENTS — all synthesized, no samples
// ============================================================
function playKick(time, vel, vol){
  const ctx = audioCtx;
  const gain = ctx.createGain();
  gain.connect(masterGain);
  const osc = ctx.createOscillator();
  osc.type = 'sine';
  osc.frequency.setValueAtTime(150, time);
  osc.frequency.exponentialRampToValueAtTime(48, time+0.08);
  // pitch envelope second stage
  osc.frequency.exponentialRampToValueAtTime(38, time+0.3);
  gain.gain.setValueAtTime(0, time);
  const peak = (vel===2?1:0.82) * vol;
  gain.gain.linearRampToValueAtTime(peak, time+0.002);
  gain.gain.exponentialRampToValueAtTime(0.01, time+0.45);
  osc.connect(gain);
  osc.start(time);
  osc.stop(time+0.5);
  // click
  const osc2 = ctx.createOscillator();
  const g2 = ctx.createGain();
  osc2.type='sine'; osc2.frequency.setValueAtTime(90, time);
  g2.gain.setValueAtTime(0,time);
  g2.gain.linearRampToValueAtTime(0.35*vol*(vel===2?1:0.7), time+0.001);
  g2.gain.exponentialRampToValueAtTime(0.001, time+0.04);
  osc2.connect(g2); g2.connect(masterGain);
  osc2.start(time); osc2.stop(time+0.05);
}

function playSnare(time, vel, vol){
  const ctx = audioCtx;
  const accent = vel===2;
  // noise burst
  const bufferSize = Math.floor(ctx.sampleRate*0.22);
  const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
  const data = buffer.getChannelData(0);
  for(let i=0;i<bufferSize;i++) data[i]= (Math.random()*2-1) * Math.pow(1 - i/bufferSize, 0.8);
  const src = ctx.createBufferSource(); src.buffer=buffer;
  const hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.setValueAtTime(accent? 1800: 1400, time);
  const bp = ctx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.setValueAtTime(180, time); bp.Q.value=1.2;
  const gNoise = ctx.createGain();
  const gTone = ctx.createGain();
  gNoise.gain.setValueAtTime(0,time);
  gNoise.gain.linearRampToValueAtTime(0.9*vol*(accent?1:0.7), time+0.002);
  gNoise.gain.exponentialRampToValueAtTime(0.01, time+0.18);
  // tonal body
  const osc = ctx.createOscillator(); osc.type='sine';
  osc.frequency.setValueAtTime(180, time);
  osc.frequency.exponentialRampToValueAtTime(90, time+0.12);
  gTone.gain.setValueAtTime(0,time);
  gTone.gain.linearRampToValueAtTime(0.55*vol, time+0.005);
  gTone.gain.exponentialRampToValueAtTime(0.01, time+0.18);
  src.connect(hp); hp.connect(gNoise); gNoise.connect(masterGain);
  osc.connect(bp); bp.connect(gTone); gTone.connect(masterGain);
  src.start(time); src.stop(time+0.25);
  osc.start(time); osc.stop(time+0.25);
}

function playHiHat(time, vel, vol, isOpen){
  const ctx = audioCtx;
  const dur = isOpen? 0.38 : 0.07;
  const bufferSize = Math.floor(ctx.sampleRate*0.5);
  const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
  const d = buffer.getChannelData(0);
  for(let i=0;i<bufferSize;i++) d[i]=Math.random()*2-1;
  const src = ctx.createBufferSource(); src.buffer=buffer;
  const hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value= 7500;
  const hp2 = ctx.createBiquadFilter(); hp2.type='highpass'; hp2.frequency.value= 9500;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0,time);
  const peak = (vel===2?0.55:0.38) * vol * (isOpen?0.9:1);
  gain.gain.linearRampToValueAtTime(peak, time+0.002);
  gain.gain.exponentialRampToValueAtTime(0.01, time+dur);
  src.connect(hp); hp.connect(hp2); hp2.connect(gain); gain.connect(masterGain);
  src.start(time); src.stop(time+dur+0.05);
  // metallic tone for accent
  if(vel===2){
    const o = ctx.createOscillator(); o.type='square';
    o.frequency.setValueAtTime(320, time);
    const g = ctx.createGain();
    g.gain.setValueAtTime(0,time);
    g.gain.linearRampToValueAtTime(0.12*vol, time+0.001);
    g.gain.exponentialRampToValueAtTime(0.001, time+0.06);
    const f = ctx.createBiquadFilter(); f.type='highpass'; f.frequency.value=6000;
    o.connect(f); f.connect(g); g.connect(masterGain);
    o.start(time); o.stop(time+0.07);
  }
}

function playClap(time, vel, vol){
  const ctx = audioCtx;
  const accent = vel===2;
  const bursts = accent? 4 : 3;
  for(let b=0;b<bursts;b++){
    const t = time + b*0.032 + (b===0?0: Math.random()*0.006);
    const bufferSize = Math.floor(ctx.sampleRate*0.09);
    const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
    const d = buffer.getChannelData(0);
    for(let i=0;i<bufferSize;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/bufferSize, 1.2);
    const src = ctx.createBufferSource(); src.buffer=buffer;
    const bp = ctx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value= 1300; bp.Q.value=0.9;
    const hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value= 700;
    const g = ctx.createGain();
    g.gain.setValueAtTime(0,t);
    const peak = (0.7 - b*0.12) * vol * (accent?1.15:1);
    g.gain.linearRampToValueAtTime(Math.max(0.08,peak), t+0.002);
    g.gain.exponentialRampToValueAtTime(0.01, t+0.14);
    src.connect(bp); bp.connect(hp); hp.connect(g); g.connect(masterGain);
    src.start(t); src.stop(t+0.16);
  }
}

function playTom(time, vel, vol){
  const ctx = audioCtx;
  const accent = vel===2;
  const osc = ctx.createOscillator(); osc.type='sine';
  const base = 92; // tuned relative to kick (50Hz)
  osc.frequency.setValueAtTime(base*1.8, time);
  osc.frequency.exponentialRampToValueAtTime(base, time+0.22);
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0,time);
  gain.gain.linearRampToValueAtTime((accent?0.9:0.65)*vol, time+0.004);
  gain.gain.exponentialRampToValueAtTime(0.01, time+0.42);
  // add subtle overtone for accent
  let osc2=null, g2=null;
  if(accent){
    osc2 = ctx.createOscillator(); osc2.type='sine';
    osc2.frequency.setValueAtTime(base*2, time);
    osc2.frequency.exponentialRampToValueAtTime(base*1.5, time+0.15);
    g2 = ctx.createGain();
    g2.gain.setValueAtTime(0,time);
    g2.gain.linearRampToValueAtTime(0.22*vol, time+0.003);
    g2.gain.exponentialRampToValueAtTime(0.001, time+0.18);
    osc2.connect(g2); g2.connect(masterGain);
    osc2.start(time); osc2.stop(time+0.25);
  }
  osc.connect(gain); gain.connect(masterGain);
  osc.start(time); osc.stop(time+0.5);
}

function triggerInstrument(trackIdx, time, vel){
  if(vel===0) return;
  // check mute/solo
  const anySolo = trackSolo.some(s=>s);
  if(anySolo && !trackSolo[trackIdx]) return;
  if(!anySolo && trackMute[trackIdx]) return;
  const vol = trackVol[trackIdx];
  switch(trackIdx){
    case 0: playKick(time, vel, vol); break;
    case 1: playSnare(time, vel, vol); break;
    case 2: playHiHat(time, vel, vol, false); break;
    case 3: playHiHat(time, vel, vol, true); break;
    case 4: playClap(time, vel, vol); break;
    case 5: playTom(time, vel, vol); break;
  }
}

// ============================================================
// SCHEDULER — lookahead pattern (sample-accurate)
// ============================================================
function scheduler(){
  if(!isPlaying || !audioCtx) return;
  const now = audioCtx.currentTime;
  const stepDur = 60 / tempo / 4; // 16th note
  const swingFrac = swing/100; // 0-0.6
  while(nextNoteTime < now + CONFIG.SCHEDULE_AHEAD_S){
    const step = currentStep;
    const isOdd = step % 2 === 1;
    const swingOffset = isOdd ? stepDur * swingFrac : 0;
    const schedTime = nextNoteTime + swingOffset;
    // schedule all tracks for this step
    for(let t=0;t<CONFIG.TRACKS;t++){
      const vel = pattern[t][step];
      if(vel) triggerInstrument(t, schedTime, vel);
    }
    // log for probe
    scheduledQueue.push({step, time:schedTime});
    if(scheduledQueue.length>64) scheduledQueue.shift();
    window.schedulerLog.push({step, time:schedTime, tempo, swing});
    if(window.schedulerLog.length>200) window.schedulerLog.shift();

    // advance
    nextNoteTime += stepDur;
    currentStep = (currentStep + 1) % CONFIG.STEPS;
  }
}

function startPlayback(){
  ensureAudio();
  if(isPlaying) return;
  isPlaying = true;
  currentStep = 0;
  nextNoteTime = audioCtx.currentTime + 0.06; // small head start
  scheduledQueue.length=0;
  window.schedulerLog.length=0;
  timerID = setInterval(scheduler, CONFIG.LOOKAHEAD_MS);
  scheduler(); // immediate
  updatePlayUI();
}

function stopPlayback(){
  isPlaying = false;
  if(timerID) clearInterval(timerID);
  timerID=null;
  scheduledQueue.length=0;
  // clear playhead highlights
  document.querySelectorAll('.cell.playing').forEach(c=>c.classList.remove('playing'));
  updatePlayUI();
  document.getElementById('stepReadout').textContent='—';
  document.getElementById('led').classList.remove('on');
}

function updatePlayUI(){
  const btn = document.getElementById('playBtn');
  const led = document.getElementById('led');
  const st = document.getElementById('statusText');
  if(isPlaying){
    btn.textContent='❚❚';
    btn.classList.add('playing');
    led.classList.add('on');
    st.textContent='PLAYING';
  } else {
    btn.textContent='▶';
    btn.classList.remove('playing');
    led.classList.remove('on');
    st.textContent='STOPPED';
  }
}

// ============================================================
// VISUAL PLAYHEAD — driven from AudioContext clock (rAF)
// ============================================================
let rafId = null;
function playheadLoop(){
  rafId = requestAnimationFrame(playheadLoop);
  if(!isPlaying || !audioCtx || scheduledQueue.length===0) return;
  const now = audioCtx.currentTime;
  // find current step: the latest scheduled step with time <= now < time+stepDur*1.2
  // also handle swing: step duration varies, but we approximate with 60/tempo/4
  let activeStep = -1;
  // scheduledQueue is sorted by time ascending
  for(let i=scheduledQueue.length-1;i>=0;i--){
    if(scheduledQueue[i].time <= now){
      // check if next event is in future (not yet passed)
      // if this event is the most recent past event, it's current
      activeStep = scheduledQueue[i].step;
      break;
    }
  }
  // if no past event yet (before first scheduled), use first upcoming
  if(activeStep===-1 && scheduledQueue[0].time > now){
    // before first note, show step 0 as upcoming
    activeStep = -1;
  }
  // update DOM
  const stepDur = 60/tempo/4;
  // highlight cells
  document.querySelectorAll('.cell').forEach(c=>c.classList.remove('playing'));
  if(activeStep>=0){
    document.querySelectorAll(`.cell[data-step="${activeStep}"]`).forEach(c=>c.classList.add('playing'));
    document.getElementById('stepReadout').textContent = String(activeStep+1).padStart(2,'0');
    // pulse led on step
    const led = document.getElementById('led');
    // blink on every step
    led.style.opacity = '1';
  }
  // playhead bar position
  const bar = document.getElementById('playhead');
  if(activeStep>=0 && bar){
    bar.classList.add('on');
    // account for swing visually? Keep bar at step position, but nudge for swing offset to track audio exactly
    // Compute exact playhead position interpolated between steps using audio time
    // Find the active event time and next event time
    let idx = scheduledQueue.findIndex(e=>e.step===activeStep && e.time<=now);
    // find last occurrence of activeStep that is <=now
    let activeEntry = null;
    for(let i=scheduledQueue.length-1;i>=0;i--) if(scheduledQueue[i].step===activeStep && scheduledQueue[i].time<=now){ activeEntry=scheduledQueue[i]; break; }
    let nextEntry = null;
    if(activeEntry){
      const ai = scheduledQueue.indexOf(activeEntry);
      nextEntry = scheduledQueue[ai+1] || null;
    }
    let frac = 0;
    if(activeEntry && nextEntry){
      const span = nextEntry.time - activeEntry.time;
      if(span>0) frac = Math.min(1, Math.max(0, (now - activeEntry.time)/span));
    }
    const stepWidth = 100/CONFIG.STEPS;
    const pos = activeStep*stepWidth + frac*stepWidth;
    bar.style.left = pos + '%';
    bar.style.width = stepWidth + '%';
  } else if(bar){
    bar.classList.remove('on');
  }
}
playheadLoop();

// ============================================================
// GRID BUILD
// ============================================================
function buildGrid(){
  const seq = document.getElementById('seq');
  seq.innerHTML='';

  // header row
  const head = document.createElement('div');
  head.className='seq-head';
  const corner = document.createElement('div');
  corner.className='corner';
  head.appendChild(corner);
  for(let s=0;s<CONFIG.STEPS;s++){
    const n = document.createElement('div');
    n.className='step-num' + (s%4===0?' beat':'');
    n.innerHTML = `<b>${s+1}</b>`;
    head.appendChild(n);
  }
  seq.appendChild(head);

  // playhead track
  const phRow = document.createElement('div');
  phRow.style.display='contents';
  const phLabel = document.createElement('div');
  phLabel.style.height='4px';
  phRow.appendChild(phLabel);
  const track = document.createElement('div');
  track.className='playhead-track';
  track.innerHTML='<div class="playhead" id="playhead"></div>';
  phRow.appendChild(track);
  seq.appendChild(phRow);

  // track rows
  for(let t=0;t<CONFIG.TRACKS;t++){
    const tr = TRACKS[t];
    const label = document.createElement('div');
    label.className='track-label' + (trackMute[t]?' muted':'');
    label.dataset.track=t;
    label.innerHTML=`
      <div class="track-icon" style="background:${tr.color};color:${t===1?'#111':'white'}">${tr.icon}</div>
      <div class="track-info">
        <div class="track-name">${tr.name}</div>
        <div class="track-sub">${tr.sub}</div>
      </div>
      <div class="track-controls">
        <button class="mute-btn ${trackMute[t]?'active':''}" data-mute="${t}" title="Mute">M</button>
        <button class="solo-btn ${trackSolo[t]?'active':''}" data-solo="${t}" title="Solo">S</button>
      </div>
    `;
    // volume mini
    const volWrap = document.createElement('div');
    volWrap.className='vol-mini';
    volWrap.innerHTML=`<input type="range" min="0" max="100" value="${Math.round(trackVol[t]*100)}" data-vol="${t}" title="Volume">`;
    label.querySelector('.track-controls').appendChild(volWrap);
    seq.appendChild(label);

    for(let s=0;s<CONFIG.STEPS;s++){
      const cell = document.createElement('div');
      const vel = pattern[t][s];
      const cls = TRACKS[t].id;
      cell.className = `cell ${cls} ${vel? 'on':''} ${vel===2?'accent':''} ${s%4===3?'beat-sep':''}`;
      cell.dataset.track=t;
      cell.dataset.step=s;
      cell.title = `${tr.name} step ${s+1} — ${vel===0?'off':vel===1?'on':'accent'} (click to toggle, right-click for accent)`;
      // events
      cell.addEventListener('mousedown', onCellDown);
      cell.addEventListener('mouseenter', onCellEnter);
      cell.addEventListener('contextmenu', e=>{ e.preventDefault(); cycleAccent(t,s); });
      seq.appendChild(cell);
    }
  }
  // attach mute/solo/vol listeners
  seq.querySelectorAll('[data-mute]').forEach(b=> b.addEventListener('click', e=>{
    const t=+e.currentTarget.dataset.mute;
    trackMute[t]=!trackMute[t];
    e.currentTarget.classList.toggle('active', trackMute[t]);
    e.currentTarget.closest('.track-label').classList.toggle('muted', trackMute[t] && !trackSolo.some(Boolean));
    saveState();
  }));
  seq.querySelectorAll('[data-solo]').forEach(b=> b.addEventListener('click', e=>{
    const t=+e.currentTarget.dataset.solo;
    trackSolo[t]=!trackSolo[t];
    e.currentTarget.classList.toggle('active', trackSolo[t]);
    // update muted appearance
    document.querySelectorAll('.track-label').forEach((lbl,i)=>{
      const anySolo = trackSolo.some(Boolean);
      const mutedVis = anySolo ? !trackSolo[i] : trackMute[i];
      lbl.classList.toggle('muted', mutedVis);
    });
    saveState();
  }));
  seq.querySelectorAll('[data-vol]').forEach(inp=> inp.addEventListener('input', e=>{
    const t=+e.currentTarget.dataset.vol;
    trackVol[t]= +e.currentTarget.value/100;
    saveState();
  }));
}

function onCellDown(e){
  if(e.button!==0) return;
  isDragging=true;
  const t=+e.currentTarget.dataset.track, s=+e.currentTarget.dataset.step;
  const cur = pattern[t][s];
  // toggle: off->on, on->off, accent->off
  // if currently off, paint on; if on, erase
  if(cur===0){ dragMode=1; pattern[t][s]=1; }
  else { dragMode=0; pattern[t][s]=0; }
  updateCell(t,s);
  saveState();
  e.preventDefault();
}
function onCellEnter(e){
  if(!isDragging) return;
  const t=+e.currentTarget.dataset.track, s=+e.currentTarget.dataset.step;
  const targetVal = dragMode===1 ? 1 : 0;
  // don't overwrite accent with drag unless erasing
  if(pattern[t][s]!==targetVal){
    // if painting and cell is accent, keep accent? No, set to 1
    pattern[t][s]=targetVal;
    updateCell(t,s);
    saveState();
  }
}
function cycleAccent(t,s){
  const cur = pattern[t][s];
  if(cur===0) pattern[t][s]=2;
  else if(cur===1) pattern[t][s]=2;
  else pattern[t][s]=0;
  updateCell(t,s);
  saveState();
}
function updateCell(t,s){
  const cell = document.querySelector(`.cell[data-track="${t}"][data-step="${s}"]`);
  if(!cell) return;
  const vel = pattern[t][s];
  cell.classList.toggle('on', vel!==0);
  cell.classList.toggle('accent', vel===2);
  const names = ['off','on','accent'];
  cell.title = `${TRACKS[t].name} step ${s+1} — ${names[vel]}`;
}

document.addEventListener('mouseup', ()=>{ isDragging=false; });
document.addEventListener('mouseleave', ()=>{ isDragging=false; });

// ============================================================
// CONTROLS
// ============================================================
const bpmSlider = document.getElementById('bpmSlider');
const swingSlider = document.getElementById('swingSlider');
const masterSlider = document.getElementById('masterSlider');

function updateBpmUI(){
  document.getElementById('bpmOut').textContent = tempo;
  document.getElementById('bpmVal').innerHTML = `${tempo} <small>BPM</small>`;
}
function updateSwingUI(){
  document.getElementById('swingOut').textContent = swing + '%';
}
function updateMasterUI(){
  document.getElementById('masterOut').textContent = Math.round(masterVol*100)+'%';
  if(masterGain) masterGain.gain.setTargetAtTime(masterVol, audioCtx?audioCtx.currentTime:0, 0.02);
}

bpmSlider.addEventListener('input', e=>{
  tempo = +e.target.value;
  updateBpmUI();
  saveState();
});
swingSlider.addEventListener('input', e=>{
  swing = +e.target.value;
  updateSwingUI();
  saveState();
});
masterSlider.addEventListener('input', e=>{
  masterVol = +e.target.value/100;
  updateMasterUI();
  saveState();
});

document.getElementById('playBtn').addEventListener('click', ()=>{
  if(!isPlaying) startPlayback();
  else stopPlayback();
});
document.getElementById('stopBtn').addEventListener('click', stopPlayback);

document.getElementById('clearBtn').addEventListener('click', ()=>{
  pattern = Array.from({length:CONFIG.TRACKS}, ()=> Array(CONFIG.STEPS).fill(0));
  buildGrid();
  saveState();
});
document.getElementById('randomBtn').addEventListener('click', ()=>{
  for(let t=0;t<CONFIG.TRACKS;t++){
    for(let s=0;s<CONFIG.STEPS;s++){
      const r = Math.random();
      // density per track
      let p = 0.22;
      if(t===0) p=0.28;
      if(t===2) p=0.45;
      if(t===3) p=0.08;
      if(r < p){
        pattern[t][s] = Math.random()<0.22 ? 2 : 1;
      } else pattern[t][s]=0;
    }
  }
  // ensure kick on 1
  pattern[0][0]=2;
  buildGrid();
  saveState();
});
document.getElementById('saveBtn').addEventListener('click', ()=>{
  saveState();
  const b=document.getElementById('saveBtn');
  const old=b.textContent; b.textContent='SAVED ✓';
  setTimeout(()=>b.textContent=old,900);
});

// keyboard
document.addEventListener('keydown', e=>{
  if(e.code==='Space'){
    e.preventDefault();
    if(!isPlaying) startPlayback(); else stopPlayback();
  }
});

// ============================================================
// PERSISTENCE
// ============================================================
function saveState(){
  try{
    localStorage.setItem('drm16-state', JSON.stringify({
      pattern, trackVol, trackMute, trackSolo, tempo, swing, masterVol
    }));
  }catch{}
}
function loadState(){
  try{
    const raw = localStorage.getItem('drm16-state');
    if(!raw) return;
    const s = JSON.parse(raw);
    if(s.pattern && Array.isArray(s.pattern) && s.pattern.length===CONFIG.TRACKS) pattern=s.pattern;
    if(s.trackVol) trackVol=s.trackVol;
    if(s.trackMute) trackMute=s.trackMute;
    if(s.trackSolo) trackSolo=s.trackSolo;
    if(s.tempo) tempo=s.tempo;
    if(typeof s.swing==='number') swing=s.swing;
    if(typeof s.masterVol==='number') masterVol=s.masterVol;
    bpmSlider.value=tempo;
    swingSlider.value=swing;
    masterSlider.value=Math.round(masterVol*100);
  }catch{}
}

// ============================================================
// INIT
// ============================================================
loadState();
updateBpmUI(); updateSwingUI(); updateMasterUI();
buildGrid();

// expose for probe/self-check
window.getSchedulerDeltas = function(){
  const log = window.schedulerLog;
  if(log.length<2) return {pass:false, message:'not enough scheduled notes'};
  // compute deltas between consecutive scheduled times at fixed tempo (ignore tempo changes)
  // group by constant tempo segments
  let deltas=[];
  for(let i=1;i<log.length;i++){
    if(log[i].tempo===log[i-1].tempo && log[i].swing===log[i-1].swing){
      deltas.push(log[i].time - log[i-1].time);
    }
  }
  if(deltas.length<4) return {pass:false, message:'not enough deltas', data:deltas};
  // expected stepDur with swing alternation: d*(1+swing), d*(1-swing) alternating
  // So deltas should alternate between two values. Check tolerance 0.008s
  const d = 60/log[0].tempo/4;
  const sw = log[0].swing/100;
  const expA = d*(1+sw); // odd step interval? Actually interval from even->odd is d+swing, odd->even is d - swing
  // deltas alternate, so we check each delta is close to either expA or d - d*sw
  const expB = d - d*sw;
  let maxErr=0;
  for(let v of deltas){
    const err = Math.min(Math.abs(v-expA), Math.abs(v-expB));
    maxErr=Math.max(maxErr, err);
  }
  return {pass:maxErr<0.012, message:`maxErr ${maxErr.toFixed(4)}s, expected ${expA.toFixed(4)}/${expB.toFixed(4)}`, data:{deltas: deltas.slice(0,8), maxErr, expA, expB}};
};
window.getPattern = ()=> pattern;
window.getTempo = ()=> tempo;
window.getSwing = ()=> swing;

// first gesture resumes audio
document.body.addEventListener('click', ()=>{ ensureAudio(); }, {once:true});

</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2-contributor","provider":"openrouter","persona":"full","sessionId":"70471faa-731c-4b32-a70c-c23288b98f7c","tokensIn":152289,"tokensOut":14979,"tokensTotal":167268,"cost":0.0074519519999999995,"turns":8,"toolCalls":7,"failedToolCalls":0,"timestamp":"2026-08-23T03:48:20.199Z"} -->