Sequencer
muse spark 1.2 · basic

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 29,391 bytes · SHA-256 4da99c72ca28
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>TR-16 — Step Sequencer</title>
<style>
:root{
--bg:#0a0c10;
--panel:#12161f;
--panel2:#181e2b;
--grid:#1e2535;
--accent:#ff3b30;
--accent2:#00e5ff;
--accent3:#ffe600;
--text:#e6eaf2;
--muted:#6b758a;
--cell:#1a2130;
--cell-on:#ff3b30;
--cell-accent:#ff6b3b;
--border:#252e42;
}
*{box-sizing:border-box}
body{
margin:0;background:radial-gradient(1200px 600px at 50% -10%,#1a2540 0%,var(--bg) 60%);
color:var(--text);font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
min-height:100vh;overflow-x:hidden;
}
.wrap{max-width:1060px;margin:0 auto;padding:18px 16px 40px}
/* header */
.top{
background:linear-gradient(180deg,#1b2233,#131722);
border:1px solid var(--border);border-radius:16px;
padding:14px 16px;display:flex;flex-wrap:wrap;gap:14px;align-items:center;
box-shadow:0 10px 30px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.06);
}
.brand{display:flex;align-items:center;gap:12px}
.logo{
width:44px;height:44px;border-radius:10px;background:#0f1320;border:1px solid #2a344e;
display:grid;place-items:center;font-weight:900;letter-spacing:.5px;color:var(--accent);
box-shadow:0 0 18px rgba(255,59,48,.35);
}
.brand h1{margin:0;font-size:18px;letter-spacing:.12em;line-height:1}
.brand p{margin:2px 0 0;font-size:10px;letter-spacing:.18em;color:var(--muted);text-transform:uppercase}
.transport{display:flex;align-items:center;gap:10px;margin-left:auto;flex-wrap:wrap}
.btn{
appearance:none;border:1px solid #2a344e;background:#0f1320;color:var(--text);
padding:10px 18px;border-radius:10px;font-weight:800;letter-spacing:.08em;
cursor:pointer;display:inline-flex;align-items:center;gap:8px;
box-shadow:inset 0 1px 0 rgba(255,255,255,.06),0 4px 12px rgba(0,0,0,.3);
transition:.15s;user-select:none
}
.btn:active{transform:translateY(1px)}
.btn.play{background:var(--accent);border-color:#ff5a4f;color:white;box-shadow:0 6px 18px rgba(255,59,48,.45)}
.btn.play.playing{background:#111;color:var(--text);box-shadow:none}
.btn.ghost{background:#151a28}
.btn.small{padding:7px 10px;font-size:12px;border-radius:8px}
/* knobs */
.controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.ctrl{
background:#0f1320;border:1px solid #252e42;border-radius:12px;padding:10px 12px;
min-width:140px;display:flex;flex-direction:column;gap:6px
}
.ctrl label{font-size:9px;letter-spacing:.16em;color:var(--muted);text-transform:uppercase;display:flex;justify-content:space-between}
.ctrl label b{color:var(--accent2);font-size:12px}
.ctrl label b.bpm{color:var(--accent3);font-size:13px;text-shadow:0 0 10px rgba(255,230,0,.5)}
input[type=range]{-webkit-appearance:none;appearance:none;height:4px;background:#232c42;border-radius:99px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:var(--text);border:2px solid #0f1320;box-shadow:0 2px 8px rgba(0,0,0,.5);cursor:pointer}
.led{
display:inline-block;width:8px;height:8px;border-radius:50%;background:#1a2a1a;box-shadow:0 0 0 rgba(0,255,100,0);
transition:.1s
}
.led.on{background:#00ff88;box-shadow:0 0 10px #00ff88}
/* main panel */
.machine{
margin-top:14px;background:linear-gradient(180deg,var(--panel2),var(--panel));
border:1px solid var(--border);border-radius:16px;overflow:hidden;
box-shadow:0 20px 50px rgba(0,0,0,.6)
}
.machine-head{
display:flex;gap:8px;padding:12px 14px;border-bottom:1px solid var(--border);
align-items:center;flex-wrap:wrap;background:#131722
}
.machine-head .hint{font-size:10px;color:var(--muted);letter-spacing:.08em}
.machine-head .hint span{color:var(--text)}
.grid-wrap{padding:14px;overflow-x:auto}
.grid{
display:grid;grid-template-columns:160px repeat(16,1fr);gap:6px;min-width:860px
}
.grid-header{display:contents}
.grid-header div{font-size:9px;letter-spacing:.12em;color:var(--muted);text-align:center;padding:4px 0}
.grid-header div.beat{color:var(--accent3)}
.row{display:contents}
.track-label{
background:#0f1320;border:1px solid #252e42;border-radius:10px;
display:flex;align-items:center;gap:8px;padding:8px 10px;
}
.track-label.muted{opacity:.45}
.icon{
width:28px;height:28px;border-radius:8px;display:grid;place-items:center;
font-size:13px;font-weight:900;flex-shrink:0
}
.t-kick .icon{background:#ff3b30;color:white}
.t-snare .icon{background:#00e5ff;color:#002a33}
.t-hatC .icon{background:#ffe600;color:#332d00}
.t-hatO .icon{background:#a78bfa;color:white}
.t-clap .icon{background:#ff8a00;color:white}
.t-tom .icon{background:#00ff88;color:#00331a}
.track-meta{line-height:1}
.track-meta strong{font-size:11px;letter-spacing:.06em;display:block}
.track-meta span{font-size:9px;color:var(--muted);letter-spacing:.08em;text-transform:uppercase}
.track-ctrl{display:flex;gap:4px;margin-left:auto;align-items:center}
.mbtn{
width:28px;height:18px;border-radius:6px;border:1px solid #2a344e;
background:#1a2130;color:var(--muted);font-size:8px;font-weight:800;letter-spacing:.06em;cursor:pointer
}
.mbtn.active{background:var(--accent);color:white;border-color:#ff5a4f;box-shadow:0 0 8px rgba(255,59,48,.6)}
.mbtn.solo.active{background:var(--accent3);color:#111;border-color:var(--accent3)}
.vslider{width:54px;height:4px}
.pad{
aspect-ratio:1;min-height:42px;border-radius:9px;
background:var(--cell);border:1px solid #252e42;
position:relative;cursor:pointer;
box-shadow:inset 0 1px 0 rgba(255,255,255,.05), inset 0 -6px 12px rgba(0,0,0,.25);
transition:.08s;
display:grid;place-items:center;
}
.pad:hover{filter:brightness(1.15)}
.pad.on{background:radial-gradient(120% 120% at 30% 20%,#ff6a5a,#ff3b30 55%,#a81c14);border-color:#ff5a4f;box-shadow:0 0 14px rgba(255,59,48,.7), inset 0 1px 0 rgba(255,255,255,.3)}
.pad.on.accent{background:radial-gradient(120% 120% at 30% 20%,#fff2a0,#ffcc00 35%,#ff6b00 70%);border-color:#ffcc00;box-shadow:0 0 16px rgba(255,204,0,.8), inset 0 1px 0 rgba(255,255,255,.6);transform:scale(1.03)}
.pad.on.accent::after{content:"•";position:absolute;top:2px;right:5px;font-size:14px;color:rgba(0,0,0,.6);line-height:1}
.pad.playhead{outline:2px solid var(--accent2);outline-offset:1px;z-index:1;box-shadow:0 0 14px rgba(0,229,255,.6)}
.pad.beat-sep{border-right:2px solid rgba(255,255,255,.08)}
.pad::before{
content:"";width:36%;height:36%;border-radius:50%;background:rgba(255,255,255,.07);
position:absolute;opacity:0;transition:.1s
}
.pad.on::before{opacity:1;background:rgba(255,255,255,.18)}
.footer{display:flex;gap:10px;flex-wrap:wrap;padding:12px 14px;border-top:1px solid var(--border);background:#0f1320;align-items:center}
.foot-label{font-size:9px;letter-spacing:.14em;color:var(--muted);text-transform:uppercase}
.sep{width:1px;height:24px;background:var(--border)}
@media(max-width:900px){
.grid{grid-template-columns:140px repeat(16, minmax(36px,1fr));min-width:740px}
.wrap{padding:10px}
}
</style>
</head>
<body>
<div class="wrap">
<div class="top">
<div class="brand">
<div class="logo">TR-16</div>
<div>
<h1>DRUM COMPUTER</h1>
<p>16-Step ● 6 Voice ● Analog Modelled</p>
</div>
</div>
<div class="transport">
<button id="playBtn" class="btn play"><span id="playIcon">▶</span> <span id="playText">PLAY</span> <i class="led" id="playLed"></i></button>
<div class="controls">
<div class="ctrl" style="min-width:150px">
<label>BPM <b class="bpm" id="bpmVal">120</b></label>
<input id="bpm" type="range" min="60" max="200" value="120">
</div>
<div class="ctrl" style="min-width:150px">
<label>SWING <b id="swingVal">0%</b></label>
<input id="swing" type="range" min="0" max="60" value="0">
</div>
<div class="ctrl" style="min-width:130px">
<label>MASTER <b id="volVal">85%</b></label>
<input id="masterVol" type="range" min="0" max="100" value="85">
</div>
</div>
</div>
</div>
<div class="machine">
<div class="machine-head">
<span class="hint"><span style="color:var(--accent)">●</span> CLICK to toggle · DRAG to paint · <span>RIGHT-CLICK</span> accent · M = mute · S = solo</span>
<div style="margin-left:auto;display:flex;gap:8px">
<button id="clearBtn" class="btn small ghost">CLEAR</button>
<button id="randBtn" class="btn small ghost" style="border-color:var(--accent2);color:var(--accent2)">RANDOMIZE</button>
</div>
</div>
<div class="grid-wrap">
<div id="grid" class="grid"></div>
</div>
<div class="footer">
<span class="foot-label">Current Step</span>
<span id="stepReadout" style="background:#0a0c10;border:1px solid #252e42;padding:4px 10px;border-radius:8px;font-weight:800;color:var(--accent3);min-width:44px;text-align:center">--</span>
<div class="sep"></div>
<span class="foot-label">Lookahead 25ms / 100ms</span>
<span class="foot-label" style="margin-left:auto;opacity:.7">SAMPLE-ACCURATE SCHEDULER • Web Audio Clock</span>
</div>
</div>
</div>
<script>
/* ==========================================================
TUNABLE CONSTANTS
========================================================== */
const BPM_MIN=60, BPM_MAX=200;
const LOOKAHEAD_MS=25; // scheduler interval
const SCHEDULE_AHEAD_SEC=0.10; // how far ahead to schedule
const SWING_MAX=0.60;
const DEFAULT_BPM=120;
const DEFAULT_SWING=0.08; // 8% for groove
const MASTER_DEFAULT=0.85;
/* Instrument patch params */
const PATCH={
kick:{base:50, peak:150, decay:0.45},
snare:{tone:180, noiseDecay:0.18},
hatClosed:{decay:0.08, hp:7000},
hatOpen:{decay:0.35, hp:7000},
clap:{bursts:4, spread:0.03},
tom:{base:90, peak:180, decay:0.45}
};
/* ==========================================================
STATE
========================================================== */
const TRACKS=[
{id:'kick', name:'KICK', short:'K'},
{id:'snare', name:'SNARE', short:'S'},
{id:'hatC', name:'CLOSED HAT', short:'CH'},
{id:'hatO', name:'OPEN HAT', short:'OH'},
{id:'clap', name:'CLAP', short:'CP'},
{id:'tom', name:'TOM', short:'T'}
];
let pattern = []; // [track][step] 0=off 1=on 2=accent
let trackVols=[0.9,0.85,0.7,0.7,0.8,0.8];
let trackMutes=[false,false,false,false,false,false];
let trackSolos=[false,false,false,false,false,false];
let bpm=DEFAULT_BPM;
let swing=DEFAULT_SWING;
let masterGainVal=MASTER_DEFAULT;
let audioCtx=null, masterGain=null;
let isPlaying=false;
let currentStep=0; // absolute step counter
let nextNoteTime=0;
let schedulerId=null;
let queued=[]; // {step,time}
let dragMode=null, isDragging=false;
/* ==========================================================
PERSISTENCE
========================================================== */
const LS_KEY='tr16_v2';
function save(){
localStorage.setItem(LS_KEY, JSON.stringify({pattern,bpm,swing,trackVols,trackMutes,trackSolos,masterGainVal}));
}
function load(){
try{
const d=JSON.parse(localStorage.getItem(LS_KEY));
if(d){
if(d.pattern) pattern=d.pattern;
if(d.bpm) bpm=d.bpm;
if(d.swing!==undefined) swing=d.swing;
if(d.trackVols) trackVols=d.trackVols;
if(d.trackMutes) trackMutes=d.trackMutes;
if(d.trackSolos) trackSolos=d.trackSolos;
if(d.masterGainVal!==undefined) masterGainVal=d.masterGainVal;
return true;
}
}catch(e){}
return false;
}
function defaultPattern(){
// groovy house / hiphop hybrid
pattern = TRACKS.map(()=>Array(16).fill(0));
// kick 0,4,8,12 with ghost on 10
[0,4,8,12].forEach(s=>pattern[0][s]=1);
pattern[0][10]=1;
// snare 4,12 accent, ghost 11
pattern[1][4]=2; pattern[1][12]=2; pattern[1][11]=1;
// closed hat 2,6,10,14 + every 2nd 16th
[2,6,10,14].forEach(s=>pattern[2][s]=1);
[1,5,9,13].forEach(s=>pattern[2][s]=0);
// extra hats
pattern[2][0]=1; pattern[2][4]=1; pattern[2][8]=1; pattern[2][12]=1;
// open hat on off-beats
pattern[3][6]=1; pattern[3][14]=1;
// clap on 4,12
pattern[4][4]=1; pattern[4][12]=1;
// tom fills
pattern[5][7]=1; pattern[5][15]=2;
// accents
pattern[2][2]=2; pattern[2][14]=2;
}
/* ==========================================================
AUDIO ENGINE
========================================================== */
function ensureAudio(){
if(!audioCtx){
audioCtx=new (window.AudioContext||window.webkitAudioContext)();
masterGain=audioCtx.createGain();
masterGain.gain.value=masterGainVal;
masterGain.connect(audioCtx.destination);
// pre-create noise buffer
noiseBuffer=createNoiseBuffer();
}
if(audioCtx.state==='suspended') audioCtx.resume();
}
let noiseBuffer=null;
function createNoiseBuffer(){
const len=audioCtx.sampleRate*1;
const buf=audioCtx.createBuffer(1,len,audioCtx.sampleRate);
const ch=buf.getChannelData(0);
for(let i=0;i<len;i++) ch[i]=Math.random()*2-1;
return buf;
}
function isTrackAudible(t){
if(trackMutes[t]) return false;
if(trackSolos.some(s=>s)) return trackSolos[t];
return true;
}
function playSound(trackIdx,time,vel){
if(!isTrackAudible(trackIdx)) return;
const isAccent=vel===2;
const gainMul=isAccent?1.35:0.85;
const vol=trackVols[trackIdx]*gainMul;
switch(TRACKS[trackIdx].id){
case 'kick': playKick(time,vol,isAccent); break;
case 'snare': playSnare(time,vol,isAccent); break;
case 'hatC': playHat(time,vol,false,isAccent); break;
case 'hatO': playHat(time,vol,true,isAccent); break;
case 'clap': playClap(time,vol,isAccent); break;
case 'tom': playTom(time,vol,isAccent); break;
}
}
function playKick(t,vol,accent){
const osc=audioCtx.createOscillator();
const g=audioCtx.createGain();
osc.type='sine';
osc.frequency.setValueAtTime(PATCH.kick.peak,t);
osc.frequency.exponentialRampToValueAtTime(PATCH.kick.base,t+0.08);
g.gain.setValueAtTime(0,t);
g.gain.linearRampToValueAtTime(vol*1.0,t+0.003);
g.gain.exponentialRampToValueAtTime(0.001,t+PATCH.kick.decay);
osc.connect(g).connect(masterGain);
osc.start(t); osc.stop(t+PATCH.kick.decay+0.05);
}
function playSnare(t,vol,accent){
// tonal body
const osc=audioCtx.createOscillator();
const og=audioCtx.createGain();
osc.type='sine'; osc.frequency.setValueAtTime(PATCH.snare.tone,t);
osc.frequency.exponentialRampToValueAtTime(60,t+0.12);
og.gain.setValueAtTime(0,t);
og.gain.linearRampToValueAtTime(vol*0.6,t+0.005);
og.gain.exponentialRampToValueAtTime(0.001,t+0.18);
osc.connect(og).connect(masterGain);
osc.start(t); osc.stop(t+0.2);
// noise
const src=audioCtx.createBufferSource(); src.buffer=noiseBuffer;
const bp=audioCtx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=accent?1800:1300; bp.Q.value=0.7;
const hp=audioCtx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=900;
const ng=audioCtx.createGain();
ng.gain.setValueAtTime(0,t);
ng.gain.linearRampToValueAtTime(vol*0.9,t+0.002);
ng.gain.exponentialRampToValueAtTime(0.001,t+PATCH.snare.noiseDecay*(accent?1.4:1));
src.connect(hp).connect(bp).connect(ng).connect(masterGain);
src.start(t); src.stop(t+0.3);
}
function playHat(t,vol,open,accent){
const src=audioCtx.createBufferSource(); src.buffer=noiseBuffer;
const hp=audioCtx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=PATCH.hatClosed.hp;
const bp=audioCtx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=9500; bp.Q.value=0.6;
const g=audioCtx.createGain();
const decay=open?PATCH.hatOpen.decay:PATCH.hatClosed.decay;
const v=open?0.45:0.38;
g.gain.setValueAtTime(0,t);
g.gain.linearRampToValueAtTime(vol*v*(accent?1.3:1),t+0.001);
g.gain.exponentialRampToValueAtTime(0.001,t+decay);
src.connect(hp).connect(bp).connect(g).connect(masterGain);
src.start(t); src.stop(t+decay+0.05);
// metallic click for closed
if(!open){
const o=audioCtx.createOscillator(); o.type='square'; o.frequency.value=1200;
const og=audioCtx.createGain();
og.gain.setValueAtTime(0,t); og.gain.linearRampToValueAtTime(vol*0.08,t+0.001); og.gain.exponentialRampToValueAtTime(0.001,t+0.02);
o.connect(og).connect(masterGain);
o.start(t); o.stop(t+0.03);
}
}
function playClap(t,vol,accent){
const bursts=PATCH.clap.bursts;
for(let i=0;i<bursts;i++){
const bt=t+i*0.028;
const src=audioCtx.createBufferSource(); src.buffer=noiseBuffer;
const bp=audioCtx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=1200+Math.random()*400; bp.Q.value=0.9;
const g=audioCtx.createGain();
const env = i===bursts-1?0.18:0.04;
g.gain.setValueAtTime(0,bt);
g.gain.linearRampToValueAtTime(vol*0.7*(accent?1.2:1)*(1-i*0.12),bt+0.002);
g.gain.exponentialRampToValueAtTime(0.001,bt+env);
src.connect(bp).connect(g).connect(masterGain);
src.start(bt); src.stop(bt+0.2);
}
}
function playTom(t,vol,accent){
const osc=audioCtx.createOscillator();
const g=audioCtx.createGain();
osc.type='sine';
const base=accent?100:PATCH.tom.base;
osc.frequency.setValueAtTime(PATCH.tom.peak*(accent?1.15:1),t);
osc.frequency.exponentialRampToValueAtTime(base,t+0.14);
g.gain.setValueAtTime(0,t);
g.gain.linearRampToValueAtTime(vol*0.9,t+0.004);
g.gain.exponentialRampToValueAtTime(0.001,t+PATCH.tom.decay);
// slight overdrive for accent
if(accent){
const hp=audioCtx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=30;
osc.connect(hp).connect(g).connect(masterGain);
}else{
osc.connect(g).connect(masterGain);
}
osc.start(t); osc.stop(t+0.5);
}
/* ==========================================================
SCHEDULER — lookahead pattern, absolute step counter
========================================================== */
function secondsPerStep(){
return 60/bpm/4; // 16th
}
function scheduler(){
if(!isPlaying || !audioCtx) return;
// schedule while next note is within lookahead window
while(nextNoteTime < audioCtx.currentTime + SCHEDULE_AHEAD_SEC){
scheduleStep(currentStep, nextNoteTime);
// compute duration to next step with swing applied (paired swing)
const stepInPair = currentStep % 2;
let dur = secondsPerStep();
if(swing>0){
if(stepInPair===0) dur = dur * (1+swing);
else dur = dur * (1-swing);
}
nextNoteTime += dur;
currentStep = (currentStep+1)%16;
// expose for self-check
window.__lastSchedule={step:currentStep,time:nextNoteTime};
}
}
function scheduleStep(step,time){
queued.push({step,time});
// keep queue small
if(queued.length>32) queued.shift();
window.__scheduledTimes=window.__scheduledTimes||[];
window.__scheduledTimes.push({step,time});
if(window.__scheduledTimes.length>64) window.__scheduledTimes.shift();
for(let tr=0;tr<TRACKS.length;tr++){
const vel=pattern[tr][step];
if(vel>0) playSound(tr,time,vel);
}
}
window.__scheduledTimes=[];
window.__lastSchedule=null;
/* ==========================================================
PLAYHEAD — driven from audio clock via rAF
========================================================== */
let rafId=null;
function updatePlayhead(){
rafId=requestAnimationFrame(updatePlayhead);
if(!isPlaying || !audioCtx || queued.length===0) return;
const now=audioCtx.currentTime;
// find current step: latest queued step whose time <= now and next time > now
// prune old
while(queued.length>1 && queued[1].time <= now){
queued.shift();
}
let cur = queued[0];
// if before first scheduled time, show last? handle
let displayStep = cur?cur.step:0;
// if now is before cur.time, the audible step is previous
if(cur && now < cur.time){
displayStep = (cur.step+15)%16;
} else {
// cur is playing
displayStep = cur.step;
}
highlightStep(displayStep);
document.getElementById('stepReadout').textContent = String(displayStep+1).padStart(2,'0');
document.getElementById('playLed').classList.toggle('on', true);
}
function highlightStep(idx){
document.querySelectorAll('.pad').forEach(p=>p.classList.remove('playhead'));
document.querySelectorAll(`.pad[data-step="${idx}"]`).forEach(p=>p.classList.add('playhead'));
}
/* ==========================================================
TRANSPORT
========================================================== */
function startPlayback(){
ensureAudio();
if(isPlaying) return;
isPlaying=true;
currentStep=0;
nextNoteTime=audioCtx.currentTime+0.06;
queued=[];
schedulerId=setInterval(scheduler, LOOKAHEAD_MS);
// start rAF
if(!rafId) updatePlayhead();
document.getElementById('playText').textContent='STOP';
document.getElementById('playIcon').textContent='■';
document.getElementById('playBtn').classList.add('playing');
document.getElementById('playLed').classList.add('on');
}
function stopPlayback(){
isPlaying=false;
clearInterval(schedulerId); schedulerId=null;
queued=[];
document.querySelectorAll('.pad').forEach(p=>p.classList.remove('playhead'));
document.getElementById('stepReadout').textContent='--';
document.getElementById('playText').textContent='PLAY';
document.getElementById('playIcon').textContent='▶';
document.getElementById('playBtn').classList.remove('playing');
document.getElementById('playLed').classList.remove('on');
// immediate silence: ramp master down/up quickly to kill lingering?
}
/* ==========================================================
UI BUILD
========================================================== */
function buildGrid(){
const grid=document.getElementById('grid');
grid.innerHTML='';
// header numbers
const hdr=document.createElement('div'); hdr.className='grid-header';
const empty=document.createElement('div'); empty.textContent='VOICE'; empty.style.textAlign='left'; empty.style.paddingLeft='10px';
hdr.appendChild(empty);
for(let s=0;s<16;s++){
const d=document.createElement('div');
d.textContent=s+1;
if(s%4===0) d.classList.add('beat');
hdr.appendChild(d);
}
grid.appendChild(hdr);
// rows
TRACKS.forEach((tr,ti)=>{
const label=document.createElement('div');
label.className='track-label t-'+tr.id + (trackMutes[ti]?' muted':'');
label.innerHTML=`
<div class="icon">${tr.short}</div>
<div class="track-meta"><strong>${tr.name}</strong><span>${tr.id}</span></div>
<div class="track-ctrl">
<button class="mbtn ${trackMutes[ti]?'active':''}" data-mute="${ti}">M</button>
<button class="mbtn solo ${trackSolos[ti]?'active':''}" data-solo="${ti}">S</button>
<input class="vslider" type="range" min="0" max="100" value="${Math.round(trackVols[ti]*100)}" data-vol="${ti}">
</div>
`;
grid.appendChild(label);
for(let s=0;s<16;s++){
const pad=document.createElement('div');
pad.className='pad'+(pattern[ti][s] ? ' on'+(pattern[ti][s]===2?' accent':'') : '')+(s%4===3?' beat-sep':'');
pad.dataset.track=ti; pad.dataset.step=s;
if(pattern[ti][s]===2) pad.title='Accent';
// mouse
pad.addEventListener('mousedown', e=>{
e.preventDefault();
isDragging=true;
const cur=pattern[ti][s];
// left click cycle 0->1->2->0, right click accent logic but we cycle
if(e.button===2){
pattern[ti][s]= cur===2?0:2;
if(cur===0) pattern[ti][s]=2;
if(cur===1) pattern[ti][s]=2;
if(cur===2) pattern[ti][s]=0;
}else{
// toggle: if off -> on, on->accent, accent->off
if(cur===0){pattern[ti][s]=1; dragMode=1}
else if(cur===1){pattern[ti][s]=2; dragMode=2}
else {pattern[ti][s]=0; dragMode=0}
// for drag, we set dragMode to the new value
if(dragMode===null) dragMode=pattern[ti][s];
}
renderPads(); save();
});
pad.addEventListener('mouseenter', ()=>{
if(!isDragging) return;
if(dragMode!==null) pattern[ti][s]=dragMode;
renderPads(); save();
});
pad.addEventListener('contextmenu', e=>e.preventDefault());
// touch
pad.addEventListener('touchstart', e=>{
e.preventDefault();
const cur=pattern[ti][s];
pattern[ti][s]=cur===0?1:cur===1?2:0;
renderPads(); save();
},{passive:false});
grid.appendChild(pad);
}
});
// mute/solo/vol listeners
grid.querySelectorAll('[data-mute]').forEach(b=>{
b.addEventListener('click',()=>{
const i=+b.dataset.mute;
trackMutes[i]=!trackMutes[i];
b.classList.toggle('active',trackMutes[i]);
b.closest('.track-label').classList.toggle('muted',trackMutes[i] && !trackSolos.some(s=>s));
save();
})
});
grid.querySelectorAll('[data-solo]').forEach(b=>{
b.addEventListener('click',()=>{
const i=+b.dataset.solo;
trackSolos[i]=!trackSolos[i];
b.classList.toggle('active',trackSolos[i]);
// update muted appearance
document.querySelectorAll('.track-label').forEach((el,idx)=>{
const audible=isTrackAudible(idx);
el.classList.toggle('muted',!audible);
});
save();
})
});
grid.querySelectorAll('[data-vol]').forEach(sl=>{
sl.addEventListener('input',()=>{
const i=+sl.dataset.vol;
trackVols[i]=sl.value/100;
save();
})
});
}
function renderPads(){
document.querySelectorAll('.pad').forEach(p=>{
const tr=+p.dataset.track, st=+p.dataset.step;
const v=pattern[tr][st];
p.className='pad'+(v?' on'+(v===2?' accent':''):'')+(st%4===3?' beat-sep':'');
});
}
/* ==========================================================
CONTROLS
========================================================== */
function initControls(){
const bpmSl=document.getElementById('bpm');
const swingSl=document.getElementById('swing');
const masterSl=document.getElementById('masterVol');
bpmSl.value=bpm; swingSl.value=Math.round(swing*100); masterSl.value=Math.round(masterGainVal*100);
document.getElementById('bpmVal').textContent=bpm;
document.getElementById('swingVal').textContent=Math.round(swing*100)+'%';
document.getElementById('volVal').textContent=Math.round(masterGainVal*100)+'%';
bpmSl.addEventListener('input',()=>{
bpm=parseInt(bpmSl.value);
document.getElementById('bpmVal').textContent=bpm;
save();
});
swingSl.addEventListener('input',()=>{
swing=parseInt(swingSl.value)/100;
document.getElementById('swingVal').textContent=swingSl.value+'%';
save();
});
masterSl.addEventListener('input',()=>{
masterGainVal=parseInt(masterSl.value)/100;
document.getElementById('volVal').textContent=masterSl.value+'%';
if(masterGain) masterGain.gain.value=masterGainVal;
save();
});
document.getElementById('playBtn').addEventListener('click',()=>{
if(isPlaying) stopPlayback(); else startPlayback();
});
document.getElementById('clearBtn').addEventListener('click',()=>{
pattern=TRACKS.map(()=>Array(16).fill(0));
renderPads(); save();
});
document.getElementById('randBtn').addEventListener('click',()=>{
pattern=TRACKS.map((_,ti)=>Array.from({length:16},()=>{
const r=Math.random();
if(ti===0) return r>0.68?(Math.random()>0.8?2:1):0;
if(ti===1) return r>0.78?(Math.random()>0.7?2:1):0;
if(ti===2) return r>0.5?(Math.random()>0.85?2:1):0;
return r>0.82?(Math.random()>0.8?2:1):0;
}));
renderPads(); save();
});
window.addEventListener('mouseup',()=>{isDragging=false; dragMode=null});
window.addEventListener('keydown',e=>{
if(e.code==='Space'){e.preventDefault(); if(isPlaying) stopPlayback(); else startPlayback();}
});
}
/* ==========================================================
INIT
========================================================== */
const loaded=load();
if(!loaded || !pattern || pattern.length!==6) defaultPattern();
if(!pattern || pattern[0].length!==16) defaultPattern();
buildGrid();
initControls();
renderPads();
// ensure UI reflects loaded values
document.getElementById('bpm').value=bpm;
document.getElementById('bpmVal').textContent=bpm;
document.getElementById('swing').value=Math.round(swing*100);
document.getElementById('swingVal').textContent=Math.round(swing*100)+'%';
document.getElementById('masterVol').value=Math.round(masterGainVal*100);
document.getElementById('volVal').textContent=Math.round(masterGainVal*100)+'%';
updatePlayhead();
// expose for self-check
window.__getScheduledDeltaStats=()=>{
const t=window.__scheduledTimes;
if(t.length<2) return null;
const deltas=[];
for(let i=1;i<t.length;i++) deltas.push(t[i].time-t[i-1].time);
const avg=deltas.reduce((a,b)=>a+b,0)/deltas.length;
const max=Math.max(...deltas), min=Math.min(...deltas);
return {avg,min,max,deltas: deltas.slice(-8)};
};
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"41952cec-61dd-42fa-b607-c5f2fd2999c0","tokensIn":1115,"tokensOut":10393,"tokensTotal":11508,"cost":0.0454397,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:29:07.472Z"} -->