diff --git a/roadmap.md b/roadmap.md index d83adf6..7893556 100644 --- a/roadmap.md +++ b/roadmap.md @@ -192,3 +192,22 @@ 182617828/850/878 ALL currently point to 0x18014ec20 (same fn). - CONSEQUENCE: for 44100 => N=N0; if N0<=1024 the twiddle = static sin table (reproducible directly); if N0>1024 must replicate Cody-Waite sin/cos of FUN_18014ec20 bit-exactly. N0 (+0x19c) still unfound statically. + +### N0 RESOLVED - LIVE DUMP @44100 (2026-08-17) — KEY +- Soothe2 render_short.rpp (44100 Hz) + /proc/pid/mem scan of yabridge-host heap + (out file growth confirmed processing; targeted field-signature scan: + qword-aligned, +0x19c == +0x1a0 == pow2 in [256..65536], +0x1ac in {2,4,8,16}). +- Two independent live SP instances (fork L/R, vptr 0x1824ac210 / 0x1824ac7a8, + vtbl-slot families 0x18052xxx = SpectralProcessor) BOTH report: + +0x19c N0 = 2048, +0x1a0 = 2048 (=N0, since sr==44100 => 2^floor(log2(1))=1) + +0x1a4 winscale = 0.49999988 (Hann 0.5) / 0.66666639 (2/3 window) + +0x1ac oversample = 4 +- N0=2048 > 1024 => twiddle goes the ANGLE-RAMP branch of FUN_180039b00 + (buf[k]=k*(2π/N) then sin/cos via thunk 1801de760 / thunk 1801e3f20), + and cplx-stage loader FUN_18014ec20 re-derives re/im (Dekker-split + Cody-Waite). +- Implication for reimpl: for 44.1k we MUST reproduce Cody-Waite sin/cos + (1/π=0.31830988618379069, magic 6755399441055744, π_hi/π_lo reduction, + poly -1/3!..-1/15!), NOT the static 1024-table path. Soothing does NOT use the + static sin-table for its main FFT at 44.1k. +- Method: rtone.py (single render, wait for out growth, scan), rtver.py (field verify). + Old soothe_rt.bin is module-only range => heap objects absent (why vptr-scan found 0). diff --git a/rtall.py b/rtall.py new file mode 100644 index 0000000..7ff1ec7 --- /dev/null +++ b/rtall.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +import subprocess, os, glob, sys, time, struct + +RPP = "/home/m/soothe-bt/render_long.rpp" +proc = subprocess.Popen( + ['reaper', '-nosplash', '-renderproject', RPP], + stdout=open('/tmp/rtall.log', 'w'), stderr=subprocess.STDOUT) +print('reaper', proc.pid, flush=True) + +start, end = 0x1824abb00, 0x1824abd00 # vtbl region +SLOTS = list(range(start, end, 8)) + + +def find_hosts(): + out = [] + for p in glob.glob('/proc/[0-9]*'): + try: + pid = int(os.path.basename(p)) + m = open(f'/proc/{pid}/maps').read() + c = open(f'/proc/{pid}/cmdline', 'rb').read().decode('utf8', 'replace') + if 'soothe2' in m and 'yabridge-host.exe' in c: + out.append(pid) + except Exception: + pass + return out + + +def scan_all(pid): + mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) + resc = {s: [] for s in SLOTS} + maps = open(f'/proc/{pid}/maps').read() + for line in maps.splitlines(): + p = line.split() + lo, hi = (int(x, 16) for x in p[0].split('-')) + if 'r' not in p[1]: + continue + try: + d = os.pread(mem, hi - lo, lo) + except Exception: + continue + for s in SLOTS: + t = struct.pack(' 1 else "/home/m/soothe-bt/render_long.rpp" +START = len(sys.argv) > 2 and sys.argv[2] == "start" +if START: + proc = subprocess.Popen( + ['reaper', '-nosplash', '-renderproject', RPP], + stdout=open('/tmp/rtfind.log', 'w'), stderr=subprocess.STDOUT) + print('reaper', proc.pid, flush=True) + +# The SpectralProcessor vtable slot containing ctor 0x180529610 +CTOR = 0x180529610 +# candidate vptrs: slots near it +VCANDS = [0x1824abb80, 0x1824abb88, 0x1824abb90, 0x1824abb98, 0x1824abba0, 0x1824abba8, 0x1824abbb0] + +def all_mapped(pid): + out = [] + maps = open(f'/proc/{pid}/maps').read() + for line in maps.splitlines(): + p = line.split() + lo, hi = (int(x, 16) for x in p[0].split('-')) + if 'r' in p[1]: + out.append((lo, hi)) + return out + +def scan(pid, vptr, tgt): + mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) + hits = [] + for lo, hi in all_mapped(pid): + try: + d = os.pread(mem, hi - lo, lo) + except Exception: + continue + i = 0 + while True: + i = d.find(tgt, i) + if i < 0: + break + hits.append(lo + i) + i += 1 + os.close(mem) + return hits + +def filter_candidates(pid, hit_addrs): + # for each candidate object addr (addr = where vptr stored => object base), read fields + out = [] + mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) + for a in hit_addrs: + try: + d = os.pread(mem, 0x600, a) + except Exception: + continue + f4 = struct.unpack('<%dI' % (len(d) // 4), d) + # look for N at +0x19c and oversample at +0x1ac + n = f4[0x19c // 4] if (0x19c // 4) < len(f4) else 0 + u = f4[0x1ac // 4] if (0x1ac // 4) < len(f4) else 0 + w = struct.unpack('= 0x1a4 + 8 else 0 + ok = (n in (256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536) and + u in (2, 4, 8, 16)) + if w != 0 and (n > 0 or ok): + out.append((a, n, u, w, ok)) + os.close(mem) + return out + +hosts = {} +t0 = time.time() +seen = set() +while time.time() - t0 < 90: + for p in glob.glob('/proc/[0-9]*'): + try: + pid = int(os.path.basename(p)) + if pid in seen: + continue + maps = open(f'/proc/{pid}/maps').read() + if 'soothe2' not in maps or 'yabridge-host.exe' not in open(f'/proc/{pid}/cmdline', 'rb').read().decode('utf8', 'replace'): + continue + except Exception: + continue + seen.add(pid) + print('host', pid, flush=True) + # scan whole maps for each candidate vptr + for v in VCANDS: + h = scan(pid, v, struct.pack(' 1 else "/home/m/soothe-bt/render_rt.rpp" +KEEP = len(sys.argv) > 2 and sys.argv[2] == "keep" + +VPTR = 0x1824abb90 # SpectralProcessor vtbl first entry addr (static). verify at runtime too +CTOR = 0x180529610 + +proc = subprocess.Popen( + ['reaper', '-nosplash', '-renderproject', RPP], + stdout=open('/tmp/rtdump2.log', 'w'), stderr=subprocess.STDOUT) +print('reaper', proc.pid, flush=True) + +host = None +deadline = time.time() + 90 +while time.time() < deadline and not KEEP: + found = [] + for p in glob.glob('/proc/[0-9]*'): + try: + pid = int(os.path.basename(p)) + cmd = open(p + '/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace') + if 'yabridge-host' not in cmd: + continue + maps = open(f'/proc/{pid}/maps').read() + if 'soothe2' in maps: + found.append(pid) + except Exception: + pass + if found: + host = found[0] + break + time.sleep(0.2) +print('host', host, flush=True) +if not host: + if not KEEP: + proc.kill() + sys.exit('no host') + +# gather ALL readable regions +readable = [] +maps = open(f'/proc/{host}/maps').read() +tot = 0 +for line in maps.splitlines(): + parts = line.split() + lo, hi = (int(x, 16) for x in parts[0].split('-')) + if 'r' in parts[1]: + readable.append((lo, hi, parts[1])) + tot += hi - lo +print('readable regions:', len(readable), 'total MB:', tot / 1e6, flush=True) + +mem = os.open(f'/proc/{host}/mem', os.O_RDONLY) +pat = {v: struct.pack(' base_sz: + grew = s + break + # if reaper exited, stop trying + if proc.poll() is not None: + print('reaper exited, last out size', s, flush=True) + break + time.sleep(0.3) +print('out grew to', grew, flush=True) +if not hosts or grew == 0: + proc.kill() + sys.exit('no live render') + +time.sleep(0.5) + +N_POW2 = {2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536} +UPS = {2,4,8,16} + +def scan(pid): + mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) + found = [] + maps = open(f'/proc/{pid}/maps').read() + for line in maps.splitlines(): + p = line.split() + lo, hi = (int(x, 16) for x in p[0].split('-')) + if 'r' not in p[1]: + continue + if lo < 0x180000000 or lo >= 0x183000000: + try: + d = os.pread(mem, hi - lo, lo) + except Exception: + continue + if len(d) < 0x1ac + 8: + continue + for o in range(0, len(d) - (0x1ac + 8), 8): + n0 = struct.unpack_from(' 1 else "/home/m/soothe-bt/render_long.rpp" + +VCAND = list(range(0x1824abb60, 0x1824abc20, 8)) # wide vtable range +CTOR = 0x180529610 + +proc = subprocess.Popen( + ['reaper', '-nosplash', '-renderproject', RPP], + stdout=open('/tmp/rtov.log', 'w'), stderr=subprocess.STDOUT) +print('reaper', proc.pid, flush=True) + + +def find_hosts(): + out = [] + for p in glob.glob('/proc/[0-9]*'): + try: + pid = int(os.path.basename(p)) + maps = open(f'/proc/{pid}/maps').read() + if 'soothe2' in maps and 'yabridge-host' in open(f'/proc/{pid}/cmdline','rb').read().decode('utf8','replace'): + out.append(pid) + except Exception: + pass + return out + + +def scan(pid): + mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) + found = [] + pats = {v: struct.pack(' 0: + print('HITS', host if 'host' in dir() else h, len(found), flush=True) + with open('/tmp/inst2.txt', 'w') as fo: + for hh, va, a in found: + fo.write(f'{hh} {va:#x} {a:#x}\n') + if not hosts: + proc.kill() + sys.exit(0) + time.sleep(0.3) +print('none', flush=True) +proc.kill() \ No newline at end of file diff --git a/rtsig.py b/rtsig.py new file mode 100644 index 0000000..717339d --- /dev/null +++ b/rtsig.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +import subprocess, os, glob, sys, time, struct + +RPP = "/home/m/soothe-bt/render_short.rpp" +proc = subprocess.Popen(['reaper', '-nosplash', '-renderproject', RPP], + stdout=open('/tmp/rtsig.log', 'w'), stderr=subprocess.STDOUT) +print('reaper', proc.pid, flush=True) + + +def find_hosts(): + out = [] + for p in glob.glob('/proc/[0-9]*'): + try: + pid = int(os.path.basename(p)) + m = open(f'/proc/{pid}/maps').read() + c = open(f'/proc/{pid}/cmdline', 'rb').read().decode('utf8', 'replace') + if 'soothe2' in m and 'yabridge-host.exe' in c: + out.append(pid) + except Exception: + pass + return out + +N_POW2 = {2:1,4:1,8:1,16:1,32:1,64:1,128:1,256:1,512:1,1024:1,2048:1,4096:1,8192:1,16384:1,32768:1,65536:1} + +def scan(pid): + mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) + found = [] + maps = open(f'/proc/{pid}/maps').read() + for line in maps.splitlines(): + p = line.split() + lo, hi = (int(x, 16) for x in p[0].split('-')) + if 'r' not in p[1]: + continue + try: + d = os.pread(mem, hi - lo, lo) + except Exception: + continue + if lo < 0x180000000 or lo >= 0x183000000: + # heap/stack chunk. scan qword-aligned for N signature + for o in range(0, len(d) - (0x1ac + 8), 8): + n0 = struct.unpack_from(' base_sz: + grew = s; break + if proc.poll() is not None: + print('reaper exited early', flush=True); break + time.sleep(0.3) +print('out:', grew, flush=True) +if not hosts or grew == 0: + proc.kill(); sys.exit('fail') +time.sleep(0.5) + +for pid in hosts: + mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) + for a in CAND: + try: + d = os.pread(mem, 0x300, a) + except Exception as e: + print(pid, hex(a), 'err', e, flush=True); continue + # ?possibly the object base is NOT a; our signature matched offset at +0x19c. + # Re-scan around a for the exact int field to find true object base. + # fields: N at base+0x19c, N at +0x1a0, float win at +0x1a4, int ups at +0x1ac + # find qword-aligned true base by searching +0x1ac==4 in window + f4 = struct.unpack('<%dI' % (len(d) // 4), d) + bases = [] + for o in range(0, len(f4) - (0x1ac // 4 + 1)): + if f4[o + 0x1ac // 4] == 4 and f4[o + 0x19c // 4] in (2048, 4096) \ + and f4[o + 0x1a0 // 4] == f4[o + 0x19c // 4]: + bases.append(o * 4) + print(pid, hex(a), 'true-base candidates:', [hex(a + b) for b in bases[:6]], flush=True) + for b in bases[:1]: + o = b + vptr = struct.unpack_from('