-
Notifications
You must be signed in to change notification settings - Fork 189
Using BlueALSA with dmix
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
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
.
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.
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.
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.
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
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.
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:
-
load the snd-aloop module if not already done
sudo modprobe snd-aloop
-
connect the bluetooth speaker
-
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
-
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
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.
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.
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.
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.
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).
- 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.
- 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.
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/
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)
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.
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
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
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.
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
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.