Drum machine · deepseek-r1-0528
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.
This one-shot run produced no runnable artifact.
agent produced no index.html
The transcript below shows what the model did — read it to see why the one shot fell short.
transcript · 2 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
We are going to build a single-page drum machine in one HTML file (index.html).
The app will have:
- A grid of 4 rows (for kick, snare, hi-hat, clap) and 16 steps (columns).
- Each cell in the grid is clickable to toggle step activation.
- A play button to start/stop the sequence.
- A BPM (beats per minute) control to adjust the tempo.
- Use the Web Audio API to synthesize the drum sounds.
We won't use any external libraries (so no jQuery, no Tone.js, etc.) and all code must be inline.
Steps for the app:
1. HTML structure: a container for the grid, one row per instrument, 16 cells per row.
2. CSS for styling: grid layout and active/inactive state for cells.
3. JavaScript for:
- Creating the Web Audio context.
- Synthesizing the four drum sounds (kick, snare, hi-hat, clap) using oscillator and noise.
- Handling the step grid: toggling steps and highlighting the current step during playback.
- Playback: a sequencer that loops over the 16 steps at the given BPM.
- Controls: play/stop and BPM adjustment.
Note: The Web Audio code will be written in a way that works in modern browsers.
Let's break down:
HTML:
- We'll have a <div> for each instrument row, and within each row, 16 step buttons (or divs).
- Play button and BPM input (slider or number input).
CSS:
- Style the grid so that each row is a horizontal list of steps.
- Style for active steps and current playing step.
JavaScript:
Web Audio:
We'll create an audio context and then define functions for each sound:
Kick: a low frequency oscillator with a quick decay.
Snare: a combination of a noise burst and a tone.
Hi-hat: a short burst of noise with a high pass filter.
Clap: a series of short noise bursts.
We'll pre-create these as audio buffers? Or we can generate them on the fly?
Since we are only playing short sounds, we can generate them when needed (on each step) without too much latency.
Alternatively, we can pre-render the buffers at the start. Let's pre-render the buffers to avoid any timing issues and for better performance.
Sequencer:
We'll have an array of 4 arrays (one per instrument) of 16 booleans (the steps).
When the play button is pressed, we'll start an interval or use Web Audio scheduling to play the steps at the correct time.
We'll use setInterval or setTimeout at first for simplicity, but note: for precise timing we should use Web Audio scheduling or the Web Clock. However, since we are generating the sounds on the fly and the sounds are short, using setTimeout might be acceptable for this demo.
Alternatively, we can schedule the sounds using the Web Audio context's current time and schedule them in advance.
Approach:
We can use setInterval: every step, we check which notes should play for that step and play them. Then we move to the next step and loop.
The step duration is: (60 * 1000) / (BPM * 4) [because we have 4 steps per beat? Actually, in a typical 4/4 pattern, 16 steps are 4 beats, so each step is 1/4 beat. So step duration in milliseconds = (60 * 1000) / (BPM) / 4 = (60000) / (BPM * 4) = 15000 / BPM.
Steps:
let stepDuration = (60 * 1000) / (bpm * 4); // for 16th notes
Then every `stepDuration` milliseconds, we advance the current step and play the notes for that step.
We also need to handle the case when BPM is changed during playback.
Alternatively, we can use the Web Audio clock by having a scheduled loop that checks the current step by the audio context's current time. But that is more complex.
For simplicity and because we are not requiring sample-accurate timing, I'll use setInterval and then clear it on stop.
However, note: setInterval is not very precise for music. We might get drift. But for this demo it might be acceptable.
We'll store the current step (0-15) and then wrap around.
For each step, we'll play the sounds for each instrument that are active on that step.
We'll also update the UI to highlight the current step.
Implementation:
We'll have:
let audioContext;
let isPlaying = false;
let currentStep = 0;
let bpm = 120; // default
let timerId = null;
const steps = [
[false, ...] for kick (16 steps),
... for snare, hihat, clap
];
Pre-render the audio buffers for each sound? We'll do:
createKickBuffer(), etc. and store them as AudioBuffer objects.
Then, when we want to play a kick, we can create an AudioBufferSourceNode and play it.
Steps to create a buffer:
function createKickBuffer() {
// create a buffer with a short sine wave that drops in frequency
// ... implementation
}
We'll create these buffers once when the audio context is started.
Alternatively, we can create the buffers on the fly when the context is created.
We'll also need to handle the case that the audio context must be resumed after a user gesture (because of autoplay policies). So we'll create the context on the first user interaction (like a play button click).
Since the first user interaction will be when the user clicks the play button, we can start the context then.
Plan for audio:
We'll create a function `initAudio()` that creates the audio context and the buffers. But note: creating buffers is asynchronous? Actually, not if we generate them by filling the buffer manually.
We can generate the buffers with `audioContext.createBuffer` and then use `buffer.getChannelData(0)` and fill it with samples.
Kick:
A simple kick: a sine wave that starts at 150Hz and drops to 50Hz over 0.3 seconds? But actually, we want it to be short.
We'll design each sound to be less than 0.5 seconds.
Let's write the sound generation functions:
Note: The buffer creation is synchronous.
We'll create:
let kickBuffer;
let snareBuffer;
// ...
function createKick() {
const sampleRate = audioContext.sampleRate;
const duration = 0.5; // seconds
const buffer = audioContext.createBuffer(1, sampleRate * duration, sampleRate);
const channelData = buffer.getChannelData(0);
let currentFrequency = 150;
const frequencyDrop = (currentFrequency - 50) / (sampleRate * duration);
for (let i = 0; i < buffer.length; i++) {
// frequency drops from 150 to 50
currentFrequency = Math.max(50, currentFrequency - frequencyDrop);
const t = i / sampleRate;
channelData[i] = Math.sin(2 * Math.PI * currentFrequency * t) * Math.exp(-t * 5); // decay envelope
}
return buffer;
}
But note: the envelope is applied as we generate the sample, and the frequency is changing per sample.
Snare:
A snare is typically a combination of a sine wave (for the body) and noise (for the snare). We'll generate a short burst of noise and a sine wave at around 180Hz.
Hi-Hat: a bandpass filtered noise with a very short decay.
Clap: multiple noise bursts in quick succession.
Alternatively, we can start with simpler versions:
Kick: as above.
Snare: a burst of noise and a short sine wave for the 'pop'. We can generate one by having a second oscillator for the noise? Actually we can generate noise in the buffer by filling with random numbers.
We'll generate the snare as:
Part 1: a sine wave at 180Hz for a very short time (0.05s) then decay.
Part 2: a noise burst (0.2 seconds) that also decays.
We can generate both in the same buffer by mixing.
However, since it's a demo, we can do:
function createSnare() {
const sampleRate = audioContext.sampleRate;
const duration = 0.3;
const buffer = audioContext.createBuffer(1, sampleRate * duration, sampleRate);
const data = buffer.getChannelData(0);
// body: sine wave at 180Hz
for (let i = 0; i < 0.05 * sampleRate; i++) {
const t = i / sampleRate;
data[i] = Math.sin(2 * Math.PI * 180 * t);
}
// noise part: from 0.01s to the end
const noiseStart = 0.01 * sampleRate;
for (let i = noiseStart; i < data.length; i++) {
data[i] += Math.random() * 2 - 1;
}
// apply an envelope to everything: a quick decay
for (let i = 0; i < data.length; i++) {
const t = i / sampleRate;
data[i] *= Math.exp(-t * 10);
}
return buffer;
}
But note: without normalizing, this might be too loud or too quiet. We might need to adjust the gains.
Alternatively, we can use a more common technique: generating the envelope for the noise and sine separately.
Given the scope, we'll aim for functional and adjust as needed.
We'll create the buffers when the audio context is created (on the first play).
Steps on play:
- If audioContext is not created, create it and create the buffers.
- Also, because of autoplay policies, we must do this on a user gesture (like the play button). So we do:
if (audioContext === undefined) {
audioContext = new AudioContext();
createBuffers(); // function that sets kickBuffer, snareBuffer, etc.
}
if (audioContext.state === 'suspended') {
audioContext.resume();
}
Then we start the sequencer.
We'll have a step() function that is called every `stepDuration` milliseconds:
step = () => {
// For each instrument, if the step at `currentStep` is active, play the sound.
if (kickSteps[currentStep]) {
playBuffer(kickBuffer);
}
... for snare, hihat, clap
// Update the UI: remove any previous step highlight, then highlight the current step.
// Then increment currentStep, and if we reach 16, reset to 0.
currentStep = (currentStep + 1) % 16;
}
timerId = setInterval(step, stepDuration);
When stopping, clearInterval(timerId) and set currentStep = 0.
We also need to update the step duration when BPM changes. If during playback, we should restart the sequencer with the new BPM.
To adjust BPM, we can:
function setBpm(newBpm) {
bpm = newBpm;
stepDuration = 60 * 1000 / (bpm * 4); // for 16th notes: 4 steps per beat, so 16 steps per 4 beats (one measure)
if (isPlaying) {
// restart the sequencer: stop and start again to pick up the new interval.
stopSequencer();
startSequencer();
}
}
Where stopSequencer() clears the interval and startSequencer() starts a new interval.
We'll also need to update the BPM input field.
UI:
We'll have:
<div class="controls">
<button id="play">Play</button>
<input id="bpm" type="number" value="120" min="60" max="240">
</div>
<div class="sequencer">
<div class="track kick">
<div class="step step0 active">1</div>
... 16 steps
</div>
... similarly for snare, hihat, clap
</div>
We'll generate the grid with JavaScript? Or we can write it in HTML? Since there are only 4 rows and 16 steps, we can write it in HTML.
But to be concise in code, let's generate the grid with JavaScript on page load. Alternatively, we can just write the HTML statically.
We'll write the HTML statically:
<div id="sequencer">
<div class="track" data-instrument="kick">
<div class="step"></div> ... 16 times
</div>
... for snare, hihat, clap
</div>
Then use CSS to style.
We'll assign event listeners to each step to toggle an 'active' class and update our steps array.
We'll store the steps array in memory and then map the step elements to the array.
Alternatively, we can store the state only in the DOM (by class) and then when playing, check which steps have the 'active' class? That might be simpler.
Let's do:
We'll have an Array for each instrument: [false, true, ...]. We'll update that array when the step is clicked.
Also, we will assign an event listener to each step to toggle the state.
But we can also have a separate data structure. For simplicity, we'll store in an array.
Implementation:
let steps = {
kick: Array(16).fill(false),
snare: Array(16).fill(false),
hihat: Array(16).fill(false),
clap: Array(16).fill(false)
};
We'll assign data attributes to each step: data-instrument and data-index (0-15).
Then on click, we can:
const stepEl = e.target;
const instrument = stepEl.parentElement.dataset.instrument;
const index = Array.from(stepEl.parentElement.children).indexOf(stepEl); // but we can assign data-index on each step?
We'll generate the grid with:
for (let i=0; i<4; i++) {
const track = document.createElement('div');
track.className = 'track';
track.dataset.instrument = instruments[i]; // ['kick','snare','hihat','clap']
for (let j=0; j<16; j++) {
const step = document.createElement('div');
step.className = 'step';
step.dataset.index = j;
track.appendChild(step);
}
sequencer.appendChild(track);
}
Then add event listeners to each step:
step.addEventListener('click', function() {
const instrument = this.parentElement.dataset.instrument;
const index = parseInt(this.dataset.index);
steps[instrument][index] = !steps[instrument][index];
this.classList.toggle('active');
});
We'll also store the step elements so we can highlight the current step.
For current step highlighting, we can do:
function highlightStep(stepIndex) {
// remove any 'current' class from all steps
document.querySelectorAll('.step').forEach(step => step.classList.remove('current'));
// then for each track, the step at stepIndex gets class 'current'
document.querySelectorAll('.track').forEach(track => {
track.children[stepIndex].classList.add('current');
});
}
Then in the step function:
highlightStep(currentStep);
... then play sounds for the step.
Then after playing, we advance and next time we'll highlight the next step.
But note: we remove the current class from all steps and then set the new current step.
We'll do this before playing the sounds? So the highlight happens exactly when the sound is played.
Now, let's put it all together.
We'll write the code inline in the <script> tag.
We'll use ES5 to be more compatible? But we are targeting modern browsers, so ES6 is fine.
We'll wrap the JavaScript in a DOMContentLoaded event to set up the event listeners.
We'll also use a class for the app to avoid global pollution? But since it's a single file and small, we can use global functions.
Alternatively, we use an IIFE.
We don't want to block the DOM, so we'll wait for the DOM to be ready.
We'll write:
document.addEventListener('DOMContentLoaded', function() {
// initialize the app
});
Let's start writing the HTML, then CSS, then JS.
We'll put the CSS in a <style> tag and JS in a <script> tag.
Note: We are writing a self-contained file.
We'll also need to include the sound generation functions.
Due to the complexity of the sound generation, we'll write the buffer generation functions inside the script.
We'll predefine the buffer variables as null, and then create them on first play.
We also need to handle the case when the audio context is suspended. We'll resume it when the user clicks play.
We'll create:
let audioContext = null;
let kickBuffer = null;
// ... etc.
Now, let's write the entire index.html.
I'll create a self-contained drum machine app in a single HTML file. Here's the implementation: html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>16-Step Drum Machine</title> <style> body { font-family: 'Arial', sans-serif; background: #2c3e50; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; color: #ecf0f1; } .drum-machine { background: #34495e; border-radius: 10px; padding: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); width: 700px; max-width: 90vw; } h1 { text-align: center; margin-top: 0; color: #ecf0f1; } .controls { display: flex; justify-content: space-between; margin: 20px 0; padding: 15px; background: #2c3e50; border-radius: 5px; } .bpm-control { display: flex; align-items: center; gap: 10px; } input[type="range"] { width: 150px; } button { background: #3498db; border: none; color: white; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-weight: bold; transition: background 0.3s; font-size: 16px; } button:hover { background: #2980b9; } button:active { transform: scale(0.98); } #playBtn.playing { background: #e74c3c; } .sequencer { display: flex; flex-direction: column; gap: 15px; } .track { display: flex; gap: 4px; } .track-label { width: 80px; display: flex; align-items: center; justify-content: center; font-weight: bold; background: #2c3e50; border-radius: 5px; padding: 5px; } .steps { display: flex; gap: 4px; flex-grow: 1; } .step { flex: 1; height: 45px; background: #2c3e50; border-radius: 5px; cursor: pointer; transition: all 0.1s; display: flex; justify-content: center; align-items: center; } .step.active { background: #3498db; } .step.active.kick { background: #3498db; } .step.active.snare { background: #2ecc71; } .step.active.hat { background: #9b59b6; } .step.active.clap { background: #e74c3c; } .step.current { box-shadow: 0 0 0 2px #f1c40f; } </style> </head> <body> <div class="drum-machine"> <h1>16-Step Drum Machine</h1> <div class="controls"> <button id="playBtn">Play</button> <div class="bpm-control"> <span id="bpmDisplay">120 BPM</span> <input type="range" id="bpmSlider" min="60" max="240" value="120"> </div> <button id="clearBtn">Clear All</button> </div> <div class="sequencer" id="sequencer"> <!-- Steps will be inserted here by JavaScript --> </div> </div> <script> class DrumMachine { constructor() { this.audioContext = null; this.isPlaying = false; this.currentStep = 0; this.bpm = 120; this.stepInterval = null; this.sounds = { kick: { oscType: 'sine', frequencies: [100, 50] }, snare: { oscType: 'noise', frequencies: [180, 0] }, hat: { oscType: 'noise', frequencies: [2500, 3500] }, clap: { oscType: 'noise', frequencies: [800, 0] } }; this.steps = {}; this.initAudioContext(); this.initDOM(); this.setupEventListeners(); } initAudioContext() { try { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { alert('Web Audio API is not supported in this browser'); } } initDOM() { this.sequencer = document.getElementById('sequencer'); this.playBtn = document.getElementById('playBtn'); this.clearBtn = document.getElementById('clearBtn'); this.bpmSlider = document.getElementById('bpmSlider'); this.bpmDisplay = document.getElementById('bpmDisplay'); // Create tracks for each drum sound ['kick', 'snare', 'hat', 'clap'].forEach(instrument => { const track = document.createElement('div'); track.className = 'track'; const label = document.createElement('div'); label.className = 'track-label'; label.textContent = instrument.charAt(0).toUpperCase() + instrument.slice(1); track.appendChild(label); const stepsContainer = document.createElement('div'); stepsContainer.className = 'steps'; // Create 16 steps for this track this.steps[instrument] = []; for (let i = 0; i < 16; i++) { const step = document.createElement('div'); step.className = `step ${instrument}`; step.dataset.instrument = instrument; step.dataset.index = i; step.textContent = i + 1; stepsContainer.appendChild(step); this.steps[instrument].push(false); } track.appendChild(stepsContainer); this.sequencer.appendChild(track); }); } setupEventListeners() { this.playBtn.addEventListener('click', () => this.togglePlay()); this.clearBtn.addEventListener('click', () => this.clearAll()); this.bpmSlider.addEventListener('input', () => this.updateBPM()); // Step event listeners document.querySelectorAll('.step').forEach(step => { step.addEventListener('click', () => { const instrument = step.dataset.instrument; const index = parseInt(step.dataset.index); this.toggleStep(instrument, index); }); }); } togglePlay() { if (!this.audioContext) return; if (this.isPlaying) { this.stop(); this.playBtn.textContent = 'Play'; this.playBtn.classList.remove('playing'); } else { this.play(); this.playBtn.textContent = 'Stop'; this.playBtn.classList.add('playing'); } } play() { this.isPlaying = true; this.currentStep = 0; this.highlightCurrentStep(); const stepDuration = (60 / this.bpm) * 0.25 * 1000; // 16th notes this.stepInterval = setInterval(() => { this.playStep(); this.currentStep = (this.currentStep + 1) % 16; this.highlightCurrentStep(); }, stepDuration); } stop() { this.isPlaying = false; clearInterval(this.stepInterval); } playStep() { Object.keys(this.steps).forEach(instrument => { if (this.steps[instrument][this.currentStep]) { this.playSound(instrument); } }); } playSound(instrument) { const data = this.sounds[instrument]; if (data.oscType === 'noise') { // Create noise buffer source const bufferSize = this.audioContext.sampleRate * 0.1; const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate); const output = buffer.getChannelData(0); // Generate white noise for (let i = 0; i < bufferSize; i++) { output[i] = Math.random() * 2 - 1; } const source = this.audioContext.createBufferSource(); source.buffer = buffer; // Create filter and gain based on instrument const filter = this.audioContext.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.value = data.frequencies[0] || 2000; const gain = this.audioContext.createGain(); gain.gain.value = (instrument === 'hat') ? 0.5 : 1.0; gain.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + 0.1); // Connect nodes: source → filter → gain → destination source.connect(filter); filter.connect(gain); gain.connect(this.audioContext.destination); source.start(); // Cleanup source.onended = () => { source.disconnect(); filter.disconnect(); gain.disconnect(); }; } else { // Use oscillator source for synthesized tones const osc = this.audioContext.createOscillator(); const gain = this.audioContext.createGain(); osc.type = data.oscType; osc.frequency.value = data.frequencies[0]; gain.gain.value = 1.0; gain.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + 0.3); // Connect nodes: osc → gain → destination osc.connect(gain); gain.connect(this.audioContext.destination); // Frequency sweep for kick if (instrument === 'kick') { osc.frequency.exponentialRampToValueAtTime( data.frequencies[1], this.audioContext.currentTime + 0.1 ); } osc.start(); // Cleanup osc.onended = () => { osc.disconnect(); gain.disconnect(); }; } } toggleStep(instrument, index) { this.steps[instrument][index] = !this.steps[instrument][index]; this.updateStepUI(instrument, index); } updateStepUI(instrument, index) { const steps = document.querySelectorAll(`[data-instrument="${instrument}"]`); const step = steps[index]; if (this.steps[instrument][index]) { step.classList.add('active'); } else { step.classList.remove('active'); } } highlightCurrentStep() { // Remove current highlighting from all steps document.querySelectorAll('.step').forEach(step => { step.classList.remove('current'); }); // Highlight current step in all tracks Object.keys(this.steps).forEach(instrument => { const steps = document.querySelectorAll(`[data-instrument="${instrument}"]`); steps[this.currentStep].classList.add('current'); }); } updateBPM() { this.bpm = parseInt(this.bpmSlider.value); this.bpmDisplay.textContent = `${this.bpm} BPM`; // Restart sequencer with new BPM if playing if (this.isPlaying) { this.stop(); this.play(); } } clearAll() { Object.keys(this.steps).forEach(instrument => { this.steps[instrument] = new Array(16).fill(false); // Update UI document.querySelectorAll(`[data-instrument="${instrument}"]`).forEach(step => { step.classList.remove('active'); }); }); } } // Initialize drum machine when page loads document.addEventListener('DOMContentLoaded', () => { new DrumMachine(); }); </script> </body> </html> This drum machine features: 1. A 4x16 grid where each row represents a drum sound (kick, snare, hi-hat, clap) 2. Clickable grid steps to activate sounds 3. Play/stop button with highlighting of current playing step 4. BPM control slider (60-240 BPM) 5. "Clear All" button to reset the pattern 6. Web Audio API-based drum synthesis: - Kick: Sine wave with pitch envelope - Snare: Filtered noise burst - Hi-hat: High-frequency noise burst - Clap: Filtered noise burst with specific frequency characteristics The entire application is self-contained in a single HTML file with no external dependencies. All sounds are synthesized using the Web Audio API without any audio samples. To use: 1. Click steps to activate/deactivate drum sounds in the sequence 2. Adjust the BPM slider to control playback speed 3. Click Play to start the sequencer 4. Click Stop to pause playback 5. Use "Clear All" to reset all steps The UI color-codes each drum row and highlights the currently playing step with a yellow border.
TAKE IT TO CODE
This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.