From 810759986a15d60a1754cd937d6ff91f3c543b24 Mon Sep 17 00:00:00 2001 From: Techy-Ninja Date: Mon, 6 Oct 2025 15:35:03 +0530 Subject: [PATCH] Interactive-Drum-Kit --- examples/Drum_kit/README.md | 34 ++++ examples/Drum_kit/index.html | 46 +++++ examples/Drum_kit/script.js | 377 +++++++++++++++++++++++++++++++++++ examples/Drum_kit/styles.css | 172 ++++++++++++++++ 4 files changed, 629 insertions(+) create mode 100644 examples/Drum_kit/README.md create mode 100644 examples/Drum_kit/index.html create mode 100644 examples/Drum_kit/script.js create mode 100644 examples/Drum_kit/styles.css diff --git a/examples/Drum_kit/README.md b/examples/Drum_kit/README.md new file mode 100644 index 00000000..a071b824 --- /dev/null +++ b/examples/Drum_kit/README.md @@ -0,0 +1,34 @@ +# Drum Kit — WebAudio demo + +This is a small interactive drum kit demo using plain HTML, CSS and JavaScript. + +Features +- Play drum sounds by clicking pads or pressing mapped keys: + - W - Crash + - A - Kick + - S - Snare + - D - Mid Tom + - J - HiHat Closed + - K - HiHat Open + - L - Clap + - ; - Low Tom +- Visual button animations on play. +- Record a short sequence and play it back. +- Adjustable master volume control. +- Save and load your drum patterns as JSON files. +- Full keyboard accessibility with ARIA labels and focus indicators.Files +- `index.html` — markup and UI +- `styles.css` — styles and small animations +- `script.js` — Web Audio synths, event handling, recording/playback logic + +Usage +1. Open `index.html` in a browser (Chrome, Firefox, Edge). For best results, open it via a web server (e.g. `python -m http.server`) because some browsers restrict AudioContext on file://. +2. Click a pad or press W/A/S/D to play sounds. +3. Press "Record", play some beats, then "Stop" and "Play" to hear them. + +Notes +- Sounds are synthesized using the Web Audio API (no external audio files required). +- The demo uses a lazy AudioContext creation strategy — the context starts when you interact with the page. +- Recording stores event times (ms) relative to the moment recording started and schedules playback with the AudioContext time base. + +Enjoy! diff --git a/examples/Drum_kit/index.html b/examples/Drum_kit/index.html new file mode 100644 index 00000000..c6df1eb4 --- /dev/null +++ b/examples/Drum_kit/index.html @@ -0,0 +1,46 @@ + + + + + + Drum Kit + + + +
+

Drum Kits 🥁

+ +
+ + + + + + + + +
+ +
+ + + 90% +
+ +
+ + + + + + + +
Idle
+
+ +

Tip: Click a pad or press the keys on your keyboard to play. Try recording a short beat!

