Connecting a set of bluetooth headphones to your Linux computer is a simple thing, if you just want to listen to music. But what about when you want to use the microphone that is, most probably, embedded in those headphones?

How I tested?

All the below information was tested on Kubuntu 20.04 with the following headphones:

How to check if your microphone is working or not?

You can test from an application that makes use of the microphone like Skype or 8x8 Meet.

For example, in this screenshot, it doesn't work. Just go to https://8x8.vc/someRandomString and click the up arrow next to the microphone symbol to see the list of available microphones your computer has. You might see the headphones (in this particular example, the EDIFIER NeoBuds Pro are conected) but only the laptop's internal microphone is detected.

the mic doesn't show up

From the command line, you can run this command:

$ pactl list

At the end of the output, you should see something like this:

Card #31
  Name: bluez_card.FC_E8_06_76_05_2C
  Driver: module-bluez5-device.c
  Owner Module: 56
  Properties:
    device.description = "EDIFIER NeoBuds Pro"
    device.string = "FC:E8:06:76:05:2C"
    device.api = "bluez"
    device.class = "sound"
    device.bus = "bluetooth"
    device.form_factor = "headset"
    bluez.path = "/org/bluez/hci0/dev_FC_E8_06_76_05_2C"
    bluez.class = "0x240404"
    bluez.alias = "EDIFIER NeoBuds Pro"
    device.icon_name = "audio-headset-bluetooth"
    device.intended_roles = "phone"
  Profiles:
    a2dp_sink: High Fidelity Playback (A2DP Sink) (sinks: 1, sources: 0, priority: 40, available: yes)
    headset_head_unit: Headset Head Unit (HSP/HFP) (sinks: 1, sources: 1, priority: 30, available: no)
    off: Off (sinks: 0, sources: 0, priority: 0, available: yes)
  Active Profile: a2dp_sink
  Ports:
    headset-output: Headset (priority: 0, latency offset: 0 usec, available)
      Part of profile(s): a2dp_sink, headset_head_unit
    headset-input: Headset (priority: 0, latency offset: 0 usec, not available)
      Part of profile(s): headset_head_unit

Notice the Profiles section and the fact that the profile headset_head_unit is not available and, also, that the Active Profile is set to a2dp_sink.

The easy way: making HSP work

This only works for JBL TUNE205BT.

This is easy, as it doesn't require any hacks or additional software. Just edit the file /etc/pulse/default.pa and add auto_switch=2 to the line load-module module-bluetooth-discover. In the end, the file should look like this in that zone:

### Automatically load driver modules for Bluetooth hardware
.ifexists module-bluetooth-policy.so
#load-module module-bluetooth-policy
load-module module-bluetooth-policy auto_switch=2
.endif

Restart bluetooth and kill pulseaudio (it should respawn by itself):

$ sudo systemctl restart bluetooth
$ pulseaudio -k

Now, not only that the HSP profile will appear as available (notice the Profiles section and, also, the Active Profile)...

$ pactl list
Card #32
  Name: bluez_card.B8_F6_53_03_D4_1C
  Driver: module-bluez5-device.c
  Owner Module: 57
  Properties:
    device.description = "JBL TUNE205BT"
    device.string = "B8:F6:53:03:D4:1C"
    device.api = "bluez"
    device.class = "sound"
    device.bus = "bluetooth"
    device.form_factor = "headset"
    bluez.path = "/org/bluez/hci0/dev_B8_F6_53_03_D4_1C"
    bluez.class = "0x240404"
    bluez.alias = "JBL TUNE205BT"
    device.icon_name = "audio-headset-bluetooth"
    device.intended_roles = "phone"
  Profiles:
    headset_head_unit: Headset Head Unit (HSP/HFP) (sinks: 1, sources: 1, priority: 30, available: yes)
    a2dp_sink: High Fidelity Playback (A2DP Sink) (sinks: 1, sources: 0, priority: 40, available: no)
    off: Off (sinks: 0, sources: 0, priority: 0, available: yes)
  Active Profile: headset_head_unit
  Ports:
    headset-output: Headset (priority: 0, latency offset: 0 usec, available)
      Part of profile(s): headset_head_unit, a2dp_sink
    headset-input: Headset (priority: 0, latency offset: 0 usec, available)
      Part of profile(s): headset_head_unit

