From c8f97e4ea32068fd3a9e6f4a12e924556e04d23f Mon Sep 17 00:00:00 2001 From: Matiq Date: Tue, 18 Aug 2026 18:08:57 +0300 Subject: [PATCH] Phase5/step2: B.12 model live (rmse 0.236) + freq-axis builder correction (offline const) --- handoff/NOTES_LEVEL.md | 227 +++++++++++++++++++++++++++++++++++++++++ model_lut.py | 67 ++++++------ 2 files changed, 264 insertions(+), 30 deletions(-) create mode 100644 handoff/NOTES_LEVEL.md diff --git a/handoff/NOTES_LEVEL.md b/handoff/NOTES_LEVEL.md new file mode 100644 index 0000000..ec8b642 --- /dev/null +++ b/handoff/NOTES_LEVEL.md @@ -0,0 +1,227 @@ +## ============ 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-цель вне дампа); пары `×(fVar30−fVar25)`/`+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 + (fVar11−fVar30) + (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-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·(1−mix) 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) + (1−fVar30) fVar30=0x540874−rnd (: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. diff --git a/model_lut.py b/model_lut.py index 0d1c95d..1d1fff7 100644 --- a/model_lut.py +++ b/model_lut.py @@ -1,35 +1,37 @@ #!/usr/bin/env python3 -"""model_lut.py — КАНОНИЧЕСКАЯ модель B.11 (2026-08-18). +"""model_lut.py — КАНОНИЧЕСКАЯ модель B.12 (2026-08-18). -Единая непараметрическая LUT-кривая "уровень -> маска" для ВСЕХ уровней входа. +Диагностический мост (model_fir.py) ЗАМЕНИЛ эмпирический tilt реальной цепочкой +FUN_180529fe0: C(f) = g·LUT(xv) + w·warp(f)^a (АДДИТИВНЫЙ аккумулятор 0x5407c8). -МОДЕЛЬ (verify, rmse=0.072 dB на 36 точках — dual_b1q 22 + t1kq fc-скан 7 -+ t1k fc-скан 7 при 3 уровнях входа: -7.14 / -18.06 / 0 dBFS): +МОДЕЛЬ (rmse=0.236 dB на 36 точках — dual_b1q 22 + t1kq fc-скан 7 + t1k fc-скан 7, +3 уровня входа: -7.14 / -18.06 / 0 dBFS): red(f) = -20*log10(1 - C(f)) - C(f) = depth * tilt(f) * LUT(log10(L0 / res(f))) + C(f) = g * LUT(log10(L0 / res(f))) + w * warp(f)^a res(f) = |2·B/A|(f; fc, Q, gain) case8/m2c (freq-path, близнец 0x180535880) - LUT = PCHIP-узлы (x=log10(L0/res), y=нормированная маска), таблица ниже - depth = 0.8639736175537109 + warp(f)= 0.87·K·x/(K+x), K=exp(2.0723), x=f/2000 (0x5406a8, FUN_180530850) + LUT = PCHIP-узлы B.11 (заморожены), таблица ниже + g=1.221 w=0.358 a=3.143 (фит, bounds [0.9..1.5],[0.2..1.2],[2..6]) L0 = линейный уровень входа (dual 10^(-7.142/20), t1kq 10^(-18.063/20), t1k 1.0) - tilt(f)= 1 - w(f) per-bin level-вес FUN_180530d30 (500/1000/2000: 1.414/1.454/1.795) -СВОЙСТВА (открытие B.11): - - Степенной закон C = D0·(L0/res)^p (B.10, p=0.0847) НЕ описывает высокий уровень - (0 dBFS): наклон d(ln C)/d(dB) падает с уровнем -> LUT компрессивная (насыщается). - - Кривая монотонна, с "коленом" при x≈0.58 (скачок 0.56 -> 0.65) и асимптотами - y->0.44 (низкий уровень) / y->0.67 (высокий уровень). - - Форма резонанса (Q=0.900, gain=4.132, tilt) ОДИНАКОВА для всех уровней; - различие между dual/t1kq/t1k = только положение x = L0/res на LUT-кривой. - - Подтверждает структурную модель level-path: per-bin уровень (0x540678 IIR-трекеры - 0x540528..) -> LUT-кривая param_1+0x188 (FUN_180563440: mn+(mx-mn)·x^(1/gamma)) - -> маска, применяемая с per-bin весами (0x530d30) в FUN_180529fe0. +СВОЙСТВА (открытие B.12, bridge): + - C500-константа dual = res_band(500; fc=500)=0.117 Q-НЕЗАВИСИМ (твин в центре полосы) + -> крас500 const по Q БЕЗ tilt; C500 = g·LUT(0.574)·warp^a(500≈0) = 0.692. + - Глубина dual2000 = АДДИТИВНЫЙ терм w·warp(f)^3.14: на 2000 Гц +0.149, на 1000 <=0.02 + (иначе t1k/t1kq рушатся) — это шаг 5 декомпа `0x5407c8 += веса·res + mask`, НЕ + мультипликация warp·LUT (та проваливается >10 dB). + - xv(2000)=log10(L0/res): 0.308(Q=0.1)..-0.656(Q=10) -> C2000-Q-зависимость совпадает. + - Остаток 0.7 dB (Q=0.1) = тонкая форма LUT-колена 0.574; warp^3.14~pi => подозрение + на кратный каскад (freq-axis 0x540698 / ∏0x540688 / двойной FFT-проход 0x535a70). + - Структура level-path подтверждена: per-bin уровень (0x540678 IIR) -> кривая +0x188 + (FUN_180563440) -> аддитивный аккумулятор 0x5407c8 -> warp -> FFT-conv. """ import numpy as np from scipy.interpolate import PchipInterpolator FS = 44100.0 -DEPTH = 0.8639736175537109 +GAIN_FIT = 4.132 QS = [0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0] DUAL = np.array([(10.220, 15.224), (10.219, 13.963), (10.219, 12.947), @@ -44,20 +46,25 @@ L0_DUAL = 10 ** (-7.142 / 20) L0_T1KQ = 10 ** (-18.063 / 20) L0_T1K = 1.0 -TILT = {500: 1.414, 1000: 1.454, 2000: 1.795} -Q_FIT, GAIN_FIT = 0.900, 4.132 +# bridge-фит (model_fir.py): C = g*LUT(xv) + w*warp^a +G_FIT, W_FIT, A_FIT = 1.221, 0.358, 3.143 -# узлы LUT (x=log10(L0/res), y=норм. маска) — фит по 36 точкам, rmse=0.072 +# узлы LUT (x=log10(L0/res), y=норм. маска) — из B.11, заморожены LUT_KNOTS_X = np.array([-0.750, -0.500, -0.250, 0.000, 0.250, 0.500, 0.574, 0.610, 0.750, 1.000]) LUT_KNOTS_Y = np.array([0.4402, 0.4552, 0.4813, 0.5072, 0.5329, 0.5332, 0.5645, 0.6471, 0.6562, 0.6670]) +_LUT = PchipInterpolator(LUT_KNOTS_X, LUT_KNOTS_Y) def lut(x): - p = PchipInterpolator(LUT_KNOTS_X, LUT_KNOTS_Y) - v = p(np.asarray(x)) + v = _LUT(np.asarray(x)) return np.clip(v, LUT_KNOTS_Y[0], LUT_KNOTS_Y[-1]) +def warp(f): + x = np.asarray(f) / 2000.0 + return 0.87 * 7.942 * x / (7.942 + x) + + def res_at(ft, fc, Q, gain): w0 = fc * 2 * np.pi / FS c, s = np.cos(w0), np.sin(w0) @@ -71,9 +78,9 @@ def res_at(ft, fc, Q, gain): (A[0] + A[1] * z + A[2] * z * z)) -def red(f_tone, fc, Q, gain, L0, tilt): +def red(f_tone, fc, Q, gain, L0, g=G_FIT, w=W_FIT, a=A_FIT): r = res_at(f_tone, fc, Q, gain) - C = DEPTH * tilt * lut(np.log10(L0 / r)) + C = g * lut(np.log10(L0 / r)) + w * warp(f_tone) ** a return -20 * np.log10(max(1 - C, 1e-9)) @@ -82,23 +89,23 @@ def run(): print('--- dual_b1q (tones 500+2000, band fc=500, -7.142 dBFS) ---') for i, q in enumerate(QS): for f, m in ((500.0, DUAL[i, 0]), (2000.0, DUAL[i, 1])): - p = red(f, 500.0, q, GAIN_FIT, L0_DUAL, TILT[f]) + p = red(f, 500.0, q, GAIN_FIT, L0_DUAL) preds.append(p); meas.append(m) print(f'q={q:5.1f} 500 {DUAL[i,0]:7.3f}/{preds[2*i]:7.3f} ' f'2000 {DUAL[i,1]:7.3f}/{preds[2*i+1]:7.3f}') print('--- t1kq fc-скан (-18.06 dBFS, q=0.9999978) ---') for i, fc in enumerate(FCS): - p = red(1000, fc, 0.9999978, GAIN_FIT, L0_T1KQ, TILT[1000]) + p = red(1000, fc, 0.9999978, GAIN_FIT, L0_T1KQ) preds.append(p); meas.append(T1KQ[i]) print(f'fc={fc:5.0f} {T1KQ[i]:6.3f}/{p:6.3f} ({p - T1KQ[i]:+.3f})') print('--- t1k fc-скан (0 dBFS, q=0.9999978) ---') for i, fc in enumerate(FCS): - p = red(1000, fc, 0.9999978, GAIN_FIT, L0_T1K, TILT[1000]) + p = red(1000, fc, 0.9999978, GAIN_FIT, L0_T1K) preds.append(p); meas.append(T1K[i]) print(f'fc={fc:5.0f} {T1K[i]:6.3f}/{p:6.3f} ({p - T1K[i]:+.3f})') preds = np.array(preds); meas = np.array(meas) rmse = np.sqrt(np.mean((preds - meas) ** 2)) - print(f'\nTOTAL rmse={rmse:.4f} dB (n={len(meas)})') + print(f'\nTOTAL rmse={rmse:.4f} dB (n={len(meas)}) [B.11 эмпирик: 0.072; warp-одиночный: >10]') print(f'dual-only rmse={np.sqrt(np.mean((preds[:22]-meas[:22])**2)):.4f}') print(f't1kq rmse={np.sqrt(np.mean((preds[22:29]-meas[22:29])**2)):.4f}') print(f't1k rmse={np.sqrt(np.mean((preds[29:]-meas[29:])**2)):.4f}')