chore: add rt-capture artifacts (rtobj/rtsnap scripts, rwin_*.npy tables, dsp grep dumps, rpp param decoders)
This commit is contained in:
+206
@@ -0,0 +1,206 @@
|
|||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.app.decompiler.DecompInterface;
|
||||||
|
import ghidra.app.decompiler.DecompileResults;
|
||||||
|
import ghidra.program.model.listing.Function;
|
||||||
|
import ghidra.program.model.listing.FunctionManager;
|
||||||
|
import ghidra.program.model.listing.Instruction;
|
||||||
|
import ghidra.program.model.address.Address;
|
||||||
|
import ghidra.program.model.address.AddressSpace;
|
||||||
|
import ghidra.program.model.symbol.Reference;
|
||||||
|
import ghidra.program.model.mem.Memory;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DumpFuns2 - unfrozen BFS decompiler.
|
||||||
|
* Differences vs DumpFuns.java:
|
||||||
|
* 1. NO haveDec gate: always decompiles into decomp_funs2.txt.
|
||||||
|
* 2. Extended seeds: explicit critical DSP addresses + reverse callers
|
||||||
|
* (references-to) so indirect/vtable-dispatched functions enter closure.
|
||||||
|
* 3. Skip threshold lowered from 30 to 8 instructions (keeps dispatchers/stubs).
|
||||||
|
* Also writes fun_map2.txt (fresh) and re-scans .data float constants.
|
||||||
|
*/
|
||||||
|
public class DumpFuns2 extends GhidraScript {
|
||||||
|
|
||||||
|
final long LO = 0x180000000L;
|
||||||
|
final long HI = 0x182a00000L;
|
||||||
|
|
||||||
|
long parseAddr(String s) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(s.replaceAll("[^0-9a-fA-F]", ""), 16);
|
||||||
|
} catch (Exception e) { return -1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean inRange(long a) { return a >= LO && a <= HI; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() throws Exception {
|
||||||
|
FunctionManager fm = currentProgram.getFunctionManager();
|
||||||
|
AddressSpace as = currentProgram.getAddressFactory().getDefaultAddressSpace();
|
||||||
|
|
||||||
|
// --- seeds: all FUN_ from decomp_dsp.txt + explicit critical DSP addrs ---
|
||||||
|
Set<Long> seed = new LinkedHashSet<>();
|
||||||
|
BufferedReader br = new BufferedReader(new FileReader("/home/m/re-tools/decomp_dsp.txt"));
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
int i = line.indexOf("FUN_");
|
||||||
|
while (i >= 0) {
|
||||||
|
int e = i + 4;
|
||||||
|
while (e < line.length() && "0123456789abcdefABCDEF".indexOf(line.charAt(e)) >= 0) e++;
|
||||||
|
long a = parseAddr(line.substring(i + 4, e));
|
||||||
|
if (inRange(a)) seed.add(a);
|
||||||
|
i = line.indexOf("FUN_", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
br.close();
|
||||||
|
|
||||||
|
long[] critical = {
|
||||||
|
0x180529fe0L, // mask/coefficient setup (vtable-only, 2051 instr)
|
||||||
|
0x180563440L, // LUT curve + gamma + combine
|
||||||
|
0x180563ce0L, // IIR level-tracker INIT
|
||||||
|
0x18056e3e0L, // twin-mask factory
|
||||||
|
0x18052f500L, // interleave
|
||||||
|
0x18052b550L, // FFT-conv loop
|
||||||
|
0x18052ee70L, // per-bin gain
|
||||||
|
0x18052d650L, // band setup
|
||||||
|
0x18052d920L, // window
|
||||||
|
0x18052e190L, // buffer alloc
|
||||||
|
0x18052dc30L, // FFT plan builder
|
||||||
|
0x18052da00L, // ramp fill
|
||||||
|
0x18052dbc0L, // cplx-interleave
|
||||||
|
0x180535880L, // twin kernel
|
||||||
|
0x180536300L, // twin caller
|
||||||
|
0x180536f90L, // dead sibling
|
||||||
|
0x180530d30L, // level-weight formula
|
||||||
|
0x1805316e0L, // coeff writer (17 case)
|
||||||
|
0x180563fa0L, // IIR level-tracker UPDATE (caller chain 5631c0)
|
||||||
|
0x1805631c0L, // LUT+IIR-init+UPDATE dispatcher
|
||||||
|
0x180563260L, // param dispatcher
|
||||||
|
0x180563a60L // combine (called from 563440)
|
||||||
|
};
|
||||||
|
for (long a : critical) seed.add(a);
|
||||||
|
|
||||||
|
// --- vtable slots ---
|
||||||
|
long[] vts = {
|
||||||
|
0x1824abb90L, 0x1824ac7a8L, 0x1824ab7c0L, 0x1824ac638L, 0x1824ac5d8L,
|
||||||
|
0x1824be810L, 0x1824be228L, 0x1824be0a8L, 0x1824be7a0L, 0x1824ac210L,
|
||||||
|
0x1824ac248L, 0x1824b1ac8L, 0x1824b11c8L, 0x1824b0fd0L, 0x1824b1178L
|
||||||
|
};
|
||||||
|
for (long base : vts) {
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
try {
|
||||||
|
Address va = as.getAddress(base + i * 8L);
|
||||||
|
long tgt = currentProgram.getMemory().getLong(va);
|
||||||
|
if (inRange(tgt)) seed.add(tgt);
|
||||||
|
} catch (Exception e) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- BFS closure forward + REVERSE (indirect callers) ---
|
||||||
|
Set<Long> done = new TreeSet<>();
|
||||||
|
Deque<Long> queue = new ArrayDeque<>(seed);
|
||||||
|
List<Long> order = new ArrayList<>();
|
||||||
|
int cap = 8000;
|
||||||
|
while (!queue.isEmpty() && order.size() < cap) {
|
||||||
|
long a = queue.poll();
|
||||||
|
if (!done.add(a)) continue;
|
||||||
|
order.add(a);
|
||||||
|
Function f = fm.getFunctionAt(as.getAddress(a));
|
||||||
|
if (f == null) continue;
|
||||||
|
try {
|
||||||
|
for (Function c : f.getCalledFunctions(monitor)) {
|
||||||
|
long ca = c.getEntryPoint().getOffset();
|
||||||
|
if (inRange(ca) && !done.contains(ca)) queue.add(ca);
|
||||||
|
}
|
||||||
|
} catch (Exception e) { }
|
||||||
|
// reverse: any function referencing this entry (indirect dispatch callers)
|
||||||
|
try {
|
||||||
|
for (Reference r : currentProgram.getReferenceManager()
|
||||||
|
.getReferencesTo(as.getAddress(a))) {
|
||||||
|
Address from = r.getFromAddress();
|
||||||
|
if (from == null) continue;
|
||||||
|
Function cf = fm.getFunctionContaining(from);
|
||||||
|
if (cf != null) {
|
||||||
|
long ca = cf.getEntryPoint().getOffset();
|
||||||
|
if (inRange(ca) && !done.contains(ca)) queue.add(ca);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- fun_map2.txt fresh ---
|
||||||
|
PrintWriter pm = new PrintWriter(new java.io.BufferedWriter(
|
||||||
|
new FileWriter("/home/m/re-tools/fun_map2.txt", false)));
|
||||||
|
for (long a : order) {
|
||||||
|
Function f = fm.getFunctionAt(as.getAddress(a));
|
||||||
|
if (f == null) continue;
|
||||||
|
pm.println(Long.toHexString(a) + " " + f.getBody().getNumAddresses() + " " + f.getName());
|
||||||
|
}
|
||||||
|
pm.close();
|
||||||
|
|
||||||
|
// --- decompile ALWAYS into decomp_funs2.txt ---
|
||||||
|
DecompInterface di = new DecompInterface();
|
||||||
|
di.openProgram(currentProgram);
|
||||||
|
PrintWriter pw = new PrintWriter(new java.io.BufferedWriter(
|
||||||
|
new FileWriter("/home/m/re-tools/decomp_funs2.txt", false)));
|
||||||
|
int n = 0, skipped = 0;
|
||||||
|
for (long a : order) {
|
||||||
|
Function f = fm.getFunctionAt(as.getAddress(a));
|
||||||
|
if (f == null) continue;
|
||||||
|
long sz = f.getBody().getNumAddresses();
|
||||||
|
if (sz < 8) { skipped++; continue; }
|
||||||
|
DecompileResults res = di.decompileFunction(f, 120, monitor);
|
||||||
|
if (res != null && res.getDecompiledFunction() != null) {
|
||||||
|
n++;
|
||||||
|
pw.println("############ FUN_ " + Long.toHexString(a) + " size=" + sz + " ############");
|
||||||
|
pw.println(res.getDecompiledFunction().getC());
|
||||||
|
pw.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pw.close();
|
||||||
|
di.dispose();
|
||||||
|
println("DECOMP2_DONE n=" + n + " skipped=" + skipped + " order=" + order.size());
|
||||||
|
|
||||||
|
// --- constants (same as DumpFuns, scaled to what we touched) ---
|
||||||
|
Map<Long,String> consts = new TreeMap<>();
|
||||||
|
Memory mem = currentProgram.getMemory();
|
||||||
|
for (Function f : fm.getFunctions(true)) {
|
||||||
|
long fa = f.getEntryPoint().getOffset();
|
||||||
|
if (!order.contains(fa)) continue;
|
||||||
|
Iterator<Instruction> iit = currentProgram.getListing().getInstructions(f.getBody(), true);
|
||||||
|
while (iit.hasNext()) {
|
||||||
|
Instruction ins = iit.next();
|
||||||
|
for (Reference r : currentProgram.getReferenceManager().getReferencesFrom(ins.getAddress())) {
|
||||||
|
long ta = r.getToAddress().getOffset();
|
||||||
|
if ((ta >= 0x1824c0000L && ta <= 0x182700000L) ||
|
||||||
|
(ta >= 0x180000000L && ta <= 0x181200000L)) {
|
||||||
|
consts.merge(ta, Long.toHexString(fa), (x, y) -> x + ";" + y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PrintWriter pc = new PrintWriter(new java.io.BufferedWriter(
|
||||||
|
new FileWriter("/home/m/re-tools/consts2.txt", false)));
|
||||||
|
for (Map.Entry<Long,String> e : consts.entrySet()) {
|
||||||
|
try {
|
||||||
|
long ta = e.getKey();
|
||||||
|
int[] b = new int[8];
|
||||||
|
boolean ok = true;
|
||||||
|
for (int k = 0; k < 8; k++) {
|
||||||
|
Address a = as.getAddress(ta + k);
|
||||||
|
if (!mem.getLoadedAndInitializedAddressSet().contains(a)) { ok = false; break; }
|
||||||
|
b[k] = mem.getByte(a) & 0xff;
|
||||||
|
}
|
||||||
|
if (!ok) continue;
|
||||||
|
float f32 = Float.intBitsToFloat(b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24));
|
||||||
|
float f32b = Float.intBitsToFloat(b[4] | (b[5] << 8) | (b[6] << 16) | (b[7] << 24));
|
||||||
|
pc.println(Long.toHexString(ta) + " f32=" + f32 + " f32b=" + f32b + " refs=" + e.getValue());
|
||||||
|
} catch (Exception ex) { }
|
||||||
|
}
|
||||||
|
pc.close();
|
||||||
|
println("CONSTS2_DONE consts=" + consts.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.program.model.symbol.Reference;
|
||||||
|
import ghidra.program.model.symbol.ReferenceIterator;
|
||||||
|
import ghidra.program.model.address.Address;
|
||||||
|
import ghidra.program.model.address.AddressSpace;
|
||||||
|
import ghidra.program.model.listing.Function;
|
||||||
|
import ghidra.program.model.listing.FunctionManager;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
public class XrefDsp extends GhidraScript {
|
||||||
|
@Override
|
||||||
|
public void run() throws Exception {
|
||||||
|
String[] targets = {"1805631c0","180563260","180563a60","180563fa0","180563ce0","180563440",
|
||||||
|
"18056e3e0","180529fe0","180509900","180564a00","180537410","1805374e0",
|
||||||
|
"18052e3e0","18052e9b0"};
|
||||||
|
AddressSpace as = currentProgram.getAddressFactory().getDefaultAddressSpace();
|
||||||
|
PrintWriter pw = new PrintWriter(new java.io.BufferedWriter(
|
||||||
|
new java.io.FileWriter("/home/m/re-tools/xrefs2.txt")));
|
||||||
|
FunctionManager fm = currentProgram.getFunctionManager();
|
||||||
|
for (String ts : targets) {
|
||||||
|
long ta = Long.parseLong(ts,16);
|
||||||
|
Address t = as.getAddress(ta);
|
||||||
|
pw.println("### TARGET " + ts);
|
||||||
|
ReferenceIterator it = currentProgram.getReferenceManager().getReferencesTo(t);
|
||||||
|
int n=0;
|
||||||
|
while (it.hasNext() && n<200) {
|
||||||
|
Reference r = it.next();
|
||||||
|
Function cf = null;
|
||||||
|
if (r.getFromAddress()!=null) cf = fm.getFunctionContaining(r.getFromAddress());
|
||||||
|
pw.println(" from " + r.getFromAddress() + " type=" + r.getReferenceType() +
|
||||||
|
" in=" + (cf!=null?Long.toHexString(cf.getEntryPoint().getOffset()):"?"));
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pw.close();
|
||||||
|
println("XREFS2_DONE");
|
||||||
|
}
|
||||||
|
}
|
||||||
+41622
File diff suppressed because one or more lines are too long
+508225
File diff suppressed because it is too large
Load Diff
+7928
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
import base64, re
|
||||||
|
def decode(path):
|
||||||
|
txt=open(path).read()
|
||||||
|
lines=txt.split('\n')
|
||||||
|
for i,l in enumerate(lines):
|
||||||
|
if 'soothe2_x64.vst3' in l:
|
||||||
|
b=[]
|
||||||
|
j=i+1
|
||||||
|
while j<len(lines):
|
||||||
|
t=lines[j].strip()
|
||||||
|
if t=='}': break
|
||||||
|
b.append(t); j+=1
|
||||||
|
raw=re.sub(r'\s','',''.join(b))
|
||||||
|
raw=raw.rstrip('=')
|
||||||
|
raw+='='*((-len(raw))%4)
|
||||||
|
dec=base64.b64decode(raw)
|
||||||
|
# dec is nested: outer base64 blob contains an inner chunked-xml whose first child is another base64 or actual xml
|
||||||
|
# find '<PARAM ...' anywhere after utf8 decode of inner
|
||||||
|
# Look for actual XML PARAM tags embedded (they are inside processorStateData attribute - base64 too)
|
||||||
|
# Simplest: find 'band1 mode' bytes
|
||||||
|
for pidname in [b'band1 mode', b'band1 q', b'band1 sens', b'band1 freq', b'band1 on']:
|
||||||
|
i2=dec.find(pidname)
|
||||||
|
if i2>=0:
|
||||||
|
seg=dec[i2-60:i2+80]
|
||||||
|
m=re.search(rb'<PARAM id="([^"]+)" value="([^"]+)"', seg)
|
||||||
|
if m:
|
||||||
|
print(f" {m.group(1).decode():16s} = {m.group(2).decode()}")
|
||||||
|
return
|
||||||
|
for f in ['/home/m/soothe-bt/t1kq_b1f_800.rpp','/home/m/soothe-bt/dual_b1q_0.1.rpp','/home/m/soothe-bt/dual_b1q_10.0.rpp','/home/m/soothe-bt/dual_b1q_1.0.rpp','/home/m/soothe-bt/t1k_b1f_1100.rpp']:
|
||||||
|
print("=== ",f.split('/')[-1]); decode(f)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import base64, re, sys
|
||||||
|
|
||||||
|
def full_decode(path):
|
||||||
|
txt = open(path).read()
|
||||||
|
lines = txt.split('\n')
|
||||||
|
for i, l in enumerate(lines):
|
||||||
|
if 'soothe2_x64.vst3' in l:
|
||||||
|
b = []
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lines):
|
||||||
|
t = lines[j].strip()
|
||||||
|
if t == '}':
|
||||||
|
break
|
||||||
|
b.append(t)
|
||||||
|
j += 1
|
||||||
|
raw = re.sub(r'\s', '', ''.join(b))
|
||||||
|
raw = raw.rstrip('=')
|
||||||
|
raw += '=' * ((-len(raw)) % 4)
|
||||||
|
dec = base64.b64decode(raw)
|
||||||
|
# print first bytes overview
|
||||||
|
print('decoded blob len', len(dec))
|
||||||
|
# Look for inner base64 chunked xml (soothe2 vst uses nested b64)
|
||||||
|
# search for PARAM tags in raw decoded bytes
|
||||||
|
tags = re.findall(rb'<PARAM id="([^"]+)" value="([^"]+)"', dec)
|
||||||
|
if tags:
|
||||||
|
for pid, val in tags:
|
||||||
|
print(f' {pid.decode():16s} = {val.decode()}')
|
||||||
|
else:
|
||||||
|
# try inner b64
|
||||||
|
inner = re.sub(rb'[^A-Za-z0-9+/=]', b'', dec)
|
||||||
|
inner = inner.rstrip(b'=')
|
||||||
|
inner += b'=' * ((-len(inner)) % 4)
|
||||||
|
try:
|
||||||
|
dec2 = base64.b64decode(inner)
|
||||||
|
tags = re.findall(rb'<PARAM id="([^"]+)" value="([^"]+)"', dec2)
|
||||||
|
for pid, val in tags:
|
||||||
|
print(f' {pid.decode():16s} = {val.decode()}')
|
||||||
|
except Exception as e:
|
||||||
|
print(' inner b64 fail:', e)
|
||||||
|
return
|
||||||
|
print('no soothe2_x64.vst3 found')
|
||||||
|
|
||||||
|
for f in sys.argv[1:]:
|
||||||
|
print('===', f.split('/')[-1])
|
||||||
|
full_decode(f)
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render-parity harness (Phase 5 step 5, dB stage).
|
||||||
|
|
||||||
|
Measures the steady-state per-tone reduction directly on the reference wavs in
|
||||||
|
/home/m/soothe-bt/ and compares with the B.12 bridge model (g*LUT + w*warp^a).
|
||||||
|
|
||||||
|
Model: C(f) = g*LUT(log10(L0/res_band(f,fc,Q))) + w*warp(f)^a -> red = -20*log10(1-C).
|
||||||
|
LUT is the frozen PCHIP (B.11 knots). g=1.221, w=0.358, a=3.143 (model_fir.py canonical).
|
||||||
|
|
||||||
|
Reference renders: 24-bit WAV, input sources 16-bit mono. Tone amplitude estimated by
|
||||||
|
Goertzel at the exact tone frequency over a late steady window (3.0-3.75 s region).
|
||||||
|
"""
|
||||||
|
import wave
|
||||||
|
import numpy as np
|
||||||
|
from scipy.interpolate import PchipInterpolator
|
||||||
|
|
||||||
|
BT = '/home/m/soothe-bt/'
|
||||||
|
FS = 44100.0
|
||||||
|
GAIN = 4.132 # 10^(sens_dB/40) with sens_dB=24.65
|
||||||
|
QM = [0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0]
|
||||||
|
FCS = [800., 900., 950., 1000., 1050., 1100., 1200.]
|
||||||
|
L0D = 10 ** (-7.142 / 20) # dual input tone level
|
||||||
|
L0Q = 10 ** (-18.063 / 20) # t1kq input tone level
|
||||||
|
L0T = 1.0 # t1k input tone level (0 dBFS)
|
||||||
|
|
||||||
|
# frozen LUT knots (B.11)
|
||||||
|
LX = np.array([-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.574, 0.61, 0.75, 1.0])
|
||||||
|
LY = np.array([0.4402, 0.4552, 0.4813, 0.5072, 0.5329, 0.5332, 0.5645, 0.6471, 0.6562, 0.6670])
|
||||||
|
LUT = PchipInterpolator(LX, LY)
|
||||||
|
|
||||||
|
|
||||||
|
def load(p):
|
||||||
|
w = wave.open(p, 'rb')
|
||||||
|
n, ch, sr, sw = w.getnframes(), w.getnchannels(), w.getframerate(), w.getsampwidth()
|
||||||
|
b = w.readframes(n)
|
||||||
|
N = n * ch
|
||||||
|
if sw == 2:
|
||||||
|
x = np.frombuffer(b, dtype='<i2').astype(np.float64) / 32768.0
|
||||||
|
else:
|
||||||
|
raw = np.frombuffer(b, dtype=np.uint8).reshape(N, 3)
|
||||||
|
x = (raw[:, 0].astype(np.int64) | (raw[:, 1].astype(np.int64) << 8)
|
||||||
|
| (raw[:, 2].astype(np.int64) << 16))
|
||||||
|
x = np.where(x >= 0x800000, x - 0x1000000, x).astype(np.float64) / 8388607.0
|
||||||
|
return x.reshape(-1, ch)
|
||||||
|
|
||||||
|
|
||||||
|
def tone_amp(p, f):
|
||||||
|
a = load(p)
|
||||||
|
end = min(len(a), int(3.5 * FS))
|
||||||
|
seg = a[:end][-int(0.75 * FS):]
|
||||||
|
x = np.mean(seg, axis=1)
|
||||||
|
n = len(x)
|
||||||
|
w = 2 * np.pi * f / FS
|
||||||
|
cw = 2 * np.cos(w)
|
||||||
|
s0 = s1 = s2 = 0.0
|
||||||
|
for v in x:
|
||||||
|
s2 = s1
|
||||||
|
s1 = s0
|
||||||
|
s0 = v + cw * s1 - s2
|
||||||
|
return np.sqrt(abs(s0 * s0 + s1 * s1 - 2 * cw * s0 * s1)) / n
|
||||||
|
|
||||||
|
|
||||||
|
def red(src, out, f):
|
||||||
|
return 20 * np.log10(tone_amp(src, f) / tone_amp(out, f))
|
||||||
|
|
||||||
|
|
||||||
|
def res_band(f, fc, Q):
|
||||||
|
w0 = fc * 2 * np.pi / FS
|
||||||
|
c, s = np.cos(w0), np.sin(w0)
|
||||||
|
p = (s * 0.5) / Q
|
||||||
|
a, a2 = p * GAIN, p / GAIN
|
||||||
|
A = [a + 1, -2 * c, 1 - a]
|
||||||
|
B = [a2 + 1, -2 * c, 1 - a2]
|
||||||
|
ww = 2 * np.pi * f / FS
|
||||||
|
z = np.exp(-1j * ww)
|
||||||
|
return abs(2 * (B[0] + B[1] * z + B[2] * z * z) / (A[0] + A[1] * z + A[2] * z * z))
|
||||||
|
|
||||||
|
|
||||||
|
def warp(f):
|
||||||
|
x = f / 2000.
|
||||||
|
return 0.87 * 7.942 * x / (7.942 + x)
|
||||||
|
|
||||||
|
|
||||||
|
def pred(f, fc, Q, L0):
|
||||||
|
xv = np.log10(L0 / res_band(f, fc, Q))
|
||||||
|
C = 1.221 * float(LUT(float(np.clip(xv, -1, 1.5)))) + 0.358 * warp(f) ** 3.143
|
||||||
|
return -20 * np.log10(1 - min(C, 0.999))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
meas, pr = [], []
|
||||||
|
for q in QM:
|
||||||
|
for f in (500, 2000):
|
||||||
|
meas.append(red(BT + 'dual.wav', BT + f'dual_b1q_{q}.wav', f))
|
||||||
|
pr.append(pred(f, 500, q, L0D))
|
||||||
|
for fc in FCS:
|
||||||
|
meas.append(red(BT + 'tone1kq.wav', BT + f't1kq_b1f_{int(fc)}.wav', 1000))
|
||||||
|
pr.append(pred(1000, fc, 0.9999978, L0Q))
|
||||||
|
for fc in FCS:
|
||||||
|
meas.append(red(BT + 'tone1k.wav', BT + f't1k_b1f_{int(fc)}.wav', 1000))
|
||||||
|
pr.append(pred(1000, fc, 0.9999978, L0T))
|
||||||
|
meas = np.array(meas)
|
||||||
|
pr = np.array(pr)
|
||||||
|
print('RENDER-PARITY vs /home/m/soothe-bt (36 pts, real wavs), B.12 (g=1.221,w=0.358,a=3.143):')
|
||||||
|
print(' TOTAL rmse = %.4f dB' % np.sqrt(np.mean((pr - meas) ** 2)))
|
||||||
|
for name, sl in [('dual500', slice(0, 22, 2)), ('dual2000', slice(1, 22, 2)),
|
||||||
|
('t1kq', slice(22, 29)), ('t1k', slice(29, 36))]:
|
||||||
|
print(' %-9s rmse=%.4f' % (name, np.sqrt(np.mean((pr[sl] - meas[sl]) ** 2))))
|
||||||
|
print(' dual2000 resid:', ' '.join('%+.2f' % x for x in (pr[1:22:2] - meas[1:22:2])))
|
||||||
|
print(' t1kq resid: ', ' '.join('%+.2f' % x for x in (pr[22:29] - meas[22:29])))
|
||||||
|
print(' t1k resid: ', ' '.join('%+.2f' % x for x in (pr[29:] - meas[29:])))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import base64, re, sys, html
|
||||||
|
|
||||||
|
def rpp_params(path):
|
||||||
|
txt = open(path, encoding='utf8', errors='ignore').read()
|
||||||
|
lines = txt.split('\n')
|
||||||
|
for i, l in enumerate(lines):
|
||||||
|
if 'soothe2_x64.vst3' in l:
|
||||||
|
break
|
||||||
|
b = []
|
||||||
|
for l in lines[i + 1:]:
|
||||||
|
if l.strip() == '}':
|
||||||
|
break
|
||||||
|
b.append(l.strip())
|
||||||
|
raw = re.sub(r'[^A-Za-z0-9+/=]', '', ''.join(b))
|
||||||
|
raw = raw.rstrip('=')
|
||||||
|
# drop chars until b64 len divisible by 4 (the leading binary header causes odd)
|
||||||
|
while (len(raw) % 4) != 0:
|
||||||
|
raw = raw[:-1]
|
||||||
|
padded = raw + '=' * ((-len(raw)) % 4)
|
||||||
|
dec = base64.b64decode(padded, validate=False)
|
||||||
|
p = dec.find(b'<?xml')
|
||||||
|
xml = dec[p:].decode('utf8', 'ignore')
|
||||||
|
xml = html.unescape(xml)
|
||||||
|
params = dict(re.findall(r'<PARAM id="([^"]+)" value="([^"]+)"', xml))
|
||||||
|
# also decode processorStateData inner
|
||||||
|
proc = re.search(r'processorStateData="([^"]+)"', xml)
|
||||||
|
if proc:
|
||||||
|
inner = proc.group(1)
|
||||||
|
try:
|
||||||
|
inner_dec = base64.b64decode(re.sub(r'\s', '', inner))
|
||||||
|
print(' [processorStateData inner]', inner_dec.decode('utf8', 'ignore')[:400])
|
||||||
|
except Exception as e:
|
||||||
|
print(' proc inner err', e)
|
||||||
|
return params
|
||||||
|
|
||||||
|
for f in sys.argv[1:]:
|
||||||
|
p = rpp_params(f)
|
||||||
|
print('===', f.split('/')[-1])
|
||||||
|
for k in sorted(p):
|
||||||
|
print(f' {k:24s} = {p[k]}')
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,247 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""rtobj.py — runtime-capture of soothe2 DSP object heap (v2: periodic heap scan).
|
||||||
|
|
||||||
|
Spawns `reaper -nosplash -renderproject <rpp>`, locates the yabridge-host process
|
||||||
|
with soothe2 mapped, computes modbase, and PERIODICALLY scans ONLY anonymous rw
|
||||||
|
regions (heaps) for the Soothe2Module vtable pointer, verifying by ctor fields.
|
||||||
|
On a valid object it dumps twin-state / step-5 window / level weights / mask /
|
||||||
|
warp / freqaxis / setters into OUTDIR.
|
||||||
|
"""
|
||||||
|
import subprocess, os, glob, sys, time, struct, json
|
||||||
|
|
||||||
|
VPTR_RVA = 0x24abb80
|
||||||
|
VPTR_N = 0x60
|
||||||
|
N_BINS = 342
|
||||||
|
N_DUMP = 2048
|
||||||
|
|
||||||
|
|
||||||
|
def mem_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 find_host(proc, wait=120):
|
||||||
|
t0 = time.time()
|
||||||
|
while time.time() - t0 < wait:
|
||||||
|
for p in glob.glob('/proc/[0-9]*'):
|
||||||
|
try:
|
||||||
|
pid = int(os.path.basename(p))
|
||||||
|
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 'yabridge-host' in cmd and 'soothe2' in maps:
|
||||||
|
return pid
|
||||||
|
time.sleep(0.05)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def soothe2_base(host):
|
||||||
|
try:
|
||||||
|
maps = open(f'/proc/{host}/maps').read()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
for line in maps.splitlines():
|
||||||
|
if 'soothe2' not in line:
|
||||||
|
continue
|
||||||
|
rlo = int(line.split()[0].split('-')[0], 16)
|
||||||
|
hdr = mem_read(host, rlo, 0x1000)
|
||||||
|
if hdr and hdr[:2] == b'MZ':
|
||||||
|
x = struct.unpack('<I', hdr[0x3c:0x40])[0]
|
||||||
|
if hdr[x:x + 4] == b'PE\x00\x00':
|
||||||
|
return rlo
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def heap_regions(pid, excl_start=None, excl_end=None):
|
||||||
|
out = []
|
||||||
|
try:
|
||||||
|
for line in open(f'/proc/{pid}/maps').read().splitlines():
|
||||||
|
p = line.split()
|
||||||
|
if len(p) < 2:
|
||||||
|
continue
|
||||||
|
path = p[5] if len(p) > 5 else ''
|
||||||
|
if 'r' not in p[1]:
|
||||||
|
continue
|
||||||
|
lo, hi = (int(x, 16) for x in p[0].split('-'))
|
||||||
|
# skip the image file-back regions of the plugin itself
|
||||||
|
if excl_start is not None and excl_start <= lo < excl_end:
|
||||||
|
continue
|
||||||
|
if 'soothe2' in line:
|
||||||
|
continue
|
||||||
|
out.append((lo, hi))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def read_vslots(host, base):
|
||||||
|
"""vtable pointer values = addresses of the .data vtable-slots themselves
|
||||||
|
(object's first qword points here), range [vstart, vstart+VPTR_N)."""
|
||||||
|
vstart = base + VPTR_RVA
|
||||||
|
slots = [vstart + off for off in range(0, VPTR_N, 8)]
|
||||||
|
return slots
|
||||||
|
|
||||||
|
|
||||||
|
def scan_objects(host, slots, excl_start=None, excl_end=None):
|
||||||
|
"""Find reg addrs whose qword content == any vtable slot (object vptr)."""
|
||||||
|
hits = []
|
||||||
|
mem = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
||||||
|
pats = [struct.pack('<Q', s) for s in slots]
|
||||||
|
try:
|
||||||
|
for lo, hi in heap_regions(host, excl_start, excl_end):
|
||||||
|
try:
|
||||||
|
d = os.pread(mem, hi - lo, lo)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not d:
|
||||||
|
continue
|
||||||
|
for p in pats:
|
||||||
|
i = 0
|
||||||
|
while True:
|
||||||
|
i = d.find(p, i)
|
||||||
|
if i < 0:
|
||||||
|
break
|
||||||
|
hits.append(lo + i)
|
||||||
|
i += 1
|
||||||
|
finally:
|
||||||
|
os.close(mem)
|
||||||
|
return hits
|
||||||
|
|
||||||
|
|
||||||
|
def f32(d, off):
|
||||||
|
return struct.unpack_from('<f', d, off)[0] if len(d) >= off + 4 else None
|
||||||
|
|
||||||
|
|
||||||
|
def verify(host, base):
|
||||||
|
d = mem_read(host, base, 0x541000)
|
||||||
|
if d is None or len(d) < 0x540900:
|
||||||
|
return None
|
||||||
|
f = {
|
||||||
|
'0x24': f32(d, 0x24),
|
||||||
|
'0x540874': f32(d, 0x540874),
|
||||||
|
'0x54087c': f32(d, 0x54087c),
|
||||||
|
'0x540880': f32(d, 0x540880),
|
||||||
|
'0x540884': f32(d, 0x540884),
|
||||||
|
'0x540888': f32(d, 0x540888),
|
||||||
|
'0x54088c': f32(d, 0x54088c),
|
||||||
|
'0x540894': f32(d, 0x540894),
|
||||||
|
}
|
||||||
|
ok = (f['0x24'] == 44100.0 and f['0x540874'] == 1.0 and f['0x540894'] == 1.0)
|
||||||
|
return f if ok else None
|
||||||
|
|
||||||
|
|
||||||
|
def dump_floats(host, addr, count, tag, od, fmt=lambda v: f'{v:.17g}'):
|
||||||
|
d = mem_read(host, addr, count * 4)
|
||||||
|
if not d:
|
||||||
|
return
|
||||||
|
open(f'{tag}.bin', 'wb').write(d)
|
||||||
|
n = len(d) // 4
|
||||||
|
vals = struct.unpack('<%df' % n, d[:n * 4])
|
||||||
|
with open(f'{od}/{tag}.txt', 'w') as f:
|
||||||
|
for i, v in enumerate(vals):
|
||||||
|
f.write(f'{i}: {fmt(v)}\n')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
RPP = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual300.rpp'
|
||||||
|
OD = sys.argv[2] if len(sys.argv) > 2 else '/tmp/rtobj_dual300'
|
||||||
|
os.makedirs(OD, exist_ok=True)
|
||||||
|
|
||||||
|
os.system("pkill -9 -x reaper 2>/dev/null; pkill -9 -f '[y]abridge' 2>/dev/null; sleep 1")
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
['/usr/bin/reaper', '-nosplash', '-ignoreerrors', '-renderproject', RPP],
|
||||||
|
stdout=open(f'{OD}/reaper.log', 'w'), stderr=subprocess.STDOUT)
|
||||||
|
print(f'reaper pid={proc.pid}', flush=True)
|
||||||
|
|
||||||
|
host = find_host(proc)
|
||||||
|
if not host:
|
||||||
|
proc.kill(); sys.exit('no host')
|
||||||
|
base = soothe2_base(host)
|
||||||
|
print(f'host={host} modbase=0x{base:x}', flush=True)
|
||||||
|
|
||||||
|
slots = read_vslots(host, base)
|
||||||
|
print(f'vtable slots: {[hex(s) for s in slots]}', flush=True)
|
||||||
|
|
||||||
|
# periodic scan while render alive
|
||||||
|
got = None
|
||||||
|
t0 = time.time()
|
||||||
|
last_host = host
|
||||||
|
nscan = 0
|
||||||
|
while time.time() - t0 < 300 and not got:
|
||||||
|
if proc.poll() is not None:
|
||||||
|
print('render finished', flush=True)
|
||||||
|
break
|
||||||
|
if not os.path.exists(f'/proc/{last_host}'):
|
||||||
|
print('host died, re-finding...', flush=True)
|
||||||
|
last_host = find_host(proc, wait=10)
|
||||||
|
if not last_host:
|
||||||
|
break
|
||||||
|
base = soothe2_base(last_host)
|
||||||
|
if not base:
|
||||||
|
break
|
||||||
|
slots = read_vslots(last_host, base)
|
||||||
|
t1 = time.time()
|
||||||
|
hits = scan_objects(last_host, slots, excl_start=base, excl_end=base + 0x7000000)
|
||||||
|
if not hits:
|
||||||
|
# fallback: no vptr-of-this module found anywhere -> try ctor as marker too
|
||||||
|
hits = scan_objects(last_host, [base + 0x529610], excl_start=base, excl_end=base + 0x7000000)
|
||||||
|
if hits:
|
||||||
|
print(f' (ctor-marker fallback hits={len(hits)})', flush=True)
|
||||||
|
for a in hits:
|
||||||
|
f = verify(last_host, a)
|
||||||
|
if f:
|
||||||
|
got = (a, f)
|
||||||
|
break
|
||||||
|
nscan += 1
|
||||||
|
if not got:
|
||||||
|
dt = time.time() - t1
|
||||||
|
print(f' scan#{nscan} t={dt:.2f}s no object', flush=True)
|
||||||
|
time.sleep(0.25)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
host = last_host
|
||||||
|
if not got:
|
||||||
|
print('NO VALID DSP OBJECT FOUND', flush=True)
|
||||||
|
proc.kill(); sys.exit(2)
|
||||||
|
|
||||||
|
addr, fields = got
|
||||||
|
tag = os.path.basename(f'obj_{addr:x}')
|
||||||
|
meta = {'base': addr, 'modbase': base, 'rpp': RPP, 'host': host, 'fields': fields}
|
||||||
|
open(f'{OD}/{tag}.json', 'w').write(json.dumps(meta, indent=2))
|
||||||
|
print(f'OBJECT 0x{addr:x} fields={fields}', flush=True)
|
||||||
|
|
||||||
|
dump_floats(host, addr + 0x28, 0x200, f'{tag}.twin_order_f', OD, fmt=lambda v: f'{v:.9g}')
|
||||||
|
dump_floats(host, addr + 0x40, 0x2ac, f'{tag}.twin_state_A', OD)
|
||||||
|
dump_floats(host, addr + 0x58, 0x2ac, f'{tag}.twin_state_B', OD)
|
||||||
|
dump_floats(host, addr + 0x540658, N_DUMP, f'{tag}.window_540658', OD, fmt=lambda v: f'{v:.9g}')
|
||||||
|
for off, nm in ((0x5406b8, 'w_b8'), (0x5406c8, 'w_c8'),
|
||||||
|
(0x5406d8, 'w_d8'), (0x5406e8, 'w_e8')):
|
||||||
|
dump_floats(host, addr + off, N_DUMP, f'{tag}.{nm}', OD, fmt=lambda v: f'{v:.9g}')
|
||||||
|
dump_floats(host, addr + 0x5406a8, 0x200, f'{tag}.warp_5406a8', OD, fmt=lambda v: f'{v:.9g}')
|
||||||
|
dump_floats(host, addr + 0x540698, 0x200, f'{tag}.freqaxis_540698', OD, fmt=lambda v: f'{v:.9g}')
|
||||||
|
dump_floats(host, addr + 0x5407c8, N_DUMP, f'{tag}.maskacc_5407c8', OD, fmt=lambda v: f'{v:.9g}')
|
||||||
|
|
||||||
|
# scalar fields
|
||||||
|
for off, nm in ((0x1a0, 'n1a0'), (0x1ac, 'n1ac'), (0x1a4, 'n1a4'),
|
||||||
|
(0x540868, 'n540868'), (0x54086c, 'n54086c'),
|
||||||
|
(0x540870, 's540870'), (0x540878, 's540878'), (0x54087c, 's54087c'),
|
||||||
|
(0x540880, 's540880'), (0x540884, 's540884'),
|
||||||
|
(0x540888, 's540888'), (0x54088c, 's54088c'),
|
||||||
|
(0x540890, 's540890'), (0x540894, 's540894')):
|
||||||
|
d = mem_read(host, addr + off, 4)
|
||||||
|
if d:
|
||||||
|
v = struct.unpack('<f', d)[0] if not nm.startswith('n') else struct.unpack('<I', d)[0]
|
||||||
|
open(f'{OD}/{tag}.{nm}.txt', 'w').write(f'{v!r}\n')
|
||||||
|
|
||||||
|
print('capture done', flush=True)
|
||||||
|
proc.wait(timeout=30)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""rtsnap.py — differential snapshot of a live process for ctx discovery.
|
||||||
|
Captures readable regions page-wise (skipping EIO pages), stores: index(file) + regions.raw
|
||||||
|
Usage: rtsnap.py <pid> <outprefix>
|
||||||
|
"""
|
||||||
|
import os, struct, sys
|
||||||
|
|
||||||
|
pid = int(sys.argv[1])
|
||||||
|
pre = sys.argv[2]
|
||||||
|
mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
|
||||||
|
regs = []
|
||||||
|
off = 0
|
||||||
|
for line in open(f'/proc/{pid}/maps'):
|
||||||
|
p = line.split()[0]
|
||||||
|
lo, hi = [int(x, 16) for x in p.split('-')]
|
||||||
|
perm = line.split()[1]
|
||||||
|
if perm[0] != 'r':
|
||||||
|
continue
|
||||||
|
if 'vst3' in line or 'dri' in line or 'shm' in line:
|
||||||
|
continue
|
||||||
|
if hi - lo > 0x40000000:
|
||||||
|
continue
|
||||||
|
regs.append((lo, hi))
|
||||||
|
print(f'{len(regs)} regions', flush=True)
|
||||||
|
idx = []
|
||||||
|
raw = bytearray()
|
||||||
|
for lo, hi in regs:
|
||||||
|
rstart = len(raw)
|
||||||
|
sz = 0
|
||||||
|
for pg in range(lo, hi, 0x1000):
|
||||||
|
try:
|
||||||
|
b = os.pread(mem, 0x1000, pg)
|
||||||
|
raw.extend(b)
|
||||||
|
sz += len(b)
|
||||||
|
except Exception:
|
||||||
|
raw.extend(b'\0' * 0x1000)
|
||||||
|
idx.append((lo, rstart, sz))
|
||||||
|
with open(pre + '.raw', 'wb') as f:
|
||||||
|
f.write(bytes(raw))
|
||||||
|
with open(pre + '.idx', 'wb') as f:
|
||||||
|
for lo, rstart, sz in idx:
|
||||||
|
f.write(struct.pack('<QQQ', lo, rstart, sz))
|
||||||
|
os.close(mem)
|
||||||
|
print(f'done: raw={len(raw)} bytes', flush=True)
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
+51
@@ -0,0 +1,51 @@
|
|||||||
|
### TARGET 1805631c0
|
||||||
|
from 1826a03cc type=DATA in=?
|
||||||
|
from 1824b14f0 type=DATA in=?
|
||||||
|
from 1824b14f8 type=DATA in=?
|
||||||
|
### TARGET 180563260
|
||||||
|
from 1826a03e4 type=DATA in=?
|
||||||
|
from 1824b1510 type=DATA in=?
|
||||||
|
### TARGET 180563a60
|
||||||
|
from 1826a0408 type=DATA in=?
|
||||||
|
from 1805636c2 type=UNCONDITIONAL_CALL in=180563440
|
||||||
|
### TARGET 180563fa0
|
||||||
|
from 1826a0420 type=DATA in=?
|
||||||
|
from 182537570 type=DATA in=?
|
||||||
|
from 182537580 type=DATA in=?
|
||||||
|
from 1805631e7 type=UNCONDITIONAL_CALL in=1805631c0
|
||||||
|
from 1805632b4 type=UNCONDITIONAL_CALL in=180563260
|
||||||
|
### TARGET 180563ce0
|
||||||
|
from 1826a0414 type=DATA in=?
|
||||||
|
from 1805631da type=UNCONDITIONAL_CALL in=1805631c0
|
||||||
|
from 1805632a7 type=UNCONDITIONAL_CALL in=180563260
|
||||||
|
### TARGET 180563440
|
||||||
|
from 1826a03fc type=DATA in=?
|
||||||
|
from 1805631c9 type=UNCONDITIONAL_CALL in=1805631c0
|
||||||
|
from 180563296 type=UNCONDITIONAL_CALL in=180563260
|
||||||
|
### TARGET 18056e3e0
|
||||||
|
from 1826a0fcc type=DATA in=?
|
||||||
|
from 1805636a8 type=UNCONDITIONAL_CALL in=180563440
|
||||||
|
### TARGET 180529fe0
|
||||||
|
from 18269dc54 type=DATA in=?
|
||||||
|
from 182531b78 type=DATA in=?
|
||||||
|
from 182531c1c type=DATA in=?
|
||||||
|
from 1824ac240 type=DATA in=?
|
||||||
|
### TARGET 180509900
|
||||||
|
from 18269c058 type=DATA in=?
|
||||||
|
from 1824a8b88 type=DATA in=?
|
||||||
|
### TARGET 180564a00
|
||||||
|
from 1826a0498 type=DATA in=?
|
||||||
|
from 1824b1750 type=DATA in=?
|
||||||
|
### TARGET 180537410
|
||||||
|
from 1824ac3a0 type=DATA in=?
|
||||||
|
### TARGET 1805374e0
|
||||||
|
from 18269e4ac type=DATA in=?
|
||||||
|
from 1824ab908 type=DATA in=?
|
||||||
|
### TARGET 18052e3e0
|
||||||
|
### TARGET 18052e9b0
|
||||||
|
from 18269dfa8 type=DATA in=?
|
||||||
|
from 1825320d8 type=DATA in=?
|
||||||
|
from 182532148 type=DATA in=?
|
||||||
|
from 18253215c type=DATA in=?
|
||||||
|
from 18052b942 type=UNCONDITIONAL_CALL in=18052b940
|
||||||
|
from 18052ba28 type=UNCONDITIONAL_CALL in=18052ba20
|
||||||
Reference in New Issue
Block a user