Drum machine · kat-coder-air-v2.5
Build a 16-step drum machine with a clickable grid for kick, snare, hi-hat, and clap, all sounds synthesized with the Web Audio API, plus play/stop and an adjustable BPM.
transcript · 12 messages
Build a 16-step drum machine with a clickable grid for kick, snare, hi-hat, and clap, all sounds synthesized with the Web Audio API, plus play/stop and an adjustable BPM.
/tmp/ws/index.html (10152 bytes)Successfully wrote 10152 bytes to /tmp/ws/index.html
Here's a complete drum machine in a single file:
/tmp/ws/index.html (10171 bytes)Successfully wrote 10171 bytes to /tmp/ws/index.html
I'll build a complete 16-step drum machine with Web Audio API synthesis, a clickable grid, play/stop, and BPM control.
/tmp/ws/index.html (10171 bytes)Successfully wrote 10171 bytes to /tmp/ws/index.html
{"command":"ls -la /tmp/ws/index.html"}-rw-r--r--. 1 node node 10171 Aug 1 19:57 /tmp/ws/index.html
{"path":"/tmp/ws/index.html"}<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Drum Machine</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { background: #1a1a2e; color: #eee; font-family: 'Segoe UI', system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; } .machine { background: #16213e; border-radius: 16px; padding: 32px; box-shadow: 0 20px 60px rgba(0,0,0,0.5); } h1 { text-align: center; font-size: 1.6rem; margin-bottom: 24px; letter-spacing: 4px; text-transform: uppercase; color: #e94560; } .controls { display: flex; justify-content: center; align-items: center; gap: 20px; margin-bottom: 24px; } button#play { background: #e94560; color: #fff; border: none; border-radius: 8px; padding: 10px 32px; font-size: 1rem; cursor: pointer; font-weight: bold; letter-spacing: 2px; transition: background 0.15s; } button#play:hover { background: #c73652; } button#play.active { background: #0f3460; } .bpm-control { display: flex; align-items: center; gap: 8px; font-size: 0.95rem; } .bpm-control input[type=range] { width: 120px; accent-color: #e94560; } #bpm-val { min-width: 48px; text-align: center; font-weight: bold; color: #e94560; } .grid-container { overflow-x: auto; } table { border-collapse: collapse; } td.step-num { text-align: center; font-size: 0.7rem; color: #555; padding: 0 2px; width: 36px; } td.step-num.beat { color: #888; } td.inst-label { text-align: right; padding-right: 12px; font-size: 0.8rem; font-weight: bold; letter-spacing: 1px; text-transform: uppercase; color: #aaa; width: 70px; } td.cell { width: 36px; height: 44px; border-radius: 4px; cursor: pointer; transition: background 0.1s, transform 0.1s; border: 1px solid #1a1a2e; } td.cell.off { background: #0f3460; } td.cell.off:hover { background: #1a4a8a; transform: scale(1.08); } td.cell.on { background: #e94560; border-color: #e94560; } td.cell.current { box-shadow: inset 0 0 0 2px #fff; } td.cell.on.current { background: #ff6b81; } /* row colors */ tr[data-inst="kick"] td.cell.off { background: #1a3a5c; } tr[data-inst="kick"] td.cell.off:hover { background: #234d78; } tr[data-inst="kick"] td.cell.on { background: #e94560; } tr[data-inst="snare"] td.cell.off { background: #1a4a4a; } tr[data-inst="snare"] td.cell.off:hover { background: #235d5d; } tr[data-inst="snare"] td.cell.on { background: #00d2ff; } tr[data-inst="hihat"] td.cell.off { background: #3a2a1a; } tr[data-inst="hihat"] td.cell.off:hover { background: #4d3a23; } tr[data-inst="hihat"] td.cell.on { background: #f5a623; } tr[data-inst="clap"] td.cell.off { background: #2a1a3a; } tr[data-inst="clap"] td.cell.off:hover { background: #3d2350; } tr[data-inst="clap"] td.cell.on { background: #bd34fe; } </style> </head> <body> <div class="machine"> <h1>Drum Machine</h1> <div class="controls"> <button id="play">PLAY</button> <div class="bpm-control"> <span>BPM</span> <input type="range" id="bpm" min="40" max="240" value="120"> <span id="bpm-val">120</span> </div> </div> <div class="grid-container"> <table> <thead> <tr id="step-headers"></tr> </thead> <tbody id="grid-body"></tbody> </table> </div> </div> <script> const STEPS = 16; const INSTRUMENTS = ['kick','snare','hihat','clap']; let audioCtx = null; let playing = false; let currentStep = 0; let bpm = 120; let nextNoteTime = 0; let timerID = null; const scheduleAhead = 0.1; const lookahead = 25; let grid = {}; INSTRUMENTS.forEach(inst => { grid[inst] = new Array(STEPS).fill(false); }); function initAudio() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); if (audioCtx.state === 'suspended') audioCtx.resume(); } function now() { return audioCtx.currentTime; } // --- Synthesis --- function playKick(time) { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(160, time); osc.frequency.exponentialRampToValueAtTime(30, time + 0.12); gain.gain.setValueAtTime(1, time); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.4); osc.connect(gain).connect(audioCtx.destination); osc.start(time); osc.stop(time + 0.4); } function playSnare(time) { // noise const bufSize = audioCtx.sampleRate * 0.15; const buf = audioCtx.createBuffer(1, bufSize, audioCtx.sampleRate); const data = buf.getChannelData(0); for (let i = 0; i < bufSize; i++) data[i] = (Math.random() * 2 - 1); const noise = audioCtx.createBufferSource(); noise.buffer = buf; const nFilter = audioCtx.createBiquadFilter(); nFilter.type = 'highpass'; nFilter.frequency.value = 1500; const nGain = audioCtx.createGain(); nGain.gain.setValueAtTime(0.8, time); nGain.gain.exponentialRampToValueAtTime(0.001, time + 0.15); noise.connect(nFilter).connect(nGain).connect(audioCtx.destination); noise.start(time); noise.stop(time + 0.15); // body const osc = audioCtx.createOscillator(); const oGain = audioCtx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(200, time); osc.frequency.exponentialRampToValueAtTime(80, time + 0.08); oGain.gain.setValueAtTime(0.7, time); oGain.gain.exponentialRampToValueAtTime(0.001, time + 0.1); osc.connect(oGain).connect(audioCtx.destination); osc.start(time); osc.stop(time + 0.1); } function playHiHat(time) { const bufSize = audioCtx.sampleRate * 0.06; const buf = audioCtx.createBuffer(1, bufSize, audioCtx.sampleRate); const data = buf.getChannelData(0); for (let i = 0; i < bufSize; i++) data[i] = (Math.random() * 2 - 1); const noise = audioCtx.createBufferSource(); noise.buffer = buf; const filter = audioCtx.createBiquadFilter(); filter.type = 'highpass'; filter.frequency.value = 7000; const gain = audioCtx.createGain(); gain.gain.setValueAtTime(0.5, time); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.06); noise.connect(filter).connect(gain).connect(audioCtx.destination); noise.start(time); noise.stop(time + 0.06); } function playClap(time) { const bufSize = audioCtx.sampleRate * 0.15; const buf = audioCtx.createBuffer(1, bufSize, audioCtx.sampleRate); const data = buf.getChannelData(0); for (let i = 0; i < bufSize; i++) data[i] = (Math.random() * 2 - 1); const noise = audioCtx.createBufferSource(); noise.buffer = buf; const filter = audioCtx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.value = 2500; filter.Q.value = 1.5; const gain = audioCtx.createGain(); gain.gain.setValueAtTime(0, time); // envelope with multiple attacks for clap texture gain.gain.linearRampToValueAtTime(0.8, time + 0.01); gain.gain.linearRampToValueAtTime(0.3, time + 0.025); gain.gain.linearRampToValueAtTime(0.7, time + 0.04); gain.gain.linearRampToValueAtTime(0.2, time + 0.055); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.15); noise.connect(filter).connect(gain).connect(audioCtx.destination); noise.start(time); noise.stop(time + 0.15); } const playFn = { kick: playKick, snare: playSnare, hihat: playHiHat, clap: playClap }; // --- Scheduler --- function scheduleNote(step, time) { INSTRUMENTS.forEach(inst => { if (grid[inst][step]) playFn[inst](time); }); // visual const t = time * 1000; setTimeout(() => highlightStep(step), (time - now()) * 1000); } function nextStep() { const secondsPerBeat = 60.0 / bpm; currentStep = (currentStep + 1) % STEPS; } function scheduler() { while (nextNoteTime < now() + scheduleAhead) { scheduleNote(currentStep, nextNoteTime); const secondsPerBeat = 60.0 / bpm; nextNoteTime += secondsPerBeat / 4; // 16th notes nextStep(); } timerID = setTimeout(scheduler, lookahead); } function highlightStep(step) { document.querySelectorAll('td.cell').forEach(c => c.classList.remove('current')); document.querySelectorAll(`td.cell[data-step="${step}"]`).forEach(c => c.classList.add('current')); } function start() { initAudio(); playing = true; currentStep = 0; nextNoteTime = now(); scheduler(); document.getElementById('play').textContent = 'STOP'; document.getElementById('play').classList.add('active'); } function stop() { playing = false; clearTimeout(timerID); document.querySelectorAll('td.cell').forEach(c => c.classList.remove('current')); document.getElementById('play').textContent = 'PLAY'; document.getElementById('play').classList.remove('active'); } document.getElementById('play').addEventListener('click', () => { if (playing) stop(); else start(); }); document.getElementById('bpm').addEventListener('input', e => { bpm = +e.target.value; document.getElementById('bpm-val').textContent = bpm; }); // --- Build Grid --- const thead = document.getElementById('step-headers'); for (let i = 0; i < STEPS; i++) { const th = document.createElement('th'); th.textContent = i + 1; th.className = 'step-num' + (i % 4 === 0 ? ' beat' : ''); thead.appendChild(th); } const tbody = document.getElementById('grid-body'); INSTRUMENTS.forEach(inst => { const tr = document.createElement('tr'); tr.dataset.inst = inst; const label = document.createElement('td'); label.className = 'inst-label'; label.textContent = inst; tr.appendChild(label); for (let i = 0; i < STEPS; i++) { const td = document.createElement('td'); td.className = 'cell off'; td.dataset.inst = inst; td.dataset.step = i; td.addEventListener('click', () => { grid[inst][i] = !grid[inst][i]; td.classList.toggle('off'); td.classList.toggle('on'); if (grid[inst][i] && !playing) { initAudio(); playFn[inst](now()); } }); tr.appendChild(td); } tbody.appendChild(tr); }); </script> </body> </html>
Done. `/tmp/ws/index.html` is a self-contained 16-step drum machine. Here's what's inside: **Instruments (synthesized with Web Audio API, no samples):** - **Kick** — sine oscillator with pitch sweep 160→30 Hz - **Snare** — filtered noise burst + triangle oscillator body - **Hi-hat** — highpass-filtered noise, short decay - **Clap** — bandpass-filtered noise with multi-attack envelope **Controls:** - **PLAY/STOP** button (toggles) - **BPM slider** (40–240) with live value display - **16-step grid** — click any cell to toggle it on/off; each row has its own color (red=kick, cyan=snare, orange=hihat, purple=clap) **Timing:** Uses a lookahead scheduler (25 ms) with 100 ms look-ahead window for sample-accurate playback — no drift. **Visual feedback:** A white border highlights the current step as it plays. Clicking a cell while stopped previews that sound immediately.
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Drum machine grid renders correctly and click toggles a cell, but play/BPM controls show no reaction to any input, no playback animation visible.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.