-
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. If you need to use PulseAudio for any reason then it is recommended to at least try its internal bluetooth audio implementation before attempting this integration. Conversely, if you do not need PulseAudio, then it is recommended to use BlueALSA for bluetooth audio and not to run PulseAudio at all.
Of course, there may be rare occasions when you find it necessary to run both services. This wiki article describes one method of using the two services in an integrated way. This approach should be considered as "experimental" and as such suffers many compromises and limitations.
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 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
Note that these commands will have to be re-run each time PulseAudio is restarted.
PulseAudio installs itself as the default ALSA PCM device. To achieve this it uses a "hook" function that can interfere with the user's own PCM definitions. As many BlueALSA configurations depend on specific entries in the ALSA configuration it is recommended to remove the "hook" function. This can be done by deleting the symbolic link:
/etc/alsa/conf.d/99-pulse.conf
If you wish to continue using PulseAudio as the default ALSA PCM device you can do so by adding this line to your ~/.asoundrc
file:
pcm.!default pulse
With the above changes, it is possible to run the BlueALSA service at the same time as PulseAudio. Applications will have to connect to ALSA directly using the ALSA API to use bluetooth devices; PulseAudio will not see them. So applications that are configured to use the PulseAudio API (including most desktop volume control panels) will be unable to use bluetooth audio. That may be good enough for many systems, so this section is optional and only relevant if you need to use BlueALSA devices via the PulseAudio API (for example with Firefox on Ubuntu).
To present BlueALSA PCMs via the PulseAudio API it is necessary to do a little more preparation of the configration, and then to load the relevant modules into PulseAudio manually. The following 3 steps describe this procedure.
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 [ DEV PROFILE CODEC VOL SOFTVOL 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.CODEC {
type string
default {
@func refer
name defaults.bluealsa.codec
}
}
@args.VOL {
type string
default {
@func refer
name defaults.bluealsa.volume
}
}
@args.SOFTVOL {
type string
default {
@func refer
name defaults.bluealsa.softvol
}
}
@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
codec $CODEC
volume $VOL
softvol $SOFTVOL
delay $DELAY
service $SRV
}
Unless configured otherwise, PulseAudio will immediately open and hold each BlueALSA PCM device that is loaded. This wastes Bluetooth bandwidth and remote device battery charge, and can cause problems for devices that support both A2DP and SCO PCMs. The only way to keep a PCM loaded but not open is to "suspend" it. However, most PulseAudio GUI tools (including pavucontrol
) do not allow the user to manually suspend and unsuspend sinks and sources. The workaround for this is to enable the "suspend-on-idle" feature, with a sufficiently low timeout. Some linux distributions enable this feature, others do not. To check if your PulseAudio instance has this module loaded, type:
pactl list modules
and look for the module-suspend-on-idle entry:
Module #20
Name: module-suspend-on-idle
Argument: timeout=5
Usage counter: n/a
Properties:
module.author = "Lennart Poettering"
module.description = "When a sink/source is idle for too long, suspend it"
module.version = "11.1"
The Module number, and other parameter values, may differ.
If the module is loaded, check the "Argument:" parameter. We need a low timeout value because there will be silence of that amount of time when switching from A2DP to SCO. If the Argument is blank, the default timeout is 5 seconds.
If the module is not loaded or the timeout needs to be changed, we must edit the file /etc/pulse/default.pa. Add an entry (or edit an existing entry) as:
### Automatically suspend sinks/sources that become idle for too long
load-module module-suspend-on-idle timeout=2
Important Note:
pavucontrol
by default shows volume meters for each device, and these prevent
the devices from being suspended while pavucontrol
is running. To disable the
volume meters, and thus enable the auto suspend feature when pavucontrol
is
running, go to the pavucontrol Configuration tab and uncheck the "Show volume meters" checkbox in the bottom left-hand corner.
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 load the module-alsa-sink
or module-alsa-source
modules with appropriate parameters. 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. The complete list of parameters for these modules can be seen here: https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/Modules/
It is 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, by using the sink_properties
or source_properties
parameter as appropriate.
It is also possible to set the display icon in the source_properties parameter - 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". The icons actually available will depend on your desktop distribution.
The device must first be connected before adding to PulseAudio.
For example, 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' sink_name=MyFriendlyName sink_properties="device.description='My\ Friendly\ Description'device.icon_name=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' source_name=MyFriendlyName source_properties="device.description='My\ Friendly\ Description'device.icon_name=bluetooth")
PulseAudio relies of the use of an accurate timer on the sound card to achieve "glitch-free" capture from an ALSA PCM. A BlueALSA PCM does not have an associated sound card, so it does not provide a timer that can be used by PulseAudio. As a result there is likely to be regular "dropouts" or "hiccups" in the resulting stream.
A common use-case with PulseAudio is to use sinks to play audio from applications to speakers, but to use sources only to route incoming audio from a device (e.g. a mobile phone) directly to the speakers using PulseAudio's internal "module_loopback" module. For this specific case with BlueALSA, the best results are usually obtained by using bluealsa-play
and not to load the BlueALSA PCM using module-alsa-source. Run bluealsa-aplay
as:
bluealsa-aplay --pcm=pulse
so that the sound is played through PulseAudio and can be controlled using PulseAudio clients.
If it is required to direct the audio to recording applications, then it is necessary to use module-alsa-source. In this case, bluealsa-aplay
cannot be used so module-loopback
is probably the best available alternative. Results can be improved by explicitly setting the buffer parameters for both the source module and the loopback module. For example, load the source module with additional parameters:
pactl load-module module-alsa-source fragments=2 fragment_size=9600 ...
The other parameters are as given above.
Load the loopback module after the source module, giving its source as the name used by the BlueALSA module parameter source_name
, and set the latency_msec
parameter. Try a latency of around 150 msec, and then perhaps experiment with different values to see if the resulting stream is improved. Use the source_dont_move
parameter to ensure that the loopback does not automatically switch to the local microphone when the Bluetooth device stops sending audio. For example:
pactl load-module module-loopback source_dont_move=true source='MyFriendlyName' latency_msec=150
It is possible to automate the above procedure by running a service that listens for D-Bus ObjectManager events from org.bluealsa and invokes the above pactl
commands in response to the addition and removal of BlueALSA PCMs. The following example script demonstrates how that might be done by using the BlueALSA utilty bluealsa-cli
to perform the event monitoring. The script was developed and tested using BlueALSA 4.1.0 on Ubuntu 22.04.2 LTS (Bluez 5.64, PulseAudio 15.99.1, ALSA 1.2.6.)
On Bluetooth source nodes (a2dp-source, hfp-ag, hsp-ag) the PulseAudio sinks and sources are created as soon as the device connects; on target nodes (a2dp-sink, hfp-hf, hsp-hs) the sinks and sources are created only when the audio stream is started by the remote device.
The script requires features of the
bluealsa-cli
utility which were introduced in BlueALSA release v4.1.0, and will not work will older releases. Be sure to add the configure option--enable-cli
to build thebluealsa-cli
utility when building BlueALSA from source.
#!/bin/bash
# revision 0.7
# select which device device types to exclude from loading into pulseaudio
# A space separated list of types, where valid types are:
# a2dp_source a2dp_sink sco_source sco_sink
# outputs (speakers, headphones) are "sinks"
# inputs (microphones) are "sources"
# By default no devices are excluded
EXCLUDE="$BLUEPULSE_EXCLUDE"
# change this if you wish to use a different icon
ICON_NAME="${BLUEPULSE_ICON_NAME:-bluetooth}"
# For BlueALSA capture devices only, setting this to "yes" will enable the
# PulseAudio loopback module to feed the Bluetooth stream to a local speaker
ENABLE_LOOPBACK="$BLUEPULSE_ENABLE_LOOPBACK"
# Pulse sink name to use for loopback output
# Leave blank to select the default sink
LOOPBACK_DEVICE_NAME="$BLUEPULSE_LOOPBACK_DEVICE_NAME"
# Fine-tune the PulseAudio buffer configuration. These values are taken as
# "hints" only, and PulseAudio may choose to use alternative values.
# The number of fragments in the BlueALSA buffer
declare -i PCM_FRAGMENTS="$BLUEPULSE_PCM_FRAGMENTS"
# The size of each BlueALSA fragment, in milliseconds
declare -i PCM_FRAGMENT_MSEC="$BLUEPULSE_PCM_FRAGMENT_MSEC"
# The target latency of the loopback for BlueALSA capture devices
declare -i LOOPBACK_LATENCY_MSEC="$BLUEPULSE_LOOPBACK_LATENCY_MSEC"
[[ "$PCM_FRAGMENT_MSEC" == 0 ]] && PCM_FRAGMENT_MSEC=50
[[ "$PCM_FRAGMENTS" == 0 ]] && PCM_FRAGMENTS=2
[[ "$LOOPBACK_LATENCY_MSEC" == 0 ]] && LOOPBACK_LATENCY_MSEC=$((PCM_FRAGMENT_MSEC * 3))
declare -A pa_format=(
[U8]=u8
[S16_LE]=s16le
[S24_LE]=s24le
[S32_LE]=s32le
)
declare -A sample_size=(
[u8]=1
[s16le]=2
[s24le]=3
)
declare -A pcms
pcm_added() {
local pcm_var="${pcms["$1"]}"
if [[ "$pcm_var" ]] ; then
unset pcms["$1"]
unset "$pcm_var"
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//_/:}"
}
# @arg $1 name of BlueALSA pulseaudio source
# @arg $2 format
# @arg $3 rate
# @arg $4 channels
load_loopback() {
local sink=
[[ "$LOOPBACK_DEVICE_NAME" ]] && sink="sink='$LOOPBACK_DEVICE_NAME'"
pactl load-module module-loopback "source_dont_move='true'" "source='$1'" "$sink" "format='$2'" "rate='$3'" "channels='$4'" "latency_msec='$LOOPBACK_LATENCY_MSEC'"
}
load_module() {
declare -n pcm="$1"
[[ "${pcm[module_id]}" ]] && return
local profile="a2dp"
[[ "${pcm[profile]}" == HFP || "${pcm[profile]}" == HSP ]] && profile="sco"
local mode="${pcm[mode]}"
[[ "$EXCLUDE" =~ "${profile}_${mode}" ]] && return
local addr="${pcm[addr]}"
local device="bluealsa_raw:DEV=$addr,PROFILE=$profile"
local name="bluealsa.$mode.${addr//:/_}.$profile"
local description="Bluetooth:\ ${pcm[alias]// /\\ }\ (${pcm[profile]})"
local format="${pa_format[${pcm[format]}]}"
[[ -n "$format" ]] || return
local channels="${pcm[channels]}"
local rate="${pcm[rate]}"
local fragment_size=$((${sample_size[$format]} * "$PCM_FRAGMENT_MSEC" * "$rate" / 1000))
local module_id=$(pactl load-module "module-alsa-$mode" "format='$format'" "rate='$rate'" "channels='$channels'" "device='$device'" "${mode}_name='$name'" "${mode}_properties=device.description='$description'device.icon_name=$ICON_NAME" "fragments='$PCM_FRAGMENTS'" "fragment_size='$fragment_size'")
[[ "$module_id" ]] || return
pcm[module_id]="$module_id"
if [[ "$ENABLE_LOOPBACK" == yes && "$mode" == "source" ]] ; then
load_loopback "$name" "$format" "$rate" "$channels" >/dev/null
fi
}
unload_module() {
declare -n pcm="$1"
local module_id=${pcm[module_id]}
if [[ "$module_id" ]] ; then
pactl unload-module "$module_id" 2>/dev/null
pcm[module_id]=
fi
}
pcm_removed() {
local pcm_var="${pcms["$1"]}"
if [[ "$pcm_var" ]] ; then
unload_module "$pcm_var"
unset pcms["$1"]
unset "$pcm_var"
fi
}
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
}
service_stopped() {
local path
for path in "${!pcms[@]}" ; do
pcm_removed "$path"
done
}
# store a changed pcm property
# @arg $1 pcm path
# @arg $2 property as printed by bluealsa-cli
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")
pcm[alias]="${alias# variant }"
[[ "${pcm[alias]}" ]] || pcm[alias]="${pcm[addr]}"
;;
Sequence*)
pcm[sequence]="${2#Sequence: }"
;;
Transport*)
temp="${2#Transport: }"
case "$temp" in
A2DP*)
pcm[profile]="A2DP"
;;
HFP*)
pcm[profile]="HFP"
;;
HSP*)
pcm[profile]="HSP"
;;
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
}
update_property() {
declare -n pcm="$1"
case "$2" in
Codec)
unload_module "$1"
pcm[codec]="$3"
case "$3" in
CVSD)
pcm[rate]=8000
;;
mSBC)
pcm[rate]=16000
;;
esac
pcm_ready "$1" && load_module "$1"
;;
Running)
case "$3" in
"true")
pcm[running]=1
;;
"false")
pcm[running]=
;;
esac
[[ "${pcm[role]}" == "origin" ]] && return
if pcm_ready "$1" ; then
load_module "$1"
else
unload_module "$1"
fi
;;
esac
}
add_initial_pcms() {
local REPLY pcm_var
while read; do
case "$REPLY" in
/*)
pcm_added "$REPLY"
pcm_var="${pcms["$REPLY"]}"
;;
"")
[[ "$pcm_var" ]] && pcm_ready "$pcm_var" && load_module "$pcm_var"
pcm_var=
;;
*)
[[ "$pcm_var" ]] && set_property "$pcm_var" "$REPLY"
;;
esac
done <<< $(bluealsa-cli --quiet --verbose list-pcms)
[[ "$pcm_var" ]] && pcm_ready "$pcm_var" && load_module "$pcm_var"
}
monitor_finished() {
service_stopped
monitor_stop=1
}
fifo=$(mktemp -u)
mkfifo $fifo
exec {monitor_fd}<>$fifo
rm $fifo
bluealsa-cli --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*)
path="${REPLY#PCMAdded }"
pcm_added "$path"
pcm_var="${pcms["$path"]}"
;;
"")
[[ "$pcm_var" ]] || continue
pcm_ready "$pcm_var" && load_module "$pcm_var"
pcm_var=
;;
PropertyChanged*)
argv=( $REPLY )
pcm_var="${pcms["${argv[1]}"]}"
[[ "$pcm_var" ]] || continue
update_property "$pcm_var" "${argv[2]}" "${argv[3]}"
pcm_var=
;;
PCMRemoved*)
pcm_removed "${REPLY#PCMRemoved }"
;;
ServiceStopped*)
service_stopped
;;
*)
[[ "$pcm_var" ]] && set_property "$pcm_var" "$REPLY"
;;
esac
done
kill "$cli_pid"
The script should be run as a normal user, not as root.
To enable the PulseAudio loopback module automatically when a BluALSA source device connects, set the environment variable:
BLUEPULSE_ENABLE_LOOPBACK=yes
To prevent some Bluetooth device types from being loaded into PulseAudio, for example to prevent A2DP capture and the SCO profiles, use:
BLUEPULSE_EXCLUDE=a2dp_source,sco_source,sco_sink
The complete list of environment variables that control the script are (showing the default values)
BLUEPULSE_EXCLUDE=
BLUEPULSE_ICON_NAME=bluetooth
BLUEPULSE_ENABLE_LOOPBACK=no
BLUEPULSE_LOOPBACK_DEVICE_NAME=
BLUEPULSE_PCM_FRAGMENTS=2
BLUEPULSE_PCM_FRAGMENT_MSEC=50
BLUEPULSE_LOOPBACK_LATENCY_MSEC=150
See the comments near the top of the script for more information.
Note that when switching a stream from an A2DP sink to a SCO sink on the same device, there may be several seconds of silence before the stream resumes, because with some Bluetooth devices SCO playback will only commence when the A2DP device is suspended by PulseAudio or vice-versa.
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
Environment=BLUEPULSE_EXCLUDE=
Environment=BLUEPULSE_ICON_NAME=bluetooth
Environment=BLUEPULSE_ENABLE_LOOPBACK=no
Environment=BLUEPULSE_LOOPBACK_DEVICE_NAME=
Environment=BLUEPULSE_PCM_FRAGMENTS=2
Environment=BLUEPULSE_PCM_FRAGMENT_MSEC=50
Environment=BLUEPULSE_LOOPBACK_LATENCY_MSEC=150
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.