fix(bluetooth): use Shared mode for Oboe + delay restart for BT route

Two fixes for BT audio silence:

1. Switch Oboe streams from Exclusive to Shared sharing mode. Exclusive
   mode bypasses Oboe's internal resampler, so opening a 48kHz stream
   against a BT SCO device (8/16kHz only) fails at the AudioPolicy
   level. Shared mode lets Oboe's resampler bridge the gap.

2. Add 500ms post-SCO delay before Oboe restart. The audio policy needs
   time to apply the bt-sco route after setCommunicationDevice returns.
   Without the delay, Oboe opens against the old device (handset).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-12 17:14:06 +04:00
parent fd0ccf8e99
commit 5dfb5b3581
2 changed files with 11 additions and 9 deletions

View File

@@ -254,16 +254,15 @@ int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) {
oboe::AudioStreamBuilder captureBuilder; oboe::AudioStreamBuilder captureBuilder;
captureBuilder.setDirection(oboe::Direction::Input) captureBuilder.setDirection(oboe::Direction::Input)
->setPerformanceMode(oboe::PerformanceMode::LowLatency) ->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) ->setFormat(oboe::AudioFormat::I16)
->setChannelCount(config->channel_count) ->setChannelCount(config->channel_count)
->setSampleRate(config->sample_rate) ->setSampleRate(config->sample_rate)
->setFramesPerDataCallback(config->frames_per_burst) ->setFramesPerDataCallback(config->frames_per_burst)
->setInputPreset(oboe::InputPreset::VoiceCommunication) ->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) ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Best)
->setDataCallback(&g_capture_cb); ->setDataCallback(&g_capture_cb);
@@ -319,13 +318,12 @@ int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) {
oboe::AudioStreamBuilder playoutBuilder; oboe::AudioStreamBuilder playoutBuilder;
playoutBuilder.setDirection(oboe::Direction::Output) playoutBuilder.setDirection(oboe::Direction::Output)
->setPerformanceMode(oboe::PerformanceMode::LowLatency) ->setPerformanceMode(oboe::PerformanceMode::LowLatency)
->setSharingMode(oboe::SharingMode::Exclusive) ->setSharingMode(oboe::SharingMode::Shared)
->setFormat(oboe::AudioFormat::I16) ->setFormat(oboe::AudioFormat::I16)
->setChannelCount(config->channel_count) ->setChannelCount(config->channel_count)
->setSampleRate(config->sample_rate) ->setSampleRate(config->sample_rate)
->setFramesPerDataCallback(config->frames_per_burst) ->setFramesPerDataCallback(config->frames_per_burst)
->setUsage(oboe::Usage::VoiceCommunication) ->setUsage(oboe::Usage::VoiceCommunication)
// Match capture: Oboe resamples 48kHz ↔ device rate (8/16kHz for BT SCO)
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Best) ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Best)
->setDataCallback(&g_playout_cb); ->setDataCallback(&g_playout_cb);

View File

@@ -794,7 +794,7 @@ async fn set_bluetooth_sco(on: bool) -> Result<(), String> {
// startBluetoothSco() is async — jumping straight to Oboe restart // startBluetoothSco() is async — jumping straight to Oboe restart
// would open streams against earpiece, not the BT device. // would open streams against earpiece, not the BT device.
let mut connected = false; let mut connected = false;
for i in 0..30 { for i in 0..50 {
tokio::time::sleep(std::time::Duration::from_millis(100)).await; tokio::time::sleep(std::time::Duration::from_millis(100)).await;
if android_audio::is_bluetooth_sco_on().unwrap_or(false) { if android_audio::is_bluetooth_sco_on().unwrap_or(false) {
tracing::info!(polls = i + 1, "set_bluetooth_sco: SCO connected"); 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 { 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 { } else {
android_audio::stop_bluetooth_sco()?; android_audio::stop_bluetooth_sco()?;
} }