Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5ef81667c | ||
|
|
7fac8b2a4f | ||
|
|
c222e054ca |
@@ -107,8 +107,10 @@ scale → LUT level-domain (t^γ·MULT, γ=0.344 decomp / MULT=4.2 placeholder)
|
|||||||
2. `combine/аккумулятор 0x5407c8`: семантика декодирована (21b), но acc/f6f8 НЕ имеют
|
2. `combine/аккумулятор 0x5407c8`: семантика декодирована (21b), но acc/f6f8 НЕ имеют
|
||||||
однополосного консюмера — искать точку потребления (межполосный каскад). ВНИМАНИЕ:
|
однополосного консюмера — искать точку потребления (межполосный каскад). ВНИМАНИЕ:
|
||||||
`FUN_1805316e0` = writer коэффициентов, НЕ комбинер масок.
|
`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 импорт.
|
4. PRNG-пролог (LCG+LUT → fVar30) — залочен (fVar30=1 при live state 112), dry/wet rnd импорт.
|
||||||
5. Бит-экзактный exp2 (0x26b820); FFT-conv понижен до P3 (окно near-flat, NOTES:18c);
|
5. Бит-экзактный exp2 (0x26b820); FFT-conv понижен до P3 (окно near-flat, NOTES:18c);
|
||||||
SR-геометрия 48k/4096 сделана (render48k).
|
SR-геометрия 48k/4096 сделана (render48k).
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ t1k/al НЕ закрыты самим по себе — см. §0 и Шаг 7.
|
|||||||
BLK-блока даёт спад am в ~последних 0.06s (косметика, на метрику почти не влияет).
|
BLK-блока даёт спад am в ~последних 0.06s (косметика, на метрику почти не влияет).
|
||||||
|
|
||||||
### Шаг 7 — BandConfig A/B/γ (level-path ctx+0x188) live-захват под конкретные конфиги
|
### Шаг 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)
|
Структурная LUT-кривая `FUN_180563a60` (A/B/γ). Снято для render_long (A=−24/B=28/γ=1)
|
||||||
и t1kq (то же), но для остальных тестов не захвачено. Метод автоматизирован
|
и t1kq (то же), но для остальных тестов не захвачено. Метод автоматизирован
|
||||||
(NOTES_CAPTURE.md). Захватить для t1k_b1f / al / dual-конфигов → реальные A/B/γ → это
|
(NOTES_CAPTURE.md). Захватить для t1k_b1f / al / dual-конфигов → реальные A/B/γ → это
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
2. Step 7 live capture ctx+0x188 (constants + possible content-dependent
|
||||||
branch), NOTES_CAPTURE.md method.
|
branch), NOTES_CAPTURE.md method.
|
||||||
3. combine consumer hunt (multiband cascade) for dual/comb.
|
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.03–1.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((dB−A)/(B−A));
|
||||||
|
γ==1 → t; flag==0 → t^γ; flag!=0 → 0.5·(1+sign(2t−1)·|2t−1|^γ).
|
||||||
|
|
||||||
|
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=buflen−16, 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.
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
-- play.lua : realtime transport playback to keep the DSP host's audio callback
|
-- 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.
|
-- 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
|
local keep_secs = 300
|
||||||
|
|
||||||
reaper.Main_OnCommand(1007, 0) -- Transport: Play (realtime audio)
|
reaper.Main_OnCommand(1007, 0) -- Transport: Play (realtime audio)
|
||||||
local t0 = reaper.time_precise()
|
local t0 = reaper.time_precise()
|
||||||
while reaper.time_precise() - t0 < keep_secs do
|
|
||||||
reaper.Sleep(200)
|
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
|
end
|
||||||
reaper.OnStopButton()
|
|
||||||
|
reaper.defer(keepalive)
|
||||||
|
|||||||
@@ -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}')
|
||||||
@@ -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}')
|
||||||
@@ -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}')
|
||||||
@@ -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))
|
||||||
@@ -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()))
|
||||||
@@ -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}')
|
||||||
@@ -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()
|
||||||
@@ -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())
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user