diff --git a/dump_params.lua b/dump_params.lua new file mode 100644 index 0000000..c88478c --- /dev/null +++ b/dump_params.lua @@ -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 diff --git a/handoff/NOTES_LEVEL.md b/handoff/NOTES_LEVEL.md index c49b517..fdb94a8 100644 --- a/handoff/NOTES_LEVEL.md +++ b/handoff/NOTES_LEVEL.md @@ -1435,3 +1435,49 @@ FUN_180529fe0 (Soothe2Module::vtbl) НЕ ссылается на {+0x168, канон не меняем (корпус держит), но источник правды теперь внутри аудио-модуля. 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 `` секция НЕ источник 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. diff --git a/scripts/rpp_setparam.py b/scripts/rpp_setparam.py new file mode 100644 index 0000000..0f5cad0 --- /dev/null +++ b/scripts/rpp_setparam.py @@ -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 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("', v0) + print(f" {k}: {xml[v0:e].decode()} -> {v}") + xml = xml[:v0] + v.encode() + xml[e:] + new_blen = len(xml) + new_buf = struct.pack("{len(wrapped)} lines, xml {blen}->{new_blen})") + + +if __name__ == "__main__": + main() diff --git a/setparam.lua b/setparam.lua new file mode 100644 index 0000000..f521cba --- /dev/null +++ b/setparam.lua @@ -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