fix(bluetooth): BT SCO mode skips 48kHz + VoiceCommunication on capture

Root cause: Oboe capture at 48kHz with InputPreset::VoiceCommunication
cannot open against a BT SCO device (only supports 8/16kHz). The stream
silently falls back to builtin mic, delivering zeros.

Fix: add bt_active flag to WzpOboeConfig. When set, capture skips
setSampleRate and setInputPreset, letting the system route to BT SCO
at its native rate. Oboe's SampleRateConversionQuality::Best resamples
to 48kHz for our ring buffers. Playout uses Usage::Media in BT mode.

New API: wzp_native_audio_start_bt() for BT mode, called from
set_bluetooth_sco(on=true). Normal audio_start() restores the
standard config when switching back to earpiece/speaker.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-12 17:23:19 +04:00
parent 5dfb5b3581
commit 137fe5f084
5 changed files with 65 additions and 12 deletions

View File

@@ -814,10 +814,18 @@ async fn set_bluetooth_sco(on: bool) -> Result<(), String> {
}
if wzp_native::is_loaded() && wzp_native::audio_is_running() {
tracing::info!(on, "set_bluetooth_sco: restarting Oboe for route change");
tokio::task::spawn_blocking(|| {
tokio::task::spawn_blocking(move || {
wzp_native::audio_stop();
wzp_native::audio_start()
.map_err(|code| format!("audio_start after BT toggle: code {code}"))
if on {
// BT mode: skip sample rate + input preset on capture
// so the system can route to the BT SCO device natively.
wzp_native::audio_start_bt()
.map_err(|code| format!("audio_start_bt after BT on: code {code}"))
} else {
// Normal mode: restore 48kHz + VoiceCommunication preset.
wzp_native::audio_start()
.map_err(|code| format!("audio_start after BT off: code {code}"))
}
})
.await
.map_err(|e| format!("spawn_blocking join: {e}"))??;

View File

@@ -26,6 +26,7 @@ static LIB: OnceLock<libloading::Library> = OnceLock::new();
static VERSION: OnceLock<unsafe extern "C" fn() -> i32> = OnceLock::new();
static HELLO: OnceLock<unsafe extern "C" fn(*mut u8, usize) -> usize> = 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_STOP: OnceLock<unsafe extern "C" fn()> = 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();
@@ -65,6 +66,7 @@ pub fn init() -> Result<(), String> {
resolve!(VERSION, unsafe extern "C" fn() -> i32, b"wzp_native_version");
resolve!(HELLO, unsafe extern "C" fn(*mut u8, usize) -> usize, b"wzp_native_hello");
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_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");
@@ -104,6 +106,14 @@ pub fn audio_start() -> Result<(), i32> {
if ret == 0 { Ok(()) } else { Err(ret) }
}
/// Start Oboe in Bluetooth SCO mode — capture skips sample rate and
/// input preset so the system routes to the BT SCO device natively.
pub fn audio_start_bt() -> Result<(), i32> {
let f = AUDIO_START_BT.get().ok_or(-100_i32)?;
let ret = unsafe { f() };
if ret == 0 { Ok(()) } else { Err(ret) }
}
/// Stop both streams. Safe to call even if not running.
pub fn audio_stop() {
if let Some(f) = AUDIO_STOP.get() {