Bluez A2DP AudioSink for ALSA

Ok, here is the promised follow up for my previous post.

I call it A2DP AudioSink for ALSA because at the moment that's all it can handle (which means it will not support HFP devices such as handsfree headset etc). That would not be necessary anyway because the existing ALSA PCM plugin (if you run bluez in socket mode) already supports bi-directional streaming with these devices. It is A2DP which is a problem.

Despite my rants about the quality of bluez DBus API documentation, it is actually quite complete and thorough when it comes to listing the available functions and their parameters. So I will not repeat that information here; I suggest that you download bluez 4.101 source tarball and look at its /doc directory, particularly audio.txt and media.txt (you can look at it online too here).

Instead, I will summarise the critical missing information that is necessary for your to build your own A2DP Sink/Source.

Let's start with the sequence of events that happens from the time of application startup, device connection and disconnection, until application shutdown. Instead of "A2DP AudioSink for ALSA" which is a mouthful, I will just call it as "your application" (or "your app", or even "you" for short) - I'm assuming here that you're reading this because you want to do your own stuff. Otherwise why bother, right?

Ok, here we go.



First, the caveat: the "bluetooth device" <--> "bluez" part of that diagram must be taken with a grain of salt, as it is not accurate. If you want to know the details you need to consult A2DP and GAVPD specifications. It's there so that you can see the big picture of what is happening.

You can see that there are 3 levels of events that happen during the lifetime of the your app. I have marked these as A, B, and C. Level A are the highest level events, these are startup/shutdown events and activities. Level B are events and actions that you must do when a remote bluetooth device is connected or disconnected. Level C are the actions you must do to carry out the actual audio streaming.


"Level A" events/actions (A1 and A2)

There are the events/actions you must do/handle when your application is starting up or shutting down. These actions/events only need to done once.

A1. Application Startup. Upon starting up, you need to tell bluez that your app will handle A2DP Sink or Sources for it. You do it by calling org.bluez.Media.RegisterEvent with the appropriate parameters, mainly UUID, Codec, and Capabilities. Bluez documentation doesn't make it clear, but you cannot just plug arbitrary made-up values here. "UUID" must be one of the pre-defined "Service Class identifiers" (from here), you want either AudioSource or AudioSink UUID. "Codec" must be one of the available supported codecs from A2DP specification, and the "Capabilities" must be filled with the particular codec's capabilities that you want to support.

If the registration is successful, you'll get an empty reply otherwise you'll get an error.

