101 lines
4.1 KiB
C++
101 lines
4.1 KiB
C++
|
|
/*
|
||
|
|
* test_noise_suppression — the RNNoise backend behind ApmProcessor actually denoises.
|
||
|
|
*
|
||
|
|
* This is the behavior exit-criterion for the noise-suppression feature (docs/voice.md §10-11):
|
||
|
|
* a real DSP backend, not the old inert passthrough. ApmProcessor::create() returns the RNNoise
|
||
|
|
* processor when the core is built with VOICECAT_HAS_NS (the dev/release presets). We feed it
|
||
|
|
* mono 48 kHz white noise in 20 ms (960-sample) frames — exercising the internal 480-sample
|
||
|
|
* chunking — and assert the output noise floor collapses while values stay finite/in-range.
|
||
|
|
*
|
||
|
|
* Registered only under VOICECAT_USE_VCPKG_DEPS, where VOICECAT_HAS_NS is defined, so a large
|
||
|
|
* reduction is expected; a passthrough build would (correctly) fail this test.
|
||
|
|
*/
|
||
|
|
#include <cmath>
|
||
|
|
#include <cstdint>
|
||
|
|
#include <cstdio>
|
||
|
|
#include <vector>
|
||
|
|
|
||
|
|
#include "audio/apm_processor.h"
|
||
|
|
|
||
|
|
namespace vca = voicecat::audio;
|
||
|
|
|
||
|
|
static int g_failures = 0;
|
||
|
|
|
||
|
|
#define CHECK(cond) \
|
||
|
|
do { \
|
||
|
|
if (!(cond)) { \
|
||
|
|
std::printf("FAIL [%s:%d]: %s\n", __FILE__, __LINE__, #cond); \
|
||
|
|
++g_failures; \
|
||
|
|
} \
|
||
|
|
} while (0)
|
||
|
|
|
||
|
|
static constexpr int kFrameSamples = 960; // 20 ms @ 48 kHz (two RNNoise 480-sample frames)
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
auto ns = vca::ApmProcessor::create();
|
||
|
|
CHECK(ns != nullptr);
|
||
|
|
if (!ns) return 1;
|
||
|
|
|
||
|
|
// Deterministic white noise (xorshift) at ~int16/10 amplitude, processed frame by frame.
|
||
|
|
uint32_t rng = 0x12345678u;
|
||
|
|
auto next_noise = [&]() -> int16_t {
|
||
|
|
rng ^= rng << 13;
|
||
|
|
rng ^= rng >> 17;
|
||
|
|
rng ^= rng << 5;
|
||
|
|
// map to roughly [-3000, 3000]
|
||
|
|
return static_cast<int16_t>((static_cast<int32_t>(rng % 6001)) - 3000);
|
||
|
|
};
|
||
|
|
|
||
|
|
const int kFrames = 200;
|
||
|
|
const int kWarmup = 60; // let RNNoise's recurrent state settle before measuring
|
||
|
|
double in_sumsq = 0.0, out_sumsq = 0.0;
|
||
|
|
long measured = 0;
|
||
|
|
std::vector<int16_t> frame(kFrameSamples);
|
||
|
|
|
||
|
|
for (int f = 0; f < kFrames; ++f) {
|
||
|
|
double frame_in_sq = 0.0;
|
||
|
|
for (int i = 0; i < kFrameSamples; ++i) {
|
||
|
|
frame[i] = next_noise();
|
||
|
|
frame_in_sq += static_cast<double>(frame[i]) * frame[i];
|
||
|
|
}
|
||
|
|
bool gate = ns->process_capture(frame.data(), kFrameSamples, 48000);
|
||
|
|
CHECK(gate); // NS never gates — always passes the frame on
|
||
|
|
|
||
|
|
if (f >= kWarmup) {
|
||
|
|
in_sumsq += frame_in_sq;
|
||
|
|
for (int i = 0; i < kFrameSamples; ++i) {
|
||
|
|
// Output must stay finite and within int16 range (clamping correctness).
|
||
|
|
CHECK(frame[i] >= -32768 && frame[i] <= 32767);
|
||
|
|
out_sumsq += static_cast<double>(frame[i]) * frame[i];
|
||
|
|
}
|
||
|
|
measured += kFrameSamples;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
CHECK(measured > 0);
|
||
|
|
double in_rms = std::sqrt(in_sumsq / measured);
|
||
|
|
double out_rms = std::sqrt(out_sumsq / measured);
|
||
|
|
double reduction = (in_rms > 0.0) ? (1.0 - out_rms / in_rms) : 0.0;
|
||
|
|
std::printf("noise-only: in_rms=%.1f out_rms=%.1f reduction=%.1f%%\n", in_rms, out_rms,
|
||
|
|
100.0 * reduction);
|
||
|
|
|
||
|
|
// RNNoise drops pure noise by ~99%; require a large, unambiguous reduction so a passthrough
|
||
|
|
// (no real backend) is caught. The threshold is deliberately conservative vs. the ~99% seen.
|
||
|
|
CHECK(reduction > 0.80);
|
||
|
|
|
||
|
|
// A 48-kHz guard miss must pass audio through untouched (our clock is always 48 kHz, but the
|
||
|
|
// backstop matters): feed a non-48k sample-rate and confirm the buffer is unchanged.
|
||
|
|
std::vector<int16_t> probe(kFrameSamples);
|
||
|
|
for (int i = 0; i < kFrameSamples; ++i) probe[i] = next_noise();
|
||
|
|
std::vector<int16_t> probe_copy = probe;
|
||
|
|
ns->process_capture(probe.data(), kFrameSamples, 16000);
|
||
|
|
CHECK(probe == probe_copy);
|
||
|
|
|
||
|
|
if (g_failures == 0) {
|
||
|
|
std::printf("noise_suppression: OK\n");
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
std::printf("noise_suppression: %d failure(s)\n", g_failures);
|
||
|
|
return 1;
|
||
|
|
}
|