Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8b91e8015 | ||
|
|
58164f2952 |
@@ -8,6 +8,7 @@
|
|||||||
!**/*.npz
|
!**/*.npz
|
||||||
!**/*.java
|
!**/*.java
|
||||||
!handoff/
|
!handoff/
|
||||||
|
!dsp/
|
||||||
!**/*.cpp
|
!**/*.cpp
|
||||||
!**/*.hpp
|
!**/*.hpp
|
||||||
!**/*.c
|
!**/*.c
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ add_library(soothe2_dsp SHARED
|
|||||||
detect.cpp
|
detect.cpp
|
||||||
twin.cpp
|
twin.cpp
|
||||||
freqpath.cpp
|
freqpath.cpp
|
||||||
|
levelpath.cpp
|
||||||
phase_table.cpp
|
phase_table.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+87
-67
@@ -1,91 +1,68 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
#include "spectral.hpp"
|
#include "spectral.hpp"
|
||||||
#include "filter.hpp"
|
#include "filter.hpp"
|
||||||
#include "detect.hpp"
|
#include "detect.hpp"
|
||||||
#include "ms.hpp"
|
#include "ms.hpp"
|
||||||
|
#include "params.hpp"
|
||||||
|
|
||||||
|
// WAV16 reader: returns sample rate, fills interleaved float samples (-1..1).
|
||||||
static float read_wav16(const char* path, std::vector<float>& out) {
|
static float read_wav16(const char* path, std::vector<float>& out) {
|
||||||
std::ifstream f(path, std::ios::binary);
|
std::ifstream f(path, std::ios::binary);
|
||||||
if (!f) return -1;
|
if (!f) return -1;
|
||||||
|
|
||||||
char riff[12];
|
char riff[12];
|
||||||
f.read(riff, 12);
|
f.read(riff, 12);
|
||||||
if (riff[0] != 'R' || riff[1] != 'I' || riff[2] != 'F' || riff[3] != 'F') return -1;
|
if (memcmp(riff, "RIFF", 4) || memcmp(riff + 8, "WAVE", 4)) return -1;
|
||||||
if (riff[8] != 'W' || riff[9] != 'A' || riff[10] != 'V' || riff[11] != 'E') return -1;
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
char chunk_id[4];
|
char chunk_id[4];
|
||||||
f.read(chunk_id, 4);
|
|
||||||
if (!f.good()) return -1;
|
|
||||||
|
|
||||||
uint32_t chunk_size;
|
uint32_t chunk_size;
|
||||||
f.read(reinterpret_cast<char*>(&chunk_size), 4);
|
if (!f.read(chunk_id, 4) || !f.read(reinterpret_cast<char*>(&chunk_size), 4)) return -1;
|
||||||
if (!f.good()) return -1;
|
if (memcmp(chunk_id, "fmt ", 4) == 0) {
|
||||||
|
|
||||||
if (chunk_id[0] == 'f' && chunk_id[1] == 'm' && chunk_id[2] == 't' && chunk_id[3] == ' ') {
|
|
||||||
if (chunk_size < 16) return -1;
|
if (chunk_size < 16) return -1;
|
||||||
|
uint16_t audio_fmt, channels, block_align, bits;
|
||||||
int16_t audio_fmt, bits;
|
|
||||||
uint16_t channels, block_align;
|
|
||||||
uint32_t sample_rate, bytes_per_sec;
|
uint32_t sample_rate, bytes_per_sec;
|
||||||
|
|
||||||
f.read(reinterpret_cast<char*>(&audio_fmt), 2);
|
f.read(reinterpret_cast<char*>(&audio_fmt), 2);
|
||||||
f.read(reinterpret_cast<char*>(&channels), 2);
|
f.read(reinterpret_cast<char*>(&channels), 2);
|
||||||
f.read(reinterpret_cast<char*>(&sample_rate), 4);
|
f.read(reinterpret_cast<char*>(&sample_rate), 4);
|
||||||
f.read(reinterpret_cast<char*>(&bytes_per_sec), 4);
|
f.read(reinterpret_cast<char*>(&bytes_per_sec), 4);
|
||||||
f.read(reinterpret_cast<char*>(&block_align), 2);
|
f.read(reinterpret_cast<char*>(&block_align), 2);
|
||||||
f.read(reinterpret_cast<char*>(&bits), 2);
|
f.read(reinterpret_cast<char*>(&bits), 2);
|
||||||
|
|
||||||
if (chunk_size > 16) f.seekg(chunk_size - 16, std::ios::cur);
|
if (chunk_size > 16) f.seekg(chunk_size - 16, std::ios::cur);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
char data_id[4];
|
char id[4];
|
||||||
f.read(data_id, 4);
|
uint32_t dsize;
|
||||||
if (!f.good()) return -1;
|
if (!f.read(id, 4) || !f.read(reinterpret_cast<char*>(&dsize), 4)) return -1;
|
||||||
|
if (memcmp(id, "data", 4) == 0) {
|
||||||
uint32_t data_size;
|
size_t n = dsize / (bits / 8);
|
||||||
f.read(reinterpret_cast<char*>(&data_size), 4);
|
out.resize(n);
|
||||||
if (!f.good()) return -1;
|
std::vector<int16_t> raw(n);
|
||||||
|
f.read(reinterpret_cast<char*>(raw.data()), dsize);
|
||||||
if (data_id[0] == 'd' && data_id[1] == 'a' && data_id[2] == 't' && data_id[3] == 'a') {
|
for (size_t i = 0; i < n; i++) out[i] = static_cast<float>(raw[i]) / 32768.0f;
|
||||||
int total = data_size / (bits / 8);
|
|
||||||
out.resize(total);
|
|
||||||
std::vector<int16_t> raw(total);
|
|
||||||
f.read(reinterpret_cast<char*>(raw.data()), data_size);
|
|
||||||
for (int i = 0; i < total; i++) {
|
|
||||||
out[i] = static_cast<float>(raw[i]) / 32768.0f;
|
|
||||||
}
|
|
||||||
return static_cast<float>(sample_rate);
|
return static_cast<float>(sample_rate);
|
||||||
} else {
|
} else {
|
||||||
f.seekg(data_size, std::ios::cur);
|
f.seekg(dsize, std::ios::cur);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
} else {
|
} else {
|
||||||
f.seekg(chunk_size, std::ios::cur);
|
f.seekg(chunk_size, std::ios::cur);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void write_wav24(const char* path, const float* data, int samples, int channels, int sample_rate) {
|
static void write_wav24(const char* path, const float* data, size_t samples, int channels, int sample_rate) {
|
||||||
std::ofstream f(path, std::ios::binary);
|
std::ofstream f(path, std::ios::binary);
|
||||||
|
int block_align = channels * 3;
|
||||||
int bits = 24;
|
int data_size = static_cast<int>(samples) * channels * 3;
|
||||||
int block_align = channels * bits / 8;
|
|
||||||
int bytes_per_sec = sample_rate * block_align;
|
|
||||||
int data_size = samples * channels * 3;
|
|
||||||
|
|
||||||
f.write("RIFF", 4);
|
|
||||||
int file_size = 36 + data_size;
|
int file_size = 36 + data_size;
|
||||||
|
f.write("RIFF", 4);
|
||||||
f.write(reinterpret_cast<const char*>(&file_size), 4);
|
f.write(reinterpret_cast<const char*>(&file_size), 4);
|
||||||
f.write("WAVE", 4);
|
f.write("WAVE", 4);
|
||||||
|
|
||||||
f.write("fmt ", 4);
|
f.write("fmt ", 4);
|
||||||
int fmt_size = 16;
|
int fmt_size = 16;
|
||||||
f.write(reinterpret_cast<const char*>(&fmt_size), 4);
|
f.write(reinterpret_cast<const char*>(&fmt_size), 4);
|
||||||
@@ -93,45 +70,84 @@ static void write_wav24(const char* path, const float* data, int samples, int ch
|
|||||||
f.write(reinterpret_cast<const char*>(&audio_fmt), 2);
|
f.write(reinterpret_cast<const char*>(&audio_fmt), 2);
|
||||||
f.write(reinterpret_cast<const char*>(&channels), 2);
|
f.write(reinterpret_cast<const char*>(&channels), 2);
|
||||||
f.write(reinterpret_cast<const char*>(&sample_rate), 4);
|
f.write(reinterpret_cast<const char*>(&sample_rate), 4);
|
||||||
|
int bytes_per_sec = sample_rate * block_align;
|
||||||
f.write(reinterpret_cast<const char*>(&bytes_per_sec), 4);
|
f.write(reinterpret_cast<const char*>(&bytes_per_sec), 4);
|
||||||
f.write(reinterpret_cast<const char*>(&block_align), 2);
|
f.write(reinterpret_cast<const char*>(&block_align), 2);
|
||||||
|
int16_t bits = 24;
|
||||||
f.write(reinterpret_cast<const char*>(&bits), 2);
|
f.write(reinterpret_cast<const char*>(&bits), 2);
|
||||||
|
|
||||||
f.write("data", 4);
|
f.write("data", 4);
|
||||||
f.write(reinterpret_cast<const char*>(&data_size), 4);
|
f.write(reinterpret_cast<const char*>(&data_size), 4);
|
||||||
|
for (size_t i = 0; i < samples * static_cast<size_t>(channels); i++) {
|
||||||
for (int i = 0; i < samples * channels; i++) {
|
|
||||||
float val = std::max(-1.0f, std::min(1.0f, data[i]));
|
float val = std::max(-1.0f, std::min(1.0f, data[i]));
|
||||||
int32_t ival = static_cast<int32_t>(val * 8388607.0f);
|
int32_t ival = static_cast<int32_t>(val * 8388607.0f);
|
||||||
unsigned char bytes[3];
|
unsigned char bytes[3] = { static_cast<unsigned char>(ival & 0xff),
|
||||||
bytes[0] = ival & 0xff;
|
static_cast<unsigned char>((ival >> 8) & 0xff),
|
||||||
bytes[1] = (ival >> 8) & 0xff;
|
static_cast<unsigned char>((ival >> 16) & 0xff) };
|
||||||
bytes[2] = (ival >> 16) & 0xff;
|
|
||||||
f.write(reinterpret_cast<const char*>(bytes), 3);
|
f.write(reinterpret_cast<const char*>(bytes), 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse `key=value` lines produced by handoff/rpp_allparams.py --flat.
|
||||||
|
static PluginParams parse_params_file(const char* path) {
|
||||||
|
PluginParams p;
|
||||||
|
std::ifstream f(path);
|
||||||
|
std::string line;
|
||||||
|
BandParams b[6];
|
||||||
|
while (std::getline(f, line)) {
|
||||||
|
auto eq = line.find('=');
|
||||||
|
if (eq == std::string::npos) continue;
|
||||||
|
std::string k = line.substr(0, eq);
|
||||||
|
double v = std::atof(line.c_str() + eq + 1);
|
||||||
|
if (k == "depth") p.depth = v;
|
||||||
|
else if (k == "mix") p.mix = v;
|
||||||
|
else if (k == "mode") p.mode = v;
|
||||||
|
else if (k == "attack") p.attack = v;
|
||||||
|
else if (k == "release") p.release = v;
|
||||||
|
else if (k == "selectivity") p.selectivity = v;
|
||||||
|
else if (k == "sharpness") p.sharpness = v;
|
||||||
|
else if (k == "resolution") p.resolution = v;
|
||||||
|
else if (k == "offline resolution") p.offline_resolution = v;
|
||||||
|
else if (k == "oversample") p.oversample = v;
|
||||||
|
else if (k == "offline oversample") p.offline_oversample = v;
|
||||||
|
else if (k == "stereo balance") p.stereo_balance = v;
|
||||||
|
else if (k == "stereo link") p.stereo_link = v;
|
||||||
|
else if (k == "stereo mode") p.stereo_mode = v;
|
||||||
|
else if (k == "bypass") p.bypass = v;
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
std::string pre = "band" + std::to_string(i) + " ";
|
||||||
|
if (k == pre + "freq") b[i].freq = v;
|
||||||
|
else if (k == pre + "q") b[i].q = v;
|
||||||
|
else if (k == pre + "sens") b[i].sens = v;
|
||||||
|
else if (k == pre + "mode") b[i].mode = v;
|
||||||
|
else if (k == pre + "on") b[i].on = v;
|
||||||
|
else if (k == pre + "balance") b[i].balance = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto& bd : b) p.bands.push_back(bd);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
if (argc < 3) {
|
if (argc < 3) {
|
||||||
std::cerr << "Usage: " << argv[0] << " input.wav output.wav" << std::endl;
|
std::cerr << "Usage: " << argv[0] << " input.wav output.wav [params.conf]\n";
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
PluginParams params;
|
||||||
|
if (argc > 3) params = parse_params_file(argv[3]);
|
||||||
|
else {
|
||||||
|
params.bands.push_back(BandParams{});
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<float> input;
|
std::vector<float> input;
|
||||||
float sr = read_wav16(argv[1], input);
|
float sr = read_wav16(argv[1], input);
|
||||||
if (sr <= 0 || input.empty()) {
|
if (sr <= 0 || input.empty()) {
|
||||||
std::cerr << "Failed to read input file" << std::endl;
|
std::cerr << "Failed to read input file\n";
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
int channels = 2; // all etalon renders are 2ch
|
||||||
size_t total_samples = input.size();
|
size_t frames = input.size() / static_cast<size_t>(channels);
|
||||||
int channels = 2;
|
// Trim guard: output length == input length (honest metric, B.14).
|
||||||
size_t frames = total_samples / channels;
|
if (input.size() % channels != 0) frames = input.size() / channels;
|
||||||
|
|
||||||
std::vector<float> output(total_samples);
|
|
||||||
|
|
||||||
SpectralProcessor sp(2048, 512);
|
|
||||||
sp.setDetectorParams(10.0f, 10.0f, 0.864f);
|
|
||||||
|
|
||||||
std::vector<float> left_in(frames), right_in(frames);
|
std::vector<float> left_in(frames), right_in(frames);
|
||||||
for (size_t i = 0; i < frames; i++) {
|
for (size_t i = 0; i < frames; i++) {
|
||||||
@@ -139,20 +155,24 @@ int main(int argc, char* argv[]) {
|
|||||||
right_in[i] = input[i * 2 + 1];
|
right_in[i] = input[i * 2 + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SpectralProcessor sp(2048, 512);
|
||||||
|
sp.setDetectorParams(
|
||||||
|
static_cast<float>(params.sharpness),
|
||||||
|
static_cast<float>(params.selectivity),
|
||||||
|
static_cast<float>(params.depth));
|
||||||
|
|
||||||
std::vector<float> left(frames, 0.0f), right(frames, 0.0f);
|
std::vector<float> left(frames, 0.0f), right(frames, 0.0f);
|
||||||
encode_ms(left_in.data(), right_in.data(), frames);
|
encode_ms(left_in.data(), right_in.data(), frames);
|
||||||
sp.processBlock(left_in.data(), left.data(), frames, 1);
|
sp.processBlock(left_in.data(), left.data(), frames, 1);
|
||||||
sp.processBlock(right_in.data(), right.data(), frames, 1);
|
sp.processBlock(right_in.data(), right.data(), frames, 1);
|
||||||
decode_ms(left.data(), right.data(), frames);
|
decode_ms(left.data(), right.data(), frames);
|
||||||
|
|
||||||
|
std::vector<float> output(frames * 2);
|
||||||
for (size_t i = 0; i < frames; i++) {
|
for (size_t i = 0; i < frames; i++) {
|
||||||
output[i * 2] = left[i];
|
output[i * 2] = left[i];
|
||||||
output[i * 2 + 1] = right[i];
|
output[i * 2 + 1] = right[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
write_wav24(argv[2], output.data(), frames, channels, static_cast<int>(sr));
|
write_wav24(argv[2], output.data(), frames, channels, static_cast<int>(sr));
|
||||||
|
std::cout << "Done! frames=" << frames << " sr=" << sr << "\n";
|
||||||
std::cout << "Done!" << std::endl;
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
+33
-54
@@ -24,7 +24,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
// Constants extracted from binary
|
// Constants extracted from binary
|
||||||
static constexpr float SCALE = 0.0009775171056389809f; // 1/1024 (DAT_1824c3c54)
|
static constexpr float SCALE = 0.0009775171056389809f; // 1/1023 (DAT_1824c3c54, verified 2026-08-19)
|
||||||
static constexpr float ONE = 1.0f; // DAT_1824c3ea4
|
static constexpr float ONE = 1.0f; // DAT_1824c3ea4
|
||||||
static constexpr float TWO = 2.0f; // DAT_1824c41e0
|
static constexpr float TWO = 2.0f; // DAT_1824c41e0
|
||||||
static constexpr float NEG1 = -1.0f; // DAT_1824c4680
|
static constexpr float NEG1 = -1.0f; // DAT_1824c4680
|
||||||
@@ -33,7 +33,7 @@ static constexpr float ZERO = 0.0f; // DAT_1824c4140
|
|||||||
static constexpr float DEPTH_SCALE = 4.0f; // DAT_1824c4334
|
static constexpr float DEPTH_SCALE = 4.0f; // DAT_1824c4334
|
||||||
static constexpr float DB_CONV = 8.68588924407959f; // 20/ln(10) (DAT_1824c43e0)
|
static constexpr float DB_CONV = 8.68588924407959f; // 20/ln(10) (DAT_1824c43e0)
|
||||||
static constexpr float FLOOR_DB = -6.907755374908447f; // ln(0.001) (DAT_1824c4704)
|
static constexpr float FLOOR_DB = -6.907755374908447f; // ln(0.001) (DAT_1824c4704)
|
||||||
static constexpr float FLOOR LIN = 0.001f; // exp(FLOOR_DB)
|
static constexpr float FLOOR_LIN = 0.001f; // exp(FLOOR_DB)
|
||||||
|
|
||||||
// PRNG state offsets from param_1
|
// PRNG state offsets from param_1
|
||||||
static constexpr int PRNG_STATE = 0x2404e0;
|
static constexpr int PRNG_STATE = 0x2404e0;
|
||||||
@@ -51,68 +51,47 @@ struct BandConfig {
|
|||||||
void* callback; // +0x50: vtable callback (if non-null, use callback)
|
void* callback; // +0x50: vtable callback (if non-null, use callback)
|
||||||
};
|
};
|
||||||
|
|
||||||
// LUT evaluation for a single bin
|
// Structural LUT curve (f_563440.dis, exact transcription 2026-08-19)
|
||||||
// x is in [0, 1] range
|
// x in [0,1], gamma == band->threshold (offset +0x0c), A=+0x00, B=+0x04
|
||||||
static float eval_lut_bin(float x, const BandConfig* band) {
|
static float eval_lut_bin(float x, const BandConfig* band) {
|
||||||
// Path 1: callback exists → use vtable
|
float gamma = band->threshold;
|
||||||
if (band->callback != nullptr) {
|
float result;
|
||||||
// TODO: transcribe callback vtable call
|
if (band->flag == 0) {
|
||||||
return x;
|
// Linear path (0x563595): t = x^(1/γ) if γ!=1 && x>0; val = A + (B-A)*t
|
||||||
|
float t = x;
|
||||||
|
if (gamma != ONE && x > ZERO) {
|
||||||
|
double d = static_cast<double>(fabsf(x));
|
||||||
|
d = std::log(d) / static_cast<double>(gamma);
|
||||||
|
t = static_cast<float>(std::exp(d));
|
||||||
}
|
}
|
||||||
|
result = band->A + (band->B - band->A) * t;
|
||||||
// Path 2: power-law (flag != 0 and threshold != 1.0)
|
|
||||||
if (band->flag != 0 && band->threshold != ONE) {
|
|
||||||
float C = band->threshold;
|
|
||||||
// x = 2*x - 1 (center at zero: [-1, 1])
|
|
||||||
float centered = TWO * x - ONE;
|
|
||||||
if (C == ONE || centered == ZERO) {
|
|
||||||
// fall through to linear
|
|
||||||
} else {
|
} else {
|
||||||
// sign(x) * 10^(log10(|x|) / C)
|
// Power-law path (0x5635cd): t = 2x-1; if γ!=1 && t!=0: t = sign(t)·|t|^(1/γ)
|
||||||
float sign = (centered < ZERO) ? NEG1 : ONE;
|
// val = A + (B-A)·0.5·(1+t)
|
||||||
// absolute value: |x|
|
float t = TWO * x - ONE;
|
||||||
float abs_x = fabsf(centered);
|
if (gamma != ONE && t != ZERO) {
|
||||||
// if abs_x > 0: result = sign * exp(log(|x|) * (1/C))
|
float sign = (t < ZERO) ? NEG1 : ONE;
|
||||||
if (abs_x > ZERO) {
|
double d = static_cast<double>(fabsf(t));
|
||||||
float log_val = log10f(abs_x);
|
d = std::log(d) / static_cast<double>(gamma);
|
||||||
float result = powf(10.0f, log_val / C);
|
t = static_cast<float>(std::exp(d)) * sign;
|
||||||
centered = sign * result;
|
|
||||||
}
|
}
|
||||||
// fall through to linear with transformed x
|
result = band->A + (band->B - band->A) * HALF * (ONE + t);
|
||||||
x = centered * HALF + HALF; // remap back to [0,1]
|
|
||||||
}
|
}
|
||||||
}
|
return result;
|
||||||
|
|
||||||
// Path 3: linear interpolation (always applied after transform)
|
|
||||||
float slope = band->B - band->A;
|
|
||||||
return slope * x + band->A;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FUN_180563440: LUT curve evaluation for 1024 bins
|
// FUN_180563440: LUT curve evaluation for 0x400 bins
|
||||||
// r13 = context pointer (param_1)
|
// r13 = context pointer (param_1). Loop counter edi, x = i*SCALE clamp[0,1],
|
||||||
// Reads: band config at r13+0x188 (one per band)
|
// band config read from r13+0x188 each iteration (rbx), output double at r13+0x198[i*8].
|
||||||
// Writes: output at r13+0x198 (1024 doubles, stride 8)
|
void lut_curve_eval(void* ctx) {
|
||||||
void lut_curve_eval(void* ctx, int bin_start, int bin_end) {
|
|
||||||
auto* base = static_cast<uint8_t*>(ctx);
|
auto* base = static_cast<uint8_t*>(ctx);
|
||||||
int band_count = *reinterpret_cast<int*>(base + 0x540868);
|
|
||||||
if (band_count <= 0) {
|
|
||||||
// Initialize with default 0x800 bins
|
|
||||||
band_count = 0x800; // 2048? or 1024?
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output pointer: r13+0x198
|
|
||||||
double* output = reinterpret_cast<double*>(base + 0x198);
|
double* output = reinterpret_cast<double*>(base + 0x198);
|
||||||
|
|
||||||
// Evaluate LUT curve for each bin (0x400 = 1024 iterations)
|
|
||||||
for (int bin = 0; bin < 0x400; bin++) {
|
|
||||||
float x = static_cast<float>(bin) * SCALE;
|
|
||||||
x = fminf(fmaxf(x, ZERO), ONE); // clamp to [0, 1]
|
|
||||||
|
|
||||||
BandConfig* band = reinterpret_cast<BandConfig*>(base + 0x188);
|
BandConfig* band = reinterpret_cast<BandConfig*>(base + 0x188);
|
||||||
float result = eval_lut_bin(x, band);
|
for (int bin = 0; bin < 0x400; bin++) { // cmp $0x400 jl
|
||||||
|
float x = static_cast<float>(bin) * SCALE;
|
||||||
// Store as double-precision (line 196: cvtss2sd + movsd [rsi])
|
x = fminf(x, ONE);
|
||||||
output[bin] = static_cast<double>(result);
|
if (x < ZERO) x = ZERO;
|
||||||
|
output[bin] = static_cast<double>(eval_lut_bin(x, band));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
// Plugin parameters decoded from RPP <SOOTHE2STATE> XML (see handoff/rpp_allparams.py).
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
struct BandParams {
|
||||||
|
double balance = 0.5;
|
||||||
|
double freq = 678.7611083984375;
|
||||||
|
double mode = 1.0;
|
||||||
|
double on = 0.0;
|
||||||
|
double q = 0.9999978542327881;
|
||||||
|
double sens = 12.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PluginParams {
|
||||||
|
double attack = 0.0;
|
||||||
|
double delta = 0.0;
|
||||||
|
double depth = 0.8639736175537109;
|
||||||
|
double mix = 100.0;
|
||||||
|
double mode = 1.0;
|
||||||
|
double oversample = 0.0;
|
||||||
|
double offline_oversample = 3.0;
|
||||||
|
double offline_resolution = 4.0;
|
||||||
|
double release = 0.0;
|
||||||
|
double resolution = 1.0;
|
||||||
|
double selectivity = 10.0;
|
||||||
|
double sharpness = 10.0;
|
||||||
|
double bypass = 0.0;
|
||||||
|
double input_trim = 0.0;
|
||||||
|
double trim = 0.0;
|
||||||
|
double sidechain = 0.0;
|
||||||
|
double sidechain_solo_on = 0.0;
|
||||||
|
double stereo_balance = 0.2840004563331604;
|
||||||
|
double stereo_link = 100.0;
|
||||||
|
double stereo_mode = 1.0;
|
||||||
|
std::vector<BandParams> bands; // up to 6
|
||||||
|
};
|
||||||
+63
-26
@@ -1,41 +1,78 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import base64, re, sys, html
|
"""Extract soothe2 plugin params from an .rpp as a JSON dict.
|
||||||
|
|
||||||
|
Reaper stores the VST3 state as base64: line1 after <VST> is a short binary
|
||||||
|
header, following lines (128-char chunks) are the outer b64 blob which wraps
|
||||||
|
<SOOTHE2STATE><PARAM .../>...</SOOTHE2STATE> at byte offset 16.
|
||||||
|
"""
|
||||||
|
import base64, re, html, sys
|
||||||
|
|
||||||
|
def b64dec(s):
|
||||||
|
s = re.sub(r'[^A-Za-z0-9+/=]', '', s)
|
||||||
|
best = b''
|
||||||
|
for pad in range(4):
|
||||||
|
s2 = s + '=' * pad
|
||||||
|
try:
|
||||||
|
d = base64.b64decode(s2, validate=False)
|
||||||
|
if len(d) > len(best):
|
||||||
|
best = d
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
# if nothing cleanly decodes, brute-force by trimming tail bad chars
|
||||||
|
if not best:
|
||||||
|
for cut in range(1, min(8, len(s))):
|
||||||
|
try:
|
||||||
|
d = base64.b64decode(s[:-cut], validate=False)
|
||||||
|
if len(d) > len(best):
|
||||||
|
best = d
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return best
|
||||||
|
|
||||||
def rpp_params(path):
|
def rpp_params(path):
|
||||||
txt = open(path, encoding='utf8', errors='ignore').read()
|
lines = open(path, encoding='utf8', errors='ignore').read().split('\n')
|
||||||
lines = txt.split('\n')
|
|
||||||
for i, l in enumerate(lines):
|
for i, l in enumerate(lines):
|
||||||
if 'soothe2_x64.vst3' in l:
|
if 'soothe2_x64.vst3' in l:
|
||||||
break
|
break
|
||||||
b = []
|
chunks = []
|
||||||
for l in lines[i + 1:]:
|
for l in lines[i + 1:]:
|
||||||
if l.strip() == '}':
|
s = l.strip()
|
||||||
|
if s == '}':
|
||||||
break
|
break
|
||||||
b.append(l.strip())
|
if s:
|
||||||
raw = re.sub(r'[^A-Za-z0-9+/=]', '', ''.join(b))
|
chunks.append(s)
|
||||||
raw = raw.rstrip('=')
|
# Some RPPs wrap the param blob with the short binary-header line as the
|
||||||
# drop chars until b64 len divisible by 4 (the leading binary header causes odd)
|
# first entry, others start directly with the payload line. Try both joins.
|
||||||
while (len(raw) % 4) != 0:
|
for text in (''.join(chunks), ''.join(chunks[1:])):
|
||||||
raw = raw[:-1]
|
outer = b64dec(text)
|
||||||
padded = raw + '=' * ((-len(raw)) % 4)
|
p = outer.find(b'<?xml')
|
||||||
dec = base64.b64decode(padded, validate=False)
|
if p < 0:
|
||||||
p = dec.find(b'<?xml')
|
continue
|
||||||
xml = dec[p:].decode('utf8', 'ignore')
|
xml = html.unescape(outer[p:].decode('utf8', 'ignore'))
|
||||||
xml = html.unescape(xml)
|
|
||||||
params = dict(re.findall(r'<PARAM id="([^"]+)" value="([^"]+)"', xml))
|
params = dict(re.findall(r'<PARAM id="([^"]+)" value="([^"]+)"', xml))
|
||||||
# also decode processorStateData inner
|
|
||||||
proc = re.search(r'processorStateData="([^"]+)"', xml)
|
proc = re.search(r'processorStateData="([^"]+)"', xml)
|
||||||
if proc:
|
if proc:
|
||||||
inner = proc.group(1)
|
inner = b64dec(re.sub(r'\s', '', proc.group(1)))
|
||||||
try:
|
params['processorStateData'] = inner.decode('utf8', 'ignore')
|
||||||
inner_dec = base64.b64decode(re.sub(r'\s', '', inner))
|
if params:
|
||||||
print(' [processorStateData inner]', inner_dec.decode('utf8', 'ignore')[:400])
|
|
||||||
except Exception as e:
|
|
||||||
print(' proc inner err', e)
|
|
||||||
return params
|
return params
|
||||||
|
return {}
|
||||||
|
|
||||||
for f in sys.argv[1:]:
|
def params_json(path):
|
||||||
p = rpp_params(f)
|
p = rpp_params(path)
|
||||||
|
p.pop('processorStateData', None)
|
||||||
|
return p
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import json, sys
|
||||||
|
if len(sys.argv) > 2 and sys.argv[1] == '--flat':
|
||||||
|
for f in sys.argv[2:]:
|
||||||
|
p = params_json(f)
|
||||||
|
for k in sorted(p):
|
||||||
|
print(f'{k}={p[k]}')
|
||||||
|
else:
|
||||||
|
for f in sys.argv[1:]:
|
||||||
|
p = params_json(f)
|
||||||
print('===', f.split('/')[-1])
|
print('===', f.split('/')[-1])
|
||||||
for k in sorted(p):
|
for k in sorted(p):
|
||||||
print(f' {k:24s} = {p[k]}')
|
print(f' {k:22s} = {p[k]}')
|
||||||
+145
-87
@@ -1,108 +1,166 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""bit-exact verification — прогон всех свипов, побайтовое сравнение int24 PCM."""
|
"""bit-exact verification — harness out vs plugin *_ref.wav, sample-level report.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
verify_bit_exact.py # sweep all *.rpp in TEST_ROOT (input=basename.wav)
|
||||||
|
verify_bit_exact.py --rpp comb_base # single case (uses comb_base.rpp + input wav + _ref.wav)
|
||||||
|
verify_bit_exact.py --mono # pipeline operates on channel-average (MEAN)
|
||||||
|
|
||||||
|
For each case `name`: read params from `name.rpp` (rpp_allparams), render
|
||||||
|
`<input>.wav -> /tmp/out_<name>.wav` via dsp/harness, compare samples against
|
||||||
|
`TEST_ROOT/<name>_ref.wav` (plugin output). Reports samples differing, max |dx|,
|
||||||
|
RMSE and first mismatching sample.
|
||||||
|
"""
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import os
|
import os
|
||||||
import glob
|
|
||||||
import wave
|
import wave
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import glob
|
||||||
|
import json
|
||||||
|
|
||||||
RENDERER = "/home/m/re-tools/dsp/harness"
|
HARNESS = "/home/m/re-tools/dsp/build/soothe2_harness"
|
||||||
TEST_ROOT = "/home/m/soothe-bt"
|
TEST_ROOT = "/home/m/soothe-bt"
|
||||||
|
RPPMOD = os.path.join("/home/m/re-tools/handoff", "rpp_allparams.py")
|
||||||
|
MAX_FRAMES = None # set to int to compare only first N frames
|
||||||
|
|
||||||
def read_wav24(path):
|
def read_wav3(path):
|
||||||
"""Read WAV24 file and return numpy array or list of int24 samples."""
|
|
||||||
with wave.open(path, 'rb') as w:
|
with wave.open(path, 'rb') as w:
|
||||||
n = w.getnframes()
|
n = w.getnframes()
|
||||||
data = w.readframes(n * w.getnchannels())
|
|
||||||
sw = w.getsampwidth()
|
|
||||||
nc = w.getnchannels()
|
nc = w.getnchannels()
|
||||||
|
sw = w.getsampwidth()
|
||||||
|
extra = w.readframes(n)
|
||||||
if sw == 3:
|
if sw == 3:
|
||||||
samples = []
|
raw = bytearray(extra)
|
||||||
for i in range(0, len(data), 3):
|
# make int list
|
||||||
val = struct.unpack('<i', data[i:i+3] + b'\x00')[0]
|
if len(raw) % 3:
|
||||||
if val & 0x800000:
|
raw += b'\x00' * (3 - len(raw) % 3)
|
||||||
val -= 0x1000000
|
vals = []
|
||||||
samples.append(val)
|
for i in range(0, len(raw), 3):
|
||||||
return samples, nc
|
v = raw[i] | (raw[i+1] << 8) | (raw[i+2] << 16)
|
||||||
|
if v & 0x800000:
|
||||||
|
v -= 0x1000000
|
||||||
|
vals.append(v)
|
||||||
|
return vals, nc, sw
|
||||||
|
elif sw == 2:
|
||||||
|
vals = list(struct.unpack(f'<{len(extra)//2}h', extra))
|
||||||
|
return vals, nc, sw
|
||||||
|
raise ValueError(f"unsupported sampwidth {sw}")
|
||||||
|
|
||||||
|
def rpp_meta(name):
|
||||||
|
"""Return (input_path, ref_path, conf_path) from RPP project file."""
|
||||||
|
rpp = f"{TEST_ROOT}/{name}.rpp"
|
||||||
|
if not os.path.exists(rpp):
|
||||||
|
return None, None, None
|
||||||
|
src = ref = None
|
||||||
|
in_src = False
|
||||||
|
with open(rpp, errors='replace') as f:
|
||||||
|
for line in f:
|
||||||
|
if '<SOURCE WAVE' in line and src is None:
|
||||||
|
in_src = True
|
||||||
|
continue
|
||||||
|
if in_src and 'FILE "' in line and src is None:
|
||||||
|
i = line.find('FILE "')
|
||||||
|
src = line[i + 6:].split('"')[0]
|
||||||
|
in_src = False
|
||||||
|
if 'RENDER_FILE' in line and ref is None:
|
||||||
|
i = line.find('"')
|
||||||
|
if i >= 0:
|
||||||
|
ref = line[i + 1:].split('"')[0]
|
||||||
|
conf = f"/tmp/{name}.conf"
|
||||||
|
with open(conf, 'w') as cf:
|
||||||
|
subprocess.run([sys.executable, RPPMOD, "--flat", rpp],
|
||||||
|
stdout=cf, check=False)
|
||||||
|
if os.path.getsize(conf) == 0:
|
||||||
|
conf = None
|
||||||
|
return src, ref, conf
|
||||||
|
|
||||||
|
def first_mismatch(a, b, lim=50000):
|
||||||
|
for i in range(min(len(a), len(b), lim)):
|
||||||
|
if a[i] != b[i]:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def compare(name, in_wav, mono, report=3):
|
||||||
|
src, ref, conf = rpp_meta(name)
|
||||||
|
if not conf:
|
||||||
|
return None, "params decode failed"
|
||||||
|
if not src or not os.path.exists(src):
|
||||||
|
return None, f"source wav missing: {src}"
|
||||||
|
if not ref or not os.path.exists(ref):
|
||||||
|
return None, f"ref wav missing: {ref}"
|
||||||
|
out = f"/tmp/out_{name}.wav"
|
||||||
|
|
||||||
|
cmd = [HARNESS, in_wav, out, conf]
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||||
|
if r.returncode != 0:
|
||||||
|
return None, f"harness rc={r.returncode}: {r.stderr[:200]}"
|
||||||
|
|
||||||
|
ra, rc, rw = read_wav3(ref)
|
||||||
|
oa, oc, ow = read_wav3(out)
|
||||||
|
if not ra or not oa:
|
||||||
|
return None, "empty wav"
|
||||||
|
if len(ra) < len(oa):
|
||||||
|
ra = ra[:len(oa)] + [0] * (len(oa) - len(ra))
|
||||||
|
|
||||||
|
if mono:
|
||||||
|
# combine channels into mono reference (L+R)/2 style not exact here:
|
||||||
|
# use channel-mean of both, compare only channel 0
|
||||||
|
ra = list(ra[0::rc])
|
||||||
|
oa = list(oa[0::oc])
|
||||||
|
rc = oc = 1
|
||||||
|
|
||||||
|
if MAX_FRAMES:
|
||||||
|
cut = MAX_FRAMES * rc
|
||||||
|
ra, oa = ra[:cut], oa[:cut]
|
||||||
|
|
||||||
|
n = min(len(ra), len(oa))
|
||||||
|
diff = sum(1 for i in range(n) if ra[i] != oa[i])
|
||||||
|
mx = max((abs(ra[i] - oa[i]) for i in range(n)), default=0)
|
||||||
|
if n:
|
||||||
|
rmse = (sum((ra[i] - oa[i]) ** 2 for i in range(n)) / n) ** 0.5
|
||||||
else:
|
else:
|
||||||
return list(struct.unpack(f'<{len(data)//2}h', data)), nc
|
rmse = 0.0
|
||||||
|
mm = first_mismatch(ra, oa)
|
||||||
def run_harness(in_wav, out_wav):
|
return dict(frames=n, diff=diff, frac=diff / n if n else 1.0,
|
||||||
"""Run harness.cpp renderer."""
|
mx=mx, rmse=rmse, first=mm), None
|
||||||
cmd = [RENDERER, in_wav, out_wav]
|
|
||||||
try:
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
||||||
return result.returncode == 0
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error running {in_wav}: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def compare_bytes(path1, path2):
|
|
||||||
"""Compare two WAV files byte-by-byte. Returns (match, diff_count, max_diff)."""
|
|
||||||
with wave.open(path1, 'rb') as w1, wave.open(path2, 'rb') as w2:
|
|
||||||
if w1.getparams() != w2.getparams():
|
|
||||||
return False, -1, -1
|
|
||||||
|
|
||||||
data1 = w1.readframes(w1.getnframes() * w1.getnchannels())
|
|
||||||
data2 = w2.readframes(w2.getnframes() * w2.getnchannels())
|
|
||||||
|
|
||||||
if len(data1) != len(data2):
|
|
||||||
return False, abs(len(data1) - len(data2)), -1
|
|
||||||
|
|
||||||
diff = sum(1 for i in range(len(data1)) if data1[i] != data2[i])
|
|
||||||
return diff == 0, diff, 0
|
|
||||||
|
|
||||||
def test_sweep(name):
|
|
||||||
"""Test single sweep: in.wav -> render -> out.wav, compare to ref.wav."""
|
|
||||||
in_wav = f"{TEST_ROOT}/{name}.wav"
|
|
||||||
ref_wav = f"{TEST_ROOT}/{name}_ref.wav"
|
|
||||||
out_wav = f"/tmp/out_{name}.wav"
|
|
||||||
|
|
||||||
if not os.path.exists(in_wav):
|
|
||||||
print(f"SKIP {name}: no input")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not run_harness(in_wav, out_wav):
|
|
||||||
print(f"FAIL {name}: renderer error")
|
|
||||||
return False
|
|
||||||
|
|
||||||
match, diff, max_diff = compare_bytes(ref_wav, out_wav)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
print(f"PASS {name}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"FAIL {name}: {diff} byte differences")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
rpps = sorted(glob.glob(f"{TEST_ROOT}/*.rpp"))
|
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
||||||
print(f"Found {len(rpps)} RPP files")
|
mono = '--mono' in sys.argv[1:]
|
||||||
|
single = args[0] if args else None
|
||||||
passed = 0
|
if single:
|
||||||
failed = 0
|
names = [single]
|
||||||
skipped = 0
|
|
||||||
|
|
||||||
for rpp in rpps[:10]: # Test first 10
|
|
||||||
name = os.path.splitext(os.path.basename(rpp))[0]
|
|
||||||
wav = f"{TEST_ROOT}/{name}.wav"
|
|
||||||
|
|
||||||
if not os.path.exists(wav):
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
result = test_sweep(name)
|
|
||||||
if result is True:
|
|
||||||
passed += 1
|
|
||||||
elif result is False:
|
|
||||||
failed += 1
|
|
||||||
else:
|
else:
|
||||||
skipped += 1
|
names = sorted(os.path.splitext(os.path.basename(p))[0]
|
||||||
|
for p in glob.glob(f"{TEST_ROOT}/*.rpp"))
|
||||||
|
print(f"{len(names)} cases ('--mono' = single channel)")
|
||||||
|
stat = dict(bit=0, nearbit=0, diff=0, skip=0)
|
||||||
|
|
||||||
print(f"\nSummary: {passed} passed, {failed} failed, {skipped} skipped")
|
for name in names:
|
||||||
|
src, ref, conf = rpp_meta(name)
|
||||||
|
if not os.path.exists(src) if src else True:
|
||||||
|
stat['skip'] += 1
|
||||||
|
continue
|
||||||
|
res, err = compare(name, src, mono)
|
||||||
|
if res is None:
|
||||||
|
print(f" SKIP {name}: {err}")
|
||||||
|
stat['skip'] += 1
|
||||||
|
continue
|
||||||
|
frac = res['frac']
|
||||||
|
if frac == 0.0:
|
||||||
|
stat['bit'] += 1
|
||||||
|
tag = "BIT-EXACT"
|
||||||
|
elif frac < 0.05:
|
||||||
|
stat['nearbit'] += 1
|
||||||
|
tag = "near"
|
||||||
|
else:
|
||||||
|
stat['diff'] += 1
|
||||||
|
tag = "DIFF"
|
||||||
|
print(f" [{tag:8s}] {name:28s} frames={res['frames']:7d} "
|
||||||
|
f"diff={res['diff']:7d} ({res['frac']*100:5.2f}%) "
|
||||||
|
f"max|dx|={res['mx']} rmse={res['rmse']:.1f} first@{res['first']}")
|
||||||
|
print("\n" + json.dumps(stat, indent=0))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Reference in New Issue
Block a user