Skip to content

PulseAudio integration

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

Using BlueALSA Together with PipeWire or PulseAudio

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

PipeWire (https://pipewire.org) is now the most commonly used audio server on modern Linux desktops, and is also very widely used in server installations too. PulseAudio (https://www.freedesktop.org/wiki/Software/PulseAudio/) was the de-facto Linux standard audio server, particularly for desktop installations, before PipeWire was adopted, and is still used by many distributions. Many linux distributions' desktops require either PipeWire or PulseAudio in order to provide full functionality.

Both PipeWire and PulseAudio have their own Bluetooth audio implementation which is sufficient in many common usage scenarios. If you need to use PipeWire or 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 a general purpose audio server, then it is recommended to use BlueALSA for Bluetooth audio and not to run PipeWire or PulseAudio at all.

Of course, there may be rare occasions when you find it necessary to run BlueALSA in combination with PipeWire or PulseAudio. This wiki article describes one method of achieving this in an integrated way.

Preparing PipeWire or PulseAudio for use with BlueALSA

BlueZ allows only one service to register as provider of a Bluetooth profile, so it is necessary to disable the PipeWire or PulseAudio Bluetooth modules in order to use BlueALSA.

Disable PipeWire Bluetooth Support

If using PipeWire, the PipeWire Bluetooth SPA module can be disabled using WirePlumber. WirePlumber changed its configuration file format and syntax in release 0.5.0; so this method depends on the WirePlumber version in use.

  • WirePlumber release 0.5.0 and later:

    Override the default configuration by creating the file

    /etc/wireplumber/wireplumber.conf.d/disable-bluetooth.conf

    with the following contents:

    wireplumber.profiles = {
        main = {
            hardware.bluetooth = disabled
            monitor.bluez = disabled
            monitor.bluez.seat-monitoring = disabled
        }
    }
    
  • WirePlumber release 0.4.17 and earlier:

    For these releases it is sufficient to override the single lua file which by default enables Bluetooth support. So we only need to create an empty file:

    /etc/wireplumber/bluetooth.lua.d/90-enable-all.lua

With both methods it is necessary to re-start the wireplumber service to apply the configuration change.

systemctl --user restart wireplumber

To restore Bluetooth support in PipeWire, simply delete the file created above, then restart WirePlumber.

Disable PulseAudio Bluetooth modules

If using PulseAudio, there are three ways to disable the PulseAudio Bluetooth modules.

  1. 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 remove pulseaudio-module-bluetooth
  2. 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
  3. 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.

Adding BlueALSA Devices as PipeWire or PulseAudio Sinks / Sources

With the above changes, it is possible to run the BlueALSA service at the same time as PipeWire or PulseAudio. Applications will have to connect to ALSA directly using the ALSA API to use Bluetooth devices; neither PipeWire nor PulseAudio will see them. So applications that are configured to use the PipeWire or PulseAudio APIs (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 PipeWire or PulseAudio API.

Important

This approach should be considered as "experimental" and as such suffers many compromises and limitations. It is not suitable for production systems.

In particular, both PipeWire and PulseAudio rely on an accurate timer on the associated sound card to achieve stream synchronization, for example with a video stream or for loopback to another audio device. A BlueALSA PCM has no associated sound card, and the ALSA external IO plugin API does not support period time-stamping, so that stream synchronization is not possible when using BlueALSA with PulseAudio and difficult with PipeWire. This issue can also cause regular "glitches" in the audio stream if PipeWire or PulseAudio is trying to synchronise the stream.

The PipeWire native API permits the creation of ALSA PCM nodes only from ALSA sound cards; it cannot be used to create PCM nodes that are based on the ALSA ioplug API (such as BlueALSA). However, from release 0.3.79, PipeWire's pulseaudio emulation API does support loading of such ALSA devices, so we can use PulseAudio tools with PipeWire to achieve our desired result.

Configure PulseAudio to suspend on idle

PipeWire and PulseAudio immediately open each BlueALSA PCM device that is loaded. To prevent wastage of Bluetooth bandwidth and remote device battery charge, and to avoid problems with some devices that support both A2DP and SCO PCMs, the PCM must be "suspended" when not actively transferring audio.

PipeWire suspends a BlueALSA device immediately that its stream stops, so there is no extra configuration required; but for PulseAudio it is necessary to explicitly suspend the device. 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

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.

Create sink and / or source objects

Each Bluetooth device that you wish to use with PipeWire or 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/

Always give the device a unique internal name, so that we can use it to identify the correct node when using PulseAudio or PipeWire command line tools such as pactl, pacmd or pw-cli. We recommend a name starting with "bluealsa" and including the device address and profile to make it easily identifiable. For example bluealsa.sink.XX_XX_XX_XX_XX_XX.a2dp.

It is helpful to also 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 (sink_name for sinks, source_name for sources), set its user-friendly description and icon, and record its allocated module index, type:

MODULE=$(pactl load-module module-alsa-sink device='bluealsa:DEV=XX:XX:XX:XX:XX:XX,PROFILE=a2dp' sink_name=UniqueInternalName sink_properties="device.description='My\ Friendly\ Description'device.icon_name=bluetooth")

(Note 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:DEV=XX:XX:XX:XX:XX:XX,PROFILE=a2dp' source_name=MyFriendlyName source_properties="device.description='My\ Friendly\ Description'device.icon_name=bluetooth")

Special notes for BlueALSA Capture (PipeWire/PulseAudio source) devices

A common use-case with PipeWire or 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 the PipeWire or PulseAudio internal loopback module. However, this module needs to be able to synchronise the Bluetooth stream with the output device, and as noted above this does not work with BlueALSA PCMs. So for this specific case with BlueALSA, the best results are usually obtained by using bluealsa-aplay and not to load the BlueALSA PCM using module-alsa-source. Note that both PipeWire and PulseAudio are normally run in the user's session, so bluealsa-aplay must also be run under the user's account to have permission to connect to the audio service. See the section bluealsa-aplay below.

If it is required to direct the audio to recording applications, then it is necessary to use module-alsa-source. As there is no need to control the rate at which samples are read from the device when recording to a file, this normally works well.

For example, if you use bluealsa.source.XX_XX_XX_XX_XX_XX.A2DP as the source_name when loading the BlueALSA PCM, then to record a stream from it use:

pw-record --target bluealsa.source.XX_XX_XX_XX_XX_XX.A2DP filename.wav

or

parecord --device bluealsa.source.XX_XX_XX_XX_XX_XX.A2DP filename.wav

Delay / Latency reporting

Neither PipeWire nor PulseAudio use the PCM delay property of an ALSA PCM.

For PulseAudio there is no alternative method informing an application of the delay, so video synchronization is impossible.

When using PipeWire, there is a node parameter called "ProcessLatency", which works much the same way as the BlueALSA "DelayAdjustment" property, and we can use "ProcessLatency" to "fix" the delay reported to the application. For example, if bluealsactl info reports Delay: 286.0 ms, then (assuming we set the sink_name to bluealsa.sink.XX_XX_XX_XX_XX_XX.a2dp) we can do:

pw-cli set-param 'bluealsa.sink.XX_XX_XX_XX_XX_XX.a2dp' ProcessLatency '{ ns = 286000000 }'

Automating The Addition Of BlueALSA Devices to PipeWire or PulseAudio

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.

This example uses the program bluealsa-agent from the bluealsa-autoconfig project to deal with the BlueALSA D-Bus interface. Install the following script as a bluealsa-agent command (for example called "pipewire"), and run the bluealsa-agent service with the command line option --status=Running,Delay.

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.

#!/bin/bash

# Some variables which can be used to customise this script.

# Select which device types to exclude from loading into pipewire/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
# For example, to exclude all sources (recommended when using with
# bluealsa-aplay):
#   EXCLUDE="a2dp_source sco_source"
EXCLUDE=

# Change this if you wish to use a different icon for clients such as
# pavucontrol.
ICON_NAME="bluetooth"

# For BlueALSA playback devices only, set this to "yes" to use the most recently
# connected BlueALSA PCM as the PipeWire or PulseAudio default device.
SET_DEFAULT_SINK=

# For BlueALSA capture devices only, setting this to "yes" will enable the
# PipeWire/PulseAudio loopback module to feed the Bluetooth stream to a local
# speaker.
# Note that this rarely works well and is not recommended.
ENABLE_LOOPBACK=

# PipeWire node name or PulseAudio sink name to use for loopback output.
# Leave blank to select the default sink. Must be a valid pipewire node name or
# pulseaudio sink name, for example "alsa_output.pci-0000_00_1b.0.analog-stereo"
LOOPBACK_DEVICE_NAME=

# Directory in which to store state information for each connected device
DATADIR="/tmp/bluealsa-agent/$(basename $0)"

# Do not change anything below this line.

pwcli=$(type -p pw-cli) && "$pwcli" ls Core &>/dev/null || pwcli=

state_file="${2:1}"
state_file="${DATADIR}/${state_file//\//_}"

declare -A pa_format=(
	[U8]=u8
	[S16_LE]=s16le
	[S24_LE]=s24le
	[S32_LE]=s32le
)

set_latency() {
	[[ "$pwcli" ]] || return
	local name="$1"
	if [[ -z "$name" ]] ; then
		local addr="$BLUEALSA_PCM_PROPERTY_ADDRESS"
		local type="$BLUEALSA_PCM_PROPERTY_TRANSPORT_TYPE"
		local mode="$BLUEALSA_PCM_PROPERTY_MODE"
		name="bluealsa.${mode}.${addr//:/_}.$type"
	fi
	local client_delay=$(( BLUEALSA_PCM_PROPERTY_CLIENT_DELAY * 100000))
	local delay=$(( BLUEALSA_PCM_PROPERTY_DELAY * 100000))
	local latency=$((client_delay + delay))
	(( latency > 0 )) || return
	"$pwcli" set-param "$name" ProcessLatency "{ ns = $latency }" &>/dev/null
}

unload_module() {
	read module_id default_id <"$state_file"
	if [[ "$module_id" ]] ; then
		pactl unload-module "$module_id" 2>/dev/null
		rm -f "$state_file"
		if [[ "$default_id" ]] ; then
			pactl set-default-sink "$default_id"
		fi
	fi
}

load_module() {
	[[ "${EXCLUDE,,}" =~ "${BLUEALSA_PCM_PROPERTY_TRANSPORT_TYPE,,}_${BLUEALSA_PCM_PROPERTY_MODE,,}" ]] && return

	[[ -f "$state_file" ]] && unload_module

	[[ -d "$DATADIR" ]] || mkdir -p "$DATADIR" || {
		echo "Cannot create state directory [$DATADIR]" >&2
		return 1
	}

	local addr="$BLUEALSA_PCM_PROPERTY_ADDRESS"
	local type="$BLUEALSA_PCM_PROPERTY_TRANSPORT_TYPE"
	local profile="$BLUEALSA_PCM_PROPERTY_PROFILE"
	local device="bluealsa:DEV=$addr,PROFILE=$type"
	local mode="$BLUEALSA_PCM_PROPERTY_MODE"
	local alias="$BLUEALSA_PCM_PROPERTY_NAME"
	local name="bluealsa.${mode}.${addr//:/_}.$type"
	local description="Bluetooth:\ ${alias// /\\ }\ ($profile)"
	local format="${pa_format[$BLUEALSA_PCM_PROPERTY_FORMAT]}"
	[[ -n "$format" ]] || return
	local channels="$BLUEALSA_PCM_PROPERTY_CHANNELS"
	local rate="$BLUEALSA_PCM_PROPERTY_SAMPLING"

	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'")
	[[ "$module_id" ]] || return

	set_latency "$name"
	default_id=
	if [[ "$SET_DEFAULT_SINK" == yes && "$mode" == "sink" ]] ; then
		default_id="$(pactl get-default-sink)"
		pactl set-default-sink "$name"
	fi

	echo "$module_id $default_id" >"$state_file"

	if [[ "$ENABLE_LOOPBACK" == yes && "$mode" == "source" ]] ; then
		local sink=
		[[ "$LOOPBACK_DEVICE_NAME" ]] && sink="sink='$LOOPBACK_DEVICE_NAME'"
		pactl load-module module-loopback "source_dont_move='true'" "source='$name'" "$sink" "format='$format'" "rate='$rate'" "channels='$channels'" >/dev/null
	fi
}

case "$BLUEALSA_PCM_PROPERTY_TRANSPORT" in
	*-source|*-AG)
		case "$1" in
			"add") load_module ;;
			"remove") unload_module ;;
			"update") set_latency ;;
		esac
		;;
	*)
		case "$1" in
			"update")
				[[ "$BLUEALSA_PCM_PROPERTY_CHANGES" == *RUNNING* ]] || exit
				if [[ "$BLUEALSA_PCM_PROPERTY_RUNNING" == "true" ]] ; then
					load_module
				else
					unload_module
				fi
				;;
			"remove")
				unload_module
				;;
		esac
		;;
