Initial commit: soothe2 RE workspace + roadmap
- Инфраструктура: RTTI-дампы (rtti_dsp/full.json), декомпиляции DSP-классов (decomp_*.txt), Ghidra-скрипты (Dump*.java, ImportRtti*.java, Diag.java), depthcurve/curve_fits LUT. - Поведенческая модель sim_v5.py + sim.py (RMSE ~0.3dB), утилиты (measure, tt_sweep, patchparam, sweep, harness_*, verify_sim). - summary.md + roadmap.md (контракт из manual M1-M12, топология пайплайна из OCR, фазы A-C). - pipeline_ocr.txt: OCR диаграммы Appendix A (mid/side ручка, trim/mix/bypass порядок).
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""soothe2 поведенческий симулятор.
|
||||
Модель: out = x − amount(t)·bp(x; fc, Q)
|
||||
bp — 2nd-order bandpass, пик gain 1.0 на fc; Q из selectivity.
|
||||
amount(t) = amt_total·env(t), env — one-pole со скоростью attack (в рост) / release (в спад).
|
||||
env управляется энергией полосы (детектор): bp-loudness > порога → целимся в amt, иначе в 0.
|
||||
Кривые из fit_curves.py / измерения.
|
||||
"""
|
||||
import numpy as np
|
||||
import wave
|
||||
|
||||
def _lut_sat(xs, ys, x):
|
||||
return np.interp(x, xs, ys, left=ys[0], right=ys[-1])
|
||||
|
||||
# --- измеренные кривые как LUT (максимальная точность) ---
|
||||
_DEPTH_LUT = (np.array([0.5, 0.864, 1.0, 2.0, 3.0, 5.0, 10.0, 20.0]),
|
||||
np.array([0.5605, 0.5879, 0.5977, 0.6673, 0.7274, 0.8199, 0.9387, 0.9893]))
|
||||
_SENS_LUT = (np.array([0.0, 6.0, 12.0, 24.0, 48.0]),
|
||||
np.array([0.4814, 0.7597, 1.0, 1.0, 1.0]))
|
||||
_SHARP_LUT = (np.array([1.0, 3.0, 5.0, 10.0, 20.0]),
|
||||
np.array([0.2041, 0.5913, 0.8386, 1.0, 1.0]))
|
||||
|
||||
def amount_total(depth=0.864, sens=12.0, sharp=10.0, mode=1):
|
||||
A = _lut_sat(*_DEPTH_LUT, depth)
|
||||
S = _lut_sat(*_SENS_LUT, sens)
|
||||
H = _lut_sat(*_SHARP_LUT, sharp)
|
||||
M = 0.745 if mode == 0 else 1.0
|
||||
return A * S * H * M
|
||||
|
||||
def read_wav(path):
|
||||
w = wave.open(path, 'rb')
|
||||
sw, nc, n = w.getsampwidth(), w.getnchannels(), w.getnframes()
|
||||
d = np.frombuffer(w.readframes(n), dtype=np.uint8).reshape(n, nc, sw)
|
||||
w.close()
|
||||
ch = d[:, 0, :]
|
||||
if sw == 3:
|
||||
v = (ch[:, 0].astype(np.int64) | (ch[:, 1].astype(np.int64) << 8) |
|
||||
(ch[:, 2].astype(np.int64) << 16))
|
||||
v = (v ^ (1 << 23)) - (1 << 23)
|
||||
return v.astype(np.float64) / (1 << 23)
|
||||
elif sw == 2:
|
||||
v = (ch[:, 0].astype(np.int64) | (ch[:, 1].astype(np.int64) << 8))
|
||||
v = (v ^ (1 << 15)) - (1 << 15)
|
||||
return v.astype(np.float64) / (1 << 15)
|
||||
else:
|
||||
v = ch[:, 0].astype(np.float64)
|
||||
return (v - 128) / 128.0
|
||||
|
||||
def write_wav(path, x, sr=44100):
|
||||
x = np.clip(x, -1, 1)
|
||||
frames = (x.astype(np.float64) * (1 << 23)).astype(np.int64)
|
||||
out = np.empty((frames.size, 1, 3), dtype=np.uint8)
|
||||
out[:, 0, 0] = (frames & 0xFF).astype(np.uint8)
|
||||
out[:, 0, 1] = ((frames >> 8) & 0xFF).astype(np.uint8)
|
||||
out[:, 0, 2] = ((frames >> 16) & 0xFF).astype(np.uint8)
|
||||
w = wave.open(path, 'wb')
|
||||
w.setnchannels(1); w.setsampwidth(3); w.setframerate(sr)
|
||||
w.writeframes(np.ascontiguousarray(out).tobytes()); w.close()
|
||||
|
||||
def tau_attack(attack):
|
||||
return 0.020 * np.exp(attack / 1.955) # a=0→20ms, a=5→258ms, a=10→3.3s
|
||||
|
||||
def tau_release(release):
|
||||
# измеренные tau (экспоненциальный фит спада к floor 0.2dB)
|
||||
xs = np.array([0.0, 1.0, 2.0, 5.0, 10.0, 100.0])
|
||||
ys = np.array([0.027, 0.08, 0.11, 0.14, 15.0, 15.0])
|
||||
return float(np.interp(release, xs, ys))
|
||||
|
||||
def q_from_sel(sel):
|
||||
# лучший фит ширины (probes-зонд): sel1→3.5, sel8→6.0 (2-порядка полоса)
|
||||
return 3.0 + 0.36 * min(sel, 10.0)
|
||||
|
||||
# --- bandpass biquad: peak gain = 1.0 на fc (RBJ normalized) ---
|
||||
def bp_coeffs(fc, q, sr):
|
||||
w0 = 2 * np.pi * fc / sr
|
||||
alpha = np.sin(w0) / (2 * q)
|
||||
b = np.array([alpha, 0.0, -alpha]) / alpha # [1, 0, -1]
|
||||
a = np.array([1 + alpha, -2 * np.cos(w0), 1 - alpha])
|
||||
an = a / a[0] # normalize denominator: a[0]=1
|
||||
# численное сканирование → норм на пик=1 (fn scale)
|
||||
w = np.linspace(w0 * 0.4, w0 * 1.6, 8000)
|
||||
H = np.abs(b[0] / a[0] * (1 - np.exp(-2j * w)) /
|
||||
(1 + an[1] * np.exp(-1j * w) + an[2] * np.exp(-2j * w)))
|
||||
G = H.max()
|
||||
bn = b / a[0] / G
|
||||
return bn, an
|
||||
|
||||
def apply_bp(x, fc, q, sr):
|
||||
b, a = bp_coeffs(fc, q, sr)
|
||||
b0, b1, b2 = b
|
||||
a1, a2 = a[1], a[2]
|
||||
y = np.zeros_like(x)
|
||||
z1 = z2 = 0.0
|
||||
for i in range(x.size):
|
||||
v = x[i]
|
||||
out = b0 * v + z1
|
||||
z1 = b1 * v - a1 * out + z2
|
||||
z2 = b2 * v - a2 * out
|
||||
y[i] = out
|
||||
return y
|
||||
|
||||
def simulate(x, sr=44100, fc=500.0, depth=0.864, sens=12.0, sharp=10.0,
|
||||
sel=10.0, mode=1, attack=0.0, release=0.0, mix=100.0, thr=0.010):
|
||||
amt = amount_total(depth, sens, sharp, mode)
|
||||
q = q_from_sel(sel)
|
||||
ta, tr = tau_attack(attack), tau_release(release)
|
||||
a_alpha = 1 - np.exp(-1.0 / (ta * sr))
|
||||
r_alpha = 1 - np.exp(-1.0 / (tr * sr))
|
||||
bp = apply_bp(x, fc, q, sr)
|
||||
# детектор: мгновенная амплитуда полосы → smoothed loudness
|
||||
if len(x) > 0:
|
||||
loud = np.abs(bp)
|
||||
N = int(0.005 * sr)
|
||||
win = np.ones(N) / N
|
||||
loud = np.convolve(loud, win, mode='same')
|
||||
env = 0.0
|
||||
out = np.zeros_like(x)
|
||||
lp = 0.0
|
||||
for i in range(x.size):
|
||||
tar = amt if loud[i] > thr else 0.0
|
||||
env += (a_alpha if tar > env else r_alpha) * (tar - env)
|
||||
out[i] = x[i] - env * bp[i]
|
||||
if mix < 100:
|
||||
out = (100 - mix) / 100.0 * x + mix / 100.0 * out
|
||||
return out
|
||||
Reference in New Issue
Block a user