98 lines
4.2 KiB
Python
98 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""model_dual.py — ВЕРИФИЦИРОВАННАЯ численная модель soothe2 (detector twin).
|
|
|
|
Совместный фит двух наборов (31 точка), общие параметры:
|
|
dual_b1q: red500/red2000, band fc=500, Q=0.1..10, sens=12, tones 500+2000 (-7.14 dBFS)
|
|
t1kq : fc-скан 800..1200, tone 1k (-18.06 dBFS), band Q=0.9999978, sens=12
|
|
rmse = 0.109 dB.
|
|
|
|
МОДЕЛЬ (декодирована из Ghidra, B.8/B.9/B.10):
|
|
red(f) = -20*log10(1 - C(f))
|
|
C(f) = depth * tilt(f) * D0 * (L0 / res(f))^p p ~= 0.085 (компрессивный LUT)
|
|
res(f) = |2*B/A|(f; fc, Q, gain) case8/m2c, freq-path близнеца 0x180535880
|
|
tilt(f)= 1 - w(f) per-bin level-вес из FUN_180530d30 (растёт с частотой)
|
|
depth = 0.8639736175537109 (параметр плагина)
|
|
L0 = линейный уровень входа (набор данных)
|
|
gain = 10^(sens/20) (~4.13 при sens=12)
|
|
|
|
Проверенные следствия:
|
|
- Q_eff(фит) ~= 0.90 vs истинный q=1.0 (в B.8 сырой |2B/A| давал Q_eff 1.39-1.48)
|
|
- tilt ratio(2000/500) = 1.27 совпадает с формулой 0x530d30 при L=0.43, d1=0.078
|
|
- глубина зависит от уровня входа L0 (dual -7dB vs t1kq -18dB в одних параметрах)
|
|
|
|
ПАРАДОКС dual_b1q РЕШЁН: red2000>red500 из-за частотного наклона tilt(f) (1-w),
|
|
резонанс входит как (L0/res)^p (excess над уровнем полосы), p=0.085 слабо-компрессивный.
|
|
"""
|
|
import numpy as np
|
|
from scipy.optimize import least_squares
|
|
|
|
FS = 44100.0
|
|
DEPTH = 0.8639736175537109
|
|
|
|
QS = [0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0]
|
|
DUAL = np.array([(10.220, 15.224), (10.219, 13.963), (10.219, 12.947),
|
|
(10.219, 11.725), (10.218, 11.119), (10.216, 10.689),
|
|
(10.210, 10.412), (10.203, 10.305), (10.182, 10.225),
|
|
(10.113, 10.183), (9.822, 10.165)])
|
|
T1KQ = np.array([(800, 7.868), (900, 8.536), (950, 8.726), (980, 8.779),
|
|
(1000, 8.788), (1020, 8.778), (1050, 8.729), (1100, 8.575),
|
|
(1200, 8.115)])
|
|
L0_DUAL = 10 ** (-7.142 / 20)
|
|
L0_T1KQ = 10 ** (-18.063 / 20)
|
|
|
|
|
|
def res_at(f_tone, fc, Q, gain):
|
|
w0 = fc * 2 * np.pi / FS
|
|
c, s = np.cos(w0), np.sin(w0)
|
|
p = (s * 0.5) / Q
|
|
a, a2 = p * gain, p / gain
|
|
A = [a + 1, -2 * c, 1 - a]
|
|
B = [a2 + 1, -2 * c, 1 - a2]
|
|
w = 2 * np.pi * f_tone / FS
|
|
z = np.exp(-1j * w)
|
|
return np.abs(2.0 * (B[0] + B[1] * z + B[2] * z * z) /
|
|
(A[0] + A[1] * z + A[2] * z * z))
|
|
|
|
|
|
def model(p):
|
|
Q, gain, pow_, D0, t500, t1000, t2000 = p
|
|
preds = []
|
|
for q in QS:
|
|
for f in (500.0, 2000.0):
|
|
r = res_at(f, 500, q, gain)
|
|
tilt = t500 if f < 1000 else t2000
|
|
c = DEPTH * tilt * D0 * (L0_DUAL / r) ** pow_
|
|
preds.append(-20 * np.log10(max(1 - c, 1e-9)))
|
|
for fc, _ in T1KQ:
|
|
r = res_at(1000, fc, 0.9999978, gain)
|
|
c = DEPTH * t1000 * D0 * (L0_T1KQ / r) ** pow_
|
|
preds.append(-20 * np.log10(max(1 - c, 1e-9)))
|
|
return np.array(preds)
|
|
|
|
|
|
def run():
|
|
meas = np.array(list(DUAL.ravel()) + list(T1KQ[:, 1]))
|
|
r = least_squares(lambda p: model(p) - meas,
|
|
[0.9, 4.24, 0.084, 0.5, 1.4, 1.5, 1.7],
|
|
bounds=([0.1, 0.5, 0.001, 0.01, 0.3, 0.3, 0.3],
|
|
[5, 12, 2, 5, 5, 5, 5]),
|
|
max_nfev=5000, xtol=1e-10, ftol=1e-10)
|
|
Q, gain, pow_, D0, t500, t1000, t2000 = r.x
|
|
rmse = np.sqrt(np.mean((model(r.x) - meas) ** 2))
|
|
print(f'JOINT FIT rmse={rmse:.4f} dB Q={Q:.3f} gain={gain:.3f} '
|
|
f'p={pow_:.4f} D0={D0:.3f}')
|
|
print(f'tilt: 500={t500:.3f} 1000={t1000:.3f} 2000={t2000:.3f} '
|
|
f'(ratio 2000/500={t2000 / t500:.3f})')
|
|
pred = model(r.x)
|
|
print('--- dual_b1q ---')
|
|
for i, q in enumerate(QS):
|
|
print(f'q={q:5.1f} red500 {DUAL[i, 0]:7.3f}/{pred[2 * i]:7.3f} '
|
|
f'red2000 {DUAL[i, 1]:7.3f}/{pred[2 * i + 1]:7.3f}')
|
|
print('--- t1kq fc-scan ---')
|
|
for i, (fc, m) in enumerate(T1KQ):
|
|
print(f'fc={fc:5.0f} {m:6.3f}/{pred[22 + i]:6.3f}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
run()
|