bluetooth: Unify A2DP sink/source volumes with AVRCP Absolute Volume

Fixes #361 (closed)
Fixes #724 (closed)
Fixes #1057 (closed)


This patchset implements A2DP Absolute Volume control for both sources and sinks, using org.bluez.MediaTransport1. This relies on AVRCP 1.4, which is the first version with the feature.

Absolute Volume works by only having the playback (sink) device apply amplitude scaling. The source device represents the sink volume without scaling anything, to provide the seamless experience we are used to on mobile phones.


Regardless of the streaming direction this volume is exposed by the readwrite Volume property on org.bluez.MediaTransport1. Any volume change on the a2dp source or sink control is written to this property, and likewise volume of a stream is updated when a PropertiesChanged for Volume signal is received.

When pulseaudio plays back audio from an a2dp_source it will apply software volume scaling (pa_source_set_volume) to the source.
When pulseaudio is playing back audio to an a2dp_sink only real_volume (distributed with pa_sink_volume_changed) is used such that the audio remains untouched. Attenuation happens on the sink device.

Feature detection is possible by checking for the Volume property on org.bluez.MediaTransport1, bluez does not expose this on unsupported targets. This is not implemented yet because Volume is often incorrectly unavailable, see issues below.


The original patch from Mathieu Tournier receives Volume changes when pulseaudio is playing back audio from an a2dp_source, but doesn't send any changes back nor is able to control the volume on an a2dp_sink.
(I was unable to extract a proper patch from the pulseaudio-discuss archive email and had to reconstruct it based on the diff. Let me know if this is okay).

The second patch implements volume sending functionality for such an a2dp_sink, and makes sure received Volume change events update the sink real_volume.

Finally, the third patch sends local volume changes back to the a2dp_source, such that it can accurately display the current volume besides controlling it.


  • Is it possible to turn the volume control into mono, while still retaining stereo output? Every time the volume is changed pulse reports it twice, once for every channel. And pa_cvolume_max makes decreasing the volume on a single channel impossible (which would be the inverse when pa_cvolume_min is used).
  • Reading up on passthrough mode, would this make sense for a2dp_sink, to be sure that the volume isn't changed at all?
  • Naming things: Sticking with the code for HFP I opted to use the word gain in most symbols, though dbus speaks about Volume. Is there any preference?
  • Is the current use of pa_cvolume_set/pa_sink_volume_changed/pa_source_set_volume correct?

Remaining issues

  • Controlling an a2dp_source from an Android phone doesn't work yet because android sets its remote to have AVRCP 1.3, causing bluez to reject the volume notification request from the phone. This is an issue in bluez's handling of dual role AVRCP, because the desktop is the remote (that emits audio on the speakers), and the device the target (where the media is playing). One would logically expect the remote version of the desktop to matter, not the phone. I'll report this issue over at bluez and see if I can come up with a fix.
    Tested workarounds: Update the Android BT stack to report AVRCP 1.4, or set target->version = 0x104 (or to AVRCP_CT_VERSION, that is what bluez supports as controller role) right after data_init in target_init.
  • The Volume property is usually not available when there is no org.mpris.MediaPlayer2.Player registered on org.bluez.Media1. This also seems to be a bluez issue as the AVRCP commands definitely arrive. I'll see about resolving this too.
    Workaround: Runing mpris-proxy usually works, or register an inexistent player:
    import dbus, time
    bus = dbus.SystemBus()
    media = dbus.Interface(bus.get_object("org.bluez", "/org/bluez/hci0"), 'org.bluez.Media1')
    path = dbus.ObjectPath('/dummy_player')
    media.RegisterPlayer(path, {})
    while True: time.sleep(10000)
  • Fallback to software volume on an a2dp_sink when the property really isn't available (AVRCP <= 1.3).
  • #724 (comment 214620) mentions in 2. that the master output sink is generally controlled, instead of the a2dp_source volume that is synchronized in this MR. Is this a point to address, or should we expect end-users to adjust their scripts if they desperately want to adjust the shared volume? The shared volume can be changed on the input devices tab of pavucontrol as well.
Edited by Marijn Suijten

Merge request reports