]> www.infradead.org Git - users/hch/misc.git/commitdiff
HID: playstation: Support DualSense audio jack hotplug detection
authorCristian Ciocaltea <cristian.ciocaltea@collabora.com>
Tue, 24 Jun 2025 21:56:54 +0000 (00:56 +0300)
committerJiri Kosina <jkosina@suse.com>
Wed, 17 Sep 2025 09:33:49 +0000 (11:33 +0200)
The default audio output path on DualSense controller hardware is set to
headphones, regardless of whether they are actually inserted or not.

Detect when the plugged state of the 3.5mm audio jack changes and toggle
audio output between headphones and internal speaker, as required.  The
latter is achieved by essentially routing the right channel of the audio
source to the mono speaker.

Additionally, adjust the speaker volume since its default level is too
low and, therefore, cannot generate any audible sound.

It's worth noting the audio functionality is currently not supported for
Bluetooth, hence it's limited to USB connectivity.

Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
Tested-by: Benjamin Tissoires <bentiss@kernel.org>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
drivers/hid/hid-playstation.c

index f523ca989a2f1d085a460c4e05db00599d19492d..4285260c7e22bc4097bc755f31a968f57ba16db8 100644 (file)
@@ -112,9 +112,13 @@ struct ps_led_info {
 #define DS_BUTTONS2_TOUCHPAD   BIT(1)
 #define DS_BUTTONS2_MIC_MUTE   BIT(2)
 
-/* Battery status field of DualSense input report. */
+/* Status fields of DualSense input report. */
 #define DS_STATUS0_BATTERY_CAPACITY            GENMASK(3, 0)
 #define DS_STATUS0_CHARGING                    GENMASK(7, 4)
+#define DS_STATUS1_HP_DETECT                   BIT(0)
+#define DS_STATUS1_MIC_DETECT                  BIT(1)
+#define DS_STATUS1_JACK_DETECT                 (DS_STATUS1_HP_DETECT | DS_STATUS1_MIC_DETECT)
+#define DS_STATUS1_MIC_MUTE                    BIT(2)
 
 /* Feature version from DualSense Firmware Info report. */
 #define DS_FEATURE_VERSION_MINOR               GENMASK(7, 0)
@@ -143,13 +147,19 @@ struct ps_led_info {
 /* Flags for DualSense output report. */
 #define DS_OUTPUT_VALID_FLAG0_COMPATIBLE_VIBRATION             BIT(0)
 #define DS_OUTPUT_VALID_FLAG0_HAPTICS_SELECT                   BIT(1)
+#define DS_OUTPUT_VALID_FLAG0_SPEAKER_VOLUME_ENABLE            BIT(5)
+#define DS_OUTPUT_VALID_FLAG0_MIC_VOLUME_ENABLE                        BIT(6)
+#define DS_OUTPUT_VALID_FLAG0_AUDIO_CONTROL_ENABLE             BIT(7)
 #define DS_OUTPUT_VALID_FLAG1_MIC_MUTE_LED_CONTROL_ENABLE      BIT(0)
 #define DS_OUTPUT_VALID_FLAG1_POWER_SAVE_CONTROL_ENABLE                BIT(1)
 #define DS_OUTPUT_VALID_FLAG1_LIGHTBAR_CONTROL_ENABLE          BIT(2)
 #define DS_OUTPUT_VALID_FLAG1_RELEASE_LEDS                     BIT(3)
 #define DS_OUTPUT_VALID_FLAG1_PLAYER_INDICATOR_CONTROL_ENABLE  BIT(4)
+#define DS_OUTPUT_VALID_FLAG1_AUDIO_CONTROL2_ENABLE            BIT(7)
 #define DS_OUTPUT_VALID_FLAG2_LIGHTBAR_SETUP_CONTROL_ENABLE    BIT(1)
 #define DS_OUTPUT_VALID_FLAG2_COMPATIBLE_VIBRATION2            BIT(2)
+#define DS_OUTPUT_AUDIO_FLAGS_OUTPUT_PATH_SEL                  GENMASK(5, 4)
+#define DS_OUTPUT_AUDIO_FLAGS2_SP_PREAMP_GAIN                  GENMASK(2, 0)
 #define DS_OUTPUT_POWER_SAVE_CONTROL_MIC_MUTE                  BIT(4)
 #define DS_OUTPUT_LIGHTBAR_SETUP_LIGHT_OUT                     BIT(1)
 
@@ -192,6 +202,11 @@ struct dualsense {
        u8 lightbar_green;
        u8 lightbar_blue;
 
+       /* Audio Jack plugged state */
+       u8 plugged_state;
+       u8 prev_plugged_state;
+       bool prev_plugged_state_valid;
+
        /* Microphone */
        bool update_mic_mute;
        bool mic_muted;
@@ -251,11 +266,15 @@ struct dualsense_output_report_common {
        u8 motor_left;
 
        /* Audio controls */
-       u8 reserved[4];
+       u8 headphone_volume;    /* 0x0 - 0x7f */
+       u8 speaker_volume;      /* 0x0 - 0xff */
+       u8 mic_volume;          /* 0x0 - 0x40 */
+       u8 audio_control;
        u8 mute_button_led;
 
        u8 power_save_control;
-       u8 reserved2[28];
+       u8 reserved2[27];
+       u8 audio_control2;
 
        /* LEDs and lightbar */
        u8 valid_flag2;
@@ -1303,6 +1322,46 @@ static void dualsense_output_worker(struct work_struct *work)
                ds->update_player_leds = false;
        }
 
+       if (ds->plugged_state != ds->prev_plugged_state) {
+               u8 val = ds->plugged_state & DS_STATUS1_HP_DETECT;
+
+               if (val != (ds->prev_plugged_state & DS_STATUS1_HP_DETECT)) {
+                       common->valid_flag0 = DS_OUTPUT_VALID_FLAG0_AUDIO_CONTROL_ENABLE;
+                       /*
+                        *  _--------> Output path setup in audio_flag0
+                        * /  _------> Headphone (HP) Left channel sink
+                        * | /  _----> Headphone (HP) Right channel sink
+                        * | | /  _--> Internal Speaker (SP) sink
+                        * | | | /
+                        * | | | |     L/R - Left/Right channel source
+                        * 0 L-R X       X - Unrouted (muted) channel source
+                        * 1 L-L X
+                        * 2 L-L R
+                        * 3 X-X R
+                        */
+                       if (val) {
+                               /* Mute SP and route L+R channels to HP */
+                               common->audio_control = 0;
+                       } else {
+                               /* Mute HP and route R channel to SP */
+                               common->audio_control =
+                                       FIELD_PREP(DS_OUTPUT_AUDIO_FLAGS_OUTPUT_PATH_SEL, 0x3);
+                               /*
+                                * Set SP hardware volume to 100%.
+                                * Note the accepted range seems to be [0x3d..0x64]
+                                */
+                               common->valid_flag0 |= DS_OUTPUT_VALID_FLAG0_SPEAKER_VOLUME_ENABLE;
+                               common->speaker_volume = 0x64;
+                               /* Set SP preamp gain to ~30% */
+                               common->valid_flag1 = DS_OUTPUT_VALID_FLAG1_AUDIO_CONTROL2_ENABLE;
+                               common->audio_control2 =
+                                       FIELD_PREP(DS_OUTPUT_AUDIO_FLAGS2_SP_PREAMP_GAIN, 0x2);
+                       }
+               }
+
+               ds->prev_plugged_state = ds->plugged_state;
+       }
+
        if (ds->update_mic_mute) {
                common->valid_flag1 |= DS_OUTPUT_VALID_FLAG1_MIC_MUTE_LED_CONTROL_ENABLE;
                common->mute_button_led = ds->mic_muted;
@@ -1406,6 +1465,32 @@ static int dualsense_parse_report(struct ps_device *ps_dev, struct hid_report *r
        }
        ds->last_btn_mic_state = btn_mic_state;
 
+       /*
+        * Parse HP/MIC plugged state data for USB use case, since Bluetooth
+        * audio is currently not supported.
+        */
+       if (hdev->bus == BUS_USB) {
+               value = ds_report->status[1] & DS_STATUS1_JACK_DETECT;
+
+               if (!ds->prev_plugged_state_valid) {
+                       /* Initial handling of the plugged state report */
+                       scoped_guard(spinlock_irqsave, &ps_dev->lock) {
+                               ds->plugged_state = (~value) & DS_STATUS1_JACK_DETECT;
+                               ds->prev_plugged_state_valid = true;
+                       }
+               }
+
+               if (value != ds->plugged_state) {
+                       scoped_guard(spinlock_irqsave, &ps_dev->lock) {
+                               ds->prev_plugged_state = ds->plugged_state;
+                               ds->plugged_state = value;
+                       }
+
+                       /* Schedule audio routing towards active endpoint. */
+                       dualsense_schedule_work(ds);
+               }
+       }
+
        /* Parse and calibrate gyroscope data. */
        for (i = 0; i < ARRAY_SIZE(ds_report->gyro); i++) {
                int raw_data = (short)le16_to_cpu(ds_report->gyro[i]);