From f60ec71e781694c8d86604b57752cdca6a16cf4f Mon Sep 17 00:00:00 2001 From: media Date: Sun, 28 Jun 2026 14:28:25 -0500 Subject: [PATCH] Audio Router --- config.yaml | 56 +++++++++++++++++++ requirements.txt | 59 ++++++++++++++++++++ router.py | 142 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 config.yaml create mode 100644 requirements.txt create mode 100755 router.py diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..0963ed2 --- /dev/null +++ b/config.yaml @@ -0,0 +1,56 @@ +mqtt: + host: 10.40.0.2 + port: 1883 + base_topic: audio/player +routes: + mixer_to_hdmi: + source: mixer + output: hdmi + mixer_to_analog: + source: mixer + output: analog + bgmusic_to_hdmi: + source: bgmusic + output: hdmi + bgmusic_to_analog: + source: bgmusic + output: analog +ports: + mixer: + left: Scarlett 2i2 3rd Gen Analog Stereo:capture_FL + right: Scarlett 2i2 3rd Gen Analog Stereo:capture_FR + bgmusic: + left: SnapBGM:monitor_FL + right: SnapBGM:monitor_FR + hdmi: + left: Built-in Audio Pro 3:playback_AUX0 + right: Built-in Audio Pro 3:playback_AUX1 + analog: + left: Built-in Audio Pro:playback_AUX0 + right: Built-in Audio Pro:playback_AUX1 +presets: + mute_all: + mixer_to_hdmi: false + mixer_to_analog: false + bgmusic_to_hdmi: false + bgmusic_to_analog: false + service: + mixer_to_hdmi: true + mixer_to_analog: false + bgmusic_to_hdmi: false + bgmusic_to_analog: false + announcements: + mixer_to_hdmi: false + mixer_to_analog: false + bgmusic_to_hdmi: true + bgmusic_to_analog: true + aux_only: + mixer_to_hdmi: false + mixer_to_analog: false + bgmusic_to_hdmi: false + bgmusic_to_analog: true + video: + mixer_to_hdmi: false + mixer_to_analog: false + bgmusic_to_hdmi: true + bgmusic_to_analog: false diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dda74d4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,59 @@ +attrs==22.2.0 +Brotli==1.0.9 +certifi==2022.9.24 +cffi==1.15.1 +chardet==5.1.0 +charset-normalizer==3.0.1 +crit==3.17.1 +cupshelpers==1.0 +dbus-python==1.3.2 +distro==1.8.0 +distro-info==1.5+deb12u1 +docker==5.0.3 +docker-compose==1.29.2 +dockerpty==0.4.1 +docopt==0.6.2 +gyp==0.1 +httplib2==0.20.4 +idna==3.3 +JACK-Client==0.5.3 +jsonpointer==2.3 +jsonschema==4.10.3 +Mako==1.2.4.dev0 +Markdown==3.4.1 +MarkupSafe==2.1.2 +mutagen==1.46.0 +numpy==1.24.2 +paho-mqtt==2.1.0 +ply==3.11 +protobuf==4.21.12 +pycairo==1.20.1 +pycparser==2.21 +pycryptodomex==3.11.0 +pycups==2.0.1 +pycurl==7.45.2 +Pygments==2.14.0 +PyGObject==3.42.2 +pyjack==0.3.2 +pyparsing==3.0.9 +pyrsistent==0.18.1 +PySimpleSOAP==1.16.2 +pysmbc==1.0.23 +python-apt==2.6.0 +python-debian==0.1.49 +python-debianbts==4.0.1 +python-dotenv==0.21.0 +pyxattr==0.8.1 +PyYAML==6.0.3 +reportbug==12.0.0 +requests==2.28.1 +rfc3987==1.3.8 +six==1.16.0 +texttable==1.6.7 +unattended-upgrades==0.1 +uritemplate==4.1.1 +urllib3==1.26.12 +webcolors==1.11.1 +websocket-client==1.2.3 +websockets==10.4 +yt-dlp==2025.9.26 diff --git a/router.py b/router.py new file mode 100755 index 0000000..80f4031 --- /dev/null +++ b/router.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +import json +import signal +import time + +import jack +import yaml +import paho.mqtt.client as mqtt + + +CONFIG_FILE = "config.yaml" + + +class JackRouter: + def __init__(self): + with open(CONFIG_FILE, "r") as f: + self.cfg = yaml.safe_load(f) + + self.client = jack.Client("AudioRouter") + self.mode = "mute_all" + self.base_topic = self.cfg["mqtt"]["base_topic"].rstrip("/") + + def connect(self, src, dst): + try: + if not self.client.get_all_connections(dst).count(src): + self.client.connect(src, dst) + print(f"Connected: {src} -> {dst}") + except jack.JackError as e: + print(f"Connect failed: {src} -> {dst}: {e}") + + def disconnect(self, src, dst): + try: + self.client.disconnect(src, dst) + print(f"Disconnected: {src} -> {dst}") + except jack.JackError: + pass + + def clear_routes(self): + all_routes = self.cfg["routes"] + + for route in all_routes.values(): + source = self.cfg["ports"][route["source"]] + output = self.cfg["ports"][route["output"]] + + pairs = [ + (source["left"], output["left"]), + (source["right"], output["right"]), + ] + + for src, dst in pairs: + self.disconnect(src, dst) + + def routes_for_mode(self, mode): + preset = self.cfg["presets"].get(mode) + + if preset is None: + return None + + resolved_routes = [] + + for route_name, enabled in preset.items(): + if not enabled: + continue + + route = self.cfg["routes"][route_name] + + source_name = route["source"] + output_name = route["output"] + + source = self.cfg["ports"][source_name] + output = self.cfg["ports"][output_name] + + resolved_routes.append((source["left"], output["left"])) + resolved_routes.append((source["right"], output["right"])) + + return resolved_routes + + def set_mode(self, mode): + routes = self.routes_for_mode(mode) + if routes is None: + print(f"Unknown mode: {mode}") + return False + + print(f"Setting mode: {mode}") + self.clear_routes() + + for src, dst in routes: + self.connect(src, dst) + + self.mode = mode + return True + + def state(self): + return { + "mode": self.mode, + "running": True, + "timestamp": int(time.time()), + } + + +def main(): + router = JackRouter() + + mqtt_cfg = router.cfg["mqtt"] + client = mqtt.Client() + + def publish_state(): + client.publish(f"{router.base_topic}/state", json.dumps(router.state()), retain=True) + client.publish(f"{router.base_topic}/mode/state", router.mode, retain=True) + + def on_connect(client, userdata, flags, rc): + print(f"MQTT connected: {rc}") + client.subscribe(f"{router.base_topic}/mode/set") + router.set_mode("aux_only") + publish_state() + + def on_message(client, userdata, msg): + mode = msg.payload.decode().strip() + print(f"MQTT received: {msg.topic} = {mode}") + + if router.set_mode(mode): + publish_state() + + def shutdown(*args): + print("Stopping router...") + router.set_mode("mute_all") + publish_state() + raise SystemExit + + signal.signal(signal.SIGINT, shutdown) + signal.signal(signal.SIGTERM, shutdown) + + client.on_connect = on_connect + client.on_message = on_message + + with router.client: + client.connect(mqtt_cfg["host"], int(mqtt_cfg["port"]), 60) + client.loop_forever() + + +if __name__ == "__main__": + main() \ No newline at end of file