Drum machine · deepseek-r1
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 create a single HTML file (index.html) that contains:
- HTML structure: a grid for the drum machine (16 steps for each instrument: kick, snare, hi-hat, clap)
- CSS: styling for the grid, buttons, and sliders
- JavaScript:
* Web Audio API for synthesizing the drum sounds
* A grid of buttons (steps) for each instrument
* Play/stop button and BPM control
* Logic to play the sequence
Steps:
1. We'll create a grid with 4 rows (one per instrument) and 16 columns (steps).
2. Each step is a button that toggles on/off (active/inactive).
3. We'll have a play button to start/stop the sequence and a slider for BPM.
4. We'll use the Web Audio API to generate the sounds:
- Kick: a low-frequency oscillator with a quick decay.
- Snare: a combination of noise and a tone.
- Hi-hat: a short burst of noise.
- Clap: a burst of noise with a slight delay.
We'll write the entire app in one file: index.html.
Let's break down:
HTML:
- A container for the grid (div with class "grid")
- For each instrument, a row of 16 buttons (each button represents a step)
- A play button and a stop button (or one toggle button)
- A slider for BPM and a label to show the current BPM.
CSS:
- Style the grid as a table with rows and columns.
- Style the active steps (e.g., background color).
JavaScript:
- Define the audio context and the synthesizer functions for each drum sound.
- Create an array for each instrument to store which steps are active (16 steps).
- When play is pressed, start a loop that:
* Goes through each step (from 0 to 15) at intervals based on BPM.
* For each step, check which instruments are active and play the corresponding sound.
- The loop runs until stop is pressed.
Important: We must use no external resources. All sounds are synthesized.
Implementation details for sounds:
Kick:
- Create an oscillator with a sine wave at a low frequency (e.g., 60Hz) and an envelope for the frequency and gain.
Snare:
- Two parts: a sine wave for the body (around 200Hz) and a noise burst.
Hi-hat:
- A very short burst of noise (high-pass filtered).
Clap:
- A burst of noise with multiple peaks (using multiple oscillators or a delay).
However, note that we are in a single HTML file and we want simplicity.
We'll create functions for each sound:
function playKick() { ... }
function playSnare() { ... }
function playHiHat() { ... }
function playClap() { ... }
We'll use the Web Audio API nodes: AudioContext, OscillatorNode, GainNode, BufferSource (for noise).
Since we cannot load external samples, we'll generate the noise on the fly.
For noise, we can create an AudioBuffer and fill it with random values.
Steps for noise:
function createNoiseBuffer() {
const buffer = audioContext.createBuffer(1, audioContext.sampleRate * duration, audioContext.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) {
data[i] = Math.random() * 2 - 1;
}
return buffer;
}
But note: we don't want to create the buffer every time we play a sound because that might be heavy. We can create one buffer per sound type and reuse.
However, for simplicity and because the sounds are very short, we'll create the buffer each time? Or we can pre-create buffers for the sounds at the start.
Alternatively, we can use a different method for the hi-hat and clap: use an oscillator with a high frequency and a very short decay? But hi-hat is noise.
Let me outline the functions:
Kick:
- Create oscillator (type: sine) at 60Hz, connect to gain node.
- Set frequency to drop quickly (from 60Hz to 0? or just use a short decay envelope on gain).
- Schedule the gain envelope: set gain to 1 at start, then exponential decay to 0 in 0.3 seconds.
Snare:
- Two parts:
Part 1: sine wave at 200Hz with a quick decay (like the kick but higher frequency).
Part 2: noise burst (using a buffer source) with a bandpass filter around 1000Hz and a quick decay.
Hi-hat:
- Noise burst with a high-pass filter (cutoff around 5000Hz) and very short decay (0.1 seconds).
Clap:
- Noise burst with a series of peaks? Actually, we can simulate by having multiple noise bursts with slight delays?
- Alternatively, use a single noise burst with a more complex envelope (multiple peaks) or a comb filter?
- For simplicity, we'll do a noise burst with a bandpass filter and a decay that has a small bump?
- Or just a noise burst with a longer decay?
Given time constraints, we'll keep the clap similar to the snare but with higher frequency and shorter decay.
We'll create the audio context when the user interacts (to avoid autoplay restrictions). We'll do it on the first button click or when the play button is pressed.
Implementation:
We'll have:
let audioContext = null;
Then in the play function, if audioContext is null, create it.
For each sound, we'll create the necessary nodes on the fly? Or we can pre-create the noise buffers?
Pre-creation: We can create the noise buffers when the audio context is created.
We'll create:
kickBuffer: not needed, we'll use oscillator.
snareNoiseBuffer, hihatBuffer, clapBuffer?
Alternatively, we can generate the noise buffers once at the start (when the page loads) but then we must create the audio context on page load? That might be blocked. So we'll create the buffers when the audio context is created (on first play).
Steps:
function setupAudio() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Create noise buffers for snare, hihat, clap? Actually, we can create one noise buffer and use it for all? But different durations and filters.
}
}
Then for each sound, we'll generate the sound using the current time.
We'll design each sound as a function that schedules the sound at a given time.
Example for kick:
function playKick(time) {
const osc = audioContext.createOscillator();
const gainNode = audioContext.createGain();
osc.connect(gainNode);
gainNode.connect(audioContext.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(60, time);
osc.frequency.exponentialRampToValueAtTime(0.01, time + 0.3); // avoid 0
gainNode.gain.setValueAtTime(1, time);
gainNode.gain.exponentialRampToValueAtTime(0.001, time + 0.3); // avoid 0
osc.start(time);
osc.stop(time + 0.3);
}
Similarly for others.
But note: exponential ramps require the value to be not zero. We use 0.001 as a very small value.
For snare:
function playSnare(time) {
// Part 1: sine tone
const osc = audioContext.createOscillator();
const gainNode = audioContext.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(200, time);
gainNode.gain.setValueAtTime(0.7, time);
gainNode.gain.exponentialRampToValueAtTime(0.001, time + 0.2);
osc.connect(gainNode);
gainNode.connect(audioContext.destination);
osc.start(time);
osc.stop(time + 0.2);
// Part 2: noise
const noiseBuffer = createNoiseBuffer(0.2); // duration 0.2 seconds
const noise = audioContext.createBufferSource();
noise.buffer = noiseBuffer;
const noiseGain = audioContext.createGain();
noise.connect(noiseGain);
noiseGain.connect(audioContext.destination);
// Apply a bandpass filter
const filter = audioContext.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 1000;
noise.connect(filter);
filter.connect(noiseGain);
noiseGain.gain.setValueAtTime(0.5, time);
noiseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2);
noise.start(time);
}
We'll need a helper function createNoiseBuffer(duration) that returns a buffer.
Similarly for hihat and clap.
Hi-hat: very short noise burst (0.05 seconds) with high-pass filter.
Clap: noise burst with a slightly longer duration and maybe a bandpass? We'll do similar to snare but without the sine part.
However, note: creating a buffer for every snare, hihat, clap on every step might be heavy? We should pre-create the buffers when the audio context is created.
We'll create:
let hihatBuffer, snareNoiseBuffer, clapBuffer;
Then in setupAudio(), if the buffers are not created, create them.
Alternatively, we can create the buffer once and then reuse it by creating a BufferSource each time? That is acceptable.
But note: the duration of the noise might be different per sound. So we'll create one buffer per sound type.
We'll do:
function createNoiseBuffer(duration) {
const buffer = audioContext.createBuffer(1, audioContext.sampleRate * duration, audioContext.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) {
data[i] = Math.random() * 2 - 1;
}
return buffer;
}
Then in setupAudio, we can pre-create:
if (!hihatBuffer) {
hihatBuffer = createNoiseBuffer(0.05);
}
if (!snareNoiseBuffer) {
snareNoiseBuffer = createNoiseBuffer(0.2);
}
if (!clapBuffer) {
clapBuffer = createNoiseBuffer(0.2);
}
Then in playHiHat, we use hihatBuffer.
But note: we might want to change the sound by filtering? So we'll still need to connect to a filter and gain.
So:
function playHiHat(time) {
const noise = audioContext.createBufferSource();
noise.buffer = hihatBuffer;
const gainNode = audioContext.createGain();
noise.connect(gainNode);
gainNode.connect(audioContext.destination);
// Apply high-pass filter
const filter = audioContext.createBiquadFilter();
filter.type = 'highpass';
filter.frequency.value = 5000;
noise.connect(filter);
filter.connect(gainNode);
gainNode.gain.setValueAtTime(0.5, time);
gainNode.gain.exponentialRampToValueAtTime(0.001, time + 0.05);
noise.start(time);
}
Similarly for clap.
Clap: we might want to simulate the multiple clap effect by having multiple noise bursts? But for simplicity, we'll do one burst with a bandpass and a longer decay.
Alternatively, we can use a different envelope? Let's do:
function playClap(time) {
const noise = audioContext.createBufferSource();
noise.buffer = clapBuffer;
const gainNode = audioContext.createGain();
noise.connect(gainNode);
gainNode.connect(audioContext.destination);
// Apply bandpass filter
const filter = audioContext.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 1200;
noise.connect(filter);
filter.connect(gainNode);
// Envelope with two peaks? We'll do a simple decay
gainNode.gain.setValueAtTime(0.5, time);
gainNode.gain.exponentialRampToValueAtTime(0.001, time + 0.3);
noise.start(time);
}
But to simulate the multiple claps, we can schedule two claps? Or we can create a more complex envelope?
We'll do a simple one for now.
Now, the grid:
We'll have an array of instruments:
const instruments = ['kick', 'snare', 'hihat', 'clap'];
And for each instrument, an array of 16 booleans (whether the step is active).
We'll initialize:
let steps = {
kick: Array(16).fill(false),
snare: Array(16).fill(false),
hihat: Array(16).fill(false),
clap: Array(16).fill(false)
};
The grid buttons will toggle the corresponding step.
We'll also have:
let currentStep = 0;
let isPlaying = false;
let bpm = 120;
let timerId = null;
The play function:
function play() {
if (isPlaying) {
return;
}
setupAudio(); // ensure audio context is created
isPlaying = true;
currentStep = 0;
const stepDuration = 60 / bpm / 4; // each step is a 16th note? Actually, we are doing 16 steps per bar? Then stepDuration = (60 / bpm) * (1/4) because 4 steps per beat?
// Actually, if we have 16 steps for a bar of 4 beats, then each step is 1/4 beat -> stepDuration = (60 / bpm) * (1/4) = 15 / bpm seconds.
function scheduleStep() {
const time = audioContext.currentTime;
// Schedule sounds for the current step
if (steps.kick[currentStep]) {
playKick(time);
}
if (steps.snare[currentStep]) {
playSnare(time);
}
if (steps.hihat[currentStep]) {
playHiHat(time);
}
if (steps.clap[currentStep]) {
playClap(time);
}
currentStep = (currentStep + 1) % 16;
}
// We'll use setInterval, but note: setInterval is not precise for audio. We can use Web Audio's own timing?
// Alternatively, we can use a loop that schedules the next step using setTimeout, but that also drifts.
// We'll use setInterval and hope it's okay for this purpose.
timerId = setInterval(scheduleStep, stepDuration * 1000);
}
Stop function:
function stop() {
isPlaying = false;
clearInterval(timerId);
}
We'll also update the UI to highlight the current step.
We'll create the grid dynamically? Or we can write the HTML statically.
Since we have only 4 rows and 16 columns, we can write static HTML.
Example row for kick:
<div class="row">
<div class="instrument-label">Kick</div>
<button class="step" data-instrument="kick" data-step="0"></button>
<button class="step" data-instrument="kick" data-step="1"></button>
... 16 buttons ...
</div>
Then we add event listeners to each button to toggle the step and update the steps array.
We'll also update the button's class to show active.
We'll also update the current step by adding a class "current" to the button of the current step? But note: we have 4 buttons per step? Actually, per step index, we have one button per instrument? So we can highlight the button for the current step and the instrument that is playing? But we are playing multiple instruments at the same step.
Alternatively, we can highlight the entire column (step index) in the grid?
We'll add a class "current" to every button in the current step (for every instrument) and remove it when the step moves.
So in the scheduleStep function:
// Remove current class from all buttons of the previous step
// Then add to the current step.
But note: we are updating the UI every step, so we need to store references to the buttons.
We can do:
const stepButtons = document.querySelectorAll('.step');
function updateCurrentStepHighlight() {
stepButtons.forEach(button => {
button.classList.remove('current');
if (parseInt(button.dataset.step) === currentStep) {
button.classList.add('current');
}
});
}
Then call updateCurrentStepHighlight() at the beginning of scheduleStep.
Also, when stopping, remove the current class.
Now, the BPM slider:
<input type="range" id="bpm" min="60" max="240" value="120">
<span id="bpm-value">120</span>
Then update the bpm variable and the span when the slider changes.
We'll also stop and restart the sequencer if it's playing when BPM changes? Or we can adjust the interval?
But we are using setInterval, so we can stop and restart?
Alternatively, we can change the interval time? But setInterval doesn't allow changing the interval. So we'll stop and restart.
So:
bpmSlider.addEventListener('input', function() {
bpm = parseInt(this.value);
document.getElementById('bpm-value').textContent = bpm;
if (isPlaying) {
stop();
play();
}
});
Now, let's write the entire index.html.
We'll put everything in one file.
Structure:
<!DOCTYPE html>
<html>
<head>
<title>Drum Machine</title>
<style>
... CSS ...
</style>
</head>
<body>
<h1>Drum Machine</h1>
<div id="grid">
<!-- 4 rows -->
</div>
<div>
<button id="play">Play</button>
<button id="stop">Stop</button>
</div>
<div>
<label for="bpm">BPM:</label>
<input type="range" id="bpm" min="60" max="240" value="120">
<span id="bpm-value">120</span>
</div>
<script>
... JavaScript ...
</script>
</body>
</html>
We'll generate the grid rows and buttons in JavaScript? Or we can write them manually? Since there are only 4 rows and 16 buttons per row, we can write them manually.
But to avoid repetition, we can generate the grid with JavaScript? However, the requirement is to write everything in one file without external resources, and we are allowed to use inline JavaScript.
We'll do:
const instruments = [
{ id: 'kick', name: 'Kick' },
{ id: 'snare', name: 'Snare' },
{ id: 'hihat', name: 'Hi-Hat' },
{ id: 'clap', name: 'Clap' }
];
Then in the script, we can generate the grid? But note: the HTML must be present when the page loads.
Alternatively, we write the grid manually in HTML.
Let's write the grid manually:
<div id="grid">
<div class="row">
<div class="instrument-label">Kick</div>
<button class="step" data-instrument="kick" data-step="0"></button>
<button class="step" data-instrument="kick" data-step="1"></button>
... up to step 15 ...
</div>
... similarly for snare, hihat, clap ...
</div>
That's 4 rows * 16 buttons = 64 buttons. We can write that.
But to save space, we'll do it in the script? Actually, we are writing the entire file, so we can write the HTML statically.
However, to avoid writing 64 lines of buttons, we can generate the grid with JavaScript when the page loads?
We'll do:
window.onload = function() {
const grid = document.getElementById('grid');
instruments.forEach(instrument => {
const row = document.createElement('div');
row.className = 'row';
const label = document.createElement('div');
label.className = 'instrument-label';
label.textContent = instrument.name;
row.appendChild(label);
for (let i = 0; i < 16; i++) {
const button = document.createElement('button');
button.className = 'step';
button.dataset.instrument = instrument.id;
button.dataset.step = i;
row.appendChild(button);
}
grid.appendChild(row);
});
};
Then we don't have to write all the buttons manually.
But note: we must define the instruments array in the script.
We'll do that.
Now, let's write the entire index.html.
We'll write:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Drum Machine</title>
<style>
body { font-family: Arial, sans-serif; }
#grid { margin-bottom: 20px; }
.row { display: flex; margin-bottom: 5px; align-items: center; }
.instrument-label { width: 80px; }
.step { width: 30px; height: 30px; margin: 0 2px; border: 1px solid #ccc; background: #fff; cursor: pointer; }
.step.active { background: #4CAF50; }
.step.current { border: 2px solid #FF5722; }
</style>
</head>
<body>
<h1>Drum Machine</h1>
<div id="grid"></div>
<div>
<button id="play">Play</button>
<button id="stop">Stop</button>
</div>
<div>
<label for="bpm">BPM:</label>
<input type="range" id="bpm" min="60" max="240" value="120">
<span id="bpm-value">120</span>
</div>
<script>
// Define instruments
const instruments = [
{ id: 'kick', name: 'Kick' },
{ id: 'snare', name: 'Snare' },
{ id: 'hihat', name: 'Hi-Hat' },
{ id: 'clap', name: 'Clap' }
];
// State
let steps = {
kick: Array(16).fill(false),
snare: Array(16).fill(false),
hihat: Array(16).fill(false),
clap: Array(16).fill(false)
};
let currentStep = 0;
let isPlaying = false;
let bpm = 120;
let timerId = null;
let audioContext = null;
let hihatBuffer = null;
let snareNoiseBuffer = null;
let clapBuffer = null;
// DOM elements
const grid = document.getElementById('grid');
const playButton = document.getElementById('play');
const stopButton = document.getElementById('stop');
const bpmSlider = document.getElementById('bpm');
const bpmValue = document.getElementById('bpm-value');
// Initialize grid
instruments.forEach(instrument => {
const row = document.createElement('div');
row.className = 'row';
const label = document.createElement('div');
label.className = 'instrument-label';
label.textContent = instrument.name;
row.appendChild(label);
for (let i = 0; i < 16; i++) {
const button = document.createElement('button');
button.className = 'step';
button.dataset.instrument = instrument.id;
button.dataset.step = i;
button.addEventListener('click', function() {
const inst = this.dataset.instrument;
const stepIndex = parseInt(this.dataset.step);
steps[inst][stepIndex] = !steps[inst][stepIndex];
this.classList.toggle('active');
});
row.appendChild(button);
}
grid.appendChild(row);
});
// Play button
playButton.addEventListener('click', play);
stopButton.addEventListener('click', stop);
// BPM slider
bpmSlider.addEventListener('input', function() {
bpm = parseInt(this.value);
bpmValue.textContent = bpm;
if (isPlaying) {
stop();
play();
}
});
function setupAudio() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Create noise buffers
hihatBuffer = createNoiseBuffer(0.05);
snareNoiseBuffer = createNoiseBuffer(0.2);
clapBuffer = createNoiseBuffer(0.2);
}
}
function createNoiseBuffer(duration) {
const buffer = audioContext.createBuffer(1, audioContext.sampleRate * duration, audioContext.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) {
data[i] = Math.random() * 2 - 1;
}
return buffer;
}
function playKick(time) {
const osc = audioContext.createOscillator();
const gainNode = audioContext.createGain();
osc.connect(gainNode);
gainNode.connect(audioContext.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(60, time);
osc.frequency.exponentialRampToValueAtTime(0.01, time + 0.3);
gainNode.gain.setValueAtTime(1, time);
gainNode.gain.exponentialRampToValueAtTime(0.001, time + 0.3);
osc.start(time);
osc.stop(time + 0.3);
}
function playSnare(time) {
// Sine part
const osc = audioContext.createOscillator();
const gainNodeSine = audioContext.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(200, time);
gainNodeSine.gain.setValueAtTime(0.7, time);
gainNodeSine.gain.exponentialRampToValueAtTime(0.001, time + 0.2);
osc.connect(gainNodeSine);
gainNodeSine.connect(audioContext.destination);
osc.start(time);
osc.stop(time + 0.2);
// Noise part
const noise = audioContext.createBufferSource();
noise.buffer = snareNoiseBuffer;
const gainNodeNoise = audioContext.createGain();
noise.connect(gainNodeNoise);
// Filter for noise
const filter = audioContext.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 1000;
noise.connect(filter);
filter.connect(gainNodeNoise);
gainNodeNoise.connect(audioContext.destination);
gainNodeNoise.gain.setValueAtTime(0.5, time);
gainNodeNoise.gain.exponentialRampToValueAtTime(0.001, time + 0.2);
noise.start(time);
}
function playHiHat(time) {
const noise = audioContext.createBufferSource();
noise.buffer = hihatBuffer;
const gainNode = audioContext.createGain();
noise.connect(gainNode);
// High-pass filter
const filter = audioContext.createBiquadFilter();
filter.type = 'highpass';
filter.frequency.value = 5000;
noise.connect(filter);
filter.connect(gainNode);
gainNode.connect(audioContext.destination);
gainNode.gain.setValueAtTime(0.5, time);
gainNode.gain.exponentialRampToValueAtTime(0.001, time + 0.05);
noise.start(time);
}
function playClap(time) {
const noise = audioContext.createBufferSource();
noise.buffer = clapBuffer;
const gainNode = audioContext.createGain();
noise.connect(gainNode);
// Bandpass filter
const filter = audioContext.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 1200;
noise.connect(filter);
filter.connect(gainNode);
gainNode.connect(audioContext.destination);
gainNode.gain.setValueAtTime(0.5, time);
gainNode.gain.exponentialRampToValueAtTime(0.001, time + 0.3);
noise.start(time);
}
function play() {
if (isPlaying) {
return;
}
setupAudio();
isPlaying = true;
currentStep = 0;
const stepDuration = 15 / bpm; // because 60/4 = 15, so 15/bpm seconds per step
// Update the play button state? We can change text to "Playing"?
// Highlight the current step
updateCurrentStepHighlight();
timerId = setInterval(() => {
// Remove current class from all buttons
document.querySelectorAll('.step').forEach(button => {
button.classList.remove('current');
});
// Play sounds for the current step
const time = audioContext.currentTime;
if (steps.kick[currentStep]) {
playKick(time);
}
if (steps.snare[currentStep]) {
playSnare(time);
}
if (steps.hihat[currentStep]) {
playHiHat(time);
}
if (steps.clap[currentStep]) {
playClap(time);
}
// Highlight the current step buttons
document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(button => {
button.classList.add('current');
});
currentStep = (currentStep + 1) % 16;
}, stepDuration * 1000);
}
function stop() {
isPlaying = false;
clearInterval(timerId);
// Remove current class from all buttons
document.querySelectorAll('.step').forEach(button => {
button.classList.remove('current');
});
}
function updateCurrentStepHighlight() {
// We'll do it in the interval
}
</script>
</body>
</html>
We'll write this entire content to index.html.
Note: We are using exponentialRampToValueAtTime and we set a very small value (0.001) to avoid errors.
Let's write the file.
I'll create a self-contained drum machine web app with synthesized sounds. Here's the complete implementation: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Audio Drum Machine</title> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #1a2a6c, #2c3e50); color: #ecf0f1; display: flex; flex-direction: column; align-items: center; min-height: 100vh; margin: 0; padding: 20px; } h1 { margin-bottom: 10px; text-shadow: 0 2px 4px rgba(0,0,0,0.3); } .subtitle { margin-bottom: 30px; opacity: 0.8; } .controls { display: flex; gap: 20px; margin-bottom: 30px; background: rgba(0, 0, 0, 0.2); padding: 15px; border-radius: 10px; width: 100%; max-width: 800px; justify-content: center; } .control-group { display: flex; flex-direction: column; align-items: center; } button { background: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 16px; font-weight: bold; transition: all 0.2s; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } button:hover { background: #2980b9; transform: translateY(-2px); box-shadow: 0 6px 8px rgba(0,0,0,0.15); } button:active { transform: translateY(0); } .bpm-control { display: flex; align-items: center; gap: 10px; } input[type="range"] { width: 150px; cursor: pointer; } .bpm-value { font-weight: bold; font-size: 18px; min-width: 40px; text-align: center; } .drum-machine { display: flex; flex-direction: column; gap: 15px; background: rgba(0, 0, 0, 0.3); padding: 20px; border-radius: 15px; width: 100%; max-width: 800px; box-shadow: 0 10px 20px rgba(0,0,0,0.2); } .instrument-row { display: flex; align-items: center; gap: 15px; } .instrument-name { width: 80px; font-weight: bold; text-align: right; } .steps { display: flex; gap: 5px; flex-wrap: wrap; flex-grow: 1; } .step { width: 40px; height: 40px; border-radius: 5px; background: rgba(255, 255, 255, 0.1); border: 2px solid rgba(255, 255, 255, 0.2); cursor: pointer; transition: all 0.1s; } .step.active { background: #e74c3c; border-color: #c0392b; box-shadow: 0 0 10px rgba(231, 76, 60, 0.5); } .step.current { box-shadow: 0 0 15px rgba(46, 204, 113, 0.8); transform: scale(1.1); } .step.active.current { background: #2ecc71; border-color: #27ae60; box-shadow: 0 0 15px rgba(46, 204, 113, 0.8); } .status { margin-top: 20px; font-size: 18px; font-weight: bold; height: 25px; } .pulse { animation: pulse 0.5s; } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } </style> </head> <body> <h1>Web Audio Drum Machine</h1> <div class="subtitle">16-step synthesizer with Kick, Snare, Hi-Hat & Clap</div> <div class="controls"> <div class="control-group"> <button id="playButton">▶ Play</button> <button id="stopButton">⏹ Stop</button> </div> <div class="control-group"> <div class="bpm-control"> <span>BPM:</span> <input type="range" id="bpmSlider" min="60" max="240" value="120"> <span id="bpmValue" class="bpm-value">120</span> </div> </div> </div> <div class="drum-machine"> <div class="instrument-row"> <div class="instrument-name">Kick:</div> <div class="steps" id="kickSteps"></div> </div> <div class="instrument-row"> <div class="instrument-name">Snare:</div> <div class="steps" id="snareSteps"></div> </div> <div class="instrument-row"> <div class="instrument-name">Hi-Hat:</div> <div class="steps" id="hihatSteps"></div> </div> <div class="instrument-row"> <div class="instrument-name">Clap:</div> <div class="steps" id="clapSteps"></div> </div> </div> <div id="status" class="status"></div> <script> // Initialize Web Audio API const AudioContext = window.AudioContext || window.webkitAudioContext; const audioCtx = new AudioContext(); // DOM elements const playButton = document.getElementById('playButton'); const stopButton = document.getElementById('stopButton'); const bpmSlider = document.getElementById('bpmSlider'); const bpmValue = document.getElementById('bpmValue'); const status = document.getElementById('status'); // Drum machine state let isPlaying = false; let bpm = 120; let currentStep = 0; let stepInterval; const steps = 16; // Sound patterns const patterns = { kick: Array(steps).fill(false), snare: Array(steps).fill(false), hihat: Array(steps).fill(false), clap: Array(steps).fill(false) }; // Initialize step buttons function initSteps() { const instruments = ['kick', 'snare', 'hihat', 'clap']; instruments.forEach(instrument => { const container = document.getElementById(`${instrument}Steps`); for (let i = 0; i < steps; i++) { const step = document.createElement('div'); step.className = 'step'; step.dataset.instrument = instrument; step.dataset.index = i; step.addEventListener('click', () => { const index = parseInt(step.dataset.index); patterns[instrument][index] = !patterns[instrument][index]; step.classList.toggle('active'); // Preview sound when clicking playSound(instrument); }); container.appendChild(step); } }); } // Drum sound synthesis function playSound(type) { switch(type) { case 'kick': // Kick drum: low frequency oscillator with quick decay const kickOsc = audioCtx.createOscillator(); const kickGain = audioCtx.createGain(); kickOsc.type = 'sine'; kickOsc.frequency.setValueAtTime(150, audioCtx.currentTime); kickOsc.frequency.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5); kickGain.gain.setValueAtTime(1, audioCtx.currentTime); kickGain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5); kickOsc.connect(kickGain); kickGain.connect(audioCtx.destination); kickOsc.start(); kickOsc.stop(audioCtx.currentTime + 0.5); break; case 'snare': // Snare: noise burst with high-pass filter const snareOsc = audioCtx.createOscillator(); const snareGain = audioCtx.createGain(); const snareFilter = audioCtx.createBiquadFilter(); snareOsc.type = 'white noise'; snareFilter.type = 'highpass'; snareFilter.frequency.value = 1000; snareGain.gain.setValueAtTime(0.7, audioCtx.currentTime); snareGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3); snareOsc.connect(snareFilter); snareFilter.connect(snareGain); snareGain.connect(audioCtx.destination); snareOsc.start(); snareOsc.stop(audioCtx.currentTime + 0.3); break; case 'hihat': // Hi-hat: short burst of filtered noise const hatOsc = audioCtx.createOscillator(); const hatGain = audioCtx.createGain(); const hatFilter = audioCtx.createBiquadFilter(); hatOsc.type = 'white noise'; hatFilter.type = 'highpass'; hatFilter.frequency.value = 5000; hatGain.gain.setValueAtTime(0.5, audioCtx.currentTime); hatGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1); hatOsc.connect(hatFilter); hatFilter.connect(hatGain); hatGain.connect(audioCtx.destination); hatOsc.start(); hatOsc.stop(audioCtx.currentTime + 0.1); break; case 'clap': // Clap: noise with delayed peaks const clapOsc = audioCtx.createOscillator(); const clapGain = audioCtx.createGain(); const clapFilter = audioCtx.createBiquadFilter(); clapOsc.type = 'white noise'; clapFilter.type = 'bandpass'; clapFilter.frequency.value = 1200; clapFilter.Q.value = 1.0; clapGain.gain.setValueAtTime(0.5, audioCtx.currentTime); clapGain.gain.setValueAtTime(0.6, audioCtx.currentTime + 0.05); clapGain.gain.setValueAtTime(0.4, audioCtx.currentTime + 0.1); clapGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3); clapOsc.connect(clapFilter); clapFilter.connect(clapGain); clapGain.connect(audioCtx.destination); clapOsc.start(); clapOsc.stop(audioCtx.currentTime + 0.3); break; } } // Sequencer functions function playSequence() { if (!isPlaying) return; // Highlight current step document.querySelectorAll('.step').forEach(step => { step.classList.remove('current'); const index = parseInt(step.dataset.index); if (index === currentStep) { step.classList.add('current'); // Add pulse animation to active steps if (step.classList.contains('active')) { step.classList.add('pulse'); setTimeout(() => step.classList.remove('pulse'), 500); } } }); // Play sounds for current step ['kick', 'snare', 'hihat', 'clap'].forEach(instrument => { if (patterns[instrument][currentStep]) { playSound(instrument); } }); // Move to next step currentStep = (currentStep + 1) % steps; // Update status status.textContent = `Playing: Step ${currentStep + 1}/${steps}`; } function startSequencer() { if (isPlaying) return; isPlaying = true; playButton.textContent = '⏸ Pause'; status.textContent = `Playing: Step ${currentStep + 1}/${steps}`; const interval = (60 / bpm) * 1000 / 4; // 16th notes stepInterval = setInterval(playSequence, interval); } function stopSequencer() { isPlaying = false; clearInterval(stepInterval); playButton.textContent = '▶ Play'; status.textContent = 'Stopped'; // Remove current step highlights document.querySelectorAll('.step').forEach(step => { step.classList.remove('current'); }); } // Event listeners playButton.addEventListener('click', () => { if (isPlaying) { stopSequencer(); } else { startSequencer(); } }); stopButton.addEventListener('click', () => { stopSequencer(); currentStep = 0; status.textContent = 'Stopped'; }); bpmSlider.addEventListener('input', () => { bpm = parseInt(bpmSlider.value); bpmValue.textContent = bpm; if (isPlaying) { stopSequencer(); startSequencer(); } }); // Initialize the drum machine initSteps(); // Add a little demo pattern setTimeout(() => { // Kick pattern patterns.kick[0] = true; patterns.kick[4] = true; patterns.kick[8] = true; patterns.kick[12] = true; // Snare pattern patterns.snare[4] = true; patterns.snare[12] = true; // Hi-hat pattern for (let i = 0; i < steps; i += 2) { patterns.hihat[i] = true; } // Clap pattern patterns.clap[7] = true; patterns.clap[15] = true; // Update UI to show active steps document.querySelectorAll('.step').forEach(step => { const instrument = step.dataset.instrument; const index = parseInt(step.dataset.index); if (patterns[instrument][index]) { step.classList.add('active'); } }); }, 500); </script> </body> </html> ``` This drum machine includes: 1. **Grid Interface**: - 4 rows for Kick, Snare, Hi-Hat, and Clap - 16-step clickable grid for each instrument - Visual feedback for active steps and current position 2. **Controls**: - Play/Pause button - Stop button - BPM slider (60-240 BPM) 3. **Sound Synthesis**: - Kick: Low-frequency oscillator with quick decay - Snare: Filtered noise burst - Hi-Hat: Short high-frequency noise - Clap: Noise with delayed peaks 4. **Visual Feedback**: - Active steps highlighted in red - Current step highlighted with green pulse - Status display showing current step - Animation for active steps during playback The entire application is self-contained in a single HTML file with no external dependencies. All sounds are synthesized using the Web Audio API and run completely in the browser sandbox without network access.
TAKE IT TO CODE
This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.