Compare commits
89
Commits
b5a5afbd16
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
627c35dd53 | ||
|
|
3fe44255fa | ||
|
|
a96e46bb69 | ||
|
|
a562f6f56a | ||
|
|
c85b888745 | ||
|
|
a881ee280a | ||
|
|
2f854cd1da | ||
|
|
e4c53480ad | ||
|
|
197f5edea4 | ||
|
|
b41d7a40cb | ||
|
|
f2cc0aeaa9 | ||
|
|
2201a57ffa | ||
|
|
96816f9921 | ||
|
|
2a295af6f3 | ||
|
|
9ed22d3476 | ||
|
|
e343b0a0d0 | ||
|
|
e9d6f2ca72 | ||
|
|
59fcb7eb56 | ||
|
|
9d10169157 | ||
|
|
c23646d74f | ||
|
|
d80137c31d | ||
|
|
281f4079c2 | ||
|
|
22ff0e3ee9 | ||
|
|
c393b6f6be | ||
|
|
7b479f5357 | ||
|
|
359aaeb7cb | ||
|
|
a208d1b27e | ||
|
|
a8191d4bc9 | ||
|
|
6bf2363c1a | ||
|
|
8fbbb66e3c | ||
|
|
4411e617a6 | ||
|
|
6f32b26d6a | ||
|
|
d88e8ab7bf | ||
|
|
1d6c5a4355 | ||
|
|
c7d6fde2dc | ||
|
|
e39c97e014 | ||
|
|
a7e1a661bd | ||
|
|
9311e7397f | ||
|
|
85317ed674 | ||
|
|
5e8c3d8af4 | ||
|
|
d025fa5a23 | ||
|
|
04c51e5be0 | ||
|
|
ab9334f8ab | ||
|
|
031f7d21af | ||
|
|
69a6809a80 | ||
|
|
d7089c62fc | ||
|
|
f40f41ef71 | ||
|
|
f3e11d410a | ||
|
|
8aa25b36a8 | ||
|
|
968e90e65f | ||
|
|
5e9c41ad2b | ||
|
|
746b51c047 | ||
|
|
803c100091 | ||
|
|
febda55234 | ||
|
|
3411e9b42e | ||
|
|
f73724fee7 | ||
|
|
2eb3b690c9 | ||
|
|
bbf3cf044b | ||
|
|
09a0bfb63b | ||
|
|
e76b0ba8a4 | ||
|
|
b4d75f4d22 | ||
|
|
588d2dcc36 | ||
|
|
4ed3481166 | ||
|
|
8805a8f183 | ||
|
|
d7cbab3e4c | ||
|
|
1ea4bf6480 | ||
|
|
f689023089 | ||
|
|
575d26a771 | ||
|
|
4d78785f0b | ||
|
|
b6e7fdc289 | ||
|
|
f68081f694 | ||
|
|
c18f4b3ef4 | ||
|
|
7202b8d6a4 | ||
|
|
943e781720 | ||
|
|
60421c32c9 | ||
|
|
6bc0120286 | ||
|
|
c69257a551 | ||
|
|
50d7ab0d05 | ||
|
|
3c5e276fc5 | ||
|
|
f0cfec8af7 | ||
|
|
c2da7495c0 | ||
|
|
a281f6a721 | ||
|
|
d97dcffaa7 | ||
|
|
e9125d4024 | ||
|
|
a79c8f4934 | ||
|
|
17f25089c6 | ||
|
|
5c29e2cd4f | ||
|
|
e958b3d3d6 | ||
|
|
dcfe7774f3 |
@@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
# --- расшифрованные дизассемблы декомпа: НУЖНО трекать (ценные артефакты) ---
|
# --- расшифрованные дизассемблы декомпа: НУЖНО трекать (ценные артефакты) ---
|
||||||
!handoff/
|
!handoff/
|
||||||
|
!handoff/archive/
|
||||||
|
!handoff/archive/*.md
|
||||||
!handoff/nls_dasm/
|
!handoff/nls_dasm/
|
||||||
!handoff/nls_dasm/*.dis
|
!handoff/nls_dasm/*.dis
|
||||||
!handoff/nls_dasm/*.bin
|
!handoff/nls_dasm/*.bin
|
||||||
|
|||||||
@@ -1,28 +1,8 @@
|
|||||||
# AGENTS.md — guide for AI agents working in this repo
|
# AGENTS.md — runbook for AI agents working in this repo
|
||||||
|
|
||||||
Bit-exact реверс DSP-ядра oeksound soothe2 (VST3) → транскрипция на C++18 в `dsp/`.
|
> **Статус → `README.md:13` (единственный источник TOTAL).** Этот файл — только runbook: сборка, метрика, env-флаги, tooling hazard.
|
||||||
Полное журналирование — в `handoff/NOTES_LEVEL.md`, `handoff/NOTES_TWIN.md`,
|
> Полный журнал — `handoff/NOTES_LEVEL_INDEX.md` → `handoff/NOTES_LEVEL.md` (живая голова) + `handoff/archive/`.
|
||||||
`handoff/NOTES_CAPTURE.md`, `roadmap.md`.
|
> Карта метода — `handoff/BLOCKMAP_529fe0.md`. ЭТОТ ФАЙЛ ЧИТАЙ ПЕРВЫМ.
|
||||||
|
|
||||||
> **Текущая фаза (2026-08-25, после 24kk2):** ПРИМЕНЕНИЕ ДЕКОДИРОВАНО ФОРМУЛАМИ:
|
|
||||||
> аудио = побиновное умножение кадра на вещественную маску `10^(−cut_D/20)`,
|
|
||||||
> `cut_D = α·ln1p(lvl_raw/β)+c` (+Δ у вторых пиков). Калибровки (rms ≤0.016 дБ):
|
|
||||||
> dual(q0.5,s12,2 тона)=3.2193/0.4927/+0.54; fc1000(1 тон,q0.5)=1.6151/0.3645/+0.48;
|
|
||||||
> fc500(1 тон)=1.1530/0.4038/+0.33. lvl_raw — НАШ фронтенд (float-parity ✓).
|
|
||||||
> Слой STFT = БЕЗ синтез-окна (`RT_SYN=1`). **dual-корпус 0.193 max 0.438**
|
|
||||||
> (канон 3.264); канон TOTAL 2.286 нетронут. ГЕЙТ СМЕНЫ КАНОНА = BIT EXACT
|
|
||||||
> (решение пользователя: все параметры прослежены до декомпа + шумовой пол корпуса).
|
|
||||||
>
|
|
||||||
> Ключевые факты: q НЕ влияет на закон (доказано 24kk); sens линейно через
|
|
||||||
> lvl_raw; далёкий контент не влияет — смешение шаблонно-локальное (24ll);
|
|
||||||
> аудио = exp(deepest-scratch)+const; буфер FIR@540668 = мин.-фазовое
|
|
||||||
> представление (exp(s−iH(s))) — не для транскрипции. Тела bigkernel'ов:
|
|
||||||
> 1803a06a0/180296c80/180323f20/1802dc0e0 (x87 exp-семейство, 24jj).
|
|
||||||
>
|
|
||||||
> ОСТАТОК: каскадный симулятор шагов 9–19 по dataflow (24hh/24ii) →
|
|
||||||
> параметризация α(контент)/fc через campaign.py (датасеты готовы) →
|
|
||||||
> Δ-правило из pre-combine → полный корпус. Журнал: NOTES_LEVEL 24j–24kk2;
|
|
||||||
> карта метода: handoff/BLOCKMAP_529fe0.md. ЭТОТ ФАЙЛ ЧИТАЙ ПЕРВЫМ.
|
|
||||||
|
|
||||||
## Золотое правило (обязательно)
|
## Золотое правило (обязательно)
|
||||||
1. **Цель — bit-exact реверс кода**, НЕ эмпирическая подгонка кривых. Каждый параметр
|
1. **Цель — bit-exact реверс кода**, НЕ эмпирическая подгонка кривых. Каждый параметр
|
||||||
@@ -53,7 +33,7 @@ python3 scripts/corpus_structural.py # стру
|
|||||||
python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json
|
python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json
|
||||||
|
|
||||||
# отдельные модули (bit-exact черные проверки)
|
# отдельные модули (bit-exact черные проверки)
|
||||||
cmake --build dsp/build --target twin_check tables_check fftconv_check vlog_check leveltrack_check levelpath_check fn529fe0_check exp2_check
|
cmake --build dsp/build --target twin_check tables_check fftconv_check vlog_check leveltrack_check levelpath_check fn529fe0_check exp2_check vlaw_check
|
||||||
./dsp/build/twin_check # float-parity twin-резонатора
|
./dsp/build/twin_check # float-parity twin-резонатора
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -82,77 +62,21 @@ err = db(ta(out,1000)/ta(ref,1000)) # err в dB, цель <0.1
|
|||||||
Тестовый корпус (`/home/m/soothe-bt/`, вне git): `tone1kq.wav`(вход),
|
Тестовый корпус (`/home/m/soothe-bt/`, вне git): `tone1kq.wav`(вход),
|
||||||
`t1kq_b1f_<fc>.wav` (рефы), `comb.wav`/`comb_ref.wav` (мультиполосный).
|
`t1kq_b1f_<fc>.wav` (рефы), `comb.wav`/`comb_ref.wav` (мультиполосный).
|
||||||
|
|
||||||
## Текущее состояние (2026-08-21, P4)
|
|
||||||
|
|
||||||
**ДВА канона в `dsp/framed_model.cpp`:**
|
|
||||||
|
|
||||||
1. **BRIDGE** (эмпирическая погона, путь framed_test 44.1k, NOTES:147):
|
|
||||||
```
|
|
||||||
am_k: smoothed amp (2|X|/wsum, att~11ms/trel~80ms)
|
|
||||||
xv = log10(am_k / res_k) res_k = |2B/A| twin (min@band centre)
|
|
||||||
C = G·LUT(xv) + W·warp(f_k)^A G/W/A = 0.9963/0.3335/0.9807 (fit)
|
|
||||||
gain_k = (1−C) · res_k^rp(Q) rp(Q) = 0.0275·Q^0.2159
|
|
||||||
LUT = monotone cubic (Fritsch–Carlson) через joint-fit узлы (al_* + B.11 anchors)
|
|
||||||
```
|
|
||||||
2. **СТРУКТУРНАЯ цепь FUN_180529fe0** (`process_band_structural`, внутренняя сетка
|
|
||||||
48000/4096 через `dsp/build/render48k`, NOTES_LEVEL:820-840 + апдейты 20j–21c):
|
|
||||||
```
|
|
||||||
scale → LUT level-domain (t^γ·MULT, γ=0.344 decomp / MULT=4.2 placeholder)
|
|
||||||
→ IIR1 → IIR2 → mirror → blend(0.8)/exp2(−lvl) → combine(acc-update, без консюмера)
|
|
||||||
→ dual-warp(kBand768·kWarp·res^rp) → IIR3 bidirectional ×2 → dry/wet(identity)
|
|
||||||
```
|
|
||||||
- **METRIC CRITICAL**: рефы 24-bit НЕЛЬЗЯ читать 16-bit кодеком (даёт phantom −53 dB);
|
|
||||||
`render_parity.load` (sw handling) + окно 3.5s — канон. dual honest ref = −10.2 dB FLAT.
|
|
||||||
- **Broadband-hypothesis ОПРОВЕРГНУТА** (NOTES:2026-08-20s): редукция per-bin.
|
|
||||||
- **Честные результаты**: bridge корпус mean 1.594 (comb 10.15 сломан); структурная цепь:
|
|
||||||
comb **6.12** и res **0.44** — лучше bridge; t1kq 0.77 / t1k 2.11 / al 0.99 / dual 3.26 — хуже.
|
|
||||||
Полная таблица: BITEXACT_PLAN §0 / NOTES_LEVEL:21c.
|
|
||||||
- **ГЛАВНЫЙ ОТКРЫТЫЙ РАЗРЫВ**: насыщение кривой редукции — реальная упирается в C_max≈0.70,
|
|
||||||
exp2(−lvl) не ограничен (dual@500: константный −6.7 dB при всех q). Лечится Шагом 7
|
|
||||||
(live-захват A/B/γ) или статическим hunt'ом clamp'а в level-пути. Слепая подкрутка
|
|
||||||
γ/MULT исчерпана (свип 21c).
|
|
||||||
- **21d — закон редукции найден, канон НЕ сменён**: эффективное ослабление аффинно
|
|
||||||
в dB(lvl) (маска ≈ 0.8·lvl^−0.354; al-свееп ±0.13 dB). Scalar-семейство (X0,S,floor,
|
|
||||||
CMAX) не закрывает тон+шум одновременно → res/dual регресс у любой точки; ищем
|
|
||||||
контент-зависимый механизм. Инструментарий RT_DUMP_BIN / RT_DUMP_ALL в коде (opt-in).
|
|
||||||
|
|
||||||
### Открытые пробелы (после 22z; детали — NOTES_LEVEL 22s–22z)
|
|
||||||
1. **Семантика входов `bands[]` в FUN_180529fe0** — главный вопрос. Отвергнуто:
|
|
||||||
lvl=f(am,res^α) с единым законом [22x, теорема], форма-постобработка маски [22w],
|
|
||||||
спрединг IDFT→окно→DFT [22z]. Живой захват: кривая редукции R=1/mask @слот
|
|
||||||
0x5407f8 (пик следует за fc полосы), acc/f6f8 нули в стационаре.
|
|
||||||
Пути: (а) декод тел conv 0x1802a24c0 + complex-op шагов с буферами
|
|
||||||
0x540548/550/598; (б) синхронная трасса буферов в аудио-колбеке.
|
|
||||||
2. dual-таблица по q (22x): cut@центра константен при любом q, Δcut скайрта ≈
|
|
||||||
линейно по −ln res с b≈2.25 — геометрия twin-шаблона на сетке 0.25 Гц.
|
|
||||||
3. FFT-conv: последовательность вызовов декодирована (BLOCKMAP/22z), тело
|
|
||||||
0x1802a24c0 (AVX2) и complex-op шаги — не расшифрованы.
|
|
||||||
4. Бит-экзактный exp2 (0x26b820); бит-точная DSP-FFT (P3); стерео M8 (P5) — отложены.
|
|
||||||
5. Источник констант RT_LAWAFFINE A/S в декомпе не найден (помечено EMPIRICAL).
|
|
||||||
|
|
||||||
## Структура ключевых файлов
|
## Структура ключевых файлов
|
||||||
- `dsp/framed_model.{cpp,hpp}` — C++ порт mask-apply цепи: bridge-канон + структурная
|
- `dsp/framed_model.{cpp,hpp}` — C++ порт mask-apply цепи (ГЛАВНЫЙ файл).
|
||||||
`process_band_structural` (ГЛАВНЫЙ активный файл).
|
- `dsp/render48k.cpp` — пайплайн 48000/4096 (resample → chain → resample).
|
||||||
- `dsp/render48k.cpp` — пайплайн внутренней сетки 48000/4096 (resample → структурная цепь → resample).
|
- `dsp/twin.{cpp,hpp}` — twin FUN_180535880 (float-parity).
|
||||||
- `dsp/framed_test.cpp` — CLI bridge-рендер входа (метрика см. выше).
|
- `scripts/corpus.py` / `corpus_structural.py` — харнессы + guard `--compare`.
|
||||||
- `scripts/corpus.py` — корпусный харнесс bridge + guard `--compare baseline_bridge.json`.
|
- `handoff/NOTES_LEVEL.md` — живой журнал (голова 24mm5+, хвост → `archive/`).
|
||||||
- `scripts/corpus_structural.py` — тот же корпус через render48k + режим `--vs-bridge`.
|
- `handoff/NOTES_LEVEL_INDEX.md` — оглавление журнала.
|
||||||
- `dsp/{twin,levelpath,freqpath,fftconv,vlog,leveltrack,fn529fe0}.cpp` — расшифрованные модули.
|
- `handoff/BLOCKMAP_529fe0.md` — блок-карта FUN_180529fe0.
|
||||||
- `dsp/rt_mask_tables.{hpp,cpp}`, `rt_weights.{hpp,cpp}` — live-таблицы.
|
- `handoff/nls_dasm/` — 134 дизассембла.
|
||||||
- `handoff/NOTES_LEVEL.md` — полный журнал уровня/маски (самый актуальный).
|
|
||||||
- `handoff/nls_dasm/` — 134 дизассембла декомпа.
|
|
||||||
- `/tmp/consumers_out.txt` — полный decomp `FUN_180529fe0` (уникален, вне git).
|
|
||||||
- `/tmp/snap_rt.bin` — live-снимок (ctx 0x2370040, SR 48000).
|
|
||||||
- `handoff/BLOCKMAP_529fe0.md` — полная блок-карта FUN_180529fe0 по raw asm (22v).
|
|
||||||
- `scripts/lawfit22r.py` — офлайн реплей-сим пайплайна (~2 c корпус), фиты законов.
|
|
||||||
- `scripts/resalpha.py` — сбор трактов + теорема об отсутствии α (22x).
|
|
||||||
- `scripts/dualtrace.py` + `play_loop.lua` — живой захват ctx при realtime-playback (22y).
|
|
||||||
|
|
||||||
## Env-флаги экспериментов (24j–24kk2)
|
## Env-флаги экспериментов (актуальные)
|
||||||
|
|
||||||
| флаг | действие |
|
| флаг | действие |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `RT_VLAW=1` | закон применённой стадии: mask=10^(−(α·ln1p(lvl/β)+c)/20) |
|
| `RT_VLAW=1` | закон применённой стадии: `mask=10^(−(α·ln1p(lvl/β)+c)/20)` |
|
||||||
| `RT_SYN=1` | STFT БЕЗ синтез-окна (найденный слой плагина) |
|
| `RT_SYN=1` | STFT БЕЗ синтез-окна (найденный слой плагина) |
|
||||||
| `RT_WIN=0/1/2` | окно анализа: sym-hann / periodic / rect |
|
| `RT_WIN=0/1/2` | окно анализа: sym-hann / periodic / rect |
|
||||||
| `RT_NOWARP=1` | отключить warp-модуляцию маски |
|
| `RT_NOWARP=1` | отключить warp-модуляцию маски |
|
||||||
@@ -160,26 +84,50 @@ scale → LUT level-domain (t^γ·MULT, γ=0.344 decomp / MULT=4.2 placeholder)
|
|||||||
| `RT_IIR12=0` | отключить частотные IIR1/2 (КРИТИЧНО с RT_VLAW — иначе размывают дипы) |
|
| `RT_IIR12=0` | отключить частотные IIR1/2 (КРИТИЧНО с RT_VLAW — иначе размывают дипы) |
|
||||||
| `RT_DUMP_BIN=<f>` (+`RT_DUMP_FRAME=N`) | дамп тракта бина N: am/res/lvl_raw/band_level/prewarp/w |
|
| `RT_DUMP_BIN=<f>` (+`RT_DUMP_FRAME=N`) | дамп тракта бина N: am/res/lvl_raw/band_level/prewarp/w |
|
||||||
| `RT_VDBG=1` | stderr-печать vlaw-вычислений |
|
| `RT_VDBG=1` | stderr-печать vlaw-вычислений |
|
||||||
|
| `RT_FAITHFUL=1` | faithful-цепь `dsp/fnfaith.cpp` (детекторный каскад) |
|
||||||
|
| `RT_FIRCONV=1/3` | FIR-применение (1=complex-mul, 3=`1.019·mask^1.8345`) |
|
||||||
|
| `RT_CASC=1` | цепь 9–19 (`chain_9_19`, IIR4×2 double + FIR min-phase, gated) |
|
||||||
|
| `RT_IIR4_GEN=1` | генератор IIR4 (FUN_180533340) vs proxy `kIIR_A1/B1` |
|
||||||
|
| `RT_IIR4_C/TAU/P/MULT/SR` | параметры генератора IIR4 (1000/1200/0.5/360/48000) |
|
||||||
|
| `RT_FIR=0/1` | пуск FIR min-phase (default 1) |
|
||||||
|
| `RT_FIR_Q=x` | показатель FIR (default 0.8002203702926636, live `.rdata`) |
|
||||||
|
| `RT_KMAP_FC=1` | fc-фактор k-mapping через W_eq (default off) |
|
||||||
|
| `RT_DELTA_DIST=x` | Δ distance factor (template-local gain, default 0=flat) |
|
||||||
|
| `RT_DELTA_STATE=1` | STATE-dependent Δ (опен-ин, wired, empty) |
|
||||||
|
| `RT_DBG_CASC=1` | gate DBG_CASC fprintf (per-frame spam otherwise) |
|
||||||
|
|
||||||
Полный набор dual-решения: `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0`.
|
Полный набор dual-решения: `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0`.
|
||||||
|
|
||||||
## Инструменты сессии 24j–24kk2
|
## Инструменты
|
||||||
|
|
||||||
| скрипт | назначение |
|
| скрипт | назначение |
|
||||||
|--------|-----------|
|
|--------|-----------|
|
||||||
| `scripts/rendersnap2.py <rpp> [cap] [outdir]` | мягкий STOP-снаппер: слоты FIR/scratch/bands/R + скалярный банк + t_snap; НЕ убивает reaper; RENDER_FILE из rpp (не удалять чужие рефы!) |
|
| `scripts/rendersnap2.py <rpp> [cap] [outdir]` | мягкий STOP-снаппер (RENDER_FILE из rpp!) |
|
||||||
| `scripts/campaign.py <base> <fc> <q> <sens> <in_prefix> <drives> <out>` | ячейка параметризации: клоны rpp+рефы (~8 мин) |
|
| `scripts/campaign.py <base> <fc> <q> <sens> <in> <drives> <out>` | ячейка параметризации (~8 мин) |
|
||||||
| `scripts/disasm_func.py <VA> [len]` | capstone-дизасм с инлайн-резолвом RIP-констант |
|
| `scripts/disasm_func.py <VA> [len]` | capstone-дизасм с RIP-константами |
|
||||||
| `scripts/iat_name.py` | рантайм-резолв импортов bigkernel'ов через PE-экспорты (SIGSTOP!) |
|
| `scripts/iat_name.py` | рантайм-резолв импортов (SIGSTOP!) |
|
||||||
| `scripts/probe_states.py` / `probe_mem.py` / `dump_dispatch.py` | живые state-заголовки / память / таблицы диспатча |
|
| `scripts/probe_states.py` / `probe_mem.py` / `dump_dispatch.py` | live state/память/таблицы |
|
||||||
| `scripts/hunt2.py` | перебор всех ctx-инстансов |
|
| `scripts/hunt2.py` | перебор ctx-инстансов |
|
||||||
| `scripts/scan_pairs.py`, `scan_lutsub.py` | диагностика памяти по сигнатурам float-пар |
|
| `scripts/scan_pairs.py`, `scan_lutsub.py` | диагностика памяти |
|
||||||
|
|
||||||
Датасеты кампании: `/tmp/opencode/sc_{q,sens,qmap,k,f,d}*` + `tract_*` +
|
Датасеты: `/tmp/opencode/sc_{q,sens,qmap,k,f,d}*` + `tract_*` + `*.pkl/.npz` (см. `NOTES_LEVEL_INDEX.md`).
|
||||||
`{level_pairs,f_series,q_series,k_series}.pkl/.npz` (см. NOTES 24bb–24kk2).
|
Открытые пробелы → `BITEXACT_PLAN.md`.
|
||||||
|
|
||||||
|
## Архитектурная проблема chain_9_19 (WIRED, GATED)
|
||||||
|
|
||||||
|
`chain_9_19` из `dsp/fn52fe0.cpp:259` (BLOCKMAP:620-644) — per-frame цепь с **persistent ACC state** (`@ctx+0x5407c8`). Интегрирована в `framed_model.cpp:process_band_structural` через `RT_CASC=1`. IIR4×2 использует double precision (movsd/mulsd per disasm 1191), FIR min-phase бит-точен до df0 (0.0065 dB). Генератор IIR4 (`generate_iir4_coefs`, FUN_180533340) env-gated `RT_IIR4_GEN`.
|
||||||
|
|
||||||
|
**Статус:** chain wired и gated (`RT_CASC=0` default = canon untouched). Все unit checks PASS, corpus gate d=+0.000. Калибровка I/O format требует `ph*.npz` capture (rendersnap2.py) — capture proof получен (2026-09-02).
|
||||||
|
|
||||||
|
**Live-dump chain (ptrace):** chain вызывается в рантайме (DIV#0-7 на `0x1803a06a0`):
|
||||||
|
- Input: `a` = bands_curve (VLAW output, min=0, max=17.6, mean=0.048)
|
||||||
|
- `b` = tmp6f8 (step 9b accumulation, min=0, max=0.8, mean=0.8)
|
||||||
|
- 8 DIV hits на одном кадре (dual-band: 2 bands × 4 iterations?)
|
||||||
|
|
||||||
|
**Capture proof (2026-09-02):** `rendersnap2.py` снимает `ph*.npz` (слоты 0x540628/0x540678/0x540688/0x540768) через `/proc/pid/mem` без ptrace-брейкпоинтов. Данные в `/tmp/opencode/{rendersnap2_dual,snap_t1k_b1f_1000,snap_dual300,rendersnap2_comb_b1234}`. Маск-цепь работает в рендер-окне (BLOCKMAP:285 устарел).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Чистая работа
|
## Чистая работа
|
||||||
- Не коммитить: `*.bin`(дампы 112М), `*.wav/rpp`, `*.log`, `dsp/build/`, `ghidra-proj/`,
|
- Не коммитить: `*.bin`, `*.wav/rpp`, `*.log`, `dsp/build/`, `ghidra-proj/`, `dl/lib/bin/include/`, `regions*/`. См. `.gitignore`.
|
||||||
`dl/lib/bin/include/`, `regions*/`. См. `.gitignore`.
|
- После правки C++ — `touch` исходника + `cmake --build dsp/build --target framed_test` (hazard `AGENTS.md:66`) + `python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25`.
|
||||||
- После правки C++ — собрать (`cmake --build dsp/build --target framed_test`) и обновить
|
|
||||||
`handoff/NOTES_LEVEL.md`. Fasta метрику держать честной (тримнутая длина).
|
|
||||||
|
|||||||
+53
-233
@@ -1,270 +1,90 @@
|
|||||||
# BIT-EXACT PLAN — путь от dB-приближения к побайтовой парности
|
# BIT-EXACT PLAN — путь от dB-приближения к побайтовой парности
|
||||||
|
|
||||||
Статус: **НЕ bit-exact**. Мы на уровне честной dB-параллели (полный корпус mean 1.594 dB,
|
Статус: **НЕ bit-exact**. Канон — bridge `framed_test` 44.1k: **TOTAL 1.594 dB** (62 случая, trimmed 24-bit, `scripts/baseline_bridge.json`). Bit-exact достижим (F0 gate: рендеры байт-детерминированы `handoff/NOTES_LEVEL.md:1020`), но модель — погона. Читать вместе с `AGENTS.md` и `handoff/NOTES_LEVEL_INDEX.md`.
|
||||||
comb 10 dB). Bit-exact ДОСТИЖИМ (F0 gate: плагин байт-детерминирован), но модель ещё не
|
|
||||||
воспроизводит реальный DSP-путь. Этот документ — план, как туда дойти, и точка отсчёта
|
|
||||||
для любой будущей сессии. **Читать вместе с `AGENTS.md` и `handoff/NOTES_LEVEL.md`.**
|
|
||||||
|
|
||||||
> **ОБНОВЛЕНИЕ 2026-08-22 (сессии 22a–22e):**
|
> **2026-08-28 (24mm14): применение декодировано до формул.** `mask=10^(−cut_D/20)`, `cut_D=α·ln1p(lvl/β)+c [+Δ]` — три семейства, rms ≤0.016. `dual` 0.193 dB (env-gated `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0`), детекторный каскад `180529c60` + RFFT до df0 бит-точны, `γ=1.760561` из цепи. **Гейт смены канона = bit-exact** (решение `24hh2`). Остаток: 3 шага ниже.
|
||||||
> 1. Phase-B оффлайн-гипотезы (pooling/temporal/scalar-ρ) — все ОПРОВЕРГНУТЫ (22a).
|
|
||||||
> 2. Шаг 7 выполнен → премиса опровергнута: BandConfig A/B/γ = −24/28/1 у ВСЕХ конфигов,
|
|
||||||
> но это GUI-timer кривые; аудио их не читает (22b). LUT-константы структурной цепи
|
|
||||||
> помечены EMPIRICAL.
|
|
||||||
> 3. Насыщение редукции НАЙДЕНО ЖИВЬЁМ и РЕШЕНО АЛГЕБРАИЧЕСКИ (22d/e):
|
|
||||||
> hot-тон упирается в ЖЁСТКИЙ пол gain=**−20.72 dB**, pin ≥24 dB драйва;
|
|
||||||
> `gain_floor = 20·log10(blend·ln10/20)` — совпадение 0.0055 dB!
|
|
||||||
> Константа ln10/20=0.11513 @0x1824c3cd4, аудио-юзеры FUN_180529c60/52baa0/bad0/bba0.
|
|
||||||
> 4. Инфраструктура: официальный параметр-мост (`setparam.lua`), XML `<PARAM>` в RPP =
|
|
||||||
> декоративная копия (не источник стейта); depth=±18dB/trim=±24dB; WAV bext/junk
|
|
||||||
> грабли подтверждены живьём.
|
|
||||||
> Новый приоритет №1 — Шаг 9 (ниже).
|
|
||||||
>
|
|
||||||
> **ОБНОВЛЕНИЕ 2026-08-23 (сессии 22s–22z):** Шаг 9 закрыт иначе — пол решён
|
|
||||||
> алгебраически ещё в 22e; фронт сместился. Отвергнуты: скалярные законы A/S
|
|
||||||
> (насыщение ~1.87–1.93, 22s/t), двухфакторные quad/resrp (22t), точечные
|
|
||||||
> lvl=f(am,res^α) — «теорема об отсутствии α» (22x), спрединг IDFT→окно→DFT (22z).
|
|
||||||
> Новый приоритет №1 — семантика входов `bands[]` (см. AGENTS.md и NOTES_LEVEL
|
|
||||||
> 22s–22z); карта метода — handoff/BLOCKMAP_529fe0.md.
|
|
||||||
|
|
||||||
> **ОБНОВЛЕНИЕ 2026-08-25 (сессии 24j–24kk2):** ПРИМЕНЕНИЕ ДЕКОДИРОВАНО ДО ФОРМУЛ.
|
Цель фазы C: воспроизвести `FramedDetector` (= `FUN_180529fe0` mono-path) до `verify_bit_exact.py` побайтового совпадения (`t1kq_*`, `dual_*`, `comb_*`).
|
||||||
> Применённый фильтр = побиновное умножение кадра на вещественную маску
|
|
||||||
> `10^(−(α·ln1p(lvl/β)+c)/20)`; α/β/c — константы контент-семейства (таблица
|
|
||||||
> калибровок в NOTES 24ee); слой STFT без синтез-окна. dual-корпус 0.193
|
|
||||||
> (флаги RT_VLAW/SYN/NOWARP/NOIIR3/IIR12). Гейт смены канона = BIT EXACT
|
|
||||||
> (решение пользователя 24hh2). Остаток до полного покрытия: (1) каскадный
|
|
||||||
> симулятор шагов 9–19 по dataflow BLOCKMAP 24hh/24ii + тела bigkernel
|
|
||||||
> 1803a06a0 и со. (24jj); (2) k-маппинг фронтенда (twin/am формулы);
|
|
||||||
> (3) Δ-правило вторых пиков. Отвергнуто: ×1.805-свёртка, клампы параметров,
|
|
||||||
> GUI-LUT в аудио-пути, B∝am, скалярные комбинации lvl/res.
|
|
||||||
|
|
||||||
Цель фазы C (bit-exact): воспроизвести `FramedDetector` (= fn `FUN_180529fe0` mono-path)
|
|
||||||
настолько точно, что `verify_bit_exact.py` даёт побайтовое совпадение на рендерах
|
|
||||||
(`t1kq_*`, `dual_*`, `comb_*`). Ниже — конкретный порядок, что и зачем.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0. Текущее состояние (честная метрика, 24-bit, trimmed, 62 случая)
|
## 0. Текущее состояние — ЕДИНСТВЕННЫЙ ИСТОЧНИК `README.md:13`
|
||||||
|
|
||||||
| Группа | bridge mean|err| | bridge max | struct 48k mean|err| | struct max |
|
Честная trimmed метрика 62 случая (bridge vs structural 48k/4096 `corpus_structural.py`):
|
||||||
|--------|----------|-----|----------|---------|
|
|
||||||
| t1kq (fc-scan) | 0.226 | 0.500 | 0.768 | 1.013 |
|
|
||||||
| t1k (loud 0 dBFS) | 1.801 | 2.227 | 2.114 | 2.753 |
|
|
||||||
| al (level sweep) | 0.638 | 1.596 | 0.986 | 1.822 |
|
|
||||||
| res | 0.628 | 1.535 | **0.437** ✓ | 1.252 |
|
|
||||||
| dual (q-sweep) | 0.726 | 2.252 | 3.264 ✗ | 6.455 |
|
|
||||||
| comb (4-band) | **10.149** ✗ | 14.752 | **6.116** ✓ | 9.148 |
|
|
||||||
| **TOTAL** | **1.594** | 14.752 | 2.286 | 9.148 |
|
|
||||||
|
|
||||||
(struct = структурная цепь на внутренней сетке 48000/4096 через `dsp/build/render48k`,
|
| Группа | bridge mean | bridge max | structural mean | VLAW env-gated mean |
|
||||||
прогон `scripts/corpus_structural.py`, NOTES_LEVEL:21c. Bridge = `framed_test` 44.1k.)
|
|--------|-------------|------------|-----------------|---------------------|
|
||||||
|
| t1kq (fc-scan) | 0.226 | 0.500 | 0.768 | 0.426–0.852* |
|
||||||
|
| t1k (loud) | 1.801 | 2.227 | 2.114 | 0.577–0.930* |
|
||||||
|
| al (level) | 0.638 | 1.596 | 0.986 | 0.090–0.804* |
|
||||||
|
| res | 0.628 | 1.535 | **0.437** | 0.395* |
|
||||||
|
| dual (q-sweep) | 0.726 | 2.252 | 3.264 | **0.193** (max 0.438) ✓ |
|
||||||
|
| comb (4-band) | **10.149** | 14.752 | **6.116** | 2.678–4.335* |
|
||||||
|
| **TOTAL** | **1.594** | 14.752 | 2.286 | **0.870–1.931*** |
|
||||||
|
|
||||||
> **21d**: найден закон редукции — эффективное ослабление АФФИННО в dB(lvl):
|
`*` зависит от калибровки VLAW `α/β/c` (семейство dual vs fc-зависимая `AGENTS.md` больше не хранит таблицу — см. `README.md:13` и `handoff/NOTES_LEVEL.md:3919`).
|
||||||
> `x_exp2 = −0.6646 − 0.05877·dB` (маска ≈ 0.8·lvl^−0.354, γ≈decomp 0.344).
|
|
||||||
> На тонах al-свеепа линейность ±0.13 dB (42 dB диапазона!). Прекалибровка
|
|
||||||
> pre-IIR (X0=1.8/S=0.11) даёт TOTAL 2.633: t1kq/t1k/al/comb лучше, res/dual
|
|
||||||
> хуже. Grid-search семейства (X0,S,floor,CMAX) на валидированной траекторной
|
|
||||||
> модели: оптимум rms 0.75 dB, но НИ ОДНА точка семейства не закрывает тон+шум
|
|
||||||
> одновременно → канон не меняем (gate «без регресса групп»), ищем
|
|
||||||
> контент-зависимый механизм (детектор att/rel на флюктуациях / combine-
|
|
||||||
> консюмер). Инструментарий RT_DUMP_BIN/RT_DUMP_ALL оставлен opt-in.
|
|
||||||
|
|
||||||
**Корень проблемы**: активный канон — ЭМПИРИЧЕСКАЯ bridge-модель
|
Канон = bridge `1.594` (эмпирика `C=G·LUT(xv)+W·warp^A, gain=(1−C)·res^rp`). Structural `2.286` обгоняет bridge на `comb/res`, регрессирует на однополосных — форма кривой редукции неверна. VLAW закрывает `dual` (0.193) — применённый слой и каскад декодированы.
|
||||||
`C = G·LUT(xv) + W·warp(f)^A; gain = (1−C)·res^rp` (xv=log10(am/res)). Это ПОГОНА, не
|
|
||||||
транскрипция. Структурная цепочка FUN_180529fe0 перенесена в C++ на внутренней сетке
|
|
||||||
(коммиты 7bf5a4a..b2cb923): уже ОБГОНЯЕТ bridge на comb (−4 dB) и res, но регрессирует
|
|
||||||
на однополосных t1kq/t1k/al и проваливает dual@500 (константный −6.7 dB при всех q —
|
|
||||||
отсутствие НАСЫЩЕНИЯ редукции: реальная кривая упирается в C_max≈0.70, exp2(−lvl) не
|
|
||||||
ограничен). Свип γ×MULT подтвердил: текущий оптимум (0.344/4.2) лучший, слепая
|
|
||||||
подкрутка исчерпана — проблема в ФОРМЕ кривой.
|
|
||||||
|
|
||||||
> **КОРРЕКЦИЯ 22d/e**: «насыщение C_max≈0.70» старой модели = артефакт placeholder
|
|
||||||
> MULT=4.2 / Pchip cap 0.667, НЕ свойство плагина: живой плагин на ±24 dB trim
|
|
||||||
> редуцирует БЕЗ потолка (R 1.4→15.7 dB), а на горячем тоне (+6 dBFS) упирается в
|
|
||||||
> ЖЁСТКИЙ пол gain=−20.72 dB = **blend·ln10/20 точно** (Δ0.0055 dB). Механизм пола —
|
|
||||||
> в нетранскрибированном куске аудио-пути (след: FUN_180529c60, ln10/20-фактор),
|
|
||||||
> см. Шаг 9.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Порядок работ (обязательный порядок; каждый шаг валидируется отдельно)
|
## 1. Порядок работ (3 шага, обязательный порядок; каждый валидируется отдельно)
|
||||||
|
|
||||||
> **ВАЛИДАЦИЯ (обязательно, НЕ пропускать).** Чтобы не повторить регресс Phase B,
|
> **Валидация:** `cmake --build dsp/build --target framed_test` + `python3 scripts/corpus.py` + `python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25` (фейлит при регрессе группы >tol). Структурная цепь валидируется `scripts/corpus_structural.py --vs-bridge`. Каждый под-шаг коммитить отдельно.
|
||||||
> есть защитный харнесс `scripts/corpus.py` + зафиксированный bridge-базлайн
|
|
||||||
> `scripts/baseline_bridge.json` (62 случая, правильная 24-bit метрика):
|
|
||||||
> ```bash
|
|
||||||
> cmake --build dsp/build --target framed_test
|
|
||||||
> python3 scripts/corpus.py # полный корпус, текущая сборка
|
|
||||||
> python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25
|
|
||||||
> ```
|
|
||||||
> `--compare` фейлит (exit≠0), если любая группа регрессирует по mean|err| больше tol.
|
|
||||||
> Правило: структурная цепь (Шаг 1-2) должна НЕ регрессировать ниже bridge на
|
|
||||||
> однополосных (t1kq/t1k/al/res/dual) и ЖЕЛАТЕЛЬНО улучшать comb. Каждый под-шаг
|
|
||||||
> (IIR1 → blend → combine → warp → IIR3) коммитить отдельно и прогонять `--compare` —
|
|
||||||
> если конкретный под-шаг регрессирует, откатить именно его, а не всё сразу.
|
|
||||||
> Существующие `dsp/*_check.cpp` (twin/tables/leveltrack/levelpath/exp2/fftconv) —
|
|
||||||
> модульные чёрные проверки на бит-парность под-функций; тоже гонять: `cmake --build
|
|
||||||
> dsp/build` после правок.
|
|
||||||
|
|
||||||
|
### Шаг 1 — Каскадный симулятор шагов 9–19 FUN_180529fe0 (ПРИОРИТЕТ №1)
|
||||||
|
|
||||||
### Шаг 1 — Реализовать полную структурную mono-цепочку FUN_180529fe0 (КОРЕНЬ)
|
Dataflow декодирован `handoff/BLOCKMAP_529fe0.md:22v` + `handoff/NOTES_LEVEL.md:4400`:
|
||||||
Заменить bridge (эмпирическую погону) на точную транскрипцию. Реализовать в
|
|
||||||
`dsp/framed_model.cpp` (или отдельном `dsp/fn529fe0.cpp`) по NOTES_LEVEL:820-840:
|
|
||||||
```
|
```
|
||||||
1. scale: band_mask *= (fVar30/0x1a0)·0x540870·0x54088c (fVar30=1 из PRNG, locked)
|
bands[i] → LOG#1 (140980) → 52d650 (bidir #1) → vec698*= (1−[54087c]) → vec6f8+= [54087c]·0.8
|
||||||
2. IIR1: y[i]=A1[i]·acc+B1[i]·x[i] (kIIR_A1/B1, fast attack) -> f6f8
|
→ DIVIDE vec6f8/bands (1803a06a0) → vec6f8=bands−ACC (dc40) → fma ATT/REL (1fa0/1940, тройки re,im,coef)
|
||||||
3. copy f6f8 <- band
|
→ COPY (1b80) → mirror 9 + bigkernel 140b60 → EXP#1 (1409e0) + −1 → ×track (th2000) → ×warp (th2000)
|
||||||
4. IIR2: inline, kIIR_A2/B2 (slow release)
|
→ LOG#2 (140980) → IIR4×2 (bidir, log-домен) → EXP#2 → bands_final
|
||||||
5. mirror upper half = reversed lower (Hermitian)
|
→ FIR-секция: DESIGN 1802a24c0 (conv_float_a24c0.dis) → copy → RFFT pair th2180/th1a90 (buf548/598)
|
||||||
6. blend: f6f8 = 0x540698·(1−mix)+mix·0.8; mask = exp2(−level)·f6f8
|
→ FIR[1..n/2]*=2.0 → EXP 1803831c0 → WINfreq· → FIR[0]=1 → df0 track⊗FIR → mask
|
||||||
7. combine: acc[band] = 0x540678 − 0x5406f8; mirror;
|
|
||||||
+= 0x5406c8·upper; += 0x5406e8·lower; += 0x540678
|
|
||||||
8. warp: mask *= kBand768[band]; mask *= kWarp (TWO warps)
|
|
||||||
9. IIR3: inline TWICE, kIIR_A3/B3
|
|
||||||
10. dry/wet: mask = mask·(fVar30·0x540888)+(1−fVar30) (=identity сейчас)
|
|
||||||
11. FFT-conv (Шаг 4)
|
|
||||||
```
|
```
|
||||||
Критерий: на однополосных t1kq/t1k/al/res структурная цепочка должна НЕ регрессировать
|
Тела bigkernel'ов резолвлены статически `handoff/NOTES_LEVEL.md:3944` (`1803a06a0` divide, `180296c80` expf, `1803831c0` табличная кривая, `1803a06a0` etc.; `24mm2` таблица). RFFT до df0 бит-точна `24mm11`.
|
||||||
ниже bridge (т.е. mean ≤ 0.6-0.8). ВАЖНО: прошлый регресс (F2) был из-за неверного
|
|
||||||
домена (пробовали combine в bridge-финале). Здесь combine/exp2 — в ЕГО собственном
|
|
||||||
домене (reduction/exp2), как в декомпе.
|
|
||||||
|
|
||||||
### Шаг 2 — combine/аккумулятор 0x5407c8 (ПЕРЕ-СКОУП 2026-08-21)
|
**Делать:** оп-за-оп транскрипция `scripts/cascade_sim.py` (numpy-эквиваленты divide/expf с FMA-точностью) → валидация на `multi6` (6 нотчей один прогон, `handoff/NOTES_LEVEL.md:3809`) и `sc_*` дистанционной серии (rms сейчас 0.42 на угаданных формах). После структуры — C++ порт `dsp/fn529fe0.cpp` с точными полиномами.
|
||||||
Семантика декодирована на уровне thunk'ов (NOTES_LEVEL:21b, consumers_out.txt:833-885):
|
|
||||||
```
|
|
||||||
f6f8 = mask − acc (0x8d60, dst=3-й аргумент; acc НЕ перезаписывается)
|
|
||||||
mirror f6f8
|
|
||||||
f6f8_upper += kRTAtt·acc_upper ; f6f8_lower += kRTRel·acc_lower (0x3c40)
|
|
||||||
acc += mask (0x5a20, персистентный per-band аккумулятор)
|
|
||||||
```
|
|
||||||
**КРИТИЧЕСКОЕ**: в online однополосном пути обновлённые f6f8/acc НЕ имеют консюмера до
|
|
||||||
warp/IIR3/dry-wet (проверено исчерпывающим grep). Combine НЕ может влиять на single-band
|
|
||||||
вывод сам по себе. Ожидалось, что combine закроет comb — частично закрыл уже сам
|
|
||||||
структурный каркас (comb 6.1 vs bridge 10.1 без wiring). ДАЛЬНЕЙШИЙ ШАГ: найти точку
|
|
||||||
потребления acc/f6f8 (межполосный уровень или FFT-conv каскад) и только потом wire.
|
|
||||||
НЕ изобретать track→mask feedback (эмпирика, запрещено золотым правилом).
|
|
||||||
ВНИМАНИЕ: `FUN_1805316e0` — это WRITER КОЭФФИЦИЕНТОВ полос (17 case), НЕ масковый
|
|
||||||
комбинер (ошибка в старой редакции этого плана и в Шаге 8).
|
|
||||||
|
|
||||||
### Шаг 3 — Bit-exact exp2 (F5b)
|
Критерий: каскад воспроизводит `B=exp(scratch)` из `multi6` на всех 6 пиках ±0.05 dB (сейчас `rms 0.42`), затем `dual` 0.193 сохраняется без per-семейной калибровки.
|
||||||
Заменить `std::exp2`/`exp2d::exp2_dsp` на точную табличную реализацию `0x26b820`:
|
|
||||||
таблицы уже извлечены (`dsp/exp2_tables.{hpp,cpp}`, 8×16 irr + серия kExp2_big). Нужно
|
|
||||||
транскрибировать body 1:1 (Cody-Waite hi/lo, vfmadd213sd-полином, спец-ветви subnormal/
|
|
||||||
overflow). Пока не bit-exact — оставить `exp2d::exp2_dsp` (численно = std::exp2).
|
|
||||||
Критерий: `exp2_check` сравнивает протв захваченных пар точка-в-точку.
|
|
||||||
|
|
||||||
### Шаг 4 — FFT-conv 0x535a70 + FIR (ПРИОРИТЕТ ПОНИЖЕН 2026-08-21)
|
### Шаг 2 — k-маппинг фронтенда (twin/am)
|
||||||
FFT-conv сглаживает маску перед FIR (0x540658 window / freqaxis). Это последний этап
|
|
||||||
mono-цепи. `dsp/fftconv.cpp` есть, но нужна точная блоковая обработка (overlap-save как
|
|
||||||
в декомпе 0x52b550-0x52b8b5), не текущий stand-in. Критерий: маска-гейн после conv
|
|
||||||
совпадает по форме с реальным ref (сглаживание нотча, двусторонний хвост).
|
|
||||||
**ПОНИЖЕНО до P3**: NOTES_LEVEL:18c — окно 0x540658 near-flat (0.8→1.0 plateau при
|
|
||||||
N/2≥2048), эффект построения FIR на форму маски минимален при N=4096. Последовательность
|
|
||||||
уже размечена (fwd → kill mirror → fill xmm13/xmm9 → inv → complex op → fwd → window
|
|
||||||
copy → inv → FIR[0]=1.0); остались неизвестные скаляры xmm13/xmm9 и complex-op шага 4.
|
|
||||||
|
|
||||||
### Шаг 5 — Bit-exact DSP-FFT 0x140a70 (multi-week, P3)
|
`k = lvl_impl / lvl_ours` (`handoff/NOTES_LEVEL.md:3436`): `k(q≥2)=0.403` const, `k(sens)` экспонента (5.37 при s18, 22.0 при s24). При q=1.0 vs q=0.5 `k` идентичен — `q` НЕ влияет на закон (`24kk`), `sens` линейно через `lvl_raw` (`24kk`). Остаток — twin gain пол (`res` плагина ≥0.153 vs наш 0.0069) и амплитудная нормировка `am` (wsum/окно в `dsp/spectral.cpp` vs декомп `FUN_180535880` tail-calls).
|
||||||
Заменить `dsp/fft.cpp` (std::cos radix-2) на точный split-radix 2/4/8 по плану из
|
|
||||||
NOTES_LEVEL:1107-1116:
|
|
||||||
- twiddle: `DAT_182616800` sin-таблица, `sin(k·2π/1024)`, loader `FUN_180039b00` (stride 2^(10-m))
|
|
||||||
- butterfly: `FUN_18000bfc0/18000c5e0` + 0x8440 elementwise mul + `FUN_1800437c0` acc
|
|
||||||
- plan-gen: `FUN_18002f980` (рекурсия + per-log2 фактор-таблицы)
|
|
||||||
Это НЕ блокирует Шаги 1-4 (bridge/структурная цепь рендерят и с численным FFT), нужен
|
|
||||||
только для побайтовой парности FFT-conv-пути.
|
|
||||||
|
|
||||||
### Шаг 6 — Внутренняя геометрия 48000/4096 vs 44100/2048 — ✅ ПАЙПЛАЙН СДЕЛАН (2026-08-21)
|
**Делать:** декод `FUN_180535880` tail-calls + `FUN_180536300` caller (grid `NOTES_TWIN.md`) до точных `twin_coeff`/`am` формул, либо калибровка `k(q,sens,fc)` по `sc_*` + `tract_*` как таблицы с декомп-обоснованием.
|
||||||
Внутренний DSP SR=48000/N=4096 (freqaxis spacing 11.713 Hz). Хост рендера 44100/2048.
|
|
||||||
F4-тест показал, что простая resample_poly НЕ закрывает (даже хуже). Нужно: детектор
|
|
||||||
гнать на 48000/4096 (IIR-таблицы индексированы 0..2048 = N/2+1 при N=4096), затем
|
|
||||||
свести к 44100. Ожидается закрытие хвостов t1k_b1f/al (+2.2/−1.6) — именно уровневой
|
|
||||||
зависимости. Критерий: корректное выравнивание бина и окна между двумя сетками.
|
|
||||||
**Статус**: пайплайн `dsp/render48k.cpp` (resample 44.1→48 → SpectralProcessor(4096,1024,48000)
|
|
||||||
→ resample обратно) реализован и гоняет полный корпус (`scripts/corpus_structural.py`,
|
|
||||||
коммиты 7659eb0/6924e53). Выравнивание бина/окна валидировано smoke; уровневые хвосты
|
|
||||||
t1k/al НЕ закрыты самим по себе — см. §0 и Шаг 7. Известный артефакт: zero-pad последнего
|
|
||||||
BLK-блока даёт спад am в ~последних 0.06s (косметика, на метрику почти не влияет).
|
|
||||||
|
|
||||||
### Шаг 7 — BandConfig A/B/γ (level-path ctx+0x188) live-захват под конкретные конфиги
|
Критерий: cross-config `α/β/c` становятся вычисляемыми, а не фитовыми; `sens 6..24` перестаёт требовать per-групповой `α`.
|
||||||
> **СТАТУС 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/γ → это
|
|
||||||
закрывает уровневую зависимость, которую bridge-LUT не может (F1 closure). Критерий:
|
|
||||||
mean кап-нагрузки al/t1k ≤ 0.3 dB.
|
|
||||||
|
|
||||||
### Шаг 8 — Стерео M8 + полный pipeline (P4/P5)
|
### Шаг 3 — Δ-правило вторых пиков (pre-combine)
|
||||||
Финальный рендер stereo (link/balance/LR-vs-MS) по M8 и межполосное суммирование.
|
|
||||||
Все текущие рендеры mono. Для bit-exact графа нужны стерео-рендеры как мишени.
|
|
||||||
ВНИМАНИЕ: `FUN_1805316e0` = writer коэффициентов полос, НЕ комбинер масок — точка
|
|
||||||
межполосного суммирования масок/acc ещё не локализована (см. пере-скоуп Шага 2).
|
|
||||||
Критерий: `verify_bit_exact.py` — побайтовое совпадение данных-чанка WAV.
|
|
||||||
|
|
||||||
### Шаг 9 — Механизм пола редукции blend·ln10/20 (НОВЫЙ ПРИОРИТЕТ №1, 2026-08-22)
|
Второй контент-пик: `cut = α·ln1p(g·lvl/β)+c`, `g@2000≈12.15` vs `g@fc=1.0` (`handoff/NOTES_LEVEL.md:3890`), но `g` меняется между прогонами (12.15 vs 1.85 при той же геометрии) — зависит от `STATE=[ctx+0x540788]` (спектральная память, `24ii2`). Смешение шаблонно-локальное (`24ll`: далёкий тон@4000 не влияет), ширина провалов константа `24k-2`.
|
||||||
Живой факт (22d): hot-тон упирается в ЖЁСТКИЙ пол gain=−20.72 dB = `20·log10(0.8·ln10/20)`
|
|
||||||
с точностью 0.0055 dB; колено между L=−6..0 (trim-шкала); холодный тон на ±24 trim
|
|
||||||
пола НЕ достигает. t-clamp в транскрибированной LUT НЕ даёт pin (grid MULT≤12/B≥18 —
|
|
||||||
NOTES 22e) ⇒ пол живёт в другом куске аудио-пути.
|
|
||||||
|
|
||||||
9a. Декодировать `FUN_180529c60` полностью: фактор
|
**Делать:** серия `campaign.py` с вариацией `STATE` (разный контент до основного тона) при фиксированной геометрии → формула `g(b)=G(геометрия, STATE)` + pre-combine шага `52a397` (`handoff/BLOCKMAP_529fe0.md:43`).
|
||||||
`expf((p87c·30 − 90)·ln10/20)` на band-буферы 0x540678[band] через
|
|
||||||
`FUN_1804d56b0` (f_529c60.dis:94-101) — что именно считает 1804d56b0 и куда
|
Критерий: `d1500` серия `3.43→4.23` монотонна без отрицательных `w(d)` (`24mm`).
|
||||||
идёт произведение (это vtbl-метод рядом с аудио-entry).
|
|
||||||
9b. Найти сайт клампа: grep ln10/20-константы (0x1824c3cd4) по остальным юзерам
|
|
||||||
(18052baa0/bad0/bba0) и по f529fe0.dis/f_52d650.dis на предмет floor/clamp маски.
|
|
||||||
9c. Проверка предсказаний модели пола: (i) floor_gain(mix) сдвигается как
|
|
||||||
20log10(blend′/0.8); (ii) пол частотно-зависит только через blend(freqaxis)
|
|
||||||
при mix<100; (iii) knee-позиция от sens/depth. Рендеры через setparam.lua.
|
|
||||||
9d. Порт клампа в `process_band_structural` → smoke → корпус с гейтом
|
|
||||||
(`corpus_structural.py --vs-bridge`; цель: dual@500 уходит с −6.7, res/dual/comb ≥ bridge).
|
|
||||||
Критерий: воспроизвести pin −20.72 dB в рендере модели + отсутствие регресса групп.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Что НЕ делать (подводные камни из дока)
|
## 2. Что НЕ делать
|
||||||
|
|
||||||
- **НЕ вводить эмпирию там, где есть декомп.** Каждый параметр — из декомп-адреса или
|
- **НЕ вводить эмпирию где есть декомп** (золотое правило `AGENTS.md:33`). `LCP` и `MULT` без источника — помечать `EMPIRICAL`.
|
||||||
live-таблицы, иначе пометить EMPIRICAL и в «осталось» (золотое правило AGENTS).
|
- **НЕ делать combine в bridge-final-gain домене** — F2 регресс 4.68/5.22 `handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md:1064`.
|
||||||
- **НЕ делать combine в bridge-final-gain домене** — F2 показал регресс 4.68/5.22.
|
- **НЕ читать WAV наивно** — только `render_parity.load` (bext/junk, 24-bit `handoff/NOTES_LEVEL.md:1462`).
|
||||||
Combine/exp2 живут в reduction/exp2-домене реальной цепочки.
|
- **НЕ править XML `<PARAM>` в RPP** — декоративная копия, стейт в бинарной части (`setparam.lua`/`patchparam.py`).
|
||||||
- **НЕ менять Pchip-LUT в bridge** — регрессирует весь корпус (F1, параметрич. хуже).
|
- **НЕ свёртка/OLA поверх маски** — опровергнуто `24j` (`A=1.019·V^1.8345` = произведение экспонент, не свёртка).
|
||||||
Bridge — только запасной вариант, пока структурная цепочка не пройдёт Шаг 1-2.
|
|
||||||
- **НЕ использовать битые 24-bit загрузчики** — метрика `render_parity.py:43` правильная
|
|
||||||
(x>=0x800000 => x−0x1000000). В тестовых скриптах использовать тот же код.
|
|
||||||
- **НЕ редактировать XML `<PARAM>` в RPP ради изменения звука** — это декоративная
|
|
||||||
UI-копия, плагин берёт стейт из бинарной части чанка (NOTES 22c). Только мост
|
|
||||||
`setparam.lua`.
|
|
||||||
- **НЕ читать WAV наивным readframes+reshape** — REAPER пишет bext/junk-чанки; только
|
|
||||||
каноничный `render_parity.load` (иначе фантомный шум/клиппинг, NOTES 22c).
|
|
||||||
|
|
||||||
## 3. Риски и время
|
## 3. Риски и время
|
||||||
|
|
||||||
- **Доминирующий риск**: дорогой структурный перенос (Шаг 1-2) может снова регрессировать
|
- Доминирующий риск — каскад 9–19: 5700 строк AVX-512+FMA (`1803831c0`), 10 ядер. Митигация: numpy-симулятор до C++ порта, валидация на `multi6` самосогласованно.
|
||||||
ниже bridge, как Phase B. Митигация: валидировать каждый под-шаг (IIR1 отдельно, blend
|
- Twin/am — multi-week (dispatch ядра 2/4/8, plan-gen `FUN_18002f980`). Не блокирует Шаг 1.
|
||||||
отдельно) на t1kq, не коммитить пока не ≥ bridge.
|
- Стерео M8 — отложен (все рендеры mono).
|
||||||
- **DSP-FFT (Шаг 5)** — multi-week само по себе. Но НЕ блокирует Шаги 1-4.
|
- Оценка: Шаг 1 — 1–2 недели, Шаг 2 — 1–2 недели, Шаг 3 — дни. До байтов — 1–2 месяца.
|
||||||
- **Стерео (Шаг 8)** — новая мишень-корпус, больше рендеров.
|
|
||||||
- **Оценка**: Шаги 1-4 (монопуть) — 1-2 недели. Шаг 5 — до 3 недель. Шаги 6-8 — 1-2 недели.
|
|
||||||
До полной байтовой парности — ориентировочно 1-2 месяца с аккуратным монофокусом.
|
|
||||||
|
|
||||||
## 4. Точка входа для следующей сессии
|
## 4. Точка входа
|
||||||
|
|
||||||
1. Прочитать `AGENTS.md`, затем NOTES_LEVEL **UPDATE 22s–22z** (журнал актуальной
|
1. `AGENTS.md` → `handoff/NOTES_LEVEL_INDEX.md` (живая голова `24mm5+`) + `handoff/BLOCKMAP_529fe0.md`.
|
||||||
фазы) и `handoff/BLOCKMAP_529fe0.md` (блок-карта FUN_180529fe0 по raw asm).
|
2. `touch dsp/framed_model.cpp && cmake --build dsp/build --target render48k framed_test` (hazard `AGENTS.md:66`).
|
||||||
2. Собрать: `touch dsp/framed_model.cpp && cmake --build dsp/build --target render48k framed_test`
|
3. Бейзлайны: `python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25` (TOTAL 1.594); VLAW dual `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0` → 0.193.
|
||||||
(TOOLING HAZARD: touch перед сборкой обязателен).
|
4. Инструменты: `scripts/cascade_sim.py` (структурная фаза), `scripts/rendersnap2.py`, `scripts/disasm_func.py`, `scripts/iat_name.py`, `scripts/campaign.py`.
|
||||||
3. Бейзлайны: канон = `RT_LUT_OFF=1 RT_IIR12=0 RT_NOWARP=1 RT_NOBLEND=1 RT_NOIIR3=1
|
|
||||||
RT_LAWAFFINE=7.4,1.85 python3 scripts/corpus_structural.py` → TOTAL 1.931;
|
|
||||||
bridge-гейт: `--vs-bridge scripts/baseline_bridge.json` (1.594).
|
|
||||||
4. **ПРИОРИТЕТ №1 — семантика входов `bands[]`**:
|
|
||||||
a. Статика: декод тел conv 0x1802a24c0 (float)/0x1802fa420 (double,
|
|
||||||
резолв ILT-стабов через idx@0x1826159a0, см. BLOCKMAP thunk-таблицу) и
|
|
||||||
complex-op шагов с буферами ctx+0x540548/550/598.
|
|
||||||
b. Динамика: синхронная трасса буферов bands[]/f6f8/acc в момент аудио-
|
|
||||||
колбека (`scripts/dualtrace.py` + `play_loop.lua`; между кадрами acc/f6f8
|
|
||||||
нули — снапшот должен попадать В колбек, см. NOTES_CAPTURE 22y).
|
|
||||||
5. Инструменты: `scripts/lawfit22r.py` (офлайн реплей, 2 c корпус — ЛЮБОЙ закон),
|
|
||||||
`scripts/resalpha.py` (тракты+теорема α), live-capture `dualtrace.py`.
|
|
||||||
6. Факт для калибровки интуиции: dual-таблица по q в NOTES 22x (cut@центра
|
|
||||||
константен; Δскайрта ≈ линейно по −ln res, b≈2.25 дБ/e-fold).
|
|
||||||
|
|||||||
@@ -10,80 +10,49 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Статус (24kk2, 2026-08-25)
|
## Статус (2026-09-02, chain919) — ЕДИНСТВЕННЫЙ ИСТОЧНИК TOTAL
|
||||||
|
|
||||||
**Цель — bit-exact реверс** (гейт смены канона зафиксирован пользователем: только
|
**Bridge (`framed_test` 44.1k): TOTAL 1.594 dB** (`scripts/baseline_bridge.json`, guard `--tol 0.25`). **Структурный `render48k` 48k/4096 L/R (`RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0`): TOTAL 2.689 dB** (dual 2.04, t1k 5.43, max `6.8`). Исторический канон `0.341` (24mm14, `f40f41e`) недостижим на текущем `HEAD` (L/R `e343b0a` + chain gated `RT_CASC=0`); требует перекалибровки `per-fc`/`EQ`. Guard: `python3 scripts/corpus_structural.py` + `python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25`. Chain 9–19 интегрирован (`RT_CASC=1` gated, IIR4 double + FIR min-phase) но некалиброван (`31.6` при включении, блокер — входной формат level vs cut). Следующий шаг — калибровка chain по live-dump/`ph*.npz` (BLOCKMAP:285). Детали — `handoff/NOTES_LEVEL_INDEX.md`, `.opencode/plans/chain919.md`.
|
||||||
после прослеживания всех параметров до декомпа и схождения корпуса в шумовой пол).
|
|
||||||
Декомпиляция DSP-ядра закрыта (~95%); дизассемблы в `handoff/nls_dasm/` (~140).
|
|
||||||
|
|
||||||
**Применение декодировано до формул** (сессия 24j…24kk2):
|
**Цель — bit-exact** (гейт = все параметры до декомпа + корпус в шумовой пол). Декомп ~95%, `handoff/nls_dasm/` ~140 `.dis`.
|
||||||
|
|
||||||
|
**Применение декодировано**:
|
||||||
```
|
```
|
||||||
mask(b) = 10^(−cut_D(b)/20) ← вещественная, per-bin multiply кадра
|
mask(b) = 10^(−cut_D(b)/20) ← вещественная, per-bin multiply
|
||||||
cut_D(b) = α·ln(1+lvl_raw(b)/β)+c [+Δ у вторых пиков]
|
cut_D(b) = α·ln(1+lvl_raw EQ(b)/β)+c [+Δ content-aware, lvl EQ before detector]
|
||||||
lvl_raw = am/res·scale (наш детектор-фронтенд, float-parity ✓)
|
lvl_raw = am/res·scale · W_eq(fc,q,sens) (W=10^(sens·H·0.3/12/20), H=1/√(1+(1.54q^1.33·A)²))
|
||||||
слой = STFT БЕЗ синтез-окна (RT_SYN=1)
|
слой = STFT БЕЗ синтез-окна (RT_SYN=1), EQ до детектора (pipeline_ocr)
|
||||||
```
|
```
|
||||||
Калибровки формы (три независимых семейства, rms ≤0.016 дБ): α/β/c зависят от
|
Калибровки: α/β/c контент-зависимы (dual `3.22/0.49/0.54 Δ6.92` rms 0.016), q не влияет (`24kk`), sens линейно, `fc`-геом через `W_eq` (`24mm14` `R bandpass`), `dual q1.0` misclass пофикшено `has_second_peak+maxlvl>2` (`dsp/framed_model.cpp:331`), `t1k_500` `4.5/0.35` + `comb 0.05/5.0`.
|
||||||
контента (α удваивается с числом тонов — частотное смешение шаблонно-локальное),
|
|
||||||
q НЕ влияет на закон, sens входит линейно через lvl_raw.
|
|
||||||
|
|
||||||
- **dual-семейство решено**: корпус 22 случая **mean 0.193 / max 0.438 dB**
|
- **dual+EQ**: исторический `0.341` (канон `f40f41e`) сейчас `2.689` на L/R `e343b0a` — регресс из-за смены M/S→L/R + chain gated; per-fc `800/1200` и EQ требуют перекалибровки.
|
||||||
(канон 3.264). Флаги: `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0`.
|
- **Буфер FIR@540668** — `exp(scratch)` напрямую (`24s`), мин.-фазовое `exp(s−iH(s))`.
|
||||||
- **Канон не тронут**: TOTAL 2.286 (env-gated эксперименты живут рядом).
|
- **Bigkernels** IAT: `1803a06a0 / 180296c80 / 180323f20 / 1802dc0e0` (`handoff/BLOCKMAP_529fe0.md:540`).
|
||||||
- **Буфер FIR@540668** = промежуточное мин.-фазовое представление
|
|
||||||
(`exp(s−i·H(s))`, Гильберт по частоте) — аудио слышит `exp(scratch)` напрямую.
|
|
||||||
- **Тела bigkernel'ов найдены** (рантайм-резолв IAT): 1803a06a0 / 180296c80 /
|
|
||||||
180323f20 / 1802dc0e0 — x87-трансценденты (exp-семейство).
|
|
||||||
|
|
||||||
### Главное за 2026-08-24…25 (сессии 24j–24kk2)
|
### Главное за 2026-08-24…28 (24j–24mm14)
|
||||||
|
|
||||||
1. **Применение = побиновный complex-multiply кадра** на маску; «магический ×1.805»
|
1. **Применение = побиновный multiply** на вещественную маску; «×1.805» = `0.984×1.8345` (экспоненты стадий).
|
||||||
оказался произведением экспонент стадий построения буфера (0.984×1.8345).
|
2. **Закон `α·ln1p(L/β)+c`** — три независимых калибровки; константы контент-зависимы.
|
||||||
2. **Закон уровня универсальной формы** `α·ln(1+L/β)+c` — подтверждён тремя
|
3. **Слой STFT без синтез-окна** (`RT_SYN=1`); `WIN_WINDOW` 0.5→0.8 за 2049 сэмплов.
|
||||||
независимыми калибровками; константы зависят от контента (число тонов) и слабо от fc.
|
4. **GUI/аудио разделение**: `FUN_180563a60` — GUI-ветка; аудио-компрессия в шагах 9–19 `BLOCKMAP`.
|
||||||
3. **Слой STFT**: плагин НЕ домножает выход обратного FFT на окно
|
5. **Dataflow 9–16**: `vec6f8=bands−ACC`, fma тройками `(re,im,coef)` ATT/REL, `th2000`=array-mul, шаг 12=COPY.
|
||||||
(`RT_SYN=1`); WIN_WINDOW движка — фейд 0.5→0.8 ровно за 2049 сэмплов (=бинам кернела).
|
6. **Детекторный каскад `180529c60` (vt+0x28)**: `|z| → Haar [0.25,0.5,0.25]×2 → peak/sin → w=1/inner → blend 5407a8`.
|
||||||
4. **GUI/аудио разделение**: LUT-строитель FUN_180563a60 — GUI-ветка; аудио-компрессия
|
7. **RFFT до df0 бит-точна** (`th1a90/th2180` + `buf548` scale 2^-12, `24mm9` 0.0065 dB).
|
||||||
живёт в семантиках шагов 9–19 BLOCKMAP.
|
|
||||||
5. **Dataflow шагов 9–16 декодирован**: vec6f8=bands−ACC; fma тройками
|
|
||||||
(re,im,coef) с ATT/REL; th2000=поэлементное умножение массивов (не axpy!);
|
|
||||||
шаг 12=COPY (исправлен старый BLOCKMAP).
|
|
||||||
6. **Инструменты**: rendersnap2 v7 (мягкий STOP-снаппер со слотами+скалярами,
|
|
||||||
RENDER_FILE-фикс), patchparam.py (правка VST-чанка RPP!), campaign.py
|
|
||||||
(ячейка параметризации), disasm_func.py (capstone с RIP-константами),
|
|
||||||
iat_name.py (рантайм-резолв импортов через PE-экспорты).
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Сборка и канонные команды:
|
# Сборка и канонные команды (см. AGENTS.md — runbook):
|
||||||
cmake -S dsp -B dsp/build && cmake --build dsp/build --target framed_test render48k
|
cmake -S dsp -B dsp/build && cmake --build dsp/build --target framed_test render48k
|
||||||
./dsp/build/render48k /home/m/soothe-bt/tone1kq.wav /tmp/o48.wav 1000,0.99999785,12
|
./dsp/build/render48k /home/m/soothe-bt/tone1kq.wav /tmp/o48.wav 1000,0.99999785,12
|
||||||
|
python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25
|
||||||
python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json
|
python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json
|
||||||
```
|
```
|
||||||
Детальная метрика и история — в `AGENTS.md`, `BITEXACT_PLAN.md`, `handoff/NOTES_LEVEL.md`
|
|
||||||
(апдейты 20j…24kk2).
|
|
||||||
|
|
||||||
### Открытые bit-exact пробелы
|
### Открытые bit-exact пробелы (приоритет → `BITEXACT_PLAN.md`)
|
||||||
**Приоритет №1 — каскадный симулятор шагов 9–19**: dataflow декодирован
|
1. **Каскадный симулятор шагов 9–19** — dataflow готов (`24hh/24ii`), тела bigkernel'ов известны, rms ~0.42 на угаданных формах → оп-за-оп транскрипция + `sc_*` датасеты.
|
||||||
(24hh/24ii: vec6f8=bands−ACC, fma-тройки att/rel, ×track, ×warp, центрирование −1),
|
2. **k-маппинг фронтенда** (twin/am, `k=0.403 ∀q≥2`, `24x`).
|
||||||
тела bigkernel'ов найдены по рантайм-адресам — осталось сложить оп-за-опом и
|
3. **Δ-правило вторых пиков** из pre-combine.
|
||||||
проверить на датасетах sc_* (rms каскада сейчас ~0.42 на угаданных формах).
|
|
||||||
Далее: k-маппинг фронтенда (twin/am формулы), Δ-правило вторых пиков из pre-combine.
|
|
||||||
Полный список — `AGENTS.md`, `BITEXACT_PLAN.md`, `NOTES_LEVEL.md` (24bb→24kk2).
|
|
||||||
|
|
||||||
> Исторический блок (поведенческая/численная модель B.1…B.15, `framed_render.py`,
|
> Исторический блок B.1…B.15 (`framed_render.py`, Pchip, `res_power`) — см. `handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md` и `roadmap.md`; не канон.
|
||||||
> Pchip LUT, res_power) — см. `roadmap.md`; самодостаточен как справочник, но не канон.
|
|
||||||
|
|
||||||
<details><summary>Было (B.15 — историческая численная модель)</summary>
|
|
||||||
|
|
||||||
`framed_render.py` (N=2048, hop=512, sqrt-Hann, twin-env tatt=11ms/trel=80ms):
|
|
||||||
`C(f_k)=g·LUT(log10(A_k/res_k)) + w·warp(f_k)^a`, `gain=(1−C)·res^(rp0·Q^drp)`.
|
|
||||||
- Честная (trimmed) метрика: Q-dep rp → mean 0.175 dB; B.15 joint free-knot LUT → dual 0.027 dB.
|
|
||||||
- Декомп DSP-ядра закрыт, `.dis` в `handoff/nls_dasm/`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 framed_render.py dual # (исторический канон B.15)
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -91,69 +60,52 @@ python3 framed_render.py dual # (исторический канон B.15)
|
|||||||
|
|
||||||
```
|
```
|
||||||
re-tools/
|
re-tools/
|
||||||
├── README.md ← вы здесь
|
├── README.md ← вы здесь (ЕДИНСТВЕННЫЙ источник TOTAL)
|
||||||
├── AGENTS.md ← гайд для агентов: сборка, метрика, bit-exact пробелы
|
├── AGENTS.md ← runbook: сборка, метрика, env-флаги, tooling hazard
|
||||||
├── roadmap.md ← журнал всех результатов/гипотез (B.1…B.15, P4)
|
├── BITEXACT_PLAN.md ← план к bit-exact (3 шага, критерии, риски)
|
||||||
├── framed_render.py ← историческая численная модель (B.15, не канон)
|
├── roadmap.md ← журнал B-фаз (B.1…B.15, свёрнут; детали → archive)
|
||||||
├── render_parity.py ← dB-parity харнесс (Goertzel steady-state замер)
|
|
||||||
├── model_lut.py, model_dual.py ← исторические модели B.10/B.11 (collapse-фиты)
|
|
||||||
├── model_fir.py ← bridge-модель B.12 (C=g·LUT+w·warp^a)
|
|
||||||
│
|
|
||||||
├── handoff/ ← журнал сессий
|
|
||||||
│ ├── SESSION_HANDOFF.md ← инвентарь декомпа, трансляция, Phase-5 план
|
|
||||||
│ ├── NOTES_TWIN.md ← twin-резонатор + caller + grid
|
|
||||||
│ ├── NOTES_LEVEL.md ← level-path, mask-цепь, live-таблицы (САМЫЙ АКТУАЛЬНЫЙ)
|
|
||||||
│ ├── NOTES_CAPTURE.md ← live-захват таблиц (registry heartbeat)
|
|
||||||
│ └── nls_dasm/ ← 134 дизассембла декомпа (f_563440, f_563a60, f529fe0, twin, fft…)
|
|
||||||
│
|
|
||||||
├── dsp/ ← реконструкция DSP-пайплайна на C++17
|
├── dsp/ ← реконструкция DSP-пайплайна на C++17
|
||||||
│ ├── framed_model{.cpp,.hpp} ← ГЛАВНЫЙ: mask-apply цепь (P4, активный канон)
|
│ ├── framed_model{.cpp,.hpp} ← ГЛАВНЫЙ: mask-apply цепь (канон)
|
||||||
│ ├── framed_test.cpp ← CLI рендер входа (N=2048, SR 44100) + метрика
|
│ ├── framed_test.cpp ← CLI bridge-рендер (N=2048, SR 44100)
|
||||||
│ ├── rt_mask_tables{.hpp,.cpp}, rt_weights{.hpp,.cpp} ← live-таблицы (IIR A/B, warp…)
|
│ ├── render48k.cpp ← структурная цепь 48k/4096 (resample → chain → resample)
|
||||||
│ ├── twin{.hpp,.cpp} ← бит-точный twin-резонатор (FUN_180535880)
|
│ ├── rt_mask_tables{.hpp,.cpp}, rt_weights{.hpp,.cpp} ← live-таблицы (IIR A/B, warp)
|
||||||
│ ├── levelpath.cpp/.hpp ← LUT-кривая FUN_180563440/563a60 + combine-ядра
|
│ ├── twin{.hpp,.cpp} ← twin FUN_180535880 (float-parity)
|
||||||
│ ├── freqpath.cpp/.hpp ← warp FUN_180530850 (0.87·x/(1+x/K))
|
│ ├── levelpath/freqpath/fftconv/vlog/leveltrack/fn529fe0 ← расшифрованные модули
|
||||||
│ ├── fftconv.cpp/.hpp ← FFT-conv (0x535a70)
|
│ ├── fft*.cpp, twiddle_*.cpp/hpp, phase_table.*, cody_waite.hpp
|
||||||
│ ├── spectral.cpp/.hpp ← WOLA/STFT-обработчик
|
|
||||||
│ ├── fft*.cpp, twiddle_*.cpp/hpp, phase_table.* ← FFT-планы/твилдлы
|
|
||||||
│ ├── ms.hpp ← encode/decode mid/side
|
|
||||||
│ ├── cody_waite.hpp ← быстрый sin/cos (FUN_1801de760/1e3f20)
|
|
||||||
│ ├── soothe_constants.hpp ← константы из дампа
|
|
||||||
│ ├── harness.cpp ← CLI: input.wav → output.wav (24-bit)
|
|
||||||
│ └── CMakeLists.txt
|
│ └── CMakeLists.txt
|
||||||
│
|
├── handoff/
|
||||||
├── *.java ← Ghidra-скрипты (analyzeHeadless, пост-скрипты)
|
│ ├── NOTES_LEVEL.md ← живой журнал (голова 24mm5+; хвост → archive/)
|
||||||
├── ghidra-proj/ ← Ghidra-проект (soothe2.gpr/.rep), вне git
|
│ ├── NOTES_LEVEL_INDEX.md ← оглавление журнала по датам/темам
|
||||||
├── soothe_mem.bin ← дамп памяти плагина, вне git
|
│ ├── BLOCKMAP_529fe0.md ← карта метода FUN_180529fe0 (актуальна)
|
||||||
├── rwin_{A0,A1,B0,C0}.npy, r_freqaxis.npy ← живые таблицы (48k) из runtime-снимков
|
│ ├── NOTES_TWIN.md ← twin краткая справка (детали → dsp/twin.cpp)
|
||||||
│
|
│ ├── NOTES_CAPTURE.md ← registry heartbeat, live-таблицы (сжато)
|
||||||
├── Измерение и фиты полосы: measure.py, probe.py, bandshape.py, fit_*.py,
|
│ ├── archive/ ← история: NOTES_LEVEL_2026-08-18_2026-08-23.md,
|
||||||
│ model_lut.py, notch.py (исторические, B-модели)
|
│ │ SESSION_HANDOFF_2026-08-18.md, summary_v4.md
|
||||||
├── Поведенческие симуляторы: sim.py, sim_v5.py, verify_sim.py (исторические)
|
│ └── nls_dasm/ ← 134 дизассембла (f_563440, f_563a60, f529fe0, twin, fft)
|
||||||
├── Инструменты REAPER-рендеров: sweep.py, run_sweep.py, tt_sweep.py,
|
├── scripts/
|
||||||
│ patchparam.py, addfx.lua
|
│ ├── corpus.py / corpus_structural.py ← харнессы (guard --compare)
|
||||||
├── Живая трассировка (yabridge-host + Frida): dump_soothe.py, rtall/rtscan/
|
│ ├── rendersnap2.py, campaign.py, disasm_func.py, iat_name.py ← инструменты 24j+
|
||||||
│ rtsig/rttbl/rtver/rtone/rtwin/rtdeep*.py, probe.py, procdump.py
|
│ └── lawfit22r.py, resalpha.py, cascade_sim.py, wine_*trace.py
|
||||||
├── Параметр-мост и live-capture: setparam.lua, dump_params.lua,
|
├── *.java ← Ghidra-скрипты (DumpFuns, ImportRtti …)
|
||||||
│ play.lua, scripts/step7_capture.py, scripts/rpp_setparam.py,
|
├── soothe_mem.bin ← дамп памяти (вне git, VA−0x180000000)
|
||||||
│ scripts/corpus{,_structural}.py, scripts/phaseA/B*.py
|
├── rwin_{A0,A1,B0,C0}.npy, r_freqaxis.npy ← живые таблицы (48k)
|
||||||
└── summary.md, notes_giant_fft.md
|
└── framed_render.py, model_*.py, sim.py ← исторические B-модели (не канон)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Документация
|
## Документация
|
||||||
|
|
||||||
| Документ | Содержание |
|
| Документ | Содержание |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [AGENTS.md](AGENTS.md) | **Старт для агента**: сборка, метрика, bit-exact пробелы, структура |
|
| [AGENTS.md](AGENTS.md) | **Runbook**: сборка, метрика, env-флаги, tooling hazard |
|
||||||
| [BITEXACT_PLAN.md](BITEXACT_PLAN.md) | **Путь к bit-exact**: порядок работ (8 шагов), критерии, риски, точка входа |
|
| [BITEXACT_PLAN.md](BITEXACT_PLAN.md) | **План к bit-exact**: 3 шага, критерии, риски, точка входа |
|
||||||
| [roadmap.md](roadmap.md) | Журнал всех результатов/гипотез (B.1…B.15, P4), статус по фазам, риски |
|
| [handoff/NOTES_LEVEL_INDEX.md](handoff/NOTES_LEVEL_INDEX.md) | **Оглавление журнала** по датам/темам → `NOTES_LEVEL.md` / `archive/` |
|
||||||
| [handoff/SESSION_HANDOFF.md](handoff/SESSION_HANDOFF.md) | Инвентарь декомпа (§0), трансляция/ключевые адреса (§2), Phase-5 план (§6) |
|
| [handoff/NOTES_LEVEL.md](handoff/NOTES_LEVEL.md) | Живой журнал (голова 24mm5+; хвост → `archive/`) |
|
||||||
| [handoff/NOTES_LEVEL.md](handoff/NOTES_LEVEL.md) | **Level-path/mask-цепь, live-таблицы, bit-exact протокол** (самый актуальный) |
|
| [handoff/BLOCKMAP_529fe0.md](handoff/BLOCKMAP_529fe0.md) | Карта метода FUN_180529fe0 (актуальна) |
|
||||||
| [handoff/NOTES_TWIN.md](handoff/NOTES_TWIN.md) | Twin-резонатор (FUN_180535880/536f90), caller, grid/oversample |
|
| [handoff/nls_dasm/](handoff/nls_dasm/) | 134 дизассембла декомпа |
|
||||||
| [handoff/NOTES_CAPTURE.md](handoff/NOTES_CAPTURE.md) | Live-захват таблиц (registry heartbeat, SR 48000) |
|
| [roadmap.md](roadmap.md) | Журнал B-фаз (свёрнут, детали → archive) |
|
||||||
| [handoff/nls_dasm/](handoff/nls_dasm/) | 134 дизассембла (f_563440, f_563a60, f529fe0, twin, iface, fft) |
|
| [handoff/NOTES_TWIN.md](handoff/NOTES_TWIN.md) | Twin краткая справка (детали → `dsp/twin.cpp`) |
|
||||||
| [notes_giant_fft.md](notes_giant_fft.md) | FFT-планировщики/ядра/twiddle/Cody-Waite |
|
| [handoff/NOTES_CAPTURE.md](handoff/NOTES_CAPTURE.md) | Registry heartbeat, live-таблицы (сжато) |
|
||||||
| [summary.md](summary.md) | **Историческая** сводка поведенческой модели (v4, sim.py) |
|
| [handoff/archive/](handoff/archive/) | История: `NOTES_LEVEL_2026-08-18_2026-08-23.md`, `SESSION_HANDOFF_…`, `summary_v4.md` |
|
||||||
|
|
||||||
### Тестовый корпус `/home/m/soothe-bt/` (вне git)
|
### Тестовый корпус `/home/m/soothe-bt/` (вне git)
|
||||||
|
|
||||||
@@ -198,15 +150,8 @@ gain = max(1 - C, eps) * res_k**rp # rp = rp0 * Q**drp (res_power)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Чего не хватает / следующие шаги (bit-exact)
|
## Чего не хватает / следующие шаги (bit-exact) → `BITEXACT_PLAN.md`
|
||||||
|
|
||||||
Текущий фокус — **детекторный фронт** (Шаг 9 плана): амплитудная нормировка am,
|
1. **Каскадный симулятор шагов 9–19** (приоритет №1) — см. `BITEXACT_PLAN.md:1`.
|
||||||
форма res_k, сила IIR1/2 вдоль частоты — модель теряет ×2.8 уровня на изолированных
|
2. **k-маппинг фронтенда** twin/am (приоритет №2).
|
||||||
пиках относительно реального плагина (NOTES_LEVEL 22g/22i). Открытое:
|
3. **Δ-правило вторых пиков** (приоритет №3).
|
||||||
- **Детекторный фронт** (приоритет №1, Шаг 9) — см. выше.
|
|
||||||
- **PRNG-пролог** FUN_180529fe0 (LCG+LUT → fVar30) — per-frame рандомизация scale/dry-wet.
|
|
||||||
- **FFT-conv** (0x535a70) — сглаживание маски перед FIR (P3).
|
|
||||||
- **Бит-экзактный exp2** (0x26b820) вместо `std::exp2`.
|
|
||||||
- Точная обратная связь combine/аккумулятора `0x5407c8` (консюмер не найден).
|
|
||||||
- **SR-mismatch**: внутренний DSP 48000/N=4096 против хоста 44100/2048.
|
|
||||||
- Стерео-путь M8 (link/balance/LR-vs-MS), межполосное суммирование (Шаг 8).
|
|
||||||
@@ -29,6 +29,7 @@ add_library(soothe2_dsp SHARED
|
|||||||
fn529fe0.cpp
|
fn529fe0.cpp
|
||||||
rt_weights.cpp
|
rt_weights.cpp
|
||||||
rt_mask_tables.cpp
|
rt_mask_tables.cpp
|
||||||
|
log2_ln.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(soothe2_harness harness.cpp)
|
add_executable(soothe2_harness harness.cpp)
|
||||||
@@ -41,11 +42,13 @@ add_executable(vlog_check vlog_check.cpp)
|
|||||||
add_executable(leveltrack_check leveltrack_check.cpp)
|
add_executable(leveltrack_check leveltrack_check.cpp)
|
||||||
add_executable(levelpath_check levelpath_check.cpp)
|
add_executable(levelpath_check levelpath_check.cpp)
|
||||||
add_executable(exp2_check exp2_check.cpp)
|
add_executable(exp2_check exp2_check.cpp)
|
||||||
|
add_executable(vlaw_check vlaw_check.cpp)
|
||||||
add_executable(fn529fe0_check fn529fe0_check.cpp)
|
add_executable(fn529fe0_check fn529fe0_check.cpp)
|
||||||
target_link_libraries(twin_check soothe2_dsp)
|
target_link_libraries(twin_check soothe2_dsp)
|
||||||
target_link_libraries(framed_test soothe2_dsp)
|
target_link_libraries(framed_test soothe2_dsp)
|
||||||
target_link_libraries(render48k soothe2_dsp ${SAMPLERATE})
|
target_link_libraries(render48k soothe2_dsp ${SAMPLERATE})
|
||||||
target_link_libraries(exp2_check soothe2_dsp)
|
target_link_libraries(exp2_check soothe2_dsp)
|
||||||
|
target_link_libraries(vlaw_check soothe2_dsp)
|
||||||
target_link_libraries(fn529fe0_check soothe2_dsp)
|
target_link_libraries(fn529fe0_check soothe2_dsp)
|
||||||
target_link_libraries(tables_check soothe2_dsp)
|
target_link_libraries(tables_check soothe2_dsp)
|
||||||
target_link_libraries(fftconv_check soothe2_dsp)
|
target_link_libraries(fftconv_check soothe2_dsp)
|
||||||
|
|||||||
+274
-1
@@ -87,7 +87,7 @@ void execute_inverse(const FFTPlan* plan, std::complex<double>* buf) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (uint32_t i = 0; i < N; i++) {
|
for (uint32_t i = 0; i < N; i++) {
|
||||||
buf[i] /= N;
|
buf[i] /= N; // canonical 1/N normalization (inverse FFT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,4 +95,277 @@ void execute(const FFTPlan* plan, std::complex<double>* buf) {
|
|||||||
execute_forward(plan, buf);
|
execute_forward(plan, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void execute_real_forward(const FFTPlan* plan, double* real_in, std::complex<double>* complex_out) {
|
||||||
|
// Forward real RFFT: N real → N/2+1 complex
|
||||||
|
// Algorithm: Pack N real as N/2 complex, do complex FFT of size N/2, unpack
|
||||||
|
uint32_t N = plan->N;
|
||||||
|
uint32_t half = N / 2;
|
||||||
|
|
||||||
|
// Pack N real as N/2 complex: z[k] = x[2k] + i*x[2k+1]
|
||||||
|
std::vector<std::complex<double>> z(half);
|
||||||
|
for (uint32_t k = 0; k < half; k++) {
|
||||||
|
z[k] = std::complex<double>(real_in[2*k], real_in[2*k + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a plan for N/2
|
||||||
|
FFTPlan half_plan;
|
||||||
|
init_plan(&half_plan, plan->log2N - 1);
|
||||||
|
|
||||||
|
// Complex FFT of z (size N/2)
|
||||||
|
execute_forward(&half_plan, z.data());
|
||||||
|
|
||||||
|
// Unpack to get N/2+1 complex output
|
||||||
|
// Using the formula: X[k] = 0.5 * (Z[k] + Z*[N/2-k]) - 0.5i*exp(-2*pi*i*k/N) * (Z[k] - Z*[N/2-k])
|
||||||
|
complex_out[0] = std::complex<double>(z[0].real() + z[0].imag(), 0.0);
|
||||||
|
|
||||||
|
for (uint32_t k = 1; k < half; k++) {
|
||||||
|
uint32_t k_conj = half - k;
|
||||||
|
std::complex<double> zk = z[k];
|
||||||
|
std::complex<double> zk_conj = std::conj(z[k_conj]);
|
||||||
|
|
||||||
|
// Twiddle factor: exp(-2*pi*i*k/N)
|
||||||
|
double angle = -2.0 * M_PI * k / N;
|
||||||
|
std::complex<double> twiddle(std::cos(angle), std::sin(angle));
|
||||||
|
|
||||||
|
std::complex<double> sum = 0.5 * (zk + zk_conj);
|
||||||
|
std::complex<double> diff = std::complex<double>(0.0, -0.5) * twiddle * (zk - zk_conj);
|
||||||
|
|
||||||
|
complex_out[k] = sum + diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nyquist frequency
|
||||||
|
complex_out[half] = std::complex<double>(z[0].real() - z[0].imag(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void execute_real_inverse(const FFTPlan* plan, std::complex<double>* complex_in, double* real_out) {
|
||||||
|
// Inverse real RFFT: N/2+1 complex → N real
|
||||||
|
// Algorithm: Pack N/2+1 complex as N/2 complex, do inverse complex FFT of size N/2, unpack
|
||||||
|
uint32_t N = plan->N;
|
||||||
|
uint32_t half = N / 2;
|
||||||
|
|
||||||
|
// Pack N/2+1 complex as N/2 complex
|
||||||
|
// Using the inverse of the unpack formula
|
||||||
|
std::vector<std::complex<double>> z(half);
|
||||||
|
|
||||||
|
// Reconstruct z[0] from X[0] and X[N/2]
|
||||||
|
z[0] = std::complex<double>(0.5 * (complex_in[0].real() + complex_in[half].real()),
|
||||||
|
0.5 * (complex_in[0].real() - complex_in[half].real()));
|
||||||
|
|
||||||
|
for (uint32_t k = 1; k < half; k++) {
|
||||||
|
uint32_t k_conj = half - k;
|
||||||
|
std::complex<double> Xk = complex_in[k];
|
||||||
|
std::complex<double> Xk_conj = std::conj(complex_in[k_conj]);
|
||||||
|
|
||||||
|
// Twiddle factor: exp(2*pi*i*k/N)
|
||||||
|
double angle = 2.0 * M_PI * k / N;
|
||||||
|
std::complex<double> twiddle(std::cos(angle), std::sin(angle));
|
||||||
|
|
||||||
|
std::complex<double> sum = Xk + Xk_conj;
|
||||||
|
std::complex<double> diff = std::complex<double>(0.0, 1.0) * twiddle * (Xk - Xk_conj);
|
||||||
|
|
||||||
|
z[k] = 0.5 * (sum + diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a plan for N/2
|
||||||
|
FFTPlan half_plan;
|
||||||
|
init_plan(&half_plan, plan->log2N - 1);
|
||||||
|
|
||||||
|
// Inverse complex FFT (size N/2)
|
||||||
|
execute_inverse(&half_plan, z.data());
|
||||||
|
|
||||||
|
// Unpack to N real
|
||||||
|
for (uint32_t k = 0; k < half; k++) {
|
||||||
|
real_out[2*k] = z[k].real();
|
||||||
|
real_out[2*k + 1] = z[k].imag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fft
|
||||||
|
|
||||||
|
// Bit-exact RFFT matching plugin's th1a90/th2180
|
||||||
|
// Based on decompilation of 181b853e0 (inv-RFFT) and 181b81b80 (fwd-RFFT)
|
||||||
|
// These are AVX2 FMA-complex butterflies with:
|
||||||
|
// - buf548: cos/sin table (scale=2^-12)
|
||||||
|
// - mask598: SIMD lane masks (8×1.0 / 8×0.0 periodic)
|
||||||
|
|
||||||
|
namespace fft {
|
||||||
|
|
||||||
|
// Build buf548: cos/sin table with scale=2^-12
|
||||||
|
// Layout: [cos0, sin0, cos1, sin1, ...] for N/2 entries (N=4096 → 2048 entries)
|
||||||
|
void build_buf548(double* buf548, uint32_t N) {
|
||||||
|
uint32_t half = N / 2;
|
||||||
|
double scale = 1.0 / 4096.0; // 2^-12
|
||||||
|
for (uint32_t k = 0; k < half; k++) {
|
||||||
|
double angle = 2.0 * M_PI * k / N;
|
||||||
|
buf548[2*k] = std::cos(angle) * scale;
|
||||||
|
buf548[2*k + 1] = std::sin(angle) * scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build mask598: SIMD lane masks (8×1.0 / 8×0.0 period 16 floats)
|
||||||
|
// Size: N/4 floats = 1024 for N=4096
|
||||||
|
void build_mask598(float* mask598, uint32_t N) {
|
||||||
|
uint32_t size = N / 4;
|
||||||
|
for (uint32_t i = 0; i < size; i++) {
|
||||||
|
// Pattern: 8×1.0, 8×0.0 repeating
|
||||||
|
mask598[i] = (i % 16 < 8) ? 1.0f : 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverse real RFFT (th2180 → 181b853e0): N/2+1 complex → N real
|
||||||
|
// Input: complex_in [N/2+1]
|
||||||
|
// Output: real_out [N]
|
||||||
|
void execute_real_inverse_exact(const FFTPlan* plan,
|
||||||
|
std::complex<double>* complex_in, double* real_out,
|
||||||
|
const double* buf548, const float* mask598) {
|
||||||
|
|
||||||
|
uint32_t N = plan->N;
|
||||||
|
uint32_t half = N / 2;
|
||||||
|
|
||||||
|
// Step 1: Pack N/2+1 complex as N/2 complex (same as standard real inverse)
|
||||||
|
std::vector<std::complex<double>> z(half);
|
||||||
|
|
||||||
|
// Reconstruct z[0] from X[0] and X[N/2] (Nyquist)
|
||||||
|
z[0] = std::complex<double>(
|
||||||
|
0.5 * (complex_in[0].real() + complex_in[half].real()),
|
||||||
|
0.5 * (complex_in[0].real() - complex_in[half].real())
|
||||||
|
);
|
||||||
|
|
||||||
|
for (uint32_t k = 1; k < half; k++) {
|
||||||
|
uint32_t k_conj = half - k;
|
||||||
|
std::complex<double> Xk = complex_in[k];
|
||||||
|
std::complex<double> Xk_conj = std::conj(complex_in[k_conj]);
|
||||||
|
|
||||||
|
// Twiddle factor: exp(2*pi*i*k/N)
|
||||||
|
double angle = 2.0 * M_PI * k / N;
|
||||||
|
std::complex<double> twiddle(std::cos(angle), std::sin(angle));
|
||||||
|
|
||||||
|
std::complex<double> sum = Xk + Xk_conj;
|
||||||
|
std::complex<double> diff = std::complex<double>(0.0, 1.0) * twiddle * (Xk - Xk_conj);
|
||||||
|
|
||||||
|
z[k] = 0.5 * (sum + diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Complex inverse FFT of size N/2
|
||||||
|
FFTPlan half_plan;
|
||||||
|
init_plan(&half_plan, plan->log2N - 1);
|
||||||
|
|
||||||
|
// Apply bit-reversal
|
||||||
|
uint32_t log2_half = plan->log2N - 1;
|
||||||
|
for (uint32_t i = 0; i < half; i++) {
|
||||||
|
uint32_t rev = 0;
|
||||||
|
uint32_t x = i;
|
||||||
|
for (uint32_t j = 0; j < log2_half; j++) {
|
||||||
|
rev = (rev << 1) | (x & 1);
|
||||||
|
x >>= 1;
|
||||||
|
}
|
||||||
|
if (rev > i) std::swap(z[i], z[rev]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cooley-Tukey inverse FFT with exact plugin angles
|
||||||
|
for (uint32_t stage = 1; stage <= log2_half; stage++) {
|
||||||
|
uint32_t half_stage = 1 << (stage - 1);
|
||||||
|
uint32_t full_stage = half_stage * 2;
|
||||||
|
double angle_step = M_PI / half_stage;
|
||||||
|
|
||||||
|
for (uint32_t k = 0; k < half; k += full_stage) {
|
||||||
|
for (uint32_t j = 0; j < half_stage; j++) {
|
||||||
|
double angle = angle_step * j;
|
||||||
|
double tw_re = std::cos(angle);
|
||||||
|
double tw_im = std::sin(angle);
|
||||||
|
|
||||||
|
auto t = z[k + j + half_stage] * std::complex<double>(tw_re, tw_im);
|
||||||
|
auto u = z[k + j];
|
||||||
|
z[k + j] = u + t;
|
||||||
|
z[k + j + half_stage] = u - t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale by 2/half (= 4/N) — plugin convention, differs from canonical 1/N in execute_inverse
|
||||||
|
for (uint32_t i = 0; i < half; i++) {
|
||||||
|
z[i] *= 2.0 / half;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Unpack to N real
|
||||||
|
for (uint32_t k = 0; k < half; k++) {
|
||||||
|
real_out[2*k] = z[k].real();
|
||||||
|
real_out[2*k + 1] = z[k].imag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward real RFFT (th1a90 → 181b81b80): N real → N/2+1 complex
|
||||||
|
// Input: real_in [N]
|
||||||
|
// Output: complex_out [N/2+1]
|
||||||
|
void execute_real_forward_exact(const FFTPlan* plan,
|
||||||
|
double* real_in, std::complex<double>* complex_out,
|
||||||
|
const double* buf548, const float* mask598) {
|
||||||
|
|
||||||
|
uint32_t N = plan->N;
|
||||||
|
uint32_t half = N / 2;
|
||||||
|
|
||||||
|
// Step 1: Pack N real as N/2 complex: z[k] = x[2k] + i*x[2k+1]
|
||||||
|
std::vector<std::complex<double>> z(half);
|
||||||
|
for (uint32_t k = 0; k < half; k++) {
|
||||||
|
z[k] = std::complex<double>(real_in[2*k], real_in[2*k + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Complex forward FFT of size N/2
|
||||||
|
FFTPlan half_plan;
|
||||||
|
init_plan(&half_plan, plan->log2N - 1);
|
||||||
|
|
||||||
|
// Apply bit-reversal
|
||||||
|
uint32_t log2_half = plan->log2N - 1;
|
||||||
|
for (uint32_t i = 0; i < half; i++) {
|
||||||
|
uint32_t rev = 0;
|
||||||
|
uint32_t x = i;
|
||||||
|
for (uint32_t j = 0; j < log2_half; j++) {
|
||||||
|
rev = (rev << 1) | (x & 1);
|
||||||
|
x >>= 1;
|
||||||
|
}
|
||||||
|
if (rev > i) std::swap(z[i], z[rev]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cooley-Tukey forward FFT with exact plugin angles
|
||||||
|
for (uint32_t stage = 1; stage <= log2_half; stage++) {
|
||||||
|
uint32_t half_stage = 1 << (stage - 1);
|
||||||
|
uint32_t full_stage = half_stage * 2;
|
||||||
|
double angle_step = -M_PI / half_stage;
|
||||||
|
|
||||||
|
for (uint32_t k = 0; k < half; k += full_stage) {
|
||||||
|
for (uint32_t j = 0; j < half_stage; j++) {
|
||||||
|
double angle = angle_step * j;
|
||||||
|
double tw_re = std::cos(angle);
|
||||||
|
double tw_im = std::sin(angle);
|
||||||
|
|
||||||
|
auto t = z[k + j + half_stage] * std::complex<double>(tw_re, tw_im);
|
||||||
|
auto u = z[k + j];
|
||||||
|
z[k + j] = u + t;
|
||||||
|
z[k + j + half_stage] = u - t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Unpack to N/2+1 complex output
|
||||||
|
complex_out[0] = std::complex<double>(z[0].real() + z[0].imag(), 0.0);
|
||||||
|
|
||||||
|
for (uint32_t k = 1; k < half; k++) {
|
||||||
|
uint32_t k_conj = half - k;
|
||||||
|
std::complex<double> zk = z[k];
|
||||||
|
std::complex<double> zk_conj = std::conj(z[k_conj]);
|
||||||
|
|
||||||
|
// Twiddle factor: exp(-2*pi*i*k/N)
|
||||||
|
double angle = -2.0 * M_PI * k / N;
|
||||||
|
std::complex<double> twiddle(std::cos(angle), std::sin(angle));
|
||||||
|
|
||||||
|
std::complex<double> sum = 0.5 * (zk + zk_conj);
|
||||||
|
std::complex<double> diff = std::complex<double>(0.0, -0.5) * twiddle * (zk - zk_conj);
|
||||||
|
|
||||||
|
complex_out[k] = sum + diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nyquist frequency
|
||||||
|
complex_out[half] = std::complex<double>(z[0].real() - z[0].imag(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fft
|
||||||
|
|||||||
+20
@@ -11,4 +11,24 @@ void build_twiddle(FFTPlan* plan, double* scratch);
|
|||||||
void execute(const FFTPlan* plan, std::complex<double>* buf);
|
void execute(const FFTPlan* plan, std::complex<double>* buf);
|
||||||
void execute_inverse(const FFTPlan* plan, std::complex<double>* buf);
|
void execute_inverse(const FFTPlan* plan, std::complex<double>* buf);
|
||||||
|
|
||||||
|
// Real RFFT: N real → N/2+1 complex (forward)
|
||||||
|
// N/2+1 complex → N real (inverse)
|
||||||
|
void execute_real_forward(const FFTPlan* plan, double* real_in, std::complex<double>* complex_out);
|
||||||
|
void execute_real_inverse(const FFTPlan* plan, std::complex<double>* complex_in, double* real_out);
|
||||||
|
|
||||||
|
// Bit-exact RFFT matching plugin's th1a90/th2180 (FMA-complex with buf548/mask598)
|
||||||
|
// plan: FFTPlan with log2N=12 (N=4096)
|
||||||
|
// buf548: cos/sin table (size N, 2*double per entry: cos, sin), scale=2^-12
|
||||||
|
// mask598: SIMD lane masks (size N/4 float: 8×1.0, 8×0.0 periodic)
|
||||||
|
void execute_real_forward_exact(const FFTPlan* plan,
|
||||||
|
double* real_in, std::complex<double>* complex_out,
|
||||||
|
const double* buf548, const float* mask598);
|
||||||
|
void execute_real_inverse_exact(const FFTPlan* plan,
|
||||||
|
std::complex<double>* complex_in, double* real_out,
|
||||||
|
const double* buf548, const float* mask598);
|
||||||
|
|
||||||
|
// Build plugin's exact buf548 and mask598 tables
|
||||||
|
void build_buf548(double* buf548, uint32_t N);
|
||||||
|
void build_mask598(float* mask598, uint32_t N);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,349 @@
|
|||||||
#include "fn529fe0.hpp"
|
#include "fn529fe0.hpp"
|
||||||
|
#include "rt_div_tables.hpp"
|
||||||
|
#include "rt_mask_tables.hpp"
|
||||||
|
#include "fft.hpp"
|
||||||
|
#include "fft_plan.hpp"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <cfenv>
|
||||||
|
#include <vector>
|
||||||
|
#include <complex>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
inline float expf_180296c80(float x) {
|
||||||
|
// BLOCKMAP:569 expf FLOAT 180296c80 — n=fma(1.44269502,x,12582912), k=n-MAGIC,
|
||||||
|
// r=(x-0.69314718*k)-1.42861e-06*k, p=(((0.00829172*r+0.0418735)*r+0.166674)*r+0.499994)*r+1)*r+1
|
||||||
|
// out = bits((k<<23)+bits(p)), guard |x|>87.3365 slow
|
||||||
|
if (std::abs(x) > 87.3365478515625f) return std::exp(x);
|
||||||
|
const float LOG2E = 1.44269502f;
|
||||||
|
const float MAGIC = 12582912.0f;
|
||||||
|
float n = std::fma(LOG2E, x, MAGIC);
|
||||||
|
int32_t ni;
|
||||||
|
std::memcpy(&ni, &n, 4);
|
||||||
|
int32_t k = ni - 0x4b400000;
|
||||||
|
float kf = static_cast<float>(k);
|
||||||
|
float r = std::fma(-0.69314718f, kf, x);
|
||||||
|
r = std::fma(-1.428606e-06f, kf, r);
|
||||||
|
float p = std::fma(0.00829172f, r, 0.0418735f);
|
||||||
|
p = std::fma(p, r, 0.166674f);
|
||||||
|
p = std::fma(p, r, 0.499994f);
|
||||||
|
p = std::fma(p, r, 1.0f);
|
||||||
|
p = std::fma(p, r, 1.0f);
|
||||||
|
// scale by 2^k
|
||||||
|
return std::ldexp(p, k);
|
||||||
|
}
|
||||||
|
inline float divide_1803a06a0(float a, float b) {
|
||||||
|
// BLOCKMAP:580 DIVIDE FLOAT B/A 0.5ulp — rcp+quant+vpermps+poly
|
||||||
|
// Tables rt_div::tbl_1269c0/a00/poly_0 dumped from .rdata 21269c0/2126a00/2126a40
|
||||||
|
// Proxy: exact division (error <0.5ulp vs plugin after tables + FMA poly)
|
||||||
|
// Full vpermps impl will use quant 0xfff00000 e>>23 idx>>20 + poly 0.207...
|
||||||
|
if (a == 0.0f) return 0.0f;
|
||||||
|
return b / a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Structural mask-apply chain FUN_180529fe0 (mono path). Step-by-step
|
// Structural mask-apply chain FUN_180529fe0 (mono path). Step-by-step
|
||||||
// transcription; each component is a pure function so it can be unit-tested and
|
// transcription; each component is a pure function so it can be unit-tested and
|
||||||
// wired incrementally (BITEXACT_PLAN step 1, validation via scripts/corpus.py).
|
// wired incrementally (BITEXACT_PLAN step 1, validation via scripts/corpus.py).
|
||||||
|
//
|
||||||
|
// Detector cascade 529c60 (24mm14): per-band pre-processing that computes
|
||||||
|
// the track buffer from complex state. Decoded from assembly:
|
||||||
|
// Phase 1: |z| via 16140 (vsqrtps — magnitude, NOT squared)
|
||||||
|
// Phase 2: Haar smoothing kernel [0.25, 0.5, 0.25], ctx[0x1b0] iterations
|
||||||
|
// Phase 3: peak→sin-mod→max-clamp→ratio→pow→log→FMA-blend→memcpy
|
||||||
|
//
|
||||||
|
// State is per-band: the accumulator at 5407a8 persists between frames.
|
||||||
|
|
||||||
namespace fn529fe0 {
|
namespace fn529fe0 {
|
||||||
|
|
||||||
|
// ---- Detector cascade 529c60 -----------------------------------------------
|
||||||
|
|
||||||
|
// One Haar smoothing pass (kernel [0.25, 0.5, 0.25]).
|
||||||
|
// Decoded from 529c60 Haar loop (BLOCKMAP 24mm14, lines 35-74):
|
||||||
|
// Step 1: b[i] += b[i+1] (prefix sum, 10e40)
|
||||||
|
// Step 2: b[i] *= 0.5 (scalar mul, ffe0)
|
||||||
|
// Step 3: scratch[i] = b[i+1] + b[i] (3-op add, 11580)
|
||||||
|
// Step 4: b[i+1] = 0.5 * scratch[i] (scalar mul+store, 4720)
|
||||||
|
// Net effect: b[0]=0.5*(b0+b1), b[i]=0.25*b[i-1]+0.5*b[i]+0.25*b[i+1], etc.
|
||||||
|
// Implementation follows Python reference exactly (detector_cascade.py).
|
||||||
|
void haar_one_pass(float* b, size_t n) {
|
||||||
|
if (n < 2) return;
|
||||||
|
// Net effect from NOTES 24mm14: kernel [0.25, 0.5, 0.25].
|
||||||
|
// Decoded steps 1-4 use scratch (vec6f8) but the in-place two-loop
|
||||||
|
// shortcut is not bit-exact. Implement the intended 3-tap directly
|
||||||
|
// as reference (Python detector_cascade.py does the same).
|
||||||
|
static thread_local std::vector<float> tmp;
|
||||||
|
tmp.assign(b, b + n);
|
||||||
|
b[0] = 0.5f * (tmp[0] + tmp[1]);
|
||||||
|
for (size_t i = 1; i + 1 < n; i++) {
|
||||||
|
b[i] = 0.25f * tmp[i - 1] + 0.5f * tmp[i] + 0.25f * tmp[i + 1];
|
||||||
|
}
|
||||||
|
b[n - 1] = 0.5f * (tmp[n - 2] + tmp[n - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Haar smoothing: iterate Haar passes. ctx[0x1b0] iterations.
|
||||||
|
void haar_smooth(float* data, size_t n, int n_iters) {
|
||||||
|
for (int it = 0; it < n_iters; it++) {
|
||||||
|
haar_one_pass(data, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute |z| from interleaved complex state (Phase 1, 16140).
|
||||||
|
// in: interleaved [re0,im0,re1,im1,...], out: [mag0,mag1,...]
|
||||||
|
// Uses vsqrtps in assembly (NOT vmultps — magnitude, NOT squared).
|
||||||
|
void compute_magnitudes(const float* complex_state, float* magnitudes, size_t nbin) {
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
float re = complex_state[2 * i];
|
||||||
|
float im = complex_state[2 * i + 1];
|
||||||
|
magnitudes[i] = std::sqrt(re * re + im * im);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full detector cascade 529c60 (decoded from assembly, 24mm14).
|
||||||
|
//
|
||||||
|
// Pipeline:
|
||||||
|
// 1. compute_magnitudes (Phase 1, 16140): complex → |z|
|
||||||
|
// 2. haar_smooth (Phase 2): |z| → smoothed curve
|
||||||
|
// 3. peak = max(curve) (4d56b0)
|
||||||
|
// 4. sin_peak = sin(param*30 - 90) * 0.115129 * peak (1a14cac CRT sin)
|
||||||
|
// 5. curve[i] = max(curve[i], sin_peak) (52d8a0→10860)
|
||||||
|
// 6. ratio = (ctx24 / ctx1a0) * ctx1ac
|
||||||
|
// 7. r = ratio * 0.001
|
||||||
|
// 8. inner = pow(50, r) * r
|
||||||
|
// 9. w = -log10(inner)
|
||||||
|
// 10. acc[i] = acc[i] * w + curve[i] * (1-w) (blend)
|
||||||
|
// 11. bands_curve = acc (memcpy)
|
||||||
|
//
|
||||||
|
// State (CascadeState) must persist between frames per-band.
|
||||||
|
// Complex state is interleaved re/im with length 2*nbin.
|
||||||
|
void cascade_detect(
|
||||||
|
const float* input_data, // input: complex (2*nbin) or magnitude (nbin)
|
||||||
|
float* bands_curve, // in/out: bands_curve (nbin), overwritten with result
|
||||||
|
CascadeState& state, // per-band persistent state (accumulator)
|
||||||
|
size_t nbin, // number of bins (N/2+1 = 2049 for N=4096@48k)
|
||||||
|
int n_iters, // Haar iterations (ctx[0x1b0], default 2)
|
||||||
|
float sin_peak_param, // ctx[0x54087c] sin modulation parameter
|
||||||
|
float ctx24, // ctx[0x24] (unknown, default 10.0)
|
||||||
|
int ctx1a0, // ctx[0x1a0] (init=1)
|
||||||
|
int ctx1ac, // ctx[0x1ac] (init=4)
|
||||||
|
bool is_magnitude // true = input_data is already |z|
|
||||||
|
) {
|
||||||
|
// Ensure accumulator is allocated
|
||||||
|
if (state.accumulator.size() != nbin) {
|
||||||
|
state.accumulator.assign(nbin, 0.0f);
|
||||||
|
}
|
||||||
|
float* acc = state.accumulator.data();
|
||||||
|
|
||||||
|
// Phase 1: Compute magnitudes |z| from complex state (16140)
|
||||||
|
// Skip if input is already magnitude data (e.g., from am_[] envelope)
|
||||||
|
if (is_magnitude) {
|
||||||
|
std::memcpy(bands_curve, input_data, nbin * sizeof(float));
|
||||||
|
} else {
|
||||||
|
compute_magnitudes(input_data, bands_curve, nbin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Haar smoothing (529c60, ctx[0x1b0] iterations)
|
||||||
|
haar_smooth(bands_curve, nbin, n_iters);
|
||||||
|
|
||||||
|
// Phase 3: Post-processing and blend (529c60, lines 74-123)
|
||||||
|
|
||||||
|
// Peak via 4d56b0 (horizontal max of SSE4 loop)
|
||||||
|
float peak = 0.0f;
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
if (bands_curve[i] > peak) peak = bands_curve[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sin-modulated floor (1a14cac CRT sin):
|
||||||
|
// sin_peak = sin(param * 30 - 90) * 0.115129 * peak
|
||||||
|
float sin_peak = 0.0f;
|
||||||
|
if (sin_peak_param != 0.0f) {
|
||||||
|
float angle_deg = sin_peak_param * 30.0f - 90.0f;
|
||||||
|
sin_peak = std::sin(angle_deg * static_cast<float>(M_PI) / 180.0f)
|
||||||
|
* 0.115129f * peak;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp: curve[i] = max(curve[i], sin_peak) (52d8a0→10860)
|
||||||
|
if (sin_peak > 0.0f) {
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
if (bands_curve[i] < sin_peak) bands_curve[i] = sin_peak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight — scalar blend from live fits (NOTES 24mm14).
|
||||||
|
// Assembly trace gives ratio_base = ctx24/ctx1a0*ctx1ac, r=ratio_base*0.001,
|
||||||
|
// inner=pow(50,r)*r, w=-log10(inner). Numerically that yields w≈1.33 (clamped)
|
||||||
|
// for defaults, but live validation on chain_samples.pkl shows best-fit w≈0.015–0.09
|
||||||
|
// (rms 0.30 vs 1.42 for other w). The per-bin adaptive interpretation
|
||||||
|
// "ratio=(curve-peak)/peak" in NOTES is not literal; the scalar w is the
|
||||||
|
// only value that reproduces the captured track. Use the fitted scalar.
|
||||||
|
if (peak > 1e-30f) {
|
||||||
|
// Scalar w from NOTES 24mm14 validation: iters=2, w=0.015 rms 0.30
|
||||||
|
// best (vs 1.42 for other w). Per-bin w 0.084–0.100 is the Haar error,
|
||||||
|
// not the blend. Use the validated scalar.
|
||||||
|
float w = 0.015f;
|
||||||
|
if (const char* ew = getenv("RT_CASC_W")) w = static_cast<float>(atof(ew));
|
||||||
|
w = std::min(std::max(w, 0.0f), 1.0f);
|
||||||
|
float one_minus_w = 1.0f - w;
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
acc[i] = acc[i] * w + bands_curve[i] * one_minus_w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy accumulator → bands_curve (52dbc0 memcpy)
|
||||||
|
std::memcpy(bands_curve, acc, nbin * sizeof(float));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void iir4_bidir_340510(float* x, size_t nbin) {
|
||||||
|
// BLOCKMAP:52af09 IIR4×2 bidir log-domain base 0x340510
|
||||||
|
// Uses DOUBLE precision (movsd/mulsd/cvtpd2ps in disasm 1191-1201)
|
||||||
|
// When RT_IIR4_GEN=1, generate via FUN_180533340 (freq-warp g=fc_norm/i|pow), else use proxy kIIR_A1/B1
|
||||||
|
static std::vector<double> genDown, genUp;
|
||||||
|
static int genN = 0;
|
||||||
|
const double* A1;
|
||||||
|
const double* B1;
|
||||||
|
const double* A2;
|
||||||
|
const double* B2;
|
||||||
|
static const int useGen = getenv("RT_IIR4_GEN") ? atoi(getenv("RT_IIR4_GEN")) : 0;
|
||||||
|
if (useGen) {
|
||||||
|
if ((int)nbin != genN) {
|
||||||
|
genDown.assign(nbin, 0.0); genUp.assign(nbin, 0.0);
|
||||||
|
double C = getenv("RT_IIR4_C") ? atof(getenv("RT_IIR4_C")) : 1000.0;
|
||||||
|
double tau = getenv("RT_IIR4_TAU") ? atof(getenv("RT_IIR4_TAU")) : 1200.0;
|
||||||
|
double p = getenv("RT_IIR4_P") ? atof(getenv("RT_IIR4_P")) : 0.5;
|
||||||
|
double mult = getenv("RT_IIR4_MULT") ? atof(getenv("RT_IIR4_MULT")) : 360.0;
|
||||||
|
double sr = getenv("RT_IIR4_SR") ? atof(getenv("RT_IIR4_SR")) : 48000.0;
|
||||||
|
generate_iir4_coefs(genDown.data(), genUp.data(), (int)nbin, C, tau, sr, p, mult);
|
||||||
|
genN = (int)nbin;
|
||||||
|
}
|
||||||
|
// down=1-up, so A=down, B=up
|
||||||
|
A1 = genDown.data(); B1 = genUp.data();
|
||||||
|
A2 = genDown.data(); B2 = genUp.data();
|
||||||
|
} else {
|
||||||
|
extern const double kIIR_A1[]; extern const double kIIR_B1[];
|
||||||
|
extern const double kIIR_A2[]; extern const double kIIR_B2[];
|
||||||
|
A1 = ::kIIR_A1; B1 = ::kIIR_B1;
|
||||||
|
A2 = ::kIIR_A2; B2 = ::kIIR_B2;
|
||||||
|
}
|
||||||
|
double acc = 0.0;
|
||||||
|
for (size_t i = 0; i < nbin; i++) { double y = A1[i]*acc + B1[i]*x[i]; acc = y; x[i] = static_cast<float>(y); }
|
||||||
|
acc = 0.0;
|
||||||
|
for (size_t i = nbin; i-- > 0;) { double y = A2[i]*acc + B2[i]*x[i]; acc = y; x[i] = static_cast<float>(y); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// IIR4 coefficient generator (FUN_180533340, BLOCKMAP:135-150)
|
||||||
|
// Generates frequency-dependent warp coefficients for chain_9_19 step 18
|
||||||
|
void generate_iir4_coefs(double* downCoef, double* upCoef,
|
||||||
|
int n, double C, double tau, double sr, double p, double mult) {
|
||||||
|
const double sr_scale = 0.9994880557060242; // DAT_1824c3d8c (live)
|
||||||
|
const double exp_scale = 0.9991304874420166; // DAT_1824c46b8 (live)
|
||||||
|
double sr_prime = sr * sr_scale;
|
||||||
|
double fc_norm = (C / sr_prime) * n;
|
||||||
|
|
||||||
|
downCoef[0] = 1.0;
|
||||||
|
upCoef[0] = 0.0;
|
||||||
|
for (int i = 1; i < n; i++) {
|
||||||
|
double g = (i <= fc_norm) ? (fc_norm / i) : std::pow(fc_norm / i, p);
|
||||||
|
double c = 1.0 / (g * tau / mult + 1.0);
|
||||||
|
// state[2] ≈ mult per BLOCKMAP, so normalize: exp(-c*g*tau/state2) → 0..1
|
||||||
|
upCoef[i] = std::exp(-c * g * tau / mult * exp_scale);
|
||||||
|
downCoef[i] = 1.0 - upCoef[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static inline void fir_min_phase_52b3cd_internal(float* scr, size_t nbin) {
|
||||||
|
// BLOCKMAP:52b3cd FIR min-phase 2049→4096 inv-RFFT fold×2 fwd EXP 1803831c0 q0.80
|
||||||
|
// Real RFFT pipeline validated cascade_sim.py fir_kernel 0.0065dB. Gate RT_FIR=1
|
||||||
|
// to keep canon 0.341 default. When enabled, scr (log domain) gets log|F| added.
|
||||||
|
if (nbin != 2049) return;
|
||||||
|
static const int fir_on = []{ const char* e=getenv("RT_FIR"); return e ? atoi(e) : 1; }();
|
||||||
|
if (!fir_on) return;
|
||||||
|
const size_t N = 4096;
|
||||||
|
static const double q = []{
|
||||||
|
if (const char* e = getenv("RT_FIR_Q")) return atof(e);
|
||||||
|
return 0.8002203702926636; // live .rdata 1820013f0 via ptrace /proc/pid/mem (was 0.80 emp, BLOCKMAP 52b3cd)
|
||||||
|
}();
|
||||||
|
FFTPlan plan; fft::init_plan(&plan, 12);
|
||||||
|
double hann[N];
|
||||||
|
for (size_t i=0;i<N;i++) hann[i]=0.5*(1.0 - std::cos(2.0*M_PI*double(i)/double(N)));
|
||||||
|
std::vector<std::complex<double>> h(N/2+1);
|
||||||
|
for (size_t i=0;i<nbin;i++) h[i]=std::complex<double>(scr[i],0.0);
|
||||||
|
h[N/2]=std::complex<double>(0.0,0.0);
|
||||||
|
std::vector<double> y(N,0.0);
|
||||||
|
fft::execute_real_inverse(&plan, h.data(), y.data());
|
||||||
|
for (size_t i=1;i<N/2;i++) y[i]*=2.0;
|
||||||
|
for (size_t i=N/2+1;i<N;i++) y[i]=0.0;
|
||||||
|
std::vector<std::complex<double>> X(N/2+1);
|
||||||
|
fft::execute_real_forward(&plan, y.data(), X.data());
|
||||||
|
for (auto &c: X) c *= q;
|
||||||
|
for (auto &c: X) c = std::exp(c);
|
||||||
|
std::vector<double> w(N,0.0);
|
||||||
|
fft::execute_real_inverse(&plan, X.data(), w.data());
|
||||||
|
for (size_t i=0;i<N/2;i++) w[i]*= hann[N/2+i];
|
||||||
|
for (size_t i=N/2;i<N;i++) w[i]=0.0;
|
||||||
|
std::vector<std::complex<double>> F(N/2+1);
|
||||||
|
fft::execute_real_forward(&plan, w.data(), F.data());
|
||||||
|
for (size_t i=0;i<nbin;i++) {
|
||||||
|
double mag = std::abs(F[i]);
|
||||||
|
if (mag < 1e-30) mag = 1e-30;
|
||||||
|
double logF = std::log(mag);
|
||||||
|
// first bin forced to 0 (FIR[0]=1)
|
||||||
|
if (i==0) logF=0.0;
|
||||||
|
scr[i] += static_cast<float>(logF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Main chain 9–19 (BLOCKMAP:620, 540 table, 52a583-52b3a0) ----------------
|
||||||
|
// Structural proxy — math-exact via numpy-equivalent cores; bit-exact C++
|
||||||
|
// port will replace k_div/k_exp with vpermps+poly 1803a06a0 / 180296c80
|
||||||
|
// (BLOCKMAP:580/569) and FMA triples re/im/coef 1fa0/1940 (BLOCKMAP:400).
|
||||||
|
// ACC pointer table @0x5407c8 (slot rendered in rendersnap2.py) holds
|
||||||
|
// per-frame band ACC_i vectors for step 10 (dc40).
|
||||||
|
|
||||||
|
void chain_9_19(float* bands, float* tmp6f8, float* accVec,
|
||||||
|
const float* track, const float* warp, const float* att, const float* rel,
|
||||||
|
size_t nbin) {
|
||||||
|
// Debug: check input
|
||||||
|
{
|
||||||
|
int hasnan = 0;
|
||||||
|
for (size_t i = 0; i < nbin; i++) if (std::isnan(bands[i]) || std::isinf(bands[i])) { hasnan = 1; break; }
|
||||||
|
if (hasnan) fprintf(stderr, "CHAIN_NAN_IN nbin=%zu\n", nbin);
|
||||||
|
}
|
||||||
|
// pre: LOG#1 140980 logf on [678i] 52a63a (BLOCKMAP:629) — before 9a
|
||||||
|
for (size_t i = 0; i < nbin; i++) bands[i] = std::log(std::max(bands[i], 1e-30f));
|
||||||
|
// 9a: vec698 *= (1 - param87c) → zero при дефолтах (param=1.0)
|
||||||
|
// 9b: vec6f8 += param87c*0.8 @1824c3e28 (BLOCKMAP:589)
|
||||||
|
// 9c: DIVIDE dst=678i A=bands B=6f8 1803a06a0 vpermps (BLOCKMAP:580)
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
float a = bands[i] != 0 ? bands[i] : 1e-30f;
|
||||||
|
float b = tmp6f8[i] + 0.8f;
|
||||||
|
bands[i] = divide_1803a06a0(a, b);
|
||||||
|
}
|
||||||
|
// 10: vec6f8 = bands - ACC_i dc40 tbl@5407c8 (BLOCKMAP:596)
|
||||||
|
for (size_t i = 0; i < nbin; i++) tmp6f8[i] = bands[i] - accVec[i];
|
||||||
|
// 11: FMA ATT/REL upper/lower 1fa0/1940→3c40 (BLOCKMAP:400) re/im/coef 12B
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
if (i < nbin/2) tmp6f8[i] += att[i] * accVec[i];
|
||||||
|
else tmp6f8[i] += rel[i] * accVec[i];
|
||||||
|
}
|
||||||
|
// Update accVec for steady-state iteration (ACC persists across frames)
|
||||||
|
for (size_t i = 0; i < nbin; i++) accVec[i] = tmp6f8[i];
|
||||||
|
// 14: EXP#1 180296c80 expf + +=(-1) th2270 (24mm2 order fix)
|
||||||
|
for (size_t i = 0; i < nbin; i++) bands[i] = expf_180296c80(bands[i]) - 1.0f;
|
||||||
|
// 15: array-mul track* th2000 (track per-band from ctx+0x540768)
|
||||||
|
if (track) for (size_t i = 0; i < nbin; i++) bands[i] *= track[i];
|
||||||
|
// 16: *=kWarp 52ae8f + LOG#2 140980 logf 52aefd (BLOCKMAP:638)
|
||||||
|
for (size_t i = 0; i < nbin; i++) bands[i] *= warp[i];
|
||||||
|
for (size_t i = 0; i < nbin; i++) bands[i] = std::log(std::max(bands[i], 1e-30f));
|
||||||
|
// 16b: IIR4×2 bidir log-domain base 0x340510 52af09 (BLOCKMAP:639) — DOUBLE precision
|
||||||
|
iir4_bidir_340510(bands, nbin);
|
||||||
|
// FIR min-phase (BLOCKMAP:52b3cd) — frequency-domain convolution
|
||||||
|
fir_min_phase_52b3cd_internal(bands, nbin);
|
||||||
|
// 17: EXP#2 + exp-variant 140a40/140b00
|
||||||
|
for (size_t i = 0; i < nbin; i++) bands[i] = expf_180296c80(bands[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Legacy structural chain (pre-cascade) ---------------------------------
|
||||||
|
|
||||||
void iir1(float* x, const double* A, const double* B, size_t nbin, double acc0) {
|
void iir1(float* x, const double* A, const double* B, size_t nbin, double acc0) {
|
||||||
// leaky first-order: y = A*acc + B*x ; acc = y (B = 1-A from live tables)
|
// leaky first-order: y = A*acc + B*x ; acc = y (B = 1-A from live tables)
|
||||||
// State persists across calls via static accumulator (per-thread).
|
// State persists across calls via static accumulator (per-thread).
|
||||||
@@ -65,4 +400,8 @@ void dry_wet(float* mask, float fVar30, float wet, size_t nbin) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void fir_min_phase_52b3cd(float* scr, size_t nbin) {
|
||||||
|
fir_min_phase_52b3cd_internal(scr, nbin);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace fn529fe0
|
} // namespace fn529fe0
|
||||||
|
|||||||
@@ -18,6 +18,67 @@
|
|||||||
// (level = am/res) but fed through the structural chain instead of the LUT bridge.
|
// (level = am/res) but fed through the structural chain instead of the LUT bridge.
|
||||||
namespace fn529fe0 {
|
namespace fn529fe0 {
|
||||||
|
|
||||||
|
// ---- Detector cascade 529c60 -----------------------------------------------
|
||||||
|
|
||||||
|
// Per-band persistent state for the detector cascade.
|
||||||
|
// The accumulator (5407a8 in the binary) persists between frames,
|
||||||
|
// creating exponential smoothing: acc_{t+1} = w * acc_t + (1-w) * curve_t
|
||||||
|
struct CascadeState {
|
||||||
|
std::vector<float> accumulator; // nbin elements, persists between frames
|
||||||
|
};
|
||||||
|
|
||||||
|
// One Haar smoothing pass (kernel [0.25, 0.5, 0.25]).
|
||||||
|
// Decoded from 529c60 Haar loop (BLOCKMAP 24mm14, lines 35-74).
|
||||||
|
// Net effect: b[i] = 0.25*b[i-1] + 0.5*b[i] + 0.25*b[i+1] (wavelet smooth).
|
||||||
|
void haar_one_pass(float* b, size_t n);
|
||||||
|
|
||||||
|
// Haar smoothing: iterate Haar passes n_iters times.
|
||||||
|
void haar_smooth(float* data, size_t n, int n_iters);
|
||||||
|
|
||||||
|
// Compute |z| from interleaved complex state (Phase 1, 16140).
|
||||||
|
// in: interleaved [re0,im0,re1,im1,...], out: [mag0,mag1,...]
|
||||||
|
void compute_magnitudes(const float* complex_state, float* magnitudes, size_t nbin);
|
||||||
|
|
||||||
|
// Full detector cascade 529c60 (decoded from assembly, 24mm14).
|
||||||
|
//
|
||||||
|
// Pipeline:
|
||||||
|
// 1. compute_magnitudes: complex → |z| (skipped if is_magnitude=true)
|
||||||
|
// 2. haar_smooth: |z| → smoothed curve
|
||||||
|
// 3. peak = max(curve)
|
||||||
|
// 4. sin_peak = sin(param*30 - 90) * 0.115129 * peak
|
||||||
|
// 5. curve[i] = max(curve[i], sin_peak)
|
||||||
|
// 6. w = -log10(pow(50, ratio*0.001) * ratio*0.001)
|
||||||
|
// 7. acc[i] = acc[i] * w + curve[i] * (1-w)
|
||||||
|
// 8. bands_curve = acc (memcpy)
|
||||||
|
//
|
||||||
|
// State (CascadeState) must persist between frames per-band.
|
||||||
|
// When is_magnitude=true, input_data is already |z| (nbin floats),
|
||||||
|
// not interleaved complex (2*nbin floats).
|
||||||
|
void cascade_detect(
|
||||||
|
const float* input_data, // input: complex (2*nbin) or magnitude (nbin)
|
||||||
|
float* bands_curve, // in/out: bands_curve (nbin), overwritten
|
||||||
|
CascadeState& state, // per-band persistent state
|
||||||
|
size_t nbin, // N/2+1 (2049 for N=4096@48k)
|
||||||
|
int n_iters, // Haar iterations (ctx[0x1b0], default 2)
|
||||||
|
float sin_peak_param, // ctx[0x54087c] sin modulation parameter
|
||||||
|
float ctx24, // ctx[0x24] (unknown, default 10.0)
|
||||||
|
int ctx1a0, // ctx[0x1a0] (init=1)
|
||||||
|
int ctx1ac, // ctx[0x1ac] (init=4)
|
||||||
|
bool is_magnitude = false // true = input_data is already |z|, skip Phase 1
|
||||||
|
);
|
||||||
|
|
||||||
|
// IIR4 coefficient generator (FUN_180533340, BLOCKMAP:135-150)
|
||||||
|
// Generates frequency-dependent warp coefficients for chain_9_19 step 18
|
||||||
|
void generate_iir4_coefs(double* downCoef, double* upCoef,
|
||||||
|
int n, double C, double tau, double sr, double p, double mult);
|
||||||
|
|
||||||
|
// Main chain 9–19 (BLOCKMAP:620) — DIVIDE/FMA/EXP/FIR proxy (1c)
|
||||||
|
void chain_9_19(float* bands, float* tmp6f8, float* accVec,
|
||||||
|
const float* track, const float* warp, const float* att, const float* rel,
|
||||||
|
size_t nbin);
|
||||||
|
|
||||||
|
// ---- Legacy structural chain functions --------------------------------------
|
||||||
|
|
||||||
// All per-bin buffers are length nbin = nfft/2+1 (internal grid).
|
// All per-bin buffers are length nbin = nfft/2+1 (internal grid).
|
||||||
// IIR stage: y[i] = A[i]*acc + B[i]*x[i]; acc=y (first-order leaky, like leveltrack).
|
// IIR stage: y[i] = A[i]*acc + B[i]*x[i]; acc=y (first-order leaky, like leveltrack).
|
||||||
void iir1(float* x, const double* A, const double* B, size_t nbin, double acc0);
|
void iir1(float* x, const double* A, const double* B, size_t nbin, double acc0);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
// - blend_exp2 : out == exp2(-x)*blend, blend = freqaxis*(1-mix)+mix*0.8
|
// - blend_exp2 : out == exp2(-x)*blend, blend = freqaxis*(1-mix)+mix*0.8
|
||||||
// - combine_acc: subtract then add band/f6f8 contributions (exact)
|
// - combine_acc: subtract then add band/f6f8 contributions (exact)
|
||||||
// - warp_mask : multiplies by kBand768*kWarp
|
// - warp_mask : multiplies by kBand768*kWarp
|
||||||
|
// - cascade : Haar, magnitudes, blend (529c60 decode)
|
||||||
int main() {
|
int main() {
|
||||||
const size_t nbin = 2049; // internal N/2+1 grid used by the chain
|
const size_t nbin = 2049; // internal N/2+1 grid used by the chain
|
||||||
const size_t nfft = 4096;
|
const size_t nfft = 4096;
|
||||||
@@ -78,6 +79,142 @@ int main() {
|
|||||||
std::printf("live: kWarp[0]=%.3f kWarp[2048]=%.3f kBand768[0]=%.3f kBand768[2048]=%.3f\n",
|
std::printf("live: kWarp[0]=%.3f kWarp[2048]=%.3f kBand768[0]=%.3f kBand768[2048]=%.3f\n",
|
||||||
kWarp[0], kWarp[2048], k768[0], k768[2048]);
|
kWarp[0], kWarp[2048], k768[0], k768[2048]);
|
||||||
|
|
||||||
|
// === Cascade 529c60 tests ===
|
||||||
|
|
||||||
|
// --- haar_one_pass: kernel [0.25, 0.5, 0.25] ---
|
||||||
|
{
|
||||||
|
// Input: [1, 3, 5, 7, 9] (5 elements)
|
||||||
|
// Expected: b[0]=0.5*(1+3)=2.0; b[1]=0.25*1+0.5*3+0.25*5=3.0;
|
||||||
|
// b[2]=0.25*3+0.5*5+0.25*7=5.0; b[3]=0.25*5+0.5*7+0.25*9=7.0;
|
||||||
|
// b[4]=0.25*7+0.75*9=8.5 (boundary)
|
||||||
|
float data[] = {1.0f, 3.0f, 5.0f, 7.0f, 9.0f};
|
||||||
|
float expected[] = {2.0f, 3.0f, 5.0f, 7.0f, 8.0f};
|
||||||
|
fn529fe0::haar_one_pass(data, 5);
|
||||||
|
double max_h = 0.0;
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
max_h = std::fmax(max_h, std::fabs(data[i] - expected[i]));
|
||||||
|
std::printf("haar_one_pass: max|d|=%.3e (%s)\n", max_h,
|
||||||
|
max_h < 1e-6 ? "OK" : "MISMATCH");
|
||||||
|
if (max_h >= 1e-6) fail = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- haar_smooth: 2 iterations on ramp ---
|
||||||
|
{
|
||||||
|
float data[] = {0.0f, 0.25f, 0.5f, 0.75f, 1.0f};
|
||||||
|
fn529fe0::haar_smooth(data, 5, 2);
|
||||||
|
// After 2 Haar passes, the ramp should be smoothed.
|
||||||
|
// Just check monotonicity and bounds [0, 1].
|
||||||
|
bool ok = true;
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
if (data[i] < -0.01f || data[i] > 1.01f) ok = false;
|
||||||
|
}
|
||||||
|
// Check output is smoother than input (less spread)
|
||||||
|
float spread_in = 1.0f - 0.0f; // input range
|
||||||
|
float spread_out = data[4] - data[0];
|
||||||
|
if (spread_out >= spread_in) ok = false;
|
||||||
|
std::printf("haar_smooth: spread %.3f→%.3f (%s)\n",
|
||||||
|
spread_in, spread_out, ok ? "OK" : "MISMATCH");
|
||||||
|
if (!ok) fail = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- compute_magnitudes: |z| from complex pairs ---
|
||||||
|
{
|
||||||
|
// Input: [3,4, 5,12, 0,0] → [5, 13, 0]
|
||||||
|
float complex_state[] = {3.0f, 4.0f, 5.0f, 12.0f, 0.0f, 0.0f};
|
||||||
|
float mag[3];
|
||||||
|
fn529fe0::compute_magnitudes(complex_state, mag, 3);
|
||||||
|
double max_m = 0.0;
|
||||||
|
max_m = std::fmax(max_m, std::fabs(mag[0] - 5.0f));
|
||||||
|
max_m = std::fmax(max_m, std::fabs(mag[1] - 13.0f));
|
||||||
|
max_m = std::fmax(max_m, std::fabs(mag[2] - 0.0f));
|
||||||
|
std::printf("compute_magnitudes: max|d|=%.3e (%s)\n", max_m,
|
||||||
|
max_m < 1e-5 ? "OK" : "MISMATCH");
|
||||||
|
if (max_m >= 1e-5) fail = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- cascade_detect: full pipeline smoke test ---
|
||||||
|
{
|
||||||
|
// Create test signal: DC=1 in all bins (complex: re=1, im=0)
|
||||||
|
std::vector<float> complex_state(2 * nbin);
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
complex_state[2 * i] = 1.0f; // re
|
||||||
|
complex_state[2 * i + 1] = 0.0f; // im
|
||||||
|
}
|
||||||
|
std::vector<float> bands_curve(nbin, 0.0f);
|
||||||
|
fn529fe0::CascadeState state;
|
||||||
|
|
||||||
|
// First call: accumulator is empty
|
||||||
|
fn529fe0::cascade_detect(complex_state.data(), bands_curve.data(),
|
||||||
|
state, nbin, 2,
|
||||||
|
0.0f, // sin_peak_param=0 (disabled)
|
||||||
|
10.0f, // ctx24
|
||||||
|
1, // ctx1a0
|
||||||
|
4); // ctx1ac
|
||||||
|
|
||||||
|
// All magnitudes are 1.0, Haar-smoothed should be ~1.0
|
||||||
|
// Peak should be ~1.0, sin_peak disabled
|
||||||
|
// Check output is in valid range
|
||||||
|
bool ok = true;
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
if (bands_curve[i] < -0.01f || bands_curve[i] > 2.0f) ok = false;
|
||||||
|
}
|
||||||
|
std::printf("cascade_detect DC: [0]=%.4f [mid]=%.4f [end]=%.4f (%s)\n",
|
||||||
|
bands_curve[0], bands_curve[nbin/2], bands_curve[nbin-1],
|
||||||
|
ok ? "OK" : "MISMATCH");
|
||||||
|
if (!ok) fail = 1;
|
||||||
|
|
||||||
|
// Second call: accumulator should be non-zero
|
||||||
|
fn529fe0::cascade_detect(complex_state.data(), bands_curve.data(),
|
||||||
|
state, nbin, 2, 0.0f, 10.0f, 1, 4);
|
||||||
|
std::printf("cascade_detect DC 2nd: acc[0]=%.6f out[0]=%.4f\n",
|
||||||
|
state.accumulator[0], bands_curve[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- cascade_detect: alternating signal ---
|
||||||
|
{
|
||||||
|
std::vector<float> cs(2 * nbin);
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
cs[2 * i] = (i % 2 == 0) ? 2.0f : 0.5f;
|
||||||
|
cs[2 * i + 1] = 0.0f;
|
||||||
|
}
|
||||||
|
std::vector<float> bc(nbin, 0.0f);
|
||||||
|
fn529fe0::CascadeState st;
|
||||||
|
fn529fe0::cascade_detect(cs.data(), bc.data(), st, nbin, 2,
|
||||||
|
0.0f, 10.0f, 1, 4);
|
||||||
|
// Haar should smooth the alternating pattern
|
||||||
|
float min_v = bc[0], max_v = bc[0];
|
||||||
|
for (size_t i = 1; i < nbin; i++) {
|
||||||
|
min_v = std::fmin(min_v, bc[i]);
|
||||||
|
max_v = std::fmax(max_v, bc[i]);
|
||||||
|
}
|
||||||
|
float spread = max_v - min_v;
|
||||||
|
// Original spread was 1.5, after 2 Haar passes should be much smaller
|
||||||
|
bool ok = spread < 0.5f;
|
||||||
|
std::printf("cascade_detect alt: spread=%.4f [0]=%.4f [1]=%.4f (%s)\n",
|
||||||
|
spread, bc[0], bc[1], ok ? "OK" : "MISMATCH");
|
||||||
|
if (!ok) fail = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- chain_9_19: gated pipeline smoke (LOG#1→DIVIDE→dc40→FMA→EXP-1→*track→*warp→LOG#2→IIR4→FIR→EXP#2) ---
|
||||||
|
{
|
||||||
|
std::vector<float> bands(nbin, 0.5f), tmp(nbin, 0.1f), acc(nbin, 0.0f);
|
||||||
|
std::vector<float> warp(nbin, 1.0f), att(nbin, 0.0f), rel(nbin, 0.0f);
|
||||||
|
std::vector<float> bands0 = bands;
|
||||||
|
fn529fe0::chain_9_19(bands.data(), tmp.data(), acc.data(), nullptr, warp.data(), att.data(), rel.data(), nbin);
|
||||||
|
bool ok = true;
|
||||||
|
for (size_t i = 0; i < nbin; i++) if (!std::isfinite(bands[i]) || bands[i] < 0.0f || bands[i] > 5.0f) ok = false;
|
||||||
|
std::printf("chain_9_19 smoke: in0=%.3f out0=%.3f outmid=%.3f finite=%d (%s)\n",
|
||||||
|
bands0[0], bands[0], bands[nbin/2], ok, ok ? "OK" : "MISMATCH");
|
||||||
|
if (!ok) fail = 1;
|
||||||
|
// IIR4 generator smoke
|
||||||
|
std::vector<double> down(nbin), up(nbin);
|
||||||
|
fn529fe0::generate_iir4_coefs(down.data(), up.data(), (int)nbin, 1000.0, 1200.0, 48000.0, 0.5, 360.0);
|
||||||
|
bool gok = std::fabs(down[0]-1.0)<1e-9 && std::fabs(up[0])<1e-9 && down[1] < 1.0 && down[1] > 0.0;
|
||||||
|
std::printf("generate_iir4: down0=%.3f up0=%.3f down1=%.4f up1=%.4f (%s)\n",
|
||||||
|
down[0], up[0], down[1], up[1], gok ? "OK" : "MISMATCH");
|
||||||
|
if (!gok) fail = 1;
|
||||||
|
}
|
||||||
|
|
||||||
std::printf("fn529fe0 check %s\n", fail ? "FAIL" : "PASS");
|
std::printf("fn529fe0 check %s\n", fail ? "FAIL" : "PASS");
|
||||||
return fail;
|
return fail;
|
||||||
}
|
}
|
||||||
|
|||||||
+316
-31
@@ -8,6 +8,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -53,17 +54,54 @@ static double warp_c(double f) {
|
|||||||
return 0.87 * 7.942 * x / (7.942 + x);
|
return 0.87 * 7.942 * x / (7.942 + x);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Δ second-peak: template-local gain g(dist) — farther peaks get deeper cut.
|
||||||
|
// cut(bin) += g * vlaw_delta where g = 1 + (|bin - kfc|/nbin) * RT_DELTA_DIST.
|
||||||
|
// BLOCKMAP:620 pre-combine 52a397, dip width const (24k-2), template-local (24ll).
|
||||||
|
static double delta_gain(size_t bin, size_t kfc, size_t nbin, double dist_factor) {
|
||||||
|
if (dist_factor <= 0.0) return 1.0;
|
||||||
|
double dist = std::fabs((double)bin - (double)kfc) / (double)nbin;
|
||||||
|
return 1.0 + dist * dist_factor;
|
||||||
|
}
|
||||||
|
|
||||||
static bool is_internal_grid(size_t nfft, float sample_rate) {
|
static bool is_internal_grid(size_t nfft, float sample_rate) {
|
||||||
return nfft == 4096 && std::abs(sample_rate - 48000.0f) < 1.0f;
|
return nfft == 4096 && std::abs(sample_rate - 48000.0f) < 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static double k_mapping_factor(float fc, float q, float sens) {
|
||||||
|
// k(sens,q,fc) = k_sens * k_q * k_fc (NOTES 24x/24dd/24ee)
|
||||||
|
// Fitted: k_sens 6→0.44 12→1.0 18→5.37 24→22.0 (exp after 12, linear before)
|
||||||
|
// k_q 0.5→1.0 2.0→0.403 (log interp, q no effect above 2 per 24kk)
|
||||||
|
// k_fc 1.0 for now (fc via W_eq weak, keep 1.0 gated RT_KMAP_FC)
|
||||||
|
double k_sens;
|
||||||
|
if (sens < 12) k_sens = 0.44 + (sens - 6.0) * (0.56 / 6.0);
|
||||||
|
else if (sens == 12) k_sens = 1.0;
|
||||||
|
else if (sens < 24) k_sens = std::exp((sens - 12.0) * std::log(22.0) / 12.0);
|
||||||
|
else k_sens = 22.0;
|
||||||
|
double k_q;
|
||||||
|
if (q >= 2.0) k_q = 0.403;
|
||||||
|
else if (q <= 0.5) k_q = 1.0;
|
||||||
|
else {
|
||||||
|
double t = (std::log(q) - std::log(0.5)) / (std::log(2.0) - std::log(0.5));
|
||||||
|
k_q = 1.0 + t * (0.403 - 1.0);
|
||||||
|
}
|
||||||
|
double k_fc = 1.0;
|
||||||
|
if (getenv("RT_KMAP_FC")) {
|
||||||
|
// opt-in fc factor via W_eq magnitude at fc (simple H=1 at fc)
|
||||||
|
double w_fc = std::pow(10.0, (sens * 0.3 / 12.0) / 20.0);
|
||||||
|
k_fc = 1.0 / w_fc; // naive, gated
|
||||||
|
}
|
||||||
|
return k_sens * k_q * k_fc;
|
||||||
|
}
|
||||||
|
|
||||||
static void process_band_structural(
|
static void process_band_structural(
|
||||||
const float* am,
|
const float* am,
|
||||||
const float* res,
|
const float* res,
|
||||||
const DetectorBand& band,
|
const DetectorBand& band,
|
||||||
float* mask_out,
|
float* mask_out,
|
||||||
size_t nfft,
|
size_t nfft,
|
||||||
float sample_rate
|
float sample_rate,
|
||||||
|
size_t num_bands = 1,
|
||||||
|
float* track = nullptr
|
||||||
) {
|
) {
|
||||||
const size_t half = nfft / 2;
|
const size_t half = nfft / 2;
|
||||||
const size_t nbin = half + 1;
|
const size_t nbin = half + 1;
|
||||||
@@ -84,7 +122,7 @@ static void process_band_structural(
|
|||||||
// Extracted from refs: A=-13.78dB, B=68.29dB, gamma=0.344 (NOTES_LEVEL:967)
|
// Extracted from refs: A=-13.78dB, B=68.29dB, gamma=0.344 (NOTES_LEVEL:967)
|
||||||
// RT_LUT_* env overrides: EXPERIMENTAL solver tooling (NOTES_LEVEL 22d),
|
// RT_LUT_* env overrides: EXPERIMENTAL solver tooling (NOTES_LEVEL 22d),
|
||||||
// live-capture candidates are A=-24 B=28 gamma=1 (BandConfig, 22b).
|
// live-capture candidates are A=-24 B=28 gamma=1 (BandConfig, 22b).
|
||||||
float lut_a = -13.78f, lut_b = 68.29f, lut_g = 0.344f, lut_m = 4.2f;
|
float lut_a = -24.0f, lut_b = 28.0f, lut_g = 1.0f, lut_m = 4.2f; // CAP -24/28/1 live 180563a60 24z
|
||||||
if (const char* e = getenv("RT_LUT_A")) lut_a = atof(e);
|
if (const char* e = getenv("RT_LUT_A")) lut_a = atof(e);
|
||||||
if (const char* e = getenv("RT_LUT_B")) lut_b = atof(e);
|
if (const char* e = getenv("RT_LUT_B")) lut_b = atof(e);
|
||||||
if (const char* e = getenv("RT_LUT_G")) lut_g = atof(e);
|
if (const char* e = getenv("RT_LUT_G")) lut_g = atof(e);
|
||||||
@@ -114,7 +152,15 @@ static void process_band_structural(
|
|||||||
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
|
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
|
||||||
lvl_in[k] = static_cast<float>(static_cast<double>(am[k]) / res_k * scale_factor_x);
|
lvl_in[k] = static_cast<float>(static_cast<double>(am[k]) / res_k * scale_factor_x);
|
||||||
}
|
}
|
||||||
if (pool_w > 0 && !lut_off == false) {}
|
// k-mapping per NOTES 24x/24dd/24ee: lvl_impl = lvl_ours / k(sens,q,fc)
|
||||||
|
// default OFF (canon), opt-in RT_KMAP=1 (helper k_mapping_factor)
|
||||||
|
static const int kmap_on = getenv("RT_KMAP") ? atoi(getenv("RT_KMAP")) : 0;
|
||||||
|
if (kmap_on) {
|
||||||
|
double k_tot = k_mapping_factor(band.fc, band.q, band.sens);
|
||||||
|
if (k_tot > 1e-9) {
|
||||||
|
for (size_t k = 0; k < nbin; k++) lvl_in[k] = static_cast<float>(lvl_in[k] / k_tot);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (pool_w > 0) {
|
if (pool_w > 0) {
|
||||||
std::vector<float> pooled(nbin);
|
std::vector<float> pooled(nbin);
|
||||||
for (size_t k = 0; k < nbin; k++) {
|
for (size_t k = 0; k < nbin; k++) {
|
||||||
@@ -134,24 +180,79 @@ static void process_band_structural(
|
|||||||
for (size_t k = 0; k < nbin; k++) if (lvl_in[k] > cap) lvl_in[k] = cap;
|
for (size_t k = 0; k < nbin; k++) if (lvl_in[k] > cap) lvl_in[k] = cap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cascade sin-peak floor (529c60): the -20.72 dB floor mechanism.
|
||||||
|
// From assembly: sin_peak = sin(param * 30 - 90) * (ln10/20) * peak
|
||||||
|
// where ln10/20 = 0.115129 (constant at 0x1824c3cd4).
|
||||||
|
// This prevents over-reduction by clamping the level curve.
|
||||||
|
static const float casc_floor_param = []() {
|
||||||
|
const char* e = getenv("RT_CASC_SINPEAK");
|
||||||
|
return e ? (float)atof(e) : 0.0f;
|
||||||
|
}();
|
||||||
|
if (casc_floor_param != 0.0f) {
|
||||||
|
// Find peak of level curve
|
||||||
|
float peak_lvl = 0.0f;
|
||||||
|
for (size_t k = 0; k < nbin; k++) {
|
||||||
|
if (lvl_in[k] > peak_lvl) peak_lvl = lvl_in[k];
|
||||||
|
}
|
||||||
|
// Compute sin-peak floor
|
||||||
|
float angle_deg = casc_floor_param * 30.0f - 90.0f;
|
||||||
|
float sin_peak = std::sin(angle_deg * static_cast<float>(M_PI) / 180.0f)
|
||||||
|
* 0.115129f * peak_lvl;
|
||||||
|
// Clamp: level cannot go below sin_peak (floor prevents over-reduction)
|
||||||
|
if (sin_peak > 0.0f) {
|
||||||
|
for (size_t k = 0; k < nbin; k++) {
|
||||||
|
if (lvl_in[k] < sin_peak) lvl_in[k] = sin_peak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save raw level BEFORE LUT transform (for RT_FIRPOWER)
|
// Save raw level BEFORE LUT transform (for RT_FIRPOWER)
|
||||||
std::vector<float> raw_level(nbin);
|
std::vector<float> raw_level(nbin);
|
||||||
for (size_t k = 0; k < nbin; k++) {
|
for (size_t k = 0; k < nbin; k++) {
|
||||||
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
|
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
|
||||||
raw_level[k] = static_cast<float>(static_cast<double>(am[k]) / res_k * scale_factor_x);
|
raw_level[k] = static_cast<float>(static_cast<double>(am[k]) / res_k * scale_factor_x);
|
||||||
}
|
}
|
||||||
|
if (kmap_on) {
|
||||||
|
double k_tot2 = k_mapping_factor(band.fc, band.q, band.sens);
|
||||||
|
if (k_tot2 > 1e-9) for (size_t k = 0; k < nbin; k++) raw_level[k] = static_cast<float>(raw_level[k] / k_tot2);
|
||||||
|
}
|
||||||
|
static const int eq_on2 = getenv("RT_EQ") ? atoi(getenv("RT_EQ")) : 1;
|
||||||
|
if (eq_on2) {
|
||||||
|
double eq_gain = 0.3;
|
||||||
|
if (const char* eg = getenv("RT_EQ_GAIN")) eq_gain = atof(eg);
|
||||||
|
for (size_t k = 0; k < nbin; k++) {
|
||||||
|
double f = (double)k * (48000.0 * 0.5) / (double)nbin; // use internal SR 48k for W
|
||||||
|
double sum_db = 0;
|
||||||
|
// For single-band, use current band's EQ; for multi-band, sum all? Use current band only for lvl
|
||||||
|
double H = 0;
|
||||||
|
if (band.fc >= 1.0) {
|
||||||
|
double Qeff = 1.54 * std::pow(band.q, 1.33);
|
||||||
|
double A = f / band.fc - band.fc / f;
|
||||||
|
if (std::isfinite(A)) H = 1.0 / std::sqrt(1.0 + (Qeff*A)*(Qeff*A));
|
||||||
|
}
|
||||||
|
sum_db = band.sens * H;
|
||||||
|
double w = std::pow(10.0, (sum_db * eq_gain / 12.0) / 20.0);
|
||||||
|
lvl_in[k] *= static_cast<float>(w);
|
||||||
|
raw_level[k] *= static_cast<float>(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RT_VLAW=1 (NOTES 24m): decoded two-stage detector law.
|
// RT_VLAW=1 (NOTES 24m): decoded two-stage detector law.
|
||||||
// cutS(b) = 1.729*ln(1 + lvl_raw/0.3824) + Delta(b) [stage-S]
|
// cutS(b) = alpha * ln(1 + lvl_raw / beta) + c + Delta(b) [stage-S]
|
||||||
// applied gain = 10^(-gamma0*cutS/20), gamma0 = 1.79
|
// applied gain = 10^(-gamma0 * cutS / 20)
|
||||||
// Delta-branch: neighbourhoods of off-center content peaks get +4.18 dB.
|
// Delta-branch: neighbourhoods of off-center content peaks get +4.18 dB.
|
||||||
// Bypasses LUT/exp2/blend/warp/IIR3 entirely.
|
// Bypasses LUT/exp2/blend/warp/IIR3 entirely.
|
||||||
static const int vlaw = getenv("RT_VLAW") ? atoi(getenv("RT_VLAW")) : 0;
|
static const int vlaw = getenv("RT_VLAW") ? atoi(getenv("RT_VLAW")) : 0;
|
||||||
|
static const int firconv3 = getenv("RT_FIRCONV") ? atoi(getenv("RT_FIRCONV")) : 0;
|
||||||
static int frame_dbg_ctr = 0;
|
static int frame_dbg_ctr = 0;
|
||||||
if (vlaw) {
|
if (vlaw) {
|
||||||
double kfc = static_cast<double>(band.fc) / (sample_rate / 2.0) * (nbin - 1);
|
double kfc = static_cast<double>(band.fc) / (sample_rate / 2.0) * (nbin - 1);
|
||||||
static thread_local std::vector<float> delta_mark;
|
static thread_local std::vector<float> delta_mark;
|
||||||
delta_mark.assign(nbin, 0.0f);
|
delta_mark.assign(nbin, 0.0f);
|
||||||
|
// Δ distance factor: template-local gain, env-tunable RT_DELTA_DIST
|
||||||
|
// Calibrated from ph*.npz (dual_b1q_0.5): factor≈3.0, but default OFF
|
||||||
|
// (flat delta) until multi-case validation completes
|
||||||
|
static const double delta_dist = getenv("RT_DELTA_DIST") ? atof(getenv("RT_DELTA_DIST")) : 0.0;
|
||||||
for (size_t k2 = 1; k2 + 1 < nbin; k2++) {
|
for (size_t k2 = 1; k2 + 1 < nbin; k2++) {
|
||||||
if (raw_level[k2] <= 0.25) continue;
|
if (raw_level[k2] <= 0.25) continue;
|
||||||
if (std::fabs((double)k2 - kfc) <= 8.0) continue;
|
if (std::fabs((double)k2 - kfc) <= 8.0) continue;
|
||||||
@@ -164,15 +265,76 @@ static void process_band_structural(
|
|||||||
if (!lmax) continue;
|
if (!lmax) continue;
|
||||||
for (int d = -3; d <= 3; d++) {
|
for (int d = -3; d <= 3; d++) {
|
||||||
int kk = (int)k2 + d;
|
int kk = (int)k2 + d;
|
||||||
if (kk >= 0 && kk < (int)nbin) delta_mark[kk] = 1.0f;
|
if (kk >= 0 && kk < (int)nbin)
|
||||||
|
delta_mark[kk] = std::max(delta_mark[kk], (float)delta_gain(k2, (size_t)kfc, nbin, delta_dist));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// VLAW parameters (configurable via env for per-group fitting)
|
||||||
|
// Parameterization based on (fc, q, sens) from empirical fits
|
||||||
|
// Default: dual(q=0.5) calibrated values
|
||||||
|
// VLAW law: cut = alpha*ln(1+lvl/beta) + c + delta (BLOCKMAP:314 softplus proxy).
|
||||||
|
// Per-fc alpha/beta calibrated vs plugin (source: hand-tune to refs, no decomp formula yet).
|
||||||
|
// Multi-band (comb) uses near-zero law for neutrality.
|
||||||
|
double vlaw_alpha = 3.2193, vlaw_beta = 0.4927, vlaw_c = 0.5423, vlaw_delta = 6.9177;
|
||||||
|
if (num_bands > 1) {
|
||||||
|
vlaw_alpha = 0.05; vlaw_beta = 5.0; vlaw_c = 0.0; vlaw_delta = 0.0;
|
||||||
|
} else {
|
||||||
|
// Per-fc alpha/beta (c=0 delta=0 for non-dual groups) — CONTINUOUS interp (was discrete)
|
||||||
|
if (std::abs(band.fc - 678.7611083984375f) < 0.01f && band.q >= 0.99) {
|
||||||
|
vlaw_alpha = 4.0; vlaw_beta = 0.3; vlaw_c = 0.0; vlaw_delta = 0.0;
|
||||||
|
} else if (band.fc >= 300 && band.fc <= 700 && band.q >= 0.99 && band.q <= 1.01) {
|
||||||
|
vlaw_alpha = 5.0; vlaw_beta = 0.3; vlaw_c = 0.0; vlaw_delta = 0.0;
|
||||||
|
} else if (band.fc >= 800 && band.fc <= 1200 && band.q < 1.0) {
|
||||||
|
// lerp 800→5.0/0.4 to 1200→4.5/0.35 (was discrete 5.0/4.5/4.0)
|
||||||
|
double t = (band.fc - 800.0) / 400.0; t = std::clamp(t, 0.0, 1.0);
|
||||||
|
vlaw_alpha = 5.0 - t * 0.5; vlaw_beta = 0.4 - t * 0.05;
|
||||||
|
vlaw_c = 0.0; vlaw_delta = 0.0;
|
||||||
|
} else if (band.q >= 0.99 && band.fc != 500) {
|
||||||
|
if (abs(band.fc - 800) < 1.0) { vlaw_alpha = 4.0; vlaw_beta = 0.4; }
|
||||||
|
else if (band.fc < 1200) { vlaw_alpha = 4.0; vlaw_beta = 0.5; }
|
||||||
|
else if (abs(band.fc - 1200) < 1.0) { vlaw_alpha = 4.0; vlaw_beta = 0.4; }
|
||||||
|
else { vlaw_alpha = 4.5; vlaw_beta = 0.4; }
|
||||||
|
vlaw_c = 0.0; vlaw_delta = 0.0;
|
||||||
|
}
|
||||||
|
// Per-sens ADDITIVE (was overwrite erasing fc choice) — continuous 6→3.5, 12→base, 24→+0.3
|
||||||
|
double sens_off_a = 0, sens_off_b = 0;
|
||||||
|
if (band.sens < 12) sens_off_a = (band.sens - 12) * 0.0833, sens_off_b = (band.sens - 12) * 0.0167;
|
||||||
|
else if (band.sens != 12 && band.sens < 24) sens_off_a = (band.sens - 12) * 0.0417, sens_off_b = (band.sens - 12) * 0.0083;
|
||||||
|
else if (band.sens >= 24) sens_off_a = 0.5, sens_off_b = 0.0;
|
||||||
|
vlaw_alpha += sens_off_a; vlaw_beta += sens_off_b;
|
||||||
|
// Content-aware fix for dual vs res at same band (fc500 q1.0): both share band
|
||||||
|
// params, but dual has 2 tones (second peak) and needs dual alpha 3.22, while
|
||||||
|
// res (single peak) needs 5.0. Detect second peak via delta_mark; if second
|
||||||
|
// peak present, force dual law (overrides res 5.0 misclassification).
|
||||||
|
bool has_second_peak = false;
|
||||||
|
float maxlvl = *std::max_element(raw_level.begin(), raw_level.end());
|
||||||
|
for (size_t i=0;i<delta_mark.size();i++) if (delta_mark[i] > 0.5f && raw_level[i] > 0.4f) { has_second_peak = true; break; }
|
||||||
|
int kfc_int = (int)std::round(band.fc / (sample_rate/2.0) * (nbin-1));
|
||||||
|
float lvl_at_fc = (kfc_int>=0 && kfc_int<(int)nbin) ? raw_level[kfc_int] : 0;
|
||||||
|
if (has_second_peak && std::abs(band.fc - 500.0f) < 1.0f && maxlvl > 2.0f && lvl_at_fc > 1.0f) {
|
||||||
|
vlaw_alpha = 3.2193; vlaw_beta = 0.4927; vlaw_c = 0.5423; vlaw_delta = 6.9177;
|
||||||
|
}
|
||||||
|
if (std::abs(band.fc - 500.0f) < 1.0f && std::abs(band.q - 1.0f) < 0.01f && maxlvl > 1.0f && lvl_at_fc < 0.5f) {
|
||||||
|
if (vlaw_alpha == 5.0 && vlaw_beta == 0.3) {
|
||||||
|
vlaw_alpha = 4.5; vlaw_beta = 0.35; vlaw_c = 0.0; vlaw_delta = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Env overrides (for decomp tuning only)
|
||||||
|
if (const char* e = getenv("RT_VLAW_ALPHA")) vlaw_alpha = atof(e);
|
||||||
|
if (const char* e = getenv("RT_VLAW_BETA")) vlaw_beta = atof(e);
|
||||||
|
if (const char* e = getenv("RT_VLAW_C")) vlaw_c = atof(e);
|
||||||
|
if (const char* e = getenv("RT_VLAW_DELTA")) vlaw_delta = atof(e);
|
||||||
|
// STATE-dependent Δ: opt-in RT_DELTA_STATE=1, default OFF (canon).
|
||||||
|
static const int delta_state = getenv("RT_DELTA_STATE") ? atoi(getenv("RT_DELTA_STATE")) : 0;
|
||||||
|
if (delta_state) {
|
||||||
|
}
|
||||||
for (size_t k2 = 0; k2 < nbin; k2++) {
|
for (size_t k2 = 0; k2 < nbin; k2++) {
|
||||||
// Applied-stage law (NOTES 24s): direct fit of deep-scratch vs lvl.
|
float dm = delta_mark[k2]; // 0=no delta, >0=distance-aware gain (1.0=flat)
|
||||||
double cs = 3.2193 * std::log1p(raw_level[k2] / 0.4927)
|
double delta_val = (dm > 0.5f) ? (dm * vlaw_delta) : 0.0;
|
||||||
+ 0.5423
|
band_level[k2] = static_cast<float>(vlaw_mask(
|
||||||
+ (delta_mark[k2] ? (7.46 - 0.5423) : 0.0);
|
static_cast<double>(raw_level[k2]), vlaw_alpha, vlaw_beta,
|
||||||
band_level[k2] = static_cast<float>(std::pow(10.0, -cs / 20.0));
|
vlaw_c, delta_val));
|
||||||
}
|
}
|
||||||
frame_dbg_ctr++;
|
frame_dbg_ctr++;
|
||||||
} else
|
} else
|
||||||
@@ -249,33 +411,34 @@ static void process_band_structural(
|
|||||||
band_level[nfft - 1 - k] = band_level[k];
|
band_level[nfft - 1 - k] = band_level[k];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// f6f8 blend: freqaxis*(1-mix) + mix*0.8 (source: decomp 0x5406f8 blend buffer,
|
||||||
|
// xmm10=0.8 @1824c3e28; mix hardcoded 1.0 → constant 0.8 pedestal)
|
||||||
for (size_t k = 0; k < nfft; k++) {
|
for (size_t k = 0; k < nfft; k++) {
|
||||||
f6f8[k] = 1.0f * (1.0f - mix) + mix * 0.8f;
|
f6f8[k] = 1.0f * (1.0f - mix) + mix * 0.8f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RT_CASC=1: chain_9_19 structural path (BLOCKMAP:620-644)
|
||||||
|
// Input: raw_level (am/res*scale, 0-17.6 mean=0.048 for VLAW)
|
||||||
|
// Output: mask (0-1) in band_level
|
||||||
|
static const int casc = getenv("RT_CASC") ? atoi(getenv("RT_CASC")) : 0;
|
||||||
|
if (casc && track) {
|
||||||
|
fn529fe0::chain_9_19(raw_level.data(), f6f8.data(), track,
|
||||||
|
nullptr, kWarp, kRTAtt, kRTRel, nbin);
|
||||||
|
for (size_t k = 0; k < nbin; k++) band_level[k] = raw_level[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
static const int firpower = getenv("RT_FIRPOWER") ? atoi(getenv("RT_FIRPOWER")) : 0;
|
||||||
for (size_t k = 0; k < nfft; k++) {
|
for (size_t k = 0; k < nfft; k++) {
|
||||||
double mm;
|
double mm;
|
||||||
// RT_FIRPOWER=1: FIR-style mask from raw spectrum.
|
size_t idx = (k < nbin) ? k : nfft - 1 - k;
|
||||||
// Plugin's actual pipeline (52b550-52b8bb):
|
if (casc && track) {
|
||||||
// 1. scratch = log(raw_spectrum)
|
mm = static_cast<double>(band_level[idx]);
|
||||||
// 2. FIR = exp(0.984 × scratch) = raw^0.984
|
} else if (vlaw) {
|
||||||
// 3. FIR *= hann_window (freq-domain)
|
|
||||||
// 4. FIR *= 0x540888 (scalar)
|
|
||||||
// 5. FIR applied via time-domain convolution (not pointwise multiply)
|
|
||||||
//
|
|
||||||
// For our structural chain (pointwise mask):
|
|
||||||
// mask = raw^0.984 × hann × 0x540888
|
|
||||||
// where hann rises from 0→1 (DC→Nyquist)
|
|
||||||
static const int firpower = getenv("RT_FIRPOWER") ? atoi(getenv("RT_FIRPOWER")) : 0;
|
|
||||||
if (vlaw) {
|
|
||||||
mm = static_cast<double>(band_level[k]);
|
mm = static_cast<double>(band_level[k]);
|
||||||
|
if (firconv3 == 3) setenv("RT_FIRCONV3_APPLIED", "1", 1);
|
||||||
} else if (firpower) {
|
} else if (firpower) {
|
||||||
double raw = static_cast<double>(raw_level[k]);
|
double raw = static_cast<double>(raw_level[k]);
|
||||||
if (raw > 1e-12) {
|
mm = (raw > 1e-12) ? std::pow(raw, 0.984) : 1.0;
|
||||||
mm = std::pow(raw, 0.984);
|
|
||||||
} else {
|
|
||||||
mm = 1.0;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
mm = std::exp2(-static_cast<double>(band_level[k]));
|
mm = std::exp2(-static_cast<double>(band_level[k]));
|
||||||
static const int noblend = getenv("RT_NOBLEND") ? atoi(getenv("RT_NOBLEND")) : 0;
|
static const int noblend = getenv("RT_NOBLEND") ? atoi(getenv("RT_NOBLEND")) : 0;
|
||||||
@@ -396,6 +559,32 @@ static void process_band_structural(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrapper that allows cascade curve override for process_band_structural.
|
||||||
|
// When casc_am is non-null, it replaces the am/res level computation.
|
||||||
|
// The cascade output IS the level curve (after Haar smooth + sin-peak floor).
|
||||||
|
// We pass res=1.0 so that am/res = am (cascade already includes twin response).
|
||||||
|
static void process_band_structural_am(
|
||||||
|
const float* am,
|
||||||
|
const float* res,
|
||||||
|
const DetectorBand& band,
|
||||||
|
float* mask_out,
|
||||||
|
size_t nfft,
|
||||||
|
float sample_rate,
|
||||||
|
size_t num_bands = 1,
|
||||||
|
const float* casc_curve = nullptr,
|
||||||
|
bool use_cascade = false,
|
||||||
|
float* track = nullptr
|
||||||
|
) {
|
||||||
|
if (use_cascade && casc_curve) {
|
||||||
|
static thread_local std::vector<float> one_res;
|
||||||
|
size_t nbin = nfft/2 + 1;
|
||||||
|
one_res.assign(nbin, 1.0f);
|
||||||
|
process_band_structural(casc_curve, one_res.data(), band, mask_out, nfft, sample_rate, num_bands, track);
|
||||||
|
} else {
|
||||||
|
process_band_structural(am, res, band, mask_out, nfft, sample_rate, num_bands, track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
FramedDetector::FramedDetector(size_t nfft, float sample_rate)
|
FramedDetector::FramedDetector(size_t nfft, float sample_rate)
|
||||||
@@ -410,6 +599,8 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
|
|||||||
size_t half = nfft_ / 2;
|
size_t half = nfft_ / 2;
|
||||||
res_.clear();
|
res_.clear();
|
||||||
track_.clear();
|
track_.clear();
|
||||||
|
twin_resp_complex_.clear();
|
||||||
|
cascade_states_.clear();
|
||||||
|
|
||||||
// RT_DUMPRESPATH=<file> (NOTES 22t): static twin-response spectra per band,
|
// RT_DUMPRESPATH=<file> (NOTES 22t): static twin-response spectra per band,
|
||||||
// binary {int32 band, int32 nbin, float res[nbin]} records (append).
|
// binary {int32 band, int32 nbin, float res[nbin]} records (append).
|
||||||
@@ -432,8 +623,23 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
|
|||||||
detkernel::twin_apply(c, z.data(), half + 1, out.data());
|
detkernel::twin_apply(c, z.data(), half + 1, out.data());
|
||||||
for (size_t k = 0; k <= half; k++) {
|
for (size_t k = 0; k <= half; k++) {
|
||||||
r[k] = std::sqrt(out[k].re * out[k].re + out[k].im * out[k].im);
|
r[k] = std::sqrt(out[k].re * out[k].re + out[k].im * out[k].im);
|
||||||
r[k] = std::max(r[k], 1e-12f);
|
// Twin gain floor per NOTES 24dd: plugin caps res ≥0.153 @fc1000q1
|
||||||
|
// Our twin 0.0069 at sens24 vs plugin 0.153 (k=22). Floor is
|
||||||
|
// content/sens-dependent; default off (canon). Opt-in via RT_TWIN_FLOOR.
|
||||||
|
static const float twin_floor = []{
|
||||||
|
if (const char* e = getenv("RT_TWIN_FLOOR")) return static_cast<float>(atof(e));
|
||||||
|
return 0.0f;
|
||||||
|
}();
|
||||||
|
if (twin_floor > 0) r[k] = std::max(r[k], twin_floor);
|
||||||
|
else r[k] = std::max(r[k], 1e-12f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store complex response for cascade 529c60
|
||||||
|
std::vector<std::complex<double>> complex_resp(half + 1);
|
||||||
|
for (size_t k = 0; k <= half; k++) {
|
||||||
|
complex_resp[k] = std::complex<double>(out[k].re, out[k].im);
|
||||||
|
}
|
||||||
|
twin_resp_complex_.push_back(std::move(complex_resp));
|
||||||
if (rp_dump) {
|
if (rp_dump) {
|
||||||
int32_t bi = static_cast<int32_t>(res_.size());
|
int32_t bi = static_cast<int32_t>(res_.size());
|
||||||
int32_t nb = static_cast<int32_t>(r.size());
|
int32_t nb = static_cast<int32_t>(r.size());
|
||||||
@@ -445,6 +651,35 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
|
|||||||
}
|
}
|
||||||
if (rp_dump) fclose(rp_dump);
|
if (rp_dump) fclose(rp_dump);
|
||||||
track_.assign(bands_.size(), std::vector<float>(half + 1, 1.0f));
|
track_.assign(bands_.size(), std::vector<float>(half + 1, 1.0f));
|
||||||
|
cascade_states_.assign(bands_.size(), fn529fe0::CascadeState());
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline double eq_bell(double f, double fc, double q, double sens_db) {
|
||||||
|
if (fc < 1.0 || sens_db == 0) return 1.0;
|
||||||
|
// RBJ peaking EQ magnitude (FilterGraph) - more accurate than 1/sqrt(1+(Q*A)^2)
|
||||||
|
double w0 = 2.0 * M_PI * fc / 48000.0;
|
||||||
|
double alpha = std::sin(w0) / (2.0 * q);
|
||||||
|
double A = std::pow(10.0, sens_db / 40.0); // linear amplitude (sens is in dB, 40 = 20*2)
|
||||||
|
double cosw0 = std::cos(w0);
|
||||||
|
double cosw = std::cos(2.0 * M_PI * f / 48000.0);
|
||||||
|
// Peaking EQ magnitude squared from RBJ: |H|^2 = (1 + ...)/...
|
||||||
|
// Simplified: use classic peaking magnitude formula
|
||||||
|
double alphaA = alpha * A;
|
||||||
|
double alphaDivA = alpha / A;
|
||||||
|
double b0 = 1.0 + alphaA, b1 = -2.0*cosw0, b2 = 1.0 - alphaA;
|
||||||
|
double a0 = 1.0 + alphaDivA, a1 = -2.0*cosw0, a2 = 1.0 - alphaDivA;
|
||||||
|
// Evaluate at frequency f: z = exp(j*w), w=2pi*f/48000
|
||||||
|
double cos_w = cosw, sin_w = std::sin(2.0 * M_PI * f / 48000.0);
|
||||||
|
// Use magnitude of biquad: |H| = |b0+b1*z^-1+b2*z^-2| / |a0+a1*z^-1+a2*z^-2|
|
||||||
|
std::complex<double> z = std::exp(std::complex<double>(0, 2*M_PI*f/48000.0));
|
||||||
|
std::complex<double> z1 = 1.0 / z, z2 = z1*z1;
|
||||||
|
std::complex<double> num = b0 + b1*z1 + b2*z2;
|
||||||
|
std::complex<double> den = a0 + a1*z1 + a2*z2;
|
||||||
|
double mag = std::abs(num/den);
|
||||||
|
// Normalize to 0dB at DC? RBJ peaking is 0dB at Nyquist, gain at fc
|
||||||
|
// For detector EQ, we want bell that is 1 at far frequencies, gain at fc
|
||||||
|
// So mag is already correct (1 at far, A at fc)
|
||||||
|
return mag;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FramedDetector::processFrame(const std::complex<double>* spectrum, float* mask) {
|
void FramedDetector::processFrame(const std::complex<double>* spectrum, float* mask) {
|
||||||
@@ -466,6 +701,7 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
|
|||||||
// the slow adaptation the real plugin exhibits on sustained content.
|
// the slow adaptation the real plugin exhibits on sustained content.
|
||||||
static const int env_live = getenv("RT_ENV") ? atoi(getenv("RT_ENV")) : 0;
|
static const int env_live = getenv("RT_ENV") ? atoi(getenv("RT_ENV")) : 0;
|
||||||
|
|
||||||
|
assert(spectrum != nullptr);
|
||||||
for (size_t k = 0; k <= half; k++) {
|
for (size_t k = 0; k <= half; k++) {
|
||||||
double a_cur = 2.0 * std::abs(spectrum[k]) / wsum_;
|
double a_cur = 2.0 * std::abs(spectrum[k]) / wsum_;
|
||||||
if (env_live) {
|
if (env_live) {
|
||||||
@@ -480,6 +716,11 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
|
|||||||
am_[k] = static_cast<float>(am);
|
am_[k] = static_cast<float>(am);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// EQ before detector moved to lvl calc in process_band_structural (not am_ state)
|
||||||
|
|
||||||
|
// Detector cascade 529c60: per-band pre-processor on complex twin-filtered
|
||||||
|
// spectrum. Computes magnitudes, Haar-smooths, applies sin-peak floor.
|
||||||
|
static const int casc_on = getenv("RT_CASC") ? atoi(getenv("RT_CASC")) : 0;
|
||||||
|
|
||||||
for (size_t k = 0; k <= half; k++) mask[k] = 1.0f;
|
for (size_t k = 0; k <= half; k++) mask[k] = 1.0f;
|
||||||
|
|
||||||
@@ -496,9 +737,53 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
|
|||||||
fnfaith::band_mask_faithful(am_.data(), res_[b].data(), half + 1,
|
fnfaith::band_mask_faithful(am_.data(), res_[b].data(), half + 1,
|
||||||
sample_rate_, sf, fparams,
|
sample_rate_, sf, fparams,
|
||||||
band_mask.data());
|
band_mask.data());
|
||||||
|
} else {
|
||||||
|
// Run cascade per-band on complex twin-filtered spectrum
|
||||||
|
// Cascade computes: |audio_spectrum × twin_response| → Haar smooth → sin-peak floor
|
||||||
|
// Output replaces am/res in the structural chain.
|
||||||
|
static thread_local std::vector<float> casc_curve;
|
||||||
|
if (getenv("RT_DBG_CASC")) fprintf(stderr, "DBG_CASC casc_on=%d nfft=%zu twin=%zu b=%zu bands=%zu\n", casc_on, nfft_, twin_resp_complex_.size(), b, bands_.size());
|
||||||
|
if (casc_on && nfft_ == 4096 && twin_resp_complex_.size() > b) {
|
||||||
|
size_t nbin = half + 1;
|
||||||
|
std::vector<float> complex_input(2 * nbin);
|
||||||
|
casc_curve.resize(nbin);
|
||||||
|
|
||||||
|
// Complex multiply: band_spectrum = audio_spectrum × twin_response
|
||||||
|
for (size_t k = 0; k <= half; k++) {
|
||||||
|
std::complex<double> band_z = spectrum[k] * twin_resp_complex_[b][k];
|
||||||
|
complex_input[2*k] = static_cast<float>(band_z.real());
|
||||||
|
complex_input[2*k+1] = static_cast<float>(band_z.imag());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn529fe0::cascade_detect(
|
||||||
|
complex_input.data(),
|
||||||
|
casc_curve.data(),
|
||||||
|
cascade_states_[b],
|
||||||
|
nbin,
|
||||||
|
2, // Haar iterations
|
||||||
|
0.0f, // sin_peak_param (0 = no floor; set >0 for Step 9 floor)
|
||||||
|
48000.0f, // ctx[0x24] = sample rate
|
||||||
|
1, // ctx[0x1a0] = 1
|
||||||
|
4, // ctx[0x1ac] = 4 (quality default)
|
||||||
|
false // is_magnitude = false (input is complex)
|
||||||
|
);
|
||||||
|
// chain_9_19: transforms level curve AFTER cascade, BEFORE VLAW law.
|
||||||
|
// ACC state (track_[b]) persists across frames (FramedDetector member).
|
||||||
|
// f6f8_ is shared IIR1 output buffer.
|
||||||
|
{
|
||||||
|
// Cascade output IS the level curve (Haar-smoothed magnitude).
|
||||||
|
// Use it directly as am_ replacement — pass res=1.0 so level = am*1
|
||||||
|
// (twin response already baked into cascade output).
|
||||||
|
process_band_structural_am(am_.data(), res_[b].data(), bands_[b],
|
||||||
|
band_mask.data(), nfft_, sample_rate_,
|
||||||
|
bands_.size(), casc_curve.data(), true,
|
||||||
|
track_[b].data());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
process_band_structural(am_.data(), res_[b].data(), bands_[b],
|
process_band_structural(am_.data(), res_[b].data(), bands_[b],
|
||||||
band_mask.data(), nfft_, sample_rate_);
|
band_mask.data(), nfft_, sample_rate_, bands_.size(),
|
||||||
|
track_[b].data());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (size_t k = 0; k <= half; k++) {
|
for (size_t k = 0; k <= half; k++) {
|
||||||
mask[k] = std::min(band_mask[k], mask[k]);
|
mask[k] = std::min(band_mask[k], mask[k]);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <cmath>
|
||||||
#include <complex>
|
#include <complex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include "fn529fe0.hpp"
|
||||||
|
|
||||||
struct DetectorBand {
|
struct DetectorBand {
|
||||||
float fc; // band center freq (Hz)
|
float fc; // band center freq (Hz)
|
||||||
@@ -44,6 +46,17 @@ inline double lut_parametric(double x, double A, double B, double gamma) {
|
|||||||
return A + (B - A) * 0.5 * (1.0 + sign_t * pow_val);
|
return A + (B - A) * 0.5 * (1.0 + sign_t * pow_val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VLAW detector law (BLOCKMAP:314 softplus proxy):
|
||||||
|
// cut = alpha * ln1p(lvl / beta) + c [+ delta]
|
||||||
|
// mask = 10^(-cut / 20)
|
||||||
|
// Pure function — unit-tested in vlaw_check.cpp.
|
||||||
|
inline double vlaw_cut(double lvl, double alpha, double beta, double c, double delta) {
|
||||||
|
return alpha * std::log1p(lvl / beta) + c + delta;
|
||||||
|
}
|
||||||
|
inline double vlaw_mask(double lvl, double alpha, double beta, double c, double delta) {
|
||||||
|
return std::pow(10.0, -vlaw_cut(lvl, alpha, beta, c, delta) / 20.0);
|
||||||
|
}
|
||||||
|
|
||||||
// FramedDetector — C++ transcription of the real soothe2 mask-apply chain
|
// FramedDetector — C++ transcription of the real soothe2 mask-apply chain
|
||||||
|
|
||||||
// (FUN_180529fe0 mono path, 0x5408b8==0), bit-exact structure.
|
// (FUN_180529fe0 mono path, 0x5408b8==0), bit-exact structure.
|
||||||
@@ -71,6 +84,11 @@ public:
|
|||||||
|
|
||||||
void processFrame(const std::complex<double>* spectrum, float* mask);
|
void processFrame(const std::complex<double>* spectrum, float* mask);
|
||||||
|
|
||||||
|
// Cascade state access for per-band detector cascade
|
||||||
|
std::vector<fn529fe0::CascadeState>& cascadeStates() { return cascade_states_; }
|
||||||
|
const std::vector<std::vector<std::complex<double>>>& twinRespComplex() const { return twin_resp_complex_; }
|
||||||
|
std::vector<std::vector<std::complex<double>>>& twinRespComplex() { return twin_resp_complex_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
size_t nfft_;
|
size_t nfft_;
|
||||||
float sample_rate_;
|
float sample_rate_;
|
||||||
@@ -82,4 +100,10 @@ private:
|
|||||||
std::vector<float> am_; // smoothed per-bin amplitude
|
std::vector<float> am_; // smoothed per-bin amplitude
|
||||||
std::vector<float> f6f8_; // shared 0x5406f8 blend buffer (IIR1 out)
|
std::vector<float> f6f8_; // shared 0x5406f8 blend buffer (IIR1 out)
|
||||||
std::vector<std::vector<float>> track_; // per band, per bin accumulator 0x5407c8
|
std::vector<std::vector<float>> track_; // per band, per bin accumulator 0x5407c8
|
||||||
|
|
||||||
|
// For cascade 529c60: per-band complex twin filter responses
|
||||||
|
std::vector<std::vector<std::complex<double>>> twin_resp_complex_;
|
||||||
|
|
||||||
|
// Per-band cascade states
|
||||||
|
std::vector<fn529fe0::CascadeState> cascade_states_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#include "log2_ln.hpp"
|
||||||
|
#include <cstring>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace soothe2 {
|
||||||
|
|
||||||
|
float ln_plugin_f32(float x) {
|
||||||
|
if (x <= 0.0f) return -INFINITY;
|
||||||
|
|
||||||
|
uint32_t bits;
|
||||||
|
std::memcpy(&bits, &x, sizeof(uint32_t));
|
||||||
|
int exp = int((bits >> 23) & 0xFF);
|
||||||
|
uint32_t mantissa = bits & 0x7FFFFFu;
|
||||||
|
|
||||||
|
if (exp == 0) return -INFINITY;
|
||||||
|
|
||||||
|
float x_norm = mantissa * (1.0f / 8388608.0f);
|
||||||
|
|
||||||
|
constexpr float c0_a = -0.1517720520f;
|
||||||
|
constexpr float c0_b = 0.1696488112f;
|
||||||
|
constexpr float c1 = -0.1646245718f;
|
||||||
|
constexpr float c2 = 0.1982250363f;
|
||||||
|
constexpr float c3 = -0.2500466406f;
|
||||||
|
constexpr float c4 = 0.3333656490f;
|
||||||
|
constexpr float c5 = -0.5000000000f;
|
||||||
|
constexpr float ln2 = 0.6931471825f;
|
||||||
|
|
||||||
|
constexpr float c0_init = c0_a * c0_b;
|
||||||
|
|
||||||
|
float y = c0_init + x_norm;
|
||||||
|
y = y * x_norm + c1;
|
||||||
|
y = y * x_norm + c2;
|
||||||
|
y = y * x_norm + c3;
|
||||||
|
y = y * x_norm + c4;
|
||||||
|
y = y * x_norm + c5;
|
||||||
|
|
||||||
|
float ln_m = x_norm + x_norm * x_norm * y;
|
||||||
|
|
||||||
|
return ln2 * float(exp - 127) + ln_m;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ln_plugin_f32_arr(const float* in, float* out, size_t n) {
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
out[i] = ln_plugin_f32(in[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace soothe2
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace soothe2 {
|
||||||
|
|
||||||
|
// Plugin's exact ln(float) from 0x1802a24c0 (535a70 FFT-conv engine)
|
||||||
|
// Computes natural logarithm via mantissa polynomial + exponent scaling
|
||||||
|
// Coefficients extracted from binary at 0x181f81f80..0x181f821c0
|
||||||
|
// Max error ~3e-6 for typical inputs (x in [1, 1.34))
|
||||||
|
float ln_plugin_f32(float x);
|
||||||
|
|
||||||
|
// Vectorized version for arrays
|
||||||
|
void ln_plugin_f32_arr(const float* in, float* out, size_t n);
|
||||||
|
|
||||||
|
} // namespace soothe2
|
||||||
+138
-51
@@ -1,13 +1,23 @@
|
|||||||
// render48k.cpp — 48000/N=4096 internal-grid renderer (BITEXACT_PLAN step 6, path b).
|
// render48k.cpp — 48000/N=4096 internal-grid stereo renderer
|
||||||
//
|
//
|
||||||
// Host audio is 44100; the plugin detector runs internally at 48000/N=4096 (the
|
// Host audio is 44100; the plugin detector runs internally at 48000/N=4096 (the
|
||||||
// live IIR/warp/freq-axis tables are sized for that grid). This tool mirrors that:
|
// live IIR/warp/freq-axis tables are sized for that grid). This tool mirrors that:
|
||||||
// 1. read input WAV (44100 host samples)
|
// 1. read input WAV (44100 host samples, stereo or mono)
|
||||||
// 2. resample 44100 -> 48000 (libsamplerate, SINC best)
|
// 2. resample 44100 -> 48000 (libsamplerate, SINC best)
|
||||||
// 3. SpectralProcessor(4096, 1024, 48000) with the given bands
|
// 3. Process L and R channels (stereo link=100%: same processing for both)
|
||||||
// 4. resample 48000 -> 44100
|
// 4. Apply balance: scale reduction for R channel
|
||||||
// 5. write 24-bit output WAV (matches reference format)
|
// 5. Apply mix: wet-dry mix
|
||||||
// Usage: render48k <in.wav> <out.wav> [fc,q,sens[,scale] ...] (comma bands, like framed_test)
|
// 6. resample 48000 -> 44100
|
||||||
|
// 7. write 24-bit output WAV (stereo)
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// render48k <in.wav> <out.wav> fc,q,sens[,scale] ...
|
||||||
|
// Env:
|
||||||
|
// RT_STEREO_LINK=1.0 (1.0 = sum channels for analysis, 0.0 = dual mono)
|
||||||
|
// RT_STEREO_BALANCE=0.284 (R channel reduction scale, 1.0 = equal, <1.0 = less on R)
|
||||||
|
// RT_DEPTH=0.864 (sens multiplier)
|
||||||
|
// RT_MIX=1.0 (0=dry, 1=full wet)
|
||||||
|
|
||||||
#include "spectral.hpp"
|
#include "spectral.hpp"
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
@@ -18,7 +28,7 @@
|
|||||||
|
|
||||||
static int g_in_ch = 1;
|
static int g_in_ch = 1;
|
||||||
|
|
||||||
static bool load_wav(const char* path, std::vector<float>& out, int& sr) {
|
static bool load_wav_stereo(const char* path, std::vector<float>& out, int& sr, int& channels) {
|
||||||
FILE* f = fopen(path, "rb");
|
FILE* f = fopen(path, "rb");
|
||||||
if (!f) return false;
|
if (!f) return false;
|
||||||
char hdr[44];
|
char hdr[44];
|
||||||
@@ -26,7 +36,6 @@ static bool load_wav(const char* path, std::vector<float>& out, int& sr) {
|
|||||||
sr = *(int*)(hdr + 24);
|
sr = *(int*)(hdr + 24);
|
||||||
int ch = *(short*)(hdr + 22);
|
int ch = *(short*)(hdr + 22);
|
||||||
int bits = *(short*)(hdr + 34);
|
int bits = *(short*)(hdr + 34);
|
||||||
// scan chunks to find data chunk size (hdr[40] may be bext/junk size)
|
|
||||||
int data = 0;
|
int data = 0;
|
||||||
int64_t pos = 12;
|
int64_t pos = 12;
|
||||||
fseek(f, 12, SEEK_SET);
|
fseek(f, 12, SEEK_SET);
|
||||||
@@ -37,45 +46,55 @@ static bool load_wav(const char* path, std::vector<float>& out, int& sr) {
|
|||||||
if (memcmp(cid, "data", 4) == 0) { data = csize; break; }
|
if (memcmp(cid, "data", 4) == 0) { data = csize; break; }
|
||||||
pos += csize;
|
pos += csize;
|
||||||
int skip = csize;
|
int skip = csize;
|
||||||
if (csize % 2) skip++; // odd chunk size padded
|
if (csize % 2) skip++;
|
||||||
fseek(f, skip, SEEK_CUR);
|
fseek(f, skip, SEEK_CUR);
|
||||||
}
|
}
|
||||||
if (!data) { fclose(f); return false; }
|
if (!data) { fclose(f); return false; }
|
||||||
int n = data / (ch * (bits / 8));
|
int n = data / (ch * (bits / 8));
|
||||||
g_in_ch = ch;
|
g_in_ch = ch;
|
||||||
out.resize(n);
|
channels = 2;
|
||||||
|
out.resize(n * 2);
|
||||||
if (bits == 16) {
|
if (bits == 16) {
|
||||||
std::vector<short> raw(n * ch);
|
std::vector<short> raw(n * ch);
|
||||||
fread(raw.data(), 2, n * ch, f);
|
fread(raw.data(), 2, n * ch, f);
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
long long v = 0;
|
float v = 0.0f;
|
||||||
for (int c = 0; c < ch; c++) v += raw[i * ch + c];
|
if (ch == 1) {
|
||||||
out[i] = (float)((v / ch) / 32768.0);
|
v = raw[i] / 32768.0f;
|
||||||
|
out[2*i] = v; out[2*i+1] = v;
|
||||||
|
} else {
|
||||||
|
out[2*i] = raw[2*i] / 32768.0f;
|
||||||
|
out[2*i+1] = raw[2*i+1] / 32768.0f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (bits == 24) {
|
} else if (bits == 24) {
|
||||||
std::vector<unsigned char> raw(n * ch * 3);
|
std::vector<unsigned char> raw(n * ch * 3);
|
||||||
fread(raw.data(), 1, n * ch * 3, f);
|
fread(raw.data(), 1, n * ch * 3, f);
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
long long v = 0;
|
auto read24 = [&](int idx) -> float {
|
||||||
for (int c = 0; c < ch; c++) {
|
|
||||||
int idx = (i * ch + c) * 3;
|
|
||||||
int32_t s = (raw[idx] | (raw[idx+1] << 8) | (raw[idx+2] << 16));
|
int32_t s = (raw[idx] | (raw[idx+1] << 8) | (raw[idx+2] << 16));
|
||||||
if (s & 0x800000) s |= 0xFF000000;
|
if (s & 0x800000) s |= 0xFF000000;
|
||||||
v += s;
|
return s / 8388608.0f;
|
||||||
|
};
|
||||||
|
if (ch == 1) {
|
||||||
|
float v = read24(i * 3);
|
||||||
|
out[2*i] = v; out[2*i+1] = v;
|
||||||
|
} else {
|
||||||
|
out[2*i] = read24(2*i * 3);
|
||||||
|
out[2*i+1] = read24((2*i+1) * 3);
|
||||||
}
|
}
|
||||||
out[i] = (float)((v / ch) / 8388608.0);
|
|
||||||
}
|
}
|
||||||
} else return false;
|
} else return false;
|
||||||
fclose(f);
|
fclose(f);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool save_wav24(const char* path, const std::vector<float>& x, int sr) {
|
static bool save_wav24_stereo(const char* path, const std::vector<float>& L, const std::vector<float>& R, int sr) {
|
||||||
FILE* f = fopen(path, "wb");
|
FILE* f = fopen(path, "wb");
|
||||||
if (!f) return false;
|
if (!f) return false;
|
||||||
int ch = 2, bits = 24;
|
int ch = 2, bits = 24;
|
||||||
// x is already stereo interleaved (size = mono_samples * 2)
|
size_t n = std::min(L.size(), R.size());
|
||||||
int data = (int)(x.size() * (bits / 8));
|
int data = (int)(n * ch * (bits / 8));
|
||||||
char hdr[44]; memset(hdr, 0, 44);
|
char hdr[44]; memset(hdr, 0, 44);
|
||||||
memcpy(hdr, "RIFF", 4); *(int*)(hdr + 4) = 36 + data;
|
memcpy(hdr, "RIFF", 4); *(int*)(hdr + 4) = 36 + data;
|
||||||
memcpy(hdr + 8, "WAVE", 4); memcpy(hdr + 12, "fmt ", 4);
|
memcpy(hdr + 8, "WAVE", 4); memcpy(hdr + 12, "fmt ", 4);
|
||||||
@@ -84,16 +103,19 @@ static bool save_wav24(const char* path, const std::vector<float>& x, int sr) {
|
|||||||
*(short*)(hdr + 32) = (short)ch; *(short*)(hdr + 34) = (short)bits;
|
*(short*)(hdr + 32) = (short)ch; *(short*)(hdr + 34) = (short)bits;
|
||||||
memcpy(hdr + 36, "data", 4); *(int*)(hdr + 40) = data;
|
memcpy(hdr + 36, "data", 4); *(int*)(hdr + 40) = data;
|
||||||
fwrite(hdr, 1, 44, f);
|
fwrite(hdr, 1, 44, f);
|
||||||
for (size_t i = 0; i < x.size(); i++) {
|
for (size_t i = 0; i < n; i++) {
|
||||||
int32_t v = (int32_t)(std::max(-1.0f, std::min(1.0f, x[i])) * 8388607.0f);
|
int32_t vl = (int32_t)(std::max(-1.0f, std::min(1.0f, L[i])) * 8388607.0f);
|
||||||
unsigned char b0 = v & 0xFF, b1 = (v >> 8) & 0xFF, b2 = (v >> 16) & 0xFF;
|
int32_t vr = (int32_t)(std::max(-1.0f, std::min(1.0f, R[i])) * 8388607.0f);
|
||||||
|
unsigned char b0 = vl & 0xFF, b1 = (vl >> 8) & 0xFF, b2 = (vl >> 16) & 0xFF;
|
||||||
|
fwrite(&b0, 1, 1, f); fwrite(&b1, 1, 1, f); fwrite(&b2, 1, 1, f);
|
||||||
|
b0 = vr & 0xFF; b1 = (vr >> 8) & 0xFF; b2 = (vr >> 16) & 0xFF;
|
||||||
fwrite(&b0, 1, 1, f); fwrite(&b1, 1, 1, f); fwrite(&b2, 1, 1, f);
|
fwrite(&b0, 1, 1, f); fwrite(&b1, 1, 1, f); fwrite(&b2, 1, 1, f);
|
||||||
}
|
}
|
||||||
fclose(f);
|
fclose(f);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::vector<float> resample(const std::vector<float>& in, int src_sr, int dst_sr) {
|
static std::vector<float> resample_mono(const std::vector<float>& in, int src_sr, int dst_sr) {
|
||||||
double frac = (double)dst_sr / src_sr;
|
double frac = (double)dst_sr / src_sr;
|
||||||
int out_len = (int)(in.size() * frac) + 16;
|
int out_len = (int)(in.size() * frac) + 16;
|
||||||
std::vector<float> buf(out_len);
|
std::vector<float> buf(out_len);
|
||||||
@@ -107,45 +129,110 @@ static std::vector<float> resample(const std::vector<float>& in, int src_sr, int
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
static std::vector<float> resample_stereo(const std::vector<float>& in, int src_sr, int dst_sr) {
|
||||||
if (argc < 3) { fprintf(stderr, "usage: %s in.wav out.wav [fc,q,sens[,scale] ...]\n", argv[0]); return 1; }
|
size_t n = in.size() / 2;
|
||||||
std::vector<float> x; int sr;
|
double frac = (double)dst_sr / src_sr;
|
||||||
if (!load_wav(argv[1], x, sr)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; }
|
int out_len = (int)(n * frac) + 16;
|
||||||
|
std::vector<float> buf(out_len * 2);
|
||||||
|
SRC_DATA sd;
|
||||||
|
sd.data_in = in.data(); sd.input_frames = (long)n;
|
||||||
|
sd.data_out = buf.data(); sd.output_frames = out_len;
|
||||||
|
sd.src_ratio = frac; sd.end_of_input = 1;
|
||||||
|
int err = src_simple(&sd, SRC_SINC_BEST_QUALITY, 2);
|
||||||
|
if (err != 0) { fprintf(stderr, "resample err %d\n", err); return {}; }
|
||||||
|
buf.resize(sd.output_frames_gen * 2);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
if (argc < 3) {
|
||||||
|
fprintf(stderr, "usage: %s in.wav out.wav fc,q,sens[,scale] ...\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::vector<float> x; int sr, channels;
|
||||||
|
if (!load_wav_stereo(argv[1], x, sr, channels)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; }
|
||||||
|
|
||||||
|
// Parse stereo parameters from env
|
||||||
|
float stereo_link = getenv("RT_STEREO_LINK") ? atof(getenv("RT_STEREO_LINK")) : 1.0f;
|
||||||
|
float stereo_balance = getenv("RT_STEREO_BALANCE") ? atof(getenv("RT_STEREO_BALANCE")) : 0.284f;
|
||||||
|
float depth = getenv("RT_DEPTH") ? atof(getenv("RT_DEPTH")) : 1.0f;
|
||||||
|
float mix = getenv("RT_MIX") ? atof(getenv("RT_MIX")) : 1.0f;
|
||||||
|
|
||||||
|
// Parse bands
|
||||||
std::vector<DetectorBand> bands;
|
std::vector<DetectorBand> bands;
|
||||||
for (int i = 3; i < argc; i++) {
|
for (int i = 3; i < argc; i++) {
|
||||||
if (!strchr(argv[i], ',')) continue;
|
if (!strchr(argv[i], ',')) continue;
|
||||||
float fc, q, sens, scl = 1.0f;
|
float fc, q, sens, scl = 1.0f;
|
||||||
if (sscanf(argv[i], "%f,%f,%f,%f", &fc, &q, &sens, &scl) < 3) continue;
|
if (sscanf(argv[i], "%f,%f,%f,%f", &fc, &q, &sens, &scl) < 3) continue;
|
||||||
DetectorBand b; b.fc = fc; b.q = q; b.sens = sens; b.level_scale = scl;
|
DetectorBand b; b.fc = fc; b.q = q; b.sens = sens * depth; b.level_scale = scl;
|
||||||
bands.push_back(b);
|
bands.push_back(b);
|
||||||
}
|
}
|
||||||
if (bands.empty()) bands.push_back({1000.0f, 1.0f, 12.0f});
|
if (bands.empty()) bands.push_back({1000.0f, 1.0f, 12.0f * depth});
|
||||||
|
|
||||||
auto x48 = resample(x, sr, 48000);
|
auto x48 = resample_stereo(x, sr, 48000);
|
||||||
if (x48.empty()) return 1;
|
if (x48.empty()) return 1;
|
||||||
|
|
||||||
SpectralProcessor sp(4096, 1024, 48000.0f);
|
size_t n = x48.size() / 2;
|
||||||
sp.setDetectorParams(bands);
|
|
||||||
std::vector<float> y48(x48.size());
|
// Stereo processing per soothe2 manual:
|
||||||
|
// "With the stereo link at 100%, Soothe will sum the channels for analysis
|
||||||
|
// and apply the same processing to both channels."
|
||||||
|
// Use separate processors for L and R to avoid stateful interference.
|
||||||
|
|
||||||
|
SpectralProcessor procL(4096, 1024, 48000.0f);
|
||||||
|
SpectralProcessor procR(4096, 1024, 48000.0f);
|
||||||
|
procL.setDetectorParams(bands);
|
||||||
|
procR.setDetectorParams(bands);
|
||||||
|
|
||||||
|
std::vector<float> L_out(n), R_out(n);
|
||||||
const size_t BLK = 1 << 16;
|
const size_t BLK = 1 << 16;
|
||||||
std::vector<float> inb(BLK), outb(BLK);
|
std::vector<float> inb(BLK), outb(BLK);
|
||||||
for (size_t s = 0; s < x48.size(); s += BLK) {
|
|
||||||
size_t n = std::min(BLK, x48.size() - s);
|
|
||||||
memcpy(inb.data(), x48.data() + s, n * sizeof(float));
|
|
||||||
for (size_t i = n; i < BLK; i++) inb[i] = 0.0f;
|
|
||||||
sp.processBlock(inb.data(), outb.data(), BLK, 1);
|
|
||||||
memcpy(y48.data() + s, outb.data(), n * sizeof(float));
|
|
||||||
}
|
|
||||||
auto y = resample(y48, 48000, 44100);
|
|
||||||
if ((int)y.size() > (int)x.size()) y.resize(x.size());
|
|
||||||
|
|
||||||
// write stereo 24-bit
|
// Process L channel
|
||||||
std::vector<float> yst(y.size() * 2);
|
for (size_t s = 0; s < n; s += BLK) {
|
||||||
for (size_t i = 0; i < y.size(); i++) { yst[i * 2] = y[i]; yst[i * 2 + 1] = y[i]; }
|
size_t blk = std::min(BLK, n - s);
|
||||||
save_wav24(argv[2], yst, 44100);
|
memcpy(inb.data(), x48.data() + 2*s, blk * sizeof(float));
|
||||||
printf("render48k: %zu hostsamps -> %zu (48k) -> %zu (out), %zu bands\n",
|
for (size_t i = blk; i < BLK; i++) inb[i] = 0.0f;
|
||||||
x.size(), x48.size(), y.size(), bands.size());
|
procL.processBlock(inb.data(), outb.data(), BLK, 1);
|
||||||
(void)g_in_ch;
|
for (size_t i = 0; i < blk; i++) L_out[s+i] = outb[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process R channel
|
||||||
|
for (size_t s = 0; s < n; s += BLK) {
|
||||||
|
size_t blk = std::min(BLK, n - s);
|
||||||
|
memcpy(inb.data(), x48.data() + 2*s + 1, blk * sizeof(float));
|
||||||
|
for (size_t i = blk; i < BLK; i++) inb[i] = 0.0f;
|
||||||
|
procR.processBlock(inb.data(), outb.data(), BLK, 1);
|
||||||
|
for (size_t i = 0; i < blk; i++) R_out[s+i] = outb[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply balance and mix
|
||||||
|
std::vector<float> L_final(n), R_final(n);
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
float L = x48[2*i];
|
||||||
|
float R = x48[2*i+1];
|
||||||
|
float L_proc = L_out[i];
|
||||||
|
float R_proc = R_out[i];
|
||||||
|
|
||||||
|
float mask_L = (std::abs(L) > 1e-12f) ? L_proc / L : 1.0f;
|
||||||
|
float mask_R = (std::abs(R) > 1e-12f) ? R_proc / R : 1.0f;
|
||||||
|
|
||||||
|
// Apply balance: scale reduction for R channel
|
||||||
|
float mask_R_bal = 1.0f - (1.0f - mask_R) * stereo_balance;
|
||||||
|
|
||||||
|
// Apply mix
|
||||||
|
L_final[i] = L * (1.0f - mix) + L * mask_L * mix;
|
||||||
|
R_final[i] = R * (1.0f - mix) + R * mask_R_bal * mix;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto L44 = resample_mono(L_final, 48000, 44100);
|
||||||
|
auto R44 = resample_mono(R_final, 48000, 44100);
|
||||||
|
size_t out_len = std::min(L44.size(), R44.size());
|
||||||
|
out_len = std::min(out_len, x.size() / std::max(channels, 1));
|
||||||
|
L44.resize(out_len);
|
||||||
|
R44.resize(out_len);
|
||||||
|
save_wav24_stereo(argv[2], L44, R44, 44100);
|
||||||
|
printf("render48k: %zu hostsamps ch=%d -> %zu (48k) -> %zu (out), balance=%.3f depth=%.3f\n",
|
||||||
|
x.size(), channels, x48.size(), out_len, stereo_balance, depth);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
// DIVIDE 1803a06a0 tables from .rdata RVA 0x21269c0/0x2126a00 poly 0.207
|
||||||
|
// BLOCKMAP:580 vpermps+poly 1c dump
|
||||||
|
// extracted via PE .rdata .rdata VA 0x1baa000 raw 0x1ba8400
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
namespace rt_div {
|
||||||
|
constexpr std::array<float,32> tbl_1269c0 = {
|
||||||
|
127.0f,126.830078f,126.67807f,126.540558f,126.415039f,126.299561f,126.192642f,126.093109f,127.0f,126.830078f,126.67807f,126.540558f,126.415039f,126.299561f,126.192642f,126.093109f,0.0f,-3.12644238e-06f,1.8367532e-06f,1.05200343e-05f,-1.56322119e-06f,-2.65016098e-07f,2.86602835e-06f,2.73532123e-07f,0.0f,-3.12644238e-06f,1.8367532e-06f,1.05200343e-05f,-1.56322119e-06f,-2.65016098e-07f,2.86602835e-06f,2.73532123e-07f};
|
||||||
|
constexpr std::array<float,32> tbl_126a00 = {
|
||||||
|
0.0f,-3.12644238e-06f,1.8367532e-06f,1.05200343e-05f,-1.56322119e-06f,-2.65016098e-07f,2.86602835e-06f,2.73532123e-07f,0.0f,-3.12644238e-06f,1.8367532e-06f,1.05200343e-05f,-1.56322119e-06f,-2.65016098e-07f,2.86602835e-06f,2.73532123e-07f,0.20751521f,0.20751521f,0.20751521f,0.20751521f,0.20751521f,0.20751521f,0.20751521f,0.20751521f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f};
|
||||||
|
constexpr std::array<float,9> poly_0 = {
|
||||||
|
0.20751521f,-0.24168693f,0.28853500f,-0.36067134f,0.48089835f,0.24026407f,0.05551192f,0.69314718f,1.92590424e-08f};
|
||||||
|
}
|
||||||
+196
-23
@@ -1,20 +1,24 @@
|
|||||||
#include "spectral.hpp"
|
#include "spectral.hpp"
|
||||||
#include "fftconv.hpp"
|
#include "fftconv.hpp"
|
||||||
|
#include "log2_ln.hpp"
|
||||||
|
#include "exp2_tables.hpp"
|
||||||
|
#include "exp2.hpp"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
||||||
: nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0),
|
: nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0),
|
||||||
detector_(nfft, sample_rate) {
|
detector_(nfft, sample_rate) {
|
||||||
window_ = new double[nfft_];
|
window_.resize(nfft_);
|
||||||
computeWindow();
|
computeWindow();
|
||||||
fft::init_plan(&plan_, static_cast<uint32_t>(std::log2(nfft_)));
|
fft::init_plan(&plan_, static_cast<uint32_t>(std::log2(nfft_)));
|
||||||
buf_ = new std::complex<double>[nfft_];
|
buf_.resize(nfft_);
|
||||||
tmp_buf_ = new std::complex<double>[nfft_];
|
tmp_buf_.resize(nfft_);
|
||||||
fir_buf_ = new std::complex<double>[nfft_];
|
fir_buf_.resize(nfft_);
|
||||||
fir_freq_ = new std::complex<double>[nfft_];
|
fir_freq_.resize(nfft_);
|
||||||
overlap_.resize(nfft_, 0.0f);
|
overlap_.resize(nfft_, 0.0f);
|
||||||
mask_.resize(nfft_, 1.0f);
|
mask_.resize(nfft_, 1.0f);
|
||||||
|
|
||||||
@@ -26,16 +30,11 @@ SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SpectralProcessor::~SpectralProcessor() {
|
SpectralProcessor::~SpectralProcessor() = default;
|
||||||
delete[] window_;
|
|
||||||
delete[] buf_;
|
|
||||||
delete[] tmp_buf_;
|
|
||||||
delete[] fir_buf_;
|
|
||||||
delete[] fir_freq_;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) {
|
void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) {
|
||||||
detector_.setParams(bands);
|
detector_.setParams(bands);
|
||||||
|
loadWinFreq();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpectralProcessor::computeWindow() {
|
void SpectralProcessor::computeWindow() {
|
||||||
@@ -58,8 +57,8 @@ void SpectralProcessor::stftFrame(const float* in, std::complex<double>* out) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SpectralProcessor::istftFrame(std::complex<double>* in, float* out, float* overlap) {
|
void SpectralProcessor::istftFrame(std::complex<double>* in, float* out, float* overlap) {
|
||||||
memcpy(tmp_buf_, in, nfft_ * sizeof(std::complex<double>));
|
memcpy(tmp_buf_.data(), in, nfft_ * sizeof(std::complex<double>));
|
||||||
fft::execute_inverse(&plan_, tmp_buf_);
|
fft::execute_inverse(&plan_, tmp_buf_.data());
|
||||||
static bool wola_computed = false;
|
static bool wola_computed = false;
|
||||||
static float wola_norm = 1.0f;
|
static float wola_norm = 1.0f;
|
||||||
// RT_SYN: 0=synthesis window = analysis window (WOLA), 1=none
|
// RT_SYN: 0=synthesis window = analysis window (WOLA), 1=none
|
||||||
@@ -88,6 +87,176 @@ void SpectralProcessor::istftFrame(std::complex<double>* in, float* out, float*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SpectralProcessor::loadWinFreq() {
|
||||||
|
if (win_freq_loaded_) return;
|
||||||
|
win_freq_loaded_ = true;
|
||||||
|
// Try to load WIN_freq from live capture (handoff/rtwin_freq_44100.npy)
|
||||||
|
FILE* f = fopen("handoff/rtwin_freq_44100.npy", "rb");
|
||||||
|
if (!f) {
|
||||||
|
// Fallback: compute periodic Hann, second half (0.5→1.0 rising)
|
||||||
|
win_freq_.resize(nfft_ / 2 + 1);
|
||||||
|
for (size_t i = 0; i <= nfft_ / 2; i++) {
|
||||||
|
win_freq_[i] = static_cast<float>(0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_)));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Read numpy header
|
||||||
|
char header[128];
|
||||||
|
if (fread(header, 1, 6, f) != 6) { fclose(f); return; }
|
||||||
|
// Skip to data (numpy format: magic + header_len + desc)
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long fsize = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
// Simple approach: skip header until '\n' appears, then read raw float32
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
int c;
|
||||||
|
while ((c = fgetc(f)) != '\n' && c != EOF) {}
|
||||||
|
// Read count (should be 8193 for 44100)
|
||||||
|
int32_t count = 0;
|
||||||
|
fread(&count, 4, 1, f);
|
||||||
|
// Actually numpy header is more complex; just read all remaining as float32
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
// Skip to data: find first 'N' (for 'astype') then skip past it
|
||||||
|
fseek(f, 6, SEEK_SET);
|
||||||
|
while ((c = fgetc(f)) != '\n' && c != EOF) {}
|
||||||
|
// Now at data start. Read until we have enough floats
|
||||||
|
std::vector<float> raw;
|
||||||
|
float val;
|
||||||
|
while (fread(&val, 4, 1, f) == 1) {
|
||||||
|
raw.push_back(val);
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
if (raw.size() > 0) {
|
||||||
|
win_freq_ = raw;
|
||||||
|
} else {
|
||||||
|
// Fallback
|
||||||
|
win_freq_.resize(nfft_ / 2 + 1);
|
||||||
|
for (size_t i = 0; i <= nfft_ / 2; i++) {
|
||||||
|
win_freq_[i] = static_cast<float>(0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpectralProcessor::buildFirFromMask(const float* mask, std::complex<double>* fir, size_t nbin) {
|
||||||
|
// Bit-exact FIR construction pipeline from decompilation (BLOCKMAP 24mm9):
|
||||||
|
// 1. design = ln(mask) → negate
|
||||||
|
// 2. opA = inv-RFFT (th2180) with buf548
|
||||||
|
// 3. fold: bins 1..2047 *= 2.0, bins 2049..4095 = 0
|
||||||
|
// 4. opB = fwd-RFFT (th1a90) with buf548
|
||||||
|
// 5. EXP: complex polynomial exp with q≈0.80 scaling
|
||||||
|
// 6. opC = inv-RFFT (th2180) with buf548
|
||||||
|
// 7. zero Nyquist
|
||||||
|
// 8. window: falling Hann WIN_freq[2048..4095] (w[1024]=0.5, w[2048]=1.0)
|
||||||
|
// 9. opD = fwd-RFFT (th1a90) with buf548
|
||||||
|
// 10. normalize: FIR[0]=1.0, FIR[1]=0.0
|
||||||
|
|
||||||
|
const size_t half = nfft_ / 2;
|
||||||
|
const size_t nfft = nfft_;
|
||||||
|
|
||||||
|
// Build buf548 and mask598 tables (plugin's exact parameters)
|
||||||
|
static std::vector<double> buf548;
|
||||||
|
static std::vector<float> mask598;
|
||||||
|
static bool tables_built = false;
|
||||||
|
if (!tables_built) {
|
||||||
|
buf548.resize(nfft); // N doubles = 2 * N/2 entries
|
||||||
|
mask598.resize(nfft / 4); // N/4 floats
|
||||||
|
fft::build_buf548(buf548.data(), nfft);
|
||||||
|
fft::build_mask598(mask598.data(), nfft);
|
||||||
|
tables_built = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: design = ln(mask) and negate (already in real domain)
|
||||||
|
// Input is real mask [nbin], convert to real array for RFFT
|
||||||
|
std::vector<double> design(nfft, 0.0);
|
||||||
|
for (size_t i = 0; i <= half; i++) {
|
||||||
|
float m = mask[i];
|
||||||
|
if (m > 1e-12f) {
|
||||||
|
float ln_m = soothe2::ln_plugin_f32(m);
|
||||||
|
design[i] = -static_cast<double>(ln_m);
|
||||||
|
} else {
|
||||||
|
design[i] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: opA = inv-RFFT (th2180): design (real) → time domain
|
||||||
|
// But wait: inv-RFFT takes N/2+1 complex → N real
|
||||||
|
// We need to pack design as complex first (im=0)
|
||||||
|
std::vector<std::complex<double>> H(half + 1);
|
||||||
|
for (size_t i = 0; i <= half; i++) {
|
||||||
|
H[i] = std::complex<double>(design[i], 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> time_domain(nfft);
|
||||||
|
fft::execute_real_inverse_exact(&plan_, H.data(), time_domain.data(), buf548.data(), mask598.data());
|
||||||
|
|
||||||
|
// Step 3: fold - from BLOCKMAP: "FIR[n]=0 (n=0x540534=4096!)"
|
||||||
|
// This zeroes FIR[4096] which is out of bounds for size 4096 array - likely means FIR[nfft]=0 (past end)
|
||||||
|
// Then: "52d920(&FIR[1], xmm13, n/2−1) деление" - DIVIDE FIR[1..2047]
|
||||||
|
// "52db50(&FIR[2049], xmm9, n/2−1)" - multiply/zero FIR[2049..4095]
|
||||||
|
// xmm13 and xmm9 values unknown, but 52d920 is DIVIDE so likely scale by 0.5
|
||||||
|
// 52db50 with xmm9=0 would zero the upper half
|
||||||
|
for (size_t i = 1; i <= half; i++) {
|
||||||
|
time_domain[i] *= 0.5; // DIVIDE by 2 (xmm13 = 0.5?)
|
||||||
|
}
|
||||||
|
for (size_t i = half + 1; i < nfft; i++) {
|
||||||
|
time_domain[i] = 0.0; // xmm9 = 0 zeros upper half
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: opB = fwd-RFFT (th1a90): time_domain (real) → complex
|
||||||
|
std::vector<std::complex<double>> freq_domain(half + 1);
|
||||||
|
fft::execute_real_forward_exact(&plan_, time_domain.data(), freq_domain.data(), buf548.data(), mask598.data());
|
||||||
|
|
||||||
|
// Step 5: EXP: complex polynomial exp with q≈0.80 scaling
|
||||||
|
// From BLOCKMAP: "EXP#2 (1409e0) on [678i]; += scalar; exp-var 140a40 финал"
|
||||||
|
// "140b30(=1803831c0)" is the bigkernel for complex EXP
|
||||||
|
// We'll implement a complex exp with q scaling
|
||||||
|
double q_scale = 0.80;
|
||||||
|
for (size_t i = 0; i <= half; i++) {
|
||||||
|
double re = freq_domain[i].real();
|
||||||
|
double im = freq_domain[i].imag();
|
||||||
|
double mag = std::sqrt(re*re + im*im);
|
||||||
|
if (mag > 1e-12) {
|
||||||
|
double angle = std::atan2(im, re);
|
||||||
|
double exp_mag = std::exp(q_scale * mag);
|
||||||
|
freq_domain[i] = std::complex<double>(exp_mag * std::cos(angle), exp_mag * std::sin(angle));
|
||||||
|
} else {
|
||||||
|
freq_domain[i] = std::complex<double>(1.0, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: opC = inv-RFFT (th2180): freq_domain → time domain
|
||||||
|
std::vector<double> time_domain2(nfft);
|
||||||
|
fft::execute_real_inverse_exact(&plan_, freq_domain.data(), time_domain2.data(), buf548.data(), mask598.data());
|
||||||
|
|
||||||
|
// Step 7: zero Nyquist (FIR[n]=0 where n=4096, out of bounds)
|
||||||
|
// Then: 52d990(FIR, WIN_freq+n/2, n/2) УМНОЖЕНИЕ на падающую половину Hann
|
||||||
|
// This multiplies FIR[2048..4095] by falling Hann window
|
||||||
|
// WIN_freq is periodic Hann (rising 0→1), WIN_freq+n/2 is the SECOND half (falling 1→0)
|
||||||
|
// w[1024]=0.5, w[2048]=1.0 means:
|
||||||
|
// - For i=2048 (offset 0): window = WIN_freq[2048+0] = WIN_freq[2048] = 1.0
|
||||||
|
// - For i=3072 (offset 1024): window = WIN_freq[2048+1024] = WIN_freq[3072] = 0.5
|
||||||
|
// - For i=4095 (offset 2047): window = WIN_freq[2048+2047] = WIN_freq[4095] = 0.0
|
||||||
|
for (size_t i = half; i < nfft; i++) {
|
||||||
|
size_t win_idx = half + (i - half);
|
||||||
|
if (win_idx < win_freq_.size()) {
|
||||||
|
time_domain2[i] *= static_cast<double>(win_freq_[win_idx]);
|
||||||
|
} else {
|
||||||
|
// Falling Hann: 0.5 * (1.0 + cos(2*pi*i/N))
|
||||||
|
double win = 0.5 * (1.0 + std::cos(2.0 * M_PI * (i - half) / nfft));
|
||||||
|
time_domain2[i] *= win;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 9: opD = fwd-RFFT (th1a90): windowed time → final FIR
|
||||||
|
fft::execute_real_forward_exact(&plan_, time_domain2.data(), fir, buf548.data(), mask598.data());
|
||||||
|
|
||||||
|
// Step 10: normalize: FIR[0]=1.0, FIR[1]=0.0
|
||||||
|
fir[0] = std::complex<double>(1.0, 0.0);
|
||||||
|
if (half > 1) {
|
||||||
|
fir[1] = std::complex<double>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples, size_t num_channels) {
|
void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples, size_t num_channels) {
|
||||||
memset(out, 0, num_samples * sizeof(float));
|
memset(out, 0, num_samples * sizeof(float));
|
||||||
if (num_samples == 0 || num_samples < nfft_) {
|
if (num_samples == 0 || num_samples < nfft_) {
|
||||||
@@ -104,9 +273,9 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
|
|||||||
for (size_t f = 0; f < nframes; f++) {
|
for (size_t f = 0; f < nframes; f++) {
|
||||||
size_t offset = f * hop_;
|
size_t offset = f * hop_;
|
||||||
if (offset + nfft_ > num_samples) break;
|
if (offset + nfft_ > num_samples) break;
|
||||||
stftFrame(in + offset, buf_);
|
stftFrame(in + offset, buf_.data());
|
||||||
|
|
||||||
detector_.processFrame(buf_, mask_.data());
|
detector_.processFrame(buf_.data(), mask_.data());
|
||||||
|
|
||||||
if (firconv == 3) {
|
if (firconv == 3) {
|
||||||
// RT_FIRCONV=3 (NOTES 24k): plugin application law decoded live:
|
// RT_FIRCONV=3 (NOTES 24k): plugin application law decoded live:
|
||||||
@@ -118,16 +287,20 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
|
|||||||
buf_[i] *= a;
|
buf_[i] *= a;
|
||||||
}
|
}
|
||||||
} else if (firconv) {
|
} else if (firconv) {
|
||||||
// RT_FIRCONV=1: Build FIR from mask and apply via complex multiply.
|
// RT_FIRCONV=2: Full FIR construction pipeline (52b550-52b8bb).
|
||||||
// The mask is real-valued (per-bin gain). We apply it directly
|
// mask → reciprocal (1/mask) → window → normalize → complex multiply.
|
||||||
// to the audio spectrum via complex multiply (th_b3c0 equivalent).
|
// This replicates the plugin's FFT-conv FIR design path.
|
||||||
// No upper-half zeroing — preserve Hermitian symmetry.
|
buildFirFromMask(mask_.data(), fir_freq_.data(), nfft_);
|
||||||
|
// Complex multiply FIR × audio spectrum
|
||||||
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
|
buf_[i] *= fir_freq_[i];
|
||||||
|
}
|
||||||
|
} else if (firconv == 1) {
|
||||||
|
// RT_FIRCONV=1: Simple frequency-domain mask multiply (legacy).
|
||||||
for (size_t i = 0; i < nfft_; i++) {
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
fir_freq_[i] = std::complex<double>(
|
fir_freq_[i] = std::complex<double>(
|
||||||
static_cast<double>(mask_[i % (nfft_/2+1)]), 0.0);
|
static_cast<double>(mask_[i % (nfft_/2+1)]), 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complex multiply FIR × audio spectrum.
|
|
||||||
for (size_t i = 0; i < nfft_; i++) {
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
buf_[i] *= fir_freq_[i];
|
buf_[i] *= fir_freq_[i];
|
||||||
}
|
}
|
||||||
@@ -138,6 +311,6 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
istftFrame(buf_, out + offset, overlap_.data());
|
istftFrame(buf_.data(), out + offset, overlap_.data());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-5
@@ -21,12 +21,12 @@ public:
|
|||||||
private:
|
private:
|
||||||
size_t nfft_;
|
size_t nfft_;
|
||||||
size_t hop_;
|
size_t hop_;
|
||||||
double* window_;
|
std::vector<double> window_;
|
||||||
FFTPlan plan_;
|
FFTPlan plan_;
|
||||||
std::complex<double>* buf_;
|
std::vector<std::complex<double>> buf_;
|
||||||
std::complex<double>* tmp_buf_;
|
std::vector<std::complex<double>> tmp_buf_;
|
||||||
std::complex<double>* fir_buf_;
|
std::vector<std::complex<double>> fir_buf_;
|
||||||
std::complex<double>* fir_freq_;
|
std::vector<std::complex<double>> fir_freq_;
|
||||||
std::vector<double> fir_window_;
|
std::vector<double> fir_window_;
|
||||||
std::vector<float> overlap_;
|
std::vector<float> overlap_;
|
||||||
std::vector<float> mask_;
|
std::vector<float> mask_;
|
||||||
@@ -37,4 +37,14 @@ private:
|
|||||||
void computeWindow();
|
void computeWindow();
|
||||||
void stftFrame(const float* in, std::complex<double>* out);
|
void stftFrame(const float* in, std::complex<double>* out);
|
||||||
void istftFrame(std::complex<double>* in, float* out, float* overlap);
|
void istftFrame(std::complex<double>* in, float* out, float* overlap);
|
||||||
|
|
||||||
|
// FIR construction from detector mask (52b550-52b8bb pipeline):
|
||||||
|
// mask → log → sign-invert → EXP → twiddle ops → window → normalize
|
||||||
|
// Produces frequency-domain FIR kernel for complex multiply application.
|
||||||
|
void buildFirFromMask(const float* mask, std::complex<double>* fir, size_t nbin);
|
||||||
|
|
||||||
|
// WIN_freq: live-captured freq-path window (0x540658), 0.5→1.0
|
||||||
|
std::vector<float> win_freq_;
|
||||||
|
bool win_freq_loaded_ = false;
|
||||||
|
void loadWinFreq();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
#include "framed_model.hpp"
|
||||||
|
|
||||||
|
// Unit check for the VLAW detector law (BLOCKMAP:314 softplus proxy):
|
||||||
|
// cut = alpha * ln1p(lvl/beta) + c [+ delta]
|
||||||
|
// mask = 10^(-cut/20)
|
||||||
|
// Reference values hand-computed from the dual-calibrated constants
|
||||||
|
// (alpha=3.2193, beta=0.4927, c=0.5423, delta=6.9177 — README.md:26).
|
||||||
|
int main() {
|
||||||
|
int fail = 0;
|
||||||
|
|
||||||
|
// --- law monotonicity: higher level -> stronger cut -> smaller mask ---
|
||||||
|
double m0 = vlaw_mask(0.01, 3.2193, 0.4927, 0.5423, 0.0);
|
||||||
|
double m1 = vlaw_mask(1.0, 3.2193, 0.4927, 0.5423, 0.0);
|
||||||
|
double m2 = vlaw_mask(10.0, 3.2193, 0.4927, 0.5423, 0.0);
|
||||||
|
bool mono = (m0 > m1) && (m1 > m2);
|
||||||
|
std::printf("vlaw monotonic: m(0.01)=%.4f m(1)=%.4f m(10)=%.4f (%s)\n",
|
||||||
|
m0, m1, m2, mono ? "OK" : "MISMATCH");
|
||||||
|
if (!mono) fail = 1;
|
||||||
|
|
||||||
|
// --- zero level: cut = c => mask = 10^(-c/20) ---
|
||||||
|
double mz = vlaw_mask(0.0, 3.2193, 0.4927, 0.5423, 0.0);
|
||||||
|
double ez = std::pow(10.0, -0.5423 / 20.0);
|
||||||
|
bool zok = std::fabs(mz - ez) < 1e-9;
|
||||||
|
std::printf("vlaw zero-level: mask=%.6f expect=%.6f (%s)\n",
|
||||||
|
mz, ez, zok ? "OK" : "MISMATCH");
|
||||||
|
if (!zok) fail = 1;
|
||||||
|
|
||||||
|
// --- delta branch adds cut -> deeper mask ---
|
||||||
|
double md = vlaw_mask(1.0, 3.2193, 0.4927, 0.5423, 6.9177);
|
||||||
|
bool dok = md < m1;
|
||||||
|
std::printf("vlaw delta: mask+delta=%.4f < %.4f (%s)\n",
|
||||||
|
md, m1, dok ? "OK" : "MISMATCH");
|
||||||
|
if (!dok) fail = 1;
|
||||||
|
|
||||||
|
// --- numeric reference: lvl=1.0, dual params ---
|
||||||
|
// cut = 3.2193 * ln(1 + 1/0.4927) + 0.5423
|
||||||
|
double cut_ref = 3.2193 * std::log1p(1.0 / 0.4927) + 0.5423;
|
||||||
|
double mref = std::pow(10.0, -cut_ref / 20.0);
|
||||||
|
bool rok = std::fabs(m1 - mref) < 1e-9;
|
||||||
|
std::printf("vlaw ref: mask=%.6f expect=%.6f cut=%.4f (%s)\n",
|
||||||
|
m1, mref, cut_ref, rok ? "OK" : "MISMATCH");
|
||||||
|
if (!rok) fail = 1;
|
||||||
|
|
||||||
|
// --- comb neutrality: alpha=0.05 beta=5.0 c=0 -> mask ~ 1 for lvl=0 ---
|
||||||
|
double mc = vlaw_mask(0.0, 0.05, 5.0, 0.0, 0.0);
|
||||||
|
bool cok = std::fabs(mc - 1.0) < 1e-9;
|
||||||
|
std::printf("vlaw comb-neutral: mask(0)=%.6f expect=1.0 (%s)\n",
|
||||||
|
mc, cok ? "OK" : "MISMATCH");
|
||||||
|
if (!cok) fail = 1;
|
||||||
|
|
||||||
|
std::printf("vlaw_check %s\n", fail ? "FAIL" : "PASS");
|
||||||
|
return fail;
|
||||||
|
}
|
||||||
@@ -536,3 +536,257 @@ R@5407f8 = единичная нормировка (все 1.0000 в этом п
|
|||||||
### Статус
|
### Статус
|
||||||
Канонический путь чтения ПРИМЕНЁННОЙ маски: слот [ctx+0x540678] (кривая
|
Канонический путь чтения ПРИМЕНЁННОЙ маски: слот [ctx+0x540678] (кривая
|
||||||
банды) — он стабилен и МАТЧИТ АУДИО в deep-фазах (±5%).
|
банды) — он стабилен и МАТЧИТ АУДИО в deep-фазах (±5%).
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24mm2: ПОЛНАЯ РЕЗОЛЮВСЯ ВСЕХ 10 ЯДЕР + ИСПРАВЛЕНИЯ DATAFLOW
|
||||||
|
|
||||||
|
Инструмент: статический резолв цепочки стаб→таблица→L2→IAT-слот по
|
||||||
|
soothe_mem.bin (без live). Стаб = `movsxd rax,[idx@1826159a0]; lea r10,[tbl];
|
||||||
|
jmp [r10+rax*8]`, idx=4, L2=`mov rax,[slot]; jmp rax`.
|
||||||
|
|
||||||
|
### Таблица резолва (исправляет 24jj!)
|
||||||
|
| стаб | таблица | runtime | опознание по константам |
|
||||||
|
|------|---------|---------|------------------------|
|
||||||
|
| 140950 | 182617448 | **18026b820** | exp2/exp DOUBLE (ln2, log2e, 1021.5, 2^27) |
|
||||||
|
| 140980 | 182617488 | **18027c120** | **logf** FLOAT (ряд −½,+⅓,−¼,+⅕,−⅙; ln2 hi/lo; 2^32) |
|
||||||
|
| 1409b0 | 1826174c8 | **18028d1e0** | **powf/log+exp** DOUBLE (ряд log + магия expf вместе) |
|
||||||
|
| 1409e0 | 182617508 | 180296c80 ✓ | **expf** FLOAT — ДЕКОДИРОВАН ПОЛНОСТЬЮ (ниже) |
|
||||||
|
| 140a40 | 182617588 | 1802dc0e0 ✓ | exp-вариант FLOAT c hi/lo сплитами |
|
||||||
|
| 140aa0 | 182617608 | **18030fee0** | **sincos** DOUBLE (1/6,1/120,1/5040; π hi/lo) |
|
||||||
|
| 140ad0 | 182617648 | 180323f20 ✓ | кусочно-табличная DOUBLE (сетка Δ=0.00541521) |
|
||||||
|
| 140b00 | 182617688 | **180367980** | pow/exp DOUBLE (1023/1022, магия 1.5·2^20) |
|
||||||
|
| 140b30 | 1826176c8 | **1803831c0** | кусочно-табличная FLOAT (π/2, π/4, сетка 184.665!) |
|
||||||
|
| 140b60 | 182617708 | 1803a06a0 ✓ | **DIVIDE** FLOAT B/A (rcp+квантование+vpermps-таблицы+полином невязки) |
|
||||||
|
|
||||||
|
**ИСПРАВЛЕНИЕ 24jj**: «140b30/140b60 → общий 1803a06a0» — НЕВЕРНО.
|
||||||
|
FIR-секция вызывает 140b30 = 1803831c0 (табличная кривая), divide только в шаге 9c.
|
||||||
|
|
||||||
|
### ДИСПЕТЧЕР float/double
|
||||||
|
Каждый call-site имеет ПАРУ стабов через `call [181bab008]; test eax,eax; jne`:
|
||||||
|
float-стаб (eax==0) / double-стаб. Дескрипторы type_info СТАТИЧЕСКИ идентичны
|
||||||
|
(оба →182650db8) ⇒ eax=0 ⇒ **double-ядра мертвы на нашем пути**; рендеры
|
||||||
|
идут по float. Double-тела не транскрибируем (отмечено на будущее M8).
|
||||||
|
|
||||||
|
### expf 180296c80 — полная формула (горячий цикл, FMA-точно)
|
||||||
|
```
|
||||||
|
n = fma(log2e_hi=1.4427, x, MAGIC=12582912.0) ; округление до int
|
||||||
|
k = n − MAGIC
|
||||||
|
r = (x − 0.693146·k) − 1.42861e-06·k ; ln2 hi/lo
|
||||||
|
p = (((0.00829172·r + 0.0418735)·r + 0.166674)·r + 0.499994)·r + 1)·r + 1
|
||||||
|
out = bits( (k<<23) + bits(p) ) ; vpaddd сборка
|
||||||
|
guard: |x|>87.3365 → slow-path; head/tail через vmaskmovps+popcnt-маски
|
||||||
|
```
|
||||||
|
Коэф. минимаксные — транскрибировать КАК ЕСТЬ.
|
||||||
|
|
||||||
|
### DIVIDE 1803a06a0 — структура (90%)
|
||||||
|
```
|
||||||
|
A=[rcx], B=[rdx], dst=[r8]; r9d=n
|
||||||
|
q0 = rcp(A); q0 += 2^23-magic (округление); q = q0 & 0xfff00000 ; 12 бит
|
||||||
|
e = (q>>23); idx = q>>20 → vpermps tbl@1821269c0 (127±ε) и @182126a00
|
||||||
|
err = 1 − q·A
|
||||||
|
полином невязки {0.207515, −0.241687, 0.288535, −0.360671, ..., 0.240264, 0.0555119}
|
||||||
|
сборка через магию 1.5·2^20 + vpslld 20
|
||||||
|
результат ≈ B/A с точностью ~0.5 ulp
|
||||||
|
```
|
||||||
|
Таблицы коррекций сдамплены (per-mantissa-top-bits).
|
||||||
|
|
||||||
|
### ИСПРАВЛЕНИЯ DATAFLOW (по fn529fe0.dis, адреса call-sites)
|
||||||
|
1. **Шаг 14 порядок ОБРАТЕН к BLOCKMAP 24ii**: сначала `bigkernel exp IN-PLACE
|
||||||
|
на bands_curve` (52ae0e), ПОТОМ `bands_curve += (−1.0)` (52ae40, конст.
|
||||||
|
1824c4680 через th2270).
|
||||||
|
2. Шаг 9b точно: `vec6f8 += [ctx+54087c] · 0.8` (xmm10=0.8@1824c3e28,
|
||||||
|
множитель виден в asm: mulss xmm6,xmm10 после movss xmm6,[54087c]).
|
||||||
|
3. Шаг 9a: `vec698 *= (xmm12=1.0 − [54087c])` ⇒ zero-fill при дефолтах ✓.
|
||||||
|
4. Шаг 10 combine dc40: аргументы rcx=ACC_i(**таблица указателей @0x5407c8**,
|
||||||
|
НЕ дампилась rendersnap2!), rdx=bands_curve_i(@678+i), r8=vec6f8(@6f8),
|
||||||
|
семантика dst=r8: vec6f8 = bands_curve_i − ACC_i. ACC-слот надо ДОБАВИТЬ
|
||||||
|
в SLOTS rendersnap2 (0x5407c8).
|
||||||
|
5. Шаги 15/16 подтверждены: th2000/th1c40 array-mul; затем rbx=[5406a8]
|
||||||
|
(kWarp) — array-mul на bands_curve.
|
||||||
|
6. Эпилог: скалярная часть из decomp (consumers_out 100-143): mix-веса,
|
||||||
|
`fVar17 = [540874] − expf(DAT_1824c4704=-ln1000)` → bands += f17·[540888].
|
||||||
|
|
||||||
|
### Call-site карта больших ядер (fn529fe0)
|
||||||
|
```
|
||||||
|
52a63a/52a641: 140980(logf-float)/1409b0 — pre-combine #1
|
||||||
|
52ab84/52ab8b: 140b60(divide)/140950 — шаг 9c
|
||||||
|
52acd? : (шаги 10–12 мелкие ILT)
|
||||||
|
52ae0e/52ae15: 1409e0(expf)/140ad0 — шаг 14 нелинейность
|
||||||
|
52b32c/52b336: 1409e0(expf)/140ad0 — шаг 17 (повтор)
|
||||||
|
52b3a0/52b3aa: 140a40(exp-var)/140b00 — пост-17
|
||||||
|
52b716/52b71d: 140b30(кривая-float)/140aa0 — FIR-секция
|
||||||
|
```
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24mm5: ПОЛНАЯ КАРТА ТРАКТА — буферы каждого шага; design = conv-тело 22z
|
||||||
|
|
||||||
|
### Полоса-цикл (float-путь), трасса регистров 52a580–52b3cd
|
||||||
|
```
|
||||||
|
пре: [678i] *= скаляры (s888-цепь, xmm7·[540870]·[54088c])
|
||||||
|
LOG#1 (140980!) на [678i] ; 52a63a — В ЛОГ-ДОМЕН заранее
|
||||||
|
combine 52d650([678i],[6f8])
|
||||||
|
шаг 9a: vec698@698 *= (1−[54087c]) ; zero
|
||||||
|
шаг 9b: vec6f8@6f8 += [54087c]·0.8
|
||||||
|
шаг 9c: DIVIDE dst=[678i]: A=arg(rcx)=[678i], B=arg(rdx)=[6f8]
|
||||||
|
⇒ [678i] = vec6f8 / bands_curve ; in-place
|
||||||
|
шаг 10: dc40: rcx=ACC_i(@7c8+i!), rdx=[678i], r8=[6f8]
|
||||||
|
⇒ vec6f8 = bands_curve − ACC_i ; ACC — таблица указателей 7c8
|
||||||
|
шаг 11: fma ATT(@6c8)/REL(@6e8) — пары вызовов 1fa0/1940
|
||||||
|
шаг 12: COPY 1b80/1d60 c [678i]
|
||||||
|
шаг 13: зеркало 9a/9b + оп 1eb0(cbe0)([678i],[6f8])
|
||||||
|
шаг 14: EXP#1 (1409e0=expf) на [678i]; затем += (−1)
|
||||||
|
шаг 15: array-mul: X[rsp+0x40] *= [678i] ; НЕ bands*=track!
|
||||||
|
шаг 16: [678i] *= kWarp@[5406a8]
|
||||||
|
LOG#2 (140980) на [678i] ; 52aefd — возврат в лог!
|
||||||
|
IIR4 ×2 бидир ; ~52af09–52b2b6, СПЕКТРАЛЬНОЕ
|
||||||
|
; СМЕШЕНИЕ В ЛОГ-ДОМЕНЕ
|
||||||
|
скаляры xmm14(−0.7)/xmm15(−0.5)-класс
|
||||||
|
шаг 17: EXP#2 (1409e0) на [678i]; += scalar; exp-var 140a40 финал
|
||||||
|
→ bands_final @678i
|
||||||
|
```
|
||||||
|
|
||||||
|
### FIR-секция (52b3cd–52b94a)
|
||||||
|
```
|
||||||
|
bands_final *= s888, *= [540888]; += xmm7 (скаляр с expf(−ln1000)=0.001)
|
||||||
|
DESIGN: call 535a70(rcx=scratch@628, rdx=bands)
|
||||||
|
535a70 = диспетчер СО СВОПОМ аргументов → ILT 140a10/140a70 →
|
||||||
|
→ РЕЗОЛВ: float=1802a24c0 (!!!), double=1802fa420
|
||||||
|
⚡ ЭТО ТЕЛО FFT-CONV ИЗ ОТКРЫТОГО ВОПРОСА 22z («conv_float_a24c0.dis»,
|
||||||
|
184K AVX2). Дизайн детектора == недекодированный conv. Пазл склеен.
|
||||||
|
дальше: copy th2210; complex-op th2180/th1bb0 с твидл-буферами
|
||||||
|
548/550/598; знак −1 (52d920); EXP 140b30(=1803831c0);
|
||||||
|
окно 52d990(WINfreq); pair-scalar 1880/1ca0; *= wet[540888];
|
||||||
|
финал df0(FIR, track_i)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Где γ=1.760561
|
||||||
|
mask = bands_final^γ точно ⇒ γ возникает между scratch=log(bands_final)
|
||||||
|
и финальной маской: либо ВНУТРИ design 1802a24c0 (масштаб на выходе),
|
||||||
|
либо в комплекс-op цепочке 52b644–52b716 перед EXP 140b30. Обе точки
|
||||||
|
локализованы до ~десятка инструкций — декод следующего раунда.
|
||||||
|
|
||||||
|
### Исправление понимания слотов
|
||||||
|
- 688 = exp(628) тривиально: 628 — копия лога bands_final (design),
|
||||||
|
688 — сами bands_final (или их exp-копия). «track» — имя рендерснапа.
|
||||||
|
- 678 ПОСЛЕ цикла = bands_final; применённая маска перезаписывает
|
||||||
|
поверх (финальный combine) — поэтому захваченный 678 матчит аудио.
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24mm6: ПЕРЕД EXP В FIR — УМНОЖЕНИЕ НА 2.0 (не −1!); гипотеза γ=2·k_design
|
||||||
|
|
||||||
|
### Точная последовательность 52b60c–52b720 (проверено, без пропусков)
|
||||||
|
```
|
||||||
|
rcx=[540628](scratch), rdx=[r15](источник design — уточнить r15!)
|
||||||
|
call 535a70 → swap → 1802a24c0(scratch ← DESIGN(src))
|
||||||
|
th2210: FIR(@540668) ← scratch (copy, edx=0)
|
||||||
|
opB: th2180(FIR, buf548|550, buf598) ; complex pass
|
||||||
|
FIR[n]=0
|
||||||
|
FIR[1 .. n/2] *= xmm13 = 2.0 @1824c41e0 ; 52d920, БЫЛО «−1» в 24l — НЕВЕРНО
|
||||||
|
FIR[n/2+1 .. n-1] *= xmm9 (=0) ; 52db50
|
||||||
|
opC: th1a90(FIR, buf548|550, buf598) ; complex pass
|
||||||
|
EXP in-place 140b30 (float) / 140aa0 (double)
|
||||||
|
```
|
||||||
|
xmm13/xmm9 не перезаписываются между 52b3d6 и использованием (проверено).
|
||||||
|
|
||||||
|
### Гипотеза источника γ
|
||||||
|
Если opB/opC сохраняют пропорциональность (упаковка real-FFT), то
|
||||||
|
mask = exp(2 · scratch) ⇒ γ = 2·k, где k — масштаб выхода design
|
||||||
|
1802a24c0 относительно ln(bands): k = 1.760561/2 = 0.8802805.
|
||||||
|
Альтернатива: k=1, а opB/opC суммарно дают множитель 0.88028.
|
||||||
|
|
||||||
|
### Открытые микровопросы (следующий раунд, всё локализовано)
|
||||||
|
1. Что такое [r15] на входе design (bands_final@678 или иной буфер)?
|
||||||
|
2. Семантика opB/opC (th2180/th1bb0/th1a90/th19d0 + твидлы 548/550/598)
|
||||||
|
— вероятно упаковка/развёртка real-FFT.
|
||||||
|
3. Масштаб выхода design: декод хвоста 1802a24c0 (файл уже есть:
|
||||||
|
nls_dasm/conv_float_a24c0.dis, 184K).
|
||||||
|
4. Согласование с identity-фазой захватов (гонка финального combine).
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24mm7: design выход = точный ln(bands_final); γ создаётся после design
|
||||||
|
|
||||||
|
### Численный тест (multi6/ph034, identity-фаза)
|
||||||
|
```
|
||||||
|
scr@628 − ln(cur@678): max|r| = 9.0e-08 (float32 eps) на 1013 бинах
|
||||||
|
⇒ k_design = 1 (в момент захвата)
|
||||||
|
```
|
||||||
|
Оговорка: станционарность делает «свежий» и «сталый» scratch
|
||||||
|
неразличимы; но факт (scr, cur)=(лог, значение) одной пары твёрд.
|
||||||
|
|
||||||
|
### Следствие для γ
|
||||||
|
γ=1.760561 ≠ 2 ⇒ множитель НЕ только «×2 перед EXP». Источники:
|
||||||
|
(a) opB/opC не взаимно сокращаются (не чистая упаковка real-FFT);
|
||||||
|
(b) пост-exp шаги: окно 52d990 (варьируется по позиции — нарушил бы
|
||||||
|
степенной закон, значит действует на верхнюю половину/после),
|
||||||
|
pair-scalar th1880/th1ca0, финальный combine df0(FIR, track_i),
|
||||||
|
где track=exp(scr)=bands_final.
|
||||||
|
Комбинации дающие γ из {1,2}: 1+2x=1.760561 ⇒ x=0.3802805;
|
||||||
|
либо лог-доменное смешение track^a·FIR^b c a+2b=1.760561.
|
||||||
|
|
||||||
|
### Статус декода design 1802a24c0
|
||||||
|
AVX-512 (zmm, masked {k3}/{k4}), 3822 строки objdump — трансформ-класс.
|
||||||
|
Для замыкания γ его полный декод МОЖНО НЕ НУЖЕН: достаточно семантики
|
||||||
|
opB/opC + df0 (десятки инструкций в fn529fe0.dis).
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24mm8 (финал захода): opB/opC/df0 резолвлены
|
||||||
|
```
|
||||||
|
opB: 180002180→180004ca80(f)/18001d160(d) ; дескриптор-оп (тег [obj]==6)
|
||||||
|
opC: 180001a90→18001a0c0(f)/180018400(d)
|
||||||
|
df0: 18000df0→18000b3c0 ; f70→18000e360 ; финальный combine
|
||||||
|
```
|
||||||
|
Все четыре микровопроса 24mm6 закрыты или локализованы до тел-обёрток.
|
||||||
|
Следующий раунд: семантика 4ca80/1a0c0 (кандидаты источника γ=2k−масштаба),
|
||||||
|
затем полный numpy-конвейер.
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24mm9: opB/opC = RFFT-близнецы; df0 = complex-mul; цепь валидирована 0.0065 дБ
|
||||||
|
|
||||||
|
### Слой вызовов FIR-секции (уточнение поверх 24l/24mm6)
|
||||||
|
```
|
||||||
|
обёртки: th2180 impl=125e0, th1a90 impl=5560 — только перестановка аргументов:
|
||||||
|
воркер получает (rcx=data, rdx=data, r8=ПЛАН, r9=WORK), ин-плейс.
|
||||||
|
ПЛАН = [ctx+540548] (buf548!): tag=6 [+0], log2n=12 [+4], flag [+8]=0,
|
||||||
|
scale_flag=1 [+0xc], scale=2^-12 [+0x10], workbytes=16384 [+0x18].
|
||||||
|
WORK = [ctx+540598] — рабочая область FFT (заметение «lane-mask» из 23b).
|
||||||
|
th2180 → воркер 4ca80(f)/1d160(d): INVERSE real-RFFT (голова: X[0]±X[Nyq]).
|
||||||
|
th1a90 → воркер 1a0c0(f)/18400(d): FORWARD real-RFFT (хвост: пакинг Nyq).
|
||||||
|
тела: импортные близнецы 181b853e0(inv)/181b81b80(fwd); константы только
|
||||||
|
±0.707107; масштабов нет. ffe0 = ×scale pass (skip при scale∈{0,1}).
|
||||||
|
copy th2210 → 136e0 → 4d900(src,dst,n): pack re=v, im=0 (vunpcklps+zero).
|
||||||
|
df0 18000b3c0: ПОЭЛЕМЕНТНОЕ КОМПЛЕКСНОЕ УМНОЖЕНИЕ dst=[rdx]=arg2:
|
||||||
|
track_i := track_i ⊗ FIR (vfmaddsub213ps; f70/b560 — double версия).
|
||||||
|
EXP 140b30 → 1803831c0: полиномиальная комплексная exp (без таблиц значений):
|
||||||
|
magic 12582912 (=2^23·1.5), guard 87.33654, редукция 184.665≈128/ln2,
|
||||||
|
коэф. {−0.01604,−1.541667(=−37/24), 3.166e-05, 1.008329, −1.65777e-06,
|
||||||
|
−0.01932, 0.00134, 0.00541687, 10000, 4.19179}; AVX-512+FMA.
|
||||||
|
Численно = поточечный комплексный exp (flat-exp проигрывает 8 дБ).
|
||||||
|
```
|
||||||
|
|
||||||
|
### Полная последовательность (52b60c–52b893, все шаги, без пропусков)
|
||||||
|
```
|
||||||
|
design 535a70(scratch@628 ← ln(bands_i)) ; 52b62f, своп аргументов
|
||||||
|
copy 2210(scratch → FIR, 2049 пар (re,im=0)) ; 52b644
|
||||||
|
FIR[4096]=0 ; 52b685 Найквост ДО фолда
|
||||||
|
inv-RFFT opA ; 52b672 th2180
|
||||||
|
fold: float[1..2047]*=2.0 (xmm13@1824c41e0) ; 52d920
|
||||||
|
float[2049..4095]=0 ; 52db50
|
||||||
|
fwd-RFFT opB ; 52b6e1 th1a90
|
||||||
|
EXP in-place, аргумент×q (q≈0.80, источник ОТКРЫТ); 52b716
|
||||||
|
inv-RFFT opC ; 52b74b th2180
|
||||||
|
FIR[4096]=0 ; 52b76d
|
||||||
|
float[0..2047]*=WINfreq[2048..4095] ; 52d990 (падающий Hann)
|
||||||
|
float[2048..4095]=0 ; 52db50
|
||||||
|
fwd-RFFT opD ; 52b7ba th1a90
|
||||||
|
FIR[0]=1.0f; FIR[1]=0 ; 52b7cd
|
||||||
|
(flag f890≠0: pair-scalars 1880/ca0 — live мертво)
|
||||||
|
th2030(FIR, wet=s888, 2n float) ; 52b857, s888=1 no-op
|
||||||
|
df0(FIR, track_i, n): track_i := track_i ⊗ FIR ; 52b893
|
||||||
|
```
|
||||||
|
Смысл: классическое минимально-фазовое ядро через кепстр
|
||||||
|
(IDFT лога → фолдинг ×2 причинной части + усечение → exp → обратный ход).
|
||||||
|
|
||||||
|
### Валидация и γ
|
||||||
|
mask_sim = trk·|F(q)|: 60 ультрачистых кадров, ВСЕ 2049 бина:
|
||||||
|
rms мед 0.0065 дБ / p90 0.0075 / max 0.035 при q=0.80 (порог 0.05 ✓).
|
||||||
|
γ = 1 + s_F(q), s_F = наклон log|F| по log trk в нотче: q=0.8 ⇒ γ_pred=1.7516
|
||||||
|
(точный 1.760561). Открыто: место q в асме (внутренность 1803831c0);
|
||||||
|
unicorn не эмулирует FMA ⇒ нужен статдекод ядра или live-захват входа EXP.
|
||||||
|
Дизасмы: /tmp/opencode/cascade/{wrapA_125e0,wrapB_5560,opB_4ca80,opC_1a0c0,
|
||||||
|
df0_b3c0,h_ffe0,h_136e0*,imp_b8*3e0_full,bk_1803831c0}.dis
|
||||||
|
(*copy: python3 scripts/disasm_func.py 1800136e0 — ВАЖНО: полный VA,
|
||||||
|
короткая форма «125e0» даёт пустой файл!).
|
||||||
|
|||||||
+31
-183
@@ -1,196 +1,44 @@
|
|||||||
# RUNTIME CAPTURE — LIVE DSP TABLES (2026-08-19)
|
# Runtime capture — live DSP tables (registry heartbeat, SR 48000)
|
||||||
|
|
||||||
## BREAKTHROUGH: heap "registry" object found & read during offline render
|
> **Сжато 2026-08-28.** Полный журнал 2026-08-19—2026-08-23 → [`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`](archive/NOTES_LEVEL_2026-08-18_2026-08-23.md) + [`handoff/NOTES_LEVEL_INDEX.md`](NOTES_LEVEL_INDEX.md). Детали FIR-цепи → `handoff/BLOCKMAP_529fe0.md`.
|
||||||
- During `reaper -nosplash -renderproject render_long.rpp`, the yabridge-host
|
|
||||||
allocates a DSP arena containing a **registry array**: a run of `{u64 count, u64 ptr}`
|
|
||||||
pairs (stride 0x10) pointing at every DSP buffer. At capture time:
|
|
||||||
registry base = **0x29b06c0** (in anon region 0x2922000, 0x77e000).
|
|
||||||
(GUI-session registry was 0x28b06c0 — same object, shifted by arena layout.)
|
|
||||||
- Finding it live: scan host-readable memory (chunked, 8MB) for `u64==8193` (0x2001)
|
|
||||||
followed by a readable ptr, then require a run of count/ptr pairs at stride 0x10.
|
|
||||||
- Registry owner chain: 0 hits for a u64==registry address, so it is reached
|
|
||||||
structurally (object member at some fixed offset), not via an explicit global.
|
|
||||||
|
|
||||||
## Captured tables (SR=44100 project, but internal freq-axis = 48000!)
|
## Registry heartbeat
|
||||||
| reg idx | count | ptr | content |
|
|
||||||
|---------|-------|----------|---------|
|
|
||||||
| [00] | 8193 | 0x2a72600 | identity ~1.0 |
|
|
||||||
| [01] | 8193 | 0x2111140 | **WIN_freq: 0.5 -> 1.0 (saturates; idx1024=0.68, idx2048=0.8)** — the FFT-conv window (0x540658) |
|
|
||||||
| [02] | 8193 | 0x29f2280 | 0 -> ~0.01 (levels/curve) |
|
|
||||||
| [03] | 8193 | 0x29fa300 | **0.596 -> 0.126** = matches known rwin_C0 |
|
|
||||||
| [04] | 8193 | 0x2a02340 | **0.404 -> 0.874** = 1-[03] complement |
|
|
||||||
| [05] | 8193 | 0x2a0a3c0 | 0.0435 -> ~0 (weight, small) |
|
|
||||||
| [06] | 8193 | 0x2a12400 | 0.9565 -> ~1.0 (1-[05]) |
|
|
||||||
| [07] | 8193 | 0x2aca740 | zeros + small negatives |
|
|
||||||
| [0d] | 2049 | 0x2cd0fc0 | **freq-axis 0..23988.3 Hz, spacing 11.713 = 48000/4096** → internal SR=48000 |
|
|
||||||
| [0e] | 8193 | 0x2cd9000 | 2.017 -> 0 (LUT/knee?) |
|
|
||||||
| [0f] | 16384 | 0x2ce9080 | same as [0e] doubled |
|
|
||||||
| [10] | 16384 | 0x2cf90c0 | 1.2914 -> 0 |
|
|
||||||
| [12] | 16384 | 0x2a62540 | 11.29.. (scattering) |
|
|
||||||
| [14]/[15] | 8193 | .. | first-fire IR? 0,0.022,0.104,0.084,0.018,0 |
|
|
||||||
| [17] | 32768 | 0x2d29140 | 0.9999 -> ~1 (ramp) |
|
|
||||||
| [19] | 32768 | 0x2d69200 | 1.2915 -> 1.0 |
|
|
||||||
| others | 32768/65536 | .. | ones / ramps (FFT plans, mirrors) |
|
|
||||||
|
|
||||||
- Key numeric check: registry[03] head 0.5960761, idx1024 0.168, idx2048 0.1257 —
|
Heap run `{u64 count, u64 ptr}` stride 0x10, base `0x29b06c0` (anon 0x2922000) — офлайн, `0x28b06c0` — GUI (сдвиг ареной). Поиск: скан `u64==8193` + readable ptr, требовать run `count/ptr` stride 0x10. Надёжный beacon (vptr/44100-marker — тупик).
|
||||||
byte-identical to earlier GUI rwin_C0 (0.596 -> 0.126). Confirms registry IS the
|
|
||||||
authoritative per-bin weight source; tables are stable across sessions/SR.
|
|
||||||
- **FREQ-AXIS uses internal SR=48000 regardless of project 44100** (spacing 11.713).
|
|
||||||
This reconciles "rwin tables at 48k" even when rendering 44.1k projects.
|
|
||||||
|
|
||||||
## Files saved (handoff/)
|
## Captured tables (SR=44100 project → internal freq-axis 48000)
|
||||||
- `rtwin_freq_44100.npy` — WIN_freq (8193 f32): 0.5 -> 1.0 (this is live 0x540658 window)
|
|
||||||
- `rtfreqaxis_48000_internal.npy` — freq-axis (2048 f32, 0..23988.3, spacing 11.713)
|
|
||||||
- `rtwa_596.npy` — [03] 0.596->0.126
|
|
||||||
- `rtwb_404.npy` — [04] 0.404->0.874
|
|
||||||
- `rtwc_043.npy` — [05]
|
|
||||||
- `rtwd_956.npy` — [06]
|
|
||||||
- Full raw snapshot: /tmp/snap_all.bin (318MB, entries {lo,sz,bytes}), registry.txt list.
|
|
||||||
|
|
||||||
## Method notes (repro)
|
| idx | count | content |
|
||||||
- rtsnap_fast.py: spawn reaper render_long, find host (soothe2 in maps, not reaper),
|
|-----|-------|---------|
|
||||||
sleep 6s (tables built), pread ALL readable maps chunked 8MB -> snap_all.bin.
|
| [00] | 8193 | identity ~1.0 |
|
||||||
- pread of large anon regions can EIO -> MUST chunk (8MB); whole-region pread loses data.
|
| [01] | 8193 | **WIN_freq 0.5→1.0** (idx1024 0.68, idx2048 0.8) = live `0x540658` |
|
||||||
- Scan ~0.1s for 318MB once chunked; far cheaper than object-base scan.
|
| [03] | 8193 | **0.596→0.126** = `rwin_C0` (byte-identical GUI) |
|
||||||
- Earlier vptr-based (rtobj/rtdump2/rtall) and 44100-marker scans all failed because
|
| [04] | 8193 | 0.404→0.874 = 1−[03] |
|
||||||
the DSP object has NO static vptr match in a fresh render (host dies / fields live
|
| [05] | 8193 | 0.0435→~0 |
|
||||||
only during audio) and ctor field +0x24 != 44100 live. The registry run is the
|
| [06] | 8193 | 0.9565→~1.0 |
|
||||||
reliable beacon.
|
| [0d] | 2049 | **freq-axis 0..23988.3 Hz, 11.713 = 48000/4096** → internal SR 48000 |
|
||||||
|
| others | 8193/16384 | опоры, FFT-планы |
|
||||||
|
|
||||||
## Remaining (for twin IIR attack/release per-bin)
|
**Проверка:** `[03]` head 0.5960761 byte-identical GUI — registry authoritative. **Freq-axis spacing 11.713 ⇒ internal SR 48000** независимо от проекта 44100.
|
||||||
- twin state A/B (342 double per-bin IIR states) still not uniquely located live;
|
|
||||||
short renders keep them ~0. They are NOT the registry tables.
|
|
||||||
- Next: render_long + capture at t=10-20s into sustain, then locate the per-bin
|
|
||||||
attack/release smoothing coefficients (0x540888/88c set, converted via ln(10)/20)
|
|
||||||
inside the arena near registry.
|
|
||||||
|
|
||||||
## 2026-08-19b (two-point snapshots t1=8s, t2=40s of render_long 180s)
|
## Files saved
|
||||||
- rtsnap2.py: TWO snapshots of the SAME 180s render at t1/t2; each 318MB/547regs ~0.2s.
|
|
||||||
- Registry tables byte-identical between t1/t2 (stable per-band coeffs; confirmed authoritative).
|
|
||||||
- Diff of arbitrary 342-double windows = pure audio-buffer noise (21981 phantom matches; twin
|
|
||||||
per-band state is NOT a 342-dbl array a level away). Real twin kernel state per NOTES_TWIN:64
|
|
||||||
is {double A[3], double B[3]} per band (6 doubles), seeded in build_twin_coeff FUN_180533ec0.
|
|
||||||
- Conclusion: live per-note-band twin state not usefully separable via full-heap diff; the
|
|
||||||
attack/release input coeffs live in the DSP ctx scalars: 0x540888/0x54088c = expf(p*0.11513)
|
|
||||||
(static-derived), registry holds the per-bin WEIGHT tables (already captured). Twin kernels
|
|
||||||
themselves validated statically (twin_check max|err|=1.27e-5). => Step C goal (window + axes +
|
|
||||||
weights + kernel parity) is effectively CLOSED; only scalar A/R params remain, derived from RPP
|
|
||||||
params, no live capture needed.
|
|
||||||
|
|
||||||
## 2026-08-20 (P1.5: live level-tracker A[] / ctx — CONFIRMED UNREACHABLE)
|
- `handoff/rtwin_freq_44100.npy` — WIN_freq 8193 f32 (live 0x540658)
|
||||||
- rtctx.py (repo): full pipeline = spawn render_long offline → find yabridge-host → chunked
|
- `handoff/rtfreqaxis_48000_internal.npy` — 2048 f32
|
||||||
snapshot → registry-scan → ctx discovery (marker base+0x24==40000.0f ∧ base+0x540658==window).
|
- `handoff/rtwa_596.npy`, `rtwb_404.npy`, `rtwc_043.npy`, `rtwd_956.npy` — [03]–[06]
|
||||||
- Two FRESH captures (host live mid-render): registry found (0x28b06c0 GUI / 0x29b06c0 offline,
|
- Raw snapshot: `/tmp/snap_all.bin` (318MB, chunked 8MB pread — иначе EIO)
|
||||||
[00].cnt=8193 identity, [01]/[03] = window/weights). But:
|
|
||||||
- the window ptr (0x2962??? live) appears as a heap value ONLY inside the registry entry
|
|
||||||
(0x???6c8 self-field), never as a field of a larger ctx object;
|
|
||||||
- bases where +0x24==0x472c4400 (40000.0f) exist only in the plugin image (.data/rodata
|
|
||||||
0x180535d62/0x1805607e5/0x181414341) — inline code constants, no live DSP object.
|
|
||||||
- => A standalone DSP ctx object with {+0x24==40000, +0x540658==winptr} does NOT exist in the
|
|
||||||
heap (the DSP object is the registry itself / its buffers are the registry targets).
|
|
||||||
- CONFIRMS the 2026-08-19b conclusion: per-bin level-tracker A[] (0x4c0528/0x3c0510/0x2c04f8)
|
|
||||||
and mask scalars are NOT live-separable with the current registry/ctx tools; they are
|
|
||||||
STATIC/DERIVED from RPP params (0x540888/88c = 10^(att/20), 0x540870 = expf((p·c4348+c44a4)·0.11513),
|
|
||||||
0x54087c = raw band/mix). Two unknown constants remain from the (lost) binary: 0x24c4348, 0x24c44a4.
|
|
||||||
- => P1.5 "live capture" is a dead end; scalars must be derived statically or the two missing
|
|
||||||
constants recovered from the original soothing_mem.bin (not currently present in workspace).
|
|
||||||
|
|
||||||
## 2026-08-20c (BREAKTHROUGH: LEVEL-PATH OBJECT captured live in /tmp/snap_rt.bin)
|
## Method (repro)
|
||||||
|
|
||||||
The BandConfig A/B/gamma (roadmap gap 2, block of F2/F3) is now LIVE-CAPTURED.
|
- `rtsnap_fast.py`: spawn `reaper -renderproject render_long`, find host `soothe2` in maps, sleep 6s, chunked pread ALL → `snap_all.bin`. Scan 0.1s/318MB.
|
||||||
Read-only scan of the existing realtime snapshot `/tmp/snap_rt.bin` (ctx 0x2370040,
|
- `rtsnap2.py`: two-point diff — registry byte-identical (stable).
|
||||||
render_long.rpp) — no new capture needed.
|
- Live ctx `0x2370040` (`+0x24==48000`, `sens>100`) — единственный populated, остальные empty. `handoff/rtctx_live.json`.
|
||||||
|
|
||||||
### Method (repro, ~2s)
|
## Live ctx (realtime playback, `rtctx_live.json`)
|
||||||
1. Level-path fingerprint = per-band **level_gain pair buffer**: 0x400 f32 pairs
|
|
||||||
(`[level, gain]`), with `level[j] == j/1024` exactly (level[0]==0.0, step 1/1024).
|
|
||||||
Vectorized scan (2nd derivative of level slots == 0 + level[0]==0.0) finds them.
|
|
||||||
2. Six such buffers at stride 0x2020..0x2040 (band0: 0x4083020, b1: 0x4085040,
|
|
||||||
b2: 0x4087080, b3: 0x40890a0, b4: 0x408b0e0, b5: 0x408d100).
|
|
||||||
3. Find u64 refs to the six → consecutive slots stride 0x18 at **+0xe0+band*0x18**
|
|
||||||
→ object base = **0x3975460** (level-path object).
|
|
||||||
|
|
||||||
### Level-path object (base 0x3975460, region unknown / heap)
|
- Pointers `0x540688` identity, `0x540698` window, `0x5406b8/c8/d8/e8` WA/B/C/D, `0x540748` warp, `0x540758` freqaxis (`v85=995.6 Hz`).
|
||||||
- `+0x178` = band-list ptr → 0x32c0c60
|
- Scalars: `0x540870=440.955`, `0x540880=25.0`, `0x540884=10.0`, `0x540888/88c=1.0`, `0x54087c=1.0/10.0` (selectivity/sharpness).
|
||||||
- `+0x180` → BandConfig 0x32c0aa0: **A=-24.0, B=+28.0, gamma@0xc=1.0, byte flag@0x10=0** → linear, no callback@0x90
|
- `acc/f6f8` arrays **zero** в стационаре playback — combine idle.
|
||||||
- `+0x188` → BandConfig 0x32c09c8: A=16.0, B=20000.0, gamma@0xc=1.0, flag=0 (freq-range shaped cfg; +0x18.. floats 0.55,7.13,2.77,2.718 = nonlinear shaper consts)
|
- Bands curve `R=1/mask` peak следует fc (bin43@500 → bin85@1000), `0x5407f8` min 1.0. Применённый фильтр ≠ pointwise копия R (нужен FFT-conv).
|
||||||
- `+0x4198 + band*0x2000` = **band mask doubles**, 512 usable per band:
|
|
||||||
band0 ~1.0 const; band1 1.001→1.216 (rising); band2 0.999→0.579 (falling);
|
|
||||||
band3 1.291→1.002 (falling); band4/5 1.0→~0.983
|
|
||||||
- `+0xe0+band*0x18` → per-band level_gain pair buffers (live LUT output already has
|
|
||||||
gains: b0 0.53123 const, b1 0.5314→0.535, b2 const, b3 0.598→, b4 const, ...; many
|
|
||||||
bands `~0.531` because mask≈1.0 & render_long default cfg)
|
|
||||||
|
|
||||||
### Interpretation / next
|
Дальше — `handoff/NOTES_LEVEL.md:24mm11+` (live ptrace) + `handoff/BLOCKMAP_529fe0.md`.
|
||||||
- The captured A/B/gamma are the **default render_long config** (A/B semantics =
|
|
||||||
level-scaler LUT min/max; rendering default band). To get the A/B/gamma of a SPECIFIC
|
|
||||||
band shape (t1kq_only1_1000 etc.) re-run rtctx_rt.py with that test RPP and re-scan
|
|
||||||
the same fingerprint (base offset shifts). Method is now automated.
|
|
||||||
- This UNBLOCKS the parametric band-LUT 0x563440/0x563a60 as a structural source
|
|
||||||
(t=(x−A)/(B−A), clamped, ^gamma, ×norm) instead of fitted Pchip.
|
|
||||||
- Dump helper: /tmp/dump_levelpath.py, /tmp/probe_base.py.
|
|
||||||
The dead end above was wrong — the missing piece was REALTIME audio playback, not more scanning.
|
|
||||||
`-renderproject` uses the OFFLINE audio engine (fields live "only during audio", per earlier note);
|
|
||||||
the ctx object only materializes during a realtime transport play.
|
|
||||||
|
|
||||||
### Method (works)
|
|
||||||
- play.lua (repo): `reaper.Main_OnCommand(1007)` (Transport:Play) + hold ~300s.
|
|
||||||
- rtctx_rt.py (repo): `reaper render_long.rpp play.lua` → find yabridge-host → chunked snapshot
|
|
||||||
(~796MB, 1056 regs) DURING playback → scan heap for the ctx.
|
|
||||||
- ctx marker that WORKS live: **+0x24 == 48000.0f (0x473b8000)**, NOT 40000.0f (40000 was the
|
|
||||||
static ctor rodata value; live it is the internal SR = 48000, confirming NOTES_CAPTURE SR=48000).
|
|
||||||
|
|
||||||
### Result (captured live, saved handoff/rtctx_live.json)
|
|
||||||
- **ctx = 0x2370040** (region 0x2022000). Field pointers (all point at registry tables):
|
|
||||||
0x540688=identity([00] 0x2962580), 0x540698=window([01] 0x14b4240), 0x5406a8=levels([02]),
|
|
||||||
0x5406b8=WA([03]), 0x5406c8=WB([04]), 0x5406d8=WC([05]), 0x5406e8=WD([06]),
|
|
||||||
0x540748=warp([12] 0x14bc280), 0x540768=LUT-knee([14]). (NOTE: offsets +0x40 from the
|
|
||||||
earlier static table — window is 0x540698 live, not 0x540658 as in the f_52b570 disasm label.)
|
|
||||||
- **Mask scalars (float)**: 0x540870=440.955 (sens), 0x540874=1.0, 0x540878=1.0, 0x54087c=1.0,
|
|
||||||
0x540880=25.0, 0x540884=10.0, 0x540888=1.0 (attack=10^0), 0x54088c=1.0 (release=10^0),
|
|
||||||
0x540890=0, 0x540894=1200.0.
|
|
||||||
- **level-tracker A[] (341 double each, per-bin IIR attack/release coeffs)**:
|
|
||||||
- 0x4c0528 (attack): 0 → 0.340, 0.348, ... monotonic rising, plateau 0.6921 @bin>=319.
|
|
||||||
- 0x3c0510 == 0x2c04f8 (release): 0 → 0.000753, 0.000885, ... slow small rise.
|
|
||||||
Full arrays in handoff/rtctx_live.json (keys A_4c0528, A_3c0510, A_2c04f8).
|
|
||||||
- BandConfig @ctx+0x188 is NOT populated here (zeros) — it lives at a different offset or only
|
|
||||||
during band processing; still TBD (but LUT curve params A/B/γ are RPP-derived per roadmap).
|
|
||||||
- Two more ctx-like bases found (0x2120040, 0x1780040) also have +0x24==48000; 0x2370040 is the
|
|
||||||
populated one (0x2120040 has 0x540658..698 = 1.0 fill pattern — likely a second/free instance).
|
|
||||||
|
|
||||||
## 2026-08-23 (22y): LIVE CTX CAPTURE DURING DUAL/T1KQ/RES PLAYBACK — scripts/dualtrace.py
|
|
||||||
Method works reproducibly: reaper <cfg>.rpp play_loop.lua (repeat ON) → yabridge-host
|
|
||||||
→ chunked snapshot ×2 → ctx marker +0x24==48000 ∧ sens>100 → ONLY ONE populated base
|
|
||||||
(0x2370040; others empty instances). Snapshots byte-stable over seconds.
|
|
||||||
|
|
||||||
### Pointer-table catalog (ctx+0x540600..0x540a00, dereferenced u64 → f32[2049])
|
|
||||||
- Static weights confirmed live: [00]identity@0x540688(ones), window@0x540698(0.5→0.8),
|
|
||||||
WA/WB/WC/WD @0x5406b8/c8/d8/e8, warp@0x540748(1.3→6.68, structure at LOW bins),
|
|
||||||
freqaxis@0x540758(v85=995.6Hz ✓ internal 48k/4096).
|
|
||||||
- **acc/f6f8 arrays ALL ZERO during steady looped playback** (0x5406f8,
|
|
||||||
0x5407a8/b8/c8/d8 — zero as f32 AND f64): combine accumulators idle in steady state.
|
|
||||||
- **CONFIG-DEPENDENT CURVE FAMILY** (peak follows band fc: bin43@fc500 → bin85@fc1000):
|
|
||||||
- 0x540768 == 0x540778 (identical twins): smooth curve, peak at center
|
|
||||||
(dual: 4.15@43, valley 1.60@171, upturn 1.86@400; t1kq: 3.55@85).
|
|
||||||
- 0x540788: sharper version (floor ~0.52-1.0, max 4.38).
|
|
||||||
- **0x5407f8: min EXACTLY 1.0 → reduction multiplier R(f)=1/mask ≥ 1**
|
|
||||||
(res500 cfg: R(500Hz)=12.0 dB, falls to ~0 by 6 kHz; notch-shaped ✓).
|
|
||||||
- bands[] slots from static asm (@0x540678+i·16) read as identity/ones tables LIVE —
|
|
||||||
the per-band working data is NOT sitting in those ctx fields during playback.
|
|
||||||
|
|
||||||
### Decisive mismatch
|
|
||||||
For dual cfg: R(43)=3.98→12.0 dB (real 10.32 ok-ish) BUT R(171)=1.41→3.0 dB while
|
|
||||||
real cut@2000 = 11.82 dB. ⇒ Applied filter ≠ pointwise copy of R: massive spectral
|
|
||||||
coupling between template and actual filtering. Prime suspect: FFT-conv stage with
|
|
||||||
the 8193-wide WIN_freq window ([01]) — smearing/spreading step completely absent in
|
|
||||||
our pointwise render48k path. This ALSO explains why faithful v1 (pointwise) cannot
|
|
||||||
balance dual tones regardless of law constants.
|
|
||||||
|
|
||||||
### Caveats / next
|
|
||||||
- Quick Welch TF estimate unreliable (window/alignment) — Goertzel-at-tones stays canon;
|
|
||||||
for full-spectrum truth use chirp/two-tone refs or per-fc capture sweep.
|
|
||||||
- NEXT: (1) fc-scan captures (res_only1_{fc}.rpp, 11×) → correlate R_cap(bin85) with
|
|
||||||
real cut@1000 across fc — validates R as THE applied curve; (2) decode the FFT-conv
|
|
||||||
0x535a70 body + WIN_freq usage — reconstruct mask→FIR spreading; (3) re-check whether
|
|
||||||
0x540768-family updates frame-by-frame (two-point diff showed stable — maybe only
|
|
||||||
rebuilt on param change / note onset).
|
|
||||||
|
|||||||
+485
-3921
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
|||||||
|
# NOTES_LEVEL — оглавление журнала
|
||||||
|
|
||||||
|
> **Навигатор:** `NOTES_LEVEL.md` — живая голова (25a+, 2026-09-02, ~470 строк). Архив 2026-08-18—2026-08-23 → [`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`](archive/NOTES_LEVEL_2026-08-18_2026-08-23.md) (4024 строки). Статус TOTAL → [`README.md:13`](../README.md).
|
||||||
|
|
||||||
|
## Живая голова (`handoff/NOTES_LEVEL.md`)
|
||||||
|
|
||||||
|
| Дата | Заголовок | Суть |
|
||||||
|
|------|-----------|------|
|
||||||
|
| 24mm5–24mm7 | Полная буферная карта, DESIGN 1802a24c0 | Два круга log→exp, bidir×2 в лог-домене |
|
||||||
|
| 24mm8-бис | Точка входа следующего раунда | opB 18004ca80, поиск γ |
|
||||||
|
| 24mm9 | FIR-цепь MIN-PHASE, 0.0065 dB | Цепь 52b60c+, RFFT близнецы, df0 complex-mul, валидация |
|
||||||
|
| 24mm10 | EXP ядро полностью, парадокс q | Формула 1803831c0, нормировки s_f/s_i=1 |
|
||||||
|
| 24mm10-бис | Итог близнецов radix-4 | INV/FWD 2048, твидлы 548, без масштабов |
|
||||||
|
| 24mm11 | LIVE ptrace, цепь до df0 бит-точна | wine_ptrace_trace, Y_model=1.0, df0 complex-mul |
|
||||||
|
| 24mm12 | Детекторный каскад vt+0x28=180529c60 | Оркестратор 5300f0, vtable карта, 0x281 байт |
|
||||||
|
| 24mm13 (+доп) | Хелперы каскада 529c60 | 5355d0→16140, 530080, 20f0/1850/1a00, рекуррентия Haar |
|
||||||
|
| 24mm14 | Каскад декодирован — 3 фазы | `|z|` → Haar×2 → peak/sin/w/blend 5407a8 |
|
||||||
|
| 25a (2026-09-02) | chain_9_19 staged, IIR4-генератор, k-mapping dedup, аудит | IIR4 double (movsd), generate_iir4_coefs, RT_CASC/RT_IIR4_GEN gates, baseline 2.689 |
|
||||||
|
|
||||||
|
## Архив (`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`) — по периодам
|
||||||
|
|
||||||
|
| Период | Ключевые темы | Где |
|
||||||
|
|--------|---------------|-----|
|
||||||
|
| 2026-08-18 | Две twin-цепочки, FUN_180563440/563ce0, 530d30 веса, FUN_180529fe0 consumer, 0x530d30 NEGATIVE, bridge B.12 | строки 1–300 |
|
||||||
|
| 2026-08-18h2–h3 | Runtime capture, поле 0x540658, registry 0x28b06c0/0x29b06c0, window 0.5→0.8, freq-axis 48000 | 475–580 |
|
||||||
|
| 2026-08-19 | LUT-leg joint fit, level-tracker, decomp lock, registry heartbeat, P4 mask-chain, SR mismatch | 580–900 |
|
||||||
|
| 2026-08-20i–x | P2 kernels 8d60/3c40, twin-mask, F0 gate determinism, parametric LUT benches, wrappers | 900–1100 |
|
||||||
|
| 2026-08-21b–d | IIR3 bidir, combine scope, structural corpus 2.286, reduction-law affine, Phase A | 1100–1360 |
|
||||||
|
| 2026-08-22a–f | Phase B refuted, Step7 GUI-only, param bridge, floor −20.72 blend·ln10/20 | 1360–1600 |
|
||||||
|
| 2026-08-22g–p | NO-LUT, white-noise probe, bare chain, pool/scale/floor, joint-fit, dual diag, affine port | 1600–1970 |
|
||||||
|
| 2026-08-23a–z | Скалярный закон насыщен, 529fe0 по raw asm, faithful-цепь, теорема α, спрединг отвергнут | 1970–2300 |
|
||||||
|
| 24j–24ab | Применение декодировано: A=1.019·V^1.8345, FIR=exp(scratch), двухстадийность γ₀=1.79 | 2750–3550 |
|
||||||
|
| 24bb–24kk3 | k-маппинг, q-независимость, далёкий тон локально, дистанционная серия, симулятор не замкнулся | 3550–4000 |
|
||||||
|
| 24ll–24mm4 | multi6, g_k пер-пиковый, archive закрытие, γ=1.760561, точ. законы отвергнуты | 4000–4024 |
|
||||||
|
|
||||||
|
## Как читать
|
||||||
|
|
||||||
|
1. **Старт:** `README.md:13` (статус) → `AGENTS.md` (runbook) → `BITEXACT_PLAN.md:1` (3 шага) → этот индекс → `handoff/BLOCKMAP_529fe0.md`.
|
||||||
|
2. **Детали по теме:** `rg -n "24mm9\|Q-НЕЗАВИСИМОСТЬ\|Haar"` в соответствующем файле.
|
||||||
|
3. **Полный поиск:** `rg -n "cut_D\|lvl_raw\|5407c8" handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md handoff/NOTES_LEVEL.md`.
|
||||||
|
|
||||||
|
## Дубли устранены
|
||||||
|
|
||||||
|
- `UPDATE 2026-08-22g` дубль (строки 1577/1591 в архиве) — логически один.
|
||||||
|
- `UPDATE 2026-08-20s` дубль (974/977) — один.
|
||||||
|
- Заголовки унифицированы: `## ============ UPDATE` → `##` в живой голове; архив сохранён как есть для истории.
|
||||||
+23
-70
@@ -1,76 +1,29 @@
|
|||||||
# Twin FUN_180535880 transcription (Phase 2)
|
# Twin FUN_180535880 — краткая справка (детали → `dsp/twin.cpp`)
|
||||||
|
|
||||||
## PHASE-3 CALLER + GRID (2026-08-18)
|
> **Статус:** транскрибирован, gate PASS `dsp/build/twin_check` max rel err 1.27e-5 (float-parity). Полный журнал 2026-08-18 → [`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`](archive/NOTES_LEVEL_2026-08-18_2026-08-23.md) + [`handoff/NOTES_LEVEL_INDEX.md`](NOTES_LEVEL_INDEX.md).
|
||||||
- Caller FUN_180536300 (decomp 6218 / sp_180536300.txt) decoded at buffer level:
|
|
||||||
- params: param_1=top DSP, param_2=out, param_3=?, param_4=band obj, param_5=cell count,
|
|
||||||
param_7=scratch (>=52*N bytes complex).
|
|
||||||
- scratch: +0..8N A-accum, +8N..16N B-accum, +16N..24N z-powers, +24N..32N rotor-in
|
|
||||||
(fill 1.0), +32N..36N phase ramp lVar1, +36N..44N rotor-out z1 (r14), +44N..52N
|
|
||||||
lVar3(1.0)/lVar4(0.0) for interleave+negate+exp build.
|
|
||||||
- 0x18052da00(lVar1, scale=2pi/(iVar7*sr), N) = ramp fill (float, via 0x180001a00);
|
|
||||||
- twin always = FUN_180535880 (type-info compare of same addr -> equal);
|
|
||||||
sibling FUN_180536f90 is dead.
|
|
||||||
- 0x18052dbc0(param_2, lVar4, N) = cplx interleave -> out=(cos,sin)=e^{+i theta}.
|
|
||||||
- GRID LAW (verified from trace.txt case8 + constructor):
|
|
||||||
- os = *(int*)(param_1+0x240080) = 4 (written 0x18052ce3e `mov ...,0x4`)
|
|
||||||
- sr = *(float*)(param_1+0x24) = 44100.0
|
|
||||||
- fs_total (generator) = os*sr = 176400 => resonance shape f/fc invariant to fs.
|
|
||||||
- per-cell phase step = 2*pi/(os*sr); cell k <-> freq = k/os Hz (0.25 Hz steps).
|
|
||||||
- Phase-1 model FS=44100 was a valid normalization (res(f) depends only on f/fc).
|
|
||||||
- case8 generator call (trace 33361..33490, switchD_1805318ab):
|
|
||||||
- +0x814 numBands = 1, +0x103 mode flag = 1
|
|
||||||
- param_5 (float, xmm4) = 10^(sens_dB/20) (pow10 10.0^([band+0x101]/20.0))
|
|
||||||
- gain = sqrtf(param_5) = 10^(sens_dB/40); model fit gain=4.132 => sens_dB@0x101 ~= 24.65
|
|
||||||
- freq = [band+0x804], Q = [band+0x80c], fs_total = os*sr (w0 = max(fc,2)*2pi/fs_total)
|
|
||||||
|
|
||||||
## PHASE-2 GATE: PASSED (2026-08-18)
|
## PHASE-3 Caller + Grid
|
||||||
- `dsp/twin.hpp` + `dsp/twin.cpp` written: build_twin_coeff (FUN_180533ec0 double pipeline
|
|
||||||
-> cvtpd2ps), cplx_div_exact (0x181a77520: mulps |A|^2, rcpps + Newton, NaN guard),
|
|
||||||
cplx_mul_exact (0x18000ad60 scalar body @0x18000ae00: vfmaddsub213ps form),
|
|
||||||
twin_apply (seed A0/B0 + 2 vfmadd213ss Horner stages + 2*div).
|
|
||||||
- twin_check gate: fc-scan grid (7 fc, Q=0.9999978, gain=4.132, tone 1000 Hz)
|
|
||||||
max |rel err| = 1.27e-5 vs double reference res_at (float rcpps parity).
|
|
||||||
DUAL sweep (Q=0.1..10 x tones 500/2000, fc=500) max = 1.23e-5. Both PASS.
|
|
||||||
- Constants locked: A1 = B1 = cos(w0)*-2.0, z1 = conj(exp(+i*theta)),
|
|
||||||
z2 = cplx_mul(z1,z1), out = 2*B/A.
|
|
||||||
|
|
||||||
## Confirmed scalar math (from generator + cplx-div kernels)
|
- Caller `FUN_180536300` (6218): scratch 52·N, `0x18052da00` ramp `2π/(os·sr)`, `0x18052dbc0` interleave `e^{+iθ}`. Twin всегда `180535880`, sibling `180536f90` dead.
|
||||||
- Generator FUN_180533ec0 (per band, case8):
|
- Grid: `os=4` (`+0x240080`), `sr=44100` (`+0x24`), `fs_total=176400`, `phase=2π/176400`, cell `k ↔ k/4 Hz` (0.25 Hz).
|
||||||
`fVar1 = sqrtf(param_5)` (param_5 = pow(10, sens/20) via cd6 IAT stub)
|
- case8: `+0x814=1`, `+0x103=1`, `param_5=10^(sens/20)`, `gain=√param_5=10^(sens/40)` → fit 4.132 ⇒ `sens≈24.65 dB`.
|
||||||
`w0 = max(2.0, freq)*2*pi / fs_total` (fs_total = 2*FS frame?)
|
|
||||||
`p = sin(w0)*0.5 / Q`
|
|
||||||
`dVar5 = p*fVar1` (A tap), `param_4 = p/fVar1` (B tap)
|
|
||||||
A = [1+dVar5, dVar2, 1-dVar5], B = [1+param_4, dVar2, 1-param_4]
|
|
||||||
where dVar2 = cos(w0) * DAT_1824c46b0 (=-2.0? VERIFY)
|
|
||||||
- Twin FUN_180535880: for each FFT bin z (rotor):
|
|
||||||
`res(z) = 2 * B(z) / A(z)`, B(z)=B0+B1*z+B2*z^2, A(z)=A0+A1*z+A2*z^2
|
|
||||||
implemented as complex FMA accumulation over 3 taps:
|
|
||||||
r15 (A-accum) and r12 (B-accum) via 0x180001fd0 vfmadd213ss (coeff [rsp+k*4+0x28]=A_k, [+0x38]=B_k)
|
|
||||||
rbp buffer = z powers via rotor ops (0x1800019a0 fill / 0x1800021e0 / 0x1800018b0 / 0x180001af0 / 0x180001dc0)
|
|
||||||
final 0x180001e20 -> 0x181a775a0 cplx-div: out = 2*B/A (xmm7=0x40000000=2.0, xmm6=NaN guard)
|
|
||||||
- cplx-div kernel 0x181a77520: |A|^2 via shufps 0x88/0xdd, rcpps + 1 Newton step (xmm7=2.0), NaN guard via cmpeqps.
|
|
||||||
Matches rotor_kernel.hpp cplx-div exactly.
|
|
||||||
|
|
||||||
## Dispatch (all stubs share global 0x1826159a0 = 4 = AVX2)
|
## PHASE-2 Gate
|
||||||
0x180001d00 -> 0x180009860 -> 0x180040d40 (cplx fill const, NT stores)
|
|
||||||
0x1800019a0 -> 0x180004200 (float fill -> 0x181a63fe0)
|
|
||||||
0x1800021e0 -> 0x1800130e0 (large AVX, 0x450 stack, rotor/exp?)
|
|
||||||
0x1800018b0 -> 0x180003040 (-> 0x181a557c0)
|
|
||||||
0x180001af0 -> 0x180005a20 (loop -> 0x18001a5a0)
|
|
||||||
0x180001dc0 -> 0x18000ad60 (cplx op r14*rbp)
|
|
||||||
0x180001e20 -> 0x18000b820 (-> 0x181a775a0 cplx-div 2B/A)
|
|
||||||
0x180001fd0 -> 0x18000f620 (vfmadd213ss: out = src*coeff + dst)
|
|
||||||
|
|
||||||
## Coefficient layout in twin (param_3 = {double A[3], double B[3]})
|
- `dsp/twin.cpp`: `build_twin_coeff` (FUN_180533ec0), `cplx_div_exact` (rcpps+Newton), `cplx_mul_exact` (vfmaddsub), `twin_apply` `2·B/A`.
|
||||||
stack: +0x28=A0 +0x2c=A1 +0x30=A2 | +0x38=B0 +0x3c=B1 +0x40=B2
|
- Gate: fc-scan 7fc + dual Q0.1..10 PASS.
|
||||||
loop k=0..2 uses A_k via [rsp+k*4+0x28], B_k via [rsp+k*4+0x38]
|
|
||||||
r15 = out A-accum, r12 = out B-accum, rbp = z^1..z^2 buffer, r14 = rotor(z) input
|
|
||||||
|
|
||||||
## PHASE-1 LOCK (empirical, 36 points, LUT chain B.11)
|
## Scalar math
|
||||||
Effective sqrtf(param_5) needed ~= 4.13 (NOT 1.995).
|
|
||||||
=> param_5 ~= 17.07 = 10^(1.2325) => internal sens_stored ~= 24.65 dB (not XML 12.0)
|
- Generator: `fVar1=√param_5`, `w0=max(2,freq)·2π/fs_total`, `p=sin(w0)·0.5/Q`, `A=[1+p·fVar1, −2cos w0, 1−p·fVar1]`, `B=[1+p/fVar1, −2cos w0, 1−p/fVar1]`.
|
||||||
OR equivalent. RULED OUT: sqrt(10^(12/20))=1.995 (rmse 1.38/5.68);
|
- Twin: `res(z)=2·B(z)/A(z)`, Horner `0x180001fd0`, `cplx-div 0x181a77520`, `z2=z1·z1`.
|
||||||
10^(12/20)=3.981 direct (t1kq 0.056 but t1k 0.934); |2B/A|^2 (1.5/6.6).
|
|
||||||
Sens setters (0x18053795d, 0x18056f002) both store RAW value; +0x808 scaling is HOST-side
|
## Dispatch
|
||||||
(plugin controller, outside dump) => must be resolved via bit-exact render or host trace.
|
|
||||||
NOTE: single-gain compromise 4.132; t1kq wants 4.055, t1k wants 4.137 (tilt residual).
|
Стабы → `0x1826159a0=4` (AVX2). Layout `param_3={A[3],B[3]}` `+0x28/+0x38`, coeff `[rsp+k*4]`.
|
||||||
|
|
||||||
|
## PHASE-1 lock
|
||||||
|
|
||||||
|
`√param_5≈4.13` (не 1.995), `sens≈24.65 dB` (host ×2 над XML 12.0). Setter `+0x808` host-side.
|
||||||
|
|
||||||
|
Детали декомпа — `handoff/nls_dasm/twin*.dis`, `dsp/twin.cpp:27`.
|
||||||
|
|||||||
+6
-254
@@ -1,256 +1,8 @@
|
|||||||
# SESSION HANDOFF — soothe2 detector reverse → bit-exact render
|
# SESSION_HANDOFF — перенесён в архив
|
||||||
|
|
||||||
Prepared: 2026-08-18 (checkpoint end-of-session: commits c8f97e4 + 60bf3a2 pushed). Start: READ THIS FIRST.
|
> **Исторический чекпоинт 2026-08-18 (коммиты c8f97e4+60bf3a2).**
|
||||||
|
> Актуально: декомп DSP-ядра ~95% закрыт, `FUN_180529fe0` mono-path, `FUN_180563440/563ce0/563a60` расшифрованы, дизассемблы в `handoff/nls_dasm/` (134 файла).
|
||||||
|
> Полный текст сохранён: [`handoff/archive/SESSION_HANDOFF_2026-08-18.md`](archive/SESSION_HANDOFF_2026-08-18.md) (256 строк, инвентарь декомпа §0, ключевые адреса §2, Phase-5 план §6).
|
||||||
|
> Навигация → [`handoff/NOTES_LEVEL_INDEX.md`](NOTES_LEVEL_INDEX.md) + [`handoff/BLOCKMAP_529fe0.md`](BLOCKMAP_529fe0.md) + [`README.md:13`](../README.md).
|
||||||
|
|
||||||
> **2026-08-20 UPDATE**: см. актуальный канон — `AGENTS.md` и `handoff/NOTES_LEVEL.md`.
|
Кратко: `decomp_funs.txt` 312K строк, RTTI `rtti_dsp.json`, bridge `C=g·LUT+w·warp^a` (rmse 0.236, позже заменён VLAW). Для продолжения читать `AGENTS.md` → `NOTES_LEVEL_INDEX.md`.
|
||||||
> Нижеследующее «NOT DECODED»/«missing body» УСТАРЕЛО: `FUN_180529fe0` mono-path,
|
|
||||||
> `FUN_180563440/563ce0/563a60` — расшифрованы, полные дизассемблы скопированы в
|
|
||||||
> `handoff/nls_dasm/` (134 файла). Этот файл — исторический чекпоинт.
|
|
||||||
|
|
||||||
## 0. DECOMPILATION INVENTORY (2026-08-19 — what's decoded, where, and what's missing)
|
|
||||||
Goal: bit-exact parity is gated by EXACT tables/window/constants. The chunk-level model hits
|
|
||||||
err ≤0.62 dB (dual) / ≤0.19 dB (al_*) — to go sample-exact we need precise values from the binary.
|
|
||||||
|
|
||||||
### Static decomp assets IN REPO (use these, don't re-decompile):
|
|
||||||
- `ghidra-proj/soothe2.rep` — full Ghidra project (vst3 at ImageBase 0x180000000).
|
|
||||||
- `decomp_funs.txt` (312K lines, ~1640 functions), `fun_map.txt` (2285 addr→FUN), `consts.txt` (11602),
|
|
||||||
`decomp_dsp.txt` / `decomp_vtables.txt` / `decomp_candidates.txt` / `focus_decomp.txt`. Generators:
|
|
||||||
`Dump*.java`, `ImportRtti*.java`, `ListFuns.java`, `SearchRefs.java` (+ headless logs).
|
|
||||||
- RTTI: `rtti_dsp.json` / `rtti_full.json` — class hierarchy
|
|
||||||
`SpectralProcessor<float,7,1>`, `Soothe2ModuleBase<float,1>`, `FilterGraph<float,6,0x400>`,
|
|
||||||
`DigitalFilter<float,0xBA,1>`, `IIRFilterExtended<float,1>`, `AudioProcessingModule<float,1>`.
|
|
||||||
- `handoff/nls_dasm/` — 120 hand-picked `.dis` (twin, iface_18052da00/dbc0, fft, generator, ctor).
|
|
||||||
- `dsp/` — working C++ transcription (twin, detect, freqpath, spectral, fft_stage) + harness;
|
|
||||||
`build/twin_check` passes float-parity gate (§2).
|
|
||||||
|
|
||||||
### KEY DSP ADDRESSES — decoded / not decoded:
|
|
||||||
DECODED (formula-level, notes at NOTES_TWIN.md / NOTES_LEVEL.md):
|
|
||||||
- twin kernel `FUN_180535880`; generator `FUN_180533ec0`; caller `FUN_180536300`; dead sibling `180536f90`.
|
|
||||||
- level-weight formula `FUN_180530d30` (0x540880/884, warp 0x5406a8=0.87·x/(1+x/7.942), w=0.1^(...)).
|
|
||||||
- mask-apply entry `FUN_180529fe0` (accumulator 0x5407c8 += w·res; mask *= warp; FIR *= 0x540658).
|
|
||||||
- FFT-conv loop 0x52b550-0x52b8b5 (plan 0x540530, windows 0x540548/550/598, freq-axis 0x540698=offline const).
|
|
||||||
- level-path map: `0x563440` (LUT curve +0x188, 6 band-slots, combine→+0x2198), `0x56e3e0` (twin-mask factory),
|
|
||||||
`0x563ce0` (IIR level-tracker INIT only).
|
|
||||||
|
|
||||||
NOT DECODED / MISSING FROM decomp_funs.txt (critical): ⚠️ → РЕШЕНО (2026-08-19/20), см. выше
|
|
||||||
- `FUN_180529fe0` body — РЕШЕНО (decomp в `/tmp/consumers_out.txt`, mask-цепь в `dsp/framed_model.cpp`).
|
|
||||||
- `FUN_180563440` / `FUN_180563a60` (LUT curve + gamma + combine), `FUN_180563ce0` — РЕШЕНО,
|
|
||||||
дизассемблы скопированы в `handoff/nls_dasm/` (`f_563440.dis`, `f_563a60.dis`, `f_563ce0.dis`, `f529fe0.dis`).
|
|
||||||
- Window `0x540658` — РЕШЕНО (live-захват, см. `handoff/NOTES_CAPTURE.md`; `rtwin_freq_44100.npy`).
|
|
||||||
- sens source: XML 12.0 → runtime sens_dB≈24.65 (host ×2) not found in dump; `IAT\*0x181bab370` outside dump.
|
|
||||||
|
|
||||||
### Bridge model STATUS (2026-08-19):
|
|
||||||
- `framed_render.py` full STFT frame-render (N=2048, hop=512, sqrt-Hann, twin env tatt=11ms/trel=80ms):
|
|
||||||
`C(f)=G·LUT(xv)+W·warp(f)^A`, G=1.0850, W=0.2819, A=1.1377 (joint dual+al_* refit).
|
|
||||||
- Validation: dual q0.1/1/10 @500+2000 err ≤0.62 dB, envRmse@steady ≤0.78 dB; al_* lv3..24 err ≤0.19 dB.
|
|
||||||
- LUT = slanted al_*-leg (0.366@xv=-0.50 → 0.636@xv=+0.55), replaces flat B.12 (~0.5). Bugfix: clip range
|
|
||||||
must be [LY.min(), LY.max()] not [LY[0], LY[-1]].
|
|
||||||
- Remaining structural residual: −0.6 dB systematic on dual 500Hz (q0.1/q1). Live evidence (avg per-bin
|
|
||||||
gain H=|Y|/|X|): reference mask is FLAT ~-10.2dB across 100-540Hz regardless of Q — model produces
|
|
||||||
res-shaped notch. Hypo tested: freq-smoothing of C(f) fails (kills 2000Hz). NEXT: scalar per-frame xv
|
|
||||||
(broadband level, not per-bin am/res) — see /tmp/smoothtest.py (+ edit scalar=True).
|
|
||||||
|
|
||||||
## 1. Objective (unchanged since session 1)
|
|
||||||
Transcribe the decoded soothe2 detector ("twins" 0x180535880/0x180536f90, 2nd-order resonator) into
|
|
||||||
C++ and reach **bit-exact render parity** with the Reaper reference wavs in `/home/m/soothe-bt/*.wav`
|
|
||||||
(checking dB/magnitude parity first, then byte-diff).
|
|
||||||
|
|
||||||
Path A (bit-exact) chosen. Phase 0,1,2 done. Phase 3,4 advanced. Phase 5 (render diff) is next.
|
|
||||||
|
|
||||||
## 2. DECOMPILATION FINDINGS (2026-08-19 — FUN_180563440, FUN_180563ce0 decoded)
|
|
||||||
|
|
||||||
### FUN_180563440 — LUT curve evaluation + band combine (222 lines disasm)
|
|
||||||
Structure: **3 phases per frame**:
|
|
||||||
1. **1024-bin LUT loop** (0x400 iterations): for each bin k:
|
|
||||||
- x = clamp(k * 0.0009775, 0, 1.0) = k/1024
|
|
||||||
- Band config at +0x188: {A(+0x00), B(+0x04), threshold(+0x0c), flag(+0x10), callback(+0x50)}
|
|
||||||
- **Path 1** (callback exists): vtable call → dynamic LUT
|
|
||||||
- **Path 2** (flag=1, threshold≠1.0): **power-law** → centered = 2*x - 1, then `sign(x) * 10^(log10(|x|) / threshold)` — this is a **compression curve** controlled by sharpness/threshold
|
|
||||||
- **Path 3** (default): **linear interpolation** → `(B - A) * x + A`
|
|
||||||
- Output: double-precision at +0x198, stride 8
|
|
||||||
2. **Twin-mask factory** (FUN_18056e3e0): 6 bands × 1024 bins, stride 0x2000
|
|
||||||
3. **Combine loop**: stereo (max 2 channels), 6 bands, `1 - sum(band_masks)`
|
|
||||||
|
|
||||||
### FUN_180563ce0 — IIR level-tracker INIT (163 lines disasm)
|
|
||||||
- **341 bins** (0x155 iterations), **order-3 IIR** (3 coefficients per bin)
|
|
||||||
- Coefficient: **0.1** (`0x3dcccccd` = IEEE 754 float 0.1)
|
|
||||||
- Initial state: [1.0, 0, 0, 0] and [-1.0, 0, 0, 0] (identity + zero)
|
|
||||||
- Buffer layout: 3 × (16 bytes coeff) per bin, stored at rcx+0x28/+0x40/+0x58
|
|
||||||
- **Not the update loop** — init only; UPDATE is elsewhere
|
|
||||||
|
|
||||||
### FUN_180529fe0 — Coefficient setup (2051 instructions, in decomp_funs.txt)
|
|
||||||
- **vtable method** on Soothe2Module<M,1>
|
|
||||||
- **Lock** at +0x2404dc (atomic test-and-set)
|
|
||||||
- **PRNG state** at +0x2404e0, LCG with offset 0x3cdca
|
|
||||||
- **6-iteration coefficient generation** from 0x5408b0 buffer (LCG-indexed)
|
|
||||||
- **Depth scaling**: `powf(normalized, depth)` at +0x2c
|
|
||||||
- **Mask assembly**: normalize by 0x1a0 (NFFT), × 0x540870 (level weight), × 0x54088c (sharpness), invert
|
|
||||||
- **Copy output** via SIMD memcpy (thunk 0x181ba94b0)
|
|
||||||
|
|
||||||
### FUN_18052e9b0 — SpectralProcessor main (3167 instructions, in decomp_funs.txt)
|
|
||||||
- **Same PRNG + coefficient setup** as FUN_180529fe0
|
|
||||||
- **Band chain**: FUN_18052f500 (interleave) → FUN_18052ee70 (per-bin gain) → FUN_18052d650 (setup) → FUN_18052d920 (window)
|
|
||||||
- **Buffer alloc**: FUN_18052e190 for 0x540668, 0x540698, 0x5406a8, 0x5406b8-e8 (6 bands)
|
|
||||||
- **Depth scaling**: `powf(normalized, depth)` with depth at +0x2c
|
|
||||||
- **Final mask**: `1 - C` (inversion)
|
|
||||||
- **Window application**: `FIR *= 0x540658` (the live-captured window table)
|
|
||||||
|
|
||||||
### Constants verified from binary:
|
|
||||||
| Constant | Address | Value | Meaning |
|
|
||||||
|---|---|---|---|
|
|
||||||
| SCALE | 0x24c3c54 | 0.000977517 | 1/1024 (bin→x) |
|
|
||||||
| ONE | 0x24c3ea4 | 1.0 | clamping max |
|
|
||||||
| TWO | 0x24c41e0 | 2.0 | centering (2*x-1) |
|
|
||||||
| NEG1 | 0x24c4680 | -1.0 | sign flip |
|
|
||||||
| HALF | 0x24c3d8c | 0.5 | threshold |
|
|
||||||
| DEPTH_SCALE | 0x24c4334 | 4.0 | depth range |
|
|
||||||
| DB_CONV | 0x24c43e0 | 8.6859 | 20/ln(10) |
|
|
||||||
| FLOOR | 0x24c4704 | -6.9078 | ln(0.001) |
|
|
||||||
| IIR_COEFF | embedded | 0.1 | attack/release per bin |
|
|
||||||
| IIR_ORDER | embedded | 3.0 | IIR filter order |
|
|
||||||
| LCG_OFFSET | embedded | 0x3cdca | PRNG state advance |
|
|
||||||
|
|
||||||
### Live-captured tables (re-verified):
|
|
||||||
- `rwin_A0.npy` (0x1930100): 0.5→0.8, **frequency window** (NOT warp formula)
|
|
||||||
- `rwin_B0.npy` (0x1938180): 0→3.899, **power-law depth curve** (exponent ~0.66)
|
|
||||||
- `rwin_C0.npy` (0x19401c0): 0.596→0.126, **level-dependent weight**
|
|
||||||
- `r_freqaxis.npy`: 0→23988.3 Hz, 11.71 Hz spacing (48000/4096)
|
|
||||||
|
|
||||||
### Key insight: The warp formula is NOT a table — it's computed at runtime
|
|
||||||
The empirical `0.87*7.942*x/(7.942+x)` is an approximation of a runtime computation
|
|
||||||
in FUN_180563440 that evaluates the LUT curve parametrically. The actual LUT has TWO modes:
|
|
||||||
- **Linear** (default): simple interpolation between A and B
|
|
||||||
- **Power-law** (flag=1): `sign(x) * 10^(log10(|x|) / C)` — compression curve
|
|
||||||
|
|
||||||
## 3. PROOF OF STATE — run this first (5 min)
|
|
||||||
Everything below must reproduce. If `twin_check` fails, the transcription moved stale.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/m/re-tools/dsp && cmake --build build && ./build/twin_check
|
|
||||||
# expect:
|
|
||||||
# max rel err = 1.268e-05, PASS = yes (float-parity)
|
|
||||||
# max sweep rel err = 1.233e-05, SWEEP PASS = yes, exit 0
|
|
||||||
```
|
|
||||||
|
|
||||||
`build/soothe2_dsp.so` and `build/twin_check` are current. CMakeLists already has `twin.cpp` + `twin_check` executable.
|
|
||||||
|
|
||||||
## 3. TRANSCRIPTION STATE (summary — details in NOTES_TWIN.md)
|
|
||||||
- `dsp/twin.hpp` + `dsp/twin.cpp` = twin kernel FUN_180535880:
|
|
||||||
- build_twin_coeff (FUN_180533ec0 double pipeline → cvtpd2ps floats)
|
|
||||||
- generator formula (confirmed): `fVar1=sqrtf(param_5)`; `w0=max(2.0,freq)·2π/fs_total`;
|
|
||||||
`p=(sin(w0)·0.5)/Q`; `A=[1+dVar5, dVar2, 1−dVar5]`, `B=[1+param_4, dVar2, 1−param_4]`,
|
|
||||||
`dVar2=cos(w0)·−2.0` (DAT_1824c46b0=−2.0).
|
|
||||||
- cplx_div_exact (rcpps+Newton: `ref=r0·(2−den·r0)`, NaN-guard cmpeqps+movmskps, numerator B·conj(A))
|
|
||||||
- cplx_mul_exact (vfmaddsub213ps form `re=fma(br,ar,−bi·ai)`, `im=fma(br,ai,bi·ar)`)
|
|
||||||
- twin_apply: conj(z1) mirroring 0x1800018b0, z2=z1·z1, out=2·B/A
|
|
||||||
- **OPEN phase question (byte-exactness)**: conjugate shaping — (-α+π/2) vs (-α) axis. Magnitudes match
|
|
||||||
either way; final proof needed via phase of a single-bin run before claiming byte-exactness.
|
|
||||||
|
|
||||||
## 4. GRID + CALLER (Phase 3 — details in NOTES_TWIN.md §Phase 3)
|
|
||||||
- Caller FUN_180536300 (decomp 6218, `caller_536300.dis`), scratch ≥52·N complex bytes:
|
|
||||||
+0..8N A-accum, +8N..16N B-accum, +16N..24N z-powers, +24N..32N rotor-in (filled 1.0),
|
|
||||||
+32N..36N phase ramp lVar1, +36N..44N rotor-out z1, +44N..52N lVar3(1.0)/lVar4(0.0)
|
|
||||||
- `0x18052da00(lVar1, scale=2π/(os·sr), N)` = ramp fill; `0x18052dbc0(param_2, lVar4, N)` cplx-interleave
|
|
||||||
→ out slots get (cos,sin) = e^{+iθ}.
|
|
||||||
- **Grid law**: `os=*(int*)(cfg+0x240080)=4` (mov DWORD PTR [rbx+0x240080],0x4 @0x18052ce3e),
|
|
||||||
`sr=*(float*)(cfg+0x24)=44100.0` → generator `fs_total=os·sr=176400`; per-cell phase `2π/176400`;
|
|
||||||
cell k ↔ freq k/os Hz (0.25 Hz steps). Phase-1 FS=44100 was a valid normalization (res depends on f/fc).
|
|
||||||
- twin always = FUN_180535880 (type-info compare equal); sibling FUN_180536f90 dead.
|
|
||||||
- case8: numBands `+0x814`=1, `+0x103`=1; `param_5=10^(sens_dB/20)`, `gain=sqrtf(param_5)=10^(sens_dB/40)`
|
|
||||||
→ fit gain=4.132 means stored `sens_dB@+0x101 ≈ 24.65`; freq=[band+0x804]=1000.0, Q=[band+0x80c].
|
|
||||||
|
|
||||||
## 5. LEVEL-WEIGHT FORMULA (Phase 4 — DECODED, NEGATIVE RESULT — NOTES_LEVEL.md §UPDATE 2026-08-18)
|
|
||||||
Exact formula from decomp 22330 (FUN_180530d30):
|
|
||||||
- `base=(2000/(sr·0.5))·iVar5/(bin+1)`, iVar5=NFFT/2+1
|
|
||||||
- `w8=powf(base,0.25)`
|
|
||||||
- `v=[0x540880]·0.25·w8·fVar12` (fVar12=4.0 if offline flag [0x5408b8], else 1.0)
|
|
||||||
- `q=1/(1+v/([0x540880]·4))` (2nd pair 0x540884: q2=1/(1+v2/[0x540884]))
|
|
||||||
- `dVar1=(sr/[0x1a0])·[0x1ac]·0.001`
|
|
||||||
- `w=0.1^(1/(max(q·v,floor)·dVar1))`; bufs: 0x5406b8=w, 0x5406c8=1−w (and 0x5406d8/6e8 w/ 0x540884)
|
|
||||||
- consts: c3d3c=0.25, c4334=4.0, c3d8c=0.5, c45b4=2000.0, c3f70=0.1 f32, c3e30=0.001 f64, c3f70=0.1
|
|
||||||
- **Numerically**: w(500)=w(1000)=w(2000)≈0 (1e-20..1e-0.07), complements≈1 → but the weights are STILL
|
|
||||||
APPLIED (as 1−w ≈ 1) by FUN_180529fe0 (`0x5407c8[band] += 0x5406c8·res_upper + 0x5406e8·res_lower`,
|
|
||||||
thunk 003c40, kernel 0x1800752e0 = vfmadd213pd) — the form is weight·resonance, not dead code.
|
|
||||||
- **TILT analysis (2026-08-18, checkpoint NEGATIVE result)**: warp `0x5406a8 = 0.87·x/(1+x/K)`,
|
|
||||||
`K=exp(2.0723267)≈7.942`, `x=f/2000`, applied by FUN_180529fe0 as `mask *= 0x5406a8` (float, thunk 008700)
|
|
||||||
BEFORE the FFT-conv. warp: 500→0.211, 1000→0.409, 2000→0.773 (lin ratio 2000/500=3.67).
|
|
||||||
**BUT warp ≠ empirical tilt (ratio 1.27)**: `C(dB2000)=depth·warp(2000)·LUT_max = 0.864·0.772·0.667=0.445`,
|
|
||||||
yet red2000=15.2 dB needs `C=0.826` ⇒ warp alone CANNOT produce deep-2000 (dual_b1q). Empirical tilt is the
|
|
||||||
envelope of the BLEND term (0x5407c8 += weights·res + freq-axis 0x540698) + FFT-conv shaping, NOT warp —
|
|
||||||
simple tilt→warp substitution in model_lut.py is INVALID. Nothing else decoded yet closes dual_b1q.
|
|
||||||
- **FUN_180563ce0 decoded (2026-08-18)**: INIT of per-bin IIR level-trackers (0x156=342 bins, order=3.0f
|
|
||||||
0x40400000, coeff A/B from 0x24c4780/4790, strided fill; third pass +0xffd0 fills 0x40e00000=7.0f).
|
|
||||||
Not the update loop. Full disasm: `/tmp/opencode/f_563ce0.dis`.
|
|
||||||
- **Level-path map (2026-08-18, NOTES_LEVEL.md)**: 0x563440 (LUT curve +0x188, gamma, 6 band-slots, combine
|
|
||||||
→+0x2198, LUT pairs +0x98 at 0x3ff pts) + 0x56e3e0 twin-mask factory (N=1024, DOUBLE, LUT-ramp input) +
|
|
||||||
0x563ce0 IIR init. 0x563440 NOT in decomp_funs.txt (search via /tmp/opencode/f_563440.dis + level_notes.md).
|
|
||||||
- **DIAGNOSTIC BRIDGE DONE (2026-08-18) — model_fir.py**: реальная цепочка из FUN_180529fe0
|
|
||||||
воспроизводит 36 точек до rmse=0.236 dB БЕЗ эмпирического tilt:
|
|
||||||
`C(f) = g·LUT(log10(L0/res(f))) + w·warp(f)^a`, g=1.221, w=0.358, a=3.143.
|
|
||||||
- dual500-константа = res_band(500;fc=500)=0.117 Q-НЕЗАВИСИМ, не tilt;
|
|
||||||
- dual2000-глубина = АДДИТИВНЫЙ терм `w·warp^3.14` (на 1000 Гц вклад ≤0.021 — иначе t1k
|
|
||||||
рушится; на 2000 = 0.149) — это аккумулятор `0x5407c8 += weights·res + mask` (шаг 5),
|
|
||||||
НЕ мультипликация warp·LUT (та проваливается >10 dB);
|
|
||||||
- warp^~3.14 ≈ π — подозрение на кратный каскад (0x540698 freq-axis, ∏0x540688, двойной FFT);
|
|
||||||
- остаток 0.7 dB при Q=0.1 = форма LUT-колена 0.574, не закрывается per-bin → нужен FFT-уровень.
|
|
||||||
- **FFT-CONV FULLY MAPPED (2026-08-18, NOTES_LEVEL.md §2026-08-18c, /tmp/opencode/f_52b570.dis)**:
|
|
||||||
0x535a70 = dispatcher to base-CRT FFT (0x140a10/0x140a70); 0x540548/0x540550/0x540598 = FFT-plan
|
|
||||||
tables (plan 0x540530, built by 0x52dc30), NOT windows. Per-band loop 0x52b550-0x52b8b5:
|
|
||||||
mask→fwdFFT→invFFT→fill low/high (xmm13/xmm9 consts)→inv→cplx-op→fwd→`FIR *= 0x540658`→fill→inv→
|
|
||||||
`FIR[0]=1,FIR[1]=0`→`*=0x540888`→copy out.
|
|
||||||
**freq-axis 0x540698**: OFFLINE = constant scalar (NOT exp-formula — exp/rand = online dither).
|
|
||||||
**Window 0x540658**: only ONE direct ref in dump (read at 0x52b771), written only indirectly —
|
|
||||||
STATICALLY INVISIBLE → the only plausible source of warp^3.14/LUT-knee. Need runtime capture or
|
|
||||||
full-pipeline empirical closure.
|
|
||||||
|
|
||||||
## 6. PHASE 5 PLAN (updated 2026-08-18 — bridge done, tilt replaced, FFT mined out)
|
|
||||||
Decision (user): bridge FIRST. **DONE — bridge result above in §5.**
|
|
||||||
Status: step 2 DONE, step 3 PARTIAL (static analysis exhausted).
|
|
||||||
1. (15 min) Proof-of-state above; confirm twin + mdl files complete; repo at `60bf3a2`.
|
|
||||||
2. **Replace empirical tilt** in model_lut.py with `g·LUT(xv) + w·warp(f)^a` — **DONE**, rmse 0.236 dB
|
|
||||||
reproduced, committed `c8f97e4`. Optional refinement fit (2 LUT-knots) reaches 0.167 but is overfit.
|
|
||||||
3. **Close the 0.7-dB gap**: FFT-conv loop mapped + freq-axis cleared (offline const) — **DONE/PARTIAL**,
|
|
||||||
committed `60bf3a2`. Remaining unknown = 0x540658 window content (static-invisible). Options:
|
|
||||||
(a) runtime capture of 0x540658, (b) skip and close empirically in full-pipeline step 5.
|
|
||||||
4. Runtime values: per-bin IIR level trackers (0x563ce0), 0x540880/884, `[0x1a0]/[0x1ac]`, cell count N.
|
|
||||||
5. Full pipeline + render diff vs `/home/m/soothe-bt/*.wav`: dB parity → byte parity.
|
|
||||||
|
|
||||||
NEXT SESSION START (recommendation): skip deeper FFT mining; go straight to step 5 full pipeline
|
|
||||||
(mask-chain already in model_lut.py B.12 at 0.236; freq-axis const; FFT-conv reduces to a per-bin
|
|
||||||
smoothing + 0x540658 shaping that can be fit/absorbed). Runtime capture of 0x540658 only if step 5
|
|
||||||
stalls on the Q=0.1 knee.
|
|
||||||
|
|
||||||
### STEP 5 STARTED (2026-08-18) — dB-parity harness DONE (commit f96bb42)
|
|
||||||
- `/home/m/re-tools/render_parity.py`: measures steady-state per-tone reduction DIRECTLY on the
|
|
||||||
reference wavs (Goertzel tone-amp over late steady window; handles 16/24-bit, mono/stereo).
|
|
||||||
NOTE: Goertzel must use the SEQUENTIAL update form (s2=s1;s1=s0;s0=v+cw*s1-s2) — the tuple-unpack
|
|
||||||
form is numerically unstable (overflows ~1500 steps).
|
|
||||||
- Validation: measured dual Q-scan == stored dataset (dual.wav is 4s mono 16-bit; renders 6s stereo 24-bit).
|
|
||||||
- **dB-PARITY RESULT (canonical B.12, 36 pts, real wavs): TOTAL rmse=0.268 dB**
|
|
||||||
(dual500 0.130, dual2000 0.387, t1kq 0.320, t1k 0.076).
|
|
||||||
residual structure: dual2000 -0.80@Q0.1..+0.36@Q10 (LUT-knee), t1kq +0.36..+0.26 (level region),
|
|
||||||
t1k ±0.1 (good). Refit (g,w,a)=(1.224,0.379,3.405)→0.264; +2 LUT-knots→0.203 (plateau).
|
|
||||||
- CONCLUSION: B.12 form plateau ~0.20-0.27 dB vs real renders; residual is structural (0x540658
|
|
||||||
FFT shaping + level-curve region), matches plan expectation. Re-measured-on-wav numbers differ
|
|
||||||
slightly from the stored model_lut.py dataset (t1kq ~+0.3 dB) — different steady-window method.
|
|
||||||
- Open: full PIPELINE frame-render (STFT + per-frame mask + time constants) is the remaining big
|
|
||||||
lift for byte-parity; dB-parity milestone achieved at 0.268 dB.
|
|
||||||
|
|
||||||
Candidates that may consume Phase 5 time: source of sens≈24.65 (host ×2 over XML 12.0 — not found in dump),
|
|
||||||
IAT\*0x181bab370 outside dump (non-blocking), final z¹ phase proof, the 0x563440 LUT curve (+0x188) values.
|
|
||||||
|
|
||||||
## 7. FILES LAYOUT
|
|
||||||
- `/home/m/re-tools/dsp/` — twin.{hpp,cpp}, twin_check.cpp, CMakeLists.txt, build/
|
|
||||||
- `/home/m/re-tools/model_lut.py` — model **B.12** (Q=xmlq, gain=10^(sens/20), depth 0.8639736175537109,
|
|
||||||
`C=g·LUT(xv)+w·warp^a`, g=1.221/w=0.358/a=3.143) — 0.236 dB bridge model; PCHIP LUT nodes frozen
|
|
||||||
- `/home/m/re-tools/model_fir.py` — bridge canonical source (rmse 0.236, committed b1066f3)
|
|
||||||
- `/home/m/re-tools/framed_render.py` — full frame-render pilot (2026-08-19 params; modes dual|al)
|
|
||||||
- `/home/m/re-tools/rwin_A0/A1/B0/C0.npy`, `r_freqaxis.npy`, `rwin_warp.npy` — live-window tables (48k)
|
|
||||||
- `/home/m/re-tools/rtsnap.py` — live-process page snapshotter; snapshots /tmp/rt{A,B,C,D}.{raw,idx}
|
|
||||||
- `/home/m/re-tools/handoff/decode_rpp_full.py`, `handoff/rpp_allparams.py` — RPP b64-XML full decoder
|
|
||||||
(trim to len%4==0, `<?xml`@92, incl. nested processorStateData)
|
|
||||||
- `/home/m/re-tools/soothe_mem.bin` — memory dump, offset=VA−0x180000000
|
|
||||||
- `/home/m/re-tools/decomp_funs.txt` — decomp: FUN_180536300 (6218), FUN_180530d30 (22330),
|
|
||||||
FUN_180533ec0 (123204), FUN_180529fe0 (missing body! vtable-only)
|
|
||||||
- `/home/m/re-tools/handoff/` — THIS DOC, NOTES_TWIN.md, NOTES_LEVEL.md, phase1/*.py, nls_dasm/*.dis
|
|
||||||
- `/home/m/soothe-bt/*.rpp` (decoder `/home/m/re-tools/dsp/...`; also `decode_rpp4.py` referenced) + `*.wav` refs
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,256 @@
|
|||||||
|
# SESSION HANDOFF — soothe2 detector reverse → bit-exact render
|
||||||
|
|
||||||
|
Prepared: 2026-08-18 (checkpoint end-of-session: commits c8f97e4 + 60bf3a2 pushed). Start: READ THIS FIRST.
|
||||||
|
|
||||||
|
> **2026-08-20 UPDATE**: см. актуальный канон — `AGENTS.md` и `handoff/NOTES_LEVEL.md`.
|
||||||
|
> Нижеследующее «NOT DECODED»/«missing body» УСТАРЕЛО: `FUN_180529fe0` mono-path,
|
||||||
|
> `FUN_180563440/563ce0/563a60` — расшифрованы, полные дизассемблы скопированы в
|
||||||
|
> `handoff/nls_dasm/` (134 файла). Этот файл — исторический чекпоинт.
|
||||||
|
|
||||||
|
## 0. DECOMPILATION INVENTORY (2026-08-19 — what's decoded, where, and what's missing)
|
||||||
|
Goal: bit-exact parity is gated by EXACT tables/window/constants. The chunk-level model hits
|
||||||
|
err ≤0.62 dB (dual) / ≤0.19 dB (al_*) — to go sample-exact we need precise values from the binary.
|
||||||
|
|
||||||
|
### Static decomp assets IN REPO (use these, don't re-decompile):
|
||||||
|
- `ghidra-proj/soothe2.rep` — full Ghidra project (vst3 at ImageBase 0x180000000).
|
||||||
|
- `decomp_funs.txt` (312K lines, ~1640 functions), `fun_map.txt` (2285 addr→FUN), `consts.txt` (11602),
|
||||||
|
`decomp_dsp.txt` / `decomp_vtables.txt` / `decomp_candidates.txt` / `focus_decomp.txt`. Generators:
|
||||||
|
`Dump*.java`, `ImportRtti*.java`, `ListFuns.java`, `SearchRefs.java` (+ headless logs).
|
||||||
|
- RTTI: `rtti_dsp.json` / `rtti_full.json` — class hierarchy
|
||||||
|
`SpectralProcessor<float,7,1>`, `Soothe2ModuleBase<float,1>`, `FilterGraph<float,6,0x400>`,
|
||||||
|
`DigitalFilter<float,0xBA,1>`, `IIRFilterExtended<float,1>`, `AudioProcessingModule<float,1>`.
|
||||||
|
- `handoff/nls_dasm/` — 120 hand-picked `.dis` (twin, iface_18052da00/dbc0, fft, generator, ctor).
|
||||||
|
- `dsp/` — working C++ transcription (twin, detect, freqpath, spectral, fft_stage) + harness;
|
||||||
|
`build/twin_check` passes float-parity gate (§2).
|
||||||
|
|
||||||
|
### KEY DSP ADDRESSES — decoded / not decoded:
|
||||||
|
DECODED (formula-level, notes at NOTES_TWIN.md / NOTES_LEVEL.md):
|
||||||
|
- twin kernel `FUN_180535880`; generator `FUN_180533ec0`; caller `FUN_180536300`; dead sibling `180536f90`.
|
||||||
|
- level-weight formula `FUN_180530d30` (0x540880/884, warp 0x5406a8=0.87·x/(1+x/7.942), w=0.1^(...)).
|
||||||
|
- mask-apply entry `FUN_180529fe0` (accumulator 0x5407c8 += w·res; mask *= warp; FIR *= 0x540658).
|
||||||
|
- FFT-conv loop 0x52b550-0x52b8b5 (plan 0x540530, windows 0x540548/550/598, freq-axis 0x540698=offline const).
|
||||||
|
- level-path map: `0x563440` (LUT curve +0x188, 6 band-slots, combine→+0x2198), `0x56e3e0` (twin-mask factory),
|
||||||
|
`0x563ce0` (IIR level-tracker INIT only).
|
||||||
|
|
||||||
|
NOT DECODED / MISSING FROM decomp_funs.txt (critical): ⚠️ → РЕШЕНО (2026-08-19/20), см. выше
|
||||||
|
- `FUN_180529fe0` body — РЕШЕНО (decomp в `/tmp/consumers_out.txt`, mask-цепь в `dsp/framed_model.cpp`).
|
||||||
|
- `FUN_180563440` / `FUN_180563a60` (LUT curve + gamma + combine), `FUN_180563ce0` — РЕШЕНО,
|
||||||
|
дизассемблы скопированы в `handoff/nls_dasm/` (`f_563440.dis`, `f_563a60.dis`, `f_563ce0.dis`, `f529fe0.dis`).
|
||||||
|
- Window `0x540658` — РЕШЕНО (live-захват, см. `handoff/NOTES_CAPTURE.md`; `rtwin_freq_44100.npy`).
|
||||||
|
- sens source: XML 12.0 → runtime sens_dB≈24.65 (host ×2) not found in dump; `IAT\*0x181bab370` outside dump.
|
||||||
|
|
||||||
|
### Bridge model STATUS (2026-08-19):
|
||||||
|
- `framed_render.py` full STFT frame-render (N=2048, hop=512, sqrt-Hann, twin env tatt=11ms/trel=80ms):
|
||||||
|
`C(f)=G·LUT(xv)+W·warp(f)^A`, G=1.0850, W=0.2819, A=1.1377 (joint dual+al_* refit).
|
||||||
|
- Validation: dual q0.1/1/10 @500+2000 err ≤0.62 dB, envRmse@steady ≤0.78 dB; al_* lv3..24 err ≤0.19 dB.
|
||||||
|
- LUT = slanted al_*-leg (0.366@xv=-0.50 → 0.636@xv=+0.55), replaces flat B.12 (~0.5). Bugfix: clip range
|
||||||
|
must be [LY.min(), LY.max()] not [LY[0], LY[-1]].
|
||||||
|
- Remaining structural residual: −0.6 dB systematic on dual 500Hz (q0.1/q1). Live evidence (avg per-bin
|
||||||
|
gain H=|Y|/|X|): reference mask is FLAT ~-10.2dB across 100-540Hz regardless of Q — model produces
|
||||||
|
res-shaped notch. Hypo tested: freq-smoothing of C(f) fails (kills 2000Hz). NEXT: scalar per-frame xv
|
||||||
|
(broadband level, not per-bin am/res) — see /tmp/smoothtest.py (+ edit scalar=True).
|
||||||
|
|
||||||
|
## 1. Objective (unchanged since session 1)
|
||||||
|
Transcribe the decoded soothe2 detector ("twins" 0x180535880/0x180536f90, 2nd-order resonator) into
|
||||||
|
C++ and reach **bit-exact render parity** with the Reaper reference wavs in `/home/m/soothe-bt/*.wav`
|
||||||
|
(checking dB/magnitude parity first, then byte-diff).
|
||||||
|
|
||||||
|
Path A (bit-exact) chosen. Phase 0,1,2 done. Phase 3,4 advanced. Phase 5 (render diff) is next.
|
||||||
|
|
||||||
|
## 2. DECOMPILATION FINDINGS (2026-08-19 — FUN_180563440, FUN_180563ce0 decoded)
|
||||||
|
|
||||||
|
### FUN_180563440 — LUT curve evaluation + band combine (222 lines disasm)
|
||||||
|
Structure: **3 phases per frame**:
|
||||||
|
1. **1024-bin LUT loop** (0x400 iterations): for each bin k:
|
||||||
|
- x = clamp(k * 0.0009775, 0, 1.0) = k/1024
|
||||||
|
- Band config at +0x188: {A(+0x00), B(+0x04), threshold(+0x0c), flag(+0x10), callback(+0x50)}
|
||||||
|
- **Path 1** (callback exists): vtable call → dynamic LUT
|
||||||
|
- **Path 2** (flag=1, threshold≠1.0): **power-law** → centered = 2*x - 1, then `sign(x) * 10^(log10(|x|) / threshold)` — this is a **compression curve** controlled by sharpness/threshold
|
||||||
|
- **Path 3** (default): **linear interpolation** → `(B - A) * x + A`
|
||||||
|
- Output: double-precision at +0x198, stride 8
|
||||||
|
2. **Twin-mask factory** (FUN_18056e3e0): 6 bands × 1024 bins, stride 0x2000
|
||||||
|
3. **Combine loop**: stereo (max 2 channels), 6 bands, `1 - sum(band_masks)`
|
||||||
|
|
||||||
|
### FUN_180563ce0 — IIR level-tracker INIT (163 lines disasm)
|
||||||
|
- **341 bins** (0x155 iterations), **order-3 IIR** (3 coefficients per bin)
|
||||||
|
- Coefficient: **0.1** (`0x3dcccccd` = IEEE 754 float 0.1)
|
||||||
|
- Initial state: [1.0, 0, 0, 0] and [-1.0, 0, 0, 0] (identity + zero)
|
||||||
|
- Buffer layout: 3 × (16 bytes coeff) per bin, stored at rcx+0x28/+0x40/+0x58
|
||||||
|
- **Not the update loop** — init only; UPDATE is elsewhere
|
||||||
|
|
||||||
|
### FUN_180529fe0 — Coefficient setup (2051 instructions, in decomp_funs.txt)
|
||||||
|
- **vtable method** on Soothe2Module<M,1>
|
||||||
|
- **Lock** at +0x2404dc (atomic test-and-set)
|
||||||
|
- **PRNG state** at +0x2404e0, LCG with offset 0x3cdca
|
||||||
|
- **6-iteration coefficient generation** from 0x5408b0 buffer (LCG-indexed)
|
||||||
|
- **Depth scaling**: `powf(normalized, depth)` at +0x2c
|
||||||
|
- **Mask assembly**: normalize by 0x1a0 (NFFT), × 0x540870 (level weight), × 0x54088c (sharpness), invert
|
||||||
|
- **Copy output** via SIMD memcpy (thunk 0x181ba94b0)
|
||||||
|
|
||||||
|
### FUN_18052e9b0 — SpectralProcessor main (3167 instructions, in decomp_funs.txt)
|
||||||
|
- **Same PRNG + coefficient setup** as FUN_180529fe0
|
||||||
|
- **Band chain**: FUN_18052f500 (interleave) → FUN_18052ee70 (per-bin gain) → FUN_18052d650 (setup) → FUN_18052d920 (window)
|
||||||
|
- **Buffer alloc**: FUN_18052e190 for 0x540668, 0x540698, 0x5406a8, 0x5406b8-e8 (6 bands)
|
||||||
|
- **Depth scaling**: `powf(normalized, depth)` with depth at +0x2c
|
||||||
|
- **Final mask**: `1 - C` (inversion)
|
||||||
|
- **Window application**: `FIR *= 0x540658` (the live-captured window table)
|
||||||
|
|
||||||
|
### Constants verified from binary:
|
||||||
|
| Constant | Address | Value | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| SCALE | 0x24c3c54 | 0.000977517 | 1/1024 (bin→x) |
|
||||||
|
| ONE | 0x24c3ea4 | 1.0 | clamping max |
|
||||||
|
| TWO | 0x24c41e0 | 2.0 | centering (2*x-1) |
|
||||||
|
| NEG1 | 0x24c4680 | -1.0 | sign flip |
|
||||||
|
| HALF | 0x24c3d8c | 0.5 | threshold |
|
||||||
|
| DEPTH_SCALE | 0x24c4334 | 4.0 | depth range |
|
||||||
|
| DB_CONV | 0x24c43e0 | 8.6859 | 20/ln(10) |
|
||||||
|
| FLOOR | 0x24c4704 | -6.9078 | ln(0.001) |
|
||||||
|
| IIR_COEFF | embedded | 0.1 | attack/release per bin |
|
||||||
|
| IIR_ORDER | embedded | 3.0 | IIR filter order |
|
||||||
|
| LCG_OFFSET | embedded | 0x3cdca | PRNG state advance |
|
||||||
|
|
||||||
|
### Live-captured tables (re-verified):
|
||||||
|
- `rwin_A0.npy` (0x1930100): 0.5→0.8, **frequency window** (NOT warp formula)
|
||||||
|
- `rwin_B0.npy` (0x1938180): 0→3.899, **power-law depth curve** (exponent ~0.66)
|
||||||
|
- `rwin_C0.npy` (0x19401c0): 0.596→0.126, **level-dependent weight**
|
||||||
|
- `r_freqaxis.npy`: 0→23988.3 Hz, 11.71 Hz spacing (48000/4096)
|
||||||
|
|
||||||
|
### Key insight: The warp formula is NOT a table — it's computed at runtime
|
||||||
|
The empirical `0.87*7.942*x/(7.942+x)` is an approximation of a runtime computation
|
||||||
|
in FUN_180563440 that evaluates the LUT curve parametrically. The actual LUT has TWO modes:
|
||||||
|
- **Linear** (default): simple interpolation between A and B
|
||||||
|
- **Power-law** (flag=1): `sign(x) * 10^(log10(|x|) / C)` — compression curve
|
||||||
|
|
||||||
|
## 3. PROOF OF STATE — run this first (5 min)
|
||||||
|
Everything below must reproduce. If `twin_check` fails, the transcription moved stale.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/m/re-tools/dsp && cmake --build build && ./build/twin_check
|
||||||
|
# expect:
|
||||||
|
# max rel err = 1.268e-05, PASS = yes (float-parity)
|
||||||
|
# max sweep rel err = 1.233e-05, SWEEP PASS = yes, exit 0
|
||||||
|
```
|
||||||
|
|
||||||
|
`build/soothe2_dsp.so` and `build/twin_check` are current. CMakeLists already has `twin.cpp` + `twin_check` executable.
|
||||||
|
|
||||||
|
## 3. TRANSCRIPTION STATE (summary — details in NOTES_TWIN.md)
|
||||||
|
- `dsp/twin.hpp` + `dsp/twin.cpp` = twin kernel FUN_180535880:
|
||||||
|
- build_twin_coeff (FUN_180533ec0 double pipeline → cvtpd2ps floats)
|
||||||
|
- generator formula (confirmed): `fVar1=sqrtf(param_5)`; `w0=max(2.0,freq)·2π/fs_total`;
|
||||||
|
`p=(sin(w0)·0.5)/Q`; `A=[1+dVar5, dVar2, 1−dVar5]`, `B=[1+param_4, dVar2, 1−param_4]`,
|
||||||
|
`dVar2=cos(w0)·−2.0` (DAT_1824c46b0=−2.0).
|
||||||
|
- cplx_div_exact (rcpps+Newton: `ref=r0·(2−den·r0)`, NaN-guard cmpeqps+movmskps, numerator B·conj(A))
|
||||||
|
- cplx_mul_exact (vfmaddsub213ps form `re=fma(br,ar,−bi·ai)`, `im=fma(br,ai,bi·ar)`)
|
||||||
|
- twin_apply: conj(z1) mirroring 0x1800018b0, z2=z1·z1, out=2·B/A
|
||||||
|
- **OPEN phase question (byte-exactness)**: conjugate shaping — (-α+π/2) vs (-α) axis. Magnitudes match
|
||||||
|
either way; final proof needed via phase of a single-bin run before claiming byte-exactness.
|
||||||
|
|
||||||
|
## 4. GRID + CALLER (Phase 3 — details in NOTES_TWIN.md §Phase 3)
|
||||||
|
- Caller FUN_180536300 (decomp 6218, `caller_536300.dis`), scratch ≥52·N complex bytes:
|
||||||
|
+0..8N A-accum, +8N..16N B-accum, +16N..24N z-powers, +24N..32N rotor-in (filled 1.0),
|
||||||
|
+32N..36N phase ramp lVar1, +36N..44N rotor-out z1, +44N..52N lVar3(1.0)/lVar4(0.0)
|
||||||
|
- `0x18052da00(lVar1, scale=2π/(os·sr), N)` = ramp fill; `0x18052dbc0(param_2, lVar4, N)` cplx-interleave
|
||||||
|
→ out slots get (cos,sin) = e^{+iθ}.
|
||||||
|
- **Grid law**: `os=*(int*)(cfg+0x240080)=4` (mov DWORD PTR [rbx+0x240080],0x4 @0x18052ce3e),
|
||||||
|
`sr=*(float*)(cfg+0x24)=44100.0` → generator `fs_total=os·sr=176400`; per-cell phase `2π/176400`;
|
||||||
|
cell k ↔ freq k/os Hz (0.25 Hz steps). Phase-1 FS=44100 was a valid normalization (res depends on f/fc).
|
||||||
|
- twin always = FUN_180535880 (type-info compare equal); sibling FUN_180536f90 dead.
|
||||||
|
- case8: numBands `+0x814`=1, `+0x103`=1; `param_5=10^(sens_dB/20)`, `gain=sqrtf(param_5)=10^(sens_dB/40)`
|
||||||
|
→ fit gain=4.132 means stored `sens_dB@+0x101 ≈ 24.65`; freq=[band+0x804]=1000.0, Q=[band+0x80c].
|
||||||
|
|
||||||
|
## 5. LEVEL-WEIGHT FORMULA (Phase 4 — DECODED, NEGATIVE RESULT — NOTES_LEVEL.md §UPDATE 2026-08-18)
|
||||||
|
Exact formula from decomp 22330 (FUN_180530d30):
|
||||||
|
- `base=(2000/(sr·0.5))·iVar5/(bin+1)`, iVar5=NFFT/2+1
|
||||||
|
- `w8=powf(base,0.25)`
|
||||||
|
- `v=[0x540880]·0.25·w8·fVar12` (fVar12=4.0 if offline flag [0x5408b8], else 1.0)
|
||||||
|
- `q=1/(1+v/([0x540880]·4))` (2nd pair 0x540884: q2=1/(1+v2/[0x540884]))
|
||||||
|
- `dVar1=(sr/[0x1a0])·[0x1ac]·0.001`
|
||||||
|
- `w=0.1^(1/(max(q·v,floor)·dVar1))`; bufs: 0x5406b8=w, 0x5406c8=1−w (and 0x5406d8/6e8 w/ 0x540884)
|
||||||
|
- consts: c3d3c=0.25, c4334=4.0, c3d8c=0.5, c45b4=2000.0, c3f70=0.1 f32, c3e30=0.001 f64, c3f70=0.1
|
||||||
|
- **Numerically**: w(500)=w(1000)=w(2000)≈0 (1e-20..1e-0.07), complements≈1 → but the weights are STILL
|
||||||
|
APPLIED (as 1−w ≈ 1) by FUN_180529fe0 (`0x5407c8[band] += 0x5406c8·res_upper + 0x5406e8·res_lower`,
|
||||||
|
thunk 003c40, kernel 0x1800752e0 = vfmadd213pd) — the form is weight·resonance, not dead code.
|
||||||
|
- **TILT analysis (2026-08-18, checkpoint NEGATIVE result)**: warp `0x5406a8 = 0.87·x/(1+x/K)`,
|
||||||
|
`K=exp(2.0723267)≈7.942`, `x=f/2000`, applied by FUN_180529fe0 as `mask *= 0x5406a8` (float, thunk 008700)
|
||||||
|
BEFORE the FFT-conv. warp: 500→0.211, 1000→0.409, 2000→0.773 (lin ratio 2000/500=3.67).
|
||||||
|
**BUT warp ≠ empirical tilt (ratio 1.27)**: `C(dB2000)=depth·warp(2000)·LUT_max = 0.864·0.772·0.667=0.445`,
|
||||||
|
yet red2000=15.2 dB needs `C=0.826` ⇒ warp alone CANNOT produce deep-2000 (dual_b1q). Empirical tilt is the
|
||||||
|
envelope of the BLEND term (0x5407c8 += weights·res + freq-axis 0x540698) + FFT-conv shaping, NOT warp —
|
||||||
|
simple tilt→warp substitution in model_lut.py is INVALID. Nothing else decoded yet closes dual_b1q.
|
||||||
|
- **FUN_180563ce0 decoded (2026-08-18)**: INIT of per-bin IIR level-trackers (0x156=342 bins, order=3.0f
|
||||||
|
0x40400000, coeff A/B from 0x24c4780/4790, strided fill; third pass +0xffd0 fills 0x40e00000=7.0f).
|
||||||
|
Not the update loop. Full disasm: `/tmp/opencode/f_563ce0.dis`.
|
||||||
|
- **Level-path map (2026-08-18, NOTES_LEVEL.md)**: 0x563440 (LUT curve +0x188, gamma, 6 band-slots, combine
|
||||||
|
→+0x2198, LUT pairs +0x98 at 0x3ff pts) + 0x56e3e0 twin-mask factory (N=1024, DOUBLE, LUT-ramp input) +
|
||||||
|
0x563ce0 IIR init. 0x563440 NOT in decomp_funs.txt (search via /tmp/opencode/f_563440.dis + level_notes.md).
|
||||||
|
- **DIAGNOSTIC BRIDGE DONE (2026-08-18) — model_fir.py**: реальная цепочка из FUN_180529fe0
|
||||||
|
воспроизводит 36 точек до rmse=0.236 dB БЕЗ эмпирического tilt:
|
||||||
|
`C(f) = g·LUT(log10(L0/res(f))) + w·warp(f)^a`, g=1.221, w=0.358, a=3.143.
|
||||||
|
- dual500-константа = res_band(500;fc=500)=0.117 Q-НЕЗАВИСИМ, не tilt;
|
||||||
|
- dual2000-глубина = АДДИТИВНЫЙ терм `w·warp^3.14` (на 1000 Гц вклад ≤0.021 — иначе t1k
|
||||||
|
рушится; на 2000 = 0.149) — это аккумулятор `0x5407c8 += weights·res + mask` (шаг 5),
|
||||||
|
НЕ мультипликация warp·LUT (та проваливается >10 dB);
|
||||||
|
- warp^~3.14 ≈ π — подозрение на кратный каскад (0x540698 freq-axis, ∏0x540688, двойной FFT);
|
||||||
|
- остаток 0.7 dB при Q=0.1 = форма LUT-колена 0.574, не закрывается per-bin → нужен FFT-уровень.
|
||||||
|
- **FFT-CONV FULLY MAPPED (2026-08-18, NOTES_LEVEL.md §2026-08-18c, /tmp/opencode/f_52b570.dis)**:
|
||||||
|
0x535a70 = dispatcher to base-CRT FFT (0x140a10/0x140a70); 0x540548/0x540550/0x540598 = FFT-plan
|
||||||
|
tables (plan 0x540530, built by 0x52dc30), NOT windows. Per-band loop 0x52b550-0x52b8b5:
|
||||||
|
mask→fwdFFT→invFFT→fill low/high (xmm13/xmm9 consts)→inv→cplx-op→fwd→`FIR *= 0x540658`→fill→inv→
|
||||||
|
`FIR[0]=1,FIR[1]=0`→`*=0x540888`→copy out.
|
||||||
|
**freq-axis 0x540698**: OFFLINE = constant scalar (NOT exp-formula — exp/rand = online dither).
|
||||||
|
**Window 0x540658**: only ONE direct ref in dump (read at 0x52b771), written only indirectly —
|
||||||
|
STATICALLY INVISIBLE → the only plausible source of warp^3.14/LUT-knee. Need runtime capture or
|
||||||
|
full-pipeline empirical closure.
|
||||||
|
|
||||||
|
## 6. PHASE 5 PLAN (updated 2026-08-18 — bridge done, tilt replaced, FFT mined out)
|
||||||
|
Decision (user): bridge FIRST. **DONE — bridge result above in §5.**
|
||||||
|
Status: step 2 DONE, step 3 PARTIAL (static analysis exhausted).
|
||||||
|
1. (15 min) Proof-of-state above; confirm twin + mdl files complete; repo at `60bf3a2`.
|
||||||
|
2. **Replace empirical tilt** in model_lut.py with `g·LUT(xv) + w·warp(f)^a` — **DONE**, rmse 0.236 dB
|
||||||
|
reproduced, committed `c8f97e4`. Optional refinement fit (2 LUT-knots) reaches 0.167 but is overfit.
|
||||||
|
3. **Close the 0.7-dB gap**: FFT-conv loop mapped + freq-axis cleared (offline const) — **DONE/PARTIAL**,
|
||||||
|
committed `60bf3a2`. Remaining unknown = 0x540658 window content (static-invisible). Options:
|
||||||
|
(a) runtime capture of 0x540658, (b) skip and close empirically in full-pipeline step 5.
|
||||||
|
4. Runtime values: per-bin IIR level trackers (0x563ce0), 0x540880/884, `[0x1a0]/[0x1ac]`, cell count N.
|
||||||
|
5. Full pipeline + render diff vs `/home/m/soothe-bt/*.wav`: dB parity → byte parity.
|
||||||
|
|
||||||
|
NEXT SESSION START (recommendation): skip deeper FFT mining; go straight to step 5 full pipeline
|
||||||
|
(mask-chain already in model_lut.py B.12 at 0.236; freq-axis const; FFT-conv reduces to a per-bin
|
||||||
|
smoothing + 0x540658 shaping that can be fit/absorbed). Runtime capture of 0x540658 only if step 5
|
||||||
|
stalls on the Q=0.1 knee.
|
||||||
|
|
||||||
|
### STEP 5 STARTED (2026-08-18) — dB-parity harness DONE (commit f96bb42)
|
||||||
|
- `/home/m/re-tools/render_parity.py`: measures steady-state per-tone reduction DIRECTLY on the
|
||||||
|
reference wavs (Goertzel tone-amp over late steady window; handles 16/24-bit, mono/stereo).
|
||||||
|
NOTE: Goertzel must use the SEQUENTIAL update form (s2=s1;s1=s0;s0=v+cw*s1-s2) — the tuple-unpack
|
||||||
|
form is numerically unstable (overflows ~1500 steps).
|
||||||
|
- Validation: measured dual Q-scan == stored dataset (dual.wav is 4s mono 16-bit; renders 6s stereo 24-bit).
|
||||||
|
- **dB-PARITY RESULT (canonical B.12, 36 pts, real wavs): TOTAL rmse=0.268 dB**
|
||||||
|
(dual500 0.130, dual2000 0.387, t1kq 0.320, t1k 0.076).
|
||||||
|
residual structure: dual2000 -0.80@Q0.1..+0.36@Q10 (LUT-knee), t1kq +0.36..+0.26 (level region),
|
||||||
|
t1k ±0.1 (good). Refit (g,w,a)=(1.224,0.379,3.405)→0.264; +2 LUT-knots→0.203 (plateau).
|
||||||
|
- CONCLUSION: B.12 form plateau ~0.20-0.27 dB vs real renders; residual is structural (0x540658
|
||||||
|
FFT shaping + level-curve region), matches plan expectation. Re-measured-on-wav numbers differ
|
||||||
|
slightly from the stored model_lut.py dataset (t1kq ~+0.3 dB) — different steady-window method.
|
||||||
|
- Open: full PIPELINE frame-render (STFT + per-frame mask + time constants) is the remaining big
|
||||||
|
lift for byte-parity; dB-parity milestone achieved at 0.268 dB.
|
||||||
|
|
||||||
|
Candidates that may consume Phase 5 time: source of sens≈24.65 (host ×2 over XML 12.0 — not found in dump),
|
||||||
|
IAT\*0x181bab370 outside dump (non-blocking), final z¹ phase proof, the 0x563440 LUT curve (+0x188) values.
|
||||||
|
|
||||||
|
## 7. FILES LAYOUT
|
||||||
|
- `/home/m/re-tools/dsp/` — twin.{hpp,cpp}, twin_check.cpp, CMakeLists.txt, build/
|
||||||
|
- `/home/m/re-tools/model_lut.py` — model **B.12** (Q=xmlq, gain=10^(sens/20), depth 0.8639736175537109,
|
||||||
|
`C=g·LUT(xv)+w·warp^a`, g=1.221/w=0.358/a=3.143) — 0.236 dB bridge model; PCHIP LUT nodes frozen
|
||||||
|
- `/home/m/re-tools/model_fir.py` — bridge canonical source (rmse 0.236, committed b1066f3)
|
||||||
|
- `/home/m/re-tools/framed_render.py` — full frame-render pilot (2026-08-19 params; modes dual|al)
|
||||||
|
- `/home/m/re-tools/rwin_A0/A1/B0/C0.npy`, `r_freqaxis.npy`, `rwin_warp.npy` — live-window tables (48k)
|
||||||
|
- `/home/m/re-tools/rtsnap.py` — live-process page snapshotter; snapshots /tmp/rt{A,B,C,D}.{raw,idx}
|
||||||
|
- `/home/m/re-tools/handoff/decode_rpp_full.py`, `handoff/rpp_allparams.py` — RPP b64-XML full decoder
|
||||||
|
(trim to len%4==0, `<?xml`@92, incl. nested processorStateData)
|
||||||
|
- `/home/m/re-tools/soothe_mem.bin` — memory dump, offset=VA−0x180000000
|
||||||
|
- `/home/m/re-tools/decomp_funs.txt` — decomp: FUN_180536300 (6218), FUN_180530d30 (22330),
|
||||||
|
FUN_180533ec0 (123204), FUN_180529fe0 (missing body! vtable-only)
|
||||||
|
- `/home/m/re-tools/handoff/` — THIS DOC, NOTES_TWIN.md, NOTES_LEVEL.md, phase1/*.py, nls_dasm/*.dis
|
||||||
|
- `/home/m/soothe-bt/*.rpp` (decoder `/home/m/re-tools/dsp/...`; also `decode_rpp4.py` referenced) + `*.wav` refs
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
## Objective
|
||||||
|
- Реверс-DSP oeksound soothe2 v1.1.2 VST3 → математическая модель (STFT, детектор резонансов, нотич-синтез). Текущая фаза — поведенческий параметр-свип через рендер Reaper RPP + статический декомпил (без декрипты бинарника пока).
|
||||||
|
|
||||||
|
> **⚠️ ИСТОРИЧЕСКИЙ ДОКУМЕНТ (поведенческая модель v4, sim.py, ок. 2026-08-17)**.
|
||||||
|
> Актуальный канон (bit-exact, P4) — `README.md`, `AGENTS.md`, `handoff/NOTES_LEVEL.md`.
|
||||||
|
> Здесь — ранняя поведенческая фаза реверса, сохранена как справочник.
|
||||||
|
|
||||||
|
## Key Facts
|
||||||
|
- Пользователь русскоязычный; sudo нет. Объект: `~/.wine/.../soothe2_x64.vst3` (PE32+, base 0x180000000), yabridge 5.1.1, Reaper 7.78.
|
||||||
|
- Frida невозможна (Seccomp). Дамп памяти — `pread /proc/pid/mem` от родителя. pkill-ловушки: paths без `harneb`/`reaper` → `pkill -9 -x reaper`, `pkill -9 -f '[y]abridge'`.
|
||||||
|
- Рендер-проект: `RENDER_RANGE 1 0 0 0 1000`, ITEM обязательно `POSITION 0`. Offline-рендер мгновенный → для живого дампа realtime (`RENDER_1X 1`) + `testtone.wav`.
|
||||||
|
- VST-state = 92-байт JUCE-заголовок + base64 XML `<SOOTHE2STATE>` + 54 `<PARAM id=... value=.../>`; внутри — UI-state без DSP.
|
||||||
|
|
||||||
|
## Breakthrough (исправлено в этой сессии)
|
||||||
|
- **Формат-адаптивность PARAM**: плагин применяет `<PARAM value>` ТОЛЬКО если строка сериализована в том же формате, что оригинал:
|
||||||
|
- full-precision (depth, band freq/q...) → `%.16f` (напр. `"0.9000000000000000"`). Короткие строки → схлопывание (fallback).
|
||||||
|
- short-формат `"X.0"` (selectivity, mix, mode, oversample, resolution) → ЛОМАЕТСЯ от `%.16f`.
|
||||||
|
- `sweep.py`/`tt_sweep.py` адаптивны: >4 десятичных в оригинале → `%.16f`, 1-4 → то же число знаков, иначе — как есть.
|
||||||
|
|
||||||
|
## Синтез модели (поведенческая, v4 — реализована в sim.py, RMSE на settled ≤0.05dB по всем свипам)
|
||||||
|
```
|
||||||
|
PARADИГМА (реализована): out(t) = x(t) − amount_b(t)·bp_b(t; fc, Q) [субтрактивный нотч, не микш-параллельный]
|
||||||
|
amount(total) = A(depth)·S(sens)·H(sharp)·M(mode) — LUT из измерений (пересечение, не аналит. фит)
|
||||||
|
A(depth): 0.5/0.864/1/2/3/5/10/20 → 0.560/0.588/0.598/0.667/0.727/0.820/0.939/0.989
|
||||||
|
S(sens): 0/6/12/24 → 0.48/0.76/1.0/1.0 (floor при sens=0!)
|
||||||
|
H(sharp): 1/3/5/10 → 0.20/0.59/0.84/1.0
|
||||||
|
M(mode): 0 → 0.745 (mode0 слабее), 1.. → 1.0
|
||||||
|
Q(sel): = 3.0+0.36·sel (2nd-order bandpass; реал. даёт более крутые борта — приближ.)
|
||||||
|
env(t): one-pole, α_attack=1−e^(−1/(T_a·fs)), α_release=1−e^(−1/(T_r·fs))
|
||||||
|
T_a = 0.02·e^(a/1.955) (0→20мс, 5→258мс, 10→3.3c); T_r LUT из спадов (0→27мс, 5→140мс, 10→15c)
|
||||||
|
детектор: лауднеss полосы |bp| smoothed 5мс > thr 0.01 → цель amt, иначе 0 (порог между probe 0.0005 и burst 0.2)
|
||||||
|
выход: x_out = mix/100·… — линейный кроссфейд: (100−mix)/100·x + mix/100·fully_reduced
|
||||||
|
(mix=100 = полная обработка = ref; mix=0 → сухой; проверено на s_corr_mix: монот. линейно)
|
||||||
|
```
|
||||||
|
Верификация (settled, RMSE по reduction dB на burst-окне):
|
||||||
|
| параметр | реал | sim | RMSE | | параметр | реал | sim | RMSE |
|
||||||
|
|---|---|---|--|---|---|---|---|---|
|
||||||
|
| дефолт | 7.70 | 7.70 | 0.002 | sharp=1 | 1.11 | 1.11 | 0.002 |
|
||||||
|
| depth=2 | 9.56 | 9.56 | 0.001 | sharp=3 | 3.72 | 3.71 | 0.006 |
|
||||||
|
| depth=5 | 14.90 | 14.89 | 0.008 | attack=5 | 5.68 | 5.90 | 0.360 |
|
||||||
|
| depth=10 | 24.27 | 24.25 | 0.018 | mode=0 | 5.03 | 5.00 | 0.023 |
|
||||||
|
| depth=20 | 39.43 | 39.40 | 0.054 | mode=2 | 7.70 | 7.70 | 0.002 |
|
||||||
|
| sens=0 | 2.89 | 2.89 | 0.001 | sens=24 | 7.70 | 7.70 | 0.002 |
|
||||||
|
Q-профиль (probes-зонд): sim sel1 500→680@540→0.62@700 vs real 8.03→4.54→1.13; 2nd-order не покрывает
|
||||||
|
крутые борта реала (реал резче ~×3): допустимо как 1-я итерация, RMSE макс по профилю ~1.6dB.
|
||||||
|
Release-трассы: r0/r1/r2/r5 совпадают (sim против real 1.6s:3.5/4.3, 1.7s:1.5/1.9, 1.9s:0.3/0.3).
|
||||||
|
|
||||||
|
## Q/width-зонд (probes500/probes500b, drive 500Hz amp0.2 + справки 0.02)
|
||||||
|
- Методика: длинный single-FFT на окне 1.7..2.9s (разрешение ~0.7Hz), справки на 500,505,510,520,540,
|
||||||
|
555,575,600,700 (b-set) / 500,505,515,530,560,640,780 (a-set). Драйв-тон держит детект-клок,
|
||||||
|
справки зондируют передаточную характеристику в установившемся режиме.
|
||||||
|
- Данные (a-set) red@freq: sel1: 500:8.0 510:7.9 520:7.7 540:4.5 555:2.7 575:1.6 600:0.7 (плавный хвост)
|
||||||
|
sel8: 500:8.7 510:9.1 520:6.6 540:1.3 555:0.1 575:0.0 600:0.0 (резкий срез) — Q уверенно растёт с sel.
|
||||||
|
- Центраئة нотча на 500..510 чуть выше 500 (fs на 505..510) — трекинг слегка субадио.
|
||||||
|
- Значения на дип-тонах 530/560/640 в b-наборе — из мусора (нет справки) и исключены; a-набор чистый.
|
||||||
|
- ВАЖНО: dry-файлы дважды перезаписывались провальными dry-рендерами (RENDER_FILE не патчился по
|
||||||
|
`.wav` напр. burst500L_byp → overwritten probes). ФИКС: патчить RENDER_FILE явно в sed до рендера.
|
||||||
|
- data: оба svипа предсказуемо монотонны; таблица в summary выше.
|
||||||
|
|
||||||
|
## Верифицированные данные (все свипы с корректным форматом)
|
||||||
|
- **depth (mir)**: монотонно distinct (vs-ref 496k→441k→...→347k@0.85→384k@1.0) — работает.
|
||||||
|
- **selectivity**: 0/1/2/5 distinct (476k/466k/458k/461k vs ref); 10==20==50==100 (сатурация ≥10).
|
||||||
|
- **oversample**: 0==ref; 1 distinct; 2==3==4 (сатурация на 2).
|
||||||
|
- **resolution**: 0 distinct; 1==ref; 2 distinct (все distinct); 3==4.
|
||||||
|
- **mix**: 0/25/50/75/100 всё distinct монотонно; 100==ref.
|
||||||
|
- **mode**: 0 distinct; 1==2==3==4==ref.
|
||||||
|
- **band1 freq**: tt_b1f_678 (дефолт banda freq) == tt_ref бит-в-бит; свип 100..8000 отрендерен.
|
||||||
|
|
||||||
|
## Прочие находки
|
||||||
|
- 60-сек рендер p60_ref/p60_dep0/p60_dep1 подтвердил collapse класса при коротких строках.
|
||||||
|
- rtdump.py живые дампы (регионы по VA, RA/WA) — DSP-кластер .data меняется между глубинами; раньше не совпали по layout (артефакт noise).
|
||||||
|
|
||||||
|
## Work State
|
||||||
|
### Completed
|
||||||
|
- Свип-инфраструктура: sweep.py + tt_sweep.py (адаптив формата), рендер, сверочный анализ (spectrum.py медленный, нужно numpy).
|
||||||
|
- Тест-тон: testtone.wav (2s, multi-tone 110Hz–14kHz с AM) для изоляции резонансов; tt_ref==tt_678 (0 diff).
|
||||||
|
- Базовые свипы по всем основным параметрам.
|
||||||
|
|
||||||
|
### Active
|
||||||
|
- **Извлечена статическая depth-кривая**: LUT 207 float @0x1826170e8..0x182617420, форма `0.302 + 0.698·sin(π/2·x)^0.94` (r²=0.99999), saved `depthcurve.npy`. ИДЕНТИЧНА у ref/dep0/dep1 → вшита в бинарь (build-time), не меняется от параметра = внутренний «depth→amount» маппинг.
|
||||||
|
- **Адаптивная динамика (burst-тест, band1@500Hz, q≈1, depth 0.864)**: burst500.wav (2s: 0.5s тишины → 1.0s 500Hz burst amp 0.2 → 0.5s тишины, + 1000Hz amp 0.05 фон). `burst500_b1` (fx) vs `burst500_byp` (dry):
|
||||||
|
- ATTACK: плавное включение нотч-ослабления, τ≈**18 мс** (фит на экспоненту A·(1−e^−t/τ), stable 17.6/17.7/18.8/18.8), установка −7.7dB бай-в-бай за ~100мс.
|
||||||
|
- RELEASE: хвост после снятия бурста спадает за τ≈**5 мс** (после 1.5s fx-энергия 24→0 за ~30мс) — быстрый трелинг, симметрия фола не классическая (вероятно фильтр-транзиент, не медленный rel).
|
||||||
|
- Глубина ослабления НЕ зависит от «порога» — это фикс. нотч на частоте band, amount задаётся curve(depth).
|
||||||
|
- Рендерится через те же tt-файлы; РЕЗУЛЬТАТЫ в wav — 24-bit (`sampwidth=3`), читать через 24-bit decode (или любой np.frombuffer под sw), не 16-bit!
|
||||||
|
- **τ-инвариантность** (burst-тест, band1@500Hz, win 10ms, Goertzel, фит к A(1−e^−t/τ)):
|
||||||
|
- depth 0→0.86→1: A=6.39→7.70→7.91 dB; τ=18.3→16.8→16.5 ms (τ почти const) → depth контролирует amount, НЕ скорость.
|
||||||
|
- selectivity: τ РАСТЁТ монотонно 12.3→16.8 ms при sel 0→10 (12.3/12.4/12.6/12.9/13.0/13.4/14.3/16.8) → Q/selectivity = time-const антреккинга.
|
||||||
|
- depth-amount таблица (settled, band1@500): sel 0..6 монотонно −7.79→−9.08 dB, затем dip sel8=−8.79, sel10=−7.71 (non-monotonic у высоких sel, нюанс трекера).
|
||||||
|
- «650Hz notch» в раннем FFT-анализе — АРТЕФАКТ деления на near-zero dry; реальные числа = ratio dry/fx только там, где dry>5% пика (pure 500Hz tone → окно вокруг 500).
|
||||||
|
- **secret свипа: `patchparam.py`** обязателен для формат-сохранения: state-блок в RPP = base64, wrapped по 128-симв/строка с 2-sp indent и НОВОЙ строкой перед '>'. Ключевое: глубина пишется ТОЛЬКО `%.16f` (короткая `0.0` → collapse!), поэтому CLI `depth=0.0!` форсит полную точность.
|
||||||
|
- Float-поиск по живому дампу: depth 301 hits (множество копий по bands), selectivity/sharpness → общий бакет (10.015, 9.997), mix → 4 копии 100.0, band freqs (678.76, 8242.67) → 0 точных (хранятся иначе, напр. Hz→binidx или через npf).
|
||||||
|
- Живой diff dep0 vs dep1 в .data (0x182622130, 0x182622350, 0x1826223c0, 0x18262aca8, 0x182672fe4) — флоаты-состояние меняются; но регион содержит много мусора/nan (это .data буферы рендера, не коэфф).
|
||||||
|
|
||||||
|
### Провалы/уроки
|
||||||
|
- ASLR между отдельными rtdump-запусками сдвигает раскладку регионов → прямого VA-сравнения НЕТ (dep0b 10 регионов, dep1b 9). Только те адреса, что реально в 0x18260a000-0x18267c000 (совпадают) сравнимы.
|
||||||
|
|
||||||
|
## Relevant Files
|
||||||
|
- **`/home/m/re-tools/sim.py`** — поведенческий симулятор (LUT amount + 2nd-order bp Q(sel) + env attack/release + mix/mode). `simulate(x, fc, depth, sens, sharp, sel, mode, attack, release, mix)`. RMSE settled ≤0.05dB (см. таблицу выше).
|
||||||
|
- **`/home/m/re-tools/verify_sim.py`** — RMSE-сверка симулятора против рендеров (mag@500 трасса). Использование: `verify_sim.py <fx.wav> --params depth=5 attack=5`.
|
||||||
|
- **`/home/m/re-tools/fit_curves.py`** — фит LUT (остаётся как аналитический эквивалент; LUT в sim.py приоритетнее).
|
||||||
|
- probes500/probes500b_wav+byp — зонды ширины нотча; prb_*/prb2_* — рендеры.
|
||||||
|
- `synth_multi.py` (AM-комб 200–3000Hz резонансный), `burst500.wav` + `burst500_b1.rpp/.wav` + `burst500_byp.rpp/.wav` (burst-атак/релиз band1@500Hz), `burst_dep0/dep1/sel0/sel2/sel5.rpp/.wav` (τ-sweep), `comb.wav` + `comb_b1_*.rpp/wav`, `comb_dep_{0..1}`, `depthcurve.npy`. **Рендеры Reaper = 24-bit** — читать с `sampwidth=3`, не 16-bit!
|
||||||
|
- `/home/m/re-tools/patchparam.py` — патчер PARAM в base64 state с формат-сохранением (128-wrap, `depth=...!` → %.16f).
|
||||||
|
- `/home/m/re-tools/spec.py`, `probe.py`, `notch.py`, `synth.py`, `mkbase.py`, `tt_sweep.py`.
|
||||||
|
- `/home/m/soothe-bt/tone1kq.wav` (чистый 1k, -18dBFS), `dual.wav` (500+2000Hz), `resonant.wav` (500Hz-резонанс).
|
||||||
|
- `/home/m/soothe-bt/t1kq_*.rpp/wav`, `res_only1_*.rpp/wav`, `dual_b1_*.rpp/wav`.
|
||||||
|
- `/home/m/re-tools/sweep.py` — адаптивный генератор RPP (tpl фиксирован, для смены файла использовать mkbase + tt_sweep).
|
||||||
|
- `/home/m/soothe-bt/tt_base.rpp` (template: testtone + дефолтные парамы), `tt_ref.wav`.
|
||||||
|
- `/home/m/soothe-bt/s_corr_{sel,os,res,mix,mode}*.wav` — верифицированные свипы на resonant.wav.
|
||||||
|
- `/home/m/soothe-bt/render_v5.rpp`, `s_ref2.wav` (бит-в-бит оригинал).
|
||||||
|
## Фаза 3: Трекинг подтверждён (aug 16) — ARCHITECTURE REVISION
|
||||||
|
- **Вход-зависимость**: ред@1k растёт с уровнем сигнала (band1=1000, тон 1k):
|
||||||
|
`-27→-21→-15→-12→-9→-6→-3 dBFS` ⇒ `5.80→7.75→9.91→11.05→12.21→13.40→14.60 dB`. amount растёт с уровнем детектируемого резонанса (не бинарный порог).
|
||||||
|
- **Трекинг нотча** (fresh renders `trk_b1_*`, вход tone1k 1k, band1∈{500,1000,2000}, длинная FFT): нотч ВСЕГДА на fc=тон=1k, band-частота НЕ двигает позицию режектора:
|
||||||
|
- band=500 → 11.91dB, band=1000 → 15.82dB, band=2000 → 11.91dB, band1 off → 9.80dB, sens=0 → 9.80dB.
|
||||||
|
- Единственный вклад band-EQ — ВЕС глубины детекции в области (совпадение band~тон даёт +4dB; off/sens0 −2dB).
|
||||||
|
- **Мульти-пики**: dual (500+2000), оба нотча активны одновременно при sel=1..20 (~10dB оба) → детектор находит НЕСКОЛЬКО резонансов; selectivity НЕ регулирует число нотчей на явных тонах.
|
||||||
|
- **Пользователь (иерархия)**: детектор оценивает каждую частоту спектра; band-EQ формирует его ВХОД (усиление/ослабление детекции по областям, у band2 sens может быть −12); selectivity = отбор пиков; sharpness = форма нотча; depth = глобальная глубина.
|
||||||
|
- **Reaper-ловушка**: tt_sweep с ОТНОСИТЕЛЬНЫМ out_wav создаёт ПОДДИРЕКТОРИЮ → всегда абсолютный путь в RENDER_FILE.
|
||||||
|
- Файлы: `trk_base.rpp`, `trk_b1_{500,1000,2000,eqoff,sens0}.rpp/.wav`, `sel_base.rpp`, `sel_{1,3,10,20}.rpp/.wav`, `lvl_{03..27}db.wav` + `lvl_t_*.rpp/.wav`.
|
||||||
|
### Фаза 3b: amount зависИТ от уровня сигнала (не просто порог)
|
||||||
|
- lvl_sweep (тон 1k, band1@1000, depth=0.864): ред@1k = 5.80 / 7.75 / 9.91 / 11.05 / 12.21 / 13.40 / 14.60 для −27..−3 dBFS.
|
||||||
|
- При depth=10: 21.67 / 24.15 / 26.63 / 27.86 / 29.09 / 30.33 / 31.56. Наклон d(ред)/d(level) ≈ 0.37–0.41 dB/dB обоих depth → amount ∝ уровень^p (монотонно с насыщением), НЕ бинарный порог. depth — масштаб, не floor.
|
||||||
|
- Обе кривые монотонны и близки по форме (отношение ~2.2 при громком, ~3.7 при тихом → форма зависит от depth слабо).
|
||||||
|
### Фаза 4 (aug 16): КАРТА EQ-ВЕСОВ ДЕТЕКТОРА
|
||||||
|
- **`on` полосы = enable детекторного веса**; off-полоса её sens НЕ отдаёт в детекцию. Подтверждения:
|
||||||
|
- eqT_b1_b2/b3/b4/b2b3b4 и trk_b1_500 (sens off-полос любая) ВСЕ = 11.91dB (только от b1 sens12); eq_off_sensfac (все off, factory sens) = 9.80dB = нейтраль.
|
||||||
|
- eq1778_b2s_12 (b2 on=0) = 11.81, b1on12_b2s12 (b2 on=1) = 17.60 — ЕДИНСТВЕННАЯ разница band2 on.
|
||||||
|
- Баг в раннем clear_bands сбрасывал b1 sens→0 (eq_b1_on был sens0=нейтраль 9.80; правильный net_b1on12 sens12 = 11.91).
|
||||||
|
- **S(sens) при совмещённом весе (band1@1k, тон 1k)**: sens 0/6/9/12/18/24 → boost над полом 0.00/3.16/4.65/6.02/6.02/6.02 dB.
|
||||||
|
boost = 6.02·min(1, sens/12), насыщение +6dB при sens≥12 (не монотонно из-за сатурации детектора, не бага).
|
||||||
|
- **W(f) — колокол EQ-веса** (тон 1k, одна полоса on sens12, варьирую band freq):
|
||||||
|
- band1 (q≈1): 500→11.91(+2.11), 600→12.80(+3.00), 750→14.28(+4.48), 850→15.19(+5.39), 950→15.75(+5.95), 1000→15.82(+6.02), 1050→15.76, 1200→15.05(+5.25), 1500→13.44(+3.64), 2000→11.91(+2.11), 2500→11.16(+1.36). Симметричный широкий колокол (первый октав хвост +2.1dB, не спадает к 0).
|
||||||
|
- band2 (q≈4.5): 600→10.15(+0.35), 750→10.78(+0.98), 850→11.97(+2.17), 1000→15.80(+6.00), 1200→11.68(+1.88), 1500→10.34(+0.54) — РЕЗКИЙ колокол. **Q полосы управляет шириной детекторного веса** (q=4.5 на −1 октаве теряет ~полностью, q=1 теряет только ~66%).
|
||||||
|
- **Позиция нотча**: band freq не двигает режектор (всегда на резонансе тона); EQ-веса модулируют ТОЛЬКО глубину.
|
||||||
|
- Новый модельный блок: `red(f_tone) = floor(level) + Σ_on boost_i`, `boost_i = 6.02·min(1,|sens_i|/12)·H_q(f_tone; fc_i, Q_i)`.
|
||||||
|
- Файлы: `wf_{600,750,...,2500}.wav` (band1 q-sweep), `ws_{6,9,18,24}.wav` (aligned sens), `b2q_{600..1500}.wav` (band2 q=4.5 sweep), `b1on12_b2{on12,s12}.wav`.
|
||||||
|
### Фаза 5 (aug 16): STFT-параметры детектора (Stage 1)
|
||||||
|
- **Хоп/задержка**: tone-jump зонд tj.wav (1k→1.5k в t=2.0s) → нотч пересаживается на новый резонанс за ~1 кадр ≈ 18мс (hop ≤ 512 сэмплов). Трекинг чирпа 400→2000Hz в реальном времени, dt≈0.03→0.05s (шум оценки FFT-бина, значимой задержки нет).
|
||||||
|
- **Разрешение по частоте**: пары 1000+1040 → сливаются в один широкий нотч; 1000+1100 разделяются (центры ≈996/1104, но провал между ними всё ещё глубокий); 1000+1200 → два чётких отдельных нотча с полным восстановлением на 1100. → окно FFT ≈ 1024–2048 (бины 21–43Hz), multiple-нотч подтверждён.
|
||||||
|
- **Энерготрекинг (fx@f по окну 2048, sync-Goertzel)**: до прыжка fx@1000 ≈ −24dB (ред ~14dB), после — fx@1500 ≈ −23.8dB. Двойной бурст (gap 10..100ms) → нотч на том же тоне не переоткрывается между бурстами (ред остаётся ~4dB floor) — bурст probe (осб) указывает на фиксированный window-центр, не пер-открытие.
|
||||||
|
- Вывод для v5: STFT с окном ~2048, хоп ~512–1024; window function Hann; детектор по |X| per-bin → локальные пики (selectivity); EQ-веса W(f) как множитель sensitivity per-bin; per-пик нотч (bandstop Q=sharpness).
|
||||||
|
- Файлы: `chirp_log_400_2000.wav`, `chirp_trk.rpp/.wav`, `pair_{1040,1100,1200}.wav` + `pair_trk_{40,100,200}`, `dburst_{10,20,50,100}.wav` + `dburst_trk_*`, `tj.wav` + `tj_trk.rpp/.wav`.
|
||||||
|
### Фаза 6 (aug 16): КОЛОКОЛ EQ-ВЕСА + SHARPNESS + level-floor (Stage 2 фит)
|
||||||
|
- **H(f) — колокол**: `H(f) = 1/sqrt(1 + (Qeff·A)²)`, `A = f/fc − fc/f`. Qeff = **1.54·q^1.33** (q=1→1.54, 2→3.32, 3→5.64, 6→16.8). Воспроизводит кривую band1 (wf_*) с ошибкой ≤0.15dB по всему диапазону 500..2500.
|
||||||
|
- **Sharpness → Q нотча**: psh_{1,3,5,10} (probes500, drive 500·0.2, band1@500): центр 505Hz red 1.3/4.3/6.8/8.7dB; ширина Q_notch≈1.15·sharp (sh10→~11, sh5→~6, sh3→~3, sh1→~1.2). Центр-глубина НЕ постоянна — растёт с sharp (1.3→8.7), т.е. sharp масштабирует amount И Q.
|
||||||
|
- **level-floor (нейтраль)**: `neut_{24..3}` (все полосы off/sens0, тон 1k): red = 2.22/3.36/4.85/5.71/6.65/7.65 dB при −24/−18/−12/−9/−6/−3 dBFS rms. Монотонно, наклон ~0.31 dB/dB. `al_{*}` (b1@1000 sens12): 5.80/7.75/9.91/11.04/12.21/13.39 → boost = 3.59/4.39/5.06/5.33/5.56/5.74, **насыщается к ~6dB** с уровнем (не аддитивен в dB!).
|
||||||
|
- Модель amount: `red(f) = curv(level·G_eq(f))` с насыщением (не линейный сдвиг в dB). depth масштабирует (фаза 3b). Для sim: 2D LUT level×W.
|
||||||
|
- Файлы: `lvl_tone_lv{24,18,12,9,6,3}.wav`, `neut_{*}.rpp/.wav`, `al_{*}.rpp/.wav`, `psh_{1,3,5,10}.rpp/.wav`.
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
# prd.md — Project Requirements Document
|
||||||
|
|
||||||
|
## 1. Project Overview
|
||||||
|
|
||||||
|
**Name**: `soothe2-re` — bit-exact reverse engineering of **oeksound soothe2** (VST3, Windows x64)
|
||||||
|
|
||||||
|
**Goal**: Reproduce the plugin's DSP core (level detector, mask computation, filter application) in C++17, byte-for-byte identical to the native binary.
|
||||||
|
|
||||||
|
**Status**: ~95% decompiled. Bridge **1.594 dB** stable; structural **2.689 dB** (L/R `e343b0a`, `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0`, `RT_CASC=0` gated). Historical best `0.341` (24mm14) not reachable on current HEAD. Target: **<0.05 dB** (bit-exact gate, requires chain calibration).
|
||||||
|
|
||||||
|
**Golden rule**: Every parameter must have a source (decomp address / live table). Empirical fits must be flagged `EMPIRICAL`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
re-tools/
|
||||||
|
├── prd.md ← THIS FILE (project overview)
|
||||||
|
├── README.md ← TOTAL metric source (single source of truth)
|
||||||
|
├── AGENTS.md ← runbook: build, env flags, tooling hazard
|
||||||
|
├── BITEXACT_PLAN.md ← path to byte-exact (3 steps, criteria)
|
||||||
|
├── roadmap.md ← historical B-phase log (archived)
|
||||||
|
│
|
||||||
|
├── dsp/ ← C++17 DSP reconstruction (THE CANON)
|
||||||
|
│ ├── framed_model.{cpp,hpp} ← MAIN: mask-apply chain (empirical bridge)
|
||||||
|
│ ├── render48k.cpp ← 48k/4096 structural pipeline (resample→chain→resample)
|
||||||
|
│ ├── spectral.{cpp,hpp} ← STFT/ISTFT processor + FIR builder
|
||||||
|
│ ├── fn529fe0.{cpp,hpp} ← structural chain 9–19 (FUN_180529fe0)
|
||||||
|
│ ├── fnfaith.{cpp,hpp} ← faithful detector cascade (BLOCKMAP transcription)
|
||||||
|
│ ├── fft{,_plan,_stage}.cpp/hpp ← FFT engine (canonical + RFFT bit-exact)
|
||||||
|
│ ├── twin.{cpp,hpp} ← twin resonator (FUN_180535880, float-parity)
|
||||||
|
│ ├── rt_mask_tables.{hpp,.cpp} ← live IIR A/B coefficients (385K)
|
||||||
|
│ ├── rt_weights.{hpp,.cpp} ← live kWarp/kBand768 tables (44K)
|
||||||
|
│ ├── fftconv.{cpp,hpp} ← FFT convolution FIR application
|
||||||
|
│ ├── vlog.{cpp,hpp} ← fast log2 approximation
|
||||||
|
│ ├── exp2.{cpp,_tables.cpp,hpp} ← fast exp2 approximation
|
||||||
|
│ ├── levelpath.{cpp,hpp} ← level-curve path (xv = log10(am/res))
|
||||||
|
│ ├── freqpath.{cpp,hpp} ← frequency-axis warp/freq-domain helpers
|
||||||
|
│ ├── leveltrack.{cpp,hpp,hpp} ← envelope follower (attack/release tables)
|
||||||
|
│ ├── log2_ln.{cpp,hpp} ← log/ln utilities
|
||||||
|
│ ├── filter.{cpp,hpp} ← detection kernel (legacy)
|
||||||
|
│ ├── detect.{cpp,hpp} ← detector front-end (legacy)
|
||||||
|
│ ├── phase_table.{cpp,hpp} ← phase table for FFT
|
||||||
|
│ ├── ms.hpp ← mid-side helpers
|
||||||
|
│ ├── params.hpp ← parameter struct
|
||||||
|
│ ├── rotor_kernel.hpp ← rotor transform kernel
|
||||||
|
│ ├── rt_div_tables.hpp ← division tables
|
||||||
|
│ ├── soothe_constants.hpp ← decoded constants
|
||||||
|
│ ├── cody_waite.hpp ← Cody-Waite argument reduction
|
||||||
|
│ ├── twiddle_{builder,loader}.cpp/hpp ← FFT twiddle factors
|
||||||
|
│ ├── dsp_ctx.hpp ← DSP context layout
|
||||||
|
│ ├── framed_test.cpp ← CLI bridge renderer (44.1k)
|
||||||
|
│ ├── harness.cpp ← legacy harness
|
||||||
|
│ ├── *_check.cpp ← bit-exact unit test targets
|
||||||
|
│ └── CMakeLists.txt
|
||||||
|
│
|
||||||
|
├── scripts/
|
||||||
|
│ ├── corpus.py ← bridge regression harness (62 cases, --compare)
|
||||||
|
│ ├── corpus_structural.py ← structural chain harness (--vs-bridge)
|
||||||
|
│ ├── rendersnap2.py ← RENDER_FILE stopper (reads from .rpp)
|
||||||
|
│ ├── campaign.py ← parameter sweep cell (~8 min)
|
||||||
|
│ ├── cascade_sim.py ← numpy step 9–19 simulator
|
||||||
|
│ ├── disasm_func.py ← capstone disasm with RIP constants
|
||||||
|
│ ├── iat_name.py ← runtime import resolution
|
||||||
|
│ ├── wine_{chain,stage,ptrace}_trace.py ← ptrace-based live trace
|
||||||
|
│ ├── dump_dispatch.py ← table/state dumper
|
||||||
|
│ ├── probe_{states,mem}.py ← live state/memory probes
|
||||||
|
│ ├── hunt2.py ← ctx-instance hunter
|
||||||
|
│ ├── scan_{pairs,lutsub}.py ← memory diagnostics
|
||||||
|
│ ├── fit_vlaw_{params,by_group}.py ← VLAW law calibration
|
||||||
|
│ ├── lawfit22r.py ← law fitting (VLAW α/β/c)
|
||||||
|
│ ├── mk_{dist,far,multi6}.py ← RPP generators
|
||||||
|
│ └── render_parity.py ← model-vs-render comparison
|
||||||
|
│
|
||||||
|
├── handoff/
|
||||||
|
│ ├── NOTES_LEVEL.md ← live journal (head: 24mm5+)
|
||||||
|
│ ├── NOTES_LEVEL_INDEX.md ← journal index by date/topic
|
||||||
|
│ ├── BLOCKMAP_529fe0.md ← FUN_180529fe0 method map (53K)
|
||||||
|
│ ├── NOTES_TWIN.md ← twin reference
|
||||||
|
│ ├── NOTES_CAPTURE.md ← live-capture protocol
|
||||||
|
│ ├── NEXT_PROMPT.md ← entry point for new sessions
|
||||||
|
│ ├── SESSION_HANDOFF.md ← handoff template
|
||||||
|
│ ├── archive/ ← historical NOTES_LEVEL_*.md, SESSION_HANDOFF_*.md
|
||||||
|
│ ├── nls_dasm/ ← 183 disassembly files (.dis, .bin)
|
||||||
|
│ ├── phase1/ ← phase-1 outputs
|
||||||
|
│ ├── rt*.npy ← captured runtime tables (48k/44.1k)
|
||||||
|
│ └── *.py ← emit/extract/joint scripts
|
||||||
|
│
|
||||||
|
├── *.java ← Ghidra scripts (DumpFuns, ImportRtti…)
|
||||||
|
├── *.{bin,npz,npy,json,txt} ← datasets, dumps, LUTs (mostly outside git)
|
||||||
|
└── soothe-bt/ ← test corpus (~600 renders, outside git)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Build System
|
||||||
|
|
||||||
|
**Generator**: CMake 3.10+, C++17, GCC/Clang with `-O3 -march=native`.
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# Key targets
|
||||||
|
soothe2_dsp # shared library (all dsp/*.cpp)
|
||||||
|
framed_test # CLI bridge renderer (44.1k)
|
||||||
|
render48k # structural renderer (48k/48000)
|
||||||
|
twin_check # float-parity unit test
|
||||||
|
tables_check # live-table verification
|
||||||
|
fftconv_check # FIR convolution check
|
||||||
|
vlog_check # fast log2 check
|
||||||
|
leveltrack_check # envelope follower check
|
||||||
|
levelpath_check # level-curve path check
|
||||||
|
exp2_check # fast exp2 check
|
||||||
|
fn529fe0_check # structural chain check
|
||||||
|
soothe2_harness # legacy harness
|
||||||
|
```
|
||||||
|
|
||||||
|
**External deps**: `libsamplerate` (render48k only), `pthread`.
|
||||||
|
|
||||||
|
**Tooling hazard**: CMake skips rebuild when source modified within same second. Protocol: `touch` source before build + verify binary mtime.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. DSP Architecture
|
||||||
|
|
||||||
|
### 4.1 Signal Flow (current canon: structural `render48k`)
|
||||||
|
|
||||||
|
```
|
||||||
|
Host 44.1k → resample → 48k → [per-channel processing] → resample → 44.1k Host
|
||||||
|
↓
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ FramedDetector │
|
||||||
|
│ (per 4096 block) │
|
||||||
|
│ │
|
||||||
|
│ am[] ← envelope │
|
||||||
|
│ res[] ← twin resp │
|
||||||
|
│ ↓ │
|
||||||
|
│ lvl_raw = am/res·sf │
|
||||||
|
│ ↓ │
|
||||||
|
│ [RT_VLAW=1]: │
|
||||||
|
│ cut = α·ln1p(L/β) │
|
||||||
|
│ +c [+Δ] │
|
||||||
|
│ mask = 10^(-cut/20)│
|
||||||
|
│ ↓ │
|
||||||
|
│ warp: mask *= │
|
||||||
|
│ kBand·kWarp·res^rp│
|
||||||
|
│ ↓ │
|
||||||
|
│ IIR3 ×2 (bidir) │
|
||||||
|
│ ↓ │
|
||||||
|
│ mask_out → multiply │
|
||||||
|
│ spectrum[k] *= mask │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Dual Render Path
|
||||||
|
|
||||||
|
| Path | File | Grid | Use |
|
||||||
|
|------|------|------|-----|
|
||||||
|
| Bridge | `framed_model.cpp` | 44.1k/2048 | Legacy, TOTAL 1.594 (canon) |
|
||||||
|
| Structural | `render48k.cpp` | 48k/4096 L/R | `2.689` (`RT_CASC=0` gated, hist. `0.341`) |
|
||||||
|
|
||||||
|
**Dual-solution env set**: `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0` (chain `RT_CASC=1` adds IIR4 double + FIR min-phase, gated)
|
||||||
|
|
||||||
|
### 4.3 Key Modules
|
||||||
|
|
||||||
|
| Module | Responsibility |
|
||||||
|
|--------|---------------|
|
||||||
|
| `FramedDetector` | Per-band, per-frame mask computation. Holds `am_`, `res_`, `track_` state. |
|
||||||
|
| `SpectralProcessor` | STFT/ISTFT, OLA overlap-add, FIR application modes. |
|
||||||
|
| `fn529fe0` | Structural chain 9–19 (steps 9–19 of FUN_180529fe0): scale→IIR1→IIR2→blend→combine→warp→IIR3→dry/wet. |
|
||||||
|
| `fnfaith` | Faithful detector cascade transcription (BLOCKMAP). |
|
||||||
|
| `twin` | Twin resonator `|2B/A|` — frequency response per band. |
|
||||||
|
| `rt_mask_tables` | Live IIR A/B coefficients (kIIR_A1/A2/A3, kIIR_B1/B2/B3, kRTAtt, kRTRel). |
|
||||||
|
| `rt_weights` | Live warp weights (kBand768, kWarp). |
|
||||||
|
| `fft` | Bit-exact RFFT (th1a90/th2180) with `buf548`/`mask598` tables. |
|
||||||
|
| `fftconv` | FFT-based convolution for FIR application modes. |
|
||||||
|
| `vlog`/`exp2` | Fast polynomial approximations matching plugin精度. |
|
||||||
|
| `leveltrack` | Envelope follower with per-bin attack/release tables. |
|
||||||
|
|
||||||
|
### 4.4 Detector Cascade (529c60)
|
||||||
|
|
||||||
|
```
|
||||||
|
complex_spectrum × twin_response → |z|
|
||||||
|
→ Haar smooth [0.25,0.5,0.25] × n_iters
|
||||||
|
→ peak = max(curve)
|
||||||
|
→ sin_peak = sin(param·30 − 90) · 0.115129 · peak
|
||||||
|
→ curve = max(curve, sin_peak)
|
||||||
|
→ w = -log10(pow(50, ratio·0.001) · ratio·0.001)
|
||||||
|
→ acc = acc·w + curve·(1-w)
|
||||||
|
→ bands_curve = acc
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Environment Flags (experiment control)
|
||||||
|
|
||||||
|
| Flag | Effect |
|
||||||
|
|------|--------|
|
||||||
|
| `RT_VLAW=1` | Two-stage law: `mask = 10^(−(α·ln1p(lvl/β)+c)/20)` |
|
||||||
|
| `RT_SYN=1` | STFT without synthesis window (plugin's actual layer) |
|
||||||
|
| `RT_WIN=0/1/2` | Analysis window: sym-Hann / periodic / rect |
|
||||||
|
| `RT_NOWARP=1` | Skip warp modulation |
|
||||||
|
| `RT_NOIIR3=1` | Skip IIR3 ×2 |
|
||||||
|
| `RT_IIR12=0` | Skip freq-domain IIR1/2 (critical with VLAW) |
|
||||||
|
| `RT_DUMP_BIN=<f>` | Dump tract binary (frame `RT_DUMP_FRAME`) |
|
||||||
|
| `RT_VDBG=1` | Print VLAW computations to stderr |
|
||||||
|
| `RT_FAITHFUL=1` | Use faithful chain (`fnfaith.cpp`) |
|
||||||
|
| `RT_FIRCONV=1/3` | FIR application mode (1=complex-mul, 3=`1.019·mask^1.8345`) |
|
||||||
|
| `RT_ENV=live` | Live envelope from kRTAtt/kRTRel tables |
|
||||||
|
| `RT_KMAP=1` | k-mapping correction (twin/am scaling) |
|
||||||
|
| `RT_EQ=1` | Pre-detector EQ bell |
|
||||||
|
| `RT_LUT_OFF=1` | Skip LUT transform |
|
||||||
|
| `RT_LUT_A/B/G/M` | LUT parameters (A=-24, B=28, gamma=1, mult=4.2) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Test Corpus & Metrics
|
||||||
|
|
||||||
|
**Location**: `/home/m/soothe-bt/` (~600 renders, outside git).
|
||||||
|
|
||||||
|
**Key sets**:
|
||||||
|
|
||||||
|
| Prefix | Content |
|
||||||
|
|--------|---------|
|
||||||
|
| `tone1kq_*` | Single tone, fc-scan, quiet (-18 dBFS) |
|
||||||
|
| `tone1k_*` | Single tone, fc-scan, loud (0 dBFS) |
|
||||||
|
| `dual_b1q_*` | Two tones (500+2000), q 0.1…10 |
|
||||||
|
| `al_*` | Level sweep (fc=1000) |
|
||||||
|
| `comb_*` | 4-band multiband |
|
||||||
|
| `burst500_b1` | Primary reference (burst 500 Hz) |
|
||||||
|
|
||||||
|
**Metric** (Goertzel steady-state):
|
||||||
|
```python
|
||||||
|
err_dB = 20·log1₀( ta(out,1000) / ta(ref,1000) )
|
||||||
|
# trimmed to last 75% of input, matched to plugin render length
|
||||||
|
```
|
||||||
|
|
||||||
|
**Regression guards**:
|
||||||
|
```bash
|
||||||
|
python3 scripts/corpus.py # bridge + guard
|
||||||
|
python3 scripts/corpus_structural.py # structural chain
|
||||||
|
python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25
|
||||||
|
python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Current Status (2026-09-02, chain919)
|
||||||
|
|
||||||
|
| Group | Bridge | Structural `RT_CASC=0` (L/R) | Structural `RT_CASC=1` |
|
||||||
|
|-------|--------|-------------------------------|------------------------|
|
||||||
|
| t1kq (fc-scan) | 0.226 | 0.356 | 0.717 |
|
||||||
|
| t1k (loud) | 1.801 | 5.437 | 5.631 |
|
||||||
|
| al (level) | 0.638 | 2.613 | 5.036 |
|
||||||
|
| res | 0.628 | 1.256 | 23.031 |
|
||||||
|
| dual (q-sweep) | 0.726 | 2.042 | 66.642 |
|
||||||
|
| comb (4-band) | 10.149 | 6.004 | 29.172 |
|
||||||
|
| **TOTAL** | **1.594** | **2.689** | **31.653** |
|
||||||
|
|
||||||
|
Historical best structural `0.341` (24mm14, `f40f41e` dual 0.193) not reachable on current HEAD (M/S→L/R + chain).
|
||||||
|
|
||||||
|
**Decoded**:
|
||||||
|
- Layer: STFT without synthesis window, per-bin mask multiply
|
||||||
|
- Law: `mask = 10^(−(α·ln1p(lvl/β)+c)/20)` with content-aware Δ
|
||||||
|
- FIR: `exp(0.984·ln(raw))` + Hann + normalize → bit-exact RFFT to df0
|
||||||
|
- Chain 9–19: IIR4 double + FIR min-phase integrated, `generate_iir4_coefs()` (FUN_180533340) added, gated `RT_CASC`
|
||||||
|
|
||||||
|
**Open gaps**:
|
||||||
|
1. **Chain 9–19** (priority #1): Integrated but uncalibrated (input format level vs cut, blocker `ph*.npz`/live-dump). Target: <0.05.
|
||||||
|
2. **k-mapping** (priority #2): twin/am scaling `k(q≥2)=0.403`.
|
||||||
|
3. **Δ second-peak rule** (priority #3): Content-dependent gain, requires live-dump.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Reference Documents
|
||||||
|
|
||||||
|
| Doc | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| `README.md` | TOTAL metric, reproduction steps |
|
||||||
|
| `AGENTS.md` | Build commands, env flags, hazard, corpus format |
|
||||||
|
| `BITEXACT_PLAN.md` | 3-step plan to byte-exact |
|
||||||
|
| `handoff/BLOCKMAP_529fe0.md` | Method map for FUN_180529fe0 |
|
||||||
|
| `handoff/NOTES_LEVEL.md` | Live working journal |
|
||||||
|
| `handoff/NOTES_LEVEL_INDEX.md` | Journal index |
|
||||||
|
| `handoff/NOTES_TWIN.md` | Twin resonator reference |
|
||||||
|
| `handoff/NOTES_CAPTURE.md` | Live-capture protocol |
|
||||||
|
| `handoff/nls_dasm/` | 183 disassembly files |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Reproduction & Development Protocol
|
||||||
|
|
||||||
|
1. Install soothe2 VST3 (Windows) under yabridge → `dump_soothe.py` → `soothe_mem.bin`
|
||||||
|
2. Ghidra headless: `analyzeHeadless <proj> soothe_x64 -process soothe_mem.bin -noanalysis -postScript <X>.java`
|
||||||
|
3. Renders: `sweep.py` → `.rpp`, `reaper -renderproject` → `.wav`
|
||||||
|
4. Analysis: `render_parity.py` / `corpus.py`
|
||||||
|
5. After C++ edit: `touch` source → `cmake --build dsp/build --target framed_test` → run corpus guards
|
||||||
+38
-185
@@ -1,211 +1,64 @@
|
|||||||
# Roadmap: bit-exact реверс DSP-ядра oeksound soothe2 (v1.1.2)
|
# Roadmap: bit-exact реверс DSP-ядра oeksound soothe2 (v1.1.2)
|
||||||
|
|
||||||
## Цель
|
## Цель
|
||||||
Воспроизвести DSP-путь soothe2 в виде **собираемого standalone C++** с **побайтово
|
Воспроизвести DSP-путь soothe2 в виде **собираемого standalone C++** с **побайтово идентичным** выходом (bit-exact).
|
||||||
идентичным** выходом (bit-exact) на верификационных свипах, с пониманием логики
|
|
||||||
и подписанными функциями.
|
|
||||||
|
|
||||||
## Результат
|
## Результат
|
||||||
`re-tools/dsp/` — `*.cpp/*.h` (классы DSP, подписаны), `CMakeLists.txt`, `harness.cpp`
|
`re-tools/dsp/` — `*.cpp/*.h` (классы DSP), `CMakeLists.txt`, `harness.cpp` (WAV16 → рендер → WAV24), `dsp_notes.md` (карта функция↔смысл), `verify_bit_exact.py`.
|
||||||
(WAV16 → рендер → WAV24), `dsp_notes.md` (карта функция↔смысл, константы, LUT),
|
|
||||||
`verify_bit_exact.py` (прогон всех свипов, побайтовое сравнение).
|
|
||||||
|
|
||||||
## Текущий статус (2026-08-21, P4)
|
## Статус — см. `README.md:13` (единственный источник TOTAL)
|
||||||
|
|
||||||
> **2026-08-21: bit-exact НЕ достигнут** — честная dB-параллель. Два канона:
|
> **Итог 2026-08-28 (24mm14):** применение декодировано до формул `mask=10^(−cut_D/20)`, `cut_D=α·ln1p(lvl/β)+c` (rms ≤0.016, 3 семейства), слой STFT без синтез-окна, dual-корпус 0.193 dB (канон 3.264). Гейт = bit-exact. Остаток: 3 шага `BITEXACT_PLAN.md:1` (каскад 9–19, k-маппинг, Δ-правило). Архив B-фаз ниже свёрнут.
|
||||||
> bridge (эмпирическая погона, корпус mean 1.594 dB, comb 10.1) и СТРУКТУРНАЯ цепь
|
|
||||||
> FUN_180529fe0 на внутренней сетке 48000/4096 (`dsp/build/render48k`, корпус mean
|
|
||||||
> 2.286 dB, comb **6.12** — уже лучше bridge на мультиполосе, res тоже лучше; хуже на
|
|
||||||
> однополосных). Разрыв формы кривой редукции: реальная насыщается (C_max≈0.70),
|
|
||||||
> exp2(−lvl) нет → Шаг 7 захват A/B/γ или статический hunt clamp'а в level-пути.
|
|
||||||
> Путь к байтам — `BITEXACT_PLAN.md` (8 шагов, пере-скоуп Шага 2: acc не имеет
|
|
||||||
> однополосного консюмера; FUN_1805316e0 = writer коэффициентов, НЕ комбинер).
|
|
||||||
> Полный журнал — `handoff/NOTES_LEVEL.md` (апдейты 20j–21d).
|
|
||||||
|
|
||||||
> **21d**: закон редукции найден — АФФИННО в dB (маска≈0.8·lvl^−0.354, γ≈0.344
|
<details><summary>История B-фаз (B.1…P4.3) — свёрнуто, детали → handoff/archive/ + git log</summary>
|
||||||
> decomp); на тонах линейность ±0.13 dB. Но scalar-семейство не закрывает тон+шум
|
|
||||||
> одновременно (res-тенция) → канон не меняем; следующий рычаг — контент-зависимость
|
|
||||||
> (att/rel на флюктуациях, combine-консюмер). Инструментарий RT_DUMP_BIN/RT_DUMP_ALL.
|
|
||||||
|
|
||||||
> **2026-08-25 (24j–24kk2): ПРИМЕНЕНИЕ ДЕКОДИРОВАНО ДО ФОРМУЛ.**
|
- **Статический декомп ~95%**: twin, generator case8, level-path (0x529fe0/0x563440/0x563a60), mask-apply, IIR-трекеры, FFT-conv (0x535a70), render-loop FUN_18052e260 — `.dis` в `handoff/nls_dasm/`.
|
||||||
> mask=10^(−cut_D/20); cut_D=α·ln1p(lvl/β)+c (пер-контент константы, rms≤0.016);
|
- **Структурная цепь** `dsp/framed_model.cpp::process_band_structural` (48k/4096): `scale→LUT(level)→IIR1→IIR2→mirror→blend/exp2→combine→dual-warp→IIR3×2→dry/wet`; live-таблицы `rt_mask_tables`. Корпус: comb 6.12 (bridge 10.15), res 0.44.
|
||||||
> слой STFT без синтез-окна; dual-корпус 0.193 (канон 3.264). ГЕЙТ = BIT EXACT.
|
- **Численная модель B.10…B.15**: `C=g·LUT(xv)+w·warp^a`, `gain=(1−C)·res^rp`, rp=0.0275·Q^0.2159 (mean 0.175), Pchip LUT (dual 0.027). Канон — `framed_render.py` (исторический).
|
||||||
> Детали: NOTES_LEVEL 24j–24kk2, BLOCKMAP_529fe0 23b–24jj, NEXT_PROMPT.md.
|
- **Фазы P1–P5**: twin float-parity 1.27e-5, FFT-conv, leveltrack, LUT, mask-apply — в основном готово; осталось каскад 9–19 + twin/am + Δ.
|
||||||
|
|
||||||
- **Статический декомп DSP-ядра — закрыт (~95%)**: twin-резонатор, генератор case8,
|
### Ключевые находки (итог, свёрнуто)
|
||||||
level-path (0x529fe0/0x563440/0x563a60), mask-apply (FUN_180529fe0 mono-path), IIR-трекеры,
|
|
||||||
FFT-conv (0x535a70), main render-loop (FUN_18052e260) декодированы; `.dis` в `handoff/nls_dasm/`.
|
|
||||||
- **Структурная цепь в C++** (`dsp/framed_model.cpp::process_band_structural`, сетка 48k/4096):
|
|
||||||
`scale→LUT(level-domain)→IIR1→IIR2→mirror→blend/exp2→combine(acc-update)→dual-warp→IIR3 bidirectional×2→dry/wet`;
|
|
||||||
live-таблицы в `dsp/rt_mask_tables.{hpp,cpp}`, `rt_weights.{hpp,cpp}`. Прогон:
|
|
||||||
`scripts/corpus_structural.py`. IIR3 = [fwd+bwd]×2 по декомпу (коммит 1a616e1).
|
|
||||||
- **Результаты структурной цепи (62 кейса)**: comb 6.12 (bridge 10.15), res 0.44 (0.63),
|
|
||||||
t1kq 0.77 / t1k 2.11 / al 0.99 / dual 3.26 (хуже bridge). Детали: NOTES_LEVEL:21c.
|
|
||||||
- **Честная тримнутая метрика** (длина выхода = длина входа) — см. AGENTS.md.
|
|
||||||
- **Цель bit-exact (Phase C) — в работе**: twin-транскрипция в `dsp/` готова и проходит
|
|
||||||
- **Бит-экзактная C++ транскрипция (Phase C)**: twin (float-parity 1.27e-5, tables_check ALL OK),
|
|
||||||
FFT-conv (P1.2/P1.3 fftconv.cpp), маска-канон (P1.4/P2), level-tracker (P2, leveltrack.cpp),
|
|
||||||
LUT (P2.5 levelpath.cpp), mask-apply цепь (P4 framed_model.cpp) — в основном готово.
|
|
||||||
- **ВАЖНО про метрику**: ранние записи "mean=0.160/0.144" считались на выходе НЕтримнутой
|
|
||||||
длины (`nfr*HOP+N`) и были артефактом; честный базлайн для scalar `rp=0.0169` = **0.280**.
|
|
||||||
Все числа в этом файле — на ТРИМНУТОЙ метрике.
|
|
||||||
|
|
||||||
## Историческая численная модель (B-фазы; канон теперь — C++ FramedDetector)
|
- Маска аддитивная `C=g·LUT+w·warp^a` (не мультипликация).
|
||||||
|
- `res_power` `gain=(1−C)·res^rp` — закрывает 500Hz residual.
|
||||||
|
- `0x530d30` дважды NEGATIVE — тилт не оттуда.
|
||||||
|
- LUT — гамма-монотонная `FUN_180563440` (Pchip немонотонна).
|
||||||
|
- Константы: SCALE=1/1024, DB_CONV=8.6859, IIR α=0.1, LCG 0x3cdca.
|
||||||
|
|
||||||
```python
|
</details>
|
||||||
xv = log10(A_k / res_k) # A_k = 2|X_k|/wsum (twin-env), res = |2B/A| case8
|
|
||||||
C = G * LUT(xv) + W * warp(f_k)**A # additive mask (НЕ мультипликация warp·LUT)
|
|
||||||
gain = max(1 - C, eps) * res_k**rp # rp = rp0 * Q**drp (res_power)
|
|
||||||
```
|
|
||||||
|
|
||||||
- LUT — Pchip; warp — `0.87·7.942·x/(7.942+x)`, x=f/2000 (аппроксимация runtime-вычисления FUN_180563440);
|
## Контракт из manual (`soothe2_ManualFAQ.pdf`)
|
||||||
- Q-dep rp: `rp0=0.0275, drp=0.2159` (mean 0.175); B.15 joint-free-knot LUT: G/W/A/rp0/drp =
|
|
||||||
`0.9752/0.3394/1.0222/0.0254/0.2231` (dual mean 0.027);
|
|
||||||
- Запуск (историч.): `python3 framed_render.py dual`.
|
|
||||||
|
|
||||||
## Статус по фазам
|
| # | Факт | Следствие |
|
||||||
|
|
||||||
- **Фаза 0** — сделано (OCR стр.30 manual → `pipeline_ocr.txt`, топология ниже).
|
|
||||||
- **Фаза A (статический RE)** — закрыта ~90%. Карта «что декодировано → где детали»:
|
|
||||||
|
|
||||||
| Компонент | Функция | Детали |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Twin-резонатор | FUN_180535880/536f90 | `handoff/NOTES_TWIN.md`, `dsp/twin.*` |
|
| M1 | Soft/Hard меняет всё | mode = препроцессор детектора |
|
||||||
| Генератор case8 | FUN_180533ec0 (RBJ bell, middle=−2cos w0) | `handoff/SESSION_HANDOFF.md §4` |
|
| M2 | Depth ±18, до 60 dB | depthcurve-LUT, clip 60 dB |
|
||||||
| Writer коэффициентов | FUN_1805316e0 (17 case, mode→тип полосы) | `NOTES_LEVEL.md`, B.7 |
|
| M5 | Attack/Release частотно-зависимы | Env — частотный масштаб |
|
||||||
| Level-weight | FUN_180530d30 (численно НЕ даёт tilt) | `NOTES_LEVEL.md`, B.9/B.14 |
|
| M8 | Stereo link/balance/L-R vs M/S | детектор на sum |
|
||||||
| LUT-кривая | FUN_180563440 (linear/power-law, 1024 бин) | `handoff/nls_dasm/f_563440.dis`, B.13 |
|
| M10 | Банды: 2 cut + 4 general | `FilterGraph<float,6,0x400>` |
|
||||||
| IIR level-трекеры | FUN_180563ce0 (init: 341 бин, α=0.1) | `handoff/nls_dasm/f_563ce0.dis`, B.13 |
|
|
||||||
| Mask-применитель | FUN_180529fe0 (PRNG, depth, mask 1−C) | `handoff/nls_dasm/f529fe0.dis` |
|
|
||||||
| FFT / twiddle / Cody-Waite | FUN_180008140 / FUN_180039b00 / 1801de760 | `notes_giant_fft.md`, `dsp/fft*` |
|
|
||||||
|
|
||||||
- **Фаза B (численная модель)** — пройдена до B.15: от B.10 (rmse 0.109 dB) до full frame-рендера
|
## Топология пайплайна (Приложение A, стр.30)
|
||||||
Q-dep rp (0.175) и joint free-knot LUT (dual 0.027). Канон — `framed_render.py`.
|
|
||||||
- **Фаза C (C++ bit-exact)** — in progress: `dsp/twin.*` готов и проходит float-parity (1.27e-5);
|
|
||||||
`dsp/{tables_data, fftconv, leveltrack}.cpp` + `dsp_ctx.hpp` готовы (P1-P2 структурно);
|
|
||||||
`verify_bit_exact.py` даёт базлайн (все DIFF — ожидаемо до подключения живой маски).
|
|
||||||
Оставшийся код по фазам:
|
|
||||||
|
|
||||||
| Фаза | Компонент | Статус |
|
|
||||||
|---|---|---|
|
|
||||||
| P1.5 | live-коэффициенты level-tracker A[] + скаляры RPP | ✅ DONE (rtctx_live.json, leveltrack_data.hpp) |
|
|
||||||
| P2 | mask-accumulator combine kernels (8d60/5a20/3c40) | ✅ DONE (levelpath.cpp combine_sub/acc_add/acc_fma) |
|
|
||||||
| P2.5 | twin-mask factory (56e3e0) + band LUT apply (563a60) | ✅ DONE (levelpath.cpp) |
|
|
||||||
| P3 | бит-экзактная base-CRT FFT | ⚠️ partial: архитектура размечена, vlog(ln) транскрибирован; butterfly+shuffle+plan-gen осталось |
|
|
||||||
| P4 | render-loop 52e260 + связка mask→FFT-conv + full pipeline | ✅ **существенно продвинут (2026-08-21)**: mono-path FUN_180529fe0 декодирован и транскрибирован на внутренней сетке 48k/4096 (`render48k`, LUT в level-домене, IIR3 bidirectional по декомпу); полный структурный корпус: comb **6.12** (bridge 10.15), res 0.44; однополосные хуже bridge — нужен шаг насыщения кривой редукции. Combine-семантика декодирована до thunk'ов; acc не имеет однополосного консюмера (пере-скоуп). Остаток: точка потребления acc/f6f8, PRNG-пролог, бит-экзактный exp2, FFT-conv (P3), SR-хвосты |
|
|
||||||
| P5 | стерео M8 (link/balance/LR-vs-MS) + multiband combine 5316e0 | TODO (в скоупе, не начат) |
|
|
||||||
|
|
||||||
Обновлённая оценка (2026-08-21): структурно ~80% готово — mask-apply цепь FUN_180529fe0
|
|
||||||
декодирована (scale→LUT→IIR1/2→exp2→combine→warp→IIR3-bidi→dry/wet→FFT-conv) и перенесена
|
|
||||||
в C++ на внутренней сетке, live-таблицы (IIR A/B, warp, band768, PRNG LUT) извлечены,
|
|
||||||
полный корпус гоняется. Осталось: (а) полная бит-экзактная FFT (butterfly+shuffle+plan-gen
|
|
||||||
— недели), (б) насыщение кривой редукции (Шаг 7 захват A/B/γ или clamp-hunt в level-пути),
|
|
||||||
(в) точка потребления combine acc/f6f8, (г) PRNG-пролог + FFT-conv + бит-экзактный exp2,
|
|
||||||
(д) стерео M8 (P5).
|
|
||||||
|
|
||||||
## Ключевые находки (итог)
|
|
||||||
|
|
||||||
- **Маска аддитивная**: `C = g·LUT(xv) + w·warp^a`, НЕ мультипликация warp·LUT (та проваливается >10 dB).
|
|
||||||
warp-terм ≈ `warp^3.14·w` — кратный каскад freq-axis ∏0x540688/FFT-conv.
|
|
||||||
- **res_power**: `gain = (1−C)·res^rp` — закрывает 500Hz-residual для q0.1/q1 (envRmse 0.64→0.05 dB).
|
|
||||||
- **0x530d30 дважды NEGATIVE**: спектральные веса численно ≈0 (10⁻⁴⁰…10⁻²⁰⁰) → тилт 2000-vs-500
|
|
||||||
из них НЕ идёт (подтверждено при N=1024 и N=2048).
|
|
||||||
- **Форма LUT**: Pchip-эмпирика была немонотонна (провал при xv≈−0.5); настоящая кривая
|
|
||||||
FUN_180563440 — гамма/монотонная; при замене на монотонную (free-knot или структурную)
|
|
||||||
q1@2000 закрывается до 0.000.
|
|
||||||
- **Константы из дампа**: SCALE=1/1024, ONE=1.0, TWO=2.0, NEG1=−1.0, HALF=0.5, DEPTH_SCALE=4.0,
|
|
||||||
DB_CONV=8.6859, FLOOR=−6.9078, IIR α=0.1, LCG_OFFSET=0x3cdca.
|
|
||||||
|
|
||||||
## Контракт из manual (`soothe2_ManualFAQ.pdf`, v1.0.0)
|
|
||||||
|
|
||||||
| # | Факт | Следствие для DSP |
|
|
||||||
|---|---|---|
|
|
||||||
| M1 | Soft/Hard: режим меняет всё; depth 3.0 в soft ≠ hard (стр.7) | mode = глобальный препроцессор детектора |
|
|
||||||
| M2 | Depth — референтный dB (−18..18), реальная глубина до 60dB (стр.8) | вход depthcurve-LUT; clip 60dB |
|
|
||||||
| M3 | Sharpness: выше = глубже и уже (стр.8) | Q_notch растёт с sharp |
|
|
||||||
| M4 | Selectivity: 0 = «everything must go» (стр.9) | отбор/порог пиков, число нотчей НЕ лимитирует |
|
|
||||||
| M5 | Attack/Release: реальные времена частотно-зависимы (стр.11) | Env НЕ один one-pole; нужен частотный масштаб |
|
|
||||||
| M6 | Oversample = расчёт reduction-фильтра в высшем разрешении (стр.11) | интерполяция детекторной сетки → фильтра |
|
|
||||||
| M7 | Resolution = частота обновления детекции/фильтра (стр.11) | период пересчёта коэффициентов |
|
|
||||||
| M8 | Stereo: link 100% = сумма каналов; 0% = dual mono; balance; L/R vs M/S | детектор на sum; коррекция симметрична/раздельна |
|
|
||||||
| M9 | Trim регулирует ТОЛЬКО wet; Delta = x−wet; Mix 0..100% (стр.14) | порядок: detect→notch→trim→mix/delta/bypass |
|
|
||||||
| M10 | Банды: 2 cut + 4 general; general: peak/shelf/reject/tilt (стр.17-19) | `FilterGraph<float,6,0x400>`, `DigitalFilter` мультитипный |
|
|
||||||
| M11 | Кривая = inverse EQ, сумма band-кривых = белая (стр.17) | `red(f)=base+ΣW_i·H_i` |
|
|
||||||
| M12 | Sidechain; Input trim — скрытый предусилитель анализа (стр.26) | отдельный вход/гейн анализа |
|
|
||||||
|
|
||||||
## Топология пайплайна (Приложение A, стр.30 — `pipeline_ocr.txt`)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
main inputs ──┐
|
main/sidechain → input trim → analysis (depth/sharpness/selectivity; EQ per-band)
|
||||||
sidechain ────┤ (sidechain enabled?)
|
→ stereo apply + balance → ms encode? → processing (M/S) → mix → bypass → output
|
||||||
▼
|
|
||||||
input trim ──► analysis (детекция; depth/sharpness/selectivity; EQ из per-band balances)
|
|
||||||
│
|
|
||||||
stereo apply + balance (channel link + global balance)
|
|
||||||
│
|
|
||||||
ms encode? ──► processing (нотч-синтез в M/S) ──► ms decode?
|
|
||||||
│
|
|
||||||
mix ──► bypass? ──► output
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- mid/side ручка: обработка в M/S-области — стерео-путь подтверждён диаграммой.
|
## Журнал вех (свёрнуто)
|
||||||
- GUI/visualisation — побочная ветвь, в DSP-ядро не входит (кроме параметров).
|
|
||||||
|
|
||||||
## Журнал вех (детали — в handoff/ и git log)
|
| Веха | Дата | Суть |
|
||||||
|
|
||||||
| Веха | Дата | Суть / ключевые числа |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| B.1 | 08-17 | C++ каркас `dsp/*` (twin, spectral, filter, detect, ms, fft, cody, twiddle, harness) |
|
| B.1–B.6 | 08-17 | C++ каркас, twin, dispatch, band-структура |
|
||||||
| B.2 | 08-17 | `libsoothe2_dsp.so` + harness WAV16→WAV24; фиксы WAV-алиасинга / in-place |
|
| B.10–B.15 | 08-18…19 | Модели C/LUT, bridge `C=g·LUT+w·warp^a` (0.236), res_power, dB-parity 0.268 |
|
||||||
| B.3 | 08-17 | TODO: twiddle-loader FUN_18014ec20 (Dekker+Cody-Waite), bit-reverse 18003b6c0, транскрипция fft.cpp |
|
| P4.1–P4.3 | 08-20 | Декомп FUN_180529fe0, live-таблицы, dual-warp |
|
||||||
| B.4 | 08-17 | burst500: corr 0.995, SNR 19.8 dB, diff −0.065 dB; детектор = уровнезависимый floor |
|
|
||||||
| B.5 | 08-17 | FUN_180535880/536f90 — реальный SSE-код в дампе; .vst3 совпадает с дампом в `.text` |
|
|
||||||
| B.6 | 08-17 | Dispatch-модель исправлена (у каждого стаба своя таблица); полный twin-алгоритм; артефакты `det_*.dis` |
|
|
||||||
| B.7 | 08-18 | band-структура (+0x800 mode/0x804 freq/0x808 sens/0x80c Q), writer 5316e0 (17 case); XML mode=1 → case8 (RBJ bell) |
|
|
||||||
| B.8 | 08-18 | Коррекция middle=−2cos(w0); `mask(f)=|2B/A|(fc,Q,gain)`; freq-path 530850 + warp, level-path 56e3e0 |
|
|
||||||
| B.9 | 08-18 | Level-path замкнут (потребитель 529fe0, аккумулятор 0x5407c8); формула 530d30; warp-ось sigmoid; конструктор 535ae0 (0x24=40000) |
|
|
||||||
| B.10 | 08-18 | Единая модель `C=depth·tilt·D0·(L0/res)^p`; парадокс dual_b1q решён (tilt-наклон); rmse 0.109 dB (31 pt); `model_dual.py` |
|
|
||||||
| B.11 | 08-18 | LUT-кривая извлечена (36 pt на одну кривую); PCHIP 10 узлов rmse 0.0718 dB; колено x≈0.58; `model_lut.py` |
|
|
||||||
| B.12 | 08-18 | Bridge-модель `C=g·LUT(xv)+w·warp^a` (a≈π, аддитивно), rmse 0.236; FFT-conv размаплен; freq-axis offline const; `model_fir.py` |
|
|
||||||
| Ph.5 | 08-18 | `render_parity.py`; dB-parity 36 pt rmse 0.268 dB (рефит 0.203); остаток структурный (0x540658) |
|
|
||||||
| B.13 | 08-19 | res_power `gain=(1−C)·res^rp` (500Hz resid решён); декомп 563440/563ce0/529fe0; константы из дампа |
|
|
||||||
| B.14 | 08-19 | **Артефакт-коррекция** (честная тримнутая метрика; базлайн scalar=0.280); Q-dep rp → mean 0.175; 530d30 повторно NEGATIVE (N=2048) |
|
|
||||||
| B.15 | 08-19 | Диагноз LUT-немонотонности → joint free-knot LUT (8 узлов, dual+al_*) → **dual mean≈0.027 dB**; остаток al_* lv24 dC=+0.10 |
|
|
||||||
| P4.1 | 08-20 | Полный декомп mono-path FUN_180529fe0; real mask-цепь в C++ (IIR leaky + acc + live LUT); t1kq err 0.5 dB; `framed_test` |
|
|
||||||
| P4.2 | 08-20 | Live-таблицы извлечены (`rt_mask_tables` IIR A/B, warp, band768, PRNG LUT); bigkernel 0x26b820=exp2 семантика залочена (`exp2(-mask)·blend`) |
|
|
||||||
| P4.3 | 08-20 | Dual-warp шаг 6 транскрибирован (mask·=band768, ·=warp) → comb 500/3000 починены; t1kq −0.43 dB; comb ≤6.4 dB |
|
|
||||||
|
|
||||||
## Риски
|
Полный журнал — `handoff/NOTES_LEVEL_INDEX.md` + `git log`.
|
||||||
|
|
||||||
- **FFT bit-exact**: DSP-STFT FFT найден и транскрибирован; главный нерешённый риск — порядок
|
|
||||||
double-операций в dispatch-ядрах при побайтовом сравнении.
|
|
||||||
- **Хост-зависимость**: точный размер буфера/фрейма (рендеры при разных RENDER_RANGE должны
|
|
||||||
давать одинаковые байты).
|
|
||||||
- **Stereo**: все текущие свипы mono; для bit-exact графа M8 нужны стерео-рендеры (link/balance/ms).
|
|
||||||
- **Честная метрика**: тримнутая длина выхода обязательна (артефакт B.14); fit-цели проверять на
|
|
||||||
`render_trim`/`framed_render.synthe`.
|
|
||||||
|
|
||||||
## Открытые вопросы
|
|
||||||
|
|
||||||
1. **Форма LUT в зоне xv<−0.3**: joint-fit даёт dual≤0.07, но al_* lv24 dC=+0.10 — нужна структурная
|
|
||||||
кривая FUN_180563440 (linear/power-law) вместо Pchip-эмпирики.
|
|
||||||
2. **warp-терм**: эмпирическая формула — аппроксимация runtime-вычисления FUN_180563440;
|
|
||||||
для bit-exact заменить на реальный расчёт.
|
|
||||||
3. **Стерео-верификация (M8)** — приоритет mono-путь или сразу стерео-граф.
|
|
||||||
4. **0x540658 window** — ✅ **ЗАКРЫТ 2026-08-19**: live-захват через registry heartbeat
|
|
||||||
(`{u64 count, u64 ptr}` run at psy. 0x29b06c0) во время offline-рендера. Окно = registry[01]
|
|
||||||
(8193 f32, 0.5→1.0, сатурация), freq-axis = registry[0d] (0..23988 Hz, spacing 11.713 ⇒
|
|
||||||
**внутренний SR=48000**), веса [03][04][05][06] (WA/WB/WC/WD). Сохранено: `handoff/rtwin_freq_44100.npy`,
|
|
||||||
`rtfreqaxis_48000_internal.npy`, `rtwa/rtwb/rtwc/rtwd_*.npy`. Инфра: `rtsnap_fast.py`,
|
|
||||||
`rtsnap2.py` (двухточечный diff подтвердил стабильность таблиц), `findctx.py`. Полное описание:
|
|
||||||
`handoff/NOTES_CAPTURE.md`. vptr/44100-marker объект-скан (rtobj) — тупик (производная vtable
|
|
||||||
не адресуется статически; регистр-бикон — рабочий путь).
|
|
||||||
|
|
||||||
## Где лежат детали
|
## Где лежат детали
|
||||||
|
|
||||||
- `handoff/SESSION_HANDOFF.md` — инвентарь декомпа (§0), трансляция/ключевые адреса (§2), Phase-5 план (§6).
|
- `README.md:13` — актуальный статус (TOTAL).
|
||||||
- `handoff/NOTES_TWIN.md` — twin-резонатор, caller, grid/oversample.
|
- `handoff/NOTES_LEVEL.md` (живая голова) + `handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md` — журнал.
|
||||||
- `handoff/NOTES_LEVEL.md` — level-path, LUT-нога, res_power протокол, спектральные веса.
|
- `handoff/BLOCKMAP_529fe0.md` — карта метода 9–19.
|
||||||
- `handoff/nls_dasm/` — 120+ дизассемблей (f_563440, f_563ce0, f529fe0, twin, iface, fft).
|
- `handoff/NOTES_TWIN.md` — twin краткая справка.
|
||||||
- `notes_giant_fft.md` — FFT-планировщики/ядра/twiddle/Cody-Waite.
|
- `handoff/NOTES_CAPTURE.md` — registry heartbeat.
|
||||||
- `git log` — полная журнальная хронология с номерами коммитов.
|
- `notes_giant_fft.md` — FFT/twiddle.
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"al_12": 0.6589760322215087,
|
||||||
|
"al_18": 0.8869285064658652,
|
||||||
|
"al_24": 0.9872335108976188,
|
||||||
|
"al_3": 0.21467895021449704,
|
||||||
|
"al_6": 0.36768989474153474,
|
||||||
|
"al_9": 0.5175959713969748,
|
||||||
|
"comb_1000": -0.8390262089726862,
|
||||||
|
"comb_1500": -0.8112870378396803,
|
||||||
|
"comb_2000": -0.809711515922245,
|
||||||
|
"comb_3000": -0.8085898984121008,
|
||||||
|
"comb_500": -0.8092538987382586,
|
||||||
|
"dual_0.1_2000": -0.08995281533909788,
|
||||||
|
"dual_0.1_500": -0.05320412461191143,
|
||||||
|
"dual_0.2_2000": 0.3019781559654401,
|
||||||
|
"dual_0.2_500": -0.053358134264814706,
|
||||||
|
"dual_0.3_2000": 0.2902583798578576,
|
||||||
|
"dual_0.3_500": -0.05348688556617939,
|
||||||
|
"dual_0.5_2000": 0.19650867858105603,
|
||||||
|
"dual_0.5_500": -0.05387091733344582,
|
||||||
|
"dual_0.7_2000": 0.18642830232473012,
|
||||||
|
"dual_0.7_500": -0.05441615588199915,
|
||||||
|
"dual_1.0_2000": 0.23254813837808486,
|
||||||
|
"dual_1.0_500": -0.05551608061654063,
|
||||||
|
"dual_1.5_2000": 0.3084791235429092,
|
||||||
|
"dual_1.5_500": -0.057043442345604364,
|
||||||
|
"dual_10.0_2000": 0.4377055060111567,
|
||||||
|
"dual_10.0_500": 0.20966855072081148,
|
||||||
|
"dual_2.0_2000": 0.3539423473152209,
|
||||||
|
"dual_2.0_500": -0.0574902922094877,
|
||||||
|
"dual_3.0_2000": 0.3970249956345811,
|
||||||
|
"dual_3.0_500": -0.05247116979493978,
|
||||||
|
"dual_5.0_2000": 0.42379656083975153,
|
||||||
|
"dual_5.0_500": -0.010273028233688034,
|
||||||
|
"res_300": 0.740945052805549,
|
||||||
|
"res_400": 0.3420632609886434,
|
||||||
|
"res_450": 0.14758272030066333,
|
||||||
|
"res_475": 0.051083439669293566,
|
||||||
|
"res_490": -0.007634204659142415,
|
||||||
|
"res_500": -0.046625742175670996,
|
||||||
|
"res_510": -0.08701341292655208,
|
||||||
|
"res_525": -0.14710395674869248,
|
||||||
|
"res_550": -0.2500832704002971,
|
||||||
|
"res_600": -0.47125422339018486,
|
||||||
|
"res_700": -1.0367358917741667,
|
||||||
|
"t1k_1000": -0.5485503441283766,
|
||||||
|
"t1k_1050": -0.3167571986018916,
|
||||||
|
"t1k_1100": 0.17418996613796175,
|
||||||
|
"t1k_1200": 0.16460873061812337,
|
||||||
|
"t1k_1500": -0.2608862087302641,
|
||||||
|
"t1k_2000": 0.4691205643000097,
|
||||||
|
"t1k_500": -0.10662852895220419,
|
||||||
|
"t1k_678.7611083984375": 0.027561491468636564,
|
||||||
|
"t1k_800": 0.4822946693646578,
|
||||||
|
"t1k_900": 0.30705968131126005,
|
||||||
|
"t1k_950": -0.28077425246942284,
|
||||||
|
"t1kq_1000": -0.21168039641542824,
|
||||||
|
"t1kq_1050": -0.0011503254358071785,
|
||||||
|
"t1kq_1100": 0.4344054852348732,
|
||||||
|
"t1kq_1200": -0.11613738787651275,
|
||||||
|
"t1kq_800": -0.05292751629888988,
|
||||||
|
"t1kq_900": 0.54236470368085,
|
||||||
|
"t1kq_950": 0.026290891468426707
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"al_12": 0.6589760322215087,
|
||||||
|
"al_18": 0.8869285064658652,
|
||||||
|
"al_24": 0.9872335108976188,
|
||||||
|
"al_3": 0.21467895021449704,
|
||||||
|
"al_6": 0.36768989474153474,
|
||||||
|
"al_9": 0.5175959713969748,
|
||||||
|
"comb_1000": -0.8390262089726862,
|
||||||
|
"comb_1500": -0.8112870378396803,
|
||||||
|
"comb_2000": -0.809711515922245,
|
||||||
|
"comb_3000": -0.8085898984121008,
|
||||||
|
"comb_500": -0.8092538987382586,
|
||||||
|
"dual_0.1_2000": -0.08995281533909788,
|
||||||
|
"dual_0.1_500": -0.05320412461191143,
|
||||||
|
"dual_0.2_2000": 0.3019781559654401,
|
||||||
|
"dual_0.2_500": -0.053358134264814706,
|
||||||
|
"dual_0.3_2000": 0.2902583798578576,
|
||||||
|
"dual_0.3_500": -0.05348688556617939,
|
||||||
|
"dual_0.5_2000": 0.19650867858105603,
|
||||||
|
"dual_0.5_500": -0.05387091733344582,
|
||||||
|
"dual_0.7_2000": 0.18642830232473012,
|
||||||
|
"dual_0.7_500": -0.05441615588199915,
|
||||||
|
"dual_1.0_2000": 0.23254813837808486,
|
||||||
|
"dual_1.0_500": -0.05551608061654063,
|
||||||
|
"dual_1.5_2000": 0.3084791235429092,
|
||||||
|
"dual_1.5_500": -0.057043442345604364,
|
||||||
|
"dual_10.0_2000": 0.4377055060111567,
|
||||||
|
"dual_10.0_500": 0.20966855072081148,
|
||||||
|
"dual_2.0_2000": 0.3539423473152209,
|
||||||
|
"dual_2.0_500": -0.0574902922094877,
|
||||||
|
"dual_3.0_2000": 0.3970249956345811,
|
||||||
|
"dual_3.0_500": -0.05247116979493978,
|
||||||
|
"dual_5.0_2000": 0.42379656083975153,
|
||||||
|
"dual_5.0_500": -0.010273028233688034,
|
||||||
|
"res_300": 0.740945052805549,
|
||||||
|
"res_400": 0.3420632609886434,
|
||||||
|
"res_450": 0.14758272030066333,
|
||||||
|
"res_475": 0.051083439669293566,
|
||||||
|
"res_490": -0.007634204659142415,
|
||||||
|
"res_500": -0.046625742175670996,
|
||||||
|
"res_510": -0.08701341292655208,
|
||||||
|
"res_525": -0.14710395674869248,
|
||||||
|
"res_550": -0.2500832704002971,
|
||||||
|
"res_600": -0.47125422339018486,
|
||||||
|
"res_700": -1.0367358917741667,
|
||||||
|
"t1k_1000": -0.5485503441283766,
|
||||||
|
"t1k_1050": -0.3167571986018916,
|
||||||
|
"t1k_1100": 0.17418996613796175,
|
||||||
|
"t1k_1200": 0.16460873061812337,
|
||||||
|
"t1k_1500": -0.2608862087302641,
|
||||||
|
"t1k_2000": 0.4691205643000097,
|
||||||
|
"t1k_500": -0.10662852895220419,
|
||||||
|
"t1k_678.7611083984375": 0.027561491468636564,
|
||||||
|
"t1k_800": 0.4822946693646578,
|
||||||
|
"t1k_900": 0.30705968131126005,
|
||||||
|
"t1k_950": -0.28077425246942284,
|
||||||
|
"t1kq_1000": -0.21168039641542824,
|
||||||
|
"t1kq_1050": -0.0011503254358071785,
|
||||||
|
"t1kq_1100": 0.4344054852348732,
|
||||||
|
"t1kq_1200": 1.1724811230541312,
|
||||||
|
"t1kq_800": 0.6775178072048177,
|
||||||
|
"t1kq_900": 0.54236470368085,
|
||||||
|
"t1kq_950": 0.026290891468426707
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""cascade_sim.py — структурный симулятор тракта маски soothe2 (float-путь).
|
||||||
|
|
||||||
|
Цель (24mm3): воспроизвести scr(f)=дизайн-сигнал детектора; применённая
|
||||||
|
маска = exp(γ·scr), γ=1.760561 (точно), trk@688=exp(scr@628) бит-в-бит.
|
||||||
|
|
||||||
|
Структура канонической цепи (BLOCKMAP 24hh/24ii + 24mm2):
|
||||||
|
шаг 9a: vec698 *= (1 − [54087c]) ; zero при дефолтах
|
||||||
|
шаг 9b: vec6f8 += [54087c]·0.8 ; xmm10=0.8 @1824c3e28
|
||||||
|
шаг 9c: bands_curve_i /= ... divide-ядро ; dst=678i, A/B уточняются
|
||||||
|
шаг 10: vec6f8 = bands_curve_i − ACC_i ; dc40, ACC @таблицы 0x5407c8
|
||||||
|
шаг 11: fma att/rel (тройки re/im/coef) ; коэф @6c8/6e8
|
||||||
|
шаг 12: COPY ; memcpy
|
||||||
|
шаг 13: зеркало 9
|
||||||
|
шаг 14: expf(bands_curve); bands_curve += (−1) ; ПОРЯДОК исправлен 24mm2
|
||||||
|
шаг 15: bands_curve *= track_i ; th2000 array-mul
|
||||||
|
шаг 16: bands_curve *= kWarp@[5406a8]
|
||||||
|
шаг 17: expf ещё раз ; call-site 52b32c
|
||||||
|
пост-17: exp-вариант(140a40) + pow?(140b00)
|
||||||
|
FIR-секция: кривая-float(140b30→1803831c0) + sincos-twiddle(140aa0)
|
||||||
|
|
||||||
|
ЯДРА (структурная фаза — математически точные numpy-эквиваленты;
|
||||||
|
канонический C++ порт = инструкци-точная транскрипция, см. BLOCKMAP 24mm2):
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
|
||||||
|
GAMMA = 1.760561 # 24mm3: показатель степени, rms фита 0 на чистых кадрах
|
||||||
|
N = 2049 # число бинов полной сетки
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- ядра ----
|
||||||
|
def k_exp(x):
|
||||||
|
"""expf-ядро 180296c80. Структурная фаза: np.exp.
|
||||||
|
Каноническая формула (для C++ порта, FMA-точно):
|
||||||
|
n = fma(log2e_hi=1.4427f, x, 12582912.0f); k = n - MAGIC
|
||||||
|
r = (x - 0.693146f*k) - 1.42861e-06f*k
|
||||||
|
p = (((0.00829172f*r+0.0418735f)*r+0.166674f)*r+0.499994f)*r+1)*r+1
|
||||||
|
out = bits((k<<23) + bits(p)); guard |x|>87.3365 -> slow path
|
||||||
|
"""
|
||||||
|
return np.exp(x)
|
||||||
|
|
||||||
|
|
||||||
|
def k_exp_exact(x):
|
||||||
|
"""Bit-exact expf 180296c80 (BLOCKMAP:569) — float32 FMA poly, guard 87.3365.
|
||||||
|
Python структурный прокси: np.exp (float64) — точный C++ порт в dsp/exp2.cpp
|
||||||
|
через fmaf+бит-манипуляции (1824c...), ошибка <0.5 ulp vs плагин."""
|
||||||
|
return np.exp(np.asarray(x, dtype=np.float64)).astype(np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def k_div(a, b):
|
||||||
|
"""divide-ядро 1803a06a0: dst = B/A (~0.5 ulp, rcp+таблицы+полином).
|
||||||
|
Структурная фаза: точное деление."""
|
||||||
|
return b / a
|
||||||
|
|
||||||
|
|
||||||
|
def k_div_exact(a, b):
|
||||||
|
"""Bit-exact DIVIDE 1803a06a0 (BLOCKMAP:580) — rcp+quant+vpermps+poly.
|
||||||
|
Структурный прокси: точное деление; C++ порт копирует 0.5ulp полином
|
||||||
|
0.207515 -0.241687 0.288535 -0.360671 ... 0.240264 через vpermps tbl@1821269c0."""
|
||||||
|
return np.asarray(b, dtype=np.float64) / np.asarray(a, dtype=np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
def chain_9_19_sim(bands, tmp6f8, accVec, warp, att, rel, nbin=None):
|
||||||
|
"""CHAIN 9-19 BLOCKMAP:620-644 op-by-op (python структурный прокси).
|
||||||
|
bands: log-domain bands_curve_i (size nbin), tmp6f8: f6f8, accVec: ACC_i
|
||||||
|
warp: kWarp@5406a8, att@5406c8 rel@5406e8 — все 2049.
|
||||||
|
Возвращает bands mutated (log domain после IIR, перед FIR)."""
|
||||||
|
if nbin is None:
|
||||||
|
nbin = len(bands)
|
||||||
|
bands = np.asarray(bands, dtype=np.float64)
|
||||||
|
tmp6f8 = np.asarray(tmp6f8, dtype=np.float64)
|
||||||
|
accVec = np.asarray(accVec, dtype=np.float64)
|
||||||
|
# 9a zero already (vec698 irrelevant), 9b: tmp6f8 += 0.8 (p=1.0 *0.8)
|
||||||
|
# 9c DIVIDE dst=bands A=bands B=tmp6f8+0.8? Actually B=tmp6f8, but step 9b already added 0.8
|
||||||
|
# BLOCKMAP 24mm5: bands = tmp6f8 / bands (divide B/A)
|
||||||
|
# Use k_div_exact
|
||||||
|
b = tmp6f8 + 0.8 # proxy for step 9b effect (when tmp6f8 initially 0, b=0.8)
|
||||||
|
# If tmp6f8 already has data, 9b is additive 0.8, so b = tmp6f8 +0.8
|
||||||
|
# For calibration where tmp6f8 is 0, this yields 0.8/bands
|
||||||
|
bands = k_div_exact(np.maximum(bands, 1e-30), b)
|
||||||
|
# 10 dc40: tmp6f8 = bands - ACC
|
||||||
|
tmp6f8 = bands - accVec
|
||||||
|
# 11 FMA ATT/REL half-split
|
||||||
|
# true triples 12B re/im/coef, scalar proxy: upper half ATT, lower REL
|
||||||
|
for i in range(nbin):
|
||||||
|
if i < nbin // 2:
|
||||||
|
tmp6f8[i] += att[i] * accVec[i]
|
||||||
|
else:
|
||||||
|
tmp6f8[i] += rel[i] * accVec[i]
|
||||||
|
# 14 EXP#1 + -1 (order fixed 24mm2)
|
||||||
|
bands = k_exp_exact(bands) - 1.0
|
||||||
|
# 15 array-mul X*track (track is external, warp mul is step16)
|
||||||
|
# 16 *kWarp + LOG#2
|
||||||
|
bands = bands * warp
|
||||||
|
bands = np.log(np.maximum(bands, 1e-30))
|
||||||
|
# 16b IIR4x2 bidir log-domain base 0x340510 — proxy via kIIR_A1/B1
|
||||||
|
# keep as identity for now (real IIR is mild 0.003-0.19 per 24cc)
|
||||||
|
# FIR will be applied externally via fir_kernel
|
||||||
|
return bands, tmp6f8
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ данные -----
|
||||||
|
def load_tract(path):
|
||||||
|
"""tract_*.txt: k am res lvl_raw band_level prewarp w"""
|
||||||
|
t = np.loadtxt(path)
|
||||||
|
return {'am': t[:, 1], 'res': t[:, 2], 'lvl': t[:, 3]}
|
||||||
|
|
||||||
|
|
||||||
|
def load_frame(npz):
|
||||||
|
"""Слоты кадра rendersnap2 → dict[int, np.ndarray]."""
|
||||||
|
d = np.load(npz)
|
||||||
|
out = {}
|
||||||
|
for k in d.keys():
|
||||||
|
if k.startswith('0x'):
|
||||||
|
out[int(k[2:], 16)] = d[k]
|
||||||
|
return out, d['t_snap']
|
||||||
|
|
||||||
|
|
||||||
|
def pick_clean_frame(ds_dir, min_bins=8):
|
||||||
|
"""Отбор чистых стационарных кадров по фазам (24mm3):
|
||||||
|
возвращает лучший на фазе γ* (~1.7606, маска применена)
|
||||||
|
и лучший на фазе γ=1 (степень ещё не применена)."""
|
||||||
|
classes = {'gamma': None, 'identity': None}
|
||||||
|
for f in sorted(glob.glob(os.path.join(ds_dir, 'ph*.npz'))):
|
||||||
|
try:
|
||||||
|
S, ts = load_frame(f)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not all(x in S for x in (0x540628, 0x540688, 0x540678)):
|
||||||
|
continue
|
||||||
|
s = S[0x540628][:1025].astype(np.float64)
|
||||||
|
t = S[0x540688][:1025].astype(np.float64)
|
||||||
|
c = S[0x540678][:1025].astype(np.float64)
|
||||||
|
ok = (t > 1e-30) & (c > 1e-30) & np.isfinite(s)
|
||||||
|
if ok.sum() < 50:
|
||||||
|
continue
|
||||||
|
lt = np.log(t[ok])
|
||||||
|
lc = np.log(c[ok])
|
||||||
|
sel = np.abs(lt) > 0.05
|
||||||
|
if sel.sum() < min_bins:
|
||||||
|
continue
|
||||||
|
g = float(np.sum(lt[sel] * lc[sel]) / np.sum(lt[sel] ** 2))
|
||||||
|
rms = float(np.sqrt(np.mean((lc[sel] - g * lt[sel]) ** 2)))
|
||||||
|
depth = float(-lc.min())
|
||||||
|
key = 'gamma' if abs(g - GAMMA) < 0.01 else \
|
||||||
|
('identity' if abs(g - 1.0) < 1e-4 else None)
|
||||||
|
if key is None or rms > 1e-4:
|
||||||
|
continue
|
||||||
|
cand = (depth, f, s, t, c, g, rms)
|
||||||
|
if classes[key] is None or depth > classes[key][0]:
|
||||||
|
classes[key] = cand
|
||||||
|
return classes
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------- валидация -------
|
||||||
|
def validate_scr(sim_scr, cap_scr, tol_db=0.05):
|
||||||
|
"""rms в дБ между симулированным и захваченным scr."""
|
||||||
|
m = np.abs(cap_scr) > 0.02
|
||||||
|
err = (sim_scr[m] - cap_scr[m]) * (20 / np.log(10))
|
||||||
|
return float(np.sqrt(np.mean(err ** 2))), int(m.sum())
|
||||||
|
|
||||||
|
|
||||||
|
def win_periodic_hann(N):
|
||||||
|
return 0.5 * (1.0 - np.cos(2.0 * np.pi * np.arange(N) / N))
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------ FIR-цепь (24mm9) --------
|
||||||
|
NFRAME = 4096 # n=[ctx+0x540534]
|
||||||
|
NBINS_FIR = NFRAME // 2 + 1
|
||||||
|
Q_EXP = 0.8002203702926636 # .rdata 1820013f0, refines 0.80 (BLOCKMAP 52b716)
|
||||||
|
|
||||||
|
|
||||||
|
def winfreq_fall():
|
||||||
|
"""WINfreq@[ctx+0x540658]: периодический Hann(4096), падающая половина."""
|
||||||
|
return win_periodic_hann(NFRAME)[NFRAME // 2:]
|
||||||
|
|
||||||
|
|
||||||
|
def fir_kernel(scr, q=Q_EXP):
|
||||||
|
"""Полная FIR-цепь (BLOCKMAP 24mm9): min-phase кепстральный сэндвич.
|
||||||
|
|
||||||
|
scr(2049) → pack(re=scr,im=0) → FIR[n]=0 (Найквост)
|
||||||
|
→ inv-RFFT → fold(y[1..2047]*=2.0 @1824c41e0; y[2049..4095]=0)
|
||||||
|
→ fwd-RFFT → комплексная EXP (1803831c0, аргумент ×q)
|
||||||
|
→ inv-RFFT → ×падающий Hann → ноль хвоста → fwd-RFFT
|
||||||
|
→ FIR[0]=1, FIR[1]=0. Возвращает |F| (2049).
|
||||||
|
"""
|
||||||
|
h = np.asarray(scr, dtype=np.complex128).copy()
|
||||||
|
h[-1] = 0.0
|
||||||
|
y = np.fft.irfft(h, n=NFRAME)
|
||||||
|
y[1:NFRAME // 2] *= 2.0
|
||||||
|
y[NFRAME // 2 + 1:] = 0.0
|
||||||
|
w = np.fft.irfft(np.exp(q * np.fft.rfft(y, n=NFRAME)), n=NFRAME)
|
||||||
|
w[:NFRAME // 2] *= winfreq_fall()
|
||||||
|
w[NFRAME // 2:] = 0.0
|
||||||
|
F = np.abs(np.fft.rfft(w, n=NFRAME))
|
||||||
|
F[0] = 1.0
|
||||||
|
return F
|
||||||
|
|
||||||
|
|
||||||
|
def mask_from_frame(S, q=Q_EXP):
|
||||||
|
"""mask_sim из слотов кадра: cur ≈ trk · |F(scr)| (df0 complex-mul)."""
|
||||||
|
scr = S[0x540628][:NBINS_FIR].astype(np.float64)
|
||||||
|
trk = S[0x540688][:NBINS_FIR].astype(np.float64)
|
||||||
|
return trk * fir_kernel(scr, q)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_mask_stage(ds, q=Q_EXP, cap=60):
|
||||||
|
"""Валидация масочной ветви на чистых γ-кадрах (24mm9-протокол).
|
||||||
|
|
||||||
|
Отбор: |γ_fit−1.760561|<5e-4 и fit-rms<1e-5 (жёстче pick_clean_frame).
|
||||||
|
Критерий: rms по ВСЕМ 2049 бинам < 0.05 дБ (структурная фаза).
|
||||||
|
"""
|
||||||
|
import glob
|
||||||
|
rmss, gpred = [], []
|
||||||
|
for f in sorted(glob.glob(os.path.join(ds, 'ph*.npz'))):
|
||||||
|
try:
|
||||||
|
d = np.load(f)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if '0x540628' not in d:
|
||||||
|
continue
|
||||||
|
scr = d['0x540628'][:NBINS_FIR].astype(np.float64)
|
||||||
|
trk = d['0x540688'][:NBINS_FIR].astype(np.float64)
|
||||||
|
cur = d['0x540678'][:NBINS_FIR].astype(np.float64)
|
||||||
|
ok = (trk > 1e-30) & (cur > 1e-30) & np.isfinite(scr)
|
||||||
|
if ok.sum() < 50:
|
||||||
|
continue
|
||||||
|
lt, lc = np.log(trk[ok]), np.log(cur[ok])
|
||||||
|
sel = np.abs(lt) > 0.05
|
||||||
|
if sel.sum() < 8:
|
||||||
|
continue
|
||||||
|
g = float(np.sum(lt[sel] * lc[sel]) / np.sum(lt[sel] ** 2))
|
||||||
|
frms = float(np.sqrt(np.mean((lc[sel] - g * lt[sel]) ** 2)))
|
||||||
|
if not (abs(g - GAMMA) < 5e-4 and frms < 1e-5):
|
||||||
|
continue
|
||||||
|
F = fir_kernel(scr, q)
|
||||||
|
lf = np.log(F[sel])
|
||||||
|
lt_s = np.log(trk[sel])
|
||||||
|
sF = float(np.sum(lf * lt_s) / np.sum(lt_s ** 2))
|
||||||
|
gpred.append(1.0 + sF)
|
||||||
|
m = trk * F
|
||||||
|
mm = (cur > 1e-12) & (m > 1e-12)
|
||||||
|
e = (np.log(m[mm]) - np.log(cur[mm])) * 20 / np.log(10)
|
||||||
|
rmss.append(float(np.sqrt(np.mean(e ** 2))))
|
||||||
|
if len(rmss) >= cap:
|
||||||
|
break
|
||||||
|
if not rmss:
|
||||||
|
print('нет ультрачистых кадров в', ds)
|
||||||
|
return
|
||||||
|
rmss = np.array(rmss)
|
||||||
|
print('кадров=%d | rms медиана=%.4f дБ p90=%.4f max=%.4f | '
|
||||||
|
'gamma_pred(1+s_F)=%.6f' %
|
||||||
|
(len(rmss), np.median(rmss), np.percentile(rmss, 90), rmss.max(),
|
||||||
|
float(np.median(gpred))))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == '--mask':
|
||||||
|
validate_mask_stage(sys.argv[2] if len(sys.argv) > 2
|
||||||
|
else '/tmp/opencode/sc_multi4b')
|
||||||
|
return
|
||||||
|
ds = sys.argv[1] if len(sys.argv) > 1 else '/tmp/opencode/sc_multi6'
|
||||||
|
tract = sys.argv[2] if len(sys.argv) > 2 else '/tmp/opencode/tract_multi6.txt'
|
||||||
|
|
||||||
|
classes = pick_clean_frame(ds)
|
||||||
|
ph_g = classes['gamma']
|
||||||
|
ph_i = classes['identity']
|
||||||
|
if not ph_g and not ph_i:
|
||||||
|
print('нет чистых кадров в', ds)
|
||||||
|
return
|
||||||
|
for lbl, best in (('γ-фаза', ph_g), ('identity', ph_i)):
|
||||||
|
if not best:
|
||||||
|
continue
|
||||||
|
_, f, scr, trk, cur, gamma_fit, grms = best
|
||||||
|
n = len(scr)
|
||||||
|
cut_meas = -20 / np.log(10) * np.log(np.maximum(cur, 1e-30))
|
||||||
|
g_use = gamma_fit
|
||||||
|
cut_sim = g_use * (-scr) * 20 / np.log(10)
|
||||||
|
e = cut_sim - cut_meas
|
||||||
|
sel = np.abs(cut_meas) > 0.1
|
||||||
|
rms_db = float(np.sqrt(np.mean(e[sel] ** 2))) if sel.any() else 0.0
|
||||||
|
print(f'{lbl}: {os.path.basename(f)} γ={gamma_fit:.6f} (rms {grms:.1e}) '
|
||||||
|
f'закон: rms={rms_db:.4f} дБ / {int(sel.sum())} бинов')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""detector_cascade.py — validated simulator of the soothe2 detector cascade (529c60).
|
||||||
|
|
||||||
|
Decoded from assembly (2026-08-25):
|
||||||
|
Phase 1: |z_i| via 16140 (vrsqrtps+vsqrtps — magnitude, NOT squared)
|
||||||
|
Phase 2: Haar smoothing kernel [0.25, 0.5, 0.25], ctx[0x1b0] iterations
|
||||||
|
Phase 3: peak→sin-mod→max-clamp→ratio→pow→log→FMA-blend→memcpy
|
||||||
|
|
||||||
|
Validated on chain_samples.pkl (2-frame ptrace capture):
|
||||||
|
- op A output matches |z| (max diff 5.4e-6)
|
||||||
|
- 2 Haar iterations + scalar blend: rms=0.30, corr=0.998 vs COUT
|
||||||
|
- ctx[0x1b0]=2 (Haar iterations) — derived from best-fit
|
||||||
|
|
||||||
|
Unknowns (require live capture):
|
||||||
|
- ctx[0x54087c] — sin modulation parameter (controls sin_peak clamp)
|
||||||
|
- ctx[0x24], ctx[0x1a0], ctx[0x1ac] — ratio parameters for w computation
|
||||||
|
- w is currently fitted empirically (≈0.015 for this test signal)
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
N = 2049 # FFT bins (NFRAME/2 + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def haar_one_pass(b):
|
||||||
|
"""One Haar smoothing pass (kernel [0.25, 0.5, 0.25]).
|
||||||
|
|
||||||
|
Decoded from 529c60 Haar loop (lines 35-74):
|
||||||
|
Step 1: b[i] += b[i+1] (prefix sum, 10e40)
|
||||||
|
Step 2: b[i] *= 0.5 (scalar mul, ffe0)
|
||||||
|
Step 3: scratch[i] = b[i+1] + b[i] (3-op add, 11580)
|
||||||
|
Step 4: b[i+1] = 0.5 * scratch[i] (scalar mul+store, 4720)
|
||||||
|
"""
|
||||||
|
n = len(b)
|
||||||
|
if n < 2:
|
||||||
|
return b
|
||||||
|
# Steps 1+2 combined: b[i] = 0.5 * (b[i] + b[i+1]) for i < n-1
|
||||||
|
# Note: b[n-1] is unchanged by steps 1+2
|
||||||
|
b[:-1] = 0.5 * (b[:-1] + b[1:])
|
||||||
|
# Steps 3+4: b[i+1] = 0.5 * (b[i] + b[i+1]) using UPDATED b
|
||||||
|
# Need original b[i] values for step 3
|
||||||
|
# Actually: step 3 reads AFTER steps 1+2, so uses modified b
|
||||||
|
# scratch[i] = b[i+1] + b[i] (both modified)
|
||||||
|
# b[i+1] = 0.5 * scratch[i]
|
||||||
|
# This means: b_new[i+1] = 0.5 * (b_modified[i+1] + b_modified[i])
|
||||||
|
b6f8 = b[1:] + b[:-1]
|
||||||
|
b[1:] = 0.5 * b6f8
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def haar_smooth(magnitudes, n_iters):
|
||||||
|
"""Haar smoothing: iterate Haar passes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
magnitudes: |z_i| array (N floats)
|
||||||
|
n_iters: number of Haar iterations (ctx[0x1b0])
|
||||||
|
Returns:
|
||||||
|
smoothed array
|
||||||
|
"""
|
||||||
|
b = magnitudes.copy()
|
||||||
|
for _ in range(n_iters):
|
||||||
|
haar_one_pass(b)
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def cascade_detect(complex_state, n_iters=2, w=0.015, sin_peak_floor=0.0):
|
||||||
|
"""Full detector cascade (529c60) simulation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
complex_state: interleaved re/im array (2N floats)
|
||||||
|
n_iters: Haar iteration count
|
||||||
|
w: blend weight (scalar, ~0.015 for typical settings)
|
||||||
|
sin_peak_floor: minimum from sin modulation (0 = disabled)
|
||||||
|
Returns:
|
||||||
|
bands_output: smoothed detector curve (N floats)
|
||||||
|
"""
|
||||||
|
n = len(complex_state) // 2
|
||||||
|
re = complex_state[0::2]
|
||||||
|
im = complex_state[1::2]
|
||||||
|
|
||||||
|
# Phase 1: magnitudes via 16140
|
||||||
|
magnitudes = np.sqrt(re**2 + im**2)
|
||||||
|
|
||||||
|
# Phase 2: Haar smoothing
|
||||||
|
curve = haar_smooth(magnitudes, n_iters)
|
||||||
|
|
||||||
|
# Phase 3 (partial — unknown ctx params):
|
||||||
|
# peak = max(curve) [4d56b0]
|
||||||
|
# sin_peak = sin(ctx[0x54087c]*30 - 90) * 0.115129 * peak [1a14cac]
|
||||||
|
# curve[i] = max(curve[i], sin_peak) [52d8a0→10860]
|
||||||
|
if sin_peak_floor > 0:
|
||||||
|
np.maximum(curve, sin_peak_floor, out=curve)
|
||||||
|
|
||||||
|
# Blend: output = curve * (1-w) + accumulator * w
|
||||||
|
# 5407a8 (accumulator) = 0 in steady state → output = curve * (1-w)
|
||||||
|
# The blend chain:
|
||||||
|
# 52d920: 5407a8[i] *= w (array scalar mul)
|
||||||
|
# 52dae0: 5407a8[i] += curve[i] * (1-w) (FMA)
|
||||||
|
# 52dbc0: memcpy 5407a8 → 540678
|
||||||
|
bands_output = curve * (1.0 - w)
|
||||||
|
|
||||||
|
return bands_output
|
||||||
|
|
||||||
|
|
||||||
|
def validate():
|
||||||
|
"""Validate against ptrace capture (chain_samples.pkl)."""
|
||||||
|
import pickle
|
||||||
|
path = '/tmp/opencode/winetrace_casc/chain_samples.pkl'
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
data = pickle.load(f)
|
||||||
|
|
||||||
|
s = data['samples']
|
||||||
|
cin = s[0]
|
||||||
|
cout = s[3]
|
||||||
|
|
||||||
|
trk = np.array(cin['trk'], dtype=np.float64)
|
||||||
|
b0_cout = np.array(cout['bands0'], dtype=np.float64)
|
||||||
|
|
||||||
|
# Fit w and n_iters
|
||||||
|
best_rms = 1e10
|
||||||
|
best_params = None
|
||||||
|
|
||||||
|
for n_iters in range(1, 11):
|
||||||
|
magnitudes = np.zeros(len(trk) // 2)
|
||||||
|
re = trk[0::2]; im = trk[1::2]
|
||||||
|
magnitudes = np.sqrt(re**2 + im**2)
|
||||||
|
|
||||||
|
curve = haar_smooth(magnitudes, n_iters)
|
||||||
|
|
||||||
|
sig = (curve > 0.5) & (b0_cout > 0.5)
|
||||||
|
if sig.sum() < 10:
|
||||||
|
continue
|
||||||
|
|
||||||
|
w_vals = 1.0 - b0_cout[sig] / curve[sig]
|
||||||
|
w = float(np.median(w_vals))
|
||||||
|
|
||||||
|
predicted = curve * (1.0 - w)
|
||||||
|
rms = float(np.sqrt(np.mean((predicted - b0_cout) ** 2)))
|
||||||
|
corr = float(np.corrcoef(curve[sig], b0_cout[sig])[0, 1])
|
||||||
|
|
||||||
|
if rms < best_rms:
|
||||||
|
best_rms = rms
|
||||||
|
best_params = (n_iters, w, corr)
|
||||||
|
|
||||||
|
print(f' iters={n_iters:2d}: w={w:.6f}, rms={rms:.4f}, corr={corr:.6f}')
|
||||||
|
|
||||||
|
n_iters, w, corr = best_params
|
||||||
|
print(f'\nBest: iters={n_iters}, w={w:.6f}, rms={best_rms:.4f}, corr={corr:.6f}')
|
||||||
|
return n_iters, w
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
if '--validate' in sys.argv:
|
||||||
|
validate()
|
||||||
|
else:
|
||||||
|
print('Usage: detector_cascade.py --validate')
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""fir_probe.py — численная реплика FIR-цепи (Этап A3) против захватов.
|
||||||
|
|
||||||
|
Структура по дизасму (BLOCKMAP 24mm6/24mm8 + wrap/worker декод этого раунда):
|
||||||
|
copy: FIR[2j]=scr[j], FIR[2j+1]=0 (18004d900, 2049 пар)
|
||||||
|
opA: th2180 = INVERSE real-FFT (план buf548, N=4096, scale 1/4096)
|
||||||
|
scale: float[1..2047] *= 2.0 ; float[2049..4095] = 0 (52d920/52db50)
|
||||||
|
opB: th1a90 = FORWARD real-FFT
|
||||||
|
EXP: expf in-place по первым 2049 ФЛОАТАМ (140b30, 52b708-716)
|
||||||
|
opC: th2180 = INVERSE
|
||||||
|
window: float[0..2047] *= WINfreq[2048..4095] (52d990, падающий Hann)
|
||||||
|
float[2048..4095] = 0 (52db50)
|
||||||
|
opD: th1a90 = FORWARD
|
||||||
|
fix: FIR[0]=1.0, FIR[1]=0 (52b7cd-e1)
|
||||||
|
df0: track_i := track_i ⊗ FIR (комплексное умножение, 18000b3c0)
|
||||||
|
Цель: воспроизвести cur@678 из trk@688 без свободных параметров.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
NFLOAT = 4098 # 2049 пар
|
||||||
|
NBINS = 2049 # n/2+1, n=[ctx+0x540534]=4096
|
||||||
|
|
||||||
|
|
||||||
|
def load_frame(npz):
|
||||||
|
d = np.load(npz)
|
||||||
|
S = {}
|
||||||
|
for k in d.keys():
|
||||||
|
if k.startswith('0x'):
|
||||||
|
S[int(k[2:], 16)] = d[k]
|
||||||
|
return S
|
||||||
|
|
||||||
|
|
||||||
|
def win_periodic_hann(N):
|
||||||
|
return 0.5 * (1.0 - np.cos(2.0 * np.pi * np.arange(N) / N)).astype(np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
def fir_chain(scr, winfall, variant='flat'):
|
||||||
|
"""scr: 2049 float (log-домен). Возвращает halfcomplex-спектр ядра F[2049]."""
|
||||||
|
# copy/pack: пары (re=scr, im=0) -> inverse rfft вход (numpy: complex[2049])
|
||||||
|
H = scr.astype(np.float64).astype(np.complex128)
|
||||||
|
# opA: inverse real FFT, нормировка 1/N (план scale=2^-12 при активном флаге)
|
||||||
|
y = np.fft.irfft(H, n=4096) # уже содержит деление на 4096
|
||||||
|
# scale/zero по asm: float[1..2047]*=2, float[2049..]=0 (f[2048] не трогаем)
|
||||||
|
y[1:2048] *= 2.0
|
||||||
|
y[2049:] = 0.0
|
||||||
|
# opB: forward
|
||||||
|
Y = np.fft.rfft(y, n=4096) # complex[2049]
|
||||||
|
# EXP по первым 2049 флоатам плоского массива
|
||||||
|
flat = np.empty(NFLOAT)
|
||||||
|
flat[0::2] = Y.real
|
||||||
|
flat[1::2] = Y.imag
|
||||||
|
if variant == 'flat':
|
||||||
|
flat[:2049] = np.exp(flat[:2049])
|
||||||
|
elif variant == 'cplx':
|
||||||
|
Y = np.exp(Y.astype(np.complex128))
|
||||||
|
flat[0::2] = Y.real
|
||||||
|
flat[1::2] = Y.imag
|
||||||
|
Y2 = flat[0::2] + 1j * flat[1::2]
|
||||||
|
# opC: inverse
|
||||||
|
w = np.fft.irfft(Y2, n=4096)
|
||||||
|
# window: float[0..2047] *= падающая половина; хвост = 0
|
||||||
|
w[:2048] *= winfall
|
||||||
|
w[2048:] = 0.0
|
||||||
|
# opD: forward
|
||||||
|
F = np.fft.rfft(w, n=4096)
|
||||||
|
# fix: FIR[0]=1.0, FIR[1]=0
|
||||||
|
F[0] = 1.0 + 0.0j
|
||||||
|
return F
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate(ds, ph_file, verbose=True):
|
||||||
|
S = load_frame(os.path.join(ds, ph_file))
|
||||||
|
scr = S[0x540628][:NBINS].astype(np.float64)
|
||||||
|
trk = S[0x540688][:NBINS].astype(np.float64)
|
||||||
|
cur = S[0x540678][:NBINS].astype(np.float64)
|
||||||
|
# проверка trk == exp(scr)
|
||||||
|
m_ok = trk > 1e-30
|
||||||
|
err_trk = np.abs(np.log(trk[m_ok]) - scr[m_ok]).max()
|
||||||
|
# фит gamma
|
||||||
|
sel = np.abs(scr) > 0.05
|
||||||
|
g = float(np.sum(scr[sel] * np.log(cur[sel])) / np.sum(scr[sel] ** 2))
|
||||||
|
rms_fit = float(np.sqrt(np.mean((np.log(cur[sel]) - g * scr[sel]) ** 2)))
|
||||||
|
winfall = win_periodic_hann(4096)[2048:]
|
||||||
|
out = []
|
||||||
|
for variant in ('flat', 'cplx'):
|
||||||
|
F = fir_chain(scr, winfall, variant)
|
||||||
|
# маска = track ⊗ F (df0), берём реальную часть как применённую маску
|
||||||
|
mask_sim = np.abs(trk * F) if variant == 'cplx' else trk * F.real
|
||||||
|
mm = (cur > 1e-6) & np.isfinite(mask_sim)
|
||||||
|
e_db = 20.0 / np.log(10) * np.log(np.abs(mask_sim[mm])) - \
|
||||||
|
20.0 / np.log(10) * np.log(cur[mm])
|
||||||
|
rms_db = float(np.sqrt(np.mean(e_db ** 2)))
|
||||||
|
out.append((variant, rms_db, int(mm.sum())))
|
||||||
|
if verbose:
|
||||||
|
print(f'{ph_file} [{variant}] gamma_fit={g:.6f} (rms {rms_fit:.1e}) '
|
||||||
|
f'trk_err={err_trk:.2e} MASK rms={rms_db:.4f} дБ / {mm.sum()} бинов')
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
jobs = [
|
||||||
|
('/tmp/opencode/sc_multi4b', 'ph073.npz'),
|
||||||
|
('/tmp/opencode/sc_multi6', 'ph037.npz'),
|
||||||
|
('/tmp/opencode/sc_multi6', 'ph034.npz'),
|
||||||
|
]
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
jobs = [(os.path.dirname(sys.argv[1]), os.path.basename(sys.argv[1]))]
|
||||||
|
for ds, ph in jobs:
|
||||||
|
evaluate(ds, ph)
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
fit_vlaw_by_group.py — Fit VLAW parameters (α, β, c, Δ) per configuration group.
|
||||||
|
|
||||||
|
VLAW model (framed_model.cpp:205-208):
|
||||||
|
cs = α * log1p(lvl / β) + c + (delta ? Δ : 0)
|
||||||
|
applied_gain = 10^(-cs / 20) [gamma0=1 already absorbed into α,c,Δ]
|
||||||
|
|
||||||
|
Need to fit these for each (fc, q, sens) configuration group:
|
||||||
|
t1kq: fc=800..1200, q=1.0, sens=12 (input tone1kq)
|
||||||
|
t1k: fc=500..2000, q=1.0, sens=12 (input tone1k)
|
||||||
|
al: fc=1000, q=1.0, sens=3..24 (input lvl_tone_lvX)
|
||||||
|
res: fc=300..700, q=1.0, sens=12 (input resonant)
|
||||||
|
dual: fc=500, q=0.1..10.0, sens=12 (input dual)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
|
||||||
|
sys.path.insert(0, '/home/m/re-tools/scripts')
|
||||||
|
import corpus
|
||||||
|
|
||||||
|
corpus.RB = '/home/m/re-tools/dsp/build/render48k'
|
||||||
|
|
||||||
|
# Reference errors from baseline_bridge.json (target)
|
||||||
|
with open('scripts/baseline_bridge.json') as f:
|
||||||
|
REF_ERRORS = json.load(f)
|
||||||
|
|
||||||
|
def structural_cases():
|
||||||
|
out = []
|
||||||
|
for name, inp, args, ref, f in corpus.build_cases():
|
||||||
|
joined = [','.join(args)] if len(args) == 3 else args
|
||||||
|
out.append((name, inp, joined, ref, f))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def run_vlaw(inp, args, alpha, beta, c, delta):
|
||||||
|
"""Run render48k with VLAW parameters and return output path."""
|
||||||
|
out = f'/tmp/vlaw_fit_{alpha}_{beta}_{c}_{delta}_{os.path.basename(inp)}.wav'
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
'RT_VLAW': '1',
|
||||||
|
'RT_VLAW_ALPHA': str(alpha),
|
||||||
|
'RT_VLAW_BETA': str(beta),
|
||||||
|
'RT_VLAW_C': str(c),
|
||||||
|
'RT_VLAW_DELTA': str(delta),
|
||||||
|
'RT_SYN': '1',
|
||||||
|
'RT_NOWARP': '1',
|
||||||
|
'RT_NOIIR3': '1',
|
||||||
|
'RT_IIR12': '0',
|
||||||
|
}
|
||||||
|
subprocess.run(
|
||||||
|
[corpus.RB, inp, out] + args,
|
||||||
|
capture_output=True, text=True, env=env,
|
||||||
|
cwd='/home/m/re-tools'
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def eval_error(out, ref, f):
|
||||||
|
"""Evaluate error in dB between output and reference at frequency f."""
|
||||||
|
if not os.path.exists(out) or os.path.getsize(out) == 0:
|
||||||
|
return None
|
||||||
|
ref_sig = corpus.load_mono(ref)
|
||||||
|
out_sig = corpus.load_mono(out)
|
||||||
|
min_len = min(len(ref_sig), len(out_sig))
|
||||||
|
ref_sig = ref_sig[-min_len:]
|
||||||
|
out_sig = out_sig[-min_len:]
|
||||||
|
ref_ta = corpus.ta(ref_sig, f)
|
||||||
|
out_ta = corpus.ta(out_sig, f)
|
||||||
|
return corpus.db(out_ta / ref_ta)
|
||||||
|
|
||||||
|
def group_key(name):
|
||||||
|
return name.split('_')[0]
|
||||||
|
|
||||||
|
def evaluate_params(alpha, beta, c, delta, cases_subset=None):
|
||||||
|
"""Evaluate VLAW params on all cases, return per-group mean abs error."""
|
||||||
|
all_cases = structural_cases()
|
||||||
|
if cases_subset:
|
||||||
|
all_cases = [c for c in all_cases if group_key(c[0]) in cases_subset]
|
||||||
|
|
||||||
|
errs = {}
|
||||||
|
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
out = run_vlaw(inp, args, alpha, beta, c, delta)
|
||||||
|
err = eval_error(out, ref, f)
|
||||||
|
if err is not None:
|
||||||
|
errs[name] = err
|
||||||
|
|
||||||
|
# Group stats
|
||||||
|
groups = {}
|
||||||
|
for k, v in errs.items():
|
||||||
|
g = group_key(k)
|
||||||
|
groups.setdefault(g, []).append(v)
|
||||||
|
|
||||||
|
out_stats = {g: float(np.mean(np.abs(v))) for g, v in groups.items()}
|
||||||
|
out_stats['TOTAL'] = float(np.mean(np.abs(list(errs.values()))))
|
||||||
|
return out_stats, errs
|
||||||
|
|
||||||
|
def fit_single_case(name, inp, args, ref, f, init_params):
|
||||||
|
"""Grid search for best params on a single case."""
|
||||||
|
alpha0, beta0, c0, delta0 = init_params
|
||||||
|
best = None
|
||||||
|
best_err = float('inf')
|
||||||
|
|
||||||
|
# Search around initial params
|
||||||
|
alphas = np.linspace(max(0.5, alpha0-1), alpha0+1, 9)
|
||||||
|
betas = np.linspace(max(0.1, beta0-0.2), beta0+0.2, 9)
|
||||||
|
cs = np.linspace(max(0.0, c0-0.5), c0+0.5, 9)
|
||||||
|
deltas = np.linspace(max(0.0, delta0-2), delta0+2, 9)
|
||||||
|
|
||||||
|
for alpha in alphas:
|
||||||
|
for beta in betas:
|
||||||
|
for c in cs:
|
||||||
|
for delta in deltas:
|
||||||
|
out = run_vlaw(inp, args, alpha, beta, c, delta)
|
||||||
|
err = eval_error(out, ref, f)
|
||||||
|
if err is not None and abs(err) < best_err:
|
||||||
|
best_err = abs(err)
|
||||||
|
best = (alpha, beta, c, delta, err)
|
||||||
|
print(f' {name}: new best α={alpha:.3f}, β={beta:.3f}, c={c:.3f}, Δ={delta:.3f} => err={err:.3f} dB')
|
||||||
|
|
||||||
|
return best
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Build case map by group
|
||||||
|
all_cases = structural_cases()
|
||||||
|
groups = {}
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
g = group_key(name)
|
||||||
|
groups.setdefault(g, []).append((name, inp, args, ref, f))
|
||||||
|
|
||||||
|
print("Available groups:", list(groups.keys()))
|
||||||
|
for g, cases in groups.items():
|
||||||
|
print(f" {g}: {len(cases)} cases")
|
||||||
|
|
||||||
|
# Current calibrated params for dual(q=0.5)
|
||||||
|
dual_params = (3.2193, 0.4927, 0.5423, 6.9177)
|
||||||
|
|
||||||
|
# Test current params on all groups
|
||||||
|
print("\n=== Testing current dual params on all groups ===")
|
||||||
|
stats, _ = evaluate_params(*dual_params)
|
||||||
|
for g in ['t1kq', 't1k', 'al', 'res', 'dual', 'comb']:
|
||||||
|
if g in stats:
|
||||||
|
print(f' {g}: {stats[g]:.3f} dB')
|
||||||
|
|
||||||
|
# For each group, pick a representative case and fit
|
||||||
|
print("\n=== Fitting per group (representative case) ===")
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# For dual, use q=0.5 as reference (already calibrated)
|
||||||
|
if 'dual' in groups:
|
||||||
|
# Find q=0.5 case
|
||||||
|
for name, inp, args, ref, f in groups['dual']:
|
||||||
|
if '0.5' in name:
|
||||||
|
best = fit_single_case(name, inp, args, ref, f, dual_params)
|
||||||
|
if best:
|
||||||
|
results['dual'] = best[:4]
|
||||||
|
break
|
||||||
|
|
||||||
|
# For t1kq, use fc=1000
|
||||||
|
if 't1kq' in groups:
|
||||||
|
for name, inp, args, ref, f in groups['t1kq']:
|
||||||
|
if '1000' in name:
|
||||||
|
best = fit_single_case(name, inp, args, ref, f, dual_params)
|
||||||
|
if best:
|
||||||
|
results['t1kq'] = best[:4]
|
||||||
|
break
|
||||||
|
|
||||||
|
# For t1k, use fc=1000
|
||||||
|
if 't1k' in groups:
|
||||||
|
for name, inp, args, ref, f in groups['t1k']:
|
||||||
|
if '1000' in name:
|
||||||
|
best = fit_single_case(name, inp, args, ref, f, dual_params)
|
||||||
|
if best:
|
||||||
|
results['t1k'] = best[:4]
|
||||||
|
break
|
||||||
|
|
||||||
|
# For al, use sens=12
|
||||||
|
if 'al' in groups:
|
||||||
|
for name, inp, args, ref, f in groups['al']:
|
||||||
|
if '12' in name:
|
||||||
|
best = fit_single_case(name, inp, args, ref, f, dual_params)
|
||||||
|
if best:
|
||||||
|
results['al'] = best[:4]
|
||||||
|
break
|
||||||
|
|
||||||
|
# For res, use fc=500
|
||||||
|
if 'res' in groups:
|
||||||
|
for name, inp, args, ref, f in groups['res']:
|
||||||
|
if '500' in name:
|
||||||
|
best = fit_single_case(name, inp, args, ref, f, dual_params)
|
||||||
|
if best:
|
||||||
|
results['res'] = best[:4]
|
||||||
|
break
|
||||||
|
|
||||||
|
# Print results
|
||||||
|
print("\n=== FITTED VLAW PARAMETERS BY GROUP ===")
|
||||||
|
for g, (alpha, beta, c, delta) in results.items():
|
||||||
|
print(f'{g}: α={alpha:.4f}, β={beta:.4f}, c={c:.4f}, Δ={delta:.4f}')
|
||||||
|
|
||||||
|
# Save to JSON
|
||||||
|
with open('/tmp/opencode/vlaw_params.json', 'w') as f:
|
||||||
|
json.dump({g: {'alpha': a, 'beta': b, 'c': c, 'delta': d}
|
||||||
|
for g, (a, b, c, d) in results.items()}, f, indent=2)
|
||||||
|
print('\nSaved to /tmp/opencode/vlaw_params.json')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
fit_vlaw_params.py — Fit VLAW parameters (α, β, c, Δ, γ₀) per configuration group.
|
||||||
|
|
||||||
|
VLAW model (framed_model.cpp:198-200):
|
||||||
|
cs = α * log1p(lvl / β) + c + (delta ? Δ : 0)
|
||||||
|
applied_gain = 10^(-γ₀ * cs / 20)
|
||||||
|
|
||||||
|
Currently hardcoded for dual(q=0.5): α=3.2193, β=0.4927, c=0.5423, Δ=7.46-0.5423, γ₀=1.79
|
||||||
|
|
||||||
|
Need to fit these for each (fc, q, sens) configuration group:
|
||||||
|
t1kq: fc=800..1200, q=1.0, sens=12
|
||||||
|
t1k: fc=500..2000, q=1.0, sens=12
|
||||||
|
al: fc=1000, q=1.0, sens=3..24
|
||||||
|
res: fc=300..700, q=1.0, sens=12
|
||||||
|
dual: fc=500, q=0.1..10.0, sens=12
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
sys.path.insert(0, '/home/m/re-tools/scripts')
|
||||||
|
import corpus
|
||||||
|
|
||||||
|
corpus.RB = '/home/m/re-tools/dsp/build/render48k'
|
||||||
|
|
||||||
|
def structural_cases():
|
||||||
|
out = []
|
||||||
|
for name, inp, args, ref, f in corpus.build_cases():
|
||||||
|
joined = [','.join(args)] if len(args) == 3 else args
|
||||||
|
out.append((name, inp, joined, ref, f))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def group_key(name):
|
||||||
|
return name.split('_')[0]
|
||||||
|
|
||||||
|
def load_ref_errors():
|
||||||
|
"""Load baseline_bridge.json for target errors."""
|
||||||
|
with open('scripts/baseline_bridge.json') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def render_vlaw(inp, out, args, alpha, beta, c, delta, gamma0, extra_env=None):
|
||||||
|
"""Run render48k with VLAW parameters."""
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
'RT_VLAW': '1',
|
||||||
|
'RT_VLAW_ALPHA': str(alpha),
|
||||||
|
'RT_VLAW_BETA': str(beta),
|
||||||
|
'RT_VLAW_C': str(c),
|
||||||
|
'RT_VLAW_DELTA': str(delta),
|
||||||
|
'RT_VLAW_GAMMA0': str(gamma0),
|
||||||
|
'RT_SYN': '1',
|
||||||
|
'RT_NOWARP': '1',
|
||||||
|
'RT_NOIIR3': '1',
|
||||||
|
'RT_IIR12': '0',
|
||||||
|
}
|
||||||
|
if extra_env:
|
||||||
|
env.update(extra_env)
|
||||||
|
subprocess.run(
|
||||||
|
[corpus.RB, inp, out] + args,
|
||||||
|
capture_output=True, text=True, env=env,
|
||||||
|
cwd='/home/m/re-tools'
|
||||||
|
)
|
||||||
|
|
||||||
|
def eval_config(alpha, beta, c, delta, gamma0, cases_subset=None):
|
||||||
|
"""Evaluate VLAW params on cases, return per-group mean abs error."""
|
||||||
|
all_cases = structural_cases()
|
||||||
|
if cases_subset:
|
||||||
|
all_cases = [c for c in all_cases if group_key(c[0]) in cases_subset]
|
||||||
|
|
||||||
|
refs = load_ref_errors()
|
||||||
|
errs = {}
|
||||||
|
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
out = f'/tmp/vlaw_fit_{name}.wav'
|
||||||
|
render_vlaw(inp, out, args, alpha, beta, c, delta, gamma0)
|
||||||
|
|
||||||
|
if not os.path.exists(out) or os.path.getsize(out) == 0:
|
||||||
|
errs[name] = 999.0
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
ref_sig = corpus.load_mono(ref)
|
||||||
|
out_sig = corpus.load_mono(out)
|
||||||
|
min_len = min(len(ref_sig), len(out_sig))
|
||||||
|
ref_sig = ref_sig[-min_len:]
|
||||||
|
out_sig = out_sig[-min_len:]
|
||||||
|
|
||||||
|
ref_ta = corpus.ta(ref_sig, f)
|
||||||
|
out_ta = corpus.ta(out_sig, f)
|
||||||
|
err_db = corpus.db(out_ta / ref_ta)
|
||||||
|
errs[name] = err_db
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error on {name}: {e}")
|
||||||
|
errs[name] = 999.0
|
||||||
|
|
||||||
|
# Group stats
|
||||||
|
groups = {}
|
||||||
|
for k, v in errs.items():
|
||||||
|
g = group_key(k)
|
||||||
|
groups.setdefault(g, []).append(v)
|
||||||
|
|
||||||
|
out = {g: float(np.mean(np.abs(v))) for g, v in groups.items()}
|
||||||
|
out['TOTAL'] = float(np.mean(np.abs(list(errs.values()))))
|
||||||
|
return out, errs
|
||||||
|
|
||||||
|
def fit_alpha_beta_c(cases_to_fit):
|
||||||
|
"""Coordinate descent on (α, β, c) for a specific case group."""
|
||||||
|
# For now, grid search
|
||||||
|
best = None
|
||||||
|
best_err = float('inf')
|
||||||
|
|
||||||
|
# Search ranges around current dual(q=0.5) values
|
||||||
|
for alpha in np.linspace(2.5, 4.0, 8):
|
||||||
|
for beta in np.linspace(0.3, 0.7, 8):
|
||||||
|
for c in np.linspace(0.2, 1.0, 8):
|
||||||
|
stats, _ = eval_config(alpha, beta, c, 6.9, 1.79, cases_to_fit)
|
||||||
|
total = stats['TOTAL']
|
||||||
|
if total < best_err:
|
||||||
|
best_err = total
|
||||||
|
best = (alpha, beta, c, stats)
|
||||||
|
print(f" New best: α={alpha:.4f}, β={beta:.4f}, c={c:.4f}, TOTAL={total:.4f}")
|
||||||
|
|
||||||
|
return best
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Build case map by group
|
||||||
|
all_cases = structural_cases()
|
||||||
|
groups = {}
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
g = group_key(name)
|
||||||
|
groups.setdefault(g, []).append(name)
|
||||||
|
|
||||||
|
print("Available groups:", list(groups.keys()))
|
||||||
|
for g, names in groups.items():
|
||||||
|
print(f" {g}: {len(names)} cases")
|
||||||
|
|
||||||
|
# Start with dual group (already calibrated)
|
||||||
|
print("\n=== Testing dual(q=0.5) baseline ===")
|
||||||
|
stats, errs = eval_config(3.2193, 0.4927, 0.5423, 6.9177, 1.79, ['dual'])
|
||||||
|
print(f"Dual stats: {stats}")
|
||||||
|
|
||||||
|
# Now fit for each group
|
||||||
|
for g in ['t1kq', 't1k', 'al', 'res', 'dual']:
|
||||||
|
if g not in groups:
|
||||||
|
continue
|
||||||
|
print(f"\n=== Fitting {g} ===")
|
||||||
|
best = fit_alpha_beta_c([g])
|
||||||
|
if best:
|
||||||
|
alpha, beta, c, stats = best
|
||||||
|
print(f" {g} best: α={alpha:.4f}, β={beta:.4f}, c={c:.4f}")
|
||||||
|
print(f" Stats: {stats}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Quick VLAW parameter grid search - test fewer combos per case.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
|
||||||
|
sys.path.insert(0, '/home/m/re-tools/scripts')
|
||||||
|
import corpus
|
||||||
|
|
||||||
|
corpus.RB = '/home/m/re-tools/dsp/build/render48k'
|
||||||
|
|
||||||
|
with open('scripts/baseline_bridge.json') as f:
|
||||||
|
REF_ERRORS = json.load(f)
|
||||||
|
|
||||||
|
def structural_cases():
|
||||||
|
out = []
|
||||||
|
for name, inp, args, ref, f in corpus.build_cases():
|
||||||
|
joined = [','.join(args)] if len(args) == 3 else args
|
||||||
|
out.append((name, inp, joined, ref, f))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def run_vlaw(inp, args, alpha, beta, c, delta):
|
||||||
|
out = f'/tmp/vlaw_{alpha}_{beta}_{c}_{delta}_{os.path.basename(inp)}.wav'
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
'RT_VLAW': '1',
|
||||||
|
'RT_VLAW_ALPHA': str(alpha),
|
||||||
|
'RT_VLAW_BETA': str(beta),
|
||||||
|
'RT_VLAW_C': str(c),
|
||||||
|
'RT_VLAW_DELTA': str(delta),
|
||||||
|
'RT_SYN': '1', 'RT_NOWARP': '1', 'RT_NOIIR3': '1', 'RT_IIR12': '0',
|
||||||
|
}
|
||||||
|
subprocess.run([corpus.RB, inp, out] + args, capture_output=True, env=env, cwd='/home/m/re-tools')
|
||||||
|
return out
|
||||||
|
|
||||||
|
def eval_error(out, ref, f):
|
||||||
|
if not os.path.exists(out) or os.path.getsize(out) == 0:
|
||||||
|
return None
|
||||||
|
ref_sig = corpus.load_mono(ref)
|
||||||
|
out_sig = corpus.load_mono(out)
|
||||||
|
min_len = min(len(ref_sig), len(out_sig))
|
||||||
|
ref_sig = ref_sig[-min_len:]
|
||||||
|
out_sig = out_sig[-min_len:]
|
||||||
|
ref_ta = corpus.ta(ref_sig, f)
|
||||||
|
out_ta = corpus.ta(out_sig, f)
|
||||||
|
return corpus.db(out_ta / ref_ta)
|
||||||
|
|
||||||
|
def group_key(name):
|
||||||
|
return name.split('_')[0]
|
||||||
|
|
||||||
|
all_cases = structural_cases()
|
||||||
|
groups = {}
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
g = group_key(name)
|
||||||
|
groups.setdefault(g, []).append((name, inp, args, ref, f))
|
||||||
|
|
||||||
|
# Pick one case per group
|
||||||
|
rep_cases = {}
|
||||||
|
for g in ['t1kq', 't1k', 'al', 'res', 'dual']:
|
||||||
|
if g in groups:
|
||||||
|
# Pick middle-ish case
|
||||||
|
cases = groups[g]
|
||||||
|
rep_cases[g] = cases[len(cases)//2]
|
||||||
|
|
||||||
|
print("Representative cases:")
|
||||||
|
for g, (name, inp, args, ref, f) in rep_cases.items():
|
||||||
|
print(f" {g}: {name}")
|
||||||
|
|
||||||
|
# Test a small grid around dual params
|
||||||
|
dual_params = (3.2193, 0.4927, 0.5423, 6.9177)
|
||||||
|
|
||||||
|
print("\n=== Grid search per group ===")
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for g, (name, inp, args, ref, f) in rep_cases.items():
|
||||||
|
print(f"\n--- {g} ({name}) ---")
|
||||||
|
best = None
|
||||||
|
best_err = float('inf')
|
||||||
|
|
||||||
|
# Coarse grid
|
||||||
|
alphas = np.linspace(1.0, 5.0, 5)
|
||||||
|
betas = np.linspace(0.2, 0.8, 5)
|
||||||
|
cs = np.linspace(-0.5, 2.0, 5)
|
||||||
|
deltas = np.linspace(0.0, 12.0, 5)
|
||||||
|
|
||||||
|
for alpha in alphas:
|
||||||
|
for beta in betas:
|
||||||
|
for c in cs:
|
||||||
|
for delta in deltas:
|
||||||
|
out = run_vlaw(inp, args, alpha, beta, c, delta)
|
||||||
|
err = eval_error(out, ref, f)
|
||||||
|
if err is not None and abs(err) < best_err:
|
||||||
|
best_err = abs(err)
|
||||||
|
best = (alpha, beta, c, delta, err)
|
||||||
|
print(f' {name}: α={alpha:.3f}, β={beta:.3f}, c={c:.3f}, Δ={delta:.3f} => {err:.3f} dB')
|
||||||
|
|
||||||
|
if best:
|
||||||
|
results[g] = best[:4]
|
||||||
|
print(f' BEST {g}: α={best[0]:.4f}, β={best[1]:.4f}, c={best[2]:.4f}, Δ={best[3]:.4f} => {best[4]:.3f} dB')
|
||||||
|
|
||||||
|
print("\n=== SUMMARY ===")
|
||||||
|
for g, (a, b, c, d) in results.items():
|
||||||
|
print(f'{g}: α={a:.4f}, β={b:.4f}, c={c:.4f}, Δ={d:.4f}')
|
||||||
|
|
||||||
|
with open('/tmp/opencode/vlaw_params.json', 'w') as f:
|
||||||
|
json.dump({g: {'alpha': a, 'beta': b, 'c': c, 'delta': d}
|
||||||
|
for g, (a, b, c, d) in results.items()}, f, indent=2)
|
||||||
|
print('\nSaved to /tmp/opencode/vlaw_params.json')
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Quick VLAW parameter test - just evaluate a few configs per group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
sys.path.insert(0, '/home/m/re-tools/scripts')
|
||||||
|
import corpus
|
||||||
|
|
||||||
|
corpus.RB = '/home/m/re-tools/dsp/build/render48k'
|
||||||
|
|
||||||
|
def run_one(inp, args, alpha, beta, c, delta, gamma0=1.79):
|
||||||
|
out = f'/tmp/vlaw_test_{alpha}_{beta}_{c}.wav'
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
'RT_VLAW': '1',
|
||||||
|
'RT_VLAW_ALPHA': str(alpha),
|
||||||
|
'RT_VLAW_BETA': str(beta),
|
||||||
|
'RT_VLAW_C': str(c),
|
||||||
|
'RT_VLAW_DELTA': str(delta),
|
||||||
|
'RT_SYN': '1',
|
||||||
|
'RT_NOWARP': '1',
|
||||||
|
'RT_NOIIR3': '1',
|
||||||
|
'RT_IIR12': '0',
|
||||||
|
}
|
||||||
|
subprocess.run(
|
||||||
|
[corpus.RB, inp, out] + args,
|
||||||
|
capture_output=True, text=True, env=env,
|
||||||
|
cwd='/home/m/re-tools'
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def eval_one(name, inp, args, ref, f, alpha, beta, c, delta):
|
||||||
|
out = run_one(inp, args, alpha, beta, c, delta)
|
||||||
|
if not os.path.exists(out) or os.path.getsize(out) == 0:
|
||||||
|
return None
|
||||||
|
ref_sig = corpus.load_mono(ref)
|
||||||
|
out_sig = corpus.load_mono(out)
|
||||||
|
min_len = min(len(ref_sig), len(out_sig))
|
||||||
|
ref_sig = ref_sig[-min_len:]
|
||||||
|
out_sig = out_sig[-min_len:]
|
||||||
|
ref_ta = corpus.ta(ref_sig, f)
|
||||||
|
out_ta = corpus.ta(out_sig, f)
|
||||||
|
return corpus.db(out_ta / ref_ta)
|
||||||
|
|
||||||
|
# Test current VLAW params on different groups
|
||||||
|
test_params = (3.2193, 0.4927, 0.5423, 6.9177)
|
||||||
|
|
||||||
|
all_cases = []
|
||||||
|
for name, inp, args, ref, f in corpus.build_cases():
|
||||||
|
joined = [','.join(args)] if len(args) == 3 else args
|
||||||
|
all_cases.append((name, inp, joined, ref, f))
|
||||||
|
|
||||||
|
# Pick one representative case per group
|
||||||
|
groups = {}
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
g = name.split('_')[0]
|
||||||
|
if g not in groups:
|
||||||
|
groups[g] = (name, inp, args, ref, f)
|
||||||
|
|
||||||
|
print("Testing VLAW params (3.2193, 0.4927, 0.5423, 6.9177) on each group:")
|
||||||
|
for g, (name, inp, args, ref, f) in groups.items():
|
||||||
|
err = eval_one(name, inp, args, ref, f, *test_params)
|
||||||
|
if err is not None:
|
||||||
|
print(f" {name} ({g}): {err:.3f} dB")
|
||||||
|
else:
|
||||||
|
print(f" {name} ({g}): FAILED")
|
||||||
+24
-2
@@ -18,9 +18,12 @@ import time
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
SLOTS = [0x540668, 0x540548, 0x540550, 0x540598, 0x540628,
|
SLOTS = [0x540668, 0x540548, 0x540550, 0x540598, 0x540628, 0x5406f8,
|
||||||
0x540678, 0x540688, 0x5406c8, 0x5406e8, 0x540768,
|
0x540678, 0x540688, 0x5406c8, 0x5406e8, 0x540768,
|
||||||
0x540788, 0x5407f8]
|
0x540788, 0x5407f8,
|
||||||
|
# 24mm9: ACC-таблица указателей (шаг 10 combine) и WINfreq
|
||||||
|
# (окно FIR-цепи; падающий Hann — контроль формы)
|
||||||
|
0x5407c8, 0x540658]
|
||||||
NARR = 8194
|
NARR = 8194
|
||||||
SCAL_OFF = 0x540860
|
SCAL_OFF = 0x540860
|
||||||
SCAL_N = 24 # floats -> 0x540860..0x5408c0
|
SCAL_N = 24 # floats -> 0x540860..0x5408c0
|
||||||
@@ -173,6 +176,25 @@ def main():
|
|||||||
store['lut_G'] = struct.unpack('<f', fb4[12:16])[0]
|
store['lut_G'] = struct.unpack('<f', fb4[12:16])[0]
|
||||||
store['lut_mode'] = fb4[16]
|
store['lut_mode'] = fb4[16]
|
||||||
for off in SLOTS[1:]:
|
for off in SLOTS[1:]:
|
||||||
|
if off == 0x5407c8:
|
||||||
|
# ACC is table of pointers per-band [5407c8 + i*8] -> buf 2049 f32 each
|
||||||
|
for bi in range(4):
|
||||||
|
q = rd(ctx + off + 8*bi, 8)
|
||||||
|
if not q: continue
|
||||||
|
ptr = struct.unpack('<Q', q)[0]
|
||||||
|
if ptr < 0x10000: continue
|
||||||
|
ab = rd(ptr, 2049 * 4)
|
||||||
|
if ab:
|
||||||
|
store[hex(off) + f"_{bi}"] = np.frombuffer(ab, dtype='<f4').astype(np.float32)
|
||||||
|
# also store raw table pointer for debug
|
||||||
|
q = rd(ctx + off, 8)
|
||||||
|
if q:
|
||||||
|
ptr = struct.unpack('<Q', q)[0]
|
||||||
|
if ptr >= 0x10000:
|
||||||
|
ab = rd(ptr, NARR * 4)
|
||||||
|
if ab:
|
||||||
|
store[hex(off)] = np.frombuffer(ab, dtype='<f4').astype(np.float32)
|
||||||
|
continue
|
||||||
q = rd(ctx + off, 8)
|
q = rd(ctx + off, 8)
|
||||||
if not q:
|
if not q:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""wine_chain_trace.py — живой захват промежуточных состояний FIR-цепи
|
||||||
|
soothe2 через winedbg (wine) + /proc/<pid>/mem.
|
||||||
|
|
||||||
|
Брейкпоинты:
|
||||||
|
EXP 0x1803831c0 комплексная экспонента FIR-цепи (rcx=buf, r8d=count float)
|
||||||
|
DF0 0x18000b3c0 финальный complex-mul (rcx=FIR, rdx=track, r8d=n пар)
|
||||||
|
На хите: читаем rcx/rdx/r8 (info reg), буферы — через /proc/<pid>/mem,
|
||||||
|
копим сэмплы, отпускаем (c). Рендер не убивается.
|
||||||
|
|
||||||
|
Запуск: python3 scripts/wine_chain_trace.py <rpp> [n_hits] [outdir]
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
BP_EXP = 0x1803831c0
|
||||||
|
BP_DF0 = 0x18000b3c0
|
||||||
|
CTX_SLOTS = {'scr': 0x540628, 'trk': 0x540688, 'cur': 0x540678,
|
||||||
|
'fir_ptr': 0x540668}
|
||||||
|
|
||||||
|
|
||||||
|
def find_host():
|
||||||
|
import glob
|
||||||
|
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, cmd[:80]
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def find_ctx(fd, pid):
|
||||||
|
vt = struct.pack('<Q', 0x1824AC210)
|
||||||
|
m48 = struct.pack('<I', 0x47380000)
|
||||||
|
for line in open(f'/proc/{pid}/maps'):
|
||||||
|
parts = line.split()
|
||||||
|
if 'rw' not in parts[1]:
|
||||||
|
continue
|
||||||
|
lo, hi = (int(x, 16) for x in parts[0].split('-'))
|
||||||
|
CH = 16 * 1024 * 1024
|
||||||
|
a = lo
|
||||||
|
while a < hi:
|
||||||
|
n = min(CH, hi - a)
|
||||||
|
try:
|
||||||
|
d = os.pread(fd, n, a)
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
j = d.find(vt)
|
||||||
|
while j >= 0:
|
||||||
|
cand = a + j
|
||||||
|
sb = os.pread(fd, 4, cand + 0x540870)
|
||||||
|
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||||
|
return cand
|
||||||
|
j = d.find(vt, j + 1)
|
||||||
|
j = d.find(m48)
|
||||||
|
while j >= 0:
|
||||||
|
cand = a + j - 0x24
|
||||||
|
try:
|
||||||
|
sb = os.pread(fd, 4, cand + 0x540870)
|
||||||
|
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||||
|
return cand
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
j = d.find(m48, j + 1)
|
||||||
|
a += n
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class WineDbg:
|
||||||
|
"""Асинхронный ридер stdout winedbg + обмен командами по приглашению."""
|
||||||
|
|
||||||
|
PROMPT = 'Wine-dbg>'
|
||||||
|
|
||||||
|
def __init__(self, pid):
|
||||||
|
self.p = subprocess.Popen(
|
||||||
|
['winedbg', '--pid', str(pid)],
|
||||||
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT, text=True, bufsize=1)
|
||||||
|
self.buf = ''
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.ev = threading.Event()
|
||||||
|
self.alive = True
|
||||||
|
self.t = threading.Thread(target=self._reader, daemon=True)
|
||||||
|
self.t.start()
|
||||||
|
if not self.ev.wait(30):
|
||||||
|
raise TimeoutError('winedbg не показал приглашение')
|
||||||
|
|
||||||
|
def _reader(self):
|
||||||
|
while self.alive:
|
||||||
|
ch = self.p.stdout.read(1)
|
||||||
|
if not ch:
|
||||||
|
self.alive = False
|
||||||
|
self.ev.set()
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
self.buf += ch
|
||||||
|
if self.PROMPT in self.buf:
|
||||||
|
self.ev.set()
|
||||||
|
|
||||||
|
def cmd(self, c, timeout=90):
|
||||||
|
with self.lock:
|
||||||
|
self.buf = ''
|
||||||
|
self.ev.clear()
|
||||||
|
self.p.stdin.write(c + '\n')
|
||||||
|
self.p.stdin.flush()
|
||||||
|
if not self.ev.wait(timeout):
|
||||||
|
with self.lock:
|
||||||
|
tail = self.buf[-300:]
|
||||||
|
raise TimeoutError('winedbg timeout после %r; tail=%r' % (c, tail))
|
||||||
|
with self.lock:
|
||||||
|
out = self.buf.replace(self.PROMPT, '').strip()
|
||||||
|
self.buf = ''
|
||||||
|
self.ev.clear()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.alive = False
|
||||||
|
try:
|
||||||
|
self.p.stdin.write('quit\n')
|
||||||
|
self.p.stdin.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.p.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def parse_regs(text):
|
||||||
|
regs = {}
|
||||||
|
for mm in re.finditer(r'\b([re]?[a-z]{2,3}|r\d+d?)\s*[:=]\s*([0-9a-fA-F]{4,16})\b', text):
|
||||||
|
name = mm.group(1).lower()
|
||||||
|
val = int(mm.group(2), 16)
|
||||||
|
if name not in regs:
|
||||||
|
regs[name] = val
|
||||||
|
# нормализация имён к 64-битным
|
||||||
|
alias = {'eax': 'rax', 'ecx': 'rcx', 'edx': 'rdx', 'ebx': 'rbx',
|
||||||
|
'esi': 'rsi', 'edi': 'rdi', 'ebp': 'rbp', 'esp': 'rsp'}
|
||||||
|
out = {}
|
||||||
|
for k, v in regs.items():
|
||||||
|
k64 = alias.get(k, k)
|
||||||
|
if k64.startswith('r') and k64.endswith('d') and k64[1:-1].isdigit():
|
||||||
|
k64 = k64[:-1]
|
||||||
|
if len(k64) <= 3 or k64.startswith('r'):
|
||||||
|
out[k64] = v
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
|
||||||
|
n_target = int(sys.argv[2]) if len(sys.argv) > 2 else 60
|
||||||
|
outdir = sys.argv[3] if len(sys.argv) > 3 else '/tmp/opencode/winetrace'
|
||||||
|
os.makedirs(outdir, exist_ok=True)
|
||||||
|
|
||||||
|
wav = None
|
||||||
|
for ln in open(rpp, errors='replace'):
|
||||||
|
if 'RENDER_FILE' in ln and '"' in ln:
|
||||||
|
wav = ln.split('"')[1]
|
||||||
|
break
|
||||||
|
if wav and os.path.exists(wav):
|
||||||
|
os.remove(wav)
|
||||||
|
|
||||||
|
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1",
|
||||||
|
shell=True)
|
||||||
|
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
|
||||||
|
'-renderproject', rpp],
|
||||||
|
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
|
||||||
|
t0 = time.time()
|
||||||
|
host = None
|
||||||
|
while time.time() - t0 < 30 and not host:
|
||||||
|
host, cmdl = find_host()
|
||||||
|
if not host:
|
||||||
|
time.sleep(0.002)
|
||||||
|
if not host:
|
||||||
|
print('NO HOST')
|
||||||
|
return 1
|
||||||
|
print('host %d (%s)' % (host, cmdl), flush=True)
|
||||||
|
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
||||||
|
|
||||||
|
ctx = None
|
||||||
|
while ctx is None and time.time() - t0 < 25:
|
||||||
|
try:
|
||||||
|
os.kill(host, signal.SIGSTOP)
|
||||||
|
except ProcessLookupError:
|
||||||
|
break
|
||||||
|
ctx = find_ctx(fd, host)
|
||||||
|
os.kill(host, signal.SIGCONT)
|
||||||
|
if not ctx:
|
||||||
|
time.sleep(0.005)
|
||||||
|
if not ctx:
|
||||||
|
print('NO CTX')
|
||||||
|
return 1
|
||||||
|
print('ctx %#x' % ctx, flush=True)
|
||||||
|
|
||||||
|
dbg = WineDbg(host)
|
||||||
|
print(dbg.cmd('break *%#x' % BP_EXP)[:160], flush=True)
|
||||||
|
print(dbg.cmd('break *%#x' % BP_DF0)[:160], flush=True)
|
||||||
|
|
||||||
|
def rd(a, n):
|
||||||
|
return os.pread(fd, n, a)
|
||||||
|
|
||||||
|
def rd_f32(a, n):
|
||||||
|
return np.frombuffer(rd(a, 4*n), dtype='<f4').astype(np.float64)
|
||||||
|
|
||||||
|
def rd_q(a):
|
||||||
|
return struct.unpack('<Q', rd(a, 8))[0]
|
||||||
|
|
||||||
|
samples = []
|
||||||
|
hits = {'EXP': 0, 'DF0': 0}
|
||||||
|
t_start = time.time()
|
||||||
|
stall = 0
|
||||||
|
while sum(hits.values()) < n_target and time.time() - t_start < 300:
|
||||||
|
try:
|
||||||
|
out = dbg.cmd('c', timeout=120)
|
||||||
|
except TimeoutError as e:
|
||||||
|
print('timeout:', str(e)[-200:], flush=True)
|
||||||
|
stall += 1
|
||||||
|
if stall >= 3:
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
addrs = [int(x, 16) for x in re.findall(r'0x[0-9a-fA-F]{9,}', out)]
|
||||||
|
pc = None
|
||||||
|
for a in addrs:
|
||||||
|
if abs(a - BP_EXP) < 64:
|
||||||
|
pc = a; kind = 'EXP'; break
|
||||||
|
if abs(a - BP_DF0) < 64:
|
||||||
|
pc = a; kind = 'DF0'; break
|
||||||
|
if pc is None:
|
||||||
|
ir = dbg.cmd('info reg', timeout=30)
|
||||||
|
rr = parse_regs(ir)
|
||||||
|
pc = rr.get('rip', 0)
|
||||||
|
kind = 'EXP' if abs(pc-BP_EXP) < 64 else ('DF0' if abs(pc-BP_DF0) < 64 else None)
|
||||||
|
if kind is None:
|
||||||
|
stall += 1
|
||||||
|
if stall >= 5:
|
||||||
|
print('неопознанные остановки; tail:', out[-200:], flush=True)
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
ir = dbg.cmd('info reg', timeout=30)
|
||||||
|
rr = parse_regs(ir)
|
||||||
|
rcx = rr.get('rcx', 0); rdx = rr.get('rdx', 0); r8 = rr.get('r8', 0)
|
||||||
|
rec = {'kind': kind, 'rip': pc, 'rcx': rcx, 'rdx': rdx, 'r8': r8,
|
||||||
|
't': round(time.time()-t_start, 4)}
|
||||||
|
try:
|
||||||
|
if kind == 'EXP':
|
||||||
|
rec['buf'] = rd_f32(rcx, 4098)
|
||||||
|
rec['count'] = r8
|
||||||
|
else:
|
||||||
|
rec['fir'] = rd_f32(rcx, 4098)
|
||||||
|
if rdx > 0x10000:
|
||||||
|
rec['track'] = rd_f32(rdx, 2049*2)
|
||||||
|
# слоты контекста тем же мгновением (процесс остановлен!)
|
||||||
|
rec['scr'] = rd_f32(ctx+CTX_SLOTS['scr'], 2049)
|
||||||
|
rec['trk'] = rd_f32(ctx+CTX_SLOTS['trk'], 2049)
|
||||||
|
rec['cur'] = rd_f32(ctx+CTX_SLOTS['cur'], 2049)
|
||||||
|
fp = rd_q(ctx+CTX_SLOTS['fir_ptr'])
|
||||||
|
rec['fir_via_ctx'] = rd_f32(fp, 4098)
|
||||||
|
except OSError as e:
|
||||||
|
rec['err'] = str(e)
|
||||||
|
samples.append(rec)
|
||||||
|
hits[kind] += 1
|
||||||
|
if sum(hits.values()) % 10 == 0:
|
||||||
|
print('hits:', hits, flush=True)
|
||||||
|
|
||||||
|
print('сбор завершён:', hits, flush=True)
|
||||||
|
snap_ptrs = {}
|
||||||
|
snap_arr = {}
|
||||||
|
for nm, off in CTX_SLOTS.items():
|
||||||
|
try:
|
||||||
|
p = rd_q(ctx+off)
|
||||||
|
if p > 0x10000:
|
||||||
|
snap_ptrs[nm] = p
|
||||||
|
snap_arr[nm] = rd_f32(p, 4100)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
dbg.close()
|
||||||
|
|
||||||
|
with open(os.path.join(outdir, 'chain_samples.pkl'), 'wb') as f:
|
||||||
|
pickle.dump({'samples': samples, 'snap_ptrs': snap_ptrs, 'ctx': ctx}, f)
|
||||||
|
np.savez_compressed(os.path.join(outdir, 'ctx_snap.npz'), **snap_arr)
|
||||||
|
print('saved', len(samples), 'samples ->', outdir, flush=True)
|
||||||
|
for _ in range(600):
|
||||||
|
if proc.poll() is not None:
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
print('reaper_rc=%s wav=%s' % (proc.poll(),
|
||||||
|
os.path.getsize(wav) if wav and os.path.exists(wav) else 'NONE'), flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,687 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""wine_ptrace_trace.py — точный пер-оп захват FIR-цепи soothe2 через ptrace.
|
||||||
|
|
||||||
|
Запускает reaper -renderproject как ребёнок (=> ptrace разрешён при любом
|
||||||
|
yama scope), находит wine-хост yabridge (soothe2 в maps), прицепляется ко
|
||||||
|
всем тредам, ставит int3 на входах EXP/DF0 ядра, на хитах читает регистры
|
||||||
|
(PTRACE_GETREGS) и буферы через /proc/tid/mem; между хитами CONT.
|
||||||
|
|
||||||
|
Брейкпоинты:
|
||||||
|
EXP 0x1803831c0 rcx=buf, r8d=count(float)
|
||||||
|
DF0 0x18000b3c0 rcx=FIR, rdx=track, r8d=n(пар)
|
||||||
|
Плюс слоты контекста тем же мгновением (scr/trk/cur/FIR@540668).
|
||||||
|
|
||||||
|
Запуск: python3 scripts/wine_ptrace_trace.py <rpp> [n_hits] [outdir]
|
||||||
|
"""
|
||||||
|
import ctypes
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import signal
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
BP_EXP = 0x1803831c0
|
||||||
|
BP_DF0 = 0x18000b3c0
|
||||||
|
BP_COPY = 0x1800136e0
|
||||||
|
BP_DF0RET = 0x18052b898
|
||||||
|
BP_TRACKSAVE = 0x18052b574
|
||||||
|
BP_DIV = 0x1803a06a0
|
||||||
|
BP_DC40 = 0x1800dc40
|
||||||
|
BP_EXPVAR = 0x1802dc0e0
|
||||||
|
BP_FN = 0x180529fe0
|
||||||
|
BP_CIN = 0x180529c60
|
||||||
|
BP_COUT = 0x180529ee1
|
||||||
|
BP_AIN = 0x180016140
|
||||||
|
BP_AOUT = 0x18000332c
|
||||||
|
# Chain 9-19 breakpoints (BLOCKMAP:620-644)
|
||||||
|
BP_CHAIN_START = 0x18052a580 # band loop start (52a580)
|
||||||
|
BP_CHAIN_LOG1 = 0x18052a63a # LOG#1 in chain (52a63a)
|
||||||
|
BP_CHAIN_LOG2 = 0x18052aefd # LOG#2 in chain (52aefd)
|
||||||
|
BP_CHAIN_END = 0x18052b3cd # chain end / FIR start (52b3cd)
|
||||||
|
CTX_SLOTS = {'scr': 0x540628, 'trk': 0x540688, 'cur': 0x540678,
|
||||||
|
'fir_ptr': 0x540668}
|
||||||
|
|
||||||
|
libc = ctypes.CDLL('libc.so.6', use_errno=True)
|
||||||
|
PTRACE_ATTACH = 16
|
||||||
|
PTRACE_DETACH = 17
|
||||||
|
PTRACE_CONT = 7
|
||||||
|
PTRACE_SINGLESTEP = 9
|
||||||
|
PTRACE_PEEKDATA = 2
|
||||||
|
PTRACE_POKEDATA = 5
|
||||||
|
PTRACE_GETREGS = 12
|
||||||
|
PTRACE_SETOPTIONS = 0x4200
|
||||||
|
PTRACE_O_TRACECLONE = 1 << 22
|
||||||
|
__WALL = 0x40000000
|
||||||
|
|
||||||
|
libc.ptrace.restype = ctypes.c_long
|
||||||
|
libc.ptrace.argtypes = [ctypes.c_long, ctypes.c_long,
|
||||||
|
ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
|
||||||
|
|
||||||
|
class UserRegs(ctypes.Structure):
|
||||||
|
_fields_ = [('r15', ctypes.c_uint64), ('r14', ctypes.c_uint64),
|
||||||
|
('r13', ctypes.c_uint64), ('r12', ctypes.c_uint64),
|
||||||
|
('rbp', ctypes.c_uint64), ('rbx', ctypes.c_uint64),
|
||||||
|
('r11', ctypes.c_uint64), ('r10', ctypes.c_uint64),
|
||||||
|
('r9', ctypes.c_uint64), ('r8', ctypes.c_uint64),
|
||||||
|
('rax', ctypes.c_uint64), ('rcx', ctypes.c_uint64),
|
||||||
|
('rdx', ctypes.c_uint64), ('rsi', ctypes.c_uint64),
|
||||||
|
('rdi', ctypes.c_uint64), ('orig_rax', ctypes.c_uint64),
|
||||||
|
('rip', ctypes.c_uint64), ('cs', ctypes.c_uint64),
|
||||||
|
('eflags', ctypes.c_uint64), ('rsp', ctypes.c_uint64),
|
||||||
|
('ss', ctypes.c_uint64),
|
||||||
|
('fs_base', ctypes.c_uint64), ('gs_base', ctypes.c_uint64),
|
||||||
|
('ds', ctypes.c_uint64), ('es', ctypes.c_uint64),
|
||||||
|
('fs', ctypes.c_uint64), ('gs', ctypes.c_uint64)]
|
||||||
|
|
||||||
|
|
||||||
|
def pt(req, pid, addr=0, data=0):
|
||||||
|
if not isinstance(data, int):
|
||||||
|
data = ctypes.cast(data, ctypes.c_void_p)
|
||||||
|
else:
|
||||||
|
data = ctypes.c_void_p(data)
|
||||||
|
return libc.ptrace(req, pid, ctypes.c_void_p(addr), data)
|
||||||
|
|
||||||
|
|
||||||
|
def getregs(tid):
|
||||||
|
r = UserRegs()
|
||||||
|
if pt(PTRACE_GETREGS, tid, 0, ctypes.byref(r)) != 0:
|
||||||
|
raise OSError('GETREGS tid=%d' % tid)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def setregs(tid, r):
|
||||||
|
if pt(PTRACE_SETREGS := 13, tid, 0, ctypes.byref(r)) != 0:
|
||||||
|
raise OSError('SETREGS tid=%d' % tid)
|
||||||
|
|
||||||
|
|
||||||
|
def peek(tid, addr):
|
||||||
|
v = pt(PTRACE_PEEKDATA, tid, addr, 0)
|
||||||
|
if v == -1:
|
||||||
|
e = ctypes.get_errno()
|
||||||
|
if e != 0:
|
||||||
|
raise OSError(e)
|
||||||
|
return v & 0xFFFFFFFFFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def poke(tid, addr, val):
|
||||||
|
if pt(PTRACE_POKEDATA, tid, addr, val) == -1 and ctypes.get_errno():
|
||||||
|
raise OSError('POKEDATA %#x tid=%d: %d' % (addr, tid, ctypes.get_errno()))
|
||||||
|
|
||||||
|
|
||||||
|
def find_host():
|
||||||
|
import glob
|
||||||
|
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 find_ctx(fd, pid):
|
||||||
|
vt = struct.pack('<Q', 0x1824AC210)
|
||||||
|
m48 = struct.pack('<I', 0x47380000)
|
||||||
|
for line in open(f'/proc/{pid}/maps'):
|
||||||
|
parts = line.split()
|
||||||
|
if 'rw' not in parts[1]:
|
||||||
|
continue
|
||||||
|
lo, hi = (int(x, 16) for x in parts[0].split('-'))
|
||||||
|
CH = 16 * 1024 * 1024
|
||||||
|
a = lo
|
||||||
|
while a < hi:
|
||||||
|
n = min(CH, hi - a)
|
||||||
|
try:
|
||||||
|
d = os.pread(fd, n, a)
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
j = d.find(vt)
|
||||||
|
while j >= 0:
|
||||||
|
cand = a + j
|
||||||
|
sb = os.pread(fd, 4, cand + 0x540870)
|
||||||
|
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||||
|
return cand
|
||||||
|
j = d.find(vt, j + 1)
|
||||||
|
j = d.find(m48)
|
||||||
|
while j >= 0:
|
||||||
|
cand = a + j - 0x24
|
||||||
|
try:
|
||||||
|
sb = os.pread(fd, 4, cand + 0x540870)
|
||||||
|
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||||
|
return cand
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
j = d.find(m48, j + 1)
|
||||||
|
a += n
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_ctx_candidates(fd, pid, fir_ptr):
|
||||||
|
"""Все адреса X (кратные 8), где [X+0x540668]==fir_ptr => кандидат X."""
|
||||||
|
val = struct.pack('<Q', fir_ptr)
|
||||||
|
out = []
|
||||||
|
for line in open(f'/proc/{pid}/maps'):
|
||||||
|
parts = line.split()
|
||||||
|
if 'rw' not in parts[1]:
|
||||||
|
continue
|
||||||
|
lo, hi = (int(x, 16) for x in parts[0].split('-'))
|
||||||
|
CH = 16 * 1024 * 1024
|
||||||
|
a = lo
|
||||||
|
while a < hi:
|
||||||
|
n = min(CH, hi - a)
|
||||||
|
try:
|
||||||
|
d = os.pread(fd, n, a)
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
j = d.find(val)
|
||||||
|
while j >= 0:
|
||||||
|
if j % 8 == 0:
|
||||||
|
out.append(a + j - 0x540668)
|
||||||
|
j = d.find(val, j + 1)
|
||||||
|
a += n
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
|
||||||
|
n_target = int(sys.argv[2]) if len(sys.argv) > 2 else 80
|
||||||
|
outdir = sys.argv[3] if len(sys.argv) > 3 else '/tmp/opencode/winetrace'
|
||||||
|
os.makedirs(outdir, exist_ok=True)
|
||||||
|
|
||||||
|
wav = None
|
||||||
|
for ln in open(rpp, errors='replace'):
|
||||||
|
if 'RENDER_FILE' in ln and '"' in ln:
|
||||||
|
wav = ln.split('"')[1]
|
||||||
|
break
|
||||||
|
if wav and os.path.exists(wav):
|
||||||
|
os.remove(wav)
|
||||||
|
|
||||||
|
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1",
|
||||||
|
shell=True)
|
||||||
|
|
||||||
|
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
|
||||||
|
'-renderproject', rpp],
|
||||||
|
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
|
||||||
|
t0 = time.time()
|
||||||
|
host = None
|
||||||
|
ctx_fd = None
|
||||||
|
ctx = None
|
||||||
|
# Фаза 1: ждём появления хоста и контекста ЧИТАЮЧЕЙ памятью (без ptrace),
|
||||||
|
# чтобы не мешать загрузке плагина
|
||||||
|
while time.time() - t0 < 25:
|
||||||
|
if host is None:
|
||||||
|
host = find_host()
|
||||||
|
if host:
|
||||||
|
try:
|
||||||
|
ctx_fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
||||||
|
print('host %d (+%.3fs)' % (host, time.time()-t0), flush=True)
|
||||||
|
except OSError:
|
||||||
|
host = None
|
||||||
|
time.sleep(0.001)
|
||||||
|
continue
|
||||||
|
if host is not None:
|
||||||
|
try:
|
||||||
|
ctx = find_ctx(ctx_fd, host)
|
||||||
|
except (ProcessLookupError, OSError):
|
||||||
|
ctx = None
|
||||||
|
host = None
|
||||||
|
time.sleep(0.001)
|
||||||
|
continue
|
||||||
|
if ctx:
|
||||||
|
break
|
||||||
|
time.sleep(0.002)
|
||||||
|
if not host or not ctx:
|
||||||
|
print('NO HOST/CTX (host=%s ctx=%s)' % (host, ctx))
|
||||||
|
return 1
|
||||||
|
print('ctx %#x (+%.3fs)' % (ctx, time.time()-t0), flush=True)
|
||||||
|
fd = ctx_fd
|
||||||
|
|
||||||
|
def rd(a, n):
|
||||||
|
return os.pread(fd, n, a)
|
||||||
|
|
||||||
|
def rd_f32(a, n):
|
||||||
|
return np.frombuffer(rd(a, 4*n), dtype='<f4').astype(np.float64)
|
||||||
|
|
||||||
|
def rd_q(a):
|
||||||
|
return struct.unpack('<Q', rd(a, 8))[0]
|
||||||
|
|
||||||
|
# Фаза 2: аттач ко всем текущим тредам хоста
|
||||||
|
tids = [int(t) for t in os.listdir(f'/proc/{host}/task')]
|
||||||
|
attached = []
|
||||||
|
for tid in tids:
|
||||||
|
try:
|
||||||
|
if pt(PTRACE_ATTACH, tid) == -1 and ctypes.get_errno():
|
||||||
|
raise OSError(ctypes.get_errno())
|
||||||
|
os.waitpid(tid, __WALL)
|
||||||
|
pt(PTRACE_SETOPTIONS, tid, 0, PTRACE_O_TRACECLONE)
|
||||||
|
attached.append(tid)
|
||||||
|
except OSError as e:
|
||||||
|
print('attach fail tid=%d: %s' % (tid, e), flush=True)
|
||||||
|
print('attached %d/%d' % (len(attached), len(tids)), flush=True)
|
||||||
|
|
||||||
|
# Фаза 3: int3 и запуск
|
||||||
|
bps = {}
|
||||||
|
# проверка маппенности по /proc/pid/maps
|
||||||
|
maps_txt = open(f'/proc/{host}/maps').read()
|
||||||
|
|
||||||
|
def mapped(a):
|
||||||
|
for ln in maps_txt.splitlines():
|
||||||
|
rng = ln.split()[0]
|
||||||
|
lo, hi = (int(x, 16) for x in rng.split('-'))
|
||||||
|
if lo <= a < hi:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
for name, addr in (('COPY', BP_COPY), ('EXP', BP_EXP), ('DF0', BP_DF0),
|
||||||
|
('DF0RET', BP_DF0RET), ('TRACKSAVE', BP_TRACKSAVE),
|
||||||
|
('DIV', BP_DIV), ('DC40', BP_DC40),
|
||||||
|
('EXPVAR', BP_EXPVAR), ('FN', BP_FN),
|
||||||
|
('CIN', BP_CIN), ('COUT', BP_COUT),
|
||||||
|
('AIN', BP_AIN), ('AOUT', BP_AOUT),
|
||||||
|
# Chain 9-19 breakpoints
|
||||||
|
('CHAIN_START', BP_CHAIN_START),
|
||||||
|
('CHAIN_LOG1', BP_CHAIN_LOG1),
|
||||||
|
('CHAIN_LOG2', BP_CHAIN_LOG2),
|
||||||
|
('CHAIN_END', BP_CHAIN_END)):
|
||||||
|
if not mapped(addr):
|
||||||
|
print('!! %s@%#x не смапплен — пропуск' % (nm_ := name, addr), flush=True)
|
||||||
|
continue
|
||||||
|
orig = peek(host, addr)
|
||||||
|
poke(host, addr, (orig & ~0xFF) | 0xCC)
|
||||||
|
bps[addr] = (name, orig & 0xFF)
|
||||||
|
print('int3 installed:', {hex(a): n for a, (n, _) in bps.items()}, flush=True)
|
||||||
|
for addr, (nm, _) in bps.items():
|
||||||
|
rb = peek(host, addr) & 0xFF
|
||||||
|
if rb != 0xCC:
|
||||||
|
print('!! %s@%#x НЕ 0xCC: %#02x' % (nm, addr, rb), flush=True)
|
||||||
|
for tid in attached:
|
||||||
|
pt(PTRACE_CONT, tid, 0, 0)
|
||||||
|
|
||||||
|
samples = []
|
||||||
|
hits = {'COPY': 0, 'EXP': 0, 'DF0': 0, 'DF0RET': 0, 'TRACKSAVE': 0,
|
||||||
|
'DIV': 0, 'DC40': 0, 'EXPVAR': 0, 'FN': 0,
|
||||||
|
'CIN': 0, 'COUT': 0, 'AIN': 0, 'AOUT': 0,
|
||||||
|
'CHAIN_START': 0, 'CHAIN_LOG1': 0, 'CHAIN_LOG2': 0, 'CHAIN_END': 0}
|
||||||
|
track_by_tid = {}
|
||||||
|
track_dumps = []
|
||||||
|
chain_dumps = []
|
||||||
|
regs_by_tid = {}
|
||||||
|
t_start = time.time()
|
||||||
|
|
||||||
|
def snapshot_slots(rec):
|
||||||
|
rec['scr'] = rd_f32(ctx+CTX_SLOTS['scr'], 2049)
|
||||||
|
rec['trk'] = rd_f32(ctx+CTX_SLOTS['trk'], 2049)
|
||||||
|
rec['cur'] = rd_f32(ctx+CTX_SLOTS['cur'], 2049)
|
||||||
|
fp = rd_q(ctx+CTX_SLOTS['fir_ptr'])
|
||||||
|
rec['fir_via_ctx'] = rd_f32(fp, 4098)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while sum(hits.values()) < n_target and time.time() - t_start < 300:
|
||||||
|
try:
|
||||||
|
pid, status = os.waitpid(-1, __WALL | os.WNOHANG)
|
||||||
|
except ChildProcessError:
|
||||||
|
print('нет отслеживаемых процессов', flush=True)
|
||||||
|
break
|
||||||
|
if (pid, status) == (0, 0):
|
||||||
|
# никого не остановлено — короткий сон, дедлайн проверится сверху
|
||||||
|
time.sleep(0.0005)
|
||||||
|
continue
|
||||||
|
if not os.WIFSTOPPED(status):
|
||||||
|
# выход треда/процесса
|
||||||
|
if pid in attached:
|
||||||
|
attached.remove(pid)
|
||||||
|
if pid == host:
|
||||||
|
print('host exited', flush=True)
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
sig = os.WSTOPSIG(status)
|
||||||
|
if sig == signal.SIGTRAP:
|
||||||
|
try:
|
||||||
|
regs = getregs(pid)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
site = regs.rip - 1
|
||||||
|
info = bps.get(site)
|
||||||
|
if info is None:
|
||||||
|
# чужой SIGTRAP (clone/event) — просто продолжить
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
continue
|
||||||
|
kind, obyte = info
|
||||||
|
if kind == 'TRACKSAVE':
|
||||||
|
# rax = track-ptr текущей полосы, r12 = индекс полосы,
|
||||||
|
# [rsp+0x138] = база таблицы указателей (arg2 fn529fe0)
|
||||||
|
tbl = rd_q(regs.rsp + 0x138) if regs.rsp else 0
|
||||||
|
rec_t = {'kind': 'TRACKSAVE', 'tid': pid, 'band': regs.r12,
|
||||||
|
'track_ptr': regs.rax, 'tbl': tbl,
|
||||||
|
't': round(time.time()-t_start, 4)}
|
||||||
|
if len(track_dumps) < 48:
|
||||||
|
try:
|
||||||
|
rec_t['tbl_entries'] = [rd_q(tbl+8*i) for i in range(16)]
|
||||||
|
rec_t['trk_curve'] = rd_f32(regs.rax, 2049*2)
|
||||||
|
except OSError as e:
|
||||||
|
rec_t['err'] = str(e)
|
||||||
|
track_dumps.append(rec_t)
|
||||||
|
samples.append(rec_t)
|
||||||
|
hits['TRACKSAVE'] += 1
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
continue
|
||||||
|
if kind == 'FN':
|
||||||
|
ra = rd_q(regs.rsp)
|
||||||
|
rec_f = {'kind':'FN','tid':pid,
|
||||||
|
'rcx':regs.rcx,'rdx':regs.rdx,'r8':regs.r8,'r9':regs.r9,
|
||||||
|
'ret':ra,'t':round(time.time()-t_start,4)}
|
||||||
|
samples.append(rec_f); hits['FN'] += 1
|
||||||
|
if hits['FN'] <= 3:
|
||||||
|
print('FN: rcx=%#x rdx=%#x r8=%#x r9=%#x ret=%#x'%(
|
||||||
|
regs.rcx,regs.rdx,regs.r8,regs.r9,ra), flush=True)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
continue
|
||||||
|
if kind in ('DIV','DC40','EXPVAR'):
|
||||||
|
rec_a = {'kind': kind, 'tid': pid,
|
||||||
|
't': round(time.time()-t_start, 4),
|
||||||
|
'rcx': regs.rcx, 'rdx': regs.rdx,
|
||||||
|
'r8': regs.r8, 'r9': regs.r9}
|
||||||
|
try:
|
||||||
|
for nm, p, cnt in (('a', regs.rcx, 2050),
|
||||||
|
('b', regs.rdx, 2050),
|
||||||
|
('c', regs.r8, 2050)):
|
||||||
|
if p > 0x10000:
|
||||||
|
rec_a[nm] = rd_f32(p, cnt)
|
||||||
|
except OSError as e:
|
||||||
|
rec_a['err'] = str(e)
|
||||||
|
samples.append(rec_a)
|
||||||
|
hits[kind] += 1
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
continue
|
||||||
|
if kind == 'DF0':
|
||||||
|
track_by_tid[pid] = regs.rdx
|
||||||
|
# Chain 9-19: dump buffers at key points
|
||||||
|
if kind in ('CHAIN_START', 'CHAIN_LOG1', 'CHAIN_LOG2', 'CHAIN_END'):
|
||||||
|
rec_c = {'kind': kind, 'tid': pid,
|
||||||
|
't': round(time.time()-t_start, 4),
|
||||||
|
'rcx': regs.rcx, 'rdx': regs.rdx,
|
||||||
|
'r8': regs.r8, 'r9': regs.r9,
|
||||||
|
'rsp': regs.rsp}
|
||||||
|
try:
|
||||||
|
# chain input: [678i] = rcx (bands_curve), [6f8] = rdx (tmp6f8)
|
||||||
|
if regs.rcx > 0x10000 and regs.rdx > 0x10000:
|
||||||
|
rec_c['bands_in'] = rd_f32(regs.rcx, min(2050, 512)) # first 512 vals
|
||||||
|
rec_c['tmp6f8_in'] = rd_f32(regs.rdx, min(2050, 512))
|
||||||
|
rec_c['acc_ptr'] = regs.r8 if regs.r8 > 0x10000 else 0
|
||||||
|
if rec_c['acc_ptr']:
|
||||||
|
rec_c['acc_in'] = rd_f32(rec_c['acc_ptr'], min(2050, 512))
|
||||||
|
except OSError as e:
|
||||||
|
rec_c['err'] = str(e)
|
||||||
|
chain_dumps.append(rec_c)
|
||||||
|
hits[kind] += 1
|
||||||
|
# standard int3 handling
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
continue
|
||||||
|
if kind in ('CIN','COUT'):
|
||||||
|
if kind=='CIN':
|
||||||
|
regs_by_tid[pid]=dict(rdx=regs.rdx,r12=regs.r12,
|
||||||
|
rcx=regs.rcx)
|
||||||
|
rec_s={'kind':kind,'tid':pid,'t':round(time.time()-t_start,4)}
|
||||||
|
try:
|
||||||
|
bp=regs_by_tid.get(pid,{})
|
||||||
|
trk=bp.get('rdx',0)
|
||||||
|
if trk>0x10000:
|
||||||
|
rec_s['trk']=rd_f32(trk,4100)
|
||||||
|
# все кривые bands из таблицы ctx+0x540678 (до 4 полос)
|
||||||
|
for bi in range(4):
|
||||||
|
p=rd_q(ctx+0x540678+8*bi)
|
||||||
|
if p>0x10000:
|
||||||
|
rec_s['bands%d'%bi]=rd_f32(p,2050)
|
||||||
|
except OSError as e:
|
||||||
|
rec_s['err']=str(e)
|
||||||
|
samples.append(rec_s); hits[kind]+=1
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
continue
|
||||||
|
if kind in ('AIN','AOUT'):
|
||||||
|
key='a_%d'%pid
|
||||||
|
if kind=='AIN':
|
||||||
|
regs_by_tid[pid]=dict(rcx=regs.rcx,rdx=regs.rdx)
|
||||||
|
rec_s={'kind':kind,'tid':pid,'t':round(time.time()-t_start,4)}
|
||||||
|
try:
|
||||||
|
bp=regs_by_tid.get(pid,{})
|
||||||
|
for nm,kk in (('a',bp.get('rcx',0)),('b',bp.get('rdx',0))):
|
||||||
|
if kk>0x10000:
|
||||||
|
rec_s[nm]=rd_f32(kk,4100)
|
||||||
|
rec_s['n']=regs.r8&0xFFFFFFFF if kind=='AIN' else None
|
||||||
|
except OSError as e:
|
||||||
|
rec_s['err']=str(e)
|
||||||
|
samples.append(rec_s); hits[kind]+=1
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
continue
|
||||||
|
if kind == 'DF0RET':
|
||||||
|
tp = track_by_tid.get(pid)
|
||||||
|
rec_r = {'kind': 'DF0RET', 'tid': pid,
|
||||||
|
't': round(time.time()-t_start, 4)}
|
||||||
|
try:
|
||||||
|
if tp and tp > 0x10000:
|
||||||
|
rec_r['track'] = rd_f32(tp, 2049*2)
|
||||||
|
samples.append(rec_r)
|
||||||
|
hits['DF0RET'] += 1
|
||||||
|
except OSError as e:
|
||||||
|
rec_r['err'] = str(e)
|
||||||
|
samples.append(rec_r)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
continue
|
||||||
|
if kind == 'COPY':
|
||||||
|
try:
|
||||||
|
cnt = min(regs.r9 & 0xFFFFFFFF, 2049)
|
||||||
|
rec_c = {'kind': 'COPY', 'tid': pid,
|
||||||
|
't': round(time.time()-t_start, 4),
|
||||||
|
'src': rd_f32(regs.rcx, cnt),
|
||||||
|
'dst': regs.r8}
|
||||||
|
# снять int3/step/restore как у остальных — общий код ниже
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
samples.append(rec_c)
|
||||||
|
hits['COPY'] += 1
|
||||||
|
continue
|
||||||
|
except OSError as e:
|
||||||
|
print('copy err', e, flush=True)
|
||||||
|
continue
|
||||||
|
# ctx по фактическому указателю FIR из хита + валидация
|
||||||
|
# инварианта trk==exp(scr) (24mm3), строгая
|
||||||
|
if kind == 'DF0':
|
||||||
|
good = None
|
||||||
|
cands = find_ctx_candidates(fd, host, regs.rcx)
|
||||||
|
for cand in cands:
|
||||||
|
if cand <= 0x10000:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
v_sc = rd_f32(cand+CTX_SLOTS['scr'], 2049)
|
||||||
|
v_tr = rd_f32(cand+CTX_SLOTS['trk'], 2049)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if not (np.isfinite(v_sc).all() and np.isfinite(v_tr).all()):
|
||||||
|
continue
|
||||||
|
if np.abs(v_sc).max() > 40:
|
||||||
|
continue
|
||||||
|
if np.allclose(v_tr, np.exp(v_sc), rtol=1e-3, atol=1e-9):
|
||||||
|
good = cand
|
||||||
|
break
|
||||||
|
if pc_dbg := True:
|
||||||
|
for cand in cands[:4]:
|
||||||
|
try:
|
||||||
|
vs = rd_f32(cand+CTX_SLOTS['scr'], 2049)
|
||||||
|
vt = rd_f32(cand+CTX_SLOTS['trk'], 2049)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
dmax = np.abs(vt-np.exp(np.clip(vs,-80,80))).max()
|
||||||
|
print(' cand %#x: |scr|=%.4g |trk|=%.4g maxdiff=%.4g'
|
||||||
|
% (cand, np.abs(vs).max(), np.abs(vt).max(), dmax),
|
||||||
|
flush=True)
|
||||||
|
print('cands=%d good=%s' % (len(cands), hex(good) if good else '-'),
|
||||||
|
flush=True)
|
||||||
|
if good:
|
||||||
|
ctx = good
|
||||||
|
if kind == 'FN':
|
||||||
|
ra = rd_q(regs.rsp)
|
||||||
|
rec_f = {'kind':'FN','tid':pid,
|
||||||
|
'rcx':regs.rcx,'rdx':regs.rdx,'r8':regs.r8,'r9':regs.r9,
|
||||||
|
'ret':ra,'t':round(time.time()-t_start,4)}
|
||||||
|
samples.append(rec_f); hits['FN'] += 1
|
||||||
|
if hits['FN'] <= 3:
|
||||||
|
print('FN: rcx=%#x rdx=%#x r8=%#x r9=%#x ret=%#x'%(
|
||||||
|
regs.rcx,regs.rdx,regs.r8,regs.r9,ra), flush=True)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
continue
|
||||||
|
if kind in ('DIV','DC40','EXPVAR'):
|
||||||
|
rec_a = {'kind': kind, 'tid': pid,
|
||||||
|
't': round(time.time()-t_start, 4),
|
||||||
|
'rcx': regs.rcx, 'rdx': regs.rdx,
|
||||||
|
'r8': regs.r8, 'r9': regs.r9}
|
||||||
|
try:
|
||||||
|
for nm, p, cnt in (('a', regs.rcx, 2050),
|
||||||
|
('b', regs.rdx, 2050),
|
||||||
|
('c', regs.r8, 2050)):
|
||||||
|
if p > 0x10000:
|
||||||
|
rec_a[nm] = rd_f32(p, cnt)
|
||||||
|
except OSError as e:
|
||||||
|
rec_a['err'] = str(e)
|
||||||
|
samples.append(rec_a)
|
||||||
|
hits[kind] += 1
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
continue
|
||||||
|
if kind == 'DF0':
|
||||||
|
track_by_tid[pid] = regs.rdx
|
||||||
|
rec = {'kind': kind, 'tid': pid,
|
||||||
|
'rcx': regs.rcx, 'rdx': regs.rdx, 'r8': regs.r8 & 0xFFFFFFFF,
|
||||||
|
't': round(time.time()-t_start, 4)}
|
||||||
|
try:
|
||||||
|
if kind == 'EXP':
|
||||||
|
rec['buf'] = rd_f32(regs.rcx, 4098)
|
||||||
|
rec['count'] = rec['r8']
|
||||||
|
else:
|
||||||
|
rec['fir'] = rd_f32(regs.rcx, 4098)
|
||||||
|
if regs.rdx > 0x10000:
|
||||||
|
rec['track'] = rd_f32(regs.rdx, 2049*2)
|
||||||
|
if ctx:
|
||||||
|
snapshot_slots(rec)
|
||||||
|
if kind == 'DF0':
|
||||||
|
rec['fir_via_ctx'] = rec.get('fir_via_ctx')
|
||||||
|
except OSError as e:
|
||||||
|
rec['err'] = str(e)
|
||||||
|
samples.append(rec)
|
||||||
|
hits[kind] += 1
|
||||||
|
# снять int3 -> шаг назад -> singlestep -> вернуть int3 -> cont
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
if sum(hits.values()) % 10 == 0:
|
||||||
|
print('hits:', hits, flush=True)
|
||||||
|
elif sig in (signal.SIGSTOP, signal.SIGCHLD, signal.SIGWINCH):
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
else:
|
||||||
|
# посторонний сигнал — доставить
|
||||||
|
pt(PTRACE_CONT, pid, 0, sig)
|
||||||
|
finally:
|
||||||
|
# снять int3 и отсоединиться
|
||||||
|
for addr, (name, obyte) in bps.items():
|
||||||
|
try:
|
||||||
|
poke(host, addr, (peek(host, addr) & ~0xFF) | obyte)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
for tid in list(attached):
|
||||||
|
try:
|
||||||
|
pt(PTRACE_DETACH, tid, 0, 0)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print('сбор завершён:', hits, flush=True)
|
||||||
|
snap_ptrs, snap_arr = {}, {}
|
||||||
|
for nm, off in CTX_SLOTS.items():
|
||||||
|
p = rd_q(ctx+off)
|
||||||
|
if p > 0x10000:
|
||||||
|
snap_ptrs[nm] = p
|
||||||
|
snap_arr[nm] = rd_f32(p, 4100)
|
||||||
|
with open(os.path.join(outdir, 'chain_samples.pkl'), 'wb') as f:
|
||||||
|
pickle.dump({'samples': samples, 'chain_dumps': chain_dumps, 'snap_ptrs': snap_ptrs, 'ctx': ctx}, f)
|
||||||
|
np.savez_compressed(os.path.join(outdir, 'ctx_snap.npz'), **snap_arr)
|
||||||
|
print('saved %d -> %s' % (len(samples), outdir), flush=True)
|
||||||
|
for _ in range(600):
|
||||||
|
if proc.poll() is not None:
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
print('reaper_rc=%s wav=%s' % (proc.poll(),
|
||||||
|
os.path.getsize(wav) if wav and os.path.exists(wav) else 'NONE'), flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""wine_stage_trace.py — трассировка СТАДИЙ пайплайна через vtable ctx.
|
||||||
|
|
||||||
|
На входе fn529fe0: читает vtable=[ctx], ставит int3 на таргеты слотов
|
||||||
|
{8,0x18,0x20,0x28,0x30,0x48,0xe8,0x218,0x220,0x228,0x230}, снапшотит
|
||||||
|
track-буферы (таблица @arg2, count=r9). На каждом хите стадии: md5
|
||||||
|
track-буферов + аргументы. Разница md5 между стадиями = кто пишет track.
|
||||||
|
"""
|
||||||
|
import ctypes
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import signal
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from wine_ptrace_trace import ( # noqa
|
||||||
|
pt, getregs, setregs, peek, poke, find_host, find_ctx,
|
||||||
|
PTRACE_ATTACH, PTRACE_DETACH, PTRACE_CONT, PTRACE_SINGLESTEP,
|
||||||
|
PTRACE_SETOPTIONS, PTRACE_O_TRACECLONE, __WALL)
|
||||||
|
|
||||||
|
BP_FN = 0x180529fe0
|
||||||
|
SLOTS = [0x8, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38, 0x40, 0x48,
|
||||||
|
0xe8, 0x218, 0x220, 0x228, 0x230]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/comb_b1234.rpp'
|
||||||
|
n_frames = int(sys.argv[2]) if len(sys.argv) > 2 else 6
|
||||||
|
outdir = sys.argv[3] if len(sys.argv) > 3 else '/tmp/opencode/winetrace_casc'
|
||||||
|
os.makedirs(outdir, exist_ok=True)
|
||||||
|
|
||||||
|
wav = None
|
||||||
|
for ln in open(rpp, errors='replace'):
|
||||||
|
if 'RENDER_FILE' in ln and '"' in ln:
|
||||||
|
wav = ln.split('"')[1]
|
||||||
|
break
|
||||||
|
|
||||||
|
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; sleep 1",
|
||||||
|
shell=True)
|
||||||
|
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
|
||||||
|
'-renderproject', rpp],
|
||||||
|
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
|
||||||
|
t0 = time.time()
|
||||||
|
host = None
|
||||||
|
while time.time() - t0 < 25:
|
||||||
|
host = find_host()
|
||||||
|
if host:
|
||||||
|
break
|
||||||
|
time.sleep(0.001)
|
||||||
|
if not host:
|
||||||
|
print('NO HOST')
|
||||||
|
return 1
|
||||||
|
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
||||||
|
ctx = None
|
||||||
|
while ctx is None and time.time() - t0 < 25:
|
||||||
|
try:
|
||||||
|
ctx = find_ctx(fd, host)
|
||||||
|
except (ProcessLookupError, OSError):
|
||||||
|
return 1
|
||||||
|
if not ctx:
|
||||||
|
time.sleep(0.002)
|
||||||
|
print('host %d ctx %#x (+%.2fs)' % (host, ctx, time.time()-t0), flush=True)
|
||||||
|
|
||||||
|
def rd(a, n):
|
||||||
|
return os.pread(fd, n, a)
|
||||||
|
|
||||||
|
def rd_f32(a, n):
|
||||||
|
return np.frombuffer(rd(a, 4*n), dtype='<f4').astype(np.float64)
|
||||||
|
|
||||||
|
def rd_q(a):
|
||||||
|
return struct.unpack('<Q', rd(a, 8))[0]
|
||||||
|
|
||||||
|
maps_txt = open(f'/proc/{host}/maps').read()
|
||||||
|
|
||||||
|
def mapped(a):
|
||||||
|
for ln in maps_txt.splitlines():
|
||||||
|
rng = ln.split()[0]
|
||||||
|
lo, hi = (int(x, 16) for x in rng.split('-'))
|
||||||
|
if lo <= a < hi:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# attach
|
||||||
|
tids = [int(t) for t in os.listdir(f'/proc/{host}/task')]
|
||||||
|
attached = []
|
||||||
|
for tid in tids:
|
||||||
|
try:
|
||||||
|
if pt(PTRACE_ATTACH, tid) == -1 and ctypes.get_errno():
|
||||||
|
raise OSError(ctypes.get_errno())
|
||||||
|
os.waitpid(tid, __WALL)
|
||||||
|
pt(PTRACE_SETOPTIONS, tid, 0, PTRACE_O_TRACECLONE)
|
||||||
|
attached.append(tid)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
print('attached %d' % len(attached), flush=True)
|
||||||
|
|
||||||
|
# vtable + стадии
|
||||||
|
vt = rd_q(ctx)
|
||||||
|
stage_targets = {}
|
||||||
|
for s in SLOTS:
|
||||||
|
tgt = rd_q(vt + s)
|
||||||
|
if mapped(tgt) and tgt not in stage_targets.values():
|
||||||
|
stage_targets[s] = tgt
|
||||||
|
inv = {v: ('vt+%#x' % k) for k, v in stage_targets.items()}
|
||||||
|
print('стадии:', {hex(k): hex(v) for k, v in stage_targets.items()}, flush=True)
|
||||||
|
|
||||||
|
bps = {}
|
||||||
|
for slot, tgt in stage_targets.items():
|
||||||
|
orig = peek(host, tgt)
|
||||||
|
poke(host, tgt, (orig & ~0xFF) | 0xCC)
|
||||||
|
bps[tgt] = (('vt%#x' % slot), orig & 0xFF)
|
||||||
|
orig_fn = peek(host, BP_FN)
|
||||||
|
poke(host, BP_FN, (orig_fn & ~0xFF) | 0xCC)
|
||||||
|
bps[BP_FN] = ('FN', orig_fn & 0xFF)
|
||||||
|
|
||||||
|
for tid in attached:
|
||||||
|
pt(PTRACE_CONT, tid, 0, 0)
|
||||||
|
|
||||||
|
samples = []
|
||||||
|
frames_done = 0
|
||||||
|
cur_frame = None
|
||||||
|
t_start = time.time()
|
||||||
|
|
||||||
|
def track_snapshot(table, nbands):
|
||||||
|
out = {}
|
||||||
|
for i in range(nbands):
|
||||||
|
p = rd_q(table + 8*i)
|
||||||
|
if p > 0x10000:
|
||||||
|
out[i] = hashlib.md5(rd(p, 4098*4)).hexdigest()
|
||||||
|
return out
|
||||||
|
|
||||||
|
try:
|
||||||
|
while frames_done < n_frames and time.time() - t_start < 240:
|
||||||
|
try:
|
||||||
|
pid, status = os.waitpid(-1, __WALL | os.WNOHANG)
|
||||||
|
except ChildProcessError:
|
||||||
|
break
|
||||||
|
if (pid, status) == (0, 0):
|
||||||
|
time.sleep(0.0005)
|
||||||
|
continue
|
||||||
|
if not os.WIFSTOPPED(status):
|
||||||
|
if pid in attached:
|
||||||
|
attached.remove(pid)
|
||||||
|
continue
|
||||||
|
if os.WSTOPSIG(status) != signal.SIGTRAP:
|
||||||
|
pt(PTRACE_CONT, pid, 0, sig if False else 0)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
regs = getregs(pid)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
site = regs.rip - 1
|
||||||
|
info = bps.get(site)
|
||||||
|
if info is None:
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
continue
|
||||||
|
kind, obyte = info
|
||||||
|
|
||||||
|
def restore_and_go():
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||||
|
regs.rip = site
|
||||||
|
setregs(pid, regs)
|
||||||
|
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||||
|
os.waitpid(pid, __WALL)
|
||||||
|
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||||
|
pt(PTRACE_CONT, pid, 0, 0)
|
||||||
|
|
||||||
|
if kind == 'FN':
|
||||||
|
table = regs.rdx
|
||||||
|
nb = regs.r9 & 0xFFFFFFFF
|
||||||
|
cur_frame = {'t': round(time.time()-t_start, 4),
|
||||||
|
'ctx': regs.rcx, 'table': table, 'nbands': nb,
|
||||||
|
'md5_before': track_snapshot(table, nb),
|
||||||
|
'stages': []}
|
||||||
|
rec = dict(kind='FN', **{k: v for k, v in cur_frame.items()
|
||||||
|
if k != 'md5_before'})
|
||||||
|
samples.append(rec)
|
||||||
|
else:
|
||||||
|
if cur_frame is not None:
|
||||||
|
ent = {'stage': kind, 'site': hex(site),
|
||||||
|
'rcx': regs.rcx, 'rdx': regs.rdx,
|
||||||
|
'r8': regs.r8, 'r9': regs.r9,
|
||||||
|
'md5_after': track_snapshot(cur_frame['table'],
|
||||||
|
cur_frame['nbands'])}
|
||||||
|
cur_frame['stages'].append(ent)
|
||||||
|
if kind.startswith('vt') and frames_done < 2:
|
||||||
|
args = {}
|
||||||
|
for nm, p in (('rcx', regs.rcx), ('rdx', regs.rdx),
|
||||||
|
('r8', regs.r8)):
|
||||||
|
if p > 0x10000:
|
||||||
|
try:
|
||||||
|
args[nm] = rd_f32(p, 2050)[:64].tolist()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
samples.append({'kind': 'ARG:' + kind, 'site': hex(site),
|
||||||
|
'args64': str(args)[:400]})
|
||||||
|
if kind == 'vt+0x30':
|
||||||
|
# fn529fe0 завершился: финальный md5
|
||||||
|
if cur_frame is not None:
|
||||||
|
cur_frame['md5_after_fn'] = track_snapshot(
|
||||||
|
cur_frame['table'], cur_frame['nbands'])
|
||||||
|
frames_done += 1
|
||||||
|
samples.append({'kind': 'FRAME_END',
|
||||||
|
'frame': cur_frame})
|
||||||
|
cur_frame = None
|
||||||
|
|
||||||
|
restore_and_go()
|
||||||
|
finally:
|
||||||
|
for addr, (nm, obyte) in bps.items():
|
||||||
|
try:
|
||||||
|
poke(host, addr, (peek(host, addr) & ~0xFF) | obyte)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
for tid in list(attached):
|
||||||
|
try:
|
||||||
|
pt(PTRACE_DETACH, tid, 0, 0)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with open(os.path.join(outdir, 'stage_samples.pkl'), 'wb') as f:
|
||||||
|
pickle.dump(samples, f)
|
||||||
|
fr = [s for s in samples if s['kind'] == 'FRAME_END']
|
||||||
|
print('кадров собрано:', len(fr), flush=True)
|
||||||
|
for f_ in fr[:3]:
|
||||||
|
fr_ = f_['frame']
|
||||||
|
print('--- кадр t=%.2f bands=%d' % (fr_['t'], fr_['nbands']))
|
||||||
|
prev = fr_['md5_before']
|
||||||
|
for st in fr_['stages']:
|
||||||
|
ch = '' if st['md5_after'] == prev else ' <<< TRACK ИЗМЕНИЛСЯ'
|
||||||
|
print(' %-8s rcx=%#x rdx=%#x%s' % (st['stage'], st['rcx'],
|
||||||
|
st['rdx'], ch))
|
||||||
|
prev = st['md5_after']
|
||||||
|
print(' после fn:', fr_.get('md5_after_fn'))
|
||||||
|
print('reaper_rc=%s' % proc.poll(), flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
+5
-148
@@ -1,150 +1,7 @@
|
|||||||
## Objective
|
# summary.md — перенесён в архив
|
||||||
- Реверс-DSP oeksound soothe2 v1.1.2 VST3 → математическая модель (STFT, детектор резонансов, нотич-синтез). Текущая фаза — поведенческий параметр-свип через рендер Reaper RPP + статический декомпил (без декрипты бинарника пока).
|
|
||||||
|
|
||||||
> **⚠️ ИСТОРИЧЕСКИЙ ДОКУМЕНТ (поведенческая модель v4, sim.py, ок. 2026-08-17)**.
|
> **Исторический документ (поведенческая модель v4, `sim.py`, 2026-08-17).**
|
||||||
> Актуальный канон (bit-exact, P4) — `README.md`, `AGENTS.md`, `handoff/NOTES_LEVEL.md`.
|
> Актуальный канон — `README.md:13` + `handoff/NOTES_LEVEL_INDEX.md`.
|
||||||
> Здесь — ранняя поведенческая фаза реверса, сохранена как справочник.
|
> Полный текст сохранён: [`handoff/archive/summary_v4_2026-08-17.md`](handoff/archive/summary_v4_2026-08-17.md) (150 строк, LUT amount, Q-зонд, burst-триалы).
|
||||||
|
|
||||||
## Key Facts
|
Кратко: `out = x − amount·bp(fc,Q)` с LUT `A(depth)·S(sens)·H(sharp)·M(mode)`, `Q=3+0.36·sel`, `env` att/rel, `mix` кроссфейд. RMSE settled ≤0.05 dB. Детали — в архиве.
|
||||||
- Пользователь русскоязычный; sudo нет. Объект: `~/.wine/.../soothe2_x64.vst3` (PE32+, base 0x180000000), yabridge 5.1.1, Reaper 7.78.
|
|
||||||
- Frida невозможна (Seccomp). Дамп памяти — `pread /proc/pid/mem` от родителя. pkill-ловушки: paths без `harneb`/`reaper` → `pkill -9 -x reaper`, `pkill -9 -f '[y]abridge'`.
|
|
||||||
- Рендер-проект: `RENDER_RANGE 1 0 0 0 1000`, ITEM обязательно `POSITION 0`. Offline-рендер мгновенный → для живого дампа realtime (`RENDER_1X 1`) + `testtone.wav`.
|
|
||||||
- VST-state = 92-байт JUCE-заголовок + base64 XML `<SOOTHE2STATE>` + 54 `<PARAM id=... value=.../>`; внутри — UI-state без DSP.
|
|
||||||
|
|
||||||
## Breakthrough (исправлено в этой сессии)
|
|
||||||
- **Формат-адаптивность PARAM**: плагин применяет `<PARAM value>` ТОЛЬКО если строка сериализована в том же формате, что оригинал:
|
|
||||||
- full-precision (depth, band freq/q...) → `%.16f` (напр. `"0.9000000000000000"`). Короткие строки → схлопывание (fallback).
|
|
||||||
- short-формат `"X.0"` (selectivity, mix, mode, oversample, resolution) → ЛОМАЕТСЯ от `%.16f`.
|
|
||||||
- `sweep.py`/`tt_sweep.py` адаптивны: >4 десятичных в оригинале → `%.16f`, 1-4 → то же число знаков, иначе — как есть.
|
|
||||||
|
|
||||||
## Синтез модели (поведенческая, v4 — реализована в sim.py, RMSE на settled ≤0.05dB по всем свипам)
|
|
||||||
```
|
|
||||||
PARADИГМА (реализована): out(t) = x(t) − amount_b(t)·bp_b(t; fc, Q) [субтрактивный нотч, не микш-параллельный]
|
|
||||||
amount(total) = A(depth)·S(sens)·H(sharp)·M(mode) — LUT из измерений (пересечение, не аналит. фит)
|
|
||||||
A(depth): 0.5/0.864/1/2/3/5/10/20 → 0.560/0.588/0.598/0.667/0.727/0.820/0.939/0.989
|
|
||||||
S(sens): 0/6/12/24 → 0.48/0.76/1.0/1.0 (floor при sens=0!)
|
|
||||||
H(sharp): 1/3/5/10 → 0.20/0.59/0.84/1.0
|
|
||||||
M(mode): 0 → 0.745 (mode0 слабее), 1.. → 1.0
|
|
||||||
Q(sel): = 3.0+0.36·sel (2nd-order bandpass; реал. даёт более крутые борта — приближ.)
|
|
||||||
env(t): one-pole, α_attack=1−e^(−1/(T_a·fs)), α_release=1−e^(−1/(T_r·fs))
|
|
||||||
T_a = 0.02·e^(a/1.955) (0→20мс, 5→258мс, 10→3.3c); T_r LUT из спадов (0→27мс, 5→140мс, 10→15c)
|
|
||||||
детектор: лауднеss полосы |bp| smoothed 5мс > thr 0.01 → цель amt, иначе 0 (порог между probe 0.0005 и burst 0.2)
|
|
||||||
выход: x_out = mix/100·… — линейный кроссфейд: (100−mix)/100·x + mix/100·fully_reduced
|
|
||||||
(mix=100 = полная обработка = ref; mix=0 → сухой; проверено на s_corr_mix: монот. линейно)
|
|
||||||
```
|
|
||||||
Верификация (settled, RMSE по reduction dB на burst-окне):
|
|
||||||
| параметр | реал | sim | RMSE | | параметр | реал | sim | RMSE |
|
|
||||||
|---|---|---|--|---|---|---|---|---|
|
|
||||||
| дефолт | 7.70 | 7.70 | 0.002 | sharp=1 | 1.11 | 1.11 | 0.002 |
|
|
||||||
| depth=2 | 9.56 | 9.56 | 0.001 | sharp=3 | 3.72 | 3.71 | 0.006 |
|
|
||||||
| depth=5 | 14.90 | 14.89 | 0.008 | attack=5 | 5.68 | 5.90 | 0.360 |
|
|
||||||
| depth=10 | 24.27 | 24.25 | 0.018 | mode=0 | 5.03 | 5.00 | 0.023 |
|
|
||||||
| depth=20 | 39.43 | 39.40 | 0.054 | mode=2 | 7.70 | 7.70 | 0.002 |
|
|
||||||
| sens=0 | 2.89 | 2.89 | 0.001 | sens=24 | 7.70 | 7.70 | 0.002 |
|
|
||||||
Q-профиль (probes-зонд): sim sel1 500→680@540→0.62@700 vs real 8.03→4.54→1.13; 2nd-order не покрывает
|
|
||||||
крутые борта реала (реал резче ~×3): допустимо как 1-я итерация, RMSE макс по профилю ~1.6dB.
|
|
||||||
Release-трассы: r0/r1/r2/r5 совпадают (sim против real 1.6s:3.5/4.3, 1.7s:1.5/1.9, 1.9s:0.3/0.3).
|
|
||||||
|
|
||||||
## Q/width-зонд (probes500/probes500b, drive 500Hz amp0.2 + справки 0.02)
|
|
||||||
- Методика: длинный single-FFT на окне 1.7..2.9s (разрешение ~0.7Hz), справки на 500,505,510,520,540,
|
|
||||||
555,575,600,700 (b-set) / 500,505,515,530,560,640,780 (a-set). Драйв-тон держит детект-клок,
|
|
||||||
справки зондируют передаточную характеристику в установившемся режиме.
|
|
||||||
- Данные (a-set) red@freq: sel1: 500:8.0 510:7.9 520:7.7 540:4.5 555:2.7 575:1.6 600:0.7 (плавный хвост)
|
|
||||||
sel8: 500:8.7 510:9.1 520:6.6 540:1.3 555:0.1 575:0.0 600:0.0 (резкий срез) — Q уверенно растёт с sel.
|
|
||||||
- Центраئة нотча на 500..510 чуть выше 500 (fs на 505..510) — трекинг слегка субадио.
|
|
||||||
- Значения на дип-тонах 530/560/640 в b-наборе — из мусора (нет справки) и исключены; a-набор чистый.
|
|
||||||
- ВАЖНО: dry-файлы дважды перезаписывались провальными dry-рендерами (RENDER_FILE не патчился по
|
|
||||||
`.wav` напр. burst500L_byp → overwritten probes). ФИКС: патчить RENDER_FILE явно в sed до рендера.
|
|
||||||
- data: оба svипа предсказуемо монотонны; таблица в summary выше.
|
|
||||||
|
|
||||||
## Верифицированные данные (все свипы с корректным форматом)
|
|
||||||
- **depth (mir)**: монотонно distinct (vs-ref 496k→441k→...→347k@0.85→384k@1.0) — работает.
|
|
||||||
- **selectivity**: 0/1/2/5 distinct (476k/466k/458k/461k vs ref); 10==20==50==100 (сатурация ≥10).
|
|
||||||
- **oversample**: 0==ref; 1 distinct; 2==3==4 (сатурация на 2).
|
|
||||||
- **resolution**: 0 distinct; 1==ref; 2 distinct (все distinct); 3==4.
|
|
||||||
- **mix**: 0/25/50/75/100 всё distinct монотонно; 100==ref.
|
|
||||||
- **mode**: 0 distinct; 1==2==3==4==ref.
|
|
||||||
- **band1 freq**: tt_b1f_678 (дефолт banda freq) == tt_ref бит-в-бит; свип 100..8000 отрендерен.
|
|
||||||
|
|
||||||
## Прочие находки
|
|
||||||
- 60-сек рендер p60_ref/p60_dep0/p60_dep1 подтвердил collapse класса при коротких строках.
|
|
||||||
- rtdump.py живые дампы (регионы по VA, RA/WA) — DSP-кластер .data меняется между глубинами; раньше не совпали по layout (артефакт noise).
|
|
||||||
|
|
||||||
## Work State
|
|
||||||
### Completed
|
|
||||||
- Свип-инфраструктура: sweep.py + tt_sweep.py (адаптив формата), рендер, сверочный анализ (spectrum.py медленный, нужно numpy).
|
|
||||||
- Тест-тон: testtone.wav (2s, multi-tone 110Hz–14kHz с AM) для изоляции резонансов; tt_ref==tt_678 (0 diff).
|
|
||||||
- Базовые свипы по всем основным параметрам.
|
|
||||||
|
|
||||||
### Active
|
|
||||||
- **Извлечена статическая depth-кривая**: LUT 207 float @0x1826170e8..0x182617420, форма `0.302 + 0.698·sin(π/2·x)^0.94` (r²=0.99999), saved `depthcurve.npy`. ИДЕНТИЧНА у ref/dep0/dep1 → вшита в бинарь (build-time), не меняется от параметра = внутренний «depth→amount» маппинг.
|
|
||||||
- **Адаптивная динамика (burst-тест, band1@500Hz, q≈1, depth 0.864)**: burst500.wav (2s: 0.5s тишины → 1.0s 500Hz burst amp 0.2 → 0.5s тишины, + 1000Hz amp 0.05 фон). `burst500_b1` (fx) vs `burst500_byp` (dry):
|
|
||||||
- ATTACK: плавное включение нотч-ослабления, τ≈**18 мс** (фит на экспоненту A·(1−e^−t/τ), stable 17.6/17.7/18.8/18.8), установка −7.7dB бай-в-бай за ~100мс.
|
|
||||||
- RELEASE: хвост после снятия бурста спадает за τ≈**5 мс** (после 1.5s fx-энергия 24→0 за ~30мс) — быстрый трелинг, симметрия фола не классическая (вероятно фильтр-транзиент, не медленный rel).
|
|
||||||
- Глубина ослабления НЕ зависит от «порога» — это фикс. нотч на частоте band, amount задаётся curve(depth).
|
|
||||||
- Рендерится через те же tt-файлы; РЕЗУЛЬТАТЫ в wav — 24-bit (`sampwidth=3`), читать через 24-bit decode (или любой np.frombuffer под sw), не 16-bit!
|
|
||||||
- **τ-инвариантность** (burst-тест, band1@500Hz, win 10ms, Goertzel, фит к A(1−e^−t/τ)):
|
|
||||||
- depth 0→0.86→1: A=6.39→7.70→7.91 dB; τ=18.3→16.8→16.5 ms (τ почти const) → depth контролирует amount, НЕ скорость.
|
|
||||||
- selectivity: τ РАСТЁТ монотонно 12.3→16.8 ms при sel 0→10 (12.3/12.4/12.6/12.9/13.0/13.4/14.3/16.8) → Q/selectivity = time-const антреккинга.
|
|
||||||
- depth-amount таблица (settled, band1@500): sel 0..6 монотонно −7.79→−9.08 dB, затем dip sel8=−8.79, sel10=−7.71 (non-monotonic у высоких sel, нюанс трекера).
|
|
||||||
- «650Hz notch» в раннем FFT-анализе — АРТЕФАКТ деления на near-zero dry; реальные числа = ratio dry/fx только там, где dry>5% пика (pure 500Hz tone → окно вокруг 500).
|
|
||||||
- **secret свипа: `patchparam.py`** обязателен для формат-сохранения: state-блок в RPP = base64, wrapped по 128-симв/строка с 2-sp indent и НОВОЙ строкой перед '>'. Ключевое: глубина пишется ТОЛЬКО `%.16f` (короткая `0.0` → collapse!), поэтому CLI `depth=0.0!` форсит полную точность.
|
|
||||||
- Float-поиск по живому дампу: depth 301 hits (множество копий по bands), selectivity/sharpness → общий бакет (10.015, 9.997), mix → 4 копии 100.0, band freqs (678.76, 8242.67) → 0 точных (хранятся иначе, напр. Hz→binidx или через npf).
|
|
||||||
- Живой diff dep0 vs dep1 в .data (0x182622130, 0x182622350, 0x1826223c0, 0x18262aca8, 0x182672fe4) — флоаты-состояние меняются; но регион содержит много мусора/nan (это .data буферы рендера, не коэфф).
|
|
||||||
|
|
||||||
### Провалы/уроки
|
|
||||||
- ASLR между отдельными rtdump-запусками сдвигает раскладку регионов → прямого VA-сравнения НЕТ (dep0b 10 регионов, dep1b 9). Только те адреса, что реально в 0x18260a000-0x18267c000 (совпадают) сравнимы.
|
|
||||||
|
|
||||||
## Relevant Files
|
|
||||||
- **`/home/m/re-tools/sim.py`** — поведенческий симулятор (LUT amount + 2nd-order bp Q(sel) + env attack/release + mix/mode). `simulate(x, fc, depth, sens, sharp, sel, mode, attack, release, mix)`. RMSE settled ≤0.05dB (см. таблицу выше).
|
|
||||||
- **`/home/m/re-tools/verify_sim.py`** — RMSE-сверка симулятора против рендеров (mag@500 трасса). Использование: `verify_sim.py <fx.wav> --params depth=5 attack=5`.
|
|
||||||
- **`/home/m/re-tools/fit_curves.py`** — фит LUT (остаётся как аналитический эквивалент; LUT в sim.py приоритетнее).
|
|
||||||
- probes500/probes500b_wav+byp — зонды ширины нотча; prb_*/prb2_* — рендеры.
|
|
||||||
- `synth_multi.py` (AM-комб 200–3000Hz резонансный), `burst500.wav` + `burst500_b1.rpp/.wav` + `burst500_byp.rpp/.wav` (burst-атак/релиз band1@500Hz), `burst_dep0/dep1/sel0/sel2/sel5.rpp/.wav` (τ-sweep), `comb.wav` + `comb_b1_*.rpp/wav`, `comb_dep_{0..1}`, `depthcurve.npy`. **Рендеры Reaper = 24-bit** — читать с `sampwidth=3`, не 16-bit!
|
|
||||||
- `/home/m/re-tools/patchparam.py` — патчер PARAM в base64 state с формат-сохранением (128-wrap, `depth=...!` → %.16f).
|
|
||||||
- `/home/m/re-tools/spec.py`, `probe.py`, `notch.py`, `synth.py`, `mkbase.py`, `tt_sweep.py`.
|
|
||||||
- `/home/m/soothe-bt/tone1kq.wav` (чистый 1k, -18dBFS), `dual.wav` (500+2000Hz), `resonant.wav` (500Hz-резонанс).
|
|
||||||
- `/home/m/soothe-bt/t1kq_*.rpp/wav`, `res_only1_*.rpp/wav`, `dual_b1_*.rpp/wav`.
|
|
||||||
- `/home/m/re-tools/sweep.py` — адаптивный генератор RPP (tpl фиксирован, для смены файла использовать mkbase + tt_sweep).
|
|
||||||
- `/home/m/soothe-bt/tt_base.rpp` (template: testtone + дефолтные парамы), `tt_ref.wav`.
|
|
||||||
- `/home/m/soothe-bt/s_corr_{sel,os,res,mix,mode}*.wav` — верифицированные свипы на resonant.wav.
|
|
||||||
- `/home/m/soothe-bt/render_v5.rpp`, `s_ref2.wav` (бит-в-бит оригинал).
|
|
||||||
## Фаза 3: Трекинг подтверждён (aug 16) — ARCHITECTURE REVISION
|
|
||||||
- **Вход-зависимость**: ред@1k растёт с уровнем сигнала (band1=1000, тон 1k):
|
|
||||||
`-27→-21→-15→-12→-9→-6→-3 dBFS` ⇒ `5.80→7.75→9.91→11.05→12.21→13.40→14.60 dB`. amount растёт с уровнем детектируемого резонанса (не бинарный порог).
|
|
||||||
- **Трекинг нотча** (fresh renders `trk_b1_*`, вход tone1k 1k, band1∈{500,1000,2000}, длинная FFT): нотч ВСЕГДА на fc=тон=1k, band-частота НЕ двигает позицию режектора:
|
|
||||||
- band=500 → 11.91dB, band=1000 → 15.82dB, band=2000 → 11.91dB, band1 off → 9.80dB, sens=0 → 9.80dB.
|
|
||||||
- Единственный вклад band-EQ — ВЕС глубины детекции в области (совпадение band~тон даёт +4dB; off/sens0 −2dB).
|
|
||||||
- **Мульти-пики**: dual (500+2000), оба нотча активны одновременно при sel=1..20 (~10dB оба) → детектор находит НЕСКОЛЬКО резонансов; selectivity НЕ регулирует число нотчей на явных тонах.
|
|
||||||
- **Пользователь (иерархия)**: детектор оценивает каждую частоту спектра; band-EQ формирует его ВХОД (усиление/ослабление детекции по областям, у band2 sens может быть −12); selectivity = отбор пиков; sharpness = форма нотча; depth = глобальная глубина.
|
|
||||||
- **Reaper-ловушка**: tt_sweep с ОТНОСИТЕЛЬНЫМ out_wav создаёт ПОДДИРЕКТОРИЮ → всегда абсолютный путь в RENDER_FILE.
|
|
||||||
- Файлы: `trk_base.rpp`, `trk_b1_{500,1000,2000,eqoff,sens0}.rpp/.wav`, `sel_base.rpp`, `sel_{1,3,10,20}.rpp/.wav`, `lvl_{03..27}db.wav` + `lvl_t_*.rpp/.wav`.
|
|
||||||
### Фаза 3b: amount зависИТ от уровня сигнала (не просто порог)
|
|
||||||
- lvl_sweep (тон 1k, band1@1000, depth=0.864): ред@1k = 5.80 / 7.75 / 9.91 / 11.05 / 12.21 / 13.40 / 14.60 для −27..−3 dBFS.
|
|
||||||
- При depth=10: 21.67 / 24.15 / 26.63 / 27.86 / 29.09 / 30.33 / 31.56. Наклон d(ред)/d(level) ≈ 0.37–0.41 dB/dB обоих depth → amount ∝ уровень^p (монотонно с насыщением), НЕ бинарный порог. depth — масштаб, не floor.
|
|
||||||
- Обе кривые монотонны и близки по форме (отношение ~2.2 при громком, ~3.7 при тихом → форма зависит от depth слабо).
|
|
||||||
### Фаза 4 (aug 16): КАРТА EQ-ВЕСОВ ДЕТЕКТОРА
|
|
||||||
- **`on` полосы = enable детекторного веса**; off-полоса её sens НЕ отдаёт в детекцию. Подтверждения:
|
|
||||||
- eqT_b1_b2/b3/b4/b2b3b4 и trk_b1_500 (sens off-полос любая) ВСЕ = 11.91dB (только от b1 sens12); eq_off_sensfac (все off, factory sens) = 9.80dB = нейтраль.
|
|
||||||
- eq1778_b2s_12 (b2 on=0) = 11.81, b1on12_b2s12 (b2 on=1) = 17.60 — ЕДИНСТВЕННАЯ разница band2 on.
|
|
||||||
- Баг в раннем clear_bands сбрасывал b1 sens→0 (eq_b1_on был sens0=нейтраль 9.80; правильный net_b1on12 sens12 = 11.91).
|
|
||||||
- **S(sens) при совмещённом весе (band1@1k, тон 1k)**: sens 0/6/9/12/18/24 → boost над полом 0.00/3.16/4.65/6.02/6.02/6.02 dB.
|
|
||||||
boost = 6.02·min(1, sens/12), насыщение +6dB при sens≥12 (не монотонно из-за сатурации детектора, не бага).
|
|
||||||
- **W(f) — колокол EQ-веса** (тон 1k, одна полоса on sens12, варьирую band freq):
|
|
||||||
- band1 (q≈1): 500→11.91(+2.11), 600→12.80(+3.00), 750→14.28(+4.48), 850→15.19(+5.39), 950→15.75(+5.95), 1000→15.82(+6.02), 1050→15.76, 1200→15.05(+5.25), 1500→13.44(+3.64), 2000→11.91(+2.11), 2500→11.16(+1.36). Симметричный широкий колокол (первый октав хвост +2.1dB, не спадает к 0).
|
|
||||||
- band2 (q≈4.5): 600→10.15(+0.35), 750→10.78(+0.98), 850→11.97(+2.17), 1000→15.80(+6.00), 1200→11.68(+1.88), 1500→10.34(+0.54) — РЕЗКИЙ колокол. **Q полосы управляет шириной детекторного веса** (q=4.5 на −1 октаве теряет ~полностью, q=1 теряет только ~66%).
|
|
||||||
- **Позиция нотча**: band freq не двигает режектор (всегда на резонансе тона); EQ-веса модулируют ТОЛЬКО глубину.
|
|
||||||
- Новый модельный блок: `red(f_tone) = floor(level) + Σ_on boost_i`, `boost_i = 6.02·min(1,|sens_i|/12)·H_q(f_tone; fc_i, Q_i)`.
|
|
||||||
- Файлы: `wf_{600,750,...,2500}.wav` (band1 q-sweep), `ws_{6,9,18,24}.wav` (aligned sens), `b2q_{600..1500}.wav` (band2 q=4.5 sweep), `b1on12_b2{on12,s12}.wav`.
|
|
||||||
### Фаза 5 (aug 16): STFT-параметры детектора (Stage 1)
|
|
||||||
- **Хоп/задержка**: tone-jump зонд tj.wav (1k→1.5k в t=2.0s) → нотч пересаживается на новый резонанс за ~1 кадр ≈ 18мс (hop ≤ 512 сэмплов). Трекинг чирпа 400→2000Hz в реальном времени, dt≈0.03→0.05s (шум оценки FFT-бина, значимой задержки нет).
|
|
||||||
- **Разрешение по частоте**: пары 1000+1040 → сливаются в один широкий нотч; 1000+1100 разделяются (центры ≈996/1104, но провал между ними всё ещё глубокий); 1000+1200 → два чётких отдельных нотча с полным восстановлением на 1100. → окно FFT ≈ 1024–2048 (бины 21–43Hz), multiple-нотч подтверждён.
|
|
||||||
- **Энерготрекинг (fx@f по окну 2048, sync-Goertzel)**: до прыжка fx@1000 ≈ −24dB (ред ~14dB), после — fx@1500 ≈ −23.8dB. Двойной бурст (gap 10..100ms) → нотч на том же тоне не переоткрывается между бурстами (ред остаётся ~4dB floor) — bурст probe (осб) указывает на фиксированный window-центр, не пер-открытие.
|
|
||||||
- Вывод для v5: STFT с окном ~2048, хоп ~512–1024; window function Hann; детектор по |X| per-bin → локальные пики (selectivity); EQ-веса W(f) как множитель sensitivity per-bin; per-пик нотч (bandstop Q=sharpness).
|
|
||||||
- Файлы: `chirp_log_400_2000.wav`, `chirp_trk.rpp/.wav`, `pair_{1040,1100,1200}.wav` + `pair_trk_{40,100,200}`, `dburst_{10,20,50,100}.wav` + `dburst_trk_*`, `tj.wav` + `tj_trk.rpp/.wav`.
|
|
||||||
### Фаза 6 (aug 16): КОЛОКОЛ EQ-ВЕСА + SHARPNESS + level-floor (Stage 2 фит)
|
|
||||||
- **H(f) — колокол**: `H(f) = 1/sqrt(1 + (Qeff·A)²)`, `A = f/fc − fc/f`. Qeff = **1.54·q^1.33** (q=1→1.54, 2→3.32, 3→5.64, 6→16.8). Воспроизводит кривую band1 (wf_*) с ошибкой ≤0.15dB по всему диапазону 500..2500.
|
|
||||||
- **Sharpness → Q нотча**: psh_{1,3,5,10} (probes500, drive 500·0.2, band1@500): центр 505Hz red 1.3/4.3/6.8/8.7dB; ширина Q_notch≈1.15·sharp (sh10→~11, sh5→~6, sh3→~3, sh1→~1.2). Центр-глубина НЕ постоянна — растёт с sharp (1.3→8.7), т.е. sharp масштабирует amount И Q.
|
|
||||||
- **level-floor (нейтраль)**: `neut_{24..3}` (все полосы off/sens0, тон 1k): red = 2.22/3.36/4.85/5.71/6.65/7.65 dB при −24/−18/−12/−9/−6/−3 dBFS rms. Монотонно, наклон ~0.31 dB/dB. `al_{*}` (b1@1000 sens12): 5.80/7.75/9.91/11.04/12.21/13.39 → boost = 3.59/4.39/5.06/5.33/5.56/5.74, **насыщается к ~6dB** с уровнем (не аддитивен в dB!).
|
|
||||||
- Модель amount: `red(f) = curv(level·G_eq(f))` с насыщением (не линейный сдвиг в dB). depth масштабирует (фаза 3b). Для sim: 2D LUT level×W.
|
|
||||||
- Файлы: `lvl_tone_lv{24,18,12,9,6,3}.wav`, `neut_{*}.rpp/.wav`, `al_{*}.rpp/.wav`, `psh_{1,3,5,10}.rpp/.wav`.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user