diff --git a/backend/src/decktation_backend.py b/backend/src/decktation_backend.py index d019232..c78d419 100644 --- a/backend/src/decktation_backend.py +++ b/backend/src/decktation_backend.py @@ -33,6 +33,63 @@ sys.path.insert(0, dependency_path) logger.info(f"Added dependency path: {dependency_path}") +# Decky plugins run under systemd as root, which lacks the desktop user's session +# environment. Discover and set the user's runtime directory so that PortAudio +# and PipeWire can connect to the user session audio daemon (allowing USB, Bluetooth, +# and dynamically selected default microphones to be captured). +def _setup_audio_environment(): + if os.environ.get("XDG_RUNTIME_DIR") and os.environ.get("XDG_RUNTIME_DIR") != "/run/user/0": + return os.environ.get("XDG_RUNTIME_DIR") + candidate_uids = [] + decky_user_home = getattr(decky, "DECKY_USER_HOME", None) or os.environ.get("DECKY_USER_HOME") + if decky_user_home and os.path.exists(decky_user_home): + try: + candidate_uids.append(str(os.stat(decky_user_home).st_uid)) + except Exception: + pass + if os.environ.get("SUDO_UID"): + candidate_uids.append(os.environ["SUDO_UID"]) + if os.path.exists("/run/user"): + try: + for entry in sorted(os.listdir("/run/user")): + if entry != "0" and entry.isdigit(): + candidate_uids.append(entry) + except Exception: + pass + candidate_uids.extend(["1000", "1001", "1002"]) + + seen = set() + for uid in candidate_uids: + if uid in seen: + continue + seen.add(uid) + run_dir = f"/run/user/{uid}" + if os.path.exists(os.path.join(run_dir, "pipewire-0")): + os.environ["XDG_RUNTIME_DIR"] = run_dir + os.environ["PIPEWIRE_RUNTIME_DIR"] = run_dir + os.environ["PULSE_SERVER"] = f"unix:{run_dir}/pulse/native" + logger.info(f"Configured audio runtime directory: {run_dir}") + return run_dir + + +def ensure_audio_environment(): + """Ensure XDG_RUNTIME_DIR points to a valid PipeWire session and sounddevice is connected.""" + current_run_dir = os.environ.get("XDG_RUNTIME_DIR") + pipewire_available = current_run_dir and os.path.exists(os.path.join(current_run_dir, "pipewire-0")) + if not pipewire_available: + new_dir = _setup_audio_environment() + if new_dir and "sounddevice" in sys.modules: + try: + import sounddevice as sd + sd._terminate() + sd._initialize() + logger.info(f"Reconnected sounddevice to PipeWire ({new_dir})") + except Exception as e: + logger.warning(f"Failed to reinitialize sounddevice: {e}") + + +_setup_audio_environment() + # sounddevice normally searches only system library paths on Linux. Store # builds bundle PortAudio in bin/lib so the plugin works on clean SteamOS # installations without modifying the read-only operating system. @@ -880,6 +937,7 @@ async def set_active_preset(self, game: str): async def start_recording(self): """Start recording audio""" try: + ensure_audio_environment() if Plugin.voice_service is None: logger.error("Voice service not initialized") return {"success": False, "error": "Service not initialized"} diff --git a/backend/src/wow_voice_chat.py b/backend/src/wow_voice_chat.py index c99a9a4..04a9269 100644 --- a/backend/src/wow_voice_chat.py +++ b/backend/src/wow_voice_chat.py @@ -12,6 +12,53 @@ import subprocess from pathlib import Path from faster_whisper import WhisperModel + + +def _setup_audio_environment(): + if os.environ.get("XDG_RUNTIME_DIR") and os.environ.get("XDG_RUNTIME_DIR") != "/run/user/0": + return os.environ.get("XDG_RUNTIME_DIR") + candidate_uids = [] + if os.environ.get("SUDO_UID"): + candidate_uids.append(os.environ["SUDO_UID"]) + if os.path.exists("/run/user"): + try: + for entry in sorted(os.listdir("/run/user")): + if entry != "0" and entry.isdigit(): + candidate_uids.append(entry) + except Exception: + pass + candidate_uids.extend(["1000", "1001", "1002"]) + + seen = set() + for uid in candidate_uids: + if uid in seen: + continue + seen.add(uid) + run_dir = f"/run/user/{uid}" + if os.path.exists(os.path.join(run_dir, "pipewire-0")): + os.environ["XDG_RUNTIME_DIR"] = run_dir + os.environ["PIPEWIRE_RUNTIME_DIR"] = run_dir + os.environ["PULSE_SERVER"] = f"unix:{run_dir}/pulse/native" + return run_dir + + +def ensure_audio_environment(): + """Ensure XDG_RUNTIME_DIR points to a valid PipeWire session and sounddevice is connected.""" + current_run_dir = os.environ.get("XDG_RUNTIME_DIR") + pipewire_available = current_run_dir and os.path.exists(os.path.join(current_run_dir, "pipewire-0")) + if not pipewire_available: + new_dir = _setup_audio_environment() + if new_dir: + try: + import sounddevice as sd + sd._terminate() + sd._initialize() + except Exception: + pass + + +_setup_audio_environment() + import sounddevice as sd import numpy as np import wave @@ -619,6 +666,7 @@ def run_continuous(self, duration=5, pause=1): def start_recording(self): """Start recording audio (for push-to-talk)""" + ensure_audio_environment() with self.recording_lock: if self.is_recording: return