Files
soothe2-re/handoff/NOTES_LEVEL.md
T
Matiq 28cd3b7c62 Q-dep rp + honest TRIMMED metric: mean=0.175 (was misleading 0.144 artifact)
- Exposed artifact: earlier fits measured tone_cmp on untracked-length output
  (nfr*HOP+N); framed_render.synthe trims to len(x). Honest scalar-rp=0.0169
  baseline is 0.280, not 0.160 as previously recorded.
- New model: rp(Q)=rp0*Q^drp, G/W/A/rp0/drp=0.9963/0.3335/0.9807/0.0275/0.2159.
  Trimmed errs: q0.1 0.000/0.001, q1 -0.256/-0.707, q10 +0.086/+0.001.
  q0.1+q10 perfect; bottleneck q1@2000 (structural warp mismatch).
2026-08-19 15:48:56 +03:00

559 lines
43 KiB
Markdown
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.
## ============ 2026-08-18 (LEVEL-PATH STRUCTURE DECODED) ============
## Две twin-цепочки (аудио vs маска-таблица)
(a) FUN_180536300 (float, N=0x200=512, per-channel AUDIO):
вызван FUN_18058e380: FUN_180536300(state+0x18, in+0x8e0+ch*0x800, in+0xe0, band, 0x200).
lVar1=scratch+N*32; normalize lVar1[i]=in[i]*2pi/(os*sr) (0x18052da00=pointwise scale);
per band: FUN_180536f90(lVar2, base, band_i, lVar1, N) -> copy lVar2->base(0x1800096c0);
base*=product (0x180008700); if band_active: save->lVar4, product=1.0; out=copy lVar4->param_2.
=> out = PROD_active_bands mask_band(норм. аудио). На |x|<1 z=ротор(1e-5)=i -> маска ~пост.
(b) FUN_18056e3e0 (double, N=0x400=1024): вызван FUN_180563440 (6 band-слотов). Аналогично.
Вход param_3 в контексте FUN_180563440 = LUT-рампа [0,1] (param_1+0x198, шаг 1/1023),
shape через кривую param_1+0x188 => это УРОВНЕВАЯ LUT-маска, не аудио.
## FUN_180563440 (per-block маска-таблица, 6 полос)
- param_1+0x198[i] = i*(1/1023), затем маппинг кривой param_1+0x188:
pfVar10[0]=min, [1]=max, [3]=gamma-кривая, [4]=флаг linear, [+0x14]=nodes.
linear: val = min+(max-min)*exp(log(x)/c) = min+(max-min)*x^(1/c) (gamma).
- 6x FUN_18056e3e0 -> 6 полосных масок (param_1+0x4198+0x2000k); комбайн -> param_1+0x2198;
затем LUT (param_1+0x98): пары (x=i*fVar3, y=mask) на 0x3ff точек (уровень->маска).
- вызывается FUN_1805631c0 (vtable 0x1824b14f0) + FUN_180563ce0/563fa0 (следующие шаги).
## FUN_180563ce0 — per-bin IIR level-трекеры (342 бина, 2nd-order state pairs)
- Константы 0x1824c4780/4788/4790/4798 (double A/B), 0x40400000=3.0f (порядок).
- Буферы: param_1+0x28 (float order), +0x40/+0x58 (double state A/B), stride 0x60, 0x156 итераций.
=> на каждый бин ставится 2nd-order IIR (атака/релиз) для сглаживания уровня.
## FUN_180530b60 — параметры сглаживания (log-interp + exp)
- cac=exp, cd0=log. freq-диапазон: online 800..1200 Гц (DAT_1824c4594=800/45a8=1200),
offline 15..180 Гц (4418=15, 4518=180), gain 0.01..1.0 (3c70=0.01, 3ea4=1.0).
- FUN_180533340(+0x2404e8/0x340500/0x440518, ...) — инициализация smoothing-фильтров
(3 экземпляра: 3 канала/oversample?).
## FUN_180530d30 — per-bin СПЕКТРАЛЬНЫЕ ВЕСА (0x5406b8/0x5406c8/0x5406d8/0x5406e8)
- fVar9 = cdc(2000/(sr*0.5)*513/(i+1)) = cdc(2000/f_bin) — pow? (IAT-стаб 0x181b3b378).
- fVar11 = [param_1+0x540880]*0.25*fVar9*fVar13 (0x540880/884 = per-band уровень/сенс).
- buf[i] = cd6(0.1, 1.0/(exp(1/(1+x))*dVar1)); buf_c[i]=1-buf[i]. cd6 = log10? (0x1824c3f70=0.1).
- dVar1 = (sr/[param_1+0x1a0])*[param_1+0x1ac]*0.001.
- => частотно-зависимый вес: 2000 Гц (f_bin больший) -> fVar9 другой, чем 500 Гц.
Кандидат объяснения red2000>red500 (dual_b1q): вес нормирует уровень бина выше на
высоких частотах -> избыток уровня больше -> глубже крас. ПОТРЕБУЕТ ВЕРИФИКАЦИЮ.
## IAT-хелперы (0x181a14xxx — jmp [GOT 0x181b3b3xx], цели 0x6fffff, динамич. загрузка)
- Последовательность стабов каждые ~6-12 байт => стандартные CRT math (в порядке импорта).
- Семантика по алгебре: cac=exp, cd0=log (log-interp в 530b60), cdc=pow (2-арг, pow(x,0.7/2.0)),
cd6=log10? (cd6(0.1, y)=log10(y)/log10(0.1) в 530d30), cd6 может быть log2.
- cf4/ce2/c9a/cc4/cfa — для case1/case8 генераторов (cfa в FUN_1805343e0: w=1/cfa(fc*pi/fs)
=> cfa=sin, cf4-цепочка для q-клипинга).
## КЛЮЧЕВОЙ ВЫВОД
- Частотная СЕЛЕКТИВНОСТЬ маски = freq-path нотч |2B/A| (m2c, центр=fc) на оси бинов
(t1kq fc-скан rmse=0.007).
- ГЛУБИНА/уровневая зависимость = level-path LUT (уровень->маска, кривая +0x188) +
per-bin IIR-трекеры уровня (0x563ce0) + спектр. веса (0x530d30, 2000/f_bin).
- dual_b1q (red2000>red500 при q<1) требует совместной модели level*веса*нотч —
единственный способ получить больше cut на 2000 чем на 500 при band=500.
## ============ UPDATE: CONSUMER OF PER-BIN WEIGHTS FOUND (FUN_180529fe0) ============
## FUN_180529fe0 — per-block СПЕКТРАЛЬНЫЙ ПРИМЕНИТЕЛЬ (5 каналов, ключевой DSP-цикл)
Сигнатура (param_1=DSP-объект, param_2, param_3, param_4=nBands). Для каждой полосы:
1. 0x540678[band] *= (fVar30/sr) * 0x540870 (нормализация уровня × sens 0x540870)
2. IIR-сглаживание спектра (5 каналов, состояния 0x540528/2c04f8/3404f8/4c0528/440510,
через DBL-петли: y += x*a + y*b, backward/forward проходы) => level-трекер per-bin
3. 0x5406f8 = резонансная реакция полосы (копия 0x540678[band], каскад 0x540688-коэфф,
scale fVar30/nBands) [здесь twin-результат 0x5406f8]
4. 0x5406f8 *= 0x540698 (частотная ось, log-интерп) и *= 0x5406a8 (axis)
5. *** ПРИМЕНЕНИЕ per-bin ВЕСОВ: ***
thunk_180003c40(lVar21=0x5406f8+N, 0x5406c8, 0x5407c8[band], iVar19, 4) -> 0x5407c8 += 0x5406c8*contrib
thunk_180003c40(lVar17=0x5406f8, 0x5406e8, 0x5407c8[band], iVar19) -> 0x5407c8 += 0x5406e8*contrib
(0x5406c8=1-0x5406b8, 0x5406e8=1-0x5406d8 — КОМПЛЕМЕНТАРНЫЕ веса)
6. накопление: thunk_180005a20(0x5407c8[band], 0x540678[band], iVar19)
7. dry/wet: 0x1c[band]*scale (0x5408b8==0: +0x540678*0x1c[band]*...)
8. FFT-свёртка в time-domain: FUN_180535a70(0x540628, 0x540678[band], nBins+1) -> frame;
FFT-таблицы 0x540548/550/598; 0x540668 -> out-буфер (0x3f800000=1.0 в DC-бин).
## FUN_180530d30 — точная формула per-bin весов (0x5406b8/6c8/6d8/6e8)
iVar6 = NFFT/2+1; fVar12 = 2000/(0x24*0.5) [p24=40000 -> fVar12=0.1]
fVar13 = 4.0 (или 1.0 если 0x5408b8); dVar1 = (0x24/sr)*0x1ac*0.001 [секунды]
for i in bins:
fVar9 = cdc( (fVar12*iVar6)/(i+1) ) = exp?/pow? (0.1*513=51.3 / (i+1))
fVar11 = 0x540880 * 0.25 * fVar9 * fVar13
dVar10 = 1/(1 + fVar11/(0x540880*4)) = 1/(1+fVar11/(4*level))
dVar10 = exp(dVar10 * fVar11)
dVar10 = cd6(0.1, 1.0/(dVar10*dVar1)) = log10(1/(dVar10*dVar1))/log10(0.1)
buf0x5406b8[i] = dVar10; buf0x5406c8[i] = 1-dVar10
(аналогично 0x540884 -> 0x5406d8/6e8)
Численно (p24=40000, sr=48000, 0x540880=1, 0x5408b8=0):
bin500: fVar9=exp(4.4)=81 -> w=2.49 ; комплемент 1-w=-1.49
bin2000: fVar9=exp(1.17)=3.2 -> w=1.60 ; комплемент = -0.60
=> частотно-зависимые (сильно убывающие с частотой) веса. НО q входит через
0x540880/884 (устанавливается из полосы) и через 0x5406f8-резонанс.
## BUFFERS (map окончательный)
0x540698 freq-ось (log-интерп, множится в band-ответ)
0x5406a8 axis-буфер (множится в band-ответ)
0x5406b8/6c8, 0x5406d8/6e8 per-bin level-веса (пары w, 1-w) — потребляются в FUN_180529fe0
0x540678[band] per-band спектр/вход (0x10-стрide, 2 слова = {ptr,count}? нет, 0x540678+0x10k)
0x5407c8[band] per-band маска-аккумулятор
0x540748 warp-ось = 8.3 - 7/(1+exp((x*20000-120)*(-0.01))) ~= 1.3 (DC-буст 6.68), НЕ f/(f+K)!
0x540628 FFT-work, 0x540548/550/598 FFT-таблицы, 0x540668 out, 0x5406f8 band-фильтр/рабочий
## FUN_180535ae0 = конструктор DSP-объекта: 0x540874=1.0, 0x54087c=0.5, 0x540884=1.0,
0x54088c=1.0, 0x540894=1.0, 0x24=0x472c4400=40000.0f (!), 0x5408ac=0x01000000.
## FUN_180535f10 = деструктор (free всех 0x5406xx-буферов).
## ИТОГ: dual_b1q объясняется комбинацией
mask(bin) = |резонанс_полосы|(bin) × freq_axis(bin) × level_weight(bin, 2000/f_bin)
где level_weight через exp(51.3/(bin+1)) даёт РАЗНЫЕ веса на 500 и 2000 Гц, и на 2000 Гц
маска глубже при низком Q (резонанс шире/мелкие). Верифицировать в standalone-харнессе.
## ============ UPDATE 2026-08-18: 0x530d30 weights DECODED + NUMERICALLY NEGATIVE ============
- Exact formula (decomp 22330, FUN_180530d30): per bin
base = (2000.0/(sr*0.5))*iVar5/(bin+1) [iVar5=NFFT/2+1]
w8 = powf(base, 0.25)
v = [0x540880]*0.25*w8*fVar12 [fVar12=4.0 if offline flag, else 1.0]
q = 1/(1 + v/([0x540880]*4)) (second pair 0x540884: q2=1/(1+v2/[0x540884]))
dVar1= (sr/[0x1a0])*[0x1ac]*0.001
w = 0.1^(1/(max(q*v,floor)*dVar1)) [0x1824c3f70=0.1, 0x1824c3e30=0.001]
buf0x5406b8 = w ; buf0x5406c8 = 1-w (and 0x5406d8/6e8 with 0x540884)
- Constants locked: c3d3c=0.25, c4334=4.0, c3d8c=0.5, c45b4=2000.0, c41e0=2.0.
- NUMERIC RESULT: at any plausible runtime (sr=44100, NFFT=1024, L=0.05..1.0, hop 1024..4096,
n1ac 1..12) -> w(500)=w(1000)=w(2000) ~= 0 (=10^-20..10^-0.07); complements ~= 1.
=> the 0x530d30 level-weights DO NOT create the 2000-vs-500 tilt (ratio2000/500 ~1.0).
- CONCLUSION: dual_b1q red2000>red500 does NOT come from 0x530d30. Suspect = freq-axis warp
(0x540698 '8.3-7/(1+exp((x*20000-120)*(-0.01)))' ~1.3 DC boost) x resonance. The model's
empirical tilt {500:1.414,1000:1.454,2000:1.795} stays as the placeholder; replacing it
requires FUN_180529fe0 consumer step-5 algebra + runtime level values (Phase-4/5 dive).
## ============ UPDATE 2026-08-18 (CHECKPOINT): tilt != warp, level-path decoded ============
- FUN_180563ce0 = INIT of per-bin IIR level trackers (not update): 0x156=342 bins, order=3.0f (0x40400000),
coeff A/B mvaps 0x24c4780/4790, strided fill; 3rd pass fills 0x40e00000=7.0f (post-gain). FULL: /tmp/opencode/f_563ce0.dis.
- Level-path map: 0x563440 (LUT curve +0x188: [min,max,gamma,linear flag,nodes]; 6 bands via 0x56e3e0
double-twin N=1024 with LUT-ramp input; combine -> +0x2198; LUT pairs +0x98 at 0x3ff pts level->mask).
NOT in decomp_funs.txt; use /tmp/opencode/f_563440.dis + level_notes.md (LOCAL: /home/m/re-tools/handoff/).
- **DECISIVE NEGATIVE**: warp(2000)=0.773 cannot reach dual red2000=15.2dB — C=depth·warp·LUTmax=0.864·0.772·0.667
=0.445 < needed 0.826 (C=1-10^(-15.2/20)). warp is NOT the empirical tilt. The gap is closed by the blend
step (0x5407c8 += weights·res_upper/lower, freq-axis 0x540698) + FFT-conv 0x535a70 shaping.
- CHECKPOINT decision (user): STOP; next session = diagnostic bridge (two-term mask: warp·LUT + α·freq-axis_term,
fit vs measured curves) BEFORE transcribing FFT. do NOT curve-fit without the blend term.
## ============ UPDATE 2026-08-18 v2: BRIDGE DONE + FUN_180529fe0 full algebra ============
- **Полный декомп FUN_180529fe0: /tmp/consumers_out.txt:471-1277** (f529fe0.dis в /tmp обрезан на 52a813).
Поправки к прежним заметкам:
- 0x5408b0 = таблица ГРАНИЦ бинов (прелюдия: cvttss2si+imul+idiv, iVar20=band-start, iVar19=len), НЕ LUT.
- Уровневая маска резолвится ИНЛАЙН: fVar30=0x5408b0[idx]; fVar25=(float)FUN_181a14cdc()=pow (IAT→GOT
0x181bab378, CRT-цель вне дампа); пары `×(fVar30fVar25)`/`+fVar25·` (dry/wet уровня).
- Цепочка per-band (по декомпу): 0x540678=res; res*=∏0x540688[k]; res*=(fVar30/nBands); *=fVar29·0x54088c
(fVar29=(fVar30/0x1a0)·0x540870); IIR 0x52d650 state 0x440518; двойное IIR-сглаживание (double,
states 0x3404f8/2c04f8 и 0x440510/3c0510); 0x5407c8+=0x5406c8·res_up + 0x5406e8·res_low + 0x540678
(003c40, kernel vfmadd213pd) — **АДДИТИВНЫЙ аккумулятор**; 0x540678 *= res(008700) и *= warp 0x5406a8;
offline: *=0x1c[band]·fVar27 (глубина, D=-0.5@0x24c4670); dry/wet: mask·fVar30·0x540888 + (fVar11fVar30)
(fVar11=1.0 c3ea4) — **педастал**; FFT-conv 0x535a70 (fwd, window 0x540550/598, kill mirror,
inv, ×0x540658, fwd, inv, 0x540668[0]=1.0[1]=0, apply FIR к param_2[band]).
- Константы offline: c3ea4=1.0, c3e28=0.8, c4670=0.5, c4674=0.7, c4680=1.0, c3c58=0.001.
- **BRIDGE RESULT (model_fir.py, rmse=0.236 dB)**: C(f)=1.221·LUT(log10(L0/res)) + 0.358·warp(f)^3.143.
res(500;fc500)=0.117 Q-независим -> C500-константа без tilt; xv(2000)=0.308..0.656 (Q) -> C2000-и-гг-форма;
аддитивный терм warp^3.14 даёт +0.15 на 2000 и ≤0.02 на 1000 (иначе t1k рушится) — единственная точка,
совместимая с 36 замерами. Мультипликативный warp·LUT проваливается (>10 dB). warp^3.14≈π: кратный каскад?
Остаток 0.7 dB (Q=0.1) = LUT-колено 0.574 -> нужен FFT-уровень (0x535a70 + окно 0x540658 + freq-axis
0x540698), см. SESSION_HANDOFF §5-6.
## ============ UPDATE 2026-08-18c: FFT-CONV LOOP FULLY MAPPED (Phase 5 step 3 partial) ============
Per-band FFT-conv loop disasm: /tmp/opencode/f_52b570.dis (0x52b550-0x52b8b5 == consumers :1195-1271).
### What 0x535a70 / helpers ARE (they're dispatchers, real kernels in base-CRT):
- 0x535a70 = FFT fwd dispatcher -> 0x140a10 (scalar) / 0x140a70 (vector). NOT hand-transcribable, unnecessary.
- 0x540548/0x540550/0x540598 = FFT-PLAN table ptrs (plan 0x540530; +0x18/+0x20/+0x68), NOT windows.
Plan built by FUN_18052dc30(param_1+0x540530, [0x64]=FFT size, 0x540868). 0x540534 = plan+0x4 = N/2 usage.
- Per-band ops (dispatch variants collapsed): iVar20=N/2; r14=FIR(0x540668);
1) 0x535a70(fftctx 0x540628, mask, N/2+1) forward FFT of mask
2) 0x2210(0x540628, 0, r14, N/2+1) inv FFT into FIR
3) fwd FFT (0x2180/0x1bb0) ; zero @[N] ; 0x52d920(r14+4, xmm13, N/2-1)=fill LOW half;
0x52db50(r14+4+N/2, xmm9, N/2-1)=fill HIGH half; inv FFT (0x1a90/0x19d0)
4) 0x140b30/0x140aa0(r14,r14,N/2+1) in-place complex op
5) fwd FFT ; zero @[N] ; 0x52d990(r14, 0x540658+N/2, N/2) = FIR *= WINDOW 0x540658;
0x52db50(r14+N/2, xmm9, N/2) = fill HIGH half; inv FFT
6) FIR[0]=1.0f, FIR[1]=0; if 0x540890!=0: complex ops (0x1880/0x1ca0) + FIR *= 0x540888 over 2*N;
copy FIR->out (0x1df0/0x1f70).
### Key unknowns (static analysis exhausted):
- **0x540658 window content**: ONLY 1 direct ref in whole dump (the read at 0x52b771). Not allocated in
consumers_out (:119-135 alloc 668/698/6a8/6b8/6c8/6d8/6e8/6f8/708/718/798), NOT set by 52dc30 (plan tables
only at +0x18/0x20/0x48/0x58/0x68). Written only indirectly -> needs runtime capture OR plan-internal
region. If 0x540658 is freq-shaped, it is the source of warp^3.14 (~π) & LUT-knee 0.574.
- xmm13/xmm9 fills = config constants from FUN_180529fe0 prologue (offline), unknown values.
- FFT size N = 2*0x540534; 0x1a0 = pow(2,floor(log2(SR/44100)))*412 (SR 44.1k -> 412); 0x540868=0x1a0*iVar9*8.
### Conclusions:
- freq-axis NOT the boost source (offline const). FFT tables NOT windows. The whole freq-shaping funnels
through 0x540658 window in step 5 + the offline fill constants. To resolve: runtime capture of 0x540658
(needs the plugin under a debugger) OR empirically back out its response from the model residual.
- Recommend: since bridge already at 0.236 and FFT window content is static-invisible, next high-value move =
full pipeline render (step 5) rather than more static FFT mining.
## ============ UPDATE 2026-08-18b: FREQ-AXIS 0x540698 BUILDER (decomp :21735-21757) ============
- Allocation+fill of freq-axis 0x540698 lives in the SAME function as the 0x540668 FIR window alloc
(decomp_funs.txt :21735-21757, the builder calls FUN_18052e190(param_1+0x540698, count=0x54086c)).
- OFFLINE branch (param_2=='\0'): the whole 0x540698 array is filled with ONE scalar
fVar29 = (DAT_18262b5c8 * LUT_BANDBOUNDS[uVar3+1] * DAT_18262b704 * LUT_BANDBOUNDS[uVar3] + fVar22)
* DAT_18262b700
(DAT_18262b5c8/DAT_18262b704/DAT_18262b700 = Soothe2ModuleBase statics, uVar3 = 0x5408b0 boundary index).
=> OFFLINE freq-axis is CONSTANT (per-band-band-width scalar), NOT the '8.3-7/(1+exp...)' formula.
The exp-formula suspicion was wrong for offline path. Any per-bin frequency dependence must enter via
WARP 0x5406a8 (freqpath.cpp, already transcribed) and the 0x540668 FIR (FFT-conv impulse).
- 0x540668 (FIR window, count 0x54086c*2) allocated right before; filled with 1.0f (0x3f800000) in offline
init at :21798-21804 (<=> flat impulse). So in offline the FFT-conv kernel starts as a pure del
a; the real kernel arrives from 0x535a70 processing (to transcribe next).
- 0x5406a0 = element count used by the fill loop (large, ~0x54086c). Buffer 0x5406b8..0x5406e8
(level-weights caps) allocated at :21753-21757 with the same count.
## ============ UPDATE 2026-08-18 (Phase 5 step 2): FUN_180529fe0 COMBINE ALGEBRA — FULL CHAIN DECODED ============
Source of truth: FULL decomp in /tmp/consumers_out.txt:471-1277 (the decomp_funs.txt copy at :11425 is
TRUNCATED — Ghidra dropped the middle because of the noreturn/dispatch thunks `(*DAT_181bab008)(&..,&..)`
guarding small-vs-vectorized variants; the consumers_out.txt copy came from a previous session's
getFunctionContaining(0x52ac64) and has the complete per-band loop + FFT-conv).
### Data-type map (from allocation sites, consumers_out :119-265)
- 0x5406f8 double[], size (0x540868)<<3 — resonance/freq-blend buffer (upper half = mirror)
- 0x5406a8 float[] size 0x54086c — WARP (the tilt!)
- 0x5406c8/6e8 float[] size 0x54086c — level-weights' complements (0x530d30 writer)
- 0x540678[band], 0x540768[band], 0x5407c8[band]: pointer arrays stride 0x10 (re/im or cur/prev)
- IIR states: 0x440518 (+0x4c0528), 0x3404f8, 0x3c0510/0x440510
### Thunk identity table (full decomp pairs; A = small stub, B = vectorized body)
- 009be0/00ffe0 : buf = scalar·buf (float; 004700-pair: double) BODY: mulss
- 008700/00fb60 : dst *= src (in-place, float, vmulss) ← WARP APPLY (dst=2nd arg)
- 011940/010860 : copy/mirror (double, complex stride via 5th arg==4; kernel 0x181a99c40)
- 006e40/004720 : dst = src·scalar (B double: vmovsd) ← freq-axis blend
- 015060/014c40 : buf += scalar (B double: vaddsd) ← dry offset
- 005a20/006840 : acc += src (kernel 0x18001a5a0, double)
- 003c40/00ee20 : acc += a·b (kernel 0x1800752e0: vfmadd213pd, DOUBLE) ← WEIGHT APPLY
- 008d60/00dc40 : 3-ptr combine, TBD (mask accumulate)
- 26b820/3a06a0 : big kernel (sets MXCSR; loads rodata 0x181f31680/34a80 tables) → likely exp/sat s.t. begin? TBD
- 2dc0e0/367980 : fills dst buffer with constant pattern 1.0/0.0 (vmovdqu rodata) — TBD (dry/wet?)
### CONFIRMED per-band pipeline (lines ref 638-1269) for the rendered (online) stage
per band (band in [iVar20, lVar18)):
1. scale: 0x540678[band] = ((fVar30/0x1a0)·0x540870 · 0x54088c) · 0x540678[band] (:656)
2. IIR: FUN_18052d650(state=0x440518, out=0x5406f8, in=0x540678[band]) → smoother (:668)
3. online: blend & weight into mask:
0x5406f8 = 0x540698·(1mix) 006e40 mix=0x54087c (:814)
0x5406f8 += mix·0.8 015060 c3e28=0.8 (:823)
0x540678[band] = bigkernel(0x540678[band],0x5406f8) 26b820 (:832)
0x5407c8[band] = combine(0x5407c8[band],0x540678[band],0x5406f8) 00dc40 (:843)
mirror 0x5406f8 halves (011940, n and n+stride4) (:852 861)
WEIGHTS: 0x5407c8[band] += 0x5406c8·0x5406f8_upper 003c40(stride4) (:869)
0x5407c8[band] += 0x5406e8·0x5406f8_lower 003c40 (:877)
0x5407c8[band] += 0x540678[band] 005a20 (:885)
4. WARP (TILT): 0x540678[band] *= 0x5406a8 008700 (:938,:947)
5. dry/wet: 0x540678[band] = mask·(fVar30·0x540888) + (1fVar30) fVar30=0x540874rnd (:1184,:1193)
6. FFT-conv: FUN_180535a70(0x540628, mask, n+1) fwd-FT → window×0x540658 (52d990) → inv-FT
→ impulse 0x540668, DC=(1.0,0.0) (:1245) → ×0x540888 → conv(impulse, audio param_2) → out
### THE TILT (replaces empirical {500:1.414,1000:1.454,2000:1.795})
warp 0x5406a8 = 0.87·x/(1+x/K), K=exp(2.0723267)≈7.942, x = f_bin/2000 [c3e50=0.87, c4208, c45b4=2000]
warp(500)=0.211 warp(1000)=0.409 warp(2000)=0.773 (linear depth ratio 2000/500 = 3.67)
applied directly to the FLOAT mask before FFT-conv ⇒ enters the dB via the impulse response.
NOTE: 3.67 linear is ≫ the fitted tilt ratio (1.27) ⇒ the rest comes from the LUT-level flattening
(step 5 dry/wet + 0x540888) and the level sidechain 0x563xxx — that is the Phase-5 step-4 integration.
## ============ 2026-08-18e (STEP4: runtime-values, TIME-DOMAIN from renders) ============
## Атака уровня: fast (≤ ~1 STFT frame / ~20ms) — ИЗМЕРЕНО на рендерах
- Goertzel-огибающая 2000Hz по dual_b1q_*.wav (100ms/20ms окна): env ПЛОСКАЯ 0.18 (q0.1),
0.30 (q1), 0.32 (q10) уже на 0.2s; пик 0.208/0.344/0.364 (атака, ~первые 50ms меньше подавления).
- red(dB) по времени (вход dual 500+2000, канал 2000): t=0: 13.2 dB -> t=0.02s: 15.2 dB (steady).
=> reduction выходит на стационар к ~20ms. 0.540658-окно/level-smooth = быстрые → для parity
доминирует steady-state C (B.12, 0.268 dB); атака нужна лишь как короткий onset (~1 frame).
- Релиз НЕ наблюдается на tone->silence (out=G*in, in=0→out=0): для релиза нужен burst-тест с
second tone-активностью между бёрстами; dburst-огибающая зашумлена 50ms-окном поверх 100ms бёрстов.
## Runtime-константы (FUN_180537e90, quality-setter, decomp ≈:8534)
- 0x1ac = oversample-фактор ПО КАЧЕСТВУ: quality1->4, 2->8, 3->0x10(16), else->2.
- 0x1a4 = fVar6 / (float)0x1ac (см. строку 482: есть деление на 0x1ac => 0x1a4 = analysis-res".
- 0x1a0 = pow(2,floor(log2(SR/44100)))*412 (@44.1k = 412) — база cell-count геометрии.
## Коэф-таблицы IIR 0x24c4780/0x24c4790 (soothe_mem.bin offset 0x24c4780)
- floats: {1,0,0,0,-1,0,0,0,0,1,0,0,1,1,0,0} (A) / {-1,0,...} (B) — шаблон блочный, НЕ чистая
attack/decay константа; doubles: 0.0078125=1/128 на 4-й/6-й слотах. => отложить, не декодить.
- Более информативна ctor-инициализация 0x540880=0x540884=1.0f (movl 0x3f800000, disasm 0x535d19).
## ============ 2026-08-18f (STEP5b: FULL FRAME-RENDER PILOT - WORKS) ============
## framed_render.py: STFT(2048/512, sqrt-Hann) -> per-bin amp 2|X|/wsum (att 11ms/rel 80ms)
## -> B.12 mask C=g*LUT(xv)+w*warp^a, gain=1-C -> OLA. VALID: dynamics match, steady ~B.12.
## al_* DATASET (центр band fc=1000 sens12, tone1000, level-свип 0..-24dBFS):
## lvl 0,-3,-6,-9,-12,-18,-24 dBFS -> red -104,-6.1,-7.9,-9.7,-11.6,-15.4,-19.5 dB.
## xv=-0.269..0.931 (res_center=0.1171) => REAL LUT-нога КРУЧЕ frozen-узлов (cap 0.667):
## на xv=0.931 C->1 (клиф), у B.12 только -9.5. КОНФЛИКТ с t1k fc-scan (15.6@0dBFS там же)
## => премьюза fc-scan рендеров неверна (вход/настройки) - ПЕРЕСОГЛАСОВАТЬ.
## al_* = калибровочный датасет для замены LUT-узлов B.11 (structural 0.540658-piece).
## ============ 2026-08-18g (STEP5c: DATASET INCONSISTENCIES DECODED) ============
## КЛЮЧ: RPP несёт base64 XML "<SOOTHE2STATE><PARAM id= value=/>" — декодер:
## b64 = join всех строк-бейс64 (>40 chars) из .rpp; decode utf8; regex id/value.
## 1) t1k/t1kq _b1f_* = МУЛЬТИБАНД: band1 on sens=12 + band2 on sens=-12 (fc=1778.7, q=4.5)
## + band3/4 on (defaults) => НЕ single-band. B.12 фит впитал multi-band depth-split
## => LUT_obs у b1f сидит на ДРУГОЙ кривой (0.07-0.17 vs 0.38-0.63 у al_/dual).
## 2) t1kq_only1_* = НАСТОЯЩИЙ single-band fc-скан (band2 on=0). НО в RPP НЕТ 'depth' =>
## default depth (не 0.8639736) => нельзя мешать с al_/dual без нормализации depth.
## 3) tone_amp АБСОЛЮТНАЯ калибровка НЕСОСТОЯТЕЛЬНА (t1k: 1.058 vs true 0.215; t1kq: 0.284 vs 0.058).
## Для xv использовать FFT-амплитуду: seg=1..4s, Hann, A=4|X|/N (проверено: lv12 0.17759==peak).
## 4) ЧИСТЫЕ single-band датасеты (depth=0.8639736, band1 один):
## dual_b1q (22) + al_* (6) — СОВМЕСТИМЫ ~0.05 LUT_obs:
## dual@2000 xv -0.66..0.31 -> LUTobs 0.434..0.547 (гладкая S-кривая)
## dual@500 xv=0.574 -> 0.564
## al_* xv -0.42..0.63 -> 0.381..0.626 (круче внизу, ~0.05-0.08 выше dual@2000)
## Остаток dual@2000 vs al_ на низких xv => семантика уровня: per-band bandpass-энергия
## (включает ликование др. тонов) vs per-bin амплитуда — НЕ РЕШЕНО. Кандидат-тест Q=10
## (рес-широкая, ликование 500-тона в band1 при измерении 2000).
## 5) b2_* семейство (band2 fc=1778.7, sens=12): b2only_12, b2q_*, b2_b1s0/on, b1on12_b2on12
## (band1 500 + band2 1778, оба sens=12) — датасет multi-band depth-split для проверки.
## 6) ПЛАН (следующая сессия): (а) refit single-band LUT по dual_b1q+al_ (не b1f!);
## (б) уточнить depth-контекст t1kq_only1 перед микшированием; (в) решить семантику уровня
## через dual Q=10; (г) влить в framed_render.py.
## ============ 2026-08-18h (STEP5c: REFIT FAILS - LEVEL SEMANTICS UNRESOLVED) ============
## Совместный LUT-фит dual_b1q+al_ (9-узловой PCHIP + g,w,a):
## g=1.1895 w=0.3749 a=2.0 (на границе!) -> TOTAL 0.474 dB, dual 0.26, al 0.894.
## АЛ-точки не ложатся на dual-кривую: xv>0.3 al хочет +1.3..-1.0 dB сверху.
## КЛЮЧЕВОЙ ФАКТ (оба = тон в ЦЕНТРЕ полосы, res=0.117, depth/sens одинаковы):
## dual@500 (A=0.4397, вход=ДВА тона) xv=0.574 -> red 10.22
## al_3 (A=0.5005, вход=тон) xv=0.631 -> red 13.38 (ВЫШЕ при меньшем заxode-sharing)
## al_6 (A=0.3543) xv=0.481 -> red 12.21
## => при практически одном xv dual режет МЕНЬШЕ (10.2) чем al_4/5 (12.2-13.4).
## Больше энергии (dual=2 тона, A=0.44) -> МЕНЬШЕ reduction = ПРОТИВОРЕЧИТ al_-монотонности.
## Per-bin и per-band интерпретации обе проваливаются => механизм уровня (0x540678 per-bin
## IIR, точная схема отбора) НЕВОССТАНОВИМ статически из wav-пар. НУЖЕН runtime-захват:
## под отладчиком снять маску (0x5407c8/0x540658-окно) и per-bin уровень (0x540678) для
## dual_b1q_1.0 и al_6 при одинаковом цене -> дифферить механизм.
## Именно поэтому dB-потолок B.12-суррогата ~0.20-0.27 dB — линия DATA-уровня, не тюнинг.
## СЛЕДУЮЩИЙ ШАГ (рекомендация): runtime-захват уровня/маски под gdb (soothe2_x64.vst3), файл
## двойного стимула; после локализации механизма — закрыть LUT и 0.540658 эмпирически в C++.
## ============ 2026-08-18i (MULTI-BAND COMBINATION - MEASURED, MODEL FAILS) ============
## Эксперимент (вход tone1778 A=0.707, band2 fc=1778.7 = центр; depth=0.86397 mix=100):
## red@1778: b2only_12/b2_b1s0/b2_b1s0on = 16.55 (ВСЕ ОДИНАКОВО)
## b2_b3s12=16.62 b2_b4s12=16.51
## b1on12_b2on12 = b1s12_b2s12 = b1on12_b2s12 = 17.08 (идентичные)
## b1only_12 (band1 fc=500, band2 off) red@1778 = 11.47 (soothe режет ВНЕ полосы сильно!)
## ВЫВОДЫ:
## 1) depth НЕ делится по числу активных полос (b2only 1-полоса == b2_b1s0on 2-полосы 16.55).
## 2) Полоса sens=0 даёт НУЛЕВОЙ вклад (b2_b1s0on == b2_b1s0 == b2only).
## 3) Комбинация band1+band2: 16.55->17.08 (+0.53 над макс), НЕ product (дал бы ~27),
## НЕ additive-C (насыщает в +180). => общая маска строится из СУММЫ displacement по полосам
## (аккумулятор 0x5407c8 += ...) через НАСЫЩЕНИЕ. exp-насыщение(0.9) даёт 19.5 > 17.08
## => точная насыщающая кривая/уровень требуют runtime-захвата (0x5407c8/0x540658).
## 4) B.12 single-band band1@1778 предсказал 10.04 vs измеренных 11.47 (уровневая семантика
## опять +1.4 dB).
## => мультибанд: сумма displacements -> sat-кривая. Каждая полоса отдельно калибруется single-
## band рендерами; комбинация закрывается runtime или sat-подгонкой на b1on12_b2on12.
## ============ 2026-08-18h2: RUNTIME CAPTURE PARTIAL (base 0x180000000, 0x540658 = FIELD NOT RVA) ============
### Runtime capture infra (rtcapture2.py, WORKS):
- Launch: `reaper -nosplash -renderproject X.rpp`; host pid found by scanning /proc/*/maps for 'soothe2'
(lowest map addr with soothe2 in path = PE base). **vst3 loads AT ImageBase 0x180000000** (no reloc).
- Reading /proc/pid/mem (+0x180000000) works DURING render; process dies right after render finishes.
- Section table (Authoritative): .text rva 0x1000 raw 0x600 vsize 0x1a52000; IPPCODE 0x1a53000/0x1a51e00;
.rdata 0x1baa000/0x1ba8400 vsize 0xa60000; .data 0x260a000/0x2608200 vsize 0x72000; .pdata 0x267c000.
- FILE->RVA mapping: raw off F maps to rva = F - raw_sec + va_sec (e.g. .text: F=0x53F658<->rva=0x541058).
### CRITICAL: 0x540658 (and 0x540678/0x5407c8/0x5408b0/etc) are FIELD OFFSETS, NOT RVAs!
- Disasm at rva 0x52b771: `mov rax,[rdi+0x540658]` -> window read is [ctx+0x540658] (ctx = DSP object).
- Static soothe_mem.bin is RVA-linear; reading at 0x540658 gives .text CODE bytes (garbage floats).
- So the "window" (FIR*=WINDOW in FFT-conv step 5) lives in the HEAP DSP object at offset 0x540658,
NOT in module image. Runtime must locate `ctx` = base of DSP object.
- Beacon idea: phase_table_1024 (twiddle doubles 0x182615608 in .data) is the FFT-plan table;
scan host heap for u64==0x182615608, then the pointer owner is plan (+0x18/+0x20/+0x68 fields)
-> ctx = byte_addr_of_(pointer_field) - field_offset; window at ctx+0x540658.
(rtdeep2.py implements this; currently racy - host may exit before numpy import + first scan.)
### Notes for next session:
- Render lifetime is short (~2-19s); to win the race: import numpy BEFORE Popen, scan /proc instantly,
and re-run if host missed. Could also pre-load module into gdb for direct ctx inspection.
- Alternative: find ctx via 'consumers_out' alloc chain (note :119-135) if heap layout known.
- Verified render outputs: out_dual300.wav etc; run_sweep works; pkill -9 -x reaper hangs shell -
use `pkill -9 -f "reap[e]r"` style to avoid killing own bash.
## ============ 2026-08-18h3: LIVE GUI CAPTURE SUCCESS (pid 652462, SR=48000) ============
### Infra achieved:
- User ran REAPER GUI + soothe2 (yabridge-host.exe.so pid 652462), tone playing, band1 fc=500, Q=1, stereo balance.
- rtsnap.py: page-wise snapshot of all readable regions -> /tmp/rtA|B|C|D.{raw,idx} (~821MB each), skips EIO pages.
- Live process loads vst3 at ImageBase 0x180000000; `.data` shifted +0x1e00 vs static RVA.
- Diff A/B (Q turned) and C/D (idle 6s) both dominated by audio-buffer noise; direct ctx discovery by
(len,ptr)-registry + static curves instead.
### Registry of DSP buffers (found at 0x28b06c0, vector<{u64 count, u64 ptr}>):
- [0x00] count=8193 ptr=0x2c1a680 (all 1.0f) - identity/gain table
- [0x10] count=8193 ptr=0x1930100 **STATIC** 0.5->0.8 (freq-path window, monotonic)
- [0x20] count=8193 ptr=0x1938180 **STATIC** 0.0->3.899
- [0x30] count=8193 ptr=0x19401c0 **STATIC** 0.596->0.126
- [0x40] count=8193 ptr=0x29000c0 0.404->0.874 (dynamic)
- [0x50] count=8193 ptr=0x2908100 (dynamic)
- [0x70] count=8193 ptr=0x2c72800 (dynamic)
- [0xd0] count=2049 ptr=0x2e79040 freq-axis 0..23988.3 Hz (linear, spacing 11.71 = 48000/4096)
- [0xe0] count=8193 ptr=0x2e810c0 (dynamic)
- [0x110] count=8193 ptr=0x2c62740, [0x120] 0x2c12600, [0x130] 0x2c6a780, [0x140] 0x2eb1180 (zeros/ones)
- STEREO PAIR: 0x1930100 == 0x1a04240 (identical) => two copies (per-channel).
- "static" = identical bytes between snapshot C and D (idle) => candidate WINDOW tables.
### THE WINDOW (0x540658 area) - EXTRACTED:
- rwin_A0.npy (0x1930100): 2048 floats, 0.50000006 .. 0.79990, strictly monotonic, reaches 1.0 at idx 2049+
(saturates: plateau 1.0 after bin 2048). Shape = 0.5 + 0.3*g where g = K*x/(K+x), x=f/24000, K≈1.9
(fit rmse 0.0024; K sweep 1.9 best). freq-axis r_freqaxis.npy (2049 floats, 48000/4096 spacing).
- Interpretation: freq-path window = 0.5+0.3*warp(f); warp = K·x/(K+x), x = f/(SR/2).
NOTE: GUI SR=48000; offline renders SR=44100 (out_dual*.wav all 44100) - must renormalize x by actual Nyquist.
### Integration attempt (framed_render refit, G/W/p free, cases dual_b1q q=0.1/1/10 @500+2000):
- baseline warp^p: G=1.278 W=0.130 p=6.077 meanerr=0.323 dB (warp=(f/2000)^p)
- real window as warp term: G=1.199 W=1.328 meanerr=0.359 dB (NOT better)
- window*input-amp + warp^p: meanerr=0.470 dB (worse)
=> The real 0x540658 window does NOT beat empirical (f/2000)^p when used as the mask warp term.
BOTTLENECK remains the LUT-leg (level semantics), not the window. See al_* conflict in framed_render.py.
### Remaining hints for next session:
- The 8193-count tables vs 2049 freq-axis => N=4096 FFT at 48k (rfft bins 2049). Window arrays sized 8193
= complex bins? or 2*N? Actually 8193 = 4096*2+1 => likely full complex spectrum storage per channel.
- Re-running parity: use tone_cmp() (ndarray) not render_parity.tone_amp (file path) when testing live windows.
- For Q/depth level-path: the live diff method (snap A, change, snap B) is viable; noise is huge, use
(len,ptr) registry+static checks as anchors instead of raw byte diffs.
## ============ 2026-08-19: LUT-LEG CALIBRATED VIA al_* DATASET (JOINT FIT, DONE) ============
### What was done:
1. Decoded full al_* RPPs (binary b64 header): trim to len%4==0, find `<?xml`@92, regex PARAM; only band1
active (fc=1000, Q=0.9999978, sens=12, mode=1), depth=0.864, input = lvl_tone_lv{3,6,9,12,18,24}.wav.
Bug fixed: dual_b1q_*.rpp had 6 band entries (band0/2-5 off) - only band1 matters, model was right.
2. Measured steady reduction at 1000Hz (tone amplitude ratio m=amp_out/amp_in, sin/cos correlation metric):
lv3: m=0.2080 (-13.64) lv6: 0.2387 (-12.44) lv9: 0.2730 (-11.28) lv12: 0.3114 (-10.13)
lv18: 0.3996 (-7.97) lv24: 0.5006 (-6.01) -> reduction INCREASES with input level.
3. Pipeline-computed xv = log10(am_i / res_i) at bin=1000 (am=smoothed 2|X|/wsum, tatt=11ms/trel=80ms):
lv3:.5488 lv6:.3988 lv9:.2488 lv12:.0988 lv18:-.2012 lv24:-.5012.
NOTE: earlier al_* xv (0.631 etc.) were computed with different am normalization - ALWAYS use pipeline's.
4. Pure-LUT nodes at each xv: lut = ((1-m) - W*warp^A) / G (mask C = G*LUT + W*warp^A).
5. JOINT FIT (dual + al_*, objective=mean|err| over 6 dual tones + 6 al levels):
best: G=1.0850 W=0.2819 A=1.1377 -> al_* err <=0.19 dB ALL; dual err <=0.62 dB ALL.
The LUT leg is a SLANTED curve (~0.36 at xv=-0.5 rising to ~0.64 at xv=+0.55), NOT the flat B.12 (~0.5).
6. framed_render.py updated: LX/LY = merged anchors + al_* nodes; G/W/A = 1.0850/0.2819/1.1377.
CRITICAL BUGFIX: lut clip must be [LY.min(), LY.max()] (0.366..1.0) not LY[0] (0.4402) - node at
xv=-0.5012 (0.366) was being floored, breaking q10@2000 by 1.4 dB.
### Final validation (framed_render.py dual):
q0.1 @500 -0.55, @2000 +0.14; q1 @500 -0.62, @2000 -0.57; q10 @500 +0.01, @2000 -0.00
envRmse@steady: 0.64/0.04/0.71/0.78/0.11/0.11 dB. al_* lv3..24 err: +0.11~+0.19 / +0.01 / -0.02.
### Conflict (t1k fc-scan vs al_* clif at xv=0.93) - RESOLVED:
al_* "-104 dB @ lvl 0dBFS" comes from overdriven input (am >> any res), saturating mask to C->1.
t1k 15.6 dB @ xv=0.931 was a *different* level point (soothe's own fc-scan dataset). Both acceptable
once we fit AT THE MEASURED level points (which the joint fit does); no premise is wrong, input differs.
### Next: (1) hammer the residual -0.6 dB on dual 500Hz cases (structural); (2) multi-band combos (band>=2); (3) C++ port.
## ============ 2026-08-19 (continuation): LEVEL-PATH DECOMPILED — FUN_180563440 + FUN_180563ce0 ============
### FUN_180563440 (dsp/levelpath.cpp dump, f_563440.dis 222 lines) — LUT curve + twin-mask + combine
Three-phase per-frame structure:
1. **1024-bin LUT loop**: bin k → x = clamp(k/1024, 0, 1). Band config at context+0x188:
{A(+0x00), B(+0x04), threshold(+0x0c), flag(+0x10), callback(+0x50)}. Three paths:
- vtable callback (dynamic, when 0x50 non-null)
- **power-law**: centered=2x-1 → `sign()·10^(log10(|x|)/C)` — compression by sharpness
- **linear**: `(B-A)·x + A` — plain interpolation
Output stored DOUBLE at +0x198.
2. **Twin-mask factory** FUN_18056e3e0: 6 bands × 1024 bins, stride 0x2000.
3. **Combine**: max 2 channels (stereo), 6 bands, `1 - Σ weights`.
### FUN_180563ce0 (f_563ce0.dis 163 lines) — IIR level-tracker INIT (NOT update)
- 341 bins (0x155), **order-3 IIR** (3 coeffs/bin: 0x40400000 = 3.0 markers).
- Level coefficient: **0.1** (0x3dcccccd IEEE 754) — this is the attack/release α.
- State layout: rcx+0x28/+0x40/+0x58 (3 buffers). Init [1,0,0,0] / [-1,0,0,0].
- **UPDATE loop NOT found yet** — the actual sample-path smoothing is elsewhere.
### Key constants (all verified from soothing_mem.bin):
| Address | Value | Meaning |
|---|---|---|
| 0x24c3c54 | 0.0009775 | 1/1024 bin scale |
| 0x24c3ea4 | 1.0 | clamp max |
| 0x24c41e0 | 2.0 | power-law centering |
| 0x24c4680 | -1.0 | sign flip |
| 0x24c3d8c | 0.5 | threshold |
| 0x24c4334 | 4.0 | depth range (oversample os=4) |
| 0x24c43e0 | 8.6859 | 20/ln(10) dB conversion |
| 0x24c4704 | -6.9078 | ln(0.001) floor |
### CRITICAL: The "warp" is NOT a table — computed at runtime
- `rwin_A0.npy` (live 0.5→0.8) is the **frequency WINDOW** (per-bin mask shaping), NOT the warp.
- Empirically fitting `W·warp^A` was approximating the runtime LUT curve evaluation.
- The LUT is **parametric** (linear or power-law by band flag), not a fixed lookup.
- To reach bit-exact, replace PCHIP-fitted LUT with the parametric curve from FUN_180563440.
### Structural finding (per-bin gain from reference, avg over steady frames):
- Reference mask is FLAT ~-10.2 dB across 100-540 Hz REGARDLESS of Q (dual_b1q q0.1/1/10).
- Model produces res-shaped notch → source of the -0.6 dB residual at 500 Hz.
- **Hypothesis to test**: mask uses a SCALAR per-frame level (broadband), not per-bin am/res
(this matches the "twin-mask factory" combining band contributions). Test in /tmp/scalartest.py.
## ============ 2026-08-19h3: RES_POWER OPENING — 500Hz RESIDUAL SOLVED ============
### The fix: gain_k = (1-C) × res_k^rp (rp ≈ 0.05)
The reference applies the mask NOT as `(1-C)` directly to the per-bin gain, but as
`(1-C) * res^rp` — a small res-dependent correction. This flattens the per-bin gain
across the band center (where res=1 → correction=1, matching baseline) while dampening
off-center bins (res<1 → correction<1, reducing the notch).
### Validation (test_joint_rp2.py, multi-start Nelder-Mead, 5000 iter):
```
BEST: G=0.9696 W=0.3503 A=1.0887 rp=0.0516 mean=0.208 (dual only, 6 pts)
q0.1@500: err=+0.00 (was -0.55)
q0.1@2000: err=+0.00 (was +0.15)
q1@500: err=-0.05 (was -0.62)
q1@2000: err=-0.41 (was -0.57)
q10@500: err=+0.57 (was +0.01)
q10@2000: err=+0.22 (was -0.00)
```
The rp parameter SOLVES the 500Hz residual for q0.1 and q1 (the main bottleneck),
but slightly degrades q10 (the narrow-Q case).
### Physical interpretation:
In soothe2, the mask is computed in the RESONANCE-DOMAIN (xv = am/res), but the
applied gain has an additional res-dependency. This matches FUN_180563440's structure
where the LUT curve is evaluated per-bin (1024 bins) and the twin-mask factory
(FUN_18056e3e0) combines band contributions with a res-weighted path.
### Next steps:
1. Refine rp jointly with al_* (need faster al_* rendering — batch the 6 lv cases)
2. Update framed_render.py with rp parameter
3. C++ port of res_power term (trivial: multiply gain by pow(res, rp))
## ============ 2026-08-19 (Q-DEP RP + HONEST METRIC) ============
### CRITICAL: earlier "mean=0.160/0.144" were ARTIFACTS (untracked output length)
- Fit scripts (qdep_rp.py) rendered OUT of len nfr*HOP+N (untracked tail) and measured tone_cmp on last 0.75s of that extended buffer.
- framed_render.synthe trims to len(x). The two disagree by ~+0.2dB systematically.
- Honest (trimmed) numbers for scalar rp=0.0169 (h3): mean=0.280 max=0.618 (q10@500).
- Earlier claim "dual mean=0.160" does NOT reproduce on trimmed metric.
### Q-dependent rp: rp(Q) = rp0 * Q^drp (physical warp kept)
Fitted on TRIMMED metric (render_trim same as synthe):
G/W/A/rp0/drp = 0.9963/0.3335/0.9807/0.0275/0.2159
mean=0.175 max=0.707:
q0.1: 500 +0.000, 2000 +0.001
q1: 500 -0.256, 2000 -0.707
q10: 500 +0.086, 2000 +0.001
q0.1 and q10 both essentially perfect. Bottleneck is q1@2000 (over-reduction -0.71).
Weighted refits move error around but never reduce max < 0.7:
- w(q1@2000)=3, w(q10@500)=3: q0.1@2000 -> +0.885 (rows shift, no global win).
- free warp (K/alpha/p): mean=0.139 but q1@2000 -0.534 and warp non-physical (alpha=3.19).
### Committed to framed_render.py (Q-dep rp, trimmed honest baseline):
G/W/A = 0.9963/0.3335/0.9807, RES_POWER = lambda q: 0.0275*q**0.2159
### Interpretation
rp(Q) ~ Q^0.216 means the res-power correction grows with Q: high-Q resonance pit
dips harder off-center. Consistent with decomp (res-weighted gain path in twin-mask
factory). The q1@2000 residual is structural warp/model mismatch, not rp.