From 6673ad187e8e65fa0bdc45c0597fb087fb8351a5 Mon Sep 17 00:00:00 2001 From: Username Date: Tue, 24 Feb 2026 12:10:50 +0100 Subject: [PATCH] app: wire audio pipeline and ptt Start AudioPipeline on server connect, send loop polls capture queue, PTT toggles mic encoding, incoming sound queued for playback. Audio failure logs to chatlog without crashing. --- src/tuimble/app.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/tuimble/app.py b/src/tuimble/app.py index eef5e78..92b591e 100644 --- a/src/tuimble/app.py +++ b/src/tuimble/app.py @@ -4,6 +4,7 @@ from __future__ import annotations import html import logging +import time from textual import events, on, work from textual.app import App, ComposeResult @@ -12,6 +13,7 @@ from textual.message import Message from textual.reactive import reactive from textual.widgets import Footer, Header, Input, RichLog, Static +from tuimble.audio import AudioPipeline from tuimble.client import Channel, MumbleClient from tuimble.config import Config, load_config from tuimble.ptt import KittyPtt, TogglePtt, detect_backend @@ -210,6 +212,13 @@ class TuimbleApp(App): username=srv.username, password=srv.password, ) + acfg = self._config.audio + self._audio = AudioPipeline( + sample_rate=acfg.sample_rate, + frame_size=acfg.frame_size, + input_device=acfg.input_device, + output_device=acfg.output_device, + ) def compose(self) -> ComposeResult: yield Header() @@ -238,6 +247,7 @@ class TuimbleApp(App): self._client.on_text_message = self._cb_text_message self._client.on_user_update = self._cb_state_changed self._client.on_channel_update = self._cb_state_changed + self._client.on_sound_received = self._cb_sound_received try: self._client.connect() @@ -256,6 +266,9 @@ class TuimbleApp(App): def _cb_state_changed(self) -> None: self.post_message(ServerStateChanged()) + def _cb_sound_received(self, _user, pcm_data: bytes) -> None: + self._audio.queue_playback(pcm_data) + def _show_error(self, text: str) -> None: chatlog = self.query_one("#chatlog", ChatLog) chatlog.write(f"[#f7768e]\u2717 {text}[/]") @@ -273,6 +286,7 @@ class TuimbleApp(App): f"[#9ece6a]\u2713 connected as {self._config.server.username}[/]" ) self._refresh_channel_tree() + self._start_audio() def on_server_disconnected(self, _msg: ServerDisconnected) -> None: status = self.query_one("#status", StatusBar) @@ -311,6 +325,29 @@ class TuimbleApp(App): f"[#e0af68]{self._config.server.username}[/] {text}" ) + # -- audio --------------------------------------------------------------- + + def _start_audio(self) -> None: + """Start audio pipeline; log error if hardware unavailable.""" + chatlog = self.query_one("#chatlog", ChatLog) + try: + self._audio.start() + chatlog.write("[dim]audio pipeline started[/dim]") + self._audio_send_loop() + except Exception as exc: + chatlog.write(f"[#f7768e]\u2717 audio: {exc}[/]") + log.warning("audio start failed: %s", exc) + + @work(thread=True) + def _audio_send_loop(self) -> None: + """Poll capture queue and send encoded frames to server.""" + while self._client.connected: + frame = self._audio.get_encoded_frame() + if frame is not None: + self._client.send_audio(frame) + else: + time.sleep(0.005) + # -- channel tree -------------------------------------------------------- def _refresh_channel_tree(self) -> None: @@ -344,12 +381,14 @@ class TuimbleApp(App): self._ptt.toggle() def _on_ptt_change(self, transmitting: bool) -> None: + self._audio.capturing = transmitting status = self.query_one("#status", StatusBar) status.ptt_active = transmitting # -- lifecycle ----------------------------------------------------------- def action_quit(self) -> None: + self._audio.stop() self._client.set_dispatcher(None) self._client.disconnect() self.exit()