Skip to content

Bluetooth mic routing fix#6436

Draft
kikura wants to merge 12 commits intohome-assistant:mainfrom
kikura:bluetooth-mic-routing-fix
Draft

Bluetooth mic routing fix#6436
kikura wants to merge 12 commits intohome-assistant:mainfrom
kikura:bluetooth-mic-routing-fix

Conversation

@kikura
Copy link
Copy Markdown

@kikura kikura commented Feb 12, 2026

Description

Fixes an issue where the Bluetooth microphone was not being used when launching the voice assistant from a Bluetooth headset button. The app would fail to recognize voice commands unless the phone's microphone was within earshot.

Problem

When triggering the voice assistant from a Bluetooth headset (e.g., pressing the button on AirPods), the app would display "no text recognised" even though the Bluetooth headset was functioning correctly with other voice assistants like Google Gemini.

Solution

Enabled Bluetooth SCO (Synchronous Connection Oriented) audio routing by:

  1. Changed audio source to VOICE_COMMUNICATION when Bluetooth SCO is available
  2. Added startBluetoothSco() call in requestFocus() to enable SCO audio mode before recording
  3. Added stopBluetoothSco() call in abandonFocus() to properly clean up SCO connection
  4. Added API level checks to ensure compatibility with older Android versions

Testing

  • Launch the voice assistant by pressing the button on a Bluetooth headset
  • Give voice commands
  • Verify that the app now recognizes speech from the Bluetooth microphone

