diff --git a/handoff/NOTES_LEVEL.md b/handoff/NOTES_LEVEL.md index ef98143..33bc3fc 100644 --- a/handoff/NOTES_LEVEL.md +++ b/handoff/NOTES_LEVEL.md @@ -2058,3 +2058,58 @@ abs-сумма по корпусу при (7.6,1.694): 115.4 дБ·кейсов, (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 по nbin−2..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..n−1] (массив @0x540688 = соседний + элемент того же массива векторов!): поэлементные thunks (float 0x20f0/double + 0x1850 пары = шаблонный transform); нормировка делением на int [rsp+0x148]; + затем второй проход: band[i] *= (xmm11−xmm7), 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·K1−K2)·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). diff --git a/scripts/lawfit22r.py b/scripts/lawfit22r.py index 56c5db8..27c0f41 100644 --- a/scripts/lawfit22r.py +++ b/scripts/lawfit22r.py @@ -156,12 +156,29 @@ class CaseInput: return lvl.shape[0] == self.nf -def masks_from_lvl(lvl, nb, A, S, Q=0.0, res=None, rp=0.0): +def _bidir_iir(lo, c): + """FUN_18052d650 semantics approx: bidirectional single-pole IIR along bins, + reset->forward->backward with persistent state, applied twice (22u). + Normalised form acc = c*acc + (1-c)*x (unity DC gain); vectorised over + leading axes via lfilter (endpoints differ slightly from C++ loop).""" + if c <= 0: + return lo + from scipy.signal import lfilter + b, a = [1.0 - c], [1.0, -c] + y = np.asarray(lo, dtype=np.float64) + for _ in range(2): + y = lfilter(b, a, y, axis=-1) + y = lfilter(b, a, y[..., ::-1], axis=-1)[..., ::-1] + return y + + +def masks_from_lvl(lvl, nb, A, S, Q=0.0, res=None, rp=0.0, iir_c=0.0): """Per-frame lower-half mask exactly as bare chain + LAWAFFINE produces. Law families (22t): scalar cut=A+S*log2(lvl); quad adds curvature Q*x^2; resrp multiplies per-band mask by res^rp (decomp warp-cascade factor, applied BEFORE cross-band min like the C++ warp section). + iir_c>0: bidirectional bin-IIR smoothing per band (22u, FUN_18052d650). """ nf = lvl.shape[0] lv64 = lvl.astype(np.float64) @@ -172,6 +189,8 @@ def masks_from_lvl(lvl, nb, A, S, Q=0.0, res=None, rp=0.0): mm[low] = np.exp2(-lv64[low]) if res is not None and rp: mm = mm * res[None].astype(np.float64) ** rp + if iir_c > 0: + mm = _bidir_iir(mm, iir_c) if nb > 1: lo = np.min(mm, axis=1) # min across bands else: @@ -182,10 +201,10 @@ def masks_from_lvl(lvl, nb, A, S, Q=0.0, res=None, rp=0.0): return full -def replay(ci, lvl, nb, A, S, Q=0.0, res=None, rp=0.0): +def replay(ci, lvl, nb, A, S, Q=0.0, res=None, rp=0.0, iir_c=0.0): """Return trimmed 44.1k output for law params on prepared CaseInput ci.""" assert ci.frames_ok(lvl), f'traj {lvl.shape[0]} != frames {ci.nf}' - full = masks_from_lvl(lvl, nb, A, S, Q, res, rp) + full = masks_from_lvl(lvl, nb, A, S, Q, res, rp, iir_c) segs = ci.xext[ci.offs[:, None] + np.arange(NFFT)[None, :]] * _WIN[None, :] yspec = np.fft.fft(segs, axis=1) * full td = np.real(np.fft.ifft(yspec, axis=1)) @@ -232,15 +251,15 @@ def build_eval_index(trajs): return idx -def sim_errors(trajs, idx, A, S, Q=0.0, rp=0.0): +def sim_errors(trajs, idx, A, S, Q=0.0, rp=0.0, iir_c=0.0): errs = {} ycache = {} for e in idx: - ck = (e['key'], e['nb'], round(Q, 6), round(rp, 6)) + ck = (e['key'], e['nb'], round(Q, 6), round(rp, 6), round(iir_c, 6)) if ck not in ycache: ci = case_input(e['key'], e['inp']) ycache[ck] = replay(ci, trajs[e['key']], e['nb'], A, S, - Q, trajs.get(e['key'] + '|res'), rp) + Q, trajs.get(e['key'] + '|res'), rp, iir_c) y44 = ycache[ck] errs[e['name']] = corpus.db(corpus.ta(y44, e['f'])) - \ corpus.db(corpus.ta(corpus.load_mono(e['ref']), e['f']))