Files
homarr/tools/motion-reel/synth.py
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

221 lines
6.3 KiB
Python

"""Procedural instruments and effects for the reel soundtrack (128 BPM). score.py arranges them."""
import numpy as np
from scipy import signal
from scipy.io import wavfile
SR = 48000
BPM = 128
BEAT = 60 / BPM
BAR = 4 * BEAT
E8 = BEAT / 2
S16 = BEAT / 4
rng = np.random.default_rng(7)
def tt(d):
return np.arange(int(d * SR)) / SR
def noise(d):
return rng.standard_normal(int(d * SR))
def mtof(m):
return 440.0 * 2 ** ((m - 69) / 12)
NOTE = {n: i for i, n in enumerate(['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'])}
def nf(name): # 'A4' -> Hz
return mtof(12 * (int(name[-1]) + 1) + NOTE[name[:-1]])
def sos(kind, f, order=2):
return signal.butter(order, f, kind, fs=SR, output='sos')
def lp(x, f, order=2):
return signal.sosfilt(sos('low', f, order), x, axis=0)
def hp(x, f, order=2):
return signal.sosfilt(sos('high', f, order), x, axis=0)
def bp(x, lo, hi, order=2):
return signal.sosfilt(signal.butter(order, [lo, hi], 'band', fs=SR, output='sos'), x, axis=0)
def tv_biquad(x, fc, q=1.0, kind='bp', block=64):
"""Time-varying RBJ biquad; fc is an array (Hz) the length of x."""
y = np.zeros_like(x)
zi = np.zeros(2)
for i in range(0, len(x), block):
f = float(np.clip(fc[min(i + block // 2, len(fc) - 1)], 20, SR * 0.45))
w0 = 2 * np.pi * f / SR
al = np.sin(w0) / (2 * q)
c = np.cos(w0)
if kind == 'bp':
b = [al, 0, -al]
elif kind == 'lp':
b = [(1 - c) / 2, 1 - c, (1 - c) / 2]
else:
b = [(1 + c) / 2, -(1 + c), (1 + c) / 2]
a = [1 + al, -2 * c, 1 - al]
y[i:i + block], zi = signal.lfilter(np.array(b) / a[0], np.array(a) / a[0], x[i:i + block], zi=zi)
return y
def saw(freq, d, maxh=40, phase=0.0):
t = tt(d)
out = np.zeros_like(t)
K = int(min(maxh, (SR / 2 - 200) // freq))
for k in range(1, K + 1):
out += np.sin(2 * np.pi * k * freq * t + phase * k) / k
return out * (2 / np.pi)
def adsr(d, a=0.005, r=0.05, tau=None):
t = tt(d)
e = np.minimum(1, t / max(a, 1e-4))
if tau:
e = e * np.exp(-t / tau)
rel = np.clip((d - t) / r, 0, 1)
return e * rel
# ─────────────────────────── instruments ───────────────────────────
def kick(d=0.45):
t = tt(d)
f = 46 + (190 - 46) * np.exp(-t / 0.026)
body = np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-t / 0.26)
ck = hp(noise(0.012), 2500) * np.exp(-tt(0.012) / 0.0025) * 0.5
body[:len(ck)] += ck
return np.tanh(body * 1.8) * 0.85
def clap(d=0.4):
t = tt(d)
n = bp(noise(d), 900, 3200)
env = np.zeros_like(t)
for o in (0, 0.010, 0.021):
env += np.where(t >= o, np.exp(-(t - o) / 0.007), 0)
env += np.where(t >= 0.028, np.exp(-(t - 0.028) / 0.11), 0) * 0.7
return n * env * 0.45
def snare(d=0.25):
t = tt(d)
tone = np.sin(2 * np.pi * (190 + 60 * np.exp(-t / 0.02)) * t) * np.exp(-t / 0.06)
n = bp(noise(d), 1500, 9000) * np.exp(-t / 0.09)
return (tone * 0.5 + n * 0.6) * 0.6
def hat(d=0.08, open_=False):
n = hp(noise(d), 7500, 4)
return n * np.exp(-tt(d) / (0.11 if open_ else 0.02)) * 0.22
def bass(freq, d):
t = tt(d)
s = saw(freq, d, 24) * 0.55 + np.sin(2 * np.pi * freq * t) * 0.9
s = lp(s, 420)
return np.tanh(s * adsr(d, 0.004, 0.03, 0.5) * 1.4) * 0.7
def pad(freqs, d, cutoff=1700, a=0.12, r=0.35):
L = np.zeros(int(d * SR))
R = np.zeros(int(d * SR))
for f in freqs:
for k, det in enumerate((-9, -3, 4, 10)):
s = saw(f * 2 ** (det / 1200), d, 30, phase=rng.uniform(0, 6))
(L if k % 2 == 0 else R)[:] += s
st = np.stack([L, R], 1) / (len(freqs) * 2)
st = lp(st, cutoff)
return st * adsr(d, a, r)[:, None]
def pluck(freq, d=0.5, bright=1.0):
t = tt(d)
out = np.zeros_like(t)
K = int(min(16, (SR / 2 - 500) // freq))
for k in range(1, K + 1):
out += np.sin(2 * np.pi * k * freq * t) / k * np.exp(-t * (4 + k * 7 / bright))
return out * np.minimum(1, t / 0.002) * 0.5
def bell(freq, d=1.2):
t = tt(d)
out = sum(a * np.sin(2 * np.pi * freq * r * t) * np.exp(-t / dcy) for r, a, dcy in
((1, 1, 0.9), (2.0, 0.45, 0.5), (3.01, 0.25, 0.3), (4.2, 0.15, 0.18), (5.43, 0.08, 0.1)))
return out * np.minimum(1, t / 0.002) * 0.35
def impact(d=2.2, big=1.0):
t = tt(d)
f = 28 + (95 - 28) * np.exp(-t / 0.22)
boom = np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-t / (0.55 * big))
nz = lp(noise(d), 2200) * np.exp(-t / 0.22) * 0.55
crack = hp(noise(d), 2500) * np.exp(-t / 0.045) * 0.5
return np.tanh((boom * 1.3 + nz + crack) * 1.4) * 0.9
def crash(d=1.6):
t = tt(d)
n = lp(hp(noise(d), 4500, 3), 12000) * np.exp(-t / 0.42)
n2 = lp(hp(noise(d), 4500, 3), 12000) * np.exp(-t / 0.42)
return np.stack([n, n2], 1) * 0.11
def whoosh(d, f0, f1, q=1.4, curve=2.0, pan0=0.0, pan1=0.0, peak=0.5):
t = tt(d)
x = t / d
fc = f0 * (f1 / f0) ** x
n = tv_biquad(noise(d), fc, q, 'bp')
env = np.where(x < peak, (x / peak) ** curve, ((1 - x) / (1 - peak)) ** 1.5)
s = n * env
pan = pan0 + (pan1 - pan0) * x
th = (pan + 1) * np.pi / 4
return np.stack([s * np.cos(th), s * np.sin(th)], 1) * np.sqrt(2) * 0.9
def riser(d, f0=250, f1=2400):
t = tt(d)
x = t / d
fc = f0 * (f1 / f0) ** x
n = tv_biquad(noise(d), fc * 3, 2.5, 'bp')
tone = np.sin(2 * np.pi * np.cumsum(fc) / SR) * 0.25
env = x ** 2.2
return (n * 0.7 + tone) * env
def revcym(d):
x = tt(d) / d
n = hp(noise(d), 3500, 3)
return np.stack([n, hp(noise(d), 3500, 3)], 1) * (x ** 3)[:, None] * 0.35
def click(freq=3200, d=0.03, tau=0.005):
t = tt(d)
return (np.sin(2 * np.pi * freq * t) * np.exp(-t / tau) + hp(noise(d), 4000) * np.exp(-t / 0.0015) * 0.4) * 0.6
def pop(f0=380, f1=950, d=0.12):
t = tt(d)
f = f0 + (f1 - f0) * (1 - np.exp(-t / 0.02))
return np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-t / 0.04) * 0.6
def thud(d=0.3):
t = tt(d)
f = 55 + 110 * np.exp(-t / 0.03)
return (np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-t / 0.09) + lp(noise(d), 900) * np.exp(-t / 0.03) * 0.3) * 0.8
def glide(f0, f1, d):
t = tt(d)
f = f0 * (f1 / f0) ** (t / d)
return np.sin(2 * np.pi * np.cumsum(f) / SR) * adsr(d, 0.02, 0.1) * 0.3