Files
soothe2-re/model_lut.py
T

109 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""model_lut.py — КАНОНИЧЕСКАЯ модель B.11 (2026-08-18).
Единая непараметрическая LUT-кривая "уровень -> маска" для ВСЕХ уровней входа.
МОДЕЛЬ (verify, rmse=0.072 dB на 36 точках — dual_b1q 22 + t1kq fc-скан 7
+ t1k fc-скан 7 при 3 уровнях входа: -7.14 / -18.06 / 0 dBFS):
red(f) = -20*log10(1 - C(f))
C(f) = depth * tilt(f) * LUT(log10(L0 / res(f)))
res(f) = |2·B/A|(f; fc, Q, gain) case8/m2c (freq-path, близнец 0x180535880)
LUT = PCHIP-узлы (x=log10(L0/res), y=нормированная маска), таблица ниже
depth = 0.8639736175537109
L0 = линейный уровень входа (dual 10^(-7.142/20), t1kq 10^(-18.063/20), t1k 1.0)
tilt(f)= 1 - w(f) per-bin level-вес FUN_180530d30 (500/1000/2000: 1.414/1.454/1.795)
СВОЙСТВА (открытие B.11):
- Степенной закон C = D0·(L0/res)^p (B.10, p=0.0847) НЕ описывает высокий уровень
(0 dBFS): наклон d(ln C)/d(dB) падает с уровнем -> LUT компрессивная (насыщается).
- Кривая монотонна, с "коленом" при x≈0.58 (скачок 0.56 -> 0.65) и асимптотами
y->0.44 (низкий уровень) / y->0.67 (высокий уровень).
- Форма резонанса (Q=0.900, gain=4.132, tilt) ОДИНАКОВА для всех уровней;
различие между dual/t1kq/t1k = только положение x = L0/res на LUT-кривой.
- Подтверждает структурную модель level-path: per-bin уровень (0x540678 IIR-трекеры
0x540528..) -> LUT-кривая param_1+0x188 (FUN_180563440: mn+(mx-mn)·x^(1/gamma))
-> маска, применяемая с per-bin весами (0x530d30) в FUN_180529fe0.
"""
import numpy as np
from scipy.interpolate import PchipInterpolator
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)])
FCS = [800.0, 900.0, 950.0, 1000.0, 1050.0, 1100.0, 1200.0]
T1KQ = np.array([7.868, 8.536, 8.726, 8.788, 8.729, 8.575, 8.115])
T1K = np.array([14.548, 15.332, 15.553, 15.626, 15.557, 15.378, 14.840])
L0_DUAL = 10 ** (-7.142 / 20)
L0_T1KQ = 10 ** (-18.063 / 20)
L0_T1K = 1.0
TILT = {500: 1.414, 1000: 1.454, 2000: 1.795}
Q_FIT, GAIN_FIT = 0.900, 4.132
# узлы LUT (x=log10(L0/res), y=норм. маска) — фит по 36 точкам, rmse=0.072
LUT_KNOTS_X = np.array([-0.750, -0.500, -0.250, 0.000, 0.250, 0.500, 0.574, 0.610, 0.750, 1.000])
LUT_KNOTS_Y = np.array([0.4402, 0.4552, 0.4813, 0.5072, 0.5329, 0.5332, 0.5645, 0.6471, 0.6562, 0.6670])
def lut(x):
p = PchipInterpolator(LUT_KNOTS_X, LUT_KNOTS_Y)
v = p(np.asarray(x))
return np.clip(v, LUT_KNOTS_Y[0], LUT_KNOTS_Y[-1])
def res_at(ft, 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 * ft / 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 red(f_tone, fc, Q, gain, L0, tilt):
r = res_at(f_tone, fc, Q, gain)
C = DEPTH * tilt * lut(np.log10(L0 / r))
return -20 * np.log10(max(1 - C, 1e-9))
def run():
preds, meas = [], []
print('--- dual_b1q (tones 500+2000, band fc=500, -7.142 dBFS) ---')
for i, q in enumerate(QS):
for f, m in ((500.0, DUAL[i, 0]), (2000.0, DUAL[i, 1])):
p = red(f, 500.0, q, GAIN_FIT, L0_DUAL, TILT[f])
preds.append(p); meas.append(m)
print(f'q={q:5.1f} 500 {DUAL[i,0]:7.3f}/{preds[2*i]:7.3f} '
f'2000 {DUAL[i,1]:7.3f}/{preds[2*i+1]:7.3f}')
print('--- t1kq fc-скан (-18.06 dBFS, q=0.9999978) ---')
for i, fc in enumerate(FCS):
p = red(1000, fc, 0.9999978, GAIN_FIT, L0_T1KQ, TILT[1000])
preds.append(p); meas.append(T1KQ[i])
print(f'fc={fc:5.0f} {T1KQ[i]:6.3f}/{p:6.3f} ({p - T1KQ[i]:+.3f})')
print('--- t1k fc-скан (0 dBFS, q=0.9999978) ---')
for i, fc in enumerate(FCS):
p = red(1000, fc, 0.9999978, GAIN_FIT, L0_T1K, TILT[1000])
preds.append(p); meas.append(T1K[i])
print(f'fc={fc:5.0f} {T1K[i]:6.3f}/{p:6.3f} ({p - T1K[i]:+.3f})')
preds = np.array(preds); meas = np.array(meas)
rmse = np.sqrt(np.mean((preds - meas) ** 2))
print(f'\nTOTAL rmse={rmse:.4f} dB (n={len(meas)})')
print(f'dual-only rmse={np.sqrt(np.mean((preds[:22]-meas[:22])**2)):.4f}')
print(f't1kq rmse={np.sqrt(np.mean((preds[22:29]-meas[22:29])**2)):.4f}')
print(f't1k rmse={np.sqrt(np.mean((preds[29:]-meas[29:])**2)):.4f}')
if __name__ == '__main__':
run()