Using BlueALSA with HFP and HSP Devices - arkq/bluez-alsa GitHub Wiki

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). It can be used in conjunction with oFono to provide full HFP functionality.

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, 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.

Gateway (AG), Headset (HS) and Handsfree (HF)

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.

HSP

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.

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.

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, 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.

HFP

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.

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.

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.

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.

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:

  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 and command-line utility to clients so that they can implement their own support for devices in groups 2 and 3.

BlueALSA RFCOMM Interface

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.

RFCOMM with HSP

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.

RFCOMM with HFP

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

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 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.

Using ALSA utilities for audio transfer on an HFP-HF or HSP-HS host

General hints

  1. 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.

  2. 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 and arecord. 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.

  3. Avoid use of ALSA dmix and dsnoop 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 ALSA hw PCM device, or if resampling is necessary use the ALSA plughw device. Note that for many cards, the default ALSA PCM will include dmix or dsnoop.

  4. 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 bluealsa-cli utility can help here. If starting the ALSA utilities from a script, we can wait for the audio stream to open with:

bluealsa-cli 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 bluealsa-cli 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.

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

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

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.

bluealsa-cli with aplay and arecord

It is also possible to use bluealsa-cli 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:

bluealsa-cli 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 | bluealsa-cli 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.

An example scripted solution for HFP-HF and HSP-HS

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

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
	bluealsa-cli --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
	bluealsa-cli 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]} | bluealsa-cli 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 <<<$(bluealsa-cli 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

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.

Use case: HFP over HCI on the TI WL1831MOD

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.

  1. Start pairing on remote device.
  2. Bring up the hci interface on the local device.
    1. (enable GPIO as needed)
    2. hciattach "/dev/ttyS[num]" texas 115200
    3. hciconfig hci0 up
    4. hciconfig hci0 name "BlueAlsaTest"
  3. Route audio from I2S pins to the HCI pins.
    1. hcitool cmd 0x3f 0x0210 0x01 0x78 0xff 0x01 0xff
  4. Enable ofonod (giving it time to come up gracefully) and BlueAlsa.
    1. ofonod && sleep 2
    2. bluealsa -S -i hci0 -p hfp-ofono
  5. Enable the bluetooth device.
    1. bluetoothctl power on
    2. bluetoothctl discoverable on
    3. bluetoothctl pairable on
  6. Use bluetoothctl to scan for a specified client
    1. bluetoothctl --timeout 30 scan on
  7. 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.

  1. bluetoothctl
  2. bluetooth> pair [remote BT mac]
  3. (Enter PIN when requested, on both devices)
  4. 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.

  1. Play audio from TI device to Jitsi
    1. aplay -D bluealsa:[local BT mac],sco [wav file]
    2. (listen on speakers configured to Jitsi session)
  2. Record audio from Jitsi to TI device
    1. arecord -D bluealsa:[local BT mac],sco [wav file]
    2. (talk on mic configured to Jitsi session)
    3. Ctrl-C to stop arecord
    4. Copy recorded wav to desktop and playback to verify audio.
⚠️ **GitHub.com Fallback** ⚠️