Compare commits
9
Commits
3b9158e63e
...
07cd4b7dc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07cd4b7dc0 | ||
|
|
738ffccad4 | ||
|
|
91101b2135 | ||
|
|
64beaa7131 | ||
|
|
13c2d028bf | ||
|
|
34156315e6 | ||
|
|
79c11dc220 | ||
|
|
376bd955c9 | ||
|
|
3857205a6b |
@@ -4,11 +4,14 @@ Bit-exact реверс DSP-ядра oeksound soothe2 (VST3) → транскри
|
||||
Полное журналирование — в `handoff/NOTES_LEVEL.md`, `handoff/NOTES_TWIN.md`,
|
||||
`handoff/NOTES_CAPTURE.md`, `roadmap.md`.
|
||||
|
||||
> **Текущая фаза: bit-exact НЕ достигнут** — dB-параллель двумя канонами: bridge
|
||||
> (корпус mean 1.594 dB, comb 10.1) и структурная цепь FUN_180529fe0 на 48k/4096
|
||||
> (comb **6.12** — уже лучше bridge; однополосные хуже; разрыв = насыщение кривой
|
||||
> редукции). Путь к байтам — в `BITEXACT_PLAN.md` (8 шагов, пере-скоуп Шага 2).
|
||||
> ЭТОТ ФАЙЛ ЧИТАЙ ПЕРВЫМ. Bridge — запасной канон; структурная цепочка — целевая.
|
||||
> **Текущая фаза (2026-08-23, после 22z):** канон = структурная цепь 48k/4096 с
|
||||
> RT_LAWAFFINE=7.4,1.85 (TOTAL 1.931; bridge 1.594 — гейт не пройден). ГЛАВНЫЙ
|
||||
> ОТКРЫТЫЙ ВОПРОС — семантика входов `bands[]` детектора: доказанно НЕ поточечная
|
||||
> функция от (am,res^α) [22x], не форма-постобработка [22w], не спрединг
|
||||
> IDFT→окно→DFT [22z]. Живая кривая редукции R=1/mask захвачена (слот 0x5407f8,
|
||||
> dualtrace.py), но применённый фильтр ≠ её поточечной копии — механизм живёт ДО
|
||||
> кривой (twin-шаблон/скалярный драйв). Журнал: NOTES_LEVEL 22s–22z; карта метода:
|
||||
> handoff/BLOCKMAP_529fe0.md. ЭТОТ ФАЙЛ ЧИТАЙ ПЕРВЫМ.
|
||||
|
||||
## Золотое правило (обязательно)
|
||||
1. **Цель — bit-exact реверс кода**, НЕ эмпирическая подгонка кривых. Каждый параметр
|
||||
@@ -102,18 +105,19 @@ scale → LUT level-domain (t^γ·MULT, γ=0.344 decomp / MULT=4.2 placeholder)
|
||||
CMAX) не закрывает тон+шум одновременно → res/dual регресс у любой точки; ищем
|
||||
контент-зависимый механизм. Инструментарий RT_DUMP_BIN / RT_DUMP_ALL в коде (opt-in).
|
||||
|
||||
### Открытые bit-exact/структурные пробелы (см. NOTES_LEVEL.md)
|
||||
1. Насыщение кривой редукции (см. выше) — приоритет №1.
|
||||
2. `combine/аккумулятор 0x5407c8`: семантика декодирована (21b), но acc/f6f8 НЕ имеют
|
||||
однополосного консюмера — искать точку потребления (межполосный каскад). ВНИМАНИЕ:
|
||||
`FUN_1805316e0` = writer коэффициентов, НЕ комбинер масок.
|
||||
3. ~~BandConfig `ctx+0x188` A/B/γ — противоречие между сессиями~~ **РАЗРЕШЕНО
|
||||
(22b): live = −24/28/1 у ВСЕХ конфигов, но весь кластер FUN_180563440/563a60 —
|
||||
GUI-timer only; аудио-путь (FUN_180529fe0) BandConfig не читает. Шаг 7 в исходной
|
||||
постановке опровергнут; LUT-константы структурной цепи помечены EMPIRICAL.**
|
||||
4. PRNG-пролог (LCG+LUT → fVar30) — залочен (fVar30=1 при live state 112), dry/wet rnd импорт.
|
||||
5. Бит-экзактный exp2 (0x26b820); FFT-conv понижен до P3 (окно near-flat, NOTES:18c);
|
||||
SR-геометрия 48k/4096 сделана (render48k).
|
||||
### Открытые пробелы (после 22z; детали — NOTES_LEVEL 22s–22z)
|
||||
1. **Семантика входов `bands[]` в FUN_180529fe0** — главный вопрос. Отвергнуто:
|
||||
lvl=f(am,res^α) с единым законом [22x, теорема], форма-постобработка маски [22w],
|
||||
спрединг IDFT→окно→DFT [22z]. Живой захват: кривая редукции R=1/mask @слот
|
||||
0x5407f8 (пик следует за fc полосы), acc/f6f8 нули в стационаре.
|
||||
Пути: (а) декод тел conv 0x1802a24c0 + complex-op шагов с буферами
|
||||
0x540548/550/598; (б) синхронная трасса буферов в аудио-колбеке.
|
||||
2. dual-таблица по q (22x): cut@центра константен при любом q, Δcut скайрта ≈
|
||||
линейно по −ln res с b≈2.25 — геометрия twin-шаблона на сетке 0.25 Гц.
|
||||
3. FFT-conv: последовательность вызовов декодирована (BLOCKMAP/22z), тело
|
||||
0x1802a24c0 (AVX2) и complex-op шаги — не расшифрованы.
|
||||
4. Бит-экзактный exp2 (0x26b820); бит-точная DSP-FFT (P3); стерео M8 (P5) — отложены.
|
||||
5. Источник констант RT_LAWAFFINE A/S в декомпе не найден (помечено EMPIRICAL).
|
||||
|
||||
## Структура ключевых файлов
|
||||
- `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 дизассембла декомпа.
|
||||
- `/tmp/consumers_out.txt` — полный decomp `FUN_180529fe0` (уникален, вне git).
|
||||
- `/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/`,
|
||||
|
||||
+25
-15
@@ -18,6 +18,13 @@ comb 10 dB). Bit-exact ДОСТИЖИМ (F0 gate: плагин байт-дете
|
||||
> декоративная копия (не источник стейта); depth=±18dB/trim=±24dB; WAV bext/junk
|
||||
> грабли подтверждены живьём.
|
||||
> Новый приоритет №1 — Шаг 9 (ниже).
|
||||
>
|
||||
> **ОБНОВЛЕНИЕ 2026-08-23 (сессии 22s–22z):** Шаг 9 закрыт иначе — пол решён
|
||||
> алгебраически ещё в 22e; фронт сместился. Отвергнуты: скалярные законы A/S
|
||||
> (насыщение ~1.87–1.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)
|
||||
настолько точно, что `verify_bit_exact.py` даёт побайтовое совпадение на рендерах
|
||||
@@ -232,18 +239,21 @@ NOTES 22e) ⇒ пол живёт в другом куске аудио-пути.
|
||||
|
||||
## 4. Точка входа для следующей сессии
|
||||
|
||||
1. Прочитать `AGENTS.md`, этот `BITEXACT_PLAN.md`, `handoff/NOTES_LEVEL.md`
|
||||
апдейты **20j / 21a-21e / 22a-22e** (цепочка, Phase-B негативы, Шаг 7 опровержение,
|
||||
параметр-мост, пол blend·ln10/20) и `dsp/rt_mask_tables.hpp` / `dsp/rt_weights.hpp`.
|
||||
2. Собрать: `cmake --build dsp/build --target render48k framed_test`.
|
||||
3. Бейзлайны: `python3 scripts/corpus.py` (bridge, 62 случая, §0) и
|
||||
`python3 scripts/corpus_structural.py --vs-bridge scripts/baseline_bridge.json`.
|
||||
4. ПРИОРИТЕТ №1 — **Шаг 9** (пол редукции):
|
||||
a. 9a: полный декод FUN_180529c60 + FUN_1804d56b0 (f_529c60.dis готов).
|
||||
b. 9b: grep 0x1824c3cd4 по аудио-функциям; hunt клампа маски.
|
||||
c. 9c: предсказания пола — рендеры mix/sens через `setparam.lua` (мост готов,
|
||||
XML-правки стейта НЕ работают — NOTES 22c).
|
||||
d. 9d: порт + корпус с гейтом.
|
||||
5. Инструменты сессии: `setparam.lua`/`dump_params.lua` (параметр-мост),
|
||||
`scripts/rpp_setparam.py` (только формат-хирургия!), `RT_LUT_*` env в
|
||||
framed_model.cpp (солвер констант), `scripts/step7_capture.py` (live BandConfig).
|
||||
1. Прочитать `AGENTS.md`, затем NOTES_LEVEL **UPDATE 22s–22z** (журнал актуальной
|
||||
фазы) и `handoff/BLOCKMAP_529fe0.md` (блок-карта FUN_180529fe0 по raw asm).
|
||||
2. Собрать: `touch dsp/framed_model.cpp && cmake --build dsp/build --target render48k framed_test`
|
||||
(TOOLING HAZARD: touch перед сборкой обязателен).
|
||||
3. Бейзлайны: канон = `RT_LUT_OFF=1 RT_IIR12=0 RT_NOWARP=1 RT_NOBLEND=1 RT_NOIIR3=1
|
||||
RT_LAWAFFINE=7.4,1.85 python3 scripts/corpus_structural.py` → TOTAL 1.931;
|
||||
bridge-гейт: `--vs-bridge scripts/baseline_bridge.json` (1.594).
|
||||
4. **ПРИОРИТЕТ №1 — семантика входов `bands[]`**:
|
||||
a. Статика: декод тел conv 0x1802a24c0 (float)/0x1802fa420 (double,
|
||||
резолв ILT-стабов через idx@0x1826159a0, см. BLOCKMAP thunk-таблицу) и
|
||||
complex-op шагов с буферами ctx+0x540548/550/598.
|
||||
b. Динамика: синхронная трасса буферов bands[]/f6f8/acc в момент аудио-
|
||||
колбека (`scripts/dualtrace.py` + `play_loop.lua`; между кадрами acc/f6f8
|
||||
нули — снапшот должен попадать В колбек, см. NOTES_CAPTURE 22y).
|
||||
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).
|
||||
|
||||
@@ -25,6 +25,7 @@ add_library(soothe2_dsp SHARED
|
||||
exp2.cpp
|
||||
leveltrack.cpp
|
||||
framed_model.cpp
|
||||
fnfaith.cpp
|
||||
fn529fe0.cpp
|
||||
rt_weights.cpp
|
||||
rt_mask_tables.cpp
|
||||
|
||||
+105
@@ -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
|
||||
@@ -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
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "rt_mask_tables.hpp"
|
||||
#include "rt_weights.hpp"
|
||||
#include "fn529fe0.hpp"
|
||||
#include "fnfaith.hpp"
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#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,
|
||||
// TWO bidirectional passes [reset, forward, backward] x2 (state persists
|
||||
// from forward into backward within a pair; reset between pairs).
|
||||
@@ -330,6 +342,11 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
|
||||
res_.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_) {
|
||||
std::vector<float> r(half + 1, 1.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::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));
|
||||
}
|
||||
if (rp_dump) fclose(rp_dump);
|
||||
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;
|
||||
|
||||
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++) {
|
||||
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],
|
||||
band_mask.data(), nfft_, sample_rate_);
|
||||
}
|
||||
for (size_t k = 0; k <= half; k++) {
|
||||
mask[k] = std::min(band_mask[k], mask[k]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# BLOCK MAP: FUN_180529fe0 (полная разметка по raw asm, 22v)
|
||||
|
||||
Источник: `handoff/nls_dasm/f529fe0_full.dis` (soothe_mem.bin, база 0x180000000,
|
||||
диапазон 529c60–52ba00; старый 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) |
|
||||
|
||||
## Карта блоков
|
||||
|
||||
### Пролог
|
||||
- `52a03a–52a396`: 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 (52a397–52a421)
|
||||
- `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-вес (52a421–52a458)
|
||||
- `52d920(f6f8, xmm11/[rsp+0x148](int), nbin)` — f6f8 /= K.
|
||||
- `xmm7 = powf([ctx+0x2c], xmm13)` — mix^p.
|
||||
|
||||
### Pre-combine #2 (52a45e–52a4f6) по всем полосам
|
||||
- band[i] *= (xmm11 − xmm7) [thunk 2030/1d30]
|
||||
- f6f8 ⊕= ... с весом xmm7 [thunk 1fd0/2150]
|
||||
⇒ взвешенное смешение кривых полос ДО основного цикла.
|
||||
|
||||
### Основной цикл по полосам (52a4fb..52b3c7, тело с 52a580)
|
||||
На каждую полосу i (r13):
|
||||
1. `scale` (52a583–52a5c4): s = xmm11·[0x540870]; если флаг 0x5408b8:
|
||||
s ← exp(PRNG)-ветка; затем ·[0x54088c]. Читается track_i (@0x540768[i]) в [rsp+0x40].
|
||||
2. `transform(bands[i], s)` (52a5cc–52a607): bands[i] *= s [2030/1d30].
|
||||
3. флаг-оп (52a608–52a645): transform(bands[i], bands[i]) через 140980/1409b0
|
||||
(bigkernel, вероятно exp2-кернел) — только при флаге.
|
||||
4. `52d650(state@0x440518, f6f8, bands[i], nbin)` (52a646–52a658) — БИДИР-IIR #1:
|
||||
band → f6f8 (double, коэф down@0x440528/up@0x4c0528, acc@0x540528, reset внутри).
|
||||
5. Инлайн БИДИР-IIR #2 (52a65d–52a85a, только при флаге; иначе прыжок на 6):
|
||||
вход f6f8 → выход bands[i] (тот же state 0x440518, reset отдельно).
|
||||
6. `transform(f6f8 ⊕ bands[i])` (52a85b–52a896) [1970/1a60 = 3f40 SUB float /
|
||||
5160 SUB double]: f6f8 −= bands[i]? (аргументы rcx=f6f8, rdx=band).
|
||||
7. **БИДИР-IIR #3** (52a897–52aa6e, инлайн): state base 0x3404f8
|
||||
(down@0x2404f8, up@0x2c04f8, acc@0x3404f8), длина из поля ctx+0x2404e8,
|
||||
IN-PLACE по bands[i]. Reset каждый вызов.
|
||||
8. `op(bands[i], скаляр xmm9=0?)` (52aa6e–52aaaf) [2060/2120].
|
||||
9. Ветка флага==0 (52aabc–52ab90):
|
||||
- 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 (52ab90–52abd7)**: 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 (52abd7–52ad04):
|
||||
- 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]` (52acf4–52ad03) [1b80→6840 / 1d60→**5a20** add] ✓.
|
||||
13. Ветка флага!=0 (52ad04–52ae57): повтор п.9 с теми же адресами
|
||||
(0x540698, f6f8-blend, bigkernel 1409e0/140ad0).
|
||||
14. `op(bands[i], скаляры 0x1824c4680 / xmm13)` (52ae58–52ae56 хвост).
|
||||
15. `bands[i] ⊕= track_i [rsp+0x40]` (52ae74–52ae8a) [2000/1c40].
|
||||
16. `op(vec@0x5406a8, bands[i])` (52ae8f–52aecа) [2000/1c40].
|
||||
17. флаг → bigkernel in-place 140980/1409b0 (52aecb–52af08).
|
||||
18. **БИДИР-IIR #4 ×2** (52af09–52b2af и повтор 52b0de–52b2af):
|
||||
state base 0x440510 (down@0x340510, up@0x3c0510, acc@0x440510),
|
||||
длина ctx+0x340500, in-place bands[i]. ДВА прохода подряд (каждый с reset).
|
||||
19. Финальные scale/op полосы (52b2af–52b3af):
|
||||
- скаляр `[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.
|
||||
|
||||
### Эпилог (52b3cd–52b935)
|
||||
- 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 | длина |
|
||||
|---|------|----------|--------|-----|-------|
|
||||
| 1–2 (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. Эпилог 52b4eb–52b935.
|
||||
|
||||
## ДОПОЛНЕНИЕ 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 по этой карте.
|
||||
@@ -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).
|
||||
- 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).
|
||||
|
||||
## 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).
|
||||
|
||||
@@ -1958,3 +1958,269 @@ blend/warp/частотных каскадов) — и сразу даёт лу
|
||||
2. dual ~3.5 стабилен — межполосный механизм (приоритет из 22p).
|
||||
3. WIN_freq HF rolloff не добавлен — после добавления comb может улучшиться ещё.
|
||||
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, НЕ (L−N)/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 по nbin−2..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..n−1] (массив @0x540688 = соседний
|
||||
элемент того же массива векторов!): поэлементные thunks (float 0x20f0/double
|
||||
0x1850 пары = шаблонный transform); нормировка делением на int [rsp+0x148];
|
||||
затем второй проход: band[i] *= (xmm11−xmm7), 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·K1−K2)·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(500−2000) ≈ 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 (52b550–52b8bb, 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
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user