Skip to content

Using BlueALSA with dmix

Arkadiusz Bokowy edited this page Nov 11, 2024 · 8 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

dmix: Using a BlueALSA playback device with multiple applications

Important note: this solution requires features of BlueALSA introduced in release 3.0.0. It is not suitable for earlier releases (release 2.1.0 or earlier); those versions are not compatible with alsaloop.

Introduction

A BlueALSA PCM can only be opened by one process at a time. This can be inconvenient in many scenarios. For example, some audio applications keep the ALSA PCM device open, even when they are not actively streaming any audio, and so it is necessary to close each application before any other can use the device; desktop systems may like to emit system sounds while music is playing; etc.

ALSA provides a solution to this for hardware cards with the "dmix" plugin. That plugin allows multiple open connections, mixes the streams together, then sends the result as a single stream to the hardware device. Unfortunately dmix works as a front-end only with hardware (ie "hw" type) devices, and not with any other plugin types such as BlueALSA.

So it is not possible to configure BlueALSA as a backend device for dmix. We are forced to find an indirect method; that is, use a "hw" device that can forward the mixed stream on to a BlueALSA PCM. Fortunately, ALSA provides just such a device, the "Loopback" device, which is a kernel driver that implements a virtual sound card.

This wiki article describes one way that BlueALSA can be used with the ALSA Loopback device and the dmix plugin to achieve mixing of multiple audio streams into a single BlueALSA playback PCM.

Overview

In this solution, we use ALSA's dmix plugin to mix the streams, ALSA's Loopback kernel module to provide the hardware sound card interface required by dmix, and ALSA's alsaloop utility to transfer the mixed stream from the Loopback device to a BlueALSA PCM device. As you will see, this is cumbersome to achieve manually, but fortunately much of the work can be scripted. An example scripted solution is given at the end of this article.

snd-aloop

The Loopback device is created by a kernel module called snd-aloop. This module is part of the upstream Linux kernel sources, and the majority of distributions now include it by default. To use it you have to load it, for example with:

sudo modprobe snd-aloop

Once loaded, the module creates a virtual soundcard called Loopback. This card provides two PCM devices, one for capture and one for playback. One application plays an audio stream into device 0, and another captures that same stream from device 1. To illustrate:

aplay -D hw:Loopback,0 music.wav
arecord -D hw:Loopback,1 captured.wav

The card does not perform any format conversions or resampling, so the two applications must use the same sample format, channel count, and sample rate. The card permits a range of formats, to see which are available, try:

aplay -D hw:Loopback --dump-hw-params -d 1 /dev/zero

The Loopback device will adopt the configuration negotiated with the first application to connect, which can be either the playback or the capture device, and the second application must accept that configuration. For that reason, it is common to use Loopback in combination with the plug plugin on the second connection. So for example, format conversion can be achieved if we connect the capture side first with the desired output format, then connect the playback side through plug:

arecord -f float_le -c 2 -r 8000 -D hw:Loopback,1 capture.wav &
aplay -D plughw:Loopback,0 /usr/share/sounds/alsa/Front_Center.wav

The source file is mono, S16_LE, sampled at 48000 Hz; the captured file will be stereo FLOAT_LE sampled at 8000Hz.

[ Note that you will have to kill the arecord process after running the example above, because the Loopback will not close its output device when the input application disconnects. ]

The Loopback device actually supports multiple "substreams": the previous examples used the default substream, number 0. The snd-aloop module produces 8 substreams by default, which is the maximum supported, but that number can be reduced if required by passing a parameter when the module is loaded. For example use only 2 substreams, load the module as:

sudo modprobe snd-aloop pcm_substreams=2

Each output substream is paired with the corresponding input substream. So, for example, hw:Loopback,0,2 will output the stream that is being sent to hw:Loopback,1,2. As noted above, if the substream is not specified it defaults to 0.

alsaloop

The second piece of the puzzle is alsaloop. Just as Loopback is a device that connects two applications, so alsaloop is an application that connects two devices. It is a standard ALSA utility, along with aplay and arecord, and is included by most Linux distributions. We will use it to capture a stream from the Loopback output device and send it to a BlueALSA PCM device.

