146 lines
5.4 KiB
Python
146 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
||
"""rtcapture.py — runtime-захват soothe2 из yabridge-host во время offline-рендера.
|
||
|
||
Запускает reaper -renderproject <rpp>, находит yabridge-host.exe.so с soothe2_x64.vst3,
|
||
вычисляет runtime-базу модуля (по MZ/PE) и непрерывно сэмплирует целевую RAM плагина
|
||
(RVA = VA - 0x180000000). Печатает последние стабильные значения по окончании рендера.
|
||
|
||
Цели (RVA от базы модуля):
|
||
0x540658 окно/FIR (размер окна из plan) [искомое 0.540658]
|
||
0x540678 per-bin уровень-трекеры
|
||
0x5407c8 аккумулятор маски (C)
|
||
0x540698 freq-axis значение
|
||
0x540534 N/2 (FFT size)
|
||
0x5408b0 узлы кривой 0x188 (20 doubles)
|
||
0x540888 0x540880 ctor-константы
|
||
0x540530..0x5405a0 план-таблицы FFT
|
||
"""
|
||
import os, sys, glob, time, struct, subprocess
|
||
|
||
RPP = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/render_long.rpp'
|
||
BASE = 0x180000000
|
||
TARGETS = {
|
||
'window_0x540658': (0x540658, 0x1000),
|
||
'levels_0x540678': (0x540678, 0x400),
|
||
'acc_0x5407c8': (0x5407c8, 0x40),
|
||
'freqaxis_0x540698': (0x540698, 0x20),
|
||
'n2_0x540534': (0x540534, 0x20),
|
||
'curve_0x5408b0': (0x5408b0, 0x200),
|
||
'ctor_0x540870': (0x540870, 0x60),
|
||
'plan_0x540530': (0x540530, 0x80),
|
||
}
|
||
|
||
|
||
def find_base(pid):
|
||
maps = open(f'/proc/{pid}/maps').read()
|
||
mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
|
||
seen = set()
|
||
for line in maps.splitlines():
|
||
p = line.split()
|
||
lo = int(p[0].split('-')[0], 16)
|
||
if lo in seen:
|
||
continue
|
||
seen.add(lo)
|
||
try:
|
||
hdr = os.pread(mem, 0x1000, lo)
|
||
except Exception:
|
||
continue
|
||
if hdr[:2] != b'MZ':
|
||
continue
|
||
try:
|
||
x = struct.unpack('<I', hdr[0x3c:0x40])[0]
|
||
if hdr[x:x + 4] != b'PE\x00\x00':
|
||
continue
|
||
magic = struct.unpack('<H', hdr[x + 24:x + 26])[0]
|
||
if magic == 0x20b:
|
||
ib = struct.unpack('<Q', hdr[x + 24 + 24:x + 24 + 32])[0] # PE32+ ImageBase
|
||
elif magic == 0x10b:
|
||
ib = struct.unpack('<I', hdr[x + 24 + 28:x + 24 + 32])[0]
|
||
else:
|
||
continue
|
||
return lo, ib
|
||
except Exception:
|
||
continue
|
||
os.close(mem)
|
||
return None, 0
|
||
|
||
|
||
def read(pid, addr, n):
|
||
mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
|
||
try:
|
||
return os.pread(mem, n, addr)
|
||
except Exception:
|
||
return None
|
||
finally:
|
||
os.close(mem)
|
||
|
||
|
||
def main():
|
||
proc = subprocess.Popen(['reaper', '-nosplash', '-renderproject', RPP],
|
||
stdout=open('/tmp/rtcap.log', 'w'), stderr=subprocess.STDOUT)
|
||
host = None
|
||
t0 = time.time()
|
||
while time.time() - t0 < 120:
|
||
for pid in glob.glob('/proc/[0-9]*'):
|
||
pid = int(os.path.basename(pid))
|
||
try:
|
||
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().decode('utf8', 'replace')
|
||
maps = open(f'/proc/{pid}/maps').read()
|
||
except Exception:
|
||
continue
|
||
if 'yabridge-host' in cmd and ('soothe2' in maps or 'vst3' in maps):
|
||
host = pid
|
||
break
|
||
if host:
|
||
break
|
||
time.sleep(0.2)
|
||
if not host:
|
||
print('HOST NOT FOUND')
|
||
proc.kill()
|
||
return
|
||
base, ib = find_base(host)
|
||
print(f'host pid={host} module_base={hex(base)} PE_ImageBase={hex(ib)} delta={hex(base - ib)}', flush=True)
|
||
if not base:
|
||
proc.kill()
|
||
return
|
||
# sample until reaper exits
|
||
last = {}
|
||
ts = time.time()
|
||
while proc.poll() is None and time.time() - ts < 150:
|
||
for name, (rva, n) in TARGETS.items():
|
||
d = read(host, base + rva, n)
|
||
if d is not None:
|
||
last[name] = d
|
||
time.sleep(0.05)
|
||
proc.wait(timeout=10)
|
||
print('--- captured (render done) ---')
|
||
for name, (rva, n) in TARGETS.items():
|
||
d = last.get(name)
|
||
if not d:
|
||
print(f'{name:20s} MISSING')
|
||
continue
|
||
vals = struct.unpack('<%dI' % (min(n, len(d)) // 4), d[:min(n, len(d))])
|
||
if name == 'window_0x540658':
|
||
fl = struct.unpack('<%df' % (len(d) // 4), d[:len(d) // 4 * 4])
|
||
nz = [(i, round(v, 6)) for i, v in enumerate(fl) if abs(v) > 1e-9]
|
||
print(f'{name:20s} nonzero={len(nz)} first20={nz[:20]}')
|
||
elif name == 'acc_0x5407c8':
|
||
print(f'{name:20s} ints={[hex(v) for v in vals[:8]]}')
|
||
elif name == 'curve_0x5408b0':
|
||
dl = struct.unpack('<%dd' % (len(d) // 8), d[:len(d) // 8 * 8])
|
||
print(f'{name:20s} doubles={[round(v, 6) for v in dl[:20]]}')
|
||
elif name == 'levels_0x540678':
|
||
fl = struct.unpack('<%df' % (len(d) // 4), d[:len(d) // 4 * 4])
|
||
print(f'{name:20s} floats={[round(v, 5) for v in fl[:40]]}')
|
||
elif name in ('ctor_0x540870',):
|
||
fl = struct.unpack('<%df' % (len(d) // 4), d[:len(d) // 4 * 4])
|
||
print(f'{name:20s} floats={[round(v, 5) for v in fl[:16]]}')
|
||
elif name in ('plan_0x540530',):
|
||
print(f'{name:20s} ints={[hex(v) for v in vals[:24]]}')
|
||
else:
|
||
print(f'{name:20s} ints={[hex(v) for v in vals[:8]]}')
|
||
print('done')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main() |