diff --git a/crates/wzp-native/cpp/oboe_bridge.cpp b/crates/wzp-native/cpp/oboe_bridge.cpp index 5d4394c..29b9e90 100644 --- a/crates/wzp-native/cpp/oboe_bridge.cpp +++ b/crates/wzp-native/cpp/oboe_bridge.cpp @@ -254,16 +254,15 @@ int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) { oboe::AudioStreamBuilder captureBuilder; captureBuilder.setDirection(oboe::Direction::Input) ->setPerformanceMode(oboe::PerformanceMode::LowLatency) - ->setSharingMode(oboe::SharingMode::Exclusive) + // Shared mode allows Oboe's internal resampler to bridge 48kHz to + // the hardware rate (8/16kHz for BT SCO). Exclusive mode bypasses + // the resampler and fails with "getInputProfile could not find profile". + ->setSharingMode(oboe::SharingMode::Shared) ->setFormat(oboe::AudioFormat::I16) ->setChannelCount(config->channel_count) ->setSampleRate(config->sample_rate) ->setFramesPerDataCallback(config->frames_per_burst) ->setInputPreset(oboe::InputPreset::VoiceCommunication) - // Bluetooth SCO only supports 8/16kHz. Without resampling, opening - // a 48kHz capture stream against a BT device fails with - // "getInputProfile could not find profile". Oboe resamples internally - // so our ring buffers stay at 48kHz regardless of the hardware rate. ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Best) ->setDataCallback(&g_capture_cb); @@ -319,13 +318,12 @@ int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) { oboe::AudioStreamBuilder playoutBuilder; playoutBuilder.setDirection(oboe::Direction::Output) ->setPerformanceMode(oboe::PerformanceMode::LowLatency) - ->setSharingMode(oboe::SharingMode::Exclusive) + ->setSharingMode(oboe::SharingMode::Shared) ->setFormat(oboe::AudioFormat::I16) ->setChannelCount(config->channel_count) ->setSampleRate(config->sample_rate) ->setFramesPerDataCallback(config->frames_per_burst) ->setUsage(oboe::Usage::VoiceCommunication) - // Match capture: Oboe resamples 48kHz ↔ device rate (8/16kHz for BT SCO) ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Best) ->setDataCallback(&g_playout_cb); diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 4ed8a7c..0a2c685 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -794,7 +794,7 @@ async fn set_bluetooth_sco(on: bool) -> Result<(), String> { // startBluetoothSco() is async — jumping straight to Oboe restart // would open streams against earpiece, not the BT device. let mut connected = false; - for i in 0..30 { + for i in 0..50 { tokio::time::sleep(std::time::Duration::from_millis(100)).await; if android_audio::is_bluetooth_sco_on().unwrap_or(false) { tracing::info!(polls = i + 1, "set_bluetooth_sco: SCO connected"); @@ -803,8 +803,12 @@ async fn set_bluetooth_sco(on: bool) -> Result<(), String> { } } if !connected { - tracing::warn!("set_bluetooth_sco: SCO did not connect within 3s, proceeding anyway"); + tracing::warn!("set_bluetooth_sco: SCO did not connect within 5s, proceeding anyway"); } + // Extra delay: even after getCommunicationDevice reports BT, + // the audio policy needs ~500ms to apply the bt-sco route. + // Without this, Oboe opens against the old device. + tokio::time::sleep(std::time::Duration::from_millis(500)).await; } else { android_audio::stop_bluetooth_sco()?; }