alsaloop will try to manage the end-to-end latency between the devices, and we need to choose the target latency carefully to avoid underruns. It also allows the user to specify a desired audio format for the loop, which we can use to avoid unnecessary format conversions.

alsaloop will also attempt to compensate for different clocks in the capture and playback devices by modifying the audio stream in various ways according to the --sync argument. If alsaloop is unable to achieve the requested latency then this clock drift compensation calculation can cause it to become unstable; it may enter a state whereby it consumes 100% of one CPU core. Since BlueALSA release 4.1.0 the latency reporting is good enough that alsaloop can successfully maintain a stable audio stream; however for earlier releases it is recommended to use --sync=none and rely on the buffering within BlueALSA to smooth out minor sync variations. BlueALSA release 3.0.0 and earlier lacked some functionality required by alsaloop and are not suitable for use in this solution.

So a simple usage scenario setting the latency to 200 milliseconds using BlueALSA 4.1.0 or later would be:

alsaloop -C hw:Loopback,1,0 -P bluealsa:DEV=XX:XX:XX:XX:XX:XX,PROFILE=a2dp -c 2 -r 48000 -f s16_le -t 200000

If this is run before an application connects to the Loopback input device, the Loopback will be configured as stereo S16_LE format with a sample rate of 48000Hz. This avoids any conversion by the BlueALSA device.

Now an application that wishes to playback to the BlueALSA device can do so as:

aplay -D plughw:Loopback,0,0 music.wav

For more information on alsaloop command line options, consult the man page

man alsaloop

A note about latency and the BlueALSA DELAY parameter

alsaloop tries hard to achieve the target latency set with the -t parameter. It uses the delay values reported by the capture and playback devices to calculate its own internal settings dynamically. If it is set an impossible target this can lead to constant underruns or very high CPU usage, and sometimes alsaloop may fail completely.

For this reason it is advisable to set the BlueALSA DELAY parameter to 0 when using with alsaloop so that BlueALSA understates, rather than overstating, the actual playback delay. Recent releases of BlueALSA source uses a default of 0 for DELAY, but release v3.0.0 and earlier used a DELAY value of 20000 (frames).

If the latency is not important to your application (within reason) then use alsaloop -t 500000 (i.e. 500ms); if latency is an issue (e.g. with VLC etc) then try alsaloop -t 200000, and increase the value if underruns occur.

dmix

The final puzzle piece is dmix. By wrapping the Loopback input device with dmix, we can allow multiple applications to send audio to the BlueALSA PCM device simultaneously. To achieve this, we need to write an ALSA configuration entry. Possibly the simplest example is:

pcm.loopback0 {
    type plug
    slave {
        pcm {
            type dmix
            ipc_key 1024
            slave {
                pcm {
                    type hw
                    card "Loopback"
                    device 0
                    subdevice 0
                }
            }
        }
    }
}

That creates a PCM called "loopback0" which wraps hw:Loopback,0,0. If used in combination with the alsaloop example above, then the audio will play on the bluetooth device XX:XX:XX:XX:XX:XX. To use it, copy the above configuration entry to your .asoundrc file, then:

  1. load the snd-aloop module if not already done

    sudo modprobe snd-aloop

  2. connect the bluetooth speaker

  3. run the alsaloop command

    alsaloop -C hw:Loopback,1,0 -P bluealsa:DEV=XX:XX:XX:XX:XX:XX,PROFILE=a2dp -c2 -r48000 -fs16_le -t 200000

  4. play the music

    aplay -D loopback0 music.wav

The above example ALSA configuration is very basic; for more information on the dmix plugin, and other official ALSA plugins, see here: https://www.alsa-project.org/alsa-doc/alsa-lib/pcm_plugins.html

Limitations

Device disconnection

When a bluetooth device is disconnected, it is normally expected that an application playing to it will be informed and respond appropriately. Unfortunately that is not the case with this solution. The Loopback devices remain active as long as the snd-aloop kernel module is loaded. So a playback application will continue to play, even though there is no speaker to render the sound. There does not appear to be any workaround for this issue.

Maximum 8 pcms

