Music
Virtual Drum Kit
Play drums with keyboard shortcuts or tap the pads. 16-step sequencer + 15 practice patterns.
—
BPM
100
Keyboard shortcuts:
J = Kick · K = Snare · L = Hi-Hat · ; = Open Hat ·
A = Crash · S = Ride · D = High Tom · F = Mid Tom · G = Floor Tom
|
Space = Play/Stop sequencer
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// ── Audio ──────────────────────────────────────────────────────────
let audioCtx = null;
function getCtx() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
return audioCtx;
}
function noise(ctx, dur) {
const n = Math.ceil(ctx.sampleRate * dur);
const buf = ctx.createBuffer(1, n, ctx.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < n; i++) d[i] = Math.random() * 2 - 1;
const src = ctx.createBufferSource(); src.buffer = buf; return src;
}
function playKick() {
const ctx = getCtx(), t = ctx.currentTime;
const osc = ctx.createOscillator(), g = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(150, t); osc.frequency.exponentialRampToValueAtTime(0.001, t+0.5);
g.gain.setValueAtTime(1.0, t); g.gain.exponentialRampToValueAtTime(0.001, t+0.5);
osc.connect(g); g.connect(ctx.destination); osc.start(t); osc.stop(t+0.5);
const o2 = ctx.createOscillator(), g2 = ctx.createGain();
o2.frequency.value = 900; g2.gain.setValueAtTime(0.3, t); g2.gain.exponentialRampToValueAtTime(0.001, t+0.025);
o2.connect(g2); g2.connect(ctx.destination); o2.start(t); o2.stop(t+0.025);
}
function playSnare() {
const ctx = getCtx(), t = ctx.currentTime;
const ns = noise(ctx, 0.22); const filt = ctx.createBiquadFilter(); const ng = ctx.createGain();
filt.type = 'bandpass'; filt.frequency.value = 3000; filt.Q.value = 0.5;
ng.gain.setValueAtTime(0.8, t); ng.gain.exponentialRampToValueAtTime(0.001, t+0.22);
ns.connect(filt); filt.connect(ng); ng.connect(ctx.destination); ns.start(t); ns.stop(t+0.22);
const osc = ctx.createOscillator(), og = ctx.createGain();
osc.type = 'triangle'; osc.frequency.value = 185;
og.gain.setValueAtTime(0.5, t); og.gain.exponentialRampToValueAtTime(0.001, t+0.08);
osc.connect(og); og.connect(ctx.destination); osc.start(t); osc.stop(t+0.08);
}
function playHiHat(open) {
const ctx = getCtx(), t = ctx.currentTime, dur = open ? 0.38 : 0.042;
const ns = noise(ctx, dur), filt = ctx.createBiquadFilter(), g = ctx.createGain();
filt.type = 'highpass'; filt.frequency.value = 8000;
g.gain.setValueAtTime(open ? 0.28 : 0.38, t); g.gain.exponentialRampToValueAtTime(0.001, t+dur);
ns.connect(filt); filt.connect(g); g.connect(ctx.destination); ns.start(t); ns.stop(t+dur);
}
function playCrash() {
const ctx = getCtx(), t = ctx.currentTime, dur = 1.6;
const ns = noise(ctx, dur), filt = ctx.createBiquadFilter(), g = ctx.createGain();
filt.type = 'highpass'; filt.frequency.value = 5000;
g.gain.setValueAtTime(0.38, t); g.gain.exponentialRampToValueAtTime(0.001, t+dur);
ns.connect(filt); filt.connect(g); g.connect(ctx.destination); ns.start(t); ns.stop(t+dur);
}
function playRide() {
const ctx = getCtx(), t = ctx.currentTime, dur = 0.9;
const ns = noise(ctx, dur), filt = ctx.createBiquadFilter(), g = ctx.createGain();
filt.type = 'bandpass'; filt.frequency.value = 6000; filt.Q.value = 0.3;
g.gain.setValueAtTime(0.22, t); g.gain.exponentialRampToValueAtTime(0.001, t+dur);
ns.connect(filt); filt.connect(g); g.connect(ctx.destination); ns.start(t); ns.stop(t+dur);
const osc = ctx.createOscillator(), og = ctx.createGain();
osc.frequency.value = 860; og.gain.setValueAtTime(0.12, t); og.gain.exponentialRampToValueAtTime(0.001, t+0.45);
osc.connect(og); og.connect(ctx.destination); osc.start(t); osc.stop(t+0.45);
}
function playTom(freq) {
const ctx = getCtx(), t = ctx.currentTime;
const osc = ctx.createOscillator(), g = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, t); osc.frequency.exponentialRampToValueAtTime(freq*0.5, t+0.3);
g.gain.setValueAtTime(0.8, t); g.gain.exponentialRampToValueAtTime(0.001, t+0.42);
osc.connect(g); g.connect(ctx.destination); osc.start(t); osc.stop(t+0.42);
const ns = noise(ctx, 0.05), ng = ctx.createGain();
ng.gain.setValueAtTime(0.18, t); ng.gain.exponentialRampToValueAtTime(0.001, t+0.05);
ns.connect(ng); ng.connect(ctx.destination); ns.start(t); ns.stop(t+0.05);
}
// ── Drum definitions ───────────────────────────────────────────────
const DRUMS = [
{ id:'crash', name:'Crash', key:'A', fn:()=>playCrash(), row:'cymbals', cls:'cymbal' },
{ id:'ride', name:'Ride', key:'S', fn:()=>playRide(), row:'cymbals', cls:'cymbal' },
{ id:'hihat_c', name:'Hi-Hat', key:'L', fn:()=>playHiHat(false), row:'cymbals', cls:'cymbal' },
{ id:'hihat_o', name:'Open Hat', key:';', fn:()=>playHiHat(true), row:'cymbals', cls:'cymbal' },
{ id:'hitom', name:'Hi Tom', key:'D', fn:()=>playTom(400), row:'toms', cls:'tom' },
{ id:'midtom', name:'Mid Tom', key:'F', fn:()=>playTom(280), row:'toms', cls:'tom' },
{ id:'floortom', name:'Flr Tom', key:'G', fn:()=>playTom(180), row:'toms', cls:'tom' },
{ id:'snare', name:'Snare', key:'K', fn:()=>playSnare(), row:'snare', cls:'snare' },
{ id:'kick', name:'Kick', key:'J', fn:()=>playKick(), row:'kick', cls:'kick' },
];
const KEYMAP = {};
const hitCounts = {};
DRUMS.forEach(d => { KEYMAP[d.key.toLowerCase()] = d.id; hitCounts[d.id] = 0; });
// ── Build pads ─────────────────────────────────────────────────────
function buildPads() {
DRUMS.forEach(d => {
const row = document.getElementById('row-' + d.row);
const pad = document.createElement('div');
pad.className = 'pad ' + d.cls;
pad.id = 'pad-' + d.id;
pad.innerHTML = `<span class="pad-name">${d.name}</span><span class="pad-key">${d.key}</span>`;
pad.addEventListener('mousedown', () => triggerDrum(d.id));
pad.addEventListener('touchstart', e => { e.preventDefault(); triggerDrum(d.id); }, {passive:false});
row.appendChild(pad);
});
}
let hitTimeout = {};
function triggerDrum(id) {
const drum = DRUMS.find(d => d.id === id);
if (!drum) return;
drum.fn();
hitCounts[id]++;
document.getElementById('hitName').textContent = drum.name.toUpperCase();
updateHitStats();
const pad = document.getElementById('pad-' + id);
if (pad) {
pad.classList.add('hit');
clearTimeout(hitTimeout[id]);
hitTimeout[id] = setTimeout(() => pad.classList.remove('hit'), 120);
}
}
function updateHitStats() {
const stats = document.getElementById('hitStats');
stats.innerHTML = DRUMS.slice(0,6).map(d =>
`<div class="hit-count"><span class="hit-count-val">${hitCounts[d.id]}</span><span class="hit-count-lbl">${d.name.replace(' ','<br>')}</span></div>`
).join('');
}
updateHitStats();
// ── Keyboard ───────────────────────────────────────────────────────
document.addEventListener('keydown', e => {
if (e.target.tagName === 'SELECT' || e.target.tagName === 'INPUT') return;
if (e.code === 'Space') { e.preventDefault(); seqPlaying ? seqStop() : seqStart(); return; }
if (e.repeat) return;
const k = e.key.toLowerCase();
if (KEYMAP[k]) triggerDrum(KEYMAP[k]);
});
// ── Sequencer state ────────────────────────────────────────────────
const STEPS = 16;
const seqState = {};
DRUMS.forEach(d => { seqState[d.id] = new Array(STEPS).fill(0); });
let seqStep = 0, seqTimer = null, seqPlaying = false;
let bpm = 100;
// ── Build sequencer grid ───────────────────────────────────────────
function buildSeqGrid() {
const grid = document.getElementById('seqGrid');
grid.innerHTML = '';
DRUMS.forEach(d => {
const row = document.createElement('div');
row.className = 'seq-row';
const label = document.createElement('div');
label.className = 'seq-track-label';
label.textContent = d.name;
row.appendChild(label);
const steps = document.createElement('div');
steps.className = 'seq-steps';
for (let s = 0; s < STEPS; s++) {
const cell = document.createElement('div');
cell.className = 'seq-step' + (s % 4 === 0 && s > 0 ? ' seq-beat-4' : '');
cell.id = `step-${d.id}-${s}`;
if (seqState[d.id][s]) cell.classList.add('on');
cell.addEventListener('click', () => {
seqState[d.id][s] ^= 1;
cell.classList.toggle('on', seqState[d.id][s] === 1);
});
steps.appendChild(cell);
}
row.appendChild(steps);
grid.appendChild(row);
});
}
function updateStepHighlight(step) {
document.querySelectorAll('.seq-step').forEach(c => c.classList.remove('current'));
DRUMS.forEach(d => {
const cell = document.getElementById(`step-${d.id}-${step}`);
if (cell) cell.classList.add('current');
});
}
function seqTick() {
DRUMS.forEach(d => {
if (seqState[d.id][seqStep]) { d.fn(); flashPad(d.id); }
});
updateStepHighlight(seqStep);
seqStep = (seqStep + 1) % STEPS;
}
function flashPad(id) {
const pad = document.getElementById('pad-' + id);
if (!pad) return;
pad.classList.add('hit');
clearTimeout(hitTimeout[id]);
hitTimeout[id] = setTimeout(() => pad.classList.remove('hit'), 100);
}
function seqStart() {
if (seqPlaying) return;
seqPlaying = true;
seqStep = 0;
const interval = (60 / bpm / 4) * 1000;
seqTimer = setInterval(seqTick, interval);
document.getElementById('seqPlayBtn').classList.add('active');
}
function seqStop() {
seqPlaying = false;
clearInterval(seqTimer); seqTimer = null; seqStep = 0;
document.querySelectorAll('.seq-step').forEach(c => c.classList.remove('current'));
document.getElementById('seqPlayBtn').classList.remove('active');
}
function seqClear() {
seqStop();
DRUMS.forEach(d => {
seqState[d.id].fill(0);
for (let s = 0; s < STEPS; s++) {
const c = document.getElementById(`step-${d.id}-${s}`);
if (c) c.classList.remove('on');
}
});
}
document.getElementById('seqPlayBtn').addEventListener('click', () => seqPlaying ? seqStop() : seqStart());
document.getElementById('seqStopBtn').addEventListener('click', seqStop);
document.getElementById('seqClearBtn').addEventListener('click', seqClear);
document.getElementById('bpmRange').addEventListener('input', e => {
bpm = +e.target.value;
document.getElementById('bpmVal').textContent = bpm;
if (seqPlaying) { seqStop(); seqStart(); }
});
// ── Patterns ───────────────────────────────────────────────────────
const PATTERNS = [
{ name:"Basic Rock Beat", bpm:100, tracks:{
kick: [1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],
snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0],
hihat_c:[1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0]
}},
{ name:"Boom Bap Hip-Hop", bpm:90, tracks:{
kick: [1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0],
snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0],
hihat_c:[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0]
}},
{ name:"Reggae One Drop", bpm:75, tracks:{
kick: [0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],
snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0],
hihat_c:[1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0]
}},
{ name:"Funk Groove", bpm:105, tracks:{
kick: [1,0,0,1,0,0,1,0,1,0,0,0,1,0,0,0],
snare: [0,0,0,0,1,0,0,1,0,0,1,0,1,0,0,0],
hihat_c:[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
}},
{ name:"Trap Beat", bpm:140, tracks:{
kick: [1,0,0,0,1,0,0,1,0,0,1,0,0,0,0,0],
snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0],
hihat_c:[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
hihat_o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0]
}},
{ name:"Bossa Nova", bpm:130, tracks:{
kick: [1,0,0,0,0,0,1,0,0,1,0,0,0,1,0,0],
snare: [0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0],
hihat_c:[1,0,1,1,0,1,1,0,1,0,1,1,0,1,1,0]
}},
{ name:"Hard Rock", bpm:125, tracks:{
kick: [1,0,1,0,0,0,0,0,1,0,1,0,0,0,1,0],
snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0],
crash: [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
ride: [0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0]
}},
{ name:"Samba", bpm:115, tracks:{
kick: [1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0],
snare: [0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0],
hihat_c:[1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1]
}},
{ name:"Afrobeat", bpm:95, tracks:{
kick: [1,0,0,1,0,0,1,0,1,0,0,1,0,0,0,0],
snare: [0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0],
hihat_c:[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
}},
{ name:"Jazz Ride", bpm:120, tracks:{
kick: [1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],
snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0],
ride: [1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0],
hihat_c:[0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0]
}},
{ name:"Drum & Bass", bpm:174, tracks:{
kick: [1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0],
snare: [0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0],
hihat_c:[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
}},
{ name:"Disco", bpm:118, tracks:{
kick: [1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0],
snare: [0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0],
hihat_c:[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
hihat_o:[0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1]
}},
{ name:"Waltz (3/4 feel)", bpm:120, tracks:{
kick: [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0],
hihat_c:[1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0]
}},
{ name:"8-Beat Shuffle", bpm:95, tracks:{
kick: [1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0],
snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0],
hihat_c:[1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0]
}},
{ name:"Straight 4/4 (Beginner)", bpm:80, tracks:{
kick: [1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],
snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0],
hihat_c:[1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0]
}}
];
function loadPattern(idx) {
seqStop();
const p = PATTERNS[idx];
DRUMS.forEach(d => {
seqState[d.id].fill(0);
if (p.tracks[d.id]) p.tracks[d.id].forEach((v,s) => { seqState[d.id][s] = v; });
for (let s = 0; s < STEPS; s++) {
const c = document.getElementById(`step-${d.id}-${s}`);
if (c) c.classList.toggle('on', seqState[d.id][s] === 1);
}
});
bpm = p.bpm;
document.getElementById('bpmRange').value = bpm;
document.getElementById('bpmVal').textContent = bpm;
}
function buildPatternSelect() {
const sel = document.getElementById('patternSel');
PATTERNS.forEach((p, i) => {
const o = document.createElement('option');
o.value = i; o.textContent = `${p.name} (${p.bpm} BPM)`;
sel.appendChild(o);
});
sel.addEventListener('change', e => { if (e.target.value !== '') loadPattern(+e.target.value); });
}
// ── Init ───────────────────────────────────────────────────────────
buildPads();
buildSeqGrid();
buildPatternSelect();