mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
213 lines
5.2 KiB
Python
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
|