step7: live BandConfig capture infra + FUN_180563a60 decode — GUI-only, audio path refutes premise (22b)

This commit is contained in:
2026-08-22 15:28:01 +03:00
parent c222e054ca
commit 7fac8b2a4f
5 changed files with 304 additions and 5 deletions
+4 -2
View File
@@ -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).
+4
View File
@@ -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/γ → это
+44
View File
@@ -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 <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 сидит там.
+10 -3
View File
@@ -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)
end
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.defer(keepalive)
+242
View File
@@ -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 <rpp> (+ 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 <file.rpp> [--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('<QQ', a, len(d))); out.write(d)
nreg += 1; nbytes += len(d)
a += n
out.close(); os.close(fd)
return nreg, nbytes
def parse_snap(path):
regs = []
data = open(path, 'rb')
while True:
hdr = data.read(16)
if len(hdr) < 16: break
lo, sz = struct.unpack('<QQ', hdr)
body = data.read(sz)
if len(body) < sz: break
regs.append((lo, body))
return regs
def readabs(regs, addr, n):
for lo, body in regs:
if lo <= addr < lo + len(body) and addr - lo + n <= len(body):
return body[addr - lo:addr - lo + n]
return None
import numpy as np
def find_levelpair_buffers(regs):
"""Buffers where f32[2j]==j/1024 exactly for j=0..1023 (level axis)."""
expect = (np.arange(1024, dtype=np.float64) / 1024).astype(np.float32)
anchors = np.nonzero(expect == np.float32(1))[0] # sanity of construction
found = []
for lo, body in regs:
if len(body) < 8192: continue
a = np.frombuffer(body[:len(body) // 4 * 4], dtype='<f4')
# anchor: level[1] == 1/1024 at slot 2
cand = np.nonzero(a == np.float32(1.0 / 1024.0))[0]
for i in cand:
i = int(i)
if i < 2 or i % 2: continue
s = i - 2 # slot of level[0]
if s + 2048 > 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('<Q', buf)
for lo, body in regs:
j = 0
while True:
j = body.find(pat, j)
if j < 0: break
addr = lo + j
base = addr - 0xe0 - k * 0x18
votes[base] += 1
detail.append((k, addr, base))
j += 1
if not votes:
return None, detail
base, cnt = votes.most_common(1)[0]
return (base, cnt) if cnt >= 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('<ff', b, 0)
g, = struct.unpack_from('<f', b, 0xc)
if not (-96.0 <= A <= 96.0): return False
if not (1.0 <= B <= 100000.0): return False
if not (0.05 <= abs(g) <= 8.0): return False
return True
def dump_bandconfigs(regs, base):
def f32(addr):
v = readabs(regs, addr, 4)
return round(struct.unpack('<f', v)[0], 6) if v else None
def u64(addr):
v = readabs(regs, addr, 8)
return struct.unpack('<Q', v)[0] if v else None
out = {}
for off in range(0x80, 0x400, 8):
p = u64(base + off)
if not p or p < 0x10000: continue
if p & 7 or not looks_like_bandconfig(regs, p): continue
out['+0x%x' % off] = {
'ptr': '0x%x' % p,
'A': f32(p), 'B': f32(p + 4),
'f08': f32(p + 8), 'gamma': f32(p + 0xc),
'flag': (readabs(regs, p + 0x10, 1) or b'\xff')[0],
'raw_f32': [f32(p + x) for x in range(0x14, 0x30, 4)],
'cb': ('set' if u64(p + 0x90) else 'none'),
}
masks = {}
for band in range(6):
b = readabs(regs, base + 0x4198 + band * 0x2000, 32)
if b:
masks['band%d' % band] = [round(v, 4) for v in struct.unpack('<4d', b)]
return out, masks
def main():
args = sys.argv[1:]
rpp = args[0]
offline = '--offline' in args
jpath = None
spath = '/tmp/opencode/step7_snap.bin'
if '--json' in args: jpath = args[args.index('--json') + 1]
if '--snap' in args: spath = args[args.index('--snap') + 1]
name = os.path.splitext(os.path.basename(rpp))[0]
if not jpath:
jpath = f'/tmp/opencode/step7_{name}.json'
os.makedirs(os.path.dirname(jpath), exist_ok=True)
subprocess.run('pkill -9 -x reaper 2>/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())