Compare commits

...
3 Commits
14 changed files with 984 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/γ → это
+23
View File
@@ -0,0 +1,23 @@
-- dump_params.lua : enumerate soothe2 FX params (name, raw, formatted) to file
local out = io.open("/tmp/opencode/fxparams.txt", "w")
local tr = reaper.GetTrack(0, 0)
if tr == nil then
out:write("NO TRACK\n"); out:close(); return
end
local nfx = reaper.TrackFX_GetCount(tr)
out:write(string.format("nfx=%d\n", nfx))
for fxi = 0, nfx - 1 do
local rv, fxname = reaper.TrackFX_GetFXName(tr, fxi, "")
out:write(string.format("FX %d: %s\n", fxi, fxname))
local np = reaper.TrackFX_GetNumParams(tr, fxi)
for p = 0, np - 1 do
local _, pname = reaper.TrackFX_GetParamName(tr, fxi, p, "")
local val, minv, maxv = reaper.TrackFX_GetParam(tr, fxi, p)
local _, fmt = reaper.TrackFX_GetFormattedParamValue(tr, fxi, p, "")
out:write(string.format("%d\t%s\traw=%.6f\t[%.3f..%.3f]\tfmt=%s\n", p, pname, val, minv, maxv, fmt))
end
end
out:close()
local t0 = reaper.time_precise()
while reaper.time_precise() - t0 < 2 do reaper.defer(function() end) end
reaper.Main_OnCommand(40004, 0) -- File: Quit REAPER
+119
View File
@@ -1362,3 +1362,122 @@ member -> committed canon stays HEAD (LUT g=0.344/m=4.2).
2. Step 7 live capture ctx+0x188 (constants + possible content-dependent
branch), NOTES_CAPTURE.md method.
3. combine consumer hunt (multiband cascade) for dual/comb.
## ============ UPDATE 2026-08-22a: PHASE B RECOVERY — offline detector hypotheses REFUTED ============
Контекст: Phase B сессия 2026-08-21 (вечер) + утро 08-22 осталась незакоммиченной —
скрипты в /tmp/opencode, результаты не сохранялись. Все 4 эксперимента перепрогнаны,
stdout зафиксирован (`phaseB_*.out`), скрипты перенесены в `scripts/phaseB_*.py`.
Движок: phaseA_grid_fast.py (валидированный офлайн-тректор, погрешность 0.09..0.19 dB
на анкорах). Анкоры res_500/al_12/al_24/t1k_1000, критерий pred_err ~ 0 на ВСЕХ.
| # | Гипотеза | Скрипт | Результат | Статус |
|---|----------|--------|-----------|--------|
| 1b | slide_max pooling lvl (w=3..33) | phaseB_pool2.py | w=3: tones 2.7/2.3/4.9, rms 3.02 (base 1.04) | **REFUTED** |
| 1c | neighborhood mean/RMS pooling | phaseB_pool3.py | best mean w=3 rms 1.35; res +1.9..+5.1 vs t1k 1.9..4.4 tradeoff, ни одна точка не закрывает все 4 | **REFUTED** |
| 2 | temporal dynamics (hold b/dbdecay r/ema a на полной траектории) | phaseB_temporal.py | ВСЕ варианты = baseline (rms 1.031.05): метрика в steady-state, состояние успевает устояться до окна | **REFUTED (no-op)** |
| 3 | ρ(IIR1)+Δ joint scan (ρ∈[0.30,0.95], Δ∈[1.5,+1.5]) | phaseB_rho.py | best ρ=0.830 D=+1.40 → rms 0.654; НО al_24 стабильно −1.19..−1.25, ρ без источника в декомпе (канон 0.692) | **REJECTED** (нарушает golden rule #1, тот же scalar-family тупик) |
Выводы:
1. Пространственный pooling ЛЮБОГО вида (max/mean/rms) не объясняет content-gap.
2. Temporal-класс гипотез НЕПРОВЕРЯЕМ на steady-state анкорах — нужен переходный
контент (burst-рефы уже есть в soothe-bt) или другой анкорный набор.
3. Лучший (ρ,D) = репараметризация affine-семейства Phase A → упирается в тот же
KEY NEGATIVE RESULT (scalar-семейство не закрывает тон+шум одновременно).
Канон НЕ сменён: HEAD (LUT γ=0.344/MULT=4.2, ρ=0.692).
NEXT (приоритеты без изменений):
1. Step 7 live capture ctx+0x188 (NOTES_CAPTURE.md метод) — константы A/B/γ +
возможная контент-зависимая ветка. Требует 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 сидит там.
## ============ UPDATE 2026-08-22c: PARAM BRIDGE + RENDER-BENCH CALIBRATION ============
Инфраструктура управления плагином через ОФИЦИАЛЬНЫЙ параметр-мост REAPER:
- `dump_params.lua` — дамп всех FX-параметров (имя, raw 0..1, formatted UI) в /tmp/opencode/fxparams.txt.
- `setparam.lua` — установка по имени через TrackFX_SetParam + SaveProjectEx (env S2_SET="name=val;...", S2_OUT).
- `scripts/rpp_setparam.py` — редактор XML-стейта в RPP с корректным апдейтом length-полей
(A=buflen16, B=len(xml), хвост JUCEPrivateData сохраняется).
### КЛЮЧЕВОЕ ОТКРЫТИЕ: XML `<PARAM>` секция НЕ источник VST3-стейта
Живой дамп: depth raw=0.524 ↔ fmt=0.9; в XML при этом "0.8639736175537109" (=UI-единицы!).
Плагин восстанавливает параметры из БИНАРНОЙ части чанка; XML-список — декоративная
копия для GUI-восстановления. Правки только XML → рассинхрон → плагин откатывается в
дефолт (объясняет «магию» идентичных рендеров −31.76 у любых depth-правок; контроль
same-value rewrite давал байт-в-байт реф ✓). ВНИМАНИЕ к rpp_setparam.py: менять можно,
но аудио это НЕ меняет.
### Семантика параметров (через мост)
- `depth`: raw 0..1 → UI **±18 линейно** (ui=36·raw−18). Рендеры подтверждают монотонность:
raw=0 → юнити; raw=1.0 (+18) → 37.6 dB @tone.
- `input trim`: raw → **±24 dB**.
- band sens: fmt в dB; stereo mode mid|side; balance 100%|57%.
### WAV-грабли (живое подтверждение класса багов 71644ff)
REAPER пишет bext(602B)+junk(28B) ПЕРЕД data; наивный парсер (readframes+reshape без
каноничного loader) даёт фантомный широкополосный шум и фальшивый клиппинг
(corr(L,R)≈0 артефакты). Каноничный render_parity.load корректен; санити bypass:
corr(L,R)=1.0000, rms=out=in — РЕНДЕР-ПАЙПЛАЙН ЧИСТЫЙ.
### Измеренные кривые редукции (t1kq-пресет, тон@1000, Goertzel 0.75s)
- Depth-свип: монотонный, raw .5→−5.5 dB; .75→−21 dB; 1.0→−37.6 dB @tone.
- Input-trim свип (холодный тон −18 dBFS): R_eff растёт 1.4→15.7 dB на всём ±24 —
ПОТОЛКА НЕТ в этом диапазоне (против гипотезы C_max≈0.70 как свойства плагина).
- Горячий тон (+6 dBFS): out@1000 ЗАМИРАЕТ на −26.74 dB при trim ≥+12 (R_eff cap ≈20.7),
плато воспроизведено дважды.
- НО: при одинаковом относительном драйве холодный/горячий дают РАЗНЫЙ gain
(15.7 vs 20.7 @L≈+6) ⇒ отклик детектора не чистая f(level) — контент/нормализация
(согласуется с KEY NEGATIVE RESULT Phase B).
### Следствия для приоритета №1
1. «Насыщение C_max≈0.70» старой модели — артефакт placeholder MULT=4.2/Pchip cap 0.667,
а не свойство плагина в рабочем диапазоне.
2. Реальный потолок проявляется как FLOOR выходного уровня (−26.74 dB @hot) — искать
механизм floor/clamp в level-path декомпе с конкретной целью.
3. Инструмент готов для систематического картирования R(L, sens, depth): скрипты +
мост позволяют прогонять десятки конфигов без ручного GUI.
+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)
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Phase A step 3 (fast): combo-vectorized law grid-search."""
import re, sys
import numpy as np
SRC = '/home/m/re-tools/dsp/rt_mask_tables.cpp'
src = open(SRC).read()
def tab(name):
m = re.search(r'const double %s\[\] = \{(.*?)\};' % name, src, re.S)
return np.array([float(x) for x in re.findall(r'[-+0-9.eE]+', m.group(1))])
A1, B1 = tab('kIIR_A1'), tab('kIIR_B1')
A2, B2 = tab('kIIR_A2'), tab('kIIR_B2')
A3, B3 = tab('kIIR_A3'), tab('kIIR_B3')
DUMPDIR = {'dump_res_new.bin': '/tmp/', 'dump_t1k.bin': '/tmp/'}
def load_dump(p):
d = np.loadtxt(p, skiprows=1); return d[:, 2], d[:, 6]
def load_traj(p):
b = open(p, 'rb').read(); off = 0; fr = []
while off < len(b):
_, nb = np.frombuffer(b, dtype=np.int32, count=2, offset=off); off += 8
fr.append(np.frombuffer(b, dtype='<f4', count=int(nb), offset=off).astype(np.float64)); off += 4*int(nb)
return np.array(fr)
WIN = {'res_500': (55, 90), 'al_12': (243, 278), 'al_24': (243, 278), 't1k_1000': (243, 278)}
MEAS_OLD = {'res_500': 0.219, 'al_12': 0.450, 'al_24': -1.822, 't1k_1000': 1.816}
DATA = {}
for name, traj, dump, bm in [
('res_500','traj_res500.bin','dump_res_new.bin',85),
('al_12','traj_al12.bin','dump_t1k.bin',85),
('al_24','traj_al24.bin','dump_t1k.bin',85),
('t1k_1000','traj_t1k.bin','dump_t1k.bin',85)]:
res_k, W = load_dump(DUMPDIR[dump]+dump)
T = load_traj('/tmp/opencode/'+traj)[WIN[name][0]:WIN[name][1]]
dB = np.log10(np.maximum(T, 1e-12))*20.0
if name != 'res_500':
lv = dB[:, bm]; keep_dB = dB[lv >= lv.max()-6]; keep_n = (keep_dB.shape[0],)
else:
keep_dB = dB; keep_n = None
DATA[name] = (np.ascontiguousarray(keep_dB, dtype=np.float64), W.astype(np.float64), bm, keep_n)
def gains_batch(dB, W, bm, X0s, SLs, CMs, C_pre=None, FLs=None):
if FLs is None: FLs = np.zeros_like(X0s)
"""dB [T,nbin]; returns G [C,T] gain at bm for each combo."""
import sys
print('gains_batch shapes:', dB.shape, W.shape, bm, X0s.shape, file=sys.stderr)
C, T, N = len(X0s), dB.shape[0], dB.shape[1]
if C_pre is not None:
c = np.broadcast_to(C_pre, (C, T, N))
else:
X0 = X0s[:, None, None]; SL = SLs[:, None, None]; CM = CMs[:, None, None]
FL = FLs[:, None, None]
c = np.clip(X0 + SL*dB, FL, CM) # [C,T,N]
acc = np.zeros((C, T))
y = np.empty_like(c)
for i in range(N):
acc = A1[i]*acc + B1[i]*c[:, :, i]
y[:, :, i] = acc
acc = np.zeros((C, T))
for i in range(N):
acc = A2[i]*acc + B2[i]*y[:, :, i]
y[:, :, i] = 0.8*np.exp2(-acc)*W[i]
# IIR3 bidi x2 on y
for _ in range(2):
st = np.zeros((C, T))
for i in range(N):
st = y[:, :, i]*B3[i] + st*A3[i]
y[:, :, i] = st
st = y[:, :, -1].copy()
for i in range(N-2, 0, -1):
st = y[:, :, i]*B3[i] + st*A3[i]
y[:, :, i] = st
return y[:, :, bm]
# old-law reference gains
GO = {}
for name,(dB,W,bm,_) in DATA.items():
c = np.clip((dB+13.78)/82.07, 0, 1)**0.344*4.2
c_old = np.clip((dB+13.78)/82.07, 0, 1)**0.344*4.2
g = gains_batch(dB, W, bm, np.array([0.]), np.array([0.]), np.array([99.]), C_pre=c_old)
GO[name] = float(g[0].mean()) if name=='res_500' else float(np.median(g[0]))
def evaluate(X0s, SLs, CMs, FLs=None):
out = {}
for name,(dB,W,bm,_) in DATA.items():
g = gains_batch(dB, W, bm, X0s, SLs, CMs, FLs=FLs) # [C,T]
agg = np.sqrt(np.mean(g**2, axis=1)) if name=='res_500' else np.median(g, axis=1)
out[name] = MEAS_OLD[name] + 20*np.log10(agg/GO[name])
return out
X0g = np.arange(1.85, 2.35, 0.05); SLg = np.arange(0.065, 0.102, 0.0025); CMg = np.array([99.])
FLg = np.array([0., 0.15, 0.3, 0.45, 0.6])
X0f, SLf, CMf, FLf = [j.ravel() for j in np.meshgrid(X0g, SLg, CMg, FLg, indexing='ij')]
names = list(DATA)
recs = []
CH = 120
for s in range(0, len(X0f), CH):
sl = slice(s, s+CH)
ev = evaluate(X0f[sl], SLf[sl], CMf[sl], FLf[sl])
for j in range(len(X0f[sl])):
e = {n: ev[n][j] for n in names}
recs.append((sum(v*v for v in e.values())/len(names),
X0f[sl][j], SLf[sl][j], CMf[sl][j], e, FLf[sl][j]))
recs.sort(key=lambda r: r[0])
print('refined top-12:')
for tot, X0, SL, CM, e, FL in recs[:12]:
print(f' X0={X0:.2f} S={SL:.4f} FL={FL:.2f} rms={np.sqrt(tot):.3f} ' +
' '.join(f'{n[:5]}:{v:+.2f}' for n,v in e.items()))
MEAS_NEW={'res_500':2.019,'al_12':0.244,'al_24':-0.069,'t1k_1000':-0.321}
ev18=evaluate(np.array([1.8]),np.array([0.11]),np.array([99.]))
print('new(1.8,.11) model-pred vs measured:')
for n in names:
print(f' {n:10} pred{ev18[n][0]:+.3f} meas{MEAS_NEW[n]:+.3f}')
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Phase B step 1b: spatial max-pooling scan on TOP of validated grid engine.
Only deviation from phaseA_grid_fast.py: trajectory transform before law.
pool_lvl w: sliding max over bins (width w). pool_db == pool_lvl (monotone),
pool_am ~ pool_lvl near flat res -> skip both.
"""
import numpy as np
exec(open('/tmp/opencode/phaseA_grid_fast.py').read().split("# old-law reference gains")[0])
def slide_max(x, w):
if w <= 1: return x
h = w // 2
xp = np.pad(x, ((0, 0), (h, h)), mode='edge')
win = np.lib.stride_tricks.sliding_window_view(xp, w, axis=1)
return np.ascontiguousarray(win.max(axis=-1))
RAW = {}
for name, traj, dump, bm in [
('res_500','traj_res500.bin','dump_res_new.bin',85),
('al_12','traj_al12.bin','dump_t1k.bin',85),
('al_24','traj_al24.bin','dump_t1k.bin',85),
('t1k_1000','traj_t1k.bin','dump_t1k.bin',85)]:
RAW[name] = load_traj('/tmp/opencode/'+traj)[WIN[name][0]:WIN[name][1]]
GO = {}
for name,(dB,W,bm,_) in DATA.items():
c_old = np.clip((dB+13.78)/82.07, 0, 1)**0.344*4.2
g = gains_batch(dB, W, bm, np.array([0.]), np.array([0.]), np.array([99.]), C_pre=c_old)
GO[name] = float(g[0].mean()) if name=='res_500' else float(np.median(g[0]))
def eval_variant(w):
global DATA
saved = {n: DATA[n] for n in DATA}
for name,(dB,W,bm,_) in DATA.items():
T = slide_max(RAW[name], w)
dBp = np.log10(np.maximum(T, 1e-12))*20.0
if name != 'res_500':
lv = dBp[:, bm]; keep = dBp[lv >= lv.max()-6]
else:
keep = dBp
DATA[name] = (np.ascontiguousarray(keep), W, bm, None)
out = {}
for name,(dB,W,bm,_) in DATA.items():
g = gains_batch(dB, W, bm, np.array([1.8]), np.array([0.11]), np.array([99.]))
agg = np.sqrt(np.mean(g**2, axis=1)) if name=='res_500' else np.median(g, axis=1)
out[name] = MEAS_OLD[name] + 20*np.log10(float(agg[0])/GO[name])
DATA.update(saved)
return out
print(f'{"w":>3} ' + ' '.join(f'{n:>9}' for n in DATA) + ' rms')
for w in [1, 3, 5, 9, 17, 33]:
e = eval_variant(w)
tot = np.sqrt(sum(v*v for v in e.values())/len(e))
print(f'{w:>3} ' + ' '.join(f'{v:+9.2f}' for v in e.values()) + f' {tot:.2f}')
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Phase B step 1c: neighborhood MEAN/RMS pooling scan (max already refuted).
Rationale: slide_max raises lvl at tone bins via sidelobes -> over-reduction
(tones broke -2.7..-4.9). Mean/RMS pooling does the opposite for an isolated
narrow peak among quiet neighbours -> less reduction on tones, ~neutral on
wide noise. Pooling in LINEAR lvl domain (am ~ lvl near flat res).
"""
import numpy as np
exec(open('/tmp/opencode/phaseA_grid_fast.py').read().split("# old-law reference gains")[0])
def slide(x, w, op):
if w <= 1: return x
h = w // 2
xp = np.pad(x, ((0, 0), (h, h)), mode='edge')
win = np.lib.stride_tricks.sliding_window_view(xp, w, axis=1)
if op == 'mean': return win.mean(axis=-1)
return np.sqrt((win ** 2).mean(axis=-1))
RAW = {}
for name, traj in [('res_500','traj_res500.bin'), ('al_12','traj_al12.bin'),
('al_24','traj_al24.bin'), ('t1k_1000','traj_t1k.bin')]:
RAW[name] = load_traj('/tmp/opencode/'+traj)[WIN[name][0]:WIN[name][1]]
GO = {}
for name,(dB,W,bm,_) in DATA.items():
c_old = np.clip((dB+13.78)/82.07, 0, 1)**0.344*4.2
g = gains_batch(dB, W, bm, np.array([0.]), np.array([0.]), np.array([99.]), C_pre=c_old)
GO[name] = float(g[0].mean()) if name=='res_500' else float(np.median(g[0]))
def eval_variant(op, w):
saved = {n: DATA[n] for n in DATA}
for name,(dB,W,bm,_) in DATA.items():
T = slide(RAW[name], w, op)
dBp = np.log10(np.maximum(T, 1e-12))*20.0
if name != 'res_500':
lv = dBp[:, bm]; keep = dBp[lv >= lv.max()-6]
else:
keep = dBp
DATA[name] = (np.ascontiguousarray(keep), W, bm, None)
out = {}
for name,(dB,W,bm,_) in DATA.items():
g = gains_batch(dB, W, bm, np.array([1.8]), np.array([0.11]), np.array([99.]))
agg = np.sqrt(np.mean(g**2, axis=1)) if name=='res_500' else np.median(g, axis=1)
out[name] = MEAS_OLD[name] + 20*np.log10(float(agg[0])/GO[name])
DATA.update(saved)
return out
print(f'{"op":>4} {"w":>3} ' + ' '.join(f'{n:>9}' for n in DATA) + ' rms')
for op in ['mean', 'rms']:
for w in [3, 5, 9, 17, 33]:
e = eval_variant(op, w)
tot = np.sqrt(sum(v*v for v in e.values())/len(e))
print(f'{op:>4} {w:>3} ' + ' '.join(f'{v:+9.2f}' for v in e.values()) + f' {tot:.2f}')
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Phase B step 1: spatial max-pooling hypothesis scan (offline, no rebuilds).
lvl'(t,k) = pool(lvl)(t,k) with width w, then law -> chain -> median/rms gain
at metric bin. Variants: pool_am (pool raw am then /res), pool_lvl, pool_db.
"""
import re
import numpy as np
SRC = '/home/m/re-tools/dsp/rt_mask_tables.cpp'
src = open(SRC).read()
def tab(name):
m = re.search(r'const double %s\[\] = \{(.*?)\};' % name, src, re.S)
return np.array([float(x) for x in re.findall(r'[-+0-9.eE]+', m.group(1))])
A1, B1 = tab('kIIR_A1'), tab('kIIR_B1')
A2, B2 = tab('kIIR_A2'), tab('kIIR_B2')
A3, B3 = tab('kIIR_A3'), tab('kIIR_B3')
DUMPDIR = {'dump_res_new.bin': '/tmp/', 'dump_t1k.bin': '/tmp/'}
def load_dump(p):
d = np.loadtxt(p, skiprows=1); return d[:, 1], d[:, 2], d[:, 6] # am,res,W
def load_traj(p):
b = open(p, 'rb').read(); off = 0; fr = []
while off < len(b):
_, nb = np.frombuffer(b, dtype=np.int32, count=2, offset=off); off += 8
fr.append(np.frombuffer(b, dtype='<f4', count=int(nb), offset=off).astype(np.float64)); off += 4*int(nb)
return np.array(fr)
def iir_fwd_m(x, A, B):
"""x [C,T,N] vectorized over C,T; sequential over N."""
C, T, N = x.shape
acc = np.zeros((C, T)); y = np.empty_like(x)
for i in range(N):
acc = A[i]*acc + B[i]*x[:, :, i]
y[:, :, i] = acc
return y
def gains_batch(c, W, bm):
y = iir_fwd_m(c, A1, B1)
y = iir_fwd_m(y, A2, B2)
y = 0.8*np.exp2(-y) * W[None, None, :]
for _ in range(2):
st = np.zeros(y.shape[:2])
for i in range(y.shape[2]):
st = y[:, :, i]*B3[i] + st*A3[i]; y[:, :, i] = st
st = y[:, :, -1].copy()
for i in range(y.shape[2]-2, 0, -1):
st = y[:, :, i]*B3[i] + st*A3[i]; y[:, :, i] = st
return y[:, :, bm]
def slide_max(x, w):
"""sliding max over last axis, width w (odd), 'same' edges."""
if w <= 1: return x.copy()
h = w//2
xp = np.pad(x, ((0,0),(0,0),(h,h)), mode='edge')
win = np.lib.stride_tricks.sliding_window_view(xp, w, axis=2)
return win.max(axis=-1)
WIN = {'res_500': (55, 90), 'al_12': (243, 278), 'al_24': (243, 278), 't1k_1000': (243, 278)}
MEAS_OLD = {'res_500': 0.219, 'al_12': 0.450, 'al_24': -1.822, 't1k_1000': 1.816}
SCALE = 15.0 * 440.95 / 2048.0
CASES = {}
for name, traj, dump, bm in [
('res_500','traj_res500.bin','dump_res_new.bin',85),
('al_12','traj_al12.bin','dump_t1k.bin',85),
('al_24','traj_al24.bin','dump_t1k.bin',85),
('t1k_1000','traj_t1k.bin','dump_t1k.bin',85)]:
am, res_k, W = load_dump(DUMPDIR[dump]+dump)
T = load_traj('/tmp/opencode/'+traj)[WIN[name][0]:WIN[name][1]]
CASES[name] = (T, res_k, W, bm)
LAW = dict(new=lambda dB: np.maximum(1.8+0.11*dB, 0))
OLD = lambda dB: np.clip((dB+13.78)/82.07, 0, 1)**0.344*4.2
def agg(g, name):
return np.sqrt(np.mean(g**2)) if name == 'res_500' else np.median(g)
print(f'{"variant":>18} {"w":>3} ' + ' '.join(f'{n:>9}' for n in CASES) + ' (pred err, dB)')
# baselines on CORRECT lvl (traj stores lvl_raw already)
GBASE = {}
for name,(T,res_k,W,bm) in CASES.items():
dB = np.log10(np.maximum(T, 1e-12))
GBASE[name] = agg(gains_batch(OLD(dB)[None], W, bm)[0], name)
print('sanity new@w=1 (vs validated):')
row=[]
for name,(T,res_k,W,bm) in CASES.items():
dB = np.log10(np.maximum(T, 1e-12))
gn = agg(gains_batch(LAW['new'](dB)[None], W, bm)[0], name)
row.append(MEAS_OLD[name] + 20*np.log10(gn/GBASE[name]))
print(f'{"new":>18} {1:>3} ' + ' '.join(f'{v:+9.2f}' for v in row))
for variant in ['pool_lvl', 'pool_am', 'pool_db']:
for w in [3, 5, 9, 17]:
row = []
for name,(T,res_k,W,bm) in CASES.items():
am = T # stored lvl_raw = am/res*scale -> recover am = lvl/res*scale... careful
# stored lvl_raw = am/res_k * SCALE => am = lvl_raw * res_k / SCALE
am_abs = T * res_k[None,:] / SCALE
if variant == 'pool_am':
lv = slide_max(am_abs[None], w)[0] / res_k[None,:] * SCALE
elif variant == 'pool_lvl':
lv = slide_max(T[None], w)[0]
else:
db_ = np.log10(np.maximum(T, 1e-12))*20
lv = 10**(slide_max(db_[None], w)[0]/20)
dB = np.log10(np.maximum(lv, 1e-12))*20
g = gains_batch(LAW['new'](dB)[None], W, bm)
row.append(MEAS_OLD[name] + 20*np.log10(agg(g[0],name)/GBASE[name]))
print(f'{variant:>18} {w:>3} ' + ' '.join(f'{v:+9.2f}' for v in row))
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Phase B step 3: (rho, Delta) joint scan.
Hypothesis: content gap lives in IIR1 spike attenuation vs law level.
rho = IIR1 pole (DC-normalized: y = rho*acc + (1-rho)*x), canon rho=0.692.
Delta = additive shift of affine law c = max(1.8+D+0.11*dB, 0).
Anchored at canon old-law gains GO. Criterion: pred_err ~ 0 on ALL 4 anchors.
"""
import numpy as np
exec(open('/tmp/opencode/phaseA_grid_fast.py').read().split("# old-law reference gains")[0])
GO = {}
for name, (dB, W, bm, _) in DATA.items():
c_old = np.clip((dB + 13.78) / 82.07, 0, 1) ** 0.344 * 4.2
g = gains_batch(dB, W, bm, np.array([0.]), np.array([0.]), np.array([99.]), C_pre=c_old)
GO[name] = float(g[0].mean()) if name == 'res_500' else float(np.median(g[0]))
print('GO:', {k: round(v, 3) for k, v in GO.items()})
def gains_rho(dB, W, bm, rho, DLs):
"""dB [T,N]; law c=max(1.8+Dl+0.11*dB,0); IIR1 pole=rho (DC-norm).
returns [C,T] gain at bm for each Delta in DLs."""
C, T, N = len(DLs), dB.shape[0], dB.shape[1]
c = np.maximum(1.8 + DLs[:, None, None] + 0.11 * dB[None], 0.0)
acc = np.zeros((C, T)); y = np.empty_like(c)
for i in range(N):
acc = rho * acc + (1 - rho) * c[:, :, i]
y[:, :, i] = acc
acc = np.zeros((C, T))
for i in range(N):
acc = A2[i] * acc + B2[i] * y[:, :, i]
y[:, :, i] = 0.8 * np.exp2(-acc) * W[i]
for _ in range(2):
st = np.zeros((C, T))
for i in range(N):
st = y[:, :, i] * B3[i] + st * A3[i]
y[:, :, i] = st
st = y[:, :, -1].copy()
for i in range(N - 2, 0, -1):
st = y[:, :, i] * B3[i] + st * A3[i]
y[:, :, i] = st
return y[:, :, bm]
RHOS = np.linspace(0.30, 0.95, 131)
DLS = np.linspace(-1.5, 1.5, 121)
names = list(DATA)
best = []
for rho in RHOS:
ev = {}
for name, (dB, W, bm, _) in DATA.items():
g = gains_rho(dB, W, bm, rho, DLS)
agg = np.sqrt(np.mean(g ** 2, axis=1)) if name == 'res_500' else np.median(g, axis=1)
ev[name] = MEAS_OLD[name] + 20 * np.log10(agg / GO[name])
E = np.stack([ev[n] for n in names]) # [4, C]
rms = np.sqrt((E ** 2).mean(axis=0)) # per Delta
j = int(rms.argmin())
best.append((rms[j], rho, DLS[j], E[:, j]))
best.sort()
print('\ntop-10 (rms over 4 anchors):')
for rms, rho, dl, e in best[:10]:
print(f' rho={rho:.3f} D={dl:+.3f} rms={rms:.3f} ' +
' '.join(f'{n[:5]}:{v:+.2f}' for n, v in zip(names, e)))
print(f'\ncanon rho=0.692 D=0 reference:')
j0 = int(np.argmin(np.abs(DLS)))
for rho in [0.692]:
ev = {}
for name, (dB, W, bm, _) in DATA.items():
g = gains_rho(dB, W, bm, rho, DLS[j0:j0+1])
agg = np.sqrt(np.mean(g[0] ** 2)) if name == 'res_500' else np.median(g[0])
ev[name] = MEAS_OLD[name] + 20 * np.log10(agg / GO[name])
print(' ' + ' '.join(f'{n[:5]}:{v:+.2f}' for n, v in ev.items()))
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Phase B step 2: temporal detector-dynamics scan (offline).
Variants with time memory applied to FULL trajectory (state settles before
metric window), then validated pipeline (window, -6dB core, median/rms).
"""
import numpy as np
exec(open('/tmp/opencode/phaseA_grid_fast.py').read().split("# old-law reference gains")[0])
FULL = {}
for name, traj, dump, bm in [
('res_500','traj_res500.bin','dump_res_new.bin',85),
('al_12','traj_al12.bin','dump_t1k.bin',85),
('al_24','traj_al24.bin','dump_t1k.bin',85),
('t1k_1000','traj_t1k.bin','dump_t1k.bin',85)]:
FULL[name] = load_traj('/tmp/opencode/'+traj)
GO = {}
for name,(dB,W,bm,_) in DATA.items():
c_old = np.clip((dB+13.78)/82.07, 0, 1)**0.344*4.2
g = gains_batch(dB, W, bm, np.array([0.]), np.array([0.]), np.array([99.]), C_pre=c_old)
GO[name] = float(g[0].mean()) if name=='res_500' else float(np.median(g[0]))
def t_hold(T, b): # linear peak-hold decay
out = T.copy()
for t in range(1, len(T)):
out[t] = np.maximum(T[t], b*out[t-1])
return out
def t_dbdecay(T, r): # dB-domain peak decay r dB/frame
db = np.log10(np.maximum(T, 1e-12))*20.0
for t in range(1, len(db)):
db[t] = np.maximum(db[t], db[t-1]-r)
return 10**(db/20)
def t_ema(T, a): # EMA in dB domain
db = np.log10(np.maximum(T, 1e-12))*20.0
out = db.copy()
for t in range(1, len(db)):
out[t] = a*db[t] + (1-a)*out[t-1]
return 10**(out/20)
def eval_tf(fn):
out = {}
for name,(dB,W,bm,_) in DATA.items():
Tt = fn(FULL[name])[WIN[name][0]:WIN[name][1]]
dBp = np.log10(np.maximum(Tt, 1e-12))*20.0
if name != 'res_500':
lv = dBp[:, bm]; keep = dBp[lv >= lv.max()-6]
else:
keep = dBp
g = gains_batch(np.ascontiguousarray(keep), W, bm,
np.array([1.8]), np.array([0.11]), np.array([99.]))
agg = np.sqrt(np.mean(g**2, axis=1)) if name=='res_500' else np.median(g, axis=1)
out[name] = MEAS_OLD[name] + 20*np.log10(float(agg[0])/GO[name])
return out
VARS = [('none', lambda T: T)]
for b in [0.8, 0.9, 0.95, 0.99]: VARS.append((f'hold b={b}', lambda T, b=b: t_hold(T, b)))
for r in [0.25, 0.5, 1.0, 2.0]: VARS.append((f'dbdec r={r}', lambda T, r=r: t_dbdecay(T, r)))
for a in [0.3, 0.5, 0.7]: VARS.append((f'ema a={a}', lambda T, a=a: t_ema(T, a)))
print(f'{"variant":>12} ' + ' '.join(f'{n:>9}' for n in DATA) + ' rms')
for lbl, fn in VARS:
e = eval_tf(fn)
tot = np.sqrt(sum(v*v for v in e.values())/len(e))
print(f'{lbl:>12} ' + ' '.join(f'{v:+9.2f}' for v in e.values()) + f' {tot:.2f}')
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""rpp_setparam.py — rewrite PARAM values inside a REAPER .rpp soothe2 state chunk.
!!! CAVEAT (NOTES_LEVEL 22c): the XML <PARAM> list is a decorative UI-restore copy,
NOT the VST3 state source. Editing values here does NOT change plugin audio behaviour
(plugin falls back to defaults on any length mismatch). For real param changes use
setparam.lua (TrackFX_SetParam bridge). This tool is kept for format surgery only.
Block layout (joined from N consecutive base64 lines):
[u32 A=len(rest)][u32 ver=1]['VC2!'][u32 B=len(xml)][xml][tail: JUCEPrivateData...]
Both length fields MUST be updated when xml size changes, otherwise the plugin
silently rejects the state and falls back to defaults.
Usage:
python3 scripts/rpp_setparam.py in.rpp out.rpp depth=1.0 release=0.5
"""
import re, base64, struct, sys
def find_block(lines):
idx = [i for i, ln in enumerate(lines) if re.fullmatch(r"[A-Za-z0-9+/=]{40,}", ln.strip())]
start = None
for i in idx:
if base64.b64decode(lines[i].strip())[:1] == b"\x95":
start = i; break
if start is None:
raise SystemExit("no state block found")
end = start
while end + 1 < len(lines) and re.fullmatch(r"[A-Za-z0-9+/=]{40,}", lines[end + 1].strip()):
end += 1
return start, end
def main():
inp, outp = sys.argv[1], sys.argv[2]
sets = dict(kv.split("=", 1) for kv in sys.argv[3:])
lines = open(inp).read().splitlines()
start, end = find_block(lines)
buf = b"".join(base64.b64decode(lines[k].strip()) for k in range(start, end + 1))
a, ver, magic, blen = struct.unpack_from("<II4sI", buf, 0)
i = buf.find(b"<?xml")
head, xml, tail = buf[:i], buf[i:i + blen], buf[i + blen:]
for k, v in sets.items():
pat = f'<PARAM id="{k}" value="'
j = xml.find(pat.encode())
assert j >= 0, f"param {k} not found"
v0 = j + len(pat)
e = xml.index(b'"/>', v0)
print(f" {k}: {xml[v0:e].decode()} -> {v}")
xml = xml[:v0] + v.encode() + xml[e:]
new_blen = len(xml)
new_buf = struct.pack("<II4sI", a - blen + new_blen, ver, magic, new_blen) + xml + tail
ind = re.match(r"\s*", lines[start]).group(0)
width = max(len(lines[k]) - len(ind) for k in range(start, end + 1))
enc = base64.b64encode(new_buf).decode("ascii")
wrapped = [ind + enc[j:j + width] for j in range(0, len(enc), width)]
lines[start:end + 1] = wrapped
open(outp, "w").write("\n".join(lines) + "\n")
print(f"wrote {outp} (block {end-start+1}->{len(wrapped)} lines, xml {blen}->{new_blen})")
if __name__ == "__main__":
main()
+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())
+44
View File
@@ -0,0 +1,44 @@
-- setparam.lua : set soothe2 FX params by name (normalized 0..1), save-as, quit.
-- Env: S2_SET "name=value;name=value" (value normalized 0..1)
-- S2_OUT full path to save the modified project copy to
local spec = os.getenv("S2_SET") or ""
local outpath = os.getenv("S2_OUT")
local log = io.open("/tmp/opencode/setparam.log", "w")
log:setvbuf("line")
local tr = reaper.GetTrack(0, 0)
if tr == nil then
log:write("NO TRACK\n") ; log:close() ; return
end
local function find_idx(name)
local np = reaper.TrackFX_GetNumParams(tr, 0)
for p = 0, np - 1 do
local _, pn = reaper.TrackFX_GetParamName(tr, 0, p, "")
if pn:lower() == name:lower() then return p end
end
return nil
end
for pair in spec:gmatch("[^;]+") do
local name, val = pair:match("^(.-)=(.-)$")
val = tonumber(val)
local idx = find_idx(name)
if idx == nil then
log:write(string.format("param %-16s NOT FOUND\n", name))
else
reaper.TrackFX_SetParam(tr, 0, idx, val)
local rv = reaper.TrackFX_GetParam(tr, 0, idx)
local _, fmt = reaper.TrackFX_GetFormattedParamValue(tr, 0, idx, "")
log:write(string.format("set %-16s -> raw=%.6f fmt=%s\n", name, rv, fmt))
end
end
if outpath and outpath ~= "" then
reaper.Main_SaveProjectEx(0, outpath, 0)
log:write("saved " .. outpath .. "\n")
end
log:close()
local t0 = reaper.time_precise()
while reaper.time_precise() - t0 < 2 do reaper.defer(function() end) end
reaper.Main_OnCommand(40004, 0) -- File: Quit REAPER