The ALSA Loopback device imposes a maximum of 8 substreams, which in turn means that we can have a maximum of 8 pcm devices connected at any one time. Note that a bluetooth device that offers both A2DP and HFP/HSP profiles will appear as 2 distinct BlueALSA pcms, so will use 2 Loopback substreams. In most situations, this limit is sufficient - (typically only one or two bluetooth playback devices will be in use at any one time). However, if paired devices auto-connect and there are many in the local bluetooth environment, then this limit can become a problem.

Synchronization

The ALSA Loopback device does not propagate the delay (latency) reporting from the output device to the input device. So applications will see only the (negligible) delay within the snd-aloop module, and not the (considerable) delay with the bluetooth system. This means that applications that try to dynamically synchronize audio playback (with video etc) will not be able to do so.

Complexity

This solution is complex; setting up the dmix wrapper requires writing ALSA configuration entries and identifying the correct arguments with which to start alsaloop. If only one or two bluetooth playback devices are used, then it is feasible to write the ALSA configuration into ~/.asoundrc and to start alsaloop manually when required. But when multiple bluetooth playback devices are in use this becomes onerous and error-prone. It is possible to automate much of the setup, and one possible approach is described in the next section.


Automation (bmix)

This section describes one possible method for automating the set-up of dmix wrappers for BlueALSA pcms. We call this solution bmix. In order to guarantee that a Loopback substream is always allocated to the same BlueALSA pcm, this solution places a hard limit of 8 playback pcms that can be configured.

This solution was originally developed and tested using BlueALSA 4.1.0 on Ubuntu 22.04.2 LTS (Bluez 5.64, ALSA 1.2.6).

Objectives

  • monitor bluetooth playback device connection and disconnection;
  • launch an instance of alsaloop for each new device connection;
  • dynamically create an ALSA pcm configuration node for the appropriate loopback substream, using the same name as the control shown by amixer;
  • remove the configuration node and terminate alsaloop when the device disconnects;
  • a particular device always uses the same Loopback substream so that applications that cache the ALSA configuration are not confused when a device disconnects then re-connects.

Components

  • service account - for managing permissions;
  • static ALSA configuration file - pcm definition templates;
  • dynamic ALSA configuration file - pcm definitions for dmix wrappers;
  • dbus configuration - grant permission to interact with dbus services;
  • daemon - script to monitor dbus, manage alsaloop and generate ALSA configuration;
  • systemd unit - for integration into system management;
  • modules unit - loads the Loopback module at boot.

Service account

To avoid the need to run any services as root, create a service account bmix specifically for use by the bmix system. This account should have its own group bmix for file ownership, and should also be a member of the audio group.

Also create a data directory for use by this account:

drwxr-xr-x 2 bmix bmix 4096 Jun 15 18:41 /var/lib/bmix/

Static alsa configuration file

To simplify the task of specifying the configuration of a dmix wrapper for BlueALSA, and to guard against unnecessary audio format conversions, we install a file into the ALSA configuration directory to define parameterized pcm nodes.

For ALSA versions 1.1.7 and later, this file should be saved as:

/etc/alsa/conf.d/21-bmix.conf

and for ALSA versions 1.1.6 and earlier:

/usr/share/alsa/alsa.conf.d/21-bmix.conf

The contents of this file is :

pcm.bluealsa_raw {
	@args [ DEV PROFILE DELAY SRV ]
	@args.DEV {
		type string
		default {
			@func refer
			name defaults.bluealsa.device
		}
	}
	@args.PROFILE {
		type string
		default {
			@func refer
			name defaults.bluealsa.profile
		}
	}
	@args.DELAY {
		type integer
		default {
			@func refer
			name defaults.bluealsa.delay
		}
	}
	@args.SRV {
		type string
		default {
			@func refer
			name defaults.bluealsa.service
		}
	}
	type bluealsa
	device $DEV
	profile $PROFILE
	delay $DELAY
	service $SRV
}

defaults.bmix {
	loop 0
	channels 2
	rate 48000
	period 20000
}

