Files
soothe2-re/handoff/NOTES_LEVEL.md
T

2505 lines
193 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.
## ============ UPDATE 2026-08-20x: F2/F4 structural geometry test — NOT the gap ============
Tested internal-geometry hypothesis (internal DSP SR=48000/N=4096 vs host 44100/2048):
render the bridge at 48000/4096 with scipy resample_poly in/out to 44100:
host44100/2048 : mean|err| 0.683 (subset)
internal48000/4096: mean|err| 1.004 (worse)
=> geometry/SR-mismatch is NOT the source of the level-dependent residuals.
ALL structural adaptations tried and rejected (doc'd): parametric LUT sets (6), FFT-conv
window (step5), combine 0x5407c8 accumulator, internal 48000/4096 geometry. The bridge
(Pchip, full-corpus mean 0.774) remains canonical; empirical ceiling.
Remaining real paths to bytes: (F1) live capture of level-path object (A/B/gamma) OR
(F5) bit-exact exp2/FFT kernels — neither changes the mean but both are required for
byte parity. No code change; NOTE-only.
## ============ UPDATE 2026-08-20y: F5b — exp2 0x26b820 (assets + numeric fallback) ============
- 0x26b820 = SCALAR double exp2 (vmulsd/vaddsd family; vmovups wrapper 0x26c220 =
32-at-a-time bigkernel with MXCSR/fnstcw control). Table-driven fdlibm-style,
Cody-Waite hi/lo: 8x16 irr tables + 0x181f31940.. series (-708.449.. step, companion
-1.684e-9) + kExp2_f2f4e0.. (log2e~1.4428 head) + vfmadd213sd poly chain.
- EXTRACTED bit-exact P3 assets: dsp/exp2_tables.{hpp,cpp} (8x16 doubles straight from
soothe_mem.bin file=RVA; lead-in series kExp2_big). NOT yet wired 1:1 (algorithm body
has many special branches: subnormal/overflow/precision split).
- exp2_dsp() (dsp/exp2.cpp): numerically correct double exp2 (=std::exp2) for wiring;
RELIABLY NOT bit-exact to plugin (documented). exp2_check: 2e6 dense grid + edges,
PASS vs std::exp2 (0 cells >1e-13) -> ensures fallback correctness only.
Bit-exact exp2 transcription stays P3 (same bucket as DSP-FFT butterfly wiring).
## ============ UPDATE 2026-08-20z: F5c — DSP-FFT 0x140a70 status (P3 confirmed) ============
Full dsp/ check run (twin/tables/fftconv/vlog/leveltrack/levelpath/exp2): ALL PASS.
FFT status: dsp/fft.cpp = numerical radix-2 (std::cos twiddle) used by the STFT
framework — produces correct transform, but NOT bit-exact to the plugin DSP-FFT
0x140a70. P3 bit-exact DSP-FFT plan: twiddle source (DAT_182616800 sin-table, sin(k*2pi/
1024), loader FUN_180039b00 stride 2^(10-m)) + split-radix 2/4/8 in-place butterflies
(FUN_18000bfc0/18000c5e0 with 0x8440 elementwise mul + FUN_1800437c0 acc) + plan-gen
giant (FUN_18002f980 recursion + per-log2 factor tables) remain unwired. This is the
multi-week gap (roadmap P3), required only for byte-parity of the FFT-conv path; the
bridge renderer does not depend on it.
## ============ UPDATE 2026-08-21: FIX — restore Pchip LUT (revert 12094f8) + comb diagnosis ============
### FIX (commit e5e9700): revert parametric LUT regression.
12094f8 replaced the empirical Pchip LUT (framed_model) with the parametric linear
form (CAP_A_LEVEL=-24/B=28/gamma=1). With gamma=1 that reduces to IDENTITY on
xv=log10(am/res), breaking the whole mask chain: t1kq fc-scan err jumped +7.9 dB.
Re-instated lut_pchip: t1kq mean|err| 0.226 (was 7.9). Confirms NOTES:1060 F1 closure
(no parametric set beats Pchip at fixed bridge params). lut_parametric helper + CAP_
consts remain in framed_model.hpp as live-capture reference (currently unused).
### 24-bit metric loader CHECK
render_parity.py:43 already uses correct 24->64-bit sign extension (x>=0x800000 =>
x-0x1000000), NOT the buggy OR-0xFF000000. Repo metric harness is correct; an earlier
symptom of ~50 dB phantom ref levels was an artifact of a scratch loader, not the repo.
### COMB DIAGNOSIS (biggest open gap, mean 10 dB / max 14.7)
comb.wav tones are at ~-25.6 dBFS (500/1000/1500/2000/3000). comb_ref reduce = +0.02 dB
= soothe2 lets ALL tones pass ~unity on this input. Our model over-cuts -7..-15 dB.
=> NOT a multiband-combine problem; it is LEVEL-dependent: at input ~-25.6 dBFS with
band sens 2.24 for two of four bands, real reducer output is ~neutral, ours still cuts.
Likely the bridge LUT/level_scale is calibrated on the al_* fit levels, not on this
low/wide band config. Same class as the known low-level over-cut (al lv18/24, NOTES:964).
Next: calibrate level_scale per input level / verify sens enters the level path correctly.
## ============ UPDATE 2026-08-21: COMB PARAMS VERIFIED — two SEPARATE phenomena ============
Decoded true comb wav params from comb_all.rpp (real 4-band config, NOT what tests assumed):
band1 fc=1000 q=1.0 sens=+12 mode 1
band2 fc=1778.7 q=4.5 sens=-12 mode 1 <-- NEGATIVE sens
band3 fc=195.1 q=0.494 sens=+2.24 mode 0
band4 fc=13408 q=1.0 sens=+12 mode 3
depth=0.864, mode=1, selectivity=10, sharpness=10, resolution=1
Earlier comb tests fed wrong params (band1=678.76, band2/3 sens=+2.24, band4 q3) — those
were NOT the reference config. Even with corrected fc/q/sens the model still over-cuts -7..-15 dB.
### Two SEPARATE problems (verified):
1. band1-ONLY (fc=1000 q1 s12, input comb.wav -25.6 dBFS): real soothe2 cuts tones ~5.9 dB
@1000 (comb_b1_1000 real ref, NOT neutral); our model cuts -8..-15 (out-real = -2.8..-7.7).
=> BASELINE depth is too big at low level (level-scale/LUT low-tail over-cut).
2. comb_ALL (=comb_ref≈comb_byp, reduce ~0 dB on all tones): real soothe2 returns to NEUTRAL
despite band1 active. => MULTIBAND interplay (combine/acc 0x5407c8) must cancel band1's cut,
consistent with M11 additive red=base+ΣW·H and band2 sens=-12 acting as a positive adder.
This is the combine gap (NOTES:1071 F2), and it needs negative-sens + per-band mode handling.
### Model gaps surfaced:
- negative sens (band2=-12) not modeled -> band2 treated as +suppressor instead of detractor.
- multiband min() (not additive combine) -> cannot reproduce the all-band cancellation to ~0.
- per-band mode 0/3 (peak vs shelf/tilt band types, M10) not dispatched.
No code change; investigation only. These require the structural combine/acc + per-band type
before 4-band comb can close.
## ============ UPDATE 2026-08-21: multi-band parser fix + combine/sens experiments ============
### infra FIX: framed_test comma form only parsed argv[3] -> multiband comb tests actually
ran a SINGLE band (first comma-arg ignored the rest). Now parses every comma-arg from
argv[3..]. With correct 4-band params and faithful min(), honest comb = mean 10.15 (over-cut
-7..-15 dB). Single-band corpus unchanged (t1kq 0.226, dual 0.726) => parser fix is safe.
### combine/sens experiments (reverted, doc only)
Tried closing comb via additive dB-sum (red=sum_i red_i) and signed sens-weight
S=sign(sens)*min(1,|sens|/12) applied to C. Neither reproduces comb_ref neutrality (~0 dB):
min() comb mean 10.15 (baseline)
additive comb mean 15.68 (worse: all bands cut, no compensation)
additive+S comb mean 15.68 (neg-sens band2 at fc=1778 doesn't counter band1 @1000)
Real soothe2 comb is neutral DESPITE band1 (fc=1000 sens+12) active. band2 sens=-12 at
fc=1778 has res~1.9 on the 500..3000 tones, so its naive compensation is tiny vs band1's
cut. => Naive additive/sens formulas CANNOT model the negative-sens band interaction; it
lives in the structural combine/acc 0x5407c8 (per-bin weights kRTAtt/kRTRel + negative
sens entering the level path with opposite sign), which requires the real exp2-domain
chain, not the bridge. comb stays OPEN (structural combine/acc, P4/P5).
## ============ UPDATE 2026-08-21: LUT moved to LEVEL domain (structural chain) ============
### Discovery: mask-domain LUT clamps on loud input
- Mask-domain LUT (committed 9b7e9d3) computes t=(mask_dB-A)/(B-A); for loud input
(tone1k 0dBFS) post-exp2 mask_dB < A=-13.78 => t clamps to 0 => val=0 => FULL cut.
Measured t1k err -24.04 dB. Root cause: exp2(-level) output dB range is unbounded
below, LUT domain [A,B] assumes bounded input.
- FIX: apply FUN_180563a60 power-law to LEVEL (am/res*scale) BEFORE IIR/exp2:
lvl = pow(t, gamma)*MULT, t=(20*log10(lvl)-A)/(B-A) clamp [0,1].
Level dB stays inside [A,B] for both quiet (t1kq: +6.2dB) and loud (t1k: +24dB).
Result: t1k err -24.04 -> +2.45 dB.
### TOOLING HAZARD: stale-binary sweeps
- cmake --build skips recompile when source mtime unchanged within same second;
param sweeps silently re-run the SAME binary (9-point sweep -> 1 unique result).
Earlier "gamma doesn't matter" observation was this artifact, NOT DSP behavior.
- Protocol now: os.utime(src) before build + assert binary mtime fresh (sweep_fresh.py).
### Fresh 2D sweep (g x mult, verified builds), 3-case smoke (render48k 48k/4096):
g=0.344 m=3.8 optimum: t1kq=-0.788 t1k=+2.453 al12=+0.037 mean|err|=1.093
(committed version was mean 8.2). gamma>=0.9 ALL worse (+2..+5): spatial IIR
after LUT redistributes peaks; naive ratio math (need g=1.17 from raw t ratio
1.744->2.0) does NOT survive the IIR chain. Empirics rule.
- MULT=3.8 is a PLACEHOLDER (maps t^gamma back to level domain; no decomp basis yet).
A/B/gamma are decomp-extracted (BandConfig ctx+0x188 refs, NOTES:967).
### Scope clarification
- scripts/corpus.py exercises the BRIDGE path only (44.1k non-internal grid):
full corpus 1.594 mean / comb 10.15 — UNCHANGED by structural work (verified
stash-compare). comb regression is pre-existing bridge state, separate issue.
- Structural chain validated via render48k smoke (3 cases) + module checks
(fn529fe0/exp2/twin/tables/leveltrack/levelpath/fftconv all PASS).
- render48k tail artifact: last BLK=65536 block zero-pads => am decays in final
frames (~last 0.06s of y48). Cosmetic; metric window barely affected.
### Commit: 535b150 (LUT level-domain restructure).
NEXT: step 3 wire combine_acc track into mask output (currently computed,
discarded); step 4 FFT-conv 0x535a70 mask smoothing; then decode BandConfig
ctx+0x188 writers to replace MULT placeholder with decomp value.
## ============ UPDATE 2026-08-21b: IIR3 bidirectional + combine semantics from raw decomp ============
### IIR3 is BIDIRECTIONAL (corrects NOTES:830 "inline TWICE")
- consumers_out.txt:955-1075: TWO [reset state 0x440510=0, forward pass, backward
pass] pairs. State PERSISTS from forward into backward within a pair; reset
between pairs. Operand order verified: y[i] = x[i]*B3[i] + state*A3[i]
(A3@0x3c0510 multiplies STATE, B3@0x340510 multiplies x). Backward iterates
nbin-2 .. 1 with descending coefficient index.
- Wired into structural chain (commit 1a616e1); LUT_MULT retuned 4.4->4.2;
smoke mean|err| 1.021 (vs 0.993 fwd-only — parity within noise, kept faithful).
### Combine block EXACT semantics (thunk-level, corrects both 20h and 20j notes)
- 8d60 sub: args (acc=0x5407c8, mask=0x540678, dst=f6f8=0x5406f8), dst=3rd arg:
f6f8[i] = mask[i] - acc[i] (acc NOT overwritten by sub; 20j was wrong)
- mirror f6f8 (11940 x2)
- 3c40 fma: f6f8_upper += kRTAtt*acc_upper ; f6f8_lower += kRTRel*acc_lower
(dst = F6F8 halves, b = acc — 20h had this right)
- 5a20: acc += mask (persistent per-band accumulator)
### CRITICAL SCOPE FINDING for Step 2 (combine wiring)
- In the ONLINE single-band path (0x5408b8==0) the updated f6f8/acc have NO
consumer inside FUN_180529fe0 before warp/IIR3/dry-wet: warp applies to
band buffer directly, blend overwrites f6f8 next band/frame. Verified by
exhaustive grep of consumers_out.txt (last 0x5406f8 use = offline branch).
- => combine CANNOT close t1k/al single-band tails by itself; its effect must
enter via (a) multiband combiner 0x5316e0 reading acc/f6f8 across bands, or
(b) FFT-conv stage. Step 2 re-scoped: decode 0x5316e0 acc consumption FIRST,
only then wire. Do NOT invent a track->mask feedback (would be empirical).
- Online blend confirmed: f6f8 = freqaxis*(1-mix) + mix*fVar25(0.8) via
6e40+15060 thunks, then bigkernel exp2 in-place on band (line 805-832).
### Commits: 9ffe60e (IIR3 fwd-only + retune), 1a616e1 (bidirectional + decode).
## ============ UPDATE 2026-08-21c: FULL STRUCTURAL CORPUS (first honest numbers) ============
### Harness: scripts/corpus_structural.py (render48k x all 62 cases, bridge-compare mode)
Structural chain (48k/4096, LUT level-domain g=0.344 m=4.2, IIR3 bidi) vs bridge:
group n bridge struct delta
t1kq 7 0.226 0.768 +0.54 worse
t1k 11 1.801 2.114 +0.31 worse
al 6 0.638 0.986 +0.35 worse
res 11 0.628 0.437 -0.19 BETTER
dual 22 0.726 3.264 +2.54 MUCH worse
comb 5 10.149 6.116 -4.03 MUCH BETTER
TOTAL 62 1.594 2.286 +0.69 worse
### KEY FINDINGS:
1. **comb: structural ALREADY beats bridge by 4 dB** (6.1 vs 10.1) WITHOUT acc
wiring — per-band structural min() + warp closes part of the multiband gap.
Plan's "combine closes comb" partially pre-empted by the chain itself.
2. **dual_*_500: constant -6.7 dB over-cut for ALL q** (q-independent!).
res[center] is q-independent (0.117) => systematic LEVEL offset at band
center on two-tone input, not shape. dual_*_2000 (off-center bin) BETTER
than bridge (-0.1..-0.4 vs +0.1..+2.3). At same xv, bridge-LUT cut ~-9.3 dB,
our exp2 path cuts ~-20 dB: real reduction SATURATES (~C_max~0.70 per
extracted LUT pts), exp2(-lvl) does not.
3. al level-sweep error is MONOTONE in level (+0.88@lv3 -> -1.82@lv24): our
level->cut curve too steep mid-range; but t1k (loudest) flips back to
under-cut (+1.8) => NON-monotonic overall, no monotone LUT fixes all groups.
4. gamma/MULT sweep (g in {0.15,0.22,0.30} x m in {4.2,5.0,5.8}, fresh builds,
12-case subset incl. dual/res/comb): ALL points worse than current
g=0.344/m=4.2; gradient points BACK to current optimum. Blind param space
EXHAUSTED around this parametrization.
### CONCLUSION (Step 1 status): criterion "structural >= bridge on single-band"
NOT met globally (regress on t1kq/t1k/al/dual; better on res). The missing
piece is the REAL reduction curve saturation — needs either:
(a) Step 7 BandConfig ctx+0x188 live capture (A/B/gamma per config), or
(b) re-examination of bigkernel INPUT: maybe level into exp2 carries an
additional transform (log-domain offset / per-band depth scalar
0x540888 / offline 0x5e80+FUN_180323f20 ops) that saturates the cut.
Do NOT tune MULT further — it is calibrated; the CURVE SHAPE is wrong.
### FFT-conv priority DOWNGRADED: NOTES:18c shows window 0x540658 tail is
near-flat (0.8->1.0 plateau at N/2>=2048), FIR-construction effect on mask
shape minimal for N=4096. Step 4 stays P3.
## ============ UPDATE 2026-08-21d: REDUCTION-LAW DISCOVERY + Phase A closure ============
### Discovery: effective reduction is AFFINE in dB(level)
RT_DUMP_BIN tracts (frame 100) across al sweep fit:
x_exp2 = -0.6646 - 0.05877 * dB(lvl_raw) (mask ~ 0.8 * lvl^(-0.354))
gamma_eff 0.354 ~= decomp BandConfig gamma 0.344 (NOTES:967) -> power law,
NOT the double-exp t^gamma->exp2 form (21c). Tone-levels verified +-0.13 dB.
### Pre-IIR application calibrated through chain model
Python simulator (tables from rt_mask_tables.cpp; IIR1 fwd, IIR2 fwd, exp2*0.8,
W, IIR3 bidi x2): pre-IIR affine peak dilutes ~2.3x by spatial IIRs ->
X0=1.8, SLOPE=0.11 (max|x-resid| 0.09 ~= +-0.5 dB on tones).
FULL CORPUS fresh-build A/B (stale-binary protocol, both reproduce exactly):
group old(HEAD LUT) new(affine pre-IIR)
t1kq 0.768 0.460 v
t1k 2.114 1.196 v
al 0.986 0.132 v (all 6 levels |e|<=0.25)
res 0.437 2.046 x
dual 3.264 4.317 x
comb 6.116 5.718 v
TOTAL 2.286 2.633
### Phase A experiments (ALL on fresh builds / validated models)
1. res regression is REAL (not stale binary; reproduced both directions).
2. High-bin/IIR3-backward propagation hypothesis REFUTED: at frame-100 the
two laws give IDENTICAL masks on res_500 (<1e-4 everywhere); divergence
lives in other frames' level zone (-13..+15 dB) where old cuts more.
3. Direct-affine (bypass IIR1/2, raw constants) REFUTED: TOTAL 2.777.
Spatial IIRs are integral to the effective curve.
4. Offline trajectory model (RT_DUMP_ALL per-frame lvl_raw; chain sim;
metric-window frames [55,90)/[243,278) @48k/1024-hop; zero-pad frames
excluded — they are beyond the trimmed output!) VALIDATED: known-law
predictions within 0.09..0.19 dB on res_500/al_24/t1k_1000 (al_12 0.53).
5. Grid search over affine family (X0, SLOPE, floor F>=0, ceiling CMAX),
objective = sum pred_err^2 over anchored cases:
OPTIMUM X0=2.00 S=0.0825 F=0 CMAX=inf -> rms 0.75 dB with
res_500 +0.84 / al12 +0.29 / al24 -1.21 / t1k -0.02.
Ceiling never active in-grid; positive floors do NOT help (top all FL=0).
### KEY NEGATIVE RESULT
NO law of the scalar affine family fits tone AND noise content
simultaneously (res tension +0.4..+1.1 unavoidable). Old Pchip "wins" on
res/dual/comb via error cancellation, not shape correctness; affine wins
tones decisively (al +-0.13 dB linearity over 42 dB!). The missing piece is
CONTENT-DEPENDENT behavior: detector dynamics (att/rel interplay with
fluctuating spectra), or combine/acc consumer (multiband feedback), or
saturation mechanism. => Gate "no group regression" FAILS for every family
member -> committed canon stays HEAD (LUT g=0.344/m=4.2).
### Tooling kept (opt-in env, zero cost when unset)
- RT_DUMP_BIN=<path> [RT_DUMP_FRAME=n]: single-frame tract dump (7 cols).
- RT_DUMP_ALL=<path>: append per-frame binary lvl_raw spectra
[i32 frame][i32 nbin][f32 x nbin]. Use ONE env per run (BIN early-return
eats ALL frames if combined!). rm target first (append mode).
Captures live under /tmp/opencode/traj_*.bin (law-independent detector).
- Offline engine: /tmp/opencode/phaseA_grid_fast.py (combo-vectorized chain
sim + grid search); estimator zoo: phaseA_estimator_zoo.py.
### NEXT (Phase B candidates, priority order)
1. Detector dynamics: att/rel on fluctuating spectra vs ref renders of
noise-like material (res group) — per-frame gain extraction protocol
ready (STFT ratio out/in at metric bin, see phaseA session notes).
2. Step 7 live capture ctx+0x188 (constants + possible content-dependent
branch), NOTES_CAPTURE.md method.
3. combine consumer hunt (multiband cascade) for dual/comb.
## ============ UPDATE 2026-08-22a: PHASE B RECOVERY — offline detector hypotheses REFUTED ============
Контекст: Phase B сессия 2026-08-21 (вечер) + утро 08-22 осталась незакоммиченной —
скрипты в /tmp/opencode, результаты не сохранялись. Все 4 эксперимента перепрогнаны,
stdout зафиксирован (`phaseB_*.out`), скрипты перенесены в `scripts/phaseB_*.py`.
Движок: phaseA_grid_fast.py (валидированный офлайн-тректор, погрешность 0.09..0.19 dB
на анкорах). Анкоры res_500/al_12/al_24/t1k_1000, критерий pred_err ~ 0 на ВСЕХ.
| # | Гипотеза | Скрипт | Результат | Статус |
|---|----------|--------|-----------|--------|
| 1b | slide_max pooling lvl (w=3..33) | phaseB_pool2.py | w=3: tones 2.7/2.3/4.9, rms 3.02 (base 1.04) | **REFUTED** |
| 1c | neighborhood mean/RMS pooling | phaseB_pool3.py | best mean w=3 rms 1.35; res +1.9..+5.1 vs t1k 1.9..4.4 tradeoff, ни одна точка не закрывает все 4 | **REFUTED** |
| 2 | temporal dynamics (hold b/dbdecay r/ema a на полной траектории) | phaseB_temporal.py | ВСЕ варианты = baseline (rms 1.031.05): метрика в steady-state, состояние успевает устояться до окна | **REFUTED (no-op)** |
| 3 | ρ(IIR1)+Δ joint scan (ρ∈[0.30,0.95], Δ∈[1.5,+1.5]) | phaseB_rho.py | best ρ=0.830 D=+1.40 → rms 0.654; НО al_24 стабильно −1.19..−1.25, ρ без источника в декомпе (канон 0.692) | **REJECTED** (нарушает golden rule #1, тот же scalar-family тупик) |
Выводы:
1. Пространственный pooling ЛЮБОГО вида (max/mean/rms) не объясняет content-gap.
2. Temporal-класс гипотез НЕПРОВЕРЯЕМ на steady-state анкорах — нужен переходный
контент (burst-рефы уже есть в soothe-bt) или другой анкорный набор.
3. Лучший (ρ,D) = репараметризация affine-семейства Phase A → упирается в тот же
KEY NEGATIVE RESULT (scalar-семейство не закрывает тон+шум одновременно).
Канон НЕ сменён: HEAD (LUT γ=0.344/MULT=4.2, ρ=0.692).
NEXT (приоритеты без изменений):
1. Step 7 live capture ctx+0x188 (NOTES_CAPTURE.md метод) — константы A/B/γ +
возможная контент-зависимая ветка. Требует REAPER+плагин.
2. combine consumer hunt (межполосный каскад) для dual/comb.
3. Опционально: transient-анкоры для проверяемости temporal-класса.
## ============ UPDATE 2026-08-22b: STEP 7 EXECUTED → PREMISE REFUTED (BandConfig = GUI-only) ============
Инфраструктура захвата готова и работает:
- `scripts/step7_capture.py <rpp>` — spawn reaper (+play.lua realtime) → chunked snapshot
хоста yabridge → fingerprint level_gain-пар (level[j]==j/1024 exact) → u64-ref голосование
базы level-path объекта (+0xe0+band*0x18) → decode всех BandConfig вокруг базы.
- `play.lua` ИСПРАВЛЕН: reaper.Sleep НЕ существует в API (падал скрипт, транспорт
продолжал играть без keep-alive) → цикл reaper.defer.
- Грабли: /proc/<pid> vs /proc/<pid>/mem (pread каталога = тихий EIO на всё); layout
объекта ПЛАВАЕТ между сессиями → оффсеты BandConfig не хардкодить, только fingerprint.
Живые константы (7 захватов: render_long, t1kq_base, t1k_b1f_1000, al_12, al_24,
dual_b1q_0.5, comb_base — ВСЕ идентичны):
- cfg@obj+0x168: **A=24.0 B=+28.0 γ=1.0 flag=0 cb=SET** («кривая редукции»)
- cfg@obj+0x170: A=16.0 B=20000.0 γ=1.0 cb=SET; shaper f32 @+0x20..: 0.55, 7.130898,
2.772589(=ln16), 2.718282(=e)
⇒ противоречие gap #3 разрешено: −24/28/1 подтверждено живьём; 13.78/68.29/0.344
был ФИТОМ к рендер-референсам, не свойством плагина.
FUN_180563a60 расшифрован ДО КОНЦА (f_563a60.dis, 165 строк):
- вход: mask-doubles obj+0x4198+band*0x2000 (пишет FUN_18056e3e0 из band-list
+0x178→[+0x78/+0x84]); x-axis пар = j*(1/1024) (конста 0x24c3c50);
dB = logf(mask)*8.685889 (=20/ln10, 0x24c43e0);
- выход: gain[j] = clamp01(cb(A,B,dB)) где cb = std::function @cfg+0x90 — В ЖИВЫХ
ЗАХВАТАХ cb ВСЕГДА SET ⇒ статическая ветка A/B/γ НЕ ВЫПОЛНЯЕТСЯ НИКОГДА;
- статическая ветка (cb==NULL, для протокола): t=clamp01((dBA)/(BA));
γ==1 → t; flag==0 → t^γ; flag!=0 → 0.5·(1+sign(2t1)·|2t1|^γ).
Callgraph: весь кластер 5631c0/563260 → 563440 → {56e3e0, 563a60} достижим ТОЛЬКО по
DATA-xrefs (vtables 1826a03cc..420, 1824b14f0..510) — GUI-timer heartbeat. АУДИО-метод
FUN_180529fe0 (Soothe2Module<float>::vtbl) НЕ ссылается на {+0x168,+0x170,+0x178,
+0xe0,+0x198,+0x4198}; его callees — только memcpy-туннели (181ba94b0 ×13, 52dbc0, 52d920).
Файл /tmp/consumers_out.txt восстановлен из decomp_funs.txt (215 строк, вне git!).
### ВЫВОДЫ (меняют приоритеты)
1. **Шаг 7 BITEXACT_PLAN в исходной постановке ОПРОВЕРГНУТ**: BandConfig A/B/γ питает
только GUI-отрисовку кривой, аудио-путь его не читает. Live-захват A/B/γ не может
закрыть насыщение кривой редукции аудио-цепи.
2. LUT-блок структурной цепи (t^γ·MULT с 13.78/68.29/0.344/MULT=4.2) = EMPIRICAL фит
БЕЗ декомп-источника в аудио-пути (формула списана с GUI-функции!). Помечен EMPIRICAL;
канон не меняем (корпус держит), но источник правды теперь внутри аудио-модуля.
3. Новый приоритет №1: полный разбор аудио-тела FUN_180529fe0 (215 строк декомпа,
/tmp/consumers_out.txt) + combine-consumer hunt — насыщение C_max≈0.70 сидит там.
## ============ UPDATE 2026-08-22c: PARAM BRIDGE + RENDER-BENCH CALIBRATION ============
Инфраструктура управления плагином через ОФИЦИАЛЬНЫЙ параметр-мост REAPER:
- `dump_params.lua` — дамп всех FX-параметров (имя, raw 0..1, formatted UI) в /tmp/opencode/fxparams.txt.
- `setparam.lua` — установка по имени через TrackFX_SetParam + SaveProjectEx (env S2_SET="name=val;...", S2_OUT).
- `scripts/rpp_setparam.py` — редактор XML-стейта в RPP с корректным апдейтом length-полей
(A=buflen16, B=len(xml), хвост JUCEPrivateData сохраняется).
### КЛЮЧЕВОЕ ОТКРЫТИЕ: XML `<PARAM>` секция НЕ источник VST3-стейта
Живой дамп: depth raw=0.524 ↔ fmt=0.9; в XML при этом "0.8639736175537109" (=UI-единицы!).
Плагин восстанавливает параметры из БИНАРНОЙ части чанка; XML-список — декоративная
копия для GUI-восстановления. Правки только XML → рассинхрон → плагин откатывается в
дефолт (объясняет «магию» идентичных рендеров −31.76 у любых depth-правок; контроль
same-value rewrite давал байт-в-байт реф ✓). ВНИМАНИЕ к rpp_setparam.py: менять можно,
но аудио это НЕ меняет.
### Семантика параметров (через мост)
- `depth`: raw 0..1 → UI **±18 линейно** (ui=36·raw−18). Рендеры подтверждают монотонность:
raw=0 → юнити; raw=1.0 (+18) → 37.6 dB @tone.
- `input trim`: raw → **±24 dB**.
- band sens: fmt в dB; stereo mode mid|side; balance 100%|57%.
### WAV-грабли (живое подтверждение класса багов 71644ff)
REAPER пишет bext(602B)+junk(28B) ПЕРЕД data; наивный парсер (readframes+reshape без
каноничного loader) даёт фантомный широкополосный шум и фальшивый клиппинг
(corr(L,R)≈0 артефакты). Каноничный render_parity.load корректен; санити bypass:
corr(L,R)=1.0000, rms=out=in — РЕНДЕР-ПАЙПЛАЙН ЧИСТЫЙ.
### Измеренные кривые редукции (t1kq-пресет, тон@1000, Goertzel 0.75s)
- Depth-свип: монотонный, raw .5→−5.5 dB; .75→−21 dB; 1.0→−37.6 dB @tone.
- Input-trim свип (холодный тон −18 dBFS): R_eff растёт 1.4→15.7 dB на всём ±24 —
ПОТОЛКА НЕТ в этом диапазоне (против гипотезы C_max≈0.70 как свойства плагина).
- Горячий тон (+6 dBFS): out@1000 ЗАМИРАЕТ на −26.74 dB при trim ≥+12 (R_eff cap ≈20.7),
плато воспроизведено дважды.
- НО: при одинаковом относительном драйве холодный/горячий дают РАЗНЫЙ gain
(15.7 vs 20.7 @L≈+6) ⇒ отклик детектора не чистая f(level) — контент/нормализация
(согласуется с KEY NEGATIVE RESULT Phase B).
### Следствия для приоритета №1
1. «Насыщение C_max≈0.70» старой модели — артефакт placeholder MULT=4.2/Pchip cap 0.667,
а не свойство плагина в рабочем диапазоне.
2. Реальный потолок проявляется как FLOOR выходного уровня (−26.74 dB @hot) — искать
механизм floor/clamp в level-path декомпе с конкретной целью.
3. Инструмент готов для систематического картирования R(L, sens, depth): скрипты +
мост позволяют прогонять десятки конфигов без ручного GUI.
## ============ UPDATE 2026-08-22d: SATURATION KNEE MAPPED — HARD FLOOR 20.72 dB ============
Hot-тон (+6 dBFS, band1 fc679 q1 sens+12 depth+0.9), gain=outdry по Goertzel@1000:
| trim | L=in+trim | gain |
|------|-----------|------|
| 0 | 6.02 | 3.39 |
| +6 | 0.02 | **20.72** |
| +9..+24 | +3..+18 | **20.72** (pinned, ≥24 dB драйва) |
1. ЖЁСТКИЙ ПОТОЛОК: gain замирает на 20.72 dB (ratio 0.0921) — не плавное плато,
а колено между L=−6..0 с последующим pin. Форма ⇒ t-clamp модели (t→1 при
dB_lvl≥B=28 → lvl=MULT → фикс. маска) КАЧЕСТВЕННО подтверждена живьём.
2. Колено: между trim 0 и +6. При γ=1/A=24/B=28 это задаёт офсет
физический уровень → внутренний lvl-dB ≈ 31±3 dB.
3. ЗАГАДКА для следующей сессии: холодный тон при том же относительном драйве
(L=+5.94 через trim+24) даёт gain только −15.74 (НЕ насыщен), горячий при L=+5.98 —
20.72 (насыщен). Trim ≠ простой pre-gain детектора ИЛИ отклик зависит от
абсолютного контента. Тест-план: (a) спектральный профиль нотча сатурированного
рендера vs форма маски модели; (b) файловый gain вместо trim; (c) алгебра пола:
0.0921 = blend·exp2(MULT)·W·kBand768·kWarp·IIR3·convwin @bin85 — все live-таблицы
есть в dsp/, решается относительно MULT офлайн.
4. RT_DUMP_ALL НЕ работает в оффлайн-рендере плагина (это инструментация только
нашего C++; у настоящего бинар её нет). Live-capture траекторий = только память.
5. +0x4198 маски уровня объекта = GUI-кривые (band1 растёт 1.0→3.98 к ВЧ) — ещё раз
не аудио-маски; для floor-hunt использовать алгебру (3c), не этот объект.
## ============ UPDATE 2026-08-22e: FLOOR ALGEBRA SOLVED — blend·ln10/20 EXACT ============
Модельный grid (render48k, live A=24/B=28/γ=1 через RT_LUT_* env, файловый gain):
| MULT | B | base(6dB) | p3(3.1dB) |
|------|---|-----------|-----------|
| 4.2 | 28 | 8.78 | 9.70 |
| 9 | 28 | 16.35 | 18.18 |
| 12 | 28 | 20.49 | 22.77 |
ВЫВОД: t-clamp в транскрибированной цепи НЕ даёт pin (base/p3 расходятся на 2+ dB
при любом MULT/B) ⇒ механизм пола живёт НЕ в LUT-блоке.
### РЕШЕНИЕ АЛГЕБРАИЧЕСКИ
Измеренный pin −20.72 dB совпадает с точностью 0.0055 dB:
gain_floor = 20·log10(blend · ln10/20) = 20·log10(0.8 · 0.115129) = 20.7145
т.е. маска пола = blend × (ln10/20). Константа ln10/20 = 0.1151292546 @0x1824c3cd4
ТА ЖЕ семья что и коэфф. attack/release expf(p·0.11513) (NOTES_CAPTURE 19b).
Audio-референсы константы: **FUN_180529c60**, 18052baa0, 18052bad0, 18052bba0.
### Ведущий след (f_529c60.dis:94-101)
xmm0 = *(this+0x54087c)
xmm0 *= 30.0 (@0x1824c4478)
xmm0 -= 90.0 (@0x1824c44d0)
xmm0 *= 0.115129 (@0x1824c3cd4)
call expf (181a14cac)
xmm0 *= результат FUN_1804d56b0(band buffer 0x540678[band], r13d=n)
⇒ параметрический dB→linear фактор (30p90 ∈ [−90,−60] dB) умножается на вычисленное
над полосой значение и идёт в band-буферы. Точная роль (floor/dither/scale) TBD.
### NEXT
1. Декодировать FUN_1804d56b0 + контекст использования фактора в 180529c60.
2. Проверка предсказания: floor_gain(mix) должен сдвигаться как 20log10(blend/0.8)
(рендер mix=50% через мост).
3. Порт найденного клампа в структурную цепь → корпус с гейтом vs bridge.
## ============ UPDATE 2026-08-22f: FLOOR DISCRIMINATION — 3 гипотезы, детекторный кап ============
Дискриминационные рендеры (hot-тон, trim+12, через параметр-мост):
| конфиг | gain изм. | mask=10^(g/20) | интерпретация |
|--------|-----------|----------------|----------------|
| mix100 sens12 | 20.72 | **0.8·ln10/20** | базовый пол |
| mix50 sens12 | 5.20 | eff 0.552=out/dry | **dry/wet кроссфейд**: 0.5+0.5·0.0921=0.546→−5.26 ✓ |
| mix100 sens6 | 18.78 | **1.0·ln10/20** | пол сдвинулся ровно на ×ln10/20 |
ОПРОВЕРГНУТЫ: (а) явный кламп mask≥const (mix50 должен был остаться −20.72);
(б) blend-floor 20log10(blend′·ln10/20) для mix50 (предсказывал 26.74);
(в) naive detector-sat lvl∝sens (предсказывал 11.33 для sens6).
ЧТО СОШЛОСЬ:
1. Mix = классический кроссфейд out=(1m)·dry+m·processed; пол в PROCESSED пути.
2. Полы = ТОЧНЫЕ кратные ln10/20: {0.8× @sens12; 1.0× @sens6}.
3. Движение пина с sens: Δx_exp2 = log2(mask/0.8): 3.1188→−2.7996, Δ=0.3192
на 6 dB sens ⇒ наклон −0.0532 dB⁻¹ ≈ аффинный закон Phase A (0.05877!)
⇒ похоже, что НАСЫЩАЕТСЯ ДЕТЕКТОР (кап его dB_lvl ≈ 36..42 в завис. от sens),
а редукция следует аффинному закону до этого капа. Точные значения ln10/20
при этом — след пересечения закона с сеткой экспонент, не отдельный кламп.
4. Наша модель на том же материале достигает lvl_max=3.012 ≈ log2(ln10/20)=3.1188
(Δ3.5%) — почти касание первого пола естественно (RT_DUMP_ALL render48k).
### NEXT (Шаг 9 продолжение)
1. Локализовать кап детектора: где dB_lvl перестаёт расти (vlog/leveltrack путь,
FUN_18052d650 IIR1 / leveltrack таблицы) — искать ограничитель амплитуды FFT/am.
2. Точная формула капа от sens: снять полы на сетке sens {3,6,9,12} × trim{18,24}.
3. Порт: вместо t-clamp LUT — детекторный кап + аффинный закон Phase A; корпус с гейтом.
## ============ UPDATE 2026-08-22g: NO-LUT TEST + CLIPPING THEORY REFINED ============
### NO-LUT эксперимент (RT_LUT_OFF=1, render48k)
Тракт@bm85 (hot-тон, RT_DUMP_BIN): lvl_raw=3.0117 → post-IIR1/2 band_level=1.0646
(IIR1 A[85]≈0.65 сильно сглаживает изолированный пик) → prewarp mask=0.3825·W=0.408
→ итоговый gain модели −7.67 dB (против real 20.72). Убрали LUT — модель всё равно
далека: разрыв теперь ЛОКАЛИЗОВАН в детекторном фронте (am/res/scale и/или IIR-сглаживание),
а не в форме кривой. Модельный lvl_max=3.012 почти касался бы пола 3.1188=log2(20/ln10),
если бы не разводился IIR1 по частоте.
### Клиппинг-теория: частично опровергнута, но указывает на clamp sidechain
- REAPER рендерит в float: trim-овердрайв кормит плагин честными float >1.0, обрезки на
itemе
## ============ UPDATE 2026-08-22g: NO-LUT TEST + CLIPPING THEORY REFINED ============
### NO-LUT эксперимент (RT_LUT_OFF=1, render48k)
Тракт@bm85 (hot-тон, RT_DUMP_BIN): lvl_raw=3.0117 → post-IIR1/2 band_level=1.0646
(IIR1 A[85]≈0.65 сильно сглаживает изолированный пик) → prewarp mask=0.3825·W=0.408
→ итоговый gain модели −7.67 dB (против real 20.72). Убрали LUT — модель всё равно
далека: разрыв теперь ЛОКАЛИЗОВАН в детекторном фронте (am/res/scale и/или IIR-сглаживание),
а не в форме кривой. Модельный lvl_max=3.012 почти касался бы пола 3.1188=log2(20/ln10),
если бы не разводился IIR1 по частоте.
### Клиппинг-теория: частично опровергнута, но указывает на clamp sidechain
- REAPER рендерит в float: trim-овердрайв кормит плагин честными float больше 1.0,
обрезки на item'е НЕТ.
- Pre-clipped файл (x12dB с клиппингом на диске) @trim0 -> -12.31 dB != -26.74: сравнение
НЕКОРРЕКТНО (искажён DRY-путь файла), но показало, что «клиппинг на входе» сам по себе
не воспроизводит пин.
### ОБЪЕДИНЁННАЯ ГИПОТЕЗА (лид): clamp копии sidechain на +-1.0 внутри плагина
Факты:
1. Колено hot-тона ровно при переходе эффективного уровня через 0 dBFS (trim 0->+6).
2. h_06 == h_12 == h_24 по метрике — после достижения 0 dBFS драйв не растёт.
3. Полы = точные кратные ln10/20 ({0.8x, 1.0x} для sens 12/6).
4. mix = сухой кроссфейд (-5.26 предсказано / -5.20 измерено).
Открытое: cold trim+24 (+6 eff) дал -15.74 вместо -20.72 => кламп либо мягкий (tanh-like,
разная глубина насыщения), либо детектор реагирует на RMS/форму, а не только пик.
### NEXT
1. Прямая верификация: live-capture sidechain-буфера плагина во время сатурированного
рендера (step7_capture-style скан памяти, искать плоскую вершину +-1.0).
2. Снять форму клампа: сетка hot-файлов {-9,-6,-3,0 dBFS} x trim {6..24} -> кривая
насыщения mask(eff_level).
3. Порт: sidechain-clamp в render48k (min(max(in,-1),1) на копии для детектора) +
аффинный закон Phase A без LUT -> корпус с гейтом.
## ============ UPDATE 2026-08-22h: FLOOR LAW MAPPED — sens и depth ============
Пол редукции как функция параметров (hot-тон 6 dBFS, trim+12, mix100):
| sens | floor gain | mask | lvl_cap=log2(0.8/mask) |
|------|-----------|--------|---------|
| 3 | 17.74 | 0.12973 | 2.6245 |
| 6 | 18.78 | 0.11502 | **=ln10/20 точно** |
| 9 | 19.78 | 0.10256 | 2.9635 |
| 12 | 20.72 | 0.09208 | 3.1191 |
### Эмпирический закон
floor_dB(sens) ≈ 20·log10(ln10/20) (sens 6)/3 (якорь sens6; ±0.06 dB)
Depth тоже сдвигает пол: depth UI 0 → **19.10 dB** против base (+0.9) → 20.72
(Δ≈1.62 dB на 0.9 ед. depth ⇒ ~1.8 дБ/ед., НЕ как sens/3).
lvl_cap(sens) квазилинеен: наклон ≈0.055/oct-per-dB ≈ аффинный наклон Phase A (0.0588).
### Статус механизма
Пол — гладкая поверхность F(sens, depth); сайт в декомпе не найден (юзеры ln10/20 —
сеттеры att/rel и dither-фактор). Гипотеза детекторного капа остаётся лидом:
lvl_raw детектора перестаёт расти при эффективном уровне ≈0 dBFS (колено), а кривая
редукции следует закону до этого капа. Точная форма F(...) — на картирование.
### NEXT сессии
1. Декод FUN_18052d650 (IIR1) + leveltrack-путь на предмет ограничителя am.
2. Картирование F(sens,depth) сеткой 4×4 через мост (~30 мин рендеров) → фит.
3. Порт эмпирического пола в структурную цепь (env-параметризованный), корпус с гейтом.
## ============ UPDATE 2026-08-22i: RT_LVL_CAP ИНЕРТЕН — разрыв в детекторном фронте ============
Порт капа пост-IIR уровня (RT_LVL_CAP env в process_band_structural, после IIR2):
корпус с cap=3.119 и 2.798 ДАЛ ТОЧНО БАЗОВЫЕ ЧИСЛА (TOTAL 2.286) — кап не связывается,
т.к. пост-IIR уровни модели максимум ~1.06 (lvl_raw 3.01 разводится IIR1 A[85]≈0.65).
⇒ Модели не ХВАТАЕТ уровня, а не лишний: реальный плагин доводит пост-IIR lvl до 3.12
(пол), наш детекторный фронт (am/res/scale нормировка + IIR-сглаживание изолированных
пиков) теряет ~x2.8. ПРИОРИТЕТ СДВИНУТ: чинить детекторный фронт (амплитудная
нормировка am, форма res_k, сила IIR1/2 вдоль частоты), а не форму кривой.
RT_LVL_CAP оставлен в коде как инструмент (opt-in, инертен при текущих уровнях).
## ============ UPDATE 2026-08-22j: WHITE-NOISE PROBE — архитектура расшифрована ============
Белошумный пробник через сатурированный конфиг (trim0 на шуме 6 dBFS RMS):
REAL (плагин): передача ПЛОСКАЯ −0.01…−1.65 dB на ВСЕХ частотах;
лёгкая вмятина −1.04 @678 (центр полосы), HF rolloff 1.5 @8k.
МОДЕЛЬ (canon): фейковый низкочастотный «пол» −24 дБ @50Гц от W=kBand768·kWarp·res^rp.
МОДЕЛЬ (NOWARP): форма совпала — плоско ~−2.1…−2.7, вмятина 4.45 @678.
### ВЫВОДЫ АРХИТЕКТУРЫ
1. W=warp НЕ является бланкетным множителем выхода: реальный плагин пропускает
широкополосный контент почти на unity ⇒ RT_NOWARP правилен по духу.
2. Реальный плагин режет СТАЦИОНАРНЫЙ ТОН на −20.72, но НЕ режет шум ⇒ редукция
управляется КОНТРАСТОМ бина к соседям/шумовому полу (смысл IIR1/2 вдоль частоты =
бегущий референс), а не абсолютным уровнем.
3. Без blend в выходе уровни ШУМА сходятся: real lvl≈+0.123 vs наш +0.098 (Δ0.02 окт);
уровни ТОНА: real 3.444 vs наш 3.012 (Δ0.43 окт = ×1.35 амплитуда) ⇒ расхождение
растёт с уровнем ⇒ подозрение на res_k (twin АДАПТИРУЕТСЯ к сигналу во времени,
res_k уменьшается при длительном тоне) либо am-нормировку при высоких уровнях.
4. blend(0.8) в нашей модели даёт фантомный −1.94 dB бланкет — в реальном выходе его нет;
exp2(lvl) без blend точнее описывает шумовой случай.
### Рабочая гипотеза цепочки (v22j)
lvl_raw[k,t] = am[k,t]/res_k(t)·scale (res_k(t) — адаптивный twin!)
lvl_ref[k,t] = IIR1/IIR2 вдоль ЧАСТОТЫ от lvl_raw (бегущий референс соседей)
редукция = f(lvl_raw lvl_ref или lvl_raw/lvl_ref) ← контраст
mask = exp2(−редукция) (без blend-бланкета)
sidechain-in clamp ±1.0 (колено/пин при 0 dBFS)
### NEXT
1. Декод FUN_18052d650/IIR1 семантики заново: аргументы (что есть x, что есть acc),
направление и КАК результат используется (референс vs сигнал).
2. Тест адаптивного twin: рендер тона разной ДЛИТЕЛЬНОСТИ (0.5/2/8 c) через мост —
если пол зависит от длины ⇒ res_k(t) адаптируется, взять динамику из NOTES_TWIN.
3. Формулы контраста перебрать офлайн на траекториях RT_DUMP_ALL (движок phaseA готов).
## ============ UPDATE 2026-08-22k: BARE-CHAIN АРХИТЕКТУРА ПОДТВЕРЖДЕНА ============
Цепочка «голая» exp2-архитектура (RT_LUT_OFF=1 RT_IIR12=0 RT_NOWARP=1 RT_NOBLEND=1
RT_NOIIR3=1) на белошумном пробнике совпала с реальным плагином в пределах ~0.5 дБ
почти везде:
| freq | real | model |
|------|------|-------|
| 50 | 0.01 | 0.12 |
| 300 | 0.08 | 0.26 |
| 678 | 1.04 | 2.59 |
| 850 | 0.76 | 0.94 |
| 1000 | 0.74 | 0.62 |
| 2000 | 0.22 | 0.20 |
⇒ АРХИТЕКТУРА ПОДТВЕРЖДЕНА:
mask[k] = exp2(lvl_raw[k]) БЕЗ blend-бланкета, БЕЗ warp-аттенюации
lvl_raw[k] = per-bin TIME envelope от am/res·scale
output = conv(audio, FIR(mask))
Частотно-направленные каскады IIR1/2/3 в аудио-пути ОТСУТСТВУЮТ (они и создавали
фантомное смазывание нотча и бланкет-пол). kIIR_A1==A_ATTACK, kIIR_A2==A_RELEASE —
это время-энвелоп коэффициенты (feed-forward атака, retention релиз), НЕ частотные.
### Остаточный разрыв ТОНА: −15.13 vs 20.72 (нужен lvl 3.4436, есть 3.0117)
Фактор ×1.1437 в амплитуде. Кандидаты:
(a) am-нормировка (wsum/окно) отличается;
(b) res_k twin АДАПТИРУЕТСЯ во времени (tatt=11ms/trel=80ms из NOTES_TWIN!) —
при длинном тоне res_k уменьшается → lvl растёт;
(c) scale_factor формула (наш 15·440.95/2048=3.231).
Мелочи: модель глубже у центра полосы на шуме (−2.59 vs 1.04 @678); у real
HF-rolloff 1.5@8k (вероятно WIN_freq окно FFT-conv — таблица [01] registry!).
### NEXT
1. Декомп амплитудной цепочки: wsum/am нормировка (spectral.cpp), масштаб окна.
2. Twin-адаптация: проверить динамику res_k по длине тона (рендеры 0.5/2/8 c).
3. WIN_freq rolloff: добавить окно FFT-conv в модель (registry [01] 0.5→1.0).
## ============ UPDATE 2026-08-22l: POOL+SCALE+FLOOR — тон точен до 0.02 дБ, корпус 5.9 ============
Новая архитектура (все за env-флагами, канон не тронут — санити TOTAL 2.286 ✓):
RT_LUT_OFF=1 RT_IIR12=0 RT_NOWARP=1 RT_NOBLEND=1 RT_NOIIR3=1
RT_POOL=5 RT_SCALE_M=1.144 RT_FLOOR=1
lvl_in = pool_max(lvl_raw, ±5 бин) · 1.144; cap = (16.78+sens/3)/6.0174
mask = exp2(min(lvl_in,cap))
Результаты:
- TONE floor: **20.74 dB vs real 20.72** (Δ0.02!) при pool∈{4,5}, scale 1.144.
- White noise: mean|err| 0.63 dB (форма плоская ✓), но перерез центра полосы
(3.54 vs 1.04 @678).
- Корпус 62: TOTAL **5.905** (t1k 60→7.2 благодаря полу; t1kq 5.57 — тихие случаи
теперь перерезаются scale/pool). Пока ХУЖЕ канона 2.286/bridge 1.594.
### Интерпретация
Архитектурно верно (пол/пин/форма тона), но константы (pool width, scale 1.144,
cap-закон) сняты с ОДНОГО случая (hot tone trim+12) и не обобщаются на тихие.
Масштаб 1.144 требует источника в декомпе (золотое правило) — кандидаты: wsum
(sqrt-Hann vs др.), формула scale_factor, sens внутри twin.
### NEXT
1. Систематический фит (pool_w, scale, cap-параметры a/b в (a+b·sens)) по МИНИМИЗАЦИИ
corpus TOTAL — офлайн через render48k прогонки (быстро, ~2 мин/конфиг).
2. Источник ×1.144: декоп wsum/am нормировки детектора (spectral.cpp vs decomp).
3. Разобраться с перерезом центра полосы на шуме (pool слишком широкий для шума?
может, pooling по МАКСИМУМУ неверно — попробовать contrast-vs-pool комбинацию).
## ============ UPDATE 2026-08-22m: ГИПОТЕЗЫ ПОЛЬЗОВАТЕЛЯ ПРОАНАЛИЗИРОВАНЫ (E1/E3/E4/E5) ============
Гипотезы пользователя: (1) BandConfig влияет на детектор; (2) a/b = слоты состояний
плагина; (3) gamma = дельта-режим.
### E1 — delta on/off: γ НЕ дельта
Захват с delta=1.0 vs база: конфиги идентичны (−24/28/γ=1 flag=0 cb=set). γ не кодирует
дельта-режим (тот «пересекает линию» только в отрисовке).
### E3 — depth ±18: BandConfig стабильна
depth UI 18 и +18 (мост): те же 24/28/1 и 16/20000/1. Вместе с E1 и 7 пресетами —
структуры НЕ реагируют ни на один параметр обработки ⇒ консистентно с ролью
конфигурации отображения кривой (диапазон графика −24..+28 dB / 16..20000 Hz!).
### E4 — callgraph расширен
FUN_180529610 (SpectralProcessor-метод, 1154B) дизассемблирован: НОЛЬ ссылок на объект
level-path и на ctx-буферы 540678/5406f8/5407c8/540870; callee — мелкие функции
(52e190×5, 52e130, 52df60, 52dc30). Grep всех .dis по оффсетам объекта: совпадения
только в GUI-кластере (563440/563a60) + 0x540678 (ctx-буфер аудио, другой объект).
Оговорка: полный pointer-flow анализ требует Ghidra-сессии; grep по готовым .dis
покрывает ~140 функций.
### E5 — heap-diff межсессионный: неинформативен
Heap layout плывёт между запусками (базы сдвигаются, соседние аллокации чужие) —
451/512 блоков «различаются» мусором. Для честного diff нужен same-session capture
с переключением входа между снапшотами.
### Статус гипотез
1. BandConfig→детектор: подтверждений нет (E1/E3/E4), но полная верификация указателей
не проведена (Ghidra data-flow). Приоритет низкий.
2. Слоты A/B: существуют (manual, AorBStateOption), наши структуры им не соответствуют
(идентичны во всех захватах в слоте «a»). Тест со слотом «b» отложен (нужен ручной
клик или ReaScript-команда переключения).
3. γ=delta: ОПРОВЕРГНУТА (E1).
## ============ UPDATE 2026-08-22n: LIVE CTX НАЙДЕН + МЕДЛЕННАЯ АДАПТАЦИЯ ДЕТЕКТОРА ============
### E2-lite (без пользователя): sidechain/L-R/lowcut/eco — конфиги опять идентичны
Итого 9 захватов на широкой поверхности параметров: структуры +0x168/+0x170 статичны
всегда ⇒ окончательно display-config.
### LIVE CTX НАЙДЕН В ТЕКУЩИХ СНАПШОТАХ (ctx≈0x2370048!)
Анкор: паттерн kIIR_A2[1..3] в куче → ctx = hit 0x2c04f8. Совпало со старой сессией
(0x2370040±8). Живые скаляры (t1kq-пресет):
int@1a0 = 4096 (=NFFT, не 2048!)
f32@870 = 1.0000 (НЕ 440.95 — то было значение другого пресета render_long!)
f32@878 = 25.0 f32@87c = 10.0 (=UI sharpness/selectivity!)
f32@888 = 0.0 f32@88c = 1200.0
⇒ Скаляры КОНФИГОЗАВИСИМЫ. Наш хардкод scale_factor=15·440.95/2048 калиброван под
чужой пресет и неверен для t1kq-корпуса! Формула масштаба требует перечтения
mangled-блока декомпа (Ghidra headless re-run) с живыми значениями.
### МЕДЛЕННАЯ АДАПТАЦИЯ ДЕТЕКТОРА (главное!)
Короткий тон 0.75с (тот же сатурированный конфиг): gain во времени:
[0.02-0.35s]: 10.32 dB [0.35-0.72s]: 10.98 dB
Длинный тон 6с: −20.72 dB (пин).
⇒ Редукция УГЛУБЛЯЕТСЯ СЕКУНДАМИ. Постоянная времени ~секунды соответствует
kRTRel≈0.96..1.0 retention на кадр (hop 1024@48k → τ≈2с)!
Следствия:
1. «Статический» ×1.144 калибровался на несошедшемся состоянии длинного рендера —
делить нужно на динамику, не на константу.
2. Короткие рендеры корпуса меряют НЕнашедшееся состояние → часть ошибок корпуса
может быть фазой адаптации, а не формой кривой!
3. Модель обязана интегрировать медленный релиз через кадры (RT_IIR12=3 каркас есть,
но нужен настоящий покадровый цикл с персистентным env и правильной FRAMES-rate).
### NEXT
1. Реализовать покадровую персистентность env в render48k (env_time уже static —
проверить, что реально тянется между вызовами process_band_structural).
2. Откалибровать att/rel семантику по переходным кривым (short-tone транзиент
−10.3→−20.7 за ~секунды — идеальный тестовый сигнал для фита kRTAtt/kRTRel).
3. Перечитать mangled scale-блок декомпа с живыми 1a0=4096/870=1.0.
## ============ UPDATE 2026-08-22o: RT_ENV=live + корпусные прогоны новой архитектуры ============
RT_ENV=live (детекторный энвелоп из kRTAtt/kRTRel: атака feed-forward, релиз retention
~tau2s) добавлен в FramedDetector::processFrame за флагом.
- Белый шум: mean|err| улучшился 0.632 → **0.461**
- Корпус: НЕЙТРАЛЕН (метрика меряет сошедшиеся окна — энвелоп сходится и там).
Корпусные варианты новой архитектуры (все с RT_LUT_OFF/IIR12=0/NOWARP/NOBLEND/NOIIR3/FLOOR):
| конфиг | TOTAL | примечание |
|--------|-------|------------|
| pool=5 scale=1.144 | 5.905 | тон точен, шум-центр перерезан |
| pool=0 scale=1.144 | **5.386** | t1kq 5.574→3.621, res хуже 1.713 |
| канон (без env) | 2.286 | HEAD, не тронут |
Вывод: архитектурные находки верны на пробниках (тон-пол точен, шум плоский), но
константы (scale 1.144 снят с одного случая) не обобщаются на корпус ⇒ нужен
систематический joint-фит (pool_w × scale × cap-law) минимизацией TOTAL, плюс
поиск источника масштаба в декомпе (золотое правило). Канон остаётся HEAD до
превышения гейта.
### NEXT (приоритеты)
1. Joint-фит сеткой: pool_w {0,3,5} x scale {1.05,1.10,1.144,1.20} x cap{on,off}
(~12 прогонов корпуса по ~2 мин).
2. Декомп амплитудной нормировки: wsum/am (spectral.cpp vs детекторный путь),
источник scale (int@1a0=4096, f32@870=1.0 живые!).
3. Транзиентный калибратор: короткий тон некорректен из-за латенси/WOLA коротких
рендеров — заменить на длинный файл со СТУПЕНЬЮ уровня внутри.
## ============ UPDATE 2026-08-22p: JOINT-FIT ГРИД — оптимум scale 0.90, пол обязателен ============
Сетка (scripts/gridfit22p.py): pool×scale×floor по корпусу (62 случая).
### Результаты
| scale | floor | TOTAL | t1kq | t1k | al | res | dual | comb |
|-------|-------|-------|------|-----|----|----|------|------|
| 0.90 | 1 | **5.215** | 1.961 | 5.801 | 5.328 | 2.663 | 7.774 | **2.707** |
| 0.95 | 1 | 5.242 | 2.255 | 5.980 | 5.541 | 2.432 | 7.699 | 2.811 |
| 1.00 | 1 | 5.261 | 2.547 | 6.155 | 5.704 | 2.201 | 7.621 | 2.915 |
| 1.05 | 1 | 5.298 | 2.892 | 6.279 | 5.842 | 2.008 | 7.588 | 3.019 |
| 1.144 | 1 | 5.386 | 3.621 | 6.456 | 6.123 | 1.713 | 7.542 | 3.213 |
| 0.95 | 0 | 11.021 | — | **22.5** | 10.4 | — | 14.4 | — |
| 1.00 | 0 | 11.396 | — | **23.4** | 11.1 | — | 14.8 | — |
### Выводы
1. **FLOOR ОБЯЗАТЕЛЕН**: без него громкие случаи уходят в тишину (t1k 60→22 дБ ошибок,
dual ×2). Кап детекторного уровня — реальный механизм плагина, подтверждён корпусом.
2. Оптимум scale≈0.90 (тренд вышел на плато 5.2-5.3); pool=0 лучше (пул перерезает
центр полосы на широкополосном материале).
3. **comb впервые лучше канона** (2.707 vs 6.116) — мультиполосный случай выигрывает от
новой архитектуры.
4. Канон 2.286 всё ещё лучше по TOTAL: однополосные тихие случаи (t1kq/al) в новой
архитектуре перерезаются (scale-закон глобальный, а нужен уровнезависимый).
dual ~7.7 не двигается ни одним параметром — его причина не в уровне/форме кривой.
### Статус
Канон остаётся HEAD (гейт «не хуже bridge» не пройден: 5.2 > 1.594). Новая архитектура
живёт за env-флагами: RT_LUT_OFF=1 RT_IIR12=0 RT_NOWARP=1 RT_NOBLEND=1 RT_NOIIR3=1
RT_POOL=0 RT_SCALE_M=0.90 RT_FLOOR=1 RT_ENV=live → TOTAL 5.215.
### NEXT сессии
1. dual@500: константная ошибка при всех q/уровнях — исследовать отдельно (возможно
межполосный combine-консюмер или второй тон 2000Hz взаимодействует).
2. Уровнезависимость закона: scale-множитель должен зависеть от уровня сигнала
(компрессия в детекторе?) — ступенчатый калибратор внутри файла.
3. comb-выигрыш закрепить: понять, почему мультиполосность выигрывает (перенос в канон?).
## ============ UPDATE 2026-08-22q: DUAL ДИАГНОСТИКА — насыщающая кривая, не контраст ============
Кейс dual q=0.5 (тона 500+2000, оба 7.2 dBFS), trакт@frame150:
| тон | bin | am | res | наш lvl | нужен (real) |
|-----|-----|----|----|---------|--------------|
| 500 (центр) | 43 | 0.321 | **0.117** (res_min) | 8.85 | 1.70 |
| 2000 (вне) | 171 | 0.321 | 0.839 | 1.24 | 1.95 |
GAIN'ы: real @500=10.24 @2000=−11.75 (почти РАВНЫ!); newarch @500 упёрся в кап
(20.4), @2000 недорез (+5.5); canon: @500 перерез 6.9, @2000 точен (12.4≈−11.75).
### ВЫВОДЫ
1. Деление am/res(twin) создаёт контраст ×5 между центром и краем — реальный плагин
такого контраста НЕ имеет: режет оба тона поровну при q=0.5. Twin-делитель в роли
полного делителя lvl ОПРОВЕРГНУТ для аудио-выхода.
2. canon согласуется с real на ВНЕполосном тоне (res^rp мягкий вес ✓) — форма
«кривая от уровня + мягкий вес res» ближе к истине, чем «жёсткое деление».
3. Мануал: «до 60 dB нотчей» при выкрученном depth + наш пин −20.7 при перегрузе
и номинальные −10 дБ здесь ⇒ ГИПОТЕЗА: cut_dB = насыщающая функция уровня
(например A·(1exp(lvl/T)) или soft-knee), а не линейный exp2(−lvl) с капом.
4. Уровни: real нужен lvl 1.70 там, где наш 8.85 — часть разрыва всё ещё
амплитудная нормировка (int@1a0=4096 и др.).
### NEXT
1. Собрать пары (наш lvl_raw, real cut_dB) по ВСЕМ корпусным случаям (62×бины тонов)
→ фит насыщающей кривой cut=f(lvl) — единый закон вместо подгонки параметров.
2. Проверить форму: линейно-лог / экспоненциальное насыщение / soft-knee.
3. Роль res: сравнить cut@center vs cut@skirt при ОДИНАКОВОМ lvl — отделить вес res
от кривой уровня.
## ============ UPDATE 2026-08-22r: АФФИННЫЙ ЗАКОН ПОРТИРОВАН — НОВАЯ АРХИТЕКТУРА БЬЁТ КАНОН ============
RT_LAWAFFINE="A_db,S_db": mask = exp2((A_db + S_db·log2(lvl))/6.0174) поверх голой цепи
(NOTES 22k). Константы с al-семейства (грубо): A=6.14/S=2.17 → **TOTAL 2.152 < канона 2.286!**
### Грид-фит
| A | S | TOTAL | t1kq | t1k | al | res | dual | comb |
|---|---|-------|------|-----|----|----|------|------|
| 6.14 | 2.17 | 2.152 | 0.426 | 0.889 | 0.090 | 1.795 | 3.953 | 2.678 |
| 6.60 | 2.00 | 2.047 | 0.409 | 0.930 | 0.294 | 1.262 | 3.702 | 3.349 |
| **7.40** | **1.85** | **1.931** | 0.852 | 0.577 | 0.804 | **0.395** | 3.480 | 4.335 |
Трейд-оффы: A↑ → res ↓↓ но comb ↑; S↓ → t1k ↓ но al ↑.
Против bridge (1.594): новая арх. лучше на t1k (0.58<1.80), al (0.80<0.64~паритет),
comb (4.3<<10.1); хуже на t1kq/res/dual. Против канона (2.286) — лучше по TOTAL и
на 4 группах из 6.
### Смысл
Аффинный закон Phase-A (21d) пересажен на ПРАВИЛЬНУЮ архитектуру (голый exp2 без
blend/warp/частотных каскадов) — и сразу даёт лучший корпусный результат среди всех
конфигураций проекта. Двух констант достаточно; их источник — следующий вопрос
(золотое правило: сейчас это фит, помечено).
### NEXT
1. Фит A/S раздельно по группам → понять зависимость от sens/q (сейчас глобальные).
2. dual ~3.5 стабилен — межполосный механизм (приоритет из 22p).
3. WIN_freq HF rolloff не добавлен — после добавления comb может улучшиться ещё.
4. При достижении ≤bridge: гейт пройден, переносить в канон.
## ============ UPDATE 2026-08-23a (22s): ОФЛАЙН-ФИТ A/S — СКАЛЯРНЫЙ ЗАКОН НАСЫТИЛСЯ ============
Инструментарий: `scripts/lawfit22r.py` — полный numpy-реплей render48k/spectral
(ресемпл 44.1<->48 poly, Hann STFT 4096/1024@48k, поточечная маска, WOLA,
FULL-BLK чанкинг = 61 хоп-фрейм на блок 65536 с zero-pad хвоста — именно так
вызывается processBlock в render48k.cpp:137, поэтому кадров 61·nblk, НЕ (LN)/hop+1).
Детектор независим от закона ⇒ один захват RT_DUMP_ALL траекторий lvl[frame][band][bin]
(`/tmp/opencode/lawfit_traj.npz`, 47 уникальных рендеров) обслуживает ЛЮБОЙ (A,S) офлайн.
Скорость: весь корпус ~2 c/точку (против ~2 мин на реальный рендер).
### Валидация реплея (A=7.4,S=1.85 против реального corpus 1.931)
sim TOTAL 1.895; группы: al +0.045 / dual +0.013 / res 0.085 — отлично;
t1kq 0.30 / t1k +0.32 / comb 0.64 — СТАТИЧЕСКИЕ смещения (риппл ресемплера
scipy≠libsamplerate вокруг крутых спектральных форм; у al полоса на тоне — плоско,
дельта нулевая). Смещение не зависит от закона ⇒ рейтинг кандидатов корректен.
### ПОЧЕМУ точечная модель запрещена (важно для будущих сессий)
1. Тон 1000 Гц = бин 85.33 (не целый!) + главный лепесток Hann ±2-3 бина: при крутой
юбке реализованный гейн тона ≠ mask[tone_bin] (res_500: mask[85]=+8.4 дБ БУСТ,
рендер режет −7.2 дБ).
2. Детектор НЕстационарен в метрическом окне: resonant.wav lvl[85@bin] падает
0.86→0.0002 за файл (медленная адаптация резонатора, 22n) при ПОСТОЯННОМ rms входа.
Медиана по хвосту смешивает 2 декады ⇒ мусор.
### Метод линий (бисекция A* при S∈{0.5,1.5,2.5} на реплее)
У однотонового рендера одно уравнение cut=A+S·x ⇒ вырожденная ЛИНИЯ в (A,S);
x_i=−dA*/dS — точная эффективная координата кейса (включая транзиенты и лики).
Результаты (`lawfit_lines.json`):
- x_eff ОТОБРАЖАЕТ уровень входа ТОЧНО: al-свитч Δx=−3.48 на Δ21 дБ = 1/6.02 окт/дБ
⇒ детектор линеен по амплитуде (подтверждение масштабной цепи).
- НО линии семейств НЕ пересекаются: локальные наклоны cut(x): t1kq 0.85 /
t1k 1.54 / res 1.65 / al 2.19. Единого аффинного закона НЕТ даже на чистых тонах
⇒ кривизна/контент-геометрия, скалярная пара (A,S) принципиально недостаточна.
- Глобальный LSQ по 35 линиям: A=7.396 S=1.973, остаток mean 0.600 дБ,
corr(resid,rms)=+0.71, corr(resid,|log2 fc/1k|)=+0.69 — два недостающих фактора.
- dual: пер-рендерный оптимум даёт floor 0.52-0.62 при q≥1.5 даже своим (A,S) ⇒
межполосный разрыв НЕ закрывается никакими константами закона (приоритет 22p стоит).
- comb (4 полосы): оптимум mean|e|=1.28, константы нестабильны (угол сетки).
### Реальные рендеры (валидация кандидатов, гейт --vs-bridge не гонялся — канон не меняем)
| конфиг | t1kq | t1k | al | res | dual | comb | TOTAL |
|--------|------|-----|----|-----|------|------|-------|
| HEAD 7.4,1.85 | 0.852 | 0.577 | 0.804 | 0.395 | 3.480 | 4.335 | **1.931** |
| LSQ-линии 7.4,1.97 | 0.926 | 0.430 | 0.986 | 0.455 | 3.660 | 4.120 | 1.988 |
| спуск 7.6,1.6 | 0.894 | 1.123 | 0.750 | **0.069** | **3.105** | 5.053 | 1.894 |
Вывод: скалярный закон насыщен в коридоре 1.87-1.93; (7.6,1.6) даёт res 0.069 (!),
но регрессирует t1k/comb — Канон ОСТАЁТСЯ HEAD (7.4,1.85). Теоретический потолок
пер-групповых оптимумов ~0.78 TOTAL недостижим одной парой констант.
### NEXT
1. Двухфакторный закон cut=A+S·x+T·g(fc-дистанция/контент): корреляции указывают
на геометрию полосы относительно тона; данные линий уже собраны (lawfit_lines.json).
2. Механизм межполосного консюмера для dual (Шаг 2/9, приоритет 22p) — константами
не решается, подтверждено покейсовой бисекцией.
3. Реплей-инфраструктура готова для быстрого перебора ЛЮБЫХ параметрических законов
(2 c корпус) — использовать вместо реальных рендеров до финальной валидации.
## ============ UPDATE 2026-08-23b (22t): ДВУХФАКТОРНЫЕ ЗАКОНЫ ОТВЕРГНУТЫ — БЮДЖЕТ ОШИБКИ В DUAL ============
Инструменты расширены:
- `RT_DUMPRESPATH=<file>` в FramedDetector::setParams — статические спектры twin-response
res[band][bin] ({i32 band,i32 nbin,f32[]}, append); lawfit collect кладёт их в npz рядом с lvl.
- `RT_RESPRP=1` в process_band_structural — применять res^rp при NOWARP (для реальных
рендеров двухфакторного закона).
- `lawfit22r.py fit`: координатный спуск по семействам законов на реплее (2 c/корпус).
### Результат фит-семейств (офлайн, старт 7.6/1.6)
| семейство | A | S | Q | rp | TOTAL(sim) |
|-----------|---|---|---|----|-----------|
| scalar | 7.600 | 1.694 | 0 | 0 | **1.860** |
| quad (Q·x²) | →scalar | | **→0** | | 1.860 |
| resrp (mask·res^rp) | →scalar | | | **→0** | 1.860 |
| quad+resrp | 7.452 | 1.540 | +0.069 | 0 | 1.865 |
ОБА вторых фактора отброшены спуском: кривизна не нужна, res-вес не нужен.
Объяснение resrp: lvl = am/res УЖЕ содержит геометрию твин-юбки; домножение на
res^rp сверху — двойной учёт (в каноне res^rp компенсировал другое — отсутствие
res в xv-домене LUT).
### Реальный рендер скалярного оптимума (7.6, 1.694)
t1kq 0.954 / t1k 0.842 / al 0.799 / res **0.117** / dual **3.246** / comb 4.855
⇒ TOTAL **1.898** (sim 1.860 + стат. смещения реплея ~0.04). Против HEAD 1.931:
выигрыш мизерный, t1kq/t1k/comb РЕГРЕССИРУЮТ ⇒ гейт не пройден, канон НЕ тронут.
### Бюджет ошибки — где лежит остаток (решающий факт)
abs-сумма по корпусу при (7.6,1.694): 115.4 дБ·кейсов, из них:
- **dual: 22 кейса × 3.246 = 71.4 (62%!)** — межполосный механизм (Шаг 2/9, консюмер acc/f6f8);
- comb: 24.3 (21%) — мультиполосное взаимодействие;
- всё прочее: ~20 (17%).
Если бы dual был на уровне bridge (0.726), TOTAL упал бы до ~0.96. Никакая
донастройка закона это не даст — ПРИОРИТЕТ №1 подтверждён количественно:
искать точку потребления acc/f6f8 (межполосный каскад), а не крутить маску.
### NEXT
1. dual/межполосный консюмер: декомп-охота вокруг FUN_180529fe0 callers
(52e260 render-loop, multiband combine), трассировка acc/f6f8 в живом плагине.
2. comb: после dual — пере-тест 4-полосного взаимодействия.
3. Реплей-инфра готова; для любых новых гипотез сначала офлайн (2 c), потом рендер.
## ============ UPDATE 2026-08-23c (22u): РЕАЛЬНАЯ СТРУКТУРА 529fe0 ПО RAW ASM — ПЕРЕСМОТР ЦЕПИ ============
Охота за консюмером acc/f6f8 по всем дизасмам/декомпам. ГЛАВНОЕ: наша транскрипция
цепи расходится с тем, что метод реально делает.
### Новые декоды (ground truth из handoff/nls_dasm/f529fe0.dis, 505 строк)
1. **FUN_18052d650 — БИДИР-ИРРАЦИОННЫЙ ФИЛЬТР В DOUBLE** (полный декод):
сигнатура (state=rcx, out=rdx/f6f8, in=r8/band, n); acc=[rcx+0x100010]=0 RESET;
forward: acc = upCoef[i]·acc + downCoef[i]·band[i], запись в f6f8[i]
(upCoef double @state+0x80010+i·8, downCoef @state+0x10+i·8 — ПЕР-БИНОВЫЕ массивы);
backward: продолжение с ТЕМ ЖЕ state по nbin2..1. Это наш «IIR3», но вход band,
выход f6f8, и коэффициенты пер-биновые double, не табличные float.
2. **ВТОРОЙ инлайн бидир-проход в теле 529fe0** (52a65d..52a85b): вход f6f8, выход
ОБРАТНО в band[i]. Итого пара [reset,fwd,bwd]×2 — как в заметках, но точные
адреса/направления теперь залочены.
3. **Межполосное ПРЕ-КОМБИНИРОВАНИЕ ВНУТРИ 529fe0** (52a39a..52a4f6, ДО основного
цикла): f6f8 ← copy bands[0]; цикл по bands[1..n1] (массив @0x540688 = соседний
элемент того же массива векторов!): поэлементные thunks (float 0x20f0/double
0x1850 пары = шаблонный transform); нормировка делением на int [rsp+0x148];
затем второй проход: band[i] *= (xmm11xmm7), xmm7=pow([ctx+0x2c],xmm13)
(mix-вес!), и f6f8 ⊕= ... Семантика thunks ещё не залочена.
4. Основной пер-полосный цикл: scale (=PRNG·[0x540870]·[0x54088c], флаг 0x5408b8
добавляет exp-random) → in-place кернел (thunks 0x140980/0x1409b0, вероятно
bigkernel exp2) → 52d650 (bidi#1) → инлайн bidi#2.
5. Пер-полосный массив состояния **0x540768[i]** читается в основном цикле
([rsp+0x40]) — роль не ясна (кандидат: трек/acc).
6. **FUN_180529c60** (vtbl, параметр = индекс полосы): band←5355d0(param_2);
осцилл-цикл по [ctx+0x1b0] c фильтрацией через f6f8; скалярная нормировка
band *= exp((0.5·K1K2)·ln10/20) · **FUN_1804d56b0(band)** (агрегат от всей
кривой полосы!); релиз-коэф pow(...) от sr/0x1a0·0x1ac; трек-обновление через
массив **0x5407a8[i]** (52dae0 axpy + 52dbc0 add). Кандидат на «медленную
адаптацию» (22n) и/или постобработку маски.
7. **0x5407c8 НЕ читается НИГДЕ в рантайме** (только ctor/dtor в f_535a70.dis) —
адрес «combine acc» из NOTES 21b был неверен; реальные пер-полосные состояния:
0x540768[i] (в 529fe0) и 0x5407a8[i] (в 529c60).
### Офлайн-проверка гипотезы «спектральное сглаживание маски» (lawfit iir_c)
Бидир-однополюсник по бинам (нормированный, ×2 прохода) при законе 7.6/1.694:
c=0.2 → TOTAL 3.23; c=0.6 → 9.83 (монотонная деградация всех групп). ОТВЕРГНУТО:
сглаживание размывает нотч в центре, но физически не дотягивается до тона-конкурента
за ~130 бинов. Двойной cut в dual объясняется НЕ межбиновым сглаживанием маски.
### Следствие для dual
Реальный плагин режет 500 и 2000 почти поровну при q=0.5. Раз контраст lvl
(res-деление) есть у нас и его нет в результате — значит либо (а) детекторный
уровень считается во ВРЕМЕННОЙ области через тот же twin-фильтр (536300 строит
res-кривые на сетке 0.25 Гц — а детекция может идти банком time-domain фильтров),
либо (б) нормировка агрегатом FUN_1804d56b0 выравнивает кривую, либо (в) закон
не логарифмический по lvl. Приоритет — полный перенос 529fe0 из raw asm (505 строк,
подъёмно) вместо текущей реконструкции из кусков.
### Инструментарий
lawfit22r.py: +iir_c параметр (бидир-IIR по бинам в реплее), +res в npz;
sim_errors кэширует по (Q,rp,iir_c).
## ============ UPDATE 2026-08-23d (22w): FAITHFUL-ЦЕПЬ v1 — ИНФРАСТРУКТУРА ГОТОВА, ДЕТЕКТОР ГЛАВЕН ============
Инфраструктура (dsp/fnfaith.{hpp,cpp}, env RT_FAITHFUL=1 в render48k):
транскрипция маско-формирующей цепи по BLOCKMAP_529fe0: band=lvl·s → exp2(-band)
→ бидир-IIR ×2 (setA tau19/mult360) → бидир (setB tau10.7/mult2400) → ×2 (setC).
Коэффициенты — точная транскрипция FUN_180533340: частотный варп
fc_bin=(1000/(sr/2))·nbin (=85 @48k/4096!), g=fc/i ниже кросса, степенной выше,
up=exp(-2π·κ·c·g·tau), down=1-up. Константы из дампа: DAT_24c46b8=-2π,
24c3d8c=0.5 (sr→Найквист), mult 1200/800/360, тау-интерполяция 15↔9 и 9↔180.
Все параметры — env (RT_FAITH_*). Combine acc в v1 опущен (нет консюмера, 22u).
### Смоук-свипы (t1kq1000 / dual500 / dual2000 / al12; err dB)
- κ=0.05,s=8: t1kq 25 / dual500 58 / **dual2000 2.84** — тон-конкурент ВПЕРВЫЕ
дотягивается до реала, но центральный тон улетает в перерез.
- κ=0.2..0.5: насыщение (up→0 на низких бинах), баланса пары нет нигде;
Δerr(5002000) ≈ 15-20 дБ во всех точках (в реале ~1.5).
- Корпус (κ=0.02, s=2): TOTAL 5.315 (t1kq 1.35 / t1k 1.55 / al 0.89 /
res 2.87 / dual 11.52 / comb **2.57** — лучший comb в истории проекта,
базлайн 4.34!). Ресурс мультиполосной механики виден.
### ВЫВОД (решающий)
Форма маски НЕ может сбалансировать dual: контраст lvl между тонами создаётся
САМИМ ДЕТЕКТОРОМ (am/res из STFT), а не пост-обработкой. Реальный детектор
даёт почти равные уровни драйва на 500 и 2000 Гц при q=0.5 ⇒ гипотеза (а)
из 22u — ПРИОРИТЕТ №1: уровни считаются во ВРЕМЕННОЙ области (банк twin-
фильтров 536300 строит res-кривые на сетке 0.25 Гц — вероятно, и уровни тоже
через них), либо тяжёлый кросс-бин пулинг ДО кривой полосы.
Смена канона НЕТ; faithful-путь остаётся за флагом для следующих экспериментов.
### NEXT
1. Детектор: декод FUN_180536300 до конца (scratch-лейаут известен, NOTES_TWIN)
→ понять, откуда берутся bands[] кривые на входе 529fe0.
2. comb 2.57 при живом faithful — разобраться после детектора.
3. RT_DUMP_BIN-трасса живого плагина на dual-конфиге (покадрово mask/lvl) —
прямой ответ на вопрос «какой lvl реально у тона 2000».
## ============ UPDATE 2026-08-23e (22x): ТЕОРЕМА О ОТСУТСТВИИ α + ТАБЛИЦА DUAL ПО q ============
Инструмент: scripts/resalpha.py — RT_DUMP_BIN-тракты всех уникальных рендеров
(57 пар case/freq: lvl_a1, res, am на бине тона + реальные cut из рефов),
CSV /tmp/opencode/resalpha_pairs.csv.
### Теорема об отсутствии α
Модель lvl = am/res^α с ЕДИНЫМ законом cut=L(log2 lvl): имплицитные наклоны
семей (концевые точки, дБ/октава lvl) при НИКАКОМ α ∈ [−0.6..1] не совпадают:
- dual требует α≈−0.5 (наклон +0.2..+0.4);
- при α≤0 t1k/t1kq/res получают БЕСКОНЕЧНЫЙ/отрицательный наклон (весь fc-скан
схлопывается в точку по x — res перестаёт различать fc);
- при α>0 dual-пара даёт отрицательный наклон (запрещено для монотонного закона).
⇒ ВЕСЬ класс точечных моделей lvl=f(am, res^α) с общим законом ОТВЕРГНУТ.
Глобальный LSQ по α плоский (1.59-1.63 дБ) — семейные смещения доминируют.
### Таблица dual по q (реальные cut, одна полоса @500)
| q | res500 | res2000 | cut@500 | cut@2000 | Δ |
|---|--------|---------|---------|----------|---|
| 0.1 | 0.117 | 0.216 | 10.32 | 15.33 | +5.01 |
| 0.5 | 0.117 | 0.839 | 10.32 | 11.82 | +1.50 |
| 1.0 | 0.117 | 1.353 | 10.32 | 10.78 | +0.46 |
| 3.0 | 0.119 | 1.880 | 10.29 | 10.32 | +0.03 |
| 10 | 0.139 | 1.988 | 9.94 | 10.25 | 0.07 |
Структура: cut@500 КОНСТАНТА по q (драйвер центра не зависит от ширины полосы);
Δcut(res2000) ≈ экспонента 5.4·exp(−res/0.49), схлопывание при res→2.
cut@2000 ПАДАЕТ с ростом res2000 — противоположно любому «lvl растёт с res»
и противоположно делению. Геометрическая интерпретация: широкий нотч (малое q)
НАКРЫВАЕТ тон-конкурент сам по себе; вклад центра всегда полный. Это свойство
ФОРМЫ twin-шаблона на мелкой сетке (0.25 Гц, FUN_180536300), а не пер-биновой
арифметики на сетке 4096.
### al-группа (якорь уровня, fc=tone=1000)
lvl_a1: 10.08/7.14/5.05/3.58/1.79/0.90 → cut: 13.64/12.44/11.28/10.13/7.97/6.01.
Ломаная: наклон ≈1.16 дБ/окт для lvl≥1.8 и ≈2.06 ниже — кривизна закона
подтверждена независимо (ср. 22s линии).
### Следствие
Детекторный уровень НЕ восстанавливается из спектра бина функционально.
Кандидаты: (а) скалярная огибающая сигнала через time-domain twin-банк
(536300 строит res-кривые на сетке 0.25 Гц — возможно, тот же банк даёт и
уровни), (б) форма кривой = res-шаблон × скалярный драйв, что естественно
даёт постоянство центра и геометрическое покрытие скайрта.
Решающий эксперимент — живая покадровая трасса детектора на dual-конфиге
(registry-heartbeat инфраструктура готова, NOTES_CAPTURE.md).
## ============ UPDATE 2026-08-23f (22z): FFT-conv статика + СПРЕДИНГ ОПРОВЕРГНУТ ============
### Статика mask→FIR (52b55052b8bb, per-band цикл построения FIR)
Полная последовательность на полосу: scale-ops на кривой → **call 0x180535a70**
(стаб → ILT → 0x180140a10 float / a70 double → импорт-прыжки → тела 0x1802a24c0
float / 0x1802fa420 double — векторизованный AVX2, функционально FFT-дизайн) →
копия в r14=[ctx+0x540668] → complex-op с буферами 0x540548/550/598 →
FIR[n/2]=0 → bigkernel in-place → ещё complex-op → **WIN_freq умножает сегмент:
rdx = win_ptr + (n/2)*4** (вторая половина 8193-таблицы!) → масштаб второй половины
xmm9 → **FIR[0]=1.0, FIR[1]=0** → init пар, dry/wet-scale 0x540888.
Размерности: n=[ctx+0x540534], половина rbp=n/2, счётчики n/2+1.
Живой каталог (22y): 0x540758=freqaxis, 0x540658=WIN_freq 0.5→0.8.
### Спрединг-гипотеза (IDFT(R)→окно L→DFT) — ЧИСЛЕННО ОТВЕРГНУТА
На живой кривой dual R(7f8) (v43=3.98, v171=1.41): окна Hann L∈{32..2048} дают
cut@171 ≤ 2.60 дБ (нужно 11.82); cut@43 падает с 12.0 до 4.4 при малых L.
Сглаживание НЕ может поднять долину до уровня соседнего пика — механика скайрта
dual НЕ в пост-обработке кривой R.
### Что остаётся (структура данных dual по q, 22x)
Δcut(скайрт−центр) ≈ линейно по −ln res_скайрта с наклоном b≈2.25 дБ/e-fold,
НО центр из этого закона выпадает (его cut константен при res≈const). Это
согласуется с моделью «скалярный драйв через time-domain twin + шаблонная форма»,
но требует знать, как именно нормируется шаблон. Прямое декодирование тел
0x1802a24c0 (3КБ AVX2) и complex-op шагов с буферами 0x540548/550/598 — следующий
статический шаг; альтернатива — трассировка промежуточных буферов живьём
(снапшоты в момент аудио-колбека, не между кадрами).
## ============ UPDATE 2026-08-23g (23b): ДИЗАЙН=LOG(band), Hann-окно, PRNG-дизеринг ============
### Что доказано декодом (детали в BLOCKMAP 23b)
1. Полная цепь построения FIR на полосу залочена по raw asm (52b55052b8bb):
scalar-shape ×2 → **DESIGN: scratch=log(bands[i])** (векторный log2, полином
идентифицирован по константам, тело 0x1802a24c0) → copy→FIR@0x540668
4 комплекс-op (A..D) на буферах 0x540548(SIMD/twiddle-workspace)/0x540598
(YMM-маски 8×1/8×0×64) вокруг нормировки &FIR[1]÷K и bigkernel →
**FIR[2048..4095] ×= Hann_freq[2048..4095]** (падающая половина периодического
Hann(4096), пик ровно на стыке!) → FIR[0]=1,FIR[1]=0 → dry/wet над 2n флоатами.
Структура log→ops→exp-класс + окно = классический frequency-sampling /
минимально-фазовый дизайн кернела. Гипотеза механизма скайрта: геометрия
задаётся ОКНОМ и комплекс-ops на log-спектре, а не постобработкой R.
2. WIN_freq опознан численно (w[1024]=0.5,w[2048]=1.0) — прежняя трактовка
«0.5→0.8» относилась к другому слоту (0x540698).
3. freqaxis: шаг 11.71303 vs точные 11.71875 — накопительная f32-ошибка построения;
сетка внутренняя 48k/4096 подтверждена независимо.
4. Выход плагина недетерминирован от рендера к рендеру (md5 разные, rms равные) —
LCG-прологи реально дизерят; честная метрика только спектральная.
### Динамика: почему живые кривые «статичны»
Realtime-playback без аудио-девайса НЕ вызывает processing (все треды спят);
кривые 22y — продукт инициализации проекта. `-renderproject` прожимает весь
цикл за ~0.8 c (init+render+exit); перехват требует SEIZE+TRACECLONE ДО первого
CONT главного треда. FUN_180529fe0/52d650/536300/52e260 не ловятся в рендер-окне
⇒ маск-цепь выполняется при загрузке/param-change, рендер использует кэш.
Следующий динамический шаг (если понадобится): bp на входе process-callback vtbl
или трасса окна init (поймать DESIGN в момент param-change).
### Инструментарий этого раунда (закommичено)
scripts/ilt_resolve.py (резолв стабов офлайн), scripts/firtrace.py (полный каталог
буферов + GUI-флаг 0x2404bc), scripts/firstop.py (SIGSTOP-семплирование),
fntrace*.py/fnhw.py/fnall.py (ptrace-лаборатория: INT3/DR, уроки — см. BLOCKMAP),
hotips.py. .dis тела: conv_float_a24c0, conv_double_fa420, imp_*, th_* (19 impl).
### Контрольные числа dual (цель раунда, неизменны)
cut@500=10.32 дБ const ∀q; @2000=11.82 @q=0.5; Δcut≈2.25·(ln res).
Модель сейчас: −0.18/@500-ок + провал скайрта.
### NEXT (пересмотр после декода)
1. numpy-реплика цепи log→opsA-D→window с КАЛИБРОВКОЙ ops по измеренным H(f)
(корпус референсов уже есть); критерий — воспроизведение Δcut(res) геометрии.
2. Если ops A-D ≈ exp-раундтрип со сглаживанием: транскрипция за RT_FIRCONV,
гейт corpus --vs-bridge + контрольные числа dual.
3. ptrace-эскалация ТОЛЬКО если численная репликация не сойдётся: цель —
брейкпоинт в момент param-change (init-окно хоста), а не рендер.
### 23c (короткое): феасибилити log-свёртки — ОТРИЦАТЕЛЬНЫЙ результат
На живой кривой R@0x5407f8 (dual q=0.5): подбор ОПТИМАЛЬНОЙ симметричной
гаусс/бокс-свёртки в log-домене (ширина до 1500 бинов, свободный аффинный сдвиг,
точное совпадение @bin43=10.32 дБ) даёт минимум ошибки на bin171 ≈ 0.941.10 дБ
при ширине ~314–324 бина (~3.7 кГц) — такая ширина уничтожает избирательность
нотча на всём остальном корпусе. Вывод: механизмы класса «свёртка log-маски с
симметричным ядром» геометрию dual НЕ воспроизводят (расширяет теорему 22x с
поточечных законов на симметрично-свёрточные). Остаются: (а) вход bands[i] у
DESIGN ≠ захваченные кривые (768/788/7f8 — возможно, GUI/init-версии);
(б) асимметричные/структурные ops (cepstral-liftering класс); (в) темпоральный
кросс-фрейм механизм. Решающий недостающий факт — ИСТИННЫЙ массив bands[0] в
момент вызова DESIGN; рецепт добычи: seize главного треда yabridge-host в момент
ПОЯВЛЕНИЯ pid (ppid=reaper, comm ещё 'wine'), INT3 на 0x1802a24c0/0x1802fa420,
CONT, дамп rcx/rdx на хите (скрипт fnall.py готов, нужен только ранний захват —
нынешний finder по maps опаздывает на init-бурст <5 мс).
## ============ UPDATE 2026-08-23h (23d): ПРОРЫВ — ЖИВОЙ КЕРНЕЛ СНЯТ, bands[] = СПЕКТР СИГНАЛА ============
Маршрут A удался БЕЗ ptrace и БЕЗ аудио-девайса: **SIGSTOP-семплирование
офлайн-рендера** (`-renderproject`; обработка back-to-back ⇒ случайные остановки
попадают внутрь конвейера). scripts/rendersnap.py: find-loop ctx (vtable-маркер
0x1824AC210 / float 48000+ sens; ctx=0x2370040 стабилен и в рендере),
затем STOP→читаем все слоты→CONT c детекцией изменений. 24 фазы за прогон.
### Решающие факты из phase-снапов (dual q=0.5)
1. **bands[0] @ctx+0x540678 = АМПЛИТУДНЫЙ СПЕКТР ТЕКУЩЕГО ФРЕЙМА**:
ненулевые группы ТОЛЬКО вокруг тонов (бины 41–45 и 168173, Hann-лепестки;
band[43]=7.65, band[171]=9.91), между ними нули. Это вход DESIGN, НЕ шаблон
маски и НЕ пост-детекторная кривая!
2. scratch@540628 ≈ log(bands)·k (после ops значения −0.66@43, 0.76@171).
3. Живой FIR@668 (комплексные пары): mag(43)=0.524 (5.61 дБ), mag(171)=0.473
(**−6.51 дБ**) — скайрт режется ГЛУБЖЕ центра, КАК В РЕАЛЕ (+1.5 дБ),
хотя кривая R@7f8 утверждает обратное (R171=1.41<R43=3.98).
4. Количественно из снапа: cut≈A+S·log2(am/res) с res(43)=1.0, res(171)=0.839
(twin-шаблон из таблицы 22x!) даёт согласованную пару — **структура
am/res ПОДТВЕРЖДЕНА, но входом служит сырой спектр сигнала**, а res-шаблон
полосы входит делителем (где именно в цепи ops A-D — осталось залочить).
### Почему раньше «не ловилось»
- realtime без аудио-девайса не вызывает process вовсе (alsa_outdev пуст!);
- в render-mode инстанс создаётся ~через 0.3 c после host-spawn: ранняя остановка
ловит нулевую фазу (первые 12 «хитов» были ей);
- после создания экземпляра кернел живёт в непрерывном перестроении — STOP-цикл
читает его спокойно.
### Следующий раунд (рецепт готов, данные добываются rendersnap.py за минуту)
1. Прогнать rendersnap на dual q∈{0.1,1,3,10} + t1kq + al: собрать пары
(bands, scratch, FIR) → восстановить точную формулу ops (ожидаем exp(log·K)
со сглаживанием/варпом res-шаблона; проверить FIR vs exp(scratch)).
2. Объяснить абсолютную глубину (снап даёт −5.6/−6.5 против реала 10.32/11.82):
кандидат — нестационарность кадра (attack-фаза) или повторное применение.
3. Транскрипция kernel-build за RT_FIRCONV=1 → контрольные числа dual → гейт.
4. reaper.ini: alsa_outdev/indev=null прописаны (бэкап /tmp/opencode/reaper.ini.bak)
— realtime колбек всё равно не ожил (cpu 0.2 t/s), вопрос закрыт в пользу
render-snap метода.
## ============ UPDATE 2026-08-23i (23e): ЗАКОН СОШЁСЯ КОЛИЧЕСТВЕННО ============
Серия rendersnap: dual_b1q_0.5 + dual_b1q_3.0 (стационарные фазы).
1. **FIR = exp(0.984·scratch)** поточечно (коэф идентичен @43/@171) ⇒ ops
между log и exp — скалярный раундтрип, никакой межбиновой магии в самих ops.
2. **real_cut / FIR_cut = 1.7981.810 константа** по всем 4 точкам
(q0.5: 5.70/6.58 FIR против 10.32/11.82 real; q3.0: 5.69/5.76 против
10.29/10.32). Между кернелом и слышимым применением — ЛИНЕЙНЫЙ масштаб
~1.805 в дБ-домене (кандидат: двойное применение/нормировка conv-стадии;
залочить при декоде потребителя, до тех пор EMPIRICAL-константа).
3. **Центр не зависит от q живьём**: scratch43 = 0.6565(q0.5) vs 0.6551(q3)
при том, что сырой bands[] мигает от 0.3 до 382 (детекторные IIR дают
стационар). Скайрт: Δscratch171(q3q0.5)=+0.0937 ln ⇒ ×0.984×1.805 =
**+1.46 дБ** против реального Δ=1.50 дБ — сходится в пределах 3%.
4. Структура механизма окончательно: mask_log(f) = G(log(am_frame(f)), res_band(f));
кернел = exp(mask_log) c падающим Hann-окном на [n/2..n); применение =
свёртка × масштаб 1.805. Это УЖЕ наша каноническая структура am/res — но
вход амплитуды берётся из спектра кадра напрямую (не через наш lvl-тракт),
и применение — FIR-свёртка, а не поточечное умножение STFT.
### NEXT (конкретика)
1. Восстановить точную зависимость scratch(band, res): прогнать q-серию
{0.1,0.2,...,10} + am-серию, аппроксимировать G (ожидаем линейность в логах
вида k·ln(am/res)+c, k≈0.984·?): две точки уже дают k_skyрт согласован.
2. Найти источник масштаба 1.805 (декод потребителя FIR: кто читает 0x540668).
3. Транскрипция RT_FIRCONV=1: bands=STFT-спектр кадра → scratch=G(...) →
kernel=exp·HannWin → rfft-свёртка overlap-add → гейт корпуса.
## ============ UPDATE 2026-08-23j (23f): q-СЕРИЯ ЖИВЫХ SCRATCH ============
scripts/qseries.py: rendersnap по dual_b1q_{0.1..10}.rpp (11 конфигов).
Стационарная подвыборка (scr43≈−0.6565): q={0.2,0.3,0.5,0.7,1.0,1.5,3.0}
(q=0.1,2,5,10 поймали нестационар scr43≈−0.18 — фильтровать по scr43!).
Таблица (FIR-cut дБ = 20log10 exp(0.984·scr); ×1.805 — эмпир. масштаб):
| q | scr43 | scr171 | FIR@43 | FIR@171 | ×1.805 @171 | real@171 |
|---|---|---|---|---|---|---|
| 0.2 | 0.6565 | 0.8965 | 5.61 | 7.66 | 13.83 | — |
| 0.3 | 0.6565 | 0.8332 | 5.61 | 7.12 | 12.85 | — |
| 0.5 | 0.6565 | 0.7570 | 5.61 | 6.47 | 11.68 | 11.82 |
| 0.7 | 0.6565 | 0.7192 | 5.61 | 6.15 | 11.09 | — |
| 1.0 | 0.6564 | 0.6923 | 5.61 | 5.92 | 10.68 | 10.78 |
| 1.5 | 0.6562 | 0.6750 | 5.61 | 5.77 | 10.41 | — |
| 3.0 | 0.6551 | 0.6633 | 5.60 | 5.67 | 10.23 | 10.32 |
Выводы: (а) центр строго const ∀q; (б) ×1.805 попадает в real с ошибкой
≤0.14 дБ на обеих известных точках; (в) scr171(res2000) НЕлинеен в ln res
(локальный наклон падает 0.135→0.088 ln/res) — согласуется с экспоненциальной
формой 22x; (г) сырой bands[] мигает на порядки (0.3↔382) при стабильном
scratch ⇒ детекторное сглаживание (attack/release IIR) сидит МЕЖДУ записью
bands[] и DESIGN — искать его адрес в цепи до 52b550.
### NEXT
1. am-серия (t_-24..t_+24 rpp уже есть в Recent!) → наклон по амплитуде →
полная форма G=log-домена: scr = c0 + ka·ln(am) + f(res).
2. Декод читателя 0x540668 (источник ×1.805) + детекторного IIR перед DESIGN.
3. RT_FIRCONV=1 транскрипция: G аппроксимировать таблично по этой серии.
### 23g (короткое): am-серия пока неинтерпретируема
t_{-24,-12,0,+24}.rpp (тон@1000, бин 85): scr85 = 1.0071 / 0.2276 / 0.4399 /
−1.0071 — НЕмонотонно по уровню; вероятная причина — steady() хватало разные
фазы (attack/стационар) или в t_* конфигах другая полоса. Данные в логе прогона;
повторить с контролем стабильности (несколько последних фаз подряд равны).
Скрипты qseries.py/amseries.py закоммичены.
## ============ UPDATE 2026-08-23k (23h): ОХОТА ЗА ПОТРЕБИТЕЛЕМ КЕРНЕЛА ============
1. Xref-скан disp32 0x540668 по всему коду: вне 52bxxx только ИНИТ
(52ed36 lea→resize, 35b71 store, 361de/f читатель+запись в 536xxx-билдере
кривых). Прямых чтений ДАННЫХ FIR в рантайме не видно ⇒ потребитель берёт
слот из таблицы указателей по индексу ЛИБО существует вторая копия цепи.
2. bp на выходе FIR-цикла (52b8bb) в render-окне: 0 попаданий (как и вход
529fe0) при том что кернель появляется ⇒ **в рендере работает ДРУГОЙ код**
(дублированная инстанциация цепи; модуль 480 МБ). fnall.py: finder заменён
на проверенный maps-вариант, FN_STAB управляет стабилизацией.
3. fnsample.py — активный RIP-сэмплер (PTRACE_INTERRUPT цикл): 10.6к сэмплов,
2375 в модуле. Горячее: 0x180b0c3e0 (MSVC 0xCC stack-fill, НЕ DSP),
кластеры 0x180bcxxxx/bdxxxx (хелперы); в ядре замечены 0x180534580 и
0x18052e1c0 (рядом с ramp-in 52e260!) — но только в части прогонов
(срабатывают на param-фазе, не в стационаре рендера).
4. reaper.ini: alsa null не оживил realtime (закрыто). Yabridge: сотни мёртвых
сокет-директорий /run/user/1000/yabridge-* от kill -9 ломали спавн —
чистить rm -rf перед серией (грабли зафиксированы).
### NEXT (продолжение)
1. fnsample с окном НА param-фазе (t≈1.31.6 c после host-spawn) и фильтром
ядра 0x180520000-0x180560000 → поймать дубликат цепи в момент построения.
2. Проверить 0x18052e1c0/0x180534580 дизасмом (уже лежат окна? нет — снять).
3. После опознания дубля: bp на его выход → [rsp]=адрес применения → декод
масштаба 1.805.
### 23i (короткое): HW-watchpoint заведён, попаданий нет
Отладочные регистры через POKEUSER: база u_debugreg = **0x350** (не 0x340/0x380;
DR7 @0x388; LEN=10(8B) отвергается ядром — использовать LEN=11(4B),
DR7=L0|R/W=11|LEN=11 = 0xF0001; сброс DR6 = poke 0xFFFF0FF0 @DR0+48).
scripts/fnwatch.py: watch на слово кернела (bin43.re) от момента создания
экземпляра до конца хоста — **0 попаданий** при подтверждённом армировании.
Гипотезы: потребитель читает КОПИЮ кернела; либо строится однократно до
армирования; либо выравнивание доступа мимо слова. Следующий шаг: watch
нескольких слов + старт с рождения хоста + контроль факта рендера (wav mtime).
### 23j (короткое): мульти-watchpoint — попаданий нет при валидном рендере
fnwatch4.py: 4 DR одновременно {слот-указатель FIR (оба dword), слово кернела,
слово scratch} → затем позитивный контроль {overlap-слово, track[43], R[43],
лок 0x2404dc}. Конфигурация: render_fresh=True (wav@1.49 c), armed=8/8 тредов
(INTERRUPT→wait→arm→CONT на resweep обязателен — армирование бегущего треда
молча не работает), WATCH_DUR=30. **0 попаданий везде**, включая лок и
аудио-переполнение, которые ОБЯЗАНЫ трогаться при обработке.
ВЫВОД: аппаратные data-watchpoints под wine-тредами НЕ доставляются tracer'у
в этом окружении (kernel/wine взаимодействие) — маршрут закрыт. Потребителя
искать статически: скан disp32 из диапазона таблицы [0x540600..0x540900]
(семейство байт `?? 06 54 00`) → функции, итерирующие слоты по индексу;
плюс декод 52e1c0/534580-окрестностей отвергнут (аллокаторы).
## ============ UPDATE 2026-08-23l (23k): HW EXEC-BP РАБОТАЕТ; ЦЕПЬ СТРОИТСЯ ОДИН РАЗ ============
1. Статический скан ВСЕХ disp32 из [0x540600..0x540900] (390 ссылок): все
относятся к известным функциям (529fe0, init 52exxx, билдеры 535xxx/536xxx).
Скрытого статического читателя таблицы НЕТ.
2. Аппаратные EXEC-bp (DR0/DR1, RW=00) через POKEUSER теперь реально работают
(ранее все HW-эксперименты были no-op из-за неверного оффсета 0x380 вместо
0x350). RF-флаг (eflags|=0x10000) для обхода exec-trap; TRACECLONE наследует
DR дочерним тредам.
3. Чистый прогон (fnexec, arm после создания экземпляра @1.46 c): **wav@1.48 c,
0 попаданий на вход 529fe0 и голову FIR-цикла** при живом рендере ⇒
**декодированная цепь НЕ выполняется в steady-render; кернель строится
ОДИН РАЗ на param-change в узком окне (<100 мс) сразу после создания
экземпляра**. Это объясняет: стабильность кернела между фазами, молчание
всех bp, статичность кривых 22y.
4. Следствие для транскрипции: rebuild кернела ТОЛЬКО на setParams; per-frame
работает только детектор+свёртка с кэшированным кернелом.
### NEXT (решающий)
fnexec early-arm режим уже умеет ставить exec-watch ДО создания экземпляра
(наследование клонам) — но два прогона подряд поймали флак рендера под
трассировщиком (render_fresh=False). Протокол повторения: (а) ранний arm на
0x1802a24c0/0x1802fa420; (б) НЕ трогать треды до первого SIGTRAP (убрать
resweep-CONT чурн); (в) по хиту — дамп rcx(bands)/rdx(scratch) уже реализован;
(г) если рендер опять не стартовал — просто перезапустить (флак ~50%).
После дампа bands[] в момент param-change: сравнить с R-кривой, восстановить
G, декодировать потребителя через backtrace ([rsp]) из того же хита.
### 23l (короткое): DR-механизм верифицирован микротестом; расхождение локализовано
Микротест /tmp/opencode/dtest.py на fork+exec ребёнке: DR0 exec-watch срабатывает
(SIGTRAP, rip==target, DR6 bit0) — ядро/CPU полностью поддерживают аппаратные
ловушки через POKEUSER 0x350/0x388. При этом fnexec с ранним армированием
(до создания экземпляра, наследование через TRACECLONE + армирование при
рождении каждого треда) даёт 0 попаданий на {529fe0, loop-head, design×2,
dispatcher, bidir} при подтверждённых рендерах и 8–12/8–12 армированных тредов.
Одновременно: disp32-скан показывает, что ВЕСЬ доступ к scratch/FIR идёт только
из декодированного цикла 52bxxx. Прямое противоречие ⇒ остался ровно один
неисключённый вариант: **трассируется не тот процесс/треды умирают до арма**.
Следующий шаг: в fnexec логировать КАЖДЫЙ stop (rip,dr6,pid) без фильтра +
счётчик всех SIGTRAP; если трапов нет вообще на тредах, которые заведомо
выполняют модульный код — сравнивать /proc/pid/task состав до/после.
### 23m (финал раунда): трассировка под wine — аномалия доставки зафиксирована
Серией прогонов с ПОЛНЫМ логом остановок установлено: за 14 c живого хоста
tracer видит ~1 остановку (не от наших DR), хотя 8+ тредов армированы
exec-watch на тела дизайна/диспетчер/bidir с верифицированной записью DR
(drprobe.py: все poke ok, readback точный) и микротестом доставки на
нативном треде (dtest.py — работает). Треды wine при этом выполняют модульный
код (рендер происходит). Вывод: доставка DR-trap'ов от wine-тредов в наш
waitpid-цикл не работает в этой конфигурации (кандидаты: wine сам использует
DR/GetThreadContext, wow64-переключения, либо события теряются из-за
групповых остановок). Отладочный вывод сохранён в fnexec/hits.json.
### Достижения раунда (суммарно, 23h–23m)
1. Доказано: цепь строится ОДИН РАЗ на param-change (<100 мс окно), steady
render её не вызывает — транскрипция: rebuild только на setParams.
2. Все 390 статических ссылок на таблицу слотов учтены; скрытого читателя нет.
3. HW exec-bp методология верифицирована end-to-end (dtest) — инструмент готов,
осталась аномалия доставки под wine.
4. rendersnap.py — рабочий канал снятия ЛЮБЫХ внутренних состояний плагина
(уже дал формулу FIR=exp(0.984·scratch), масштаб ×1.805, q-серию).
### Дорога к bit-exact (порядок)
1. G(band,res): q-серия есть; добавить am-серию со стабилизацией фаз;
аппроксимировать и проверить на hold-out.
2. Потребитель/×1.805: (а) полный декод init-семьи 52e9b0531xxx (param-time
строитель — потребитель рядом); (б) либо повторить DR-охоту после решения
аномалии доставки (например, трассировать через PTRACE_SEIZE опции или
perf_event_open с breakpoint типом HW).
3. RT_FIRCONV=1 + контрольные числа dual + гейт корпуса.