Sequencer
Deepseek v4.1 Flash · 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 37,447 bytes · SHA-256 28bf88f23929
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TR-8086 · Step Sequencer</title>
<style>
/* ============================================================
THEME / LAYOUT
============================================================ */
:root{
--bg:#0b0d10;
--panel:#14181d;
--panel2:#1b2027;
--line:#2a323c;
--line2:#333d49;
--txt:#c9d4e0;
--dim:#69788a;
--accent:#ff5a2b;
--accent2:#ffb03a;
--led:#4dff88;
--leddim:#1f3a2a;
--glow:0 0 10px rgba(255,90,43,.55);
--mono:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;background:
radial-gradient(1200px 600px at 50% -10%, #1a2029 0%, transparent 60%),
repeating-linear-gradient(180deg,#0b0d10 0 2px,#0a0c0f 2px 4px);
color:var(--txt);
font-family:var(--mono);
font-size:13px;
-webkit-font-smoothing:antialiased;
user-select:none;
display:flex;justify-content:center;
padding:18px 14px 40px;
}
#app{width:100%;max-width:1180px}
/* ---------- header ---------- */
header{
display:flex;align-items:center;gap:18px;flex-wrap:wrap;
background:linear-gradient(180deg,#1d232b,#131820);
border:1px solid var(--line);
border-radius:12px;
padding:12px 16px;
box-shadow:0 10px 30px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.04);
margin-bottom:14px;
}
.brand{display:flex;flex-direction:column;line-height:1.15}
.brand b{font-size:17px;letter-spacing:.16em;color:#fff}
.brand span{font-size:10px;letter-spacing:.24em;color:var(--dim)}
.transport{display:flex;gap:8px;align-items:center}
/* ---------- buttons ---------- */
button{
font-family:var(--mono);font-size:11px;letter-spacing:.09em;
color:var(--txt);background:linear-gradient(180deg,#242b34,#171c23);
border:1px solid var(--line2);border-radius:7px;
padding:9px 13px;cursor:pointer;text-transform:uppercase;
box-shadow:0 2px 0 #0a0d11, inset 0 1px 0 rgba(255,255,255,.05);
transition:filter .1s, transform .04s, background .12s;
}
button:hover{filter:brightness(1.22)}
button:active{transform:translateY(1px);box-shadow:none}
button.on{
background:linear-gradient(180deg,#ff7a44,#e0461c);
border-color:#ff8f61;color:#1a0d07;font-weight:700;
box-shadow:0 0 16px rgba(255,90,43,.45), inset 0 1px 0 rgba(255,255,255,.35);
}
#btnPlay{min-width:96px}
#btnPlay.on{background:linear-gradient(180deg,#63ff9d,#16c25f);border-color:#8dffb5;color:#04180c}
/* ---------- readouts ---------- */
.readout{
background:#07090c;border:1px solid #000;border-radius:8px;
padding:6px 12px;min-width:126px;text-align:right;
box-shadow:inset 0 2px 8px rgba(0,0,0,.9);
}
.readout .lbl{font-size:9px;letter-spacing:.22em;color:var(--dim);display:block;text-align:left}
.readout .val{font-size:22px;color:var(--accent2);text-shadow:0 0 12px rgba(255,176,58,.55);font-variant-numeric:tabular-nums}
.readout .val small{font-size:10px;color:var(--dim);text-shadow:none}
.stepLed{
display:flex;gap:5px;align-items:center;
background:#07090c;border:1px solid #000;border-radius:8px;padding:9px 12px;
box-shadow:inset 0 2px 8px rgba(0,0,0,.9);
}
.stepLed i{
width:9px;height:9px;border-radius:50%;background:#1c2229;
box-shadow:inset 0 0 4px #000;display:block;transition:background .05s;
}
.stepLed i.beat{background:#2c3742}
.stepLed i.cur{background:var(--led);box-shadow:0 0 10px var(--led),0 0 22px rgba(77,255,136,.6)}
.statusBox{display:flex;align-items:center;gap:7px;font-size:9.5px;letter-spacing:.18em;color:var(--dim);text-transform:uppercase}
.statusBox .sLed{width:7px;height:7px;border-radius:50%;background:var(--led);box-shadow:0 0 9px var(--led);flex:0 0 7px}
/* ---------- slider controls ---------- */
.knobs{display:flex;gap:18px;align-items:center;flex-wrap:wrap;margin-left:auto}
.knob{display:flex;flex-direction:column;gap:4px;min-width:140px}
.knob .top{display:flex;justify-content:space-between;font-size:9px;letter-spacing:.18em;color:var(--dim)}
.knob .top b{color:var(--accent2);letter-spacing:0;font-size:10px}
input[type=range]{
-webkit-appearance:none;appearance:none;width:100%;height:16px;background:transparent;cursor:pointer;
}
input[type=range]::-webkit-slider-runnable-track{height:6px;border-radius:4px;background:linear-gradient(90deg,#ff7a44,#2a323c);border:1px solid #000}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:15px;height:15px;margin-top:-6px;border-radius:4px;
background:linear-gradient(180deg,#e9eef4,#9aa7b6);border:1px solid #05080b;
box-shadow:0 1px 3px rgba(0,0,0,.8), inset 0 1px 0 #fff;
}
input[type=range]::-moz-range-track{height:6px;border-radius:4px;background:#2a323c;border:1px solid #000}
input[type=range]::-moz-range-thumb{width:14px;height:14px;border-radius:4px;background:#cfd8e3;border:1px solid #05080b}
/* ---------- grid ---------- */
.gridPanel{
background:linear-gradient(180deg,#161b21,#101419);
border:1px solid var(--line);border-radius:12px;
padding:12px;box-shadow:0 10px 30px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.04);
overflow-x:auto;
}
.grid{min-width:820px}
.row{
display:grid;
grid-template-columns:132px repeat(16,1fr) 54px 40px;
gap:5px;align-items:center;
padding:4px 4px;border-radius:8px;
}
.row.muted{opacity:.42}
.row.head{position:sticky;top:0;z-index:3;background:#101419;padding-bottom:6px}
.num{
text-align:center;font-size:10px;color:var(--dim);padding-bottom:2px;
border-bottom:2px solid transparent;
}
.num.beat{border-bottom-color:var(--line2)}
.num.cur{color:var(--led);text-shadow:0 0 8px rgba(77,255,136,.8)}
.num b{display:block;font-size:9px;letter-spacing:.14em;font-weight:400}
/* track label cell */
.tlabel{display:flex;align-items:center;gap:8px;padding-left:4px;overflow:hidden}
.tlabel .ico{
width:26px;height:26px;flex:0 0 26px;border-radius:6px;display:grid;place-items:center;
background:#0c1014;border:1px solid var(--line2);font-size:14px;
box-shadow:inset 0 0 8px rgba(0,0,0,.8);
}
.tlabel .nm{font-size:10px;letter-spacing:.1em;color:var(--txt);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* pads */
.pad{
position:relative;height:40px;border-radius:8px;cursor:pointer;
background:linear-gradient(180deg,#222931,#161b21);
border:1px solid #05080b;
box-shadow:inset 0 1px 0 rgba(255,255,255,.05), inset 0 -3px 6px rgba(0,0,0,.6), 0 1px 0 #2c3742;
transition:background .09s, box-shadow .09s, transform .05s;
touch-action:none;
}
.pad.beatL{background:linear-gradient(180deg,#252d37,#1a2028);border-left-color:rgba(130,150,175,.34)}
.pad:hover{filter:brightness(1.3)}
.pad:active{transform:translateY(1px)}
.pad.on::after{
content:"";position:absolute;inset:5px;border-radius:5px;
background:linear-gradient(180deg,var(--pc1),var(--pc2));
box-shadow:0 0 14px var(--pcg), inset 0 1px 0 rgba(255,255,255,.4);
}
.pad.acc::after{inset:2px;filter:brightness(1.25) saturate(1.15)}
.pad.acc{box-shadow:inset 0 1px 0 rgba(255,255,255,.05), 0 0 0 1px rgba(255,255,255,.16), 0 1px 0 #2c3742}
.pad.hit::after{animation:pop .16s ease-out}
.pad.hit{box-shadow:0 0 18px var(--pcg), inset 0 1px 0 rgba(255,255,255,.1)}
.pad.col{border-top-color:rgba(77,255,136,.85)}
@keyframes pop{from{transform:scale(1.12);filter:brightness(1.8)}to{transform:scale(1)}}
/* per-track controls */
.tvol{display:flex;align-items:center}
.tvol input[type=range]{height:14px}
.tbtn{
width:34px;height:30px;padding:0;font-size:9px;
}
.tbtn.muteOn{background:linear-gradient(180deg,#ff5f5f,#c01f1f);border-color:#ff9090;color:#210606}
.tbtn.soloOn{background:linear-gradient(180deg,#ffd15c,#d99a0a);border-color:#ffe08e;color:#241a02}
/* ---------- footer / help ---------- */
footer{margin-top:12px;display:flex;gap:16px;flex-wrap:wrap;align-items:center;color:var(--dim);font-size:10.5px;letter-spacing:.06em}
kbd{background:#1d232b;border:1px solid var(--line2);border-bottom-width:2px;border-radius:4px;padding:1px 5px;color:var(--txt);font-size:10px}
.legend{display:flex;gap:12px;align-items:center}
.legend i{display:inline-block;width:12px;height:12px;border-radius:3px;vertical-align:-2px;margin-right:4px}
</style>
</head>
<body>
<div id="app">
<header>
<div class="brand"><b>TR‑8086</b><span>RHYTHM COMPOSER</span></div>
<div class="transport">
<button id="btnPlay">▶ PLAY</button>
<button id="btnClear">Clear</button>
<button id="btnRand">Random</button>
</div>
<div class="readout">
<span class="lbl">TEMPO</span>
<span class="val" id="bpmVal">120<small> BPM</small></span>
</div>
<div class="statusBox"><i class="sLed"></i><span id="status">ready</span></div>
<div class="stepLed" id="stepLed"></div>
<div class="knobs">
<div class="knob">
<div class="top"><span>SWING</span><b id="swingVal">0%</b></div>
<input type="range" id="swing" min="0" max="60" step="1" value="0">
</div>
<div class="knob">
<div class="top"><span>MASTER</span><b id="volVal">80%</b></div>
<input type="range" id="master" min="0" max="100" step="1" value="80">
</div>
<div class="knob">
<div class="top"><span>TEMPO</span><b id="bpmVal2">120</b></div>
<input type="range" id="bpm" min="60" max="200" step="1" value="120">
</div>
</div>
</header>
<div class="gridPanel">
<div class="grid" id="grid"></div>
</div>
<footer>
<span><kbd>Space</kbd> play/stop</span>
<span><kbd>Click</kbd> toggle</span>
<span><kbd>Drag</kbd> paint/erase</span>
<span><kbd>Right‑click</kbd> cycle hit / accent / off</span>
<span><kbd>Shift‑Click</kbd> accent</span>
<span><kbd>M</kbd> mute · <kbd>Shift‑M</kbd> solo</span>
<span class="legend">
<span><i style="background:linear-gradient(180deg,#ff8a55,#e0461c)"></i>hit</span>
<span><i style="background:linear-gradient(180deg,#ffd98a,#ff9a1f)"></i>accent</span>
<span><i style="background:#1c2229"></i>off</span>
</span>
</footer>
</div>
<script>
/* ============================================================================
TR-8086 — 16-step drum sequencer
Vanilla JS + Web Audio API. No assets, no libraries.
SECTION MAP
1. CONSTANTS / PATCH PARAMS 2. STATE MODEL
3. AUDIO ENGINE (context, buses, noise)
4. INSTRUMENTS (synthesized voices) 5. SCHEDULER (lookahead)
6. GRID UI (pads, velocity, paint/drag) 7. TRACK CONTROLS
8. TRANSPORT / CONTROLS 9. PLAYHEAD (audio-clock driven)
10. PERSISTENCE 11. KEYBOARD / BOOT
========================================================================== */
'use strict';
/* ============================ 1. CONSTANTS ================================ */
const C = {
STEPS: 16, // steps per bar
TRACKS: 6, // instrument count
BPM_MIN: 60, BPM_MAX: 200, BPM_DEF: 120,
SWING_MAX: 60, // percent
// swing delay (in step-fractions) at 100% swing; 0.55 ~= hard triplet
// shuffle at the 60% ceiling. Applied per-step in the time calculation.
SWING_SCALE: 0.55,
LOOKAHEAD_MS: 25, // how often the scheduler wakes up
SCHEDULE_AHEAD: 0.10, // seconds of audio scheduled in advance
START_PAD: 0.06, // safety offset for the very first step
VEL_NORMAL: 0.78, // per-track gain multiplier, normal hit
VEL_ACCENT: 1.18, // ...accented hit
LS_KEY: 'tr8086.pattern.v1'
};
/* Instrument patch parameters (all tunable in one place) */
const PATCH = {
kick: { f0:150, f1:48, pDecay:0.075, decay:0.52, click:0.14 },
snare: { tone:186, tone2:236, tDecay:0.12, noiseDecay:0.20,
hp:1150, hpAcc:2400, bp:1800 },
hatC: { hp:7600, bp:9500, decay:0.055, q:0.9 },
hatO: { hp:6600, bp:8200, decay:0.33, q:0.8 },
clap: { bp:1150, q:1.4, spacing:0.0095, bursts:4, decay:0.20 },
tom: { f0:210, f1:118, pDecay:0.10, decay:0.30 }
};
const TRACKS = [
{ id:'kick', name:'KICK', icon:'\u25CF', c1:'#ff8a55', c2:'#e0461c', cg:'rgba(255,90,43,.75)', vol:0.95 },
{ id:'snare', name:'SNARE', icon:'\u25B2', c1:'#ffd98a', c2:'#f09000', cg:'rgba(255,176,58,.7)', vol:0.80 },
{ id:'hatC', name:'HI-HAT', icon:'\u25C6', c1:'#9fe8ff', c2:'#2b9ec4', cg:'rgba(90,200,255,.7)', vol:0.55 },
{ id:'hatO', name:'OPEN HAT', icon:'\u25CB', c1:'#8fe0ff', c2:'#207fa8', cg:'rgba(80,190,255,.6)', vol:0.38 },
{ id:'clap', name:'CLAP', icon:'\u2737', c1:'#ffb2e6', c2:'#c02f8e', cg:'rgba(255,120,200,.65)',vol:0.62 },
{ id:'tom', name:'TOM', icon:'\u25AD', c1:'#b6a6ff', c2:'#6a4fd8', cg:'rgba(150,120,255,.65)',vol:0.66 }
];
const DEFAULT_PATTERN = [
// kick snare hatC hatO clap tom
[2,0,0,0, 2,0,0,0, 2,0,0,0, 2,0,0,0], // 8th-note thump with accents on 1 & 3
[0,0,0,0, 2,0,0,0, 0,0,0,0, 2,0,0,1],
[2,0,1,0, 2,0,1,0, 2,0,1,0, 2,0,1,0],
[0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0],
[0,0,0,0, 2,0,0,0, 0,0,0,0, 0,0,0,0],
[0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,2]
];
/* ============================ 2. STATE =================================== */
const state = {
pattern: DEFAULT_PATTERN.map(r => r.slice()), // 0=off 1=hit 2=accent
bpm: C.BPM_DEF,
swing: 0, // percent
master: 0.80,
trackVol: TRACKS.map(t => t.vol),
trackMute: TRACKS.map(() => false),
trackSolo: TRACKS.map(() => false),
playing: false
};
/* ============================ 3. AUDIO ENGINE ============================ */
let ac = null; // single shared AudioContext
let masterGain, limiter, trackGain = [];
let noiseBuf = null;
function initAudio(){
if (ac) { if (ac.state === 'suspended') ac.resume(); return ac; }
ac = new (window.AudioContext || window.webkitAudioContext)();
// master bus: gain -> soft limiter -> speakers
masterGain = ac.createGain();
masterGain.gain.value = state.master;
limiter = ac.createDynamicsCompressor();
limiter.threshold.value = -6; limiter.knee.value = 4;
limiter.ratio.value = 12; limiter.attack.value = 0.002; limiter.release.value = 0.12;
masterGain.connect(limiter).connect(ac.destination);
// one gain node per track
for (let i = 0; i < C.TRACKS; i++){
const g = ac.createGain();
g.gain.value = state.trackVol[i];
g.connect(masterGain);
trackGain.push(g);
}
// shared white-noise buffer (reused by every noise voice)
const n = Math.floor(ac.sampleRate * 2);
noiseBuf = ac.createBuffer(1, n, ac.sampleRate);
const d = noiseBuf.getChannelData(0);
for (let i = 0; i < n; i++) d[i] = Math.random() * 2 - 1;
return ac;
}
function noiseSource(t){
const s = ac.createBufferSource();
s.buffer = noiseBuf;
s.loop = true;
s.playbackRate.value = 0.9 + Math.random() * 0.2; // tiny variation = life
const off = ac.sampleRate * 0.05 * Math.random();
s.start(t, off);
return s;
}
/* output bus for a track, respecting mute / solo */
function busFor(i){
const anySolo = state.trackSolo.some(Boolean);
const audible = anySolo ? state.trackSolo[i] : !state.trackMute[i];
return { g: trackGain[i], audible, gain: audible ? 1 : 0 };
}
/* ============================ 4. INSTRUMENTS ============================= */
/* Every voice: gain envelopes begin at 0 and end at ~0 → no clicks/pops. */
function voiceKick(t, v, out){
const P = PATCH.kick;
const o = ac.createOscillator(), g = ac.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(P.f0 * (1 + (v - 1) * 0.12), t);
o.frequency.exponentialRampToValueAtTime(P.f1, t + P.pDecay);
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(0.001, t + 0.002);
g.gain.linearRampToValueAtTime(1.0 * v, t + 0.006);
g.gain.exponentialRampToValueAtTime(0.0008, t + P.decay);
o.connect(g).connect(out);
o.start(t); o.stop(t + P.decay + 0.02);
// beater click for attack definition
const n = noiseSource(t), hp = ac.createBiquadFilter(), ng = ac.createGain();
hp.type = 'highpass'; hp.frequency.value = 1400;
ng.gain.setValueAtTime(0, t);
ng.gain.linearRampToValueAtTime(P.click * v, t + 0.001);
ng.gain.exponentialRampToValueAtTime(0.0005, t + 0.018);
n.connect(hp).connect(ng).connect(out);
n.stop(t + 0.03);
}
function voiceSnare(t, v, out){
const P = PATCH.snare;
// tonal body
const body = ac.createGain();
body.gain.setValueAtTime(0, t);
body.gain.linearRampToValueAtTime(0.001, t + 0.002);
body.gain.linearRampToValueAtTime(0.5 * v, t + 0.004);
body.gain.exponentialRampToValueAtTime(0.0006, t + P.tDecay);
body.connect(out);
[P.tone, P.tone2].forEach((f, k) => {
const o = ac.createOscillator(), og = ac.createGain();
o.type = k ? 'sine' : 'triangle';
o.frequency.setValueAtTime(f * (1 + (v - 1) * 0.06), t);
o.frequency.exponentialRampToValueAtTime(f * 0.72, t + P.tDecay * 1.1);
og.gain.value = k ? 0.5 : 1;
o.connect(og).connect(body);
o.start(t); o.stop(t + P.tDecay + 0.03);
});
// snap: noise through a highpass (brighter when accented) + bandpass body
const n = noiseSource(t), hp = ac.createBiquadFilter(), bp = ac.createBiquadFilter(),
ng = ac.createGain();
hp.type = 'highpass';
hp.frequency.setValueAtTime(v > 1 ? P.hpAcc : P.hp, t);
bp.type = 'bandpass'; bp.frequency.value = P.bp; bp.Q.value = 0.7;
const amp = 0.85 * (v > 1 ? 1.15 : 0.92);
ng.gain.setValueAtTime(0, t);
ng.gain.linearRampToValueAtTime(amp * v * 0.7, t + 0.002);
ng.gain.exponentialRampToValueAtTime(0.0005, t + P.noiseDecay);
n.connect(hp).connect(bp).connect(ng).connect(out);
n.stop(t + P.noiseDecay + 0.05);
}
function voiceHat(t, v, out, open){
const P = open ? PATCH.hatO : PATCH.hatC;
const n = noiseSource(t), hp = ac.createBiquadFilter(), bp = ac.createBiquadFilter(),
g = ac.createGain();
hp.type = 'highpass'; hp.frequency.value = P.hp * (v > 1 ? 1.15 : 1);
bp.type = 'bandpass'; bp.frequency.value = P.bp; bp.Q.value = P.q;
const amp = open ? 0.55 : 0.5;
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(amp * (v > 1 ? 1.1 : 0.85), t + 0.0015);
g.gain.exponentialRampToValueAtTime(0.0004, t + P.decay * (v > 1 ? 1.15 : 1));
n.connect(hp).connect(bp).connect(g).connect(out);
n.stop(t + P.decay * 1.3 + 0.05);
}
function voiceClap(t, v, out){
const P = PATCH.clap;
const n = noiseSource(t), bp = ac.createBiquadFilter(), g = ac.createGain();
bp.type = 'bandpass'; bp.frequency.value = P.bp; bp.Q.value = P.q;
const peak = 0.75 * (v > 1 ? 1.1 : 0.85);
g.gain.setValueAtTime(0, t);
// several tight bursts in quick succession...
for (let k = 0; k < P.bursts; k++){
const bt = t + k * P.spacing;
const p = peak * (1 - k * 0.13);
g.gain.linearRampToValueAtTime(0.001, bt);
g.gain.linearRampToValueAtTime(p, bt + 0.0012);
g.gain.exponentialRampToValueAtTime(0.02, bt + P.spacing * 0.92);
}
// ...then the room tail
const tt = t + P.bursts * P.spacing;
g.gain.linearRampToValueAtTime(peak * 0.8, tt);
g.gain.exponentialRampToValueAtTime(0.0004, tt + P.decay);
n.connect(bp).connect(g).connect(out);
n.stop(tt + P.decay + 0.06);
}
function voiceTom(t, v, out){
const P = PATCH.tom;
const o = ac.createOscillator(), g = ac.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(P.f0 * (1 + (v - 1) * 0.08), t);
o.frequency.exponentialRampToValueAtTime(P.f1, t + P.pDecay);
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(0.001, t + 0.002);
g.gain.linearRampToValueAtTime(0.8 * v, t + 0.005);
g.gain.exponentialRampToValueAtTime(0.0006, t + P.decay);
o.connect(g).connect(out);
o.start(t); o.stop(t + P.decay + 0.04);
// a touch of noise transient
const n = noiseSource(t), bp = ac.createBiquadFilter(), ng = ac.createGain();
bp.type = 'bandpass'; bp.frequency.value = 900; bp.Q.value = 1.2;
ng.gain.setValueAtTime(0, t);
ng.gain.linearRampToValueAtTime(0.16 * v, t + 0.001);
ng.gain.exponentialRampToValueAtTime(0.0004, t + 0.05);
n.connect(bp).connect(ng).connect(out);
n.stop(t + 0.08);
}
const VOICES = [voiceKick, voiceSnare,
(t,v,o)=>voiceHat(t,v,o,false), (t,v,o)=>voiceHat(t,v,o,true),
voiceClap, voiceTom];
/* ============================ 5. SCHEDULER =============================== */
/* The classic lookahead pattern: a slow JS timer wakes up every LOOKAHEAD_MS
and pushes every step that falls inside the next SCHEDULE_AHEAD seconds
onto the *Web Audio clock*. Audio events are never fired from the timer —
only scheduled. Step times come from an absolute step counter times the
step duration, added to a fixed anchor time (no per-tick re-anchoring, so
timer jitter cannot accumulate). */
let timerId = null;
let stepCounter = 0; // absolute index since playback started
let nextStepTime = 0; // audio-clock time of the next UNSWUNG step
let schedTimes = []; // future {step, time} queue → drives the playhead
const MAX_QUEUE = 64;
let events = []; // instrumentation: window.__events mirror
let voiceLog = []; // instrumentation: which voices actually fired
function logVoice(track, vel, time){
voiceLog.push({ track, vel, time: +time.toFixed(5) });
if (voiceLog.length > 600) voiceLog.splice(0, voiceLog.length - 600);
}
function secPerStep(){ return 60 / state.bpm / 4; } // 16th note
function swingOffset(step){
// every second 16th ("off-beat") is pushed late by a fraction of a step.
return (step % 2 === 1) ? (state.swing / 100) * C.SWING_SCALE * secPerStep() : 0;
}
function scheduleStep(step, t){
const s = step % C.STEPS;
for (let i = 0; i < C.TRACKS; i++){
const cell = state.pattern[i][s];
if (!cell) continue;
const bus = busFor(i);
if (!bus.audible) continue; // muted/soloed-out: no voice at all
const v = cell === 2 ? C.VEL_ACCENT : C.VEL_NORMAL;
VOICES[i](t, v, bus.g);
logVoice(i, v, t);
}
schedTimes.push({ step: step, time: t });
if (schedTimes.length > MAX_QUEUE) schedTimes.splice(0, schedTimes.length - MAX_QUEUE);
events.push({ step: step, time: t, grid: t - swingOffset(step), bpm: state.bpm });
if (events.length > 400) events.splice(0, events.length - 400);
}
let lastWake = 0; // for adaptive lookahead under timer throttling
let timerEvery = C.LOOKAHEAD_MS;
function scheduler(){
const now = ac.currentTime;
/* Adaptive lookahead: normally we keep SCHEDULE_AHEAD seconds of audio
queued. If the timer woke up late (background-tab throttling stretches
setInterval to ~1s), we queue proportionally further ahead so the audio
never runs dry. Timing itself is unaffected — only how far in advance
events are placed on the audio clock. */
const sinceWake = lastWake ? now - lastWake : 0;
lastWake = now;
const horizon = Math.min(3, Math.max(C.SCHEDULE_AHEAD, sinceWake * 2.2));
while (nextStepTime < now + horizon){
const t = nextStepTime + swingOffset(stepCounter);
scheduleStep(stepCounter, t);
nextStepTime += secPerStep(); // absolute grid advance (no swing drift)
stepCounter++;
}
}
function armTimer(){
if (timerId) clearInterval(timerId);
timerId = setInterval(scheduler, timerEvery);
}
function startTransport(){
initAudio();
if (ac.state === 'suspended') ac.resume();
state.playing = true;
stepCounter = 0;
schedTimes = [];
events = [];
voiceLog = [];
curStepAbs = -1;
lastPainted = -2;
nextStepTime = ac.currentTime + C.START_PAD;
lastWake = 0;
scheduler();
armTimer();
uiPlay(true);
}
function stopTransport(){
state.playing = false;
if (timerId){ clearInterval(timerId); timerId = null; }
lastWake = 0;
// silence anything already scheduled in the lookahead window
if (ac){
const now = ac.currentTime;
masterGain.gain.cancelScheduledValues(now);
masterGain.gain.setValueAtTime(masterGain.gain.value, now);
masterGain.gain.linearRampToValueAtTime(0, now + 0.015);
masterGain.gain.setTargetAtTime(state.master, now + 0.02, 0.005);
}
schedTimes = [];
curStepAbs = -1;
drawPlayhead(-1);
uiPlay(false);
}
/* tempo changes: re-anchor the pending step so the grid glides smoothly */
function reanchorTempo(oldSps){
if (!state.playing || !ac) return;
const now = ac.currentTime;
const rem = Math.max(0, (nextStepTime - now) / oldSps); // step-fractions left
nextStepTime = now + rem * secPerStep();
}
/* ============================ 6. GRID UI ================================= */
const gridEl = document.getElementById('grid');
let pads = []; // pads[track][step]
let nums = [];
function buildGrid(){
gridEl.innerHTML = '';
pads = TRACKS.map(() => []);
nums = [];
muteBtns = [];
// header row: step numbers + beat grouping
const head = el('div', 'row head');
head.appendChild(el('div', 'num')); // label spacer
for (let s = 0; s < C.STEPS; s++){
const n = el('div', 'num' + (s % 4 === 0 ? ' beat' : ''));
n.innerHTML = (s % 4 === 0 ? '•' : '') + '<b>' + (s + 1) + '</b>';
head.appendChild(n); nums.push(n);
}
head.appendChild(el('div', 'num'));
head.appendChild(el('div', 'num'));
gridEl.appendChild(head);
TRACKS.forEach((tr, i) => {
const row = el('div', 'row');
row.dataset.track = i;
const lab = el('div', 'tlabel');
lab.innerHTML = '<span class="ico">' + tr.icon + '</span><span class="nm">' + tr.name + '</span>';
row.appendChild(lab);
for (let s = 0; s < C.STEPS; s++){
const p = el('div', 'pad' + (s % 4 === 0 ? ' beatL' : ''));
p.style.setProperty('--pc1', tr.c1);
p.style.setProperty('--pc2', tr.c2);
p.style.setProperty('--pcg', tr.cg);
p.dataset.track = i; p.dataset.step = s;
row.appendChild(p);
pads[i].push(p);
}
// per-track volume
const volCell = el('div', 'tvol');
const vs = document.createElement('input');
vs.type = 'range'; vs.min = 0; vs.max = 100; vs.step = 1;
vs.value = Math.round(state.trackVol[i] * 100);
vs.title = tr.name + ' volume';
vs.addEventListener('input', () => {
state.trackVol[i] = +vs.value / 100;
setTrackGain(i);
save();
});
volCell.appendChild(vs);
row.appendChild(volCell);
// mute / solo toggle (click = mute, shift-click or dbl = solo)
const mb = el('button', 'tbtn', 'M');
mb.title = tr.name + ' mute';
mb.addEventListener('click', e => {
if (e.shiftKey){ toggleSolo(i); } else { toggleMute(i); }
});
mb.addEventListener('contextmenu', e => { e.preventDefault(); toggleSolo(i); });
muteBtns.push(mb);
row.appendChild(mb);
gridEl.appendChild(row);
});
// LED strip in the header
const led = document.getElementById('stepLed');
led.innerHTML = '';
for (let s = 0; s < C.STEPS; s++){
const d = document.createElement('i');
if (s % 4 === 0) d.className = 'beat';
led.appendChild(d);
}
}
function el(tag, cls, txt){
const e = document.createElement(tag);
if (cls) e.className = cls;
if (txt != null) e.textContent = txt;
return e;
}
/* ---- pad painting ---- */
function paintCell(i, s, val){
state.pattern[i][s] = val;
const p = pads[i][s];
p.classList.toggle('on', val > 0);
p.classList.toggle('acc', val === 2);
}
function cellAt(x, y){
const e = document.elementFromPoint(x, y);
if (!e) return null;
const p = e.closest ? e.closest('.pad') : null;
if (!p) return null;
return { t: +p.dataset.track, s: +p.dataset.step };
}
let paint = null; // {mode:0|1|2, lastStart:timestamp}
gridEl.addEventListener('pointerdown', e => {
const c = cellAt(e.clientX, e.clientY);
if (!c) return;
e.preventDefault();
if (e.button === 2){ // right-click: cycle 0→1→2→0
const v = (state.pattern[c.t][c.s] + 1) % 3;
paintCell(c.t, c.s, v);
save();
return;
}
// left-click: toggle (0↔1). Note-off cells paint as accents with Shift.
const cur = state.pattern[c.t][c.s];
paint = { mode: cur > 0 ? 0 : (e.shiftKey ? 2 : 1), last: c };
paintCell(c.t, c.s, paint.mode);
try { gridEl.setPointerCapture(e.pointerId); } catch (err) { /* capture unsupported */ }
});
gridEl.addEventListener('pointermove', e => {
if (!paint) return;
const c = cellAt(e.clientX, e.clientY);
if (!c) return;
if (c.t === paint.last.t && c.s === paint.last.s) return;
paint.last = c;
paintCell(c.t, c.s, paint.mode);
});
function endPaint(){ if (paint){ paint = null; save(); } }
gridEl.addEventListener('pointerup', endPaint);
gridEl.addEventListener('pointercancel', endPaint);
gridEl.addEventListener('contextmenu', e => e.preventDefault());
window.addEventListener('blur', () => { if (paint) endPaint(); });
/* ============================ 7. TRACK CONTROLS ========================== */
let muteBtns = [];
function setTrackGain(i){
const anySolo = state.trackSolo.some(Boolean);
const audible = anySolo ? state.trackSolo[i] : !state.trackMute[i];
const g = trackGain[i];
if (!g) return;
const target = state.trackVol[i] * (audible ? 1 : 0);
g.gain.setTargetAtTime(target, ac.currentTime, 0.01);
}
function refreshTrackRows(){
const anySolo = state.trackSolo.some(Boolean);
TRACKS.forEach((tr, i) => {
const row = gridEl.children[i + 1];
const audible = anySolo ? state.trackSolo[i] : !state.trackMute[i];
row.classList.toggle('muted', !audible);
const b = muteBtns[i];
b.classList.toggle('muteOn', state.trackMute[i]);
b.classList.toggle('soloOn', state.trackSolo[i]);
b.textContent = state.trackSolo[i] ? 'S' : 'M';
setTrackGain(i);
});
}
function toggleMute(i){ state.trackMute[i] = !state.trackMute[i]; refreshTrackRows(); save(); }
function toggleSolo(i){ state.trackSolo[i] = !state.trackSolo[i]; refreshTrackRows(); save(); }
/* ============================ 9. PLAYHEAD ================================ */
/* The playhead is read from the AudioContext clock, NOT from the timer and
NOT from step counting in rAF: we look at the queue of steps that were
scheduled on the audio clock and pick the latest one whose time has passed. */
let rafId = null;
let lastPainted = -2;
let curStepAbs = -1; // last step whose scheduled time has passed (audio clock)
function drawPlayhead(step){
if (step === lastPainted) return;
lastPainted = step;
const cur = step < 0 ? -1 : step;
nums.forEach((n, s) => n.classList.toggle('cur', s === cur));
const leds = document.getElementById('stepLed').children;
for (let s = 0; s < C.STEPS; s++) leds[s].classList.toggle('cur', s === cur);
TRACKS.forEach((tr, i) => {
for (let s = 0; s < C.STEPS; s++){
const p = pads[i][s];
p.classList.toggle('col', s === cur);
p.classList.toggle('hit', s === cur && state.pattern[i][s] > 0);
}
});
}
function frame(){
rafId = requestAnimationFrame(frame);
if (!ac || !state.playing) return;
const now = ac.currentTime;
while (schedTimes.length && schedTimes[0].time <= now + 0.0002){
curStepAbs = schedTimes.shift().step; // most recent step that has begun
}
drawPlayhead(curStepAbs < 0 ? -1 : curStepAbs % C.STEPS);
}
function startRaf(){ if (rafId == null) rafId = requestAnimationFrame(frame); }
/* ============================ 8. TRANSPORT / CONTROLS ==================== */
function uiPlay(on){
const b = document.getElementById('btnPlay');
b.classList.toggle('on', on);
b.textContent = on ? '\u25A0 STOP' : '\u25B6 PLAY';
document.getElementById('status').textContent = on ? 'playing' : 'stopped';
}
const btnPlay = document.getElementById('btnPlay');
btnPlay.addEventListener('click', () => { state.playing ? stopTransport() : startTransport(); });
document.getElementById('btnClear').addEventListener('click', () => {
for (let i = 0; i < C.TRACKS; i++) for (let s = 0; s < C.STEPS; s++) paintCell(i, s, 0);
save();
});
document.getElementById('btnRand').addEventListener('click', () => { randomize(); save(); });
function randomize(){
// musical probabilities: kick on the grid, hats on 8ths, snare on 2 & 4
const kern = [
s => (s % 8 === 0 ? 3 : s % 4 === 0 ? 2 : s % 2 === 0 ? 0.25 : 0.05), // kick
s => (s === 4 || s === 12 ? 1.0 : s === 15 ? 0.35 : 0.04), // snare
s => (s % 2 === 0 ? 0.85 : 0.10), // closed hat
s => (s === 6 || s === 14 ? 0.22 : 0.03), // open hat
s => (s === 4 || s === 12 ? 0.30 : 0), // clap
s => (s === 10 ? 0.25 : s === 7 ? 0.18 : 0.04) // tom
];
for (let i = 0; i < C.TRACKS; i++)
for (let s = 0; s < C.STEPS; s++){
let v = 0;
if (Math.random() < kern[i](s)) v = Math.random() < 0.25 ? 2 : 1;
paintCell(i, s, v);
}
}
/* --- tempo --- */
const bpmSlider = document.getElementById('bpm');
function setBpm(v, fromSlider){
const oldSps = secPerStep();
state.bpm = Math.max(C.BPM_MIN, Math.min(C.BPM_MAX, Math.round(v)));
if (!fromSlider) bpmSlider.value = state.bpm;
document.getElementById('bpmVal').innerHTML = state.bpm + '<small> BPM</small>';
document.getElementById('bpmVal2').textContent = state.bpm;
reanchorTempo(oldSps);
save();
}
bpmSlider.addEventListener('input', () => setBpm(+bpmSlider.value, true));
/* --- swing / master --- */
const swingSlider = document.getElementById('swing');
swingSlider.addEventListener('input', () => {
state.swing = +swingSlider.value;
document.getElementById('swingVal').textContent = state.swing + '%';
save();
});
const masterSlider = document.getElementById('master');
masterSlider.addEventListener('input', () => {
state.master = +masterSlider.value / 100;
document.getElementById('volVal').textContent = masterSlider.value + '%';
if (masterGain) masterGain.gain.setTargetAtTime(state.master, ac.currentTime, 0.01);
save();
});
/* ============================ 10. PERSISTENCE ============================ */
let saveTimer = null;
function save(){
if (saveTimer) return;
saveTimer = setTimeout(() => {
saveTimer = null;
const data = {
pattern: state.pattern, bpm: state.bpm, swing: state.swing, master: state.master,
trackVol: state.trackVol, trackMute: state.trackMute, trackSolo: state.trackSolo
};
try { localStorage.setItem(C.LS_KEY, JSON.stringify(data)); } catch (e) { /* opaque origin */ }
const st = document.getElementById('status');
if (st) st.textContent = state.playing ? 'playing \u00b7 saved' : 'saved';
}, 250);
}
function load(){
let raw = null;
try { raw = localStorage.getItem(C.LS_KEY); } catch (e) { raw = null; }
if (!raw) return false;
try {
const d = JSON.parse(raw);
if (Array.isArray(d.pattern) && d.pattern.length === C.TRACKS){
state.pattern = d.pattern.map(r => Array.from({ length: C.STEPS }, (_, s) => (r[s] | 0) % 3));
}
if (typeof d.bpm === 'number') state.bpm = Math.max(C.BPM_MIN, Math.min(C.BPM_MAX, Math.round(d.bpm)));
if (typeof d.swing === 'number') state.swing = Math.max(0, Math.min(C.SWING_MAX, d.swing));
if (typeof d.master === 'number') state.master = Math.max(0, Math.min(1, d.master));
if (Array.isArray(d.trackVol)) state.trackVol = d.trackVol.slice(0, C.TRACKS).map(Number);
if (Array.isArray(d.trackMute)) state.trackMute = d.trackMute.slice(0, C.TRACKS).map(Boolean);
if (Array.isArray(d.trackSolo)) state.trackSolo = d.trackSolo.slice(0, C.TRACKS).map(Boolean);
return true;
} catch (e) { return false; }
}
/* ============================ 11. KEYBOARD / BOOT ======================== */
window.addEventListener('keydown', e => {
if (e.code === 'Space'){
e.preventDefault();
state.playing ? stopTransport() : startTransport();
}
});
function syncUiFromState(){
bpmSlider.value = state.bpm;
document.getElementById('bpmVal').innerHTML = state.bpm + '<small> BPM</small>';
document.getElementById('bpmVal2').textContent = state.bpm;
swingSlider.value = state.swing;
document.getElementById('swingVal').textContent = state.swing + '%';
masterSlider.value = Math.round(state.master * 100);
document.getElementById('volVal').textContent = Math.round(state.master * 100) + '%';
TRACKS.forEach((tr, i) => {
for (let s = 0; s < C.STEPS; s++) paintCell(i, s, state.pattern[i][s]);
gridEl.children[i + 1].querySelector('input[type=range]').value =
Math.round(state.trackVol[i] * 100);
});
}
buildGrid();
load();
syncUiFromState();
refreshTrackRows();
startRaf();
/* test hooks (automated verification) */
window.__seq = {
state: state,
get events(){ return events; },
get voiceLog(){ return voiceLog; },
get schedTimes(){ return schedTimes; },
secPerStep: secPerStep,
get ctx(){ return ac; },
get now(){ return ac ? ac.currentTime : 0; },
swingOffset: swingOffset,
setBpm: setBpm,
start: startTransport,
stop: stopTransport,
randomize: randomize,
save: save,
load: load,
patchCell: paintCell,
scheduler: scheduler,
setTimerInterval: (ms) => { timerEvery = ms; if (state.playing) armTimer(); },
get timerId(){ return timerId; }
};
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"a1019073-3496-4389-a7d6-398140c5b308","tokensIn":4157233,"tokensOut":46062,"tokensTotal":4203295,"cost":0.05027674199999998,"turns":64,"toolCalls":63,"failedToolCalls":1,"timestamp":"2026-09-10T23:52:18.470Z"} -->