Files
soothe2-re/framed_render.py
T

185 lines
7.0 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
"""framed_render.py — PILOT полного frame-рендера (Phase 5, step 5b).
Структура (зеркалит soothe):
STFT входа -> per-frame per-bin амплитуда (2|X_k|/wsum, twin-зрение)
-> сглаживание (attack ~11ms / release ~80ms, из рендеров: <=20ms)
-> B.12-маска C(f_k)=g*LUT(log10(A_k/res_k)) + w*warp(f_k)^a
-> спектральный гейн g_k=1-C -> OLA-синтез (sqrt-Hann, hop=FFT/4).
СТАТУС (2026-08-18, пилот): full-pipeline РАБОТАЕТ, динамика совпадает.
- steady dual: q0.1: 500 -9.66/-10.22, 2000 -14.08/-15.22; q1: 2000 -10.94/-10.68;
q10: 2000 -10.66/-10.16 (остатки = структурная маска 0.540658, не пайплайн).
- атака: lag 0 на старте, стационар к ~0.1s — совпадает с reference (лага нет).
- НАХОДКА (al_*, центр band fc=1000 sens=12, tone=1000, 0..-24dBFS):
lvl 0 -3 -6 -9 -12 -18 -24
red -104 -6.1 -7.9 -9.7 -11.6 -15.4 -19.5
xv=-0.269..0.931 (res_center=0.1171). => реальная LUT-нога НАМНОГО КРУЧЕ frozen-узлов
B.12 (cap 0.667): на xv=0.93 реальная C->1 (клиф -104), у B.12 лишь -9.5 dB.
КОНФЛИКТ: t1k fc-scan (fc=1000, 0dBFS) дал 15.6 dB при том же xv=0.931 -> одна
из премьюз неверна (вероятно вход/настройки fc-scan рендеров) — пересогласовать.
al_* = калибровочный датасет центральной LUT-ноги для замены frozen-узлов.
Стационарный тон: A_k/res_k = B.12 xv => формула = B.12 точно; остаток = маска.
"""
import sys
import numpy as np
from scipy.interpolate import PchipInterpolator
from render_parity import load, tone_amp
BT = '/home/m/soothe-bt/'
FS = 44100.0
GAIN = 4.132
G_FIT, W_FIT, A_FIT = 1.221, 0.358, 3.143
LX = np.array([-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.574, 0.61, 0.75, 1.0])
LY = np.array([0.4402, 0.4552, 0.4813, 0.5072, 0.5329, 0.5332, 0.5645, 0.6471, 0.6562, 0.6670])
LUT = PchipInterpolator(LX, LY)
def lut(x):
return np.clip(LUT(np.asarray(x)), LY[0], LY[-1])
def warp(f):
x = np.asarray(f) / 2000.0
return 0.87 * 7.942 * x / (7.942 + x)
def bandres(f, fc, Q):
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 * np.asarray(f) / 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 frames_gains(x, fc, Q, N=2048, hop=512, tatt=0.011, trel=0.08):
win = np.sqrt(np.hanning(N))
wsum = win.sum()
n = len(x)
nfr = max(1, int(np.ceil((n - N) / hop)) + 1)
X = np.empty((nfr, N // 2 + 1), dtype=np.complex128)
for m in range(nfr):
s = m * hop
seg = np.zeros(N)
k = min(N, n - s)
seg[:k] = x[s:s + k]
X[m] = np.fft.rfft(win * seg)
freqs = np.fft.rfftfreq(N, 1 / FS)
res = bandres(freqs, fc, Q)
att = np.exp(-hop / (tatt * FS))
rel = np.exp(-hop / (trel * FS))
am = np.zeros(freqs.size)
G = np.empty(X.shape)
for m in range(nfr):
a_cur = 2 * np.abs(X[m]) / wsum # per-bin input amplitude
am = np.where(a_cur > am, att * am + (1 - att) * a_cur,
rel * am + (1 - rel) * a_cur)
xv = np.log10(np.maximum(am / np.maximum(res, 1e-12), 1e-9))
C = G_FIT * lut(xv) + W_FIT * warp(freqs) ** A_FIT
G[m] = np.maximum(1 - C, 1e-9)
return X, G, win, hop, n
def synthe(X, G, win, hop, n):
out = np.zeros(n)
acc = np.zeros(n)
N = len(win)
for m in range(X.shape[0]):
seg = np.fft.irfft(X[m] * G[m]) * win
s = m * hop
lay = min(N, n - s)
out[s:s + lay] += seg[:lay]
acc[s:s + lay] += (win * win)[:lay]
return out / np.maximum(acc, 1e-12)
def env(x, f, win=4410, hop=882):
w = 2 * np.pi * f / FS
cw = 2 * np.cos(w)
out = []
for st in range(0, len(x) - win, hop):
s0 = s1 = s2 = 0.0
for v in x[st:st + win]:
s2 = s1
s1 = s0
s0 = v + cw * s1 - s2
out.append(np.sqrt(abs(s0 * s0 + s1 * s1 - 2 * cw * s0 * s1)) / win)
return np.array(out)
def run_case(inp, ref, fc, q, ft1, ft2=None):
x = np.mean(load(BT + inp), axis=1)
X, G, win, hop, n = frames_gains(x, fc, q)
y = synthe(X, G, win, hop, n)
r = np.mean(load(BT + ref), axis=1)
nmin = min(len(r), len(y))
to = tone_amp(wav_align(y), ft1)
rr = tone_amp(BT + ref, ft1)
print(f'{ref} tone{ft1}: ref_amp={rr:.4f} out_amp={to:.4f} '
f'redRef={dB(rr / tone_amp(BT + inp, ft1)):.2f} redOut={dB(to / tone_amp(BT + inp, ft1)):.2f}dB')
eo = env(y, ft1, 8820, 882)[:50]
er = env(r, ft1, 8820, 882)[:50]
k = len(eo)
a = dB_ratio(eo, er)
print(f' env dB-lag (out/ref): offset={a[0]:+.1f} rmse={np.sqrt(np.mean(a[1:5] ** 2)):.1f} (atto) '
f'steady={np.sqrt(np.mean(a[35:45] ** 2)):.1f}')
return y
def tone_amp_raw(x, f):
x = np.asarray(x, dtype=np.float64)
n = len(x)
w = 2 * np.pi * f / FS
cw = 2 * np.cos(w)
s0 = s1 = s2 = 0.0
for v in x:
s2 = s1
s1 = s0
s0 = v + cw * s1 - s2
return np.sqrt(abs(s0 * s0 + s1 * s1 - 2 * cw * s0 * s1)) / n
def wav_align(x):
return x
def dB(v):
return 20 * np.log10(np.clip(v, 1e-9, None))
def dB_ratio(a, b):
n = min(len(a), len(b))
return dB(np.clip(a[:n], 1e-9, None)) - dB(np.clip(b[:n], 1e-9, None))
if __name__ == '__main__':
modo = sys.argv[1] if len(sys.argv) > 1 else 'dual'
if modo == 'dual':
x = np.mean(load(BT + 'dual.wav'), axis=1)
for q, ref in [(0.1, 'dual_b1q_0.1.wav'), (1.0, 'dual_b1q_1.0.wav'), (10.0, 'dual_b1q_10.0.wav')]:
X, G, win, hop, n = frames_gains(x, 500.0, q, tatt=0.011, trel=0.08)
y = synthe(X, G, win, hop, n)
r = np.mean(load(BT + ref), axis=1)
for f in (500, 2000):
to = tone_amp_raw(y, f)
ti = tone_amp(BT + 'dual.wav', f)
tr = tone_amp(BT + ref, f)
print(f'{ref} tone{f}: redRef={dB(tr / ti):6.2f} redOut={dB(to / ti):6.2f} '
f'envRmse@steady={np.sqrt(np.mean(dB_ratio(env(y, f, 8820, 882)[35:45], env(r, f, 8820, 882)[35:45]) ** 2)):.2f}dB')
elif modo == 't1k':
x = np.mean(load(BT + 't1k_ref.wav'), axis=1)
X, G, win, hop, n = frames_gains(x, 1000.0, 0.9999978, tatt=0.011, trel=0.08)
y = synthe(X, G, win, hop, n)
r = np.mean(load(BT + 'b1only_12.wav'), axis=1)
to = tone_amp_raw(y, 1000)
ti = tone_amp(BT + 't1k.wav', 1000)
tr = tone_amp(BT + 'b1only_12.wav', 1000)
print(f'b1only_12 tone1000: redRef={dB(tr / ti):6.2f} redOut={dB(to / ti):6.2f}')
else:
print('usage: framed_render.py dual|t1k')