pcm.bmix {
	@args [ IPC_KEY LOOP CHANNELS RATE PERIOD ]
	@args.IPC_KEY {
		type integer
	}
	@args.LOOP {
		type integer
		default {
			@func refer
			name defaults.bmix.loop
		}
	}
	@args.CHANNELS {
		type integer
		default {
			@func refer
			name defaults.bmix.channels
		}
	}
	@args.RATE {
		type integer
		default {
			@func refer
			name defaults.bmix.rate
		}
	}
	@args.PERIOD {
		type integer
		default {
			@func refer
			name defaults.bmix.period
		}
	}
	type plug
	slave {
		pcm {
			type dmix
			ipc_key $IPC_KEY
			ipc_perm 0660
			ipc_gid audio
			slave {
				channels $CHANNELS
				pcm {
					type hw
					card "Loopback"
					device 0
					subdevice $LOOP
				}
				format "s16_le"
				rate $RATE
				period_time $PERIOD
			}
		}
	}
	hint {
		show {
			@func refer
			name defaults.namehint.basic
		}
		description "Bluetooth Audio - Mix Multiple Streams"
	}
}

ctl.bmix {
	@args [ BAT SRV ]
	@args.BAT {
		type string
		default {
			@func refer
			name defaults.bluealsa.battery
		}
	}
	@args.SRV {
		type string
		default {
			@func refer
			name defaults.bluealsa.service
		}
	}
	type bluealsa
	battery $BAT
	service $SRV
}

</var/lib/bmix/bmix.conf>

This file should be owned by root, with permissions 644 (ie readable by all users, writable only by root)

Dynamic ALSA configuration file

We also need an ALSA configuration node for each specific bluetooth pcm that is connected. These entries will be added and removed as devices connect and disconnect, so the file needs to be edited by the bmix system. This file is included by the static configuration file above, and so it must exist, even if empty, otherwise ALSA applications will report a configuration error and fail.

The file should be owned by bmix, and be readable by everyone:

-rw-r--r-- 1 bmix bmix 0 Jun 15 18:41 /var/lib/bmix/bmix.conf

Initially it should be empty, the bmix system will manage its contents.

D-Bus configuration file

The bmix daemon needs to exchange messages via d-bus to both org.bluealsa and org.bluez. Membership of the audio group permits the org.bluealsa communication, but to permit the org.bluez messages we create a new D-Bus configuration file, saved as:

/etc/dbus-1/system.d/bmix.conf

<!-- This configuration file specifies the required security policies
     for bmix bluealsa helper daemon to work. -->

<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>

  <!-- ../system.conf have denied everything, so we just punch some holes -->

  <policy user="bmix">
    <allow send_destination="org.bluez"/>
  </policy>

</busconfig>

D-Bus will need to re-load its configuration before this new policy will become effective. For systems using systemd, this can be achieved by:

sudo systemctl reload dbus.service

Daemon (monitor script)

This script is the active component of the bmix system, it detects when a bluetooth audio device connects or disconnects, and in response manages the necessary alsaloop processes and edits the dynamic alsa configuration. It should be installed as:

/usr/local/bin/bmixd.bash

Ownership and permissions:

-rwxr-xr-x 1 root root 6183 Jun 4 16:49 /usr/local/bin/bmixd.bash

#!/bin/bash

# revision 0.9

# select which profile types to use: "a2dp", "sco", or "all"
profiles="${BMIX_PROFILE:-all}"

# experiment with higher latency if you experience underruns/overruns
latency="${BMIX_LATENCY:-200000}"
# location of dynamic alsa configuration file
conffile="${BMIX_ALSA_CONF:-/var/lib/bmix/bmix.conf}"

# dmix ipc will use 8 consecutive ipc key numbers starting with this value
ipc_key_start=${BMIX_IPC_KEY:-30000}

# to reserve some Loopback substreams for other applications, set the lowest
# substream to use here:
lowest_loop_substream=${BMIX_LOOPBACK:-0}

# extra arguments to apply to alsaloop
alsaloop_args="${BMIX_ALSALOOP_ARGS}"

# 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

ALSALOOP="alsaloop $alsaloop_args"

declare -A pcms

