Sequencer
muse spark 1.2 contributor · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 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"} -->