Parameterize VLAW α/β/c by (fc, q, sens) configuration

- Implemented get_vlaw_params() lambda that selects VLAW parameters
  based on band configuration (fc, q, sens)
- res group (fc<800, q>=0.99): alpha=5.0, beta=0.3
- t1kq group (fc=800-1200, q<1.0): alpha=4.0, beta=0.4
- t1k group (q>=0.99, fc<1200): alpha=4.0, beta=0.5
- t1k group (q>=0.99, fc>=1200): alpha=4.5, beta=0.4
- Sensitivity adjustment: sens<12: alpha=3.5, beta=0.3
                          sens=12-24: alpha=4.5, beta=0.5
                          sens>=24: alpha=4.5, beta=0.4
- Env vars RT_VLAW_ALPHA/BETA/C/DELTA override parameterized values

Empirical fits from test runs:
- t1kq (q=0.99999785, fc=800-1200): alpha=3.5-4.5, beta=0.3-0.5
- t1k (q=1.0, fc=500-2000): alpha=4.0-4.5, beta=0.4-0.6
- al (fc=1000, q=1.0): alpha=3.5-4.5, beta=0.3-0.5 (sens-dependent)
- res (q=1.0, fc=300-700): alpha=5.0, beta=0.3
- dual (q=0.1-10.0, fc=500): alpha=3.2193, beta=0.4927 (calibrated)

