B.7: XML mode=1 исполняет case8 (Q=+0x80c, fvar=sens/20), не case1; Q_eff-маппинг

This commit is contained in:
2026-08-18 10:06:25 +03:00
parent cbe7da9698
commit 33a36bfe68
3 changed files with 217 additions and 3 deletions
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""bandshape.py — эмпирическая форма полосы детектора из рендеров soothe2.
Тон 1 кГц (tone1kq.wav / tone1k.wav), одна полоса b1 с on=1, sens=12,
freq=f_c меняется по серии t1kq_only1_<fc> / t1k_b1f_<fc>.
Глубина подавления тона при стабильном состоянии = -20log10(|H(f=1k)|) полосы.
Кривая D(f_c) = форма |H(f_c)| (Q-ширина), нормируется на D(1000).
Результаты: stdout "fc D_dB D_norm_dB", а также печать подбора к моделям
case1 (Q=0.707) и case8 (Q=0.9999978542327881) с хелпером cfa.
"""
import numpy as np, wave, glob, os, sys, re
SR = 44100
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)
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)
def rms_db(x, t0, t1):
s0, s1 = int(t0 * SR), int(t1 * SR)
seg = x[s0:s1]
if seg.size == 0: return -999.0
return 20.0 * np.log10(np.sqrt(np.mean(seg ** 2)) + 1e-12)
def band1_freq(rpp):
data = open(rpp, 'rb').read()
s = data.decode('utf-8', 'replace')
i = s.find('VST3: soothe2')
line = s[i:].split('\n', 1)[1]
j = line.find('\n >')
import base64
b = re.sub(r'[^A-Za-z0-9+/=]', '', line[:j])
d = base64.b64decode(b)
for m in re.finditer(r'<PARAM id="([^"]+)" value="([^"]+)"', d.decode('utf-8', 'replace')):
if m.group(1) == 'band1 freq':
return float(m.group(2))
return None
def measure_series(prefix, dry_path, freqs, t0=1.6, t1=5.5, tag=''):
dry = read_wav(dry_path)
rows = []
for fc in freqs:
rpp = f'/home/m/soothe-bt/{prefix}_{fc}.rpp'
wav = f'/home/m/soothe-bt/{prefix}_{fc}.wav'
if not (os.path.exists(rpp) and os.path.exists(wav)):
continue
fcr = band1_freq(rpp)
fx = read_wav(wav)
n = min(dry.size, fx.size)
red = rms_db(dry[:n], t0, t1) - rms_db(fx[:n], t0, t1)
rows.append((fcr if fcr else fc, red))
rows.sort()
if not rows:
print(f'[{tag}] no data')
return []
center = min(rows, key=lambda r: abs(r[0] - 1000.0))
for fc, red in rows:
print(f'[{tag}] fc={fc:10.3f} D={red:7.3f} dB norm={red-center[1]:7.3f} dB')
return rows
if __name__ == '__main__':
measure_series('t1kq_only1',
'/home/m/soothe-bt/tone1kq.wav',
[800, 900, 950, 980, 1000, 1020, 1050, 1100, 1200], tag='t1kq_only1')
print()
measure_series('t1k_b1f',
'/home/m/soothe-bt/tone1k.wav',
[500, 800, 900, 950, 1000, 1050, 1100, 1200, 1500, 2000], tag='t1k_b1f')
print()
measure_series('t1kq_b1f',
'/home/m/soothe-bt/tone1kq.wav',
[500, 800, 900, 950, 1000, 1050, 1100, 1200, 1500, 2000], tag='t1kq_b1f')
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""fit_bandshape.py — подбор модели формы полосы к измеренной кривой.
Модели (из декомпиляции, /tmp/det/detector_decoded.md):
case1 (mode=1 bell): FUN_1805343e0(fs, fc, Q=0.707)
w = 1/FUN_181a14cfa(fc*pi/fs); k = (1/Q)*w; w2=w*w; a=1/(k+1+w2)
A=[a,2a,a]; B=[1,(1-w2)*2a,(1-k+w2)*a]
case8 (RBJ bell+gain): FUN_180533ec0(fs, fc, Q=0.9999978542327881, gain=10^(sens/20))
w0=fc*2pi/fs; c=cos(w0); s=sin(w0); p=(c*0.5)/Q
alpha=p*gain; alpha2=p/gain
A=[alpha+1, s*(-0.5), 1-alpha]; B=[alpha2+1, s*(-0.5), 1-alpha2]
Передаточная функция H(f) = |(A0+A1 z^-1+A2 z^-2)/(B0+B1 z^-1+B2 z^-2)|, z=e^{j2pi f/fs}.
Кривая подавления тона f_tone при полосе на fc: D(fc) = -20log10(|H(f_tone)|), норм.
"""
import numpy as np
from bandshape import measure_series
FS = 44100.0
def cfa_candidates(x):
return {
'sin': np.sin(x),
'sqrt': np.sqrt(x),
'tan': np.tan(x),
'asin': np.arcsin(np.clip(x, -1, 1)),
'csc': 1.0/np.sin(x),
'cot': 1.0/np.tan(x),
'none': x,
}
def cfa_fn(name):
return cfa_candidates(1.0)[name].__class__ if False else {
'sin': np.sin, 'sqrt': np.sqrt, 'tan': np.tan,
'asin': lambda x: np.arcsin(np.clip(x, -1, 1)),
'csc': lambda x: 1.0/np.sin(x),
'cot': lambda x: 1.0/np.tan(x),
'none': lambda x: x,
}[name]
def h_mag(A, B, f, fs=FS, zsign=-1.0):
z = np.exp(zsign*2j*np.pi*f/fs)
num = B[0] + B[1]*z + B[2]*z**2
den = A[0] + A[1]*z + A[2]*z**2
return np.abs(2.0*num/den)
def model_case1(fc, f_tone, Q, cfa, fs=FS):
x = fc*np.pi/fs
w = 1.0/cfa(x)
k = (1.0/Q)*w
w2 = w*w
a = 1.0/(k+1.0+w2)
A = [a, 2*a, a]
B = [1.0, (1.0-w2)*2*a, (1.0-k+w2)*a]
return h_mag(A, B, f_tone, fs)
def model_case8(fc, f_tone, Q, gain, fs=FS):
w0 = fc*2*np.pi/fs
c, s = np.cos(w0), np.sin(w0)
p = (c*0.5)/Q
alpha = p*gain
alpha2 = p/gain
A = [alpha+1.0, s*(-0.5), 1.0-alpha]
B = [alpha2+1.0, s*(-0.5), 1.0-alpha2]
return h_mag(A, B, f_tone, fs)
def norm_db(d):
d = np.asarray(d, float)
return d - d[np.argmax(d)]
def fit(rows, pred_fn, f_tone=1000.0):
fc = np.array([r[0] for r in rows])
meas = np.array([r[1] for r in rows])
pred = np.array([pred_fn(f, f_tone) for f in fc])
pred_db = norm_db(-20*np.log10(np.maximum(pred, 1e-12)))
meas_db = norm_db(meas)
se = np.mean((pred_db - meas_db)**2)
rmse = np.sqrt(se)
return rmse, pred_db, meas_db, fc
def run(rows, tag):
print(f'=== {tag} ===')
best = []
cfa_fns = cfa_fn
# case1
for qname, q in [('Q0.707', 0.707), ('Q1.0', 1.0), ('Q0.99999', 0.9999978542327881)]:
for cfa_name in ['sin', 'sqrt', 'tan', 'asin', 'csc', 'cot', 'none']:
def make(cfa_name, q):
def f_(fc, ft):
return model_case1(fc, ft, q, cfa_fns(cfa_name))
return f_
rmse, p, m, fc = fit(rows, make(cfa_name, q))
best.append((rmse, f'case1 {qname} cfa={cfa_name}', p))
# case8
for qname, q in [('Q0.99999', 0.9999978542327881), ('Q1.0', 1.0), ('Q0.707', 0.707)]:
for gain in [10.0**(12.0/20.0), 1.0]:
def make(q, gain):
def f_(fc, ft):
return model_case8(fc, ft, q, gain)
return f_
rmse, p, m, fc = fit(rows, make(q, gain))
best.append((rmse, f'case8 {qname} gain={gain:.2f}', p))
best.sort()
print(f'{"model":45s} rmse')
for rmse, name, _ in best[:8]:
print(f'{name:45s} {rmse:.4f}')
print()
# best curve vs meas
rmse, name, p = best[0]
m = np.array([r[1] for r in rows])
print(f'best={name} rmse={rmse:.4f}')
for i, r in enumerate(rows):
print(f' fc={r[0]:8.1f} meas={m[i]:7.3f} model={p[i]:7.3f} dB')
return best
if __name__ == '__main__':
rows = measure_series('t1kq_only1',
'/home/m/soothe-bt/tone1kq.wav',
[800, 900, 950, 980, 1000, 1020, 1050, 1100, 1200], tag='t1kq_only1')
run(rows, 't1kq_only1')
+14 -3
View File
@@ -334,6 +334,17 @@
- **Матем. хелперы 0x181a14xxx — IAT-стабы** (`JMP [0x181bab3xx]`), цели 0x6fffff... вне дампа - **Матем. хелперы 0x181a14xxx — IAT-стабы** (`JMP [0x181bab3xx]`), цели 0x6fffff... вне дампа
(грузит плагин). Семантика `FUN_181a14cfa` не подтверждена: кандидаты sin/sqrt/tan — (грузит плагин). Семантика `FUN_181a14cfa` не подтверждена: кандидаты sin/sqrt/tan —
проверяется на харнессе против baseline (corr 0.99475/SNR 19.80/diff 0.065). проверяется на харнессе против baseline (corr 0.99475/SNR 19.80/diff 0.065).
- **ОТКРЫТЫЙ ВОПРОС**: какой case фактически исполняется для band1 при XML mode=1 - **ОТВЕТ НА ВОПРОС (c): XML mode=1 реально исполняет case8 (mode=8/default), НЕ case1**.
(case1: Q=0.707 фикс vs case8: Q из +0x80c + gain) — нужен замер. Вопрос (c): factory default Доказательство — серия `dual_b1q_*` (тон 500+2000, band1 на 500, sens=12, mode=1):
band mode=8 (0x18052f306), а XML mode=1 — где маппинг. подавление @2000 (red@500≈10.2 насыщена и q-независима) монотонно убывает с q:
q=0.1→+5.00 dB, 0.3→+2.73, 0.5→+1.5, 0.7→+0.9, 1.0→+0.47, 1.5→+0.09, 2.0→−0.04,
3.0→−0.06. case1 (фикс Q=0.707) q НЕ читает → исключён. case8 (Q=+0x80c, fvar=0.6=sens/20):
delta(2000)=[4.94,1.9,0.85,0.46,0.24,0.11,0.06,0.03] rmse≈0.42, верный знак/направление
(завышен при q≥0.5 — модель шире факта). Свободный Q_eff(q): ratio≈0.70.95 при q≤1,
→ рост при q≥1.5 (Δ<0 — вне bell). Структура `FUN_180533ec0` подтверждена.
mode4-путь (Q=0.5 фикс, fvar=(q0.707)/2+1) даёт ОТРИЦАТЕЛЬНЫЙ тренд (delta 0.57→−5.45) —
несовместим. Вывод: case8-структура (A=[1+α,2s,1−α], B=[1+α2,2s,1−α2], α=p·f, α2=p/f,
p=0.5·cos(w0)/Q, fvar≈sens/20 или sin(sens/20)=0.565) — модель формы полосы; fvar-различение
sin vs x — на харнессе (Q_eff ratio 0.96/0.70 vs 1.25/0.75 при q=0.1/1.0).
Несоответствие формы t1kq_only (D(800)=0.92 при Q=0.99999; case8 даёт ~0) — полоса на тон
шире биквада → учёт WOLA/region-smoothing маски (п. B.4) + Q_eff-маппинг — следующий шаг.