Drum machine · solar-pro4
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 · 8 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.
thinking
The user wants a 16-step drum machine with: - Kick, snare, hi-hat, and clap sounds synthesized with Web Audio API - Clickable grid for each instrument - Play/stop controls - Adjustable BPM - All in one self-contained index.html file Let me build this carefully. I'll use the Web Audio API to synthesize: - Kick: low frequency oscillator with pitch decay - Snare: noise burst + tone - Hi-hat: filtered noise with short decay - Clap: noise burst with envelope Let me create a nice-looking drum machine with a grid layout.
{"content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, iniValidation failed for tool "write": - path: must have required properties path Received arguments: { "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Drum Machine</title>\n<style>\n :root {\n --bg: #1a1a2e;\n --panel: #16213e;\n --accent: #e94560;\n --accent2: #0f3460;\n --text: #eee;\n --text-dim: #8899aa;\n --grid-cell-on: #e94560;\n --grid-cell-off: #2a2a4a;\n --hover: #3a3a5a;\n --bpm-input: #0f3460;\n }\n\n * { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;\n background: var(--bg);\n color: var(--text);\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n padding: 20px;\n }\n\n .drum-machine {\n background: var(--panel);\n border-radius: 20px;\n padding: 32px;\n box-shadow: 0 20px 60px rgba(0,0,0,0.5);\n width: 100%;\n max-width: 800px;\n border: 1px solid #2a3a5a;\n }\n\n h1 {\n text-align: center;\n font-size: 1.6rem;\n margin-bottom: 24px;\n letter-spacing: 2px;\n text-transform: uppercase;\n color: var(--accent);\n }\n\n /* Transport bar */\n .transport {\n display: flex;\n align-items: center;\n gap: 16px;\n margin-bottom: 24px;\n padding: 16px;\n background: var(--accent2);\n border-radius: 12px;\n }\n\n .btn {\n border: none;\n border-radius: 50%;\n cursor: pointer;\n font-size: 1.1rem;\n font-weight: 700;\n transition: transform 0.1s, box-shadow 0.1s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n }\n\n .btn:active { transform: scale(0.92); }\n\n .btn-play {\n background: var(--accent);\n color: #fff;\n box-shadow: 0 0 20px rgba(233, 69, 96, 0.4);\n }\n\n .btn-play.playing {\n background: #ff6b81;\n box-shadow: 0 0 20px rgba(255, 107, 129, 0.6);\n }\n\n .btn-stop {\n background: #444;\n color: #fff;\n }\n\n .bpm-group {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n }\n\n .bpm-group label {\n font-size: 0.85rem;\n color: var(--text-dim);\n text-transform: uppercase;\n letter-spacing: 1px;\n }\n\n .bpm-input {\n width: 70px;\n padding: 8px 12px;\n border-radius: 8px;\n border: 2px solid #3a4a6a;\n background: var(--bpm-input);\n color: var(--text);\n font-size: 1rem;\n font-weight: 600;\n text-align: center;\n outline: none;\n transition: border-color 0.2s;\n }\n\n .bpm-input:focus { border-color: var(--accent); }\n\n .bpm-value {\n font-size: 1.2rem;\n font-weight: 700;\n color: var(--accent);\n min-width: 36px;\n }\n\n /* Master volume */\n .master-vol {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-left: 12px;\n }\n\n .master-vol label {\n font-size: 0.85rem;\n color: var(--text-dim);\n text-transform: uppercase;\n }\n\n .master-vol input[type=\"range\"] {\n width: 80px;\n height: 4px;\n -webkit-appearance: none;\n background: #3a4a6a;\n border-radius: 2px;\n outline: none;\n }\n\n .master-vol input[type=\"range\"]::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n background: var(--accent);\n cursor: pointer;\n }\n\n /* Step grid */\n .grid-container {\n margin-bottom: 20px;\n }\n\n .grid-header {\n display: grid;\n grid-template-columns: 100px repeat(16, 1fr);\n gap: 2px;\n margin-bottom: 2px;\n }\n\n .step-label {\n text-align: center;\n font-size: 0.7rem;\n color: var(--text-dim);\n padding: 4px 0;\n font-weight: 600;\n letter-spacing: 0.5px;\n }\n\n .step-label.highlight {\n color: var(--accent);\n background: rgba(233, 69, 96, 0.1);\n border-radius: 4px;\n }\n\n .instrument-row {\n display: grid;\n grid-template-columns: 100px repeat(16, 1fr);\n gap: 2px;\n align-items: center;\n margin-bottom: 2px;\n }\n\n .instrument-name {\n font-size: 0.75rem;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n color: var(--text-dim);\n padding-left: 4px;\n display: flex;\n align-items: center;\n }\n\n .step-cell {\n aspect-ratio: 1;\n border-radius: 6px;\n border: 1px solid #2a3a5a;\n background: var(--grid-cell-off);\n cursor: pointer;\n transition: background 0.12s, transform 0.08s, box-shadow 0.12s;\n min-height: 28px;\n }\n\n .step-cell:hover {\n background: var(--hover);\n }\n\n .step-cell.active {\n background: var(--grid-cell-on);\n box-shadow: 0 0 12px rgba(233, 69, 96, 0.4);\n }\n\n .step-cell.current {\n border-color: #fff;\n box-shadow: 0 0 0 2px var(--accent);\n }\n\n .step-cell.current.active {\n border-color: #fff;\n box-shadow: 0 0 12px rgba(233, 69, 96, 0.6), 0 0 0 2px var(--accent);\n }\n\n /* Instrument colors */\n .instrument-row.kick .step-cell.active { background: #e94560; box-shadow: 0 0 12px rgba(233, 69, 96, 0.4); }\n .instrument-row.snare .step-cell.active { background: #00d2ff; box-shadow: 0 0 12px rgba(0, 210, 255, 0.4); }\n .instrument-row.hihat .step-cell.active { background: #ffd700; box-shadow: 0 0 12px rgba(255, 215, 0, 0.4); }\n .instrument-row.clap .step-cell.active { background: #a78bfa; box-shadow: 0 0 12px rgba(167, 139, 250, 0.4); }\n\n .instrument-row.kick .step-name { color: #e94560; }\n .instrument-row.snare .step-name { color: #00d2ff; }\n .instrument-row.hihat .step-name { color: #ffd700; }\n .instrument-row.clap .step-name { color: #a78bfa; }\n\n /* Pattern controls */\n .pattern-controls {\n display: flex;\n align-items: center;\n gap: 16px;\n margin-top: 16px;\n padding: 12px;\n background: var(--accent2);\n border-radius: 12px;\n }\n\n .pattern-btn {\n background: #3a4a6a;\n color: var(--text);\n border: none;\n border-radius: 8px;\n padding: 8px 14px;\n font-size: 0.8rem;\n text-transform: uppercase;\n letter-spacing: 1px;\n cursor: pointer;\n transition: background 0.2s;\n }\n\n .pattern-btn:hover { background: #4a5a7a; }\n\n .pattern-btn.active {\n background: var(--accent);\n color: #fff;\n }\n\n .clear-btn {\n margin-left: auto;\n background: #553333;\n color: #ff8888;\n }\n\n .clear-btn:hover { background: #664444; }\n\n .status {\n margin-top: 12px;\n text-align: center;\n font-size: 0.75rem;\n color: var(--text-dim);\n letter-spacing: 1px;\n text-transform: uppercase;\n }\n\n .status .beat {\n color: var(--accent);\n font-weight: 600;\n }\n</style>\n</head>\n<body>\n\n<div class=\"drum-machine\">\n <h1>Drum Machine</h1>\n\n <div class=\"transport\">\n <button class=\"btn btn-play\" id=\"playBtn\" title=\"Play / Stop\">▶</button>\n <button class=\"btn btn-stop\" id=\"stopBtn\" title=\"Stop\">■</button>\n\n <div class=\"bpm-group\">\n <label for=\"bpmInput\">BPM</label>\n <input type=\"number\" id=\"bpmInput\" class=\"bpm-input\" value=\"128\" min=\"40\" max=\"240\" step=\"1\">\n <span class=\"bpm-value\" id=\"bpmValue\">128</span>\n </div>\n\n <div class=\"master-vol\">\n <label>Vol</label>\n <input type=\"range\" id=\"masterVol\" min=\"0\" max=\"100\" value=\"75\">\n </div>\n </div>\n\n <div class=\"grid-container\">\n <div class=\"grid-header\" id=\"gridHeader\"></div>\n <div id=\"gridBody\"></div>\n </div>\n\n <div class=\"pattern-controls\">\n <button class=\"pattern-btn active\" data-pattern=\"0\">Pattern A</button>\n <button class=\"pattern-btn\" data-pattern=\"1\">Pattern B</button>\n <button class=\"pattern-btn\" data-pattern=\"2\">Pattern C</button>\n <button class=\"pattern-btn\" data-pattern=\"3\">Pattern D</button>\n <button class=\"clear-btn\" id=\"clearBtn\">Clear</button>\n </div>\n\n <div class=\"status\">\n Step: <span class=\"beat\" id=\"stepDisplay\">--</span> | Bar: <span id=\"barDisplay\">--</span>\n </div>\n</div>\n\n<script>\n(() => {\n // ─── Audio Engine ────────────────────────────────────────────────\n let audioCtx = null;\n\n function getAudioCtx() {\n if (!audioCtx) {\n audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n }\n if (audioCtx.state === 'suspended') {\n audioCtx.resume();\n }\n return audioCtx;\n }\n\n // Master gain\n const masterGain = (() => {\n const g = getAudioCtx().createGain();\n g.gain.value = 0.75;\n g.connect(getAudioCtx().destination);\n return g;\n })();\n\n // ─── Synthesized Drum Sounds ─────────────────────────────────────\n\n function playKick(time, vol) {\n const ctx = getAudioCtx();\n const osc = ctx.createOscillator();\n const gain = ctx.createGain();\n const filter = ctx.createBiquadFilter();\n\n osc.type = 'sine';\n osc.frequency.setValueAtTime(150, time);\n osc.frequency.exponentialRampToValueAtTime(40, time + 0.12);\n\n filter.type = 'lowpass';\n filter.frequency.setValueAtTime(200, time);\n filter.frequency.exponentialRampToValueAtTime(80, time + 0.12);\n\n gain.gain.setValueAtTime(vol * 0.7, time);\n gain.gain.exponentialRampToValueAtTime(0.001, time + 0.3);\n\n osc.connect(filter);\n filter.connect(gain);\n gain.connect(masterGain);\n\n osc.start(time);\n osc.stop(time + 0.35);\n }\n\n function playSnare(time, vol) {\n const ctx = getAudioCtx();\n\n // Noise component\n const noiseBuf = ctx.createBuffer(1, ctx.sampleRate * 0.3, ctx.sampleRate);\n const noiseData = noiseBuf.getChannelData(0);\n for (let i = 0; i < noiseData.length; i++) {\n noiseData[i] = Math.random() * 2 - 1;\n }\n const noiseSrc = ctx.createBufferSource();\n noiseSrc.buffer = noiseBuf;\n\n const noiseGain = ctx.createGain();\n noiseGain.gain.setValueAtTime(vol * 0.5, time);\n noiseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.15);\n\n const noiseFilter = ctx.createBiquadFilter();\n noiseFilter.type = 'highpass';\n noiseFilter.frequency.value = 1000;\n\n noiseSrc.connect(noiseFilter);\n noiseFilter.connect(noiseGain);\n noiseGain.connect(masterGain);\n noiseSrc.start(time);\n noiseSrc.stop(time + 0.2);\n\n // Tone component\n const osc = ctx.createOscillator();\n osc.type = 'triangle';\n osc.frequency.setValueAtTime(180, time);\n osc.frequency.exponentialRampToValueAtTime(120, time + 0.06);\n\n const oscGain = ctx.createGain();\n oscGain.gain.setValueAtTime(vol * 0.35, time);\n oscGain.gain.exponentialRampToValueAtTime(0.001, time + 0.1);\n\n osc.connect(oscGain);\n oscGain.connect(masterGain);\n osc.start(time);\n osc.stop(time + 0.12);\n }\n\n function playHihat(time, vol, isOpen = false) {\n const ctx = getAudioCtx();\n\n const noiseBuf = ctx.createBuffer(1, ctx.sampleRate * (isOpen ? 0.6 : 0.12), ctx.sampleRate);\n const noiseData = noiseBuf.getChannelData(0);\n for (let i = 0; i < noiseData.length; i++) {\n noiseData[i] = Math.random() * 2 - 1;\n }\n const noiseSrc = ctx.createBufferSource();\n noiseSrc.buffer = noiseBuf;\n\n const gain = ctx.createGain();\n const decay = isOpen ? 0.5 : 0.08;\n gain.gain.setValueAtTime(vol * (isOpen ? 0.25 : 0.4), time);\n gain.gain.exponentialRampToValueAtTime(0.001, time + decay);\n\n const filter = ctx.createBiquadFilter();\n filter.type = 'highpass';\n filter.frequency.value = isOpen ? 5000 : 7000;\n\n noiseSrc.connect(filter);\n filter.connect(gain);\n gain.connect(masterGain);\n noiseSrc.start(time);\n noiseSrc.stop(time + decay + 0.05);\n }\n\n function playClap(time, vol) {\n const ctx = getAudioCtx();\n\n // Multiple noise bursts for clap effect\n for (let i = 0; i < 3; i++) {\n const burstTime = time + i * 0.012;\n const noiseBuf = ctx.createBuffer(1, ctx.sampleRate * 0.25, ctx.sampleRate);\n const noiseData = noiseBuf.getChannelData(0);\n for (let j = 0; j < noiseData.length; j++) {\n noiseData[j] = Math.random() * 2 - 1;\n }\n const noiseSrc = ctx.createBufferSource();\n noiseSrc.buffer = noiseBuf;\n\n const gain = ctx.createGain();\n const amp = i === 1 ? vol * 0.55 : vol * 0.3;\n gain.gain.setValueAtTime(amp, burstTime);\n gain.gain.exponentialRampToValueAtTime(0.001, burstTime + 0.1);\n\n const filter = ctx.createBiquadFilter();\n filter.type = 'bandpass';\n filter.frequency.value = 2000 + i * 500;\n filter.Q.value = 0.8;\n\n noiseSrc.connect(filter);\n filter.connect(gain);\n gain.connect(masterGain);\n noiseSrc.start(burstTime);\n noiseSrc.stop(burstTime + 0.15);\n }\n\n // Body tone\n const osc = ctx.createOscillator();\n osc.type = 'sine';\n osc.frequency.setValueAtTime(260, time);\n osc.frequency.exponentialRampToValueAtTime(180, time + 0.05);\n\n const oscGain = ctx.createGain();\n oscGain.gain.setValueAtTime(vol * 0.2, time);\n oscGain.gain.exponentialRampToValueAtTime(0.001, time + 0.08);\n\n osc.connect(oscGain);\n oscGain.connect(masterGain);\n osc.start(time);\n osc.stop(time + 0.1);\n }\n\n // ─── State ────────────────────────────────────────────────────────\n const INSTRUMENTS = [\n { id: 'kick', name: 'KICK', color: '#e94560' },\n { id: 'snare', name: 'SNARE', color: '#00d2ff' },\n { id: 'hihat', name: 'HI-HAT', color: '#ffd700' },\n { id: 'clap', name: 'CLAP', color: '#a78bfa' },\n ];\n\n const NUM_STEPS = 16;\n\n // Patterns: patterns[patternIdx][instrumentIdx] = Uint8Array of 16 bools\n const patterns = [\n Array.from({ length: 4 }, () => new Uint8Array(NUM_STEPS)),\n Array.from({ length: 4 }, () => new Uint8Array(NUM_STEPS)),\n Array.from({ length: 4 }, () => new Uint8Array(NUM_STEPS)),\n" }
thinking
The write failed because I didn't include the path property properly. Let me retry.
/tmp/ws/index.html (19676 bytes)Successfully wrote 19676 bytes to /tmp/ws/index.html
thinking
The file was written successfully. Let me verify it's complete and correct by reading it back.
{"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> :root { --bg: #1a1a2e; --panel: #16213e; --accent: #e94560; --accent2: #0f3460; --text: #eee; --text-dim: #8899aa; --grid-cell-on: #e94560; --grid-cell-off: #2a2a4a; --hover: #3a3a5a; --bpm-input: #0f3460; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px; } .drum-machine { background: var(--panel); border-radius: 20px; padding: 32px; box-shadow: 0 20px 60px rgba(0,0,0,0.5); width: 100%; max-width: 800px; border: 1px solid #2a3a5a; } h1 { text-align: center; font-size: 1.6rem; margin-bottom: 24px; letter-spacing: 2px; text-transform: uppercase; color: var(--accent); } .transport { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; padding: 16px; background: var(--accent2); border-radius: 12px; } .btn { border: none; border-radius: 50%; cursor: pointer; font-size: 1.1rem; font-weight: 700; transition: transform 0.1s, box-shadow 0.1s; display: flex; align-items: center; justify-content: center; width: 48px; height: 48px; } .btn:active { transform: scale(0.92); } .btn-play { background: var(--accent); color: #fff; box-shadow: 0 0 20px rgba(233, 69, 96, 0.4); } .btn-play.playing { background: #ff6b81; box-shadow: 0 0 20px rgba(255, 107, 129, 0.6); } .btn-stop { background: #444; color: #fff; } .bpm-group { display: flex; align-items: center; gap: 8px; margin-left: auto; } .bpm-group label { font-size: 0.85rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; } .bpm-input { width: 70px; padding: 8px 12px; border-radius: 8px; border: 2px solid #3a4a6a; background: var(--bpm-input); color: var(--text); font-size: 1rem; font-weight: 600; text-align: center; outline: none; transition: border-color 0.2s; } .bpm-input:focus { border-color: var(--accent); } .bpm-value { font-size: 1.2rem; font-weight: 700; color: var(--accent); min-width: 36px; } .master-vol { display: flex; align-items: center; gap: 8px; margin-left: 12px; } .master-vol label { font-size: 0.85rem; color: var(--text-dim); text-transform: uppercase; } .master-vol input[type="range"] { width: 80px; height: 4px; -webkit-appearance: none; background: #3a4a6a; border-radius: 2px; outline: none; } .master-vol input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; } .grid-container { margin-bottom: 20px; } .grid-header { display: grid; grid-template-columns: 100px repeat(16, 1fr); gap: 2px; margin-bottom: 2px; } .step-label { text-align: center; font-size: 0.7rem; color: var(--text-dim); padding: 4px 0; font-weight: 600; letter-spacing: 0.5px; } .step-label.highlight { color: var(--accent); background: rgba(233, 69, 96, 0.1); border-radius: 4px; } .instrument-row { display: grid; grid-template-columns: 100px repeat(16, 1fr); gap: 2px; align-items: center; margin-bottom: 2px; } .instrument-name { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1.5px; color: var(--text-dim); padding-left: 4px; display: flex; align-items: center; } .step-cell { aspect-ratio: 1; border-radius: 6px; border: 1px solid #2a3a5a; background: var(--grid-cell-off); cursor: pointer; transition: background 0.12s, transform 0.08s, box-shadow 0.12s; min-height: 28px; } .step-cell:hover { background: var(--hover); } .step-cell.active { background: var(--grid-cell-on); box-shadow: 0 0 12px rgba(233, 69, 96, 0.4); } .step-cell.current { border-color: #fff; box-shadow: 0 0 0 2px var(--accent); } .step-cell.current.active { border-color: #fff; box-shadow: 0 0 12px rgba(233, 69, 96, 0.6), 0 0 0 2px var(--accent); } .instrument-row.kick .step-cell.active { background: #e94560; box-shadow: 0 0 12px rgba(233, 69, 96, 0.4); } .instrument-row.snare .step-cell.active { background: #00d2ff; box-shadow: 0 0 12px rgba(0, 210, 255, 0.4); } .instrument-row.hihat .step-cell.active { background: #ffd700; box-shadow: 0 0 12px rgba(255, 215, 0, 0.4); } .instrument-row.clap .step-cell.active { background: #a78bfa; box-shadow: 0 0 12px rgba(167, 139, 250, 0.4); } .instrument-row.kick .instrument-name { color: #e94560; } .instrument-row.snare .instrument-name { color: #00d2ff; } .instrument-row.hihat .instrument-name { color: #ffd700; } .instrument-row.clap .instrument-name { color: #a78bfa; } .pattern-controls { display: flex; align-items: center; gap: 16px; margin-top: 16px; padding: 12px; background: var(--accent2); border-radius: 12px; } .pattern-btn { background: #3a4a6a; color: var(--text); border: none; border-radius: 8px; padding: 8px 14px; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; cursor: pointer; transition: background 0.2s; } .pattern-btn:hover { background: #4a5a7a; } .pattern-btn.active { background: var(--accent); color: #fff; } .clear-btn { margin-left: auto; background: #553333; color: #ff8888; } .clear-btn:hover { background: #664444; } .status { margin-top: 12px; text-align: center; font-size: 0.75rem; color: var(--text-dim); letter-spacing: 1px; text-transform: uppercase; } .status .beat { color: var(--accent); font-weight: 600; } </style> </head> <body> <div class="drum-machine"> <h1>Drum Machine</h1> <div class="transport"> <button class="btn btn-play" id="playBtn" title="Play / Stop">▶</button> <button class="btn btn-stop" id="stopBtn" title="Stop">■</button> <div class="bpm-group"> <label for="bpmInput">BPM</label> <input type="number" id="bpmInput" class="bpm-input" value="128" min="40" max="240" step="1"> <span class="bpm-value" id="bpmValue">128</span> </div> <div class="master-vol"> <label>Vol</label> <input type="range" id="masterVol" min="0" max="100" value="75"> </div> </div> <div class="grid-container"> <div class="grid-header" id="gridHeader"></div> <div id="gridBody"></div> </div> <div class="pattern-controls"> <button class="pattern-btn active" data-pattern="0">Pattern A</button> <button class="pattern-btn" data-pattern="1">Pattern B</button> <button class="pattern-btn" data-pattern="2">Pattern C</button> <button class="pattern-btn" data-pattern="3">Pattern D</button> <button class="clear-btn" id="clearBtn">Clear</button> </div> <div class="status"> Step: <span class="beat" id="stepDisplay">--</span> | Bar: <span id="barDisplay">--</span> </div> </div> <script> (() => { // ─── Audio Engine ──────────────────────────────────────────────── let audioCtx = null; function getAudioCtx() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } if (audioCtx.state === 'suspended') audioCtx.resume(); return audioCtx; } const masterGain = (() => { const g = getAudioCtx().createGain(); g.gain.value = 0.75; g.connect(getAudioCtx().destination); return g; })(); // ─── Synthesized Drum Sounds ───────────────────────────────────── function playKick(time, vol) { const ctx = getAudioCtx(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); const filter = ctx.createBiquadFilter(); osc.type = 'sine'; osc.frequency.setValueAtTime(150, time); osc.frequency.exponentialRampToValueAtTime(40, time + 0.12); filter.type = 'lowpass'; filter.frequency.setValueAtTime(200, time); filter.frequency.exponentialRampToValueAtTime(80, time + 0.12); gain.gain.setValueAtTime(vol * 0.7, time); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.3); osc.connect(filter); filter.connect(gain); gain.connect(masterGain); osc.start(time); osc.stop(time + 0.35); } function playSnare(time, vol) { const ctx = getAudioCtx(); const noiseBuf = ctx.createBuffer(1, ctx.sampleRate * 0.3, ctx.sampleRate); const nd = noiseBuf.getChannelData(0); for (let i = 0; i < nd.length; i++) nd[i] = Math.random() * 2 - 1; const noiseSrc = ctx.createBufferSource(); noiseSrc.buffer = noiseBuf; const noiseGain = ctx.createGain(); noiseGain.gain.setValueAtTime(vol * 0.5, time); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.15); const nf = ctx.createBiquadFilter(); nf.type = 'highpass'; nf.frequency.value = 1000; noiseSrc.connect(nf); nf.connect(noiseGain); noiseGain.connect(masterGain); noiseSrc.start(time); noiseSrc.stop(time + 0.2); const osc = ctx.createOscillator(); osc.type = 'triangle'; osc.frequency.setValueAtTime(180, time); osc.frequency.exponentialRampToValueAtTime(120, time + 0.06); const oscGain = ctx.createGain(); oscGain.gain.setValueAtTime(vol * 0.35, time); oscGain.gain.exponentialRampToValueAtTime(0.001, time + 0.1); osc.connect(oscGain); oscGain.connect(masterGain); osc.start(time); osc.stop(time + 0.12); } function playHihat(time, vol, isOpen = false) { const ctx = getAudioCtx(); const dur = isOpen ? 0.6 : 0.12; const noiseBuf = ctx.createBuffer(1, ctx.sampleRate * dur, ctx.sampleRate); const nd = noiseBuf.getChannelData(0); for (let i = 0; i < nd.length; i++) nd[i] = Math.random() * 2 - 1; const noiseSrc = ctx.createBufferSource(); noiseSrc.buffer = noiseBuf; const gain = ctx.createGain(); const decay = isOpen ? 0.5 : 0.08; gain.gain.setValueAtTime(vol * (isOpen ? 0.25 : 0.4), time); gain.gain.exponentialRampToValueAtTime(0.001, time + decay); const filter = ctx.createBiquadFilter(); filter.type = 'highpass'; filter.frequency.value = isOpen ? 5000 : 7000; noiseSrc.connect(filter); filter.connect(gain); gain.connect(masterGain); noiseSrc.start(time); noiseSrc.stop(time + decay + 0.05); } function playClap(time, vol) { const ctx = getAudioCtx(); for (let i = 0; i < 3; i++) { const bt = time + i * 0.012; const noiseBuf = ctx.createBuffer(1, ctx.sampleRate * 0.25, ctx.sampleRate); const nd = noiseBuf.getChannelData(0); for (let j = 0; j < nd.length; j++) nd[j] = Math.random() * 2 - 1; const noiseSrc = ctx.createBufferSource(); noiseSrc.buffer = noiseBuf; const gain = ctx.createGain(); const amp = i === 1 ? vol * 0.55 : vol * 0.3; gain.gain.setValueAtTime(amp, bt); gain.gain.exponentialRampToValueAtTime(0.001, bt + 0.1); const filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.value = 2000 + i * 500; filter.Q.value = 0.8; noiseSrc.connect(filter); filter.connect(gain); gain.connect(masterGain); noiseSrc.start(bt); noiseSrc.stop(bt + 0.15); } const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(260, time); osc.frequency.exponentialRampToValueAtTime(180, time + 0.05); const oscGain = ctx.createGain(); oscGain.gain.setValueAtTime(vol * 0.2, time); oscGain.gain.exponentialRampToValueAtTime(0.001, time + 0.08); osc.connect(oscGain); oscGain.connect(masterGain); osc.start(time); osc.stop(time + 0.1); } // ─── State ──────────────────────────────────────────────────────── const INSTRUMENTS = [ { id: 'kick', name: 'KICK', play: playKick }, { id: 'snare', name: 'SNARE', play: playSnare }, { id: 'hihat', name: 'HI-HAT', play: (t, v) => playHihat(t, v, false) }, { id: 'clap', name: 'CLAP', play: playClap }, ]; const NUM_STEPS = 16; const NUM_PATTERNS = 4; // patterns[p][i] = Uint8Array of 16 const patterns = Array.from({ length: NUM_PATTERNS }, () => INSTRUMENTS.map(() => new Uint8Array(NUM_STEPS)) ); let currentPattern = 0; let bpm = 128; let masterVol = 0.75; let isPlaying = false; let currentStep = -1; let timerId = null; // ─── DOM refs ──────────────────────────────────────────────────── const playBtn = document.getElementById('playBtn'); const stopBtn = document.getElementById('stopBtn'); const bpmInput = document.getElementById('bpmInput'); const bpmValue = document.getElementById('bpmValue'); const masterVolInput = document.getElementById('masterVol'); const gridHeader = document.getElementById('gridHeader'); const gridBody = document.getElementById('gridBody'); const stepDisplay = document.getElementById('stepDisplay'); const barDisplay = document.getElementById('barDisplay'); const patternBtns = document.querySelectorAll('.pattern-btn'); const clearBtn = document.getElementById('clearBtn'); // ─── Build Grid UI ─────────────────────────────────────────────── function buildHeader() { gridHeader.innerHTML = ''; const spacer = document.createElement('div'); spacer.className = 'step-label'; gridHeader.appendChild(spacer); for (let s = 0; s < NUM_STEPS; s++) { const lbl = document.createElement('div'); lbl.className = 'step-label'; lbl.textContent = s + 1; lbl.dataset.step = s; gridHeader.appendChild(lbl); } } function buildGrid() { gridBody.innerHTML = ''; INSTRUMENTS.forEach((inst, i) => { const row = document.createElement('div'); row.className = `instrument-row ${inst.id}`; const name = document.createElement('div'); name.className = 'instrument-name'; name.textContent = inst.name; row.appendChild(name); for (let s = 0; s < NUM_STEPS; s++) { const cell = document.createElement('div'); cell.className = 'step-cell'; cell.dataset.instrument = i; cell.dataset.step = s; cell.addEventListener('click', () => { const pat = patterns[currentPattern]; pat[i][s] = pat[i][s] ? 0 : 1; cell.classList.toggle('active'); // Preview sound on click const ctx = getAudioCtx(); inst.play(ctx.currentTime + 0.01, masterVol * 0.8); }); row.appendChild(cell); } gridBody.appendChild(row); }); } function refreshGridActive() { const cells = gridBody.querySelectorAll('.step-cell'); cells.forEach(c => { const i = +c.dataset.instrument; const s = +c.dataset.step; c.classList.toggle('active', !!patterns[currentPattern][i][s]); }); } function highlightStep(step) { const cells = gridBody.querySelectorAll('.step-cell'); const labels = gridHeader.querySelectorAll('.step-label'); cells.forEach(c => c.classList.remove('current')); labels.forEach(l => l.classList.remove('highlight')); if (step >= 0 && step < NUM_STEPS) { cells.forEach(c => { if (+c.dataset.step === step) c.classList.add('current'); }); labels.forEach(l => { if (+l.dataset.step === step) l.classList.add('highlight'); }); } } // ─── Sequencer ─────────────────────────────────────────────────── function getStepInterval() { return (60 / bpm) / 4 * 1000; // 16th note } function scheduleStep(stepIdx) { const now = getAudioCtx().currentTime; const pat = patterns[currentPattern]; INSTRUMENTS.forEach((inst, i) => { if (pat[i][stepIdx]) { inst.play(now + 0.005, masterVol); } }); } function startSequencer() { if (isPlaying) return; isPlaying = true; currentStep = -1; scheduleLoop(); playBtn.textContent = '\u23F8'; playBtn.classList.add('playing'); } function scheduleLoop() { if (!isPlaying) return; const interval = getStepInterval(); const now = performance.now(); // Align to next 16th const drift = now % interval; const firstDelay = drift < 2 ? interval : interval - drift; setTimeout(() => { if (!isPlaying) return; currentStep = (currentStep + 1) % NUM_STEPS; scheduleStep(currentStep); updateStepDisplay(currentStep); highlightStep(currentStep); scheduleLoop(); }, firstDelay); } function stopSequencer() { isPlaying = false; if (timerId) { clearTimeout(timerId); timerId = null; } currentStep = -1; playBtn.textContent = '\u25B6'; playBtn.classList.remove('playing'); highlightStep(-1); stepDisplay.textContent = '--'; barDisplay.textContent = '--'; } function updateStepDisplay(step) { stepDisplay.textContent = step + 1; const bar = Math.floor(step / 4) + 1; barDisplay.textContent = bar; } // Tap tempo: clicking play while playing taps tempo function tapTempo() { if (!isPlaying) return; // BPM can be adjusted live via input; tap tempo is a bonus: // We handle it via the BPM input change, which reschedules on next step. } // ─── BPM ───────────────────────────────────────────────────────── bpmInput.addEventListener('input', () => { let val = parseInt(bpmInput.value, 10); if (isNaN(val)) val = 128; val = Math.max(40, Math.min(240, val)); bpm = val; bpmInput.value = val; bpmValue.textContent = val; // If playing, the loop auto-adjusts on next iteration }); masterVolInput.addEventListener('input', () => { masterVol = parseInt(masterVolInput.value, 10) / 100; masterGain.gain.setValueAtTime(masterVol, getAudioCtx().currentTime); }); // ─── Play / Stop ───────────────────────────────────────────────── playBtn.addEventListener('click', () => { if (!isPlaying) { getAudioCtx(); // ensure context is running startSequencer(); } else { stopSequencer(); } }); stopBtn.addEventListener('click', () => { stopSequencer(); }); // ─── Patterns ──────────────────────────────────────────────────── patternBtns.forEach(btn => { btn.addEventListener('click', () => { patternBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentPattern = parseInt(btn.dataset.pattern, 10); refreshGridActive(); }); }); // ─── Clear ─────────────────────────────────────────────────────── clearBtn.addEventListener('click', () => { patterns[currentPattern] = INSTRUMENTS.map(() => new Uint8Array(NUM_STEPS)); refreshGridActive(); }); // ─── Keyboard shortcuts ────────────────────────────────────────── document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT') return; if (e.code === 'Space') { e.preventDefault(); playBtn.click(); } if (e.code === 'KeyC') { clearBtn.click(); } }); // ─── Init ──────────────────────────────────────────────────────── buildHeader(); buildGrid(); })(); </script> </body> </html>
thinking
The file is complete and verified. Let me provide a summary of what was built.
`index.html` is ready in `/tmp/ws/`. Here's what it contains: **UI Layout** - Transport bar: **Play/Stop** buttons, **BPM** input (40–240, live-adjustable while playing), **master volume** slider - 16-step grid: 4 instrument rows × 16 cells, with color-coded instruments and a step-number header - 4 pattern slots (A/B/C/D) with a **Clear** button for the current pattern - Status line showing current step and bar number **Instruments (all synthesized with Web Audio API — no samples)** | Instrument | Synthesis | |---|---| | Kick | Sine oscillator with pitch & filter sweep, lowpass envelope | | Snare | White noise burst (highpass) + triangle tone with pitch drop | | Hi-hat | Short white noise burst, highpass at 7 kHz | | Clap | 3 stacked bandpass-filtered noise bursts + sine body tone | **Sequencer** - 16th-note scheduler aligned to the nearest beat (drift-corrected `setTimeout` loop) - Click any cell to toggle it on/off (and hear a preview of the hit) - Step highlight moves across the grid; active cells glow in each instrument's color - Space = play/stop, C = clear pattern Open `index.html` in a browser — everything is inline, no external resources needed.
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
16-step drum machine grid with kick/snare/hi-hat/clap rows, BPM control, play toggling, and step highlighting works via click and keyboard.
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.