Skip to content

Commit 9d81b05

Browse files
balloobclaude
andauthored
Fall back to software volume when PulseAudio is unreachable (#143)
## Summary - Add `async_check_available()` to `hardware_volume.py` that tests actual PulseAudio connectivity, not just library import - In `cli.py`, use the runtime check before starting — if PulseAudio isn't reachable, disable hardware volume upfront with a warning - In `audio_connector.py`, add a safety net in `read_initial_volume()` so if PulseAudio fails at runtime, it falls back to software volume instead of crashing Fixes #141 ## Test plan - [ ] On Linux with PulseAudio running: hardware volume should work as before - [ ] On Linux without PulseAudio running: should log a warning and fall back to software volume - [ ] On Linux without `pulsectl-asyncio` installed + `--hardware-volume true`: should still raise a clear error asking the user to install the library - [ ] Daemon mode auto-detection: should only enable hardware volume if PulseAudio is actually reachable 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1ac529a commit 9d81b05

2 files changed

Lines changed: 26 additions & 0 deletions

File tree

‎sendspin/cli.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing import TYPE_CHECKING
1414

1515
from sendspin.hardware_volume import AVAILABLE as HW_VOLUME_AVAILABLE
16+
from sendspin.hardware_volume import async_check_available as hw_volume_check_available
1617
from sendspin.settings import ClientSettings, get_client_settings, get_serve_settings
1718

1819
if TYPE_CHECKING:
@@ -600,6 +601,10 @@ async def _run_client_mode(args: argparse.Namespace) -> int:
600601
# Set up logging with resolved log level
601602
logging.basicConfig(level=getattr(logging, args.log_level))
602603

604+
if args.hardware_volume and not await hw_volume_check_available():
605+
LOGGER.warning("PulseAudio server not reachable, falling back to software volume control")
606+
args.hardware_volume = False
607+
603608
# Handle daemon subcommand
604609
if args.command == "daemon":
605610
return await _run_daemon_mode(args, settings)

‎sendspin/hardware_volume.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@
2323
VolumeChangeCallback = Callable[[int, bool], None]
2424

2525

26+
async def async_check_available(timeout: float = 2.0) -> bool:
27+
"""Check if PulseAudio is actually reachable at runtime.
28+
29+
Returns True only if we can connect to the PulseAudio server.
30+
This goes beyond the module-level AVAILABLE check (which only verifies
31+
the library is installed) by testing the actual connection. The check
32+
is bounded by *timeout* seconds to keep CLI startup responsive when
33+
PulseAudio is down or unreachable.
34+
"""
35+
if not AVAILABLE:
36+
return False
37+
38+
try:
39+
async with asyncio.timeout(timeout):
40+
async with pulsectl_asyncio.PulseAsync("sendspin-cli-check") as client:
41+
await client.server_info()
42+
return True
43+
except Exception: # noqa: BLE001
44+
return False
45+
46+
2647
class HardwareVolumeController:
2748
"""Controls Linux system output volume through PulseAudio API.
2849

0 commit comments

Comments
 (0)