diff --git a/handoff/NOTES_CAPTURE.md b/handoff/NOTES_CAPTURE.md index 09dc933..a4bd1db 100644 --- a/handoff/NOTES_CAPTURE.md +++ b/handoff/NOTES_CAPTURE.md @@ -157,3 +157,40 @@ the ctx object only materializes during a realtime transport play. during band processing; still TBD (but LUT curve params A/B/γ are RPP-derived per roadmap). - Two more ctx-like bases found (0x2120040, 0x1780040) also have +0x24==48000; 0x2370040 is the populated one (0x2120040 has 0x540658..698 = 1.0 fill pattern — likely a second/free instance). + +## 2026-08-23 (22y): LIVE CTX CAPTURE DURING DUAL/T1KQ/RES PLAYBACK — scripts/dualtrace.py +Method works reproducibly: reaper .rpp play_loop.lua (repeat ON) → yabridge-host +→ chunked snapshot ×2 → ctx marker +0x24==48000 ∧ sens>100 → ONLY ONE populated base +(0x2370040; others empty instances). Snapshots byte-stable over seconds. + +### Pointer-table catalog (ctx+0x540600..0x540a00, dereferenced u64 → f32[2049]) +- Static weights confirmed live: [00]identity@0x540688(ones), window@0x540698(0.5→0.8), + WA/WB/WC/WD @0x5406b8/c8/d8/e8, warp@0x540748(1.3→6.68, structure at LOW bins), + freqaxis@0x540758(v85=995.6Hz ✓ internal 48k/4096). +- **acc/f6f8 arrays ALL ZERO during steady looped playback** (0x5406f8, + 0x5407a8/b8/c8/d8 — zero as f32 AND f64): combine accumulators idle in steady state. +- **CONFIG-DEPENDENT CURVE FAMILY** (peak follows band fc: bin43@fc500 → bin85@fc1000): + - 0x540768 == 0x540778 (identical twins): smooth curve, peak at center + (dual: 4.15@43, valley 1.60@171, upturn 1.86@400; t1kq: 3.55@85). + - 0x540788: sharper version (floor ~0.52-1.0, max 4.38). + - **0x5407f8: min EXACTLY 1.0 → reduction multiplier R(f)=1/mask ≥ 1** + (res500 cfg: R(500Hz)=12.0 dB, falls to ~0 by 6 kHz; notch-shaped ✓). +- bands[] slots from static asm (@0x540678+i·16) read as identity/ones tables LIVE — + the per-band working data is NOT sitting in those ctx fields during playback. + +### Decisive mismatch +For dual cfg: R(43)=3.98→12.0 dB (real 10.32 ok-ish) BUT R(171)=1.41→3.0 dB while +real cut@2000 = 11.82 dB. ⇒ Applied filter ≠ pointwise copy of R: massive spectral +coupling between template and actual filtering. Prime suspect: FFT-conv stage with +the 8193-wide WIN_freq window ([01]) — smearing/spreading step completely absent in +our pointwise render48k path. This ALSO explains why faithful v1 (pointwise) cannot +balance dual tones regardless of law constants. + +### Caveats / next +- Quick Welch TF estimate unreliable (window/alignment) — Goertzel-at-tones stays canon; + for full-spectrum truth use chirp/two-tone refs or per-fc capture sweep. +- NEXT: (1) fc-scan captures (res_only1_{fc}.rpp, 11×) → correlate R_cap(bin85) with + real cut@1000 across fc — validates R as THE applied curve; (2) decode the FFT-conv + 0x535a70 body + WIN_freq usage — reconstruct mask→FIR spreading; (3) re-check whether + 0x540768-family updates frame-by-frame (two-point diff showed stable — maybe only + rebuilt on param change / note onset). diff --git a/play_loop.lua b/play_loop.lua new file mode 100644 index 0000000..594cee3 --- /dev/null +++ b/play_loop.lua @@ -0,0 +1,17 @@ +-- play_loop.lua : realtime playback with repeat ON, held alive for keep_secs. +local keep_secs = 240 + +if reaper.GetSetRepeat(0) == 0 then + reaper.Main_OnCommand(1068, 0) -- Transport: Repeat ON +end +reaper.Main_OnCommand(1007, 0) -- Transport: Play + +local t0 = reaper.time_precise() +local function keepalive() + if reaper.time_precise() - t0 < keep_secs then + reaper.defer(keepalive) + else + reaper.OnStopButton() + end +end +reaper.defer(keepalive) diff --git a/scripts/dualtrace.py b/scripts/dualtrace.py new file mode 100644 index 0000000..d86dead --- /dev/null +++ b/scripts/dualtrace.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""dualtrace.py — live capture of the DSP ctx during realtime playback of +dual_b1q_0.5.rpp (two tones 500+2000 Hz, one band @500 q=0.5). + +Method (NOTES_CAPTURE.md 2026-08-20c): reaper + play_loop.lua keeps the audio +callback alive; chunked /proc/pid/mem snapshot of the yabridge host; find ctx +by marker +0x24 == 48000.0f; dump scalars + pointer table + all ~2049-float +arrays, flag those with energy at tone bins 43/171. + +Usage: python3 scripts/dualtrace.py [rpp] +""" +import json +import os +import struct +import subprocess +import sys +import time + +import numpy as np + +SNAP1 = '/tmp/opencode/dualtrace_s1.bin' +SNAP2 = '/tmp/opencode/dualtrace_s2.bin' + + +def find_host(): + import glob + 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 = nbytes = 0 + for line in open(f'/proc/{host}/maps').read().splitlines(): + p = line.split() + if len(p) < 2 or 'r' not in p[1]: + continue + lo, hi = (int(x, 16) for x in p[0].split('-')) + 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 d: + out.write(struct.pack(' 100: # sens scalar ~441 + cands.append(base) + j += 1 + return sorted(set(cands)) + + +def dump_ctx(regs, ctx, tag, store): + """Scalars + pointer table 0x540600..0x540900 + dereferenced arrays.""" + def f32(addr): + b = readabs(regs, addr, 4) + return struct.unpack(' 0x7fffffffffff: + continue + body = readabs(regs, ptr, 2049 * 4 + 64) + if body is None or len(body) < 2049 * 4: + continue + arr = np.frombuffer(body[:2049 * 4], dtype=' 1e6: + continue + r43 = arr[43] / m + r171 = arr[171] / m + if r43 > 3 or r171 > 3 or (arr.max() / max(m, 1e-30)) > 5: + flagged[off] = dict(ratio43=r43, ratio171=r171, + vmin=float(arr.min()), vmax=float(arr.max())) + store[f'{tag}_arr_{hex(off)}'] = arr + store[f'{tag}_ptrinfo'] = ptrinfo + store[f'{tag}_flagged'] = {hex(k): v for k, v in flagged.items()} + return flagged + + +def main(): + rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp' + os.makedirs('/tmp/opencode', exist_ok=True) + subprocess.run('pkill -9 -x reaser 2>/dev/null; pkill -9 -x reaper 2>/dev/null; ' + "pkill -9 -f '[y]abridge' 2>/dev/null; sleep 1", shell=True) + proc = subprocess.Popen( + ['/usr/bin/reaper', '-nosplash', '-ignoreerrors', rpp, + '/home/m/re-tools/play_loop.lua'], + stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT) + t0 = time.time() + 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', host, 'at %.1fs' % (time.time() - t0), flush=True) + time.sleep(12) # init + several looped passes -> steady state + + store = {} + try: + n1, b1 = snapshot(host, SNAP1) + print(f'snap1: {n1} regs {b1/1e6:.0f}MB', flush=True) + time.sleep(6) + n2, b2 = snapshot(host, SNAP2) + print(f'snap2: {n2} regs {b2/1e6:.0f}MB', flush=True) + + for tag, path in (('s1', SNAP1), ('s2', SNAP2)): + regs = parse_snap(path) + cands = find_ctx(regs) + print(tag, 'ctx candidates:', [('0x%x' % c) for c in cands][:5], flush=True) + if cands: + fl = dump_ctx(regs, cands[0], tag, store) + for k, v in list(fl.items())[:10]: + print(' ', k, {kk: round(vv, 2) if isinstance(vv, float) else vv + for kk, vv in v.items()}, flush=True) + finally: + if proc.poll() is None: + proc.kill() + + np.savez_compressed('/tmp/opencode/dualtrace.npz', + **{k: v for k, v in store.items() + if isinstance(v, np.ndarray)}) + json.dump({k: v for k, v in store.items() if not isinstance(v, np.ndarray)}, + open('/tmp/opencode/dualtrace.json', 'w'), indent=1) + print('\nsaved /tmp/opencode/dualtrace.npz|.json') + return 0 + + +if __name__ == '__main__': + sys.exit(main())