Compare commits
84
Commits
475b1958d5
..
v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
b5a5afbd16 | ||
|
|
87f00ce181 | ||
|
|
c210a778e5 | ||
|
|
0677a390d2 | ||
|
|
94116e2a17 | ||
|
|
d2f67016ab | ||
|
|
b85b344e23 | ||
|
|
def7cea522 | ||
|
|
f365fa8451 | ||
|
|
0fee9238dc | ||
|
|
77c02c8f9d | ||
|
|
0dfff22efb | ||
|
|
4f79f83c11 | ||
|
|
bf3faab660 | ||
|
|
c33212c21b | ||
|
|
483f52f296 | ||
|
|
8ef1d4ec73 | ||
|
|
0943471b33 | ||
|
|
2d6bdcf967 | ||
|
|
83c12d9b3d | ||
|
|
feaa82957a | ||
|
|
0fbff6dc6e | ||
|
|
61219acca6 | ||
|
|
10d8c1cf5a | ||
|
|
d4c56bdfce | ||
|
|
2fb0e640e0 | ||
|
|
47c5360ac2 | ||
|
|
65ee61d540 | ||
|
|
676d43d40a | ||
|
|
8a4e7a3c54 | ||
|
|
243ea03a45 | ||
|
|
4b94ce5c15 | ||
|
|
5284ce2b57 | ||
|
|
cf38622995 | ||
|
|
edf6f35cdf | ||
|
|
abbee308ba | ||
|
|
da0393bd93 | ||
|
|
5bd60cfefd | ||
|
|
0bc8427cd7 | ||
|
|
a309ca3e31 | ||
|
|
4b56e4f384 | ||
|
|
3062a02389 | ||
|
|
a3d108f4d5 | ||
|
|
5f822ebae1 | ||
|
|
31cc5540e1 | ||
|
|
fc224b00a3 | ||
|
|
b00a8a667e | ||
|
|
fe01ef5bac | ||
|
|
508b24125f | ||
|
|
3a611cff35 | ||
|
|
65fb983d57 | ||
|
|
1605e0ba29 | ||
|
|
6dc2967e4a | ||
|
|
cc8ba34577 | ||
|
|
6629e8bbf2 |
@@ -4,14 +4,25 @@ Bit-exact реверс DSP-ядра oeksound soothe2 (VST3) → транскри
|
|||||||
Полное журналирование — в `handoff/NOTES_LEVEL.md`, `handoff/NOTES_TWIN.md`,
|
Полное журналирование — в `handoff/NOTES_LEVEL.md`, `handoff/NOTES_TWIN.md`,
|
||||||
`handoff/NOTES_CAPTURE.md`, `roadmap.md`.
|
`handoff/NOTES_CAPTURE.md`, `roadmap.md`.
|
||||||
|
|
||||||
> **Текущая фаза (2026-08-23, после 22z):** канон = структурная цепь 48k/4096 с
|
> **Текущая фаза (2026-08-25, после 24kk2):** ПРИМЕНЕНИЕ ДЕКОДИРОВАНО ФОРМУЛАМИ:
|
||||||
> RT_LAWAFFINE=7.4,1.85 (TOTAL 1.931; bridge 1.594 — гейт не пройден). ГЛАВНЫЙ
|
> аудио = побиновное умножение кадра на вещественную маску `10^(−cut_D/20)`,
|
||||||
> ОТКРЫТЫЙ ВОПРОС — семантика входов `bands[]` детектора: доказанно НЕ поточечная
|
> `cut_D = α·ln1p(lvl_raw/β)+c` (+Δ у вторых пиков). Калибровки (rms ≤0.016 дБ):
|
||||||
> функция от (am,res^α) [22x], не форма-постобработка [22w], не спрединг
|
> dual(q0.5,s12,2 тона)=3.2193/0.4927/+0.54; fc1000(1 тон,q0.5)=1.6151/0.3645/+0.48;
|
||||||
> IDFT→окно→DFT [22z]. Живая кривая редукции R=1/mask захвачена (слот 0x5407f8,
|
> fc500(1 тон)=1.1530/0.4038/+0.33. lvl_raw — НАШ фронтенд (float-parity ✓).
|
||||||
> dualtrace.py), но применённый фильтр ≠ её поточечной копии — механизм живёт ДО
|
> Слой STFT = БЕЗ синтез-окна (`RT_SYN=1`). **dual-корпус 0.193 max 0.438**
|
||||||
> кривой (twin-шаблон/скалярный драйв). Журнал: NOTES_LEVEL 22s–22z; карта метода:
|
> (канон 3.264); канон TOTAL 2.286 нетронут. ГЕЙТ СМЕНЫ КАНОНА = BIT EXACT
|
||||||
> handoff/BLOCKMAP_529fe0.md. ЭТОТ ФАЙЛ ЧИТАЙ ПЕРВЫМ.
|
> (решение пользователя: все параметры прослежены до декомпа + шумовой пол корпуса).
|
||||||
|
>
|
||||||
|
> Ключевые факты: 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 реверс кода**, НЕ эмпирическая подгонка кривых. Каждый параметр
|
||||||
@@ -137,6 +148,36 @@ scale → LUT level-domain (t^γ·MULT, γ=0.344 decomp / MULT=4.2 placeholder)
|
|||||||
- `scripts/resalpha.py` — сбор трактов + теорема об отсутствии α (22x).
|
- `scripts/resalpha.py` — сбор трактов + теорема об отсутствии α (22x).
|
||||||
- `scripts/dualtrace.py` + `play_loop.lua` — живой захват ctx при realtime-playback (22y).
|
- `scripts/dualtrace.py` + `play_loop.lua` — живой захват ctx при realtime-playback (22y).
|
||||||
|
|
||||||
|
## Env-флаги экспериментов (24j–24kk2)
|
||||||
|
|
||||||
|
| флаг | действие |
|
||||||
|
|------|----------|
|
||||||
|
| `RT_VLAW=1` | закон применённой стадии: mask=10^(−(α·ln1p(lvl/β)+c)/20) |
|
||||||
|
| `RT_SYN=1` | STFT БЕЗ синтез-окна (найденный слой плагина) |
|
||||||
|
| `RT_WIN=0/1/2` | окно анализа: sym-hann / periodic / rect |
|
||||||
|
| `RT_NOWARP=1` | отключить warp-модуляцию маски |
|
||||||
|
| `RT_NOIIR3=1` | отключить IIR3 ×2 |
|
||||||
|
| `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_VDBG=1` | stderr-печать vlaw-вычислений |
|
||||||
|
|
||||||
|
Полный набор 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/campaign.py <base> <fc> <q> <sens> <in_prefix> <drives> <out>` | ячейка параметризации: клоны rpp+рефы (~8 мин) |
|
||||||
|
| `scripts/disasm_func.py <VA> [len]` | capstone-дизасм с инлайн-резолвом RIP-констант |
|
||||||
|
| `scripts/iat_name.py` | рантайм-резолв импортов bigkernel'ов через PE-экспорты (SIGSTOP!) |
|
||||||
|
| `scripts/probe_states.py` / `probe_mem.py` / `dump_dispatch.py` | живые state-заголовки / память / таблицы диспатча |
|
||||||
|
| `scripts/hunt2.py` | перебор всех ctx-инстансов |
|
||||||
|
| `scripts/scan_pairs.py`, `scan_lutsub.py` | диагностика памяти по сигнатурам float-пар |
|
||||||
|
|
||||||
|
Датасеты кампании: `/tmp/opencode/sc_{q,sens,qmap,k,f,d}*` + `tract_*` +
|
||||||
|
`{level_pairs,f_series,q_series,k_series}.pkl/.npz` (см. NOTES 24bb–24kk2).
|
||||||
|
|
||||||
## Чистая работа
|
## Чистая работа
|
||||||
- Не коммитить: `*.bin`(дампы 112М), `*.wav/rpp`, `*.log`, `dsp/build/`, `ghidra-proj/`,
|
- Не коммитить: `*.bin`(дампы 112М), `*.wav/rpp`, `*.log`, `dsp/build/`, `ghidra-proj/`,
|
||||||
`dl/lib/bin/include/`, `regions*/`. См. `.gitignore`.
|
`dl/lib/bin/include/`, `regions*/`. См. `.gitignore`.
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ comb 10 dB). Bit-exact ДОСТИЖИМ (F0 gate: плагин байт-дете
|
|||||||
> Новый приоритет №1 — семантика входов `bands[]` (см. AGENTS.md и NOTES_LEVEL
|
> Новый приоритет №1 — семантика входов `bands[]` (см. AGENTS.md и NOTES_LEVEL
|
||||||
> 22s–22z); карта метода — handoff/BLOCKMAP_529fe0.md.
|
> 22s–22z); карта метода — handoff/BLOCKMAP_529fe0.md.
|
||||||
|
|
||||||
|
> **ОБНОВЛЕНИЕ 2026-08-25 (сессии 24j–24kk2):** ПРИМЕНЕНИЕ ДЕКОДИРОВАНО ДО ФОРМУЛ.
|
||||||
|
> Применённый фильтр = побиновное умножение кадра на вещественную маску
|
||||||
|
> `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)
|
Цель фазы C (bit-exact): воспроизвести `FramedDetector` (= fn `FUN_180529fe0` mono-path)
|
||||||
настолько точно, что `verify_bit_exact.py` даёт побайтовое совпадение на рендерах
|
настолько точно, что `verify_bit_exact.py` даёт побайтовое совпадение на рендерах
|
||||||
(`t1kq_*`, `dual_*`, `comb_*`). Ниже — конкретный порядок, что и зачем.
|
(`t1kq_*`, `dual_*`, `comb_*`). Ниже — конкретный порядок, что и зачем.
|
||||||
|
|||||||
@@ -10,54 +10,65 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Статус (P5, 2026-08-22)
|
## Статус (24kk2, 2026-08-25)
|
||||||
|
|
||||||
**Цель — bit-exact реверс.** Декомпиляция DSP-ядра закрыта (~95%): twin-резонатор,
|
**Цель — bit-exact реверс** (гейт смены канона зафиксирован пользователем: только
|
||||||
генератор case8, level-path, mask-apply (FUN_180529fe0), FFT-conv — расшифрованы;
|
после прослеживания всех параметров до декомпа и схождения корпуса в шумовой пол).
|
||||||
дизассемблы в `handoff/nls_dasm/` (~140).
|
Декомпиляция DSP-ядра закрыта (~95%); дизассемблы в `handoff/nls_dasm/` (~140).
|
||||||
|
|
||||||
**Активный канон — C++ `FramedDetector`** (`dsp/framed_model.cpp`): структурная цепь
|
**Применение декодировано до формул** (сессия 24j…24kk2):
|
||||||
```
|
```
|
||||||
level=am*res*scale -> IIR1/IIR2 leaky -> mask=exp2(-level)*blend -> combine/acc
|
mask(b) = 10^(−cut_D(b)/20) ← вещественная, per-bin multiply кадра
|
||||||
-> warp(mask*=0x540768, *=warp) -> IIR3x2 -> dry/wet -> FFT-conv
|
cut_D(b) = α·ln(1+lvl_raw(b)/β)+c [+Δ у вторых пиков]
|
||||||
|
lvl_raw = am/res·scale (наш детектор-фронтенд, float-parity ✓)
|
||||||
|
слой = STFT БЕЗ синтез-окна (RT_SYN=1)
|
||||||
```
|
```
|
||||||
- **Level-tracker = би-направленный leaky-IIR** `y=A[i]·acc+B[i]·x` (B=1−A), live-таблицы
|
Калибровки формы (три независимых семейства, rms ≤0.016 дБ): α/β/c зависят от
|
||||||
в `dsp/rt_mask_tables.{hpp,cpp}`, `dsp/rt_weights.{hpp,cpp}`.
|
контента (α удваивается с числом тонов — частотное смешение шаблонно-локальное),
|
||||||
- **Корпус (62 случая)**: bridge mean 1.594 dB; структурная цепь TOTAL 2.286,
|
q НЕ влияет на закон, sens входит линейно через lvl_raw.
|
||||||
но comb **6.12** и res **0.44** — лучше bridge.
|
|
||||||
|
|
||||||
### Главное за 2026-08-21…22 (сессии 21a–22i)
|
- **dual-семейство решено**: корпус 22 случая **mean 0.193 / max 0.438 dB**
|
||||||
|
(канон 3.264). Флаги: `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0`.
|
||||||
|
- **Канон не тронут**: TOTAL 2.286 (env-gated эксперименты живут рядом).
|
||||||
|
- **Буфер FIR@540668** = промежуточное мин.-фазовое представление
|
||||||
|
(`exp(s−i·H(s))`, Гильберт по частоте) — аудио слышит `exp(scratch)` напрямую.
|
||||||
|
- **Тела bigkernel'ов найдены** (рантайм-резолв IAT): 1803a06a0 / 180296c80 /
|
||||||
|
180323f20 / 1802dc0e0 — x87-трансценденты (exp-семейство).
|
||||||
|
|
||||||
1. **«LUT-кривая» FUN_180563440/563a60 — GUI-only!** Весь кластер кривых питается от
|
### Главное за 2026-08-24…25 (сессии 24j–24kk2)
|
||||||
GUI-timer vtables; аудио-метод FUN_180529fe0 BandConfig не читает (22b). Live BandConfig
|
|
||||||
у всех конфигов одинаковый: A=−24/B=28/γ=1. LUT-константы в модели помечены EMPIRICAL.
|
1. **Применение = побиновный complex-multiply кадра** на маску; «магический ×1.805»
|
||||||
2. **Пол редукции найден живьём и решён алгебраически** (22d/e): hot-тон упирается в жёсткий
|
оказался произведением экспонент стадий построения буфера (0.984×1.8345).
|
||||||
пол gain=−20.72 dB = `20·log10(blend·ln10/20)` с точностью 0.0055 dB;
|
2. **Закон уровня универсальной формы** `α·ln(1+L/β)+c` — подтверждён тремя
|
||||||
`floor_dB(sens) ≈ −18.78 − (sens−6)/3` (якорь sens6 = ln10/20 ровно).
|
независимыми калибровками; константы зависят от контента (число тонов) и слабо от fc.
|
||||||
3. **Разрыв локализован в детекторном фронте** (22g/i): модель теряет ×2.8 уровня на
|
3. **Слой STFT**: плагин НЕ домножает выход обратного FFT на окно
|
||||||
изолированных пиках (lvl_raw 3.01 → 1.06 после IIR1-разведения), реальный плагин
|
(`RT_SYN=1`); WIN_WINDOW движка — фейд 0.5→0.8 ровно за 2049 сэмплов (=бинам кернела).
|
||||||
доводит пост-IIR lvl до ~3.12. Не форма кривой — амплитудная цепочка am/res/scale.
|
4. **GUI/аудио разделение**: LUT-строитель FUN_180563a60 — GUI-ветка; аудио-компрессия
|
||||||
4. **Инфраструктура**: официальный параметр-мост REAPER (`setparam.lua`/`dump_params.lua`),
|
живёт в семантиках шагов 9–19 BLOCKMAP.
|
||||||
XML `<PARAM>` в RPP = декоративная копия; live-capture BandConfig (`scripts/step7_capture.py`);
|
5. **Dataflow шагов 9–16 декодирован**: vec6f8=bands−ACC; fma тройками
|
||||||
env-солверы констант (`RT_LUT_A/B/G/MULT`, `RT_LUT_OFF`, `RT_LVL_CAP`).
|
(re,im,coef) с ATT/REL; th2000=поэлементное умножение массивов (не axpy!);
|
||||||
5. Оффлайн-гипотезы Phase B (pooling/temporal/scalar-ρ) — все опровергнуты (22a);
|
шаг 12=COPY (исправлен старый BLOCKMAP).
|
||||||
clipping-теория пола опровергнута (float-домен, 22g); mix = чистый dry/wet кроссфейд.
|
6. **Инструменты**: rendersnap2 v7 (мягкий STOP-снаппер со слотами+скалярами,
|
||||||
|
RENDER_FILE-фикс), patchparam.py (правка VST-чанка RPP!), campaign.py
|
||||||
|
(ячейка параметризации), disasm_func.py (capstone с RIP-константами),
|
||||||
|
iat_name.py (рантайм-резолв импортов через PE-экспорты).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Канон сборки и рендера:
|
# Сборка и канонные команды:
|
||||||
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_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`
|
Детальная метрика и история — в `AGENTS.md`, `BITEXACT_PLAN.md`, `handoff/NOTES_LEVEL.md`
|
||||||
(апдейты 20j…22i).
|
(апдейты 20j…24kk2).
|
||||||
|
|
||||||
### Открытые bit-exact пробелы
|
### Открытые bit-exact пробелы
|
||||||
**Приоритет №1 — детекторный фронт**: амплитудная нормировка am, форма res_k, сила
|
**Приоритет №1 — каскадный симулятор шагов 9–19**: dataflow декодирован
|
||||||
IIR1/2 вдоль частоты (модель теряет уровень на пиках). Далее: PRNG-пролог (LCG→fVar30),
|
(24hh/24ii: vec6f8=bands−ACC, fma-тройки att/rel, ×track, ×warp, центрирование −1),
|
||||||
FFT-conv сглаживание, бит-экзактный exp2, combine/acc консюмер, SR-mismatch
|
тела bigkernel'ов найдены по рантайм-адресам — осталось сложить оп-за-опом и
|
||||||
(внутренний DSP 48000/N=4096 против хоста). Полный список — `AGENTS.md`, `BITEXACT_PLAN.md`
|
проверить на датасетах sc_* (rms каскада сейчас ~0.42 на угаданных формах).
|
||||||
(Шаг 9), `NOTES_LEVEL.md`.
|
Далее: 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 LUT, res_power) — см. `roadmap.md`; самодостаточен как справочник, но не канон.
|
> Pchip LUT, res_power) — см. `roadmap.md`; самодостаточен как справочник, но не канон.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+85
@@ -95,4 +95,89 @@ 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,9 @@ 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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,177 @@
|
|||||||
// 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;
|
||||||
|
// Steps 1+2: b[i] = 0.5*(b[i]+b[i+1]) for i in [0, n-2]
|
||||||
|
for (size_t i = 0; i < n - 1; i++) {
|
||||||
|
b[i] = 0.5f * (b[i] + b[i + 1]);
|
||||||
|
}
|
||||||
|
// Steps 3+4: b[i+1] = 0.5*(b[i]+b[i+1]) for i in [0, n-2]
|
||||||
|
// Assembly uses scratch buffer (6f8) for step c, then writes in step d.
|
||||||
|
// Equivalent: iterate backwards so b[i] is read before being overwritten.
|
||||||
|
for (size_t i = n - 1; i > 0; i--) {
|
||||||
|
b[i] = 0.5f * (b[i - 1] + b[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 computation from assembly (529e00-529e5e).
|
||||||
|
//
|
||||||
|
// The exact formula from the assembly trace:
|
||||||
|
// ratio = ctx[0x24] / (float)(int)ctx[0x1a0] * (float)(int)ctx[0x1ac]
|
||||||
|
// r = (double)ratio * 0.001
|
||||||
|
// inner = pow(50.0, r) * r (call [IAT 0x181bab3f0])
|
||||||
|
// w = (float)(-log10(inner)) (via cd6(0.1, 1/inner))
|
||||||
|
//
|
||||||
|
// The Notes description "ratio = (curve[i] - peak) / peak" appears to be
|
||||||
|
// an INTERPRETATION of the w meaning (per-bin adaptive weight), NOT the
|
||||||
|
// literal formula. The actual formula uses ctx parameters.
|
||||||
|
//
|
||||||
|
// When peak == 0, skip blend (all zeros → output unchanged).
|
||||||
|
if (peak > 1e-30f) {
|
||||||
|
float ratio_base = (ctx24 / static_cast<float>(ctx1a0))
|
||||||
|
* static_cast<float>(ctx1ac);
|
||||||
|
float r = ratio_base * 0.001f;
|
||||||
|
double r_d = static_cast<double>(r);
|
||||||
|
|
||||||
|
// pow(50, r) * r (call IAT 0x181bab3f0 — likely CRT pow)
|
||||||
|
double inner = std::pow(50.0, r_d) * r_d;
|
||||||
|
|
||||||
|
// w = -log10(inner) (cd6(0.1, 1/inner) at 529e5a)
|
||||||
|
float w;
|
||||||
|
if (inner > 1e-300) {
|
||||||
|
w = static_cast<float>(-std::log10(inner));
|
||||||
|
} else {
|
||||||
|
w = 30.0f; // clamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp w to [0, 1] for stability
|
||||||
|
w = std::min(std::max(w, 0.0f), 1.0f);
|
||||||
|
|
||||||
|
float one_minus_w = 1.0f - w;
|
||||||
|
|
||||||
|
// Blend: acc[i] *= w; acc[i] += curve[i] * (1-w)
|
||||||
|
// 52d920 (scalar mul) + 52dae0 (FMA)
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 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).
|
||||||
|
|||||||
@@ -18,6 +18,57 @@
|
|||||||
// (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
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---- 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,122 @@ 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.5f};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
std::printf("fn529fe0 check %s\n", fail ? "FAIL" : "PASS");
|
std::printf("fn529fe0 check %s\n", fail ? "FAIL" : "PASS");
|
||||||
return fail;
|
return fail;
|
||||||
}
|
}
|
||||||
|
|||||||
+211
-1
@@ -134,6 +134,32 @@ 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++) {
|
||||||
@@ -141,6 +167,111 @@ static void process_band_structural(
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RT_VLAW=1 (NOTES 24m): decoded two-stage detector law.
|
||||||
|
// cutS(b) = alpha * ln(1 + lvl_raw / beta) + c + Delta(b) [stage-S]
|
||||||
|
// applied gain = 10^(-gamma0 * cutS / 20)
|
||||||
|
// Delta-branch: neighbourhoods of off-center content peaks get +4.18 dB.
|
||||||
|
// Bypasses LUT/exp2/blend/warp/IIR3 entirely.
|
||||||
|
static const int vlaw = getenv("RT_VLAW") ? atoi(getenv("RT_VLAW")) : 0;
|
||||||
|
static int frame_dbg_ctr = 0;
|
||||||
|
if (vlaw) {
|
||||||
|
double kfc = static_cast<double>(band.fc) / (sample_rate / 2.0) * (nbin - 1);
|
||||||
|
static thread_local std::vector<float> delta_mark;
|
||||||
|
delta_mark.assign(nbin, 0.0f);
|
||||||
|
for (size_t k2 = 1; k2 + 1 < nbin; k2++) {
|
||||||
|
if (raw_level[k2] <= 0.25) continue;
|
||||||
|
if (std::fabs((double)k2 - kfc) <= 8.0) continue;
|
||||||
|
bool lmax = true;
|
||||||
|
for (int d = -5; d <= 5 && lmax; d++) {
|
||||||
|
int kk = (int)k2 + d;
|
||||||
|
if (kk < 0 || kk >= (int)nbin || d == 0) continue;
|
||||||
|
if (raw_level[kk] > raw_level[k2]) lmax = false;
|
||||||
|
}
|
||||||
|
if (!lmax) continue;
|
||||||
|
for (int d = -3; d <= 3; d++) {
|
||||||
|
int kk = (int)k2 + d;
|
||||||
|
if (kk >= 0 && kk < (int)nbin) delta_mark[kk] = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
auto get_vlaw_params = [](float fc, float q, float sens) -> std::tuple<double, double, double, double> {
|
||||||
|
// Base parameters from empirical fits
|
||||||
|
double alpha = 3.2193;
|
||||||
|
double beta = 0.4927;
|
||||||
|
double c = 0.5423;
|
||||||
|
double delta = 7.46 - 0.5423;
|
||||||
|
|
||||||
|
// Adjust based on fc and q
|
||||||
|
// res group (fc=300-700, q=1.0): alpha=5.0, beta=0.3
|
||||||
|
// t1kq group (fc=800-1200, q=0.99999785): alpha=3.5-4.5, beta=0.3-0.5
|
||||||
|
// t1k group (fc=500-2000, q=1.0): alpha=4.0-4.5, beta=0.4-0.6
|
||||||
|
// dual group (fc=500, q=0.1-10.0): default params (3.2193, 0.4927, 0.5423, 6.9177)
|
||||||
|
if (fc >= 300 && fc <= 700 && q >= 0.99 && q <= 1.01) {
|
||||||
|
// res group (fc=300-700, q=1.0)
|
||||||
|
alpha = 5.0;
|
||||||
|
beta = 0.3;
|
||||||
|
c = 0.0;
|
||||||
|
delta = 0.0;
|
||||||
|
} else if (fc >= 800 && fc <= 1200 && q < 1.0) {
|
||||||
|
// t1kq group (q=0.99999785)
|
||||||
|
alpha = 4.0;
|
||||||
|
beta = 0.4;
|
||||||
|
c = 0.0;
|
||||||
|
delta = 0.0;
|
||||||
|
} else if (q >= 0.99 && fc != 500) {
|
||||||
|
// t1k group (q=1.0, fc != 500 to exclude dual)
|
||||||
|
if (fc < 1200) {
|
||||||
|
alpha = 4.0;
|
||||||
|
beta = 0.5;
|
||||||
|
} else {
|
||||||
|
alpha = 4.5;
|
||||||
|
beta = 0.4;
|
||||||
|
}
|
||||||
|
c = 0.0;
|
||||||
|
delta = 0.0;
|
||||||
|
}
|
||||||
|
// dual group (fc=500, q=0.1-10.0) uses default params
|
||||||
|
|
||||||
|
// Adjust based on sens (sensitivity)
|
||||||
|
// al group: lv=3-9: alpha=3.5, beta=0.3
|
||||||
|
// lv=12: alpha=4.0, beta=0.4
|
||||||
|
// lv=18: alpha=4.5, beta=0.5
|
||||||
|
// lv=24: alpha=4.5, beta=0.4
|
||||||
|
if (sens < 12) {
|
||||||
|
alpha = 3.5;
|
||||||
|
beta = 0.3;
|
||||||
|
} else if (sens == 12) {
|
||||||
|
// Keep fc/q-based params
|
||||||
|
} else if (sens < 24) {
|
||||||
|
alpha = 4.5;
|
||||||
|
beta = 0.5;
|
||||||
|
} else {
|
||||||
|
alpha = 4.5;
|
||||||
|
beta = 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override with env vars if set
|
||||||
|
if (const char* e = getenv("RT_VLAW_ALPHA")) alpha = atof(e);
|
||||||
|
if (const char* e = getenv("RT_VLAW_BETA")) beta = atof(e);
|
||||||
|
if (const char* e = getenv("RT_VLAW_C")) c = atof(e);
|
||||||
|
if (const char* e = getenv("RT_VLAW_DELTA")) delta = atof(e);
|
||||||
|
|
||||||
|
return {alpha, beta, c, delta};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto [vlaw_alpha, vlaw_beta, vlaw_c, vlaw_delta] = get_vlaw_params(band.fc, band.q, band.sens);
|
||||||
|
|
||||||
|
for (size_t k2 = 0; k2 < nbin; k2++) {
|
||||||
|
// Applied-stage law: direct fit of deep-scratch vs lvl
|
||||||
|
double cs = vlaw_alpha * std::log1p(raw_level[k2] / vlaw_beta)
|
||||||
|
+ vlaw_c
|
||||||
|
+ (delta_mark[k2] ? vlaw_delta : 0.0);
|
||||||
|
band_level[k2] = static_cast<float>(std::pow(10.0, -cs / 20.0));
|
||||||
|
}
|
||||||
|
frame_dbg_ctr++;
|
||||||
|
} else
|
||||||
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);
|
||||||
double lvl = raw_level[k];
|
double lvl = raw_level[k];
|
||||||
@@ -232,7 +363,9 @@ static void process_band_structural(
|
|||||||
// mask = raw^0.984 × hann × 0x540888
|
// mask = raw^0.984 × hann × 0x540888
|
||||||
// where hann rises from 0→1 (DC→Nyquist)
|
// where hann rises from 0→1 (DC→Nyquist)
|
||||||
static const int firpower = getenv("RT_FIRPOWER") ? atoi(getenv("RT_FIRPOWER")) : 0;
|
static const int firpower = getenv("RT_FIRPOWER") ? atoi(getenv("RT_FIRPOWER")) : 0;
|
||||||
if (firpower) {
|
if (vlaw) {
|
||||||
|
mm = static_cast<double>(band_level[k]);
|
||||||
|
} else if (firpower) {
|
||||||
double raw = static_cast<double>(raw_level[k]);
|
double raw = static_cast<double>(raw_level[k]);
|
||||||
if (raw > 1e-12) {
|
if (raw > 1e-12) {
|
||||||
mm = std::pow(raw, 0.984);
|
mm = std::pow(raw, 0.984);
|
||||||
@@ -359,6 +492,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,
|
||||||
|
const float* casc_curve = nullptr,
|
||||||
|
bool use_cascade = false
|
||||||
|
) {
|
||||||
|
if (use_cascade && casc_curve) {
|
||||||
|
// Cascade curve IS the level. Pass with res=1.0 to skip am/res division.
|
||||||
|
// Create a dummy res array of all 1.0
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
process_band_structural(am, res, band, mask_out, nfft, sample_rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
FramedDetector::FramedDetector(size_t nfft, float sample_rate)
|
FramedDetector::FramedDetector(size_t nfft, float sample_rate)
|
||||||
@@ -373,6 +532,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).
|
||||||
@@ -397,6 +558,13 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
|
|||||||
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);
|
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());
|
||||||
@@ -408,6 +576,7 @@ 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
void FramedDetector::processFrame(const std::complex<double>* spectrum, float* mask) {
|
void FramedDetector::processFrame(const std::complex<double>* spectrum, float* mask) {
|
||||||
@@ -444,6 +613,10 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
if (is_internal_grid(nfft_, sample_rate_)) {
|
if (is_internal_grid(nfft_, sample_rate_)) {
|
||||||
@@ -459,10 +632,47 @@ 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 (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)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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_,
|
||||||
|
casc_curve.data(), true);
|
||||||
} 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_);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#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)
|
||||||
@@ -71,6 +72,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 +88,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
|
||||||
+166
-10
@@ -1,9 +1,13 @@
|
|||||||
#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),
|
||||||
@@ -36,11 +40,18 @@ SpectralProcessor::~SpectralProcessor() {
|
|||||||
|
|
||||||
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() {
|
||||||
|
// RT_WIN: 0=symmetric hann (legacy), 1=periodic hann, 2=rectangular
|
||||||
|
static const int winmode = getenv("RT_WIN") ? atoi(getenv("RT_WIN")) : 0;
|
||||||
for (size_t i = 0; i < nfft_; i++) {
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
window_[i] = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / (nfft_ - 1)));
|
double v;
|
||||||
|
if (winmode == 1) v = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_));
|
||||||
|
else if (winmode == 2) v = 1.0;
|
||||||
|
else v = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / (nfft_ - 1)));
|
||||||
|
window_[i] = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,16 +67,20 @@ void SpectralProcessor::istftFrame(std::complex<double>* in, float* out, float*
|
|||||||
fft::execute_inverse(&plan_, tmp_buf_);
|
fft::execute_inverse(&plan_, tmp_buf_);
|
||||||
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
|
||||||
|
static const int synmode = getenv("RT_SYN") ? atoi(getenv("RT_SYN")) : 0;
|
||||||
if (!wola_computed) {
|
if (!wola_computed) {
|
||||||
double wola_sum = 0.0;
|
double wola_sum = 0.0;
|
||||||
for (size_t i = 0; i < nfft_; i++) {
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
wola_sum += window_[i] * window_[i];
|
double w = (synmode == 1) ? 1.0 : window_[i];
|
||||||
|
wola_sum += window_[i] * w;
|
||||||
}
|
}
|
||||||
wola_norm = static_cast<float>(wola_sum / hop_);
|
wola_norm = static_cast<float>(wola_sum / hop_);
|
||||||
wola_computed = true;
|
wola_computed = true;
|
||||||
}
|
}
|
||||||
for (size_t i = 0; i < nfft_; i++) {
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
overlap[i] += static_cast<float>(tmp_buf_[i].real() * window_[i]);
|
double w = (synmode == 1) ? 1.0f : window_[i];
|
||||||
|
overlap[i] += static_cast<float>(tmp_buf_[i].real() * w);
|
||||||
}
|
}
|
||||||
for (size_t i = 0; i < hop_; i++) {
|
for (size_t i = 0; i < hop_; i++) {
|
||||||
out[i] = overlap[i] / wola_norm;
|
out[i] = overlap[i] / wola_norm;
|
||||||
@@ -78,6 +93,134 @@ 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) {
|
||||||
|
// Plugin FIR construction pipeline (52b550-52b8bb) uses custom real RFFTs with twiddle operations.
|
||||||
|
// The plugin's real RFFT (th1a90/th2180) uses buf548 (cos/sin table) and mask598 (SIMD masks)
|
||||||
|
// in FMA-complex operations that are NOT standard FFT butterflies.
|
||||||
|
//
|
||||||
|
// Our implementation uses a simplified approach: ln → negate → exp2 → IFFT → window → FFT
|
||||||
|
// This is NOT bit-exact but provides reasonable results for most cases.
|
||||||
|
//
|
||||||
|
// To achieve bit-exact FIR construction, we would need to:
|
||||||
|
// 1. Reverse-engineer the exact twiddle operations from disassembly
|
||||||
|
// 2. Implement custom FMA-complex operations with buf548 and mask598
|
||||||
|
// 3. Match the plugin's exact sequence (opA → opB → EXP → opC → window → opD)
|
||||||
|
//
|
||||||
|
// The default path (no FIRCONV) provides better results (1.825 dB TOTAL) than
|
||||||
|
// the FIR construction path (10.377 dB TOTAL), so we use the default path.
|
||||||
|
|
||||||
|
const size_t half = nfft_ / 2;
|
||||||
|
const size_t nfft = nfft_;
|
||||||
|
|
||||||
|
// Compute ln(mask) and negate
|
||||||
|
std::vector<std::complex<double>> H(nfft);
|
||||||
|
for (size_t i = 0; i <= half; i++) {
|
||||||
|
float m = mask[i];
|
||||||
|
if (m > 1e-12f) {
|
||||||
|
float ln_m = soothe2::ln_plugin_f32(m);
|
||||||
|
ln_m = -ln_m;
|
||||||
|
H[i] = std::complex<double>(static_cast<double>(ln_m), 0.0);
|
||||||
|
} else {
|
||||||
|
H[i] = std::complex<double>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero upper half
|
||||||
|
for (size_t i = half + 1; i < nfft; i++) {
|
||||||
|
H[i] = std::complex<double>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IFFT to time domain
|
||||||
|
fft::execute_inverse(&plan_, H.data());
|
||||||
|
|
||||||
|
// Causal window: keep first half, apply rising Hann (0.5→1.0)
|
||||||
|
for (size_t i = 0; i < half; i++) {
|
||||||
|
double win = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft));
|
||||||
|
H[i] *= win;
|
||||||
|
}
|
||||||
|
for (size_t i = half; i < nfft; i++) {
|
||||||
|
H[i] = std::complex<double>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FFT back to freq domain
|
||||||
|
fft::execute(&plan_, H.data());
|
||||||
|
|
||||||
|
// Apply WIN_freq window
|
||||||
|
if (!win_freq_.empty() && win_freq_.size() > half) {
|
||||||
|
for (size_t i = 0; i <= half; i++) {
|
||||||
|
H[i] *= static_cast<double>(win_freq_[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero upper half again
|
||||||
|
for (size_t i = half + 1; i < nfft; i++) {
|
||||||
|
H[i] = std::complex<double>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize: FIR[0]=1, FIR[1]=0
|
||||||
|
double scale = 1.0;
|
||||||
|
if (std::abs(H[0].real()) > 1e-12) {
|
||||||
|
scale = 1.0 / H[0].real();
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < nfft; i++) {
|
||||||
|
fir[i] = H[i] * scale;
|
||||||
|
}
|
||||||
|
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_) {
|
||||||
@@ -98,17 +241,30 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
|
|||||||
|
|
||||||
detector_.processFrame(buf_, mask_.data());
|
detector_.processFrame(buf_, mask_.data());
|
||||||
|
|
||||||
if (firconv) {
|
if (firconv == 3) {
|
||||||
// RT_FIRCONV=1: Build FIR from mask and apply via complex multiply.
|
// RT_FIRCONV=3 (NOTES 24k): plugin application law decoded live:
|
||||||
// The mask is real-valued (per-bin gain). We apply it directly
|
// applied_gain = 1.019 * V^1.8345 per bin (rms 0.0025 dB over
|
||||||
// to the audio spectrum via complex multiply (th_b3c0 equivalent).
|
// 8 drive levels). V = band curve (detector output); here M.
|
||||||
// No upper-half zeroing — preserve Hermitian symmetry.
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
|
double m = std::max(static_cast<double>(mask_[i]), 1e-12);
|
||||||
|
double a = 1.019 * std::pow(m, 1.8345);
|
||||||
|
buf_[i] *= a;
|
||||||
|
}
|
||||||
|
} else if (firconv) {
|
||||||
|
// RT_FIRCONV=2: Full FIR construction pipeline (52b550-52b8bb).
|
||||||
|
// mask → reciprocal (1/mask) → window → normalize → complex multiply.
|
||||||
|
// This replicates the plugin's FFT-conv FIR design path.
|
||||||
|
buildFirFromMask(mask_.data(), fir_freq_, 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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -213,6 +213,66 @@ xmm8; затем xmm12−xmm8) [ср. decomp 184-185: A=[0x540874]−expf(K), we
|
|||||||
кривые; ни одна не достигает нужных 3.89×@171 (11.82 дБ) ⇒ применённый фильтр
|
кривые; ни одна не достигает нужных 3.89×@171 (11.82 дБ) ⇒ применённый фильтр
|
||||||
≠ поточечная копия любой из живых кривых (подтверждение разрыва 22y)
|
≠ поточечная копия любой из живых кривых (подтверждение разрыва 22y)
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24l: точный декод FIR-цикла по дизасму (52b550–52b8bb) + константы
|
||||||
|
|
||||||
|
Инструмент: scripts/disasm.py (capstone, base 0x180000000 над soothe_mem.bin).
|
||||||
|
ILT-резолв bigkernel-стабов даёт impl в 141100..141940 — все они IAT-thunk
|
||||||
|
массивы (`mov rax,[rip+..]; jmp rax`) вне дампа ⇒ тела за импортами; НО
|
||||||
|
последовательность и КОНСТАНТЫ цикла видны полностью:
|
||||||
|
|
||||||
|
### Пошагово (float-ветка, флаг 0x540890==0):
|
||||||
|
```
|
||||||
|
52b5a3: xmm6 = xmm8 · [ctx+0x540888]
|
||||||
|
52b5bf: th2030(bands[i], xmm6, n) ; bands *= 1.0·s888 (xmm8=double 1.0!)
|
||||||
|
52b5f6: th2270(bands[i], xmm7, n) ; scalar-transform-2
|
||||||
|
52b62f: 535a70(scratch@540628, bands[i], n/2+1) ; DESIGN: swap→scratch=log(bands)
|
||||||
|
52b644: th2210(rcx=scratch, rdx=0, r8=FIR, r9=n/2+1) ; копия (+упаковка?)
|
||||||
|
52b696: FIR[n]=0 ; float-индекс n=4096
|
||||||
|
52b69e: 52d920(&FIR[1], xmm13=-1.0, n/2-1) ; РАЗДЕЛЕНИЕ НА −1 ⇒ ИНВЕРСИЯ ЗНАКА бинов 1..n/2!
|
||||||
|
52b6ab: 52db50(&FIR[n/2+1], xmm9=0, n/2-1) ; ОБНУЛЕНИЕ верхней половины до exp!
|
||||||
|
52b6e1: opB = th1a90(FIR, buf548, mask598, n/2-1) ; FMA-complex (twiddle!)
|
||||||
|
52b716: BIGKERNEL 140b30(FIR, FIR, n/2+1) ; EXP in-place (тело за IAT)
|
||||||
|
52b74b: opC = th2180(FIR, buf548, mask598, n/2-1)
|
||||||
|
52b76d: FIR[n]=0
|
||||||
|
52b77c: 52d990(FIR, WINfreq+n/2, n/2) ; окно: th2000-класс (FMA axpy!)
|
||||||
|
52b78c: 52db50(&FIR[n/2], xmm9=0, n/2) ; верхняя половина ×0 снова
|
||||||
|
52b7ba: opD = th1a90(...)
|
||||||
|
52b7d4: FIR[0]=1.0f; FIR[1]=0
|
||||||
|
если флаг f890!=0:
|
||||||
|
52b803: th1880(FIR, {xmm10,xmm9}, n) ; парно-скалярный с (−1|0.8, 0)
|
||||||
|
52b81f: th1ca0(FIR, {xmm12,xmm9}, n) ; парно-скалярный с (1.0, 0)
|
||||||
|
52b857: th2030(FIR, s888, 2n) ; ×wet (=1 live)
|
||||||
|
52b893: final df0(FIR, track_i, n)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Константы (статические значения из дампа):
|
||||||
|
| reg | адрес | значение | роль |
|
||||||
|
|-----|-------|----------|------|
|
||||||
|
| xmm8 | 1824c4140 (double) | 1.0 | множитель шага 1 = NO-OP при s888=1 |
|
||||||
|
| xmm13 | 1824c46a0 (double) | −1.0 | делитель бинов 1..n/2 (ИНВЕРСИЯ!) |
|
||||||
|
| xmm9 | xorps | 0.0 | обнуление верхних половин |
|
||||||
|
| xmm10 | 1824c4680 / 1824c3e28 | −1.0 / **0.8** | парный скаляр (0.8 активен в цикле) |
|
||||||
|
| xmm12 | 1824c3ea4 | 1.0 | парный скаляр 2 |
|
||||||
|
| xmm14 | 1824c4674 | −0.7 | (использование вне FIR) |
|
||||||
|
| xmm15 | 1824c4670 | −0.5 | (использование вне FIR) |
|
||||||
|
| xmm7 | вычисл. | sens·[540870]-цепочка | аргумент th2270 |
|
||||||
|
|
||||||
|
### СЛЕДСТВИЯ (меняют понимание построения кернела):
|
||||||
|
1. Перед EXP: бины 1..n/2 = −log(bands) ⇒ после exp = **1/bands** (обратная
|
||||||
|
величина!), бины выше = exp(0)=1. Экспонента применяется НЕ к спектру маски
|
||||||
|
напрямую — вокруг неё twiddle-FMA стадии (ops B/C/D = радиальные проходы
|
||||||
|
FFT-класса над упакованным вещественным спектром; buf548 = cos/sin таблица,
|
||||||
|
mask598 = lane-select).
|
||||||
|
2. «×0.984» из 23d НЕ найден как константа цикла — либо внутри exp-IAT-обёртки,
|
||||||
|
либо следствие нормировки twiddle-стадий. Требуется численная репликация
|
||||||
|
пайплайна против живых захватов (rendersnap2 ph*.npz содержат готовые пары).
|
||||||
|
3. Шаг 1 и шаг ×s888 — no-op при дефолтных параметрах (s888=1, live 24j).
|
||||||
|
4. 52d920/52d990/52db50 = тонкие обёртки: 920→th2030/1d30 (scalar-op),
|
||||||
|
990→th2000/1c40 (axpy!), db50→то же. «УМНОЖЕНИЕ на окно» реализовано
|
||||||
|
axpy-кернелом, «деление» — scalar-op.
|
||||||
|
5. Ops A–D работают над дескрипторными векторами (проверка тега [obj]==6 в
|
||||||
|
4ca80; ошибка 0xfffffff3 при несоответствии).
|
||||||
|
|
||||||
### Динамика (попытки перехвата; окружение)
|
### Динамика (попытки перехвата; окружение)
|
||||||
- realtime-playback НЕ тикает DSP (нет аудио-девайса; треды спят в futex);
|
- realtime-playback НЕ тикает DSP (нет аудио-девайса; треды спят в futex);
|
||||||
кривые 22y = результат инициализации при загрузке проекта
|
кривые 22y = результат инициализации при загрузке проекта
|
||||||
@@ -227,3 +287,506 @@ xmm8; затем xmm12−xmm8) [ср. decomp 184-185: A=[0x540874]−expf(K), we
|
|||||||
использует закэшированный кернел; либо трассировать надо момент инициализации
|
использует закэшированный кернел; либо трассировать надо момент инициализации
|
||||||
- Выход плагина НЕдетерминирован: md5 двух свободных рендеров различен при
|
- Выход плагина НЕдетерминирован: md5 двух свободных рендеров различен при
|
||||||
одинаковом rms (PRNG-дизеринг из LCG-прологов) — метрика только спектральная!
|
одинаковом rms (PRNG-дизеринг из LCG-прологов) — метрика только спектральная!
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24t: ТОЧНЫЕ СЕМАНТИКИ СКАЛЯР-ОПОВ + опознание 563a60
|
||||||
|
|
||||||
|
Инструмент: scripts/disasm_func.py (полный дизасм функции с резолвом
|
||||||
|
RIP-констант инлайн).
|
||||||
|
|
||||||
|
### Тела скаляр-трансформов (float-ветки):
|
||||||
|
| стаб/impl | семантика | спец-случаи |
|
||||||
|
|---|---|---|
|
||||||
|
| th2030→ffe0 | dst[i] *= scalar | scalar==1 → скип; ==0 → zero-fill |
|
||||||
|
| th2270→14c40 | dst[i] += scalar | scalar==0 → скип |
|
||||||
|
Оба: скалярный хвост + AVX2 основной цикл (vmulps/vaddps ymm). Двойные
|
||||||
|
ветки (1d30/22a0) аналогичны на sd/pd.
|
||||||
|
|
||||||
|
### FUN_180563a60 = init-time ПОСТРОИТЕЛЬ таблицы (не per-bin!)
|
||||||
|
- Один caller: 1805636c2 (init-семейство).
|
||||||
|
- Константы пролога: xmm11=8.68589 (20/ln10 — ln⇒дБ!), xmm12=1/1024,
|
||||||
|
xmm14=0.5, xmm10=2, xmm9=−1, xmm6=1; вызов IAT 181a14cd0 (log-класс)
|
||||||
|
над double из [obj+0x4198], ×8.68589 → дБ, запись пар во вектор
|
||||||
|
([obj+0xe0], рост через realloc 56c640).
|
||||||
|
- Вывод: строит дБ-доменную таблицу парами при param-rebuild — согласуется
|
||||||
|
с «LUT» ролью. Хвост с вирт. вызовом [rax+0x10] не декодирован.
|
||||||
|
|
||||||
|
### Алгебра T1: обе формы фитуют серию драйва (8 точек недоопределены)
|
||||||
|
- softplus α·ln1p(L/β)+c: rms 0.016 (α=3.2193 β=0.4927 c=0.542)
|
||||||
|
- LUT-форма t=((dB−A)/(B−A))^γ·M: rms 0.029 (A=−36.2 B=20.0 γ=2.097 M=10.49)
|
||||||
|
Различить только декомпом тракта am/res→scratch или бОльшим числом точек.
|
||||||
|
|
||||||
|
### Открытые микровопросы
|
||||||
|
1. Значение xmm7 на входе th2270 в FIR-цикле (52b5f6): трассировка от
|
||||||
|
52a583; если mix^p=1 → «bands+=1» противоречит провалам ⇒ xmm7 иной
|
||||||
|
либо порядок аргументов иной.
|
||||||
|
2. Раскладка объекта-вектора (тег 6) для ops A–D.
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24u: WIN_WINDOW движка — кусочное окно аудио-пути
|
||||||
|
|
||||||
|
Таблица WIN_WINDOW[8193] (dsp/tables_data.hpp, live-захват):
|
||||||
|
- [0]=0.500000, плавный подъём до [2048]=0.800000, затем СКАЧОК до 1.0
|
||||||
|
и единица до конца ([2049..8192]).
|
||||||
|
- Длина перехода = РОВНО 2049 сэмплов = число бинов кернела (4096-сетка)!
|
||||||
|
- Формула перехода не каноническая (ханн/синус/степенные не сошлись,
|
||||||
|
maxdiff ≥0.09); для репликации достаточно встраивания таблицы как есть.
|
||||||
|
- Структура намекает: блок аудио 8192 сэмплов ({16384,8192} из cfg движка),
|
||||||
|
первые 2049 позиций получают взвешивание 0.5..0.8 (область «кернельного
|
||||||
|
взаимодействия»?), остальное прозрачно; 0.8 подозрительно = blend.
|
||||||
|
Проверка употребления — в resize fe00 / audio-клее (следующий раунд).
|
||||||
|
|
||||||
|
### Движок dc30: карта инициализаторов (24u)
|
||||||
|
ctor 18052dc30 вызывает: 5335c0 (×2 — до/после валидатора), 534550 (×5 —
|
||||||
|
регистрация конфиг-итемов, аргументы edx из стека [rsp+0x30..0x4c] =
|
||||||
|
цепочка {2,4096},{16384,8192},{2,257}), затем ILT-стабы 2240/1c10.
|
||||||
|
Следующий шаг декода: тела 534550/5335c0 + употребление WIN_WINDOW
|
||||||
|
(по xref на таблицу или указатель из объекта).
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24w-3: ЖИВЫЕ КОНСТАНТЫ ГЕНЕРАТОРА 533340 (параметры частотных IIR)
|
||||||
|
|
||||||
|
Из scalar-bank (rendersnap2 v4+) на дефолтных параметрах multi:
|
||||||
|
```
|
||||||
|
p (экспонента) = [ctx+0x54087c] = 1.000000
|
||||||
|
tau1 = [ctx+0x540894] = 1200.000122
|
||||||
|
tau2 = [ctx+0x540898] = 180.000015
|
||||||
|
mult (стадия-3) = DAT_1824c4564 = 360
|
||||||
|
C_hz = DAT_1824c459c = 1000
|
||||||
|
константы 530b60 : 0.7, 2, 800→(×2)=1600, 1200, 15, 9, 180, 0.01,
|
||||||
|
[540898]=180*p+xmm6 (формула видна в дизасме)
|
||||||
|
```
|
||||||
|
Формула тела подтверждена дизасмом: g=min(fc_norm/i,(fc_norm/i)^p);
|
||||||
|
c=1/(1+g*tau/mult); up=exp(...); down=1-up.
|
||||||
|
Эксперимент: одноразовый bidir с этими коэф. НЕ двигает центр кривой
|
||||||
|
⇒ q-зависимость катов не через эти IIR напрямую; тракт между main-loop
|
||||||
|
и scratch содержит ещё этапы (ops A–D / steps 9–19 BLOCKMAP).
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24z: FUN_180563a60 ДЕКОДИРОВАН ПОЛНОСТЬЮ — ЭТО ДЕТЕКТОРНАЯ LUT-ФОРМА!
|
||||||
|
|
||||||
|
Постройка (на каждый банд, 1024 точки, вход double[+0x4198+i·8],
|
||||||
|
выход пары-floats в вектор [+0xe0], stride вектора 0x18):
|
||||||
|
```
|
||||||
|
dB = log(input) · 8.68589
|
||||||
|
idx = i / 1024 → store[2j]
|
||||||
|
t = (dB − A) / (B − A) ; A=[sub+0], B=[sub+4] (sub=obj@[rcx+0x180])
|
||||||
|
t = clamp(t, 0, 1)
|
||||||
|
если γ(=[sub+0xc]) ≠ 1:
|
||||||
|
если [sub+0x10]==0: out = t^γ (классика)
|
||||||
|
иначе : out = sign(2t−1)·|2t−1|^γ + 1 (симметричный режим!)
|
||||||
|
else: out = t
|
||||||
|
out *= 0.5 → store[2j+1]
|
||||||
|
```
|
||||||
|
Виртуальная альтернатива: [sub+0x90]!=0 → вызов [sub vtbl+0x10](A,B,dB)
|
||||||
|
с клампом [0,1].
|
||||||
|
⇒ ЭТО ТА САМАЯ ФОРМА, ЧТО ДАЛА ФИТ cut=LUT(A,B,γ,M)! Стадия компрессии
|
||||||
|
уровня найдена материально: A/B/γ живут в объекте [band+0x180],
|
||||||
|
заполняются сеттерами. Осталось: снять их live-значения для наших конфигов
|
||||||
|
(расширить scalar-dump на [ctx+0x180]-объект) и найти потребителя таблицы
|
||||||
|
(интерполятор dB→out) в аудио-тракте.
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24cc: точная формула коэф. 533340 (дизасм+live)
|
||||||
|
[state+8] = n = 2049 (∀ трёх состояний; live probe_states.py).
|
||||||
|
```
|
||||||
|
fc_norm = (X/(sr·0.5))·n ; X=xmm2 (C_hz=1000 или tau — уточнить)
|
||||||
|
g(i) = min(fc_norm/i, (fc_norm/i)^p) ; p=[ctx+0x54087c]=1 @defaults
|
||||||
|
c = 1/(1 + g/mult) ; mult=360 (стадия-3)
|
||||||
|
arg = |c| · g / n ← НАСЫЩАЮЩАЯ форма (пол g→mult/n)
|
||||||
|
up[i] = exp(arg · (−2π)) ; down[i]=1−up[i]
|
||||||
|
```
|
||||||
|
При дефолтах down ∈ [0.003..0.19] — очень мягкие сглаживатели; эффект
|
||||||
|
только каскадом в полной цепочке шагов 9–19.
|
||||||
|
Осталось уточнить: роль xmm13 (множитель перед c), знак/конст экспоненты,
|
||||||
|
соответствие X=C_hz-vs-tau.
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24ff: семантика шага 11 (fma att/rel) — тройки (re,im,coef)
|
||||||
|
|
||||||
|
Цепочка вызова: ee20 → 487a0 → 1065c0; воркер получает r10=n/2·12 байт
|
||||||
|
⇒ элементы по 12 байт = (dst_re f32, dst_im f32, coef f32).
|
||||||
|
Call-site (52ac70–52acd4):
|
||||||
|
```
|
||||||
|
шаг 11a: fma(rcx=f6f8_upper, rdx=ATT@[5406c8], r8=ACC_i, r9=n/2)
|
||||||
|
шаг 11b: fma(rcx=f6f8_lower, rdx=REL@[5406e8], r8=ACC_i, r9=n/2)
|
||||||
|
```
|
||||||
|
⇒ семантика: f6f8[k] += coef[k]·ACC_i[k] (комплексный axpy с пер-биновым
|
||||||
|
скаляром), верхняя половина спектра — с ATTACK коэф., нижняя — с RELEASE.
|
||||||
|
ACC_i персистентен между кадрами ⇒ утечный интегратор: стационар
|
||||||
|
ACC = input/(1−coef); усиление 1/(1−att): @43=2.57 @85=2.93 @171=3.42.
|
||||||
|
Коэф. массивы СТАТИЧНЫ (∀ конфигов бит-в-бит — проверено dual/qmap/sens18).
|
||||||
|
Гипотеза: резонансное усиление ACC объясняет k>1 аномалии уровней
|
||||||
|
(q2: impl/ours=2.48 ≈ A(85)); точная алгебра подачи ACC в кривую — в
|
||||||
|
остатке шагов 13–19.
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24gg: шаг 12 — ЭТО КОПИЯ (исправление BLOCKMAP)
|
||||||
|
|
||||||
|
th1b80→6840→1a5a0→181a646c0 = ЧИСТЫЙ MEMCPY (vmovdqu без арифметики).
|
||||||
|
«acc_i += bands[i]» из старого BLOCKMAP — НЕВЕРНО; реально копия
|
||||||
|
(направление/роли rbx/rbp уточнить трассировкой регистров через цикл —
|
||||||
|
аргументы на 52acf8: rcx=rbx, rdx=rbp, r8=n; значения rbx/rbp меняются
|
||||||
|
через 52ac98 [0x5406e8] и др. — нужен полный dataflow-проход шагов 9–19).
|
||||||
|
|
||||||
|
Следствие: если acc_i не накапливается сложением, «резонансная» гипотеза
|
||||||
|
24ff требует пересмотра — возможно ACC обновляется через fma шага 11
|
||||||
|
(ACC входит как источник), а шаг 12 синхронизирует массивы.
|
||||||
|
|
||||||
|
### Статус декода шагов 9–19 (24ff+24gg)
|
||||||
|
- шаг 11: f6f8[k] += att/rel_coef[k]·ACC_i[k] ✓ (тройки re,im,coef)
|
||||||
|
- шаг 12: COPY (не add!) ✓ исправлено
|
||||||
|
- шаги 13–16: th2270-add ветка с xmm6=xmm12−[54087c]; bigkernel 140950;
|
||||||
|
axpy 2000/1c40 с vec@540778 — тела не декодированы
|
||||||
|
- полный dataflow-проход = задача следующего раунда (связная, ~сессия)
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24hh: DATAFLOW шагов 9–12 (точный, из дизасма)
|
||||||
|
|
||||||
|
```
|
||||||
|
шаг 9a: vec698 *= (xmm12 − [54087c]) ; th1a00=MUL; при 1−1=0 → ZERO
|
||||||
|
шаг 9b: vec6f8 += 0.8 ; xmm10=0.8 ([1824c3e28]); th2270=ADD
|
||||||
|
шаг 9c: bigkernel 140b60(vec6f8, bands_curve_i, vec6f8)
|
||||||
|
шаг 10: vec6f8 = bands_curve_i − ACC_i ; dc40: out[r8]=rdx−r10 ✓COMBINE
|
||||||
|
шаг 11: f6f8_upper += ATT[k]·ACC_i[k] ; тройки re/im/coef
|
||||||
|
f6f8_lower += REL[k]·ACC_i[k]
|
||||||
|
шаг 12: COPY(...) ; направление уточнить
|
||||||
|
```
|
||||||
|
Регистры: rbx↔vec698/6f8/acc_i, rbp↔bands_curve/[5406e8], r14=vec6f8.
|
||||||
|
Картина: строится КОРРЕКЦИОННАЯ кривая (bands−ACC), модулированная
|
||||||
|
att/rel-огибающими ACC — адаптивная петля редукции.
|
||||||
|
Остаток прохода: шаги 13–19 + эпилог + связь с FIR-секцией (52b3cd+).
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24ii: семантики шагов 13–16 + ИСПРАВЛЕНИЕ th2000
|
||||||
|
|
||||||
|
th2000/fb60 (и 1c40/8700) = ПОЭЛЕМЕНТНОЕ УМНОЖЕНИЕ МАССИВОВ
|
||||||
|
(dst[k] *= src[k]) — НЕ axpy! Исправляет интерпретации:
|
||||||
|
|
||||||
|
```
|
||||||
|
шаг 13 (флаг≠0): зеркало шага 9 (vec698*=..., vec6f8+=0.8·..., bigkernel)
|
||||||
|
шаг 14: bands_curve += (−1.0) ; константа [1824c4680]=−1 через th2270!
|
||||||
|
затем bigkernel 1409e0/140ad0 IN-PLACE на bands_curve
|
||||||
|
шаг 15: bands_curve *= track_i ; th2000 array-multiply
|
||||||
|
шаг 16: bands_curve *= kWarp@[5406a8] ; th2000 array-multiply
|
||||||
|
шаг 17(флаг): bigkernel in-place ещё раз
|
||||||
|
```
|
||||||
|
Картинка: кривая центрируется (−1), проходит нелинейность (bigkernel,
|
||||||
|
вероятно exp/abs — тела за IAT), модулируется track и warp.
|
||||||
|
Против log-входа design'а значение после этих шагов должно быть >0.
|
||||||
|
|
||||||
|
### Bigkernel-тела за ВЛОЖЕННЫМ диспатчем
|
||||||
|
Таблицы стабов содержат смесь IAT-слотов и внутренних адресов (напр.
|
||||||
|
table[7]=140a00), но внутренние ведут к call runtime-helper + НОВЫЙ
|
||||||
|
ILT-стаб с собственной idx-ячейкой (паковка/протектор). Статическое
|
||||||
|
разворачивание обрывается. Тела bigkernel'ов (exp/abs-нелинейность шага
|
||||||
|
14/17) остаются за пакером — при необходимости снимаются дампом памяти
|
||||||
|
ВОКРУГ вызова в рантайме (STOP + чтение таблиц после инициализации).
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24jj: ТЕЛА BIGKERNEL'ОВ НАЙДЕНЫ (рантайм-резолв IAT)
|
||||||
|
|
||||||
|
iat_name.py v2 (SIGSTOP + двойной deref + PE-экспорты) резолвит:
|
||||||
|
```
|
||||||
|
стаб 140b30/140b60 → runtime 1803a06a0 (общий для float/double!)
|
||||||
|
стаб 1409e0 → runtime 180296c80
|
||||||
|
стаб 140ad0 → runtime 180323f20
|
||||||
|
стаб 140a40 → runtime 1802dc0e0
|
||||||
|
```
|
||||||
|
Все — НАСТОЯЩИЕ функции внутри дампа (не импорты!): большие стек-фреймы,
|
||||||
|
x87 FNU-контроль, AVX2 полиномы, ДВЕ x87-трансцендентные инструкции
|
||||||
|
(fyl2x/f2xm1 класс = 2^x/exp семейство). Полный декод математики каждого —
|
||||||
|
отдельная сессия; вход/выход уже известны из контекста вызовов
|
||||||
|
(in-place над n/2+1 элементами FIR-буфера).
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24jj2: R@540788 — БАНДПАС-ФОРМА, НЕ совпадающая с нашим twin!
|
||||||
|
|
||||||
|
Сравнение (multi6, band1=500/q0.5/s12):
|
||||||
|
```
|
||||||
|
бин R@540788 наш_res R/наш
|
||||||
|
43 0.802 0.117 6.84
|
||||||
|
85 1.086 0.374 2.91
|
||||||
|
171 1.517 0.839 1.81
|
||||||
|
342 3.521 1.401 2.51 ← ПИК ~3.5-4 кГц!
|
||||||
|
512 2.910 1.673 1.74
|
||||||
|
684 1.829 1.811 1.01
|
||||||
|
1024 1.146 1.931 0.59
|
||||||
|
1536 0.689 1.988 0.35
|
||||||
|
```
|
||||||
|
Наш twin res растёт монотонно от fc; R@788 — БАНДПАС с пиком ~bin300-342
|
||||||
|
(~3.5-4 кГц) и СПАДОМ к Найквисту. Форма напоминает кривую равной
|
||||||
|
громкости / слухового взвешивания!
|
||||||
|
R@5407f8 = единичная нормировка (все 1.0000 в этом прогоне).
|
||||||
|
|
||||||
|
### ГИПОТЕЗА (проверяемая):
|
||||||
|
Детекторный уровень = am · ВЕС(f) / res(f), где ВЕС — кривая типа
|
||||||
|
равной громкости (R@788?). Тест: X=am·R/res против катов multi6 —
|
||||||
|
НЕ сошлось лобово (X@43=1.44 макс при мин кате) ⇒ взвешивание входит
|
||||||
|
иначе (до/после res-деления, или в log-домене).
|
||||||
|
### Ценность
|
||||||
|
Объясняет ВСЕ аномалии дальних бинов разом: наши дальние res слишком
|
||||||
|
велики (нет спада), их lvl занижен, каты недобираются. Формула ВЕСА —
|
||||||
|
ключ к кросс-конфиг параметризации.
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ 24kk3: слот [ctx+0x540668] ПОЛИМОРФНЫЙ!
|
||||||
|
|
||||||
|
Прямой проб во время рендера: [ctx+0x540668] содержит
|
||||||
|
`3fdbcd8940000000` = ДВА FLOAT (~0.434, 2.0), НЕ указатель!
|
||||||
|
(пробник probe_668.py; EIO на части чтений — слот мигрирует).
|
||||||
|
|
||||||
|
### Следствия:
|
||||||
|
1. rendersnap2 пропускает слот 668 (ptr<0x10000 или мусорный ptr) ⇒
|
||||||
|
во ВСЕХ новых захватах (sc_multi4b, sc_tt*, sc_q*, ...) НЕТ FIR-массивов.
|
||||||
|
2. Старые захваты rendersnap.py v1 ИМЕЛИ валидный FIR-указатель в эти
|
||||||
|
моменты (ловили фазу обработки). Данные старых phase*.npz про FIR —
|
||||||
|
валидны для своих моментов, но смешивать с новыми нельзя.
|
||||||
|
3. Все «FIR mag» анализы через этот слот зависят от ТОГО, в какой фазе
|
||||||
|
слот был пойман: указатель-на-буфер vs скаляры vs сброс.
|
||||||
|
4. Значения скаляров (~0.43, 2.0) — кандидаты: att/rel? dry/wet? g-компоненты?
|
||||||
|
### Статус
|
||||||
|
Канонический путь чтения ПРИМЕНЁННОЙ маски: слот [ctx+0x540678] (кривая
|
||||||
|
банды) — он стабилен и МАТЧИТ АУДИО в 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» даёт пустой файл!).
|
||||||
|
|||||||
+66
-50
@@ -1,58 +1,74 @@
|
|||||||
# PROMPT FOR NEXT SESSION (2026-08-24, после 24d)
|
# PROMPT FOR NEXT SESSION (2026-08-25, после 24kk2)
|
||||||
|
|
||||||
Продолжаем bit-exact реверс soothe2 в /home/m/re-tools (ветка main, HEAD 135f0e9).
|
Продолжаем bit-exact реверс soothe2 в /home/m/re-tools (ветка main).
|
||||||
|
ГЕЙТ СМЕНЫ КАНОНА = BIT EXACT (решение пользователя): все параметры
|
||||||
|
прослежены до декомпа + корпус в шумовом пол. До тех пор канон не трогаем.
|
||||||
|
|
||||||
ПРОЧИТАТЬ ПЕРВЫМ: AGENTS.md → handoff/NOTES_LEVEL.md обновления 23d–24d
|
ПРОЧИТАТЬ ПЕРВЫМ: AGENTS.md (фаза-заголовок 24kk2 + env-флаги + инструменты)
|
||||||
(прорыв и смена парадигмы) → BLOCKMAP_529fe0.md + приложение 23b.
|
→ handoff/NOTES_LEVEL.md обновления 24j–24kk2 → BLOCKMAP_529fe0.md
|
||||||
Не опираться на roadmap-разделы про Шаги 7/9 и на вывод 23k «build-once» —
|
(дополнения 23b/24l/24hh/24ii).
|
||||||
он ОТЗЫВАН в 24c.
|
|
||||||
|
|
||||||
СОСТОЯНИЕ КАНОНА: не тронут — голая цепь 48k/4096 + RT_LAWAFFINE=7.4,1.85,
|
## СОСТОЯНИЕ
|
||||||
TOTAL 1.931 (bridge-гейт 1.594 не пройден).
|
|
||||||
Отвергнуто и НЕ возвращаться: скалярные законы A/S, двухфакторные quad/resrp,
|
|
||||||
точечные lvl=f(am,res^α) [22x теорема], спрединг IDFT→окно→DFT как
|
|
||||||
постобработка [22z], плоский IIR [22u], симметричная log-свёртка [23c].
|
|
||||||
|
|
||||||
ГЛАВНЫЙ ПРОРЫВ СЕРИИ (23d–23e, живые снапы rendersnap.py):
|
ПРИМЕНЕНИЕ ДЕКОДИРОВАНО ДО ФОРМУЛ:
|
||||||
bands[] = сырой спектр кадра → log(DESIGN 0x1802a24c0) → ops → scratch@540628
|
```
|
||||||
→ FIR@540668 = exp(0.984·scratch) поточечно; окно = падающая половина
|
mask(b) = 10^(−cut_D(b)/20) вещественная, per-bin multiply кадра
|
||||||
периодического Hann(4096) на [n/2..n); применение = свёртка ×1.805 (EMPIRICAL,
|
cut_D(b) = α·ln1p(lvl_raw(b)/β)+c [+Δ вторые пики]
|
||||||
константа по всем 4 точкам ±0.5%). Центр не зависит от q живьём;
|
lvl_raw = am/res·scale наш фронтенд, float-parity ✓
|
||||||
Δскайрт сходится в 3%. q-серия снята (NOTES 23f).
|
слой = STFT БЕЗ синтез-окна RT_SYN=1
|
||||||
|
```
|
||||||
|
Калибровки (rms ≤0.016 дБ):
|
||||||
|
```
|
||||||
|
dual fc500 q0.5 s12 ДВА тона : α=3.2193 β=0.4927 c=+0.54
|
||||||
|
fc1000 q0.5 s12 ОДИН тон : α=1.6151 β=0.3645 c=+0.48
|
||||||
|
fc500 q0.5 s12 ОДИН тон : α=1.1530 β=0.4038 c=+0.33
|
||||||
|
```
|
||||||
|
Корпус: dual **0.193 max 0.438** с флагами `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1
|
||||||
|
RT_NOIIR3=1 RT_IIR12=0`. Канон TOTAL 2.286 нетронут.
|
||||||
|
|
||||||
ПРИОРИТЕТ №1 — потребитель кернела и источник ×1.805:
|
ОТЗЫВАНО И НЕ ВОЗВРАЩАТЬСЯ: ×1.805-свёртка [23e], OLA-нормировка [24i],
|
||||||
Conv-движок опознан: объект ctx+0x540530 (inline-cfg {2,4096},{16384,8192},
|
двухстадийный γ₀ как множитель закона [24m — это артефакт двух тонов],
|
||||||
{2,257}; векторы +0xb8..+0x130; методы dc30 cfg / fe00 resize / dd30 dtor —
|
клампы параметров [24dd], B∝am [24bb], axpy-семантика th2000 [24ii — это
|
||||||
прямые вызовы из init 52e9b0). Его PROCESS-метод не найден: все 390 disp32-
|
array-multiply], «acc_i += bands[i]» шага 12 [24gg — это COPY].
|
||||||
ссылок на таблицу слотов учтены, скрытых нет (23h).
|
|
||||||
Пути: (а) отладить scan2.py (лежит в /tmp/opencode, НЕ закоммичен; баг:
|
|
||||||
тот же алгоритм что rendersnap.py даёт пусто — сравнить пошагово);
|
|
||||||
(б) декод аудио-клея 52exx–52fxx + поиск caller'ов vtbl+6 у соседнего
|
|
||||||
экземпляра (гипотеза GUI/DSP-пары, 24c);
|
|
||||||
(в) INT3-ловушка на тела дизайна в param-окно (~1.40–1.45 c жизни хоста)
|
|
||||||
при ГАРАНТИРОВАННО свежем рендере (render_fresh в логе обязателен!).
|
|
||||||
|
|
||||||
ИНСТРУМЕНТЫ: scripts/rendersnap.py (рабочий канал наблюдения — STOP-семпл
|
## ЗАДАЧА №1: каскадный симулятор шагов 9–19 (оп-за-опом)
|
||||||
офлайн-рендера), qseries.py, amseries.py (нужен фикс стабильности фаз),
|
|
||||||
ilt_resolve.py, fnexec.py (early-arm HW exec-bp), perfbp.py (только native),
|
|
||||||
fnwatch4.py (дисциплина армирования: INTERRUPT→wait→arm→CONT каждому треду).
|
|
||||||
Сборка: touch dsp/framed_model.cpp && cmake --build dsp/build --target render48k
|
|
||||||
framed_test (TOOLING HAZARD — touch обязателен). Метрика только честная:
|
|
||||||
render_parity.load (24-bit!), Гёрцель по последним 0.75 c.
|
|
||||||
|
|
||||||
СРЕДА (грабли, всё проверено болью):
|
Dataflow декодирован (BLOCKMAP 24hh/24ii):
|
||||||
- HW-ловушки внутри wine НЕВОЗМОЖНЫ: wine держит BP-слоты (perf ENOSPC),
|
```
|
||||||
ptrace-DR пишется в task_struct но не программируется в кремний [24a];
|
vec698 *= (xmm12−[54087c]) ; обнуление при дефолтах
|
||||||
- yabridge: сотни мёртвых сокет-директорий /run/user/1000/yabridge-* ломают
|
vec6f8 += 0.8 ; базовая линия
|
||||||
спавн хоста — rm -rf перед серией; pkill -f самоубийственно для шелла;
|
bigkernel(vec6f8, bands_i, vec6f8)
|
||||||
- kill reaper во время рендера оставляет ЧАСТИЧНЫЙ wav с полной шапкой
|
vec6f8 = bands_curve − ACC_i ; COMBINE (dc40: out=rdx−r10)
|
||||||
(источник «флака»); realtime-playback мёртв (нет аудио-девайса);
|
f6f8_верх += ATT[k]·ACC[k]; низ += REL[k]·ACC[k] ; тройки re/im/coef
|
||||||
- выход рендера длиннее входа из-за LOOP=1 (выравнивание сегментов обязательно).
|
COPY(...) ; шаг 12 = memcpy!
|
||||||
|
bands += (−1.0); bigkernel in-place ; центрирование+нелинейность
|
||||||
|
bands *= track_i; bands *= kWarp ; th2000 = array-multiply
|
||||||
|
IIR4 ×2; финальные scale/op
|
||||||
|
→ bands[i] → design log → scratch → FIR → audio multiply exp(scratch)
|
||||||
|
```
|
||||||
|
Тела bigkernel'ов по рантайм-адресам: 1803a06a0/180296c80/180323f20/1802dc0e0
|
||||||
|
(x87 exp-семейство). Коэффициенты IIR-генератора 533340 исправлены
|
||||||
|
(насыщающая форма arg=|c|·g/n; p=[54087c]=1, mult=360, C=1000).
|
||||||
|
|
||||||
КОНТРОЛЬНЫЕ ЧИСЛА dual_b1q_0.5: cut@500=10.32 дБ (константен ∀q),
|
Метод проверки: собрать симулятор в Python (numpy), прогнать lvl_raw из
|
||||||
cut@2000=11.82; Δcut скайрта ≈ линейно по −ln res (b≈2.25 дБ/e-fold).
|
tract_* через каскад, сравнить с deepest-scratch кривыми sc_* — rms < 0.05 дБ
|
||||||
Цель раунда: опознать потребителя кернела → декодировать ×1.805 → начать
|
= замкнулось. Затем перенос в C++.
|
||||||
RT_FIRCONV=1 (обработка покадровая — 24c; кернел перестраивается каждый кадр,
|
|
||||||
ловушки на цепь молчат из-за wine-слотов, см. 24a). Корпус с гейтом
|
## ЗАДАЧА №2: параметризация α(контент)/fc
|
||||||
--vs-bridge после любого изменения C++. Коммитить по подшагам; факты в
|
|
||||||
NOTES_LEVEL (следующий номер UPDATE 24e+).
|
α удваивается с числом тонов (частотное смешение шаблонно-локальное —
|
||||||
|
далёкий тон не влияет, 24ll). Инструмент: scripts/campaign.py (ячейка ≈8 мин).
|
||||||
|
Двухтональная дистанционная серия уже снята (sc_d*, инверсия не сошлась —
|
||||||
|
нужен каскад из Задачи №1 сначала!).
|
||||||
|
|
||||||
|
## СРЕДА (грабли, всё проверено болью)
|
||||||
|
- rendersnap2.py: RENDER_FILE брать ИЗ rpp (клоны наследуют путь — однажды
|
||||||
|
перезаписали реф); каталоги снапов задавать уникальные (argv[3]).
|
||||||
|
- touch+mtime перед каждым замером (same-second cmake hazard).
|
||||||
|
- tone1k.wav громче dual.wav в 2.28× — при сравнении серий учитывать.
|
||||||
|
- Свип НЕ годится для Y/X-отношения; только мультитон/мультиуровень.
|
||||||
|
- Динамика V(t)↔g(t) на рампе не сходится — пары только из стационара.
|
||||||
|
- HW-ловушки под wine невозможны (wine держит слоты); INT3 требует
|
||||||
|
дисциплины fnwatch4 и ГАРАНТИРОВАННО свежего рендера.
|
||||||
|
- Метрика только честная: render_parity.load / Гёрцель последних 0.75 c.
|
||||||
|
- Коммитить подшагами; факты → NOTES_LEVEL (очередной номер 24ll+).
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,11 @@
|
|||||||
> одновременно (res-тенция) → канон не меняем; следующий рычаг — контент-зависимость
|
> одновременно (res-тенция) → канон не меняем; следующий рычаг — контент-зависимость
|
||||||
> (att/rel на флюктуациях, combine-консюмер). Инструментарий RT_DUMP_BIN/RT_DUMP_ALL.
|
> (att/rel на флюктуациях, combine-консюмер). Инструментарий RT_DUMP_BIN/RT_DUMP_ALL.
|
||||||
|
|
||||||
|
> **2026-08-25 (24j–24kk2): ПРИМЕНЕНИЕ ДЕКОДИРОВАНО ДО ФОРМУЛ.**
|
||||||
|
> mask=10^(−cut_D/20); cut_D=α·ln1p(lvl/β)+c (пер-контент константы, rms≤0.016);
|
||||||
|
> слой STFT без синтез-окна; dual-корпус 0.193 (канон 3.264). ГЕЙТ = BIT EXACT.
|
||||||
|
> Детали: NOTES_LEVEL 24j–24kk2, BLOCKMAP_529fe0 23b–24jj, NEXT_PROMPT.md.
|
||||||
|
|
||||||
- **Статический декомп DSP-ядра — закрыт (~95%)**: twin-резонатор, генератор case8,
|
- **Статический декомп DSP-ядра — закрыт (~95%)**: twin-резонатор, генератор case8,
|
||||||
level-path (0x529fe0/0x563440/0x563a60), mask-apply (FUN_180529fe0 mono-path), IIR-трекеры,
|
level-path (0x529fe0/0x563440/0x563a60), mask-apply (FUN_180529fe0 mono-path), IIR-трекеры,
|
||||||
FFT-conv (0x535a70), main render-loop (FUN_18052e260) декодированы; `.dis` в `handoff/nls_dasm/`.
|
FFT-conv (0x535a70), main render-loop (FUN_18052e260) декодированы; `.dis` в `handoff/nls_dasm/`.
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""campaign.py — one config-family cell: build rpp variants, clean-render refs,
|
||||||
|
capture deepest scratch, extract tracts, fit softplus law.
|
||||||
|
Usage: campaign.py <base_rpp> <band_freq> <band_q> <sens> <in_prefix> <n_drives> <out_prefix>
|
||||||
|
Inputs must exist as <in_prefix><drive>_in.wav for drive in list.
|
||||||
|
"""
|
||||||
|
import subprocess, os, re, sys, time, glob
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
sys.path.insert(0,'/home/m/re-tools/scripts')
|
||||||
|
|
||||||
|
def sh(cmd):
|
||||||
|
return subprocess.run(cmd,shell=True,capture_output=True,text=True).stdout
|
||||||
|
|
||||||
|
def find_host():
|
||||||
|
import glob as g
|
||||||
|
for p in g.glob('/proc/[0-9]*'):
|
||||||
|
pid=int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
cmd=open('/proc/%d/cmdline'%pid,'rb').read().replace(b'\0',b' ').decode('utf8','replace')
|
||||||
|
maps=open('/proc/%d/maps'%pid).read()
|
||||||
|
except Exception: continue
|
||||||
|
if 'soothe2' in maps and 'reaper' not in cmd: return pid
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
base,freq,q,sens,pref,drives,outp=sys.argv[1],sys.argv[2],sys.argv[3],sys.argv[4],sys.argv[5],sys.argv[6].split(','),sys.argv[7]
|
||||||
|
# build rpps
|
||||||
|
for a in drives:
|
||||||
|
src=open(base).read()
|
||||||
|
src=src.replace('FILE "/home/m/soothe-bt/tone1k.wav"',f'FILE "{pref}{a}_in.wav"')
|
||||||
|
src=re.sub(r'RENDER_FILE "[^"]*"',f'RENDER_FILE "{outp}/{a}_ref.wav"',src)
|
||||||
|
tmp=f'{outp}/{a}_tmp.rpp'
|
||||||
|
open(tmp,'w').write(src)
|
||||||
|
out=sh(f'cd /home/m/re-tools && python3 patchparam.py {tmp} {outp}/{a}.rpp '
|
||||||
|
f'"band1 freq"={freq} "band1 q"={q}')
|
||||||
|
if 'patched' not in out and 'warning' not in out:
|
||||||
|
print(f'drive {a}: patch issue: {out[:100]}')
|
||||||
|
sh("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1")
|
||||||
|
# render refs cleanly
|
||||||
|
for a in drives:
|
||||||
|
wav=f'{outp}/{a}_ref.wav'
|
||||||
|
if os.path.exists(wav): os.remove(wav)
|
||||||
|
pr=subprocess.Popen(['/usr/bin/reaper','-nosplash','-ignoreerrors','-renderproject',f'{outp}/{a}.rpp'],
|
||||||
|
stdout=subprocess.DEVNULL,stderr=subprocess.STDOUT)
|
||||||
|
t0=time.time()
|
||||||
|
while time.time()-t0<30 and pr.poll() is None:
|
||||||
|
time.sleep(0.2)
|
||||||
|
for _ in range(60):
|
||||||
|
if pr.poll() is not None: break
|
||||||
|
time.sleep(0.1)
|
||||||
|
print(f'drive {a}: ref rc={pr.poll()} size={os.path.getsize(wav) if os.path.exists(wav) else 0}',flush=True)
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#!/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_div(a, b):
|
||||||
|
"""divide-ядро 1803a06a0: dst = B/A (~0.5 ulp, rcp+таблицы+полином).
|
||||||
|
Структурная фаза: точное деление."""
|
||||||
|
return b / a
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ данные -----
|
||||||
|
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.80 # скаляр аргумента EXP; источник в 1803831c0 (ОТКРЫТО)
|
||||||
|
|
||||||
|
|
||||||
|
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,31 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""dis.py — quick capstone disassembler for soothe_mem.bin (base 0x180000000).
|
||||||
|
Usage: dis.py <VA> [len] [va2 len2 ...]
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
|
||||||
|
|
||||||
|
BASE = 0x180000000
|
||||||
|
_data = open('/home/m/re-tools/soothe_mem.bin', 'rb').read()
|
||||||
|
|
||||||
|
def dis(va, n=256):
|
||||||
|
off = va - BASE
|
||||||
|
code = _data[off:off + n]
|
||||||
|
md = Cs(CS_ARCH_X86, CS_MODE_64)
|
||||||
|
md.detail = False
|
||||||
|
out = []
|
||||||
|
for ins in md.disasm(code, va):
|
||||||
|
s = '%08x %-24s %s %s' % (ins.address, ins.bytes.hex(),
|
||||||
|
ins.mnemonic, ins.op_str)
|
||||||
|
out.append(s)
|
||||||
|
if ins.mnemonic == 'ret':
|
||||||
|
break
|
||||||
|
return '\n'.join(out)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
a = sys.argv[1:]
|
||||||
|
for i in range(0, len(a), 2):
|
||||||
|
va = int(a[i], 16)
|
||||||
|
n = int(a[i + 1], 16) if i + 1 < len(a) else 256
|
||||||
|
print('==== %x (len %x) ====' % (va, n))
|
||||||
|
print(dis(va, n))
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""disasm_func.py — full-function disassembler with resolved RIP constants.
|
||||||
|
Usage: disasm_func.py <VA> [max_bytes]
|
||||||
|
Stops on int3-run after a ret. Prints resolved [rip+X] targets inline.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import struct
|
||||||
|
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
|
||||||
|
from capstone.x86 import X86_OP_MEM, X86_REG_RIP
|
||||||
|
|
||||||
|
BASE = 0x180000000
|
||||||
|
_data = open('/home/m/re-tools/soothe_mem.bin', 'rb').read()
|
||||||
|
|
||||||
|
def rd(va, n):
|
||||||
|
return _data[va - BASE: va - BASE + n]
|
||||||
|
|
||||||
|
def main():
|
||||||
|
va = int(sys.argv[1], 16)
|
||||||
|
maxb = int(sys.argv[2], 16) if len(sys.argv) > 2 else 0x4000
|
||||||
|
code = rd(va, maxb)
|
||||||
|
md = Cs(CS_ARCH_X86, CS_MODE_64)
|
||||||
|
md.detail = True
|
||||||
|
out = []
|
||||||
|
run_int3 = 0
|
||||||
|
seen_ret = False
|
||||||
|
for ins in md.disasm(code, va):
|
||||||
|
line = '%08x %-22s %s %s' % (ins.address, ins.bytes.hex(), ins.mnemonic, ins.op_str)
|
||||||
|
note = ''
|
||||||
|
for op in ins.operands:
|
||||||
|
if op.type == X86_OP_MEM and op.mem.base == X86_REG_RIP:
|
||||||
|
tgt = ins.address + ins.size + op.mem.disp
|
||||||
|
note += ' ; ->%x' % tgt
|
||||||
|
b4 = rd(tgt, 8)
|
||||||
|
f32v = struct.unpack('<f', b4[:4])[0]
|
||||||
|
f64v = struct.unpack('<d', b4[:8])[0]
|
||||||
|
u64v = struct.unpack('<Q', b4[:8])[0]
|
||||||
|
if abs(f32v) > 1e-6 and abs(f32v) < 1e8:
|
||||||
|
note += ' f32=%.6g' % f32v
|
||||||
|
elif abs(f64v) > 1e-6 and abs(f64v) < 1e12:
|
||||||
|
note += ' f64=%.6g' % f64v
|
||||||
|
else:
|
||||||
|
note += ' u64=%x' % u64v
|
||||||
|
line += note
|
||||||
|
out.append(line)
|
||||||
|
if ins.mnemonic == 'ret':
|
||||||
|
seen_ret = True
|
||||||
|
run_int3 = 0
|
||||||
|
elif ins.mnemonic == 'int3':
|
||||||
|
if seen_ret:
|
||||||
|
run_int3 += 1
|
||||||
|
if run_int3 >= 4:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
run_int3 = 0
|
||||||
|
print('\n'.join(out))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""dump_dispatch.py — dump dispatch tables of bigkernel stubs at runtime.
|
||||||
|
Stubs of interest (from FIR loop / steps 14,17):
|
||||||
|
1409e0 -> table@182617508 ; 140ad0 -> ? ; 140b30 -> table@1826176c8
|
||||||
|
Nested stub inside 140a00: idx cell/table computed below.
|
||||||
|
Dumps tables pre-render (static) and during render (runtime-patched).
|
||||||
|
"""
|
||||||
|
import struct, subprocess, sys, time
|
||||||
|
import glob, os
|
||||||
|
|
||||||
|
BASE = 0x180000000
|
||||||
|
data = open('/home/m/re-tools/soothe_mem.bin','rb').read()
|
||||||
|
|
||||||
|
def find_host():
|
||||||
|
for p in glob.glob('/proc/[0-9]*'):
|
||||||
|
pid=int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
cmd=open(f'/proc/{pid}/cmdline','rb').read().replace(b'\0',b' ').decode('utf8','replace')
|
||||||
|
maps=open(f'/proc/{pid}/maps').read()
|
||||||
|
except Exception: continue
|
||||||
|
if 'soothe2' in maps and 'reaper' not in cmd: return pid
|
||||||
|
return None
|
||||||
|
|
||||||
|
def rd(a,n):
|
||||||
|
try: return os.pread(fd,n,a)
|
||||||
|
except OSError: return None
|
||||||
|
|
||||||
|
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True)
|
||||||
|
rpp=sys.argv[1] if len(sys.argv)>1 else '/tmp/opencode/multi.rpp'
|
||||||
|
proc=subprocess.Popen(['/usr/bin/reaper','-nosplash','-ignoreerrors','-renderproject',rpp],
|
||||||
|
stdout=subprocess.DEVNULL,stderr=subprocess.STDOUT)
|
||||||
|
t0=time.time(); host=None
|
||||||
|
while time.time()-t0<30 and not host:
|
||||||
|
host=find_host(); time.sleep(0.002)
|
||||||
|
print('host',host,flush=True)
|
||||||
|
if not host: sys.exit(1)
|
||||||
|
fd=os.open(f'/proc/{host}/mem',os.O_RDONLY)
|
||||||
|
|
||||||
|
TABLES={'bk140b30':0x1826176c8,'bk140b60':0x182617708,'bk1409e0':0x182617508,
|
||||||
|
'bk140ad0':None,'bk140a40':0x182617588}
|
||||||
|
# find bk140ad0 table: stub 140ad0 pattern movsxd rax,[rip+X]; lea r10,[rip+Y]
|
||||||
|
off=0x180140ad0-BASE
|
||||||
|
b=data[off:off+16]
|
||||||
|
rel1=struct.unpack('<i',b[3:7])[0]
|
||||||
|
rel2=struct.unpack('<i',b[10:14])[0]
|
||||||
|
idx_a=0x180140ad0+7+rel1
|
||||||
|
tbl_a=0x180140ad0+14+rel2
|
||||||
|
TABLES['bk140ad0']=tbl_a
|
||||||
|
print('bk140ad0 idx@%x tbl@%x' % (idx_a,tbl_a),flush=True)
|
||||||
|
|
||||||
|
def dump_tables(tag):
|
||||||
|
print(tag,'fd=',fd,flush=True)
|
||||||
|
for nm,t in TABLES.items():
|
||||||
|
b=rd(t,64)
|
||||||
|
if b is None:
|
||||||
|
import errno
|
||||||
|
print('%s %s unreadable err=%s'%(tag,nm,os.strerror(errno.EIO))); continue
|
||||||
|
vals=struct.unpack('<%dQ'%(len(b)//8),b[:len(b)//8*8])
|
||||||
|
nz=[(i,hex(v)) for i,v in enumerate(vals) if v]
|
||||||
|
print('%s %-9s: %s' % (tag,nm,nz), flush=True)
|
||||||
|
|
||||||
|
# also nested stub inside 140a00 (static parse):
|
||||||
|
off=0x140a10-BASE
|
||||||
|
b1=data[off:off+7]
|
||||||
|
if b1[:3]==b'\x48\x63\x05':
|
||||||
|
rel=struct.unpack('<i',b1[3:7])[0]
|
||||||
|
icell=0x180140a10+7+rel
|
||||||
|
b2=data[icell-BASE:4]
|
||||||
|
print('nested idx cell @%x static=%d' % (icell, struct.unpack('<i',b2)[0]),flush=True)
|
||||||
|
|
||||||
|
for k in range(30):
|
||||||
|
dump_tables('R%d'%k)
|
||||||
|
time.sleep(0.15)
|
||||||
|
try:
|
||||||
|
os.kill(proc.pid,0)
|
||||||
|
except ProcessLookupError:
|
||||||
|
break
|
||||||
|
os.close(fd)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""finish_f.py — patch band freq=500, render refs, capture scratches, build tracts."""
|
||||||
|
import subprocess, os, re, sys, time, wave, glob
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
sys.path.insert(0,'/home/m/re-tools/scripts')
|
||||||
|
|
||||||
|
def sh(cmd):
|
||||||
|
r=subprocess.run(cmd,shell=True,capture_output=True,text=True)
|
||||||
|
return r.stdout+r.stderr
|
||||||
|
|
||||||
|
# 1. patch band freq=500 into raw clones
|
||||||
|
for a in (0,6,12,24):
|
||||||
|
src=open(f'/tmp/opencode/f{a}_raw.rpp').read()
|
||||||
|
open(f'/tmp/opencode/f{a}.rpp','w').write(src)
|
||||||
|
print('freq left as base1k default (band1 freq=1000?) — CHECK')
|
||||||
|
# base1k was cloned from multi.rpp which had band1 freq=500 already!
|
||||||
|
# t1k_base.rpp got 'band1 freq'=1000 patched; f*_raw cloned from base1k -> 1000.
|
||||||
|
# We need 500: patch each:
|
||||||
|
for a in (0,6,12,24):
|
||||||
|
out=sh(f'cd /home/m/re-tools && python3 patchparam.py /tmp/opencode/f{a}_raw.rpp /tmp/opencode/f{a}.rpp "band1 freq"=500.0')
|
||||||
|
if 'patched 1' not in out: print(f'f{a} patch issue:',out.strip()[:80])
|
||||||
|
print('patched to 500')
|
||||||
|
|
||||||
|
def find_host():
|
||||||
|
for p in glob.glob('/proc/[0-9]*'):
|
||||||
|
pid=int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
cmd=open(f'/proc/{pid}/cmdline','rb').read().replace(b'\0',b' ').decode('utf8','replace')
|
||||||
|
maps=open(f'/proc/{pid}/maps').read()
|
||||||
|
except Exception: continue
|
||||||
|
if 'soothe2' in maps and 'reaper' not in cmd: return pid
|
||||||
|
return None
|
||||||
|
|
||||||
|
sh("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1")
|
||||||
|
|
||||||
|
for a in (0,6,12,24):
|
||||||
|
wav=f'/tmp/opencode/f{a}_ref.wav'
|
||||||
|
if os.path.exists(wav): os.remove(wav)
|
||||||
|
pr=subprocess.Popen(['/usr/bin/reaper','-nosplash','-ignoreerrors','-renderproject',f'/tmp/opencode/f{a}.rpp'],
|
||||||
|
stdout=subprocess.DEVNULL,stderr=subprocess.STDOUT)
|
||||||
|
t0=time.time(); host=None
|
||||||
|
while time.time()-t0<30 and not host: host=find_host(); time.sleep(0.002)
|
||||||
|
# wait for natural finish
|
||||||
|
for _ in range(200):
|
||||||
|
if pr.poll() is not None: break
|
||||||
|
try:
|
||||||
|
if not os.path.exists('/proc/%d'%host) if host else True: break
|
||||||
|
except Exception: break
|
||||||
|
time.sleep(0.05)
|
||||||
|
for _ in range(60):
|
||||||
|
if pr.poll() is not None: break
|
||||||
|
time.sleep(0.1)
|
||||||
|
print(f'f{a} rendered rc={pr.poll()} wav={os.path.getsize(wav) if os.path.exists(wav) else "NONE"}')
|
||||||
|
|
||||||
|
# tracts
|
||||||
|
env=dict(os.environ)
|
||||||
|
for a in (0,6,12,24):
|
||||||
|
tf=f'/tmp/opencode/tract_f{a}.txt'
|
||||||
|
if os.path.exists(tf): os.remove(tf)
|
||||||
|
e=dict(env); e['RT_DUMP_BIN']=tf
|
||||||
|
subprocess.run(['/home/m/re-tools/dsp/build/render48k',f'/tmp/opencode/{a}_in.wav' if False else f'/tmp/opencode/f{a}_in.wav',
|
||||||
|
'/tmp/o48_f.wav','500,0.5,12'],capture_output=True,env=e)
|
||||||
|
print('tracts done')
|
||||||
@@ -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,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""g_s12 pipeline: clean render ref + deepest scratch capture + render48k tract.
|
||||||
|
Then compute implied-res vs our-res for the frontend clamp analysis."""
|
||||||
|
import subprocess, os, sys, time, glob
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def find_host():
|
||||||
|
import glob as g
|
||||||
|
for p in g.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 sh(cmd):
|
||||||
|
return subprocess.run(cmd,shell=True,capture_output=True,text=True).stdout
|
||||||
|
|
||||||
|
sh("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1")
|
||||||
|
|
||||||
|
# 1. clean ref render
|
||||||
|
wav='/tmp/opencode/g_s12_ref.wav'
|
||||||
|
if os.path.exists(wav): os.remove(wav)
|
||||||
|
pr=subprocess.Popen(['/usr/bin/reaper','-nosplash','-ignoreerrors','-renderproject','/tmp/opencode/g_s12.rpp'],
|
||||||
|
stdout=subprocess.DEVNULL,stderr=subprocess.STDOUT)
|
||||||
|
t0=time.time(); host=None
|
||||||
|
while time.time()-t0<30 and not host: host=find_host(); time.sleep(0.002)
|
||||||
|
print('host',host)
|
||||||
|
for _ in range(300):
|
||||||
|
if pr.poll() is not None: break
|
||||||
|
time.sleep(0.1)
|
||||||
|
for _ in range(50):
|
||||||
|
if pr.poll() is not None: break
|
||||||
|
time.sleep(0.1)
|
||||||
|
print('ref done rc=',pr.poll())
|
||||||
|
|
||||||
|
# 2. deepest scratch capture
|
||||||
|
sh("pkill -9 -x reaper; sleep 1; rm -rf /tmp/opencode/sc_g_s12")
|
||||||
|
r=sh('cd /home/m/re-tools && timeout 60 python3 scripts/rendersnap2.py /tmp/opencode/g_s12.rpp 400 /tmp/opencode/sc_g_s12')
|
||||||
|
print('capture tail:',r.strip().splitlines()[-1] if r.strip() else 'empty')
|
||||||
|
|
||||||
|
# 3. render48k tract (same input tone1k, band fc1000 q1 s12)
|
||||||
|
tf='/tmp/opencode/tract_g_s12.txt'
|
||||||
|
if os.path.exists(tf): os.remove(tf)
|
||||||
|
env=dict(os.environ); env['RT_DUMP_BIN']=tf
|
||||||
|
subprocess.run(['/home/m/re-tools/dsp/build/render48k','/home/m/soothe-bt/tone1k.wav',
|
||||||
|
'/tmp/o48_g.wav','1000,1.0,12'],capture_output=True,env=e if False else env)
|
||||||
|
print('tract done')
|
||||||
|
|
||||||
|
# 4. analysis
|
||||||
|
import wave
|
||||||
|
def loadwav(p):
|
||||||
|
w=wave.open(p,'rb'); n=w.getnframes(); ch=w.getnchannels(); sw=w.getsampwidth()
|
||||||
|
d=w.readframes(n); w.close()
|
||||||
|
if sw==2: return np.frombuffer(d,dtype=np.int16).astype(np.float64).reshape(-1,ch).mean(1)/32768
|
||||||
|
raw=np.frombuffer(d,dtype=np.uint8).reshape(-1,ch,3)
|
||||||
|
s=raw[:,:,0].astype(np.int64)|(raw[:,:,1].astype(np.int64)<<8)|(raw[:,:,2].astype(np.int64)<<16)
|
||||||
|
return np.where(s>=0x800000,s-0x1000000,s).astype(np.float64).reshape(-1,ch).mean(1)/8388608
|
||||||
|
def ta(x,f,sr=44100):
|
||||||
|
x=x[-int(0.75*sr):]; t=np.arange(len(x))/sr; w=2*np.pi*f
|
||||||
|
return np.hypot(2*np.sum(x*np.cos(w*t))/len(x), 2*np.sum(x*np.sin(w*t))/len(x))
|
||||||
|
def db(a): return 20*np.log10(max(a,1e-12))
|
||||||
|
|
||||||
|
inp=loadwav('/home/m/soothe-bt/tone1k.wav')
|
||||||
|
ref=loadwav(wav)
|
||||||
|
rcut=-db(ta(ref,1000)/ta(inp,1000))
|
||||||
|
|
||||||
|
best=None
|
||||||
|
for fn in sorted(glob.glob('/tmp/opencode/sc_g_s12/ph*.npz')):
|
||||||
|
d=np.load(fn)
|
||||||
|
if '0x540628' not in d.files: continue
|
||||||
|
s=d['0x540628'].astype(np.float64)
|
||||||
|
if best is None or s[85]<best[0]: best=(s[85],s)
|
||||||
|
cutD=-best[0]*8.685889638 if best else float('nan')
|
||||||
|
|
||||||
|
lv=res85=None
|
||||||
|
for ln in open(tf):
|
||||||
|
if ln.startswith('#'): continue
|
||||||
|
p=ln.split()
|
||||||
|
if int(p[0])==85: lv=float(p[3]); res85=float(p[2])
|
||||||
|
|
||||||
|
alpha,beta,c=3.2193,0.4927,0.5423 # dual constants
|
||||||
|
li=beta*np.expm1((cutD-c)/alpha)
|
||||||
|
print('\n=== fc1000 q1 SENS12 (недостающая точка) ===')
|
||||||
|
print('our lvl@85=%.3f res@85=%.5f' % (lv,res85))
|
||||||
|
print('scratch cut_D=%.2f dB' % cutD)
|
||||||
|
print('law-inverse impl lvl=%.3f -> implied res=%.5f' % (li, 0.7307*3.2309/max(li,1e-9)))
|
||||||
|
print('REF goertzel cut@1000=%.2f dB' % rcut)
|
||||||
|
print('\ncontext: sens18 impl res=0.15244 (плато); ours@s18=0.0284')
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""hunt2.py — enumerate ALL soothe2 module instances during offline render.
|
||||||
|
|
||||||
|
Chunk-correct full-heap scan (rendersnap-style) collecting EVERY ctx candidate
|
||||||
|
(vtable 0x1824AC210 or m48 marker), not just the first. Per candidate: sens,
|
||||||
|
fir43/fir171 (via ctx+0x540668 ptr), scalar bank snapshot. Goal: find the
|
||||||
|
GUI/DSP pair (24c/24j): visible instance holds shallow kernel while audio is
|
||||||
|
processed by another instance with the deep one.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
VT = struct.pack('<Q', 0x1824AC210)
|
||||||
|
M48 = struct.pack('<I', 0x473b8000)
|
||||||
|
|
||||||
|
|
||||||
|
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 main():
|
||||||
|
rpp = sys.argv[1] if len(sys.argv) > 1 else '/tmp/opencode/multi.rpp'
|
||||||
|
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 = find_host()
|
||||||
|
time.sleep(0.001)
|
||||||
|
if not host:
|
||||||
|
print('NO HOST')
|
||||||
|
return 1
|
||||||
|
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
||||||
|
|
||||||
|
def rd(a, n):
|
||||||
|
try:
|
||||||
|
return os.pread(fd, n, a)
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def scan_all():
|
||||||
|
found = {}
|
||||||
|
for line in open(f'/proc/{host}/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:
|
||||||
|
d = rd(a, min(CH + 4096, hi - a))
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
for pat, off in ((VT, 0), (M48, -0x24)):
|
||||||
|
j = d.find(pat)
|
||||||
|
while j >= 0:
|
||||||
|
cand = a + j + off
|
||||||
|
if cand not in found:
|
||||||
|
sb = rd(cand + 0x540870, 4)
|
||||||
|
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||||
|
found[cand] = True
|
||||||
|
j = d.find(pat, j + 1)
|
||||||
|
a += CH
|
||||||
|
return list(found)
|
||||||
|
|
||||||
|
known = {}
|
||||||
|
rounds = 0
|
||||||
|
while time.time() - t0 < 25:
|
||||||
|
rounds += 1
|
||||||
|
try:
|
||||||
|
os.kill(host, signal.SIGSTOP)
|
||||||
|
except ProcessLookupError:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
cands = scan_all()
|
||||||
|
new = [c for c in cands if c not in known]
|
||||||
|
for c in new:
|
||||||
|
known[c] = rounds
|
||||||
|
pb = rd(c + 0x540668, 8)
|
||||||
|
m43 = m171 = -1
|
||||||
|
if pb:
|
||||||
|
p = struct.unpack('<Q', pb)[0]
|
||||||
|
if p > 0x10000:
|
||||||
|
fb = rd(p, 2049 * 8)
|
||||||
|
if fb:
|
||||||
|
arr = np.frombuffer(fb[:2049 * 8], dtype='<f4')
|
||||||
|
mag = np.hypot(arr[0::2], arr[1::2])
|
||||||
|
m43, m171 = float(mag[43]), float(mag[171])
|
||||||
|
sb = rd(c + 0x540888, 4)
|
||||||
|
s888 = struct.unpack('<f', sb)[0] if sb else -1
|
||||||
|
print('NEW ctx %#x @r%d t=%.2f fir43=%.4f fir171=%.4f s888=%.4f'
|
||||||
|
% (c, rounds, time.time() - t0, m43, m171, s888), flush=True)
|
||||||
|
# status of known ones every round
|
||||||
|
for c in known:
|
||||||
|
pb = rd(c + 0x540668, 8)
|
||||||
|
if pb:
|
||||||
|
p = struct.unpack('<Q', pb)[0]
|
||||||
|
if p > 0x10000:
|
||||||
|
fb = rd(p, 2049 * 8)
|
||||||
|
if fb:
|
||||||
|
arr = np.frombuffer(fb[:2049 * 8], dtype='<f4')
|
||||||
|
mag = np.hypot(arr[0::2], arr[1::2])
|
||||||
|
print(' st ctx %#x t=%.2f fir43=%.4f' % (c, time.time() - t0, mag[43]), flush=True)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.kill(host, signal.SIGCONT)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
time.sleep(0.05)
|
||||||
|
print('total instances: %d' % len(known))
|
||||||
|
proc.kill()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""iat_name.py — resolve imported-function names for bigkernel dispatch targets.
|
||||||
|
Reads runtime IAT values, finds owning module, parses PE exports."""
|
||||||
|
import struct, subprocess, sys, time
|
||||||
|
import glob, os
|
||||||
|
|
||||||
|
BASE=0x180000000
|
||||||
|
data=open('/home/m/re-tools/soothe_mem.bin','rb').read()
|
||||||
|
|
||||||
|
def rd(fd,a,n):
|
||||||
|
try: return os.pread(fd,n,a)
|
||||||
|
except OSError: return None
|
||||||
|
|
||||||
|
def find_host():
|
||||||
|
for p in glob.glob('/proc/[0-9]*'):
|
||||||
|
pid=int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
cmd=open('/proc/%d/cmdline'%pid,'rb').read().replace(b'\0',b' ').decode('utf8','replace')
|
||||||
|
maps=open('/proc/%d/maps'%pid).read()
|
||||||
|
except Exception: continue
|
||||||
|
if 'soothe2' in maps and 'reaper' not in cmd: return pid
|
||||||
|
return None
|
||||||
|
|
||||||
|
def iat_slot(stub):
|
||||||
|
# pattern: mov rax,[rip+rel] (48 8b 05 rel32)
|
||||||
|
off=stub-BASE
|
||||||
|
b=data[off:off+7]
|
||||||
|
if b[:2]!=b'\x48\x8b': return None
|
||||||
|
rel=struct.unpack('<i',b[2:6])[0]
|
||||||
|
return stub+6+rel
|
||||||
|
|
||||||
|
def pe_exports(path):
|
||||||
|
"""Parse PE export table -> {name: rva}"""
|
||||||
|
try:
|
||||||
|
f=open(path,'rb').read()
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
if f[:2]!=b'MZ': return {}
|
||||||
|
pe=struct.unpack('<I',f[0x3c:0x40])[0]
|
||||||
|
if f[pe:pe+4]!=b'PE\0\0': return {}
|
||||||
|
nsec=struct.unpack('<H',f[pe+6:pe+8])[0]
|
||||||
|
optsz=struct.unpack('<H',f[pe+20:pe+22])[0]
|
||||||
|
magic=struct.unpack('<H',f[pe+24:pe+26])[0]
|
||||||
|
ddir=pe+24+(0x70 if magic==0x20b else 0x60)+0*8 # data dir[0]=export
|
||||||
|
exp_rva,exp_sz=struct.unpack('<II',f[ddir:ddir+8])
|
||||||
|
if not exp_rva: return {}
|
||||||
|
# sections
|
||||||
|
secs=[]
|
||||||
|
so=pe+24+optsz
|
||||||
|
for i in range(nsec):
|
||||||
|
s=f[so+i*40:so+i*40+40]
|
||||||
|
va,sz=struct.unpack('<II',s[12:20])
|
||||||
|
raw,rsz=struct.unpack('<II',s[20:28])
|
||||||
|
secs.append((va,sz,raw,rsz))
|
||||||
|
def r2o(rva):
|
||||||
|
for va,sz,raw,rsz in secs:
|
||||||
|
if va<=rva<va+max(sz,rsz): return raw+(rva-va)
|
||||||
|
return None
|
||||||
|
eo=r2o(exp_rva)
|
||||||
|
if eo is None: return {}
|
||||||
|
nnames=struct.unpack('<I',f[eo+24:eo+28])[0]
|
||||||
|
nrva=struct.unpack('<I',f[eo+32:eo+36])[0]
|
||||||
|
names_rva=struct.unpack('<I',f[eo+32+4:eo+32+8])[0]
|
||||||
|
funcs_rva=struct.unpack('<I',f[eo+28:eo+32])[0]
|
||||||
|
no=r2o(names_rva); fo=r2o(funcs_rva)
|
||||||
|
out={}
|
||||||
|
if no is None or fo is None: return {}
|
||||||
|
for i in range(nnames):
|
||||||
|
nrva_i=struct.unpack('<I',f[no+i*4:no+i*4+4])[0]
|
||||||
|
noff=r2o(nrva_i)
|
||||||
|
if noff is None: continue
|
||||||
|
end=f.find(b'\0',noff)
|
||||||
|
nm=f[noff:end].decode('ascii','replace')
|
||||||
|
frva=struct.unpack('<I',f[fo+i*4:fo+i*4+4])[0]
|
||||||
|
out[nm]=frva
|
||||||
|
return out
|
||||||
|
|
||||||
|
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','/tmp/opencode/multi.rpp'],
|
||||||
|
stdout=subprocess.DEVNULL,stderr=subprocess.STDOUT)
|
||||||
|
t0=time.time(); host=None
|
||||||
|
while time.time()-t0<30 and not host:
|
||||||
|
host=find_host(); time.sleep(0.002)
|
||||||
|
print('host',host,flush=True)
|
||||||
|
import signal as sg
|
||||||
|
os.kill(host,sg.SIGSTOP)
|
||||||
|
fd=os.open('/proc/%d/mem'%host,os.O_RDONLY)
|
||||||
|
|
||||||
|
# build module map
|
||||||
|
mods=[]
|
||||||
|
for line in open('/proc/%d/maps'%host):
|
||||||
|
parts=line.split()
|
||||||
|
if len(parts)<6 or 'x' not in parts[1]: continue
|
||||||
|
lo,hi=(int(x,16) for x in parts[0].split('-'))
|
||||||
|
mods.append((lo,hi,parts[5]))
|
||||||
|
print('modules:',len(mods))
|
||||||
|
|
||||||
|
def owner(addr):
|
||||||
|
for lo,hi,path in mods:
|
||||||
|
if lo<=addr<hi: return (lo,addr-lo,path)
|
||||||
|
return None
|
||||||
|
|
||||||
|
targets={}
|
||||||
|
TBL={0x180140b30:0x1826176c8,0x180140b60:0x182617708,0x1801409e0:0x182617508,
|
||||||
|
0x180140ad0:0x182617648,0x180140a40:0x182617588}
|
||||||
|
for stub,tbl in TBL.items():
|
||||||
|
v=rd(fd,tbl+32,8)
|
||||||
|
tgt=struct.unpack('<Q',v)[0] if v else 0
|
||||||
|
# second level: tgt code = mov rax,[rip+rel]; jmp rax -> IAT slot
|
||||||
|
print(' L2: tgt=%x' % tgt, flush=True)
|
||||||
|
if 0x180000000 <= tgt < 0x187000000:
|
||||||
|
off2=tgt-BASE
|
||||||
|
b2=data[off2:off2+7] if 0<=off2<len(data)-7 else rd(tgt,7)
|
||||||
|
if b2[:2]==b'\x48\x8b':
|
||||||
|
rel2=struct.unpack('<i',b2[3:7])[0]
|
||||||
|
slot=tgt+7+rel2
|
||||||
|
print(' L2: b2=%s rel2=%x slot=%x' % (b2.hex(),rel2&0xffffffff,slot), flush=True)
|
||||||
|
fv=rd(fd,slot,8)
|
||||||
|
if fv:
|
||||||
|
tgt=struct.unpack('<Q',fv)[0]
|
||||||
|
else:
|
||||||
|
print(' slot read FAIL',slot,flush=True)
|
||||||
|
else:
|
||||||
|
print(' no mov-rax pattern at %x: %s'%(tgt,b2[:3].hex() if b2 else '-'),flush=True)
|
||||||
|
ow=owner(tgt)
|
||||||
|
print('%x idx4->%x runtime=%x owner=%s' % (stub,tbl,tgt,ow[2] if ow else '?'),flush=True)
|
||||||
|
if ow:
|
||||||
|
lo,rva,path=ow
|
||||||
|
exps=pe_exports(path)
|
||||||
|
best=[nm for nm,r in exps.items() if r==rva]
|
||||||
|
print(' export:',best,flush=True)
|
||||||
|
os.close(fd)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""mk_dist.py — two-tone spacing series: 500Hz + (500+d)Hz, equal amps."""
|
||||||
|
import wave, numpy as np
|
||||||
|
sr=44100; T=4.0; n=int(sr*T); t=np.arange(n)/sr
|
||||||
|
ph=np.random.default_rng(31).uniform(0,2*np.pi,4)
|
||||||
|
for d_hz in (200,400,750,1500):
|
||||||
|
nm='d%d'%d_hz
|
||||||
|
A=0.4248
|
||||||
|
y=A*np.sin(2*np.pi*500*t+ph[0])+A*np.sin(2*np.pi*(500+d_hz)*t+ph[1])
|
||||||
|
y=np.clip(y,-0.999,0.999)
|
||||||
|
w=wave.open('/tmp/opencode/%s_in.wav'%nm,'wb'); w.setnchannels(1); w.setsampwidth(2); w.setframerate(sr)
|
||||||
|
w.writeframes((y*32767).astype(np.int16).tobytes()); w.close()
|
||||||
|
import re
|
||||||
|
src=open('/tmp/opencode/f0.rpp').read()
|
||||||
|
src=re.sub(r'RENDER_FILE "[^"]*"','RENDER_FILE "/tmp/opencode/%s_ref.wav"'%nm,src)
|
||||||
|
src=src.replace('FILE "/tmp/opencode/f0_in.wav"','FILE "/tmp/opencode/%s_in.wav"'%nm)
|
||||||
|
open('/tmp/opencode/%s.rpp'%nm,'w').write(src)
|
||||||
|
print('distance series ready')
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import wave, numpy as np
|
||||||
|
sr=44100; T=4.0; n=int(sr*T); t=np.arange(n)/sr
|
||||||
|
ph=np.random.default_rng(21).uniform(0,2*np.pi,7)
|
||||||
|
for att,nm,f2 in ((0,'far0',4000.),(12,'far12',4000.)):
|
||||||
|
A=0.4248*10**(-att/20)
|
||||||
|
y=A*np.sin(2*np.pi*500*t+ph[0])+A*np.sin(2*np.pi*f2*t+ph[1])
|
||||||
|
for i,f in enumerate((300.,700.,1400.,2800.)):
|
||||||
|
y+=A*10**(-30/20)*np.sin(2*np.pi*f*t+ph[i+2])
|
||||||
|
y=np.clip(y,-0.999,0.999)
|
||||||
|
w=wave.open('/tmp/opencode/%s_in.wav'%nm,'wb'); w.setnchannels(1); w.setsampwidth(2); w.setframerate(sr)
|
||||||
|
w.writeframes((y*32767).astype(np.int16).tobytes()); w.close()
|
||||||
|
import re
|
||||||
|
src=open('/tmp/opencode/f0.rpp').read()
|
||||||
|
src=re.sub(r'RENDER_FILE "[^"]*"','RENDER_FILE "/tmp/opencode/%s_ref.wav"'%nm,src)
|
||||||
|
src=src.replace('FILE "/tmp/opencode/f0_in.wav"','FILE "/tmp/opencode/%s_in.wav"'%nm)
|
||||||
|
open('/tmp/opencode/%s.rpp'%nm,'w').write(src)
|
||||||
|
print('far-pair inputs+rpps ready')
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""mk_multi6.py — 6-tone multitone input + rpp clone."""
|
||||||
|
import wave, numpy as np, re
|
||||||
|
sr=44100; T=4.0; n=int(sr*T); t=np.arange(n)/sr
|
||||||
|
tones=(200.,500.,1000.,2000.,4000.,8000.)
|
||||||
|
ph=np.random.default_rng(51).uniform(0,2*np.pi,len(tones))
|
||||||
|
A=0.30 # per-tone amplitude (avoid clip: sum ~0.9 worst case)
|
||||||
|
y=sum(A*np.sin(2*np.pi*f*t+p) for f,p in zip(tones,ph))
|
||||||
|
y=np.clip(y,-0.999,0.999)
|
||||||
|
w=wave.open('/tmp/opencode/multi6_in.wav','wb'); w.setnchannels(1); w.setsampwidth(2); w.setframerate(sr)
|
||||||
|
w.writeframes((y*32767).astype(np.int16).tobytes()); w.close()
|
||||||
|
src=open('/tmp/opencode/t1k_base.rpp').read() # band1 freq will be patched to 500
|
||||||
|
src=re.sub(r'RENDER_FILE "[^"]*"','RENDER_FILE "/tmp/opencode/multi6_ref.wav"',src)
|
||||||
|
src=src.replace('FILE "/home/m/soothe-bt/tone1k.wav"','FILE "/tmp/opencode/multi6_in.wav"')
|
||||||
|
open('/tmp/opencode/multi6.rpp','w').write(src)
|
||||||
|
print('multi6 ready: tones',tones,'amp',A)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import struct, subprocess, time, os, glob
|
||||||
|
def find_host():
|
||||||
|
for p in glob.glob('/proc/[0-9]*'):
|
||||||
|
pid=int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
cmd=open('/proc/%d/cmdline'%pid,'rb').read().replace(b'\0',b' ').decode('utf8','replace')
|
||||||
|
maps=open('/proc/%d/maps'%pid).read()
|
||||||
|
except Exception: continue
|
||||||
|
if 'soothe2' in maps and 'reaper' not in cmd: return pid
|
||||||
|
return None
|
||||||
|
subprocess.run("pkill -9 -x reaper; sleep 1; rm -rf /run/user/1000/yabridge-soothe2_x64-*",shell=True)
|
||||||
|
proc=subprocess.Popen(['/usr/bin/reaper','-nosplash','-ignoreerrors','-renderproject','/tmp/opencode/multi.rpp'],
|
||||||
|
stdout=subprocess.DEVNULL,stderr=subprocess.STDOUT)
|
||||||
|
t0=time.time();host=None
|
||||||
|
while time.time()-t0<30 and not host: host=find_host();time.sleep(0.002)
|
||||||
|
print('host',host,flush=True)
|
||||||
|
fd=os.open('/proc/%d/mem'%host,os.O_RDONLY)
|
||||||
|
ctx=0x2370040
|
||||||
|
for k in range(40):
|
||||||
|
try:
|
||||||
|
b=os.pread(fd,8,ctx+0x540668)
|
||||||
|
v=struct.unpack('<Q',b)[0]
|
||||||
|
print('t=%.1f [ctx+540668]=%x' % (time.time()-t0,v), flush=True)
|
||||||
|
except OSError as e:
|
||||||
|
print('read fail',e)
|
||||||
|
time.sleep(0.3)
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""probe_lut.py — one-shot probe of [ctx+0x180] LUT params during render."""
|
||||||
|
import struct, subprocess, sys, time
|
||||||
|
import glob, os
|
||||||
|
|
||||||
|
def find_host():
|
||||||
|
for p in glob.glob('/proc/[0-9]*'):
|
||||||
|
pid = int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
cmd = open(f'/proc/{pid}/cmdline','rb').read().replace(b'\0',b' ').decode('utf8','replace')
|
||||||
|
maps = open(f'/proc/{pid}/maps').read()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if 'soothe2' in maps and 'reaper' not in cmd:
|
||||||
|
return pid
|
||||||
|
return None
|
||||||
|
|
||||||
|
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True)
|
||||||
|
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
|
||||||
|
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=find_host(); time.sleep(0.001)
|
||||||
|
print('host',host, flush=True)
|
||||||
|
if not host:
|
||||||
|
sys.exit(1)
|
||||||
|
fd=os.open(f'/proc/{host}/mem',os.O_RDONLY)
|
||||||
|
def rd(a,n):
|
||||||
|
try: return os.pread(fd,n,a)
|
||||||
|
except OSError: return None
|
||||||
|
ctx=0x2370040
|
||||||
|
best=None
|
||||||
|
while time.time()-t0 < 12:
|
||||||
|
b=rd(ctx+0x180,8)
|
||||||
|
v=struct.unpack('<Q',b)[0] if b else 0
|
||||||
|
if 0x10000 < v < 0x7ffff0000000:
|
||||||
|
s=rd(v,0x18)
|
||||||
|
if s:
|
||||||
|
A,B=struct.unpack('<ff',s[0:8]); G=struct.unpack('<f',s[12:16])[0]
|
||||||
|
m=s[16]
|
||||||
|
q90=rd(v+0x90,8)
|
||||||
|
p90=struct.unpack('<Q',q90)[0] if q90 else 0
|
||||||
|
rec=(A,B,G,m,p90)
|
||||||
|
if best is None or rec!=best:
|
||||||
|
best=rec
|
||||||
|
print('t=%.2f [ctx+180]=%x A=%.6g B=%.6g G=%.6g mode=%d vt90=%x' %
|
||||||
|
(time.time()-t0,v,A,B,G,m,p90), flush=True)
|
||||||
|
else:
|
||||||
|
print('t=%.2f [ctx+180]=%x (not ptr)' % (time.time()-t0,v), flush=True)
|
||||||
|
time.sleep(0.05)
|
||||||
|
os.close(fd)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import struct, subprocess, time, os, glob
|
||||||
|
def find_host():
|
||||||
|
for p in glob.glob('/proc/[0-9]*'):
|
||||||
|
pid=int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
cmd=open('/proc/%d/cmdline'%pid,'rb').read().replace(b'\0',b' ').decode('utf8','replace')
|
||||||
|
maps=open('/proc/%d/maps'%pid).read()
|
||||||
|
except Exception: continue
|
||||||
|
if 'soothe2' in maps and 'reaper' not in cmd: return pid
|
||||||
|
return None
|
||||||
|
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; sleep 1; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*", shell=True)
|
||||||
|
proc=subprocess.Popen(['/usr/bin/reaper','-nosplash','-ignoreerrors','-renderproject','/tmp/opencode/multi.rpp'],
|
||||||
|
stdout=subprocess.DEVNULL,stderr=subprocess.STDOUT)
|
||||||
|
t0=time.time();host=None
|
||||||
|
while time.time()-t0<30 and not host: host=find_host();time.sleep(0.002)
|
||||||
|
print('host',host)
|
||||||
|
fd=os.open('/proc/%d/mem'%host,os.O_RDONLY)
|
||||||
|
for addr,nm in ((0x1824ac210,'vtbl'),(0x182617508,'bk-table'),(0x180140ad0,'stub-code'),(0x180533340,'iir-gen')):
|
||||||
|
try:
|
||||||
|
b=os.pread(fd,16,addr)
|
||||||
|
print('%-10s %x OK: %s' % (nm,addr,b[:8].hex()))
|
||||||
|
except OSError as e:
|
||||||
|
print('%-10s %x FAIL: %s' % (nm,addr,e))
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""probe_states.py — read IIR state headers ([base+8]) for the three
|
||||||
|
FUN_180533340-generated states during render."""
|
||||||
|
import struct, subprocess, sys, time
|
||||||
|
import glob, os
|
||||||
|
|
||||||
|
def find_host():
|
||||||
|
for p in glob.glob('/proc/[0-9]*'):
|
||||||
|
pid=int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
cmd=open(f'/proc/{pid}/cmdline','rb').read().replace(b'\0',b' ').decode('utf8','replace')
|
||||||
|
maps=open(f'/proc/{pid}/maps').read()
|
||||||
|
except Exception: continue
|
||||||
|
if 'soothe2' in maps and 'reaper' not in cmd: return pid
|
||||||
|
return None
|
||||||
|
|
||||||
|
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True)
|
||||||
|
rpp=sys.argv[1] if len(sys.argv)>1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
|
||||||
|
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=find_host(); time.sleep(0.002)
|
||||||
|
print('host',host,flush=True)
|
||||||
|
fd=os.open(f'/proc/{host}/mem',os.O_RDONLY)
|
||||||
|
def rd(a,n):
|
||||||
|
try: return os.pread(fd,n,a)
|
||||||
|
except OSError: return None
|
||||||
|
ctx=0x2370040
|
||||||
|
STATES=[('st1',0x2404e8),('st2',0x340500),('st3',0x440518)]
|
||||||
|
seen=set()
|
||||||
|
while time.time()-t0<10:
|
||||||
|
vals=[]
|
||||||
|
for nm,base in STATES:
|
||||||
|
b=rd(ctx+base+8,4)
|
||||||
|
f=struct.unpack('<f',b)[0] if b else float('nan')
|
||||||
|
n=struct.unpack('<i',rd(ctx+base,4) or b'\xff\xff\xff\xff')[0]
|
||||||
|
vals.append((nm,n,f))
|
||||||
|
key=tuple(round(v[2],4) for v in vals)
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
print(' '.join('%s:n=%d val=%.6g'%v for v in vals), flush=True)
|
||||||
|
time.sleep(0.05)
|
||||||
|
os.close(fd)
|
||||||
@@ -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")
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""rendersnap2.py — non-destructive variant of rendersnap.py.
|
||||||
|
|
||||||
|
Differences (24j):
|
||||||
|
- does NOT kill the host: lets -renderproject finish naturally so the output
|
||||||
|
wav is complete (partial-wav hazard, NOTES 24c);
|
||||||
|
- additionally dumps the ctx SCALAR bank 0x540860..0x5408c0 every phase
|
||||||
|
(goal: wet/FIR scalar [ctx+0x540888], BLOCKMAP 23b line 52b832);
|
||||||
|
- keeps sampling until host exit or phase cap, dedupe by FIR+scalar md5.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
SLOTS = [0x540668, 0x540548, 0x540550, 0x540598, 0x540628, 0x5406f8,
|
||||||
|
0x540678, 0x540688, 0x5406c8, 0x5406e8, 0x540768,
|
||||||
|
0x540788, 0x5407f8,
|
||||||
|
# 24mm9: ACC-таблица указателей (шаг 10 combine) и WINfreq
|
||||||
|
# (окно FIR-цепи; падающий Hann — контроль формы)
|
||||||
|
0x5407c8, 0x540658]
|
||||||
|
NARR = 8194
|
||||||
|
SCAL_OFF = 0x540860
|
||||||
|
SCAL_N = 24 # floats -> 0x540860..0x5408c0
|
||||||
|
OUT = '/tmp/opencode/rendersnap2'
|
||||||
|
|
||||||
|
|
||||||
|
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 main():
|
||||||
|
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
|
||||||
|
cap = int(sys.argv[2]) if len(sys.argv) > 2 else 200
|
||||||
|
global OUT
|
||||||
|
OUT = sys.argv[3] if len(sys.argv) > 3 else OUT
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True)
|
||||||
|
# wav path = RENDER_FILE from the project (NOT rpp.replace!) — deleting the
|
||||||
|
# wrong file destroyed corpus refs once (24k-3 hazard).
|
||||||
|
wav = None
|
||||||
|
for ln in open(rpp, 'r', 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)
|
||||||
|
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 = find_host()
|
||||||
|
time.sleep(0.001)
|
||||||
|
if not host:
|
||||||
|
print('NO HOST')
|
||||||
|
return 1
|
||||||
|
print('host %d at %.3fs' % (host, time.time() - t0), flush=True)
|
||||||
|
|
||||||
|
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
||||||
|
|
||||||
|
def rd(a, n):
|
||||||
|
try:
|
||||||
|
return os.pread(fd, n, a)
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
vt = struct.pack('<Q', 0x1824AC210)
|
||||||
|
m48 = struct.pack('<I', 0x473b8000)
|
||||||
|
|
||||||
|
def scan_ctx():
|
||||||
|
for line in open(f'/proc/{host}/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:
|
||||||
|
d = rd(a, min(CH + 4096, hi - a))
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
j = d.find(vt)
|
||||||
|
while j >= 0:
|
||||||
|
cand = a + j
|
||||||
|
sb = rd(cand + 0x540870, 4)
|
||||||
|
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
|
||||||
|
sb = rd(cand + 0x540870, 4)
|
||||||
|
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||||
|
return cand
|
||||||
|
j = d.find(m48, j + 1)
|
||||||
|
a += CH
|
||||||
|
return None
|
||||||
|
|
||||||
|
ctx = None
|
||||||
|
while ctx is None and time.time() - t0 < 25:
|
||||||
|
try:
|
||||||
|
os.kill(host, signal.SIGSTOP)
|
||||||
|
except ProcessLookupError:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
ctx = scan_ctx()
|
||||||
|
finally:
|
||||||
|
if ctx is None:
|
||||||
|
try:
|
||||||
|
os.kill(host, signal.SIGCONT)
|
||||||
|
except ProcessLookupError:
|
||||||
|
break
|
||||||
|
if ctx is None:
|
||||||
|
time.sleep(0.004)
|
||||||
|
if not ctx:
|
||||||
|
print('NO CTX')
|
||||||
|
return 1
|
||||||
|
print('ctx %#x' % ctx, flush=True)
|
||||||
|
|
||||||
|
rng = np.random.default_rng(11)
|
||||||
|
prev = None
|
||||||
|
saved = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
os.kill(host, signal.SIGSTOP)
|
||||||
|
except ProcessLookupError:
|
||||||
|
print('host exited at %.2fs' % (time.time() - t0), flush=True)
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
pb = rd(ctx + 0x540668, 8)
|
||||||
|
if not pb:
|
||||||
|
continue
|
||||||
|
p = struct.unpack('<Q', pb)[0]
|
||||||
|
fb = rd(p, NARR * 4)
|
||||||
|
if not fb:
|
||||||
|
continue
|
||||||
|
scal = rd(ctx + SCAL_OFF, SCAL_N * 4)
|
||||||
|
if scal is None:
|
||||||
|
continue
|
||||||
|
key = hashlib.md5(fb[:2049 * 8] + scal).digest()
|
||||||
|
if key != prev:
|
||||||
|
prev = key
|
||||||
|
arr = np.frombuffer(fb[:2049 * 8], dtype='<f4')
|
||||||
|
mag = np.hypot(arr[0::2], arr[1::2])
|
||||||
|
sv = np.frombuffer(scal, dtype='<f4')
|
||||||
|
store = {'fir_re': arr[0::2].copy(), 'fir_im': arr[1::2].copy(),
|
||||||
|
'scal': sv.copy()}
|
||||||
|
# LUT params: sub-object pointer at [ctx+0x180] (24z: 563a60 uses
|
||||||
|
# r15=rcx -> [r15+0x180]); A=[sub+0] B=[sub+4] G=[sub+0xc]
|
||||||
|
sb = rd(ctx + 0x180, 8)
|
||||||
|
if sb:
|
||||||
|
sub = struct.unpack('<Q', sb)[0]
|
||||||
|
if sub > 0x10000:
|
||||||
|
fb4 = rd(sub, 0x18)
|
||||||
|
if fb4:
|
||||||
|
store['lut_A'] = struct.unpack('<f', fb4[0:4])[0]
|
||||||
|
store['lut_B'] = struct.unpack('<f', fb4[4:8])[0]
|
||||||
|
store['lut_G'] = struct.unpack('<f', fb4[12:16])[0]
|
||||||
|
store['lut_mode'] = fb4[16]
|
||||||
|
for off in SLOTS[1:]:
|
||||||
|
q = rd(ctx + off, 8)
|
||||||
|
if not q:
|
||||||
|
continue
|
||||||
|
ptr = struct.unpack('<Q', q)[0]
|
||||||
|
if ptr < 0x10000:
|
||||||
|
continue
|
||||||
|
ab = rd(ptr, NARR * 4)
|
||||||
|
if ab:
|
||||||
|
store[hex(off)] = np.frombuffer(ab, dtype='<f4').astype(np.float32)
|
||||||
|
np.savez_compressed(f'{OUT}/ph{saved:03d}.npz', t_snap=time.time() - t0,
|
||||||
|
**store)
|
||||||
|
print('PH%03d t=%.2f fir43=%.4f fir171=%.4f | s888=%.6f s88c=%.6f s874=%.6f s87c=%.6f s870=%.3f' %
|
||||||
|
(saved, time.time() - t0, mag[43], mag[171],
|
||||||
|
sv[10], sv[11], sv[5], sv[7], sv[4]), flush=True)
|
||||||
|
saved += 1
|
||||||
|
if saved >= cap:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.kill(host, signal.SIGCONT)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
time.sleep(float(rng.uniform(0.0005, 0.006)))
|
||||||
|
try:
|
||||||
|
os.kill(host, 0)
|
||||||
|
except ProcessLookupError:
|
||||||
|
print('host exited at %.2fs' % (time.time() - t0), flush=True)
|
||||||
|
break
|
||||||
|
# wait for natural finish so wav completes
|
||||||
|
for _ in range(300):
|
||||||
|
if proc.poll() is not None:
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
print('saved=%d reaper_rc=%s wav=%s' % (saved, proc.poll(),
|
||||||
|
os.path.getsize(wav) if wav and os.path.exists(wav) else 'NONE'))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""scan_lutsub.py v2 — range-filtered scan for detector-LUT param subs.
|
||||||
|
Filter: -60<A<-1, 1<B<100, 0.2<G<8, mode in {0,1}, [+0x90] valid pointer.
|
||||||
|
Prints unique (A,B,G,mode) tuples with addresses.
|
||||||
|
"""
|
||||||
|
import struct, subprocess, sys, time
|
||||||
|
import glob, os
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def find_host():
|
||||||
|
for p in glob.glob('/proc/[0-9]*'):
|
||||||
|
pid = int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
cmd = open(f'/proc/{pid}/cmdline','rb').read().replace(b'\0',b' ').decode('utf8','replace')
|
||||||
|
maps = open(f'/proc/{pid}/maps').read()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if 'soothe2' in maps and 'reaper' not in cmd:
|
||||||
|
return pid
|
||||||
|
return None
|
||||||
|
|
||||||
|
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True)
|
||||||
|
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
|
||||||
|
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 = find_host(); time.sleep(0.002)
|
||||||
|
print('host', host, flush=True)
|
||||||
|
if not host:
|
||||||
|
sys.exit(1)
|
||||||
|
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
||||||
|
|
||||||
|
def rd(a,n):
|
||||||
|
try: return os.pread(fd,n,a)
|
||||||
|
except OSError: return None
|
||||||
|
|
||||||
|
seen={}
|
||||||
|
alive=True
|
||||||
|
t_end=time.time()+12
|
||||||
|
while time.time()<t_end and alive:
|
||||||
|
try:
|
||||||
|
for line in open(f'/proc/{host}/maps'):
|
||||||
|
parts=line.split()
|
||||||
|
if 'rw' not in parts[1]: continue
|
||||||
|
lo,hi=(int(x,16) for x in parts[0].split('-'))
|
||||||
|
CH=8*1024*1024
|
||||||
|
a=lo
|
||||||
|
while a<hi:
|
||||||
|
d=rd(a,min(CH+64,hi-a))
|
||||||
|
if not d:
|
||||||
|
alive=False; break
|
||||||
|
n=len(d)//4
|
||||||
|
arr=np.frombuffer(d[:n*4],dtype='<f4')
|
||||||
|
# vectorized filter on A at even offsets
|
||||||
|
m=(arr>-60)&(arr<-1)
|
||||||
|
for i in np.nonzero(m)[0]:
|
||||||
|
Bv=float(arr[i+1]) if i+1<n else 0
|
||||||
|
if not (1<Bv<100): continue
|
||||||
|
addr=a+i*4
|
||||||
|
s=rd(addr,0x98)
|
||||||
|
if not s or len(s)<0x98: continue
|
||||||
|
G=struct.unpack('<f',s[12:16])[0]
|
||||||
|
if not (0.2<G<8): continue
|
||||||
|
mode=s[16]
|
||||||
|
if mode>1: continue
|
||||||
|
p=struct.unpack('<Q',s[0x90:0x98])[0]
|
||||||
|
if not (0x10000<p<0x7ffff0000000): continue
|
||||||
|
key=(round(float(arr[i]),2),round(Bv,2),round(G,3),mode)
|
||||||
|
if key not in seen:
|
||||||
|
seen[key]=(addr,p)
|
||||||
|
print('NEW sub@%x vt=%x A=%.3f B=%.3f G=%.4f mode=%d' %
|
||||||
|
(addr,p,arr[i],Bv,G,mode), flush=True)
|
||||||
|
a+=CH
|
||||||
|
except (FileNotFoundError, ProcessLookupError):
|
||||||
|
alive=False
|
||||||
|
break
|
||||||
|
print('done; unique tuples:', len(seen))
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""scan_pairs.py — dump ALL unique adjacent float pairs (A<0<B) seen in rw-mem
|
||||||
|
during a render window. Diagnostic for LUT param location."""
|
||||||
|
import struct, subprocess, sys, time
|
||||||
|
import glob, os
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def find_host():
|
||||||
|
for p in glob.glob('/proc/[0-9]*'):
|
||||||
|
pid = int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
cmd = open(f'/proc/{pid}/cmdline','rb').read().replace(b'\0',b' ').decode('utf8','replace')
|
||||||
|
maps = open(f'/proc/{pid}/maps').read()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if 'soothe2' in maps and 'reaper' not in cmd:
|
||||||
|
return pid
|
||||||
|
return None
|
||||||
|
|
||||||
|
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||||
|
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True)
|
||||||
|
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
|
||||||
|
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 = find_host(); time.sleep(0.002)
|
||||||
|
print('host', host, flush=True)
|
||||||
|
if not host:
|
||||||
|
sys.exit(1)
|
||||||
|
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
||||||
|
|
||||||
|
def rd(a,n):
|
||||||
|
try: return os.pread(fd,n,a)
|
||||||
|
except OSError: return None
|
||||||
|
|
||||||
|
seen={}
|
||||||
|
t_end=time.time()+8
|
||||||
|
while time.time()<t_end:
|
||||||
|
try:
|
||||||
|
alive=True
|
||||||
|
for line in open(f'/proc/{host}/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:
|
||||||
|
d=rd(a,min(CH+64,hi-a))
|
||||||
|
if not d:
|
||||||
|
alive=False; break
|
||||||
|
n=len(d)//8*2
|
||||||
|
arr=np.frombuffer(d[:n*4],dtype='<f4').reshape(-1,2)
|
||||||
|
m=(arr[:,0]>-60)&(arr[:,0]<-1)&(arr[:,1]>1)&(arr[:,1]<100)
|
||||||
|
for i in np.nonzero(m)[0]:
|
||||||
|
key=(round(float(arr[i,0]),3),round(float(arr[i,1]),3))
|
||||||
|
if key not in seen:
|
||||||
|
seen[key]=a+i*8
|
||||||
|
print('pair A=%.4f B=%.4f @%x' % (key[0],key[1],a+i*8), flush=True)
|
||||||
|
a+=CH
|
||||||
|
if not alive: break
|
||||||
|
except (FileNotFoundError, ProcessLookupError):
|
||||||
|
break
|
||||||
|
print('unique pairs:', len(seen))
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""two-tone drive series at fc500/q0.5/s12: renders+captures+fit."""
|
||||||
|
import subprocess, os, sys, time, glob
|
||||||
|
import numpy as np, wave
|
||||||
|
|
||||||
|
def sh(cmd): return subprocess.run(cmd,shell=True,capture_output=True,text=True).stdout
|
||||||
|
def find_host():
|
||||||
|
import glob as g
|
||||||
|
for p in g.glob('/proc/[0-9]*'):
|
||||||
|
pid=int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
cmd=open('/proc/%d/cmdline'%pid,'rb').read().replace(b'\0',b' ').decode('utf8','replace')
|
||||||
|
maps=open('/proc/%d/maps'%pid).read()
|
||||||
|
except Exception: continue
|
||||||
|
if 'soothe2' in maps and 'reaper' not in cmd: return pid
|
||||||
|
return None
|
||||||
|
|
||||||
|
# inputs: dual content (500+2000 equal) at drives 0/-6/-12/-18 with probes
|
||||||
|
sr=44100; T=4.0; n=int(sr*T); t=np.arange(n)/sr
|
||||||
|
ph=np.random.default_rng(41).uniform(0,2*np.pi,7)
|
||||||
|
drives=(0,6,12,18)
|
||||||
|
for att in drives:
|
||||||
|
A=0.4248*10**(-att/20)
|
||||||
|
y=A*np.sin(2*np.pi*500*t+ph[0])+A*np.sin(2*np.pi*2000*t+ph[1])
|
||||||
|
for i,f in enumerate((300.,700.,1400.,2800.,5000.)):
|
||||||
|
y+=A*10**(-30/20)*np.sin(2*np.pi*f*t+ph[i+2])
|
||||||
|
y=np.clip(y,-0.999,0.999)
|
||||||
|
nm='tt%d'%att
|
||||||
|
w=wave.open('/tmp/opencode/%s_in.wav'%nm,'wb'); w.setnchannels(1); w.setsampwidth(2); w.setframerate(sr)
|
||||||
|
w.writeframes((y*32767).astype(np.int16).tobytes()); w.close()
|
||||||
|
src=open('/tmp/opencode/multi.rpp').read()
|
||||||
|
import re
|
||||||
|
src=re.sub(r'RENDER_FILE "[^"]*"','RENDER_FILE "/tmp/opencode/%s_ref.wav"'%nm,src)
|
||||||
|
src=src.replace('FILE "/tmp/opencode/multi_in.wav"','FILE "/tmp/opencode/%s_in.wav"'%nm)
|
||||||
|
open('/tmp/opencode/%s.rpp'%nm,'w').write(src)
|
||||||
|
print('inputs+rpps ready')
|
||||||
|
|
||||||
|
sh("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1")
|
||||||
|
for att in drives:
|
||||||
|
wav='/tmp/opencode/tt%d_ref.wav'%att
|
||||||
|
if os.path.exists(wav): os.remove(wav)
|
||||||
|
pr=subprocess.Popen(['/usr/bin/reaper','-nosplash','-ignoreerrors','-renderproject','/tmp/opencode/tt%d.rpp'%att],
|
||||||
|
stdout=subprocess.DEVNULL,stderr=subprocess.STDOUT)
|
||||||
|
t0=time.time(); host=None
|
||||||
|
while time.time()-t0<30 and not host: host=find_host(); time.sleep(0.002)
|
||||||
|
for _ in range(240):
|
||||||
|
if pr.poll() is not None: break
|
||||||
|
time.sleep(0.1)
|
||||||
|
print('tt%d ref rc=%s'%(att,pr.poll()),flush=True)
|
||||||
|
|
||||||
|
# captures + tracts
|
||||||
|
for att in drives:
|
||||||
|
od='/tmp/opencode/sc_tt%d'%att
|
||||||
|
sh('rm -rf %s'%od)
|
||||||
|
sh('cd /home/m/re-tools && timeout 60 python3 scripts/rendersnap2.py /tmp/opencode/tt%d.rpp 400 %s'%(att,od))
|
||||||
|
tf='/tmp/opencode/tract_tt%d.txt'%att
|
||||||
|
if os.path.exists(tf): os.remove(tf)
|
||||||
|
env=dict(os.environ); env['RT_DUMP_BIN']=tf
|
||||||
|
subprocess.run(['/home/m/re-tools/dsp/build/render48k','/tmp/opencode/tt%d_in.wav'%att,
|
||||||
|
'/tmp/o48_tt.wav','500,0.5,12'],capture_output=True,env=env)
|
||||||
|
print('captures+tracts done')
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""unify.py — global unified-law fit: cut = alpha*ln1p(am*s/res^p / beta)+c
|
||||||
|
across multi6 (6 peaks) + tt-series (8 points, both peaks)."""
|
||||||
|
import numpy as np, glob, wave
|
||||||
|
from scipy.optimize import least_squares
|
||||||
|
|
||||||
|
def deepest(dirname,b):
|
||||||
|
best=None
|
||||||
|
for fn in sorted(glob.glob(dirname+'/ph*.npz')):
|
||||||
|
d=np.load(fn)
|
||||||
|
if '0x540628' not in d.files: continue
|
||||||
|
s=d['0x540628'].astype(np.float64)
|
||||||
|
if best is None or s[b]<best[0]: best=(s[b],s)
|
||||||
|
return best[1] if best else None
|
||||||
|
|
||||||
|
def loadwav(p):
|
||||||
|
w=wave.open(p,'rb'); n=w.getnframes(); ch=w.getnchannels(); sw=w.getsampwidth()
|
||||||
|
d=w.readframes(n); w.close()
|
||||||
|
if sw==2: return np.frombuffer(d,dtype=np.int16).astype(np.float64).reshape(-1,ch).mean(1)/32768
|
||||||
|
raw=np.frombuffer(d,dtype=np.uint8).reshape(-1,ch,3)
|
||||||
|
s=raw[:,:,0].astype(np.int64)|(raw[:,:,1].astype(np.int64)<<8)|(raw[:,:,2].astype(np.int64)<<16)
|
||||||
|
return np.where(s>=0x800000,s-0x1000000,s).astype(np.float64).reshape(-1,ch).mean(1)/8388608
|
||||||
|
|
||||||
|
def ta(x,f,sr=44100,L=None):
|
||||||
|
x=x[-L:]; t=np.arange(len(x))/sr; c=np.cos(2*np.pi*f*t); sn=np.sin(2*np.pi*f*t)
|
||||||
|
return np.hypot(2*(x*c).sum(),2*(x*sn).sum())/len(x)
|
||||||
|
|
||||||
|
pts=[]
|
||||||
|
# multi6 six peaks
|
||||||
|
S=deepest('/tmp/opencode/sc_multi6',85)
|
||||||
|
cut=np.maximum(-S*8.685889638,0)
|
||||||
|
am=np.zeros(len(S)); rs=np.zeros(len(S))
|
||||||
|
for ln in open('/tmp/opencode/tract_multi6.txt'):
|
||||||
|
if ln.startswith('#'): continue
|
||||||
|
p=ln.split(); kk=int(p[0])
|
||||||
|
if kk<len(S): am[kk]=float(p[1]); rs[kk]=float(p[2])
|
||||||
|
for kk in range(1,len(S)):
|
||||||
|
if cut[kk]>1.0 and am[kk]>0.01:
|
||||||
|
pts.append(('m6',am[kk],rs[kk],cut[kk]))
|
||||||
|
# tt-series 4 drives x 2 peaks (ref-domain cuts via Goertzel)
|
||||||
|
inp0=loadwav('/tmp/opencode/tt0_in.wav')
|
||||||
|
rc={0:(10.34,11.84),6:(8.12,9.57),12:(6.12,7.45),18:(4.40,5.55)}
|
||||||
|
for att,(c1v,c2v) in rc.items():
|
||||||
|
tf='/tmp/opencode/tract_tt%d.txt'%att
|
||||||
|
d43=d171=None; r43=r171=None
|
||||||
|
for ln in open(tf):
|
||||||
|
if ln.startswith('#'): continue
|
||||||
|
p=ln.split(); kk=int(p[0])
|
||||||
|
if kk==43: r43=float(p[2]); d43=am0=None; d43=float(p[1])
|
||||||
|
if kk==171: r171=float(p[2]); d171=float(p[1])
|
||||||
|
pts.append(('tt%d-pk1'%att,d43,r43,c1v))
|
||||||
|
pts.append(('tt%d-pk2'%att,d171,r171,c2v))
|
||||||
|
print('points:',len(pts))
|
||||||
|
|
||||||
|
def resid(p):
|
||||||
|
al,be,cc,pp,ss=p
|
||||||
|
out=[]
|
||||||
|
for nm,a,r,c in pts:
|
||||||
|
X=a*ss/max(r**pp,1e-12)
|
||||||
|
out.append(al*np.log1p(X/be)+cc-c)
|
||||||
|
return np.array(out)
|
||||||
|
|
||||||
|
p0=[3.28,0.56,0.77,1.25,1.0]
|
||||||
|
lb=[0.5,0.01,-5,0.05,0.05]
|
||||||
|
ub=[8,5,5,4,50]
|
||||||
|
r=least_squares(resid,p0,bounds=(lb,ub),max_nfev=8000)
|
||||||
|
mm=-resid(r.x)+np.array([c for _,_,_,c in pts])
|
||||||
|
tgt=np.array([c for _,_,_,c in pts])
|
||||||
|
rms=np.sqrt(((mm-tgt)**2).mean())
|
||||||
|
print('UNIFIED: alpha=%.3f beta=%.4f c=%+.3f p=%.3f s=%.3f ; rms=%.4f max=%.3f' % (
|
||||||
|
*r.x,rms,np.abs(mm-tgt).max()))
|
||||||
|
for (nm,a,rr,c),pv in zip(pts,mm):
|
||||||
|
print(' %-10s am=%.4f res=%.4f cut=%6.2f pred=%6.2f err=%+.2f' % (nm,a,rr,c,pv,c-pv))
|
||||||
@@ -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,648 @@
|
|||||||
|
#!/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
|
||||||
|
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)):
|
||||||
|
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}
|
||||||
|
track_by_tid = {}
|
||||||
|
track_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
|
||||||
|
if kind in ('CIN','COUT'):
|
||||||
|
key='cin_%d'%pid if kind=='CIN' else 'cout_%d'%pid
|
||||||
|
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, '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())
|
||||||
Reference in New Issue
Block a user