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