Spaces:
Sleeping
Sleeping
| """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!") | |