Commit 3953d445 authored by Marijn Suijten's avatar Marijn Suijten 🦀
Browse files

bluetooth: Handle muting over A2DP Absolute Volume

Muting is a special case that is explicit and separated from volume
within PulseAudio, but merged together into a single variable over the
A2DP AVRCP connection.
Most devices report either 0 or 1 as their lowest volume.

Note that capital Volume in the following paragraphs refer to the Volume
property on org.bluez.MediaTransport1.

This commit deals with the following cases:

1. When the PA stream is muted, notify the peer by setting Volume to 0.
   While the stream is muted real_volume should _not_ be updated in the
   callback when Volume changes to <= 1, or unmuting will _not_ return
   to the original volume.

2. When locally changing stream volume, and muting is enabled, do _not_
   send updates to the peer.  The peer should stick with Volume = 0.

3. When locally changing stream volume (and sending that to the peer)
   any resulting updates on the Volume property should _not_ turn on
   muting.

4. When the peer changes Volume, turn off muting (if enabled) when
   Volume > 1.

5. When the peer changes Volume, turn on muting when Volume <= 1.

Such an implementation matches what happens on an Android device.
Muting sets the shared volume to 0, and upping the volume on the peer
(in case of headphones which usually only have up/down buttons) will set
it a single increment above that.  Unmuting the stream from PA however
will return the stream back to the original volume, and notify the peer
of the same.

The discrepancy between merged and separate muting+volume results in a
conflict between point 3. and 5.: The peer (and/or dbus API) always
responds with the Volume property changing after PA writes it.  If PA's
stream volume is decreased to a point where the callback triggers with
Volume <= 1 the stream is consequently muted, in accordance to point 5.
This is especially problematic when decreasing the local volume too far
(whether intentionally or not), as muting is not turned off by default
when the stream volume is increased afterwards.

