mroctopus / scripts /generate_drums.py
Ewan
Sample-based drum kit and tighter rhythm snapping
8fbe43c
"""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!")