Compare commits

..
9 Commits
Author SHA1 Message Date
Matiq 07cd4b7dc0 23a: docs sync to post-22z state — AGENTS header/open-gaps/key-files and BITEXACT_PLAN entry-point updated: priority #1 = bands[] input semantics (static decode of conv body or in-callback trace), canon stays LAWAFFINE 7.4/1.85 TOTAL 1.931, refuted branches documented 2026-08-23 19:00:44 +03:00
Matiq 738ffccad4 22z: mask->FIR call sequence decoded (535a70 thunk chain -> 1802a24c0 body; WIN_freq second-half segment multiply; FIR[0]=1,FIR[1]=0); IDFT-window-DFT spreading hypothesis NUMERICALLY REFUTED (max 2.6dB at skirt vs needed 11.8) -> dual skirt mechanics live in detector/template domain, not post-processing 2026-08-23 18:55:36 +03:00
Matiq 91101b2135 22y: live ctx capture pipeline works (scripts/dualtrace.py, realtime playback + chunked heap snapshot); found config-dependent reduction curves 0x540768/788/7f8 (peak follows band fc; 7f8 min exactly 1.0 => R=1/mask, notch-shaped); acc/f6f8 arrays ZERO in steady state; DECISIVE: applied filter != pointwise R (dual skirt 3dB vs real 11.8) -> FFT-conv wide-window spreading is the missing mechanism 2026-08-23 18:40:25 +03:00
Matiq 64beaa7131 22x: NO-alpha theorem — no res-exponent makes detector level consistent across families (dual needs alpha<0, fc-scans break at alpha<=0); dual-vs-q table: cut@center CONSTANT across q, skirt cut falls ~exp(-res/0.49) converging at res>=2 -> notch geometry lives in the fine-grid twin template (536300), not per-bin arithmetic; pointwise (am,res^alpha)+single-law class REFUTED 2026-08-23 18:04:47 +03:00
Matiq 13c2d028bf 22w: faithful mask-shaping chain v1 behind RT_FAITHFUL (fnfaith.cpp) — exact 180533340 freq-warped bidir-IIR coefficients (fc_bin=85@48k/4096, constants from dump); corpus TOTAL 5.315 but comb 2.57 best-ever; decisive: mask shape cannot balance dual tones -> level contrast is created by the DETECTOR (time-domain twin bank hypothesis priority #1) 2026-08-23 17:04:23 +03:00
Matiq 34156315e6 22v: full block map of FUN_180529fe0 from raw asm — 4 bidir-IIR states, per-band acc array [0x5407c8+i*16] confirmed (21b semantics), ILT thunk table resolved; coefficient generator decoded (530b60 + 533340): frequency-warped one-pole g=fc/i below crossover — the real mask smearing mechanism; full disasm f529fe0_full.dis (old was truncated at 52a813) 2026-08-23 16:47:51 +03:00
Matiq 79c11dc220 22u: raw-asm re-decode of FUN_180529fe0 — multiband pre-combine inside method, per-band scale->kernel->2x bidir-double-IIR (52d650 fully decoded, per-bin double coefs), band-state arrays 0x540768[i]/0x5407a8[i]; 0x5407c8 acc has NO runtime reader (21b addr wrong); offline bidir-IIR smoothing hypothesis REFUTED (monotonic degradation) 2026-08-23 15:53:58 +03:00
Matiq 376bd955c9 22t: two-factor laws REFUTED by descent (quad Q->0, resrp rp->0 — geometry already in lvl=am/res); real render of sim-optimum 7.6/1.694 = 1.898 with group regressions, canon stays; error budget: dual = 62% of corpus abs-error -> inter-band acc/f6f8 consumer is priority #1 2026-08-23 15:10:06 +03:00
Matiq 3857205a6b 22s: offline law-fit infra (full replay sim, 2s/corpus) — NO single affine (A,S) exists even on pure tones; family slopes 0.85-2.19, global LSQ resid 0.60 dB structured by rms/fc-dist; scalar law saturated ~1.87-1.93, canon stays HEAD 2026-08-23 14:36:27 +03:00
15 changed files with 3485 additions and 32 deletions
+25 -17
View File
@@ -4,11 +4,14 @@ Bit-exact реверс DSP-ядра oeksound soothe2 (VST3) → транскри
Полное журналирование — в `handoff/NOTES_LEVEL.md`, `handoff/NOTES_TWIN.md`, Полное журналирование — в `handoff/NOTES_LEVEL.md`, `handoff/NOTES_TWIN.md`,
`handoff/NOTES_CAPTURE.md`, `roadmap.md`. `handoff/NOTES_CAPTURE.md`, `roadmap.md`.
> **Текущая фаза: bit-exact НЕ достигнут** — dB-параллель двумя канонами: bridge > **Текущая фаза (2026-08-23, после 22z):** канон = структурная цепь 48k/4096 с
> (корпус mean 1.594 dB, comb 10.1) и структурная цепь FUN_180529fe0 на 48k/4096 > RT_LAWAFFINE=7.4,1.85 (TOTAL 1.931; bridge 1.594 — гейт не пройден). ГЛАВНЫЙ
> (comb **6.12** — уже лучше bridge; однополосные хуже; разрыв = насыщение кривой > ОТКРЫТЫЙ ВОПРОС — семантика входов `bands[]` детектора: доказанно НЕ поточечная
> редукции). Путь к байтам — в `BITEXACT_PLAN.md` (8 шагов, пере-скоуп Шага 2). > функция от (am,res^α) [22x], не форма-постобработка [22w], не спрединг
> ЭТОТ ФАЙЛ ЧИТАЙ ПЕРВЫМ. Bridge — запасной канон; структурная цепочка — целевая. > IDFT→окно→DFT [22z]. Живая кривая редукции R=1/mask захвачена (слот 0x5407f8,
> dualtrace.py), но применённый фильтр ≠ её поточечной копии — механизм живёт ДО
> кривой (twin-шаблон/скалярный драйв). Журнал: NOTES_LEVEL 22s22z; карта метода:
> handoff/BLOCKMAP_529fe0.md. ЭТОТ ФАЙЛ ЧИТАЙ ПЕРВЫМ.
## Золотое правило (обязательно) ## Золотое правило (обязательно)
1. **Цель — bit-exact реверс кода**, НЕ эмпирическая подгонка кривых. Каждый параметр 1. **Цель — bit-exact реверс кода**, НЕ эмпирическая подгонка кривых. Каждый параметр
@@ -102,18 +105,19 @@ scale → LUT level-domain (t^γ·MULT, γ=0.344 decomp / MULT=4.2 placeholder)
CMAX) не закрывает тон+шум одновременно → res/dual регресс у любой точки; ищем CMAX) не закрывает тон+шум одновременно → res/dual регресс у любой точки; ищем
контент-зависимый механизм. Инструментарий RT_DUMP_BIN / RT_DUMP_ALL в коде (opt-in). контент-зависимый механизм. Инструментарий RT_DUMP_BIN / RT_DUMP_ALL в коде (opt-in).
### Открытые bit-exact/структурные пробелы (см. NOTES_LEVEL.md) ### Открытые пробелы (после 22z; детали — NOTES_LEVEL 22s22z)
1. Насыщение кривой редукции (см. выше) — приоритет №1. 1. **Семантика входов `bands[]` в FUN_180529fe0** — главный вопрос. Отвергнуто:
2. `combine/аккумулятор 0x5407c8`: семантика декодирована (21b), но acc/f6f8 НЕ имеют lvl=f(am,res^α) с единым законом [22x, теорема], форма-постобработка маски [22w],
однополосного консюмера — искать точку потребления (межполосный каскад). ВНИМАНИЕ: спрединг IDFT→окно→DFT [22z]. Живой захват: кривая редукции R=1/mask @слот
`FUN_1805316e0` = writer коэффициентов, НЕ комбинер масок. 0x5407f8 (пик следует за fc полосы), acc/f6f8 нули в стационаре.
3. ~~BandConfig `ctx+0x188` A/B/γ — противоречие между сессиями~~ **РАЗРЕШЕНО Пути: (а) декод тел conv 0x1802a24c0 + complex-op шагов с буферами
(22b): live = 24/28/1 у ВСЕХ конфигов, но весь кластер FUN_180563440/563a60 — 0x540548/550/598; (б) синхронная трасса буферов в аудио-колбеке.
GUI-timer only; аудио-путь (FUN_180529fe0) BandConfig не читает. Шаг 7 в исходной 2. dual-таблица по q (22x): cut@центра константен при любом q, Δcut скайрта ≈
постановке опровергнут; LUT-константы структурной цепи помечены EMPIRICAL.** линейно по −ln res с b≈2.25 — геометрия twin-шаблона на сетке 0.25 Гц.
4. PRNG-пролог (LCG+LUT → fVar30) — залочен (fVar30=1 при live state 112), dry/wet rnd импорт. 3. FFT-conv: последовательность вызовов декодирована (BLOCKMAP/22z), тело
5. Бит-экзактный exp2 (0x26b820); FFT-conv понижен до P3 (окно near-flat, NOTES:18c); 0x1802a24c0 (AVX2) и complex-op шаги — не расшифрованы.
SR-геометрия 48k/4096 сделана (render48k). 4. Бит-экзактный exp2 (0x26b820); бит-точная DSP-FFT (P3); стерео M8 (P5) — отложены.
5. Источник констант RT_LAWAFFINE A/S в декомпе не найден (помечено EMPIRICAL).
## Структура ключевых файлов ## Структура ключевых файлов
- `dsp/framed_model.{cpp,hpp}` — C++ порт mask-apply цепи: bridge-канон + структурная - `dsp/framed_model.{cpp,hpp}` — C++ порт mask-apply цепи: bridge-канон + структурная
@@ -128,6 +132,10 @@ scale → LUT level-domain (t^γ·MULT, γ=0.344 decomp / MULT=4.2 placeholder)
- `handoff/nls_dasm/` — 134 дизассембла декомпа. - `handoff/nls_dasm/` — 134 дизассембла декомпа.
- `/tmp/consumers_out.txt` — полный decomp `FUN_180529fe0` (уникален, вне git). - `/tmp/consumers_out.txt` — полный decomp `FUN_180529fe0` (уникален, вне git).
- `/tmp/snap_rt.bin` — live-снимок (ctx 0x2370040, SR 48000). - `/tmp/snap_rt.bin` — live-снимок (ctx 0x2370040, SR 48000).
- `handoff/BLOCKMAP_529fe0.md` — полная блок-карта FUN_180529fe0 по raw asm (22v).
- `scripts/lawfit22r.py` — офлайн реплей-сим пайплайна (~2 c корпус), фиты законов.
- `scripts/resalpha.py` — сбор трактов + теорема об отсутствии α (22x).
- `scripts/dualtrace.py` + `play_loop.lua` — живой захват ctx при realtime-playback (22y).
## Чистая работа ## Чистая работа
- Не коммитить: `*.bin`(дампы 112М), `*.wav/rpp`, `*.log`, `dsp/build/`, `ghidra-proj/`, - Не коммитить: `*.bin`(дампы 112М), `*.wav/rpp`, `*.log`, `dsp/build/`, `ghidra-proj/`,
+25 -15
View File
@@ -18,6 +18,13 @@ comb 10 dB). Bit-exact ДОСТИЖИМ (F0 gate: плагин байт-дете
> декоративная копия (не источник стейта); depth=±18dB/trim=±24dB; WAV bext/junk > декоративная копия (не источник стейта); depth=±18dB/trim=±24dB; WAV bext/junk
> грабли подтверждены живьём. > грабли подтверждены живьём.
> Новый приоритет №1 — Шаг 9 (ниже). > Новый приоритет №1 — Шаг 9 (ниже).
>
> **ОБНОВЛЕНИЕ 2026-08-23 (сессии 22s22z):** Шаг 9 закрыт иначе — пол решён
> алгебраически ещё в 22e; фронт сместился. Отвергнуты: скалярные законы A/S
> (насыщение ~1.871.93, 22s/t), двухфакторные quad/resrp (22t), точечные
> lvl=f(am,res^α) — «теорема об отсутствии α» (22x), спрединг IDFT→окно→DFT (22z).
> Новый приоритет №1 — семантика входов `bands[]` (см. AGENTS.md и NOTES_LEVEL
> 22s–22z); карта метода — handoff/BLOCKMAP_529fe0.md.
Цель фазы C (bit-exact): воспроизвести `FramedDetector` (= fn `FUN_180529fe0` mono-path) Цель фазы C (bit-exact): воспроизвести `FramedDetector` (= fn `FUN_180529fe0` mono-path)
настолько точно, что `verify_bit_exact.py` даёт побайтовое совпадение на рендерах настолько точно, что `verify_bit_exact.py` даёт побайтовое совпадение на рендерах
@@ -232,18 +239,21 @@ NOTES 22e) ⇒ пол живёт в другом куске аудио-пути.
## 4. Точка входа для следующей сессии ## 4. Точка входа для следующей сессии
1. Прочитать `AGENTS.md`, этот `BITEXACT_PLAN.md`, `handoff/NOTES_LEVEL.md` 1. Прочитать `AGENTS.md`, затем NOTES_LEVEL **UPDATE 22s22z** (журнал актуальной
апдейты **20j / 21a-21e / 22a-22e** (цепочка, Phase-B негативы, Шаг 7 опровержение, фазы) и `handoff/BLOCKMAP_529fe0.md` (блок-карта FUN_180529fe0 по raw asm).
параметр-мост, пол blend·ln10/20) и `dsp/rt_mask_tables.hpp` / `dsp/rt_weights.hpp`. 2. Собрать: `touch dsp/framed_model.cpp && cmake --build dsp/build --target render48k framed_test`
2. Собрать: `cmake --build dsp/build --target render48k framed_test`. (TOOLING HAZARD: touch перед сборкой обязателен).
3. Бейзлайны: `python3 scripts/corpus.py` (bridge, 62 случая, §0) и 3. Бейзлайны: канон = `RT_LUT_OFF=1 RT_IIR12=0 RT_NOWARP=1 RT_NOBLEND=1 RT_NOIIR3=1
`python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json`. RT_LAWAFFINE=7.4,1.85 python3 scripts/corpus_structural.py` → TOTAL 1.931;
4. ПРИОРИТЕТ №1 — **Шаг 9** (пол редукции): bridge-гейт: `--vs-bridge scripts/baseline_bridge.json` (1.594).
a. 9a: полный декод FUN_180529c60 + FUN_1804d56b0 (f_529c60.dis готов). 4. **ПРИОРИТЕТ №1 — семантика входов `bands[]`**:
b. 9b: grep 0x1824c3cd4 по аудио-функциям; hunt клампа маски. a. Статика: декод тел conv 0x1802a24c0 (float)/0x1802fa420 (double,
c. 9c: предсказания пола — рендеры mix/sens через `setparam.lua` (мост готов, резолв ILT-стабов через idx@0x1826159a0, см. BLOCKMAP thunk-таблицу) и
XML-правки стейта НЕ работают — NOTES 22c). complex-op шагов с буферами ctx+0x540548/550/598.
d. 9d: порт + корпус с гейтом. b. Динамика: синхронная трасса буферов bands[]/f6f8/acc в момент аудио-
5. Инструменты сессии: `setparam.lua`/`dump_params.lua` (параметр-мост), колбека (`scripts/dualtrace.py` + `play_loop.lua`; между кадрами acc/f6f8
`scripts/rpp_setparam.py` (только формат-хирургия!), `RT_LUT_*` env в нули — снапшот должен попадать В колбек, см. NOTES_CAPTURE 22y).
framed_model.cpp (солвер констант), `scripts/step7_capture.py` (live BandConfig). 5. Инструменты: `scripts/lawfit22r.py` (офлайн реплей, 2 c корпус — ЛЮБОЙ закон),
`scripts/resalpha.py` (тракты+теорема α), live-capture `dualtrace.py`.
6. Факт для калибровки интуиции: dual-таблица по q в NOTES 22x (cut@центра
константен; Δскайрта ≈ линейно по −ln res, b≈2.25 дБ/e-fold).
+1
View File
@@ -25,6 +25,7 @@ add_library(soothe2_dsp SHARED
exp2.cpp exp2.cpp
leveltrack.cpp leveltrack.cpp
framed_model.cpp framed_model.cpp
fnfaith.cpp
fn529fe0.cpp fn529fe0.cpp
rt_weights.cpp rt_weights.cpp
rt_mask_tables.cpp rt_mask_tables.cpp
+105
View File
@@ -0,0 +1,105 @@
#include "fnfaith.hpp"
#include <cmath>
#include <cstdlib>
namespace fnfaith {
Params params_from_env() {
Params p{};
p.scale = getenv("RT_FAITH_SCALE") ? atof(getenv("RT_FAITH_SCALE")) : 1.0;
p.kappa = getenv("RT_FAITH_KAPPA") ? atof(getenv("RT_FAITH_KAPPA")) : 0.0094;
p.p = getenv("RT_FAITH_P") ? atof(getenv("RT_FAITH_P")) : 0.822;
p.tau1 = getenv("RT_FAITH_TAU1") ? atof(getenv("RT_FAITH_TAU1")) : 19.03;
p.mult1 = getenv("RT_FAITH_MULT1") ? atof(getenv("RT_FAITH_MULT1")) : 360.0;
p.tau3 = getenv("RT_FAITH_TAU3") ? atof(getenv("RT_FAITH_TAU3")) : 10.68;
p.mult3 = getenv("RT_FAITH_MULT3") ? atof(getenv("RT_FAITH_MULT3")) : 2400.0;
p.tau4 = getenv("RT_FAITH_TAU4") ? atof(getenv("RT_FAITH_TAU4")) : 10.68;
p.mult4 = getenv("RT_FAITH_MULT4") ? atof(getenv("RT_FAITH_MULT4")) : 2400.0;
p.C_hz = getenv("RT_FAITH_C") ? atof(getenv("RT_FAITH_C")) : 1000.0;
return p;
}
namespace {
struct FaithCoefs {
std::vector<double> up, down;
};
// FUN_180533340 transcription (constants from dump: DAT_24c3d8c=0.5, DAT_24c46b8=-2pi)
FaithCoefs gen_coefs(size_t nbin, double sr, double tau, double mult, double p,
double C_hz, double kappa) {
FaithCoefs c;
c.up.assign(nbin, 0.0);
c.down.assign(nbin, 1.0);
const double srh = sr * 0.5;
const double fcnorm = (C_hz / srh) * (double)nbin;
for (size_t i = 1; i < nbin; i++) {
const double r = fcnorm / (double)i;
const double g = ((double)i <= fcnorm) ? r : std::pow(r, p);
const double cd = 1.0 / (g * tau / mult + 1.0);
double up = std::exp(cd * g * tau * kappa * (-6.283185307179586));
if (up > 1.0) up = 1.0;
if (up < 0.0) up = 0.0;
c.up[i] = up;
c.down[i] = 1.0 - up;
}
return c;
}
// FUN_18052d650: acc=0; forward all bins; backward n-2..1, acc persists into bwd.
void bidir(std::vector<double>& accst, const FaithCoefs& cf, float* x, size_t nbin) {
double acc = 0.0;
accst[0] = 0.0;
for (size_t i = 0; i < nbin; i++) {
acc = cf.up[i] * acc + cf.down[i] * (double)x[i];
x[i] = (float)acc;
}
for (size_t i = (size_t)((long long)nbin - 2); i >= 1; i--) {
acc = cf.up[i] * acc + cf.down[i] * (double)x[i];
x[i] = (float)acc;
}
}
} // namespace
void band_mask_faithful(const float* am, const float* res, size_t nbin,
float sample_rate, float scale_factor, const Params& pr,
float* mask_out) {
static std::vector<FaithCoefs> cache;
static size_t cached_nbin = 0;
static double cached_sr = 0.0;
static Params cached_pr{};
if (cache.empty() || cached_nbin != nbin || cached_sr != sample_rate ||
cached_pr.kappa != pr.kappa || cached_pr.p != pr.p ||
cached_pr.tau1 != pr.tau1 || cached_pr.mult1 != pr.mult1 ||
cached_pr.tau3 != pr.tau3 || cached_pr.mult3 != pr.mult3 ||
cached_pr.tau4 != pr.tau4 || cached_pr.mult4 != pr.mult4 ||
cached_pr.C_hz != pr.C_hz) {
cache.clear();
cache.push_back(gen_coefs(nbin, sample_rate, pr.tau1, pr.mult1, pr.p, pr.C_hz, pr.kappa));
cache.push_back(gen_coefs(nbin, sample_rate, pr.tau3, pr.mult3, pr.p, pr.C_hz, pr.kappa));
cache.push_back(gen_coefs(nbin, sample_rate, pr.tau4, pr.mult4, pr.p, pr.C_hz, pr.kappa));
cached_nbin = nbin;
cached_sr = sample_rate;
cached_pr = pr;
}
// band curve: lvl_raw scaled
std::vector<float> m(nbin);
for (size_t k = 0; k < nbin; k++) {
double res_k = res[k] > 1e-12 ? (double)res[k] : 1e-12;
double lvl = (double)am[k] / res_k * (double)scale_factor;
m[k] = (float)(std::exp2(-(lvl * pr.scale)) );
}
std::vector<double> accst(1, 0.0);
bidir(accst, cache[0], m.data(), nbin); // #1
bidir(accst, cache[0], m.data(), nbin); // #2
bidir(accst, cache[1], m.data(), nbin); // #3
bidir(accst, cache[2], m.data(), nbin); // #4a
bidir(accst, cache[2], m.data(), nbin); // #4b
for (size_t k = 0; k < nbin; k++) mask_out[k] = m[k];
}
} // namespace fnfaith
+45
View File
@@ -0,0 +1,45 @@
#pragma once
// FAITHFUL mask-shaping chain (22w) — transcription of FUN_180529fe0 per
// handoff/BLOCKMAP_529fe0.md (raw asm). Replaces process_band_structural
// when RT_FAITHFUL=1.
//
// band = lvl * s (s = RT_FAITH_SCALE, decomp: /0x1a0*0x870*0x88c)
// mask = exp2(-band) (bigkernel exp2 family)
// bidir(mask, A); bidir(mask, A) // states reset each pass (52d650 + inline)
// bidir(mask, B) // pass #3
// bidir(mask, C); bidir(mask, C) // pass #4 x2
// mirror upper half; dry/wet identity.
//
// Bidirectional one-pole per FUN_18052d650: acc = up[i]*acc + down[i]*x[i],
// forward over all bins, backward n-2..1 with persistent acc, reset per call.
// Coefficients per FUN_180533340 (freq-warped): fc_bin = C/(sr/2)*nbin;
// g = fc/i below crossover, powf(fc/i, p) above; c = 1/(g*tau/mult+1);
// up[i] = exp(-2*pi*kappa*c*g*tau), down[i] = 1-up[i].
//
// Decoded defaults (FUN_180530b60 log-interp @ live 0x540878/87c = 0.5):
// setA/B tau=19.03 mult=360 ; set3/set4 tau=10.68 mult=2400 ; p=0.822 ;
// C=1000 Hz ; kappa = 1/T8 ~ 0.0094 (T8 unknown -> env).
#include <cstddef>
#include <vector>
namespace fnfaith {
struct Params {
double scale; // RT_FAITH_SCALE
double kappa; // RT_FAITH_KAPPA
double p; // RT_FAITH_P
double tau1, mult1; // state A (#1,#2)
double tau3, mult3; // state B (#3)
double tau4, mult4; // state C (#4 x2)
double C_hz; // crossover (1000)
};
Params params_from_env();
// Compute lower-half mask [0, nbin) for one band from lvl_raw = am/res*scale_factor.
void band_mask_faithful(const float* am, const float* res, size_t nbin,
float sample_rate, float scale_factor, const Params& pr,
float* mask_out /* nbin */);
} // namespace fnfaith
+37
View File
@@ -4,6 +4,7 @@
#include "rt_mask_tables.hpp" #include "rt_mask_tables.hpp"
#include "rt_weights.hpp" #include "rt_weights.hpp"
#include "fn529fe0.hpp" #include "fn529fe0.hpp"
#include "fnfaith.hpp"
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
#include <algorithm> #include <algorithm>
@@ -245,6 +246,17 @@ static void process_band_structural(
} }
} }
// RT_RESPRP=1 (NOTES 22t): keep ONLY the res^rp factor of the warp cascade
// while NOWARP skips the full kBand768*kWarp*res^rp blanket. Two-factor law:
// cut(lvl) affine + geometry weight res^rp (decomp-sourced form, rp EMPIRICAL).
static const int resrp_only = getenv("RT_RESPRP") ? atoi(getenv("RT_RESPRP")) : 0;
if (nowarp && resrp_only) {
for (size_t k = 0; k < nbin; k++) {
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
mask_out[k] *= std::pow(res_k, rp);
}
}
// Step 9 (NOTES_LEVEL:830 + consumers_out.txt:955-1075): IIR3 inline, // Step 9 (NOTES_LEVEL:830 + consumers_out.txt:955-1075): IIR3 inline,
// TWO bidirectional passes [reset, forward, backward] x2 (state persists // TWO bidirectional passes [reset, forward, backward] x2 (state persists
// from forward into backward within a pair; reset between pairs). // from forward into backward within a pair; reset between pairs).
@@ -330,6 +342,11 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
res_.clear(); res_.clear();
track_.clear(); track_.clear();
// RT_DUMPRESPATH=<file> (NOTES 22t): static twin-response spectra per band,
// binary {int32 band, int32 nbin, float res[nbin]} records (append).
FILE* rp_dump = nullptr;
if (const char* dp = getenv("RT_DUMPRESPATH")) rp_dump = fopen(dp, "ab");
for (const auto& b : bands_) { for (const auto& b : bands_) {
std::vector<float> r(half + 1, 1.0f); std::vector<float> r(half + 1, 1.0f);
float sens_lin = std::pow(10.0f, b.sens * SENS_SCALE / 20.0f); float sens_lin = std::pow(10.0f, b.sens * SENS_SCALE / 20.0f);
@@ -348,8 +365,16 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
r[k] = std::sqrt(out[k].re * out[k].re + out[k].im * out[k].im); r[k] = std::sqrt(out[k].re * out[k].re + out[k].im * out[k].im);
r[k] = std::max(r[k], 1e-12f); r[k] = std::max(r[k], 1e-12f);
} }
if (rp_dump) {
int32_t bi = static_cast<int32_t>(res_.size());
int32_t nb = static_cast<int32_t>(r.size());
fwrite(&bi, sizeof(int32_t), 1, rp_dump);
fwrite(&nb, sizeof(int32_t), 1, rp_dump);
fwrite(r.data(), sizeof(float), r.size(), rp_dump);
}
res_.push_back(std::move(r)); res_.push_back(std::move(r));
} }
if (rp_dump) fclose(rp_dump);
track_.assign(bands_.size(), std::vector<float>(half + 1, 1.0f)); track_.assign(bands_.size(), std::vector<float>(half + 1, 1.0f));
} }
@@ -390,10 +415,22 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
for (size_t k = 0; k <= half; k++) mask[k] = 1.0f; for (size_t k = 0; k <= half; k++) mask[k] = 1.0f;
if (is_internal_grid(nfft_, sample_rate_)) { if (is_internal_grid(nfft_, sample_rate_)) {
// RT_FAITHFUL=1 (NOTES 22w): BLOCKMAP_529fe0 transcription path
static const int faithful = getenv("RT_FAITHFUL") ? atoi(getenv("RT_FAITHFUL")) : 0;
static const fnfaith::Params fparams = faithful ? fnfaith::params_from_env()
: fnfaith::Params{};
// same scale_factor as process_band_structural (line ~79)
constexpr float sf = 15.0f * 440.95f / 2048.0f;
for (size_t b = 0; b < bands_.size(); b++) { for (size_t b = 0; b < bands_.size(); b++) {
std::vector<float> band_mask(nfft_, 1.0f); std::vector<float> band_mask(nfft_, 1.0f);
if (faithful) {
fnfaith::band_mask_faithful(am_.data(), res_[b].data(), half + 1,
sample_rate_, sf, fparams,
band_mask.data());
} else {
process_band_structural(am_.data(), res_[b].data(), bands_[b], process_band_structural(am_.data(), res_[b].data(), bands_[b],
band_mask.data(), nfft_, sample_rate_); band_mask.data(), nfft_, sample_rate_);
}
for (size_t k = 0; k <= half; k++) { for (size_t k = 0; k <= half; k++) {
mask[k] = std::min(band_mask[k], mask[k]); mask[k] = std::min(band_mask[k], mask[k]);
} }
+155
View File
@@ -0,0 +1,155 @@
# BLOCK MAP: FUN_180529fe0 (полная разметка по raw asm, 22v)
Источник: `handoff/nls_dasm/f529fe0_full.dis` (soothe_mem.bin, база 0x180000000,
диапазон 529c6052ba00; старый f529fe0.dis был ОБРЕЗАН на 52a813).
Регистры: r13 = индекс полосы, r12 = state-ptr ctx+0x440518, esi/r8d = nbin,
rbx = nbands ([rsp+0x30]), rdi = ctx. Флаг 0x5408b8 выбирает float/double
вариант thunk-операций (семантика пар идентична).
## Исправление адресации (главное)
«Скалярные» буферы NOTES 21b — МАССИВЫ пер-полосных векторов (шаг 16 байт):
- bands[i] = [ctx+0x540678+i·16] — кривые полос
- acc[i] = [ctx+0x5407c8+i·16] — ПЕР-ПОЛОСНЫЙ аккумулятор combine
- track[i] = [ctx+0x540768+i·16] — трек основного цикла
## Thunk-таблица (ILT-стаб 180001xxx -> jmp [table + idx*8], idx @1826159a0)
Резолв при live idx=4; семантика из тел + NOTES 21b:
| стаб float | стаб double | impl | op |
|---|---|---|---|
| 0x180001f10 | 0x180001c70 | dc40 / **8d60** | dst = B A (sub) |
| 0x180001fa0 | 0x180001940 | ee20→487a0 / **3c40** | fma att/rel половин |
| 0x180001d60 | —(встречен в 1b80-паре?) | — / **5a20** | dst += B (add) |
| 0x180001970 | — | **3f40** | sub (float) |
| 0x1800019a0 | — | **4200** | mul scalar? (делегирует 181a63fe0) |
| 0x180001a00 | — | **4720** | mul |
| 0x180001a60 | — | **5160** | sub DOUBLE |
| 0x180001850 | — | **25e0** | add DOUBLE |
| 0x180002000 | 0x180001c40 | ? | axpy-класс (band ⊕ track) |
| 0x180002060 | 0x180002120 | 10860/11940 | transform со скаляром xmm3 |
| 0x180002270 | 0x1800022a0 | 14c40/15060 | transform со скаляром xmm0/xmm1 |
| 0x180002030 | 0x180001d30 | ? | transform float/double |
| in-place кернелы | | 140950/140980/1409b0/1409e0/140a40/140ad0/140b00/140b60 | bigkernel-семейство (exp2/mask) |
## Карта блоков
### Пролог
- `52a03a52a396`: PRNG-пролог. LCG state @ctx+0x2404e0, шаги +0x3cdca,
+0x140236, +0x10d56, +0xdf6b6, +0xa8c5e, +0x72916; LUT ptr @ctx+0x5408b0;
константы 262b704/262b5c8(int)/262b700(int)/24c3c58. Результат: индексы
iVar7/iVar8 ([rsp+0x130]/счётчики) и стартовая позиция цикла.
Live-поведение залочено раньше (fVar30=1).
- `52a397`: r14 = &bands[0].
### Pre-combine #1 (52a39752a421)
- `52dbc0(f6f8_data, bands[0], nbin)` — copy bands[0] → f6f8.
- цикл i=1..nbands−1 по массиву @0x540688 (= &bands[1]!): transform(f6f8, bands[i])
через 20f0(float)/1850(double=25e0 ADD). ⇒ **f6f8 = Σ_{i≥1} bands[i]** (или min/max
— точный op 20f0 не залочен, кандидат ADD по double-паре!).
### Нормировка + mix-вес (52a42152a458)
- `52d920(f6f8, xmm11/[rsp+0x148](int), nbin)` — f6f8 /= K.
- `xmm7 = powf([ctx+0x2c], xmm13)` — mix^p.
### Pre-combine #2 (52a45e52a4f6) по всем полосам
- band[i] *= (xmm11 xmm7) [thunk 2030/1d30]
- f6f8 ⊕= ... с весом xmm7 [thunk 1fd0/2150]
⇒ взвешенное смешение кривых полос ДО основного цикла.
### Основной цикл по полосам (52a4fb..52b3c7, тело с 52a580)
На каждую полосу i (r13):
1. `scale` (52a58352a5c4): s = xmm11·[0x540870]; если флаг 0x5408b8:
s ← exp(PRNG)-ветка; затем ·[0x54088c]. Читается track_i (@0x540768[i]) в [rsp+0x40].
2. `transform(bands[i], s)` (52a5cc52a607): bands[i] *= s [2030/1d30].
3. флаг-оп (52a60852a645): transform(bands[i], bands[i]) через 140980/1409b0
(bigkernel, вероятно exp2-кернел) — только при флаге.
4. `52d650(state@0x440518, f6f8, bands[i], nbin)` (52a64652a658) — БИДИР-IIR #1:
band → f6f8 (double, коэф down@0x440528/up@0x4c0528, acc@0x540528, reset внутри).
5. Инлайн БИДИР-IIR #2 (52a65d52a85a, только при флаге; иначе прыжок на 6):
вход f6f8 → выход bands[i] (тот же state 0x440518, reset отдельно).
6. `transform(f6f8 ⊕ bands[i])` (52a85b52a896) [1970/1a60 = 3f40 SUB float /
5160 SUB double]: f6f8 = bands[i]? (аргументы rcx=f6f8, rdx=band).
7. **БИДИР-IIR #3** (52a89752aa6e, инлайн): state base 0x3404f8
(down@0x2404f8, up@0x2c04f8, acc@0x3404f8), длина из поля ctx+0x2404e8,
IN-PLACE по bands[i]. Reset каждый вызов.
8. `op(bands[i], скаляр xmm9=0?)` (52aa6e52aaaf) [2060/2120].
9. Ветка флага==0 (52aabc52ab90):
- op(vec@0x540698, f6f8, scalar=(конст1 [0x54087c])) [1a00/1be0 = mul]
- op(f6f8, scalar=[0x54087c]·xmm10) [2270/22a0]
- bigkernel `140b60/140950(bands[i], f6f8, bands[i], nbin)`
10. **COMBINE (52ab9052abd7)**: acc_i = [0x5407c8+i·16];
`op(rcx=acc_i, rdx=bands[i], r8=f6f8, r9=nbin)` [1f10→dc40 float-SUB /
1c70→8d60 double-SUB]: **f6f8 = bands[i] acc_i** ✓ NOTES 21b.
11. Половины f6f8 + fma (52abd752ad04):
- rbp = f6f8 + nbin (верхняя половина); op(f6f8_low?, ...) [2060/2120, scalar 0]
- op(rcx=f6f8_upper, rdx=vec@0x5406c8, r8=acc_i) [1fa0/1940 = **3c40 fma**]
- op(rcx=f6f8_lower, rdx=vec@0x5406e8, r8=acc_i) [1fa0/1940]
⇒ fma с коэф-массивами 0x5406c8 (upper/att) и 0x5406e8 (lower/rel) ✓.
12. `acc_i += bands[i]` (52acf452ad03) [1b80→6840 / 1d60→**5a20** add] ✓.
13. Ветка флага!=0 (52ad0452ae57): повтор п.9 с теми же адресами
(0x540698, f6f8-blend, bigkernel 1409e0/140ad0).
14. `op(bands[i], скаляры 0x1824c4680 / xmm13)` (52ae5852ae56 хвост).
15. `bands[i] ⊕= track_i [rsp+0x40]` (52ae7452ae8a) [2000/1c40].
16. `op(vec@0x5406a8, bands[i])` (52ae8f52aecа) [2000/1c40].
17. флаг → bigkernel in-place 140980/1409b0 (52aecb52af08).
18. **БИДИР-IIR #4 ×2** (52af0952b2af и повтор 52b0de52b2af):
state base 0x440510 (down@0x340510, up@0x3c0510, acc@0x440510),
длина ctx+0x340500, in-place bands[i]. ДВА прохода подряд (каждый с reset).
19. Финальные scale/op полосы (52b2af52b3af):
- скаляр `[ctx+0x1c+i·4] · xmm14` → transform [2030/1d30]
- bigkernel in-place 1409e0/140ad0 (при флаге) ИЛИ
op(scalar=xmm12/xmm8) [2270/22a0] + `140a40/140b00(band,band,scalar)`
20. Инкремент i (52b3af–52b3c7), выход при i ≥ nbands.
### Эпилог (52b3cd52b935)
- GUI-snapshot блок (флаг 0x2404bc): копии векторов @0x540728 ← bands[i]
(совпадает с consumers_out.txt:144-183).
- Остаток до 52b935: восстановить при транскрипции (вероятно dry/wet + mirror).
## Состояния бидир-IIR (4 базы, лейаут {down,+0x80000 up,+0x80010/+0x100010 acc})
| # | база | downCoef | upCoef | acc | длина |
|---|------|----------|--------|-----|-------|
| 12 (52d650+инлайн) | 0x440518 | +0x10 | +0x80010 | +0x100010 | nbin |
| 3 | 0x2c04f8-группа | 0x2404f8 | 0x2c04f8 | 0x3404f8 | поле ctx+0x2404e8 |
| 4 ×2 | 0x340510/0x3c0510 | 0x340510 | 0x3c0510 | 0x440510 | поле ctx+0x340500 |
## Открытые вопросы к транскрипции
1. Точный op thunk 20f0/1850 в pre-combine #1 (ADD — кандидат).
2. Аргументный порядок 2000/1c40 (axpy) и 2060/2120.
3. Семантика bigkernel-семейства 1409xx/140axx/140bxx (какой где: exp2(-x),
exp2(-x)·blend, mirror?).
4. Что пишется в track_i @0x540768[i] (writer вне метода — искать отдельной охотой;
кандидат FUN_18052e9b0).
5. Эпилог 52b4eb52b935.
## ДОПОЛНЕНИЕ 22v: декод генератора коэффициентов
### FUN_180530b60 (апдейт параметров, size=462)
- fVar2 = powf([ctx+0x540878], K1); fVar3 = powf([ctx+0x54087c], K2)
(оба live = 0.5 — параметры attack/release-класса);
- лог-интерполяция констант → [ctx+0x540894] и [ctx+0x540898] (тау-скаляры);
- вызовы генератора на все три состояния:
- 180533340(ctx+0x2404e8, tau=0x540894-ветка, C=DAT_1824c459c, sr, ...)
- 180533340(ctx+0x340500, tau=[0x540894], ...)
- 180533340(ctx+0x440518, tau=[0x540898], ..., mult=DAT_1824c4564)
### FUN_180533340(state, tau, C, sr, p, mult) — ГЕНЕРАТОР КОЭФ. БИДИР-IIR
```
sr' = sr · DAT_1824c3d8c
acc = 0; downCoef[0] = 1.0 (int-пара {0, 0x3ff00000} @+0x10)
n = state[0]; fc_norm = (C/sr')·n
for i in 1..n-1:
g = (i <= fc_norm) ? fc_norm/i : powf(fc_norm/i, p) # частотный варп!
c = 1/(g·tau/mult + 1)
up[i] = exp(c·g·tau/state[2] · DAT_1824c46b8)
down[i] = 1 up[i]
```
ЛЕЙАУТ СОСТОЯНИЯ СОШЁССЯ: downCoef[] @base+0x10, upCoef[] @base+0x80010
(pdVar6[0x10000] = +0x80000 байт), acc — скаляр в хвосте. Это ЧАСТОТНО-
ЗАВИСИМОЕ сглаживание маски: сила растёт к низким бинам (1/i) с питч-законом
выше кросса. Вот где «размазывание» нотча в реале — НЕ плоский IIR, который
мы отвергли офлайн (22u), а пер-биновый варп!
### Статус сбора (решение: без коммитов до первой валидации)
Незакоммичено: f529fe0_full.dis, BLOCKMAP_529fe0.md, thunk-резолвер,
правки NOTES_LEVEL (22v будет добавлена при транскрипции). Следующий шаг —
транскрипция process_frame_faithful по этой карте.
+37
View File
@@ -157,3 +157,40 @@ the ctx object only materializes during a realtime transport play.
during band processing; still TBD (but LUT curve params A/B/γ are RPP-derived per roadmap). during band processing; still TBD (but LUT curve params A/B/γ are RPP-derived per roadmap).
- Two more ctx-like bases found (0x2120040, 0x1780040) also have +0x24==48000; 0x2370040 is the - Two more ctx-like bases found (0x2120040, 0x1780040) also have +0x24==48000; 0x2370040 is the
populated one (0x2120040 has 0x540658..698 = 1.0 fill pattern — likely a second/free instance). populated one (0x2120040 has 0x540658..698 = 1.0 fill pattern — likely a second/free instance).
## 2026-08-23 (22y): LIVE CTX CAPTURE DURING DUAL/T1KQ/RES PLAYBACK — scripts/dualtrace.py
Method works reproducibly: reaper <cfg>.rpp play_loop.lua (repeat ON) → yabridge-host
→ chunked snapshot ×2 → ctx marker +0x24==48000 ∧ sens>100 → ONLY ONE populated base
(0x2370040; others empty instances). Snapshots byte-stable over seconds.
### Pointer-table catalog (ctx+0x540600..0x540a00, dereferenced u64 → f32[2049])
- Static weights confirmed live: [00]identity@0x540688(ones), window@0x540698(0.5→0.8),
WA/WB/WC/WD @0x5406b8/c8/d8/e8, warp@0x540748(1.3→6.68, structure at LOW bins),
freqaxis@0x540758(v85=995.6Hz ✓ internal 48k/4096).
- **acc/f6f8 arrays ALL ZERO during steady looped playback** (0x5406f8,
0x5407a8/b8/c8/d8 — zero as f32 AND f64): combine accumulators idle in steady state.
- **CONFIG-DEPENDENT CURVE FAMILY** (peak follows band fc: bin43@fc500 → bin85@fc1000):
- 0x540768 == 0x540778 (identical twins): smooth curve, peak at center
(dual: 4.15@43, valley 1.60@171, upturn 1.86@400; t1kq: 3.55@85).
- 0x540788: sharper version (floor ~0.52-1.0, max 4.38).
- **0x5407f8: min EXACTLY 1.0 → reduction multiplier R(f)=1/mask ≥ 1**
(res500 cfg: R(500Hz)=12.0 dB, falls to ~0 by 6 kHz; notch-shaped ✓).
- bands[] slots from static asm (@0x540678+i·16) read as identity/ones tables LIVE —
the per-band working data is NOT sitting in those ctx fields during playback.
### Decisive mismatch
For dual cfg: R(43)=3.98→12.0 dB (real 10.32 ok-ish) BUT R(171)=1.41→3.0 dB while
real cut@2000 = 11.82 dB. ⇒ Applied filter ≠ pointwise copy of R: massive spectral
coupling between template and actual filtering. Prime suspect: FFT-conv stage with
the 8193-wide WIN_freq window ([01]) — smearing/spreading step completely absent in
our pointwise render48k path. This ALSO explains why faithful v1 (pointwise) cannot
balance dual tones regardless of law constants.
### Caveats / next
- Quick Welch TF estimate unreliable (window/alignment) — Goertzel-at-tones stays canon;
for full-spectrum truth use chirp/two-tone refs or per-fc capture sweep.
- NEXT: (1) fc-scan captures (res_only1_{fc}.rpp, 11×) → correlate R_cap(bin85) with
real cut@1000 across fc — validates R as THE applied curve; (2) decode the FFT-conv
0x535a70 body + WIN_freq usage — reconstruct mask→FIR spreading; (3) re-check whether
0x540768-family updates frame-by-frame (two-point diff showed stable — maybe only
rebuilt on param change / note onset).
+266
View File
@@ -1958,3 +1958,269 @@ blend/warp/частотных каскадов) — и сразу даёт лу
2. dual ~3.5 стабилен — межполосный механизм (приоритет из 22p). 2. dual ~3.5 стабилен — межполосный механизм (приоритет из 22p).
3. WIN_freq HF rolloff не добавлен — после добавления comb может улучшиться ещё. 3. WIN_freq HF rolloff не добавлен — после добавления comb может улучшиться ещё.
4. При достижении ≤bridge: гейт пройден, переносить в канон. 4. При достижении ≤bridge: гейт пройден, переносить в канон.
## ============ UPDATE 2026-08-23a (22s): ОФЛАЙН-ФИТ A/S — СКАЛЯРНЫЙ ЗАКОН НАСЫТИЛСЯ ============
Инструментарий: `scripts/lawfit22r.py` — полный numpy-реплей render48k/spectral
(ресемпл 44.1<->48 poly, Hann STFT 4096/1024@48k, поточечная маска, WOLA,
FULL-BLK чанкинг = 61 хоп-фрейм на блок 65536 с zero-pad хвоста — именно так
вызывается processBlock в render48k.cpp:137, поэтому кадров 61·nblk, НЕ (LN)/hop+1).
Детектор независим от закона ⇒ один захват RT_DUMP_ALL траекторий lvl[frame][band][bin]
(`/tmp/opencode/lawfit_traj.npz`, 47 уникальных рендеров) обслуживает ЛЮБОЙ (A,S) офлайн.
Скорость: весь корпус ~2 c/точку (против ~2 мин на реальный рендер).
### Валидация реплея (A=7.4,S=1.85 против реального corpus 1.931)
sim TOTAL 1.895; группы: al +0.045 / dual +0.013 / res 0.085 — отлично;
t1kq 0.30 / t1k +0.32 / comb 0.64 — СТАТИЧЕСКИЕ смещения (риппл ресемплера
scipy≠libsamplerate вокруг крутых спектральных форм; у al полоса на тоне — плоско,
дельта нулевая). Смещение не зависит от закона ⇒ рейтинг кандидатов корректен.
### ПОЧЕМУ точечная модель запрещена (важно для будущих сессий)
1. Тон 1000 Гц = бин 85.33 (не целый!) + главный лепесток Hann ±2-3 бина: при крутой
юбке реализованный гейн тона ≠ mask[tone_bin] (res_500: mask[85]=+8.4 дБ БУСТ,
рендер режет −7.2 дБ).
2. Детектор НЕстационарен в метрическом окне: resonant.wav lvl[85@bin] падает
0.86→0.0002 за файл (медленная адаптация резонатора, 22n) при ПОСТОЯННОМ rms входа.
Медиана по хвосту смешивает 2 декады ⇒ мусор.
### Метод линий (бисекция A* при S∈{0.5,1.5,2.5} на реплее)
У однотонового рендера одно уравнение cut=A+S·x ⇒ вырожденная ЛИНИЯ в (A,S);
x_i=−dA*/dS — точная эффективная координата кейса (включая транзиенты и лики).
Результаты (`lawfit_lines.json`):
- x_eff ОТОБРАЖАЕТ уровень входа ТОЧНО: al-свитч Δx=−3.48 на Δ21 дБ = 1/6.02 окт/дБ
⇒ детектор линеен по амплитуде (подтверждение масштабной цепи).
- НО линии семейств НЕ пересекаются: локальные наклоны cut(x): t1kq 0.85 /
t1k 1.54 / res 1.65 / al 2.19. Единого аффинного закона НЕТ даже на чистых тонах
⇒ кривизна/контент-геометрия, скалярная пара (A,S) принципиально недостаточна.
- Глобальный LSQ по 35 линиям: A=7.396 S=1.973, остаток mean 0.600 дБ,
corr(resid,rms)=+0.71, corr(resid,|log2 fc/1k|)=+0.69 — два недостающих фактора.
- dual: пер-рендерный оптимум даёт floor 0.52-0.62 при q≥1.5 даже своим (A,S) ⇒
межполосный разрыв НЕ закрывается никакими константами закона (приоритет 22p стоит).
- comb (4 полосы): оптимум mean|e|=1.28, константы нестабильны (угол сетки).
### Реальные рендеры (валидация кандидатов, гейт --vs-bridge не гонялся — канон не меняем)
| конфиг | t1kq | t1k | al | res | dual | comb | TOTAL |
|--------|------|-----|----|-----|------|------|-------|
| HEAD 7.4,1.85 | 0.852 | 0.577 | 0.804 | 0.395 | 3.480 | 4.335 | **1.931** |
| LSQ-линии 7.4,1.97 | 0.926 | 0.430 | 0.986 | 0.455 | 3.660 | 4.120 | 1.988 |
| спуск 7.6,1.6 | 0.894 | 1.123 | 0.750 | **0.069** | **3.105** | 5.053 | 1.894 |
Вывод: скалярный закон насыщен в коридоре 1.87-1.93; (7.6,1.6) даёт res 0.069 (!),
но регрессирует t1k/comb — Канон ОСТАЁТСЯ HEAD (7.4,1.85). Теоретический потолок
пер-групповых оптимумов ~0.78 TOTAL недостижим одной парой констант.
### NEXT
1. Двухфакторный закон cut=A+S·x+T·g(fc-дистанция/контент): корреляции указывают
на геометрию полосы относительно тона; данные линий уже собраны (lawfit_lines.json).
2. Механизм межполосного консюмера для dual (Шаг 2/9, приоритет 22p) — константами
не решается, подтверждено покейсовой бисекцией.
3. Реплей-инфраструктура готова для быстрого перебора ЛЮБЫХ параметрических законов
(2 c корпус) — использовать вместо реальных рендеров до финальной валидации.
## ============ UPDATE 2026-08-23b (22t): ДВУХФАКТОРНЫЕ ЗАКОНЫ ОТВЕРГНУТЫ — БЮДЖЕТ ОШИБКИ В DUAL ============
Инструменты расширены:
- `RT_DUMPRESPATH=<file>` в FramedDetector::setParams — статические спектры twin-response
res[band][bin] ({i32 band,i32 nbin,f32[]}, append); lawfit collect кладёт их в npz рядом с lvl.
- `RT_RESPRP=1` в process_band_structural — применять res^rp при NOWARP (для реальных
рендеров двухфакторного закона).
- `lawfit22r.py fit`: координатный спуск по семействам законов на реплее (2 c/корпус).
### Результат фит-семейств (офлайн, старт 7.6/1.6)
| семейство | A | S | Q | rp | TOTAL(sim) |
|-----------|---|---|---|----|-----------|
| scalar | 7.600 | 1.694 | 0 | 0 | **1.860** |
| quad (Q·x²) | →scalar | | **→0** | | 1.860 |
| resrp (mask·res^rp) | →scalar | | | **→0** | 1.860 |
| quad+resrp | 7.452 | 1.540 | +0.069 | 0 | 1.865 |
ОБА вторых фактора отброшены спуском: кривизна не нужна, res-вес не нужен.
Объяснение resrp: lvl = am/res УЖЕ содержит геометрию твин-юбки; домножение на
res^rp сверху — двойной учёт (в каноне res^rp компенсировал другое — отсутствие
res в xv-домене LUT).
### Реальный рендер скалярного оптимума (7.6, 1.694)
t1kq 0.954 / t1k 0.842 / al 0.799 / res **0.117** / dual **3.246** / comb 4.855
⇒ TOTAL **1.898** (sim 1.860 + стат. смещения реплея ~0.04). Против HEAD 1.931:
выигрыш мизерный, t1kq/t1k/comb РЕГРЕССИРУЮТ ⇒ гейт не пройден, канон НЕ тронут.
### Бюджет ошибки — где лежит остаток (решающий факт)
abs-сумма по корпусу при (7.6,1.694): 115.4 дБ·кейсов, из них:
- **dual: 22 кейса × 3.246 = 71.4 (62%!)** — межполосный механизм (Шаг 2/9, консюмер acc/f6f8);
- comb: 24.3 (21%) — мультиполосное взаимодействие;
- всё прочее: ~20 (17%).
Если бы dual был на уровне bridge (0.726), TOTAL упал бы до ~0.96. Никакая
донастройка закона это не даст — ПРИОРИТЕТ №1 подтверждён количественно:
искать точку потребления acc/f6f8 (межполосный каскад), а не крутить маску.
### NEXT
1. dual/межполосный консюмер: декомп-охота вокруг FUN_180529fe0 callers
(52e260 render-loop, multiband combine), трассировка acc/f6f8 в живом плагине.
2. comb: после dual — пере-тест 4-полосного взаимодействия.
3. Реплей-инфра готова; для любых новых гипотез сначала офлайн (2 c), потом рендер.
## ============ UPDATE 2026-08-23c (22u): РЕАЛЬНАЯ СТРУКТУРА 529fe0 ПО RAW ASM — ПЕРЕСМОТР ЦЕПИ ============
Охота за консюмером acc/f6f8 по всем дизасмам/декомпам. ГЛАВНОЕ: наша транскрипция
цепи расходится с тем, что метод реально делает.
### Новые декоды (ground truth из handoff/nls_dasm/f529fe0.dis, 505 строк)
1. **FUN_18052d650 — БИДИР-ИРРАЦИОННЫЙ ФИЛЬТР В DOUBLE** (полный декод):
сигнатура (state=rcx, out=rdx/f6f8, in=r8/band, n); acc=[rcx+0x100010]=0 RESET;
forward: acc = upCoef[i]·acc + downCoef[i]·band[i], запись в f6f8[i]
(upCoef double @state+0x80010+i·8, downCoef @state+0x10+i·8 — ПЕР-БИНОВЫЕ массивы);
backward: продолжение с ТЕМ ЖЕ state по nbin2..1. Это наш «IIR3», но вход band,
выход f6f8, и коэффициенты пер-биновые double, не табличные float.
2. **ВТОРОЙ инлайн бидир-проход в теле 529fe0** (52a65d..52a85b): вход f6f8, выход
ОБРАТНО в band[i]. Итого пара [reset,fwd,bwd]×2 — как в заметках, но точные
адреса/направления теперь залочены.
3. **Межполосное ПРЕ-КОМБИНИРОВАНИЕ ВНУТРИ 529fe0** (52a39a..52a4f6, ДО основного
цикла): f6f8 ← copy bands[0]; цикл по bands[1..n1] (массив @0x540688 = соседний
элемент того же массива векторов!): поэлементные thunks (float 0x20f0/double
0x1850 пары = шаблонный transform); нормировка делением на int [rsp+0x148];
затем второй проход: band[i] *= (xmm11xmm7), xmm7=pow([ctx+0x2c],xmm13)
(mix-вес!), и f6f8 ⊕= ... Семантика thunks ещё не залочена.
4. Основной пер-полосный цикл: scale (=PRNG·[0x540870]·[0x54088c], флаг 0x5408b8
добавляет exp-random) → in-place кернел (thunks 0x140980/0x1409b0, вероятно
bigkernel exp2) → 52d650 (bidi#1) → инлайн bidi#2.
5. Пер-полосный массив состояния **0x540768[i]** читается в основном цикле
([rsp+0x40]) — роль не ясна (кандидат: трек/acc).
6. **FUN_180529c60** (vtbl, параметр = индекс полосы): band←5355d0(param_2);
осцилл-цикл по [ctx+0x1b0] c фильтрацией через f6f8; скалярная нормировка
band *= exp((0.5·K1K2)·ln10/20) · **FUN_1804d56b0(band)** (агрегат от всей
кривой полосы!); релиз-коэф pow(...) от sr/0x1a0·0x1ac; трек-обновление через
массив **0x5407a8[i]** (52dae0 axpy + 52dbc0 add). Кандидат на «медленную
адаптацию» (22n) и/или постобработку маски.
7. **0x5407c8 НЕ читается НИГДЕ в рантайме** (только ctor/dtor в f_535a70.dis) —
адрес «combine acc» из NOTES 21b был неверен; реальные пер-полосные состояния:
0x540768[i] (в 529fe0) и 0x5407a8[i] (в 529c60).
### Офлайн-проверка гипотезы «спектральное сглаживание маски» (lawfit iir_c)
Бидир-однополюсник по бинам (нормированный, ×2 прохода) при законе 7.6/1.694:
c=0.2 → TOTAL 3.23; c=0.6 → 9.83 (монотонная деградация всех групп). ОТВЕРГНУТО:
сглаживание размывает нотч в центре, но физически не дотягивается до тона-конкурента
за ~130 бинов. Двойной cut в dual объясняется НЕ межбиновым сглаживанием маски.
### Следствие для dual
Реальный плагин режет 500 и 2000 почти поровну при q=0.5. Раз контраст lvl
(res-деление) есть у нас и его нет в результате — значит либо (а) детекторный
уровень считается во ВРЕМЕННОЙ области через тот же twin-фильтр (536300 строит
res-кривые на сетке 0.25 Гц — а детекция может идти банком time-domain фильтров),
либо (б) нормировка агрегатом FUN_1804d56b0 выравнивает кривую, либо (в) закон
не логарифмический по lvl. Приоритет — полный перенос 529fe0 из raw asm (505 строк,
подъёмно) вместо текущей реконструкции из кусков.
### Инструментарий
lawfit22r.py: +iir_c параметр (бидир-IIR по бинам в реплее), +res в npz;
sim_errors кэширует по (Q,rp,iir_c).
## ============ UPDATE 2026-08-23d (22w): FAITHFUL-ЦЕПЬ v1 — ИНФРАСТРУКТУРА ГОТОВА, ДЕТЕКТОР ГЛАВЕН ============
Инфраструктура (dsp/fnfaith.{hpp,cpp}, env RT_FAITHFUL=1 в render48k):
транскрипция маско-формирующей цепи по BLOCKMAP_529fe0: band=lvl·s → exp2(-band)
→ бидир-IIR ×2 (setA tau19/mult360) → бидир (setB tau10.7/mult2400) → ×2 (setC).
Коэффициенты — точная транскрипция FUN_180533340: частотный варп
fc_bin=(1000/(sr/2))·nbin (=85 @48k/4096!), g=fc/i ниже кросса, степенной выше,
up=exp(-2π·κ·c·g·tau), down=1-up. Константы из дампа: DAT_24c46b8=-2π,
24c3d8c=0.5 (sr→Найквист), mult 1200/800/360, тау-интерполяция 15↔9 и 9↔180.
Все параметры — env (RT_FAITH_*). Combine acc в v1 опущен (нет консюмера, 22u).
### Смоук-свипы (t1kq1000 / dual500 / dual2000 / al12; err dB)
- κ=0.05,s=8: t1kq 25 / dual500 58 / **dual2000 2.84** — тон-конкурент ВПЕРВЫЕ
дотягивается до реала, но центральный тон улетает в перерез.
- κ=0.2..0.5: насыщение (up→0 на низких бинах), баланса пары нет нигде;
Δerr(5002000) ≈ 15-20 дБ во всех точках (в реале ~1.5).
- Корпус (κ=0.02, s=2): TOTAL 5.315 (t1kq 1.35 / t1k 1.55 / al 0.89 /
res 2.87 / dual 11.52 / comb **2.57** — лучший comb в истории проекта,
базлайн 4.34!). Ресурс мультиполосной механики виден.
### ВЫВОД (решающий)
Форма маски НЕ может сбалансировать dual: контраст lvl между тонами создаётся
САМИМ ДЕТЕКТОРОМ (am/res из STFT), а не пост-обработкой. Реальный детектор
даёт почти равные уровни драйва на 500 и 2000 Гц при q=0.5 ⇒ гипотеза (а)
из 22u — ПРИОРИТЕТ №1: уровни считаются во ВРЕМЕННОЙ области (банк twin-
фильтров 536300 строит res-кривые на сетке 0.25 Гц — вероятно, и уровни тоже
через них), либо тяжёлый кросс-бин пулинг ДО кривой полосы.
Смена канона НЕТ; faithful-путь остаётся за флагом для следующих экспериментов.
### NEXT
1. Детектор: декод FUN_180536300 до конца (scratch-лейаут известен, NOTES_TWIN)
→ понять, откуда берутся bands[] кривые на входе 529fe0.
2. comb 2.57 при живом faithful — разобраться после детектора.
3. RT_DUMP_BIN-трасса живого плагина на dual-конфиге (покадрово mask/lvl) —
прямой ответ на вопрос «какой lvl реально у тона 2000».
## ============ UPDATE 2026-08-23e (22x): ТЕОРЕМА О ОТСУТСТВИИ α + ТАБЛИЦА DUAL ПО q ============
Инструмент: scripts/resalpha.py — RT_DUMP_BIN-тракты всех уникальных рендеров
(57 пар case/freq: lvl_a1, res, am на бине тона + реальные cut из рефов),
CSV /tmp/opencode/resalpha_pairs.csv.
### Теорема об отсутствии α
Модель lvl = am/res^α с ЕДИНЫМ законом cut=L(log2 lvl): имплицитные наклоны
семей (концевые точки, дБ/октава lvl) при НИКАКОМ α ∈ [−0.6..1] не совпадают:
- dual требует α≈−0.5 (наклон +0.2..+0.4);
- при α≤0 t1k/t1kq/res получают БЕСКОНЕЧНЫЙ/отрицательный наклон (весь fc-скан
схлопывается в точку по x — res перестаёт различать fc);
- при α>0 dual-пара даёт отрицательный наклон (запрещено для монотонного закона).
⇒ ВЕСЬ класс точечных моделей lvl=f(am, res^α) с общим законом ОТВЕРГНУТ.
Глобальный LSQ по α плоский (1.59-1.63 дБ) — семейные смещения доминируют.
### Таблица dual по q (реальные cut, одна полоса @500)
| q | res500 | res2000 | cut@500 | cut@2000 | Δ |
|---|--------|---------|---------|----------|---|
| 0.1 | 0.117 | 0.216 | 10.32 | 15.33 | +5.01 |
| 0.5 | 0.117 | 0.839 | 10.32 | 11.82 | +1.50 |
| 1.0 | 0.117 | 1.353 | 10.32 | 10.78 | +0.46 |
| 3.0 | 0.119 | 1.880 | 10.29 | 10.32 | +0.03 |
| 10 | 0.139 | 1.988 | 9.94 | 10.25 | 0.07 |
Структура: cut@500 КОНСТАНТА по q (драйвер центра не зависит от ширины полосы);
Δcut(res2000) ≈ экспонента 5.4·exp(−res/0.49), схлопывание при res→2.
cut@2000 ПАДАЕТ с ростом res2000 — противоположно любому «lvl растёт с res»
и противоположно делению. Геометрическая интерпретация: широкий нотч (малое q)
НАКРЫВАЕТ тон-конкурент сам по себе; вклад центра всегда полный. Это свойство
ФОРМЫ twin-шаблона на мелкой сетке (0.25 Гц, FUN_180536300), а не пер-биновой
арифметики на сетке 4096.
### al-группа (якорь уровня, fc=tone=1000)
lvl_a1: 10.08/7.14/5.05/3.58/1.79/0.90 → cut: 13.64/12.44/11.28/10.13/7.97/6.01.
Ломаная: наклон ≈1.16 дБ/окт для lvl≥1.8 и ≈2.06 ниже — кривизна закона
подтверждена независимо (ср. 22s линии).
### Следствие
Детекторный уровень НЕ восстанавливается из спектра бина функционально.
Кандидаты: (а) скалярная огибающая сигнала через time-domain twin-банк
(536300 строит res-кривые на сетке 0.25 Гц — возможно, тот же банк даёт и
уровни), (б) форма кривой = res-шаблон × скалярный драйв, что естественно
даёт постоянство центра и геометрическое покрытие скайрта.
Решающий эксперимент — живая покадровая трасса детектора на dual-конфиге
(registry-heartbeat инфраструктура готова, NOTES_CAPTURE.md).
## ============ UPDATE 2026-08-23f (22z): FFT-conv статика + СПРЕДИНГ ОПРОВЕРГНУТ ============
### Статика mask→FIR (52b55052b8bb, per-band цикл построения FIR)
Полная последовательность на полосу: scale-ops на кривой → **call 0x180535a70**
(стаб → ILT → 0x180140a10 float / a70 double → импорт-прыжки → тела 0x1802a24c0
float / 0x1802fa420 double — векторизованный AVX2, функционально FFT-дизайн) →
копия в r14=[ctx+0x540668] → complex-op с буферами 0x540548/550/598 →
FIR[n/2]=0 → bigkernel in-place → ещё complex-op → **WIN_freq умножает сегмент:
rdx = win_ptr + (n/2)*4** (вторая половина 8193-таблицы!) → масштаб второй половины
xmm9 → **FIR[0]=1.0, FIR[1]=0** → init пар, dry/wet-scale 0x540888.
Размерности: n=[ctx+0x540534], половина rbp=n/2, счётчики n/2+1.
Живой каталог (22y): 0x540758=freqaxis, 0x540658=WIN_freq 0.5→0.8.
### Спрединг-гипотеза (IDFT(R)→окно L→DFT) — ЧИСЛЕННО ОТВЕРГНУТА
На живой кривой dual R(7f8) (v43=3.98, v171=1.41): окна Hann L∈{32..2048} дают
cut@171 ≤ 2.60 дБ (нужно 11.82); cut@43 падает с 12.0 до 4.4 при малых L.
Сглаживание НЕ может поднять долину до уровня соседнего пика — механика скайрта
dual НЕ в пост-обработке кривой R.
### Что остаётся (структура данных dual по q, 22x)
Δcut(скайрт−центр) ≈ линейно по −ln res_скайрта с наклоном b≈2.25 дБ/e-fold,
НО центр из этого закона выпадает (его cut константен при res≈const). Это
согласуется с моделью «скалярный драйв через time-domain twin + шаблонная форма»,
но требует знать, как именно нормируется шаблон. Прямое декодирование тел
0x1802a24c0 (3КБ AVX2) и complex-op шагов с буферами 0x540548/550/598 — следующий
статический шаг; альтернатива — трассировка промежуточных буферов живьём
(снапшоты в момент аудио-колбека, не между кадрами).
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
-- play_loop.lua : realtime playback with repeat ON, held alive for keep_secs.
local keep_secs = 240
if reaper.GetSetRepeat(0) == 0 then
reaper.Main_OnCommand(1068, 0) -- Transport: Repeat ON
end
reaper.Main_OnCommand(1007, 0) -- Transport: Play
local t0 = reaper.time_precise()
local function keepalive()
if reaper.time_precise() - t0 < keep_secs then
reaper.defer(keepalive)
else
reaper.OnStopButton()
end
end
reaper.defer(keepalive)
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""dualtrace.py — live capture of the DSP ctx during realtime playback of
dual_b1q_0.5.rpp (two tones 500+2000 Hz, one band @500 q=0.5).
Method (NOTES_CAPTURE.md 2026-08-20c): reaper + play_loop.lua keeps the audio
callback alive; chunked /proc/pid/mem snapshot of the yabridge host; find ctx
by marker +0x24 == 48000.0f; dump scalars + pointer table + all ~2049-float
arrays, flag those with energy at tone bins 43/171.
Usage: python3 scripts/dualtrace.py [rpp]
"""
import json
import os
import struct
import subprocess
import sys
import time
import numpy as np
SNAP1 = '/tmp/opencode/dualtrace_s1.bin'
SNAP2 = '/tmp/opencode/dualtrace_s2.bin'
def find_host():
import glob
for p in glob.glob('/proc/[0-9]*'):
pid = int(os.path.basename(p))
try:
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace')
maps = open(f'/proc/{pid}/maps').read()
except Exception:
continue
if 'soothe2' in maps and 'reaper' not in cmd:
return pid
return None
def snapshot(host, path):
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
out = open(path, 'wb')
nreg = nbytes = 0
for line in open(f'/proc/{host}/maps').read().splitlines():
p = line.split()
if len(p) < 2 or 'r' not in p[1]:
continue
lo, hi = (int(x, 16) for x in p[0].split('-'))
a = lo
while a < hi:
n = min(hi - a, 8 * 1024 * 1024)
try:
d = os.pread(fd, n, a)
except Exception:
a += n
continue
if d:
out.write(struct.pack('<QQ', a, len(d)))
out.write(d)
nreg += 1
nbytes += len(d)
a += n
out.close()
os.close(fd)
return nreg, nbytes
def parse_snap(path):
data = open(path, 'rb').read()
regs = []
i = 0
while i + 16 <= len(data):
lo, sz = struct.unpack_from('<QQ', data, i)
regs.append((lo, data[i + 16:i + 16 + sz]))
i += 16 + sz
return regs
def readabs(regs, addr, n):
for lo, body in regs:
if lo <= addr < lo + len(body) and addr - lo + n <= len(body):
return body[addr - lo:addr - lo + n]
return None
def find_ctx(regs):
sig = struct.pack('<I', 0x473b8000) # 48000.0f
cands = []
for lo, body in regs:
j = 0
while True:
j = body.find(sig, j)
if j < 0:
break
base = lo + j - 0x24
b = readabs(regs, base + 0x540870, 4)
if b and struct.unpack('<f', b)[0] > 100: # sens scalar ~441
cands.append(base)
j += 1
return sorted(set(cands))
def dump_ctx(regs, ctx, tag, store):
"""Scalars + pointer table 0x540600..0x540900 + dereferenced arrays."""
def f32(addr):
b = readabs(regs, addr, 4)
return struct.unpack('<f', b)[0] if b else None
scal = {hex(a): f32(ctx + a) for a in range(0x540860, 0x5408a8, 4)}
store[f'{tag}_scalars'] = scal
arrays = {}
ptrinfo = []
for off in range(0x540600, 0x540900, 8):
b = readabs(regs, ctx + off, 8)
if not b:
continue
ptr = struct.unpack('<Q', b)[0]
if ptr < 0x10000 or ptr > 0x7fffffffffff:
continue
body = readabs(regs, ptr, 2049 * 4 + 64)
if body is None or len(body) < 2049 * 4:
continue
arr = np.frombuffer(body[:2049 * 4], dtype='<f4').astype(np.float64)
if not np.isfinite(arr).all():
continue
arrays[off] = arr
ptrinfo.append((hex(ctx + off), hex(ptr)))
# classify: energy at tone bins relative to local median
flagged = {}
med = None
for off, arr in arrays.items():
m = float(np.median(arr))
if m <= 0 or not np.isfinite(m) or float(np.max(np.abs(arr))) > 1e6:
continue
r43 = arr[43] / m
r171 = arr[171] / m
if r43 > 3 or r171 > 3 or (arr.max() / max(m, 1e-30)) > 5:
flagged[off] = dict(ratio43=r43, ratio171=r171,
vmin=float(arr.min()), vmax=float(arr.max()))
store[f'{tag}_arr_{hex(off)}'] = arr
store[f'{tag}_ptrinfo'] = ptrinfo
store[f'{tag}_flagged'] = {hex(k): v for k, v in flagged.items()}
return flagged
def main():
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
os.makedirs('/tmp/opencode', exist_ok=True)
subprocess.run('pkill -9 -x reaser 2>/dev/null; pkill -9 -x reaper 2>/dev/null; '
"pkill -9 -f '[y]abridge' 2>/dev/null; sleep 1", shell=True)
proc = subprocess.Popen(
['/usr/bin/reaper', '-nosplash', '-ignoreerrors', rpp,
'/home/m/re-tools/play_loop.lua'],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
t0 = time.time()
host = None
while time.time() - t0 < 60 and not host:
host = find_host()
time.sleep(0.2)
if not host:
print('NO HOST')
return 1
print('host', host, 'at %.1fs' % (time.time() - t0), flush=True)
time.sleep(12) # init + several looped passes -> steady state
store = {}
try:
n1, b1 = snapshot(host, SNAP1)
print(f'snap1: {n1} regs {b1/1e6:.0f}MB', flush=True)
time.sleep(6)
n2, b2 = snapshot(host, SNAP2)
print(f'snap2: {n2} regs {b2/1e6:.0f}MB', flush=True)
for tag, path in (('s1', SNAP1), ('s2', SNAP2)):
regs = parse_snap(path)
cands = find_ctx(regs)
print(tag, 'ctx candidates:', [('0x%x' % c) for c in cands][:5], flush=True)
if cands:
fl = dump_ctx(regs, cands[0], tag, store)
for k, v in list(fl.items())[:10]:
print(' ', k, {kk: round(vv, 2) if isinstance(vv, float) else vv
for kk, vv in v.items()}, flush=True)
finally:
if proc.poll() is None:
proc.kill()
np.savez_compressed('/tmp/opencode/dualtrace.npz',
**{k: v for k, v in store.items()
if isinstance(v, np.ndarray)})
json.dump({k: v for k, v in store.items() if not isinstance(v, np.ndarray)},
open('/tmp/opencode/dualtrace.json', 'w'), indent=1)
print('\nsaved /tmp/opencode/dualtrace.npz|.json')
return 0
if __name__ == '__main__':
sys.exit(main())
+501
View File
@@ -0,0 +1,501 @@
#!/usr/bin/env python3
"""lawfit22r.py — offline affine-law fitting infra (NOTES_LEVEL 22r NEXT-1).
The detector path is law-independent (RT_DUMP_ALL in framed_model.cpp), so ONE
trajectory capture per unique render suffices to evaluate ANY affine dB law
cut(lvl) = K * (A_db + S_db * log2(lvl)), K = 20*log10(2)/6.0174
offline: mask[frame][band][bin] = exp2(-(A+S*log2 lvl)/6.0174) replayed through
an exact numpy replica of render48k.cpp + spectral.cpp (resample 44.1<->48,
Hann STFT 4096/1024 @48k, pointwise mask, WOLA istft, FULL-BLK chunking with
zero-padded tail = 61 hop-frames per 65536-block) + corpus metric.
NOTE (22s): pointwise per-bin fitting is INVALID for out-of-band evals — the
detector level at far bins is transient during the metric window (slow twin
adaptation, NOTES 22n) and Hann leakage couples neighbouring bins.
Modes:
collect capture lvl trajectories -> /tmp/opencode/lawfit_traj.npz
sanity A S JSON full-sim group table vs an actual corpus run
fit A0 S0 coordinate-descent (A,S) on TOTAL + per-group optima
Bare-chain env (NOTES 22k, matches 22r baseline TOTAL 1.931 @ A=7.4,S=1.85):
RT_LUT_OFF=1 RT_IIR12=0 RT_NOWARP=1 RT_NOBLEND=1 RT_NOIIR3=1 (no FLOOR)
"""
import json
import os
import struct
import subprocess
import sys
import time
import numpy as np
from scipy.signal import resample_poly
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import corpus
corpus.RB = '/home/m/re-tools/dsp/build/render48k'
NFFT = 4096
HOP = 1024
RBIN = NFFT // 2 + 1
BLK = 1 << 16
NF_BLK = (BLK - NFFT) // HOP + 1 # 61 hop-frames per block
TRAJ = '/tmp/opencode/lawfit_traj.npz'
K_DB = 20.0 * np.log10(2.0) / 6.0174 # exp2(-y) dB factor (informational)
BASE_ENV = {
'RT_LUT_OFF': '1', 'RT_IIR12': '0', 'RT_NOWARP': '1',
'RT_NOBLEND': '1', 'RT_NOIIR3': '1',
}
_WIN = 0.5 * (1.0 - np.cos(2.0 * np.pi * np.arange(NFFT) / (NFFT - 1)))
_WOLA = float(np.sum(_WIN * _WIN) / HOP)
def structural_cases():
out = []
for name, inp, args, ref, f in corpus.build_cases():
joined = [','.join(args)] if len(args) == 3 else args
out.append((name, inp, joined, ref, f))
return out
def n_bands(argstr_list):
if len(argstr_list) == 1:
return max(1, len(argstr_list[0].split(',')) // 3)
return len(argstr_list)
def read_traj(path):
recs = []
with open(path, 'rb') as fh:
while True:
hdr = fh.read(8)
if len(hdr) < 8:
break
fr, nb = struct.unpack('<ii', hdr)
recs.append(np.frombuffer(fh.read(4 * nb), dtype='<f4').astype(np.float32))
if not recs:
return None
return np.stack(recs)
def collect():
cases = structural_cases()
uniq = {}
for name, inp, args, ref, f in cases:
key = f'{os.path.basename(inp)}|{";".join(args)}'
uniq.setdefault(key, {'inp': inp, 'args': list(args),
'nb': n_bands(list(args))})
store = {}
traj_bin = '/tmp/opencode/lawfit_traj.bin'
resp_bin = '/tmp/opencode/lawfit_res.bin'
for key, u in sorted(uniq.items()):
if os.path.exists(traj_bin):
os.remove(traj_bin)
if os.path.exists(resp_bin):
os.remove(resp_bin)
env = {**os.environ, **BASE_ENV, 'RT_DUMP_ALL': traj_bin,
'RT_DUMPRESPATH': resp_bin}
subprocess.run([corpus.RB, u['inp'], '/tmp/opencode/lawfit_out.wav'] + u['args'],
capture_output=True, text=True, env=env,
cwd='/home/m/re-tools')
arr = read_traj(traj_bin)
if arr is None or arr.shape[1] != RBIN or arr.shape[0] % u['nb']:
print(f'NO/BAD TRAJ: {key}: {None if arr is None else arr.shape}')
continue
lvl = arr.reshape(-1, u['nb'], RBIN) # [frame][band][bin]
store[key] = lvl
# static twin-response spectra per band ({i32 band, i32 nbin, f32[nbin]})
res = None
if os.path.exists(resp_bin):
raw = open(resp_bin, 'rb').read()
off = 0
bands = []
while off < len(raw):
bi, nb = struct.unpack_from('<ii', raw, off)
off += 8
r = np.frombuffer(raw, dtype='<f4', count=nb, offset=off)
off += 4 * nb
bands.append(r.astype(np.float32))
if len(bands) == u['nb'] and all(b.size == RBIN for b in bands):
res = np.stack(bands)
if res is not None:
store[key + '|res'] = res
print(f'{key}: {lvl.shape[0]} frames x {u["nb"]} bands'
+ (' +res' if res is not None else ' NORES'))
np.savez_compressed(TRAJ, **store)
print(f'\nwrote {len(store)} entries -> {TRAJ}')
def load_traj():
z = np.load(TRAJ)
return {k: z[k] for k in z.files}
# ---------------- exact render48k/spectral replay ----------------
class CaseInput:
"""Law-independent per-render data: resampled+blocked input, frame count."""
def __init__(self, inp_path):
self.x44 = corpus.load_mono(inp_path)
x48 = resample_poly(self.x44, 160, 147)
self.blocks = [(s, min(BLK, len(x48) - s)) for s in range(0, len(x48), BLK)]
self.nf = len(self.blocks) * NF_BLK
need = self.blocks[-1][0] + BLK
self.xext = np.zeros(need)
self.xext[:len(x48)] = x48
self.offs = np.array([b[0] + f * HOP
for b in self.blocks for f in range(NF_BLK)],
dtype=np.int64)
def frames_ok(self, lvl):
return lvl.shape[0] == self.nf
def _bidir_iir(lo, c):
"""FUN_18052d650 semantics approx: bidirectional single-pole IIR along bins,
reset->forward->backward with persistent state, applied twice (22u).
Normalised form acc = c*acc + (1-c)*x (unity DC gain); vectorised over
leading axes via lfilter (endpoints differ slightly from C++ loop)."""
if c <= 0:
return lo
from scipy.signal import lfilter
b, a = [1.0 - c], [1.0, -c]
y = np.asarray(lo, dtype=np.float64)
for _ in range(2):
y = lfilter(b, a, y, axis=-1)
y = lfilter(b, a, y[..., ::-1], axis=-1)[..., ::-1]
return y
def masks_from_lvl(lvl, nb, A, S, Q=0.0, res=None, rp=0.0, iir_c=0.0):
"""Per-frame lower-half mask exactly as bare chain + LAWAFFINE produces.
Law families (22t): scalar cut=A+S*log2(lvl); quad adds curvature Q*x^2;
resrp multiplies per-band mask by res^rp (decomp warp-cascade factor,
applied BEFORE cross-band min like the C++ warp section).
iir_c>0: bidirectional bin-IIR smoothing per band (22u, FUN_18052d650).
"""
nf = lvl.shape[0]
lv64 = lvl.astype(np.float64)
x = np.log2(np.maximum(lv64, 1e-12))
mm = np.exp2(-((A + S * x + Q * x * x) / 6.0174))
low = lv64 <= 1e-6 # C++ fallback: exp2(-level)
if low.any():
mm[low] = np.exp2(-lv64[low])
if res is not None and rp:
mm = mm * res[None].astype(np.float64) ** rp
if iir_c > 0:
mm = _bidir_iir(mm, iir_c)
if nb > 1:
lo = np.min(mm, axis=1) # min across bands
else:
lo = mm[:, 0, :]
full = np.ones((nf, NFFT))
full[:, :RBIN] = lo
full[:, RBIN:] = lo[:, 1:RBIN - 1][:, ::-1] # C++ 418: mask[k]=mask[nfft-k]
return full
def replay(ci, lvl, nb, A, S, Q=0.0, res=None, rp=0.0, iir_c=0.0):
"""Return trimmed 44.1k output for law params on prepared CaseInput ci."""
assert ci.frames_ok(lvl), f'traj {lvl.shape[0]} != frames {ci.nf}'
full = masks_from_lvl(lvl, nb, A, S, Q, res, rp, iir_c)
segs = ci.xext[ci.offs[:, None] + np.arange(NFFT)[None, :]] * _WIN[None, :]
yspec = np.fft.fft(segs, axis=1) * full
td = np.real(np.fft.ifft(yspec, axis=1))
del segs, yspec
td *= _WIN[None, :]
L = len(ci.x44)
y48 = np.zeros(L)
overlap = np.zeros(NFFT)
fi = 0
zeros = np.zeros(HOP)
for s, n in ci.blocks:
for f in range(NF_BLK):
off = s + f * HOP
acc = overlap + td[fi]
fi += 1
e = min(off + HOP, L)
if e > off:
y48[off:e] = acc[:e - off] / _WOLA
overlap[:NFFT - HOP] = acc[HOP:]
overlap[NFFT - HOP:] = zeros
y44 = resample_poly(y48, 147, 160)
return y44[:L]
# ---------------- evaluation ----------------
_CI_CACHE = {}
def case_input(key, inp):
if key not in _CI_CACHE:
_CI_CACHE[key] = CaseInput(inp)
return _CI_CACHE[key]
def build_eval_index(trajs):
idx = []
for name, inp, args, ref, f in structural_cases():
key = f'{os.path.basename(inp)}|{";".join(args)}'
if key not in trajs:
continue
idx.append({'name': name, 'key': key, 'inp': inp, 'ref': ref,
'f': f, 'nb': trajs[key].shape[1], 'grp': name.split('_')[0]})
return idx
def sim_errors(trajs, idx, A, S, Q=0.0, rp=0.0, iir_c=0.0):
errs = {}
ycache = {}
for e in idx:
ck = (e['key'], e['nb'], round(Q, 6), round(rp, 6), round(iir_c, 6))
if ck not in ycache:
ci = case_input(e['key'], e['inp'])
ycache[ck] = replay(ci, trajs[e['key']], e['nb'], A, S,
Q, trajs.get(e['key'] + '|res'), rp, iir_c)
y44 = ycache[ck]
errs[e['name']] = corpus.db(corpus.ta(y44, e['f'])) - \
corpus.db(corpus.ta(corpus.load_mono(e['ref']), e['f']))
return errs
def group_stats(errs):
gs = {}
for k, v in errs.items():
gs.setdefault(k.split('_')[0], []).append(v)
out = {g: float(np.mean(np.abs(v))) for g, v in gs.items()}
out['TOTAL'] = float(np.mean(np.abs(list(errs.values()))))
return out
def sanity(A, S, json_path):
trajs = load_traj()
idx = build_eval_index(trajs)
refs = json.load(open(json_path))
t0 = time.time()
errs = sim_errors(trajs, idx, A, S)
st = group_stats(errs)
ast = group_stats(refs)
print(f'{"group":>8} {"sim":>8} {"actual":>8} {"d":>7} ({time.time()-t0:.1f}s)')
for g in ['t1kq', 't1k', 'al', 'res', 'dual', 'comb', 'TOTAL']:
print(f'{g:>8} {st[g]:>8.3f} {ast[g]:>8.3f} {st[g]-ast[g]:>+7.3f}')
print('\nper-case worst deltas:')
deltas = sorted(((abs(errs[k] - refs[k]), k) for k in errs),
reverse=True)[:8]
for d, k in deltas:
print(f' {k:>18}: sim {errs[k]:+8.3f} actual {refs[k]:+8.3f} d {errs[k]-refs[k]:+.3f}')
def fit(A0, S0):
trajs = load_traj()
idx = build_eval_index(trajs)
fit2(trajs, idx, A0, S0)
def fit2(trajs, idx, A0, S0):
"""Law-family comparison: scalar / quad / resrp / quad+resrp (NOTES 22t)."""
def objective(p, sub=None):
A, S, Q, rp = p
ii = idx if sub is None else sub
return group_stats(sim_errors(trajs, ii, A, S, Q, rp))
def descend(p0, steps, sub=None, label=''):
best = list(p0)
bst = objective(tuple(best), sub)
print(f'{label} start {[round(v, 4) for v in best]}: '
f'TOTAL={bst["TOTAL"]:.3f}', flush=True)
while all(s > 1e-4 for s in steps.values()):
moved = False
for i, nm in enumerate(['A', 'S', 'Q', 'rp']):
if nm not in steps:
continue
st = steps[nm]
for d in (+st, -st):
cand = list(best)
cand[i] = round(cand[i] + d, 6)
if cand[3] < 0 or (nm == 'Q' and abs(cand[2]) > 3):
continue
s2 = objective(tuple(cand), sub)
if s2['TOTAL'] < bst['TOTAL'] - 1e-4:
best, bst = cand, s2
moved = True
print(f' {label} {nm}{d:+.4g}: TOTAL={s2["TOTAL"]:.3f} '
f'{[round(v, 4) for v in best]}', flush=True)
if not moved:
for nm in steps:
steps[nm] /= 2
return best, bst
res_all = {}
b, t = descend([A0, S0, 0.0, 0.0], {'A': 0.4, 'S': 0.15}, label='[scalar]')
res_all['scalar'] = (list(b), dict(t))
bA, bS, _, _ = res_all['scalar'][0]
b, t = descend([bA, bS, 0.0, 0.0], {'A': 0.3, 'S': 0.15, 'Q': 0.06},
label='[quad]')
res_all['quad'] = (list(b), dict(t))
b, t = descend([bA, bS, 0.0, 0.03], {'A': 0.3, 'S': 0.15, 'rp': 0.01},
label='[resrp]')
res_all['resrp'] = (list(b), dict(t))
bA, bS, bQ, _ = res_all['quad'][0]
b, t = descend([bA, bS, bQ, 0.03], {'A': 0.25, 'S': 0.12, 'Q': 0.05,
'rp': 0.008}, label='[quad+resrp]')
res_all['quad+resrp'] = (list(b), dict(t))
print('\n================ LAW FAMILY SUMMARY ================')
for fam, (p, st) in res_all.items():
gs = ' '.join(f'{g}={st[g]:.3f}' for g in
['t1kq', 't1k', 'al', 'res', 'dual', 'comb'])
print(f'{fam:>12}: A={p[0]:7.3f} S={p[1]:6.3f} Q={p[2]:+6.3f} '
f'rp={p[3]:5.3f} TOTAL={st["TOTAL"]:.3f}\n{"":>14}{gs}')
json.dump({f: {'params': p, 'groups': s} for f, (p, s) in res_all.items()},
open('/tmp/opencode/lawfit_fit2.json', 'w'), indent=1)
print('\nwrote /tmp/opencode/lawfit_fit2.json')
def freq_of(name):
for nm, inp, args, ref, f in structural_cases():
if nm == name:
return f
raise KeyError(name)
def percase():
"""Per-render greedy (A,S) optima -> /tmp/opencode/lawfit_percase.json."""
trajs = load_traj()
idx = build_eval_index(trajs)
renders = {}
for e in idx:
renders.setdefault(e['key'], {'nb': e['nb'], 'evals': []})['evals'].append(e)
out = {}
for key, r in sorted(renders.items()):
lvl = trajs[key]
ci = case_input(key, r['evals'][0]['inp'])
def ev(A, S):
y44 = replay(ci, lvl, r['nb'], A, S)
return [corpus.db(corpus.ta(y44, e['f'])) -
corpus.db(corpus.ta(corpus.load_mono(e['ref']), e['f']))
for e in r['evals']]
bA, bS, bval = None, None, 1e9
grid = [(float(A), float(S))
for A in np.arange(4.0, 11.01, 0.75)
for S in np.arange(0.25, 3.01, 0.25)]
for A, S in grid:
errs = ev(A, S)
m = float(np.mean(np.abs(errs)))
if m < bval:
bA, bS, bval = A, S, m
# band params from args string(s)
bands = []
for a in r['evals'][0]['inp'] and key.split('|')[1].split(';'):
p = a.split(',')
bands.append({'fc': float(p[0]), 'q': float(p[1]), 'sens': float(p[2])})
out[key] = {
'group': sorted({e['grp'] for e in r['evals']}),
'bands': bands, 'best_A': bA, 'best_S': bS, 'best_err': bval,
'evals': [{'name': e['name'], 'freq': e['f']} for e in r['evals']],
}
print(f'{key:>44}: A={bA:5.2f} S={bS:5.2f} mean|e|={bval:.3f}', flush=True)
json.dump(out, open('/tmp/opencode/lawfit_percase.json', 'w'), indent=1)
print('\nwrote /tmp/opencode/lawfit_percase.json')
def _bisect_A(ev, f_idx, S, lo=0.0, hi=16.0, iters=11):
"""Find A s.t. signed err at eval f_idx == 0 (monotone decreasing in A)."""
def e(A):
return ev(A, S)[f_idx]
elo, ehi = e(lo), e(hi)
if elo <= 0:
return lo
if ehi >= 0:
return hi
for _ in range(iters):
mid = 0.5 * (lo + hi)
if e(mid) > 0:
lo = mid
else:
hi = mid
return 0.5 * (lo + hi)
def lines():
"""Per-render A*(S) at fixed S anchors -> /tmp/opencode/lawfit_lines.json."""
trajs = load_traj()
idx = build_eval_index(trajs)
renders = {}
for e in idx:
renders.setdefault(e['key'], {'nb': e['nb'], 'evals': []})['evals'].append(e)
S_ANCHORS = [0.5, 1.5, 2.5]
out = {}
for key, r in sorted(renders.items()):
lvl = trajs[key]
ci = case_input(key, r['evals'][0]['inp'])
rms = float(np.sqrt(np.mean(ci.x44 ** 2)))
rec = {'group': sorted({e['grp'] for e in r['evals']}),
'bands': key.split('|')[1], 'rms_db': 20 * np.log10(max(rms, 1e-9)),
'A_star': {}}
if len(r['evals']) == 1:
def ev(A, S):
y44 = replay(ci, lvl, r['nb'], A, S)
return [corpus.db(corpus.ta(y44, r['evals'][0]['f'])) -
corpus.db(corpus.ta(corpus.load_mono(r['evals'][0]['ref']),
r['evals'][0]['f']))]
for S in S_ANCHORS:
rec['A_star'][S] = round(_bisect_A(ev, 0, S), 3)
rec['err_at_Astar'] = round(abs(ev(rec['A_star'][1.5], 1.5)[0]), 4)
else:
# multi-eval render: minimise mean|err| per S anchor (grid+refine)
refs = [corpus.db(corpus.ta(corpus.load_mono(e['ref']), e['f']))
for e in r['evals']]
for S in S_ANCHORS:
best = (None, 1e9)
for A in np.arange(0, 16.01, 0.5):
y44 = replay(ci, lvl, r['nb'], A, S)
m = float(np.mean([abs(corpus.db(corpus.ta(y44, e['f'])) - rf)
for e, rf in zip(r['evals'], refs)]))
if m < best[1]:
best = (float(A), m)
rec['A_star'][S] = round(best[0], 3)
rec.setdefault('multi_err', {})[S] = round(best[1], 4)
out[key] = rec
extra = f" multi={rec.get('multi_err', {}).get(1.5)}" if 'multi_err' in rec else ''
print(f'{key:>58}: A*=' +
','.join(f'{rec["A_star"][S]:6.2f}' for S in S_ANCHORS) +
f' rms={rec["rms_db"]:6.1f}{extra}', flush=True)
json.dump(out, open('/tmp/opencode/lawfit_lines.json', 'w'), indent=1)
print('\nwrote /tmp/opencode/lawfit_lines.json')
if __name__ == '__main__':
if not sys.argv[1:]:
print(__doc__)
sys.exit(1)
cmd = sys.argv[1]
if cmd == 'collect':
collect()
elif cmd == 'sanity':
sanity(float(sys.argv[2]), float(sys.argv[3]), sys.argv[4])
elif cmd == 'fit':
fit(float(sys.argv[2]), float(sys.argv[3]))
elif cmd == 'percase':
percase()
elif cmd == 'lines':
lines()
else:
print(f'unknown mode {cmd}')
sys.exit(1)
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""pairfit22q.py — collect (model lvl_raw, real cut_dB) pairs across corpus."""
import sys, os, subprocess, numpy as np
sys.path.insert(0, "/home/m/re-tools/scripts")
sys.path.insert(0, "/home/m/re-tools/handoff")
import corpus
from render_parity import load, FS
RB = "/home/m/re-tools/dsp/build/render48k"
ENVBASE = {"RT_LUT_OFF": "1", "RT_IIR12": "0", "RT_NOWARP": "1",
"RT_NOBLEND": "1", "RT_NOIIR3": "1", "RT_POOL": "0",
"RT_SCALE_M": "1.0", "RT_FLOOR": "0",
"RT_DUMP_BIN": "/tmp/opencode/tract_pair.txt",
"RT_DUMP_FRAME": "120"}
def amp(p, f):
a = load(p)
seg = np.mean(a[:min(len(a), int(3.5*FS))][-int(0.75*FS):], axis=1)
n = len(seg); tt = np.arange(n)/FS
return 2*np.abs(np.dot(seg, np.exp(-2j*np.pi*f*tt)))/n
def bin_of(f):
return int(round(f/48000*4096))
def main():
e = {**os.environ, **ENVBASE}
pairs = []
seen_render = {}
cases = corpus.build_cases()
# unique renders first (input,args): dump tract once, remember lvl per freq
uniq = {}
for name, inp, args, ref, f in cases:
key = (inp, ",".join(args) if isinstance(args, list) else args)
uniq.setdefault(key, []).append((name, ref, f))
n_done = 0
for (inp, argstr), items in sorted(uniq.items()):
out_wav = "/tmp/opencode/pf_model.wav"
env = {**e}
bands = argstr.split(",") if "," in argstr else [argstr]
nbands = max(1, len(bands)//3)
env["RT_DUMP_FRAME"] = str(120 * nbands - 1) # land mid-frame of last band
r = subprocess.run([RB, inp, out_wav] + ([argstr] if "," in argstr else [" ".join([])]),
capture_output=True, text=True, env=env, cwd="/home/m/re-tools")
if not os.path.exists("/tmp/opencode/tract_pair.txt"):
print("no tract for", argstr); continue
try:
d = np.loadtxt("/tmp/opencode/tract_pair.txt")
except Exception as ex:
print("load fail", ex); continue
if d.ndim == 1: continue
k, am, res, lvl = d.T[0], d.T[1], d.T[2], d.T[3]
for name, ref, f in items:
b = bin_of(f)
if b >= len(lvl): continue
lv = float(lvl[b])
i_db = 20*np.log10(max(amp(inp, f), 1e-12))
r_db = 20*np.log10(max(amp(ref, f), 1e-12))
cut = i_db - r_db
pairs.append((name, f, lv, cut))
n_done += 1
os.remove("/tmp/opencode/tract_pair.txt")
print("collected %d pairs from %d renders" % (len(pairs), n_done))
with open("/tmp/opencode/pairs.csv", "w") as fh:
fh.write("case,freq,lvl,cut_db\n")
for nm, f, lv, c in pairs:
fh.write("%s,%.1f,%.4f,%.3f\n" % (nm, f, lv, c))
arr = np.array([(lv, c) for _, _, lv, c in pairs])
o = np.argsort(arr[:, 0]); s = arr[o]
print("lvl -> cut samples:")
step = max(1, len(s)//18)
for i in range(0, len(s), step):
print(" %8.3f -> %+8.2f dB" % tuple(s[i]))
if __name__ == "__main__":
main()
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""resalpha.py — find detector res-exponent alpha from existing references.
Model: lvl_case(alpha) = lvl_case(alpha=1) * res_case^(1-alpha)
real cut = L(log2 lvl) with ONE monotone law L shared by all cases.
For an alpha grid -> global LSQ of cut on (1, log2 lvl) -> mean|resid|(alpha).
The alpha minimising residuals is the detector's true res-weighting.
lvl(1), res and am at the tone bin come from RT_DUMP_BIN tracts of the current
build (bare-chain env; detector path identical). comb skipped (multiband).
"""
import os
import subprocess
import sys
import numpy as np
sys.path.insert(0, "/home/m/re-tools/scripts")
sys.path.insert(0, "/home/m/re-tools/handoff")
import corpus
from render_parity import FS
import corpus as _corpus_mod
load = _corpus_mod.load_mono
RB = '/home/m/re-tools/dsp/build/render48k'
ENVBASE = {'RT_LUT_OFF': '1', 'RT_IIR12': '0', 'RT_NOWARP': '1',
'RT_NOBLEND': '1', 'RT_NOIIR3': '1',
'RT_DUMP_BIN': '/tmp/opencode/resalpha_tract.txt'}
OUT = '/tmp/opencode/resalpha_pairs.csv'
def bin_of(f):
return int(round(f / 48000 * 4096))
def ta(x, f, sr=44100):
x = x[-int(0.75 * sr):].astype(np.float64)
t = np.arange(len(x)) / sr
w = 2 * np.pi * f
return np.hypot(2 * np.sum(x * np.cos(w * t)) / len(x),
2 * np.sum(x * np.sin(w * t)) / len(x))
def db(a):
return 20 * np.log10(max(a, 1e-12))
def main():
all_cases = []
for name, inp, args, ref, f in corpus.build_cases():
joined = [','.join(args)] if len(args) == 3 else args
all_cases.append((name, inp, joined, ref, f))
uniq = {}
case_meta = {c[0]: c for c in all_cases}
for name, inp, args, ref, f in all_cases:
uniq.setdefault((inp, tuple(args)), []).append((name, ref, f))
rows = []
out_wav = '/tmp/opencode/resalpha_out.wav'
for (inp, args), items in sorted(uniq.items()):
tract = '/tmp/opencode/resalpha_tract.txt'
if os.path.exists(tract):
os.remove(tract)
env = {**os.environ, **ENVBASE,
'RT_DUMP_FRAME': str(120 * max(1, len(args)) - 1)}
subprocess.run([RB, inp, out_wav] + list(args),
capture_output=True, text=True, env=env,
cwd='/home/m/re-tools')
try:
d = np.loadtxt(tract)
except Exception as ex:
print('load fail', items[0][0], ex, flush=True)
continue
if d.ndim == 1:
continue
k = d[:, 0].astype(int)
am, res, lvl = d[:, 1], d[:, 2], d[:, 3]
for name, ref, f in items:
b = min(bin_of(f), len(lvl) - 1)
rows.append((name.split('_')[0], name, f, float(lvl[b]),
float(res[b]), float(am[b]), inp, ref))
print(f'{items[0][0]:>18}: dumped', flush=True)
final = []
for g, name, f, lvl1, res, am, inp, ref in rows:
if g == 'comb':
continue
cut = db(ta(load(inp), f)) - db(ta(load(ref), f))
final.append((g, name, f, lvl1, res, cut))
with open(OUT, 'w') as fh:
fh.write('group,case,freq,lvl_a1,res,cut_db\n')
for g, name, f, lvl1, res, cut in final:
fh.write(f'{g},{name},{f},{lvl1:.9g},{res:.9g},{cut:.6f}\n')
print(f'\nwrote {len(final)} rows -> {OUT}')
# ---- alpha scan ----
lx = np.array([np.log2(max(r[3], 1e-9)) for r in final])
lr = np.array([np.log2(max(r[4], 1e-12)) for r in final])
y = np.array([r[5] for r in final])
gs = np.array([r[0] for r in final])
print(f'\n{"alpha":>6} {"mean|resid|":>11} per-group resid mean')
best = None
for alpha in np.arange(-0.5, 1.51, 0.05):
x = lx + (1 - alpha) * lr
A = np.stack([np.ones_like(x), x], axis=1)
coef, *_ = np.linalg.lstsq(A, y, rcond=None)
resid = y - A @ coef
m = float(np.mean(np.abs(resid)))
pergrp = ' '.join(
f'{g_}:{np.mean(resid[gs == g_]):+.2f}' for g_ in ['t1kq', 't1k', 'al', 'res', 'dual'])
print(f'{alpha:>6.2f} {m:>11.3f} {pergrp}')
if best is None or m < best[1]:
best = (float(alpha), m)
print(f'\nBEST alpha={best[0]:.2f} mean|resid|={best[1]:.3f}')
if __name__ == '__main__':
main()