#include "audio/apm_processor.h" #include #include #include #include #ifdef VOICECAT_HAS_NS #include "rnnoise.h" #endif namespace voicecat::audio { namespace { int64_t steady_now_ms() { return std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()) .count(); } } // namespace // ── ApmPassthrough ──────────────────────────────────────────────────────────── // No-op: returns true (VAD always open), does not modify PCM. // Replaced by WebrtcApmProcessor when VOICECAT_HAS_APM is defined. class ApmPassthrough final : public ApmProcessor { public: void process_render(const int16_t*, int, int) override {} bool process_capture(int16_t*, int, int) override { return true; } }; // ── EnergyVadProcessor ────────────────────────────────────────────────────── // Lightweight, dependency-free energy/RMS VAD — see apm_processor.h's create_vad() doc comment // for why this exists instead of a real APM. No AEC (process_render is a no-op); doesn't modify // the PCM it's given, only inspects it. class EnergyVadProcessor final : public ApmProcessor { public: EnergyVadProcessor(float rms_threshold, int64_t hang_time_ms) : threshold_(rms_threshold), hang_time_ms_(hang_time_ms) {} void process_render(const int16_t*, int, int) override {} bool process_capture(int16_t* pcm, int samples, int /*sample_rate*/) override { if (samples > 0) { double sum_sq = 0.0; for (int i = 0; i < samples; ++i) { double s = static_cast(pcm[i]) / 32768.0; sum_sq += s * s; } double rms = std::sqrt(sum_sq / samples); if (rms >= threshold_.load(std::memory_order_relaxed)) last_voice_ms_ = steady_now_ms(); } return (steady_now_ms() - last_voice_ms_) < hang_time_ms_; } void set_threshold(float t) override { threshold_.store(t, std::memory_order_relaxed); } private: std::atomic threshold_; int64_t hang_time_ms_; int64_t last_voice_ms_ = 0; // epoch start -> gate begins closed until first loud frame }; #ifdef VOICECAT_HAS_NS // ── RnnoiseProcessor ───────────────────────────────────────────────────────── // Real noise suppression via vendored RNNoise (third_party/rnnoise; docs/voice.md §10-11). // RNNoise is a mono, 48 kHz, fixed 480-sample (10 ms) speech denoiser; our engine clock is fixed // at 48 kHz and every Opus frame size (480/960/1920/2880) is a multiple of 480, so we process // whole 480-sample chunks with no resampling and no cross-call carry. Mono only — callers gate // on a single channel (a stereo screen-audio share is never voice and isn't denoised). // // RT-safety (docs/architecture.md §3): the DenoiseState and the float scratch are allocated in the // ctor; process_capture() does no allocation/locking. The state is owned per-stream (recv) or // per-mic (send) so it persists across calls, which is exactly what RNNoise's overlap needs. class RnnoiseProcessor final : public ApmProcessor { public: RnnoiseProcessor() : st_(rnnoise_create(nullptr)) {} ~RnnoiseProcessor() override { if (st_) rnnoise_destroy(st_); } void process_render(const int16_t*, int, int) override {} // NS needs no AEC reference bool process_capture(int16_t* pcm, int samples, int sample_rate) override { // RNNoise is 48 kHz only; anything else passes through untouched (our clock is 48 kHz, so // this guard never trips in practice — it's just a correctness backstop). if (!st_ || sample_rate != 48000) return true; for (int off = 0; off + kFrame <= samples; off += kFrame) { for (int i = 0; i < kFrame; ++i) in_[i] = static_cast(pcm[off + i]); rnnoise_process_frame(st_, out_, in_); for (int i = 0; i < kFrame; ++i) { int32_t v = static_cast(std::lround(out_[i])); pcm[off + i] = static_cast(std::clamp(v, -32768, 32767)); } } return true; // NS doesn't gate; the send path's VAD stays a separate stage } private: static constexpr int kFrame = 480; // rnnoise_get_frame_size() DenoiseState* st_; float in_[kFrame]; float out_[kFrame]; }; #endif // VOICECAT_HAS_NS std::unique_ptr ApmProcessor::create() { #ifdef VOICECAT_HAS_NS return std::make_unique(); #else return std::make_unique(); #endif } std::unique_ptr ApmProcessor::create_vad(float rms_threshold, int64_t hang_time_ms) { return std::make_unique(rms_threshold, hang_time_ms); } } // namespace voicecat::audio