Skip to content

Using BlueALSA with HFP and HSP Devices

borine edited this page Oct 8, 2021 · 27 revisions

Hands-Free (HFP) and Headset (HSP) Profiles

Introduction

This article discusses some issues around using BlueALSA with the SCO profiles, the "Hands Free" profile (HFP) and the "Headset" profile (HSP).

Both HFP and HSP are designed for use with voice audio: they have low sample rates and deliberately prioritize (relatively) low latency over audio quality. HSP is the older, and simpler, of the two; HFP adds a lot of complexity in order to fully support telephony applications.

BlueALSA does not implement any telephony functions, so it supports only a subset of the HFP specification but has full support for HSP (except the button press requirement).

Before You Start

  1. Make absolutely sure that PulseAudio is not running - otherwise it will certainly ruin your day. Not only does it steal the bluetooth audio profile endpoints from BlueALSA, but it also steals the ALSA PCM devices, making it difficult, or impossible, for ALSA applications to access the devices directly.

  2. bluealsa-aplay is not duplex. It captures from bluetooth and plays back to a local speaker. Some other application is required to capture from local microphone and play back to bluetooth. See Using ALSA utilities for audio transfer below.

  3. Not all bluetooth adapters are capable of supporting SCO data links with Linux. Most work fine, some allow outgoing (playback) streams only, others do not work at all. There does not appear to be any on-line database of known working modules, and no certain pattern to which brands or types will work. BlueALSA implements a workaround that enables SCO in most Broadcom modules, but there is no known equivalent for other brands. See The SCO Routing Issue below.

HSP

HSP-HS Role

Support for the HSP-HS role was not added to Bluez 5 until release 5.55 (September 2020). So although BlueALSA has support for HSP-HS, it cannot be used with earlier releases of Bluez. With Bluez 5.55 and later, BlueALSA takes care of the audio connection setup when a client opens a PCM, so no other action is required by the client. Capture and Playback PCMs can be opened at the same time, so full duplex operation is permitted.

HSP-AG Role

BlueALSA has full support for audio connections in the HSP-AG role. A client merely needs to open a PCM on the device and the audio connection is created. Capture and Playback PCMs can be opened at the same time.

HFP

HFP-HF Role

In the HFP-HF role BlueALSA is playing the part of the hands-free unit in the HFP model, so the bluetooth devices connected are typically mobile 'phones. Most 'phones, perhaps all, require full support for the HFP specification. So although it may be (theoretically) possible to implement this using the BlueALSA D-Bus RFCOMM API, it would certainly not be trivial and is beyond the scope of this article. We recommend the use of oFono to handle the HFP-HF protocol messaging in combination with BlueALSA to handle the audio connection.

Start the bluealsa service with the option -p hfp-ofono (you can also enable the a2dp profile options too if required). It will detect a running instance of ofono and register with it to provide audio support. When a 'phone initiates an audio connection BlueALSA will present the connection to ALSA as two PCMs, for Playback and Capture.

HFP-AG Role

When BlueALSA is in the HFP-AG role it is playing the part of the 'phone within the HFP model, so the bluetooth devices it connects to are headsets or speakers.

We recommend using BlueALSA's in-built HFP support in this role, and not to use oFono. Although oFono does register the HFP-AG profile end-points with Bluez, and will allow a 'phone to obtain a service-level conection, it fails to perform the Codec negotiation that is a required part of an audio connection setup. It is impossible for BlueALSA to perform this negotiation as it has no access to the RFCOMM connection used by oFono. So the resulting PCMs are in fact unusable.

If anyone has actually succeeded in using oFono with BlueALSA as an HFP-AG please edit this page to provide hints to others on how to do it.

BlueALSA's internal HFP-AG implementation is incomplete, so soes not comply with the HFP specification, but it is sufficient to enable most HFP hands-free units and headphones to successfully transfer audio in both directions. There is no support for dialling or other call management functions though.

The Bluetooth Hands-Free Profile specification mandates that compliant devices must allow audio connections to be established during a call, but it is only optional for them to allow audio connections outside of a call. This choice has given rise to varied behaviours among different hands-free devices. For the purposes of this article, we distinguish three types of Hands-Free device:

  1. Full support for out-of-call audio connections

    These devices accept full duplex audio connections without any need for call setup commands. They can be used in much the same way as HSP devices above.

  2. Playback-only support for out-of-call audio connections

    These devices allow audio connections to be opened outside of a call, but do not enable their microphone until a call is established. With these a BlueALSA client can playback audio without any need for call management commands, but must begin a call session before it can capture audio from the device microphone (otherwise it will capture only silence).

  3. No support for out-of-call audio connections

    These devices do not permit audio connections until a call session has begun.