pcm_added() {
	local pcm_var="${pcms["$1"]}"
	if [[ "$pcm_var" ]] ; then
		remove_loop "$pcm_var"
		return
	fi

	local REPLY substream
	for dir in /proc/asound/Loopback/pcm1c/sub*; do
		read -r <"${dir}/status";
		[[ "$REPLY" == "closed" ]] || continue
		substream="${dir: -1}"
		[[ "$substream" -ge "$lowest_loop_substream" ]] && break
		substream=
	done
	if [[ "$substream" != [0-7] ]] ; then
		echo "No Loopback devices available" >&2
		return 1
	fi

	local names=" ${pcms[@]} "
	local index=0
	while [[ " $names " = *" pcm$index "* ]] ; do
		index=$((index+1))
	done
	pcm_var="pcm$index"
	pcms["$1"]="$pcm_var"
	declare -g -A "$pcm_var"
	declare -n pcm="$pcm_var"

	local addr="${1#*dev_}"
	addr="${addr%%/*}"
	pcm[addr]="${addr//_/:}"
	pcm[loopback]="$substream"
	lowest_loop_substream=$((lowest_loop_substream + 1))
}

pcm_ready() {
	declare -n pcm="$1"

	[[ "${pcm[profile]}" && "${pcm[format]}" && "${pcm[channels]}" && "${pcm[rate]}" -gt 0 && "${pcm[mode]}" ]] || return 1

	[[ "${pcm[role]}" == "origin" || "${pcm[running]}" ]] || return 1

	return 0
}

set_property() {
	declare -n pcm="$1"
	local temp
	case "$2" in
		Device*)
			pcm[device]="${2#Device: }"
			local alias=$(dbus-send --print-reply=literal --system --dest=org.bluez "${pcm[device]}" org.freedesktop.DBus.Properties.Get string:"org.bluez.Device1" string:"Alias")
			alias="${alias#   variant       }"
			pcm[alias]="${alias##*[[:space:]]}"
			[[ "${pcm[alias]}" ]] || pcm[alias]="${pcm[addr]}"
			;;
		Transport*)
			temp="${2#Transport: }"
			case "$temp" in
				A2DP*)
					pcm[profile]="a2dp"
					;;
				HFP*|HSP*)
					pcm[profile]="sco"
					;;
			esac
			case "$temp" in
				*-source|*-AG)
					pcm[role]="origin"
					;;
				*)
					pcm[role]="target"
					;;
			esac
			;;
		Mode*)
			pcm[mode]="${2#Mode: }"
			;;
		Format*)
			pcm[format]="${2#Format: }"
			;;
		Channels*)
			pcm[channels]="${2#Channels: }"
			;;
		Sampling*)
			temp="${2#Sampling: }"
			pcm[rate]="${temp% Hz}"
			;;
		"Selected codec"*)
			pcm[codec]="${2#Selected codec: }"
			;;
		Running*)
			[[ "${2#Running: }" == "true" ]] && pcm[running]=1
			;;
	esac
}

add_loop() {
	declare -n pcm="$1"
	[[ "$profiles" == "all" || "${profiles,,}" == "${pcm[profile]}" ]] || return

	$ALSALOOP -f "${pcm[format]}" -c "${pcm[channels]}" -r "${pcm[rate]}" -C hw:Loopback,1,${pcm[loopback]} -P bluealsa_raw:DEV="${pcm[addr]}",PROFILE="${pcm[profile]}" -t "$latency" &>/dev/null &

	pcm[pid]=$!
	pcm[name]="${pcm[alias]} ${pcm[profile]^^}"

	declare -i ipc_key=$((ipc_key_start + substream))

	cat >> "$conffile" <<-EOF
	pcm."${pcm[name]}".type empty
	pcm."${pcm[name]}".slave.pcm "bmix:IPC_KEY=${ipc_key},LOOP=${pcm[loopback]},CHANNELS=${pcm[channels]},RATE=${pcm[rate]},PERIOD=$(($latency / 2))"
	pcm."${pcm[name]}".hint.show.@func refer
	pcm."${pcm[name]}".hint.show.name defaults.namehint.basic
	pcm."${pcm[name]}".hint.description "$alias (${pcm[profile]^^}) Bluetooth Audio Playback"
	EOF
}

remove_loop() {
	declare -n pcm="$1"
	if [[ "${pcm[pid]}" ]] ; then
		kill "${pcm[pid]}" 2>/dev/null
		pcm[pid]=
	fi
	[[ "${pcm[name]}" ]] && sed -i '/^pcm."'"${pcm[name]}"'/d' "$conffile"
}

