bluetooth: Delay A2DP Absolute Volume setup until property is available
authorMarijn Suijten <marijns95@gmail.com>
Sat, 27 Mar 2021 13:05:00 +0000 (14:05 +0100)
committerPulseAudio Marge Bot <pulseaudio-maintainers@lists.freedesktop.org>
Mon, 17 May 2021 14:50:03 +0000 (14:50 +0000)
The Volume property on org.bluez.MediaTransport1 is required to utilize
Absolute Volume, but it will only become availabe if the peer device
supports the feature.  This happens asynchronously somewhere after the
transport itself has been acquired, after which the callbacks are
attached and software volume is reset.

To prevent race conditions availability of the property is also checked
on startup through a "Get" call.

Part-of: <https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/merge_requests/239>

src/modules/bluetooth/bluez5-util.c
src/modules/bluetooth/bluez5-util.h
src/modules/bluetooth/module-bluez5-device.c

index c442ba6..f24e12e 100644 (file)
@@ -606,6 +606,16 @@ static void pa_bluetooth_transport_remote_volume_changed(pa_bluetooth_transport
             return;
         t->sink_volume = volume;
         hook = PA_BLUETOOTH_HOOK_TRANSPORT_SINK_VOLUME_CHANGED;
+
+        /* A2DP Absolute Volume is optional.  This callback is only
+         * attached when the peer supports it, and the hook handler
+         * further attaches the necessary hardware callback to the
+         * pa_sink and disables software attenuation.
+         */
+        if (!t->set_sink_volume) {
+            pa_log_debug("A2DP sink supports volume control");
+            t->set_sink_volume = pa_bluetooth_transport_set_sink_volume;
+        }
     } else {
         pa_assert_not_reached();
     }
@@ -724,6 +734,78 @@ static void bluez5_transport_release_cb(pa_bluetooth_transport *t) {
         pa_log_info("Transport %s released", t->path);
 }
 
