diff --git a/AGENTS.md b/AGENTS.md index e0001ec..865ec10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,8 +107,10 @@ scale → LUT level-domain (t^γ·MULT, γ=0.344 decomp / MULT=4.2 placeholder) 2. `combine/аккумулятор 0x5407c8`: семантика декодирована (21b), но acc/f6f8 НЕ имеют однополосного консюмера — искать точку потребления (межполосный каскад). ВНИМАНИЕ: `FUN_1805316e0` = writer коэффициентов, НЕ комбинер масок. -3. BandConfig `ctx+0x188` A/B/γ — противоречие между сессиями (−24/28/1 vs −13.78/68.29/0.344); - разрешить при захвате. +3. ~~BandConfig `ctx+0x188` A/B/γ — противоречие между сессиями~~ **РАЗРЕШЕНО + (22b): live = −24/28/1 у ВСЕХ конфигов, но весь кластер FUN_180563440/563a60 — + GUI-timer only; аудио-путь (FUN_180529fe0) BandConfig не читает. Шаг 7 в исходной + постановке опровергнут; LUT-константы структурной цепи помечены EMPIRICAL.** 4. PRNG-пролог (LCG+LUT → fVar30) — залочен (fVar30=1 при live state 112), dry/wet rnd импорт. 5. Бит-экзактный exp2 (0x26b820); FFT-conv понижен до P3 (окно near-flat, NOTES:18c); SR-геометрия 48k/4096 сделана (render48k). diff --git a/BITEXACT_PLAN.md b/BITEXACT_PLAN.md index 021a45b..2ae7c42 100644 --- a/BITEXACT_PLAN.md +++ b/BITEXACT_PLAN.md @@ -145,6 +145,10 @@ t1k/al НЕ закрыты самим по себе — см. §0 и Шаг 7. BLK-блока даёт спад am в ~последних 0.06s (косметика, на метрику почти не влияет). ### Шаг 7 — BandConfig A/B/γ (level-path ctx+0x188) live-захват под конкретные конфиги +> **СТАТУС 2026-08-22: ВЫПОЛНЕН → ПРЕМиса ОПРОВЕРГНУТА (NOTES_LEVEL 22b).** Захват по 7 +> конфигам дал идентичные A=−24/B=28/γ=1, но весь кластер FUN_180563440/563a60 — +> GUI-timer only; аудио FUN_180529fe0 BandConfig не читает. Насыщение кривой редукции +> искать в теле аудио-функции (см. NOTES_LEVEL 22b, выводы). Структурная LUT-кривая `FUN_180563a60` (A/B/γ). Снято для render_long (A=−24/B=28/γ=1) и t1kq (то же), но для остальных тестов не захвачено. Метод автоматизирован (NOTES_CAPTURE.md). Захватить для t1k_b1f / al / dual-конфигов → реальные A/B/γ → это diff --git a/handoff/NOTES_LEVEL.md b/handoff/NOTES_LEVEL.md index 8957035..c49b517 100644 --- a/handoff/NOTES_LEVEL.md +++ b/handoff/NOTES_LEVEL.md @@ -1391,3 +1391,47 @@ NEXT (приоритеты без изменений): возможная контент-зависимая ветка. Требует 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 ` — 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/ vs /proc//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((dB−A)/(B−A)); + γ==1 → t; flag==0 → t^γ; flag!=0 → 0.5·(1+sign(2t−1)·|2t−1|^γ). + +Callgraph: весь кластер 5631c0/563260 → 563440 → {56e3e0, 563a60} достижим ТОЛЬКО по +DATA-xrefs (vtables 1826a03cc..420, 1824b14f0..510) — GUI-timer heartbeat. АУДИО-метод +FUN_180529fe0 (Soothe2Module::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 сидит там. diff --git a/play.lua b/play.lua index 74cf8d6..e11bd00 100644 --- a/play.lua +++ b/play.lua @@ -1,10 +1,17 @@ -- play.lua : realtime transport playback to keep the DSP host's audio callback -- alive, so an external script can snapshot memory while ctx fields are live. +-- NOTE: reaper.Sleep does not exist in the REAPER API; use reaper.defer loop. local keep_secs = 300 reaper.Main_OnCommand(1007, 0) -- Transport: Play (realtime audio) local t0 = reaper.time_precise() -while reaper.time_precise() - t0 < keep_secs do - reaper.Sleep(200) + +local function keepalive() + if reaper.time_precise() - t0 < keep_secs then + reaper.defer(keepalive) -- yield; runs on every UI update tick + else + reaper.OnStopButton() + end end -reaper.OnStopButton() + +reaper.defer(keepalive) diff --git a/scripts/step7_capture.py b/scripts/step7_capture.py new file mode 100644 index 0000000..3c5b976 --- /dev/null +++ b/scripts/step7_capture.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""step7_capture.py — Step 7: live capture of level-path BandConfig A/B/gamma per RPP. + +Method (NOTES_CAPTURE 2026-08-20c): + 1. spawn reaper with (+ play.lua realtime transport by default), + 2. wait for yabridge-host (soothe2 mapped, not reaper), sleep for init, + 3. chunked full-heap snapshot (8MB pread chunks; large regions EIO otherwise), + 4. fingerprint scan: per-band level_gain pair buffers = 0x400 [level,gain] + f32 pairs with level[j] == j/1024 EXACTLY (j<1024 -> exact in fp32), + 5. u64 refs to those buffers land at base+0xe0+band*0x18 (stride 0x18) + -> majority-vote the level-path object base, + 6. decode every qword ptr at base+0x170..0x1a8 as BandConfig: + A@+0x0 B@+0x4 gamma@+0xc flag@+0x10 shaper@+0x18.. callback@+0x90, + plus first mask doubles at base+0x4198+band*0x2000. + +Usage: + python3 scripts/step7_capture.py [--offline] [--json PATH] [--snap PATH] + +Default mode is realtime playback (play.lua) so short projects keep the DSP +host alive during capture. --offline uses -renderproject instead. +""" +import subprocess, os, glob, time, struct, sys, json, collections + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PLAY_LUA = os.path.join(REPO, 'play.lua') + + +def find_host(): + for p in glob.glob('/proc/[0-9]*'): + pid = int(os.path.basename(p)) + try: + cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace') + maps = open(f'/proc/{pid}/maps').read() + except Exception: + continue + if 'soothe2' in maps and 'reaper' not in cmd: + return pid + return None + + +def snapshot(host, path): + fd = os.open(f'/proc/{host}/mem', os.O_RDONLY) + out = open(path, 'wb') + nreg = 0; nbytes = 0 + for line in open(f'/proc/{host}/maps').read().splitlines(): + p = line.split() + if len(p) < 2: continue + lo, hi = (int(x, 16) for x in p[0].split('-')) + if 'r' not in p[1]: continue + a = lo + while a < hi: + n = min(hi - a, 8 * 1024 * 1024) + try: + d = os.pread(fd, n, a) + except Exception: + a += n; continue + if not d: + a += n; continue + out.write(struct.pack(' len(a): continue + if np.array_equal(a[s:s + 2048:2], expect): + found.append(lo + 4 * s) + found = sorted(set(found)) + dedup = [] + for h in found: + if not dedup or h - dedup[-1] > 0x100: + dedup.append(h) + return dedup + + +def find_object_base(regs, bufs): + """u64 refs to band buffers sit at base+0xe0+band*0x18.""" + votes = collections.Counter() + detail = [] + for k, buf in enumerate(bufs): + pat = struct.pack('= 2 else (None, detail) + + +def looks_like_bandconfig(regs, p): + b = readabs(regs, p, 0x98) + if not b: return False + A, B = struct.unpack_from('/dev/null; pkill -9 -f "[y]abridge" 2>/dev/null; sleep 1', shell=True) + if offline: + cmd = ['/usr/bin/reaper', '-nosplash', '-ignoreerrors', '-renderproject', rpp] + else: + cmd = ['/usr/bin/reaper', '-nosplash', '-ignoreerrors', rpp, PLAY_LUA] + t0 = time.time() + proc = subprocess.Popen(cmd, stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT) + host = None + while time.time() - t0 < 60 and not host: + host = find_host(); time.sleep(0.2) + if not host: + print('NO HOST'); return 1 + print('host %d at %.1fs' % (host, time.time() - t0)) + time.sleep(8) + for attempt in range(3): + try: + nreg, nbytes = snapshot(host, spath) + except Exception as e: + print('snapshot failed:', e); break + print('snapshot #%d: %d regs %.1f MB' % (attempt, nreg, nbytes / 1e6)) + if nbytes > 50e6: break + time.sleep(3) + if proc.poll() is None: proc.kill() + + regs = parse_snap(spath) + bufs = find_levelpair_buffers(regs) + print('level-pair buffers:', ['0x%x' % b for b in bufs]) + res = {'rpp': rpp, 'buffers': ['0x%x' % b for b in bufs]} + if not bufs: + json.dump(res, open(jpath, 'w'), indent=1); print('saved', jpath); return 2 + # group into bands: cluster addrs with stride ~0x2000 + groups = [[bufs[0]]] + for b in bufs[1:]: + if b - groups[-1][-1] <= 0x3000: groups[-1].append(b) + else: groups.append([b]) + best = None + for g in groups: + base, cnt = find_object_base(regs, g) + print('group n=%d -> base %s (votes=%s)' % (len(g), ('0x%x' % base) if base else None, cnt if isinstance(cnt, int) else '-')) + if base and (best is None or cnt > best[1]): + best = (base, cnt) + if not best: + json.dump(res, open(jpath, 'w'), indent=1); print('saved', jpath); return 3 + base = best[0] + cfgs, masks = dump_bandconfigs(regs, base) + res.update(levelpath_base='0x%x' % base, configs=cfgs, masks_head=masks) + json.dump(res, open(jpath, 'w'), indent=1) + print(json.dumps(cfgs, indent=1)) + print('masks head:', json.dumps(masks)) + print('saved', jpath) + return 0 + + +if __name__ == '__main__': + sys.exit(main())