Files Changed

  • `common/src/main/kotlin/io/homeassistant/companion/android/common/util/AudioRecorder.kt

*Please not that the neccessary code has been generated by Co-Pilot, but has been tested and works.

…outing, replacing line 33 with dynamic audio source selection.
Updated AudioRecorder to dynamically select audio source based on connected Bluetooth devices, enhancing audio recording functionality.
Refactor audio source selection to prioritize VOICE_COMMUNICATION for Bluetooth audio. Enable and disable Bluetooth SCO as needed based on API level.
Copilot AI review requested due to automatic review settings February 12, 2026 14:42
Copy link
Copy Markdown

@home-assistant home-assistant Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @kikura

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant home-assistant Bot marked this pull request as draft February 12, 2026 14:42
@home-assistant
Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@kikura kikura marked this pull request as ready for review February 12, 2026 14:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to fix voice-assistant speech recognition when invoked from a Bluetooth headset button by routing input through Bluetooth SCO and selecting an appropriate AudioRecord source in the shared :common audio recording utility.

Changes:

  • Update AudioRecorder to select AudioSource.VOICE_COMMUNICATION when Bluetooth SCO is available
  • Start/stop Bluetooth SCO around audio focus acquisition/release
  • (Accidental) Adds a repository-root AudioRecorder.kt file containing non-compilable pseudo-code

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
common/src/main/kotlin/io/homeassistant/companion/android/common/util/AudioRecorder.kt Adds Bluetooth SCO routing and dynamic audio source selection for voice recording
AudioRecorder.kt New file at repo root that appears to be Copilot-generated pseudo-code and will break compilation

Comment on lines +54 to +71
/**
* Determine the appropriate audio source based on connected devices.
* Prefers Bluetooth SCO if available, falls back to VOICE_COMMUNICATION,
* and finally defaults to MIC if neither is available.
*/
private fun getAudioSource(): Int {
if (audioManager == null) {
return AudioSource.MIC
}

// Check if Bluetooth SCO is available
return if (audioManager.isBluetoothScoAvailableOffCall()) {
// Use VOICE_COMMUNICATION which is the standard for Bluetooth audio
AudioSource.VOICE_COMMUNICATION
} else {
AudioSource.MIC
}
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The KDoc for getAudioSource() doesn't match the actual behavior: the function returns VOICE_COMMUNICATION only when Bluetooth SCO is available, otherwise MIC (there is no separate “fallback to VOICE_COMMUNICATION” path). Please update the documentation (or adjust the logic) so it accurately reflects what the code does.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +144
// Enable Bluetooth SCO if available (requires API 11+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (audioManager.isBluetoothScoAvailableOffCall()) {
audioManager.startBluetoothSco()
}
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startBluetoothSco() is guarded by an API 11 check, but this project’s minSdk is 23, so the version guard is redundant. Also, consider tracking whether this instance actually started SCO so you can stop it deterministically (rather than relying on a global stop later).

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +176

// Disable Bluetooth SCO (requires API 11+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
audioManager.stopBluetoothSco()
}

Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

abandonFocus() calls stopBluetoothSco() unconditionally (only gated by the redundant API-level check), even if this AudioRecorder never started SCO. Since SCO is a global AudioManager state, this can interfere with other audio usage. Please gate the stop call on a local “SCO started” flag and/or the same condition used to start SCO.

Suggested change
// Disable Bluetooth SCO (requires API 11+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
audioManager.stopBluetoothSco()
}
// Disable Bluetooth SCO (requires API 11+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB &&
audioManager.isBluetoothScoAvailableOffCall()
) {
audioManager.stopBluetoothSco()
}

Copilot uses AI. Check for mistakes.
Comment thread AudioRecorder.kt Outdated
Comment on lines +2 to +19
import android.media.AudioManager;
import android.telecom.Call;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;

// Function to detect connected Bluetooth devices
private AudioDevice getAudioSource() {
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) {
for (BluetoothDevice device : bluetoothAdapter.getBondedDevices()) {
if (device.getType() == BluetoothDevice.DEVICE_TYPE_AUDIO) {
// return audio source associated with the Bluetooth device
return AudioDevice.BLUETOOTH;
}
}
}
// Fallback to default audio source if no Bluetooth devices are connected
return AudioDevice.DEFAULT;
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This AudioRecorder.kt at the repository root is Java-like pseudo-code (semicolon imports, non-Kotlin function syntax, unknown types like AudioDevice) and will not compile in this Kotlin/Gradle project. It looks like an accidentally committed Copilot-generated snippet—please remove it from the PR (or convert it into valid Kotlin in the correct module/package if it’s actually needed).

Suggested change
import android.media.AudioManager;
import android.telecom.Call;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
// Function to detect connected Bluetooth devices
private AudioDevice getAudioSource() {
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) {
for (BluetoothDevice device : bluetoothAdapter.getBondedDevices()) {
if (device.getType() == BluetoothDevice.DEVICE_TYPE_AUDIO) {
// return audio source associated with the Bluetooth device
return AudioDevice.BLUETOOTH;
}
}
}
// Fallback to default audio source if no Bluetooth devices are connected
return AudioDevice.DEFAULT;
import android.media.AudioManager
import android.telecom.Call
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
private enum class AudioDevice {
BLUETOOTH,
DEFAULT,
}
// Function to detect connected Bluetooth devices
private fun getAudioSource(): AudioDevice {
val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter?.isEnabled == true) {
val bondedDevices: Set<BluetoothDevice>? = bluetoothAdapter.bondedDevices
if (!bondedDevices.isNullOrEmpty()) {
// Return audio source associated with any bonded Bluetooth device
return AudioDevice.BLUETOOTH
}
}
// Fallback to default audio source if no Bluetooth devices are connected
return AudioDevice.DEFAULT

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +21
@@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import android.os.Build
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AudioDeviceInfo is imported but never used, and Build is imported after non-Android imports. This will fail ktlint/unused-import checks—please remove the unused import and keep imports grouped/ordered consistently.

Copilot uses AI. Check for mistakes.
Copilot AI and others added 3 commits February 12, 2026 16:59
Co-authored-by: kikura <37535682+kikura@users.noreply.github.com>
Fix Bluetooth SCO state management in AudioRecorder
Copy link
Copy Markdown
Member

@TimoPtr TimoPtr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for proposing this change, but I'm not sure it actually works. The API documentation is mentioning that when using startBluetoothSco that you should wait for a given state

As the SCO connection establishment can take several seconds, applications should not rely on the connection to be available when the method returns but instead register to receive the intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED.

And I don't see that in your code.

For the next steps you should do the modification yourself and test, and only use AI to help you on the issues you have.

}

// Check if Bluetooth SCO is available
return if (audioManager.isBluetoothScoAvailableOffCall()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the property instead of the function

Suggested change
return if (audioManager.isBluetoothScoAvailableOffCall()) {
return if (audioManager.isBluetoothScoAvailableOffCall) {

/**
* Wrapper around [AudioRecord] providing pre-configured audio recording functionality.
*/
class AudioRecorder(private val audioManager: AudioManager?) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like a small update of the documentation that explains the behavior of the recorder.

// Check if Bluetooth SCO is available
return if (audioManager.isBluetoothScoAvailableOffCall()) {
// Use VOICE_COMMUNICATION which is the standard for Bluetooth audio
AudioSource.VOICE_COMMUNICATION
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not use that all the time, it might be beneficial to other scenario than only Bluetooth.

Microphone audio source tuned for voice communications such as VoIP. It will for instance take advantage of echo cancellation or automatic gain control if available.


companion object {
// Docs: 'currently the only rate that is guaranteed to work on all devices'
const val SAMPLE_RATE = 44100
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the documentation

Even if a SCO connection is established, the following restrictions apply on audio output streams so that they can be routed to SCO headset:
the stream type must be STREAM_VOICE_CALL
the format must be mono
the sampling must be 16kHz or 8kHz
The following restrictions apply on input streams:
the format must be mono
the sampling must be 8kHz

It means we should not use 44100 when we are going to use Bluetooth but 8000 otherwise you might get weird behavior.


// Enable Bluetooth SCO if available
if (audioManager.isBluetoothScoAvailableOffCall()) {
audioManager.startBluetoothSco()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is deprecated and the documentation suggest to use setCommunicationDevice instead can you adjust and try again?

Comment thread AudioRecorder.kt Outdated
@@ -0,0 +1,20 @@
// Other existing imports
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this file.

@TimoPtr TimoPtr self-requested a review February 13, 2026 13:14
@home-assistant home-assistant Bot marked this pull request as draft February 13, 2026 13:14
Refactor audio recording logic to support dynamic sample rates based on Bluetooth SCO availability. Update methods to use currentSampleRate and improve Bluetooth handling.
…o make changes as this is beyond my capabilities.
@kikura kikura marked this pull request as ready for review February 15, 2026 09:55
@home-assistant home-assistant Bot requested a review from TimoPtr February 15, 2026 09:55
@TimoPtr TimoPtr marked this pull request as draft February 16, 2026 08:55
@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented Feb 16, 2026

Take the time to review my comments, but also the code generated before asking for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bluetooth headset microphone not used for Assistant voice input (audio input remains on phone mic)

4 participants