noise suppression added via nnnoiseless + vad + gain-normalizer + few frontend changes
This commit is contained in:
parent
a640e6caea
commit
eb0d40bae6
73 changed files with 1235 additions and 112 deletions
28
crates/jarvis-core/src/audio_processing/gain_normalizer.rs
Normal file
28
crates/jarvis-core/src/audio_processing/gain_normalizer.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
mod simple;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static NORMALIZER: OnceCell<Mutex<simple::GainNormalizer>> = OnceCell::new();
|
||||
|
||||
pub fn init() {
|
||||
if NORMALIZER.get().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
NORMALIZER.set(Mutex::new(simple::GainNormalizer::new())).ok();
|
||||
info!("Gain normalizer: enabled");
|
||||
}
|
||||
|
||||
pub fn normalize(input: &[i16]) -> Vec<i16> {
|
||||
match NORMALIZER.get() {
|
||||
Some(n) => n.lock().unwrap().normalize(input),
|
||||
None => input.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset() {
|
||||
if let Some(n) = NORMALIZER.get() {
|
||||
n.lock().unwrap().reset();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
use crate::config;
|
||||
|
||||
pub struct GainNormalizer {
|
||||
current_gain: f32,
|
||||
}
|
||||
|
||||
impl GainNormalizer {
|
||||
pub fn new() -> Self {
|
||||
Self { current_gain: 1.0 }
|
||||
}
|
||||
|
||||
pub fn normalize(&mut self, input: &[i16]) -> Vec<i16> {
|
||||
let rms = self.calculate_rms(input);
|
||||
|
||||
if rms < 1.0 {
|
||||
return input.to_vec();
|
||||
}
|
||||
|
||||
let target_gain = config::GAIN_TARGET_RMS / rms;
|
||||
let clamped_gain = target_gain.clamp(config::GAIN_MIN, config::GAIN_MAX);
|
||||
|
||||
self.current_gain = self.current_gain * 0.9 + clamped_gain * 0.1;
|
||||
|
||||
input.iter()
|
||||
.map(|&s| {
|
||||
let amplified = (s as f32) * self.current_gain;
|
||||
amplified.clamp(i16::MIN as f32, i16::MAX as f32) as i16
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.current_gain = 1.0;
|
||||
}
|
||||
|
||||
fn calculate_rms(&self, samples: &[i16]) -> f32 {
|
||||
if samples.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let sum: f64 = samples.iter()
|
||||
.map(|&s| (s as f64).powi(2))
|
||||
.sum();
|
||||
|
||||
(sum / samples.len() as f64).sqrt() as f32
|
||||
}
|
||||
}
|
||||
66
crates/jarvis-core/src/audio_processing/noise_suppression.rs
Normal file
66
crates/jarvis-core/src/audio_processing/noise_suppression.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
mod none;
|
||||
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
mod nnnoiseless;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::config::structs::NoiseSuppressionBackend;
|
||||
|
||||
static BACKEND: OnceCell<NoiseSuppressionBackend> = OnceCell::new();
|
||||
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
static NNNOISELESS_STATE: OnceCell<Mutex<nnnoiseless::NnnoiselessNS>> = OnceCell::new();
|
||||
|
||||
pub fn init(backend: NoiseSuppressionBackend) {
|
||||
if BACKEND.get().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
BACKEND.set(backend).ok();
|
||||
|
||||
match backend {
|
||||
NoiseSuppressionBackend::None => {
|
||||
info!("Noise suppression: disabled");
|
||||
}
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
NoiseSuppressionBackend::Nnnoiseless => {
|
||||
NNNOISELESS_STATE.set(Mutex::new(nnnoiseless::NnnoiselessNS::new())).ok();
|
||||
info!("Noise suppression: Nnnoiseless");
|
||||
}
|
||||
#[cfg(not(feature = "nnnoiseless"))]
|
||||
NoiseSuppressionBackend::Nnnoiseless => {
|
||||
warn!("Nnnoiseless not compiled in, falling back to None");
|
||||
BACKEND.set(NoiseSuppressionBackend::None).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(input: &[i16]) -> Vec<i16> {
|
||||
match BACKEND.get() {
|
||||
Some(NoiseSuppressionBackend::None) | None => none::process(input),
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
Some(NoiseSuppressionBackend::Nnnoiseless) => {
|
||||
if let Some(state) = NNNOISELESS_STATE.get() {
|
||||
state.lock().unwrap().process(input)
|
||||
} else {
|
||||
none::process(input)
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "nnnoiseless"))]
|
||||
Some(NoiseSuppressionBackend::Nnnoiseless) => none::process(input),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset() {
|
||||
match BACKEND.get() {
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
Some(NoiseSuppressionBackend::Nnnoiseless) => {
|
||||
if let Some(state) = NNNOISELESS_STATE.get() {
|
||||
state.lock().unwrap().reset();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
use nnnoiseless::DenoiseState;
|
||||
use crate::config;
|
||||
|
||||
pub struct NnnoiselessNS {
|
||||
state: Box<DenoiseState<'static>>,
|
||||
buffer: Vec<f32>,
|
||||
}
|
||||
|
||||
impl NnnoiselessNS {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: DenoiseState::new(),
|
||||
buffer: Vec::with_capacity(config::NNNOISELESS_FRAME_SIZE * 2),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(&mut self, input: &[i16]) -> Vec<i16> {
|
||||
for &sample in input {
|
||||
self.buffer.push(sample as f32);
|
||||
}
|
||||
|
||||
let mut output: Vec<i16> = Vec::with_capacity(input.len());
|
||||
|
||||
while self.buffer.len() >= config::NNNOISELESS_FRAME_SIZE {
|
||||
let mut input_frame = [0.0f32; 480];
|
||||
let mut output_frame = [0.0f32; 480];
|
||||
|
||||
input_frame.copy_from_slice(&self.buffer[..config::NNNOISELESS_FRAME_SIZE]);
|
||||
self.buffer.drain(..config::NNNOISELESS_FRAME_SIZE);
|
||||
|
||||
// process: input -> output (denoised)
|
||||
let _ = self.state.process_frame(&mut output_frame, &input_frame);
|
||||
|
||||
for &sample in &output_frame {
|
||||
let clamped = sample.clamp(i16::MIN as f32, i16::MAX as f32);
|
||||
output.push(clamped as i16);
|
||||
}
|
||||
}
|
||||
|
||||
if output.is_empty() {
|
||||
return input.to_vec();
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.state = DenoiseState::new();
|
||||
self.buffer.clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// return unprocessed input
|
||||
pub fn process(input: &[i16]) -> Vec<i16> {
|
||||
input.to_vec()
|
||||
}
|
||||
72
crates/jarvis-core/src/audio_processing/vad.rs
Normal file
72
crates/jarvis-core/src/audio_processing/vad.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
mod none;
|
||||
mod energy;
|
||||
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
mod nnnoiseless;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::config::structs::VadBackend;
|
||||
|
||||
static BACKEND: OnceCell<VadBackend> = OnceCell::new();
|
||||
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
static NNNOISELESS_STATE: OnceCell<Mutex<nnnoiseless::NnnoiselessVAD>> = OnceCell::new();
|
||||
|
||||
pub fn init(backend: VadBackend) {
|
||||
if BACKEND.get().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
BACKEND.set(backend).ok();
|
||||
|
||||
match backend {
|
||||
VadBackend::None => {
|
||||
info!("VAD: disabled");
|
||||
}
|
||||
VadBackend::Energy => {
|
||||
info!("VAD: Energy-based");
|
||||
}
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
VadBackend::Nnnoiseless => {
|
||||
NNNOISELESS_STATE.set(Mutex::new(nnnoiseless::NnnoiselessVAD::new())).ok();
|
||||
info!("VAD: Nnnoiseless");
|
||||
}
|
||||
#[cfg(not(feature = "nnnoiseless"))]
|
||||
VadBackend::Nnnoiseless => {
|
||||
warn!("Nnnoiseless not compiled in, falling back to Energy");
|
||||
BACKEND.set(VadBackend::Energy).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns (is_voice, confidence)
|
||||
pub fn detect(input: &[i16]) -> (bool, f32) {
|
||||
match BACKEND.get() {
|
||||
Some(VadBackend::None) | None => none::detect(input),
|
||||
Some(VadBackend::Energy) => energy::detect(input),
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
Some(VadBackend::Nnnoiseless) => {
|
||||
if let Some(state) = NNNOISELESS_STATE.get() {
|
||||
state.lock().unwrap().detect(input)
|
||||
} else {
|
||||
energy::detect(input)
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "nnnoiseless"))]
|
||||
Some(VadBackend::Nnnoiseless) => energy::detect(input),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset() {
|
||||
match BACKEND.get() {
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
Some(VadBackend::Nnnoiseless) => {
|
||||
if let Some(state) = NNNOISELESS_STATE.get() {
|
||||
state.lock().unwrap().reset();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
24
crates/jarvis-core/src/audio_processing/vad/energy.rs
Normal file
24
crates/jarvis-core/src/audio_processing/vad/energy.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use crate::config;
|
||||
|
||||
// Simple energy-based VAD
|
||||
pub fn detect(input: &[i16]) -> (bool, f32) {
|
||||
let rms = calculate_rms(input);
|
||||
let is_voice = rms > config::VAD_ENERGY_THRESHOLD;
|
||||
|
||||
// normalize confidence to 0-1 range (rough approximation)
|
||||
let confidence = (rms / (config::VAD_ENERGY_THRESHOLD * 2.0)).min(1.0);
|
||||
|
||||
(is_voice, confidence)
|
||||
}
|
||||
|
||||
fn calculate_rms(samples: &[i16]) -> f32 {
|
||||
if samples.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let sum: f64 = samples.iter()
|
||||
.map(|&s| (s as f64).powi(2))
|
||||
.sum();
|
||||
|
||||
(sum / samples.len() as f64).sqrt() as f32
|
||||
}
|
||||
51
crates/jarvis-core/src/audio_processing/vad/nnnoiseless.rs
Normal file
51
crates/jarvis-core/src/audio_processing/vad/nnnoiseless.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use nnnoiseless::DenoiseState;
|
||||
use crate::config;
|
||||
|
||||
pub struct NnnoiselessVAD {
|
||||
state: Box<DenoiseState<'static>>,
|
||||
buffer: Vec<f32>,
|
||||
}
|
||||
|
||||
impl NnnoiselessVAD {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: DenoiseState::new(),
|
||||
buffer: Vec::with_capacity(config::NNNOISELESS_FRAME_SIZE * 2),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect(&mut self, input: &[i16]) -> (bool, f32) {
|
||||
for &sample in input {
|
||||
self.buffer.push(sample as f32);
|
||||
}
|
||||
|
||||
let mut total_vad = 0.0f32;
|
||||
let mut frame_count = 0u32;
|
||||
|
||||
while self.buffer.len() >= config::NNNOISELESS_FRAME_SIZE {
|
||||
let mut input_frame = [0.0f32; 480];
|
||||
let mut output_frame = [0.0f32; 480];
|
||||
|
||||
input_frame.copy_from_slice(&self.buffer[..config::NNNOISELESS_FRAME_SIZE]);
|
||||
self.buffer.drain(..config::NNNOISELESS_FRAME_SIZE);
|
||||
|
||||
let vad_prob = self.state.process_frame(&mut output_frame, &input_frame);
|
||||
total_vad += vad_prob;
|
||||
frame_count += 1;
|
||||
}
|
||||
|
||||
if frame_count == 0 {
|
||||
return (true, 0.5);
|
||||
}
|
||||
|
||||
let avg_vad = total_vad / frame_count as f32;
|
||||
let is_voice = avg_vad >= config::VAD_NNNOISELESS_THRESHOLD;
|
||||
|
||||
(is_voice, avg_vad)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.state = DenoiseState::new();
|
||||
self.buffer.clear();
|
||||
}
|
||||
}
|
||||
4
crates/jarvis-core/src/audio_processing/vad/none.rs
Normal file
4
crates/jarvis-core/src/audio_processing/vad/none.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Always returns voice detected (no vad)
|
||||
pub fn detect(_input: &[i16]) -> (bool, f32) {
|
||||
(true, 1.0)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue