From a801b78f21c93fed68ad110f2f6608643adb9c52 Mon Sep 17 00:00:00 2001
From: dequis <dx@dxzone.com.ar>
Date: Wed, 27 Dec 2023 16:33:22 +0100
Subject: [PATCH] Headphone detection override state in audio settings

Audio settings in the headphones section now has a "jack detection"
on/off setting that when set to off shows a "headphone state" setting.
If jack detection is off and headphone state is on, headphones will be
assumed to be connected. If headphone state is off, headphones will be
assumed to be disconnected.

On the backend, this uses the existing but almost unused API
audio.headphones_detection_override(). This API is extended to take a
second optional parameter for the desired state (default True to keep
the previous behavior)

For UI clarity/space reasons, audio settings refers to jack detection
being on or off which is the opposite of override being on or off.
Jack detection on means override off.

The main motivation for this change is to allow the user to record audio
through the headset mic (which, unlike line in, can receive power from
TRRS) but still output sound through the internal speaker. So connecting
a TRRS lavalier mic to headset out doesn't result in losing sound output.
---
 components/micropython/usermodule/mp_audio.c | 16 +++++--
 components/st3m/st3m_audio.c                 | 17 ++++++--
 components/st3m/st3m_audio.h                 | 17 +++++---
 docs/api/audio.rst                           | 22 +++++++---
 python_payload/apps/audio_config/__init__.py | 45 ++++++++++++++++++--
 python_payload/st3m/run.py                   |  5 +++
 python_payload/st3m/settings.py              | 12 ++++++
 7 files changed, 113 insertions(+), 21 deletions(-)

diff --git a/components/micropython/usermodule/mp_audio.c b/components/micropython/usermodule/mp_audio.c
index 4762e87b33..557c4973bd 100644
--- a/components/micropython/usermodule/mp_audio.c
+++ b/components/micropython/usermodule/mp_audio.c
@@ -28,12 +28,20 @@ STATIC mp_obj_t mp_headphones_are_connected() {
 STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_headphones_are_connected_obj,
                                  mp_headphones_are_connected);
 
-STATIC mp_obj_t mp_headphones_detection_override(mp_obj_t enable) {
-    st3m_audio_headphones_detection_override(mp_obj_get_int(enable));
+STATIC mp_obj_t mp_headphones_detection_override(size_t n_args,
+                                                 const mp_obj_t *args) {
+    bool enable = mp_obj_get_int(args[0]);
+    bool override_state = true;
+    if (n_args > 1) {
+        override_state = mp_obj_get_int(args[1]);
+    }
+
+    st3m_audio_headphones_detection_override(enable, override_state);
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_headphones_detection_override_obj,
-                                 mp_headphones_detection_override);
+STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_headphones_detection_override_obj,
+                                           1, 2,
+                                           mp_headphones_detection_override);
 
 STATIC mp_obj_t mp_headphones_set_volume_dB(mp_obj_t vol_dB) {
     return mp_obj_new_float(
diff --git a/components/st3m/st3m_audio.c b/components/st3m/st3m_audio.c
index 19eb042243..214bd6726e 100644
--- a/components/st3m/st3m_audio.c
+++ b/components/st3m/st3m_audio.c
@@ -216,9 +216,14 @@ static void _audio_speaker_apply(st3m_audio_output_t *out) {
 typedef struct {
     flow3r_bsp_audio_jacksense_state_t jacksense;
 
-    // True if system should pretend headphones are plugged in.
+    // True if system should ignore jacksense and use
+    // headphones_detection_override_state to determine
+    // whether headphones are connected (true) or not (false)
     bool headphones_detection_override;
 
+    // Defaults to true
+    bool headphones_detection_override_state;
+
     // The two output channels.
     st3m_audio_output_t headphones;
     st3m_audio_output_t speaker;
@@ -262,6 +267,7 @@ static st3m_audio_state_t state = {
             .line_in = false,
         },
     .headphones_detection_override = false,
+    .headphones_detection_override_state = true,
     .headphones =
         {
             .volume = 0,
@@ -310,7 +316,10 @@ static st3m_audio_state_t state = {
 //
 // Lock must be taken.
 static bool _headphones_connected(void) {
-    return state.jacksense.headphones || state.headphones_detection_override;
+    if (state.headphones_detection_override) {
+        return state.headphones_detection_override_state;
+    }
+    return state.jacksense.headphones;
 }
 
 static void _audio_input_set_source(st3m_audio_input_source_t source) {
@@ -915,9 +924,11 @@ float st3m_audio_headphones_set_volume_dB(float vol_dB) {
     LOCKED(float, _output_set_volume(&state.headphones, vol_dB));
 }
 
-void st3m_audio_headphones_detection_override(bool enable) {
+void st3m_audio_headphones_detection_override(bool enable,
+                                              bool override_state) {
     LOCK;
     state.headphones_detection_override = enable;
+    state.headphones_detection_override_state = override_state;
     _output_apply(&state.headphones);
     _output_apply(&state.speaker);
     UNLOCK;
diff --git a/components/st3m/st3m_audio.h b/components/st3m/st3m_audio.h
index 6830be463c..51ea9339f2 100644
--- a/components/st3m/st3m_audio.h
+++ b/components/st3m/st3m_audio.h
@@ -76,12 +76,19 @@ bool st3m_audio_headset_is_connected(void);
 /* Returns true if the line-in jack is connected to a cable. */
 bool st3m_audio_line_in_is_connected(void);
 
-/* If a sleeve contact mic doesn't pull the detection pin low enough the
- * codec's built in headphone detection might fail. Calling this function
- * with 'enable = 1' overrides the detection and assumes there's headphones
- * plugged in. Call with 'enable = 0' to revert to automatic detection.
+/* Set to 'enable = 1' if the system should ignore jacksense and use
+ * headphones_detection_override_state to determine whether headphones are
+ * connected (true) or not (false).
+ *
+ * Use cases:
+ * - If a sleeve contact mic doesn't pull the detection pin low enough the
+ *   codec's built in headphone detection might fail.
+ * - If the headset only has a mic connected but we wish to use the internal
+ *   speaker anyway
+ *
+ * Call with 'enable = 0' to revert to automatic detection.
  */
-void st3m_audio_headphones_detection_override(bool enable);
+void st3m_audio_headphones_detection_override(bool enable, bool override_state);
 
 /* Attempts to set target volume for the headphone output/onboard speakers
  * respectively, clamps/rounds if necessary and returns the actual volume.
diff --git a/docs/api/audio.rst b/docs/api/audio.rst
index bb3a8cb769..9b980ecb72 100644
--- a/docs/api/audio.rst
+++ b/docs/api/audio.rst
@@ -152,16 +152,28 @@ in the user config.
 OS development
 --------------
 
+.. warning::
+
+   These functions are not to be used in applications, but only by OS settings.
+
 Many of these functions are available in three variants: headphone volume,
 speaker volume, and volume. If :code:`headphones_are_connected()` returns 1
 the "headphone" variant is chosen, else the "speaker" variant is chosen.
 
-.. py:function:: headphones_detection_override(enable : bool)
+.. py:function:: headphones_detection_override(enable : bool, override_state : bool)
+
+   Set to 'enable = True' if the system should ignore jacksense and use
+   override_state to determine whether headphones are connected (True) or not
+   (False).
+
+   Use cases:
+
+   - If a sleeve contact mic doesn't pull the detection pin low enough the
+     codec's built in headphone detection might fail.
+   - If the headset only has a mic connected but we wish to use the internal
+     speaker anyway
 
-   If a sleeve contact mic doesn't pull the detection pin low enough the
-   codec's built in headphone detection might fail. Calling this function
-   with 'enable = 1' overrides the detection and assumes there's headphones
-   plugged in. Call with 'enable = 0' to revert to automatic detection.
+   Call with 'enable = 0' to revert to automatic detection.
 
 .. py:function:: headphones_set_volume_dB(vol_dB : float) -> float
 .. py:function:: speaker_set_volume_dB(vol_dB : float) -> float
diff --git a/python_payload/apps/audio_config/__init__.py b/python_payload/apps/audio_config/__init__.py
index db87a1cfe5..c0177976e9 100644
--- a/python_payload/apps/audio_config/__init__.py
+++ b/python_payload/apps/audio_config/__init__.py
@@ -334,12 +334,12 @@ class SpeakerMenu(Submenu):
 class HeadphonesMenu(Submenu):
     def __init__(self, press):
         super().__init__(press)
-        self.num_widgets = 5
-        self.overhang = -40
+        self.num_widgets = 6
+        self.overhang = -80
         self.mid_x = 50
-        self.focus_pos_limit_min = -100
+        self.focus_pos_limit_min = -20
         self.focus_pos_limit_max = 100
-        self.focus_pos_limit_first = -100
+        self.focus_pos_limit_first = -40
         self.focus_pos_limit_last = 100
 
     def _draw(self, ctx):
@@ -377,6 +377,43 @@ class HeadphonesMenu(Submenu):
                 audio.headphones_get_maximum_volume_dB()
             )
 
+        self.y += 8
+
+        # note: jack detection is the inverse of headphones detection override
+        # jack detection off means headphones detection override on
+        tmp = self.draw_boolean(
+            "jack detection",
+            settings.onoff_headphones_detection_override.value,
+            off_str="on",  # sic
+            on_str="off",  # sic
+        )
+        if settings.onoff_headphones_detection_override.value != tmp:
+            settings.onoff_headphones_detection_override.set_value(tmp)
+            audio.headphones_detection_override(
+                settings.onoff_headphones_detection_override.value,
+                settings.onoff_headphones_detection_override_state.value,
+            )
+
+        if tmp:
+            self.num_widgets = 7
+            tmp = self.draw_boolean(
+                "headphone state",
+                settings.onoff_headphones_detection_override_state.value,
+                on_str="on",
+                off_str="off",
+                on_hint="sound will always play\nthrough headphones",
+                off_hint="sound will always play\nthrough speaker",
+            )
+            if settings.onoff_headphones_detection_override_state.value != tmp:
+                settings.onoff_headphones_detection_override_state.set_value(tmp)
+                audio.headphones_detection_override(
+                    settings.onoff_headphones_detection_override.value,
+                    settings.onoff_headphones_detection_override_state.value,
+                )
+        else:
+            self.num_widgets = 6
+            self.overhang = -80
+
 
 class VolumeControlMenu(Submenu):
     def __init__(self, press):
diff --git a/python_payload/st3m/run.py b/python_payload/st3m/run.py
index 644fa5c607..357cf372fe 100644
--- a/python_payload/st3m/run.py
+++ b/python_payload/st3m/run.py
@@ -159,6 +159,11 @@ def run_main() -> None:
     audio.onboard_mic_to_speaker_set_allowed(
         settings.onoff_onboard_mic_to_speaker_allowed.value
     )
+    if settings.onoff_headphones_detection_override.value:
+        audio.headphones_detection_override(
+            settings.onoff_headphones_detection_override.value,
+            settings.onoff_headphones_detection_override_state.value,
+        )
 
     leds.set_brightness(settings.num_leds_brightness.value)
     sys_display.set_backlight(settings.num_display_brightness.value)
diff --git a/python_payload/st3m/settings.py b/python_payload/st3m/settings.py
index 611ea14afe..979a95b320 100644
--- a/python_payload/st3m/settings.py
+++ b/python_payload/st3m/settings.py
@@ -255,6 +255,16 @@ onoff_onboard_mic_to_speaker_allowed = OnOffTunable(
     "system.audio.onboard_mic_to_speaker_allowed",
     False,
 )
+onoff_headphones_detection_override = OnOffTunable(
+    "Headphones detection override enabled",
+    "system.audio.headphones_detection_override",
+    False,
+)
+onoff_headphones_detection_override_state = OnOffTunable(
+    "Headphones detection override state",
+    "system.audio.headphones_detection_override_state",
+    True,
+)
 
 num_headset_mic_gain_db = NumberTunable(
     "Headset Mic Gain dB", "system.audio.headset_mic_gain_dB", 0
@@ -306,6 +316,8 @@ load_save_settings: List[UnaryTunable] = [
     onoff_onboard_mic_allowed,
     onoff_line_in_allowed,
     onoff_onboard_mic_to_speaker_allowed,
+    onoff_headphones_detection_override,
+    onoff_headphones_detection_override_state,
     num_headset_mic_gain_db,
     num_onboard_mic_gain_db,
     num_line_in_gain_db,
-- 
GitLab