Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 58 additions & 39 deletions crates/app/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ pub struct AudioEngine {
pub eq_mid_gain: Arc<AtomicU32>,
pub eq_high_gain: Arc<AtomicU32>,

pub eq_enabled: Arc<AtomicBool>,
pub agc_enabled: Arc<AtomicBool>,
pub _agc_target: Arc<AtomicU32>, // Kept for potential GUI control
pub bypass_enabled: Arc<AtomicBool>,
pub jitter_max_us: Arc<AtomicU32>,
pub jitter_ewma_us: Arc<AtomicU32>,
pub gate_threshold: Arc<AtomicU32>,
pub suppression_strength: Arc<AtomicU32>,
pub dynamic_threshold_enabled: Arc<AtomicBool>,
Expand Down Expand Up @@ -135,18 +136,20 @@ impl AudioEngine {

// Build reference capture stream if echo cancellation is enabled
let reference_stream: Option<cpal::Stream> = if let Some(ref_dev) = &reference_device {
let stream = ref_dev.build_input_stream(
match ref_dev.build_input_stream(
&config,
move |data: &[f32], _| {
let _ = prod_ref.push_slice(data);
},
|err| warn!("Reference input error: {}", err),
None,
).ok();
if stream.is_none() {
warn!("Failed to open reference device for echo cancellation");
) {
Ok(stream) => Some(stream),
Err(e) => {
warn!("Failed to open reference device for echo cancellation: {}", e);
None
}
}
stream
} else {
None
};
Expand Down Expand Up @@ -175,14 +178,11 @@ impl AudioEngine {
)?;

// Initialize Processor
// Always pass real EQ params; eq_enabled atomic controls whether EQ runs
let mut processor = VoidProcessor::new(
1, // Mono for App
vad_sensitivity,
if eq_enabled {
eq_params
} else {
(0.0, 0.0, 0.0)
},
eq_params,
agc_target_level,
echo_cancel_enabled,
);
Expand All @@ -197,6 +197,7 @@ impl AudioEngine {
processor
.dynamic_threshold_enabled
.store(dynamic_threshold_enabled, Ordering::Relaxed);
processor.eq_enabled.store(eq_enabled, Ordering::Relaxed);
processor.agc_enabled.store(agc_enabled, Ordering::Relaxed);
processor
.bypass_enabled
Expand All @@ -213,10 +214,11 @@ impl AudioEngine {
let eq_low_atomic = processor.eq_low_gain.clone();
let eq_mid_atomic = processor.eq_mid_gain.clone();
let eq_high_atomic = processor.eq_high_gain.clone();
let eq_enabled_atomic = processor.eq_enabled.clone();
let agc_enabled_atomic = processor.agc_enabled.clone();
let agc_target_atomic = processor.agc_target.clone();
let bypass_enabled_atomic = processor.bypass_enabled.clone();
let jitter_atomic = processor.jitter_max_us.clone();
let jitter_atomic = processor.jitter_ewma_us.clone();
let gate_threshold_atomic = processor.gate_threshold.clone();
let suppression_atomic = processor.suppression_strength.clone();
let dynamic_threshold_atomic = processor.dynamic_threshold_enabled.clone();
Expand All @@ -231,10 +233,10 @@ impl AudioEngine {
let mut output_frame = [0.0f32; FRAME_SIZE];
let mut ref_frame = [0.0f32; FRAME_SIZE];

// Jitter State
// Jitter State - EWMA for smoother, more responsive display
let mut last_loop_time = std::time::Instant::now();
let mut jitter_accum = 0;
let mut frames_since_jitter_reset = 0;
let mut jitter_ewma: f32 = 0.0;
let mut frames_since_jitter_report = 0u32;

loop {
if !run_flag.load(Ordering::Relaxed) {
Expand All @@ -245,29 +247,26 @@ impl AudioEngine {
processor.process_updates();

if cons_in.occupied_len() >= FRAME_SIZE {
// Jitter Calculation
// Jitter Calculation - skip obviously invalid deltas (e.g. system suspend)
let now = std::time::Instant::now();
let loop_delta = now.duration_since(last_loop_time).as_micros() as u32;
last_loop_time = now;

let expected = 10_000;
let jitter = if loop_delta > expected {
loop_delta - expected
} else {
expected - loop_delta
};
if loop_delta < 100_000 {
let expected = 10_000u32;
let jitter = loop_delta.abs_diff(expected) as f32;

if jitter > jitter_accum {
jitter_accum = jitter;
// EWMA: alpha=0.1 gives ~10-frame smoothing
jitter_ewma = jitter_ewma * 0.9 + jitter * 0.1;
}

frames_since_jitter_reset += 1;
if frames_since_jitter_reset >= 100 {
// Report to GUI every 50 frames (~500ms)
frames_since_jitter_report += 1;
if frames_since_jitter_report >= 50 {
processor
.jitter_max_us
.store(jitter_accum, Ordering::Relaxed);
jitter_accum = 0;
frames_since_jitter_reset = 0;
.jitter_ewma_us
.store(jitter_ewma as u32, Ordering::Relaxed);
frames_since_jitter_report = 0;
}

// Read Audio
Expand All @@ -291,11 +290,18 @@ impl AudioEngine {
processor.dynamic_threshold_enabled.load(Ordering::Relaxed),
);

// Write Audio
// Write Audio - retry briefly if output buffer is full
let mut retries = 0;
while prod_out.vacant_len() < FRAME_SIZE {
thread::sleep(Duration::from_micros(500));
thread::yield_now();
retries += 1;
if retries > 100 {
break;
}
}
if prod_out.vacant_len() >= FRAME_SIZE {
prod_out.push_slice(&output_frame);
}
prod_out.push_slice(&output_frame);
} else {
thread::sleep(Duration::from_micros(200));
}
Expand All @@ -320,14 +326,15 @@ impl AudioEngine {
eq_low_gain: eq_low_atomic,
eq_mid_gain: eq_mid_atomic,
eq_high_gain: eq_high_atomic,
eq_enabled: eq_enabled_atomic,
agc_enabled: agc_enabled_atomic,
_agc_target: agc_target_atomic,
bypass_enabled: bypass_enabled_atomic,
gate_threshold: gate_threshold_atomic,
suppression_strength: suppression_atomic,
dynamic_threshold_enabled: dynamic_threshold_atomic,
_spectrum_sender: spectrum_sender,
jitter_max_us: jitter_atomic,
jitter_ewma_us: jitter_atomic,
})
}
}
Expand All @@ -346,6 +353,7 @@ pub struct OutputFilterEngine {
_input_stream: cpal::Stream,
_output_stream: cpal::Stream,
is_running: Arc<AtomicBool>,
pub suppression_strength: Arc<AtomicU32>,
}

impl OutputFilterEngine {
Expand Down Expand Up @@ -417,6 +425,8 @@ impl OutputFilterEngine {

let is_running = Arc::new(AtomicBool::new(true));
let run_flag = is_running.clone();
let suppression_atomic = Arc::new(AtomicU32::new(suppression_strength.to_bits()));
let suppression_for_thread = suppression_atomic.clone();

thread::spawn(move || {
let mut denoise = DenoiseState::new();
Expand All @@ -430,16 +440,24 @@ impl OutputFilterEngine {
// Denoise with RNNoise
denoise.process_frame(&mut output_frame, &input_frame);

// Blend based on suppression strength
// Blend based on suppression strength (live-updated from GUI)
let strength = f32::from_bits(suppression_for_thread.load(Ordering::Relaxed));
for i in 0..FRAME_SIZE {
output_frame[i] = input_frame[i] * (1.0 - suppression_strength)
+ output_frame[i] * suppression_strength;
output_frame[i] = input_frame[i] * (1.0 - strength)
+ output_frame[i] * strength;
}

let mut retries = 0;
while prod_out.vacant_len() < FRAME_SIZE {
thread::sleep(Duration::from_micros(500));
thread::yield_now();
retries += 1;
if retries > 100 {
break;
}
}
if prod_out.vacant_len() >= FRAME_SIZE {
prod_out.push_slice(&output_frame);
}
prod_out.push_slice(&output_frame);
} else {
thread::sleep(Duration::from_micros(500));
}
Expand All @@ -453,6 +471,7 @@ impl OutputFilterEngine {
_input_stream: input_stream,
_output_stream: output_stream,
is_running,
suppression_strength: suppression_atomic,
})
}
}
Expand Down
Loading
Loading