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:
2026-08-16 18:54:40 +03:00
parent 45a13dcebd
commit 25cf786c54
44 changed files with 18208 additions and 0 deletions
+125
View File
@@ -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