Files
soothe2-re/handoff/NOTES_LEVEL.md
T

41 KiB
Raw Blame History

============ 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+ch0x800, in+0xe0, band, 0x200). lVar1=scratch+N32; normalize lVar1[i]=in[i]2pi/(ossr) (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.25fVar9*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 += xa + yb, 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 += 0x5406c8contrib thunk_180003c40(lVar17=0x5406f8, 0x5406e8, 0x5407c8[band], iVar19) -> 0x5407c8 += 0x5406e8contrib (0x5406c8=1-0x5406b8, 0x5406e8=1-0x5406d8 — КОМПЛЕМЕНТАРНЫЕ веса)
  6. накопление: thunk_180005a20(0x5407c8[band], 0x540678[band], iVar19)
  7. dry/wet: 0x1c[band]scale (0x5408b8==0: +0x5406780x1c[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/(0x240.5) [p24=40000 -> fVar12=0.1] fVar13 = 4.0 (или 1.0 если 0x5408b8); dVar1 = (0x24/sr)0x1ac0.001 [секунды] for i in bins: fVar9 = cdc( (fVar12iVar6)/(i+1) ) = exp?/pow? (0.1513=51.3 / (i+1)) fVar11 = 0x540880 * 0.25 * fVar9 * fVar13 dVar10 = 1/(1 + fVar11/(0x5408804)) = 1/(1+fVar11/(4level)) dVar10 = exp(dVar10 * fVar11) dVar10 = cd6(0.1, 1.0/(dVar10dVar1)) = 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((x20000-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.25w8fVar12 [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(qv,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((x20000-120)(-0.01)))' ~1.3 DC boost) x resonance. The model's empirical tilt {500:1.414,1000:1.454,2000:1.795} stays as the placeholder; replacing it requires FUN_180529fe0 consumer step-5 algebra + runtime level values (Phase-4/5 dive).

============ UPDATE 2026-08-18 (CHECKPOINT): tilt != warp, level-path decoded ============

  • FUN_180563ce0 = INIT of per-bin IIR level trackers (not update): 0x156=342 bins, order=3.0f (0x40400000), coeff A/B mvaps 0x24c4780/4790, strided fill; 3rd pass fills 0x40e00000=7.0f (post-gain). FULL: /tmp/opencode/f_563ce0.dis.
  • Level-path map: 0x563440 (LUT curve +0x188: [min,max,gamma,linear flag,nodes]; 6 bands via 0x56e3e0 double-twin N=1024 with LUT-ramp input; combine -> +0x2198; LUT pairs +0x98 at 0x3ff pts level->mask). NOT in decomp_funs.txt; use /tmp/opencode/f_563440.dis + level_notes.md (LOCAL: /home/m/re-tools/handoff/).
  • DECISIVE NEGATIVE: warp(2000)=0.773 cannot reach dual red2000=15.2dB — C=depth·warp·LUTmax=0.864·0.772·0.667 =0.445 < needed 0.826 (C=1-10^(-15.2/20)). warp is NOT the empirical tilt. The gap is closed by the blend step (0x5407c8 += weights·res_upper/lower, freq-axis 0x540698) + FFT-conv 0x535a70 shaping.
  • CHECKPOINT decision (user): STOP; next session = diagnostic bridge (two-term mask: warp·LUT + α·freq-axis_term, fit vs measured curves) BEFORE transcribing FFT. do NOT curve-fit without the blend term.

============ UPDATE 2026-08-18 v2: BRIDGE DONE + FUN_180529fe0 full algebra ============

  • Полный декомп FUN_180529fe0: /tmp/consumers_out.txt:471-1277 (f529fe0.dis в /tmp обрезан на 52a813). Поправки к прежним заметкам:
    • 0x5408b0 = таблица ГРАНИЦ бинов (прелюдия: cvttss2si+imul+idiv, iVar20=band-start, iVar19=len), НЕ LUT.
    • Уровневая маска резолвится ИНЛАЙН: fVar30=0x5408b0[idx]; fVar25=(float)FUN_181a14cdc()=pow (IAT→GOT 0x181bab378, CRT-цель вне дампа); пары ×(fVar30fVar25)/+fVar25· (dry/wet уровня).
    • Цепочка per-band (по декомпу): 0x540678=res; res*=∏0x540688[k]; res*=(fVar30/nBands); *=fVar29·0x54088c (fVar29=(fVar30/0x1a0)·0x540870); IIR 0x52d650 state 0x440518; двойное IIR-сглаживание (double, states 0x3404f8/2c04f8 и 0x440510/3c0510); 0x5407c8+=0x5406c8·res_up + 0x5406e8·res_low + 0x540678 (003c40, kernel vfmadd213pd) — АДДИТИВНЫЙ аккумулятор; 0x540678 *= res(008700) и *= warp 0x5406a8; offline: *=0x1c[band]·fVar27 (глубина, D=-0.5@0x24c4670); dry/wet: mask·fVar30·0x540888 + (fVar11fVar30) (fVar11=1.0 c3ea4) — педастал; FFT-conv 0x535a70 (fwd, window 0x540550/598, kill mirror, inv, ×0x540658, fwd, inv, 0x540668[0]=1.0[1]=0, apply FIR к param_2[band]).
    • Константы offline: c3ea4=1.0, c3e28=0.8, c4670=0.5, c4674=0.7, c4680=1.0, c3c58=0.001.
  • BRIDGE RESULT (model_fir.py, rmse=0.236 dB): C(f)=1.221·LUT(log10(L0/res)) + 0.358·warp(f)^3.143. res(500;fc500)=0.117 Q-независим -> C500-константа без tilt; xv(2000)=0.308..0.656 (Q) -> C2000-и-гг-форма; аддитивный терм warp^3.14 даёт +0.15 на 2000 и ≤0.02 на 1000 (иначе t1k рушится) — единственная точка, совместимая с 36 замерами. Мультипликативный warp·LUT проваливается (>10 dB). warp^3.14≈π: кратный каскад? Остаток 0.7 dB (Q=0.1) = LUT-колено 0.574 -> нужен FFT-уровень (0x535a70 + окно 0x540658 + freq-axis 0x540698), см. SESSION_HANDOFF §5-6.

============ UPDATE 2026-08-18c: FFT-CONV LOOP FULLY MAPPED (Phase 5 step 3 partial) ============

Per-band FFT-conv loop disasm: /tmp/opencode/f_52b570.dis (0x52b550-0x52b8b5 == consumers :1195-1271).

What 0x535a70 / helpers ARE (they're dispatchers, real kernels in base-CRT):

  • 0x535a70 = FFT fwd dispatcher -> 0x140a10 (scalar) / 0x140a70 (vector). NOT hand-transcribable, unnecessary.
  • 0x540548/0x540550/0x540598 = FFT-PLAN table ptrs (plan 0x540530; +0x18/+0x20/+0x68), NOT windows. Plan built by FUN_18052dc30(param_1+0x540530, [0x64]=FFT size, 0x540868). 0x540534 = plan+0x4 = N/2 usage.
  • Per-band ops (dispatch variants collapsed): iVar20=N/2; r14=FIR(0x540668);
    1. 0x535a70(fftctx 0x540628, mask, N/2+1) forward FFT of mask
    2. 0x2210(0x540628, 0, r14, N/2+1) inv FFT into FIR
    3. fwd FFT (0x2180/0x1bb0) ; zero @[N] ; 0x52d920(r14+4, xmm13, N/2-1)=fill LOW half; 0x52db50(r14+4+N/2, xmm9, N/2-1)=fill HIGH half; inv FFT (0x1a90/0x19d0)
    4. 0x140b30/0x140aa0(r14,r14,N/2+1) in-place complex op
    5. fwd FFT ; zero @[N] ; 0x52d990(r14, 0x540658+N/2, N/2) = FIR *= WINDOW 0x540658; 0x52db50(r14+N/2, xmm9, N/2) = fill HIGH half; inv FFT
    6. FIR[0]=1.0f, FIR[1]=0; if 0x540890!=0: complex ops (0x1880/0x1ca0) + FIR = 0x540888 over 2N; 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 = 20x540534; 0x1a0 = pow(2,floor(log2(SR/44100)))412 (SR 44.1k -> 412); 0x540868=0x1a0iVar98.

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=gLUT(xv)+wwarp^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 "<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.3g where g = Kx/(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.3warp(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)
  • windowinput-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 2N? Actually 8193 = 40962+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) - Wwarp^A) / G (mask C = GLUT + W*warp^A).
  5. JOINT FIT (dual + al_, objective=mean|err| over 6 dual tones + 6 al levels): best: G=1.0850 W=0.2819 A=1.1377 -> al_ err <=0.19 dB ALL; dual err <=0.62 dB ALL. The LUT leg is a SLANTED curve (~0.36 at xv=-0.5 rising to ~0.64 at xv=+0.55), NOT the flat B.12 (~0.5).
  6. framed_render.py updated: LX/LY = merged anchors + al_* nodes; G/W/A = 1.0850/0.2819/1.1377. CRITICAL BUGFIX: lut clip must be [LY.min(), LY.max()] (0.366..1.0) not LY[0] (0.4402) - node at xv=-0.5012 (0.366) was being floored, breaking q10@2000 by 1.4 dB.

Final validation (framed_render.py dual):

q0.1 @500 -0.55, @2000 +0.14; q1 @500 -0.62, @2000 -0.57; q10 @500 +0.01, @2000 -0.00 envRmse@steady: 0.64/0.04/0.71/0.78/0.11/0.11 dB. al_* lv3..24 err: +0.11~+0.19 / +0.01 / -0.02.

Conflict (t1k fc-scan vs al_* clif at xv=0.93) - RESOLVED:

al_* "-104 dB @ lvl 0dBFS" comes from overdriven input (am >> any res), saturating mask to C->1. t1k 15.6 dB @ xv=0.931 was a different level point (soothe's own fc-scan dataset). Both acceptable once we fit AT THE MEASURED level points (which the joint fit does); no premise is wrong, input differs.

Next: (1) hammer the residual -0.6 dB on dual 500Hz cases (structural); (2) multi-band combos (band>=2); (3) C++ port.

============ 2026-08-19 (continuation): LEVEL-PATH DECOMPILED — FUN_180563440 + FUN_180563ce0 ============

FUN_180563440 (dsp/levelpath.cpp dump, f_563440.dis 222 lines) — LUT curve + twin-mask + combine

Three-phase per-frame structure:

  1. 1024-bin LUT loop: bin k → x = clamp(k/1024, 0, 1). Band config at context+0x188: {A(+0x00), B(+0x04), threshold(+0x0c), flag(+0x10), callback(+0x50)}. Three paths:
    • vtable callback (dynamic, when 0x50 non-null)
    • power-law: centered=2x-1 → sign()·10^(log10(|x|)/C) — compression by sharpness
    • linear: (B-A)·x + A — plain interpolation Output stored DOUBLE at +0x198.
  2. Twin-mask factory FUN_18056e3e0: 6 bands × 1024 bins, stride 0x2000.
  3. Combine: max 2 channels (stereo), 6 bands, 1 - Σ weights.

FUN_180563ce0 (f_563ce0.dis 163 lines) — IIR level-tracker INIT (NOT update)

  • 341 bins (0x155), order-3 IIR (3 coeffs/bin: 0x40400000 = 3.0 markers).
  • Level coefficient: 0.1 (0x3dcccccd IEEE 754) — this is the attack/release α.
  • State layout: rcx+0x28/+0x40/+0x58 (3 buffers). Init [1,0,0,0] / [-1,0,0,0].
  • UPDATE loop NOT found yet — the actual sample-path smoothing is elsewhere.

Key constants (all verified from soothing_mem.bin):

Address Value Meaning
0x24c3c54 0.0009775 1/1024 bin scale
0x24c3ea4 1.0 clamp max
0x24c41e0 2.0 power-law centering
0x24c4680 -1.0 sign flip
0x24c3d8c 0.5 threshold
0x24c4334 4.0 depth range (oversample os=4)
0x24c43e0 8.6859 20/ln(10) dB conversion
0x24c4704 -6.9078 ln(0.001) floor

CRITICAL: The "warp" is NOT a table — computed at runtime

  • rwin_A0.npy (live 0.5→0.8) is the frequency WINDOW (per-bin mask shaping), NOT the warp.
  • Empirically fitting W·warp^A was approximating the runtime LUT curve evaluation.
  • The LUT is parametric (linear or power-law by band flag), not a fixed lookup.
  • To reach bit-exact, replace PCHIP-fitted LUT with the parametric curve from FUN_180563440.

Structural finding (per-bin gain from reference, avg over steady frames):

  • Reference mask is FLAT ~-10.2 dB across 100-540 Hz REGARDLESS of Q (dual_b1q q0.1/1/10).
  • Model produces res-shaped notch → source of the -0.6 dB residual at 500 Hz.
  • Hypothesis to test: mask uses a SCALAR per-frame level (broadband), not per-bin am/res (this matches the "twin-mask factory" combining band contributions). Test in /tmp/scalartest.py.

============ 2026-08-19h3: RES_POWER OPENING — 500Hz RESIDUAL SOLVED ============

The fix: gain_k = (1-C) × res_k^rp (rp ≈ 0.05)

The reference applies the mask NOT as (1-C) directly to the per-bin gain, but as (1-C) * res^rp — a small res-dependent correction. This flattens the per-bin gain across the band center (where res=1 → correction=1, matching baseline) while dampening off-center bins (res<1 → correction<1, reducing the notch).

Validation (test_joint_rp2.py, multi-start Nelder-Mead, 5000 iter):

BEST: G=0.9696 W=0.3503 A=1.0887 rp=0.0516 mean=0.208 (dual only, 6 pts)
  q0.1@500:  err=+0.00  (was -0.55)
  q0.1@2000: err=+0.00  (was +0.15)
  q1@500:    err=-0.05  (was -0.62)
  q1@2000:   err=-0.41  (was -0.57)
  q10@500:   err=+0.57  (was +0.01)
  q10@2000:  err=+0.22  (was -0.00)

The rp parameter SOLVES the 500Hz residual for q0.1 and q1 (the main bottleneck), but slightly degrades q10 (the narrow-Q case).

Physical interpretation:

In soothe2, the mask is computed in the RESONANCE-DOMAIN (xv = am/res), but the applied gain has an additional res-dependency. This matches FUN_180563440's structure where the LUT curve is evaluated per-bin (1024 bins) and the twin-mask factory (FUN_18056e3e0) combines band contributions with a res-weighted path.

Next steps:

  1. Refine rp jointly with al_* (need faster al_* rendering — batch the 6 lv cases)
  2. Update framed_render.py with rp parameter
  3. C++ port of res_power term (trivial: multiply gain by pow(res, rp))