+
+ + + + diff --git a/examples/Drum_kit/script.js b/examples/Drum_kit/script.js new file mode 100644 index 00000000..3e3dec19 --- /dev/null +++ b/examples/Drum_kit/script.js @@ -0,0 +1,377 @@ +// Drum kit using Web Audio API +// - Click or press W A S D to play sounds +// - Record/Stop/Play/Clear sequence with timing + +// Simple mapping: key -> instrument +const MAPPING = { + w: 'crash', + a: 'kick', + s: 'snare', + d: 'tom', + j: 'hihatClosed', + k: 'hihatOpen', + l: 'clap', + ';': 'lowTom' +}; + +// Create AudioContext lazily (user gesture required in many browsers) +let audioCtx = null; +let masterGain = null; +function ensureAudio(){ + if(!audioCtx){ + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + masterGain = audioCtx.createGain(); + masterGain.gain.value = 0.9; // master volume + masterGain.connect(audioCtx.destination); + } + // Some browsers (Chrome mobile) start suspended until explicit resume after a gesture + if(audioCtx.state === 'suspended'){ + audioCtx.resume(); + } + return audioCtx; +} + +// Basic drum synthesizers using WebAudio primitives +function playCrash(time){ + const ctx = ensureAudio(); + const o = ctx.createBufferSource(); + // generate short noise buffer + const buffer = ctx.createBuffer(1, ctx.sampleRate * 1.0, ctx.sampleRate); + const data = buffer.getChannelData(0); + for(let i=0;i { + const bp = ctx.createBiquadFilter(); + bp.type='bandpass'; + bp.frequency.value = f; + node.connect(bp); + node = bp; + }); + const g = ctx.createGain(); + const decay = closed ? 0.09 : 0.4; + g.gain.setValueAtTime(closed?0.8:0.7, t); + g.gain.exponentialRampToValueAtTime(0.0001, t + decay); + node.connect(g).connect(masterGain || ctx.destination); + src.start(t); + src.stop(t + decay + 0.02); +} + +function playClap(time){ + const ctx = ensureAudio(); + const t = (time ?? ctx.currentTime); + // Clap: 3 quick noise bursts with exponential decay + const bursts = [0, 0.02, 0.04]; + bursts.forEach(offset => { + const noise = ctx.createBufferSource(); + const buffer = ctx.createBuffer(1, ctx.sampleRate * 0.25, ctx.sampleRate); + const data = buffer.getChannelData(0); + for(let i=0;i{ + const val = e.target.value; + if(masterGain){ + masterGain.gain.value = val / 100; + } + volumeValue.textContent = val + '%'; +}); + +function animateButton(key){ + const btn = buttons.find(b => b.dataset.key === key); + if(!btn) return; + btn.classList.add('playing','pulse'); + setTimeout(()=> btn.classList.remove('pulse'), 360); + setTimeout(()=> btn.classList.remove('playing'), 140); +} + +// Recording +let recording = []; +let isRecording = false; +let recordStart = 0; + +const recordBtn = document.getElementById('record'); +const stopBtn = document.getElementById('stop'); +const playBtn = document.getElementById('play'); +const clearBtn = document.getElementById('clear'); + +recordBtn.addEventListener('click', ()=>{ + ensureAudio(); + recording = []; + isRecording = true; + recordStart = performance.now(); + statusEl.textContent = 'Recording...'; + statusEl.classList.add('recording'); + recordBtn.disabled = true; + stopBtn.disabled = false; + playBtn.disabled = true; + clearBtn.disabled = true; + downloadBtn.disabled = true; +}); + +stopBtn.addEventListener('click', ()=>{ + isRecording = false; + statusEl.textContent = 'Idle'; + statusEl.classList.remove('recording'); + recordBtn.disabled = false; + stopBtn.disabled = true; + playBtn.disabled = recording.length === 0; + clearBtn.disabled = recording.length === 0; + downloadBtn.disabled = recording.length === 0; +}); + +playBtn.addEventListener('click', async ()=>{ + if(recording.length === 0) return; + statusEl.textContent = 'Playing...'; + recordBtn.disabled = true; playBtn.disabled = true; stopBtn.disabled = true; clearBtn.disabled = true; + const ctx = ensureAudio(); + const start = ctx.currentTime + 0.1; + for(const ev of recording){ + playInstrument(ev.inst, start + ev.time/1000); + // schedule UI animation (Best-effort using setTimeout) + setTimeout(()=> animateButton(ev.key), (start - ctx.currentTime) * 1000 + ev.time); + } + // re-enable after last event + const duration = Math.max(...recording.map(r=>r.time)) + 800; + setTimeout(()=>{ + statusEl.textContent = 'Idle'; + recordBtn.disabled = false; + playBtn.disabled = false; + clearBtn.disabled = false; + }, duration); +}); + +clearBtn.addEventListener('click', ()=>{ + recording = []; + playBtn.disabled = true; + clearBtn.disabled = true; + downloadBtn.disabled = true; + statusEl.textContent = 'Cleared'; + setTimeout(()=> statusEl.textContent = 'Idle', 800); +}); + +const downloadBtn = document.getElementById('download'); +const uploadBtn = document.getElementById('upload'); +const fileInput = document.getElementById('file-input'); + +// Download pattern as JSON +downloadBtn.addEventListener('click', ()=>{ + if(recording.length === 0) return; + const data = JSON.stringify(recording, null, 2); + const blob = new Blob([data], {type: 'application/json'}); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `drum-pattern-${Date.now()}.json`; + a.click(); + URL.revokeObjectURL(url); + statusEl.textContent = 'Pattern saved!'; + setTimeout(()=> statusEl.textContent = 'Idle', 1500); +}); + +// Upload pattern from JSON +uploadBtn.addEventListener('click', ()=>{ + fileInput.click(); +}); + +fileInput.addEventListener('change', (e)=>{ + const file = e.target.files[0]; + if(!file) return; + const reader = new FileReader(); + reader.onload = (evt)=>{ + try{ + const loaded = JSON.parse(evt.target.result); + if(Array.isArray(loaded) && loaded.length > 0){ + recording = loaded; + playBtn.disabled = false; + clearBtn.disabled = false; + downloadBtn.disabled = false; + statusEl.textContent = 'Pattern loaded!'; + setTimeout(()=> statusEl.textContent = 'Idle', 1500); + }else{ + statusEl.textContent = 'Invalid pattern'; + setTimeout(()=> statusEl.textContent = 'Idle', 1500); + } + }catch(err){ + statusEl.textContent = 'Error loading file'; + setTimeout(()=> statusEl.textContent = 'Idle', 1500); + } + }; + reader.readAsText(file); + e.target.value = ''; // reset input +}); + +// Play one event: handles scheduling and recording +function triggerKey(key){ + const inst = MAPPING[key]; + if(!inst) return; + const ctx = ensureAudio(); + playInstrument(inst, ctx.currentTime + 0); + animateButton(key); + if(isRecording){ + recording.push({key, inst, time: performance.now() - recordStart}); + playBtn.disabled = false; clearBtn.disabled = false; + } +} + +// Mouse click handlers +buttons.forEach(btn => btn.addEventListener('click', e => { + // ensure audio is started on first interaction + ensureAudio(); + const key = btn.dataset.key; + triggerKey(key); +})); + +// Keyboard handlers +window.addEventListener('keydown', (e)=>{ + // Prevent repeated triggering when key is held down + if(e.repeat) return; + const k = e.key.toLowerCase(); + if(MAPPING[k]){ + ensureAudio(); + triggerKey(k); + } +}); + +// Accessibility: expose mapping in console +console.log('Drum kit ready. Keys:', MAPPING); diff --git a/examples/Drum_kit/styles.css b/examples/Drum_kit/styles.css new file mode 100644 index 00000000..a4fdbcad --- /dev/null +++ b/examples/Drum_kit/styles.css @@ -0,0 +1,172 @@ +:root{ + --bg: #ffd6e0; + --card: #fff; + --accent: #ff6f91; + --shadow: rgba(0,0,0,0.12); +} +*{box-sizing:border-box} +body{ + margin:0; + font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; + background: linear-gradient(180deg, var(--bg), #ffeef6); + color:#333; + display:flex; + align-items:center; + justify-content:center; + min-height:100vh; +} +.container{ + width:min(900px, 94%); + text-align:center; +} +.title{ + font-size:2.4rem; + letter-spacing:1px; + margin:0 0 18px 0; + color: #e46363; + text-shadow: 0 4px 10px rgba(0,0,0,0.12); +} +.kits{ + display:flex; + gap:18px; + justify-content:center; + flex-wrap:wrap; + margin-bottom:18px; +} +.drum{ + background:var(--card); + border-radius:12px; + width:140px; + height:92px; + border:0; + box-shadow:0 8px 20px var(--shadow); + cursor:pointer; + font-size:1.1rem; + padding:12px; + position:relative; + transition:transform .08s ease, box-shadow .12s ease; + display:flex; + align-items:center; + justify-content:center; + flex-direction:column; +} +.drum:active, +.drum.playing{ + transform:translateY(6px) scale(.98); + box-shadow:0 4px 12px rgba(0,0,0,0.10); +} +.drum:focus-visible{ + outline: 3px solid var(--accent); + outline-offset: 2px; +} +.drum .key{ + display:block; + font-size:.85rem; + margin-top:6px; + color:#666; +} +.controls{ + display:flex; + gap:8px; + justify-content:center; + align-items:center; + margin-top:12px; + flex-wrap:wrap; +} +.controls button{ + background:transparent; + border:2px solid rgba(255,111,145,0.18); + padding:8px 14px; + border-radius:8px; + cursor:pointer; + color:#222; + font-weight:600; + transition: all 0.2s ease; +} +.controls button:hover:not(:disabled){ + background:rgba(255,111,145,0.1); + border-color:rgba(255,111,145,0.3); + transform:translateY(-1px); +} +.controls button:active:not(:disabled){ + transform:translateY(0); +} +.controls button:focus-visible{ + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.controls button:disabled{opacity:.4;cursor:not-allowed} +.status{padding:6px 10px;border-radius:8px;background:rgba(255,255,255,0.6);margin-left:8px} +.status.recording{ + animation: recordPulse 1s ease-in-out infinite; + background:rgba(255,99,99,0.3); + color:#c42; + font-weight:600; +} +.hint{color:#5a3b4d;margin-top:10px} + +.volume-control{ + display:flex; + align-items:center; + justify-content:center; + gap:10px; + margin:16px 0 8px 0; + font-size:0.95rem; +} +.volume-control label{ + font-weight:600; + color:#5a3b4d; +} +.volume-control input[type="range"]{ + width:180px; + height:6px; + border-radius:3px; + background:rgba(255,111,145,0.2); + outline:none; + -webkit-appearance:none; + appearance:none; +} +.volume-control input[type="range"]::-webkit-slider-thumb{ + -webkit-appearance:none; + appearance:none; + width:18px; + height:18px; + border-radius:50%; + background:var(--accent); + cursor:pointer; + transition:transform 0.15s ease; +} +.volume-control input[type="range"]::-webkit-slider-thumb:hover{ + transform:scale(1.15); +} +.volume-control input[type="range"]::-moz-range-thumb{ + width:18px; + height:18px; + border-radius:50%; + background:var(--accent); + cursor:pointer; + border:none; +} +#volume-value{ + min-width:42px; + font-weight:600; + color:#666; +} + +@media (max-width:520px){ + .drum{width:44%;height:86px} +} + +/* small animation for visual pulse */ +.pulse{ + animation: pulse 360ms ease-out; +} +@keyframes pulse{ + 0%{box-shadow:0 8px 30px rgba(255,111,145,0.15)} + 100%{box-shadow:0 8px 20px rgba(0,0,0,0.08)} +} + +@keyframes recordPulse{ + 0%, 100%{opacity:1} + 50%{opacity:0.6} +}