Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions backend/src/decktation_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"}
Expand Down
48 changes: 48 additions & 0 deletions backend/src/wow_voice_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down