Toybox / step_sequencer.html
zxbc2023's picture
add 16-step drum sequencer (Qwen3.8-27B-Fable-Distill-Q4_K_M)
1ed8f3a verified
Raw History Blame Contribute Delete
11.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>16-step drum sequencer</title>
<style>
/* Explicit dark palette (overrides app theme on purpose) */
.seq {
--bg: #131519; --panel: #191c23; --line: #2a2f3a;
--cell: #20242e; --cell-beat: #282d39;
--fg: #e8eaef; --mut: #98a0ae; --acc: #4f7cff;
display: inline-flex; flex-direction: column; gap: 18px;
padding: 22px 24px 24px; border: 1px solid #262a33;
border-radius: 16px; background: var(--bg);
color: var(--fg); font-family: inherit; font-size: 14px;
box-shadow: 0 10px 34px rgba(0,0,0,.55);
}
.seq button { font: inherit; cursor: pointer; user-select: none; }
/* ---- transport ---- */
.transport { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.play-btn {
display: inline-flex; align-items: center; justify-content: center; gap: 10px;
min-width: 132px; min-height: 48px; padding: 12px 26px;
font-size: 15px; font-weight: 600; letter-spacing: .03em;
color: #fff; background: var(--acc);
border: none; border-radius: 12px;
box-shadow: 0 2px 14px rgba(79,124,255,.45);
transition: transform .06s ease, filter .15s ease;
}
.play-btn:hover { filter: brightness(1.1); }
.play-btn:active { transform: scale(.97); }
.play-btn svg { width: 18px; height: 18px; fill: currentColor; }
.knob {
display: inline-flex; flex-direction: column; align-items: flex-start; gap: 6px;
color: var(--mut); font-size: 11px; text-transform: uppercase; letter-spacing: .12em;
}
.knob input[type=range] { width: 170px; accent-color: var(--acc); cursor: pointer; }
.knob .value {
font-size: 18px; font-weight: 650; color: var(--fg);
font-variant-numeric: tabular-nums; letter-spacing: 0;
}
.knob .value small { font-size: 11px; font-weight: 500; color: var(--mut); }
.ghost-btn {
min-height: 48px; padding: 12px 20px; font-size: 14px;
color: var(--mut);
background: transparent; border: 1px solid #333a48; border-radius: 12px;
transition: color .15s, border-color .15s;
}
.ghost-btn:hover { color: var(--fg); border-color: var(--fg); }
/* ---- step grid ---- */
.grid {
display: grid; grid-template-columns: 140px repeat(16, 40px);
gap: 2px; align-items: center;
touch-action: none;
}
.lbl { display: flex; flex-direction: column; gap: 4px; padding: 2px 0; }
.lbl .top { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 550; color: var(--mut); }
.lbl .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--tc); flex: none;
box-shadow: 0 0 8px color-mix(in srgb, var(--tc) 70%, transparent); }
.lbl input[type=range] {
width: 108px; height: 18px; accent-color: var(--tc); cursor: pointer;
}
.lbl input[type=range]:hover { filter: brightness(1.2); }
.cell {
width: 40px; height: 40px; padding: 0;
border-radius: 6px; border: 1px solid var(--line);
background: var(--cell);
transition: background .08s, border-color .08s, transform .05s;
}
.cell:hover { border-color: #4a5262; }
.cell:active { transform: scale(.95); }
.cell.beat { background: var(--cell-beat); }
.cell.on {
background: var(--tc); border-color: var(--tc);
box-shadow: 0 0 10px color-mix(in srgb, var(--tc) 55%, transparent),
inset 0 0 5px rgba(255,255,255,.3);
}
/* playhead: whole column */
.cell.playhead { border-color: #5a6478; }
.cell.playhead.on {
box-shadow: 0 0 0 2px var(--fg),
0 0 14px color-mix(in srgb, var(--tc) 70%, transparent);
}
</style>
</head>
<body>
<div class="seq" id="seq"></div>
<script>
(function () {
var STEPS = 16;
var TRACKS = [
{ name: 'Kick', color: '#ff5d5d' },
{ name: 'Snare', color: '#ffb020' },
{ name: 'Clap', color: '#c084fc' },
{ name: 'Hi-hat', color: '#2dd4bf' }
];
var grid = TRACKS.map(function () { return new Array(STEPS).fill(false); });
// Default groove: four-on-the-floor kick, clap on 5 & 13, hats on eighths
[0, 4, 8, 12].forEach(function (s) { grid[0][s] = true; });
[4, 12].forEach(function (s) { grid[2][s] = true; });
for (var s = 0; s < STEPS; s += 2) grid[3][s] = true;
var el = document.getElementById('seq');
/* ---------- build UI ---------- */
var transport = document.createElement('div');
transport.className = 'transport';
var playBtn = document.createElement('button');
playBtn.className = 'play-btn';
var ICON_PLAY = '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>';
var ICON_STOP = '<svg viewBox="0 0 24 24"><path d="M6 6h12v12H6z"/></svg>';
playBtn.innerHTML = ICON_PLAY + '<span>Play</span>';
var clearBtn = document.createElement('button');
clearBtn.className = 'ghost-btn';
clearBtn.textContent = 'Clear pattern';
var bpmKnob = document.createElement('div');
bpmKnob.className = 'knob';
bpmKnob.innerHTML = 'Tempo' +
'<input id="bpm" type="range" min="60" max="200" value="120">' +
'<span class="value" id="bpmVal">120 <small>BPM</small></span>';
var volKnob = document.createElement('div');
volKnob.className = 'knob';
volKnob.innerHTML = 'Master' +
'<input id="vol" type="range" min="0" max="100" value="85">';
transport.appendChild(playBtn);
transport.appendChild(bpmKnob);
transport.appendChild(volKnob);
transport.appendChild(clearBtn);
el.appendChild(transport);
var gridEl = document.createElement('div');
gridEl.className = 'grid';
var cells = [];
TRACKS.forEach(function (tr, r) {
var lbl = document.createElement('div');
lbl.className = 'lbl';
lbl.style.setProperty('--tc', tr.color);
lbl.innerHTML =
'<div class="top"><span class="dot"></span>' + tr.name + '</div>' +
'<input type="range" min="0" max="100" value="100" data-trk="' + r + '" ' +
'aria-label="' + tr.name + ' volume">';
gridEl.appendChild(lbl);
cells[r] = [];
for (var s = 0; s < STEPS; s++) {
var b = document.createElement('button');
b.className = 'cell' + (s % 4 === 0 ? ' beat' : '');
b.style.setProperty('--tc', tr.color);
b.dataset.r = r; b.dataset.s = s;
b.setAttribute('aria-label', tr.name + ' step ' + (s + 1));
if (grid[r][s]) b.classList.add('on');
gridEl.appendChild(b);
cells[r][s] = b;
}
});
el.appendChild(gridEl);
var bpmInput = bpmKnob.querySelector('#bpm');
var bpmVal = bpmKnob.querySelector('#bpmVal');
var volInput = volKnob.querySelector('#vol');
bpmInput.addEventListener('input', function () {
bpmVal.innerHTML = bpmInput.value + ' <small>BPM</small>';
});
/* ---------- Web Audio synthesis (no samples) ---------- */
var ctx = null, master = null, noiseBuf = null;
var trackGains = [];
var trackVols = TRACKS.map(function () { return 1; });
var masterVol = +volInput.value / 100;
function ac() {
if (!ctx) {
ctx = new (window.AudioContext || window.webkitAudioContext)();
master = ctx.createGain();
master.gain.value = masterVol;
master.connect(ctx.destination);
trackGains = TRACKS.map(function (_, i) {
var g = ctx.createGain();
g.gain.value = trackVols[i];
g.connect(master);
return g;
});
noiseBuf = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate);
var d = noiseBuf.getChannelData(0);
for (var i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
}
return ctx;
}
volInput.addEventListener('input', function () {
masterVol = +volInput.value / 100;
if (master) master.gain.value = masterVol;
});
gridEl.querySelectorAll('input[type=range][data-trk]').forEach(function (f) {
f.addEventListener('input', function () {
var r = +f.dataset.trk;
trackVols[r] = +f.value / 100;
if (trackGains[r]) trackGains[r].gain.value = trackVols[r];
});
});
function env(g, t, peak, decay) {
g.gain.setValueAtTime(peak, t);
g.gain.exponentialRampToValueAtTime(0.0001, t + decay);
}
function noise(t, filterType, freq, q, peak, decay, dest) {
var src = ctx.createBufferSource(); src.buffer = noiseBuf;
var f = ctx.createBiquadFilter(); f.type = filterType; f.frequency.value = freq; if (q) f.Q.value = q;
var g = ctx.createGain(); env(g, t, peak, decay);
src.connect(f); f.connect(g); g.connect(dest);
src.start(t); src.stop(t + decay + 0.05);
}
var SOUNDS = {
Kick: function (t, dest) {
var o = ctx.createOscillator(), g = ctx.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(150, t);
o.frequency.exponentialRampToValueAtTime(45, t + 0.12);
env(g, t, 0.9, 0.3);
o.connect(g); g.connect(dest);
o.start(t); o.stop(t + 0.35);
},
Snare: function (t, dest) {
noise(t, 'bandpass', 1800, 0.8, 0.5, 0.18, dest);
var o = ctx.createOscillator(), g = ctx.createGain();
o.type = 'triangle'; o.frequency.value = 190;
env(g, t, 0.35, 0.1);
o.connect(g); g.connect(dest);
o.start(t); o.stop(t + 0.15);
},
Clap: function (t, dest) {
for (var i = 0; i < 3; i++) noise(t + i * 0.012, 'bandpass', 1200, 1.2, 0.5 - i * 0.1, 0.12, dest);
},
'Hi-hat': function (t, dest) {
noise(t, 'highpass', 7000, 1, 0.35, 0.05, dest);
}
};
/* ---------- cell painting: click + drag ---------- */
function setCell(b, on) {
grid[+b.dataset.r][+b.dataset.s] = on;
b.classList.toggle('on', on);
}
var painting = null;
gridEl.addEventListener('pointerdown', function (e) {
var b = e.target.closest('.cell');
if (!b) return;
e.preventDefault();
painting = { state: !grid[+b.dataset.r][+b.dataset.s] };
setCell(b, painting.state);
});
gridEl.addEventListener('pointermove', function (e) {
if (!painting) return;
var hit = document.elementFromPoint(e.clientX, e.clientY);
var b = hit && hit.closest('.cell');
if (b && grid[+b.dataset.r][+b.dataset.s] !== painting.state) setCell(b, painting.state);
});
window.addEventListener('pointerup', function () { painting = null; });
window.addEventListener('pointercancel', function () { painting = null; });
// Keyboard access: Enter/Space on a focused cell (native click is not used)
gridEl.addEventListener('keydown', function (e) {
var b = e.target.closest('.cell');
if (!b || (e.key !== ' ' && e.key !== 'Enter')) return;
e.preventDefault();
setCell(b, !grid[+b.dataset.r][+b.dataset.s]);
});
/* ---------- lookahead scheduler (stable timing, survives tab throttling) ---------- */
var playing = false, step = 0, nextTime = 0, timer = null;
function markPlayhead(s) {
for (var c = 0; c < STEPS; c++)
for (var r = 0; r < TRACKS.length; r++)
cells[r][c].classList.toggle('playhead', c === s);
}
function scheduleStep(s, t) {
TRACKS.forEach(function (tr, r) {
if (grid[r][s]) SOUNDS[tr.name](t, trackGains[r]);
});
setTimeout(function () { if (playing) markPlayhead(s); }, Math.max(0, (t - ctx.currentTime) * 1000));
}
function loop() {
while (nextTime < ctx.currentTime + 0.12) {
scheduleStep(step, nextTime);
step = (step + 1) % STEPS;
nextTime += 60 / +bpmInput.value / 4;
}
}
playBtn.addEventListener('click', function () {
if (playing) {
playing = false;
clearInterval(timer);
playBtn.innerHTML = ICON_PLAY + '<span>Play</span>';
markPlayhead(-1);
} else {
ac().resume();
playing = true;
step = 0;
nextTime = ctx.currentTime + 0.06;
playBtn.innerHTML = ICON_STOP + '<span>Stop</span>';
timer = setInterval(loop, 25);
}
});
clearBtn.addEventListener('click', function () {
grid.forEach(function (row) { row.fill(false); });
cells.forEach(function (row) { row.forEach(function (b) { b.classList.remove('on'); }); });
});
// Debug/test handle
window.__seq = { grid: grid, trackGains: function () { return trackGains; }, master: function () { return master; } };
})();
</script>
</body>
</html>