Unfortunately the specification does not define any way for the AG to enquire whether the HF device supports out-of-call audio connections, so it is not possible for BlueALSA to determine this. So BlueALSA implements enough of the specification to be able to set up an out-of-call audio connection (i.e. it works with devices of group 1 in the above classification). For HF devices in group 2 BlueALSA will playback audio to the device speaker, but will not enable the device microphone; and for group 3 neither the microphone nor the speaker will be enabled. BlueALSA does however provide a D-Bus RFCOMM API to clients so that they can implement their own support for devices in groups 2 and 3.

The bluez-alsa project also includes a simple utility called bluealsa-rfcomm to give access to that API from the command-line or scripts.

The D-Bus API is documented here: https://github.com/Arkq/bluez-alsa/blob/master/doc/bluealsa-api.txt

Possibly the simplest way to create an audio connection to a HF when an in-progress call is required is to simulate the message sequence that is used by a 'phone to transfer the audio of an in-progress call to a hands-free unit. The sequence is this:

  • inform the HF that we have a connection to our service provider:
+CIEV:1,1
  • inform the HF that we have a strong mobile signal:
+CIEV:5,5
  • inform the HF that a call is in progress:
+CIEV:2,1

We must also inform the HF device that the call is finished when we are no longer transfering audio with this sequence:

  • inform the HF that a call is terminated:
+CIEV:2,0
  • inform the HF that we have lost the mobile signal:
+CIEV:5,0
  • inform the HF that we have lost the connection our service provider:
+CIEV:1,0

We can use the blueasa-rfcomm utility to perform this procedure from the command line:

transfer call:

user@host:~ bluealsa-rfcomm /org/bluealsa/hci0/dev_01_23_45_67_89_AB 
01:23:45:67:89:AB> +CIEV:1,1
01:23:45:67:89:AB> +CIEV:5,5
01:23:45:67:89:AB> +CIEV:2,1
01:23:45:67:89:AB> 

terminate call:

user@host:~ bluealsa-rfcomm /org/bluealsa/hci0/dev_01_23_45_67_89_AB 
01:23:45:67:89:AB> +CIEV:2,0
01:23:45:67:89:AB> +CIEV:5,0
01:23:45:67:89:AB> +CIEV:1,0
01:23:45:67:89:AB> 

Alternatively, we can store the sequences as shell scripts; the following example scripts can be used as:

user@host:~ hfp-ag-start 01:23:45:67:89:AB
user@host:~ aplay -D bluealsa:01:23:45:67:89:AB,sco audio.wav
user@host:~ hfp-ag-stop 01:23:45:67:89:AB

/usr/local/bin/hfp-ag-start

#!/bin/sh
if test -z "$1" ; then
	exit 1
fi
device=`echo "$1" | tr '[a-z]' '[A-Z]'`
device=`echo "$device" | tr : _`
bluealsa-rfcomm "/org/bluealsa/hci0/dev_$device" <<EOF
+CIEV:1,1
+CIEV:5,5
+CIEV:2,1
EOF

/usr/local/bin/hfp-ag-stop

#!/bin/sh
if test -z "$1" ; then
        exit 1
fi
device=`echo "$1" | tr '[a-z]' '[A-Z]'`
device=`echo "$device" | tr : _`
bluealsa-rfcomm "/org/bluealsa/hci0/dev_$device" <<EOF
+CIEV:2,0
+CIEV:5,0
+CIEV:1,0
EOF

The SCO Routing Issue

The bluetooth module used by an adapter generally has a number of physical interfaces. The HCI (host-controller interface) is a virtual interface implemented on a physical serial interface (typically USB or UART, but other types can also be used), other common hardware interfaces include PCM, i2c, etc. The Bluetooth Core Specification allows manufacturers to route SCO traffic through any interface, so they do not have to use the HCI for this.

Bluez (and the Linux kernel bluetooth subsystem) can only communicate with a bluetooth module through the HCI. So for BlueALSA (and other Bluez based services) to handle SCO profiles the SCO data must be transferred over the HCI. Therefore BlueALSA cannot support HFP or HSP with adapters that use some other interface for SCO.

Broadcom adapters allow the routing of SCO packets to be selected by a custom vendor HCI command. By default they route SCO to a PCM interface, but an application can use that custom command to change the routing to the HCI. BlueALSA sends this command when used with a Broadcom module, so HFP/HSP hopefully be available with adapters based on these modules.

Other brands may implement similar custom commands, but none at present has been made public so it is not possible for BlueALSA to support them unless they already default to using the HCI for SCO.

Most adapters do pass SCO data over the HCI, so you would need to test yours to know if is affected by this issue. One way to test is to use the hciconfig utility included by many distributions. To do this, first run hciconfig and make a note of the RX and TX packets for sco:

