<
<
<
<Web Audio Drum Machine
<
:root {
--bg-color: #1a1a1a;
--card-color: #2d2d2d;
--text-color: #e0e0e0;
--accent-color: #00adb5;
--kick-color: #ff4d4d;
--snare-color: #ffaf40;
--hihat-color: #fffa65;
--clap-color: #32ff7e;
--step-off: #444;
--step-on: #fff;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.container {
background-color: var(--card-color);
padding: 2rem;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
width: 90%;
max-width: 800px;
}
h1 {
text-align: center;
margin-top: 0;
color: var(--accent-color);
}
.controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
.btn {
padding: 0.8rem 1.5rem;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
border: none;
border-radius: 4px;
transition: opacity 0.2s, transform 0.1s;
user-select: none;
}
.btn:active {
transform: scale(0.95);
}
#play-stop-btn {
background-color: var(--accent-color);
color: white;
min-width: 100px;
}
#play-stop-btn.playing {
background-color: #ff4d4d;
}
.bpm-control {
display: flex;
align-items: center;
gap: 10px;
}
input[type="range"] {
cursor: pointer;
}
.grid {
display: grid;
grid-template-columns: 100px repeat(16, 1fr);
gap: 5px;
align-items: center;
}
.track-label {
font-weight: bold;
font-size: 0.9rem;
}
.step {
aspect-ratio: 1/1;
background-color: var(--step-off);
border-radius: 3px;
cursor: pointer;
transition: background-color 0.1s;
}
.step.active {
background-color: var(--step-on);
box-shadow: 0 0 8px var(--step-on);
}
.step.playing-now {
outline: 2px solid white;
outline-offset: 2px;
}
/* Color coding for active steps by instrument type */
.step[data-instr="kick"].active { background-color: var(--kick-color); box-shadow: 0 0 8px var(--kick-color); }
.step[data-instr="snare"].active { background-color: var(--snare-color); box-shadow: 0 0 8px var(--snare-color); }
.step[data-instr="hihat"].active { background-color: var(--hihat-color); box-shadow: 0 0 8px var(--hihat-color); }
.step[data-instr="clap"].active { background-color: var(--clap-color); box-shadow: 0 0 8px var(--clap-color); }
@media (max-width: 600px) {
.grid {
grid-template-columns: 60px repeat(16, 1fr);
}
.track-label {
font-size: 0.7rem;
}
}
<
SynthDrum 16
<
<PLAY
<
<BPM: <120
<
<
<
// Audio Context Setup
let audioCtx;
let isPlaying = false;
let currentStep = 0;
let nextStepTime = 0;
let bpm = 120;
let timerID;
const instruments = ['kick', 'snare', 'hihat', 'clap'];
const stepsCount = 16;
const sequence = instruments.map(() => Array(stepsCount).fill(false));
// DOM Elements
const gridContainer = document.getElementById('drum-grid');
const playStopBtn = document.getElementById('play-stop-btn');
const bpmSlider = document.getElementById('bpm-slider');
const bpmValDisplay = document.getElementById('bpm-val');
// Initialize Grid
function createGrid() {
gridContainer.innerHTML = '';
instruments.forEach((instr, instrIdx) => {
const label = document.createElement('div');
label.className = 'track-label';
label.textContent = instr.toUpperCase();
gridContainer.appendChild(label);
for (let s = 0; s << steps stepsCount; s++) {
const step = document.createElement('div');
step.className = 'step';
step.dataset.instr = instr;
step.dataset.step = s;
step.addEventListener('click', () => toggleStep(instrIdx, s, step));
gridContainer.appendChild(step);
}
});
}
function toggleStep(instrIdx, stepIdx, element) {
sequence[instrIdx][stepIdx] = !sequence[instrIdx][stepIdx];
element.classList.toggle('active');
}
// --- SYNTHESIZERS ---
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
}
function playKick(time) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.frequency.setValueAtTime(150, time);
osc.frequency.exponentialRampToValueAtTime(0.01, time + 0.5);
gain.gain.setValueAtTime(1, time);
gain.gain.exponentialRampToValueAtTime(0.01, time + 0.5);
osc.start(time);
osc.stop(time + 0.5);
}
function playSnare(time) {
// Noise component
const bufferSize = audioCtx.sampleRate * 0.2;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i << buffer bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const noiseFilter = audioCtx.createBiquadFilter();
noiseFilter.type = 'highpass';
noiseFilter.frequency.setValueAtTime(1000, time);
const noiseGain = audioCtx.createGain();
noiseGain.gain.setValueAtTime(0.5, time);
noiseGain.gain.exponentialRampToValueAtTime(0.01, time + 0.2);
noise.connect(noiseFilter);
noiseFilter.connect(noiseGain);
noiseGain.connect(audioCtx.destination);
// Tone component (the "snap")
const osc = audioCtx.createOscillator();
const oscGain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(250, time);
oscGain.gain.setValueAtTime(0.5, time);
oscGain.gain.exponentialRampToValueAtTime(0.01, time + 0.1);
osc.connect(oscGain);
oscGain.connect(audioCtx.destination);
noise.start(time);
osc.start(time);
noise.stop(time + 0.2);
osc.stop(time + 0.2);
}
function playHiHat(time) {
const bufferSize = audioCtx.sampleRate * 0.05;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i << buffer bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const filter = audioCtx.createBiquadFilter();
filter.type = 'highpass';
filter.frequency.setValueAtTime(7000, time);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.3, time);
gain.gain.exponentialRampToValueAtTime(0.01, time + 0.05);
noise.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
noise.start(time);
noise.stop(time + 0.05);
}
function playClap(time) {
// Clap is basically a series of fast noise bursts
const numBursts = 6;
const burstDuration = 0.02;
for (let i = 0; i << num numBursts; i++) {
const burstTime = time + (i * 0.01);
const bufferSize = audioCtx.sampleRate * burstDuration;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let j = 0; j << buffer bufferSize; j++) {
data[j] = Math.random() * 2 - 1;
}
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const filter = audioCtx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(1200, burstTime);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.4, burstTime);
gain.gain.exponentialRampToValueAtTime(0.01, burstTime + burstDuration);
noise.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
noise.start(burstTime);
noise.stop(burstTime + burstDuration);
}
}
// --- SCHEDULER ---
function scheduler() {
while (nextStepTime << audio audioCtx.currentTime + 0.1) {
scheduleStep(currentStep, nextStepTime);
advanceStep();
}
timerID = requestAnimationFrame(scheduler);
}
function advanceStep() {
const secondsPerBeat = 60.0 / bpm / 4; // 16th notes
nextStepTime += secondsPerBeat;
currentStep = (currentStep + 1) % stepsCount;
}
function scheduleStep(step, time) {
// Visual update
// We use setTimeout for visual because the scheduler works on audio time
setTimeout(() => {
updateVisuals(step);
}, (time - audioCtx.currentTime) * 1000);
// Audio trigger
if (sequence[0][step]) playKick(time);
if (sequence[1][step]) playSnare(time);
if (sequence[2][step]) playHiHat(time);
if (sequence[3][step]) playClap(time);
}
function updateVisuals(step) {
// Remove highlighting from all steps
const allSteps = document.querySelectorAll('.step');
allSteps.forEach(s => s.classList.remove('playing-now'));
// Add highlighting to the current step column
const currentColumnSteps = document.querySelectorAll(`.step[data-step="${step}"]`);
currentColumnSteps.forEach(s => s.classList.add('playing-now'));
}
// --- EVENT HANDLERS ---
playStopBtn.addEventListener('click', () => {
initAudio();
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
isPlaying = !isPlaying;
if (isPlaying) {
currentStep = 0;
nextStepTime = audioCtx.currentTime;
playStopBtn.textContent = 'STOP';
playStopBtn.classList.add('playing');
scheduler();
} else {
playStopBtn.textContent = 'PLAY';
playStopBtn.classList.remove('playing');
cancelAnimationFrame(timerID);
// Remove all visual highlights
document.querySelectorAll('.step').forEach(s => s.classList.remove('playing-now'));
}
});
bpmSlider.addEventListener('input', (e) => {
bpm = e.target.value;
bpmValDisplay.textContent = bpm;
});
// Initialization
createGrid();