Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/Jennys-Drumste / audio-engine.js |
| Date | Not specified in source |
| Repository | Janady13/Jennys-Drumste |
| Revision | 1f946b9e18e552f80f4b2ffd7f59c01d0126aa53 |
| Branch | claude/jenny-drumsets-research-MofSb |
| Path or archive member | audio-engine.js |
| Original SHA-256 | e766fd4152b92fac49e008e31871a68ce5c81c6fb6692ce7851dcdaef386d2ae |
| Git blob SHA | 4a3f9afee3b0b1d876ede841f0b50d40ad6f6229 |
| Reading-copy SHA-256 | e766fd4152b92fac49e008e31871a68ce5c81c6fb6692ce7851dcdaef386d2ae |
Recovered source transcript
Download complete source text ↓632 lines · 20.8 KB
// ═══════════════════════════════════════════════════════════════════════════════
// 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');