esac

See the comments near the top of the script for more information on customising various aspects of the script.

Note

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.

bluealsa-aplay

It is possible to have the sound from bluealsa-aplay played through PipeWire (or PulseAudio), and controlled using PipeWire or PulseAudio clients. Note that both PipeWire and PulseAudio are normally run in the user's session, so bluealsa-aplay must also be run under the user's account to have permission to connect to the audio service.

To run bluealsa-aplay as a user service, create a service unit file called /etc/systemd/user/bluealsa-aplay.service containing:

[Unit]
Description=BlueALSA player service
Documentation=man:bluealsa-aplay(1)
Requisite=dbus.service

[Service]
Type=simple
ExecStart=/usr/bin/bluealsa-aplay --pcm=pipewire --volume=software
Restart=on-failure

[Install]
WantedBy=default.target

To use the PulseAudio ALSA plugin, or to select a specific output device rather than the default, modify the bluealsa-aplay command line. For example:

--pcm=pipewire:NODE=alsa_output.pci-0000_00_1b.0.analog-stereo
--pcm=pulse:DEVICE=alsa_output.pci-0000_00_1b.0.analog-stereo

Both the pipewire and pulse ALSA ctl plugins have one control called Master, but this control does not provide any dB scale information. So bluealsa-aplay is unable to use this control to implement remote volume control.

If you wish to allow the remote device to control the volume in hardware then it is necessary to by-pass the pipewire/pulse Master control and operate the underlying ALSA device control directly. For this to work reliably it is necessary to choose a specific output PCM using the --pcm= option above and then set the --mixer-device= and --mixer-name options appropriately. For example:

bluealsa-aplay --pcm=pipewire:NODE=alsa_output.pci-0000_00_1b.0.analog-stereo --volume=mixer --mixer-device=hw:PCH --mixer-name=Master

or when using PulseAudio:

bluealsa-aplay --pcm=pulse:DEVICE=alsa_output.pci_0000_00_1b.0.analog-stereo --volume=mixer --mixer-device=hw:PCH --mixer-name=Master

To record a Bluetooth stream at the same time as playing it through speakers, use the PulseAudio monitor of the output device. This also works with PipeWire using its PulseAudio compatibility. So if bluealsa-aplay is playing a stream to alsa_output.pci-0000_00_1b.0.analog-stereo then we can record it as

parecord --device alsa_output.pci-0000_00_1b.0.analog-stereo.monitor filename.wav