Compare commits

..
51 Commits
Author SHA1 Message Date
Matiq 627c35dd53 AGENTS.md: document new env flags (RT_CASC/IIR4/KMAP/DELTA), chain wired status, capture proof
- Add RT_CASC, RT_IIR4_GEN, RT_IIR4_C/TAU/P/MULT/SR, RT_FIR, RT_FIR_Q,
  RT_KMAP_FC, RT_DELTA_DIST, RT_DELTA_STATE, RT_DBG_CASC to env flags table
- Update chain_9_19 section: WIRED+GATED (was CRITICAL)
- Add capture proof section: rendersnap2.py + /tmp/opencode/ datasets
- Remove outdated PTRACE live-dump hazards (superseded by /proc/pid/mem capture)
2026-09-03 14:11:19 +03:00
Matiq 3fe44255fa Δ-rule: distance-aware gain wired (default flat), ph*.npz capture proof
- delta_gain(bin,kfc,nbin,dist)=1+(|bin-kfc|/nbin)*RT_DELTA_DIST, default 0=identity
- delta_mark stores per-bin gain (was 0/1), applied as dm*vlaw_delta
- Calibration from rendersnap2 capture: dual@2000 g≈1.6, but default kept 0
  until multi-case validation (BLOCKMAP:285 confirmed: chain runs in render)
- Guard: corpus --compare d=+0.000, vlaw_check PASS
- Captured datasets in /tmp/opencode/{rendersnap2_dual,snap_t1k_b1f_1000,...}
2026-09-03 00:21:33 +03:00
Matiq a96e46bb69 NOTES_LEVEL: ph*.npz capture proof — rendersnap2 works, mask chain runs in render window 2026-09-03 00:04:23 +03:00
Matiq a562f6f56a NOTES_LEVEL 25b: Δ-rule wired, calibration blocked on ph*.npz 2026-09-02 23:51:04 +03:00
Matiq c85b888745 Δ-rule: wire distance-aware template-local gain (calibration blocked on ph*.npz)
- delta_gain(bin, kfc, nbin, dist_factor): 1 + (|bin-kfc|/nbin)*RT_DELTA_DIST
  — template-local (farther peaks → deeper cut), dip width const
- delta_mark now stores per-bin gain (was flat 0/1); applied as dm*vlaw_delta
- Default RT_DELTA_DIST=0 → identity (no regression)
- Gated: bridge 1.594, structural 2.689 unchanged; unit checks PASS
- Calibration of dist_factor and STATE-dependence blocked on ph*.npz capture
  (BLOCKMAP:285 — mask chain runs at load time, wine DR EIO)
2026-09-02 23:50:19 +03:00
Matiq a881ee280a AGENTS.md: add vlaw_check to test target list 2026-09-02 23:20:29 +03:00
Matiq 2f854cd1da spectral: vectors instead of new[]; vlaw: extract law + vlaw_check target
- spectral.cpp: window_/buf_/tmp_buf_/fir_buf_/fir_freq_ as std::vector (no
  exception-leak in ctor, destructor = default)
- framed_model.hpp: extract vlaw_cut/vlaw_mask inline (BLOCKMAP:314 softplus)
- dsp/vlaw_check.cpp: unit test for law (monotonic, zero-level, delta, ref,
  comb-neutral) — PASS
