From 9ac23e9f1dd833e1f3b60c3f0e7dd55a923aa265 Mon Sep 17 00:00:00 2001 From: Jonathan Schuster Date: Sun, 26 Apr 2026 14:39:15 +0200 Subject: [PATCH] fix: allow virtual ALSA PCM names (dsnoop etc.); log arecord stderr AudioSystem.find_device() now falls back to treating an unmatched spec as a direct ALSA PCM name when the ALSA backend is available. Virtual devices defined in asound.conf (dsnoop, plug, etc.) never appear in 'arecord -l' so they were always rejected as 'not found', even when valid. ALSAStream now captures arecord stderr via a reader thread instead of discarding it, so errors like 'Device or resource busy' are logged as warnings and visible in docker compose logs. --- isr.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/isr.py b/isr.py index 01abfac..4b05ec0 100644 --- a/isr.py +++ b/isr.py @@ -186,13 +186,15 @@ class ALSAStream: self._process = subprocess.Popen( cmd, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, + stderr=subprocess.PIPE, ) except FileNotFoundError: raise RuntimeError("arecord not found - install alsa-utils: sudo apt install alsa-utils") self._thread = threading.Thread(target=self._read_loop, daemon=True) + self._stderr_thread = threading.Thread(target=self._log_stderr, daemon=True) self._thread.start() + self._stderr_thread.start() return self def __exit__(self, *args): @@ -206,6 +208,12 @@ class ALSAStream: if self._thread: self._thread.join(timeout=1) + def _log_stderr(self): + for line in self._process.stderr: + line = line.decode('utf-8', errors='replace').rstrip() + if line: + self.logger.warning(f"arecord: {line}") + def _read_loop(self): chunk_size = self.sample_rate * self.channels * 2 // 10 # 100 ms chunks while self._running and self._process.poll() is None: @@ -306,6 +314,14 @@ class AudioSystem: if spec_lower in dev.name.lower(): return dev + # No hardware device matched. Treat spec as a direct ALSA PCM name so + # virtual devices defined in asound.conf (dsnoop, plug, etc.) work without + # appearing in `arecord -l`. + if 'alsa' in self._backends: + return AudioDevice( + id=spec, name=spec, channels=2, sample_rate=44100, backend='alsa' + ) + return None