Skip to content

Using BlueALSA as default ALSA PCM

Arkadiusz Bokowy edited this page Nov 11, 2024 · 7 revisions

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 called bluealsa
  • The bluealsactl utility was called bluealsa-cli, and it was necessary to build bluez-alsa with the configuration option --enable-cli.

Introduction

It is possible to use a BlueALSA device as the default for ALSA, but with certain limitations:

  • Bluetooth devices are not always connected, so it may be necessary to define a "fallback" to the local soundcard to ensure there is always a default.

  • A BlueALSA PCM does not support multiple client connections, and there is no equivalent to the "dmix" plugin for use with BlueALSA. So you need to be sure that only one process at a time has the default audio device open.

  • If you require that an active stream can continue to run uninterrupted by switching to the soundcard when a bluetooth device disconnects, then you will be disappointed. That is beyond the capabilities of ALSA alone, and you will have to resort to using an audio server such as PulseAudio or PipeWire.

  • Some applications just do not properly use the libasound API where a PCM device is unplugged. When an API call returns the error code ENODEV ("No such device"), or the PCM state is SND_PCM_STATE_DISCONNECTED, the only correct action for the application is to close its handle on the PCM. Some do not this, so cannot recover from disconnection of a bluetooth device. Even if the device subsequently re-connects, the old pcm handle is still invalid and cannot be used. There is nothing that BlueALSA or ALSA can do about this, it is a bug in the application; the only work-around is to quit then re-start the application.

  • Many applications assume that the volume control for the default PCM is called "Master". Therefore their internal volume controls will be ineffective with a bluetooth default device. The ALSA "remap" ctl plugin, introduced in alsa-lib version 1.2.5, which defines aliases for control elements can sometimes be used to overcome this limitation, see Using BlueALSA with other ALSA plugins for more information.

  • If the BlueALSA mixer is used as default (ctl.default) then it will be used as default for both Playback and Capture by all applications.

If your system can work within these limitations, and you can tolerate some manual intervention, then the ideas presented here may be helpful. We consider only A2DP playback devices here in order to keep the presentation simple; but the same principles can also be applied to capture devices and possibly SCO profile too.

This article relies on features that were introduced in bluez-alsa 3.1.0. The examples will not work with older releases.

Simple Example

The simplest possible case is just to override the definition of the default device. To have bluealsa as default for playback, but use the soundcard for capture, put the following into your ~/.asoundrc file:

pcm.!default {
	type asym
	capture.pcm cards.pcm.default
	playback.pcm bluealsa
	hint.description "Capture: sysdefault, Playback: BlueALSA Bluetooth
Default Audio Device"
}

ctl.!default {
	type bluealsa
	hint.description "Default control device (BlueALSA Bluetooth)"
}

The above will cause the BlueALSA mixer to be default for both playback and capture. It is not possible to define separate defaults for ctl devices. Omit the ctl.!default definition if you prefer.

Now this is fine until the bluetooth device disconnects. Then there will be no default playback device. So next we consider how to introduce a "fallback".

Defining a "Fallback" Default

The ALSA configuration has no concept of "fallback" defaults, it is left to each application to implement its own approach. The best we can do within libasound is to modify the configuration when a device becomes unavailable.

We can edit our ~/.asoundrc file every time the bluetooth device connects or disconnects, but even with some automation this can be unreliable - and if an ALSA configuration file is corrupted this can make all devices unusable.

So we try to avoid edits and instead use symbolic links to point to different files as needed. Rather than maintaining two complete versions of ~/.asoundrc, we separate out the bluealsa defaults and have a single ~/.asoundrc file that can be used in all cases.

The BlueALSA specific configuration can be stored in a file called ~/.asoundrc-bluealsa

bluealsa.default {
	pcm {
		name pcm.bluealsa
		description "Bluealsa Bluetooth"
	}
	ctl.type bluealsa
}

We put the following into our ~/.asoundrc

bluealsa.default.link {
	@func concat
	strings [
		{
			@func getenv
			vars [ HOME ]
			default "/tmp"
		}
		"/.asoundrc-default"
	]
}