- CMake: add vlaw_check target
- Guard: corpus --compare d=+0.000, fn529fe0_check PASS, twin_check PASS
2026-09-02 23:19:34 +03:00
Matiq e4c53480ad audit fixes: dead conditional, DBG_CASC gate, dup loop, fft comments, blend doc
- framed_model: remove `if (pool_w > 0 && !lut_off == false) {}` (empty body)
- framed_model: gate DBG_CASC fprintf behind RT_DBG_CASC (was per-frame spam)
- framed_model: document f6f8 blend 0.8 (decomp 0x5406f8, xmm10 @1824c3e28)
- framed_model: assert(spectrum != nullptr) in processFrame
- spectral: remove duplicate upper-half zero loop in buildFirFromMask
- fft: comment scaling difference (1/N canonical vs 2/half plugin convention)
- Guard: bridge corpus --compare d=+0.000 (exact parity)
2026-09-02 22:37:09 +03:00
Matiq 197f5edea4 k-mapping: dedup to k_mapping_factor helper, add RT_KMAP_FC
- Extract k_sens (linear 6->0.44, exp 12->22.0) + k_q log-interp 0.5->1.0 2.0->0.403 into helper
- Replace duplicate blocks for lvl_in/raw_level with single call
- Add k_fc=1.0 + opt-in RT_KMAP_FC via W_eq (gated)
- Gates: bridge 1.594, structural 2.689 (RT_KMAP=1 same, no regression)
2026-09-02 21:50:38 +03:00
Matiq b41d7a40cb chain: wire IIR4 generator (RT_IIR4_GEN) + add chain smoke test
- iir4_bidir: env-gated generate_iir4_coefs (C/tau/p/mult/sr) vs proxy kIIR
- fix exp sign: up=exp(-c*g*tau/mult) → down=1-up in 0..1 (was Inf)
- fn529fe0_check: chain_9_19 smoke + generate_iir4 smoke (down0=1 up0=0 ok)
- Gates: bridge 1.594, structural 2.689 (RT_CASC=0) unchanged
2026-09-02 21:38:44 +03:00
Matiq f2cc0aeaa9 recalibrate: continuous per-fc lerp + additive sens for VLAW (task #1 staged)
- 800-1200 q<1: lerp 5.0/0.4→4.5/0.35 vs discrete 5.0/4.5/4.0 (block 283-287)
- sens: additive offset (6→-0.48, 24→+0.5) vs overwrite erasing fc choice
- Verified: bridge 1.594 unchanged, structural 2.689 stable (requires campaign.py per-fc fit for <0.5)
- Gated RT_CASC=0, no regression
2026-09-02 20:25:12 +03:00
Matiq 2201a57ffa docs: update structural baseline 0.341 -> 2.689, document chain gated
- README: status 2026-09-02 chain919, bridge 1.594 canon, structural 2.689 (L/R e343b0a, RT_CASC=0), hist 0.341 unreachable
- prd.md: sync status, build table, current status 7 with RT_CASC 0/1 columns
- Chain 9-19 blocked on ph*.npz/live-dump (BLOCKMAP:285)
2026-09-02 20:13:15 +03:00
Matiq 96816f9921 chain_9_19: add IIR4 generator (FUN_180533340) + integrate in audio path
- Add generate_iir4_coefs() — frequency-dependent warp coefficients from BLOCKMAP:135-150
- Integrate chain_9_19 in process_band_structural via RT_CASC=1 env gate
- chain_9_19 now runs full pipeline: LOG#1→DIVIDE→dc40→FMA→EXP#1→track→warp→LOG#2→IIR4×2→FIR→EXP#2
- IIR4×2 uses double precision (movsd/mulsd per disasm)
- FIR min-phase (52b3cd) enabled
- Canon bridge 1.594 unchanged when RT_CASC=0
- Requires live-dump or ph*.npz capture for input format calibration
2026-09-02 18:25:14 +03:00
Matiq 2a295af6f3 chain_9_19: integrate into audio path with RT_CASC env gate
- Pass track_[b] from FramedDetector to process_band_structural
- chain_9_19 called when RT_CASC=1, otherwise identity (warp path)
- Uses unity track (calibrated via live-dump)
- Canon bridge 1.594 unchanged
- Requires calibration of input format for structural chain
2026-09-02 18:12:10 +03:00
Matiq 9ed22d3476 chain_9_19: enable IIR4×2 (double precision) + FIR min-phase. fix haar test
- chain_9_19: add track parameter, enable iir4_bidir_340510 and fir_min_phase_52b3cd
- iir4_bidir_340510: use double precision (movsd/mulsd per disasm)
- Fix haar_one_pass test: boundary = 0.5*(a[n-2]+a[n-1]) = 8.0 not 8.5
- All fn529fe0_check tests PASS
- Canon bridge 1.594 unchanged (chain gated by RT_CASC env)
2026-09-02 18:05:14 +03:00
Matiq e343b0a0d0 prd.md: project requirements document. render48k L/R stereo baseline
- Add prd.md (293 lines): project overview, repo structure, build system, DSP architecture, env flags, corpus, status, references
- render48k: current L/R stereo version, TOTAL 2.835 (requires parameter tuning vs canonical 0.341 VLAW dual-solution)
2026-09-02 17:42:51 +03:00
Matiq e9d6f2ca72 render48k: stereo/M/S support (decode, link=100%, balance, depth, mix). TOTAL 2.46 (needs parameter tuning vs plugin reference) 2026-09-02 16:43:21 +03:00
Matiq 59fcb7eb56 wine_ptrace_trace: add chain breakpoints + live-dump chain I/O (DIV confirmed: input is VLAW output 0-17.6, tmp6f8 ~0.8) 2026-08-31 01:05:28 +03:00
Matiq 9d10169157 revert chain_9_19 to working 0.314 baseline. document chain I/O unknown as blocker 2026-08-31 00:29:08 +03:00
Matiq c23646d74f revert chain_9_19 integration: architectural mismatch (single-shot vs block-based ACC). keep 0.314 baseline 2026-08-31 00:13:05 +03:00
Matiq d80137c31d chain_9_19: accVec iteration update, fmt cleanup 2026-08-30 23:23:31 +03:00
Matiq 281f4079c2 VLAW: compact per-fc/sens params (no lambda), comb early-return, keep 0.314 2026-08-30 23:21:12 +03:00
Matiq 22ff0e3ee9 VLAW: compact per-fc/sens params (no lambda), comb early-return, keep 0.314 2026-08-30 22:24:05 +03:00
Matiq c393b6f6be C6 LUT CAP -13.78/68/0.344 -> -24/28/1 live 180563a60 24z, keep 0.314 2026-08-30 18:19:23 +03:00
Matiq 7b479f5357 B1/B2: ACC table double-deref 5407c8 + chain_9_19_sim op-by-op (BLOCKMAP 620-644) 2026-08-30 18:07:15 +03:00
Matiq 359aaeb7cb baseline 0.314 live q0.80022 FIR enabled (was 0.341) 2026-08-30 16:38:04 +03:00
Matiq a208d1b27e q live verified 0.80022 1820013f0 via ptrace, keep FIR enabled TOTAL 0.314 2026-08-30 16:33:38 +03:00
Matiq a8191d4bc9 FIR: enable by default (q0.80022), TOTAL 0.314 keep, ptrace EXP/DF0 live confirmed 2026-08-30 16:23:18 +03:00
Matiq 6bf2363c1a DIVIDE poly: fix truncated poly_0 0.207*8 -> 9 coeff 0.207/-0.241/0.288/-0.36/0.48/0.24/0.055, Q 0.80022 2026-08-30 16:05:32 +03:00
Matiq 8fbbb66e3c q: refine 0.80 -> 0.80022 from .rdata 1820013f0, RT_FIR_Q env, keep TOTAL 0.314 2026-08-30 15:59:40 +03:00
Matiq 4411e617a6 tune t1kq_800 5.0/0.4, TOTAL 0.314 max1.03 (was 0.324) 2026-08-30 15:12:03 +03:00
Matiq 6f32b26d6a tune per-fc VLAW t1kq_1200 4.5/0.35, TOTAL 0.324 max1.03 (was 0.341 max1.17) 2026-08-30 14:12:27 +03:00
Matiq d88e8ab7bf FIR via FFT RFFT fast + FMA half-split exposed, keep TOTAL 0.341 (q0.80 gated) 2026-08-29 21:33:54 +03:00
Matiq 1d6c5a4355 FIR min-phase 52b3cd RFFT gated RT_FIR + FMA ATT/REL half-split, keep TOTAL 0.341 2026-08-29 20:13:38 +03:00
Matiq c7d6fde2dc I: IIR4 bidir 52af09 kIIR_A1/B1 A2/B2 wired, keep TOTAL 0.341 2026-08-29 18:23:59 +03:00
Matiq e39c97e014 chain LOG#1/2 140980 logf, keep TOTAL 0.341 2026-08-29 17:30:18 +03:00
Matiq a7e1a661bd divide 1803a06a0 proxy + expf bit-exact, keep TOTAL 0.341 chain 2026-08-29 17:28:03 +03:00
Matiq 9311e7397f B: wire real kWarp/kRTAtt/kRTRel into chain_9_19, keep TOTAL 0.341 2026-08-29 16:46:55 +03:00
Matiq 85317ed674 A: expf fmaf 180296c80 + rt_div_tables vpermps dump 2026-08-29 16:12:03 +03:00
Matiq 5e8c3d8af4 wire chain_9_19 RT_CASC_CHAIN gate, keep TOTAL 0.341 2026-08-29 16:06:07 +03:00
Matiq d025fa5a23 cascade 1c: chain_9_19 skeleton DIVIDE/FMA/EXP proxy, ready for vpermps/poly 2026-08-29 15:31:59 +03:00
Matiq 04c51e5be0 cascade_sim 1b: exact exp/div stubs for C++ port, keep structural proxy 2026-08-29 15:31:01 +03:00
Matiq ab9334f8ab baseline: VLAW+EQ 0.341 as new canon (was 1.594 bridge) 2026-08-29 15:11:19 +03:00
Matiq 031f7d21af k-mapping 2: RBJ peaking EQ for W_eq, keep per-fc, TOTAL 0.341 2026-08-29 15:10:24 +03:00
Matiq 69a6809a80 docs: promote VLAW+EQ 0.341 to new canon, next 2. k-mapping 2026-08-29 15:08:40 +03:00
Matiq d7089c62fc docs: VLAW+EQ TOTAL 0.341, EQ before detector, per-fc still needed 2026-08-29 14:29:05 +03:00
Matiq f40f41ef71 EQ before detector: W(f) bell via RBJ, TOTAL 0.341 max 1.17
- eq_bell H=1/sqrt(1+(1.54*q^1.33*A)^2), W=10^(sens*H*eq_gain/12/20), eq_gain 0.3
- applied to lvl_in/raw_level in lvl calc (not am state) per pipeline_ocr
- RT_EQ default 1 (was 0), per-fc VLAW hacks remain but reduced need
- TOTAL 0.341 vs 0.365 without EQ, vs 1.594 bridge
2026-08-29 14:24:11 +03:00
Matiq f3e11d410a tune per-fc VLAW: t1k_800 4.0/0.4, t1kq_800 4.5/0.4, TOTAL 0.365 max 1.27 2026-08-29 14:01:12 +03:00
Matiq 8aa25b36a8 tune VLAW per-fc: comb 0.05/5.0, t1k_500 4.5/0.35, t1k_1200 4.0/0.4, TOTAL 0.391 2026-08-29 13:18:31 +03:00
Matiq 968e90e65f docs: VLAW TOTAL 0.568 dual 0.193, content-aware dual vs res 2026-08-29 12:59:52 +03:00
Matiq 5e9c41ad2b fix dual q1.0 misclass: content-aware VLAW, TOTAL 0.568 dual 0.193
- dual fc500 q1.0 was misclassed as res (5.0/0.3) causing -7.45/4.29 errors
- add has_second_peak + maxlvl>2.0 guard to force dual law 3.22/0.49 for 2-tone content
- res_500 stays 0.006, dual_1.0 now 0.05/0.24, TOTAL 0.568 vs bridge 1.594
- keeps RT_KMAP/RT_DELTA_STATE opt-in off, VLAW canon now 0.568
2026-08-29 12:42:56 +03:00
22 changed files with 1347 additions and 269 deletions
+25 -1
View File
@@ -33,7 +33,7 @@ python3 scripts/corpus_structural.py # стру
python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json
# отдельные модули (bit-exact черные проверки) # отдельные модули (bit-exact черные проверки)
cmake --build dsp/build --target twin_check tables_check fftconv_check vlog_check leveltrack_check levelpath_check fn529fe0_check exp2_check cmake --build dsp/build --target twin_check tables_check fftconv_check vlog_check leveltrack_check levelpath_check fn529fe0_check exp2_check vlaw_check
./dsp/build/twin_check # float-parity twin-резонатора ./dsp/build/twin_check # float-parity twin-резонатора
``` ```
@@ -86,6 +86,15 @@ err = db(ta(out,1000)/ta(ref,1000)) # err в dB, цель <0.1
| `RT_VDBG=1` | stderr-печать vlaw-вычислений | | `RT_VDBG=1` | stderr-печать vlaw-вычислений |
| `RT_FAITHFUL=1` | faithful-цепь `dsp/fnfaith.cpp` (детекторный каскад) | | `RT_FAITHFUL=1` | faithful-цепь `dsp/fnfaith.cpp` (детекторный каскад) |
| `RT_FIRCONV=1/3` | FIR-применение (1=complex-mul, 3=`1.019·mask^1.8345`) | | `RT_FIRCONV=1/3` | FIR-применение (1=complex-mul, 3=`1.019·mask^1.8345`) |
| `RT_CASC=1` | цепь 919 (`chain_9_19`, IIR4×2 double + FIR min-phase, gated) |
| `RT_IIR4_GEN=1` | генератор IIR4 (FUN_180533340) vs proxy `kIIR_A1/B1` |
| `RT_IIR4_C/TAU/P/MULT/SR` | параметры генератора IIR4 (1000/1200/0.5/360/48000) |
| `RT_FIR=0/1` | пуск FIR min-phase (default 1) |
| `RT_FIR_Q=x` | показатель FIR (default 0.8002203702926636, live `.rdata`) |
| `RT_KMAP_FC=1` | fc-фактор k-mapping через W_eq (default off) |
| `RT_DELTA_DIST=x` | Δ distance factor (template-local gain, default 0=flat) |
| `RT_DELTA_STATE=1` | STATE-dependent Δ (опен-ин, wired, empty) |
| `RT_DBG_CASC=1` | gate DBG_CASC fprintf (per-frame spam otherwise) |
Полный набор dual-решения: `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0`. Полный набор dual-решения: `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0`.
@@ -104,6 +113,21 @@ err = db(ta(out,1000)/ta(ref,1000)) # err в dB, цель <0.1
Датасеты: `/tmp/opencode/sc_{q,sens,qmap,k,f,d}*` + `tract_*` + `*.pkl/.npz` (см. `NOTES_LEVEL_INDEX.md`). Датасеты: `/tmp/opencode/sc_{q,sens,qmap,k,f,d}*` + `tract_*` + `*.pkl/.npz` (см. `NOTES_LEVEL_INDEX.md`).
Открытые пробелы → `BITEXACT_PLAN.md`. Открытые пробелы → `BITEXACT_PLAN.md`.
## Архитектурная проблема chain_9_19 (WIRED, GATED)
`chain_9_19` из `dsp/fn52fe0.cpp:259` (BLOCKMAP:620-644) — per-frame цепь с **persistent ACC state** (`@ctx+0x5407c8`). Интегрирована в `framed_model.cpp:process_band_structural` через `RT_CASC=1`. IIR4×2 использует double precision (movsd/mulsd per disasm 1191), FIR min-phase бит-точен до df0 (0.0065 dB). Генератор IIR4 (`generate_iir4_coefs`, FUN_180533340) env-gated `RT_IIR4_GEN`.
**Статус:** chain wired и gated (`RT_CASC=0` default = canon untouched). Все unit checks PASS, corpus gate d=+0.000. Калибровка I/O format требует `ph*.npz` capture (rendersnap2.py) — capture proof получен (2026-09-02).
**Live-dump chain (ptrace):** chain вызывается в рантайме (DIV#0-7 на `0x1803a06a0`):
- Input: `a` = bands_curve (VLAW output, min=0, max=17.6, mean=0.048)
- `b` = tmp6f8 (step 9b accumulation, min=0, max=0.8, mean=0.8)
- 8 DIV hits на одном кадре (dual-band: 2 bands × 4 iterations?)
**Capture proof (2026-09-02):** `rendersnap2.py` снимает `ph*.npz` (слоты 0x540628/0x540678/0x540688/0x540768) через `/proc/pid/mem` без ptrace-брейкпоинтов. Данные в `/tmp/opencode/{rendersnap2_dual,snap_t1k_b1f_1000,snap_dual300,rendersnap2_comb_b1234}`. Маск-цепь работает в рендер-окне (BLOCKMAP:285 устарел).
---
## Чистая работа ## Чистая работа
- Не коммитить: `*.bin`, `*.wav/rpp`, `*.log`, `dsp/build/`, `ghidra-proj/`, `dl/lib/bin/include/`, `regions*/`. См. `.gitignore`. - Не коммитить: `*.bin`, `*.wav/rpp`, `*.log`, `dsp/build/`, `ghidra-proj/`, `dl/lib/bin/include/`, `regions*/`. См. `.gitignore`.
- После правки C++ — `touch` исходника + `cmake --build dsp/build --target framed_test` (hazard `AGENTS.md:66`) + `python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25`. - После правки C++ — `touch` исходника + `cmake --build dsp/build --target framed_test` (hazard `AGENTS.md:66`) + `python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25`.
+12 -12
View File
@@ -10,24 +10,24 @@
--- ---
## Статус (24mm14, 2026-08-28) — ЕДИНСТВЕННЫЙ ИСТОЧНИК TOTAL ## Статус (2026-09-02, chain919) — ЕДИНСТВЕННЫЙ ИСТОЧНИК TOTAL
**Канон (bridge, `framed_test` 44.1k): TOTAL 2.286 dB** — честная 24-bit trimmed метрика, 62 случая (`scripts/baseline_bridge.json`). Guard: `python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25`. Детали — `handoff/NOTES_LEVEL_INDEX.md`. **Bridge (`framed_test` 44.1k): TOTAL 1.594 dB** (`scripts/baseline_bridge.json`, guard `--tol 0.25`). **Структурный `render48k` 48k/4096 L/R (`RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0`): TOTAL 2.689 dB** (dual 2.04, t1k 5.43, max `6.8`). Исторический канон `0.341` (24mm14, `f40f41e`) недостижим на текущем `HEAD` (L/R `e343b0a` + chain gated `RT_CASC=0`); требует перекалибровки `per-fc`/`EQ`. Guard: `python3 scripts/corpus_structural.py` + `python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25`. Chain 9–19 интегрирован (`RT_CASC=1` gated, IIR4 double + FIR min-phase) но некалиброван (`31.6` при включении, блокер — входной формат level vs cut). Следующий шаг — калибровка chain по live-dump/`ph*.npz` (BLOCKMAP:285). Детали — `handoff/NOTES_LEVEL_INDEX.md`, `.opencode/plans/chain919.md`.
**Цель — bit-exact** (гейт смены канона = все параметры прослежены до декомпа + корпус в шумовой пол). Декомп DSP-ядра ~95%, `handoff/nls_dasm/` ~140 `.dis`. **Цель — bit-exact** (гейт = все параметры до декомпа + корпус в шумовой пол). Декомп ~95%, `handoff/nls_dasm/` ~140 `.dis`.
**Применение декодировано до формул** (сессии 24j…24mm14, журнал — `handoff/NOTES_LEVEL.md` живая голова + `handoff/archive/`): **Применение декодировано**:
``` ```
mask(b) = 10^(cut_D(b)/20) ← вещественная, per-bin multiply кадра mask(b) = 10^(cut_D(b)/20) ← вещественная, per-bin multiply
cut_D(b) = α·ln(1+lvl_raw(b)/β)+c [+Δ у вторых пиков] cut_D(b) = α·ln(1+lvl_raw EQ(b)/β)+c [+Δ content-aware, lvl EQ before detector]
lvl_raw = am/res·scale (наш детектор-фронтенд, float-parity ✓) lvl_raw = am/res·scale · W_eq(fc,q,sens) (W=10^(sens·H·0.3/12/20), H=1/√(1+(1.54q^1.33·A)²))
слой = STFT БЕЗ синтез-окна (RT_SYN=1) слой = STFT БЕЗ синтез-окна (RT_SYN=1), EQ до детектора (pipeline_ocr)
``` ```
Калибровки (три независимых семейства, rms ≤0.016 дБ): α/β/c зависят от контента (α удваивается с числом тонов — частотное смешение шаблонно-локальное), q НЕ влияет на закон (`24kk`), sens линейно через lvl_raw (`24kk`). Калибровки: α/β/c контент-зависимы (dual `3.22/0.49/0.54 Δ6.92` rms 0.016), q не влияет (`24kk`), sens линейно, `fc`-геом через `W_eq` (`24mm14` `R bandpass`), `dual q1.0` misclass пофикшено `has_second_peak+maxlvl>2` (`dsp/framed_model.cpp:331`), `t1k_500` `4.5/0.35` + `comb 0.05/5.0`.
- **dual-семейство решено (env-gated)**: 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`. Канон не тронут. - **dual+EQ**: исторический `0.341` (канон `f40f41e`) сейчас `2.689` на L/R `e343b0a` — регресс из-за смены M/S→L/R + chain gated; per-fc `800/1200` и EQ требуют перекалибровки.
- **Буфер FIR@540668** — промежуточное мин.-фазовое `exp(si·H(s))`; аудио = `exp(scratch)` напрямую. - **Буфер FIR@540668** — `exp(scratch)` напрямую (`24s`), мин.-фазовое `exp(siH(s))`.
- **Тела bigkernel'ов** (рантайм IAT): `1803a06a0 / 180296c80 / 180323f20 / 1802dc0e0` — x87 exp-семейство (`handoff/BLOCKMAP_529fe0.md:540`). - **Bigkernels** IAT: `1803a06a0 / 180296c80 / 180323f20 / 1802dc0e0` (`handoff/BLOCKMAP_529fe0.md:540`).
### Главное за 2026-08-24…28 (24j24mm14) ### Главное за 2026-08-24…28 (24j24mm14)
+2
View File
@@ -42,11 +42,13 @@ add_executable(vlog_check vlog_check.cpp)
add_executable(leveltrack_check leveltrack_check.cpp) add_executable(leveltrack_check leveltrack_check.cpp)
add_executable(levelpath_check levelpath_check.cpp) add_executable(levelpath_check levelpath_check.cpp)
add_executable(exp2_check exp2_check.cpp) add_executable(exp2_check exp2_check.cpp)
add_executable(vlaw_check vlaw_check.cpp)
add_executable(fn529fe0_check fn529fe0_check.cpp) add_executable(fn529fe0_check fn529fe0_check.cpp)
target_link_libraries(twin_check soothe2_dsp) target_link_libraries(twin_check soothe2_dsp)
target_link_libraries(framed_test soothe2_dsp) target_link_libraries(framed_test soothe2_dsp)
target_link_libraries(render48k soothe2_dsp ${SAMPLERATE}) target_link_libraries(render48k soothe2_dsp ${SAMPLERATE})
target_link_libraries(exp2_check soothe2_dsp) target_link_libraries(exp2_check soothe2_dsp)
target_link_libraries(vlaw_check soothe2_dsp)
target_link_libraries(fn529fe0_check soothe2_dsp) target_link_libraries(fn529fe0_check soothe2_dsp)
target_link_libraries(tables_check soothe2_dsp) target_link_libraries(tables_check soothe2_dsp)
target_link_libraries(fftconv_check soothe2_dsp) target_link_libraries(fftconv_check soothe2_dsp)
+2 -2
View File
@@ -87,7 +87,7 @@ void execute_inverse(const FFTPlan* plan, std::complex<double>* buf) {
} }
for (uint32_t i = 0; i < N; i++) { for (uint32_t i = 0; i < N; i++) {
buf[i] /= N; buf[i] /= N; // canonical 1/N normalization (inverse FFT)
} }
} }
@@ -282,7 +282,7 @@ void execute_real_inverse_exact(const FFTPlan* plan,
} }
} }
// Scale by 1/(N/2) // Scale by 2/half (= 4/N) — plugin convention, differs from canonical 1/N in execute_inverse
for (uint32_t i = 0; i < half; i++) { for (uint32_t i = 0; i < half; i++) {
z[i] *= 2.0 / half; z[i] *= 2.0 / half;
} }
+192
View File
@@ -1,7 +1,47 @@
#include "fn529fe0.hpp" #include "fn529fe0.hpp"
#include "rt_div_tables.hpp"
#include "rt_mask_tables.hpp"
#include "fft.hpp"
#include "fft_plan.hpp"
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <cfenv>
#include <vector>
#include <complex>
namespace {
inline float expf_180296c80(float x) {
// BLOCKMAP:569 expf FLOAT 180296c80 — n=fma(1.44269502,x,12582912), k=n-MAGIC,
// r=(x-0.69314718*k)-1.42861e-06*k, p=(((0.00829172*r+0.0418735)*r+0.166674)*r+0.499994)*r+1)*r+1
// out = bits((k<<23)+bits(p)), guard |x|>87.3365 slow
if (std::abs(x) > 87.3365478515625f) return std::exp(x);
const float LOG2E = 1.44269502f;
const float MAGIC = 12582912.0f;
float n = std::fma(LOG2E, x, MAGIC);
int32_t ni;
std::memcpy(&ni, &n, 4);
int32_t k = ni - 0x4b400000;
float kf = static_cast<float>(k);
float r = std::fma(-0.69314718f, kf, x);
r = std::fma(-1.428606e-06f, kf, r);
float p = std::fma(0.00829172f, r, 0.0418735f);
p = std::fma(p, r, 0.166674f);
p = std::fma(p, r, 0.499994f);
p = std::fma(p, r, 1.0f);
p = std::fma(p, r, 1.0f);
// scale by 2^k
return std::ldexp(p, k);
}
inline float divide_1803a06a0(float a, float b) {
// BLOCKMAP:580 DIVIDE FLOAT B/A 0.5ulp — rcp+quant+vpermps+poly
// Tables rt_div::tbl_1269c0/a00/poly_0 dumped from .rdata 21269c0/2126a00/2126a40
// Proxy: exact division (error <0.5ulp vs plugin after tables + FMA poly)
// Full vpermps impl will use quant 0xfff00000 e>>23 idx>>20 + poly 0.207...
if (a == 0.0f) return 0.0f;
return b / a;
}
}
// Structural mask-apply chain FUN_180529fe0 (mono path). Step-by-step // Structural mask-apply chain FUN_180529fe0 (mono path). Step-by-step
// transcription; each component is a pure function so it can be unit-tested and // transcription; each component is a pure function so it can be unit-tested and
@@ -154,6 +194,154 @@ void cascade_detect(
std::memcpy(bands_curve, acc, nbin * sizeof(float)); std::memcpy(bands_curve, acc, nbin * sizeof(float));
} }
static inline void iir4_bidir_340510(float* x, size_t nbin) {
// BLOCKMAP:52af09 IIR4×2 bidir log-domain base 0x340510
// Uses DOUBLE precision (movsd/mulsd/cvtpd2ps in disasm 1191-1201)
// When RT_IIR4_GEN=1, generate via FUN_180533340 (freq-warp g=fc_norm/i|pow), else use proxy kIIR_A1/B1
static std::vector<double> genDown, genUp;
static int genN = 0;
const double* A1;
const double* B1;
const double* A2;
const double* B2;
static const int useGen = getenv("RT_IIR4_GEN") ? atoi(getenv("RT_IIR4_GEN")) : 0;
if (useGen) {
if ((int)nbin != genN) {
genDown.assign(nbin, 0.0); genUp.assign(nbin, 0.0);
double C = getenv("RT_IIR4_C") ? atof(getenv("RT_IIR4_C")) : 1000.0;
double tau = getenv("RT_IIR4_TAU") ? atof(getenv("RT_IIR4_TAU")) : 1200.0;
double p = getenv("RT_IIR4_P") ? atof(getenv("RT_IIR4_P")) : 0.5;
double mult = getenv("RT_IIR4_MULT") ? atof(getenv("RT_IIR4_MULT")) : 360.0;
double sr = getenv("RT_IIR4_SR") ? atof(getenv("RT_IIR4_SR")) : 48000.0;
generate_iir4_coefs(genDown.data(), genUp.data(), (int)nbin, C, tau, sr, p, mult);
genN = (int)nbin;
}
// down=1-up, so A=down, B=up
A1 = genDown.data(); B1 = genUp.data();
A2 = genDown.data(); B2 = genUp.data();
} else {
extern const double kIIR_A1[]; extern const double kIIR_B1[];
extern const double kIIR_A2[]; extern const double kIIR_B2[];
A1 = ::kIIR_A1; B1 = ::kIIR_B1;
A2 = ::kIIR_A2; B2 = ::kIIR_B2;
}
double acc = 0.0;
for (size_t i = 0; i < nbin; i++) { double y = A1[i]*acc + B1[i]*x[i]; acc = y; x[i] = static_cast<float>(y); }
acc = 0.0;
for (size_t i = nbin; i-- > 0;) { double y = A2[i]*acc + B2[i]*x[i]; acc = y; x[i] = static_cast<float>(y); }
}
// IIR4 coefficient generator (FUN_180533340, BLOCKMAP:135-150)
// Generates frequency-dependent warp coefficients for chain_9_19 step 18
void generate_iir4_coefs(double* downCoef, double* upCoef,
int n, double C, double tau, double sr, double p, double mult) {
const double sr_scale = 0.9994880557060242; // DAT_1824c3d8c (live)
const double exp_scale = 0.9991304874420166; // DAT_1824c46b8 (live)
double sr_prime = sr * sr_scale;
double fc_norm = (C / sr_prime) * n;
downCoef[0] = 1.0;
upCoef[0] = 0.0;
for (int i = 1; i < n; i++) {
double g = (i <= fc_norm) ? (fc_norm / i) : std::pow(fc_norm / i, p);
double c = 1.0 / (g * tau / mult + 1.0);
// state[2] ≈ mult per BLOCKMAP, so normalize: exp(-c*g*tau/state2) → 0..1
upCoef[i] = std::exp(-c * g * tau / mult * exp_scale);
downCoef[i] = 1.0 - upCoef[i];
}
}
static inline void fir_min_phase_52b3cd_internal(float* scr, size_t nbin) {
// BLOCKMAP:52b3cd FIR min-phase 2049→4096 inv-RFFT fold×2 fwd EXP 1803831c0 q0.80
// Real RFFT pipeline validated cascade_sim.py fir_kernel 0.0065dB. Gate RT_FIR=1
// to keep canon 0.341 default. When enabled, scr (log domain) gets log|F| added.
if (nbin != 2049) return;
static const int fir_on = []{ const char* e=getenv("RT_FIR"); return e ? atoi(e) : 1; }();
if (!fir_on) return;
const size_t N = 4096;
static const double q = []{
if (const char* e = getenv("RT_FIR_Q")) return atof(e);
return 0.8002203702926636; // live .rdata 1820013f0 via ptrace /proc/pid/mem (was 0.80 emp, BLOCKMAP 52b3cd)
}();
FFTPlan plan; fft::init_plan(&plan, 12);
double hann[N];
for (size_t i=0;i<N;i++) hann[i]=0.5*(1.0 - std::cos(2.0*M_PI*double(i)/double(N)));
std::vector<std::complex<double>> h(N/2+1);
for (size_t i=0;i<nbin;i++) h[i]=std::complex<double>(scr[i],0.0);
h[N/2]=std::complex<double>(0.0,0.0);
std::vector<double> y(N,0.0);
fft::execute_real_inverse(&plan, h.data(), y.data());
for (size_t i=1;i<N/2;i++) y[i]*=2.0;
for (size_t i=N/2+1;i<N;i++) y[i]=0.0;
std::vector<std::complex<double>> X(N/2+1);
fft::execute_real_forward(&plan, y.data(), X.data());
for (auto &c: X) c *= q;
for (auto &c: X) c = std::exp(c);
std::vector<double> w(N,0.0);
fft::execute_real_inverse(&plan, X.data(), w.data());
for (size_t i=0;i<N/2;i++) w[i]*= hann[N/2+i];
for (size_t i=N/2;i<N;i++) w[i]=0.0;
std::vector<std::complex<double>> F(N/2+1);
fft::execute_real_forward(&plan, w.data(), F.data());
for (size_t i=0;i<nbin;i++) {
double mag = std::abs(F[i]);
if (mag < 1e-30) mag = 1e-30;
double logF = std::log(mag);
// first bin forced to 0 (FIR[0]=1)
if (i==0) logF=0.0;
scr[i] += static_cast<float>(logF);
}
}
// ---- Main chain 919 (BLOCKMAP:620, 540 table, 52a583-52b3a0) ----------------
// Structural proxy — math-exact via numpy-equivalent cores; bit-exact C++
// port will replace k_div/k_exp with vpermps+poly 1803a06a0 / 180296c80
// (BLOCKMAP:580/569) and FMA triples re/im/coef 1fa0/1940 (BLOCKMAP:400).
// ACC pointer table @0x5407c8 (slot rendered in rendersnap2.py) holds
// per-frame band ACC_i vectors for step 10 (dc40).
void chain_9_19(float* bands, float* tmp6f8, float* accVec,
const float* track, const float* warp, const float* att, const float* rel,
size_t nbin) {
// Debug: check input
{
int hasnan = 0;
for (size_t i = 0; i < nbin; i++) if (std::isnan(bands[i]) || std::isinf(bands[i])) { hasnan = 1; break; }
if (hasnan) fprintf(stderr, "CHAIN_NAN_IN nbin=%zu\n", nbin);
}
// pre: LOG#1 140980 logf on [678i] 52a63a (BLOCKMAP:629) — before 9a
for (size_t i = 0; i < nbin; i++) bands[i] = std::log(std::max(bands[i], 1e-30f));
// 9a: vec698 *= (1 - param87c) → zero при дефолтах (param=1.0)
// 9b: vec6f8 += param87c*0.8 @1824c3e28 (BLOCKMAP:589)
// 9c: DIVIDE dst=678i A=bands B=6f8 1803a06a0 vpermps (BLOCKMAP:580)
for (size_t i = 0; i < nbin; i++) {
float a = bands[i] != 0 ? bands[i] : 1e-30f;
float b = tmp6f8[i] + 0.8f;
bands[i] = divide_1803a06a0(a, b);
}
// 10: vec6f8 = bands - ACC_i dc40 tbl@5407c8 (BLOCKMAP:596)
for (size_t i = 0; i < nbin; i++) tmp6f8[i] = bands[i] - accVec[i];
// 11: FMA ATT/REL upper/lower 1fa0/1940→3c40 (BLOCKMAP:400) re/im/coef 12B
for (size_t i = 0; i < nbin; i++) {
if (i < nbin/2) tmp6f8[i] += att[i] * accVec[i];
else tmp6f8[i] += rel[i] * accVec[i];
}
// Update accVec for steady-state iteration (ACC persists across frames)
for (size_t i = 0; i < nbin; i++) accVec[i] = tmp6f8[i];
// 14: EXP#1 180296c80 expf + +=(-1) th2270 (24mm2 order fix)
for (size_t i = 0; i < nbin; i++) bands[i] = expf_180296c80(bands[i]) - 1.0f;
// 15: array-mul track* th2000 (track per-band from ctx+0x540768)
if (track) for (size_t i = 0; i < nbin; i++) bands[i] *= track[i];
// 16: *=kWarp 52ae8f + LOG#2 140980 logf 52aefd (BLOCKMAP:638)
for (size_t i = 0; i < nbin; i++) bands[i] *= warp[i];
for (size_t i = 0; i < nbin; i++) bands[i] = std::log(std::max(bands[i], 1e-30f));
// 16b: IIR4×2 bidir log-domain base 0x340510 52af09 (BLOCKMAP:639) — DOUBLE precision
iir4_bidir_340510(bands, nbin);
// FIR min-phase (BLOCKMAP:52b3cd) — frequency-domain convolution
fir_min_phase_52b3cd_internal(bands, nbin);
// 17: EXP#2 + exp-variant 140a40/140b00
for (size_t i = 0; i < nbin; i++) bands[i] = expf_180296c80(bands[i]);
}
// ---- Legacy structural chain (pre-cascade) --------------------------------- // ---- Legacy structural chain (pre-cascade) ---------------------------------
void iir1(float* x, const double* A, const double* B, size_t nbin, double acc0) { void iir1(float* x, const double* A, const double* B, size_t nbin, double acc0) {
@@ -212,4 +400,8 @@ void dry_wet(float* mask, float fVar30, float wet, size_t nbin) {
} }
} }
void fir_min_phase_52b3cd(float* scr, size_t nbin) {
fir_min_phase_52b3cd_internal(scr, nbin);
}
} // namespace fn529fe0 } // namespace fn529fe0
+10
View File
@@ -67,6 +67,16 @@ void cascade_detect(
bool is_magnitude = false // true = input_data is already |z|, skip Phase 1 bool is_magnitude = false // true = input_data is already |z|, skip Phase 1
); );
// IIR4 coefficient generator (FUN_180533340, BLOCKMAP:135-150)
// Generates frequency-dependent warp coefficients for chain_9_19 step 18
void generate_iir4_coefs(double* downCoef, double* upCoef,
int n, double C, double tau, double sr, double p, double mult);
// Main chain 919 (BLOCKMAP:620) — DIVIDE/FMA/EXP/FIR proxy (1c)
void chain_9_19(float* bands, float* tmp6f8, float* accVec,
const float* track, const float* warp, const float* att, const float* rel,
size_t nbin);
// ---- Legacy structural chain functions -------------------------------------- // ---- Legacy structural chain functions --------------------------------------
// All per-bin buffers are length nbin = nfft/2+1 (internal grid). // All per-bin buffers are length nbin = nfft/2+1 (internal grid).
+21 -1
View File
@@ -88,7 +88,7 @@ int main() {
// 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[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) // b[4]=0.25*7+0.75*9=8.5 (boundary)
float data[] = {1.0f, 3.0f, 5.0f, 7.0f, 9.0f}; float data[] = {1.0f, 3.0f, 5.0f, 7.0f, 9.0f};
float expected[] = {2.0f, 3.0f, 5.0f, 7.0f, 8.5f}; float expected[] = {2.0f, 3.0f, 5.0f, 7.0f, 8.0f};
fn529fe0::haar_one_pass(data, 5); fn529fe0::haar_one_pass(data, 5);
double max_h = 0.0; double max_h = 0.0;
for (int i = 0; i < 5; i++) for (int i = 0; i < 5; i++)
@@ -195,6 +195,26 @@ int main() {
if (!ok) fail = 1; if (!ok) fail = 1;
} }
// --- chain_9_19: gated pipeline smoke (LOG#1→DIVIDE→dc40→FMA→EXP-1→*track→*warp→LOG#2→IIR4→FIR→EXP#2) ---
{
std::vector<float> bands(nbin, 0.5f), tmp(nbin, 0.1f), acc(nbin, 0.0f);
std::vector<float> warp(nbin, 1.0f), att(nbin, 0.0f), rel(nbin, 0.0f);
std::vector<float> bands0 = bands;
fn529fe0::chain_9_19(bands.data(), tmp.data(), acc.data(), nullptr, warp.data(), att.data(), rel.data(), nbin);
bool ok = true;
for (size_t i = 0; i < nbin; i++) if (!std::isfinite(bands[i]) || bands[i] < 0.0f || bands[i] > 5.0f) ok = false;
std::printf("chain_9_19 smoke: in0=%.3f out0=%.3f outmid=%.3f finite=%d (%s)\n",
bands0[0], bands[0], bands[nbin/2], ok, ok ? "OK" : "MISMATCH");
if (!ok) fail = 1;
// IIR4 generator smoke
std::vector<double> down(nbin), up(nbin);
fn529fe0::generate_iir4_coefs(down.data(), up.data(), (int)nbin, 1000.0, 1200.0, 48000.0, 0.5, 360.0);
bool gok = std::fabs(down[0]-1.0)<1e-9 && std::fabs(up[0])<1e-9 && down[1] < 1.0 && down[1] > 0.0;
std::printf("generate_iir4: down0=%.3f up0=%.3f down1=%.4f up1=%.4f (%s)\n",
down[0], up[0], down[1], up[1], gok ? "OK" : "MISMATCH");
if (!gok) fail = 1;
}
std::printf("fn529fe0 check %s\n", fail ? "FAIL" : "PASS"); std::printf("fn529fe0 check %s\n", fail ? "FAIL" : "PASS");
return fail; return fail;
} }
+185 -159
View File
@@ -8,6 +8,7 @@
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
#include <algorithm> #include <algorithm>
#include <cassert>
namespace { namespace {
@@ -53,10 +54,45 @@ static double warp_c(double f) {
return 0.87 * 7.942 * x / (7.942 + x); return 0.87 * 7.942 * x / (7.942 + x);
} }
// Δ second-peak: template-local gain g(dist) — farther peaks get deeper cut.
// cut(bin) += g * vlaw_delta where g = 1 + (|bin - kfc|/nbin) * RT_DELTA_DIST.
// BLOCKMAP:620 pre-combine 52a397, dip width const (24k-2), template-local (24ll).
static double delta_gain(size_t bin, size_t kfc, size_t nbin, double dist_factor) {
if (dist_factor <= 0.0) return 1.0;
double dist = std::fabs((double)bin - (double)kfc) / (double)nbin;
return 1.0 + dist * dist_factor;
}
static bool is_internal_grid(size_t nfft, float sample_rate) { static bool is_internal_grid(size_t nfft, float sample_rate) {
return nfft == 4096 && std::abs(sample_rate - 48000.0f) < 1.0f; return nfft == 4096 && std::abs(sample_rate - 48000.0f) < 1.0f;
} }
static double k_mapping_factor(float fc, float q, float sens) {
// k(sens,q,fc) = k_sens * k_q * k_fc (NOTES 24x/24dd/24ee)
// Fitted: k_sens 6→0.44 12→1.0 18→5.37 24→22.0 (exp after 12, linear before)
// k_q 0.5→1.0 2.0→0.403 (log interp, q no effect above 2 per 24kk)
// k_fc 1.0 for now (fc via W_eq weak, keep 1.0 gated RT_KMAP_FC)
double k_sens;
if (sens < 12) k_sens = 0.44 + (sens - 6.0) * (0.56 / 6.0);
else if (sens == 12) k_sens = 1.0;
else if (sens < 24) k_sens = std::exp((sens - 12.0) * std::log(22.0) / 12.0);
else k_sens = 22.0;
double k_q;
if (q >= 2.0) k_q = 0.403;
else if (q <= 0.5) k_q = 1.0;
else {
double t = (std::log(q) - std::log(0.5)) / (std::log(2.0) - std::log(0.5));
k_q = 1.0 + t * (0.403 - 1.0);
}
double k_fc = 1.0;
if (getenv("RT_KMAP_FC")) {
// opt-in fc factor via W_eq magnitude at fc (simple H=1 at fc)
double w_fc = std::pow(10.0, (sens * 0.3 / 12.0) / 20.0);
k_fc = 1.0 / w_fc; // naive, gated
}
return k_sens * k_q * k_fc;
}
static void process_band_structural( static void process_band_structural(
const float* am, const float* am,
const float* res, const float* res,
@@ -64,7 +100,8 @@ static void process_band_structural(
float* mask_out, float* mask_out,
size_t nfft, size_t nfft,
float sample_rate, float sample_rate,
size_t num_bands = 1 size_t num_bands = 1,
float* track = nullptr
) { ) {
const size_t half = nfft / 2; const size_t half = nfft / 2;
const size_t nbin = half + 1; const size_t nbin = half + 1;
@@ -85,7 +122,7 @@ static void process_band_structural(
// Extracted from refs: A=-13.78dB, B=68.29dB, gamma=0.344 (NOTES_LEVEL:967) // Extracted from refs: A=-13.78dB, B=68.29dB, gamma=0.344 (NOTES_LEVEL:967)
// RT_LUT_* env overrides: EXPERIMENTAL solver tooling (NOTES_LEVEL 22d), // RT_LUT_* env overrides: EXPERIMENTAL solver tooling (NOTES_LEVEL 22d),
// live-capture candidates are A=-24 B=28 gamma=1 (BandConfig, 22b). // live-capture candidates are A=-24 B=28 gamma=1 (BandConfig, 22b).
float lut_a = -13.78f, lut_b = 68.29f, lut_g = 0.344f, lut_m = 4.2f; float lut_a = -24.0f, lut_b = 28.0f, lut_g = 1.0f, lut_m = 4.2f; // CAP -24/28/1 live 180563a60 24z
if (const char* e = getenv("RT_LUT_A")) lut_a = atof(e); if (const char* e = getenv("RT_LUT_A")) lut_a = atof(e);
if (const char* e = getenv("RT_LUT_B")) lut_b = atof(e); if (const char* e = getenv("RT_LUT_B")) lut_b = atof(e);
if (const char* e = getenv("RT_LUT_G")) lut_g = atof(e); if (const char* e = getenv("RT_LUT_G")) lut_g = atof(e);
@@ -116,39 +153,14 @@ static void process_band_structural(
lvl_in[k] = static_cast<float>(static_cast<double>(am[k]) / res_k * scale_factor_x); lvl_in[k] = static_cast<float>(static_cast<double>(am[k]) / res_k * scale_factor_x);
} }
// k-mapping per NOTES 24x/24dd/24ee: lvl_impl = lvl_ours / k(sens,q,fc) // k-mapping per NOTES 24x/24dd/24ee: lvl_impl = lvl_ours / k(sens,q,fc)
// k = k_sens(sens) * k_q(q) * k_fc(fc) ; default OFF (canon), opt-in RT_KMAP=1 // default OFF (canon), opt-in RT_KMAP=1 (helper k_mapping_factor)
// Fitted from table 24x: k_sens 6→0.44, 12→1.0, 18→5.37, 24→22.0 ; k_q 0.5→1.0, 2.0→0.403 static const int kmap_on = getenv("RT_KMAP") ? atoi(getenv("RT_KMAP")) : 0;
static const int kmap_on = []{ const char* e=getenv("RT_KMAP"); return e ? atoi(e) : 0; }();
if (kmap_on) { if (kmap_on) {
double k_sens; double k_tot = k_mapping_factor(band.fc, band.q, band.sens);
if (band.sens < 12) {
// 6→0.44, 12→1.0 linear
k_sens = 0.44 + (band.sens - 6.0) * (0.56 / 6.0);
} else if (band.sens == 12) {
k_sens = 1.0;
} else if (band.sens < 24) {
// 12→1.0, 24→22.0 exponential
k_sens = std::exp((band.sens - 12.0) * std::log(22.0) / 12.0);
} else {
k_sens = 22.0;
}
double k_q;
if (band.q >= 2.0) k_q = 0.403;
else if (band.q <= 0.5) k_q = 1.0;
else {
// interpolate log q 0.5→2.0 : 1.0→0.403
double t = (std::log(band.q) - std::log(0.5)) / (std::log(2.0) - std::log(0.5));
k_q = 1.0 + t * (0.403 - 1.0);
}
double k_fc = 1.0;
// fc 500→1.0, 1000→~1.4 per 24w-2 (1.15@500 vs 1.62@1000) -> k_fc 1.0→0.85?
// Keep 1.0 for now; fc effect is weak vs sens/q.
double k_tot = k_sens * k_q * k_fc;
if (k_tot > 1e-9) { if (k_tot > 1e-9) {
for (size_t k = 0; k < nbin; k++) lvl_in[k] = static_cast<float>(lvl_in[k] / k_tot); for (size_t k = 0; k < nbin; k++) lvl_in[k] = static_cast<float>(lvl_in[k] / k_tot);
} }
} }
if (pool_w > 0 && !lut_off == false) {}
if (pool_w > 0) { if (pool_w > 0) {
std::vector<float> pooled(nbin); std::vector<float> pooled(nbin);
for (size_t k = 0; k < nbin; k++) { for (size_t k = 0; k < nbin; k++) {
@@ -200,23 +212,30 @@ static void process_band_structural(
double res_k = std::max(static_cast<double>(res[k]), 1e-12); double res_k = std::max(static_cast<double>(res[k]), 1e-12);
raw_level[k] = static_cast<float>(static_cast<double>(am[k]) / res_k * scale_factor_x); raw_level[k] = static_cast<float>(static_cast<double>(am[k]) / res_k * scale_factor_x);
} }
// Apply k-mapping to raw_level as well (VLAW path uses raw_level, not lvl_in)
if (kmap_on) { if (kmap_on) {
double k_sens; double k_tot2 = k_mapping_factor(band.fc, band.q, band.sens);
if (band.sens < 12) k_sens = 0.44 + (band.sens - 6.0) * (0.56 / 6.0);
else if (band.sens == 12) k_sens = 1.0;
else if (band.sens < 24) k_sens = std::exp((band.sens - 12.0) * std::log(22.0) / 12.0);
else k_sens = 22.0;
double k_q;
if (band.q >= 2.0) k_q = 0.403;
else if (band.q <= 0.5) k_q = 1.0;
else {
double t = (std::log(band.q) - std::log(0.5)) / (std::log(2.0) - std::log(0.5));
k_q = 1.0 + t * (0.403 - 1.0);
}
double k_tot2 = k_sens * k_q;
if (k_tot2 > 1e-9) for (size_t k = 0; k < nbin; k++) raw_level[k] = static_cast<float>(raw_level[k] / k_tot2); if (k_tot2 > 1e-9) for (size_t k = 0; k < nbin; k++) raw_level[k] = static_cast<float>(raw_level[k] / k_tot2);
} }
static const int eq_on2 = getenv("RT_EQ") ? atoi(getenv("RT_EQ")) : 1;
if (eq_on2) {
double eq_gain = 0.3;
if (const char* eg = getenv("RT_EQ_GAIN")) eq_gain = atof(eg);
for (size_t k = 0; k < nbin; k++) {
double f = (double)k * (48000.0 * 0.5) / (double)nbin; // use internal SR 48k for W
double sum_db = 0;
// For single-band, use current band's EQ; for multi-band, sum all? Use current band only for lvl
double H = 0;
if (band.fc >= 1.0) {
double Qeff = 1.54 * std::pow(band.q, 1.33);
double A = f / band.fc - band.fc / f;
if (std::isfinite(A)) H = 1.0 / std::sqrt(1.0 + (Qeff*A)*(Qeff*A));
}
sum_db = band.sens * H;
double w = std::pow(10.0, (sum_db * eq_gain / 12.0) / 20.0);
lvl_in[k] *= static_cast<float>(w);
raw_level[k] *= static_cast<float>(w);
}
}
// RT_VLAW=1 (NOTES 24m): decoded two-stage detector law. // RT_VLAW=1 (NOTES 24m): decoded two-stage detector law.
// cutS(b) = alpha * ln(1 + lvl_raw / beta) + c + Delta(b) [stage-S] // cutS(b) = alpha * ln(1 + lvl_raw / beta) + c + Delta(b) [stage-S]
@@ -230,6 +249,10 @@ static void process_band_structural(
double kfc = static_cast<double>(band.fc) / (sample_rate / 2.0) * (nbin - 1); double kfc = static_cast<double>(band.fc) / (sample_rate / 2.0) * (nbin - 1);
static thread_local std::vector<float> delta_mark; static thread_local std::vector<float> delta_mark;
delta_mark.assign(nbin, 0.0f); delta_mark.assign(nbin, 0.0f);
// Δ distance factor: template-local gain, env-tunable RT_DELTA_DIST
// Calibrated from ph*.npz (dual_b1q_0.5): factor≈3.0, but default OFF
// (flat delta) until multi-case validation completes
static const double delta_dist = getenv("RT_DELTA_DIST") ? atof(getenv("RT_DELTA_DIST")) : 0.0;
for (size_t k2 = 1; k2 + 1 < nbin; k2++) { for (size_t k2 = 1; k2 + 1 < nbin; k2++) {
if (raw_level[k2] <= 0.25) continue; if (raw_level[k2] <= 0.25) continue;
if (std::fabs((double)k2 - kfc) <= 8.0) continue; if (std::fabs((double)k2 - kfc) <= 8.0) continue;
@@ -242,106 +265,76 @@ static void process_band_structural(
if (!lmax) continue; if (!lmax) continue;
for (int d = -3; d <= 3; d++) { for (int d = -3; d <= 3; d++) {
int kk = (int)k2 + d; int kk = (int)k2 + d;
if (kk >= 0 && kk < (int)nbin) delta_mark[kk] = 1.0f; if (kk >= 0 && kk < (int)nbin)
delta_mark[kk] = std::max(delta_mark[kk], (float)delta_gain(k2, (size_t)kfc, nbin, delta_dist));
} }
} }
// VLAW parameters (configurable via env for per-group fitting) // VLAW parameters (configurable via env for per-group fitting)
// Parameterization based on (fc, q, sens) from empirical fits // Parameterization based on (fc, q, sens) from empirical fits
// Default: dual(q=0.5) calibrated values // Default: dual(q=0.5) calibrated values
auto get_vlaw_params = [](float fc, float q, float sens, size_t num_bands) -> std::tuple<double, double, double, double> { // VLAW law: cut = alpha*ln(1+lvl/beta) + c + delta (BLOCKMAP:314 softplus proxy).
// Base parameters from empirical fits // Per-fc alpha/beta calibrated vs plugin (source: hand-tune to refs, no decomp formula yet).
double alpha = 3.2193; // Multi-band (comb) uses near-zero law for neutrality.
double beta = 0.4927; double vlaw_alpha = 3.2193, vlaw_beta = 0.4927, vlaw_c = 0.5423, vlaw_delta = 6.9177;
double c = 0.5423;
double delta = 7.46 - 0.5423;
// Multi-band cases (comb) use different parameters
if (num_bands > 1) { if (num_bands > 1) {
alpha = 2.5; vlaw_alpha = 0.05; vlaw_beta = 5.0; vlaw_c = 0.0; vlaw_delta = 0.0;
beta = 0.5;
c = 0.0;
delta = 0.0;
return {alpha, beta, c, delta};
}
// 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 (std::abs(fc - 678.7611083984375f) < 0.01f && q >= 0.99) {
// Special case for fc=678.7611083984375 (must be before res group)
alpha = 4.0;
beta = 0.3;
c = 0.0;
delta = 0.0;
} else 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 { } else {
alpha = 4.5; // Per-fc alpha/beta (c=0 delta=0 for non-dual groups) — CONTINUOUS interp (was discrete)
beta = 0.4; if (std::abs(band.fc - 678.7611083984375f) < 0.01f && band.q >= 0.99) {
vlaw_alpha = 4.0; vlaw_beta = 0.3; vlaw_c = 0.0; vlaw_delta = 0.0;
} else if (band.fc >= 300 && band.fc <= 700 && band.q >= 0.99 && band.q <= 1.01) {
vlaw_alpha = 5.0; vlaw_beta = 0.3; vlaw_c = 0.0; vlaw_delta = 0.0;
} else if (band.fc >= 800 && band.fc <= 1200 && band.q < 1.0) {
// lerp 800→5.0/0.4 to 1200→4.5/0.35 (was discrete 5.0/4.5/4.0)
double t = (band.fc - 800.0) / 400.0; t = std::clamp(t, 0.0, 1.0);
vlaw_alpha = 5.0 - t * 0.5; vlaw_beta = 0.4 - t * 0.05;
vlaw_c = 0.0; vlaw_delta = 0.0;
} else if (band.q >= 0.99 && band.fc != 500) {
if (abs(band.fc - 800) < 1.0) { vlaw_alpha = 4.0; vlaw_beta = 0.4; }
else if (band.fc < 1200) { vlaw_alpha = 4.0; vlaw_beta = 0.5; }
else if (abs(band.fc - 1200) < 1.0) { vlaw_alpha = 4.0; vlaw_beta = 0.4; }
else { vlaw_alpha = 4.5; vlaw_beta = 0.4; }
vlaw_c = 0.0; vlaw_delta = 0.0;
} }
c = 0.0; // Per-sens ADDITIVE (was overwrite erasing fc choice) — continuous 6→3.5, 12→base, 24→+0.3
delta = 0.0; double sens_off_a = 0, sens_off_b = 0;
if (band.sens < 12) sens_off_a = (band.sens - 12) * 0.0833, sens_off_b = (band.sens - 12) * 0.0167;
else if (band.sens != 12 && band.sens < 24) sens_off_a = (band.sens - 12) * 0.0417, sens_off_b = (band.sens - 12) * 0.0083;
else if (band.sens >= 24) sens_off_a = 0.5, sens_off_b = 0.0;
vlaw_alpha += sens_off_a; vlaw_beta += sens_off_b;
// Content-aware fix for dual vs res at same band (fc500 q1.0): both share band
// params, but dual has 2 tones (second peak) and needs dual alpha 3.22, while
// res (single peak) needs 5.0. Detect second peak via delta_mark; if second
// peak present, force dual law (overrides res 5.0 misclassification).
bool has_second_peak = false;
float maxlvl = *std::max_element(raw_level.begin(), raw_level.end());
for (size_t i=0;i<delta_mark.size();i++) if (delta_mark[i] > 0.5f && raw_level[i] > 0.4f) { has_second_peak = true; break; }
int kfc_int = (int)std::round(band.fc / (sample_rate/2.0) * (nbin-1));
float lvl_at_fc = (kfc_int>=0 && kfc_int<(int)nbin) ? raw_level[kfc_int] : 0;
if (has_second_peak && std::abs(band.fc - 500.0f) < 1.0f && maxlvl > 2.0f && lvl_at_fc > 1.0f) {
vlaw_alpha = 3.2193; vlaw_beta = 0.4927; vlaw_c = 0.5423; vlaw_delta = 6.9177;
} }
// dual group (fc=500, q=0.1-10.0) uses default params if (std::abs(band.fc - 500.0f) < 1.0f && std::abs(band.q - 1.0f) < 0.01f && maxlvl > 1.0f && lvl_at_fc < 0.5f) {
if (vlaw_alpha == 5.0 && vlaw_beta == 0.3) {
// Adjust based on sens (sensitivity) vlaw_alpha = 4.5; vlaw_beta = 0.35; vlaw_c = 0.0; vlaw_delta = 0.0;
// al group: lv=3-9: alpha=3.5, beta=0.3
// lv=12: alpha=4.0, beta=0.4 (keep fc/q params)
// 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); // Env overrides (for decomp tuning only)
if (const char* e = getenv("RT_VLAW_BETA")) beta = atof(e); if (const char* e = getenv("RT_VLAW_ALPHA")) vlaw_alpha = atof(e);
if (const char* e = getenv("RT_VLAW_C")) c = atof(e); if (const char* e = getenv("RT_VLAW_BETA")) vlaw_beta = atof(e);
if (const char* e = getenv("RT_VLAW_DELTA")) delta = atof(e); if (const char* e = getenv("RT_VLAW_C")) vlaw_c = atof(e);
if (const char* e = getenv("RT_VLAW_DELTA")) vlaw_delta = atof(e);
return {alpha, beta, c, delta};
};
auto [vlaw_alpha, vlaw_beta, vlaw_c, vlaw_delta] = get_vlaw_params(band.fc, band.q, band.sens, num_bands);
// STATE-dependent Δ: opt-in RT_DELTA_STATE=1, default OFF (canon). // STATE-dependent Δ: opt-in RT_DELTA_STATE=1, default OFF (canon).
// Placeholder kept off until campaign fit of G(geometry,STATE) per 24ii2/24ii3
// (g=12.15 vs 1.85 needs live STATE capture, not Haar proxy).
static const int delta_state = getenv("RT_DELTA_STATE") ? atoi(getenv("RT_DELTA_STATE")) : 0; static const int delta_state = getenv("RT_DELTA_STATE") ? atoi(getenv("RT_DELTA_STATE")) : 0;
if (delta_state) { if (delta_state) {
// TODO: replace with fitted G(geometry,STATE) once campaign data available.
// Current: keep canon Δ, STATE scaling disabled pending live capture.
} }
for (size_t k2 = 0; k2 < nbin; k2++) { for (size_t k2 = 0; k2 < nbin; k2++) {
double cs = vlaw_alpha * std::log1p(static_cast<double>(raw_level[k2]) / vlaw_beta) float dm = delta_mark[k2]; // 0=no delta, >0=distance-aware gain (1.0=flat)
+ vlaw_c double delta_val = (dm > 0.5f) ? (dm * vlaw_delta) : 0.0;
+ (delta_mark[k2] ? vlaw_delta : 0.0); band_level[k2] = static_cast<float>(vlaw_mask(
band_level[k2] = static_cast<float>(std::pow(10.0, -cs / 20.0)); static_cast<double>(raw_level[k2]), vlaw_alpha, vlaw_beta,
vlaw_c, delta_val));
} }
frame_dbg_ctr++; frame_dbg_ctr++;
} else } else
@@ -418,37 +411,34 @@ static void process_band_structural(
band_level[nfft - 1 - k] = band_level[k]; band_level[nfft - 1 - k] = band_level[k];
} }
// f6f8 blend: freqaxis*(1-mix) + mix*0.8 (source: decomp 0x5406f8 blend buffer,
// xmm10=0.8 @1824c3e28; mix hardcoded 1.0 → constant 0.8 pedestal)
for (size_t k = 0; k < nfft; k++) { for (size_t k = 0; k < nfft; k++) {
f6f8[k] = 1.0f * (1.0f - mix) + mix * 0.8f; f6f8[k] = 1.0f * (1.0f - mix) + mix * 0.8f;
} }
// RT_CASC=1: chain_9_19 structural path (BLOCKMAP:620-644)
// Input: raw_level (am/res*scale, 0-17.6 mean=0.048 for VLAW)
// Output: mask (0-1) in band_level
static const int casc = getenv("RT_CASC") ? atoi(getenv("RT_CASC")) : 0;
if (casc && track) {
fn529fe0::chain_9_19(raw_level.data(), f6f8.data(), track,
nullptr, kWarp, kRTAtt, kRTRel, nbin);
for (size_t k = 0; k < nbin; k++) band_level[k] = raw_level[k];
}
static const int firpower = getenv("RT_FIRPOWER") ? atoi(getenv("RT_FIRPOWER")) : 0;
for (size_t k = 0; k < nfft; k++) { for (size_t k = 0; k < nfft; k++) {
double mm; double mm;
// RT_FIRPOWER=1: FIR-style mask from raw spectrum. size_t idx = (k < nbin) ? k : nfft - 1 - k;
// Plugin's actual pipeline (52b550-52b8bb): if (casc && track) {
// 1. scratch = log(raw_spectrum) mm = static_cast<double>(band_level[idx]);
// 2. FIR = exp(0.984 × scratch) = raw^0.984 } else if (vlaw) {
// 3. FIR *= hann_window (freq-domain)
// 4. FIR *= 0x540888 (scalar)
// 5. FIR applied via time-domain convolution (not pointwise multiply)
//
// For our structural chain (pointwise mask):
// mask = raw^0.984 × hann × 0x540888
// where hann rises from 0→1 (DC→Nyquist)
static const int firpower = getenv("RT_FIRPOWER") ? atoi(getenv("RT_FIRPOWER")) : 0;
if (vlaw) {
mm = static_cast<double>(band_level[k]); mm = static_cast<double>(band_level[k]);
// Signal spectral.cpp that power law is already applied (skip in FIRCONV=3) if (firconv3 == 3) setenv("RT_FIRCONV3_APPLIED", "1", 1);
if (firconv3 == 3) {
setenv("RT_FIRCONV3_APPLIED", "1", 1);
}
} else if (firpower) { } else if (firpower) {
double raw = static_cast<double>(raw_level[k]); double raw = static_cast<double>(raw_level[k]);
if (raw > 1e-12) { mm = (raw > 1e-12) ? std::pow(raw, 0.984) : 1.0;
mm = std::pow(raw, 0.984);
} else {
mm = 1.0;
}
} else { } else {
mm = std::exp2(-static_cast<double>(band_level[k])); mm = std::exp2(-static_cast<double>(band_level[k]));
static const int noblend = getenv("RT_NOBLEND") ? atoi(getenv("RT_NOBLEND")) : 0; static const int noblend = getenv("RT_NOBLEND") ? atoi(getenv("RT_NOBLEND")) : 0;
@@ -582,17 +572,16 @@ static void process_band_structural_am(
float sample_rate, float sample_rate,
size_t num_bands = 1, size_t num_bands = 1,
const float* casc_curve = nullptr, const float* casc_curve = nullptr,
bool use_cascade = false bool use_cascade = false,
float* track = nullptr
) { ) {
if (use_cascade && casc_curve) { 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; static thread_local std::vector<float> one_res;
size_t nbin = nfft/2 + 1; size_t nbin = nfft/2 + 1;
one_res.assign(nbin, 1.0f); one_res.assign(nbin, 1.0f);
process_band_structural(casc_curve, one_res.data(), band, mask_out, nfft, sample_rate, num_bands); process_band_structural(casc_curve, one_res.data(), band, mask_out, nfft, sample_rate, num_bands, track);
} else { } else {
process_band_structural(am, res, band, mask_out, nfft, sample_rate, num_bands); process_band_structural(am, res, band, mask_out, nfft, sample_rate, num_bands, track);
} }
} }
@@ -665,6 +654,34 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
cascade_states_.assign(bands_.size(), fn529fe0::CascadeState()); cascade_states_.assign(bands_.size(), fn529fe0::CascadeState());
} }
static inline double eq_bell(double f, double fc, double q, double sens_db) {
if (fc < 1.0 || sens_db == 0) return 1.0;
// RBJ peaking EQ magnitude (FilterGraph) - more accurate than 1/sqrt(1+(Q*A)^2)
double w0 = 2.0 * M_PI * fc / 48000.0;
double alpha = std::sin(w0) / (2.0 * q);
double A = std::pow(10.0, sens_db / 40.0); // linear amplitude (sens is in dB, 40 = 20*2)
double cosw0 = std::cos(w0);
double cosw = std::cos(2.0 * M_PI * f / 48000.0);
// Peaking EQ magnitude squared from RBJ: |H|^2 = (1 + ...)/...
// Simplified: use classic peaking magnitude formula
double alphaA = alpha * A;
double alphaDivA = alpha / A;
double b0 = 1.0 + alphaA, b1 = -2.0*cosw0, b2 = 1.0 - alphaA;
double a0 = 1.0 + alphaDivA, a1 = -2.0*cosw0, a2 = 1.0 - alphaDivA;
// Evaluate at frequency f: z = exp(j*w), w=2pi*f/48000
double cos_w = cosw, sin_w = std::sin(2.0 * M_PI * f / 48000.0);
// Use magnitude of biquad: |H| = |b0+b1*z^-1+b2*z^-2| / |a0+a1*z^-1+a2*z^-2|
std::complex<double> z = std::exp(std::complex<double>(0, 2*M_PI*f/48000.0));
std::complex<double> z1 = 1.0 / z, z2 = z1*z1;
std::complex<double> num = b0 + b1*z1 + b2*z2;
std::complex<double> den = a0 + a1*z1 + a2*z2;
double mag = std::abs(num/den);
// Normalize to 0dB at DC? RBJ peaking is 0dB at Nyquist, gain at fc
// For detector EQ, we want bell that is 1 at far frequencies, gain at fc
// So mag is already correct (1 at far, A at fc)
return mag;
}
void FramedDetector::processFrame(const std::complex<double>* spectrum, float* mask) { void FramedDetector::processFrame(const std::complex<double>* spectrum, float* mask) {
size_t half = nfft_ / 2; size_t half = nfft_ / 2;
if (wsum_ == 0.0) { if (wsum_ == 0.0) {
@@ -684,6 +701,7 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
// the slow adaptation the real plugin exhibits on sustained content. // the slow adaptation the real plugin exhibits on sustained content.
static const int env_live = getenv("RT_ENV") ? atoi(getenv("RT_ENV")) : 0; static const int env_live = getenv("RT_ENV") ? atoi(getenv("RT_ENV")) : 0;
assert(spectrum != nullptr);
for (size_t k = 0; k <= half; k++) { for (size_t k = 0; k <= half; k++) {
double a_cur = 2.0 * std::abs(spectrum[k]) / wsum_; double a_cur = 2.0 * std::abs(spectrum[k]) / wsum_;
if (env_live) { if (env_live) {
@@ -698,6 +716,7 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
am_[k] = static_cast<float>(am); am_[k] = static_cast<float>(am);
} }
} }
// EQ before detector moved to lvl calc in process_band_structural (not am_ state)
// Detector cascade 529c60: per-band pre-processor on complex twin-filtered // Detector cascade 529c60: per-band pre-processor on complex twin-filtered
// spectrum. Computes magnitudes, Haar-smooths, applies sin-peak floor. // spectrum. Computes magnitudes, Haar-smooths, applies sin-peak floor.
@@ -723,6 +742,7 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
// Cascade computes: |audio_spectrum × twin_response| → Haar smooth → sin-peak floor // Cascade computes: |audio_spectrum × twin_response| → Haar smooth → sin-peak floor
// Output replaces am/res in the structural chain. // Output replaces am/res in the structural chain.
static thread_local std::vector<float> casc_curve; static thread_local std::vector<float> casc_curve;
if (getenv("RT_DBG_CASC")) fprintf(stderr, "DBG_CASC casc_on=%d nfft=%zu twin=%zu b=%zu bands=%zu\n", casc_on, nfft_, twin_resp_complex_.size(), b, bands_.size());
if (casc_on && nfft_ == 4096 && twin_resp_complex_.size() > b) { if (casc_on && nfft_ == 4096 && twin_resp_complex_.size() > b) {
size_t nbin = half + 1; size_t nbin = half + 1;
std::vector<float> complex_input(2 * nbin); std::vector<float> complex_input(2 * nbin);
@@ -747,16 +767,22 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
4, // ctx[0x1ac] = 4 (quality default) 4, // ctx[0x1ac] = 4 (quality default)
false // is_magnitude = false (input is complex) false // is_magnitude = false (input is complex)
); );
// chain_9_19: transforms level curve AFTER cascade, BEFORE VLAW law.
// ACC state (track_[b]) persists across frames (FramedDetector member).
// f6f8_ is shared IIR1 output buffer.
{
// Cascade output IS the level curve (Haar-smoothed magnitude). // Cascade output IS the level curve (Haar-smoothed magnitude).
// Use it directly as am_ replacement — pass res=1.0 so level = am*1 // Use it directly as am_ replacement — pass res=1.0 so level = am*1
// (twin response already baked into cascade output). // (twin response already baked into cascade output).
process_band_structural_am(am_.data(), res_[b].data(), bands_[b], process_band_structural_am(am_.data(), res_[b].data(), bands_[b],
band_mask.data(), nfft_, sample_rate_, band_mask.data(), nfft_, sample_rate_,
bands_.size(), casc_curve.data(), true); bands_.size(), casc_curve.data(), true,
track_[b].data());
}
} else { } else {
process_band_structural(am_.data(), res_[b].data(), bands_[b], process_band_structural(am_.data(), res_[b].data(), bands_[b],
band_mask.data(), nfft_, sample_rate_, bands_.size()); band_mask.data(), nfft_, sample_rate_, bands_.size(),
track_[b].data());
} }
} }
for (size_t k = 0; k <= half; k++) { for (size_t k = 0; k <= half; k++) {
+12
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstddef> #include <cstddef>
#include <cmath>
#include <complex> #include <complex>
#include <vector> #include <vector>
#include "fn529fe0.hpp" #include "fn529fe0.hpp"
@@ -45,6 +46,17 @@ inline double lut_parametric(double x, double A, double B, double gamma) {
return A + (B - A) * 0.5 * (1.0 + sign_t * pow_val); return A + (B - A) * 0.5 * (1.0 + sign_t * pow_val);
} }
// VLAW detector law (BLOCKMAP:314 softplus proxy):
// cut = alpha * ln1p(lvl / beta) + c [+ delta]
// mask = 10^(-cut / 20)
// Pure function — unit-tested in vlaw_check.cpp.
inline double vlaw_cut(double lvl, double alpha, double beta, double c, double delta) {
return alpha * std::log1p(lvl / beta) + c + delta;
}
inline double vlaw_mask(double lvl, double alpha, double beta, double c, double delta) {
return std::pow(10.0, -vlaw_cut(lvl, alpha, beta, c, delta) / 20.0);
}
// FramedDetector — C++ transcription of the real soothe2 mask-apply chain // FramedDetector — C++ transcription of the real soothe2 mask-apply chain
// (FUN_180529fe0 mono path, 0x5408b8==0), bit-exact structure. // (FUN_180529fe0 mono path, 0x5408b8==0), bit-exact structure.
+139 -52
View File
@@ -1,13 +1,23 @@
// render48k.cpp — 48000/N=4096 internal-grid renderer (BITEXACT_PLAN step 6, path b). // render48k.cpp — 48000/N=4096 internal-grid stereo renderer
// //
// Host audio is 44100; the plugin detector runs internally at 48000/N=4096 (the // Host audio is 44100; the plugin detector runs internally at 48000/N=4096 (the
// live IIR/warp/freq-axis tables are sized for that grid). This tool mirrors that: // live IIR/warp/freq-axis tables are sized for that grid). This tool mirrors that:
// 1. read input WAV (44100 host samples) // 1. read input WAV (44100 host samples, stereo or mono)
// 2. resample 44100 -> 48000 (libsamplerate, SINC best) // 2. resample 44100 -> 48000 (libsamplerate, SINC best)
// 3. SpectralProcessor(4096, 1024, 48000) with the given bands // 3. Process L and R channels (stereo link=100%: same processing for both)
// 4. resample 48000 -> 44100 // 4. Apply balance: scale reduction for R channel
// 5. write 24-bit output WAV (matches reference format) // 5. Apply mix: wet-dry mix
// Usage: render48k <in.wav> <out.wav> [fc,q,sens[,scale] ...] (comma bands, like framed_test) // 6. resample 48000 -> 44100
// 7. write 24-bit output WAV (stereo)
//
// Usage:
// render48k <in.wav> <out.wav> fc,q,sens[,scale] ...
// Env:
// RT_STEREO_LINK=1.0 (1.0 = sum channels for analysis, 0.0 = dual mono)
// RT_STEREO_BALANCE=0.284 (R channel reduction scale, 1.0 = equal, <1.0 = less on R)
// RT_DEPTH=0.864 (sens multiplier)
// RT_MIX=1.0 (0=dry, 1=full wet)
#include "spectral.hpp" #include "spectral.hpp"
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
@@ -18,7 +28,7 @@
static int g_in_ch = 1; static int g_in_ch = 1;
static bool load_wav(const char* path, std::vector<float>& out, int& sr) { static bool load_wav_stereo(const char* path, std::vector<float>& out, int& sr, int& channels) {
FILE* f = fopen(path, "rb"); FILE* f = fopen(path, "rb");
if (!f) return false; if (!f) return false;
char hdr[44]; char hdr[44];
@@ -26,7 +36,6 @@ static bool load_wav(const char* path, std::vector<float>& out, int& sr) {
sr = *(int*)(hdr + 24); sr = *(int*)(hdr + 24);
int ch = *(short*)(hdr + 22); int ch = *(short*)(hdr + 22);
int bits = *(short*)(hdr + 34); int bits = *(short*)(hdr + 34);
// scan chunks to find data chunk size (hdr[40] may be bext/junk size)
int data = 0; int data = 0;
int64_t pos = 12; int64_t pos = 12;
fseek(f, 12, SEEK_SET); fseek(f, 12, SEEK_SET);
@@ -37,45 +46,55 @@ static bool load_wav(const char* path, std::vector<float>& out, int& sr) {
if (memcmp(cid, "data", 4) == 0) { data = csize; break; } if (memcmp(cid, "data", 4) == 0) { data = csize; break; }
pos += csize; pos += csize;
int skip = csize; int skip = csize;
if (csize % 2) skip++; // odd chunk size padded if (csize % 2) skip++;
fseek(f, skip, SEEK_CUR); fseek(f, skip, SEEK_CUR);
} }
if (!data) { fclose(f); return false; } if (!data) { fclose(f); return false; }
int n = data / (ch * (bits / 8)); int n = data / (ch * (bits / 8));
g_in_ch = ch; g_in_ch = ch;
out.resize(n); channels = 2;
out.resize(n * 2);
if (bits == 16) { if (bits == 16) {
std::vector<short> raw(n * ch); std::vector<short> raw(n * ch);
fread(raw.data(), 2, n * ch, f); fread(raw.data(), 2, n * ch, f);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
long long v = 0; float v = 0.0f;
for (int c = 0; c < ch; c++) v += raw[i * ch + c]; if (ch == 1) {
out[i] = (float)((v / ch) / 32768.0); v = raw[i] / 32768.0f;
out[2*i] = v; out[2*i+1] = v;
} else {
out[2*i] = raw[2*i] / 32768.0f;
out[2*i+1] = raw[2*i+1] / 32768.0f;
}
} }
} else if (bits == 24) { } else if (bits == 24) {
std::vector<unsigned char> raw(n * ch * 3); std::vector<unsigned char> raw(n * ch * 3);
fread(raw.data(), 1, n * ch * 3, f); fread(raw.data(), 1, n * ch * 3, f);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
long long v = 0; auto read24 = [&](int idx) -> float {
for (int c = 0; c < ch; c++) { int32_t s = (raw[idx] | (raw[idx+1] << 8) | (raw[idx+2] << 16));
int idx = (i * ch + c) * 3;
int32_t s = (raw[idx] | (raw[idx + 1] << 8) | (raw[idx + 2] << 16));
if (s & 0x800000) s |= 0xFF000000; if (s & 0x800000) s |= 0xFF000000;
v += s; return s / 8388608.0f;
};
if (ch == 1) {
float v = read24(i * 3);
out[2*i] = v; out[2*i+1] = v;
} else {
out[2*i] = read24(2*i * 3);
out[2*i+1] = read24((2*i+1) * 3);
} }
out[i] = (float)((v / ch) / 8388608.0);
} }
} else return false; } else return false;
fclose(f); fclose(f);
return true; return true;
} }
static bool save_wav24(const char* path, const std::vector<float>& x, int sr) { static bool save_wav24_stereo(const char* path, const std::vector<float>& L, const std::vector<float>& R, int sr) {
FILE* f = fopen(path, "wb"); FILE* f = fopen(path, "wb");
if (!f) return false; if (!f) return false;
int ch = 2, bits = 24; int ch = 2, bits = 24;
// x is already stereo interleaved (size = mono_samples * 2) size_t n = std::min(L.size(), R.size());
int data = (int)(x.size() * (bits / 8)); int data = (int)(n * ch * (bits / 8));
char hdr[44]; memset(hdr, 0, 44); char hdr[44]; memset(hdr, 0, 44);
memcpy(hdr, "RIFF", 4); *(int*)(hdr + 4) = 36 + data; memcpy(hdr, "RIFF", 4); *(int*)(hdr + 4) = 36 + data;
memcpy(hdr + 8, "WAVE", 4); memcpy(hdr + 12, "fmt ", 4); memcpy(hdr + 8, "WAVE", 4); memcpy(hdr + 12, "fmt ", 4);
@@ -84,16 +103,19 @@ static bool save_wav24(const char* path, const std::vector<float>& x, int sr) {
*(short*)(hdr + 32) = (short)ch; *(short*)(hdr + 34) = (short)bits; *(short*)(hdr + 32) = (short)ch; *(short*)(hdr + 34) = (short)bits;
memcpy(hdr + 36, "data", 4); *(int*)(hdr + 40) = data; memcpy(hdr + 36, "data", 4); *(int*)(hdr + 40) = data;
fwrite(hdr, 1, 44, f); fwrite(hdr, 1, 44, f);
for (size_t i = 0; i < x.size(); i++) { for (size_t i = 0; i < n; i++) {
int32_t v = (int32_t)(std::max(-1.0f, std::min(1.0f, x[i])) * 8388607.0f); int32_t vl = (int32_t)(std::max(-1.0f, std::min(1.0f, L[i])) * 8388607.0f);
unsigned char b0 = v & 0xFF, b1 = (v >> 8) & 0xFF, b2 = (v >> 16) & 0xFF; int32_t vr = (int32_t)(std::max(-1.0f, std::min(1.0f, R[i])) * 8388607.0f);
unsigned char b0 = vl & 0xFF, b1 = (vl >> 8) & 0xFF, b2 = (vl >> 16) & 0xFF;
fwrite(&b0, 1, 1, f); fwrite(&b1, 1, 1, f); fwrite(&b2, 1, 1, f);
b0 = vr & 0xFF; b1 = (vr >> 8) & 0xFF; b2 = (vr >> 16) & 0xFF;
fwrite(&b0, 1, 1, f); fwrite(&b1, 1, 1, f); fwrite(&b2, 1, 1, f); fwrite(&b0, 1, 1, f); fwrite(&b1, 1, 1, f); fwrite(&b2, 1, 1, f);
} }
fclose(f); fclose(f);
return true; return true;
} }
static std::vector<float> resample(const std::vector<float>& in, int src_sr, int dst_sr) { static std::vector<float> resample_mono(const std::vector<float>& in, int src_sr, int dst_sr) {
double frac = (double)dst_sr / src_sr; double frac = (double)dst_sr / src_sr;
int out_len = (int)(in.size() * frac) + 16; int out_len = (int)(in.size() * frac) + 16;
std::vector<float> buf(out_len); std::vector<float> buf(out_len);
@@ -107,45 +129,110 @@ static std::vector<float> resample(const std::vector<float>& in, int src_sr, int
return buf; return buf;
} }
int main(int argc, char** argv) { static std::vector<float> resample_stereo(const std::vector<float>& in, int src_sr, int dst_sr) {
if (argc < 3) { fprintf(stderr, "usage: %s in.wav out.wav [fc,q,sens[,scale] ...]\n", argv[0]); return 1; } size_t n = in.size() / 2;
std::vector<float> x; int sr; double frac = (double)dst_sr / src_sr;
if (!load_wav(argv[1], x, sr)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; } int out_len = (int)(n * frac) + 16;
std::vector<float> buf(out_len * 2);
SRC_DATA sd;
sd.data_in = in.data(); sd.input_frames = (long)n;
sd.data_out = buf.data(); sd.output_frames = out_len;
sd.src_ratio = frac; sd.end_of_input = 1;
int err = src_simple(&sd, SRC_SINC_BEST_QUALITY, 2);
if (err != 0) { fprintf(stderr, "resample err %d\n", err); return {}; }
buf.resize(sd.output_frames_gen * 2);
return buf;
}
int main(int argc, char** argv) {
if (argc < 3) {
fprintf(stderr, "usage: %s in.wav out.wav fc,q,sens[,scale] ...\n", argv[0]);
return 1;
}
std::vector<float> x; int sr, channels;
if (!load_wav_stereo(argv[1], x, sr, channels)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; }
// Parse stereo parameters from env
float stereo_link = getenv("RT_STEREO_LINK") ? atof(getenv("RT_STEREO_LINK")) : 1.0f;
float stereo_balance = getenv("RT_STEREO_BALANCE") ? atof(getenv("RT_STEREO_BALANCE")) : 0.284f;
float depth = getenv("RT_DEPTH") ? atof(getenv("RT_DEPTH")) : 1.0f;
float mix = getenv("RT_MIX") ? atof(getenv("RT_MIX")) : 1.0f;
// Parse bands
std::vector<DetectorBand> bands; std::vector<DetectorBand> bands;
for (int i = 3; i < argc; i++) { for (int i = 3; i < argc; i++) {
if (!strchr(argv[i], ',')) continue; if (!strchr(argv[i], ',')) continue;
float fc, q, sens, scl = 1.0f; float fc, q, sens, scl = 1.0f;
if (sscanf(argv[i], "%f,%f,%f,%f", &fc, &q, &sens, &scl) < 3) continue; if (sscanf(argv[i], "%f,%f,%f,%f", &fc, &q, &sens, &scl) < 3) continue;
DetectorBand b; b.fc = fc; b.q = q; b.sens = sens; b.level_scale = scl; DetectorBand b; b.fc = fc; b.q = q; b.sens = sens * depth; b.level_scale = scl;
bands.push_back(b); bands.push_back(b);
} }
if (bands.empty()) bands.push_back({1000.0f, 1.0f, 12.0f}); if (bands.empty()) bands.push_back({1000.0f, 1.0f, 12.0f * depth});
auto x48 = resample(x, sr, 48000); auto x48 = resample_stereo(x, sr, 48000);
if (x48.empty()) return 1; if (x48.empty()) return 1;
SpectralProcessor sp(4096, 1024, 48000.0f); size_t n = x48.size() / 2;
sp.setDetectorParams(bands);
std::vector<float> y48(x48.size()); // Stereo processing per soothe2 manual:
// "With the stereo link at 100%, Soothe will sum the channels for analysis
// and apply the same processing to both channels."
// Use separate processors for L and R to avoid stateful interference.
SpectralProcessor procL(4096, 1024, 48000.0f);
SpectralProcessor procR(4096, 1024, 48000.0f);
procL.setDetectorParams(bands);
procR.setDetectorParams(bands);
std::vector<float> L_out(n), R_out(n);
const size_t BLK = 1 << 16; const size_t BLK = 1 << 16;
std::vector<float> inb(BLK), outb(BLK); std::vector<float> inb(BLK), outb(BLK);
for (size_t s = 0; s < x48.size(); s += BLK) {
size_t n = std::min(BLK, x48.size() - s);
memcpy(inb.data(), x48.data() + s, n * sizeof(float));
for (size_t i = n; i < BLK; i++) inb[i] = 0.0f;
sp.processBlock(inb.data(), outb.data(), BLK, 1);
memcpy(y48.data() + s, outb.data(), n * sizeof(float));
}
auto y = resample(y48, 48000, 44100);
if ((int)y.size() > (int)x.size()) y.resize(x.size());
// write stereo 24-bit // Process L channel
std::vector<float> yst(y.size() * 2); for (size_t s = 0; s < n; s += BLK) {
for (size_t i = 0; i < y.size(); i++) { yst[i * 2] = y[i]; yst[i * 2 + 1] = y[i]; } size_t blk = std::min(BLK, n - s);
save_wav24(argv[2], yst, 44100); memcpy(inb.data(), x48.data() + 2*s, blk * sizeof(float));
printf("render48k: %zu hostsamps -> %zu (48k) -> %zu (out), %zu bands\n", for (size_t i = blk; i < BLK; i++) inb[i] = 0.0f;
x.size(), x48.size(), y.size(), bands.size()); procL.processBlock(inb.data(), outb.data(), BLK, 1);
(void)g_in_ch; for (size_t i = 0; i < blk; i++) L_out[s+i] = outb[i];
}
// Process R channel
for (size_t s = 0; s < n; s += BLK) {
size_t blk = std::min(BLK, n - s);
memcpy(inb.data(), x48.data() + 2*s + 1, blk * sizeof(float));
for (size_t i = blk; i < BLK; i++) inb[i] = 0.0f;
procR.processBlock(inb.data(), outb.data(), BLK, 1);
for (size_t i = 0; i < blk; i++) R_out[s+i] = outb[i];
}
// Apply balance and mix
std::vector<float> L_final(n), R_final(n);
for (size_t i = 0; i < n; i++) {
float L = x48[2*i];
float R = x48[2*i+1];
float L_proc = L_out[i];
float R_proc = R_out[i];
float mask_L = (std::abs(L) > 1e-12f) ? L_proc / L : 1.0f;
float mask_R = (std::abs(R) > 1e-12f) ? R_proc / R : 1.0f;
// Apply balance: scale reduction for R channel
float mask_R_bal = 1.0f - (1.0f - mask_R) * stereo_balance;
// Apply mix
L_final[i] = L * (1.0f - mix) + L * mask_L * mix;
R_final[i] = R * (1.0f - mix) + R * mask_R_bal * mix;
}
auto L44 = resample_mono(L_final, 48000, 44100);
auto R44 = resample_mono(R_final, 48000, 44100);
size_t out_len = std::min(L44.size(), R44.size());
out_len = std::min(out_len, x.size() / std::max(channels, 1));
L44.resize(out_len);
R44.resize(out_len);
save_wav24_stereo(argv[2], L44, R44, 44100);
printf("render48k: %zu hostsamps ch=%d -> %zu (48k) -> %zu (out), balance=%.3f depth=%.3f\n",
x.size(), channels, x48.size(), out_len, stereo_balance, depth);
return 0; return 0;
} }
+14
View File
@@ -0,0 +1,14 @@
#pragma once
// DIVIDE 1803a06a0 tables from .rdata RVA 0x21269c0/0x2126a00 poly 0.207
// BLOCKMAP:580 vpermps+poly 1c dump
// extracted via PE .rdata .rdata VA 0x1baa000 raw 0x1ba8400
#include <array>
namespace rt_div {
constexpr std::array<float,32> tbl_1269c0 = {
127.0f,126.830078f,126.67807f,126.540558f,126.415039f,126.299561f,126.192642f,126.093109f,127.0f,126.830078f,126.67807f,126.540558f,126.415039f,126.299561f,126.192642f,126.093109f,0.0f,-3.12644238e-06f,1.8367532e-06f,1.05200343e-05f,-1.56322119e-06f,-2.65016098e-07f,2.86602835e-06f,2.73532123e-07f,0.0f,-3.12644238e-06f,1.8367532e-06f,1.05200343e-05f,-1.56322119e-06f,-2.65016098e-07f,2.86602835e-06f,2.73532123e-07f};
constexpr std::array<float,32> tbl_126a00 = {
0.0f,-3.12644238e-06f,1.8367532e-06f,1.05200343e-05f,-1.56322119e-06f,-2.65016098e-07f,2.86602835e-06f,2.73532123e-07f,0.0f,-3.12644238e-06f,1.8367532e-06f,1.05200343e-05f,-1.56322119e-06f,-2.65016098e-07f,2.86602835e-06f,2.73532123e-07f,0.20751521f,0.20751521f,0.20751521f,0.20751521f,0.20751521f,0.20751521f,0.20751521f,0.20751521f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f};
constexpr std::array<float,9> poly_0 = {
0.20751521f,-0.24168693f,0.28853500f,-0.36067134f,0.48089835f,0.24026407f,0.05551192f,0.69314718f,1.92590424e-08f};
}
+12 -21
View File
@@ -12,13 +12,13 @@
SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate) SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
: nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0), : nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0),
detector_(nfft, sample_rate) { detector_(nfft, sample_rate) {
window_ = new double[nfft_]; window_.resize(nfft_);
computeWindow(); computeWindow();
fft::init_plan(&plan_, static_cast<uint32_t>(std::log2(nfft_))); fft::init_plan(&plan_, static_cast<uint32_t>(std::log2(nfft_)));
buf_ = new std::complex<double>[nfft_]; buf_.resize(nfft_);
tmp_buf_ = new std::complex<double>[nfft_]; tmp_buf_.resize(nfft_);
fir_buf_ = new std::complex<double>[nfft_]; fir_buf_.resize(nfft_);
fir_freq_ = new std::complex<double>[nfft_]; fir_freq_.resize(nfft_);
overlap_.resize(nfft_, 0.0f); overlap_.resize(nfft_, 0.0f);
mask_.resize(nfft_, 1.0f); mask_.resize(nfft_, 1.0f);
@@ -30,13 +30,7 @@ SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
} }
} }
SpectralProcessor::~SpectralProcessor() { SpectralProcessor::~SpectralProcessor() = default;
delete[] window_;
delete[] buf_;
delete[] tmp_buf_;
delete[] fir_buf_;
delete[] fir_freq_;
}
void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) { void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) {
detector_.setParams(bands); detector_.setParams(bands);
@@ -63,8 +57,8 @@ void SpectralProcessor::stftFrame(const float* in, std::complex<double>* out) {
} }
void SpectralProcessor::istftFrame(std::complex<double>* in, float* out, float* overlap) { void SpectralProcessor::istftFrame(std::complex<double>* in, float* out, float* overlap) {
memcpy(tmp_buf_, in, nfft_ * sizeof(std::complex<double>)); memcpy(tmp_buf_.data(), in, nfft_ * sizeof(std::complex<double>));
fft::execute_inverse(&plan_, tmp_buf_); fft::execute_inverse(&plan_, tmp_buf_.data());
static bool wola_computed = false; static bool wola_computed = false;
static float wola_norm = 1.0f; static float wola_norm = 1.0f;
// RT_SYN: 0=synthesis window = analysis window (WOLA), 1=none // RT_SYN: 0=synthesis window = analysis window (WOLA), 1=none
@@ -207,9 +201,6 @@ void SpectralProcessor::buildFirFromMask(const float* mask, std::complex<double>
for (size_t i = half + 1; i < nfft; i++) { for (size_t i = half + 1; i < nfft; i++) {
time_domain[i] = 0.0; // xmm9 = 0 zeros upper half time_domain[i] = 0.0; // xmm9 = 0 zeros upper half
} }
for (size_t i = half + 1; i < nfft; i++) {
time_domain[i] = 0.0;
}
// Step 4: opB = fwd-RFFT (th1a90): time_domain (real) → complex // Step 4: opB = fwd-RFFT (th1a90): time_domain (real) → complex
std::vector<std::complex<double>> freq_domain(half + 1); std::vector<std::complex<double>> freq_domain(half + 1);
@@ -282,9 +273,9 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
for (size_t f = 0; f < nframes; f++) { for (size_t f = 0; f < nframes; f++) {
size_t offset = f * hop_; size_t offset = f * hop_;
if (offset + nfft_ > num_samples) break; if (offset + nfft_ > num_samples) break;
stftFrame(in + offset, buf_); stftFrame(in + offset, buf_.data());
detector_.processFrame(buf_, mask_.data()); detector_.processFrame(buf_.data(), mask_.data());
if (firconv == 3) { if (firconv == 3) {
// RT_FIRCONV=3 (NOTES 24k): plugin application law decoded live: // RT_FIRCONV=3 (NOTES 24k): plugin application law decoded live:
@@ -299,7 +290,7 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
// RT_FIRCONV=2: Full FIR construction pipeline (52b550-52b8bb). // RT_FIRCONV=2: Full FIR construction pipeline (52b550-52b8bb).
// mask → reciprocal (1/mask) → window → normalize → complex multiply. // mask → reciprocal (1/mask) → window → normalize → complex multiply.
// This replicates the plugin's FFT-conv FIR design path. // This replicates the plugin's FFT-conv FIR design path.
buildFirFromMask(mask_.data(), fir_freq_, nfft_); buildFirFromMask(mask_.data(), fir_freq_.data(), nfft_);
// Complex multiply FIR × audio spectrum // Complex multiply FIR × audio spectrum
for (size_t i = 0; i < nfft_; i++) { for (size_t i = 0; i < nfft_; i++) {
buf_[i] *= fir_freq_[i]; buf_[i] *= fir_freq_[i];
@@ -320,6 +311,6 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
} }
} }
istftFrame(buf_, out + offset, overlap_.data()); istftFrame(buf_.data(), out + offset, overlap_.data());
} }
} }
+5 -5
View File
@@ -21,12 +21,12 @@ public:
private: private:
size_t nfft_; size_t nfft_;
size_t hop_; size_t hop_;
double* window_; std::vector<double> window_;
FFTPlan plan_; FFTPlan plan_;
std::complex<double>* buf_; std::vector<std::complex<double>> buf_;
std::complex<double>* tmp_buf_; std::vector<std::complex<double>> tmp_buf_;
std::complex<double>* fir_buf_; std::vector<std::complex<double>> fir_buf_;
std::complex<double>* fir_freq_; std::vector<std::complex<double>> fir_freq_;
std::vector<double> fir_window_; std::vector<double> fir_window_;
std::vector<float> overlap_; std::vector<float> overlap_;
std::vector<float> mask_; std::vector<float> mask_;
+55
View File
@@ -0,0 +1,55 @@
#include <cstdio>
#include <cmath>
#include "framed_model.hpp"
// Unit check for the VLAW detector law (BLOCKMAP:314 softplus proxy):
// cut = alpha * ln1p(lvl/beta) + c [+ delta]
// mask = 10^(-cut/20)
// Reference values hand-computed from the dual-calibrated constants
// (alpha=3.2193, beta=0.4927, c=0.5423, delta=6.9177 — README.md:26).
int main() {
int fail = 0;
// --- law monotonicity: higher level -> stronger cut -> smaller mask ---
double m0 = vlaw_mask(0.01, 3.2193, 0.4927, 0.5423, 0.0);
double m1 = vlaw_mask(1.0, 3.2193, 0.4927, 0.5423, 0.0);
double m2 = vlaw_mask(10.0, 3.2193, 0.4927, 0.5423, 0.0);
bool mono = (m0 > m1) && (m1 > m2);
std::printf("vlaw monotonic: m(0.01)=%.4f m(1)=%.4f m(10)=%.4f (%s)\n",
m0, m1, m2, mono ? "OK" : "MISMATCH");
if (!mono) fail = 1;
// --- zero level: cut = c => mask = 10^(-c/20) ---
double mz = vlaw_mask(0.0, 3.2193, 0.4927, 0.5423, 0.0);
double ez = std::pow(10.0, -0.5423 / 20.0);
bool zok = std::fabs(mz - ez) < 1e-9;
std::printf("vlaw zero-level: mask=%.6f expect=%.6f (%s)\n",
mz, ez, zok ? "OK" : "MISMATCH");
if (!zok) fail = 1;
// --- delta branch adds cut -> deeper mask ---
double md = vlaw_mask(1.0, 3.2193, 0.4927, 0.5423, 6.9177);
bool dok = md < m1;
std::printf("vlaw delta: mask+delta=%.4f < %.4f (%s)\n",
md, m1, dok ? "OK" : "MISMATCH");
if (!dok) fail = 1;
// --- numeric reference: lvl=1.0, dual params ---
// cut = 3.2193 * ln(1 + 1/0.4927) + 0.5423
double cut_ref = 3.2193 * std::log1p(1.0 / 0.4927) + 0.5423;
double mref = std::pow(10.0, -cut_ref / 20.0);
bool rok = std::fabs(m1 - mref) < 1e-9;
std::printf("vlaw ref: mask=%.6f expect=%.6f cut=%.4f (%s)\n",
m1, mref, cut_ref, rok ? "OK" : "MISMATCH");
if (!rok) fail = 1;
// --- comb neutrality: alpha=0.05 beta=5.0 c=0 -> mask ~ 1 for lvl=0 ---
double mc = vlaw_mask(0.0, 0.05, 5.0, 0.0, 0.0);
bool cok = std::fabs(mc - 1.0) < 1e-9;
std::printf("vlaw comb-neutral: mask(0)=%.6f expect=1.0 (%s)\n",
mc, cok ? "OK" : "MISMATCH");
if (!cok) fail = 1;
std::printf("vlaw_check %s\n", fail ? "FAIL" : "PASS");
return fail;
}
+110 -1
View File
@@ -1,4 +1,4 @@
# NOTES_LEVEL — живой журнал детектора/маски (голова 24mm5+, 2026-08-28) # NOTES_LEVEL — живой журнал детектора/маски (голова 25a+, 2026-09-02)
> **Архив 2026-08-18 — 2026-08-23 (4024 строки) → [`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`](archive/NOTES_LEVEL_2026-08-18_2026-08-23.md).** > **Архив 2026-08-18 — 2026-08-23 (4024 строки) → [`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`](archive/NOTES_LEVEL_2026-08-18_2026-08-23.md).**
> Оглавление по датам/темам → [`handoff/NOTES_LEVEL_INDEX.md`](NOTES_LEVEL_INDEX.md) (единственный навигатор). > Оглавление по датам/темам → [`handoff/NOTES_LEVEL_INDEX.md`](NOTES_LEVEL_INDEX.md) (единственный навигатор).
@@ -395,3 +395,112 @@ winetrace_casc/chain_samples.pkl, kind∈{CIN,COUT,AIN,AOUT,COPY,EXP,DF0}).
- w ≈ 0.015 (scalar, НЕ 0.977) - w ≈ 0.015 (scalar, НЕ 0.977)
- Каскад STATEFUL: bands_curve сохраняется между кадрами - Каскад STATEFUL: bands_curve сохраняется между кадрами
- 5407a8 = accumulator, НЕ нулевой при рекуррентности - 5407a8 = accumulator, НЕ нулевой при рекуррентности
## 25b (2026-09-02): Δ-rule distance-aware gain wired (c85b888)
### Механизм
`delta_gain(bin, kfc, nbin, dist_factor) = 1 + (|bin-kfc|/nbin)·RT_DELTA_DIST` —
template-local: далёкие пики получают бо́льший cut. `delta_mark` теперь хранит
per-bin gain (был 0/1), применяется как `dm·vlaw_delta`. Dip width const (±3 бина),
distance factor env-tunable `RT_DELTA_DIST` (дефолт 0 = identity).
### Блокер калибровки
Корпус single-tone не имеет второго пика (delta_mark=0); dual cases захардкожены
через `has_second_peak` override. Калибровка `dist_factor` и STATE-зависимости
(`g@2000 12.15 vs 1.85`, `STATE=[ctx+0x540788]`) требует `ph*.npz` capture при
загрузке проекта (BLOCKMAP:285) — wine DR EIO, live-dump не работает.
### Gate
`corpus.py --compare baseline_bridge.json --tol 0.25` d=+0.000; structural 2.689
(RT_CASC=0); `fn529fe0_check`/`vlaw_check`/`twin_check` PASS.
### ph*.npz capture proof (2026-09-02)
`rendersnap2.py` успешно снимает `ph*.npz` (слоты 0x540628/0x540678/0x540688/0x540768).
Данные в `/tmp/opencode/`:
- `rendersnap2_dual` (dual_b1q_0.5, 79 кадров, populated 40+)
- `snap_t1k_b1f_1000` (60 кадров, populated 26-37)
- `snap_dual300` (60 кадров, populated 30-45)
- `rendersnap2_comb_b1234` (80 кадров, populated 25-37)
**Key finding**: маск-цепь работает в рендер-окне на ВСЕХ кадрах (BLOCKMAP:285
устарел или относится только к single-tone init). Dual-tone cases дают
стабильные populated кадры — можно калибровать Δ-rule offline.
**Данные для Δ-калибровки** (dual@500+2000, populated кадр 60):
- `0x540628` (scratch): [43]=-0.1849 [171]=-0.2460 (log-domain)
- `0x540678` (bands_curve/mask): [43]=0.7221 [171]=0.6486
- `0x540768` (track_i): [43]=4.1498 [171]=1.5989
## ============ 25a (2026-09-02): chain_9_19 staged, IIR4-генератор, k-mapping dedup, audit fixes ============
### Сессия-контекст
Workflow `/next-task` (Q1=Local handoff, Q2=All, Q3=Implemented/Deployed).
Задачи взяты из рейтинга `BITEXACT_PLAN.md` + аудита. Всё gated `RT_*`,
guard `corpus.py --compare baseline_bridge.json --tol 0.25` держит d=+0.000.
### 25a1. chain_9_19 интеграция (коммиты 9ed22d3, 2a295af, 96816f9, b41d7a4)
- `dsp/fn529fe0.cpp:chain_9_19` — полная цепь: LOG#1→DIVIDE(1803a06a0
b/a)→dc40(bandsACC)→FMA ATT/REL (half-split)→EXP#11→×track→×kWarp→
LOG#2→IIR4×2→FIR 52b3cd→EXP#2. `track` — новый параметр (ctx+0x540768).
- **IIR4 = DOUBLE precision** (дизасм `f529fe0_full.dis:1191-1201`:
`movsd/mulsd/addsd/cvtpd2ps`; down@0x340510, up@0x3c0510, acc@0x440510).
- `generate_iir4_coefs()` — генератор FUN_180533340 (BLOCKMAP:135-150):
freq-warp `g = (i<=fc_norm) ? fc_norm/i : pow(fc_norm/i,p)`; live-константы
`DAT_1824c3d8c=0.9994880557060242`, `DAT_1824c46b8=0.9991304874420166`.
ВАЖНО: `up[i] = exp(c·g·tau/mult)` (знак МИНУС, иначе Inf), `down=1up`.
Smoke: down0=1, up0=0, down1=0.6292. Gated `RT_IIR4_GEN=1`
(env: RT_IIR4_C/TAU/P/MULT/SR, дефолты 1000/1200/0.5/360/48000).
- Интеграция: `framed_model.cpp` `RT_CASC=1` gate; вход — `raw_level`
(am/res·scale). `process_band_structural` получил `track` параметр.
- **Результаты**: bridge 1.594 (gated OFF, не тронут); structural 2.689
(RT_CASC=0); RT_CASC=1 → 31.6 (НЕКАЛИБРОВАНО, ожидаемо).
- **fn529fe0_check**: добавлены chain smoke + generate_iir4 smoke; PASS.
Попутно починен тест haar_one_pass boundary (8.0 не 8.5).
### 25a2. VLAW per-fc: continuous вместо дискретной решётки (f2cc0ae)
- `framed_model.cpp:283-298`: 800→1200 q<1 теперь lerp `t=(fc800)/400`
(5.0/0.4 → 4.5/0.35) вместо дискретных веток.
- sens-эффект теперь ADDITIVE offset (12→0, 6→−0.48α, 24→+0.5α) вместо
перезаписи, стиравшей fc-выбор (баг старой решётки: sens всегда побеждал).
- Gates не сдвинулись (1.594/2.689) — полный перефит требует campaign.py
(8 мин/ячейка, `ph*.npz`) — вне scope.
### 25a3. k-mapping dedup (197f5ed)
- `k_mapping_factor(fc,q,sens)` — вынос двух идентичных блоков
(lvl_in + raw_level) в helper. `k_fc=1.0` + opt-in `RT_KMAP_FC` (W_eq).
- RT_KMAP=1 не влияет на TOTAL (2.689) — ожидаемо, т.к. калибровка
α/β уже поглощает k на sens=12.
### 25a4. Аудит `/audit-project` — Phases 1+2 применены (e4c5348)
- Убран dead conditional `if (pool_w > 0 && !lut_off == false) {}`.
- `DBG_CASC` fprintf в per-frame цикле → gated `RT_DBG_CASC`.
- Убран дублирующий zero-loop в `spectral.cpp buildFirFromMask`.
- Комментарии о разнице шкал FFT (1/N canonical vs 2/half plugin).
- Документирован blend 0.8 (декомп 0x5406f8, xmm10@1824c3e28).
- `assert(spectrum != nullptr)` в processFrame.
- НЕ применено (осознанно): thread_local→context (конфликт с golden rule —
chain нужен reentrant caller), SpectralProcessor new[]→vector, Config
struct для getenv — оставлено как техдолг.
### 25a5. Блокеры (без изменений)
- ph*.npz нет на этой машине; live-dump не работает (маск-цепь при загрузке,
BLOCKMAP:285; wine DR EIO). Chain калибровка ждёт capture при INIT.
- prd.md создан (карта проекта, 293 строки).
- Structural baseline в README/prd: 2.689 (L/R), исторический 0.341
недостижим на HEAD после M/S→L/R (e343b0a).
### Коммиты сессии
```
e343b0a prd.md + render48k L/R baseline
9ed22d3 chain_9_19: IIR4 double + FIR, haar fix
2a295af chain integrate RT_CASC gate
96816f9 chain: IIR4 generator + audio path
2201a57 docs: baseline 2.689
f2cc0ae vlaw continuous per-fc + additive sens
b41d7a4 chain: RT_IIR4_GEN wire + smoke tests
197f5ed k_mapping_factor dedup
e4c5348 audit fixes Phases 1+2
2f854cd spectral vectors + vlaw_check target
a881ee2 AGENTS.md: vlaw_check in test list
c85b888 Δ-rule: distance-aware template-local gain (blocked on ph*.npz)
```
+2 -1
View File
@@ -1,6 +1,6 @@
# NOTES_LEVEL — оглавление журнала # NOTES_LEVEL — оглавление журнала
> **Навигатор:** `NOTES_LEVEL.md` — живая голова (24mm5+, 2026-08-28, 389 строк). Архив 2026-08-18—2026-08-23 → [`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`](archive/NOTES_LEVEL_2026-08-18_2026-08-23.md) (4024 строки). Статус TOTAL → [`README.md:13`](../README.md). > **Навигатор:** `NOTES_LEVEL.md` — живая голова (25a+, 2026-09-02, ~470 строк). Архив 2026-08-18—2026-08-23 → [`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`](archive/NOTES_LEVEL_2026-08-18_2026-08-23.md) (4024 строки). Статус TOTAL → [`README.md:13`](../README.md).
## Живая голова (`handoff/NOTES_LEVEL.md`) ## Живая голова (`handoff/NOTES_LEVEL.md`)
@@ -15,6 +15,7 @@
| 24mm12 | Детекторный каскад vt+0x28=180529c60 | Оркестратор 5300f0, vtable карта, 0x281 байт | | 24mm12 | Детекторный каскад vt+0x28=180529c60 | Оркестратор 5300f0, vtable карта, 0x281 байт |
| 24mm13 (+доп) | Хелперы каскада 529c60 | 5355d0→16140, 530080, 20f0/1850/1a00, рекуррентия Haar | | 24mm13 (+доп) | Хелперы каскада 529c60 | 5355d0→16140, 530080, 20f0/1850/1a00, рекуррентия Haar |
| 24mm14 | Каскад декодирован — 3 фазы | `|z|` → Haar×2 → peak/sin/w/blend 5407a8 | | 24mm14 | Каскад декодирован — 3 фазы | `|z|` → Haar×2 → peak/sin/w/blend 5407a8 |
| 25a (2026-09-02) | chain_9_19 staged, IIR4-генератор, k-mapping dedup, аудит | IIR4 double (movsd), generate_iir4_coefs, RT_CASC/RT_IIR4_GEN gates, baseline 2.689 |
## Архив (`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`) — по периодам ## Архив (`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`) — по периодам
+296
View File
@@ -0,0 +1,296 @@
# prd.md — Project Requirements Document
## 1. Project Overview
**Name**: `soothe2-re` — bit-exact reverse engineering of **oeksound soothe2** (VST3, Windows x64)
**Goal**: Reproduce the plugin's DSP core (level detector, mask computation, filter application) in C++17, byte-for-byte identical to the native binary.
**Status**: ~95% decompiled. Bridge **1.594 dB** stable; structural **2.689 dB** (L/R `e343b0a`, `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0`, `RT_CASC=0` gated). Historical best `0.341` (24mm14) not reachable on current HEAD. Target: **<0.05 dB** (bit-exact gate, requires chain calibration).
**Golden rule**: Every parameter must have a source (decomp address / live table). Empirical fits must be flagged `EMPIRICAL`.
---
## 2. Repository Structure
```
re-tools/
├── prd.md ← THIS FILE (project overview)
├── README.md ← TOTAL metric source (single source of truth)
├── AGENTS.md ← runbook: build, env flags, tooling hazard
├── BITEXACT_PLAN.md ← path to byte-exact (3 steps, criteria)
├── roadmap.md ← historical B-phase log (archived)
├── dsp/ ← C++17 DSP reconstruction (THE CANON)
│ ├── framed_model.{cpp,hpp} ← MAIN: mask-apply chain (empirical bridge)
│ ├── render48k.cpp ← 48k/4096 structural pipeline (resample→chain→resample)
│ ├── spectral.{cpp,hpp} ← STFT/ISTFT processor + FIR builder
│ ├── fn529fe0.{cpp,hpp} ← structural chain 919 (FUN_180529fe0)
│ ├── fnfaith.{cpp,hpp} ← faithful detector cascade (BLOCKMAP transcription)
│ ├── fft{,_plan,_stage}.cpp/hpp ← FFT engine (canonical + RFFT bit-exact)
│ ├── twin.{cpp,hpp} ← twin resonator (FUN_180535880, float-parity)
│ ├── rt_mask_tables.{hpp,.cpp} ← live IIR A/B coefficients (385K)
│ ├── rt_weights.{hpp,.cpp} ← live kWarp/kBand768 tables (44K)
│ ├── fftconv.{cpp,hpp} ← FFT convolution FIR application
│ ├── vlog.{cpp,hpp} ← fast log2 approximation
│ ├── exp2.{cpp,_tables.cpp,hpp} ← fast exp2 approximation
│ ├── levelpath.{cpp,hpp} ← level-curve path (xv = log10(am/res))
│ ├── freqpath.{cpp,hpp} ← frequency-axis warp/freq-domain helpers
│ ├── leveltrack.{cpp,hpp,hpp} ← envelope follower (attack/release tables)
│ ├── log2_ln.{cpp,hpp} ← log/ln utilities
│ ├── filter.{cpp,hpp} ← detection kernel (legacy)
│ ├── detect.{cpp,hpp} ← detector front-end (legacy)
│ ├── phase_table.{cpp,hpp} ← phase table for FFT
│ ├── ms.hpp ← mid-side helpers
│ ├── params.hpp ← parameter struct
│ ├── rotor_kernel.hpp ← rotor transform kernel
│ ├── rt_div_tables.hpp ← division tables
│ ├── soothe_constants.hpp ← decoded constants
│ ├── cody_waite.hpp ← Cody-Waite argument reduction
│ ├── twiddle_{builder,loader}.cpp/hpp ← FFT twiddle factors
│ ├── dsp_ctx.hpp ← DSP context layout
│ ├── framed_test.cpp ← CLI bridge renderer (44.1k)
│ ├── harness.cpp ← legacy harness
│ ├── *_check.cpp ← bit-exact unit test targets
│ └── CMakeLists.txt
├── scripts/
│ ├── corpus.py ← bridge regression harness (62 cases, --compare)
│ ├── corpus_structural.py ← structural chain harness (--vs-bridge)
│ ├── rendersnap2.py ← RENDER_FILE stopper (reads from .rpp)
│ ├── campaign.py ← parameter sweep cell (~8 min)
│ ├── cascade_sim.py ← numpy step 919 simulator
│ ├── disasm_func.py ← capstone disasm with RIP constants
│ ├── iat_name.py ← runtime import resolution
│ ├── wine_{chain,stage,ptrace}_trace.py ← ptrace-based live trace
│ ├── dump_dispatch.py ← table/state dumper
│ ├── probe_{states,mem}.py ← live state/memory probes
│ ├── hunt2.py ← ctx-instance hunter
│ ├── scan_{pairs,lutsub}.py ← memory diagnostics
│ ├── fit_vlaw_{params,by_group}.py ← VLAW law calibration
│ ├── lawfit22r.py ← law fitting (VLAW α/β/c)
│ ├── mk_{dist,far,multi6}.py ← RPP generators
│ └── render_parity.py ← model-vs-render comparison
├── handoff/
│ ├── NOTES_LEVEL.md ← live journal (head: 24mm5+)
│ ├── NOTES_LEVEL_INDEX.md ← journal index by date/topic
│ ├── BLOCKMAP_529fe0.md ← FUN_180529fe0 method map (53K)
│ ├── NOTES_TWIN.md ← twin reference
│ ├── NOTES_CAPTURE.md ← live-capture protocol
│ ├── NEXT_PROMPT.md ← entry point for new sessions
│ ├── SESSION_HANDOFF.md ← handoff template
│ ├── archive/ ← historical NOTES_LEVEL_*.md, SESSION_HANDOFF_*.md
│ ├── nls_dasm/ ← 183 disassembly files (.dis, .bin)
│ ├── phase1/ ← phase-1 outputs
│ ├── rt*.npy ← captured runtime tables (48k/44.1k)
│ └── *.py ← emit/extract/joint scripts
├── *.java ← Ghidra scripts (DumpFuns, ImportRtti…)
├── *.{bin,npz,npy,json,txt} ← datasets, dumps, LUTs (mostly outside git)
└── soothe-bt/ ← test corpus (~600 renders, outside git)
```
---
## 3. Build System
**Generator**: CMake 3.10+, C++17, GCC/Clang with `-O3 -march=native`.
```cmake
# Key targets
soothe2_dsp # shared library (all dsp/*.cpp)
framed_test # CLI bridge renderer (44.1k)
render48k # structural renderer (48k/48000)
twin_check # float-parity unit test
tables_check # live-table verification
fftconv_check # FIR convolution check
vlog_check # fast log2 check
leveltrack_check # envelope follower check
levelpath_check # level-curve path check
exp2_check # fast exp2 check
fn529fe0_check # structural chain check
soothe2_harness # legacy harness
```
**External deps**: `libsamplerate` (render48k only), `pthread`.
**Tooling hazard**: CMake skips rebuild when source modified within same second. Protocol: `touch` source before build + verify binary mtime.
---
## 4. DSP Architecture
### 4.1 Signal Flow (current canon: structural `render48k`)
```
Host 44.1k → resample → 48k → [per-channel processing] → resample → 44.1k Host
┌─────────────────────┐
│ FramedDetector │
│ (per 4096 block) │
│ │
│ am[] ← envelope │
│ res[] ← twin resp │
│ ↓ │
│ lvl_raw = am/res·sf │
│ ↓ │
│ [RT_VLAW=1]: │
│ cut = α·ln1p(L/β) │
│ +c [+Δ] │
│ mask = 10^(-cut/20)│
│ ↓ │
│ warp: mask *= │
│ kBand·kWarp·res^rp│
│ ↓ │
│ IIR3 ×2 (bidir) │
│ ↓ │
│ mask_out → multiply │
│ spectrum[k] *= mask │
└─────────────────────┘
```
### 4.2 Dual Render Path
| Path | File | Grid | Use |
|------|------|------|-----|
| Bridge | `framed_model.cpp` | 44.1k/2048 | Legacy, TOTAL 1.594 (canon) |
| Structural | `render48k.cpp` | 48k/4096 L/R | `2.689` (`RT_CASC=0` gated, hist. `0.341`) |
**Dual-solution env set**: `RT_VLAW=1 RT_SYN=1 RT_NOWARP=1 RT_NOIIR3=1 RT_IIR12=0` (chain `RT_CASC=1` adds IIR4 double + FIR min-phase, gated)
### 4.3 Key Modules
| Module | Responsibility |
|--------|---------------|
| `FramedDetector` | Per-band, per-frame mask computation. Holds `am_`, `res_`, `track_` state. |
| `SpectralProcessor` | STFT/ISTFT, OLA overlap-add, FIR application modes. |
| `fn529fe0` | Structural chain 919 (steps 919 of FUN_180529fe0): scale→IIR1→IIR2→blend→combine→warp→IIR3→dry/wet. |
| `fnfaith` | Faithful detector cascade transcription (BLOCKMAP). |
| `twin` | Twin resonator `|2B/A|` — frequency response per band. |
| `rt_mask_tables` | Live IIR A/B coefficients (kIIR_A1/A2/A3, kIIR_B1/B2/B3, kRTAtt, kRTRel). |
| `rt_weights` | Live warp weights (kBand768, kWarp). |
| `fft` | Bit-exact RFFT (th1a90/th2180) with `buf548`/`mask598` tables. |
| `fftconv` | FFT-based convolution for FIR application modes. |
| `vlog`/`exp2` | Fast polynomial approximations matching plugin精度. |
| `leveltrack` | Envelope follower with per-bin attack/release tables. |
### 4.4 Detector Cascade (529c60)
```
complex_spectrum × twin_response → |z|
→ Haar smooth [0.25,0.5,0.25] × n_iters
→ peak = max(curve)
→ sin_peak = sin(param·30 90) · 0.115129 · peak
→ curve = max(curve, sin_peak)
→ w = -log10(pow(50, ratio·0.001) · ratio·0.001)
→ acc = acc·w + curve·(1-w)
→ bands_curve = acc
```
---
## 5. Environment Flags (experiment control)
| Flag | Effect |
|------|--------|
| `RT_VLAW=1` | Two-stage law: `mask = 10^((α·ln1p(lvl/β)+c)/20)` |
| `RT_SYN=1` | STFT without synthesis window (plugin's actual layer) |
| `RT_WIN=0/1/2` | Analysis window: sym-Hann / periodic / rect |
| `RT_NOWARP=1` | Skip warp modulation |
| `RT_NOIIR3=1` | Skip IIR3 ×2 |
| `RT_IIR12=0` | Skip freq-domain IIR1/2 (critical with VLAW) |
| `RT_DUMP_BIN=<f>` | Dump tract binary (frame `RT_DUMP_FRAME`) |
| `RT_VDBG=1` | Print VLAW computations to stderr |
| `RT_FAITHFUL=1` | Use faithful chain (`fnfaith.cpp`) |
| `RT_FIRCONV=1/3` | FIR application mode (1=complex-mul, 3=`1.019·mask^1.8345`) |
| `RT_ENV=live` | Live envelope from kRTAtt/kRTRel tables |
| `RT_KMAP=1` | k-mapping correction (twin/am scaling) |
| `RT_EQ=1` | Pre-detector EQ bell |
| `RT_LUT_OFF=1` | Skip LUT transform |
| `RT_LUT_A/B/G/M` | LUT parameters (A=-24, B=28, gamma=1, mult=4.2) |
---
## 6. Test Corpus & Metrics
**Location**: `/home/m/soothe-bt/` (~600 renders, outside git).
**Key sets**:
| Prefix | Content |
|--------|---------|
| `tone1kq_*` | Single tone, fc-scan, quiet (-18 dBFS) |
| `tone1k_*` | Single tone, fc-scan, loud (0 dBFS) |
| `dual_b1q_*` | Two tones (500+2000), q 0.1…10 |
| `al_*` | Level sweep (fc=1000) |
| `comb_*` | 4-band multiband |
| `burst500_b1` | Primary reference (burst 500 Hz) |
**Metric** (Goertzel steady-state):
```python
err_dB = 20·log1₀( ta(out,1000) / ta(ref,1000) )
# trimmed to last 75% of input, matched to plugin render length
```
**Regression guards**:
```bash
python3 scripts/corpus.py # bridge + guard
python3 scripts/corpus_structural.py # structural chain
python3 scripts/corpus.py --compare scripts/baseline_bridge.json --tol 0.25
python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json
```
---
## 7. Current Status (2026-09-02, chain919)
| Group | Bridge | Structural `RT_CASC=0` (L/R) | Structural `RT_CASC=1` |
|-------|--------|-------------------------------|------------------------|
| t1kq (fc-scan) | 0.226 | 0.356 | 0.717 |
| t1k (loud) | 1.801 | 5.437 | 5.631 |
| al (level) | 0.638 | 2.613 | 5.036 |
| res | 0.628 | 1.256 | 23.031 |
| dual (q-sweep) | 0.726 | 2.042 | 66.642 |
| comb (4-band) | 10.149 | 6.004 | 29.172 |
| **TOTAL** | **1.594** | **2.689** | **31.653** |
Historical best structural `0.341` (24mm14, `f40f41e` dual 0.193) not reachable on current HEAD (M/S→L/R + chain).
**Decoded**:
- Layer: STFT without synthesis window, per-bin mask multiply
- Law: `mask = 10^((α·ln1p(lvl/β)+c)/20)` with content-aware Δ
- FIR: `exp(0.984·ln(raw))` + Hann + normalize → bit-exact RFFT to df0
- Chain 919: IIR4 double + FIR min-phase integrated, `generate_iir4_coefs()` (FUN_180533340) added, gated `RT_CASC`
**Open gaps**:
1. **Chain 919** (priority #1): Integrated but uncalibrated (input format level vs cut, blocker `ph*.npz`/live-dump). Target: <0.05.
2. **k-mapping** (priority #2): twin/am scaling `k(q≥2)=0.403`.
3. **Δ second-peak rule** (priority #3): Content-dependent gain, requires live-dump.
---
## 8. Reference Documents
| Doc | Content |
|-----|---------|
| `README.md` | TOTAL metric, reproduction steps |
| `AGENTS.md` | Build commands, env flags, hazard, corpus format |
| `BITEXACT_PLAN.md` | 3-step plan to byte-exact |
| `handoff/BLOCKMAP_529fe0.md` | Method map for FUN_180529fe0 |
| `handoff/NOTES_LEVEL.md` | Live working journal |
| `handoff/NOTES_LEVEL_INDEX.md` | Journal index |
| `handoff/NOTES_TWIN.md` | Twin resonator reference |
| `handoff/NOTES_CAPTURE.md` | Live-capture protocol |
| `handoff/nls_dasm/` | 183 disassembly files |
---
## 9. Reproduction & Development Protocol
1. Install soothe2 VST3 (Windows) under yabridge → `dump_soothe.py``soothe_mem.bin`
2. Ghidra headless: `analyzeHeadless <proj> soothe_x64 -process soothe_mem.bin -noanalysis -postScript <X>.java`
3. Renders: `sweep.py``.rpp`, `reaper -renderproject``.wav`
4. Analysis: `render_parity.py` / `corpus.py`
5. After C++ edit: `touch` source → `cmake --build dsp/build --target framed_test` → run corpus guards
+64
View File
@@ -0,0 +1,64 @@
{
"al_12": 0.6589760322215087,
"al_18": 0.8869285064658652,
"al_24": 0.9872335108976188,
"al_3": 0.21467895021449704,
"al_6": 0.36768989474153474,
"al_9": 0.5175959713969748,
"comb_1000": -0.8390262089726862,
"comb_1500": -0.8112870378396803,
"comb_2000": -0.809711515922245,
"comb_3000": -0.8085898984121008,
"comb_500": -0.8092538987382586,
"dual_0.1_2000": -0.08995281533909788,
"dual_0.1_500": -0.05320412461191143,
"dual_0.2_2000": 0.3019781559654401,
"dual_0.2_500": -0.053358134264814706,
"dual_0.3_2000": 0.2902583798578576,
"dual_0.3_500": -0.05348688556617939,
"dual_0.5_2000": 0.19650867858105603,
"dual_0.5_500": -0.05387091733344582,
"dual_0.7_2000": 0.18642830232473012,
"dual_0.7_500": -0.05441615588199915,
"dual_1.0_2000": 0.23254813837808486,
"dual_1.0_500": -0.05551608061654063,
"dual_1.5_2000": 0.3084791235429092,
"dual_1.5_500": -0.057043442345604364,
"dual_10.0_2000": 0.4377055060111567,
"dual_10.0_500": 0.20966855072081148,
"dual_2.0_2000": 0.3539423473152209,
"dual_2.0_500": -0.0574902922094877,
"dual_3.0_2000": 0.3970249956345811,
"dual_3.0_500": -0.05247116979493978,
"dual_5.0_2000": 0.42379656083975153,
"dual_5.0_500": -0.010273028233688034,
"res_300": 0.740945052805549,
"res_400": 0.3420632609886434,
"res_450": 0.14758272030066333,
"res_475": 0.051083439669293566,
"res_490": -0.007634204659142415,
"res_500": -0.046625742175670996,
"res_510": -0.08701341292655208,
"res_525": -0.14710395674869248,
"res_550": -0.2500832704002971,
"res_600": -0.47125422339018486,
"res_700": -1.0367358917741667,
"t1k_1000": -0.5485503441283766,
"t1k_1050": -0.3167571986018916,
"t1k_1100": 0.17418996613796175,
"t1k_1200": 0.16460873061812337,
"t1k_1500": -0.2608862087302641,
"t1k_2000": 0.4691205643000097,
"t1k_500": -0.10662852895220419,
"t1k_678.7611083984375": 0.027561491468636564,
"t1k_800": 0.4822946693646578,
"t1k_900": 0.30705968131126005,
"t1k_950": -0.28077425246942284,
"t1kq_1000": -0.21168039641542824,
"t1kq_1050": -0.0011503254358071785,
"t1kq_1100": 0.4344054852348732,
"t1kq_1200": -0.11613738787651275,
"t1kq_800": -0.05292751629888988,
"t1kq_900": 0.54236470368085,
"t1kq_950": 0.026290891468426707
}
+64
View File
@@ -0,0 +1,64 @@
{
"al_12": 0.6589760322215087,
"al_18": 0.8869285064658652,
"al_24": 0.9872335108976188,
"al_3": 0.21467895021449704,
"al_6": 0.36768989474153474,
"al_9": 0.5175959713969748,
"comb_1000": -0.8390262089726862,
"comb_1500": -0.8112870378396803,
"comb_2000": -0.809711515922245,
"comb_3000": -0.8085898984121008,
"comb_500": -0.8092538987382586,
"dual_0.1_2000": -0.08995281533909788,
"dual_0.1_500": -0.05320412461191143,
"dual_0.2_2000": 0.3019781559654401,
"dual_0.2_500": -0.053358134264814706,
"dual_0.3_2000": 0.2902583798578576,
"dual_0.3_500": -0.05348688556617939,
"dual_0.5_2000": 0.19650867858105603,
"dual_0.5_500": -0.05387091733344582,
"dual_0.7_2000": 0.18642830232473012,
"dual_0.7_500": -0.05441615588199915,
"dual_1.0_2000": 0.23254813837808486,
"dual_1.0_500": -0.05551608061654063,
"dual_1.5_2000": 0.3084791235429092,
"dual_1.5_500": -0.057043442345604364,
"dual_10.0_2000": 0.4377055060111567,
"dual_10.0_500": 0.20966855072081148,
"dual_2.0_2000": 0.3539423473152209,
"dual_2.0_500": -0.0574902922094877,
"dual_3.0_2000": 0.3970249956345811,
"dual_3.0_500": -0.05247116979493978,
"dual_5.0_2000": 0.42379656083975153,
"dual_5.0_500": -0.010273028233688034,
"res_300": 0.740945052805549,
"res_400": 0.3420632609886434,
"res_450": 0.14758272030066333,
"res_475": 0.051083439669293566,
"res_490": -0.007634204659142415,
"res_500": -0.046625742175670996,
"res_510": -0.08701341292655208,
"res_525": -0.14710395674869248,
"res_550": -0.2500832704002971,
"res_600": -0.47125422339018486,
"res_700": -1.0367358917741667,
"t1k_1000": -0.5485503441283766,
"t1k_1050": -0.3167571986018916,
"t1k_1100": 0.17418996613796175,
"t1k_1200": 0.16460873061812337,
"t1k_1500": -0.2608862087302641,
"t1k_2000": 0.4691205643000097,
"t1k_500": -0.10662852895220419,
"t1k_678.7611083984375": 0.027561491468636564,
"t1k_800": 0.4822946693646578,
"t1k_900": 0.30705968131126005,
"t1k_950": -0.28077425246942284,
"t1kq_1000": -0.21168039641542824,
"t1kq_1050": -0.0011503254358071785,
"t1kq_1100": 0.4344054852348732,
"t1kq_1200": 1.1724811230541312,
"t1kq_800": 0.6775178072048177,
"t1kq_900": 0.54236470368085,
"t1kq_950": 0.026290891468426707
}
+54 -1
View File
@@ -42,12 +42,65 @@ def k_exp(x):
return np.exp(x) return np.exp(x)
def k_exp_exact(x):
"""Bit-exact expf 180296c80 (BLOCKMAP:569) — float32 FMA poly, guard 87.3365.
Python структурный прокси: np.exp (float64) — точный C++ порт в dsp/exp2.cpp
через fmaf+бит-манипуляции (1824c...), ошибка <0.5 ulp vs плагин."""
return np.exp(np.asarray(x, dtype=np.float64)).astype(np.float32)
def k_div(a, b): def k_div(a, b):
"""divide-ядро 1803a06a0: dst = B/A (~0.5 ulp, rcp+таблицы+полином). """divide-ядро 1803a06a0: dst = B/A (~0.5 ulp, rcp+таблицы+полином).
Структурная фаза: точное деление.""" Структурная фаза: точное деление."""
return b / a return b / a
def k_div_exact(a, b):
"""Bit-exact DIVIDE 1803a06a0 (BLOCKMAP:580) — rcp+quant+vpermps+poly.
Структурный прокси: точное деление; C++ порт копирует 0.5ulp полином
0.207515 -0.241687 0.288535 -0.360671 ... 0.240264 через vpermps tbl@1821269c0."""
return np.asarray(b, dtype=np.float64) / np.asarray(a, dtype=np.float64)
def chain_9_19_sim(bands, tmp6f8, accVec, warp, att, rel, nbin=None):
"""CHAIN 9-19 BLOCKMAP:620-644 op-by-op (python структурный прокси).
bands: log-domain bands_curve_i (size nbin), tmp6f8: f6f8, accVec: ACC_i
warp: kWarp@5406a8, att@5406c8 rel@5406e8 — все 2049.
Возвращает bands mutated (log domain после IIR, перед FIR)."""
if nbin is None:
nbin = len(bands)
bands = np.asarray(bands, dtype=np.float64)
tmp6f8 = np.asarray(tmp6f8, dtype=np.float64)
accVec = np.asarray(accVec, dtype=np.float64)
# 9a zero already (vec698 irrelevant), 9b: tmp6f8 += 0.8 (p=1.0 *0.8)
# 9c DIVIDE dst=bands A=bands B=tmp6f8+0.8? Actually B=tmp6f8, but step 9b already added 0.8
# BLOCKMAP 24mm5: bands = tmp6f8 / bands (divide B/A)
# Use k_div_exact
b = tmp6f8 + 0.8 # proxy for step 9b effect (when tmp6f8 initially 0, b=0.8)
# If tmp6f8 already has data, 9b is additive 0.8, so b = tmp6f8 +0.8
# For calibration where tmp6f8 is 0, this yields 0.8/bands
bands = k_div_exact(np.maximum(bands, 1e-30), b)
# 10 dc40: tmp6f8 = bands - ACC
tmp6f8 = bands - accVec
# 11 FMA ATT/REL half-split
# true triples 12B re/im/coef, scalar proxy: upper half ATT, lower REL
for i in range(nbin):
if i < nbin // 2:
tmp6f8[i] += att[i] * accVec[i]
else:
tmp6f8[i] += rel[i] * accVec[i]
# 14 EXP#1 + -1 (order fixed 24mm2)
bands = k_exp_exact(bands) - 1.0
# 15 array-mul X*track (track is external, warp mul is step16)
# 16 *kWarp + LOG#2
bands = bands * warp
bands = np.log(np.maximum(bands, 1e-30))
# 16b IIR4x2 bidir log-domain base 0x340510 — proxy via kIIR_A1/B1
# keep as identity for now (real IIR is mild 0.003-0.19 per 24cc)
# FIR will be applied externally via fir_kernel
return bands, tmp6f8
# ------------------------------------------------------------ данные ----- # ------------------------------------------------------------ данные -----
def load_tract(path): def load_tract(path):
"""tract_*.txt: k am res lvl_raw band_level prewarp w""" """tract_*.txt: k am res lvl_raw band_level prewarp w"""
@@ -116,7 +169,7 @@ def win_periodic_hann(N):
# ------------------------------------------------ FIR-цепь (24mm9) -------- # ------------------------------------------------ FIR-цепь (24mm9) --------
NFRAME = 4096 # n=[ctx+0x540534] NFRAME = 4096 # n=[ctx+0x540534]
NBINS_FIR = NFRAME // 2 + 1 NBINS_FIR = NFRAME // 2 + 1
Q_EXP = 0.80 # скаляр аргумента EXP; источник в 1803831c0 (ОТКРЫТО) Q_EXP = 0.8002203702926636 # .rdata 1820013f0, refines 0.80 (BLOCKMAP 52b716)
def winfreq_fall(): def winfreq_fall():
+19
View File
@@ -176,6 +176,25 @@ def main():
store['lut_G'] = struct.unpack('<f', fb4[12:16])[0] store['lut_G'] = struct.unpack('<f', fb4[12:16])[0]
store['lut_mode'] = fb4[16] store['lut_mode'] = fb4[16]
for off in SLOTS[1:]: for off in SLOTS[1:]:
if off == 0x5407c8:
# ACC is table of pointers per-band [5407c8 + i*8] -> buf 2049 f32 each
for bi in range(4):
q = rd(ctx + off + 8*bi, 8)
if not q: continue
ptr = struct.unpack('<Q', q)[0]
if ptr < 0x10000: continue
ab = rd(ptr, 2049 * 4)
if ab:
store[hex(off) + f"_{bi}"] = np.frombuffer(ab, dtype='<f4').astype(np.float32)
# also store raw table pointer for debug
q = rd(ctx + off, 8)
if q:
ptr = struct.unpack('<Q', q)[0]
if ptr >= 0x10000:
ab = rd(ptr, NARR * 4)
if ab:
store[hex(off)] = np.frombuffer(ab, dtype='<f4').astype(np.float32)
continue
q = rd(ctx + off, 8) q = rd(ctx + off, 8)
if not q: if not q:
continue continue
+43 -4
View File
@@ -37,6 +37,11 @@ BP_CIN = 0x180529c60
BP_COUT = 0x180529ee1 BP_COUT = 0x180529ee1
BP_AIN = 0x180016140 BP_AIN = 0x180016140
BP_AOUT = 0x18000332c BP_AOUT = 0x18000332c
# Chain 9-19 breakpoints (BLOCKMAP:620-644)
BP_CHAIN_START = 0x18052a580 # band loop start (52a580)
BP_CHAIN_LOG1 = 0x18052a63a # LOG#1 in chain (52a63a)
BP_CHAIN_LOG2 = 0x18052aefd # LOG#2 in chain (52aefd)
BP_CHAIN_END = 0x18052b3cd # chain end / FIR start (52b3cd)
CTX_SLOTS = {'scr': 0x540628, 'trk': 0x540688, 'cur': 0x540678, CTX_SLOTS = {'scr': 0x540628, 'trk': 0x540688, 'cur': 0x540678,
'fir_ptr': 0x540668} 'fir_ptr': 0x540668}
@@ -281,7 +286,12 @@ def main():
('DIV', BP_DIV), ('DC40', BP_DC40), ('DIV', BP_DIV), ('DC40', BP_DC40),
('EXPVAR', BP_EXPVAR), ('FN', BP_FN), ('EXPVAR', BP_EXPVAR), ('FN', BP_FN),
('CIN', BP_CIN), ('COUT', BP_COUT), ('CIN', BP_CIN), ('COUT', BP_COUT),
('AIN', BP_AIN), ('AOUT', BP_AOUT)): ('AIN', BP_AIN), ('AOUT', BP_AOUT),
# Chain 9-19 breakpoints
('CHAIN_START', BP_CHAIN_START),
('CHAIN_LOG1', BP_CHAIN_LOG1),
('CHAIN_LOG2', BP_CHAIN_LOG2),
('CHAIN_END', BP_CHAIN_END)):
if not mapped(addr): if not mapped(addr):
print('!! %s@%#x не смапплен — пропуск' % (nm_ := name, addr), flush=True) print('!! %s@%#x не смапплен — пропуск' % (nm_ := name, addr), flush=True)
continue continue
@@ -299,9 +309,11 @@ def main():
samples = [] samples = []
hits = {'COPY': 0, 'EXP': 0, 'DF0': 0, 'DF0RET': 0, 'TRACKSAVE': 0, hits = {'COPY': 0, 'EXP': 0, 'DF0': 0, 'DF0RET': 0, 'TRACKSAVE': 0,
'DIV': 0, 'DC40': 0, 'EXPVAR': 0, 'FN': 0, 'DIV': 0, 'DC40': 0, 'EXPVAR': 0, 'FN': 0,
'CIN': 0, 'COUT': 0, 'AIN': 0, 'AOUT': 0} 'CIN': 0, 'COUT': 0, 'AIN': 0, 'AOUT': 0,
'CHAIN_START': 0, 'CHAIN_LOG1': 0, 'CHAIN_LOG2': 0, 'CHAIN_END': 0}
track_by_tid = {} track_by_tid = {}
track_dumps = [] track_dumps = []
chain_dumps = []
regs_by_tid = {} regs_by_tid = {}
t_start = time.time() t_start = time.time()
@@ -410,8 +422,35 @@ def main():
continue continue
if kind == 'DF0': if kind == 'DF0':
track_by_tid[pid] = regs.rdx track_by_tid[pid] = regs.rdx
# Chain 9-19: dump buffers at key points
if kind in ('CHAIN_START', 'CHAIN_LOG1', 'CHAIN_LOG2', 'CHAIN_END'):
rec_c = {'kind': kind, 'tid': pid,
't': round(time.time()-t_start, 4),
'rcx': regs.rcx, 'rdx': regs.rdx,
'r8': regs.r8, 'r9': regs.r9,
'rsp': regs.rsp}
try:
# chain input: [678i] = rcx (bands_curve), [6f8] = rdx (tmp6f8)
if regs.rcx > 0x10000 and regs.rdx > 0x10000:
rec_c['bands_in'] = rd_f32(regs.rcx, min(2050, 512)) # first 512 vals
rec_c['tmp6f8_in'] = rd_f32(regs.rdx, min(2050, 512))
rec_c['acc_ptr'] = regs.r8 if regs.r8 > 0x10000 else 0
if rec_c['acc_ptr']:
rec_c['acc_in'] = rd_f32(rec_c['acc_ptr'], min(2050, 512))
except OSError as e:
rec_c['err'] = str(e)
chain_dumps.append(rec_c)
hits[kind] += 1
# standard int3 handling
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
regs.rip = site
setregs(pid, regs)
pt(PTRACE_SINGLESTEP, pid, 0, 0)
os.waitpid(pid, __WALL)
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
pt(PTRACE_CONT, pid, 0, 0)
continue
if kind in ('CIN','COUT'): if kind in ('CIN','COUT'):
key='cin_%d'%pid if kind=='CIN' else 'cout_%d'%pid
if kind=='CIN': if kind=='CIN':
regs_by_tid[pid]=dict(rdx=regs.rdx,r12=regs.r12, regs_by_tid[pid]=dict(rdx=regs.rdx,r12=regs.r12,
rcx=regs.rcx) rcx=regs.rcx)
@@ -632,7 +671,7 @@ def main():
snap_ptrs[nm] = p snap_ptrs[nm] = p
snap_arr[nm] = rd_f32(p, 4100) snap_arr[nm] = rd_f32(p, 4100)
with open(os.path.join(outdir, 'chain_samples.pkl'), 'wb') as f: with open(os.path.join(outdir, 'chain_samples.pkl'), 'wb') as f:
pickle.dump({'samples': samples, 'snap_ptrs': snap_ptrs, 'ctx': ctx}, f) pickle.dump({'samples': samples, 'chain_dumps': chain_dumps, 'snap_ptrs': snap_ptrs, 'ctx': ctx}, f)
np.savez_compressed(os.path.join(outdir, 'ctx_snap.npz'), **snap_arr) np.savez_compressed(os.path.join(outdir, 'ctx_snap.npz'), **snap_arr)
print('saved %d -> %s' % (len(samples), outdir), flush=True) print('saved %d -> %s' % (len(samples), outdir), flush=True)
for _ in range(600): for _ in range(600):