This case is dealt with by preventing any Volume lower than 2 to be sent
to the device at any given point.  Only the device itself can return a
Volume lower than that to enable implicit muting.
However, when point 5. happens, it is important to note that it can only
be unmuted by explicitly unmuting the stream from PA, _or_ increasing
the volume on the peer.
parent 5af2afba
Pipeline #515008 passed with stages
in 5 minutes and 1 second
......@@ -103,11 +103,7 @@
"</node>"
static pa_volume_t a2dp_gain_to_volume(uint16_t gain) {
pa_volume_t volume = (pa_volume_t) ((
gain * PA_VOLUME_NORM
/* Round to closest by adding half the denominator */
+ A2DP_MAX_GAIN / 2
) / A2DP_MAX_GAIN);
pa_volume_t volume = A2DP_GAIN_TO_VOLUME(gain);
if (volume > PA_VOLUME_NORM)
volume = PA_VOLUME_NORM;
......@@ -116,11 +112,7 @@ static pa_volume_t a2dp_gain_to_volume(uint16_t gain) {
}
static uint16_t volume_to_a2dp_gain(pa_volume_t volume) {
uint16_t gain = (uint16_t) ((
volume * A2DP_MAX_GAIN
/* Round to closest by adding half the denominator */
+ PA_VOLUME_NORM / 2
) / PA_VOLUME_NORM);
uint16_t gain = VOLUME_TO_A2DP_GAIN(volume);
if (gain > A2DP_MAX_GAIN)
gain = A2DP_MAX_GAIN;
......
......@@ -54,9 +54,22 @@
#define PA_BLUETOOTH_UUID_HFP_HF "0000111e-0000-1000-8000-00805f9b34fb"
#define PA_BLUETOOTH_UUID_HFP_AG "0000111f-0000-1000-8000-00805f9b34fb"
#define A2DP_MAX_GAIN 127
#define HSP_MAX_GAIN 15
#define A2DP_MAX_GAIN 127
/* Some devices only go as low as 1 */
#define A2DP_MUTE_GAIN 1
#define A2DP_MIN_GAIN (A2DP_MUTE_GAIN + 1)
/* Round to closest by adding half the denominator */
#define A2DP_GAIN_TO_VOLUME(gain) \
((pa_volume_t)(((gain)*PA_VOLUME_NORM + A2DP_MAX_GAIN / 2) / A2DP_MAX_GAIN))
#define VOLUME_TO_A2DP_GAIN(volume) \
((uint16_t)(((volume)*A2DP_MAX_GAIN + PA_VOLUME_NORM / 2) / PA_VOLUME_NORM))
#define A2DP_MUTE_VOLUME A2DP_GAIN_TO_VOLUME(A2DP_MUTE_GAIN)
#define A2DP_MIN_VOLUME A2DP_GAIN_TO_VOLUME(A2DP_MIN_GAIN)
typedef struct pa_bluetooth_transport pa_bluetooth_transport;
typedef struct pa_bluetooth_device pa_bluetooth_device;
typedef struct pa_bluetooth_adapter pa_bluetooth_adapter;
......
......@@ -940,12 +940,36 @@ static void source_set_volume_cb(pa_source *s) {
pa_assert(u->transport);
pa_assert(u->transport->set_source_volume);
volume = pa_cvolume_max(&s->real_volume);
/* Prevent setting a gain below A2DP_MIN_GAIN, this is used to detect muting */
if (volume < A2DP_MIN_VOLUME)
volume = A2DP_MIN_VOLUME;
/* In the AG role, send a command to change microphone gain on the HS/HF */
volume = u->transport->set_source_volume(u->transport, pa_cvolume_max(&s->real_volume));
if (!s->muted)
volume = u->transport->set_source_volume(u->transport, volume);
pa_cvolume_set(&s->real_volume, u->decoder_sample_spec.channels, volume);
}
static void source_set_mute_cb(pa_source *s) {
struct userdata *u;
pa_assert(s);
u = s->userdata;
pa_assert(u);
pa_assert(u->source == s);
pa_assert(!pa_bluetooth_profile_should_attenuate_volume(u->profile));
pa_assert(u->transport);
pa_assert(u->transport->set_source_volume);
if (s->muted)
u->transport->set_source_volume(u->transport, 0);
else
source_set_volume_cb(s);
}
/* Run from main thread */
static void source_setup_volume_callback(pa_source *s) {
struct userdata *u;
......@@ -982,6 +1006,8 @@ static void source_setup_volume_callback(pa_source *s) {
u->source_volume_changed_slot = pa_hook_connect(&s->core->hooks[PA_CORE_HOOK_SOURCE_VOLUME_CHANGED],
PA_HOOK_NORMAL, sink_source_volume_changed_cb, u);
// TODO: Mute hook!
/* Send initial volume to peer, signalling support for volume control */
u->transport->set_source_volume(u->transport, pa_cvolume_max(&s->real_volume));
} else {
......@@ -1001,6 +1027,7 @@ static void source_setup_volume_callback(pa_source *s) {
pa_source_set_soft_volume(s, NULL);
pa_source_set_set_volume_callback(s, source_set_volume_cb);
pa_source_set_set_mute_callback(s, source_set_mute_cb);
s->n_volume_steps = HSP_MAX_GAIN + 1;
}
}
......@@ -1177,12 +1204,36 @@ static void sink_set_volume_cb(pa_sink *s) {
pa_assert(u->transport);
pa_assert(u->transport->set_sink_volume);
volume = pa_cvolume_max(&s->real_volume);
/* Prevent setting a gain below A2DP_MIN_GAIN, this is used to detect muting */
if (volume < A2DP_MIN_VOLUME)
volume = A2DP_MIN_VOLUME;
/* In the AG role, send a command to change speaker gain on the HS/HF */
volume = u->transport->set_sink_volume(u->transport, pa_cvolume_max(&s->real_volume));
if (!s->muted)
volume = u->transport->set_sink_volume(u->transport, volume);
pa_cvolume_set(&s->real_volume, u->encoder_sample_spec.channels, volume);
}
static void sink_set_mute_cb(pa_sink *s) {
struct userdata *u;
pa_assert(s);
u = s->userdata;
pa_assert(u);
pa_assert(u->sink == s);
pa_assert(!pa_bluetooth_profile_should_attenuate_volume(u->profile));
pa_assert(u->transport);
pa_assert(u->transport->set_sink_volume);
if (s->muted)
u->transport->set_sink_volume(u->transport, 0);
else
sink_set_volume_cb(s);
}
/* Run from main thread */
static void sink_setup_volume_callback(pa_sink *s) {
struct userdata *u;
......@@ -1224,6 +1275,8 @@ static void sink_setup_volume_callback(pa_sink *s) {
u->sink_volume_changed_slot = pa_hook_connect(&s->core->hooks[PA_CORE_HOOK_SINK_VOLUME_CHANGED],
PA_HOOK_NORMAL, sink_source_volume_changed_cb, u);
// TODO: Hook up mute callback!
/* Send initial volume to peer, signalling support for volume control */
u->transport->set_sink_volume(u->transport, pa_cvolume_max(&s->real_volume));
} else {
......@@ -1236,6 +1289,7 @@ static void sink_setup_volume_callback(pa_sink *s) {
pa_sink_set_soft_volume(s, NULL);
pa_sink_set_set_volume_callback(s, sink_set_volume_cb);
pa_sink_set_set_mute_callback(s, sink_set_mute_cb);
if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK)
s->n_volume_steps = A2DP_MAX_GAIN + 1;
......@@ -2433,12 +2487,25 @@ static pa_hook_result_t transport_sink_volume_changed_cb(pa_bluetooth_discovery
sink_setup_volume_callback(u->sink);
if (t->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) {
// TODO: Apply to HSP too
if (volume <= A2DP_MUTE_VOLUME) {
pa_sink_mute_changed(u->sink, true);
/* Do not update local volume; unmute should jump back to previous */
return PA_HOOK_OK;
}
}
pa_cvolume_set(&v, u->encoder_sample_spec.channels, volume);
if (pa_bluetooth_profile_should_attenuate_volume(t->profile))
pa_sink_set_volume(u->sink, &v, true, true);
else
pa_sink_volume_changed(u->sink, &v);
/* Unmute _after_ reflecting peer volume */
if(u->sink->muted)
pa_sink_mute_changed(u->sink, false);
return PA_HOOK_OK;
}
......@@ -2461,6 +2528,15 @@ static pa_hook_result_t transport_source_volume_changed_cb(pa_bluetooth_discover
source_setup_volume_callback(u->source);
if (t->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) {
// TODO: Apply to HSP too
if (volume <= A2DP_MUTE_VOLUME) {
pa_source_mute_changed(u->source, true);
/* Do not update local volume; unmute should jump back to previous */
return PA_HOOK_OK;
}
}
pa_cvolume_set(&v, u->decoder_sample_spec.channels, volume);
if (pa_bluetooth_profile_should_attenuate_volume(t->profile))
......@@ -2468,6 +2544,10 @@ static pa_hook_result_t transport_source_volume_changed_cb(pa_bluetooth_discover
else
pa_source_volume_changed(u->source, &v);
/* Unmute _after_ reflecting peer volume */
if(u->source->muted)
pa_source_mute_changed(u->source, false);
return PA_HOOK_OK;
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment