Sequencer
Qwen 3.6 27b MTP · 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 28,900 bytes · SHA-256 64566f2c906a
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>STEP SEQ-16 — Drum Sequencer</title>
<style>
/* ============================================
CSS — Dark Hardware Drum Machine Aesthetic
============================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #12122a;
color: #d0d0e0;
font-family: 'Courier New', monospace;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
padding: 16px;
user-select: none;
}
#app {
background: linear-gradient(145deg, #1a1a3e, #12122a);
border-radius: 14px;
padding: 28px 32px;
box-shadow: 0 0 60px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.04);
border: 1px solid #2a2a5e;
max-width: 1160px;
width: 100%;
}
/* ── Header ── */
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 2px solid #2a2a5e;
flex-wrap: wrap;
gap: 12px;
}
.title {
font-size: 22px;
font-weight: bold;
color: #ff3366;
text-shadow: 0 0 14px rgba(255,51,102,0.5);
letter-spacing: 4px;
}
/* ── Transport Controls ── */
.transport {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.btn {
background: #2a2a5e;
color: #d0d0e0;
border: 1px solid #4a3a8e;
padding: 9px 20px;
border-radius: 6px;
cursor: pointer;
font-family: inherit;
font-size: 13px;
letter-spacing: 1px;
transition: all 0.15s;
}
.btn:hover { background: #4a3a8e; }
.btn.active {
background: #ff3366;
border-color: #ff3366;
box-shadow: 0 0 16px rgba(255,51,102,0.5);
color: #fff;
}
/* ── Tempo Display ── */
.tempo-group {
display: flex;
align-items: center;
gap: 8px;
}
.bpm-display {
background: #0a0a1e;
color: #ff3366;
font-size: 24px;
font-weight: bold;
padding: 4px 14px;
border-radius: 5px;
min-width: 72px;
text-align: center;
border: 1px solid #3a3a6e;
text-shadow: 0 0 10px rgba(255,51,102,0.7);
font-variant-numeric: tabular-nums;
}
.bpm-label { font-size: 11px; color: #888; }
/* ── Sliders ── */
.slider-group {
display: flex;
align-items: center;
gap: 6px;
}
.slider-group label {
font-size: 11px;
color: #999;
white-space: nowrap;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 80px;
height: 6px;
background: #0a0a1e;
border-radius: 3px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #ff3366;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 6px rgba(255,51,102,0.4);
}
input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
background: #ff3366;
border-radius: 50%;
cursor: pointer;
border: none;
}
/* ── Sequencer Grid ── */
.sequencer-wrapper {
overflow-x: auto;
margin-top: 18px;
padding-bottom: 4px;
}
.grid-container {
display: flex;
gap: 2px;
min-width: fit-content;
}
.track-row {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
}
.track-label {
font-size: 10px;
color: #777;
text-align: center;
width: 100%;
padding-bottom: 2px;
white-space: nowrap;
letter-spacing: 1px;
}
.steps-row {
display: flex;
gap: 3px;
}
.step-cell {
width: 50px;
height: 42px;
background: #1a1a3e;
border-radius: 5px;
cursor: pointer;
transition: background 0.08s, box-shadow 0.08s;
position: relative;
border: 1px solid transparent;
}
.step-cell:hover { border-color: #4a3a8e; }
/* Velocity states */
.step-cell.off { background: #16162e; }
.step-cell.normal {
background: linear-gradient(135deg, #4a3a8e, #3a2a7e);
box-shadow: inset 0 0 10px rgba(74,58,142,0.6), 0 0 4px rgba(74,58,142,0.3);
}
.step-cell.accent {
background: linear-gradient(135deg, #ff3366, #cc2255);
box-shadow: inset 0 0 14px rgba(255,51,102,0.7), 0 0 8px rgba(255,51,102,0.4);
}
/* Beat grouping */
.step-cell.beat-start { border-left: 2px solid #3a3a6e; }
/* Playhead highlight — LED strip effect */
.step-cell.playhead::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
background: #fff;
border-radius: 5px 5px 0 0;
box-shadow: 0 0 12px rgba(255,255,255,0.8), 0 0 24px rgba(255,255,255,0.3);
}
/* ── Track Controls Row ── */
.track-controls-row {
display: flex;
align-items: center;
gap: 3px;
margin-top: 4px;
}
.mute-btn {
width: 22px;
height: 18px;
background: #2a2a5e;
border: none;
color: #999;
font-size: 9px;
cursor: pointer;
border-radius: 3px;
padding: 2px;
letter-spacing: 0.5px;
}
.mute-btn.muted { background: #ff3366; color: #fff; }
.track-vol {
width: 48px !important;
height: 4px !important;
}
/* ── Step Numbers ── */
.step-numbers {
display: flex;
gap: 3px;
margin-left: 76px;
margin-bottom: 5px;
}
.step-number {
width: 50px;
text-align: center;
font-size: 10px;
color: #444;
}
/* ── Utility Buttons Row ── */
.util-row {
display: flex;
gap: 8px;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #2a2a5e;
}
/* ── Status Bar ── */
.status-bar {
margin-top: 14px;
font-size: 11px;
color: #555;
display: flex;
justify-content: space-between;
}
/* Dimmed track when muted */
.track-row.muted .step-cell { opacity: 0.25; }
.track-row.muted .track-label { color: #3a3a6e; }
/* ── Responsive ── */
@media (max-width: 900px) {
.step-cell { width: 38px; height: 34px; }
.step-number { width: 38px; }
}
</style>
</head>
<body>
<div id="app">
<div class="header">
<div class="title">⚡ STEP SEQ-16</div>
<div class="transport">
<button class="btn" id="playBtn">▶ PLAY</button>
<button class="btn" id="stopBtn">■ STOP</button>
<div class="tempo-group">
<span class="bpm-label">BPM</span>
<input type="range" id="bpmSlider" min="60" max="200" value="120">
<div class="bpm-display" id="bpmDisplay">120</div>
</div>
<div class="slider-group">
<label>SWING</label>
<input type="range" id="swingSlider" min="0" max="60" value="0">
<span id="swingDisplay" style="font-size:12px;min-width:32px;text-align:right;">0%</span>
</div>
<div class="slider-group">
<label>VOL</label>
<input type="range" id="masterVolSlider" min="0" max="100" value="80">
</div>
</div>
</div>
<!-- Step numbers -->
<div class="step-numbers" id="stepNumbers"></div>
<!-- Sequencer Grid -->
<div class="sequencer-wrapper">
<div class="grid-container" id="gridContainer"></div>
</div>
<!-- Utility buttons -->
<div class="util-row">
<button class="btn" id="clearBtn">CLEAR</button>
<button class="btn" id="randomBtn">RANDOM</button>
<span style="font-size:11px;color:#555;margin-left:auto;">Left-click = cycle velocity · Right-click = accent · Drag to paint/erase</span>
</div>
<!-- Status -->
<div class="status-bar">
<span id="statusText">Ready — press Play or Space</span>
<span id="debugInfo"></span>
</div>
</div>
<script>
/* ================================================================
TUNABLE CONSTANTS
================================================================ */
const CONFIG = {
STEPS: 16,
LOOKAHEAD_INTERVAL_MS: 25, // Scheduler tick interval (ms)
SCHEDULE_AHEAD_SEC: 0.12, // How far ahead to schedule audio (seconds)
BPM_MIN: 60,
BPM_MAX: 200,
DEFAULT_BPM: 120,
DEFAULT_SWING: 0,
MASTER_VOL_DEFAULT: 80,
// Instrument synthesis parameters
KICK: { freqStart: 150, freqEnd: 45, decay: 0.35 },
SNARE: { noiseDecay: 0.2, toneFreq: 180, toneDecay: 0.12 },
HIHAT_CLOSED: { filterFreq: 7800, decay: 0.06 },
HIHAT_OPEN: { filterFreq: 9500, decay: 0.35 },
CLAP: { filterFreq: 2400, burstCount: 5, burstInterval: 0.012, decay: 0.25 },
TOM: { freqStart: 200, freqEnd: 75, decay: 0.3 }
};
/* ================================================================
STATE
================================================================ */
const INSTRUMENTS = [
{ name: 'KICK', icon: '🥁' },
{ name: 'SNARE', icon: '💥' },
{ name: 'HI-HAT C', icon: '🔔' },
{ name: 'HI-HAT O', icon: '🔔' },
{ name: 'CLAP', icon: '👏' },
{ name: 'TOM', icon: '🪘' }
];
// pattern[track][step] = velocity: 0=off, 1=normal, 2=accent
let pattern = INSTRUMENTS.map(() => new Array(CONFIG.STEPS).fill(0));
let trackVolumes = INSTRUMENTS.map(() => 80);
let trackMutes = INSTRUMENTS.map(() => false);
// Scheduler state
let isPlaying = false;
let currentStep = 0; // absolute step counter (never resets during playback)
let nextNoteTime = 0;
let schedulerTimerId = null;
let audioCtx = null;
let masterGainNode = null;
let swingAmount = CONFIG.DEFAULT_SWING; // 0–60
// Drag painting state
let isDragging = false;
let dragValue = -1;
/* ================================================================
AUDIO ENGINE — Instrument Synthesis (Web Audio API)
================================================================ */
function initAudio() {
if (audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGainNode = audioCtx.createGain();
masterGainNode.gain.value = CONFIG.MASTER_VOL_DEFAULT / 100;
masterGainNode.connect(audioCtx.destination);
}
function resumeAudio() {
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
}
// ── Kick Drum: sine with fast pitch envelope + amplitude envelope ──
function playKick(time, velocity) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
const vol = (velocity / 2) * (trackVolumes[0] / 100);
osc.type = 'sine';
osc.frequency.setValueAtTime(CONFIG.KICK.freqStart, time);
osc.frequency.exponentialRampToValueAtTime(CONFIG.KICK.freqEnd, time + 0.08);
gain.gain.setValueAtTime(vol * 1.3, time);
gain.gain.exponentialRampToValueAtTime(0.001, time + CONFIG.KICK.decay);
osc.connect(gain).connect(masterGainNode);
osc.start(time);
osc.stop(time + CONFIG.KICK.decay + 0.05);
}
// ── Snare: filtered noise burst + tonal body (triangle) ──
function playSnare(time, velocity) {
const vol = (velocity / 2) * (trackVolumes[1] / 100);
const brightBoost = velocity === 2 ? 1.4 : 1.0; // accent → brighter
// Noise component
const nLen = audioCtx.sampleRate * CONFIG.SNARE.noiseDecay;
const buf = audioCtx.createBuffer(1, nLen, audioCtx.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < nLen; i++) d[i] = Math.random() * 2 - 1;
const noiseSrc = audioCtx.createBufferSource();
noiseSrc.buffer = buf;
const hp = audioCtx.createBiquadFilter();
hp.type = 'highpass';
hp.frequency.value = 1000 * brightBoost;
const nGain = audioCtx.createGain();
nGain.gain.setValueAtTime(vol * 0.7, time);
nGain.gain.exponentialRampToValueAtTime(0.001, time + CONFIG.SNARE.noiseDecay);
noiseSrc.connect(hp).connect(nGain).connect(masterGainNode);
noiseSrc.start(time);
noiseSrc.stop(time + CONFIG.SNARE.noiseDecay + 0.05);
// Tonal body
const osc = audioCtx.createOscillator();
const oGain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.value = CONFIG.SNARE.toneFreq;
oGain.gain.setValueAtTime(vol * 0.5, time);
oGain.gain.exponentialRampToValueAtTime(0.001, time + CONFIG.SNARE.toneDecay);
osc.connect(oGain).connect(masterGainNode);
osc.start(time);
osc.stop(time + CONFIG.SNARE.toneDecay + 0.05);
}
// ── Hi-Hat (closed / open): highpass-filtered noise, short vs long decay ──
function playHiHat(time, velocity, isOpen) {
const cfg = isOpen ? CONFIG.HIHAT_OPEN : CONFIG.HIHAT_CLOSED;
const tIdx = isOpen ? 3 : 2;
const vol = (velocity / 2) * (trackVolumes[tIdx] / 100);
const nLen = audioCtx.sampleRate * cfg.decay * 2;
const buf = audioCtx.createBuffer(1, nLen, audioCtx.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < nLen; i++) d[i] = Math.random() * 2 - 1;
const src = audioCtx.createBufferSource();
src.buffer = buf;
const hp = audioCtx.createBiquadFilter();
hp.type = 'highpass';
hp.frequency.value = cfg.filterFreq;
const g = audioCtx.createGain();
g.gain.setValueAtTime(vol * 0.45, time);
g.gain.exponentialRampToValueAtTime(0.001, time + cfg.decay);
src.connect(hp).connect(g).connect(masterGainNode);
src.start(time);
src.stop(time + cfg.decay + 0.05);
}
// ── Clap: several short noise bursts through a bandpass filter ──
function playClap(time, velocity) {
const vol = (velocity / 2) * (trackVolumes[4] / 100);
for (let b = 0; b < CONFIG.CLAP.burstCount; b++) {
const bt = time + b * CONFIG.CLAP.burstInterval;
const nLen = audioCtx.sampleRate * 0.025;
const buf = audioCtx.createBuffer(1, nLen, audioCtx.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < nLen; i++) d[i] = Math.random() * 2 - 1;
const src = audioCtx.createBufferSource();
src.buffer = buf;
const bp = audioCtx.createBiquadFilter();
bp.type = 'bandpass';
bp.frequency.value = CONFIG.CLAP.filterFreq;
bp.Q.value = 3;
const g = audioCtx.createGain();
g.gain.setValueAtTime(vol * 0.5, bt);
g.gain.exponentialRampToValueAtTime(0.001, bt + CONFIG.CLAP.decay / CONFIG.CLAP.burstCount);
src.connect(bp).connect(g).connect(masterGainNode);
src.start(bt);
src.stop(bt + CONFIG.CLAP.decay + 0.05);
}
}
// ── Tom: pitched sine with decay, musically related to kick ──
function playTom(time, velocity) {
const vol = (velocity / 2) * (trackVolumes[5] / 100);
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(CONFIG.TOM.freqStart, time);
osc.frequency.exponentialRampToValueAtTime(CONFIG.TOM.freqEnd, time + 0.12);
gain.gain.setValueAtTime(vol * 1.0, time);
gain.gain.exponentialRampToValueAtTime(0.001, time + CONFIG.TOM.decay);
osc.connect(gain).connect(masterGainNode);
osc.start(time);
osc.stop(time + CONFIG.TOM.decay + 0.05);
}
// Dispatch table: index matches INSTRUMENTS order
const playFn = [playKick, playSnare, (t,v) => playHiHat(t,v,false), (t,v) => playHiHat(t,v,true), playClap, playTom];
/* ================================================================
SCHEDULER — Lookahead Pattern (sample-accurate timing)
================================================================ */
let bpmValue = CONFIG.DEFAULT_BPM;
// Compute the duration between step i and step i+1 with swing.
// Even→odd: shortened by swing compensation, odd→even: lengthened.
// Symmetric so bar duration stays constant regardless of swing amount.
function stepDuration(stepIdx) {
const base = 60.0 / bpmValue / 4; // quarter-note in seconds
if (stepIdx % 2 === 0) return base * (1 - swingAmount / 200);
return base * (1 + swingAmount / 200);
}
// Main scheduler: runs every LOOKAHEAD_INTERVAL_MS, schedules notes ahead.
function scheduler() {
if (!isPlaying || !audioCtx) return;
const now = audioCtx.currentTime;
// Schedule all notes that fall within the lookahead window
while (nextNoteTime < now + CONFIG.SCHEDULE_AHEAD_SEC) {
scheduleNote(currentStep, nextNoteTime);
// Debug: record scheduled step times for verification
if (!window._scheduledSteps) window._scheduledSteps = [];
window._scheduledSteps.push({
step: currentStep % CONFIG.STEPS,
time: nextNoteTime,
scheduledAt: now
});
if (window._scheduledSteps.length > 200) {
window._scheduledSteps = window._scheduledSteps.slice(-100);
}
// Advance to next step time using incremental calculation from current position
nextNoteTime += stepDuration(currentStep % CONFIG.STEPS);
currentStep++;
}
// Update visual playhead from audio clock (not timer-driven)
updatePlayheadFromClock();
}
function scheduleNote(step, time) {
const stepInBar = step % CONFIG.STEPS;
for (let t = 0; t < INSTRUMENTS.length; t++) {
if (trackMutes[t]) continue;
const vel = pattern[t][stepInBar];
if (vel > 0) playFn[t](time, vel);
}
}
// Derive current visual step from audio clock — not timer-driven.
function updatePlayheadFromClock() {
if (!audioCtx || window._barStartTime === undefined) return;
const elapsed = audioCtx.currentTime - window._barStartTime;
if (elapsed < 0) return; // first note hasn't fired yet
const base = 60.0 / bpmValue / 4;
const barDur = base * CONFIG.STEPS; // constant regardless of swing
let normElapsed = elapsed % barDur;
let acc = 0, found = CONFIG.STEPS - 1;
for (let i = 0; i < CONFIG.STEPS; i++) {
acc += stepDuration(i);
if (normElapsed < acc) { found = i; break; }
found = i;
}
const prev = window._currentVisualStep;
window._currentVisualStep = found;
if (prev !== found) renderPlayhead(found);
}
function startPlayback() {
initAudio();
resumeAudio();
isPlaying = true;
currentStep = 0;
nextNoteTime = audioCtx.currentTime + 0.05; // small initial delay for clean start
window._barStartTime = nextNoteTime;
window._scheduledSteps = [];
schedulerTimerId = setInterval(scheduler, CONFIG.LOOKAHEAD_INTERVAL_MS);
document.getElementById('playBtn').classList.add('active');
document.getElementById('statusText').textContent = `▶ Playing at ${bpmValue} BPM`;
}
function stopPlayback() {
isPlaying = false;
if (schedulerTimerId) { clearInterval(schedulerTimerId); schedulerTimerId = null; }
currentStep = 0;
window._barStartTime = undefined;
window._currentVisualStep = -1;
window._scheduledSteps = [];
document.querySelectorAll('.step-cell.playhead').forEach(el => el.classList.remove('playhead'));
document.getElementById('playBtn').classList.remove('active');
document.getElementById('statusText').textContent = '■ Stopped';
}
/* ================================================================
UI — Grid Rendering & Interaction
================================================================ */
function buildGrid() {
const container = document.getElementById('gridContainer');
const stepNums = document.getElementById('stepNumbers');
container.innerHTML = '';
stepNums.innerHTML = '';
// Step numbers
for (let s = 0; s < CONFIG.STEPS; s++) {
const n = document.createElement('div');
n.className = 'step-number';
n.textContent = s + 1;
if (s % 4 === 0) n.style.color = '#888';
stepNums.appendChild(n);
}
// Track rows
INSTRUMENTS.forEach((inst, tIdx) => {
const row = document.createElement('div');
row.className = 'track-row' + (trackMutes[tIdx] ? ' muted' : '');
row.dataset.track = tIdx;
const label = document.createElement('div');
label.className = 'track-label';
label.textContent = `${inst.icon} ${inst.name}`;
row.appendChild(label);
const stepsRow = document.createElement('div');
stepsRow.className = 'steps-row';
for (let s = 0; s < CONFIG.STEPS; s++) {
const cell = document.createElement('div');
cell.className = `step-cell ${velClass(pattern[tIdx][s])}`;
if (s % 4 === 0) cell.classList.add('beat-start');
cell.dataset.track = tIdx;
cell.dataset.step = s;
cell.addEventListener('mousedown', onCellMouseDown);
cell.addEventListener('mouseenter', onCellMouseEnter);
cell.addEventListener('contextmenu', onCellRightClick);
stepsRow.appendChild(cell);
}
row.appendChild(stepsRow);
// Track controls: mute + volume
const ctrls = document.createElement('div');
ctrls.className = 'track-controls-row';
const mBtn = document.createElement('button');
mBtn.className = `mute-btn${trackMutes[tIdx] ? ' muted' : ''}`;
mBtn.textContent = 'M';
mBtn.title = 'Toggle Mute';
mBtn.addEventListener('click', () => toggleMute(tIdx));
ctrls.appendChild(mBtn);
const vSlider = document.createElement('input');
vSlider.type = 'range';
vSlider.className = 'track-vol';
vSlider.min = 0; vSlider.max = 100;
vSlider.value = trackVolumes[tIdx];
vSlider.title = `Vol: ${trackVolumes[tIdx]}%`;
vSlider.addEventListener('input', e => {
trackVolumes[tIdx] = +e.target.value;
e.target.title = `Vol: ${trackVolumes[tIdx]}%`;
saveState();
});
ctrls.appendChild(vSlider);
row.appendChild(ctrls);
container.appendChild(row);
});
}
function velClass(v) { return v === 0 ? 'off' : v === 1 ? 'normal' : 'accent'; }
function renderPlayhead(stepIdx) {
document.querySelectorAll('.step-cell.playhead').forEach(el => el.classList.remove('playhead'));
document.querySelectorAll(`.step-cell[data-step="${stepIdx}"]`).forEach(el => el.classList.add('playhead'));
}
// ── Cell interaction (click / drag / right-click) ──
function onCellMouseDown(e) {
if (e.button !== 0) return; // only left click
e.preventDefault();
const t = +this.dataset.track, s = +this.dataset.step;
isDragging = true;
pattern[t][s] = (pattern[t][s] + 1) % 3;
dragValue = pattern[t][s];
updateCell(this);
saveState();
}
function onCellMouseEnter(e) {
if (!isDragging) return;
const t = +this.dataset.track, s = +this.dataset.step;
pattern[t][s] = dragValue;
updateCell(this);
saveState();
}
function onCellRightClick(e) {
e.preventDefault();
const t = +this.dataset.track, s = +this.dataset.step;
// Cycle: off→accent, accent→normal, normal→off
pattern[t][s] = (pattern[t][s] === 2 ? 1 : (pattern[t][s] + 1));
updateCell(this);
saveState();
}
function updateCell(cell) {
const t = +cell.dataset.track, s = +cell.dataset.step;
cell.className = `step-cell ${velClass(pattern[t][s])}`;
if (s % 4 === 0) cell.classList.add('beat-start');
if (window._currentVisualStep === s && isPlaying) cell.classList.add('playhead');
}
document.addEventListener('mouseup', () => { isDragging = false; });
function toggleMute(tIdx) {
trackMutes[tIdx] = !trackMutes[tIdx];
const row = document.querySelector(`.track-row[data-track="${tIdx}"]`);
if (row) {
row.classList.toggle('muted', trackMutes[tIdx]);
row.querySelector('.mute-btn').classList.toggle('muted', trackMutes[tIdx]);
}
saveState();
}
/* ================================================================
DEFAULT PATTERN — A Groovy Beat
================================================================ */
function loadDefaultPattern() {
pattern = INSTRUMENTS.map(() => new Array(CONFIG.STEPS).fill(0));
// Kick: four-on-the-floor + syncopated hits
[0,2,4,6,8,10,14].forEach(s => { pattern[0][s] = s === 4 ? 2 : 1; });
// Snare: backbeat on steps 3 and 11 (accented)
pattern[1][3] = 2; pattern[1][11] = 2;
// Closed hi-hat: steady eighths with accents
for (let i = 0; i < 16; i += 2) pattern[2][i] = 1;
pattern[2][6] = 2; pattern[2][14] = 2;
// Open hi-hat: off-beat flourishes
pattern[3][7] = 1; pattern[3][15] = 1;
// Clap: on backbeat with snare
pattern[4][3] = 1; pattern[4][11] = 1;
// Tom: fills at end of phrases
pattern[5][6] = 1; pattern[5][7] = 2; pattern[5][14] = 1;
bpmValue = CONFIG.DEFAULT_BPM;
swingAmount = 8;
}
function randomPattern() {
pattern = INSTRUMENTS.map(() => new Array(CONFIG.STEPS).fill(0));
// Kick: 4-6 hits, favoring even steps
for (let i = 0; i < 4 + Math.floor(Math.random()*3); i++) {
const s = Math.floor(Math.random() * CONFIG.STEPS);
pattern[0][s] = Math.random() > 0.7 ? 2 : 1;
}
// Snare: 2-3 hits on backbeat positions
for (let i = 0; i < 2 + Math.floor(Math.random()*2); i++) {
const s = [3,7,11,15][Math.floor(Math.random()*4)];
pattern[1][s] = Math.random() > 0.5 ? 2 : 1;
}
// Closed hi-hat: fill most even steps
for (let i = 0; i < CONFIG.STEPS; i += 2) {
if (Math.random() > 0.3) pattern[2][i] = Math.random() > 0.8 ? 2 : 1;
}
// Open hi-hat: occasional off-beats
for (let i = 1; i < CONFIG.STEPS; i += 2) {
if (Math.random() > 0.75) pattern[3][i] = 1;
}
// Clap: 1-2 hits
for (let i = 0; i < Math.floor(Math.random()*3); i++) {
const s = [3,7,11][Math.floor(Math.random()*3)];
pattern[4][s] = 1;
}
// Tom: random fills
for (let i = 0; i < CONFIG.STEPS; i++) {
if (Math.random() > 0.85) pattern[5][i] = Math.random() > 0.5 ? 2 : 1;
}
rebuildGridFromData();
saveState();
}
function clearPattern() {
pattern = INSTRUMENTS.map(() => new Array(CONFIG.STEPS).fill(0));
rebuildGridFromData();
saveState();
}
function rebuildGridFromData() {
document.querySelectorAll('.step-cell').forEach(cell => updateCell(cell));
}
/* ================================================================
PERSISTENCE — localStorage
================================================================ */
function saveState() {
try {
const state = {
pattern, trackVolumes, trackMutes,
bpm: bpmValue, swing: swingAmount,
masterVol: +document.getElementById('masterVolSlider').value
};
localStorage.setItem('stepSeq16', JSON.stringify(state));
} catch(e) {}
}
function loadState() {
try {
const raw = localStorage.getItem('stepSeq16');
if (!raw) return false;
const s = JSON.parse(raw);
pattern = s.pattern || pattern;
trackVolumes = s.trackVolumes || trackVolumes;
trackMutes = s.trackMutes || trackMutes;
bpmValue = s.bpm ?? CONFIG.DEFAULT_BPM;
swingAmount = s.swing ?? CONFIG.DEFAULT_SWING;
document.getElementById('bpmSlider').value = bpmValue;
document.getElementById('bpmDisplay').textContent = bpmValue;
document.getElementById('swingSlider').value = swingAmount;
document.getElementById('swingDisplay').textContent = swingAmount + '%';
if (s.masterVol !== undefined) {
document.getElementById('masterVolSlider').value = s.masterVol;
}
return true;
} catch(e) { return false; }
}
/* ================================================================
CONTROLS WIRING
================================================================ */
function wireControls() {
// Play / Stop
document.getElementById('playBtn').addEventListener('click', () => {
isPlaying ? stopPlayback() : startPlayback();
});
document.getElementById('stopBtn').addEventListener('click', stopPlayback);
// BPM slider + display
const bpmSlider = document.getElementById('bpmSlider');
const bpmDisp = document.getElementById('bpmDisplay');
bpmSlider.addEventListener('input', () => {
bpmValue = +bpmSlider.value;
bpmDisp.textContent = bpmValue;
saveState();
});
// Swing slider + display
const swingSlider = document.getElementById('swingSlider');
const swingDisp = document.getElementById('swingDisplay');
swingSlider.addEventListener('input', () => {
swingAmount = +swingSlider.value;
swingDisp.textContent = swingAmount + '%';
saveState();
});
// Master volume
const mvSlider = document.getElementById('masterVolSlider');
mvSlider.addEventListener('input', () => {
if (masterGainNode) masterGainNode.gain.value = +mvSlider.value / 100;
saveState();
});
// Utility buttons
document.getElementById('clearBtn').addEventListener('click', clearPattern);
document.getElementById('randomBtn').addEventListener('click', randomPattern);
// Keyboard: Space = play/stop
document.addEventListener('keydown', e => {
if (e.code === 'Space' && e.target.tagName !== 'INPUT') {
e.preventDefault();
isPlaying ? stopPlayback() : startPlayback();
}
});
}
/* ================================================================
INITIALIZATION
================================================================ */
function init() {
const hasSaved = loadState();
if (!hasSaved) loadDefaultPattern();
buildGrid();
wireControls();
// Expose for debugging / probing
window._sequencer = {
get pattern() { return pattern; },
get isPlaying() { return isPlaying; },
get currentStep() { return currentStep % CONFIG.STEPS; },
get bpm() { return bpmValue; },
get swing() { return swingAmount; }
};
document.getElementById('statusText').textContent =
hasSaved ? 'Pattern restored from save — press Play or Space' : 'Default pattern loaded — press Play or Space';
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"full","sessionId":"b4e4004f-a5c1-461e-ab4d-3c343713ddde","tokensIn":1255884,"tokensOut":35744,"tokensTotal":1291628,"turns":33,"toolCalls":32,"failedToolCalls":1,"timestamp":"2026-07-29T23:22:47.477Z"} -->