-
Notifications
You must be signed in to change notification settings - Fork 189
PulseAudio integration
PulseAudio (https://www.freedesktop.org/wiki/Software/PulseAudio/) is the de-facto Linux standard audio server, particularly for desktop installations. Many linux distributions' desktops require PulseAudio in order to provide full functionality.
PulseAudio has its own bluetooth audio implementation which is sufficient in many common usage scenarios. However, it does not support the range of bluetooth codecs and other features that are offered by Bluealsa. Therefore it is sometimes desirable to have installed both Bluealsa (for all bluetooth audio capabilities) and PulseAudio (for desktop integration, audio stream routing, etc, etc).
Using these two services in the chain of audio processing results in a high latency. Not so high that it is an issue for music player applications, but high enough that video applications will find it difficult to synchronize sound to video. Therefore this is not a universal solution, but may be useful in some circumstances.
Bluez allows only one service to register as provider of a bluetooth profile, so it is necessary to disable the PulseAudio bluetooth modules in order to use Pulseaudio in combination with Bluealsa.
There are three ways to disable the PulseAudio bluetooth modules.
- Uninstall the module packages
Many distributions deliver the PulseAudio bluetooth modules as separate packages, and so for these the simplest solution is to uninstall those packages. For example, on Ubuntu
sudo apt purge pulseaudio-module-bluetooth
- Remove the modules from the PulseAudio configuration
If the modules cannot be uninstalled, then they can still be disabled in the PulseAudio global configuration. This is achieved by editing the file /etc/pulse/default.pa to comment out the bluetooth module entries as shown here:
### Automatically load driver modules for Bluetooth hardware
#.ifexists module-bluetooth-policy.so
#load-module module-bluetooth-policy
#.endif
#.ifexists module-bluetooth-discover.so
#load-module module-bluetooth-discover
#.endif
Restart the PulseAudio service to read the new configuration. On most desktop systems this service is configured to automatically restart, so it is sufficient to type:
pulseaudio --kill
- Unload the PulseAudio bluetooth modules at runtime
It is possible to temporarily remove the modules at runtime with the commands:
pactl unload-module module-bluetooth-policy
pactl unload-module module-bluetooth-discover
It is recommended to allow PulseAudio to perform any necessary audio format conversions internally, and not to use the alsa-lib "plug" or other alsa plugins. This is especially important if you have libasound version 1.1.2 or 1.1.3 which will otherwise cause the PulseAudio daemon to deadlock. A simple way to achieve this is to create a file 21-bluealsa-raw.conf in the appropriate alsa config directory
- /usr/share/alsa/alsa.conf.d/21-bluealsa-raw.conf ( alsa-lib version <= 1.1.6 )
- /etc/alsa/conf.d/21-bluealsa-raw.conf ( alsa-lib version >= 1.1.7 )
Put the following into that file:
pcm.bluealsa_raw {
@args [ SRV DEV PROFILE DELAY ]
@args.SRV {
type string
default {
@func refer
name defaults.bluealsa.service
}
}
@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
}
}
type bluealsa
service $SRV
device $DEV
profile $PROFILE
delay $DELAY
}
Each bluetooth device that you wish to use with PulseAudio needs to be added individually as a sink, source, or both as appropriate. If you wish to use both A2DP and SCO profiles for a single device, then they too will need to be added individually.
The way to add a device is to use the pactl
utility. In order to later remove a device, it is helpful to make a note of the module index allocated to it. The utility pactl
returns that index on successful completion. It is also helpful to give the device a user-friendly description for use in the description displayed in graphical tools such as pavucontrol
or other desktop sound control applications/widgets.
It is also possible to set the display icon in the device properties - it will default to "audio-card" but you may prefer to use "audio-speakers-bluetooth", "audio-headphones-bluetooth", "audio-headset-bluetooth", "phone-bluetooth", or maybe just "bluetooth".
It is necessary to explicitly set the "fragments" and "fragment_size" module arguments because the defaults used by module-alsa-sink and module-alsa-source can cause PulseAudio to become unresponsive for many seconds at a time. I recommend to try fragments=1
and fragment_size=960
as a first guess, and experiment from there.
The device must first be connected before adding to PulseAudio.
To load a playback (sink) device, set its internal name (used by command line tools such as pactl
and pacmd
), set its user-friendly description and icon, and record its allocated module index, type:
MODULE=$(pactl load-module module-alsa-sink device='bluealsa_raw:DEV=XX:XX:XX:XX:XX:XX,PROFILE=a2dp' fragments='1' fragment_size='960' sink_name=MyFriendlyName sink_properties="device.description='My\ Friendly\ Description'device.icon_name=audio-speakers-bluetooth")
(Note the use of "bluealsa_raw" as defined above in the device field, and the need to escape space characters with a backslash in the description field. Do not leave any space between property settings)
To remove the device from PulseAudio, type
pactl unload-module $MODULE
The procedure for a capture (source) device is the same, except to use 'source' in place of 'sink' in the commands:
MODULE=$(pactl load-module module-alsa-source device='bluealsa_raw:DEV=XX:XX:XX:XX:XX:XX,PROFILE=a2dp' fragments='1' fragment_size='960' source_name=MyFriendlyName source_properties="device.description='My\ Friendly\ Description'device.icon_name=phone-bluetooth")
It is possible to automate the above procedure by running a service that listens for D-Bus PCMAdded and PCMRemoved events from bluealsa and invokes the above commands in response.
The following script is provided as a simple example to demonstrate how that might be done. It should be run as a normal user, not as root.
The script depends on GNU Awk (gawk) to parse the dbus messages.
#!/bin/bash
FRAGMENTS=1
FRAGMENT_MSEC=5
ICON_DEFAULT="bluetooth"
ICON_A2DP_SINK="audio-speaker-bluetooth"
ICON_A2DP_SOURCE="audio-microphone-bluetooth"
ICON_SCO_SINK="audio-headset-bluetooth"
ICON_SCO_SOURCE="audio-headset-bluetooth"
declare -A formats
formats[8]=u8
formats[32784]=s16le
formats[32792]=s24le
declare -A sample_size
sample_size[u8]=1
sample_size[s16le]=2
sample_size[s24le]=3
# an array to store pulseaudio module ids so they can be removed later
declare -A modules
awk_parse_dbus_message='
/member=PCMAdded/ { added = 1; next; }
/member=PCMRemoved/ { removed = 1; next; }
/member=NameOwnerChanged/ { printf("ServiceStopped\n"); fflush(); next; }
!/variant/ && /object path/ {
match($0, /dev_([[:xdigit:]_]+)\/(a2dp|sco|hfp|hsp)(.*)/, arr)
if (RSTART > 0) {
brackets = 0
addr = arr[1]
profile = arr[2]
if (removed) {
if (profile ~ /hfp|hsp/)
profile = "sco"
if (arr[3] ~ /source/)
printf("PCMRemoved %s %s %s\n", addr, profile, "source")
else if (arr[3] ~ /sink/)
printf("PCMRemoved %s %s %s\n", addr, profile, "sink")
else {
printf("PCMRemoved %s %s %s\n", addr, profile, "source")
printf("PCMRemoved %s %s %s\n", addr, profile, "sink")
}
fflush()
removed = 0
next
}
if (substr($0, length($0) - length(profile), length(profile)) == profile)
reverse_mode = 1
else
profile = ""
split("", modes)
format = ""
channels = ""
sampling = ""
next
}
}
/string "Transport"/ { t = 1; next; }
/string "Mode/ { m = 1; next; }
/string "Format"/ { f = 1; next; }
/string "Channels"/ { c = 1; next; }
/string "Sampling"/ { s = 1; next; }
t && /A2DP/ { profile = "a2dp"; t = 0; next; }
t && /HFP|HSP/ { profile = "sco"; t = 0; next; }
m && /string/ {
sub(/^.*string /,"")
gsub("\"","")
if (reverse_mode) {
if ($0 == "source") {
modes[length(modes)] = "sink"
}
else {
modes[length(modes)] = "source"
}
}
else
modes[length(modes)] = $0
next
}
f && /uint16/ { format = $NF; f = 0; next; }
c && /byte/ { channels = $NF; c = 0; next; }
s && /uint32/ { sampling = $NF; s = 0; next; }
m && /)/ { m = 0; next; }
m && /]/ { m = 0; brackets--; next; }
t && /)/ { t = 0; next; }
f && /)/ { f = 0; next; }
c && /)/ { c = 0; next; }
s && /)/ { s = 0; next; }
/\[/ { brackets++; next; }
addr && /]/ && (--brackets == 0) {
if (addr && profile && length(modes) && format && channels && sampling) {
for (i in modes) {
if (added)
printf("PCMAdded ")
printf("%s %s %s %s %s %s\n", addr, profile, modes[i], format, channels, sampling)
}
fflush()
added = 0
}
}
'
# get device alias from Bluez
get_alias() {
alias=$(dbus-send --print-reply=literal --system --dest=org.bluez \
/org/bluez/hci0/dev_$1 \
org.freedesktop.DBus.Properties.Get string:"org.bluez.Device1" string:"Alias")
echo ${alias# variant}
}
# add a device to PulseAudio
add_device() {
alias="$(get_alias $1)"
device="bluealsa_raw:DEV=${1//_/:},PROFILE=$2"
mode=$3
name=bluealsa.$3.$1.$2
description="Bluetooth:\ ${alias// /\\ }\ ($2)"
addr="$1"
format=${formats[$4]}
[[ -n "$format" ]] || return
channels=$5
rate=$6
fragment_size=$(( $FRAGMENT_MSEC * ${sample_size[$format]} * $channels * $rate / 1000 ))
module_id=$(pactl load-module "module-alsa-$mode" "format='$format'" "rate='$rate'" "channels='$channels'" "fragments='$FRAGMENTS'" "fragment_size='$fragment_size'" "fixed_latency_range='true'" "device='$device'" "${mode}_name='$name'" "${mode}_properties=device.description='$description'device.icon_name=$ICON_DEFAULT")
[[ -n "$module_id" ]] && modules[$name]=$module_id
}
# remove a device from PulseAudio
remove_device() {
name=bluealsa.$3.$1.$2
module_id=${modules[$name]}
if [[ -n "$module_id" ]] ; then
pactl unload-module $module_id 2>/dev/null
unset modules[$name]
fi
}
# get list of connected devices
get_devices() {
dbus-send --print-reply --system --dest=org.bluealsa \
/org/bluealsa org.bluealsa.Manager1.GetPCMs 2>/dev/null | gawk "$awk_parse_dbus_message"
}
handle_device_added_event() {
shift
add_device "$@"
}
handle_device_removed_event() {
shift
remove_device "$@"
}
add_initial_devices() {
readarray -t devices < <(get_devices)
for device in "${devices[@]}" ; do
add_device $device
done
}
# remove all devices if bluealsa service terminates
handle_service_stopped_event() {
for name in "${!modules[@]}" ; do
pactl unload-module ${modules[$name]} 2>/dev/null
unset modules[$name]
done
}
# create a temporary named pipe to communicate with dbus monitor
PIPE=$(mktemp -u)
mkfifo $PIPE
# attach it to unused file descriptor FD
exec {FD}<>$PIPE
# unlink the named pipe
rm $PIPE
# make sure the pipeline is shut down if this script interrupted
trap "kill %1; exec {FD}>&-; handle_service_stopped_event; exit 0" INT TERM
# start dbus monitor in background
dbus-monitor --system "type='signal',sender='org.bluealsa',interface='org.bluealsa.Manager1'" "sender='org.freedesktop.DBus',member='NameOwnerChanged',arg0='org.bluealsa',arg2=''" 2>/dev/null | gawk "$awk_parse_dbus_message" >&$FD &
# load initial set of connected devices
add_initial_devices
# now listen for connect signals
while read
do
case "$REPLY" in
PCMAdded*)
handle_device_added_event $REPLY
;;
PCMRemoved*)
handle_device_removed_event $REPLY
;;
ServiceStopped*)
handle_service_stopped_event
;;
esac
done <&$FD
If you are running PulseAudio within your desktop session and using systemd, then the above script (or your own improved service) can be launched whenever you log in to the desktop by enabling it as a systemd user service.
For example, if you install the above script as /usr/local/bin/bluepulse.bash, save the following systemd service file as:
/usr/local/lib/systemd/user/bluepulse.service
[Unit]
Description=Bluealsa PulseAudio Integration
BindsTo=pulseaudio.service
After=pulseaudio.service
[Service]
Type=simple
ExecStart=/usr/local/bin/bluepulse.bash
RestartSec=2
Restart=on-failure
[Install]
WantedBy=pulseaudio.service
Each user that wishes to use this service can then enable it to start at login with:
systemctl --user enable bluepulse.service
Systemd will now start the bluepulse service for that user whenever pulseaudio is started, and stop it when pulseaudio stops.