... but, also, the headphones will switch between A2DP (output/headphones only; high quality sound) and HSP (output and input, headphones and microphone, but low quality sound) when needed. You should see the microphone in https://8x8.vc/someRandomString:

the mic shows up

The hard way: making HFP work

Solution 1: Pulseaudio + ofono

This is documented on Pulseaudio's official documentation and on many other blog posts, but I coudn't make it work no matter how hard I tried...

Solution 2: Replacing Pulseaudio with PipeWire

I tested this on all these headphones and it works:

Replacing Pulseaudio with PipeWire is a much better solution, because it also brings support for better codecs (even LDAC works; see below).

This is how I did it, by following the official documentation:

Add the PipeWire repository:

$ sudo add-apt-repository ppa:pipewire-debian/pipewire-upstream

Install PipeWire and the codecs:

$ sudo apt install libfdk-aac2 libldacbt-{abr,enc}2 libopenaptx0 gstreamer1.0-pipewire libpipewire-0.3-{0,dev,modules} libspa-0.2-{bluetooth,dev,jack,modules} pipewire{,-{audio-client-libraries,pulse,media-session,bin,locales,tests}}

Disable and mask Pulseaudio:

$ systemctl --user --now disable pulseaudio.{socket,service}
$ systemctl --user mask pulseaudio 

Enable and start PipeWire:

$ systemctl --user --now enable pipewire{,-pulse}.{socket,service} pipewire-media-session.service

Check and see if it's running:

$ pactl info | grep '^Server Name'

The above command should return something like

Server Name: PulseAudio (on PipeWire 0.3.37)

Now just connect the headphones via bluetooth and run

$ pactl list

Notice the Profiles section, the fact that the profile headset_head_unit is available and the amount of codecs supported:

Card #88
  Name: bluez_card.FC_E8_06_76_05_2C
  Driver: module-bluez5-device.c
  Owner Module: n/a
  Properties:
    device.api = "bluez5"
    device.bus = "bluetooth"
    media.class = "Audio/Device"
    device.name = "bluez_card.FC_E8_06_76_05_2C"
    device.description = "EDIFIER NeoBuds Pro"
    device.alias = "EDIFIER NeoBuds Pro"
    device.form_factor = "headset"
    device.string = "FC:E8:06:76:05:2C"
    api.bluez5.icon = "audio-card"
    api.bluez5.path = "/org/bluez/hci0/dev_FC_E8_06_76_05_2C"
    api.bluez5.address = "FC:E8:06:76:05:2C"
    api.bluez5.device = ""
    api.bluez5.class = "0x240404"
    api.bluez5.connection = "connected"
    device.icon_name = "audio-headset-bluetooth"
    bluez5.auto-connect = "[ hfp_hf hsp_hs a2dp_sink ]"
    factory.id = "14"
    client.id = "31"
    object.id = "88"
  Profiles:
    off: Off (sinks: 0, sources: 0, priority: 0, available: yes)
    a2dp-sink: High Fidelity Playback (A2DP Sink) (sinks: 1, sources: 0, priority: 0, available: yes)
    headset-head-unit: Headset Head Unit (HSP/HFP) (sinks: 1, sources: 1, priority: 0, available: yes)
    a2dp-sink-sbc: High Fidelity Playback (A2DP Sink, codec SBC) (sinks: 1, sources: 0, priority: 0, available: yes)
    a2dp-sink-sbc_xq: High Fidelity Playback (A2DP Sink, codec SBC-XQ) (sinks: 1, sources: 0, priority: 0, available: yes)
    a2dp-sink-aac: High Fidelity Playback (A2DP Sink, codec AAC) (sinks: 1, sources: 0, priority: 0, available: yes)
    headset-head-unit-cvsd: Headset Head Unit (HSP/HFP, codec CVSD) (sinks: 1, sources: 1, priority: 0, available: yes)
    headset-head-unit-msbc: Headset Head Unit (HSP/HFP, codec mSBC) (sinks: 1, sources: 1, priority: 0, available: yes)
  Active Profile: a2dp-sink-aac
  Ports:
    headset-input: Headset (priority: 0, latency offset: 0 usec, available)
      Properties:
        port.type = "headset"
      Part of profile(s): headset-head-unit, headset-head-unit-cvsd, headset-head-unit-msbc
    headset-output: Headset (priority: 0, latency offset: 0 usec, available)
      Properties:
        port.type = "headset"
      Part of profile(s): a2dp-sink, headset-head-unit, a2dp-sink-sbc, a2dp-sink-sbc_xq, a2dp-sink-aac, headset-head-unit-cvsd, headset-head-unit-msbc

