"""Generate one-shot drum samples using DSP synthesis. Creates WAV files for: kick, snare, hihat, crash, ride, tom_high, tom_low. Uses scipy filters for realistic frequency shaping and layered synthesis. """ import numpy as np from scipy.io import wavfile from scipy.signal import butter, lfilter from pathlib import Path SR = 44100 OUT_DIR = Path(__file__).parent.parent / "app" / "public" / "drums" np.random.seed(42) def _normalize(signal, peak=0.85): mx = np.max(np.abs(signal)) if mx > 0: signal = signal * (peak / mx) return signal.astype(np.float32) def _highpass(signal, freq, sr=SR, order=4): nyq = 0.5 * sr b, a = butter(order, freq / nyq, btype='high') return lfilter(b, a, signal) def _lowpass(signal, freq, sr=SR, order=4): nyq = 0.5 * sr b, a = butter(order, freq / nyq, btype='low') return lfilter(b, a, signal) def _bandpass(signal, low, high, sr=SR, order=4): nyq = 0.5 * sr b, a = butter(order, [low / nyq, high / nyq], btype='band') return lfilter(b, a, signal) def make_kick(): dur = 0.45 n = int(SR * dur) t = np.linspace(0, dur, n, endpoint=False) # Pitch sweep: 150Hz -> 45Hz with exponential decay freq = 45 + 105 * np.exp(-t * 25) phase = 2 * np.pi * np.cumsum(freq) / SR body = np.sin(phase) # Amplitude envelope: punchy attack, moderate decay env = np.exp(-t * 7) * (1 - np.exp(-t * 500)) # Sub-bass thump (separate low sine for weight) sub = np.sin(2 * np.pi * 50 * t) * np.exp(-t * 12) * 0.4 # Transient click click_n = int(SR * 0.004) click = np.random.randn(click_n) * 0.25 click *= np.linspace(1, 0, click_n) signal = body * env + sub signal[:click_n] += click # Lowpass to remove harmonics above 200Hz signal = _lowpass(signal, 200) return _normalize(signal) def make_snare(): dur = 0.3 n = int(SR * dur) t = np.linspace(0, dur, n, endpoint=False) # Body: sine at 185Hz with fast decay body = np.sin(2 * np.pi * 185 * t) * np.exp(-t * 18) * 0.6 # Second harmonic for richness body2 = np.sin(2 * np.pi * 330 * t) * np.exp(-t * 25) * 0.2 # Snare wires: bandpassed noise (1kHz-9kHz) noise = np.random.randn(n) wires = _bandpass(noise, 1000, 9000) wire_env = np.exp(-t * 12) wires = wires * wire_env * 0.5 # Sharp transient click_n = int(SR * 0.002) click = np.random.randn(click_n) * 0.5 click *= np.linspace(1, 0, click_n) signal = body + body2 + wires signal[:click_n] += click return _normalize(signal) def make_hihat(): dur = 0.08 n = int(SR * dur) t = np.linspace(0, dur, n, endpoint=False) # High-frequency noise noise = np.random.randn(n) signal = _highpass(noise, 6000) # Very tight envelope env = np.exp(-t * 60) * (1 - np.exp(-t * 2000)) signal = signal * env return _normalize(signal, peak=0.7) def make_crash(): dur = 1.5 n = int(SR * dur) t = np.linspace(0, dur, n, endpoint=False) # Broadband noise with emphasis on 3-8kHz noise = np.random.randn(n) bright = _bandpass(noise, 3000, 12000) * 0.7 body = _bandpass(noise, 800, 4000) * 0.3 signal = bright + body # Long decay envelope with sharp attack env = np.exp(-t * 2.5) * (1 - np.exp(-t * 1000)) signal = signal * env return _normalize(signal, peak=0.7) def make_ride(): dur = 0.6 n = int(SR * dur) t = np.linspace(0, dur, n, endpoint=False) # Tighter noise than crash, more "ping" character noise = np.random.randn(n) bright = _highpass(noise, 5000) * 0.6 # Add a subtle metallic tone (inharmonic mix of sines) ping = (np.sin(2 * np.pi * 4200 * t) * 0.15 + np.sin(2 * np.pi * 5800 * t) * 0.10 + np.sin(2 * np.pi * 7100 * t) * 0.08) signal = bright + ping # Medium decay env = np.exp(-t * 4) * (1 - np.exp(-t * 1500)) signal = signal * env return _normalize(signal, peak=0.6) def make_tom(freq_base=120, dur=0.35): n = int(SR * dur) t = np.linspace(0, dur, n, endpoint=False) # Pitched membrane: frequency sweep down freq = freq_base * 0.6 + freq_base * 0.4 * np.exp(-t * 15) phase = 2 * np.pi * np.cumsum(freq) / SR body = np.sin(phase) # Amplitude envelope env = np.exp(-t * 8) * (1 - np.exp(-t * 800)) # Slight noise for attack character click_n = int(SR * 0.005) click = np.random.randn(click_n) * 0.2 click *= np.linspace(1, 0, click_n) signal = body * env signal[:click_n] += click # Bandpass around the fundamental signal = _bandpass(signal, freq_base * 0.4, freq_base * 3) return _normalize(signal, peak=0.8) def write_wav(name, data): path = OUT_DIR / f"{name}.wav" # Convert float32 [-1, 1] to int16 int_data = (data * 32767).astype(np.int16) wavfile.write(str(path), SR, int_data) size_kb = path.stat().st_size / 1024 print(f" {name}.wav: {len(data)/SR:.3f}s, {size_kb:.1f}KB") if __name__ == "__main__": OUT_DIR.mkdir(parents=True, exist_ok=True) print("Generating drum samples...") write_wav("kick", make_kick()) write_wav("snare", make_snare()) write_wav("hihat", make_hihat()) write_wav("crash", make_crash()) write_wav("ride", make_ride()) write_wav("tom_high", make_tom(freq_base=200, dur=0.25)) write_wav("tom_low", make_tom(freq_base=100, dur=0.4)) print("Done!")