Bluetooth audio in Linux: ALSA and LDAC
Audio using pure ALSA (no PulseAudio) and LDAC codec
I never use PulseAudio on my main rig, but it seems to be the de facto standard of connecting Bluetooth audio devices on desktop Linux. Luckily, there's the BlueALSA project, which is under active development as of late 2018, but is mature and stable enough for everyday use already. BlueALSA (also known as bluez-alsa) is needed because ever since Bluez (the Linux Bluetooth stack) version 5, Bluetooth audio is not supported anymore except via PulseAudio.
Preparing and compiling everything
Follow the instructions on the BlueALSA GitHub page to install it. I recommend using the latest master version from the repository and not from your distro's package management system, as pretty significant features have been added in the last few months and more is coming all the time.
There's also the instructions for compiling the LDAC library in the BlueALSA Wiki. I had to manually copy some header files to another directory as at least the autotools method didn't seem to find them all, but other than that everything seemed to work. I was not able to get it to work using custom prefix, though, so I recommend just installing everything to the standard location. You may also use similar commands to compile the apt-X codec for use with BlueALSA.
Setting up Bluetooth permissions
Check out the file /etc/dbus-1/system.d/bluetooth.conf. It seems to be usual to add there something along the following lines:
<!-- allow users of lp group (printing subsystem) to communicate with bluetoothd -->
<policy group="lp">
<allow send_destination="org.bluez"/>
</policy>
I just created a new group called "bluetooth" and added a policy for it. Remember to add users to the correct group also, whichever group you use.
Connecting the Bluetooth device
To find out your Bluetooth device MAC address and to pair it, either use a GUI tool or bluetoothctl from the command line:
$ bluetoothctl
[bluetooth]# power on
[bluetooth]# scan on
[bluetooth]# pair <device MAC>
[bluetooth]# trust <device MAC>
[bluetooth]# connect <device MAC>
What the trust does is that your computer automatically accepts all connection attempts initiated by the other device.
Setting up ALSA as the source
I use the following in my system-wide ALSA config (/etc/asound.conf). In my .bashrc I export the MAC address of the Bluetooth device I want to use.
# Bluetooth device.
# The control device is implicitly called ctl.bluealsa.
# Thus the control device defined here is for compatibility reasons only.
# Also notice the --a2dp-force-audio-cd parameter for bluealsa.
pcm.bluetooth {
type plug
slave.pcm {
type bluealsa
device { @func getenv
vars [ ALSA_BLUETOOTH_MAC ]
default "00:00:00:00:00:00"
}
profile "a2dp"
}
hint {
show on
description "Bluetooth audio device"
}
}
ctl.bluetooth {
type bluealsa
}
The device type in ALSA config is "bluealsa". A problem with this is it cannot operate as a slave for a dmix PCM device, i.e. you would only be able to playback one source at a time when using the Bluetooth device. Another problem is, and this might be because of bluealsa being a non-standard ALSA device, some applications simply hang up when you try to play anything into the device, at least unless LIBASOUND_THREAD_SAFE=0 is set. However, JACK audio connection kit does play nicely into a bluealsa device.
I figured out a way to overcome this obstacle by using an ALSA loopback device as the slave for a dmix device. Then you are able to connect the other end of the loopback device either to a JACK server or to aplay via pipe. For example, put the following in your ALSA config:
pcm.loop_playback_out {
type plug
slave.pcm "hw:Loopback,1,0"
hint {
show on
description "ALSA playback loop out"
}
}
# Mixing loopback device.
pcm.loop_playback_in_mix {
type plug
slave.pcm {
type dmix
ipc_key 10000
slave {
pcm "hw:Loopback,0,0"
format S32_LE
periods 4
period_size 128
rate 44100
}
}
hint {
show on
description "Loopback in mixer device, outputs to loop_playback_out"
}
}
Note the number and size of periods (4 times 128 equals 512) and the sample rate (CD quality). Now, if you start BlueALSA with the --a2dp-force-audio-cd
parameter (resulting in a sampling rate of 44100 Hz) and have the Bluetooth device connected, this will give you an ALSA device with mixing you can use to playback multiple sources:
$ arecord -f cd --buffer-size 512 -D loop_playback_out | env LIBASOUND_THREAD_SAFE=0 aplay --buffer-size 512 -D bluetooth -f cd &
Note the buffer size of 512 here again. If I tried using any smaller buffer size I got way too much buffer overruns. There is still quite a noticiable latency using the size of 512, but it's tolerable depending on use case. For music, it doesn't matter.
To make the loop_playback_in_mix device the default, use a similar technique as with the Bluetooth device MAC and read it from an environment variable:
pcm.!default {
@func refer
name { @func concat
strings [ "pcm."
{ @func getenv
vars [ ALSA_OVERRIDE_PCM ]
default "my_normal_default"
}
]
}
}
Now you may export, e.g. in .bashrc, the ALSA_OVERRIDE_PCM variable to point to the loopback device. You may also start applications with the modified environment: for example, to start Chromium using the loopback device as default: $ ALSA_OVERRIDE_PCM=loop_playback_in_mix chromium-browser &
Setting up ALSA as the sink
To use your computer as the playback device for a mobile or other Bluetooth device, start BlueALSA with the following flags: -p a2dp-source -p a2dp-sink
. This enables audio both ways.
Then just run bluealsa-aplay --profile-a2dp 00:00:00:00:00:00
. Using zeroes as the MAC will make BlueALSA to listen to all Bluetooth devices. I actually use a .desktop file to autostart the listener even if I don't have my Bluetooth dongle powered on by default. In the shortcut define: Exec=bash -c 'ps -e | grep bluealsa && bluealsa-aplay --profile-a2dp 00:00:00:00:00:00'
and it will just work.
Conclusion and further reading
It is possible to set up Bluetooth audio in Linux both ways (as input and output) without PulseAudio. Playback is also possible using the high-quality LDAC codec (I did not test BlueALSA as a sink with LDAC). Latency remains a bit of a problem, and to overcome the software mixing problem some preparations need to be done.
I have made extensive scripting to streamline the whole process into a single click on my Gentoo system. I use the following scripts and shortcut files to achieve an adequate user experience:
- bt_dev_connect.sh
- bt_audio_connect.sh
- bluealsa.sh
- bluealsa_dev_select.sh
- bluealsa.desktop
- bluealsa_dev_select.desktop
- bluetooth_audio_sink.desktop
Each of the files are available in my dotfiles repository at GitHub, but I've linked them above as they currently stand.