update_property() {
	declare -n pcm="$1"

	case "$2" in
		Codec)
			remove_loop "$1"
			pcm[codec]="$3"
			case "$3" in
				CVSD)
					pcm[rate]=8000
					;;
				mSBC)
					pcm[rate]=16000
					;;
			esac
			pcm_ready "$1" && add_loop "$1"
			;;
		Running)
			case "$3" in
				"true")
					pcm[running]=1
					;;
				"false")
					pcm[running]=
					;;
			esac
			[[ "${pcm[role]}" == "origin" ]] && return

			if pcm_ready "$1" ; then
				add_loop "$1"
			else
				remove_loop "$1"
			fi
			;;
	esac
}

remove_pcm() {
	local pcm_var="${pcms["$1"]}"
	[[ "$pcm_var" ]] && remove_loop "$pcm_var"
}

add_initial_pcms() {
	local REPLY pcm_var addr
	while read; do
		case "$REPLY" in
			/*)
				[[ "${REPLY##*/}" = sink ]] || continue
				pcm_added "$REPLY"
				pcm_var="${pcms["$REPLY"]}"
				;;
			"")
				[[ "$pcm_var" ]] && pcm_ready "$pcm_var" && add_loop "$pcm_var"
				pcm_var=
				;;
			*)
				[[ "$pcm_var" ]] && set_property "$pcm_var" "$REPLY"
				;;
		esac
	done <<< $($BLUEALSACTL --quiet --verbose list-pcms)
	[[ "$pcm_var" ]] && pcm_ready "$pcm_var" && add_loop "$pcm_var"
}

service_stopped() {
	local loop
	for loop in "${pcms[@]}" ; do
		remove_loop "$loop"
	done
}

echo "# DO NOT EDIT - automatically managed by bmixd" > "$conffile"

monitor_finished() {
	service_stopped
	monitor_stop=1
}

fifo=$(mktemp -u)
mkfifo $fifo
exec {monitor_fd}<>$fifo
rm $fifo

$BLUEALSACTL --quiet --verbose monitor --properties=Codec,Running >&$monitor_fd &
cli_pid=$!
trap "monitor_finished; exec {monitor_fd}>&-" INT TERM

add_initial_pcms

unset monitor_stop
path=
pcm_var=
while [[ -z "$monitor_stop" ]] && read -u $monitor_fd 2>/dev/null
do
	case "$REPLY" in
		PCMAdded*)
			[[ "${REPLY##*/}" = sink ]] || continue
			path="${REPLY#PCMAdded }"
			pcm_added "$path"
			pcm_var="${pcms["$path"]}"
			;;
		"")
			[[ "$pcm_var" ]] || continue
			pcm_ready "$pcm_var" && add_loop "$pcm_var"
			pcm_var=
			;;
		PropertyChanged*)
			argv=( $REPLY )
			[[ "${argv[1]##*/}" = sink ]] || continue
			pcm_var="${pcms["${argv[1]}"]}"
			[[ "$pcm_var" ]] || continue
			update_property "$pcm_var" "${argv[2]}" "${argv[3]}"
			pcm_var=
			;;
		PCMRemoved*)
			remove_pcm "${REPLY#PCMRemoved }"
			;;
		ServiceStopped*)
			service_stopped
			;;
		*)
			[[ "$pcm_var" ]] && set_property "$pcm_var" "$REPLY"
			;;
	esac
done
kill "$cli_pid"

The daemon should not be run as root, as that might present a security risk. Run it as user "bmix" in group "audio" as described in the "Service account" section above.

There are no command-line arguments, but some properties can be modified by setting evironment variables:

  • BMIX_PROFILE
    select which profile types to use: "a2dp", "sco", or "all"
    default "a2dp"

  • BMIX_LATENCY
    experiment with higher latency if you experience underruns/overruns.
    default 200000 (microseconds)

  • BMIX_ALSA_CONF
    location of dynamic alsa configuration file: must be the same as the value included at the end of the static alsa configuration file
    default /var/lib/bmix/bmix.conf

  • BMIX_IPC_KEY
    dmix ipc will use 8 consecutive ipc key numbers starting with this value
    default 30000

  • BMIX_LOOPBACK
    to reserve some Loopback substreams for other applications, set the lowest substream to use
    default 0

  • BMIX_ALSALOOP_ARGS
    extra arguments to apply to alsaloop

