Compare commits
89
Commits
135f0e9f21
...
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 | ||
|
|
475b1958d5 | ||
|
|
a1632a9fce | ||
|
|
0e4d3177fe | ||
|
|
2c99a4fc96 | ||
|
|
03777fdaee |
@@ -4,14 +4,25 @@ Bit-exact реверс DSP-ядра oeksound soothe2 (VST3) → транскри
|
||||
Полное журналирование — в `handoff/NOTES_LEVEL.md`, `handoff/NOTES_TWIN.md`,
|
||||
`handoff/NOTES_CAPTURE.md`, `roadmap.md`.
|
||||
|
||||
> **Текущая фаза (2026-08-23, после 22z):** канон = структурная цепь 48k/4096 с
|
||||
> RT_LAWAFFINE=7.4,1.85 (TOTAL 1.931; bridge 1.594 — гейт не пройден). ГЛАВНЫЙ
|
||||
> ОТКРЫТЫЙ ВОПРОС — семантика входов `bands[]` детектора: доказанно НЕ поточечная
|
||||
> функция от (am,res^α) [22x], не форма-постобработка [22w], не спрединг
|
||||
> IDFT→окно→DFT [22z]. Живая кривая редукции R=1/mask захвачена (слот 0x5407f8,
|
||||
> dualtrace.py), но применённый фильтр ≠ её поточечной копии — механизм живёт ДО
|
||||
> кривой (twin-шаблон/скалярный драйв). Журнал: NOTES_LEVEL 22s–22z; карта метода:
|
||||
> handoff/BLOCKMAP_529fe0.md. ЭТОТ ФАЙЛ ЧИТАЙ ПЕРВЫМ.
|
||||
> **Текущая фаза (2026-08-25, после 24kk2):** ПРИМЕНЕНИЕ ДЕКОДИРОВАНО ФОРМУЛАМИ:
|
||||
> аудио = побиновное умножение кадра на вещественную маску `10^(−cut_D/20)`,
|
||||
> `cut_D = α·ln1p(lvl_raw/β)+c` (+Δ у вторых пиков). Калибровки (rms ≤0.016 дБ):
|
||||
> dual(q0.5,s12,2 тона)=3.2193/0.4927/+0.54; fc1000(1 тон,q0.5)=1.6151/0.3645/+0.48;
|
||||
> fc500(1 тон)=1.1530/0.4038/+0.33. lvl_raw — НАШ фронтенд (float-parity ✓).
|
||||
> Слой STFT = БЕЗ синтез-окна (`RT_SYN=1`). **dual-корпус 0.193 max 0.438**
|
||||
> (канон 3.264); канон TOTAL 2.286 нетронут. ГЕЙТ СМЕНЫ КАНОНА = BIT EXACT
|
||||
> (решение пользователя: все параметры прослежены до декомпа + шумовой пол корпуса).
|
||||
>
|
||||
> Ключевые факты: q НЕ влияет на закон (доказано 24kk); sens линейно через
|
||||
> lvl_raw; далёкий контент не влияет — смешение шаблонно-локальное (24ll);
|
||||
> аудио = exp(deepest-scratch)+const; буфер FIR@540668 = мин.-фазовое
|
||||
> представление (exp(s−iH(s))) — не для транскрипции. Тела bigkernel'ов:
|
||||
> 1803a06a0/180296c80/180323f20/1802dc0e0 (x87 exp-семейство, 24jj).
|
||||
>
|
||||
> ОСТАТОК: каскадный симулятор шагов 9–19 по dataflow (24hh/24ii) →
|
||||
> параметризация α(контент)/fc через campaign.py (датасеты готовы) →
|
||||
> Δ-правило из pre-combine → полный корпус. Журнал: NOTES_LEVEL 24j–24kk2;
|
||||
> карта метода: handoff/BLOCKMAP_529fe0.md. ЭТОТ ФАЙЛ ЧИТАЙ ПЕРВЫМ.
|
||||
|
||||
## Золотое правило (обязательно)
|
||||
1. **Цель — bit-exact реверс кода**, НЕ эмпирическая подгонка кривых. Каждый параметр
|
||||
@@ -137,6 +148,36 @@ scale → LUT level-domain (t^γ·MULT, γ=0.344 decomp / MULT=4.2 placeholder)
|
||||
- `scripts/resalpha.py` — сбор трактов + теорема об отсутствии α (22x).
|
||||
- `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/`,
|
||||
`dl/lib/bin/include/`, `regions*/`. См. `.gitignore`.
|
||||
|
||||
@@ -26,6 +26,17 @@ comb 10 dB). Bit-exact ДОСТИЖИМ (F0 gate: плагин байт-дете
|
||||
> Новый приоритет №1 — семантика входов `bands[]` (см. AGENTS.md и NOTES_LEVEL
|
||||
> 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)
|
||||
настолько точно, что `verify_bit_exact.py` даёт побайтовое совпадение на рендерах
|
||||
(`t1kq_*`, `dual_*`, `comb_*`). Ниже — конкретный порядок, что и зачем.
|
||||
|
||||
@@ -10,54 +10,65 @@
|
||||
|
||||
---
|
||||
|
||||
## Статус (P5, 2026-08-22)
|
||||
## Статус (24kk2, 2026-08-25)
|
||||
|
||||
**Цель — bit-exact реверс.** Декомпиляция DSP-ядра закрыта (~95%): twin-резонатор,
|
||||
генератор case8, level-path, mask-apply (FUN_180529fe0), FFT-conv — расшифрованы;
|
||||
дизассемблы в `handoff/nls_dasm/` (~140).
|
||||
**Цель — bit-exact реверс** (гейт смены канона зафиксирован пользователем: только
|
||||
после прослеживания всех параметров до декомпа и схождения корпуса в шумовой пол).
|
||||
Декомпиляция 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
|
||||
-> warp(mask*=0x540768, *=warp) -> IIR3x2 -> dry/wet -> FFT-conv
|
||||
mask(b) = 10^(−cut_D(b)/20) ← вещественная, per-bin multiply кадра
|
||||
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-таблицы
|
||||
в `dsp/rt_mask_tables.{hpp,cpp}`, `dsp/rt_weights.{hpp,cpp}`.
|
||||
- **Корпус (62 случая)**: bridge mean 1.594 dB; структурная цепь TOTAL 2.286,
|
||||
но comb **6.12** и res **0.44** — лучше bridge.
|
||||
Калибровки формы (три независимых семейства, rms ≤0.016 дБ): α/β/c зависят от
|
||||
контента (α удваивается с числом тонов — частотное смешение шаблонно-локальное),
|
||||
q НЕ влияет на закон, sens входит линейно через lvl_raw.
|
||||
|
||||
### Главное за 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!** Весь кластер кривых питается от
|
||||
GUI-timer vtables; аудио-метод FUN_180529fe0 BandConfig не читает (22b). Live BandConfig
|
||||
у всех конфигов одинаковый: A=−24/B=28/γ=1. LUT-константы в модели помечены EMPIRICAL.
|
||||
2. **Пол редукции найден живьём и решён алгебраически** (22d/e): hot-тон упирается в жёсткий
|
||||
пол gain=−20.72 dB = `20·log10(blend·ln10/20)` с точностью 0.0055 dB;
|
||||
`floor_dB(sens) ≈ −18.78 − (sens−6)/3` (якорь sens6 = ln10/20 ровно).
|
||||
3. **Разрыв локализован в детекторном фронте** (22g/i): модель теряет ×2.8 уровня на
|
||||
изолированных пиках (lvl_raw 3.01 → 1.06 после IIR1-разведения), реальный плагин
|
||||
доводит пост-IIR lvl до ~3.12. Не форма кривой — амплитудная цепочка am/res/scale.
|
||||
4. **Инфраструктура**: официальный параметр-мост REAPER (`setparam.lua`/`dump_params.lua`),
|
||||
XML `<PARAM>` в RPP = декоративная копия; live-capture BandConfig (`scripts/step7_capture.py`);
|
||||
env-солверы констант (`RT_LUT_A/B/G/MULT`, `RT_LUT_OFF`, `RT_LVL_CAP`).
|
||||
5. Оффлайн-гипотезы Phase B (pooling/temporal/scalar-ρ) — все опровергнуты (22a);
|
||||
clipping-теория пола опровергнута (float-домен, 22g); mix = чистый dry/wet кроссфейд.
|
||||
### Главное за 2026-08-24…25 (сессии 24j–24kk2)
|
||||
|
||||
1. **Применение = побиновный complex-multiply кадра** на маску; «магический ×1.805»
|
||||
оказался произведением экспонент стадий построения буфера (0.984×1.8345).
|
||||
2. **Закон уровня универсальной формы** `α·ln(1+L/β)+c` — подтверждён тремя
|
||||
независимыми калибровками; константы зависят от контента (число тонов) и слабо от fc.
|
||||
3. **Слой STFT**: плагин НЕ домножает выход обратного FFT на окно
|
||||
(`RT_SYN=1`); WIN_WINDOW движка — фейд 0.5→0.8 ровно за 2049 сэмплов (=бинам кернела).
|
||||
4. **GUI/аудио разделение**: LUT-строитель FUN_180563a60 — GUI-ветка; аудио-компрессия
|
||||
живёт в семантиках шагов 9–19 BLOCKMAP.
|
||||
5. **Dataflow шагов 9–16 декодирован**: vec6f8=bands−ACC; fma тройками
|
||||
(re,im,coef) с ATT/REL; th2000=поэлементное умножение массивов (не axpy!);
|
||||
шаг 12=COPY (исправлен старый BLOCKMAP).
|
||||
6. **Инструменты**: rendersnap2 v7 (мягкий STOP-снаппер со слотами+скалярами,
|
||||
RENDER_FILE-фикс), patchparam.py (правка VST-чанка RPP!), campaign.py
|
||||
(ячейка параметризации), disasm_func.py (capstone с RIP-константами),
|
||||
iat_name.py (рантайм-резолв импортов через PE-экспорты).
|
||||
|
||||
```bash
|
||||
# Канон сборки и рендера:
|
||||
# Сборка и канонные команды:
|
||||
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
|
||||
python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json
|
||||
```
|
||||
Детальная метрика и история — в `AGENTS.md`, `BITEXACT_PLAN.md`, `handoff/NOTES_LEVEL.md`
|
||||
(апдейты 20j…22i).
|
||||
(апдейты 20j…24kk2).
|
||||
|
||||
### Открытые bit-exact пробелы
|
||||
**Приоритет №1 — детекторный фронт**: амплитудная нормировка am, форма res_k, сила
|
||||
IIR1/2 вдоль частоты (модель теряет уровень на пиках). Далее: PRNG-пролог (LCG→fVar30),
|
||||
FFT-conv сглаживание, бит-экзактный exp2, combine/acc консюмер, SR-mismatch
|
||||
(внутренний DSP 48000/N=4096 против хоста). Полный список — `AGENTS.md`, `BITEXACT_PLAN.md`
|
||||
(Шаг 9), `NOTES_LEVEL.md`.
|
||||
**Приоритет №1 — каскадный симулятор шагов 9–19**: dataflow декодирован
|
||||
(24hh/24ii: vec6f8=bands−ACC, fma-тройки att/rel, ×track, ×warp, центрирование −1),
|
||||
тела bigkernel'ов найдены по рантайм-адресам — осталось сложить оп-за-опом и
|
||||
проверить на датасетах sc_* (rms каскада сейчас ~0.42 на угаданных формах).
|
||||
Далее: k-маппинг фронтенда (twin/am формулы), Δ-правило вторых пиков из pre-combine.
|
||||
Полный список — `AGENTS.md`, `BITEXACT_PLAN.md`, `NOTES_LEVEL.md` (24bb→24kk2).
|
||||
|
||||
> Исторический блок (поведенческая/численная модель B.1…B.15, `framed_render.py`,
|
||||
> Pchip LUT, res_power) — см. `roadmap.md`; самодостаточен как справочник, но не канон.
|
||||
|
||||
@@ -29,6 +29,7 @@ add_library(soothe2_dsp SHARED
|
||||
fn529fe0.cpp
|
||||
rt_weights.cpp
|
||||
rt_mask_tables.cpp
|
||||
log2_ln.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);
|
||||
}
|
||||
|
||||
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_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);
|
||||
|
||||
}
|
||||
|
||||
+174
-2
@@ -6,12 +6,184 @@
|
||||
// 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
|
||||
// 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 {
|
||||
|
||||
void iir1(float* x, const double* A, const double* B, size_t nbin, double /*acc0*/) {
|
||||
// ---- 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) {
|
||||
// leaky first-order: y = A*acc + B*x ; acc = y (B = 1-A from live tables)
|
||||
double acc = 0.0;
|
||||
// State persists across calls via static accumulator (per-thread).
|
||||
static thread_local double acc = 0.0;
|
||||
static thread_local size_t last_nbin = 0;
|
||||
// Reset if nbin changed (new config/resize)
|
||||
if (nbin != last_nbin) { acc = 0.0; last_nbin = nbin; }
|
||||
for (size_t i = 0; i < nbin; i++) {
|
||||
double y = A[i] * acc + B[i] * static_cast<double>(x[i]);
|
||||
acc = y;
|
||||
|
||||
@@ -18,6 +18,57 @@
|
||||
// (level = am/res) but fed through the structural chain instead of the LUT bridge.
|
||||
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).
|
||||
// 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);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// - blend_exp2 : out == exp2(-x)*blend, blend = freqaxis*(1-mix)+mix*0.8
|
||||
// - combine_acc: subtract then add band/f6f8 contributions (exact)
|
||||
// - warp_mask : multiplies by kBand768*kWarp
|
||||
// - cascade : Haar, magnitudes, blend (529c60 decode)
|
||||
int main() {
|
||||
const size_t nbin = 2049; // internal N/2+1 grid used by the chain
|
||||
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",
|
||||
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");
|
||||
return fail;
|
||||
}
|
||||
|
||||
+256
-14
@@ -134,9 +134,147 @@ static void process_band_structural(
|
||||
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)
|
||||
std::vector<float> raw_level(nbin);
|
||||
for (size_t k = 0; k < nbin; k++) {
|
||||
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
|
||||
double lvl = lvl_in[k];
|
||||
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++) {
|
||||
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
|
||||
double lvl = raw_level[k];
|
||||
if (!lut_off) {
|
||||
// dB-domain LUT (FUN_180563a60) on LEVEL before IIR/exp2: keeps both
|
||||
// quiet (t1kq) and loud (t1k) inputs inside the LUT domain [A,B],
|
||||
@@ -145,6 +283,10 @@ static void process_band_structural(
|
||||
double t = (dB - LUT_A) / (LUT_B - LUT_A);
|
||||
t = std::min(std::max(t, 0.0), 1.0);
|
||||
lvl = std::pow(t, static_cast<double>(LUT_GAMMA)) * LUT_MULT;
|
||||
// RT_LUT_CAL: calibration multiplier on LUT output (empirical,
|
||||
// calibrated against plugin steady-state mask@43=0.510).
|
||||
static const double lut_cal = getenv("RT_LUT_CAL") ? atof(getenv("RT_LUT_CAL")) : 1.0;
|
||||
lvl *= lut_cal;
|
||||
}
|
||||
band_level[k] = static_cast<float>(lvl);
|
||||
}
|
||||
@@ -208,17 +350,40 @@ static void process_band_structural(
|
||||
}
|
||||
|
||||
for (size_t k = 0; k < nfft; k++) {
|
||||
double mm = std::exp2(-static_cast<double>(band_level[k]));
|
||||
static const int noblend = getenv("RT_NOBLEND") ? atoi(getenv("RT_NOBLEND")) : 0;
|
||||
if (!noblend) mm *= f6f8[k];
|
||||
// RT_LAWAFFINE="A,S" (NOTES 22q): cut_dB = A + S*log2(lvl) — affine dB law
|
||||
static const char* la = getenv("RT_LAWAFFINE");
|
||||
if (la && lut_off) {
|
||||
double A_db = atof(la); const char* cm = strchr(la, ',');
|
||||
double S_db = cm ? atof(cm + 1) : 2.17;
|
||||
if (band_level[k] > 1e-6) {
|
||||
double y = (A_db + S_db * std::log2(band_level[k])) / 6.0174;
|
||||
mm = std::exp2(-y);
|
||||
double mm;
|
||||
// RT_FIRPOWER=1: FIR-style mask from raw spectrum.
|
||||
// Plugin's actual pipeline (52b550-52b8bb):
|
||||
// 1. scratch = log(raw_spectrum)
|
||||
// 2. FIR = exp(0.984 × scratch) = raw^0.984
|
||||
// 3. FIR *= hann_window (freq-domain)
|
||||
// 4. FIR *= 0x540888 (scalar)
|
||||
// 5. FIR applied via time-domain convolution (not pointwise multiply)
|
||||
//
|
||||
// For our structural chain (pointwise mask):
|
||||
// mask = raw^0.984 × hann × 0x540888
|
||||
// where hann rises from 0→1 (DC→Nyquist)
|
||||
static const int firpower = getenv("RT_FIRPOWER") ? atoi(getenv("RT_FIRPOWER")) : 0;
|
||||
if (vlaw) {
|
||||
mm = static_cast<double>(band_level[k]);
|
||||
} else if (firpower) {
|
||||
double raw = static_cast<double>(raw_level[k]);
|
||||
if (raw > 1e-12) {
|
||||
mm = std::pow(raw, 0.984);
|
||||
} else {
|
||||
mm = 1.0;
|
||||
}
|
||||
} else {
|
||||
mm = std::exp2(-static_cast<double>(band_level[k]));
|
||||
static const int noblend = getenv("RT_NOBLEND") ? atoi(getenv("RT_NOBLEND")) : 0;
|
||||
if (!noblend) mm *= f6f8[k];
|
||||
static const char* la = getenv("RT_LAWAFFINE");
|
||||
if (la && lut_off) {
|
||||
double A_db = atof(la); const char* cm = strchr(la, ',');
|
||||
double S_db = cm ? atof(cm + 1) : 2.17;
|
||||
if (band_level[k] > 1e-6) {
|
||||
double y = (A_db + S_db * std::log2(band_level[k])) / 6.0174;
|
||||
mm = std::exp2(-y);
|
||||
}
|
||||
}
|
||||
}
|
||||
mask_out[k] = static_cast<float>(mm);
|
||||
@@ -327,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
|
||||
|
||||
FramedDetector::FramedDetector(size_t nfft, float sample_rate)
|
||||
@@ -341,6 +532,8 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
|
||||
size_t half = nfft_ / 2;
|
||||
res_.clear();
|
||||
track_.clear();
|
||||
twin_resp_complex_.clear();
|
||||
cascade_states_.clear();
|
||||
|
||||
// RT_DUMPRESPATH=<file> (NOTES 22t): static twin-response spectra per band,
|
||||
// binary {int32 band, int32 nbin, float res[nbin]} records (append).
|
||||
@@ -365,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::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) {
|
||||
int32_t bi = static_cast<int32_t>(res_.size());
|
||||
int32_t nb = static_cast<int32_t>(r.size());
|
||||
@@ -376,6 +576,7 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
|
||||
}
|
||||
if (rp_dump) fclose(rp_dump);
|
||||
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) {
|
||||
@@ -412,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;
|
||||
|
||||
if (is_internal_grid(nfft_, sample_rate_)) {
|
||||
@@ -428,8 +633,45 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
|
||||
sample_rate_, sf, fparams,
|
||||
band_mask.data());
|
||||
} else {
|
||||
process_band_structural(am_.data(), res_[b].data(), bands_[b],
|
||||
band_mask.data(), nfft_, sample_rate_);
|
||||
// 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 {
|
||||
process_band_structural(am_.data(), res_[b].data(), bands_[b],
|
||||
band_mask.data(), nfft_, sample_rate_);
|
||||
}
|
||||
}
|
||||
for (size_t k = 0; k <= half; k++) {
|
||||
mask[k] = std::min(band_mask[k], mask[k]);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <cstddef>
|
||||
#include <complex>
|
||||
#include <vector>
|
||||
#include "fn529fe0.hpp"
|
||||
|
||||
struct DetectorBand {
|
||||
float fc; // band center freq (Hz)
|
||||
@@ -71,6 +72,11 @@ public:
|
||||
|
||||
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:
|
||||
size_t nfft_;
|
||||
float sample_rate_;
|
||||
@@ -82,4 +88,10 @@ private:
|
||||
std::vector<float> am_; // smoothed per-bin amplitude
|
||||
std::vector<float> f6f8_; // shared 0x5406f8 blend buffer (IIR1 out)
|
||||
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
|
||||
+196
-5
@@ -1,7 +1,13 @@
|
||||
#include "spectral.hpp"
|
||||
#include "fftconv.hpp"
|
||||
#include "log2_ln.hpp"
|
||||
#include "exp2_tables.hpp"
|
||||
#include "exp2.hpp"
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
|
||||
SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
||||
: nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0),
|
||||
@@ -11,23 +17,41 @@ SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
||||
fft::init_plan(&plan_, static_cast<uint32_t>(std::log2(nfft_)));
|
||||
buf_ = new std::complex<double>[nfft_];
|
||||
tmp_buf_ = new std::complex<double>[nfft_];
|
||||
fir_buf_ = new std::complex<double>[nfft_];
|
||||
fir_freq_ = new std::complex<double>[nfft_];
|
||||
overlap_.resize(nfft_, 0.0f);
|
||||
mask_.resize(nfft_, 1.0f);
|
||||
|
||||
// Build FIR window: falling half of periodic Hann(4096).
|
||||
// Plugin reads window[N/2..N-1] of periodic Hann (rising 0→1).
|
||||
fir_window_.resize(nfft_);
|
||||
for (size_t i = 0; i < nfft_; i++) {
|
||||
fir_window_[i] = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_));
|
||||
}
|
||||
}
|
||||
|
||||
SpectralProcessor::~SpectralProcessor() {
|
||||
delete[] window_;
|
||||
delete[] buf_;
|
||||
delete[] tmp_buf_;
|
||||
delete[] fir_buf_;
|
||||
delete[] fir_freq_;
|
||||
}
|
||||
|
||||
void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) {
|
||||
detector_.setParams(bands);
|
||||
loadWinFreq();
|
||||
}
|
||||
|
||||
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++) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,16 +67,20 @@ void SpectralProcessor::istftFrame(std::complex<double>* in, float* out, float*
|
||||
fft::execute_inverse(&plan_, tmp_buf_);
|
||||
static bool wola_computed = false;
|
||||
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) {
|
||||
double wola_sum = 0.0;
|
||||
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_computed = true;
|
||||
}
|
||||
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++) {
|
||||
out[i] = overlap[i] / wola_norm;
|
||||
@@ -65,12 +93,145 @@ 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) {
|
||||
memset(out, 0, num_samples * sizeof(float));
|
||||
if (num_samples == 0 || num_samples < nfft_) {
|
||||
return;
|
||||
}
|
||||
|
||||
static const int firconv = []() {
|
||||
const char* e = getenv("RT_FIRCONV");
|
||||
return e ? atoi(e) : 0;
|
||||
}();
|
||||
|
||||
size_t nframes = (num_samples - nfft_) / hop_ + 1;
|
||||
|
||||
for (size_t f = 0; f < nframes; f++) {
|
||||
@@ -80,8 +241,38 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
|
||||
|
||||
detector_.processFrame(buf_, mask_.data());
|
||||
|
||||
for (size_t i = 0; i < nfft_; i++) {
|
||||
buf_[i] *= mask_[i];
|
||||
if (firconv == 3) {
|
||||
// RT_FIRCONV=3 (NOTES 24k): plugin application law decoded live:
|
||||
// applied_gain = 1.019 * V^1.8345 per bin (rms 0.0025 dB over
|
||||
// 8 drive levels). V = band curve (detector output); here M.
|
||||
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++) {
|
||||
fir_freq_[i] = std::complex<double>(
|
||||
static_cast<double>(mask_[i % (nfft_/2+1)]), 0.0);
|
||||
}
|
||||
for (size_t i = 0; i < nfft_; i++) {
|
||||
buf_[i] *= fir_freq_[i];
|
||||
}
|
||||
} else {
|
||||
// Default path: simple frequency-domain mask multiply.
|
||||
for (size_t i = 0; i < nfft_; i++) {
|
||||
buf_[i] *= mask_[i];
|
||||
}
|
||||
}
|
||||
|
||||
istftFrame(buf_, out + offset, overlap_.data());
|
||||
|
||||
@@ -25,6 +25,9 @@ private:
|
||||
FFTPlan plan_;
|
||||
std::complex<double>* buf_;
|
||||
std::complex<double>* tmp_buf_;
|
||||
std::complex<double>* fir_buf_;
|
||||
std::complex<double>* fir_freq_;
|
||||
std::vector<double> fir_window_;
|
||||
std::vector<float> overlap_;
|
||||
std::vector<float> mask_;
|
||||
FramedDetector detector_;
|
||||
@@ -34,4 +37,14 @@ private:
|
||||
void computeWindow();
|
||||
void stftFrame(const float* in, std::complex<double>* out);
|
||||
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 дБ) ⇒ применённый фильтр
|
||||
≠ поточечная копия любой из живых кривых (подтверждение разрыва 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);
|
||||
кривые 22y = результат инициализации при загрузке проекта
|
||||
@@ -227,3 +287,506 @@ xmm8; затем xmm12−xmm8) [ср. decomp 184-185: A=[0x540874]−expf(K), we
|
||||
использует закэшированный кернел; либо трассировать надо момент инициализации
|
||||
- Выход плагина НЕдетерминирован: md5 двух свободных рендеров различен при
|
||||
одинаковом 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» даёт пустой файл!).
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# PROMPT FOR NEXT SESSION (2026-08-25, после 24kk2)
|
||||
|
||||
Продолжаем bit-exact реверс soothe2 в /home/m/re-tools (ветка main).
|
||||
ГЕЙТ СМЕНЫ КАНОНА = BIT EXACT (решение пользователя): все параметры
|
||||
прослежены до декомпа + корпус в шумовом пол. До тех пор канон не трогаем.
|
||||
|
||||
ПРОЧИТАТЬ ПЕРВЫМ: AGENTS.md (фаза-заголовок 24kk2 + env-флаги + инструменты)
|
||||
→ handoff/NOTES_LEVEL.md обновления 24j–24kk2 → BLOCKMAP_529fe0.md
|
||||
(дополнения 23b/24l/24hh/24ii).
|
||||
|
||||
## СОСТОЯНИЕ
|
||||
|
||||
ПРИМЕНЕНИЕ ДЕКОДИРОВАНО ДО ФОРМУЛ:
|
||||
```
|
||||
mask(b) = 10^(−cut_D(b)/20) вещественная, per-bin multiply кадра
|
||||
cut_D(b) = α·ln1p(lvl_raw(b)/β)+c [+Δ вторые пики]
|
||||
lvl_raw = am/res·scale наш фронтенд, float-parity ✓
|
||||
слой = 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.805-свёртка [23e], OLA-нормировка [24i],
|
||||
двухстадийный γ₀ как множитель закона [24m — это артефакт двух тонов],
|
||||
клампы параметров [24dd], B∝am [24bb], axpy-семантика th2000 [24ii — это
|
||||
array-multiply], «acc_i += bands[i]» шага 12 [24gg — это COPY].
|
||||
|
||||
## ЗАДАЧА №1: каскадный симулятор шагов 9–19 (оп-за-опом)
|
||||
|
||||
Dataflow декодирован (BLOCKMAP 24hh/24ii):
|
||||
```
|
||||
vec698 *= (xmm12−[54087c]) ; обнуление при дефолтах
|
||||
vec6f8 += 0.8 ; базовая линия
|
||||
bigkernel(vec6f8, bands_i, vec6f8)
|
||||
vec6f8 = bands_curve − ACC_i ; COMBINE (dc40: out=rdx−r10)
|
||||
f6f8_верх += ATT[k]·ACC[k]; низ += REL[k]·ACC[k] ; тройки re/im/coef
|
||||
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).
|
||||
|
||||
Метод проверки: собрать симулятор в Python (numpy), прогнать lvl_raw из
|
||||
tract_* через каскад, сравнить с deepest-scratch кривыми sc_* — rms < 0.05 дБ
|
||||
= замкнулось. Затем перенос в C++.
|
||||
|
||||
## ЗАДАЧА №2: параметризация α(контент)/fc
|
||||
|
||||
α удваивается с числом тонов (частотное смешение шаблонно-локальное —
|
||||
далёкий тон не влияет, 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-тенция) → канон не меняем; следующий рычаг — контент-зависимость
|
||||
> (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,
|
||||
level-path (0x529fe0/0x563440/0x563a60), mask-apply (FUN_180529fe0 mono-path), IIR-трекеры,
|
||||
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()
|
||||
@@ -32,6 +32,9 @@ def load_mono(p):
|
||||
ch = w.getnchannels(); b = w.getsampwidth()
|
||||
if b == 2:
|
||||
return np.frombuffer(d, dtype=np.int16).astype(np.float64).reshape(-1, ch).mean(1) / 32768
|
||||
# 24-bit: handle misaligned data (extra bytes from bext/junk chunks)
|
||||
n_samples = len(d) // (ch * 3)
|
||||
d = d[:n_samples * ch * 3]
|
||||
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).mean(1).astype(np.float64) / 8388608.0
|
||||
|
||||
@@ -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())
|
||||
Executable
+241
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""scan3.py — Multi-instance DSP context scanner for soothe2.
|
||||
|
||||
Fixes over scan2.py:
|
||||
1. Pre-scans ALL processes for ctx (no host-finding delay)
|
||||
2. Scans for ALL instances (GUI/DSP pair hypothesis from 24c)
|
||||
3. Captures full state per instance for comparison
|
||||
4. Uses rendersnap-style sampling for FIR/slot captures
|
||||
"""
|
||||
import glob
|
||||
import hashlib
|
||||
import os
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
OUT = '/tmp/opencode/scan3'
|
||||
NARR = 8194
|
||||
|
||||
SLOTS_FULL = [
|
||||
0x540548, 0x540550, 0x540598, 0x540628, 0x540668, 0x540678, 0x540688,
|
||||
0x540698, 0x5406a8, 0x5406b8, 0x5406c8, 0x5406d8, 0x5406e8, 0x5406f8,
|
||||
0x540708, 0x540718, 0x540728, 0x540738, 0x540748, 0x540758,
|
||||
0x540768, 0x540778, 0x540788, 0x540798, 0x5407a8, 0x5407b8,
|
||||
0x5407c8, 0x5407d8, 0x5407e8, 0x5407f8, 0x540808, 0x540818,
|
||||
0x540828, 0x540838, 0x540848,
|
||||
]
|
||||
|
||||
VTQ = struct.pack('<Q', 0x1824AC210)
|
||||
M48 = struct.pack('<I', 0x473b8000)
|
||||
|
||||
|
||||
def pre_scan_all(max_region=50*1024*1024):
|
||||
"""Scan ALL processes for DSP ctx instances (no host needed)."""
|
||||
instances = {} # pid -> [(ctx_addr, sens)]
|
||||
for p in glob.glob('/proc/[0-9]*'):
|
||||
pid = int(os.path.basename(p))
|
||||
try:
|
||||
maps = open(f'/proc/{pid}/maps').read()
|
||||
except Exception:
|
||||
continue
|
||||
if 'soothe2' not in maps:
|
||||
continue
|
||||
try:
|
||||
fd = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
|
||||
except Exception:
|
||||
continue
|
||||
pid_insts = []
|
||||
for line in maps.split('\n'):
|
||||
parts = line.split()
|
||||
if len(parts) < 2 or 'rw' not in parts[1]:
|
||||
continue
|
||||
lo, hi = (int(x, 16) for x in parts[0].split('-'))
|
||||
if hi - lo > max_region:
|
||||
continue
|
||||
try:
|
||||
data = os.pread(fd, min(hi - lo, 4*1024*1024), lo)
|
||||
except Exception:
|
||||
continue
|
||||
for pat, off in ((VTQ, 0), (M48, -0x24)):
|
||||
j = data.find(pat)
|
||||
while j >= 0:
|
||||
cand = lo + j + off
|
||||
try:
|
||||
sb = os.pread(fd, 4, cand + 0x540870)
|
||||
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||
pid_insts.append(cand)
|
||||
except Exception:
|
||||
pass
|
||||
j = data.find(pat, j + 1)
|
||||
os.close(fd)
|
||||
if pid_insts:
|
||||
instances[pid] = list(set(pid_insts))
|
||||
return instances
|
||||
|
||||
|
||||
def read_state(fd, ctx):
|
||||
"""Read key DSP state from a context."""
|
||||
state = {'ctx': ctx}
|
||||
for off, name, fmt in [
|
||||
(0x540870, 'sens', '<f'),
|
||||
(0x540874, 'depth', '<f'),
|
||||
(0x54087c, 'mix', '<f'),
|
||||
(0x540888, 'att_coeff', '<f'),
|
||||
(0x54088c, 'rel_coeff', '<f'),
|
||||
(0x1a0, 'nfft', '<i'),
|
||||
]:
|
||||
try:
|
||||
sb = os.pread(fd, 4, ctx + off)
|
||||
state[name] = struct.unpack(fmt, sb)[0]
|
||||
except Exception:
|
||||
state[name] = None
|
||||
|
||||
try:
|
||||
pb = os.pread(fd, 8, ctx + 0x540668)
|
||||
p = struct.unpack('<Q', pb)[0]
|
||||
if p > 0x10000:
|
||||
fb = os.pread(fd, 2049*8, p)
|
||||
arr = np.frombuffer(fb, dtype='<f4')
|
||||
mag = np.hypot(arr[0::2], arr[1::2])
|
||||
state['fir_mag0'] = float(mag[0])
|
||||
state['fir_mag43'] = float(mag[43]) if len(mag) > 43 else -1
|
||||
state['fir_mag171'] = float(mag[171]) if len(mag) > 171 else -1
|
||||
state['fir_is_identity'] = bool(np.all(np.abs(mag[:50] - 1.0) < 0.01))
|
||||
except Exception:
|
||||
pass
|
||||
return state
|
||||
|
||||
|
||||
def sampling_phase(fd, host, ctx, t_start):
|
||||
"""Rendersnap-style FIR sampling."""
|
||||
rng = np.random.default_rng(3)
|
||||
prev_sig = None
|
||||
saved = 0
|
||||
while time.time() - t_start < 20:
|
||||
try:
|
||||
os.kill(host, signal.SIGSTOP)
|
||||
except ProcessLookupError:
|
||||
break
|
||||
try:
|
||||
pb = os.pread(fd, 8, ctx + 0x540668)
|
||||
p = struct.unpack('<Q', pb)[0]
|
||||
if p < 0x10000:
|
||||
continue
|
||||
fb = os.pread(fd, NARR * 4, p)
|
||||
arr = np.frombuffer(fb[:2049*8], dtype='<f4').astype(np.float32)
|
||||
sig = arr.tobytes()[:4096]
|
||||
|
||||
rb_ptr = os.pread(fd, 8, ctx + 0x5407f8)
|
||||
rp = struct.unpack('<Q', rb_ptr)[0]
|
||||
rb = os.pread(fd, 2049*4, rp) if rp > 0x10000 else None
|
||||
rsig = rb[:512] if rb else b''
|
||||
|
||||
key = hashlib.md5(sig + rsig).digest()
|
||||
if key != prev_sig:
|
||||
prev_sig = key
|
||||
mag = np.hypot(arr[0::2], arr[1::2])
|
||||
rv = np.frombuffer(rb, dtype='<f4') if rb else None
|
||||
phase = dict(
|
||||
t=round(time.time() - t_start, 3),
|
||||
fir43=float(mag[43]),
|
||||
fir171=float(mag[171]),
|
||||
r43=float(rv[43]) if rv is not None else -1,
|
||||
r171=float(rv[171]) if rv is not None else -1,
|
||||
)
|
||||
store = {}
|
||||
for off in SLOTS_FULL:
|
||||
try:
|
||||
q = os.pread(fd, 8, ctx + off)
|
||||
ptr = struct.unpack('<Q', q)[0]
|
||||
if ptr > 0x10000:
|
||||
ab = os.pread(fd, NARR * 4, ptr)
|
||||
store[hex(off)] = np.frombuffer(ab, dtype='<f4').astype(np.float32)
|
||||
except Exception:
|
||||
pass
|
||||
fn = f'{OUT}/scan{saved:03d}.npz'
|
||||
np.savez_compressed(fn, **store)
|
||||
saved += 1
|
||||
print(f' PHASE {phase} -> {fn}', flush=True)
|
||||
if saved >= 24:
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
os.kill(host, signal.SIGCONT)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
time.sleep(float(rng.uniform(0.001, 0.01)))
|
||||
try:
|
||||
os.kill(host, 0)
|
||||
except ProcessLookupError:
|
||||
print(f' host exited at {time.time()-t_start:.2f}s')
|
||||
break
|
||||
return saved
|
||||
|
||||
|
||||
def main():
|
||||
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; sleep 1", shell=True)
|
||||
wav = rpp.replace('.rpp', '.wav')
|
||||
if 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()
|
||||
all_instances = {} # pid -> [ctx_addrs]
|
||||
|
||||
# Phase 1: aggressive pre-scan (no host needed)
|
||||
print('Pre-scanning for DSP contexts...', flush=True)
|
||||
for att in range(100):
|
||||
found = pre_scan_all()
|
||||
for pid, ctxs in found.items():
|
||||
if pid not in all_instances:
|
||||
all_instances[pid] = ctxs
|
||||
print(f' pid={pid} ctx={[hex(c) for c in ctxs]} at {time.time()-t0:.3f}s', flush=True)
|
||||
if all_instances:
|
||||
break
|
||||
time.sleep(0.0002)
|
||||
|
||||
if not all_instances:
|
||||
print('NO CTX FOUND')
|
||||
proc.kill()
|
||||
return 1
|
||||
|
||||
# Phase 2: read state of each instance
|
||||
for pid, ctxs in all_instances.items():
|
||||
for ctx in ctxs:
|
||||
fd = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
|
||||
state = read_state(fd, ctx)
|
||||
os.close(fd)
|
||||
print(f'\n=== Instance pid={pid} ctx={hex(ctx)} ===')
|
||||
for k, v in sorted(state.items()):
|
||||
if isinstance(v, float):
|
||||
print(f' {k}: {v:.6f}')
|
||||
else:
|
||||
print(f' {k}: {v}')
|
||||
|
||||
# Phase 3: rendersnap-style sampling on the primary
|
||||
primary_pid = min(all_instances.keys())
|
||||
primary_ctx = min(all_instances[primary_pid])
|
||||
print(f'\n--- Sampling primary {hex(primary_ctx)} (pid={primary_pid}) ---')
|
||||
|
||||
fd = os.open(f'/proc/{primary_pid}/mem', os.O_RDONLY)
|
||||
saved = sampling_phase(fd, primary_pid, primary_ctx, time.time())
|
||||
os.close(fd)
|
||||
|
||||
print(f'\ntotal samples={saved}')
|
||||
proc.kill()
|
||||
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