-
Notifications
You must be signed in to change notification settings - Fork 189
Using BlueALSA with HFP and HSP Devices
Important
This document refers to BlueALSA components by the names used in the latest sources. For release v4.3.1 or earlier please note that:
- The
bluealsad
daemon was calledbluealsa
- The
bluealsactl
utility was calledbluealsa-cli
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). It can be used in conjunction with oFono to provide full HFP functionality.
-
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. -
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. -
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, and there is also a workaround for some Texas Instruments' modules, but there is no known equivalent for other brands. See The SCO Routing Issue below.
Both HSP and HFP profiles distinguish two roles: a gateway role (HSP-AG, HFP-AG) and a headset/handsfree role (HSP-HS, HFP-HF). A profile connection consists of one device in each role. A typical device acting as AG is a mobile phone or personal computer; whereas a typical HS/HF device is a headset, speaker, in-car handsfree unit, etc.
BlueALSA can be used to support either role, or even both roles in the same device, for both profiles. The scope of BlueALSA is to provide access to the audio connection used by these profiles, other aspects such as telephone call management are out of scope.
Many devices implement both HSP and HFP. In general such devices will attempt to connect HFP first, and only use HSP as a fall-back when the HFP connection request fails. BlueALSA cannot support both HSP and HFP connections at the same time to a single device; that is in line with normal practice.
Connecting either HSP or HFP establishes a service level connection, which allows the two devices to exchange control messages, but does not immediately open the audio connection. So although BlueALSA creates the PCMs associated with the HFP or HSP service as soon as the device connects, it is not immediately possible to send or receive audio. It is the AG role that creates the audio connection. The HS device can only wait for that to happen. Both HFP and HSP allow the AG to create the audio connection at any time after the profile is connected; but the HS device cannot force an audio connection.
When acting as AG, BlueALSA opens the audio connection on demand when a client opens a PCM. When acting as HF/HS, BlueALSA will accept client PCM open requests, but the audio streams will not flow unless and until the remote AG device opens the audio connection. So BlueALSA clients in the HF/HS role may experience a significant delay after opening the PCM before audio starts, and this has particular consequences for ALSA clients. See Using ALSA utilities for audio transfer on an HFP-HF or HSP-HS host below for more information.
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.
Initially, the remote HS device will treat the incoming audio stream as an "in-band ring tone". So playing audio from the BlueALSA device to the headset device will be heard on the headset. Many headsets do not enable the microphone until the user presses a button, so with these devices a BlueALSA capture application will receive only silence until the button is pressed. If still no sound is captured on the BlueALSA device after pressing the headset button, it is possible that the headset is waiting for an acknowledgment from BlueALSA. BlueALSA does not acknowledge the HS button press message, so in these (rare) cases it may be necessary to use the BlueALSA RFCOMM interface. See BlueALSA RFCOMM Interface below for more information.
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, Capture and Playback PCMs can be opened at the same time, so full duplex operation is permitted.
The audio streams will begin once the AG device has created the audio connection (which may be some time after the profile connection was made). If a BlueALSA client opens a PCM during this time, it will be blocked until the audio connection is ready. BlueALSA does not wait for a user button press event, nor does it send the HSP button press message to the AG. It is possible to use the BlueALSA RFCOMM interface to implement your own button press functionality if required.
BlueALSA has limited built-in support for HFP-AG and HFP-HF roles. It can establish and release the service level connection, and it can perform audio connection setup (initiated by the AG) and audio connection release. It does not implement any of the call management features of HFP.
BlueALSA also has support for oFono, so that oFono handles all of the service level and call management functions, and BlueALSA then acts as the audio agent making the audio streams available to applications.
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.
BlueALSA's in-built HFP-HF support is limited to service level connection establishment, accepting audio connections, volume control and battery level indicators. So without any external application to handle the necessary RFCOMM interactions all calls must be initiated or accepted, rejected or terminated, by the 'phone. Once a call is established, then the 'phone is able to transfer the audio connection to BlueALSA. If the 'phone sends an in-band dialtone then that too will be received by BlueALSA.
It is possible to implement certain simple call functions, such as accept or reject incoming call, terminate an ongoing call, by using BlueALSA's RFCOMM API.
We recommend the use of oFono to handle the HFP-HF protocol messaging in combination with BlueALSA to handle the audio connection. In this case, all HFP control messaging is handled by oFono, including volume control. BlueALSA has no access to the RFCOMM connection. So it is necesssary to use oFono clients for all user interaction. BlueALSA has a passive role, making the HFP audio paths available to applications under the control of oFono.
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.
HFP, like HSP, requires that the audio connection is created by the AG device, and this causes similar issues for BlueALSA in the HFP-HF role as in the HSP-HS role. See Using ALSA utilities for audio transfer on an HFP-HF or HSP-HS host below for more information.
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.
BlueALSA has limited built-in HFP-AG support, and also supports the use of oFono in HFP-AG mode for full HFP-AG functionality.
oFono only registers the HFP-AG profile with Bluez if there is a locally installed GSM modem. On such a system the bluealsa service can be started with the option -p hfp-ofono
to provide Bluetooth audio support to oFono. As from BlueALSA release 4.1.0 both CVSD and mSBC codecs are supported with oFono.
We can also use BlueALSA's in-built HFP support in the HFP-AG role. BlueALSA's internal HFP-AG implementation is incomplete, so does 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:
-
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.
-
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).
-
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 and command-line utility to clients so that they can implement their own support for devices in groups 2 and 3.
BlueALSA includes an RFCOMM interface so that clients can implement their own support for additional HSP/HFP features (however, this interface is not available when using oFono).
The D-Bus API is documented in this manual page
The bluez-alsa project includes a simple utility called bluealsa-rfcomm
(see this manual page) to give access to that API from the command-line or scripts.
It is not usually necessary to use RFCOMM to achieve audio connections with HSP. However, if you are building a headset device and wish to implement a button press to accept audio from an AG, then the command to send when the button is pressed is:
AT+CKPD=200
The AG should respond with a simple OK
, but most commercial headset devices accept the audio connection without waiting for the OK
response.
Using the bluealsa-rfcomm
utility interactively, this would look like:
user@host:~ bluealsa-rfcomm /org/bluealsa/hci0/dev_01_23_45_67_89_AB
01:23:45:67:89:AB> AT+CKPD=200
> :OK
Similarly, if you are implementing a gateway device and wish to send a dial tone before starting the "real" audio stream, you can wait for the RFCOMM message:
user@host:~ bluealsa-rfcomm /org/bluealsa/hci0/dev_01_23_45_67_89_AB
> AT+CKPD=200
01:23:45:67:89:AB> OK
Send the OK
when the "real" audio stream has been started.
Possibly the simplest way to create an audio connection from a HFP-AG to a HFP-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
It is possible to send these sequences using an ALSA plugin, so that an application can simply open a BlueALSA ALSA PCM device to transfer audio, or we can use the BlueALSA D-Bus API. For an ALSA PCM solution, see for example HFP AG Helper Plugin for BlueALSA.
Alternatively we can use the bluealsa-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>
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 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 that are not used for HCI include i2s/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.
Texas Instruments also have a custom vendor command for selecting HCI as the SCO interface that is known to work on some of their module families; however it is not clear from their documentation whether this command works on all their BT modules. The command can be invoked from the command line using the bluez utility hcitool
thus:
hcitool cmd 0x3f 0x0210 0x01 0x00 0x00 0x00 0xFF
This command should be run be before any remote devices are connected, and must be re-run whenever the TI adapter is powered on. If we can gather evidence that this command works on all TI module families then it may be possible to have it invoked by BlueALSA in the same way as is done for Broadcom modules. The list of Texas Instruments module families with which this is known to work is:
- CC256x
- WL183x
If you have a different TI module which appears to suffer this SCO routing issue then you can help by testing the above command then updating the above list.
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.
-
Avoid resampling if possible. If resampling is necessary, try to do it only on the playback device (card for incoming audio, BlueALSA for outgoing audio) and use natively supported formats and rates for the capture device.
-
If the hardware device supports the Bluetooth audio parameters natively, then use those parameters for capture and playback. If it does not support the Bluetooth rate (8000Hz for CVSD and 16000Hz for mSBC), then try to choose a supported rate that is a multiple of the Bluetooth rate (e.g. most hardware devices will support a rate of 48000Hz)
To find the native parameters of your hardware devices you can use
aplay
andarecord
. Select the correct card and device number on that card by using the-D
option.For playback devices (speakers):
aplay -D hw:0,0 --dump-hw-params -t raw /dev/null
For capture devices (microphones):
arecord -D hw:0,0 --dump-hw-params | :
Ignore any warning or error messages at the start and end of the output, we are only interested in HW Params of the device. The output will be something like:
HW Params of device "hw:0,0": -------------------- ACCESS: MMAP_INTERLEAVED RW_INTERLEAVED FORMAT: S16_LE S32_LE SUBFORMAT: STD SAMPLE_BITS: [16 32] FRAME_BITS: [32 64] CHANNELS: 2 RATE: [44100 192000] PERIOD_TIME: (83 11888617) PERIOD_SIZE: [16 524288] PERIOD_BYTES: [128 2097152] PERIODS: [2 32] BUFFER_TIME: (166 23777234) BUFFER_SIZE: [32 1048576] BUFFER_BYTES: [128 4194304] TICK_TIME: ALL --------------------
So we see that this device has rates from 44100 to 192000. We want a multiple of 8000 or 16000, so the nearest available is 48000 and we choose that.
-
Avoid use of ALSA
dmix
anddsnoop
plugins if possible. See the bluealsa-aplay manual page and the wiki page ALSA devices with bluealsa-aplay for more information and work-arounds if these plugins cannot be avoided. For best results, use the ALSAhw
PCM device, or if resampling is necessary use the ALSAplughw
device. Note that for many cards, thedefault
ALSA PCM will includedmix
ordsnoop
. -
Do not start microphone capture until the BlueALSA playback device is ready. On AG nodes this means waiting for the codec to be set, and on HF/HS nodes this means waiting until the audio connection is open. BlueALSA creates the PCM when the service connection is established. The audio connection is opened by the AG device some time after the service connection is established, so there is a period after the PCM is created during which it is not capable of transfering audio. Depending on the configuration of your application this can lead to very high latency if captured samples are stored in a buffer waiting for the BlueALSA PCM to begin transferring (e.g. a Linux pipe can store over 4 seconds of 16-bit audio sampled at 8000Hz).
To avoid potential issues (XRUNs, excessively large delays) when used with HFP-HF or HSP-HS profile it is advisable to avoid starting audio applications until after it is known that the HFP/HSP audio connection is available.
From release 4.1.1 BlueALSA provides a PCM property, "Running" that indicates when the PCM is actively transferring audio to/from Bluetooth. This property can be used to determine when to start the audio applications. See An example scripted solution for HFP-HF and HSP-HS below.
A simpler strategy for avoiding large delays in audio on HS/HF nodes (which also works when when using BlueALSA 4.0.0 or earlier), is to listen for incoming audio to begin, and when that happens then start the outgoing audio application. Unfortunately none of the ALSA utilities can achieve this alone, but the bluealsactl
utility can help here. If starting the ALSA utilities from a script, we can wait for the audio stream to open with:
bluealsactl open SOURCE_PCM_PATH | dd bs=2 count=1 of=/dev/null
SOURCE_PCM_PATH
must be the D-Bus object path of the BlueALSA source PCM, as printed by bluealsactl list-pcms
. For example /org/bluealsa/hci0/dev_11_22_33_44_55_66/hsphs/source
The above command pipeline blocks until dd
has read one audio sample (or until the AG device disconnects). When it returns then start the incoming and outgoing audio applications. This method results in a loss of a tiny amount of incoming audio (typically less than 10 milliseconds). In most use cases this is not noticeable.
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
For incoming audio streams, arecord
will simply block until the stream starts, without any issues.
arecord -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 | aplay -D plughw:0,0 -t raw -f s16_le -c 1 -r 8000 --period-time=10000 --buffer-time=30000
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 uses the delay value reported by the ALSA devices to calculate the latency, so it is important that the BlueALSA PCM DELAY
parameter is set to 0 so as not to exaggerate the Bluetooth delay.
It is best to avoid resampling if possible; however if the local sound card device does not support the Bluetooth audio parameters (1 channel, S16_LE format, 16000 rate (mSBC) or 8000 rate (CVSD)), then it is necessary to use the alsa-lib plug
plugin to perform the necessary conversion(s). To enable rate or format conversion, use the '-n' option to alsaloop
. Care must be taken to avoid errors in the delay calculation by alsa-lib which may cause alsaloop
to crash or to consume 100% cpu. For this reason, always configure alsaloop
to use a natively supported rate and format of the playback device.
So, for example assuming the local sound card supports only 48000 sample rate and we wish to achieve a latency of 40 ms with mSBC:
when capturing from bluealsa
and playing to the local card (note that here the hw
device is used without plug
):
alsaloop -C bluealsa:PROFILE=sco -P hw:0 -r 48000 -c 2 -f s16_le -n -t 40000
when capturing from the local sound card and playing to bluealsa
(here we use plughw
):
alsaloop -C plughw:0 -P bluealsa:PROFILE=sco -r 16000 -c 1 -f s16_le -n -t 40000
To perform full duplex audio between local sound card device and bluealsa
:
alsaloop -g /dev/stdin <<EOF
-C bluealsa:PROFILE=sco -P hw:0 -r 48000 -c 2 -f s16_le -n -t 40000 -T 1
-C plughw:0 -P bluealsa:PROFILE=sco -r 16000 -c 1 -f s16_le -n -t 40000 -T 2
EOF
alsaloop
is not suitable for use on HFP-HF or HSP-HS devices unless it can be guaranteed that it will not be started before the audio connection is available. See the notes on this for aplay/arecord above, but note that alsaloop
will not wait for the incoming stream like arecord
does, so it is important alsaloop
must not be started for either direction until that PCM has started running. Similarly, the alsaloop
process must be killed when the audio connection is closed, otherwise it will consume 100% cpu.
It is also possible to use bluealsactl
to interface with the BlueALSA server, and then pipe the audio to aplay
or from arecord
. This approach has the advantage that the audio streams only pass once through alsalib
. For example:
bluealsactl open /org/bluealsa/hci0/dev_11_22_33_44_55_66/hsphs/source | aplay -D plughw:0,0 -t raw -r 8000 -c 1 -f s16_le
and
arecord -D plughw:0,0 -t raw -r 8000 -c1 -f s16_le | bluealsactl open /org/bluealsa/hci0/dev_11_22_33_44_55_66/hsphs/sink
Once again, neither direction should be started until the PCM has entered the "Running" state.
We present here a bash script to manage starting and stopping the ALSA applications when the BlueALSA PCMs enter or leave the "Running" state. This example requires BlueALSA release 4.1.0 or later. BlueALSA versions 4.0.0 and earlier do not have the necessary PCM "Running" property. The idea is to start this at boot (or when the Bluetooth subsystem starts) and then it will transfer audio between BlueALSA and the ALSA soundcard whenever a Bluealsa PCM is running. It is a very simple example that will not work if more than one Bluetooth device is connected at once.
The script can be used either with oFono, or with BlueALSA's built-in HFP-HF and HSP-HS support. It can be started by systemd
, but it is recommended not to run it as root
. It must be run by an account that is a member of the audio
group so that it can access BlueALSA and the soundcard.
There are no command-line options. To specify the chosen ALSA devices, set the environment variables BA_HF_SPEAKER
and BA_HF_MICROPHONE
. If these variables are not set, then the script uses the ALSA default
device. To enable only the speaker and disable the microphone, use BA_HF_MICROPHONE=none
.
It is also possible to set the target latency by setting the environment variable
BA_HF_LATENCY
to an integer value in milliseconds. Increasing the latency will reduce the likelihood of overruns and underruns in the ALSA devices, but will also increase the delay. The default latency is 100 milliseconds.
For example, assuming the script is saved as "bluealsa-audio-agent" in a directory in your PATH:
BA_HF_SPEAKER="plughw:0,0" BA_HF_MICROPHONE="plughw:0,0" BA_HF_LATENCY=60 bluealsa-audio-agent
#!/bin/bash
# Default ALSA speaker, microphone, and latency
# override by defining in environment
BA_HF_SPEAKER="${BA_HF_SPEAKER:-default}"
BA_HF_MICROPHONE="${BA_HF_MICROPHONE:-default}"
BA_HF_LATENCY="${BA_HF_LATENCY:-100}"
declare -A alsa audio device monitor sink source
# Determine correct name for `bluealsactl` utility
BLUEALSACTL=$(type -p bluealsactl 2>/dev/null) || BLUEALSACTL=$(type -p bluealsa-cli 2>/dev/null)
if [[ -z "$BLUEALSACTL" ]] ; then
echo "ERROR: Cannot find either `bluealsactl` or `bluealsa-cli`" >&2
exit 1
fi
reset_state() {
audio=()
sink[running]=
source[running]=
}
init_alsa_properties() {
alsa[spk]="$BA_HF_SPEAKER"
alsa[mic]="$BA_HF_MICROPHONE"
BA_HF_LATENCY=$(( (BA_HF_LATENCY + 10)/ 20 * 20 ))
(( BA_HF_LATENCY >= 20 )) || BA_HF_LATENCY=20
alsa[threshold]="$(( BA_HF_LATENCY * 1000 ))"
alsa[period]=$(( alsa[threshold] / 2 ))
alsa[buffer]=$(( alsa[period] * 4 ))
cat <<-EOF >&2
ALSA CONFIG:
period: ${alsa[period]}us, buffer: ${alsa[buffer]}us, latency: ${alsa[threshold]}us
Speaker: ${alsa[spk]}
Microphone: ${alsa[mic]}
EOF
}
set_codec() {
local codec="${1,,}"
case "$codec" in
cvsd)
audio[format]=s16_le
audio[channels]=1
audio[rate]=8000
device[codec]="$codec"
;;
msbc)
audio[format]=s16_le
audio[channels]=1
audio[rate]=16000
device[codec]="$codec"
;;
esac
}
start_monitor() {
local fd path=$(mktemp -u)
mkfifo $path
exec {fd}<>$path
rm $path
$BLUEALSACTL --quiet --verbose monitor --properties=Codec,Running >&$fd &
monitor[pid]=$!
monitor[fifo]=$fd
}
stop_monitor() {
if [[ "${monitor[pid]}" ]] ; then
kill ${monitor[pid]}
monitor[pid]=
fi
exec {monitor[fifo]}>&-
monitor[fifo]=
}
start_source() {
[[ "${device[source]}" ]] || return
$BLUEALSACTL open "${device[path]}source" > >(aplay -q -t raw -f "${audio[format]}" -c 1 -r "${audio[rate]}" -D "${alsa[spk]}" -F "${alsa[period]}" -B "${alsa[buffer]}" -R "${alsa[threshold]}") &
source[pid]=$!
}
stop_source() {
[[ "${source[pid]}" ]] || return
kill -TERM "${source[pid]}"
source[pid]=
}
start_sink() {
[[ "${device[sink]}" ]] || return
[[ "${alsa[mic],,}" = none ]] && return
arecord -q -t raw -f "${audio[format]}" -c 1 -r "${audio[rate]}" -F "${alsa[period]}" -B "${alsa[buffer]}" -R "${alsa[threshold]}" -D ${alsa[mic]} | $BLUEALSACTL open "${device[path]}sink" &
sink[pid]=$!
}
stop_sink() {
[[ "${sink[pid]}" ]] || return
kill -TERM "${sink[pid]}"
sink[pid]=
}
cleanup() {
stop_sink
stop_source
stop_monitor
kill -- -$$
}
# @param $1 device path
get_device_codec() {
local REPLY
while read -r ; do
case "$REPLY" in
"Selected codec:"*)
set_codec "${REPLY#*: }"
;;
esac
done <<<$($BLUEALSACTL info "$1")
}
trap 'trap "" EXIT INT TERM; cleanup 2>/dev/null; exit' EXIT INT TERM
init_alsa_properties
start_monitor
stream=
properties=
while read -u ${monitor[fifo]} ; do
case "$REPLY" in
ServiceStopped*)
[[ "${sink[running]}" ]] && stop_sink
[[ "${source[running]}" ]] && stop_source
reset_state
continue
;;
*": "*)
[[ "$stream" && "$properties" ]] || continue
property_name="${REPLY%%:*}"
property_value="${REPLY##*: }"
case "$property_name" in
"Selected codec")
set_codec "${property_value}"
;;
esac
continue
;;
"")
properties=
stream=""
continue
;;
esac
reply=( $REPLY )
path="${reply[1]}"
[[ "$path" == */hfphf/* || "$path" == */hsphs/* ]] || continue
stream="${path##*/}"
dev_path="${path%$stream}"
if [[ "${device[path]}" && "${device[path]}" != "$dev_path" ]] ; then
stream=""
continue
fi
case "${reply[0]}" in
PCMAdded)
device[path]="$dev_path"
device[$stream]=1
properties=1
;;
PCMRemoved)
stop_sink
stop_source
reset_state
device[path]=""
device[$stream]=""
stream=""
properties=""
;;
PropertyChanged)
property_name="${reply[2]}"
property_value="${reply[3]}"
if [[ -z "${device[path]}" ]] ; then
device[path]="$dev_path"
device["$stream"]=1
get_device_codec "$path"
fi
case "$property_name" in
Codec)
set_codec "${property_value}"
;;
Running)
declare -n pcm=$stream
if [[ "$property_value" = "false" ]] ; then
stop_$stream
pcm[running]=
else
pcm[running]=1
if [[ "${sink[running]}" && "${source[running]}" ]] ; then
start_sink
start_source
fi
fi
;;
esac
;;
esac
done
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.
The TI WL1831MOD is a WiFi/Bluetooth combo chip that is supported by stock BT drivers in the Linux kernel. The chip provides both HCI and I2S pins. The chip is connected to an Intel SoM and would require custom software support to use ALSA over the I2S pins. Use of the HCI pins can be handled via BlueAlsa. The HCI pins are connected to a UART, which means they show up in Linux as /dev/ttyS[num]. HFP with BlueAlsa will be supported with ofonod.
The TI WL1831MOD defaults to HFP over I2S. However, the audio can be routed to the HCI pins using a Vendor Specific command issued with the hcitool utility (see the section on The SCO Routing Issue). Sadly this command is not included in the TI WL1832MOD documentation, but can be found in a similar chip's documentation.
The following steps are taken to test HFP on this chip.
- Start pairing on remote device.
- Bring up the hci interface on the local device.
- (enable GPIO as needed)
- hciattach "/dev/ttyS[num]" texas 115200
- hciconfig hci0 up
- hciconfig hci0 name "BlueAlsaTest"
- Route audio from I2S pins to the HCI pins.
- hcitool cmd 0x3f 0x0210 0x01 0x78 0xff 0x01 0xff
- Enable ofonod (giving it time to come up gracefully) and BlueAlsa.
- ofonod && sleep 2
- bluealsa -S -i hci0 -p hfp-ofono
- Enable the bluetooth device.
- bluetoothctl power on
- bluetoothctl discoverable on
- bluetoothctl pairable on
- Use bluetoothctl to scan for a specified client
- bluetoothctl --timeout 30 scan on
- Configure BT agents for the chip
bluetoothctl << EOF <br>
trust [remote BT mac] <br>
agent KeyboardDisplay <br>
default-agent <br>
EOF
Launch bluetoothctl cli to pair and connect the local device to the remote device.
- bluetoothctl
- bluetooth> pair [remote BT mac]
- (Enter PIN when requested, on both devices)
- bluetooth> connect [remote BT mac]
Note: Routing audio to the HCI pins may be integrated into bluealsa at a later time which means that manual step will no longer be required.
At this point the TI WL1831MOD is connected to the remote device, such as a mobile phone. If you use the phone to dial into, for example, a Jitsi session you can then test the connection in both directions without requiring the local device to have speakers configured.
- Play audio from TI device to Jitsi
- aplay -D bluealsa:[local BT mac],sco [wav file]
- (listen on speakers configured to Jitsi session)
- Record audio from Jitsi to TI device
- arecord -D bluealsa:[local BT mac],sco [wav file]
- (talk on mic configured to Jitsi session)
- Ctrl-C to stop arecord
- Copy recorded wav to desktop and playback to verify audio.