250 lines
21 KiB
Markdown
250 lines
21 KiB
Markdown
# BIT-EXACT PLAN — путь от dB-приближения к побайтовой парности
|
||
|
||
Статус: **НЕ bit-exact**. Мы на уровне честной dB-параллели (полный корпус mean 1.594 dB,
|
||
comb 10 dB). Bit-exact ДОСТИЖИМ (F0 gate: плагин байт-детерминирован), но модель ещё не
|
||
воспроизводит реальный DSP-путь. Этот документ — план, как туда дойти, и точка отсчёта
|
||
для любой будущей сессии. **Читать вместе с `AGENTS.md` и `handoff/NOTES_LEVEL.md`.**
|
||
|
||
> **ОБНОВЛЕНИЕ 2026-08-22 (сессии 22a–22e):**
|
||
> 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 (ниже).
|
||
|
||
Цель фазы C (bit-exact): воспроизвести `FramedDetector` (= fn `FUN_180529fe0` mono-path)
|
||
настолько точно, что `verify_bit_exact.py` даёт побайтовое совпадение на рендерах
|
||
(`t1kq_*`, `dual_*`, `comb_*`). Ниже — конкретный порядок, что и зачем.
|
||
|
||
---
|
||
|
||
## 0. Текущее состояние (честная метрика, 24-bit, trimmed, 62 случая)
|
||
|
||
| Группа | bridge mean|err| | bridge max | struct 48k mean|err| | struct max |
|
||
|--------|----------|-----|----------|---------|
|
||
| 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`,
|
||
прогон `scripts/corpus_structural.py`, NOTES_LEVEL:21c. Bridge = `framed_test` 44.1k.)
|
||
|
||
> **21d**: найден закон редукции — эффективное ослабление АФФИННО в dB(lvl):
|
||
> `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-модель
|
||
`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. Порядок работ (обязательный порядок; каждый шаг валидируется отдельно)
|
||
|
||
> **ВАЛИДАЦИЯ (обязательно, НЕ пропускать).** Чтобы не повторить регресс Phase B,
|
||
> есть защитный харнесс `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 — Реализовать полную структурную mono-цепочку FUN_180529fe0 (КОРЕНЬ)
|
||
Заменить bridge (эмпирическую погону) на точную транскрипцию. Реализовать в
|
||
`dsp/framed_model.cpp` (или отдельном `dsp/fn529fe0.cpp`) по NOTES_LEVEL:820-840:
|
||
```
|
||
1. scale: band_mask *= (fVar30/0x1a0)·0x540870·0x54088c (fVar30=1 из PRNG, locked)
|
||
2. IIR1: y[i]=A1[i]·acc+B1[i]·x[i] (kIIR_A1/B1, fast attack) -> f6f8
|
||
3. copy f6f8 <- band
|
||
4. IIR2: inline, kIIR_A2/B2 (slow release)
|
||
5. mirror upper half = reversed lower (Hermitian)
|
||
6. blend: f6f8 = 0x540698·(1−mix)+mix·0.8; mask = exp2(−level)·f6f8
|
||
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 структурная цепочка должна НЕ регрессировать
|
||
ниже bridge (т.е. mean ≤ 0.6-0.8). ВАЖНО: прошлый регресс (F2) был из-за неверного
|
||
домена (пробовали combine в bridge-финале). Здесь combine/exp2 — в ЕГО собственном
|
||
домене (reduction/exp2), как в декомпе.
|
||
|
||
### Шаг 2 — combine/аккумулятор 0x5407c8 (ПЕРЕ-СКОУП 2026-08-21)
|
||
Семантика декодирована на уровне 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)
|
||
Заменить `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)
|
||
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)
|
||
Заменить `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)
|
||
Внутренний 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-захват под конкретные конфиги
|
||
> **СТАТУС 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)
|
||
Финальный рендер 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)
|
||
Живой факт (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` полностью: фактор
|
||
`expf((p87c·30 − 90)·ln10/20)` на band-буферы 0x540678[band] через
|
||
`FUN_1804d56b0` (f_529c60.dis:94-101) — что именно считает 1804d56b0 и куда
|
||
идёт произведение (это 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. Что НЕ делать (подводные камни из дока)
|
||
|
||
- **НЕ вводить эмпирию там, где есть декомп.** Каждый параметр — из декомп-адреса или
|
||
live-таблицы, иначе пометить EMPIRICAL и в «осталось» (золотое правило AGENTS).
|
||
- **НЕ делать combine в bridge-final-gain домене** — F2 показал регресс 4.68/5.22.
|
||
Combine/exp2 живут в reduction/exp2-домене реальной цепочки.
|
||
- **НЕ менять Pchip-LUT в bridge** — регрессирует весь корпус (F1, параметрич. хуже).
|
||
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. Риски и время
|
||
|
||
- **Доминирующий риск**: дорогой структурный перенос (Шаг 1-2) может снова регрессировать
|
||
ниже bridge, как Phase B. Митигация: валидировать каждый под-шаг (IIR1 отдельно, blend
|
||
отдельно) на t1kq, не коммитить пока не ≥ bridge.
|
||
- **DSP-FFT (Шаг 5)** — multi-week само по себе. Но НЕ блокирует Шаги 1-4.
|
||
- **Стерео (Шаг 8)** — новая мишень-корпус, больше рендеров.
|
||
- **Оценка**: Шаги 1-4 (монопуть) — 1-2 недели. Шаг 5 — до 3 недель. Шаги 6-8 — 1-2 недели.
|
||
До полной байтовой парности — ориентировочно 1-2 месяца с аккуратным монофокусом.
|
||
|
||
## 4. Точка входа для следующей сессии
|
||
|
||
1. Прочитать `AGENTS.md`, этот `BITEXACT_PLAN.md`, `handoff/NOTES_LEVEL.md`
|
||
апдейты **20j / 21a-21e / 22a-22e** (цепочка, Phase-B негативы, Шаг 7 опровержение,
|
||
параметр-мост, пол blend·ln10/20) и `dsp/rt_mask_tables.hpp` / `dsp/rt_weights.hpp`.
|
||
2. Собрать: `cmake --build dsp/build --target render48k framed_test`.
|
||
3. Бейзлайны: `python3 scripts/corpus.py` (bridge, 62 случая, §0) и
|
||
`python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json`.
|
||
4. ПРИОРИТЕТ №1 — **Шаг 9** (пол редукции):
|
||
a. 9a: полный декод FUN_180529c60 + FUN_1804d56b0 (f_529c60.dis готов).
|
||
b. 9b: grep 0x1824c3cd4 по аудио-функциям; hunt клампа маски.
|
||
c. 9c: предсказания пола — рендеры mix/sens через `setparam.lua` (мост готов,
|
||
XML-правки стейта НЕ работают — NOTES 22c).
|
||
d. 9d: порт + корпус с гейтом.
|
||
5. Инструменты сессии: `setparam.lua`/`dump_params.lua` (параметр-мост),
|
||
`scripts/rpp_setparam.py` (только формат-хирургия!), `RT_LUT_*` env в
|
||
framed_model.cpp (солвер констант), `scripts/step7_capture.py` (live BandConfig).
|