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();