user@host:~ hciconfig
hci0:	Type: Primary  Bus: USB
	BD Address: FE:DC:BA:09:87:65  ACL MTU: 1021:8  SCO MTU: 64:1
	UP RUNNING PSCAN ISCAN 
	RX bytes:2305 acl:0 sco:0 events:228 errors:0
	TX bytes:37729 acl:0 sco:0 commands:227 errors:0

Now connect a bluetooth device that supports HFP or HSP and play a short audio:

user@host:~ aplay -D bluealsa:01:23:45:67:89:AB,sco /usr/share/sounds/alsa/Front_Centre.wav

If the device plays the audio, then you know that outgoing SCO is working. Because SCO is a duplex link, there will also be SCO data received over the HCI, even if no application is capturing it (in that case the packets are discarded by the kernel driver). So now we run hciconfig again:

user@host:~ hciconfig
hci0:	Type: Primary  Bus: USB
	BD Address: FE:DC:BA:09:87:65  ACL MTU: 1021:8  SCO MTU: 64:1
	UP RUNNING PSCAN ISCAN 
	RX bytes:38449 acl:158 sco:601 events:356 errors:0
	TX bytes:67745 acl:139 sco:500 commands:259 errors:0

In the above example, the RX sco: packet count is sco:601, so we know that the adapter is suitable for use with SCO on Linux. If the RX bytes still reports sco:0 then your adapter does not route SCO over the HCI. If you did hear the sound from the device even though RX sco: is zero, that indicates that the adapter will process SCO packets sent from the host over the HCI but will not send the return packets by the same route. In that case it is possible to playback to a HFP/HSP speaker but not to capture from a HFP/HSP microphone. If the TX sco: packet count is zero, that indicates audio is not passing from your application to BlueALSA; there must be some issue other than SCO routing.

Appendix

Using ALSA utilities for audio transfer

arecord and aplay

In order to capture audio from a local microphone and transfer it to a bluetooth device, it is possible to pipe from arecord into aplay. In their default configurations this will lead to very high latency because of the way they configure their buffers and because of the time taken to establish a bluetooth SCO connection.

To avoid this latency you can set more appropriate period and buffer sizes from the command line; and you should delay the start of arecord slightly to permit aplay to complete the SCO setup before capturing begins. You should always explictly specify the audio format too. For example the following command can achieve latency of less than 100ms:

{ sleep 1; arecord -D plughw:0,0 -t raw -r 8000 -f s16_le -c 1 --period-time=10000 --buffer-time=30000; } | aplay -D bluealsa:DEV=01:23:45:67:89:AB,PROFILE=sco -t raw -f s16_le -c 1 -r 8000 --period-time=20000 --buffer-time=60000

alsaloop

alsaloop can transfer audio in either direction, or in both directions at the same time. It permits more direct control over the latency than using arecord and aplay, and also compensates for clock drift between the devices. However it can become unstable if the target latency cannot be achieved.

It is best to avoid resampling if possible, however if the local device does not support the SCO audio format (for example many budget cards do not support mono streams or 8000Hz sample rate) then avoid resampling the rate by the capture device as the ALSA rate plugin can cause problems here. So it is recommended to use the SCO format (s16_le) and channels (1) for both directions, and the SCO rate (8000) when capturing from BlueALSA but a rate directly supported by the local microphone (eg 48000) when capturing from local device.

For example, to capture from the local microphone with a target latency of 100 milliseconds:

alsaloop -C plughw:0,0 -P bluealsa:DEV=01:23:45:67:89:AB,PROFILE=sco -r 48000 -c 1 -f s16_le -t 100000 --sync=samplerate

To perform full duplex audio between local device and BlueALSA:

alsaloop -g /dev/stdin <<EOF
-C bluealsa:DEV=01:23:45:67:89:AB,PROFILE=sco -P plughw:0,0 -r 8000 -c 1 -f s16_le -t 100000 --sync=samplerate -T 1
-C plughw:0,0 -P bluealsa:DEV=01:23:45:67:89:AB,PROFILE=sco -r 48000 -c 1 -f s16_le -t 100000 --sync=samplerate -T 2
EOF

Hints on local microphone setup

Setting local microphone control levels with alsamixer or amixer is critical to avoid high noise levels and clipping. Unfortunately there does not appear to be any general, reliable, guidance on this. Here are some simple tips to get you started - they assume you are using the internal microphone on card 0 ("Internal Mic" on "hw:0"); you will need to adjust those names for other devices.

  • make sure "Capture" is enabled on your card: amixer -D hw:0 sset Capture cap
  • set the "Capture" control to 0 dB amixer -D hw:0 sset Capture 0dB
  • set the relevant "Mic Boost" to zero: amixer -D hw:0 sset "Internal Mic Boost" 0

If the sound level from the microphone is too low, steadily increase the "Capture" control level until the "hiss" becomes noticable. If the sound level is still too low, increase the microphone boost level, one step at a time, again stopping before the sound becomes distorted or the "hiss" becomes too loud.