bluetooth: Unify A2DP sink/source volumes with AVRCP Absolute Volume
Fixes #361 (closed)
Fixes #724 (closed)
Fixes #1057 (closed)
What
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.
How
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.
Patches
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.
Questions
- 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 whenpa_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 aboutVolume
. 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 settarget->version = 0x104
(or toAVRCP_CT_VERSION
, that is what bluez supports as controller role) right afterdata_init
intarget_init
. - The
Volume
property is usually not available when there is noorg.mpris.MediaPlayer2.Player
registered onorg.bluez.Media1
. This also seems to be a bluez issue as the AVRCP commands definitely arrive. I'll see about resolving this too.
Workaround: Runingmpris-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 ofpavucontrol
as well.