diff --git a/crates/wzp-native/src/lib.rs b/crates/wzp-native/src/lib.rs index 8999380..89f2e1d 100644 --- a/crates/wzp-native/src/lib.rs +++ b/crates/wzp-native/src/lib.rs @@ -264,6 +264,12 @@ pub extern "C" fn wzp_native_audio_stop() { } } +/// Number of capture samples available to read without blocking. +#[unsafe(no_mangle)] +pub extern "C" fn wzp_native_audio_capture_available() -> usize { + backend().capture.available_read() +} + /// Read captured PCM samples from the capture ring. Returns the number /// of `i16` samples actually copied into `out` (may be less than /// `out_len` if the ring is empty). diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index c99c756..9d03269 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -570,16 +570,21 @@ impl CallEngine { if !send_r.load(Ordering::Relaxed) { break; } - // wzp-native doesn't expose `available()`, so we just try - // to read a full frame and sleep briefly if the ring is - // short. Oboe's capture callback fills at a steady rate - // so in steady state this spins once per frame. - let read = crate::wzp_native::audio_read_capture(&mut buf[..frame_samples]); - if read < frame_samples { + // Check ring has enough samples before reading to avoid + // partial reads that consume samples and then get + // overwritten on the next attempt (caused 40ms codecs + // like Opus6k to produce ~11 frames/s instead of 25). + if crate::wzp_native::audio_capture_available() < frame_samples { short_reads += 1; tokio::time::sleep(std::time::Duration::from_millis(5)).await; continue; } + let read = crate::wzp_native::audio_read_capture(&mut buf[..frame_samples]); + if read < frame_samples { + // Shouldn't happen after available() check, but guard anyway. + short_reads += 1; + continue; + } if !first_full_read_logged { info!( t_ms = send_t0.elapsed().as_millis(), diff --git a/desktop/src-tauri/src/wzp_native.rs b/desktop/src-tauri/src/wzp_native.rs index e96e39e..4088f33 100644 --- a/desktop/src-tauri/src/wzp_native.rs +++ b/desktop/src-tauri/src/wzp_native.rs @@ -28,6 +28,7 @@ static HELLO: OnceLock usize> = OnceLock static AUDIO_START: OnceLock i32> = OnceLock::new(); static AUDIO_START_BT: OnceLock i32> = OnceLock::new(); static AUDIO_STOP: OnceLock = OnceLock::new(); +static AUDIO_CAPTURE_AVAILABLE: OnceLock usize> = OnceLock::new(); static AUDIO_READ_CAPTURE: OnceLock usize> = OnceLock::new(); static AUDIO_WRITE_PLAYOUT: OnceLock usize> = OnceLock::new(); static AUDIO_IS_RUNNING: OnceLock i32> = OnceLock::new(); @@ -68,6 +69,7 @@ pub fn init() -> Result<(), String> { resolve!(AUDIO_START, unsafe extern "C" fn() -> i32, b"wzp_native_audio_start"); resolve!(AUDIO_START_BT, unsafe extern "C" fn() -> i32, b"wzp_native_audio_start_bt"); resolve!(AUDIO_STOP, unsafe extern "C" fn(), b"wzp_native_audio_stop"); + resolve!(AUDIO_CAPTURE_AVAILABLE, extern "C" fn() -> usize, b"wzp_native_audio_capture_available"); resolve!(AUDIO_READ_CAPTURE, unsafe extern "C" fn(*mut i16, usize) -> usize, b"wzp_native_audio_read_capture"); resolve!(AUDIO_WRITE_PLAYOUT, unsafe extern "C" fn(*const i16, usize) -> usize, b"wzp_native_audio_write_playout"); resolve!(AUDIO_IS_RUNNING, unsafe extern "C" fn() -> i32, b"wzp_native_audio_is_running"); @@ -121,6 +123,12 @@ pub fn audio_stop() { } } +/// Number of capture samples available to read without blocking. +pub fn audio_capture_available() -> usize { + let Some(f) = AUDIO_CAPTURE_AVAILABLE.get() else { return 0; }; + f() +} + /// Read captured i16 PCM into `out`. Returns bytes actually copied. pub fn audio_read_capture(out: &mut [i16]) -> usize { let Some(f) = AUDIO_READ_CAPTURE.get() else { return 0; };