+static void get_volume_reply(DBusPendingCall *pending, void *userdata) {
+    DBusMessage *r;
+    DBusMessageIter iter, variant;
+    pa_dbus_pending *p;
+    pa_bluetooth_discovery *y;
+    pa_bluetooth_transport *t;
+    uint16_t gain;
+    pa_volume_t volume;
+
+    pa_assert(pending);
+    pa_assert_se(p = userdata);
+    pa_assert_se(y = p->context_data);
+    pa_assert_se(t = p->call_data);
+    pa_assert_se(r = dbus_pending_call_steal_reply(pending));
+
+    if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+        pa_log_error(DBUS_INTERFACE_PROPERTIES ".Get %s Volume failed: %s: %s",
+                     dbus_message_get_path(p->message),
+                     dbus_message_get_error_name(r),
+                     pa_dbus_get_error_message(r));
+        goto finish;
+    }
+    dbus_message_iter_init(r, &iter);
+    pa_assert(dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_VARIANT);
+    dbus_message_iter_recurse(&iter, &variant);
+    pa_assert(dbus_message_iter_get_arg_type(&variant) == DBUS_TYPE_UINT16);
+    dbus_message_iter_get_basic(&variant, &gain);
+
+    if (gain > A2DP_MAX_GAIN)
+        gain = A2DP_MAX_GAIN;
+
+    pa_log_debug("Received A2DP Absolute Volume %d", gain);
+
+    volume = a2dp_gain_to_volume(gain);
+
+    pa_bluetooth_transport_remote_volume_changed(t, volume);
+
+finish:
+    dbus_message_unref(r);
+
+    PA_LLIST_REMOVE(pa_dbus_pending, y->pending, p);
+    pa_dbus_pending_free(p);
+}
+
+static void bluez5_transport_get_volume(pa_bluetooth_transport *t) {
+    static const char *volume_str = "Volume";
+    static const char *mediatransport_str = BLUEZ_MEDIA_TRANSPORT_INTERFACE;
+    DBusMessage *m;
+
+    pa_assert(t);
+    pa_assert(t->device);
+    pa_assert(t->device->discovery);
+
+    pa_assert(t->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK || t->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE);
+
+    pa_assert_se(m = dbus_message_new_method_call(BLUEZ_SERVICE, t->path, DBUS_INTERFACE_PROPERTIES, "Get"));
+    pa_assert_se(dbus_message_append_args(m,
+        DBUS_TYPE_STRING, &mediatransport_str,
+        DBUS_TYPE_STRING, &volume_str,
+        DBUS_TYPE_INVALID));
+
+    send_and_add_to_pending(t->device->discovery, m, get_volume_reply, t);
+}
+
+void pa_bluetooth_transport_load_a2dp_sink_volume(pa_bluetooth_transport *t) {
+    pa_assert(t);
+
+    if (t->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK)
+        /* A2DP Absolute Volume control (AVRCP 1.4) is optional */
+        bluez5_transport_get_volume(t);
+}
+
 static ssize_t a2dp_transport_write(pa_bluetooth_transport *t, int fd, const void* buffer, size_t size, size_t write_mtu) {
     ssize_t l = 0;
     size_t written = 0;
@@ -2114,7 +2196,6 @@ static DBusMessage *endpoint_set_configuration(DBusConnection *conn, DBusMessage
     t = pa_bluetooth_transport_new(d, sender, path, p, config, size);
     t->acquire = bluez5_transport_acquire_cb;
     t->release = bluez5_transport_release_cb;
-    t->set_sink_volume = pa_bluetooth_transport_set_sink_volume;
     /* A2DP Absolute Volume is optional but BlueZ unconditionally reports
      * feature category 2, meaning supporting it is mandatory.
      * PulseAudio can and should perform the attenuation anyway in
index c08a7a3..762ec50 100644 (file)
@@ -192,6 +192,7 @@ void pa_bluetooth_transport_set_state(pa_bluetooth_transport *t, pa_bluetooth_tr
 void pa_bluetooth_transport_put(pa_bluetooth_transport *t);
 void pa_bluetooth_transport_unlink(pa_bluetooth_transport *t);
 void pa_bluetooth_transport_free(pa_bluetooth_transport *t);
+void pa_bluetooth_transport_load_a2dp_sink_volume(pa_bluetooth_transport *t);
 
 bool pa_bluetooth_device_any_transport_connected(const pa_bluetooth_device *d);
 bool pa_bluetooth_device_switch_codec(pa_bluetooth_device *device, pa_bluetooth_profile_t profile, pa_hashmap *capabilities_hashmap, const pa_a2dp_endpoint_conf *endpoint_conf, void (*codec_switch_cb)(bool, pa_bluetooth_profile_t profile, void *), void *userdata);
index fa29ef4..5dfe576 100644 (file)
@@ -1688,6 +1688,22 @@ static int start_thread(struct userdata *u) {
         if (u->bt_codec)
             pa_proplist_sets(u->card->proplist, PA_PROP_BLUETOOTH_CODEC, u->bt_codec->name);
 
+    /* Now that everything is set up we are ready to check for the Volume property.
+     * Sometimes its initial "change" notification arrives too early when the sink
+     * is not available or still in UNLINKED state; check it again here to know if
+     * our sink peer supports Absolute Volume; in that case we should not perform
+     * any attenuation but delegate all set_volume calls to the peer through this
+     * Volume property.
+     *
+     * Note that this works the other way around if the peer is in source profile:
+     * we are rendering audio and hence responsible for applying attenuation.  The
+     * set_volume callback is always registered, and Volume is always passed to
+     * BlueZ unconditionally.  BlueZ only sends a notification to the peer if it
+     * registered a notification request for absolute volume previously.
+     */
+    if (u->transport && u->sink)
+        pa_bluetooth_transport_load_a2dp_sink_volume(u->transport);
+
     return 0;
 }