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.
This commit is contained in:
Username
2026-02-24 12:10:50 +01:00
parent 0aa7b81439
commit 6673ad187e

View File

@@ -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()