pcm.!default {
	type asym
	capture.pcm cards.pcm.default
	playback.pcm {
		@func refer
		file {
			@func refer
			name bluealsa.default.link
		}
		name "bluealsa.default.pcm.name"
		default "cards.pcm.default"
	}
	hint.description {
		@func concat
		strings [
			"Capture: sysdefault, Playback: "
			{
				@func refer
				name "bluealsa.default.pcm.description"
				default "sysdefault"
			}
			"
Default Audio Device"
		]
	}
}

ctl.!default {
	@func refer
	file {
		@func refer
		name bluealsa.default.link
	}
	name "bluealsa.default.ctl"
	default {
		@func refer
		name ctl.sysdefault
	}
	hint.description "Default control device"
}

Again, omit the ctl!.default definiton if you do not want the BlueALSA mixer to become default for both playback and capture.

Now the definition of pcm.default depends on the contents of a file called ~/.asoundrc-default.

If we create a symbolic link of this name pointing to ~/.asoundrc-bluealsa then the default device will be bluealsa. If we point the symbolic link at /dev/null then the default device reverts to the system default.

If the symbolic link does not exist, libasound will emit a warning when it reads its configuration, but will still correctly load the configuration and use the system default.

Application Considerations

Applications using libasound normally read in the ALSA configuration when the first device is opened, and then refer to that cached configuration if they later open or re-open devices. So once an application has started, it will not see later changes to the definition of pcm.default. Using the above configuration alone it will be necessary to stop and restart every audio application when a bluetooth device connects or disconnects. We can improve on this for some applications by using an environment variable that changes libasound's default behaviour.

Unfortunately, many applications do not handle PCM device disconnection properly at all, and so for these their audio output is broken if playing to a BlueALSA device which disconnects. For such applications there is no workaround. The default fallback mechanism described here will not work because it requires the application to close its handle on the disconnected device and then open a new handle on default to get the fallback device instead.

When an application opens a device, libasound will update its configuration by checking the timestamp on its core configuration file(s). These files are set in an environment variable ALSA_CONFIG_PATH. If that variable is not set, libasound uses a built-in default, usually /usr/share/alsa/alsa.conf. We can include our bluetooth defaults in the core configuration by including the symbolic link file above in the ALSA_CONFIG_PATH. We need to make sure that the ALSA core configuration is also included. So we export in our environment:

ALSA_CONFIG_PATH=/usr/share/alsa/alsa.conf:$HOME/.asoundrc-default

This variable must be defined in the environment of every application that uses libasound. For command-line applications it can be set in the users ~/.bashrc (or equivalent for other shells). For GUI desktop launchers, consult the documentation for your system.

Now every time an application opens the default device, libasound will update its definition of pcm.default and the application will hopefully be given the correct device. It is important that the application closes then re-opens the default device for this to work - as noted above not all applications do this.

Automation

The above is as far as we can go using ALSA components alone. To have the default device updated automatically requires some external program to modify our symbolic link.

The BlueALSA service emits D-Bus PCMAdded and PCMRemoved signals which can be used to provide some automation here.

As a very simple example we can use a script that uses the bluealsactl utility to listen for the bluealsa signals and then manage the symbolic link according to whether any A2DP sinks are connected.

#!/bin/bash

# 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

# create a temporary named pipe to communicate with bluealsactl monitor
FIFO_PATH=$(mktemp -u)
mkfifo $FIFO_PATH
# attach it to unused file descriptor FIFO_FD
exec {FIFO_FD}<>$FIFO_PATH
# unlink the named pipe
rm $FIFO_PATH

# make sure the pipeline is shut down if this script interrupted
trap "kill %1; exec {FIFO_FD}>&-; ln -sf /dev/null ~/.asoundrc-default; exit 0" INT TERM

# start bluealsactl monitor in background
"$BLUEALSACTL" --quiet monitor >&$FIFO_FD &

until false; do
	if [[ $("$BLUEALSACTL" --quiet list-pcms | grep -Ec '/a2dp(src|sink)/sink$') -gt 0 ]] ; then
		ln -sf ~/.asoundrc-bluealsa ~/.asoundrc-default
	else
		ln -sf /dev/null ~/.asoundrc-default
	fi
	read
done <&$FIFO_FD

This script should be run in the background (with the user's account, not as root) when the user logs in so that the symbolic link is always kept updated and ready for use.