// ═══════════════════════════════════════════════════════════════════════════════ // DRUMMASTER PRO - WEB AUDIO DRUM ENGINE // High-quality synthesized drum sounds using Web Audio API // ═══════════════════════════════════════════════════════════════════════════════ class DrumAudioEngine { constructor() { this.audioContext = null; this.masterGain = null; this.compressor = null; this.reverb = null; this.isInitialized = false; this.volume = 0.8; this.reverbMix = 0.2; // Drum kit presets this.kits = { rock: { name: 'Rock Kit', color: '#ff6b35' }, jazz: { name: 'Jazz Kit', color: '#4ecdc4' }, electronic: { name: 'Electronic Kit', color: '#8338ec' }, metal: { name: 'Metal Kit', color: '#ff006e' } }; this.currentKit = 'rock'; } async init() { if (this.isInitialized) return; try { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); // Master compressor for punch this.compressor = this.audioContext.createDynamicsCompressor(); this.compressor.threshold.value = -24; this.compressor.knee.value = 30; this.compressor.ratio.value = 12; this.compressor.attack.value = 0.003; this.compressor.release.value = 0.25; // Master gain this.masterGain = this.audioContext.createGain(); this.masterGain.gain.value = this.volume; // Create reverb this.reverb = await this.createReverb(); this.reverbGain = this.audioContext.createGain(); this.reverbGain.gain.value = this.reverbMix; this.dryGain = this.audioContext.createGain(); this.dryGain.gain.value = 1 - this.reverbMix; // Routing this.compressor.connect(this.dryGain); this.compressor.connect(this.reverb); this.reverb.connect(this.reverbGain); this.dryGain.connect(this.masterGain); this.reverbGain.connect(this.masterGain); this.masterGain.connect(this.audioContext.destination); this.isInitialized = true; console.log('[DrumAudio] Engine initialized'); } catch (e) { console.error('[DrumAudio] Failed to initialize:', e); } } async createReverb() { const convolver = this.audioContext.createConvolver(); const rate = this.audioContext.sampleRate; const length = rate * 1.5; const impulse = this.audioContext.createBuffer(2, length, rate); for (let channel = 0; channel < 2; channel++) { const channelData = impulse.getChannelData(channel); for (let i = 0; i < length; i++) { channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2); } } convolver.buffer = impulse; return convolver; } resume() { if (this.audioContext && this.audioContext.state === 'suspended') { this.audioContext.resume(); } } setVolume(value) { this.volume = Math.max(0, Math.min(1, value)); if (this.masterGain) { this.masterGain.gain.value = this.volume; } } setReverb(value) { this.reverbMix = Math.max(0, Math.min(1, value)); if (this.reverbGain && this.dryGain) { this.reverbGain.gain.value = this.reverbMix; this.dryGain.gain.value = 1 - this.reverbMix; } } setKit(kitName) { if (this.kits[kitName]) { this.currentKit = kitName; } } // ═══════════════════════════════════════════════════════════════════════════ // DRUM SOUND SYNTHESIS // ═══════════════════════════════════════════════════════════════════════════ playKick(velocity = 1) { if (!this.isInitialized) return; this.resume(); const time = this.audioContext.currentTime; const vel = velocity * 0.8; // Sub oscillator const osc = this.audioContext.createOscillator(); const oscGain = this.audioContext.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(150, time); osc.frequency.exponentialRampToValueAtTime(40, time + 0.1); oscGain.gain.setValueAtTime(vel, time); oscGain.gain.exponentialRampToValueAtTime(0.001, time + 0.5); // Click/attack const clickOsc = this.audioContext.createOscillator(); const clickGain = this.audioContext.createGain(); clickOsc.type = 'square'; clickOsc.frequency.setValueAtTime(200, time); clickOsc.frequency.exponentialRampToValueAtTime(50, time + 0.02); clickGain.gain.setValueAtTime(vel * 0.3, time); clickGain.gain.exponentialRampToValueAtTime(0.001, time + 0.05); // Noise transient const noise = this.createNoise(0.03); const noiseGain = this.audioContext.createGain(); const noiseFilter = this.audioContext.createBiquadFilter(); noiseFilter.type = 'lowpass'; noiseFilter.frequency.value = 200; noiseGain.gain.setValueAtTime(vel * 0.2, time); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.03); // Connect osc.connect(oscGain); oscGain.connect(this.compressor); clickOsc.connect(clickGain); clickGain.connect(this.compressor); noise.connect(noiseFilter); noiseFilter.connect(noiseGain); noiseGain.connect(this.compressor); // Start and stop osc.start(time); osc.stop(time + 0.5); clickOsc.start(time); clickOsc.stop(time + 0.05); noise.start(time); noise.stop(time + 0.03); } playSnare(velocity = 1) { if (!this.isInitialized) return; this.resume(); const time = this.audioContext.currentTime; const vel = velocity * 0.7; // Body tone const osc = this.audioContext.createOscillator(); const oscGain = this.audioContext.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(200, time); osc.frequency.exponentialRampToValueAtTime(120, time + 0.1); oscGain.gain.setValueAtTime(vel * 0.5, time); oscGain.gain.exponentialRampToValueAtTime(0.001, time + 0.15); // Snare noise (the "snare" wires) const noise = this.createNoise(0.2); const noiseGain = this.audioContext.createGain(); const noiseFilter = this.audioContext.createBiquadFilter(); const noiseHighpass = this.audioContext.createBiquadFilter(); noiseFilter.type = 'highpass'; noiseFilter.frequency.value = 1000; noiseHighpass.type = 'lowpass'; noiseHighpass.frequency.value = 8000; noiseGain.gain.setValueAtTime(vel, time); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2); // Connect osc.connect(oscGain); oscGain.connect(this.compressor); noise.connect(noiseFilter); noiseFilter.connect(noiseHighpass); noiseHighpass.connect(noiseGain); noiseGain.connect(this.compressor); osc.start(time); osc.stop(time + 0.15); noise.start(time); noise.stop(time + 0.2); } playHihat(velocity = 1, open = false) { if (!this.isInitialized) return; this.resume(); const time = this.audioContext.currentTime; const vel = velocity * 0.4; const duration = open ? 0.4 : 0.08; // Metallic noise const noise = this.createNoise(duration); const noiseGain = this.audioContext.createGain(); const bandpass = this.audioContext.createBiquadFilter(); const highpass = this.audioContext.createBiquadFilter(); bandpass.type = 'bandpass'; bandpass.frequency.value = 10000; bandpass.Q.value = 1; highpass.type = 'highpass'; highpass.frequency.value = 7000; noiseGain.gain.setValueAtTime(vel, time); if (open) { noiseGain.gain.exponentialRampToValueAtTime(vel * 0.3, time + 0.1); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + duration); } else { noiseGain.gain.exponentialRampToValueAtTime(0.001, time + duration); } // Square wave for metallic quality const sq1 = this.audioContext.createOscillator(); const sq2 = this.audioContext.createOscillator(); const sqGain = this.audioContext.createGain(); sq1.type = 'square'; sq1.frequency.value = 800; sq2.type = 'square'; sq2.frequency.value = 1342; sqGain.gain.setValueAtTime(vel * 0.1, time); sqGain.gain.exponentialRampToValueAtTime(0.001, time + 0.02); // Connect noise.connect(bandpass); bandpass.connect(highpass); highpass.connect(noiseGain); noiseGain.connect(this.compressor); sq1.connect(sqGain); sq2.connect(sqGain); sqGain.connect(this.compressor); noise.start(time); noise.stop(time + duration); sq1.start(time); sq1.stop(time + 0.02); sq2.start(time); sq2.stop(time + 0.02); } playCrash(velocity = 1) { if (!this.isInitialized) return; this.resume(); const time = this.audioContext.currentTime; const vel = velocity * 0.6; // Long noise wash const noise = this.createNoise(2); const noiseGain = this.audioContext.createGain(); const bandpass = this.audioContext.createBiquadFilter(); const highpass = this.audioContext.createBiquadFilter(); bandpass.type = 'bandpass'; bandpass.frequency.value = 8000; bandpass.Q.value = 0.5; highpass.type = 'highpass'; highpass.frequency.value = 4000; noiseGain.gain.setValueAtTime(vel, time); noiseGain.gain.exponentialRampToValueAtTime(vel * 0.4, time + 0.3); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + 2); // Metallic oscillators const freqs = [420, 560, 630, 840]; freqs.forEach(freq => { const osc = this.audioContext.createOscillator(); const gain = this.audioContext.createGain(); osc.type = 'sine'; osc.frequency.value = freq + Math.random() * 20; gain.gain.setValueAtTime(vel * 0.05, time); gain.gain.exponentialRampToValueAtTime(0.001, time + 1.5); osc.connect(gain); gain.connect(this.compressor); osc.start(time); osc.stop(time + 1.5); }); noise.connect(bandpass); bandpass.connect(highpass); highpass.connect(noiseGain); noiseGain.connect(this.compressor); noise.start(time); noise.stop(time + 2); } playRide(velocity = 1, bell = false) { if (!this.isInitialized) return; this.resume(); const time = this.audioContext.currentTime; const vel = velocity * 0.5; // Ping tone const osc = this.audioContext.createOscillator(); const oscGain = this.audioContext.createGain(); osc.type = 'sine'; osc.frequency.value = bell ? 900 : 600; oscGain.gain.setValueAtTime(vel * 0.3, time); oscGain.gain.exponentialRampToValueAtTime(0.001, time + (bell ? 0.5 : 0.8)); // Shimmer noise const noise = this.createNoise(bell ? 0.3 : 0.6); const noiseGain = this.audioContext.createGain(); const bandpass = this.audioContext.createBiquadFilter(); bandpass.type = 'bandpass'; bandpass.frequency.value = bell ? 6000 : 5000; bandpass.Q.value = 2; noiseGain.gain.setValueAtTime(vel * 0.2, time); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + (bell ? 0.3 : 0.6)); // Connect osc.connect(oscGain); oscGain.connect(this.compressor); noise.connect(bandpass); bandpass.connect(noiseGain); noiseGain.connect(this.compressor); osc.start(time); osc.stop(time + 0.8); noise.start(time); noise.stop(time + 0.6); } playTom(velocity = 1, pitch = 'high') { if (!this.isInitialized) return; this.resume(); const time = this.audioContext.currentTime; const vel = velocity * 0.7; const pitchMap = { high: { start: 200, end: 120 }, mid: { start: 150, end: 90 }, low: { start: 100, end: 60 }, floor: { start: 80, end: 45 } }; const p = pitchMap[pitch] || pitchMap.mid; // Body const osc = this.audioContext.createOscillator(); const oscGain = this.audioContext.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(p.start, time); osc.frequency.exponentialRampToValueAtTime(p.end, time + 0.15); oscGain.gain.setValueAtTime(vel, time); oscGain.gain.exponentialRampToValueAtTime(0.001, time + 0.4); // Attack click const click = this.audioContext.createOscillator(); const clickGain = this.audioContext.createGain(); click.type = 'sine'; click.frequency.value = p.start * 2; clickGain.gain.setValueAtTime(vel * 0.3, time); clickGain.gain.exponentialRampToValueAtTime(0.001, time + 0.02); // Connect osc.connect(oscGain); oscGain.connect(this.compressor); click.connect(clickGain); clickGain.connect(this.compressor); osc.start(time); osc.stop(time + 0.4); click.start(time); click.stop(time + 0.02); } // Helper to create noise buffer createNoise(duration) { const bufferSize = this.audioContext.sampleRate * duration; const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; } const noise = this.audioContext.createBufferSource(); noise.buffer = buffer; return noise; } // Play drum by name play(drumName, velocity = 1) { switch (drumName) { case 'kick': this.playKick(velocity); break; case 'snare': this.playSnare(velocity); break; case 'hihat': this.playHihat(velocity, false); break; case 'hihat-open': this.playHihat(velocity, true); break; case 'crash': this.playCrash(velocity); break; case 'ride': this.playRide(velocity, false); break; case 'ride-bell': this.playRide(velocity, true); break; case 'tom1': this.playTom(velocity, 'high'); break; case 'tom2': this.playTom(velocity, 'mid'); break; case 'tom3': case 'floor': this.playTom(velocity, 'floor'); break; default: console.log('[DrumAudio] Unknown drum:', drumName); } } // ═══════════════════════════════════════════════════════════════════════════ // METRONOME // ═══════════════════════════════════════════════════════════════════════════ playMetronomeClick(accent = false) { if (!this.isInitialized) return; this.resume(); const time = this.audioContext.currentTime; const osc = this.audioContext.createOscillator(); const gain = this.audioContext.createGain(); osc.type = 'sine'; osc.frequency.value = accent ? 1200 : 800; gain.gain.setValueAtTime(0.3, time); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.05); osc.connect(gain); gain.connect(this.masterGain); osc.start(time); osc.stop(time + 0.05); } // Cowbell for count-in playCowbell() { if (!this.isInitialized) return; this.resume(); const time = this.audioContext.currentTime; const osc1 = this.audioContext.createOscillator(); const osc2 = this.audioContext.createOscillator(); const gain = this.audioContext.createGain(); const filter = this.audioContext.createBiquadFilter(); osc1.type = 'square'; osc1.frequency.value = 560; osc2.type = 'square'; osc2.frequency.value = 845; filter.type = 'bandpass'; filter.frequency.value = 700; filter.Q.value = 3; gain.gain.setValueAtTime(0.4, time); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.2); osc1.connect(filter); osc2.connect(filter); filter.connect(gain); gain.connect(this.masterGain); osc1.start(time); osc1.stop(time + 0.2); osc2.start(time); osc2.stop(time + 0.2); } } // ═══════════════════════════════════════════════════════════════════════════════ // METRONOME CLASS // ═══════════════════════════════════════════════════════════════════════════════ class Metronome { constructor(audioEngine) { this.audio = audioEngine; this.bpm = 120; this.beatsPerMeasure = 4; this.isRunning = false; this.currentBeat = 0; this.intervalId = null; this.onBeat = null; // Callback for visual sync } setBPM(bpm) { this.bpm = Math.max(30, Math.min(300, bpm)); if (this.isRunning) { this.stop(); this.start(); } } setTimeSignature(beats) { this.beatsPerMeasure = beats; } start() { if (this.isRunning) return; this.isRunning = true; this.currentBeat = 0; const interval = 60000 / this.bpm; this.tick(); this.intervalId = setInterval(() => this.tick(), interval); } stop() { this.isRunning = false; if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } } tick() { const isAccent = this.currentBeat === 0; this.audio.playMetronomeClick(isAccent); if (this.onBeat) { this.onBeat(this.currentBeat, isAccent); } this.currentBeat = (this.currentBeat + 1) % this.beatsPerMeasure; } countIn(callback) { let count = 4; const interval = 60000 / this.bpm; const tick = () => { this.audio.playCowbell(); count--; if (count > 0) { setTimeout(tick, interval); } else { if (callback) callback(); } }; tick(); } } // ═══════════════════════════════════════════════════════════════════════════════ // GLOBAL EXPORTS // ═══════════════════════════════════════════════════════════════════════════════ window.DrumAudioEngine = DrumAudioEngine; window.Metronome = Metronome; // Auto-initialize window.drumAudio = new DrumAudioEngine(); document.addEventListener('click', () => window.drumAudio.init(), { once: true }); document.addEventListener('keydown', () => window.drumAudio.init(), { once: true }); console.log('[DrumAudio] Module loaded - click or press key to initialize');