Now you can manually select the Headset Head Unit (HSP/HFP) profile before starting a meeting, via the Volume Control, for example:

selecting Headset Head Unit

And the applications needing the microphone should now see it:

the mic shows up

What about automatically switching between Headset Head Unit (HSP/HFP) and High Fidelity Playback (A2DP) profiles? As described in here, create the file /etc/pipewire/media-session.d/bluez-monitor.conf with this content...

# Bluez monitor config file for PipeWire version "0.3.33" #
#
# Copy and edit this file in /etc/pipewire/media-session.d/
# for systemwide changes or in
# ~/.config/pipewire/media-session.d/ for local changes.

properties = {
    # These features do not work on all headsets, so they are enabled
    # by default based on the hardware database. They can also be
    # forced on/off for all devices by the following options:

    #bluez5.enable-sbc-xq = true
    #bluez5.enable-msbc = true
    #bluez5.enable-hw-volume = true

    # See bluez-hardware.conf for the hardware database.

    # Enabled headset roles (default: [ hsp_hs hfp_ag ]), this
    # property only applies to native backend. Currently some headsets
    # (Sony WH-1000XM3) are not working with both hsp_ag and hfp_ag
    # enabled, disable either hsp_ag or hfp_ag to work around it.
    #
    # Supported headset roles: hsp_hs (HSP Headset),
    #                          hsp_ag (HSP Audio Gateway),
    #                          hfp_hf (HFP Hands-Free),
    #                          hfp_ag (HFP Audio Gateway)
    #bluez5.headset-roles = [ hsp_hs hsp_ag hfp_hf hfp_ag ]

    # Enabled A2DP codecs (default: all).
    #bluez5.codecs = [ sbc aac ldac aptx aptx_hd ]

    # Properties for the A2DP codec configuration
    #bluez5.default.rate     = 48000
    #bluez5.default.channels = 2
}

rules = [
    # An array of matches/actions to evaluate.
    {
        # Rules for matching a device or node. It is an array of
        # properties that all need to match the regexp. If any of the
        # matches work, the actions are executed for the object.
        matches = [
            {
                # This matches all cards.
                device.name = "~bluez_card.*"
            }
        ]
        actions = {
            # Actions can update properties on the matched object.
            update-props = {

                # Auto-connect device profiles on start up or when only partial
                # profiles have connected. Disabled by default if the property
                # is not specified.
                #bluez5.auto-connect = [
                #    hfp_hf
                #    hsp_hs
                #    a2dp_sink
                #    hfp_ag
                #    hsp_ag
                #    a2dp_source
                #]
                bluez5.auto-connect  = [ hfp_hf hsp_hs a2dp_sink ]

                # Hardware volume control (default: all)
                #bluez5.hw-volume = [
                #    hfp_hf
                #    hsp_hs
                #    a2dp_sink
                #    hfp_ag
                #    hsp_ag
                #    a2dp_source
                #]

                # LDAC encoding quality
                # Available values: auto (Adaptive Bitrate, default)
                #                   hq   (High Quality, 990/909kbps)
                #                   sq   (Standard Quality, 660/606kbps)
                #                   mq   (Mobile use Quality, 330/303kbps)
                #bluez5.a2dp.ldac.quality  = auto

                # AAC variable bitrate mode
                # Available values: 0 (cbr, default), 1-5 (quality level)
                #bluez5.a2dp.aac.bitratemode = 0

                # Profile connected first
                # Available values: a2dp-sink (default), headset-head-unit
                #device.profile = a2dp-sink

                # A2DP  HFP profile auto-switching (when device is default output)
                # Available values: false, role (default), true
                # 'role' will switch the profile if the recording application
                # specifies Communication (or "phone" in PA) as the stream role.
                bluez5.autoswitch-profile = true
            }
        }
    }
    {
        matches = [
            {
                # Matches all sources.
                node.name = "~bluez_input.*"
            }
            {
                # Matches all sinks.
                node.name = "~bluez_output.*"
            }
        ]
        actions = {
            update-props = {
                #node.nick            = "My Node"
                #node.nick            = null
                #priority.driver      = 100
                #priority.session     = 100
                node.pause-on-idle    = false
                #resample.quality     = 4
                #channelmix.normalize = false
                #channelmix.mix-lfe   = false
                #session.suspend-timeout-seconds = 5      # 0 disables suspend
                #monitor.channel-volumes = false

                # A2DP source role, "input" or "playback"
                # Defaults to "playback", playing stream to speakers
                # Set to "input" to use as an input for apps
                #bluez5.a2dp-source-role = input
            }
        }
    }
]

