[v5,2/4] bluetooth: separate HSP and HFP

Submitted by Rodrigo Araujo on Sept. 30, 2017, 11:10 p.m.

Details

Message ID ad21408a-5640-eced-632a-85fa41f9edaa@gmail.com
State New
Headers show
Series "use bluetooth HFP in pulseaudio native backend when available" ( rev: 4 ) in PulseAudio

Not browsing as part of any series.

Commit Message

Rodrigo Araujo Sept. 30, 2017, 11:10 p.m.
Good evening.

I found the problem with the distorted microphone audio.

According to the HFP 1.6 spec, if a device supports remote volume
control, only the speaker is mandatory.

It happens that my HF headset does not support setting the microphone
volume, only the speaker. So the volume was too loud and was clipping
(I'm using Fedora, where flat_volumes defaults to "no").

But anyway, the spec says that remote volume control isn't mandatory at
all, so the same problem must be present for the speaker on HF devices
that do not support it.

So I've added some flags on the hfp_config struct and made some checks.
By default, we assume software volume control. When the HF sends us its
features during negotiation, if it supports remote volume control then
we disable software VC for the speaker, but we keep assuming software VC
for the mic unless we receive a unsolicited +VGM at some point.

Then, when setting the volume, we check those flags and act accordingly.

A consequence is that struct hfp_config must be known by
module-bluez5-device so its declaration went to one of the header files
(I think we'll need it there anyway later when we start adding flags for
wide-band-speech, but more on that later).

I also added some other fixes, mostly related to gain control but some
minor ones related to better HFP 1.6 compliance.

I really can't tell which changes should go to James' patches, so I'm
attaching a full patch with all of my modifications (including the
previous one that fixes auto_switch) in hope it will be useful. James,
could you take a look and if all if fine, merge these changes with
yours? As soon as you have a new version ready of your patches, I'm
willing to test them.

Also, should you also need help implementing the suggestions/fixes given
by Georg, please say so and I'll try to take some time to help. For
instance, I agree the new naming conventions need a bit of rethinking,
because we are implementing a HFP AG but the constants we are using
sometimes refer to HFP_HF or HFP_AG inconsistently. So feel free to tell
me should you need anything.

I've used this modified version with success with my HFP only headset,
in applications like skype and linphone. Will test it further during the
next week and will report any fixes I find needed, but for now I think
I'm happy with it on a "works for me" base. However several other
devices can still have issues due to supporting or not the
aforementioned features, so to the anyone reading this, all feedback is
welcome and will certainly help to make native HFP AG support official
in pulseaudio (and consequently, easily supported in all recent Linux
distributions).

Now, regarding wide band speech that I mentioned earlier: I started
implementing the codec negotiation and SDP advertising, and I think I
got it done (in the end, it's not too complicated when you find the
right screw), but only when I was about to test it was that I realized
that my headset has only HFP version 1.5. So I can't really test the
basics, much less starting to mess with the transport using mSBC. For
what I've seen though, it shouldn't be much hard to implement the rest,
as the sbc library already supports mSBC so it seems to the part I'm now
missing should be a variation of part of the A2DP code already present
(but don't just take my word for it).

But since I can't test anything related to that (I lack a premium HFP
headset, I suppose) I've decided not to proceed with it, at least until
we get the all the basics right and these patches are ready be official.
So to avoid confusion, the related changes I've made to support WBS
aren't in the attached patch, but should anyone having a HFP device that
supports it want them to proceed with the rest of the implementation,
feel free to ask (preferably on a new thread, I think).

Best regards.



On 09/26/2017 11:54 AM, Rodrigo Araujo wrote:
> Hello again.
>
> On 09/25/2017 03:30 PM, Rodrigo Araujo wrote:
>> Hi.
>>
>> On 09/25/2017 02:37 PM, James Bottomley wrote:
>>> On Sat, 2017-09-23 at 23:33 +0100, Rodrigo Araujo wrote:
>>>> Hi.
>>>>
>>>> First, just to say that your patches are going great. Finally I can
>>>> use
>>>> the microphone of my HFP only headset (a version of a Bluedio T2+).
>>>>
>>>> So far, I've only encontered one problem: the auto_switch option of
>>>> module_bluetooth_policy stops working. Dug through the code and I
>>>> think
>>>> you missed a few spots were you have to hangle the new
>>>> headset_handsfree
>>>> profile in module_bluetooth_policy.c
>>>>
>>>> Applying the following after applying your v5 patches fixed the issue
>>>> for me, now when I start making a VOIP call the profile switches to
>>>> headset_handsfree and the mic works automatically, and when the call
>>>> finishes it reverts back to a2dp.
>>> That's great, thanks!  Sad to say I didn't think automatic profile
>>> switching worked in pulseaudio, so I hadn't thought to investigate it.
>>> Did you want me to merge this into patch 2 or add it as a new patch
>>> with you as the author?
>>>
>>> James
>> Seems to me it makes sense to merge into patch 2. Would simplify things
>> also, too many patches and lots of work for you guys already ;)
>>
>> If you want to mention my name as contributor in that patch, also go
>> ahead :)
>>
>> Best regards.
> Meanwhile I found another issue: setting the volume (gain) of the
> speaker and microphone doesn't work. It's easily reproducible when using
> pavucontrol.
>
> This happens because of a clash with the HS role, and because we are a
> HFP AG we are falling on the reversed logic and sending mic commands
> when we want to send speaker commands, and vice-versa.
>
> The following patch is simple and fixes that:
>
> --- a/src/modules/bluetooth/backend-native.c	2017-09-23 23:12:35.220031951 +0100
> +++ b/src/modules/bluetooth/backend-native.c	2017-09-25 22:18:29.094127139 +0100
> @@ -565,7 +565,8 @@
>      /* If we are in the AG role, we send a command to the head set to change
>       * the speaker gain. In the HS role, source and sink are swapped, so
>       * in this case we notify the AG that the microphone gain has changed */
> -    if (t->profile == PA_BLUETOOTH_PROFILE_HSP_HS) {
> +    if (t->profile == PA_BLUETOOTH_PROFILE_HSP_HS ||
> +	t->profile == PA_BLUETOOTH_PROFILE_HFP_HF) {
>          len = sprintf(buf, "\r\n+VGS=%d\r\n", gain);
>          pa_log_debug("RFCOMM >> +VGS=%d", gain);
>      } else {
> @@ -592,7 +593,8 @@
>      /* If we are in the AG role, we send a command to the head set to change
>       * the microphone gain. In the HS role, source and sink are swapped, so
>       * in this case we notify the AG that the speaker gain has changed */
> -    if (t->profile == PA_BLUETOOTH_PROFILE_HSP_HS) {
> +    if (t->profile == PA_BLUETOOTH_PROFILE_HSP_HS ||
> +	t->profile == PA_BLUETOOTH_PROFILE_HFP_HF) {
>          len = sprintf(buf, "\r\n+VGM=%d\r\n", gain);
>          pa_log_debug("RFCOMM >> +VGM=%d", gain);
>      } else {
>
>
> Can you also merge that in you patch? :)
>
> This fixes when we explicitly set the volume, but also should help when
> the system tries to set it because it needs to.
>
> However, audio quality still leaves to be desired when compared with
> what is achieved using the HSP role (the voice is still too "metallic"
> and with excessively poor quality). This will surely be fixed when
> implementing wide band audio, but still should be better as it is now
> since the audio encoding is mostly the same. I will try to look into
> into too and if I find that it has to do with any of the other patches,
> I will reply to the respective thread. I will also see if I can help
> with implementing wide band audio, but not I'm promising anything since
> this is not my area.
>
> Best regards and keep up the good work.
> Rodrigo Araujo

Patch hide | download patch | download mbox

diff -ur a/src/modules/bluetooth/backend-native.c b/src/modules/bluetooth/backend-native.c
--- a/src/modules/bluetooth/backend-native.c	2017-09-30 21:40:19.692344798 +0100
+++ b/src/modules/bluetooth/backend-native.c	2017-09-30 21:29:30.180710836 +0100
@@ -53,11 +53,6 @@ 
     pa_mainloop_api *mainloop;
 };
 
-struct hfp_config {
-    uint32_t capabilities;
-    int state;
-};
-
 /*
  * the separate hansfree headset (HF) and Audio Gateway (AG) features
  */
@@ -163,7 +158,7 @@ 
 {
     char buf[512];
 
-    sprintf(buf, "+BRSF: %d", hfp_features);
+    sprintf(buf, "+BRSF: %u", hfp_features);
     rfcomm_write(fd, buf);
 }
 
@@ -405,6 +400,11 @@ 
         version = 0x0102;
         pa_dbus_append_basic_variant_dict_entry(&d, "Version", DBUS_TYPE_UINT16, &version);
     }
+    else if (pa_streq (uuid, PA_BLUETOOTH_UUID_HFP_AG)) {
+        /* HFP version 1.6 */
+        version = 0x0106;
+        pa_dbus_append_basic_variant_dict_entry(&d, "Version", DBUS_TYPE_UINT16, &version);
+    }
     dbus_message_iter_close_container(&i, &d);
 
     send_and_add_to_pending(b, m, register_profile_reply, pa_xstrdup(profile));
@@ -426,6 +426,10 @@ 
     if (c->state == 0 && sscanf(buf, "AT+BRSF=%d", &val) == 1) {
           c->capabilities = val;
           pa_log_info("HFP capabilities returns 0x%x", val);
+	  if (val & HFP_HF_RVOL) {
+	      c->speaker_gain_supported = true;
+	      c->mic_gain_supported = false;
+	  }
           hfp_send_features(fd);
           c->state = 1;
           return true;
@@ -453,7 +457,7 @@ 
     if (c->state != 4) {
         pa_log_error("HFP negotiation failed in state %d with inbound %s\n",
                      c->state, buf);
-        rfcomm_write(fd, "ERROR");
+        rfcomm_write(fd, "\r\nERROR\r\n");
         return false;
     }
 
@@ -481,6 +485,7 @@ 
         ssize_t len;
         int gain, dummy;
         bool  do_reply = false;
+        struct hfp_config *c = t->config;
 
         len = pa_read(fd, buf, 511, NULL);
         if (len < 0) {
@@ -499,18 +504,23 @@ 
          * is changed on the AG side.
          * AT+CKPD=200: Sent by HS when headset button is pressed.
          * RING: Sent by AG to HS to notify of an incoming call. It can safely be ignored because
-         * it does not expect a reply. */
-        if (sscanf(buf, "AT+VGS=%d", &gain) == 1 || sscanf(buf, "\r\n+VGM=%d\r\n", &gain) == 1) {
+         * it does not expect a reply.
+	 *
+	 * Also in HFP we need to handle negotiation, including codec updating by the HG */
+        if (sscanf(buf, "AT+VGS=%d", &gain) == 1 || sscanf(buf, "\r\n+VGM%*[=:]%d\r\n", &gain) == 1) {
             t->speaker_gain = gain;
             pa_hook_fire(pa_bluetooth_discovery_hook(t->device->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_SPEAKER_GAIN_CHANGED), t);
             do_reply = true;
-
-        } else if (sscanf(buf, "AT+VGM=%d", &gain) == 1 || sscanf(buf, "\r\n+VGS=%d\r\n", &gain) == 1) {
+        } else if (sscanf(buf, "AT+VGM=%d", &gain) == 1 || sscanf(buf, "\r\n+VGS%*[=:]%d\r\n", &gain) == 1) {
+	    c->mic_gain_supported = true;
             t->microphone_gain = gain;
             pa_hook_fire(pa_bluetooth_discovery_hook(t->device->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_MICROPHONE_GAIN_CHANGED), t);
             do_reply = true;
         } else if (sscanf(buf, "AT+CKPD=%d", &dummy) == 1) {
             do_reply = true;
+        } else if (pa_startswith(buf, "AT+NREC=0") == 1) {
+	    /* TODO: Handle disabling echo cancelation if active */
+            do_reply = true;
         } else if (t->config) { /* t->config is only non-null for hfp profile */
             do_reply = hfp_rfcomm_handle(fd, t, buf);
         } else {
@@ -568,6 +578,9 @@ 
     if (t->profile == PA_BLUETOOTH_PROFILE_HSP_HS) {
         len = sprintf(buf, "\r\n+VGS=%d\r\n", gain);
         pa_log_debug("RFCOMM >> +VGS=%d", gain);
+    } else if (t->profile == PA_BLUETOOTH_PROFILE_HFP_HF) {
+        len = sprintf(buf, "\r\n+VGS:%d\r\n", gain);
+        pa_log_debug("RFCOMM >> +VGS:%d", gain);
     } else {
         len = sprintf(buf, "\r\nAT+VGM=%d\r\n", gain);
         pa_log_debug("RFCOMM >> AT+VGM=%d", gain);
@@ -595,7 +608,11 @@ 
     if (t->profile == PA_BLUETOOTH_PROFILE_HSP_HS) {
         len = sprintf(buf, "\r\n+VGM=%d\r\n", gain);
         pa_log_debug("RFCOMM >> +VGM=%d", gain);
-    } else {
+    } else if (t->profile == PA_BLUETOOTH_PROFILE_HFP_HF) {
+        len = sprintf(buf, "\r\n+VGM:%d\r\n", gain);
+        pa_log_debug("RFCOMM >> +VGM:%d", gain);
+    }
+    else {
         len = sprintf(buf, "\r\nAT+VGS=%d\r\n", gain);
         pa_log_debug("RFCOMM >> AT+VGS=%d", gain);
     }
diff -ur a/src/modules/bluetooth/bluez5-util.h b/src/modules/bluetooth/bluez5-util.h
--- a/src/modules/bluetooth/bluez5-util.h	2017-09-30 21:40:19.693344789 +0100
+++ b/src/modules/bluetooth/bluez5-util.h	2017-09-30 00:27:45.207883419 +0100
@@ -133,6 +133,14 @@ 
 pa_bluetooth_backend *pa_bluetooth_native_backend_new(pa_core *c, pa_bluetooth_discovery *y, bool enable_hs_role);
 void pa_bluetooth_native_backend_free(pa_bluetooth_backend *b);
 void pa_bluetooth_native_backend_enable_hs_role(pa_bluetooth_backend *b, bool enable_hs_role);
+
+struct hfp_config {
+    uint32_t capabilities;
+    int state;
+    bool speaker_gain_supported;
+    bool mic_gain_supported;
+    //bool msbc;
+};
 #else
 static inline pa_bluetooth_backend *pa_bluetooth_native_backend_new(pa_core *c, pa_bluetooth_discovery *y, bool enable_hs_role) {
     return NULL;
diff -ur a/src/modules/bluetooth/module-bluetooth-policy.c b/src/modules/bluetooth/module-bluetooth-policy.c
--- a/src/modules/bluetooth/module-bluetooth-policy.c	2017-09-30 21:40:19.690344815 +0100
+++ b/src/modules/bluetooth/module-bluetooth-policy.c	2017-09-27 22:44:37.634064848 +0100
@@ -160,7 +160,7 @@ 
             if (!pa_streq(profile->name, "a2dp") && !pa_streq(profile->name, "a2dp_sink"))
                 continue;
         } else {
-            if (!pa_streq(profile->name, "hsp") && !pa_streq(profile->name, "headset_head_unit"))
+            if (!pa_streq(profile->name, "hsp") && !pa_streq(profile->name, "headset_head_unit") && !pa_streq(profile->name, "headset_handsfree"))
                 continue;
         }
 
@@ -195,7 +195,7 @@ 
             return;
 
         /* Skip card if does not have active hsp profile */
-        if (!pa_streq(card->active_profile->name, "hsp") && !pa_streq(card->active_profile->name, "headset_head_unit"))
+        if (!pa_streq(card->active_profile->name, "hsp") && !pa_streq(card->active_profile->name, "headset_head_unit") && !pa_streq(card->active_profile->name, "headset_handsfree"))
             return;
 
         /* Skip card if already has active a2dp profile */
@@ -207,7 +207,7 @@ 
             return;
 
         /* Skip card if already has active hsp profile */
-        if (pa_streq(card->active_profile->name, "hsp") || pa_streq(card->active_profile->name, "headset_head_unit"))
+        if (pa_streq(card->active_profile->name, "hsp") || pa_streq(card->active_profile->name, "headset_head_unit") || pa_streq(card->active_profile->name, "headset_handsfree"))
             return;
     }
 
diff -ur a/src/modules/bluetooth/module-bluez5-device.c b/src/modules/bluetooth/module-bluez5-device.c
--- a/src/modules/bluetooth/module-bluez5-device.c	2017-09-30 21:40:19.690344815 +0100
+++ b/src/modules/bluetooth/module-bluez5-device.c	2017-09-30 00:40:24.845861105 +0100
@@ -959,11 +959,14 @@ 
     uint16_t gain;
     pa_volume_t volume;
     struct userdata *u;
+    bool softonly;
+    struct hfp_config *c;
 
     pa_assert(s);
     pa_assert(s->core);
 
     u = s->userdata;
+    c = u->transport->config;
 
     pa_assert(u);
     pa_assert(u->source == s);
@@ -984,14 +987,17 @@ 
 
     pa_cvolume_set(&s->real_volume, u->sample_spec.channels, volume);
 
-    /* Set soft volume when in headset role */
-    if (u->profile == PA_BLUETOOTH_PROFILE_HFP_AG)
+    /* Set soft volume when in headset role or when in hf gateway mode and
+       the hf does not support remote volume control */
+    softonly = (u->profile == PA_BLUETOOTH_PROFILE_HFP_HF && !c->mic_gain_supported);
+    if (u->profile == PA_BLUETOOTH_PROFILE_HFP_AG || softonly )
         pa_cvolume_set(&s->soft_volume, u->sample_spec.channels, volume);
 
     /* If we are in the AG role, we send a command to the head set to change
      * the microphone gain. In the HS role, source and sink are swapped, so
      * in this case we notify the AG that the speaker gain has changed */
-    u->transport->set_microphone_gain(u->transport, gain);
+    if (!softonly)
+	u->transport->set_microphone_gain(u->transport, gain);
 }
 
 /* Run from main thread */
@@ -1133,11 +1139,14 @@ 
     uint16_t gain;
     pa_volume_t volume;
     struct userdata *u;
+    bool softonly;
+    struct hfp_config *c;
 
     pa_assert(s);
     pa_assert(s->core);
 
     u = s->userdata;
+    c = u->transport->config;
 
     pa_assert(u);
     pa_assert(u->sink == s);
@@ -1158,14 +1167,17 @@ 
 
     pa_cvolume_set(&s->real_volume, u->sample_spec.channels, volume);
 
-    /* Set soft volume when in headset role */
-    if (u->profile == PA_BLUETOOTH_PROFILE_HFP_AG)
+    /* Set soft volume when in headset role or when in hf gateway mode and
+       the hf does not support remote volume control */
+    softonly = (u->profile == PA_BLUETOOTH_PROFILE_HFP_HF && !c->speaker_gain_supported);
+    if (u->profile == PA_BLUETOOTH_PROFILE_HFP_AG || softonly)
         pa_cvolume_set(&s->soft_volume, u->sample_spec.channels, volume);
 
     /* If we are in the AG role, we send a command to the head set to change
      * the speaker gain. In the HS role, source and sink are swapped, so
      * in this case we notify the AG that the microphone gain has changed */
-    u->transport->set_speaker_gain(u->transport, gain);
+    if (!softonly)
+	u->transport->set_speaker_gain(u->transport, gain);
 }
 
 /* Run from main thread */
@@ -2218,7 +2230,8 @@ 
         volume++;
 
     pa_cvolume_set(&v, u->sample_spec.channels, volume);
-    if (t->profile == PA_BLUETOOTH_PROFILE_HSP_HS)
+    if (t->profile == PA_BLUETOOTH_PROFILE_HSP_HS ||
+        t->profile == PA_BLUETOOTH_PROFILE_HFP_HF)
         pa_sink_volume_changed(u->sink, &v);
     else
         pa_sink_set_volume(u->sink, &v, true, true);
@@ -2246,7 +2260,8 @@ 
 
     pa_cvolume_set(&v, u->sample_spec.channels, volume);
 
-    if (t->profile == PA_BLUETOOTH_PROFILE_HSP_HS)
+    if (t->profile == PA_BLUETOOTH_PROFILE_HSP_HS ||
+        t->profile == PA_BLUETOOTH_PROFILE_HFP_HF)
         pa_source_volume_changed(u->source, &v);
     else
         pa_source_set_volume(u->source, &v, true, true);