fix(audio): check capture ring available before read (fixes Opus6k choppy)
Partial reads from the capture ring consumed samples that were then discarded when the send loop retried from buf[0]. For 20ms codecs this was invisible (single Oboe burst fills 960 samples in one read), but 40ms codecs (Opus6k, 1920 samples) needed 2 bursts — the first partial read consumed 960 real samples and threw them away. Result: Opus6k produced ~11 frames/s instead of 25 (~44% of expected). Fix: expose wzp_native_audio_capture_available() and check it before reading, matching the desktop capture_ring.available() pattern. Partial reads no longer occur because we only read when enough samples exist. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
/// Read captured PCM samples from the capture ring. Returns the number
|
||||||
/// of `i16` samples actually copied into `out` (may be less than
|
/// of `i16` samples actually copied into `out` (may be less than
|
||||||
/// `out_len` if the ring is empty).
|
/// `out_len` if the ring is empty).
|
||||||
|
|||||||
@@ -570,16 +570,21 @@ impl CallEngine {
|
|||||||
if !send_r.load(Ordering::Relaxed) {
|
if !send_r.load(Ordering::Relaxed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// wzp-native doesn't expose `available()`, so we just try
|
// Check ring has enough samples before reading to avoid
|
||||||
// to read a full frame and sleep briefly if the ring is
|
// partial reads that consume samples and then get
|
||||||
// short. Oboe's capture callback fills at a steady rate
|
// overwritten on the next attempt (caused 40ms codecs
|
||||||
// so in steady state this spins once per frame.
|
// like Opus6k to produce ~11 frames/s instead of 25).
|
||||||
let read = crate::wzp_native::audio_read_capture(&mut buf[..frame_samples]);
|
if crate::wzp_native::audio_capture_available() < frame_samples {
|
||||||
if read < frame_samples {
|
|
||||||
short_reads += 1;
|
short_reads += 1;
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||||
continue;
|
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 {
|
if !first_full_read_logged {
|
||||||
info!(
|
info!(
|
||||||
t_ms = send_t0.elapsed().as_millis(),
|
t_ms = send_t0.elapsed().as_millis(),
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ static HELLO: OnceLock<unsafe extern "C" fn(*mut u8, usize) -> usize> = OnceLock
|
|||||||
static AUDIO_START: OnceLock<unsafe extern "C" fn() -> i32> = OnceLock::new();
|
static AUDIO_START: OnceLock<unsafe extern "C" fn() -> i32> = OnceLock::new();
|
||||||
static AUDIO_START_BT: OnceLock<unsafe extern "C" fn() -> i32> = OnceLock::new();
|
static AUDIO_START_BT: OnceLock<unsafe extern "C" fn() -> i32> = OnceLock::new();
|
||||||
static AUDIO_STOP: OnceLock<unsafe extern "C" fn()> = OnceLock::new();
|
static AUDIO_STOP: OnceLock<unsafe extern "C" fn()> = OnceLock::new();
|
||||||
|
static AUDIO_CAPTURE_AVAILABLE: OnceLock<extern "C" fn() -> usize> = OnceLock::new();
|
||||||
static AUDIO_READ_CAPTURE: OnceLock<unsafe extern "C" fn(*mut i16, usize) -> usize> = OnceLock::new();
|
static AUDIO_READ_CAPTURE: OnceLock<unsafe extern "C" fn(*mut i16, usize) -> usize> = OnceLock::new();
|
||||||
static AUDIO_WRITE_PLAYOUT: OnceLock<unsafe extern "C" fn(*const i16, usize) -> usize> = OnceLock::new();
|
static AUDIO_WRITE_PLAYOUT: OnceLock<unsafe extern "C" fn(*const i16, usize) -> usize> = OnceLock::new();
|
||||||
static AUDIO_IS_RUNNING: OnceLock<unsafe extern "C" fn() -> i32> = OnceLock::new();
|
static AUDIO_IS_RUNNING: OnceLock<unsafe extern "C" fn() -> 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, 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_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_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_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_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");
|
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.
|
/// Read captured i16 PCM into `out`. Returns bytes actually copied.
|
||||||
pub fn audio_read_capture(out: &mut [i16]) -> usize {
|
pub fn audio_read_capture(out: &mut [i16]) -> usize {
|
||||||
let Some(f) = AUDIO_READ_CAPTURE.get() else { return 0; };
|
let Some(f) = AUDIO_READ_CAPTURE.get() else { return 0; };
|
||||||
|
|||||||
Reference in New Issue
Block a user