A2. Application Termination. Assuming you have successfully registered, bluez will notify you that your registration has been cancelled. This usually only happens the the bluetooth daemon itself is about to shutdown. Bluez does it by calling org.bluez.MediaEndpoint.Release method, which you must implement and handle (don't you wish now that bluez documentation differentiates between real "API" calls and "callback" interfaces, like this one? ). At this stage you don't need to de-register or do any other cleanup with bluez, you just need to clean-up your own resources. Reply with a blank message, and after that you are free to terminate your app.


"Level B" events/actions (B1 and B2)

There are the events/actions that happen / you must do when remote bluetooth devices get connected. It can happen multiple times within the lifetime or your app (ie between Level A events), for the same devices (in pairs), and for different devices (may be overlapping).

B1. Device Connection Events happen when a remote bluetooth device is connected. Assuming that your registration is successful, bluez will call your app again when an A2DP device is trying to connect to the computer. It does it using org.bluez.MediaEndpoint.SelectConfiguration. You will need to implement this method and interface and handle the call. Through this call, bluez will pass you some "Capabilities" codec parameters from the other end. You are supposed to compare this with your own capabilities and choose the best match that provide the highest quality audio. Your reply to bluez will contain the this chosen configuration.

If everything is all right, bluez will then call your app again, using org.bluez.MediaEndpoint.SetConfiguration. The parameter to this call should contain exactly the same codec parameters you gave back earlier in your reply to "SelectConfiguration". Among other things, the most important thing you must do here is this: you must record the "transport path" given as parameter of this call. It is a unique object path that you need to pass along to org.bluez.MediaTransport.Acquire to get the file descriptor you need to use for the actual streaming. If you don't keep that path, you can't find it again. All being good, you reply with empty message.

B2. Device Disconnection Events happen the remote bluetooth device is disconnected. Bluez will call you on org.bluez.MediaEndpoint.ClearConfiguration method. You are supposed to clear any of your resources you keep for that particular bluetooth device connection (ie, that particular "transport path"). Reply with a blank message.


"Level C" events/actions (C1 and C2)

These are the events/actions that happen / you must do to do the actual audio streaming. It can happen multiple times within "Level B" events for the same remote device, usually in pairs.

C1. Start streaming event. To detect this event, you must listen to org.bluez.AudioSource.PropertyChanged signal and keep track of its "State" property. The "start streaming" event happens when the state changes from "connected" to "playing". (There are a few other events too, which may be interesting for other purposes but not for us).

When this happens, you need to call org.bluez.MediaTransport.Acquire. Bluez will give you a file descriptor that you can read from, as well as its Read MTU (maximum transfer unit) - which is how big each packet would be. From here onwards, you can read this descriptor to obtain the A2DP packet, decode it, and output it. The Read MTU helps to determine how big a buffer you need to allocate. Note that the read isn't always successful, you must allow for error conditions such as EAGAIN because your CPU will be much faster at reading than what bluetooth (and the remote device) can send.

C2. Stop streaming event. Like "start streaming event", you can't decide this from org.bluez.AudioSource.PropertyChanged signal alone; you need to detect the transition, which is "playing" to "connected". When this happens, you need to call org.bluez.MediaTransport.Release to release the transport back to bluez. In my tests, this is not strictly necessary but it is the polite way of doing it. It is also good for you to detect this event so that you can can tell our "streaming" function to stop its work and rest for a while.

That's it! Easy peasy eh?


How about A2DP Source?

The events described above are to for you to make your computer act as A2DP Sink (or "Source", in bluez' parlance). What about building A2DP Source (the computer to send audio data to bluetooth speakers)? As it turns out, the sequence of events is exactly the same with very minor change:
1. Instead of AudioSource.PropertyChange, you need to listen to AudioSink.PropertyChange.
2. The transition you need to detect is a bit different - instead of "connected" -> "playing" (and vice versa), you listen to "disconnected" -> "connected" (and vice versa).
3. You write to the descriptor with encoded data instead of reading from it.


About The code

In the source, I create a thread for doing the actual streaming (reading/writing to the file descriptors). I create the thread when I received B1 event (SetConfiguration) but they are suspended until I receive C1 event - that is, after I have completed MediaTransport.Acquire call to get the file descriptor. I suspend the thread again when I receive C2 event, and only when I receive B2 event (ClearConfiguration) I terminate the thread.

The rest is straightforward. The code implements both Sink and Source. As you can see, the difference in handling is minimal.

The code is provided as an illustration and working example. It skimps on error checking; it focuses neither on performance nor robustness, but more on the working (and hopefully correct) way of handling A2DP connection under bluez. That being said, I find that the Sink is good enough, while the Source is a bit unsatisfactory. There is a README inside the tarball that shows how you can setup ALSA asoundrc for use with the A2DP Source so that it can act as a poor man's ALSA PCM plugin.

As usual, the code is released under GNU GPL Version 3 or later unless the bits that I took from PulseAudion and bluez itself (SBC stuff, SBC setup stuff, and actual A2DP packet encoding/decoding) - they are licensed as per the original PulseAudio and bluez licenses.

Get it from here.


Bluez 5 and beyond

Question: Bluez 4.x is already obsolete by now. What do I have to do to get this example to work with bluez 5?

Answer: A lot of work. I have not investigated bluez 5 version of this fully as I'm quite satisfied with bluez 4 for now. But from what I have gathered, the sequence of events is identical. Sure the DBus interfaces change their names (bluez 5 add "1" to the interface names, e.g. "org.bluez.MediaEndpoint" becomes "org.bluez.MediaEndpoint1"); and the signals change their skins too (AudioSource/AudioSink are gone, replaced by generic org.freedesktop.Properties.PropertyChanged, and you can probably decide whether to start/stop streaming directly from the state instead of having to watch the transitions), but the underlying events are still the same.

Conclusion

A2DP is just a small part of Bluetooth specification. If you look at the links I gave earlier, you will see Bluetooth comes with over two dozen "profiles" (ie, functionalities). Bluez doesn't implement all of them (although the unimplemented list is getting smaller very day, thanks for the very hard work of bluez developers), which is fine, but bluez could really do better with its documentation. At least give us userspace programmers something to get around our head on. Until that happens, I still consider that "bluez is one of the best kept secrets in Linux".



Posted on 25 Jun 2013, 6:33 - Categories: Linux Bluetooth
Edit - Delete


Comments:

Posted on 26 Jun 2013, 9:01 by cr
"File Missing"
This is interesting, would like to try it out. The get it now file link seems to be broken.
Delete

Posted on 27 Jun 2013, 4:22 by jamesbond
"Oops, fixed"
Thanks, I fixed the download link. It should work now.
Delete

Posted on 27 Sep 2013, 22:30 by dennis
"how to adjust volume "
Hi
do you know how to adjust volume via any tool, I tried use dbus command VolumeUP sending to device, but not all of device accept this method, if use amixer, it need ctl_bluetooth, but not support now. If don't use pulseaudio, I don't know how to adjust volume. Do you have any idea. Thanks
Delete

Posted on 31 Sep 2013, 3:55 by jamesbond
"Re: how to adjust volume"
Yes, the proper way is to use VolumeUp but it doesn't always work as you found out. Ctl_bluetooth is part of the "Socket" interface so once you switch to Dbus interface, it's gone.

An alternative is to use ALSA softvol plugin, see here and here.

Good luck.
Delete

Posted on 21 Mar 2014, 1:15 by MrUser
"Not running select_config"
Hello James,

Your blog posts about a2dp streaming from a BT device to a BT speaker have been some of the only useful documents I have found on the interwebs regarding this project. I have downloaded your source code and compiled it, but am having some difficulties with the streaming. How can I contact you to ask questions? (after this one)
Delete

Posted on 24 Mar 2014, 16:52 by jamesbond
"Re: Not running select_config"
Please post them here.
Delete

Posted on 24 Mar 2014, 19:42 by MrUser
"Re: Not running select_config"
I posted the question on StackOverflow:http://stackoverflow.com/questions/22565145/is-bluez-bluetoothd-not-sending-dbus-method-call-selectconfiguration-upon-conn

Thank you for the quick response.
Delete

Posted on 8 Apr 2014, 2:03 by MrUser
"Bluez5 and PulseAudio 5"
Hey James,

I finally found success by updating to BlueZ 5 and PulseAudio 5. BlueZ 5 moved A2DP Profile implementation to third parties, such as PA or JACK. I was able to get audio to stream from my phone to the computer then to a BT speaker just using the $pactl load-module module-loopback command. Your tutorial was very informative, though. Thanks again!
Delete

Posted on 26 Apr 2014, 9:27 by Corrosion
"A2DP source"
Great article. Could you give a few more details on how to get the transport fd when trying to stream to remote BT device... I suppose "your_app" should call org.bluez.AudioSource.Connect, right? Not the remote device... I've been playing with that scenario but bluez never calls my app's MediaEndpoint Select and Set configuration. Where is the fd to write to supposed to come from?

Thanks!
Delete

Posted on 11 May 2014, 5:42 by ThePlumber
"Almost there!"
Greetings and fond thanks. Your write-up and source has been illuminating and where it doesn't work, it's clearly bluez' fault!

Specifically: Using a combination of "dantheman"s instructions here: http://www.instructables.com/id/Turn-your-Raspberry-Pi-into-a-Portable-Bluetooth-A/

... which use pulseaudio, and your sink program, I was streaming at least into a wav file. The problem I have is that when bluez is set to

Enable=Source,Socket,

it doesn't give a2dp-sink the SelectConfiguration call; it must be using the old deprecated socket interface. But when I tell it

Enable=Source

alone, bluez doesn't advertise itself as a sink!
Do you have an example bluez configuration that you could share?

If you're interested, I can gather a tarball of all I think might be relevant on my Pi system, A couple of interesting tidbits:

In /usr/lib/udev/bluetooth, under "$ACTION" = "add":

/usr/local/bin/a2dp-alsa --sink | /usr/local/bin/pifm - 90.7 44100 stereo 2>&1 >> /var/log/bluetooth_dev

... My car has no line in or any other such niceties...

Also, from /var/log/bluetooth_dev:

/usr/local/bin/a2dp-alsa --sink | /usr/local/bin/pifm - 90.7 44100 stereo
run sink
a2dp started
get_system_bus : Name :1.28
Getting object path for adapter (null)
Object path for (null) is /org/bluez/3125/hci0
CALL media_register_endpoint
RX DBUS MSG: signal from member NameAcquired
RX DBUS MSG: signal from member PropertyChanged
CALL audiosource_property_changed : state for /org/bluez/3125/hci0/dev_DE_AD_BE_EF_A3_13: disconnected
RX DBUS MSG: method_call from member Release
Release endpoint
CALL endpoint_release
a2dp ended

Cheers!
Mike
Delete

Posted on 20 May 2014, 5:06 by ThePlumber
"There now!"
So, don't I feel dumb.

I realize now that the udev triggered /usr/lib/udev/bluetooth mechanism isn't necessary to a2dp-alsa.c.

Between bluez and bluetooth-agent, I can connect my phone to the Pi and stream audio to it. All I needed to do to turn your example into a bluetooth FM transmitter was a couple of lines:

- Add FILE* FMOUT to io_thread_tcb_s

- At the top of io_thread_run(), open the process and a pipe:
data->FMOUT = popen ( "/usr/local/bin/pifm - 103.3 44100 stereo", "w" );

- In stream_bt_input(), instead of writing to stdout, write to FMOUT.

- on IO_THREAD_TERMINATE, pclose (data->FMOUT);

I also noticed that I never get a SelectConfiguration message; it goes straight to SetConfiguration.

Thanks very much for the great tools.

Cheers,
Mike

Delete

Posted on 16 Sep 2014, 19:43 by User
"A question about connecting"
Thanks for your excellent article.
But I have a question about AudioSource connect.
I don't want use the command
dbus-send --system --dest=org.bluez /org/bluez/[bluetoothd-pid]/hci0/dev_XX_XX_XX_XX_XX_XX org.bluez.AudioSource.Connect


And I use my phone to connect my computer(runs a2dp-alsa --sink) directly, but I can't see any "connected" state:
state for /org/bluez/923/hci0/dev_CC_AF_78_F2_12_D8: connecting

state for /org/bluez/923/hci0/dev_CC_AF_78_F2_12_D8: disconnected

This is my bluetoothd log:

bluetoothd[923]: plugins/hciops.c:link_key_request() Matching key found
bluetoothd[923]: plugins/hciops.c:link_key_request() link key type 0x04
bluetoothd[923]: audio/avdtp.c:avdtp_confirm_cb() AVDTP: incoming connect from CC:AF:78:F2:12:D8
bluetoothd[923]: Can't find device agent
bluetoothd[923]: audio/avdtp.c:avdtp_unref() 0x403d9010: ref=0
bluetoothd[923]: audio/avdtp.c:avdtp_unref() 0x403d9010: freeing session and removing from list
bluetoothd[923]: audio/avdtp.c:session_cb()
bluetoothd[923]: plugins/hciops.c:link_key_request() hci0 dba CC:AF:78:F2:12:D8
bluetoothd[923]: plugins/hciops.c:get_auth_info() hci0 dba CC:AF:78:F2:12:D8
bluetoothd[923]: plugins/hciops.c:link_key_request() kernel auth requirements = 0x04 >bluetoothd[923]: plugins/hciops.c:link_key_request() Matching key found
bluetoothd[923]: plugins/hciops.c:link_key_request() link key type 0x04
bluetoothd[923]: audio/avdtp.c:avdtp_confirm_cb() AVDTP: incoming connect from CC:AF:78:F2:12:D8
bluetoothd[923]: Can't find device agent
bluetoothd[923]: audio/avdtp.c:avdtp_unref() 0x403d9010: ref=0
bluetoothd[923]: audio/avdtp.c:avdtp_unref() 0x403d9010: freeing session and removing from list
bluetoothd[923]: audio/avdtp.c:session_cb()
bluetoothd[923]: plugins/hciops.c:link_key_request() hci0 dba CC:AF:78:F2:12:D8
bluetoothd[923]: plugins/hciops.c:get_auth_info() hci0 dba CC:AF:78:F2:12:D8
bluetoothd[923]: plugins/hciops.c:link_key_request() kernel auth requirements = 0x04 />

Delete

Posted on 27 Sep 2014, 4:58 by jamesbond
"RE: A question about connecting"
It has been over a year since I've looked at this so memory is not so clear anymore. But I do have some questions - if you don't use dbus-send, then how to you connect your phone to the PC? Also, have you paired your phone with the PC? You need to do both of this first before you can get a successful A2DP connection.
Delete

Posted on 16 Jan 2016, 02:17 by pberndt
"Patch for Bluez 5"
Thanks a lot for this tool! I've been trying to write something similar years ago, 2006'ish, and failed miserably - the documentation situation has clearly improved ;-)

I made the modifications you suggested for Bluez 5 support and wanted to report back that despite them also having removed the org.bluez.Manager interface (which means you'd have to use Dbus Introspection to get the correct device path), and a changed signature for Acquire/Release, you already listed all the ingredients.

Here's a quick & dirty patch, with which I have been able to make this work in Raspian on a Raspberry Pi 2, where your solution runs by far smoother then the Pulseaudio based alternative:

https://gist.github.com/phillipberndt/fcb01bad5cd18b4ebb2f

(You wouldn't want to merge this, I didn't bother to retain Bluez 4 support or work around the removed Manager interface, and I didn't test source support yet.)

- Phillip Berndt
Delete

Posted on 20 Jan 2016, 03:49 by jamesbond
"RE: bluez5 patch"
Phillip - thank you for your report, and for the patch. I'll keep the patch and see if I can merge it.

Another thing that has been on my mind is to make an ALSA PCM driver out of a2dp-alsa; I have been thinking whether it is worth doing since bluez4 is now 3 years deprecated. Your report and patch however confirm that it may be worth it because moving it to bluez5 won't be too difficult once the bluez4 version is done.

By the way, did you test the patch on the original a2dp-release (which you can get from this page) or the updated one, from the wiki (Articles) page? The updated one fixes some buffering issue; it's mainly a problem for source though, not sink.

Delete

Posted on 4 Feb 2016, 18:37 by Blaudio3183685
"Bluez 5"
This is the single best documentation for bluez audio I can find. Kudos!

I'm also very interested in getting bluez 5 to work with plain old alsa. I haven't made either source or sink work with Phillip's patch (on a laptop, not a Raspberry Pi though).

For sink mode (which I thought would be easier to get working since I don't have to format any input), after connecting using `connect` from `bluetoothctl`, the device disconnects. I don't receive any of SetConfiguration or SelectConfiguration like in ThePlumber's issue.

All I get is UnitNew and then org.bluez.Device1's Connected property being set to true (and then false shortly thereafter).

For source, it does connect and stay connected but `a2dp-alsa` never calls `Acquire` and so is stuck on idle.

And what are the new methods from bluez 5 supposed to do anyways? Like Device1.Connect()?

By the way, I noticed something odd in `a2dp-alsa.c`



if (dbus_message_is_signal (msg, "org.freedesktop.DBus.Properties", "PropertiesChanged")) // bt --> alsa
audiosource_property_changed (system_bus, msg, 0, &io_threads_table);
else if (dbus_message_is_signal (msg, "org.freedesktop.DBus.Properties", "PropertiesChanged")) // alsa --> bt
audiosink_property_changed (system_bus, msg, 1, &io_threads_table);


The two checks are exactly the same so the second branch is never executed! Fortunately the two functions `audiosource_property_changed` and `audiosink_property_changed` are actually the same from the define. But I found this confusing.
Delete

Posted on 9 Feb 2016, 17:08 by Phillip2
"Re"
@jamesbond: I used the original release from this page.

@Blaudio3183685: The code is from my patch. As I said, I haveb't tested/fixed source support. Indeed, there would have to be another test to fix this; I don't know which one would do. As for your connect/disconnect issue: I observed the immediate disconnect behaviour if a2dp-alsa didn't work/run. It might be useful to add lots of debug output, and run dbus-monitor --system in a second terminal.
Delete

Posted on 4 Mar 2016, 04:51 by Blaudio3183685
"Re: Re"
sudo dbus-monitor --system | tee dbus.log

Sorry, I missed that about the a2dp source the first time around.

Actually, `dbus-print-message.h` doesn't catch all messages being passed and I can't figure out what's wrong. BUT, I've done way worse than that and actually included a hacked up version of `dbus-print-message.h` and `dbus-print-message.c` from dbus-monitor itself so I could see exactly what `a2dp-alsa` was sending and receiving (with calls to `print_message`).

Unfortunately its been a month since I more or less gave up on this so I might get some details wrong. Here's the last version of the modified a2dp-alsa.c I have in case anyone else is interested.

a2dp-alsa.c http://paste.debian.net/411478/
dbus-print-message.h http://paste.debian.net/411480/
dbus-print-message.c http://paste.debian.net/411481/

These might be in some intermediate testing version. I didn't use version control (bad, I know).

What do you mean by didn't work/run? It was certainly running and doing things but of course I didn't know if those were the right things or not.
Delete

Posted on 4 Mar 2016, 04:56 by Blaudio3183685
"Typos"
I can't seem to be delete my last reply to correct it ("Wrong password").

Actually, `dbus-print-message.h` doesn't catch all messages

Should be

Actually, `sudo dbus-monitor --system` doesn't catch all messages
Delete



Add Comment

Title
Author
 
Content
Show Smilies
Security Code 7938149
Mascot of Fatdog64
Password (to protect your identity)