...and restart bluetooth and PipeWire:

$ sudo systemctl restart bluetooth
$ systemctl --user restart pipewire

LDAC support

This only works for SONY WH-1000XM3.

Run

$ pactl list

... and you should see (and hear) the LDAC codec in action (see the Active Profile section):

Card #54
  Name: bluez_card.14_3F_A6_35_65_8E
  Driver: module-bluez5-device.c
  Owner Module: n/a
  Properties:
    device.api = "bluez5"
    device.bus = "bluetooth"
    media.class = "Audio/Device"
    device.name = "bluez_card.14_3F_A6_35_65_8E"
    device.description = "WH-1000XM3"
    device.alias = "WH-1000XM3"
    device.vendor.id = "usb:054c"
    device.product.id = "0x0cd3"
    device.form_factor = "unknown"
    device.string = "14:3F:A6:35:65:8E"
    api.bluez5.path = "/org/bluez/hci0/dev_14_3F_A6_35_65_8E"
    api.bluez5.address = "14:3F:A6:35:65:8E"
    api.bluez5.device = ""
    api.bluez5.class = "0x000000"
    api.bluez5.connection = "connected"
    device.icon_name = "audio-card-bluetooth"
    bluez5.auto-connect = "[ hfp_hf hsp_hs a2dp_sink ]"
    bluez5.autoswitch-profile = "true"
    factory.id = "14"
    client.id = "31"
    object.id = "54"
  Profiles:
    off: Off (sinks: 0, sources: 0, priority: 0, available: yes)
    a2dp-sink: High Fidelity Playback (A2DP Sink) (sinks: 1, sources: 0, priority: 0, available: yes)
    headset-head-unit: Headset Head Unit (HSP/HFP) (sinks: 1, sources: 1, priority: 0, available: yes)
    a2dp-sink-sbc: High Fidelity Playback (A2DP Sink, codec SBC) (sinks: 1, sources: 0, priority: 0, available: yes)
    a2dp-sink-sbc_xq: High Fidelity Playback (A2DP Sink, codec SBC-XQ) (sinks: 1, sources: 0, priority: 0, available: yes)
    a2dp-sink-aac: High Fidelity Playback (A2DP Sink, codec AAC) (sinks: 1, sources: 0, priority: 0, available: yes)
    a2dp-sink-aptx: High Fidelity Playback (A2DP Sink, codec aptX) (sinks: 1, sources: 0, priority: 0, available: yes)
    a2dp-sink-aptx_hd: High Fidelity Playback (A2DP Sink, codec aptX HD) (sinks: 1, sources: 0, priority: 0, available: yes)
    a2dp-sink-ldac: High Fidelity Playback (A2DP Sink, codec LDAC) (sinks: 1, sources: 0, priority: 0, available: yes)
    headset-head-unit-cvsd: Headset Head Unit (HSP/HFP, codec CVSD) (sinks: 1, sources: 1, priority: 0, available: yes)
    headset-head-unit-msbc: Headset Head Unit (HSP/HFP, codec mSBC) (sinks: 1, sources: 1, priority: 0, available: yes)
  Active Profile: a2dp-sink-ldac
  Ports:
    bluetooth-input: Bluetooth (priority: 0, latency offset: 0 usec, available)
      Properties:
        port.type = "bluetooth"
      Part of profile(s): headset-head-unit, headset-head-unit-cvsd, headset-head-unit-msbc
    bluetooth-output: Bluetooth (priority: 0, latency offset: 0 usec, available)
      Properties:
        port.type = "bluetooth"
      Part of profile(s): a2dp-sink, headset-head-unit, a2dp-sink-sbc, a2dp-sink-sbc_xq, a2dp-sink-aac, a2dp-sink-aptx, a2dp-sink-aptx_hd, a2dp-sink-ldac, headset-head-unit-cvsd, headset-head-unit-msbc

Enjoy!