Systemd unit

For systems using systemd, the bmix daemon can be managed by installing the following unit file as:

/usr/local/lib/systemd/system/bmix.service

[Unit]
Description=Bluealsa Dmix daemon
Documentation=https://github.com/arkq/bluez-alsa/wiki/Using-bluealsa-with-dmix
After=bluealsa.service
Wants=bluealsa.service
ConditionPathExists=/proc/asound/Loopback

[Service]
Type=simple
EnvironmentFile=-/etc/default/bmix
ExecStart=/usr/local/bin/bmixd.bash
Restart=on-failure
User=bmix
Group=audio

[Install]
WantedBy=bluetooth.target

To use any of the supported environment variables, create a file

/etc/default/bmix

with the required enviroment variable definitions, one per line. For example:

BMIX_PROFILE=a2dp
BMIX_LATENCY=300000

The unit needs to be enabled to start at boot; to do that use:

sudo systemctl enable bmix.service

This unit file will cause the bmix daemon to be started as part of the bluetooth system, but only if the snd-aloop Loopback module is loaded.

Modules unit

For systems using systemd, it is possible to have the ALSA Loopback module loaded at boot by installing the following file as:

/usr/local/lib/modules-load.d/snd_aloop.conf

# load the alsa Loopback device at boot
snd-aloop

If you wish to prevent the Loopback card grabbing card index 0, then also create the file:

/etc/modprobe.d/snd_aloop.conf

# prevent Loopback module from grabbing card index 0
options snd_aloop index=-2

Usage

The bmix daemon hides all the complexity of setting up the loopback. When a bluetooth playback device is connected, the system creates an ALSA pcm configuration for a dmix interface to the device. The name is generated using a similar algorithm as BlueALSA uses for the corresponding control device. So, for example, when a device called Jabra MOVE v2.3.0 is connected using the A2DP profile, then amixer -D bluealsa scontrols will show:

Simple mixer control 'Jabra MOVE v2.3.0 A2DP',0

and aplay -L will show

Jabra MOVE v2.3.0 A2DP
    Jabra MOVE v2.3.0 (A2DP) Bluetooth Audio Playback

Sending audio to the bluetooth device is then as simple as:

aplay -D 'Jabra MOVE v2.3.0 A2DP' music.wav

Multiple programs can use the pcm at the same time: no other setup by the user is necessary.

Multiple bluetooth playback devices can be connected at the same time, and each will be given its own ALSA configuration entry with its own name.

For "one-shot" applications such as aplay and mpg123, that is sufficient. However, for "long-running" applications that may open and close pcm devices as the application is running, there is one more gotcha in alsa-lib to deal with. Alsa-lib reads its configuration files when the first pcm is opened. This configuration is then cached within the process, and only updated when alsa-lib detects that its main configuration file has changed. By default, alsa-lib does not check any other configuration files. In particular, it does not check for changes to the bmix dynamic sonfiguration. So if an application first opens any pcm when a bluetooth speaker is disconnected, it will not later be able to open a pcm on that bluetooth speaker; its configuration will always say "no such device". Fortunately, there is a workaround: the downside is that it has to be applied to all such applications.

Although the default alsa-lib behaviour is to only check its "main" configuration file for changes, this can be overridden using an environment variable. When an alsa application has the variable ALSA_CONFIG_PATH defined in its enviroment, then it will check the files listed in that environment variable instead of the default file. So, if this technique is used, it is important to include the default file in the given path list:

ALSA_CONFIG_PATH=/usr/share/alsa/alsa.conf:/var/lib/bmix/bmix.conf

This environment variable must be applied to all ALSA applications that require to be aware of new bluetooth device connections while they are running. For desktop sessions, include it in the session environment so that all applications launched from the desktop have it set. For system services, set it via the system management script (eg in the systemd unit file, etc).

The daemon will only configure the first 8 bluetooth playback pcms that connect. To introduce a new pcm after that, it is necessary to restart the daemon; this will clear the current substream mapping.

Note that if a bluetooth device disconnects while in use, this does not affect the dmix device that the playing applications are using. So they will continue to play audio even though there is no device there to render it. There does not appear to be any way to force the input Loopback device to close its stream when the output device is closed.