Files
soothe2-re/handoff/NOTES_LEVEL.md
T
Matiq fa1ef205df P4: F2 structural wiring tests — combine+FFT-conv naive wiring both regress (doc only)
Wiring FFT-conv step5 (mask*=win) gives mean 5.2, combine/acc (att/rel from kRTAtt/
kRTRel) gives 4.7 vs base 0.75-0.80 on representative subset. Both belong to the
exp2(mask) structural chain (blend 0.8 + bigkernel) NOT the fitted bridge final-gain.
No code change; bridge + Pchip stays canonical (0.773).
2026-08-20 20:52:00 +03:00

1080 lines
78 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-20h: P2 mask-accumulator combine kernels DECODED ============
Raw bytes from rt snap (objdump of image 0x180008d60/5a20/3c40). Implemented in dsp/levelpath.cpp.
### Exact CRT thunk semantics (per-bin double ops in FUN_180529fe0):
- **0x8d60 combine3(dst,a,b,n) = sub**: out[i] = a[i] b[i] (vsubpd; dst is the 3rd pointer).
Per-band call: 0x5406f8[i] = 0x540678[i] 0x5407c8[i]. (corrects NOTES:348 "combine" TBD)
- **0x5a20 acc_add(dst,src,n)**: dst[i] += src[i] (double; kernel 0x18001a5a0).
- **0x3c40 acc_fma(dst,a,b,n)**: dst[i] += a[i]·b[i] (double; vfmadd213pd).
- 0x11940 = copy/mirror (complex, 5th arg stride 0/4 → kernel 0x1a88300), 0x6e40 = dst=src·scalar,
0x15060 = buf+=scalar, 0x8700 = dst*=src (float, warp), 0x9be0 = buf=scalar·buf.
### Full mask-accumulator per band (0x5408b8==0):
1. 0x5406f8 = 0x540678 0x5407c8 (8d60 sub)
2. mirror 0x5406f8 halves (11940)
3. 0x5407c8 += 0x5406c8 · 0x5406f8_upper (3c40, stride4)
4. 0x5407c8 += 0x5406e8 · 0x5406f8_lower (3c40)
5. 0x5407c8 += 0x540678 (5a20)
## ============ UPDATE 2026-08-20g: P2.5 twin-mask factory + band LUT apply DECODED ============
Source: decomp_funs2.txt (FUN_18056e3e0:7988, FUN_180563a60:8975, FUN_180563440:7697) +
f_56e3e0.dis + f_563a60.dis. Implemented in dsp/levelpath.cpp (levelpath_check ALL OK).
### FUN_18056e3e0 = "twin-mask factory" is a CONSTANT FILL, NOT per-bin twin resonance:
s = DAT_1824c4248 / (double)((float)[ctx+0x240080] * [ctx+0x24])
= 2π / (count · SR) (0x24c4248 = 6.283185307 = 2π)
fills 0x400 bins of the mask (stride 0x2000/band) with s. The twin resonance shape enters
elsewhere (via the LUT curve FUN_180563440), NOT here. (corrects NOTES:547 "twin resonance".)
### FUN_180563a60 = band LUT apply (level -> gain), 6 bands × 0x400 bins:
level_dB = logf(mask[band][bin]) · 8.6859 (20·log10)
axis[bin] = bin · (1/1024) (0x24c3c50 = 0.0009765625, DIFFERENT from 1/1023)
t = clamp((dB A)/(B A), 0, 1) (BandConfig ctx+0x180: A=min,B=max,gamma,flag)
if gamma == 1.0: val = t
elif flag == 0 (linear): val = t^gamma (powf, DIRECT gamma)
else (power-law): val = 0.5·(1 + sign(2t1)·|2t1|^gamma)
axis[bin+1] = val (pairs level, gain)
NOTE: this is the INVERSE of FUN_180563440 (which uses x^(1/γ)); 563a60 uses t^γ.
### Constants locked (from rt snap rodata):
0x24c4248 = 2π (double), 0x24c4230 = 5 (double), 0x24c3c50 = 1/1024, 0x24c3c54 = 1/1023.
## ============ UPDATE 2026-08-19e: FUN_180529fe0 FULL DECOMP CONFIRMS MASK-CANON ============
Source: /tmp/consumers_out.txt:471-1277 (full Ghidra decomp). Validates + completes the
"CONFIRMED per-band pipeline" section below (lines 237-253). Notes:
### Mask chain (0x5408b8==0 mono path; 0x5408b8!=0 = dynamic/stereo-extra path)
Per band (band in [iVar20, lVar18)):
1. scale: 0x540678[band] *= (fVar30/nBands)·0x540870·0x54088c (0x9be0 = buf*scalar)
2. IIR smoother: FUN_18052d650(state 0x440518, out 0x5406f8, in 0x540678) then a 2nd IIR
(states 0x3404f8/0x2c04f8, count 0x2404e8) — double smoothing (att/release).
3. online blend (0x5408b8==0):
0x5406f8 = 0x540698·(1-mix), mix=0x54087c (0x6e40)
0x5406f8 += mix·0.8 (fVar25=0x24c3e28=0.8) (0x15060)
0x540678[band] = bigkernel(0x540678,0x5406f8) (0x26b820)
4. combine: 0x5407c8[band] = combine3(0x5407c8,0x540678,0x5406f8) (0x8d60)
+ mirror both halves (0x11940) + WEIGHT appends:
0x5407c8 += 0x5406c8·0x5406f8_upper (0x3c40, stride4)
0x5407c8 += 0x5406e8·0x5406f8_lower (0x3c40)
+ 0x5407c8 += 0x540678[band] (0x5a20)
5. (0x5408b8!=0 branch: freq-axis blend 0x6e40/0x15060 + 0x26b820 + 0x4f0400 weight)
6. WARP (tilt): 0x540678 *= 0x5406a8 (freqpath warp) (0x8700)
7. dry/wet: mask = mask·(fVar30·0x540888) + (1-fVar30) fVar30=0x540874rnd (0x8800-ish)
8. FFT-conv (0x52b550 steps 1-6) -> FIR -> conv with audio -> out[band]
- 0x5407c8 / 0x540678 / 0x540768: pointer arrays stride 0x10 (per band), 0x5406c8/6e8 = weight tables.
- Constants: fVar11=1.0 (c3ea4), fVar25=0.8 (c3e28), fVar28=0x24c4674(-0.7), fVar27=0x24c4670(-0.5).
### Confirmed scalar A/R (0x540888/88c) = expf(p·0.11513), captured window tail degenerate flat.
=> Mask-canonic is now FULLY mapped structurally. Remaining for bit-exact = exact scalar values
(XMM9/XMM13, 0x540870/874/87c/88c from RPP) + the level-LUT curve A/B/γ (ctx+0x188 BandConfig).
Structural C++ port of steps 3-8 exists: dsp/fftconv.cpp (P1.3) + dsp/levelpath.cpp (LUT curve).
## ============ UPDATE 2026-08-19d: f_52d990 / f_52d920 / f_52db50 DECODED (CORRECTION) ============
Disasm: handoff/nls_dasm/f_52d990.dis (52 B), f_52d920.dis, f_52db50.dis; caller f_52b570.dis.
### Identity (they are CRT-style thunks, NOT arithmetic kernels):
- **f_52d990(rcx=dst, rdx=src, r8d=count) = memmove/memcpy thunk.**
Prologue saves rbx/rsi/rdi; movs args into callee regs; indirect call through GOT 0x1bab008
(the CRT "select scalar vs vector" resolver, same 0x2650db8 pair used everywhere); then tail-jumps
to 0x2000 (scalar path) or 0x1c40 (vector path). Returns void. This is a COPY, not a multiply.
- **f_52d920(rcx=dst, xmm1=val, r8d=count) = scalar fill (low-half filler).** tail 0x2030(float)/0x1d30(double).
- **f_52db50(rcx=dst, xmm1=val, r8d=count) = scalar fill variant.** tail 0x19a0/0x22d0.
=> The earlier NOTES "0x52d990 = FIR *= WINDOW" was WRONG. Corrected step map below.
### Corrected step 5 (f_52b570:52b75e-52b78c):
```
rax = [ctx+0x540534] ; N (FFT size)
rbp = N/2
FIR[N] = 0 ; zero the Nyquist bin
rax = [ctx+0x540658] ; window base (WIN_WINDOW, 8193 f32)
rdx = rax + N/2*4 ; src = window + N/2
call f_52d990(FIR, window+N/2, N/2) ; FIR[0..N/2-1] = window[N/2..N-1] (COPY)
call f_52db50(FIR+N/2, xmm9, N/2) ; FIR[N/2..N-1] = xmm9 (const fill)
inverse FFT
```
So step 5 replaces the FIR's lower half with the SECOND half of the captured window
(window saturates to 1.0 at idx 2049; so window[N/2:] is all 1.0 WHATEVER N/2>=2049 — the tail
copy degenerates to a flat 1.0 unless N/2 < 2049, i.e. FFT size would have to be < 4098 for the
window shape to matter here). Upper half filled with a constant xmm9.
### Corrected step 3 (f_52b570:52b685-52b6b3):
```
rax = N; rbp = N/2
FIR[N] = 0
call f_52d920(FIR+4, xmm13, N/2-1) ; FIR[1..N/2-1] = xmm13 (LOW fill)
call f_52db50(FIR+4+N/2, xmm9, N/2-1) ; FIR[N/2+1..N-1] = xmm9 (HIGH fill)
inverse FFT
```
### Size inference & effective semantics
- window = 8193 f32; it saturates to 1.0 at idx 2049 (w[2048]=0.8, plateau 1.0 beyond).
- freq-axis = 2048 f32, spacing 48000/4096 = 11.718 (matches 4096 FFT half). => N≈4096, N/2≈2048.
- Then step 5 copies window[2048:4096] = {0.8 -> 1.0 (mostly flat)} into FIR[0:2048],
upper FIR[2048:4096]=xmm9(=0?), FIR[4096]=0. Effective = gentle low-pass ramp, near-flat.
This matches NOTES line 416: captured window adds almost nothing over empirical (it's essentially 1.0).
### Remaining unknowns (now narrowed):
- xmm9 / xmm13 scalar values = still the FUN_180529fe0 prologue constants (need prologue read or live capture).
- window[N/2:] copy + xmm9 fill IS the whole "window" mechanism; the earlier "FIR *= WINDOW"
and "0x540658 is freq-shaped" speculation is replaced by: **FIR spectrum lower half = window tail,
upper half = const xmm9**. Window content already captured (rtwin_freq_44100.npy → WIN_WINDOW).
## ============ 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.
## ============ UPDATE 2026-08-19f: LEVEL-TRACKER UPDATE-LOOP LOCATED (structural) ============
The UPDATE loop is NOT a separate function — it is the inline bidirectional IIR smoothing
inside FUN_180529fe0 per-band loop (consumers_out.txt). Three first-order IIR stages:
- (a) FUN_18052d650(state 0x440518, out 0x5406f8, in 0x540678[band]) — outer smoother
- (b) inline: coeff-A array 0x4c0528, scalar-acc state 0x540528, mask src/dst 0x5406f8 (count iVar19)
- (c) inline: coeff-A array 0x3c0510 / 0x2c04f8, acc 0x440510 / 0x3404f8+0x2404e8 (two more)
Scalar form (proven, consumers 745-1015):
acc = 0; for i in 0..N-1: y = x[i]*A[i] + acc; acc = y; x[i] = (float)y
i.e. cumulative leaky-integrator over A[i] (running-acc coefficient). Vector variant does 4/iter.
=> The sample-path smoothing the tracker needs is STATICALLY known in structure; only the
coefficient arrays A (0x4c0528/0x3c0510/0x2c04f8) + the 0x530d30 per-bin level-weight caps
remain runtime values (0x530d30 shown numerically flat w~=0, NOT the tilt source).
Bit-exact port of the UPDATE requires A[] live capture (or FUN_18052d650 body) — next P1.5-ish.
Boundary: structural scaffolding now complete (levelpath + fftconv + mask-canon + tracker form);
all remaining unknowns are runtime scalar/array values, not structure.
## ============ 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.
## ============ 2026-08-19: STATIC DECOMP LOCKED (all DSP bodies recovered) ============
New full decomp run (DumpFuns2.java, no haveDec gate, seeds=dsp+vtables+reverse-callers,
skip<8): decomp_funs2.txt (7746 bodies), fun_map2.txt (7928 funcs), consts2.txt (41622).
Previously-cold roots now present: 180529fe0 (2051in), 180563440, 180563ce0 (IIR INIT),
18056e3e0, 18052e9b0 (3167in), 18052e260, 18052e190/52e9b0 allocators.
### MAIN RENDER LOOP (found): FUN_18052e260
- Signature (param_1=DSP obj, param_2=nSamples). iVar5=NFFT/2+1; loop per band
(count=0x2404d0): FUN_180536300(param_1+0x3d8, band_in_ptr, 0x540758+bufoff, band, n).
Uses full-STFT frame budget 0x2404d8; tail marks bands needing rebuild (0x240467 flags).
- => 52e260 = per-frame dual-band SIDE-CHAIN driver; twin = per-band.
### FULL decomp FUN_18052e260 (decomp_funs2.txt:348915) — the frame driver:
```
iVar5 = NFFT/2+1; iVar2 = min(iVar5, [0x2404d8]); [0x2404d8] = iVar2; param_2=min(iVar2,param_2)
for band in 0..[0x2404d0]: FUN_180536300(ctx+0x3d8, band_in + (iVar5-iVar2)*4,
[0x540758] + (iVar5-iVar2)*4, band, param_2)
[0x2404d8] -= param_2
if [0x2404d8]<1 && [0x30]>0: fill(0x540788, 1.0, iVar5) // frame done -> reset dry/wet
```
So 52e260 only DRIVES the per-band twin side-chain (536300); the LUT (563440) +
mask-apply (529fe0) + FFT-conv (52b550) run elsewhere (triggered when [0x2404d8]
drains, i.e. per STFT frame). Full pipeline assembly (P4) = wire these in order:
twin(536300) -> level -> LUT(563440/56e3e0/563a60) -> mask(529fe0) -> FFT-conv(52b550).
### FUN_180536300 (twin caller, size=475) — audio path per band:
- lVar1 normalize = FUN_18052da00(scratch, in, scale_bin, n): pointwise in[i]*scale.
- band active (0x814): twin kernel FUN_180535880/180536f90(lVar2, base, band_i, lVar1, n),
else fill FUN_18052db50 (1.0 / 0.0).
- combine lVar4/lVar3 (52d990, 0x814>1), output copy 52dbc0(base, lVar4, n).
- TWIN KERNELS (535880/536f90): Ghidra emits ONLY the tail-call frame —
"WARNING: Removing unreachable block ..." => state shown = {pdVar1,*pdVar2} double pairs
read from param_3/param_4, then noreturn tail into thunk_FUN_181ba94b0. **The recursive
per-bin IIR level-smoothing (tatt/trel) lives INSIDE these two tail-calls, which static
decomp cannot recover** — same family as 0x540658 window (step-5 window). Confirmed
statically-invisible frontier = {532a715 twin kernels, 0x540658}.
### Level-param setters (vtable stubs, auto-named .?AV?$Soothe2Module@M$01@@::vtbl_...):
- 18052bba0: 0x540870 = expf((p*DAT_1824c4348 + DAT_1824c44a4)*DAT_1824c3cd4) [gain/sens]
- 18052bb80/bb60: 0x540878 / 0x54087c = raw int [freq?/bandwidth?]
- 18052bb40/bb20: 0x540880 / 0x540884 = raw int [per-band level / dur]
- 18052bb00: 0x540874 = p (locked); 18052bad0/baa0: 0x540888 / 0x54088c =
expf(p*DAT_1824c3cd4) [attack / release coeff db]
- DAT_1824c3cd4 = 0.115129255 = ln(10)/20 => expf(p*0.11513) = 10^(p/20): **all these
setters convert dB-speed params to linear coeffs**. 0x540888/88c feed FUN_180529fe0
step-5 dry/wet (mask peak) — the real attack/release coeffs.
- FUN_180530d30 (0x5406b8/6c8/6d8/6e8 weights): fVar9 = 2000.0/NFFT? base; w8=pow(base,0.25);
v = level*0.25*w8*factor(4.0); q=1/(1+v/(level*4)); dVar1=(sr/0x1a0)*0x1ac*0.001;
w = 0.1^(1/(max(q*v)*dVar1)); pairs (w,1-w).
- Numeric check (this session, real values): bin500 w~10^-6, bin2000 similar => 0x530d30
does NOT tilt 2000>500. Tilt stays = warp (0x5406a8, K=7.942) + twin-mask curving.
Static decomp now COMPLETE up to the statically-invisible frontier confirmed above.
## ============ 2026-08-19 (runtime capture SETTLED): registry heartbeat + live tables ============
## **Window 0x540658 + freq-axis CAPTURED live. Internal DSP sample rate = 48000 (not 44100).**
- rtsnap_fast.py: spawn reaper render_long offline -> find host (soothe2 in /proc maps, not reaper
cmd) -> sleep 6s -> snapshot ALL readable maps chunked 8MB -> /tmp/snap_all.bin (318MB, 546 regs,
entries {lo,sz,bytes}+pad, idx in /tmp/snap_all.idx). Chunked pread REQUIRED: whole-region pread EIO.
- **registry heartbeat**: heap run of {u64 count, u64 ptr} pairs (stride 0x10) at **0x29b06c0**
(arena anon 0x2922000, 0x77e000), 36 entries — analog of GUI note 0x28b06c0. Every DSP buffer
base is a registry entry: [00] identity, **[01] = WIN_freq window 0.5->1.0 (saturates)** (this is
live 0x540658 8193 f32: 0.5, idx512=0.345?; idx1024=0.68, idx2048=0.8, tail 1.0),
[02] 0->~0.01, [03]=rwin_C0 0.596->0.126, [04]=0.404->0.874 complement, [05]=0.0435->~0,
[06]=0.9565->~1.0, [07] zeros+neg, [0d]=**freq-axis 0..23988.3 spacing 11.713 = 48000/4096**,
[0e]=2.017->0, [0f] doubled, [10]=1.2914->0, FFT work bufs 131072/32768/65536/16384.
- **CRITICAL**: freq-axis spacing 11.713 Hz => internal SR = 48000 regardless of project 44100.
Registry[01] window & weights are therefore the 48k tables; rwin_C0[03] byte-matches GUI 44100 file
(same table reused). Saved live tables:
- handoff/rtfreqaxis_48000_internal.npy (2048 f32, 0..23988.287, spacing 11.713)
- handoff/rtwin_freq_44100.npy (8193 f32 WIN window 0.5->1.0) <-- THE 0x540658 live capture
- handoff/rtwa_596.npy, rtwb_404.npy, rtwc_043.npy, rtwd_956.npy (registry [03][04][05][06])
- vptr/44100-marker object-scan (rtobj/rtdump2/rtall) remains DEAD: no vtable 0x1824abb90 bank hit in
heap, fresh-render host has no 44100.0 constants (internal is 48k). The registry run is the beacon.
- twin IIR per-bin attack/release: NOT in registry (those are {att,rel} pair floats at 0x540888/88c,
expf(p*0.11513), per-note-band smoothing inside tail-call thunks). Live capture needs the twin band
object; not yet located. Registry gives per-bin WEIGHTS (done) — attack/release remain static-only.
- Registry dump + all raw tables saved: /tmp/rtcapt/registry.txt, /tmp/rtcapt/*.f32.
- Scripts captured into repo: rtsnap_fast.py (snapshot), findctx.py (locate), rtchunk.py (chunked IO).
## ============ 2026-08-19 (STRUCTURAL LUT CURVE FUN_180563440 — EXACT FORMULAS) ============
- Verified against f_563440.dis + PE constants 0x1824c3c54=0.0009775171(=1/1023? 1/0x3ff),
0x1824c3ea4=1.0, 0x1824c41e0=2.0, 0x1824c4680=-1.0, 0x1824c3d8c=0.5, 0x1824c4f10=0x7fff.. (double |x| mask).
- Loop: 0x400 iterations (0..0x3ff), x = i*(1/1023)? const 0.0009775171 = 1/1023 → last bin x≈1.0.
(prior note said 1/1024 scale — corrected: constant value is 1/1023.)
- Curve config at ctx+0x188: word A(+0x00)=min, B(+0x04)=max, gamma(+0x0c), flag(+0x10),
callback(+0x50). Output written as double to ctx+0x198[i*8] (0x400 doubles).
- **Linear path (flag 0x10==0, 0x563595)**: t = x; if gamma!=1.0 and x>0: t = exp(log(x)/gamma);
val = A + (B-A)*t. (A=min of output, B=max).
- **Power-law path (flag!=0, 0x5635cd)**: t = 2x-1 (centered -1..1); if gamma!=1.0:
t = sign(t) * exp(log(|t|)/gamma) (abs-mask 0x24c4f10, log DIVSS gamma, exp, sign via xmm11/xmm8);
val = A + (B-A)*0.5*(1+t). When gamma=1.0 → val = A + (B-A)*x (identity linear).
- This is the exact runtime curve to replace Pchip/LUT empirical (roadmap Q1). Need live (A,B,gamma)
per band config from ctx+0x188 — requires DSP ctx base (registry doesn't own the curve struct).
- Level-tracker IIR (FUN_180563ce0) INIT confirmed: 341 bins x2?, state rows at +0x28/+0x40/+0x58
init [1,0,0,0]/[-1,0,0,0], level coeff 0.1 (3dcccccd) = attack/release α; UPDATE loop remains
unmapped (field for future live capture; per registry tables stable between snapshots).
## ============ UPDATE 2026-08-20i: REAL MASK CHAIN IN C++ (P4) ============
Replaced empirical PCHIP detector with the live-calibrated mask chain:
### Structure (mirrors FUN_180529fe0 mono path):
level_k = am_k * res_b[k] * level_scale
track += w_k * (level - track) per-bin attack/release (rt_weights.hpp)
acc_k = (level - track) + level accumulator (live peak ~10.08 at tone)
mask_k = (1/(1+K*acc))^n K=9.8026, n=0.25966 (fit to live mask)
final = min over bands
### Live calibration zipped:
- live acc 0x5407c8 @1000Hz = 10.08 ; mask @1000 = 0.2976 (level_scale 1630 makes
am*res*1630*... hit acc ~78 at tone -> mask 0.178, matches ref -14.98dB).
- 678[0]=mask (0..1) is what we model as gain; warp/band-mult still missing.
### Results (framed_test, N=2048 hop=512, sqrt-Hann OLA):
- t1kq single band (678.76,q~1,sens12,scale1630): ref -14.98 dB, out -14.47 dB, err +0.51 dB
- comb 4-band (678.76/q1/s12,1778.7/q3/2.24,195.1/q3/2.24,13408/q3/s12): per-tone err
500:+5.8, 1000:+1.3, 1500:+1.8, 2000:-3.4, 3000:+5.6 dB. RMS err -16.8 dB (ref rms +4.3).
- Old PCHIP cut comb by ~6dB; new mask chain preserves comb (mostly ~1dB off).
### Remaining:
- warp tilt (0x5406a8) + per-band 0x540768 mult + FFT-conv smoothing (step 6-8 decomp)
- exact BandConfig (A/B/gamma) for band LUT 0x563a60
- rt_weights tables only cover bins 0..1024 (2049 len, rest zero); extend if higher bins matter
## ============ UPDATE 2026-08-20j: FUN_180529fe0 MONO-PATH FULLY DECODED + TABLES EXTRACTED ============
Read /tmp/consumers_out.txt:471-1277 (full decomp) + f529fe0.dis + live snap_rt.bin (ctx 0x2370040).
### Exact per-band mono-path (0x5408b8==0), FUN_180529fe0:
1. scale: 0x540678[band] *= (fVar30/0x1a0)·0x540870·0x54088c (0x9be0) [fVar30=PRNG, 0x540870=440.95]
2. IIR1: FUN_18052d650(state 0x440518, out 0x5406f8, in 0x540678) = leaky:
y[i] = A[i]·acc + B[i]·x[i]; acc=y; x[i]=(float)y
A@0x4c0528 B@0x440528 (2049 doubles), B=1A, A ramps 0→0.692 (fast attack at low bins)
3. copy: 0x5406f8 <- 0x540678[band] (0x5160) — actually the IIR1 in/out feed; then
4. IIR2: inline on 0x540678[band]: A@0x2c04f8 B@0x2404f8 (A ramps 0→0.086, SLOW release)
5. mirror halves (0x11940)
6. blend: 0x5406f8 = 0x540698·(1mix) + mix·0.8; mask[band] = bigkernel(mask,0x5406f8)
bigkernel 0x26b820 = VECTORIZED exp2 (tables log2e=1.4424@0x1f31740, floor 708.9,
mantissa 2^(k/256)@0x1f38c80) — computes mask[i]=exp2-based op on (mask,0x5406f8)
7. combine: 0x5407c8[band] = 0x540678 0x5406f8 (0x8d60 sub); mirror;
+= 0x5406c8·upper (0x3c40 stride4); += 0x5406e8·lower (0x3c40); += 0x540678 (0x5a20)
8. WARP: 0x540678 *= 0x540768[band] (0x8700); *= 0x5406a8 (0x8700) [TWO warp mults]
9. IIR3: inline on 0x540678[band] TWICE: A@0x3c0510 B@0x340510 (=A2/B2 shape)
10. dry/wet: 0x540678 = mask·(fVar30·0x540888) + (1fVar30); fVar30=0x540874rnd
11. FFT-conv (0x535a70) -> FIR -> conv(audio param_2)
### Runtime tables EXTRACTED -> dsp/rt_mask_tables.{hpp,cpp} (2049 doubles each, live):
- kIIR_A1 (0x4c0528): 0→0.692 rising; kIIR_B1 (0x440528) = 1A1
- kIIR_A2/B2 (0x2c04f8/0x2404f8): A 0→0.086 (slow); kIIR_A3/B3 (0x3c0510/0x340510) = same
- kWarp (0x5406a8): 0→3.899 (freqpath tilt, 8193 f32)
- kBand768 (0x540768[band0]): 2.017→0.271 (per-band warp mult, 2049 f32)
- kPRNGLut (0x5408b0 -> 0x152a3d0): 512 f32 (0.35..6.66, 189 nz)
- PRNG state 0x2404e0 = 112; DAT_18262b5c8=0.4552, b704=0.6089, b700=0.6070 (int32->float)
- scalars: 0x540870=440.95, 0x540874=1.0, 0x54087c=1.0, 0x540888=1.0, 0x54088c=1.0
- 0x1a0=2048, 0x540868=16384, 0x54086c=8193, band count 0x2404d0=7, 0x30=2 (stereo)
### NOTE (SR mismatch): internal DSP SR=48000, N=4096 (bin spacing 11.713 Hz). Host refs 44100.
IIR tables indexed 0..2048 (N/2+1). For bit-exact the detector must run N=4096@48k internally.
## ============ UPDATE 2026-08-20k: bigkernel 0x26b820 = vectorized exp2 (semantics LOCKED) ============
Read the SIMD loop (objdump 0x18026b820-0x18026bcaf). Key line 0x18026bba0:
vmulpd ymm12 (exp2 result) by ymm11 (loaded from rbp = blend buffer 0x5406f8).
=> mask[i] = exp2(x) * blend[i], where x is the mask value (0x540678[band]) and
blend = freqaxis*(1-mix)+mix*0.8 (=0.8 at mix=1.0). sign: to get attenuation
(mask<1) x must be negative, so mask = exp2(-level) * blend. Locked.
exp2 is standard log2e/poly/mantissa-table (0x1f34a80..., log2e~1.4424, floor -708.9).
### Calibration (framed_test, structural chain now in framed_model.cpp):
- t1kq single band (678.76,q~1,sens12): level_scale=600 -> err -0.06 dB (ref -14.98).
- comb 4-band at scale 600: per-tone err 300:-5.1, 500:+7.8, 1000:+4.4, 1500:+3.9,
2000:-1.3, 3000:+4.8 dB (max 7.8). Mask shape (warp tilt + FFT-conv smoothing)
still approximated.
## ============ UPDATE 2026-08-20m: PRNG prologue transcribed (structure) ============
FUN_180529fe0 prologue (decomp :515-583) = PRNG coefficient generation feeding
scale fVar30 and dry/wet, via LCG + PRNG LUT (0x5408b0 -> 0x152a3d0, 512 f32, live).
LCG: state += (round offsets) & 0x8000007f with sign fixup. Constants:
DAT_18262b5c8=0.4552, DAT_18262b704=0.6089, DAT_18262b700=0.6070, DAT_1824c3c58=0.001
Round products fVar27*LUT[..]*fVar28*LUT[..]+0.001 are ~0.28 at PRNG state 112
(live), so (int) truncates to 0 => fVar30 can be 0..~ range frame-to-frame.
This is genuine per-frame RANDOMIZATION in the scale step (x *= (fVar30/0x1a0)*...).
structural model averages it via level_scale calibration; bit-exact requires the
exact LCG sequence per frame. scale already calibrated (t1kq -0.43 dB).
### Remaining bit-exact gaps (documented, next sessions):
1. exact LCG/LUT per-frame fVar30 (scale + dry/wet randomization)
2. FFT-conv (0x535a70) mask smoothing before FIR
3. bit-exact vectorized exp2 (0x26b820) vs std::exp2
4. combine/accumulator (0x5407c8) exact feedback (1500Hz comb err +6.4 dB)
5. internal SR=48000/N=4096 vs host 44100/N=2048 and twin per-bin IIR (statically
invisible, FUN_180535880 tail-calls)
## ============ UPDATE 2026-08-20n: PRNG fVar30 LOCKED + CONSTANT FIX ============
### CRITICAL CORRECTION: DAT_18262b5c8/b704/b700 == 1 (NOT 0.4552/0.6089/0.6070)
soothe_mem.bin is a VA-linear dump: file offset = RVA = VA - 0x180000000.
Earlier 0.4552/0.6089/0.6070 came from adding a spurious (+0x1e00-ish) section
adjustment that is NOT applicable. Verified at raw=RVA=0x262b5c8/b704/b700:
bytes 01 00 00 00 => int 1. Code does cvtdq2ps => (float)1 = 1.0. All three = 1.
Verified constants at raw=RVA: 0x24c3c58=0.001, 0x24c3e28=0.8, 0x24c4674=-0.7,
0x24c4670=-0.5 (match NOTES).
### PRNG prologue (FUN_180529fe0 :515-583) TRANSCRIBED to C++ (prng_fvar30):
LCG state 0x2404e0 (live=112), round offsets: +0x3cdca,+0x140236,+0x10d56,+0xdf6b6
fVar30(line554) = (int)(f32(LUT[s+1])*f32(LUT[s]) + 0.001f) [b5c8=b704=1]
=> at live state 112, fVar30 == 1.0 deterministically over 300 frames.
=> scale step: level *= (fVar30/0x1a0)*0x540870*0x54088c = /2048*440.95*1.0.
scale is NOT effectively randomized for this LUT/state.
dry/wet fVar30 (line1163) = 0x540874 - rnd, rnd from IAT stub 0x181a14cac
(jmp *0x181bab330, CRT import) - random dither, not statically lockable.
### framed_model.cpp: scale coeff now computed via prng_fvar30() (prng_state_=112),
not a hardcoded constant. t1kq still -0.43 dB at level_scale=600 (unchanged).
## ============ UPDATE 2026-08-20o: CRITICAL — level = am / res (not am * res) ============
Found + fixed a sign/direction bug in framed_model.cpp level computation.
res = |2B/A| (twin) is MINIMAL at band centre (0.117), NOT maximal. The model uses
xv = log10(A_k / res_k), i.e. LEVEL = am / res. My code had level = am * res which
inverted the fc-resonance: it cut MORE off-center and LESS at fc, producing a flat
inverted fc-response.
Fix: level = am_ / res_[b][k] (with 1e-12 floor). Now the fc-scan (tone1kq, band fc
swept 800..1200, refs t1kq_only1_<fc> = true single-band) tracks the reference shape
(deepest at fc==tone):
scale=42: fc900 +0.3, fc950 +1.3, fc1000 +0.9, fc1100 +0.2; mean|err| 2.46 dB,
max 5.3 dB (worst at edges fc800 +3.9, fc1200 +5.3 - ref cuts broader than model,
likely needs FFT-conv mask smoothing / wider effective notch).
Remaining under-cut off-center: real soothe reduces ~16-22 dB broadly across
800-1200 while model narrows; candidate = FFT-conv smoothing + warp + q shape.
DEFAULT level_scale for am/res path ~= 35-45 (was 600 for the wrong am*res path).
## ============ UPDATE 2026-08-20p: Phase B — framed_model RE-TRANSCRIBED ============
Re-transcribed dsp/framed_model.cpp per confirmed chain (NOTES:199-226, source
/tmp/consumers_out.txt:638-1111). Structural divergences from the old approximation:
- IIR1 (FUN_18052d650, state 0x440518) writes into the SHARED 0x5406f8 buffer,
then bridges into the band's 0x540678 (:731 0x5160); IIR2 (0x3404f8/0x2c04f8 on
the band mask) kept separate.
- mirror 0x11940: upper half = reversed lower (Hermitian), full-nfft scratch.
- blend step: f6f8 = 0x540698·(1mix) + mix·0.8; mask = exp2(mask)·f6f8. C_AXIS=1.3
placeholder (offline 0x540698 = per-band scalar, NOT the exp-formula; NOTES:354).
- combine (0x8d60/0x3c40/0x5a20): track = (maskblend) + kRTAtt·blend +
kRTRel·blend + mask, using live weight tables kRTAtt/kRTRel (0x5406c8/6e8).
- warp (0x540768·0x5406a8), IIR3 (A3/B3)x2, dry/wet identity.
VALIDATION (scale sweep):
- DUAL (band fc=500, tones 500+2000 at 7.23 dBFS): ref @500 53.7/@2000 29.6.
model @500 matches over scale (s30 51.5, s42 58.1); @2000 stuck ~4..13 (way
short). The DAZING far-field reduction (reduce 2000 by 29.6 when band is at 500)
does NOT come from the exp2(linear·level) chain — exp2(am/res) collapses to ~0
once res@2000 (1.344) ≫ res@500 (0.1175).
- T1KQ only1 fc1000: 21.1 vs ref 22.1 (OK). fc-scan shape intact.
=> Phase B confirms structurally: the missing stage for dual-far-field is the
dB-domain LUT FIX_180563a60 (level_dB = 20·log10(mask) → t=(dBA)/(BA) clamp →
t^γ), which COMPRESSES the exp2 collapse into a smooth log curve. NEXT = decode
BandConfig ctx+0x188 writers (A/B/gamma) + 0x540698 -> then wire into the chain.
## ============ UPDATE 2026-08-20q: METRIC FIX + LOG-DOMAIN LUT PORTED (honest baseline) ============
### CRITICAL METRIC BUG FOUND: 24-bit refs mis-decoded as 16-bit
Earlier dual
## ============ UPDATE 2026-08-20r: FULL-CORPUS HONEST ASSESSMENT (committed baseline) ============
Honest 24-bit metric across ALL datasets (input=3.5s window, correct sw decoding):
dual (band fc=500, q-sweep 11): mean|err| 0.41-0.61, max 0.72 ✓
al_* (fc=1000==tone, level sweep): lv3-12 err 0.05-0.43; lv18 -1.12, lv24 -1.80 (LOW-level over-cut)
res_only1 (resonant.wav, fc 300..700): -1.71..+0.81 (under-cut off-center, shape)
t1k_b1f (tone1k 0dBFS loud, fc sweep): +1.6..+2.12 (LOUD under-cut: LUT cap 0.667 too low)
t1kq_only1 (tone1kq, fc 800..1200): -0.59..+0.01 ✓
OVERALL mean|err|=0.77, max 2.12.
Systematics: Pchip LUT frozen (floor 0.366/cap 0.667 from al_* mid-level) under-predicts
BOTH the loud tail (t1k_b1f xv>0.8 needs LUT~0.70) and over-predicts low tail (al lv24
xv<-0.4 needs LUT~0.35). Real LUT extracted from refs: 0.351@-0.5, 0.60@0.07, 0.70@0.85.
Parametric FUN_180563a60 fit to extracted points: A=-13.78dB B=68.29dB gamma=0.344
(mean|err| 0.017 over extraction pts) but full-render VALIDATION regressed dual (real
multiband interplay absent) -> keep Pchip baseline committed; A/B/gamma = runtime
BandConfig ctx+0x188, NOT live-captured (SET TO: static path uses preset curve).
Structural next: decode BandConfig ctx+0x188 writers (FUN_1804835d0/487200 at
decomp_funs2.txt:21937/21962), wire combine 0x5407c8 + FFT-conv 0x535a70.
## ============ UPDATE 2026-08-20s: BROADBAND HYPOTHESIS TESTED (NEGATIVE) + LUT re-cut attempt ============
Tested the mask
## ============ UPDATE 2026-08-20s: BROADBAND HYPOTHESIS TESTED (NEGATIVE) + LUT re-cut attempt ============
Tested the "mask uses scalar broadband level" hypothesis (NOTES:637) directly: windowed
FFT of dual_b1q_1.0 ref vs dual.wav input (2.5-3.5s steady):
gain@500 = 0.309 (-10.2dB), gain@2000 = 0.292 (-10.7dB), gain@1000 (NO tone) = 0.995 (-0.05dB),
gain@1500/3000/5000 = 0.85/1.00/1.00.
=> REDUCTION IS PER-BIN: only bins *with a tone* are cut, empty bins pass through (~1.0).
So the level path is am_k/res_k (per-bin), NOT a scalar broadband level. Model structure correct.
LUT re-cut attempt (floor 0.35/cap 0.70 from t1k_b1f+al_ extraction, monotone 16 knots):
REGRESSED whole corpus (mean|err| 0.77 -> 1.09, dual/t1kq/local worse). Reverted to the
joint-fit 12-knot Pchip (dual<=0.72, fc-scan<=0.59, corpus mean 0.77, max 2.12) = KEEP.
Root cause of t1k_b1f residual (+2.1 loud) and al low-level (-1.8): NOT LUT cap shape alone;
al/t1k use DIFFERENT input (0dBFS vs -18dBFS) and the am normalization (pipeline am vs
tone_amp metric) differs; the joint-fit nodes were fit AT the al_* pipeline xv, so they
don't extrapolate cleanly to t1k_b1f's xv~0.85. Structural A/B/gamma (BandConfig ctx+0x188)
would give the *curve*, but writers are JUCE param plumbing (FUN_1804835d0/487200 = UI curve
setters, not DSP scalar setters) - static decode exhausted for now.
NEXT: wire combine/accumulator 0x5407c8 + FFT-conv 0x535a70 into the chain (structural, may
close the t1k_b1f/al tail by smoothing the mask) - todo #3.
## ============ UPDATE 2026-08-20t: FIT EXPLORATION (todo #3 diagnostics) ============
Tracked what DOESN'T move the needle (all reverted, committed 12-knot Pchip + G/W/A/rp
= 0.9963/0.3335/0.9807/0.0275/0.2159 remains optimal, corpus mean|err| 0.774 max 2.12):
1. sqrt-Hann STFT analysis window (matching framed_render canon) REGRESSED to 0.788 max
2.91 -> kept Hann-Hann (the C++ full-render metric is authoritative, not the python
mask-level canon).
2. Runtime (env) full-corpus fit of G/W/A/rp via framed_test renderer - mask-level
optimizer said G=0.877/W=0.402/A=0.673/rp0=0.0041/drp=0.28 (mean 0.66 mask-level) but
FULL RENDER regressed to 0.812 max 2.37 -> reverted. Mask-level objectives do NOT
transfer to the OLA render (mask applied per-bin interacts with window vs tone_amp
metric); the render metric is the ONLY honest one.
3. Residuals are level-dependent (t1k_b1f loud +0.8..2.9, al low -1.1..-1.8, everything
else <=0.9): the empirical LUT is optimal AT the fit levels; structural A/B/gamma
(FUN_180563a60 band curve) is the missing level-dependence.
TODO todo#3 remains: structural combine/acc 0x5407c8 + FFT-conv 0x535a70 wiring (NOTES:
FFT-conv window ~flat, likely marginal; combine adds band-weight interplay).
### TWIN RES vs PY CANON (todo#3 check)
C++ twin |2B/A| (fs=44100,N=2048, sens_stored=24.65, built via build_twin_coeff/twin_apply)
vs Python bandres (GAIN=4.132=10^(24.65/40), render_parity): match within ~1.5% at fc
300..2000 (fc300: 1.1798 vs 1.1887; fc1000: 0.11746 vs 0.11714; fc2000: 0.7057 vs 0.6963).
=> res shape is NOT the source of the res_only1 fc=300 residual (-1.71 over-cut); that
residual is level-LUT related (input resonant.wav level differs from fit levels).
## ============ UPDATE 2026-08-20u: F0 GATE — DETERMINISM VERIFIED (bit-exact REACHABLE) ============
Re-rendered test RPPs twice each (reaper -nosplash -renderproject on /tmp copies with
RENDER_FILE -> /tmp), then byte-compared data chunks:
1. res_only1_500.rpp (4s):
- a vs b (two fresh renders): data chunk IDENTICAL (max|dx|=0 over 352800 samples).
- a vs committed res_only1_500.wav: data chunk IDENTICAL.
2. t1kq_only1_1000.rpp (6s, 1587600 data bytes):
- a vs b: IDENTICAL. a vs committed t1kq_only1_1000.wav: IDENTICAL.
Whole-file cmp differs ONLY in the bext chunk timestamp (offset ~381: "02-08-2020")
= Reaper render-metadata, NOT audio. Dither/PRNG does not corrupt the rendered signal
(dry/wet rnd in FUN_180529fe0:1163 evidently locked/stabilized in offline render).
CONCLUSION: soothe2 output is byte-deterministic and our Reaper/yabridge CLI re-renders
reproduce the committed references EXACTLY. => Bit-exact (bytes) is REACHABLE; the gate
that could have made it impossible (per-run randomization) is closed. The remaining work
to bytes-parity is purely: exact structural chain + exact kernels + internal 48000/4096
geometry + live A/B/gamma, NOT fighting host nondeterminism.
## ============ UPDATE 2026-08-20v: EXISTING PARAMETRIC LUT SETS BENCHED (F1 closure) ============
All previously-derived parametric-LUT sets (from repo scripts + NOTES) tested on the
FULL honest 24-bit corpus (59 cases) inside ONE fixed bridge pipeline
(C = G*LUT(xv)+W*warp^A, gain=(1-C)*res^rp, committed G/W/A/rp). Result:
LUT form mean|err| max
------------------ -------- -----
pchip (committed) 0.773 3.426 <-- BEST (unchanged)
linear (0.483/0.717) 1.017 3.116
realpts (lutpts.npy) 1.134 3.192
u563a60 (A=-13.78/B=68.29/g=0.344) 1.669 6.004
lut5 (0.4356/0.5914/1.999/0.9364) 1.785 3.951
lut4 (fit_lut) 1.785 3.951
powerlaw(C=8) 6.809 11.880
=> The empirical Pchip curve remains the best form-factor; NO existing parametric set
beats it at fixed bridge params. Realistic conclusion: the recovered runtime curve
(NOTES:967) was fit to extracted (xv,C) points, but those points themselves came from
the SAME bridge-shaped model (LUT=xv->C), so parameteric fits overfit their source
and lose on the honest render metric. Pchip baseline (0.773) stays canonical.
F1 (params of the LUT curve) is hereby CLOSED as "already produced, none accepted";
the structural A/B/gamma from ctx+0x188 remains the only path to bytes (blocked:
level-path object not live-captured; see F2/F5.)
## ============ UPDATE 2026-08-20w: F2 STRUCTURAL WIRING TESTS — BOTH REGRESS (documented) ============
Tested wiring the two remaining structural chain pieces onto the committed bridge
(quick python precompute+OLA on a representative subset; honest 24-bit tone metric):
base : mean|err| 0.75-0.80, max 2.1
FFT-conv step5 (mask *= win8193 0.5->1.0) : mean 5.22 (worse) — masking the gain by
the freq window is WRONG direction; the 0x540658 window enters the FIR step, not a
simple gain multiply (confirms NOTES "window ~flat, adds almost nothing").
combine 0x5407c8 accumulator (acc += att*df+rel*df+g, df=g-acc, per-bin kRTAtt/kRTRel):
mean 4.68 (worse) — naive acc toward 2.36*g overd red by ~6x; the decomp combine
operates in the REDUCTION/exp2 domain (blend 0.8 pedestal + bigkernel exp2), NOT on
the fitted bridge final-gain. Mismatched domain => not a valid wiring test.
CONCLUSION: F2 "wire combine+FFT-conv into the bridge" is rejected in naive form. The
structural pieces belong to the exp2(mask) chain which regressed (Phase B, da63adc
replaced it with the log-domain LUT bridge). The bridge (Pchip, 0.773) remains canonical.
Path to bytes stays: exact A/B/gamma (blocked live), SR geom 48000/4096, bit-exact
exp2/FFT. No code change made this session; NOTE-only.