Custom Code Experiment •
16 Step Sequencer
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>SEQ-16</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Instrument+Sans:wght@400;500&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--black: #000;
--white: #fff;
--dim: rgba(255,255,255,0.12);
--mid: rgba(255,255,255,0.35);
--mono: 'DM Mono', monospace;
--sans: 'Instrument Sans', sans-serif;
}
html, body {
width: 100%; height: 100%;
background: var(--black);
color: var(--white);
font-family: var(--sans);
overflow: hidden;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
#app {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100vh;
padding: 14px 20px 12px;
gap: 10px;
max-width: 960px;
margin: 0 auto;
}
/* ── TOP BAR ── */
#topbar {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.tag {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.16em;
color: rgba(255,255,255,0.3);
text-transform: uppercase;
}
.transport {
display: flex;
gap: 6px;
}
.btn {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--mid);
background: none;
border: 1px solid rgba(255,255,255,0.14);
padding: 5px 14px;
cursor: pointer;
border-radius: 2px;
transition: color 0.1s, border-color 0.1s, background 0.1s;
}
.btn:hover { color: var(--white); border-color: rgba(255,255,255,0.4); }
.btn.active { color: var(--black); background: var(--white); border-color: var(--white); }
.knob-row {
display: flex;
align-items: center;
gap: 18px;
margin-left: auto;
}
.knob-item {
display: flex;
align-items: center;
gap: 8px;
}
.knob-item label {
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(255,255,255,0.28);
}
.knob-item input[type=range] {
-webkit-appearance: none;
width: 80px; height: 1px;
background: rgba(255,255,255,0.16);
outline: none;
cursor: pointer;
}
.knob-item input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 10px; height: 10px;
border-radius: 50%;
background: var(--white);
border: none;
}
.knob-item input[type=range]::-moz-range-thumb {
width: 10px; height: 10px;
border-radius: 50%;
background: var(--white);
border: none;
}
.knob-val {
font-family: var(--mono);
font-size: 10px;
color: var(--white);
min-width: 28px;
}
/* ── GRID ── */
#grid-wrap {
display: flex;
flex-direction: column;
min-height: 0; /* allow shrink inside grid row */
overflow: hidden;
}
/* sticky beat header — doesn't scroll */
.beat-header {
display: grid;
grid-template-columns: 46px 1fr;
gap: 8px;
margin-bottom: 4px;
flex-shrink: 0;
}
.beat-nums {
display: grid;
grid-template-columns: repeat(16, 1fr);
gap: 3px;
}
.beat-num {
font-family: var(--mono);
font-size: 8px;
text-align: center;
color: rgba(255,255,255,0.13);
}
.beat-num.b1 { color: rgba(255,255,255,0.38); }
.beat-num:nth-child(5),
.beat-num:nth-child(9),
.beat-num:nth-child(13) { margin-left: 4px; }
/* scrollable track area */
#tracks {
overflow-y: auto;
overflow-x: hidden;
flex: 1;
min-height: 0;
/* hide scrollbar but keep functionality */
scrollbar-width: none;
}
#tracks::-webkit-scrollbar { display: none; }
.track-row {
display: grid;
grid-template-columns: 46px 1fr;
align-items: center;
gap: 8px;
padding: 2px 0;
border-bottom: 1px solid rgba(255,255,255,0.03);
}
.track-row:last-child { border-bottom: none; }
.track-name {
font-family: var(--mono);
font-size: 8px;
letter-spacing: 0.05em;
color: rgba(255,255,255,0.22);
text-transform: uppercase;
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 1px;
transition: color 0.15s;
}
.track-row.has-on .track-name { color: rgba(255,255,255,0.6); }
.steps {
display: grid;
grid-template-columns: repeat(16, 1fr);
gap: 3px;
}
.step-cell:nth-child(5),
.step-cell:nth-child(9),
.step-cell:nth-child(13) { margin-left: 4px; }
.step-cell {
/* fixed small height instead of aspect-ratio so rows stay compact */
height: 22px;
border-radius: 2px;
cursor: pointer;
background: rgba(255,255,255,0.055);
border: 1px solid rgba(255,255,255,0.07);
transition: background 0.07s, border-color 0.07s;
}
.step-cell:hover {
background: rgba(255,255,255,0.14);
border-color: rgba(255,255,255,0.22);
}
.step-cell.on {
background: var(--white);
border-color: var(--white);
}
.step-cell.current {
outline: 1.5px solid rgba(255,255,255,0.5);
outline-offset: 1.5px;
}
.step-cell.on.current {
outline-color: rgba(255,255,255,0.85);
}
@keyframes fp {
0% { opacity: 1; }
50% { opacity: 0.4; }
100% { opacity: 1; }
}
.step-cell.flash { animation: fp 0.11s ease-out; }
/* ── BOTTOM BAR ── */
#bottombar {
display: flex;
align-items: center;
gap: 16px;
border-top: 1px solid rgba(255,255,255,0.06);
padding-top: 10px;
}
.step-readout {
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.1em;
color: rgba(255,255,255,0.3);
text-transform: uppercase;
min-width: 56px;
}
.step-readout span { color: var(--white); }
.beat-dots {
display: flex;
gap: 5px;
align-items: center;
}
.beat-dot {
width: 4px; height: 4px;
border-radius: 50%;
background: rgba(255,255,255,0.12);
transition: background 0.04s;
}
.beat-dot.lit { background: var(--white); }
.audio-notice {
margin-left: auto;
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.12em;
color: rgba(255,255,255,0.22);
text-transform: uppercase;
transition: opacity 0.5s;
}
.audio-notice.gone { opacity: 0; pointer-events: none; }
@media (max-width: 580px) {
#app { padding: 10px 10px 10px; gap: 8px; }
.track-row, .beat-header { grid-template-columns: 32px 1fr; gap: 6px; }
.steps, .beat-nums { gap: 2px; }
.step-cell { height: 18px; }
.step-cell:nth-child(5),
.step-cell:nth-child(9),
.step-cell:nth-child(13),
.beat-num:nth-child(5),
.beat-num:nth-child(9),
.beat-num:nth-child(13) { margin-left: 3px; }
.knob-item input[type=range] { width: 55px; }
.knob-row { gap: 10px; }
}
</style>
</head>
<body>
<div id="app">
<div id="topbar">
<span class="tag">SEQ / 16</span>
<div class="transport">
<button class="btn" id="playBtn">Play</button>
<button class="btn" id="clearBtn">Clear</button>
</div>
<div class="knob-row">
<div class="knob-item">
<label>BPM</label>
<input type="range" id="bpmSlider" min="60" max="200" value="120">
<span class="knob-val" id="bpmVal">120</span>
</div>
<div class="knob-item">
<label>Swing</label>
<input type="range" id="swingSlider" min="0" max="75" value="0">
</div>
</div>
</div>
<div id="grid-wrap">
<div class="beat-header">
<div></div>
<div class="beat-nums" id="beatNums"></div>
</div>
<div id="tracks"></div>
</div>
<div id="bottombar">
<div class="step-readout">Step <span id="stepDisp">—</span></div>
<div class="beat-dots" id="beatDots"></div>
<div class="audio-notice" id="audioNotice">click to activate audio</div>
</div>
</div>
<script>
const TRACKS = [
{ name:'Kick', color:'#c0392b' },
{ name:'Sub', color:'#e74c3c' },
{ name:'Tom', color:'#e67e22' },
{ name:'HH Cl', color:'#f39c12' },
{ name:'Bell', color:'#f1c40f' },
{ name:'Snare', color:'#2ecc71' },
{ name:'Rim', color:'#27ae60' },
{ name:'Chord', color:'#1abc9c' },
{ name:'Pad', color:'#16a085' },
{ name:'Stab', color:'#3498db' },
{ name:'Pulse', color:'#2980b9' },
{ name:'Sweep', color:'#9b59b6' },
{ name:'Vibr', color:'#8e44ad' },
{ name:'HH Op', color:'#e91e8c' },
{ name:'Clap', color:'#c2185b' },
{ name:'Arp', color:'#00bcd4' },
];
const NUM_STEPS = 16;
const grid = TRACKS.map(() => new Array(NUM_STEPS).fill(false));
const cells = [];
// Beat header
const beatNumsEl = document.getElementById('beatNums');
for (let s = 0; s < 16; s++) {
const d = document.createElement('div');
d.className = 'beat-num' + (s % 4 === 0 ? ' b1' : '');
d.textContent = s % 4 === 0 ? (s/4+1) : '';
beatNumsEl.appendChild(d);
}
// Build tracks
const tracksEl = document.getElementById('tracks');
TRACKS.forEach((def, ti) => {
cells[ti] = [];
const row = document.createElement('div');
row.className = 'track-row';
const lbl = document.createElement('div');
lbl.className = 'track-name';
lbl.textContent = def.name;
const stepsDiv = document.createElement('div');
stepsDiv.className = 'steps';
for (let si = 0; si < NUM_STEPS; si++) {
const cell = document.createElement('div');
cell.className = 'step-cell';
cell.addEventListener('pointerdown', e => {
e.preventDefault();
unlockAudio();
grid[ti][si] = !grid[ti][si];
cell.classList.toggle('on', grid[ti][si]);
if (grid[ti][si]) tryPlay(def.color);
row.classList.toggle('has-on', grid[ti].some(Boolean));
});
stepsDiv.appendChild(cell);
cells[ti][si] = cell;
}
row.appendChild(lbl);
row.appendChild(stepsDiv);
tracksEl.appendChild(row);
});
// Beat dots
const beatDotsEl = document.getElementById('beatDots');
const dotEls = [];
for (let i = 0; i < 16; i++) {
const d = document.createElement('div');
d.className = 'beat-dot';
beatDotsEl.appendChild(d);
dotEls.push(d);
}
let lastDot = -1;
function setDot(s) {
if (lastDot >= 0) dotEls[lastDot].classList.remove('lit');
dotEls[s].classList.add('lit');
lastDot = s;
}
// Audio
let audioCtx = null, masterNode = null, audioReady = false;
const notice = document.getElementById('audioNotice');
function getCtx() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
return audioCtx;
}
function getMaster() {
if (masterNode) return masterNode;
const ctx = getCtx();
const lim = ctx.createDynamicsCompressor();
lim.threshold.value=-6; lim.knee.value=3; lim.ratio.value=12;
lim.attack.value=0.001; lim.release.value=0.1;
lim.connect(ctx.destination);
return (masterNode = lim);
}
function unlockAudio() {
if (audioReady) return;
const ctx = getCtx();
if (ctx.state === 'suspended') ctx.resume();
audioReady = true;
notice.classList.add('gone');
}
['pointerdown','touchstart','keydown'].forEach(e =>
document.addEventListener(e, unlockAudio, {once:false, passive:true}));
const soundDefs = {
'#c0392b':(ctx,out)=>{ const o=ctx.createOscillator(),e=ctx.createGain(); o.frequency.setValueAtTime(120,ctx.currentTime); o.frequency.exponentialRampToValueAtTime(30,ctx.currentTime+0.18); e.gain.setValueAtTime(0.7,ctx.currentTime); e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.25); o.connect(e);e.connect(out);o.start();o.stop(ctx.currentTime+0.3); },
'#e74c3c':(ctx,out)=>{ const o=ctx.createOscillator(),e=ctx.createGain(); o.type='sawtooth'; o.frequency.setValueAtTime(200,ctx.currentTime); o.frequency.exponentialRampToValueAtTime(50,ctx.currentTime+0.12); e.gain.setValueAtTime(0.5,ctx.currentTime); e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.18); o.connect(e);e.connect(out);o.start();o.stop(ctx.currentTime+0.2); },
'#e67e22':(ctx,out)=>{ const o=ctx.createOscillator(),e=ctx.createGain(); o.type='triangle';o.frequency.value=392; e.gain.setValueAtTime(0.5,ctx.currentTime); e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.5); o.connect(e);e.connect(out);o.start();o.stop(ctx.currentTime+0.55); },
'#f39c12':(ctx,out)=>{ const o=ctx.createOscillator(),e=ctx.createGain(); o.type='triangle';o.frequency.value=440; e.gain.setValueAtTime(0.45,ctx.currentTime); e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.45); o.connect(e);e.connect(out);o.start();o.stop(ctx.currentTime+0.5); },
'#f1c40f':(ctx,out)=>{ [523.25,1046.5].forEach((f,i)=>{ const o=ctx.createOscillator(),e=ctx.createGain(); o.type='sine';o.frequency.value=f; e.gain.setValueAtTime(i===0?0.4:0.15,ctx.currentTime); e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.8); o.connect(e);e.connect(out);o.start();o.stop(ctx.currentTime+0.85); }); },
'#2ecc71':(ctx,out)=>{ const buf=ctx.createBuffer(1,ctx.sampleRate*0.4,ctx.sampleRate),d=buf.getChannelData(0); for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*(1-i/d.length); const src=ctx.createBufferSource();src.buffer=buf; const f=ctx.createBiquadFilter();f.type='bandpass';f.frequency.setValueAtTime(400,ctx.currentTime);f.frequency.exponentialRampToValueAtTime(2000,ctx.currentTime+0.4);f.Q.value=3; const e=ctx.createGain();e.gain.setValueAtTime(0.3,ctx.currentTime);e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.4); src.connect(f);f.connect(e);e.connect(out);src.start();src.stop(ctx.currentTime+0.45); },
'#27ae60':(ctx,out)=>{ const o=ctx.createOscillator(),e=ctx.createGain(); o.type='sine';o.frequency.setValueAtTime(220,ctx.currentTime);o.frequency.exponentialRampToValueAtTime(660,ctx.currentTime+0.3); e.gain.setValueAtTime(0.35,ctx.currentTime);e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.35); o.connect(e);e.connect(out);o.start();o.stop(ctx.currentTime+0.4); },
'#1abc9c':(ctx,out)=>{ [523.25,659.25,783.99].forEach((f,i)=>{ const o=ctx.createOscillator(),e=ctx.createGain(); o.type='sine';o.frequency.value=f; e.gain.setValueAtTime(0.18,ctx.currentTime+i*0.04);e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.7); o.connect(e);e.connect(out);o.start(ctx.currentTime+i*0.04);o.stop(ctx.currentTime+0.75); }); },
'#16a085':(ctx,out)=>{ const o=ctx.createOscillator(),e=ctx.createGain(); o.type='sine';o.frequency.value=349.23; e.gain.setValueAtTime(0.4,ctx.currentTime);e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.6); o.connect(e);e.connect(out);o.start();o.stop(ctx.currentTime+0.65); },
'#3498db':(ctx,out)=>{ const o=ctx.createOscillator(),e=ctx.createGain(),f=ctx.createBiquadFilter(); f.type='lowpass';f.frequency.value=800;f.Q.value=8; o.type='sawtooth';o.frequency.value=110; e.gain.setValueAtTime(0.5,ctx.currentTime);e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.4); o.connect(f);f.connect(e);e.connect(out);o.start();o.stop(ctx.currentTime+0.45); },
'#2980b9':(ctx,out)=>{ const o=ctx.createOscillator(),e=ctx.createGain(); o.type='square';o.frequency.value=130.81; e.gain.setValueAtTime(0.25,ctx.currentTime);e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.35); o.connect(e);e.connect(out);o.start();o.stop(ctx.currentTime+0.4); },
'#9b59b6':(ctx,out)=>{ const o=ctx.createOscillator(),e=ctx.createGain(); o.type='sine';o.frequency.setValueAtTime(880,ctx.currentTime);o.frequency.exponentialRampToValueAtTime(110,ctx.currentTime+0.5); e.gain.setValueAtTime(0.35,ctx.currentTime);e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.55); o.connect(e);e.connect(out);o.start();o.stop(ctx.currentTime+0.6); },
'#8e44ad':(ctx,out)=>{ const car=ctx.createOscillator(),lfo=ctx.createOscillator(),lg=ctx.createGain(),e=ctx.createGain(); lfo.frequency.value=8;lg.gain.value=60;car.type='sine';car.frequency.value=440; lfo.connect(lg);lg.connect(car.frequency);e.gain.setValueAtTime(0.3,ctx.currentTime);e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.5); car.connect(e);e.connect(out);lfo.start();car.start();lfo.stop(ctx.currentTime+0.55);car.stop(ctx.currentTime+0.55); },
'#e91e8c':(ctx,out)=>{ const buf=ctx.createBuffer(1,ctx.sampleRate*0.08,ctx.sampleRate),d=buf.getChannelData(0); for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*(1-i/d.length); const src=ctx.createBufferSource();src.buffer=buf; const f=ctx.createBiquadFilter();f.type='highpass';f.frequency.value=6000; const e=ctx.createGain();e.gain.setValueAtTime(0.5,ctx.currentTime);e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.08); src.connect(f);f.connect(e);e.connect(out);src.start();src.stop(ctx.currentTime+0.1); },
'#c2185b':(ctx,out)=>{ const buf=ctx.createBuffer(1,ctx.sampleRate*0.05,ctx.sampleRate),d=buf.getChannelData(0); for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*Math.exp(-i/200); const src=ctx.createBufferSource();src.buffer=buf; const e=ctx.createGain();e.gain.setValueAtTime(0.6,ctx.currentTime);e.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.06); src.connect(e);e.connect(out);src.start();src.stop(ctx.currentTime+0.08); },
'#00bcd4':(ctx,out)=>{ [523.25,659.25,783.99,1046.5].forEach((f,i)=>{ const o=ctx.createOscillator(),e=ctx.createGain(); o.type='triangle';o.frequency.value=f; const t=ctx.currentTime+i*0.06;e.gain.setValueAtTime(0.22,t);e.gain.exponentialRampToValueAtTime(0.001,t+0.15); o.connect(e);e.connect(out);o.start(t);o.stop(t+0.2); }); },
};
const cooldowns = new Map();
function tryPlay(color) {
if (!audioReady) return;
const now = Date.now();
if (now - (cooldowns.get(color)||0) < 50) return;
cooldowns.set(color, now);
const fn = soundDefs[color];
if (!fn) return;
try { const c=getCtx(); if(c.state==='suspended')c.resume(); fn(c, getMaster()); } catch(e) {}
}
// Sequencer
let playing = false, tempo = 120, swing = 0;
let seqTimer = null, schStep = -1, schTime = 0, dispStep = -1;
const LOOK = 0.1, TICK = 25;
function stepDur() { return (60/tempo)/4; }
function swingOff(si) { return si%2===1 ? (swing/100)*stepDur()*0.5 : 0; }
function scheduler() {
const ctx = getCtx();
while (schTime < ctx.currentTime + LOOK) {
schStep = (schStep + 1) % NUM_STEPS;
const t = schTime + swingOff(schStep);
const si = schStep;
TRACKS.forEach((def, ti) => {
if (!grid[ti][si]) return;
const fn = soundDefs[def.color];
if (!fn) return;
const delay = Math.max(0, (t - ctx.currentTime) * 1000);
setTimeout(() => {
if (!playing) return;
fn(ctx, getMaster());
cells[ti][si].classList.add('flash');
setTimeout(() => cells[ti][si].classList.remove('flash'), 110);
}, delay);
});
const delay = Math.max(0, (t - ctx.currentTime) * 1000);
setTimeout(() => {
if (!playing) return;
if (dispStep >= 0) TRACKS.forEach((_, ti) => cells[ti][dispStep].classList.remove('current'));
TRACKS.forEach((_, ti) => cells[ti][si].classList.add('current'));
setDot(si);
document.getElementById('stepDisp').textContent = si + 1;
dispStep = si;
}, delay);
schTime += stepDur();
}
}
function startSeq() {
const ctx = getCtx();
if (ctx.state === 'suspended') ctx.resume();
schStep = -1; schTime = ctx.currentTime + 0.03;
seqTimer = setInterval(scheduler, TICK);
}
function stopSeq() {
clearInterval(seqTimer);
if (dispStep >= 0) TRACKS.forEach((_, ti) => cells[ti][dispStep].classList.remove('current'));
dispStep = -1; lastDot = -1;
dotEls.forEach(d => d.classList.remove('lit'));
document.getElementById('stepDisp').textContent = '—';
}
document.getElementById('playBtn').addEventListener('click', () => {
unlockAudio();
playing = !playing;
const btn = document.getElementById('playBtn');
btn.textContent = playing ? 'Stop' : 'Play';
btn.classList.toggle('active', playing);
playing ? startSeq() : stopSeq();
});
document.getElementById('clearBtn').addEventListener('click', () => {
TRACKS.forEach((_, ti) => {
grid[ti].fill(false);
cells[ti].forEach(c => c.classList.remove('on'));
tracksEl.children[ti].classList.remove('has-on');
});
});
const bpmSlider = document.getElementById('bpmSlider');
bpmSlider.addEventListener('input', () => {
tempo = parseInt(bpmSlider.value);
document.getElementById('bpmVal').textContent = tempo;
});
document.getElementById('swingSlider').addEventListener('input', function() {
swing = parseInt(this.value);
});
document.addEventListener('keydown', e => {
if (e.code === 'Space' && e.target === document.body) {
e.preventDefault();
document.getElementById('playBtn').click();
}
});
</script>
</body>
</html>