Skip to content

Instantly share code, notes, and snippets.

What would you like to do?

Headless Linux A2DP server

Date: 2014-10-11
Author: Fredrik Boulund
System: HP Microserver running Debian Linux (Debian 7.5 Wheezy)
TLDR: Short recipe to enable Bluetooth A2DP server that is always visible, with automatic bluetooth device pairing, and automatic pulseaudio module loading on Debian 7.5 wheezy.

Thanks to:
Joerg Schiller (
Domen Puncer (
Daniel Hodgson (
This might also be useful to have a look at:

Note that I spent lots of time on trial and error for this to work, so I might have forgotten some important steps, YMMV. These instructions are mainly for myself to be able to redo everything again, should I ever need to. All instructions assume that pulseaudio already works. Getting it to work was messy for me and I can't really remember how I did it. I use a USB soundcard that required a little fiddling. All commandlines below use # and $ to signify root and regular prompt, respectively.

  1. First of all, install all bluetooth related packages via apt-get, e.g.:
  # apt-get install bluetooth, bluez, bluez-alsa, bluez-gstreamer, bluez-tools, pulseaudio-module-bluetooth

It could be a good idea to toy around abit with hciconfig, hcitool, and perhaps bluetooth-agent to see that you can manually connect a device and set the server's bluetooth device to discoverable etc. Then we can start working to enable Bluetooth A2DP server, automatic bluetooth pairing and continous bluetooth visibility/discoverability, and so that pulseaudio loopback modules are automatically loaded and unloaded when connecting a device.

  1. Edit /etc/bluetooth/audio.conf to enable A2DP server abilities:

Most other recipes I've seen only enable "Source" here, however the A2DP service never worked for me unless all these four were present. Make sure to restart the bluetooth device to reload the configuration:

  # service bluetooth restart
  1. Make the computer discoverable:
  # hciconfig hci0 piscan

This should be run occassionally (every 300 seconds, as this seems to be the maxlimit on "always visible" for Bluetooth devies):

  # while true; do hciconfig hci0 piscan; sleep 300; done

Leave it running in tmux or screen window. Actually, being a bit paranoid about having my server's bluetooth device always visibile, I decided it might be a good idea to print the names of paired devices every now and then to see if I should blacklist some device (blacklist: hcitool block BDADDR). My while loop now looks like this:

  # while true; do clear; date; cat /var/lib/bluetooth/NN:NN:NN:NN:NN:NN/names; hciconfig hci0 piscan; sleep 300; done

This also uses date to show the date and time when the commands were last run.

  1. Run bluetooth-agent to enable automatic pairing (this needs to keep running in a window, use screen or tmux). NNNN is your desired PIN:
  # bluetooth-agent NNNN 

Leave it running in tmux or screen window. This could be in some kind of wrapper to restart it, should something happen with the bluetooth daemon (it stops when the daemon restarts for example).

  1. Download this brilliant Python script that automatically loads the required loopback module into pulseaudio so that audio is actually played via the soundcard sink (also attached to this gist):
    The script has some dependencies as well (I'm guessing python-gi, python-dbus), but they were covered for me. This script needs to run as a regular user (at least on my system, because root cannot use pulseaudio) and it must be left running, i.e. use screen or tmux. The manual way to do this would be something like this (after bluetooth device connected):
  $ pactl list sources
  $ pactl load-module module-loopback source=N   (N is the number of the source found when listing sources)

Then it magically starts playing. However, when you finish playing back audio the module must be unloaded:

  $ pactl list short modules
  $ pactl unload-module NN    (NN is the number of the loopback module that was loaded, should be last in the list of the previous command)
  1. Open an ssh connection in a tmux or screen session with the same user as the one running the Python script. This is ugly-hacky but if the user isn't logged in the sound will cut out a couple of seconds after connecting each time. I have no idea why this is the case, but after lots of testing I noticed it just works if you leave the user logged in. I think a smart way to go about this could be to have the terminal in this ssh connection actually run the Python script, so you don't need four tmux panes.


When everything is up an running there should be three panes in a tmux (or screen) session:

  1. A root prompt running while true; do hciconfig hci0 piscan; sleep 300; done, to continously make your device discoverable.
  2. A root prompt running bluetooth-agent NNNN where NNNN is your PIN, to enable devices to connect using this PIN.
  3. A user prompt running, to load and unload the loopback module in pulseaudio whenever a device connects/disconnects.
  4. A user prompt running ssh localhost, just sitting there being logged in (or use this to run the Python script).

Additional notes

It appears I had to run pactl load-module module-loopback source=bluez_source.XX_XX_XX_XX_XX_XX at least once for it to work automatically later.

# based on monitor-bluetooth
# Changes by Domen Puncer <> (
# This code was taken from Daniel Hodgson's Gists (
import gobject
import dbus
import dbus.mainloop.glib
import os
def property_changed(name, value, path, interface):
iface = interface[interface.rfind(".") + 1:]
val = str(value)
print "{%s.PropertyChanged} [%s] %s = %s" % (iface, path, name, val)
# we want this event: {Control.PropertyChanged} [/org/bluez/16797/hci0/dev_00_24_7E_51_F7_52] Connected = true
# and when that happens: pactl load-module module-loopback source=bluez_source.00_24_7E_51_F7_52
if iface == "Control" and name == "Connected" and val == "1":
bt_addr = "_".join(path.split('/')[-1].split('_')[1:])
cmd = "pactl load-module module-loopback source=bluez_source.%s" % bt_addr
# here we want this event: {Control.PropertyChanged} [/org/bluez/16797/hci0/dev_00_24_7E_51_F7_52] Connected = false
# and when that happens, we unload all loopback modules whose source is our bluetooth device
elif iface == "Control" and name == "Connected" and val == "0":
bt_addr = "_".join(path.split('/')[-1].split('_')[1:])
cmd = "for i in $(pactl list short modules | grep module-loopback | grep source=bluez_source.%s | cut -f 1); do pactl unload-module $i; done" % bt_addr
def object_signal(value, path, interface, member):
iface = interface[interface.rfind(".") + 1:]
val = str(value)
print "{%s.%s} [%s] Path = %s" % (iface, member, path, val)
if __name__ == '__main__':
bus = dbus.SystemBus()
bus.add_signal_receiver(property_changed, bus_name="org.bluez", signal_name = "PropertyChanged", path_keyword="path", interface_keyword="interface")
mainloop = gobject.MainLoop()

This comment has been minimized.

Copy link

@erotavlasme erotavlasme commented Apr 21, 2015

do you know if the script works also for Debian 8 Jessie? Since the packages bluez-gstreamer and bluez-alsa are available only in the repositories of Debian Wheezy 7.5.
Do you know if the script works also for Raspbian?

Thank you


This comment has been minimized.

Copy link
Owner Author

@boulund boulund commented Sep 24, 2015

Hi, I don't know if it runs on Debian 8 Jessie or Raspbian as well. I guess it should but the only way to make sure is to try. Please report back if you tried it! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment