From 72e7b5d92eae664a9e370f7127bb6484d394db9e Mon Sep 17 00:00:00 2001
From: ave <ave@ave.zone>
Date: Sun, 20 Aug 2023 00:12:06 +0200
Subject: [PATCH] app: add audio passthrough app

---
 .../apps/audio_passthrough/__init__.py        | 142 ++++++++++++++++++
 .../apps/audio_passthrough/flow3r.toml        |  13 ++
 python_payload/mypystubs/audio.pyi            |   9 ++
 3 files changed, 164 insertions(+)
 create mode 100644 python_payload/apps/audio_passthrough/__init__.py
 create mode 100644 python_payload/apps/audio_passthrough/flow3r.toml

diff --git a/python_payload/apps/audio_passthrough/__init__.py b/python_payload/apps/audio_passthrough/__init__.py
new file mode 100644
index 0000000000..aa60653d6c
--- /dev/null
+++ b/python_payload/apps/audio_passthrough/__init__.py
@@ -0,0 +1,142 @@
+from st3m.application import Application, ApplicationContext
+from st3m.input import InputState
+from st3m.goose import Optional
+from st3m.ui.view import ViewManager
+from ctx import Context
+import audio
+import math
+
+
+# Assume this is an enum
+ForceModes = ["AUTO", "FORCE_LINE_IN", "FORCE_LINE_OUT", "FORCE_MIC", "FORCE_NONE"]
+
+
+STATE_TEXT: dict[int, str] = {
+    audio.INPUT_SOURCE_HEADSET_MIC: "using headset mic (line out)",
+    audio.INPUT_SOURCE_LINE_IN: "using line in",
+    audio.INPUT_SOURCE_ONBOARD_MIC: "using onboard mic",
+    audio.INPUT_SOURCE_NONE: "plug cable to line in/out",
+}
+
+
+class AudioPassthrough(Application):
+    def __init__(self, app_ctx: ApplicationContext) -> None:
+        super().__init__(app_ctx)
+        self._button_0_pressed = False
+        self._button_5_pressed = False
+        self._force_mode: str = "AUTO"
+
+    def on_enter(self, vm: Optional[ViewManager]) -> None:
+        self._force_mode = "AUTO"
+
+    def draw(self, ctx: Context) -> None:
+        ctx.text_align = ctx.CENTER
+        ctx.text_baseline = ctx.MIDDLE
+        ctx.font = ctx.get_font_name(8)
+
+        ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
+        ctx.rgb(1, 1, 1)
+
+        # top button
+        ctx.move_to(105, 0)
+        ctx.font_size = 15
+        ctx.save()
+        ctx.rotate((math.pi / 180) * 270)
+        ctx.text(">")
+        ctx.restore()
+
+        ctx.move_to(0, -90)
+        ctx.text("toggle passthrough")
+
+        # middle text
+        ctx.font_size = 25
+        ctx.move_to(0, 0)
+        ctx.save()
+        if audio.input_thru_get_mute():
+            # 0xff4500, red
+            ctx.rgb(1, 0.41, 0)
+        else:
+            # 0x3cb043, green
+            ctx.rgb(0.24, 0.69, 0.26)
+        ctx.text("passthrough off" if audio.input_thru_get_mute() else "passthrough on")
+        ctx.restore()
+
+        # bottom text
+        ctx.move_to(0, 25)
+        ctx.save()
+        ctx.font_size = 15
+        ctx.text(STATE_TEXT.get(audio.input_get_source(), ""))
+
+        # have red text when sleep mode isn't auto
+        if self._force_mode != "AUTO":
+            # 0xff4500, red
+            ctx.rgb(1, 0.41, 0)
+
+        ctx.move_to(0, 40)
+        ctx.text("(auto)" if self._force_mode == "AUTO" else "(forced)")
+
+        # mic has a loopback risk so has precautions
+        # so we warn users about it to not confuse them
+        if self._force_mode == "FORCE_MIC":
+            ctx.move_to(0, 55)
+            ctx.text("headphones only")
+            ctx.move_to(0, 70)
+            ctx.text("will not persist app exit")
+        ctx.restore()
+
+        # bottom button
+        ctx.move_to(105, 0)
+        ctx.font_size = 15
+        ctx.save()
+        ctx.rotate((math.pi / 180) * 90)
+        ctx.text(">")
+        ctx.restore()
+
+        ctx.move_to(0, 90)
+        ctx.text("force line in/out")
+
+    def on_exit(self) -> None:
+        # Mic passthrough has a loopback risk
+        if self._force_mode == "FORCE_MIC":
+            self._force_mode = "FORCE_NONE"
+            audio.input_set_source(audio.INPUT_SOURCE_NONE)
+            audio.input_thru_set_mute(True)
+
+    def think(self, ins: InputState, delta_ms: int) -> None:
+        super().think(ins, delta_ms)
+
+        headset_connected = audio.headset_is_connected()
+        if self._force_mode == "FORCE_MIC":
+            audio.input_set_source(audio.INPUT_SOURCE_ONBOARD_MIC)
+        elif headset_connected or self._force_mode == "FORCE_LINE_OUT":
+            audio.input_set_source(audio.INPUT_SOURCE_HEADSET_MIC)
+        elif audio.line_in_is_connected() or self._force_mode == "FORCE_LINE_IN":
+            audio.input_set_source(audio.INPUT_SOURCE_LINE_IN)
+        else:
+            audio.input_set_source(audio.INPUT_SOURCE_NONE)
+
+        if ins.captouch.petals[0].pressed:
+            if not self._button_0_pressed:
+                self._button_0_pressed = True
+                audio.input_thru_set_mute(not audio.input_thru_get_mute())
+        else:
+            self._button_0_pressed = False
+
+        if ins.captouch.petals[5].pressed:
+            if not self._button_5_pressed:
+                self._button_5_pressed = True
+                self._force_mode = ForceModes[ForceModes.index(self._force_mode) + 1]
+                if ForceModes.index(self._force_mode) >= ForceModes.index("FORCE_NONE"):
+                    self._force_mode = "AUTO"
+        else:
+            self._button_5_pressed = False
+
+        if self._force_mode == "FORCE_MIC" and not audio.headphones_are_connected():
+            self._force_mode = "AUTO"
+
+
+# For running with `mpremote run`:
+if __name__ == "__main__":
+    import st3m.run
+
+    st3m.run.run_view(AudioPassthrough(ApplicationContext()))
diff --git a/python_payload/apps/audio_passthrough/flow3r.toml b/python_payload/apps/audio_passthrough/flow3r.toml
new file mode 100644
index 0000000000..81f864112a
--- /dev/null
+++ b/python_payload/apps/audio_passthrough/flow3r.toml
@@ -0,0 +1,13 @@
+[app]
+name = "Audio Passthrough"
+menu = "Apps"
+
+[entry]
+class = "AudioPassthrough"
+
+[metadata]
+author = "ave"
+license = "LGPL-3.0-only"
+url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
+description = "Allows toggling audio passthrough through line-in/mic to speaker or lineout."
+version = 6
diff --git a/python_payload/mypystubs/audio.pyi b/python_payload/mypystubs/audio.pyi
index 40570a450a..2524d67333 100644
--- a/python_payload/mypystubs/audio.pyi
+++ b/python_payload/mypystubs/audio.pyi
@@ -10,6 +10,9 @@ def adjust_volume_dB(v: float) -> float:
 def headphones_are_connected() -> bool:
     pass
 
+def headset_is_connected() -> bool:
+    pass
+
 def line_in_is_connected() -> bool:
     pass
 
@@ -25,6 +28,12 @@ def input_set_source(source: int) -> None:
 def input_get_source() -> int:
     pass
 
+def input_thru_get_mute() -> bool:
+    pass
+
+def input_thru_set_mute(mute: bool) -> None:
+    pass
+
 INPUT_SOURCE_NONE: int
 INPUT_SOURCE_LINE_IN: int
 INPUT_SOURCE_HEADSET_MIC: int
-- 
GitLab