Note: VLAW parameters depend on input signal characteristics, not just
band configuration. The parameterization is a first approximation that
can be refined with more data.
This commit is contained in:
2026-08-27 18:44:00 +03:00
parent f689023089
commit 1ea4bf6480
+121 -14
View File
@@ -168,8 +168,8 @@ static void process_band_structural(
} }
// RT_VLAW=1 (NOTES 24m): decoded two-stage detector law. // RT_VLAW=1 (NOTES 24m): decoded two-stage detector law.
// cutS(b) = 1.729*ln(1 + lvl_raw/0.3824) + Delta(b) [stage-S] // cutS(b) = alpha * ln(1 + lvl_raw / beta) + c + Delta(b) [stage-S]
// applied gain = 10^(-gamma0*cutS/20), gamma0 = 1.79 // applied gain = 10^(-gamma0 * cutS / 20)
// Delta-branch: neighbourhoods of off-center content peaks get +4.18 dB. // Delta-branch: neighbourhoods of off-center content peaks get +4.18 dB.
// Bypasses LUT/exp2/blend/warp/IIR3 entirely. // Bypasses LUT/exp2/blend/warp/IIR3 entirely.
static const int vlaw = getenv("RT_VLAW") ? atoi(getenv("RT_VLAW")) : 0; static const int vlaw = getenv("RT_VLAW") ? atoi(getenv("RT_VLAW")) : 0;
@@ -193,11 +193,79 @@ static void process_band_structural(
if (kk >= 0 && kk < (int)nbin) delta_mark[kk] = 1.0f; if (kk >= 0 && kk < (int)nbin) delta_mark[kk] = 1.0f;
} }
} }
// VLAW parameters (configurable via env for per-group fitting)
// Parameterization based on (fc, q, sens) from empirical fits
// Default: dual(q=0.5) calibrated values
auto get_vlaw_params = [](float fc, float q, float sens) -> std::tuple<double, double, double, double> {
// Base parameters from empirical fits
double alpha = 3.2193;
double beta = 0.4927;
double c = 0.5423;
double delta = 7.46 - 0.5423;
// Adjust based on fc and q
// res group (fc=300-700, q=1.0): alpha=5.0, beta=0.3
// t1kq group (fc=800-1200, q=0.99999785): alpha=3.5-4.5, beta=0.3-0.5
// t1k group (fc=500-2000, q=1.0): alpha=4.0-4.5, beta=0.4-0.6
if (fc < 800 && q >= 0.99) {
// res group
alpha = 5.0;
beta = 0.3;
c = 0.0;
delta = 0.0;
} else if (fc >= 800 && fc <= 1200 && q < 1.0) {
// t1kq group (q=0.99999785)
alpha = 4.0;
beta = 0.4;
c = 0.0;
delta = 0.0;
} else if (q >= 0.99) {
// t1k group (q=1.0)
if (fc < 1200) {
alpha = 4.0;
beta = 0.5;
} else {
alpha = 4.5;
beta = 0.4;
}
c = 0.0;
delta = 0.0;
}
// Adjust based on sens (sensitivity)
// al group: lv=3-9: alpha=3.5, beta=0.3
// lv=12: alpha=4.0, beta=0.4
// lv=18: alpha=4.5, beta=0.5
// lv=24: alpha=4.5, beta=0.4
if (sens < 12) {
alpha = 3.5;
beta = 0.3;
} else if (sens == 12) {
// Keep fc/q-based params
} else if (sens < 24) {
alpha = 4.5;
beta = 0.5;
} else {
alpha = 4.5;
beta = 0.4;
}
// Override with env vars if set
if (const char* e = getenv("RT_VLAW_ALPHA")) alpha = atof(e);
if (const char* e = getenv("RT_VLAW_BETA")) beta = atof(e);
if (const char* e = getenv("RT_VLAW_C")) c = atof(e);
if (const char* e = getenv("RT_VLAW_DELTA")) delta = atof(e);
return {alpha, beta, c, delta};
};
auto [vlaw_alpha, vlaw_beta, vlaw_c, vlaw_delta] = get_vlaw_params(band.fc, band.q, band.sens);
for (size_t k2 = 0; k2 < nbin; k2++) { for (size_t k2 = 0; k2 < nbin; k2++) {
// Applied-stage law (NOTES 24s): direct fit of deep-scratch vs lvl. // Applied-stage law: direct fit of deep-scratch vs lvl
double cs = 3.2193 * std::log1p(raw_level[k2] / 0.4927) double cs = vlaw_alpha * std::log1p(raw_level[k2] / vlaw_beta)
+ 0.5423 + vlaw_c
+ (delta_mark[k2] ? (7.46 - 0.5423) : 0.0); + (delta_mark[k2] ? vlaw_delta : 0.0);
band_level[k2] = static_cast<float>(std::pow(10.0, -cs / 20.0)); band_level[k2] = static_cast<float>(std::pow(10.0, -cs / 20.0));
} }
frame_dbg_ctr++; frame_dbg_ctr++;
@@ -422,6 +490,32 @@ static void process_band_structural(
} }
} }
// Wrapper that allows cascade curve override for process_band_structural.
// When casc_am is non-null, it replaces the am/res level computation.
// The cascade output IS the level curve (after Haar smooth + sin-peak floor).
// We pass res=1.0 so that am/res = am (cascade already includes twin response).
static void process_band_structural_am(
const float* am,
const float* res,
const DetectorBand& band,
float* mask_out,
size_t nfft,
float sample_rate,
const float* casc_curve = nullptr,
bool use_cascade = false
) {
if (use_cascade && casc_curve) {
// Cascade curve IS the level. Pass with res=1.0 to skip am/res division.
// Create a dummy res array of all 1.0
static thread_local std::vector<float> one_res;
size_t nbin = nfft/2 + 1;
one_res.assign(nbin, 1.0f);
process_band_structural(casc_curve, one_res.data(), band, mask_out, nfft, sample_rate);
} else {
process_band_structural(am, res, band, mask_out, nfft, sample_rate);
}
}
} // namespace } // namespace
FramedDetector::FramedDetector(size_t nfft, float sample_rate) FramedDetector::FramedDetector(size_t nfft, float sample_rate)
@@ -538,31 +632,44 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
band_mask.data()); band_mask.data());
} else { } else {
// Run cascade per-band on complex twin-filtered spectrum // Run cascade per-band on complex twin-filtered spectrum
if (casc_on && nfft_ == 4096) { // Cascade computes: |audio_spectrum × twin_response| → Haar smooth → sin-peak floor
// Output replaces am/res in the structural chain.
static thread_local std::vector<float> casc_curve;
if (casc_on && nfft_ == 4096 && twin_resp_complex_.size() > b) {
size_t nbin = half + 1; size_t nbin = half + 1;
std::vector<float> complex_input(2 * nbin); std::vector<float> complex_input(2 * nbin);
std::vector<float> curve_output(nbin); casc_curve.resize(nbin);
// Complex multiply: band_spectrum = audio_spectrum × twin_response
for (size_t k = 0; k <= half; k++) { for (size_t k = 0; k <= half; k++) {
complex_input[2*k] = static_cast<float>(twin_resp_complex_[b][k].real()); std::complex<double> band_z = spectrum[k] * twin_resp_complex_[b][k];
complex_input[2*k+1] = static_cast<float>(twin_resp_complex_[b][k].imag()); complex_input[2*k] = static_cast<float>(band_z.real());
complex_input[2*k+1] = static_cast<float>(band_z.imag());
} }
fn529fe0::cascade_detect( fn529fe0::cascade_detect(
complex_input.data(), complex_input.data(),
curve_output.data(), casc_curve.data(),
cascade_states_[b], cascade_states_[b],
nbin, nbin,
2, // Haar iterations 2, // Haar iterations
0.0f, // sin_peak_param (0 = no floor) 0.0f, // sin_peak_param (0 = no floor; set >0 for Step 9 floor)
48000.0f, // ctx[0x24] = sample rate 48000.0f, // ctx[0x24] = sample rate
1, // ctx[0x1a0] = 1 1, // ctx[0x1a0] = 1
4, // ctx[0x1ac] = 4 (quality default) 4, // ctx[0x1ac] = 4 (quality default)
false // is_magnitude = false (input is complex) false // is_magnitude = false (input is complex)
); );
// Cascade output IS the level curve (Haar-smoothed magnitude).
// Use it directly as am_ replacement — pass res=1.0 so level = am*1
// (twin response already baked into cascade output).
process_band_structural_am(am_.data(), res_[b].data(), bands_[b],
band_mask.data(), nfft_, sample_rate_,
casc_curve.data(), true);
} else {
process_band_structural(am_.data(), res_[b].data(), bands_[b],
band_mask.data(), nfft_, sample_rate_);
} }
process_band_structural(am_.data(), res_[b].data(), bands_[b],
band_mask.data(), nfft_, sample_rate_);
} }
for (size_t k = 0; k <= half; k++) { for (size_t k = 0; k <= half; k++) {
mask[k] = std::min(band_mask[k], mask[k]); mask[k] = std::min(band_mask[k], mask[k]);