Files
MobileGL/intro-video/hype/dsp.py
T

213 lines
5.2 KiB
Python

"""Small numba DSP kit: band-limited oscillators, a TPT state-variable filter, a ping-pong delay, a
compressor and a look-ahead limiter. Everything runs on float64 mono arrays."""
import math
import numpy as np
from numba import njit
SR = 48000
@njit(cache=True)
def _blep(t, dt):
if t < dt:
t /= dt
return t + t - t * t - 1.0
if t > 1.0 - dt:
t = (t - 1.0) / dt
return t * t + t + t + 1.0
return 0.0
@njit(cache=True)
def saw(freq, phase0):
n = freq.shape[0]
out = np.empty(n)
p = phase0
for i in range(n):
dt = freq[i] / SR
out[i] = 2.0 * p - 1.0 - _blep(p, dt)
p += dt
if p >= 1.0:
p -= 1.0
return out
@njit(cache=True)
def pulse(freq, width, phase0):
n = freq.shape[0]
out = np.empty(n)
p = phase0
for i in range(n):
dt = freq[i] / SR
v = 1.0 if p < width else -1.0
v += _blep(p, dt)
q = p - width
if q < 0.0:
q += 1.0
v -= _blep(q, dt)
out[i] = v
p += dt
if p >= 1.0:
p -= 1.0
return out
@njit(cache=True)
def sine(freq, phase0):
n = freq.shape[0]
out = np.empty(n)
p = phase0
for i in range(n):
out[i] = math.sin(2.0 * math.pi * p)
p += freq[i] / SR
if p >= 1.0:
p -= 1.0
return out
@njit(cache=True)
def svf(x, cutoff, res, mode):
"""Zavalishin TPT SVF. cutoff: per-sample Hz array; res 0..1; mode 0=lp 1=bp 2=hp."""
n = x.shape[0]
out = np.empty(n)
ic1 = 0.0
ic2 = 0.0
k = 2.0 - 2.0 * min(res, 0.98)
for i in range(n):
fc = min(max(cutoff[i], 10.0), SR * 0.45)
g = math.tan(math.pi * fc / SR)
a1 = 1.0 / (1.0 + g * (g + k))
a2 = g * a1
a3 = g * a2
v3 = x[i] - ic2
v1 = a1 * ic1 + a2 * v3
v2 = ic2 + a2 * ic1 + a3 * v3
ic1 = 2.0 * v1 - ic1
ic2 = 2.0 * v2 - ic2
if mode == 0:
out[i] = v2
elif mode == 1:
out[i] = v1
else:
out[i] = x[i] - k * v1 - v2
return out
@njit(cache=True)
def onepole_hp(x, fc):
n = x.shape[0]
out = np.empty(n)
a = math.exp(-2.0 * math.pi * fc / SR)
yp = 0.0
xp = 0.0
for i in range(n):
yp = a * (yp + x[i] - xp)
xp = x[i]
out[i] = yp
return out
@njit(cache=True)
def pingpong(l, r, delay_s, fb, damp_hz):
n = l.shape[0]
d = int(delay_s * SR)
bl = np.zeros(d)
br = np.zeros(d)
ol = np.empty(n)
orr = np.empty(n)
a = math.exp(-2.0 * math.pi * damp_hz / SR)
zl = 0.0
zr = 0.0
j = 0
for i in range(n):
yl = bl[j]
yr = br[j]
ol[i] = yl
orr[i] = yr
zl = (1 - a) * yl + a * zl
zr = (1 - a) * yr + a * zr
bl[j] = 0.5 * (l[i] + r[i]) + fb * zr # mono in on the left tap, cross-feed
br[j] = fb * zl
j += 1
if j >= d:
j = 0
return ol, orr
@njit(cache=True)
def compress(l, r, thresh_db, ratio, attack, release, makeup_db):
n = l.shape[0]
ol = np.empty(n)
orr = np.empty(n)
aa = math.exp(-1.0 / (attack * SR))
ar = math.exp(-1.0 / (release * SR))
env = 0.0
mk = 10 ** (makeup_db / 20)
for i in range(n):
x = max(abs(l[i]), abs(r[i]))
if x > env:
env = aa * env + (1 - aa) * x
else:
env = ar * env + (1 - ar) * x
db = 20 * math.log10(env + 1e-9)
over = db - thresh_db
gr = 0.0
if over > 0:
gr = over * (1 - 1 / ratio)
g = 10 ** (-gr / 20) * mk
ol[i] = l[i] * g
orr[i] = r[i] * g
return ol, orr
@njit(cache=True)
def limit(l, r, ceiling, lookahead_s, release_s):
"""Look-ahead peak limiter: gain = min over the window ahead, smoothed on release."""
n = l.shape[0]
la = int(lookahead_s * SR)
need = np.ones(n)
for i in range(n):
p = max(abs(l[i]), abs(r[i]))
if p > ceiling:
need[i] = ceiling / p
# running minimum over the next `la` samples (simple deque-free two-pass block min)
gmin = np.ones(n)
for i in range(n - 1, -1, -1):
m = need[i]
if i + 1 < n:
# decay toward the earlier future minimum with a linear attack ramp over `la`
nxt = gmin[i + 1]
ramp = nxt + (1.0 - nxt) / la
if ramp < m:
m = ramp
gmin[i] = m
ar = math.exp(-1.0 / (release_s * SR))
g = 1.0
ol = np.empty(n)
orr = np.empty(n)
for i in range(n):
t = gmin[i]
if t < g:
g = t
else:
g = ar * g + (1 - ar) * t
ol[i] = l[i] * g
orr[i] = r[i] * g
return ol, orr
def mtof(m):
return 440.0 * 2.0 ** ((np.asarray(m, dtype=np.float64) - 69.0) / 12.0)
def adsr(n, a, d, s, r_start, r, sr=SR):
"""Envelope of n samples; release begins at sample r_start and lasts r seconds."""
t = np.arange(n) / sr
e = np.where(t < a, t / max(a, 1e-6), s + (1 - s) * np.exp(-(t - a) / max(d, 1e-6)))
rs = r_start / sr
if r_start < n:
lvl = e[min(r_start, n - 1)]
tail = t >= rs
e[tail] = lvl * np.exp(-(t[tail] - rs) / max(r / 4.0, 1e-6))
return e