On-demand audio visualizer capture + UI fixes

- Audio capture starts only when first client subscribes,
  stops when last client unsubscribes (saves CPU/battery)
- Add lifecycle lock to AudioAnalyzer for thread-safe start/stop
- Status badge uses local visualizer state instead of server flag
- Fix script name vertical text break on narrow screens
- Fix script grid minimum column width on small viewports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 17:34:17 +03:00
parent 92d6709d58
commit 3846610042
5 changed files with 68 additions and 40 deletions

View File

@@ -53,6 +53,7 @@ class AudioAnalyzer:
self._running = False
self._thread: threading.Thread | None = None
self._lock = threading.Lock()
self._lifecycle_lock = threading.Lock()
self._data: dict | None = None
self._current_device_name: str | None = None
@@ -88,24 +89,26 @@ class AudioAnalyzer:
def start(self) -> bool:
"""Start audio capture in a background thread. Returns False if unavailable."""
if self._running:
return True
if not self.available:
return False
with self._lifecycle_lock:
if self._running:
return True
if not self.available:
return False
self._running = True
self._thread = threading.Thread(target=self._capture_loop, daemon=True)
self._thread.start()
return True
self._running = True
self._thread = threading.Thread(target=self._capture_loop, daemon=True)
self._thread.start()
return True
def stop(self) -> None:
"""Stop audio capture and cleanup."""
self._running = False
if self._thread:
self._thread.join(timeout=3.0)
self._thread = None
with self._lock:
self._data = None
with self._lifecycle_lock:
self._running = False
if self._thread:
self._thread.join(timeout=3.0)
self._thread = None
with self._lock:
self._data = None
def get_frequency_data(self) -> dict | None:
"""Return latest frequency data (thread-safe). None if not running."""