From 2c2620317b73067bd3c1a078e2473a7d49fce512 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:52:07 +0000 Subject: [PATCH 1/3] Initial plan From f70c958024e3355becf929b8e2cff12d518252af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:56:26 +0000 Subject: [PATCH 2/3] Accept ALSA devices on PortAudio no-match errors --- sendspin/audio_devices.py | 18 ++++++++++++++---- tests/test_audio_devices.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/sendspin/audio_devices.py b/sendspin/audio_devices.py index a365188..67b8eb3 100644 --- a/sendspin/audio_devices.py +++ b/sendspin/audio_devices.py @@ -268,6 +268,11 @@ def list_alsa_devices() -> list[tuple[str, str]]: return devices +def _alsa_device_exists(name: str) -> bool: + """Return True if ALSA lists *name* as a PCM device.""" + return any(dev_name == name for dev_name, _ in list_alsa_devices()) + + def resolve_audio_device(device_arg: str | None) -> AudioDevice: """Resolve audio device from a CLI argument. @@ -319,13 +324,18 @@ def _try_alsa_device(name: str) -> AudioDevice | None: """ try: sounddevice.check_output_settings(device=name) - except sounddevice.PortAudioError: - return None + except sounddevice.PortAudioError as err: + # PortAudio doesn't recognize this device name (e.g. hw:CARD=...,DEV=...). + # Validate it exists in ALSA before accepting it with safe defaults. + err_msg = str(err.args[0]) if err.args else "" + if "No output device matching" not in err_msg: + return None + if not _alsa_device_exists(name): + return None except ValueError: # PortAudio doesn't recognize this device name (e.g. hw:CARD=...,DEV=...). # Validate it exists in ALSA before accepting it with safe defaults. - alsa_names = {dev_name for dev_name, _ in list_alsa_devices()} - if name not in alsa_names: + if not _alsa_device_exists(name): return None # Try to query device info from PortAudio diff --git a/tests/test_audio_devices.py b/tests/test_audio_devices.py index dc52e42..aa751de 100644 --- a/tests/test_audio_devices.py +++ b/tests/test_audio_devices.py @@ -68,6 +68,39 @@ def test_try_alsa_device_accepts_hw_card_format_known_to_alsa(): assert result.sample_rate == 48000.0 +def test_try_alsa_device_accepts_hw_card_format_when_portaudio_reports_no_match(): + """hw:CARD=...,DEV=... should work when ALSA lists it but PortAudio can't match it.""" + alsa_list = [ + ( + "hw:CARD=sndrpihifiberry,DEV=0", + "snd_rpi_hifiberry_dacplus, HiFiBerry DAC+ HiFi", + ), + ] + with ( + patch.object( + sounddevice, + "check_output_settings", + side_effect=sounddevice.PortAudioError( + "No output device matching 'hw:CARD=sndrpihifiberry,DEV=0'", + -1, + "", + ), + ), + patch.object( + sounddevice, + "query_devices", + side_effect=ValueError("not found"), + ), + patch.object(_mod, "list_alsa_devices", return_value=alsa_list), + ): + result = _try_alsa_device("hw:CARD=sndrpihifiberry,DEV=0") + + assert result is not None + assert result.alsa_device_name == "hw:CARD=sndrpihifiberry,DEV=0" + assert result.output_channels == 2 + assert result.sample_rate == 48000.0 + + def test_try_alsa_device_returns_none_for_unknown_hw_card(): """hw:CARD=...,DEV=... not in ALSA list should return None.""" with ( From 3bc50fa5ce97305f6f72b35f4f5e5d08d220c76a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:58:03 +0000 Subject: [PATCH 3/3] Document PortAudio no-match fallback --- sendspin/audio_devices.py | 8 ++++++-- tests/test_audio_devices.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/sendspin/audio_devices.py b/sendspin/audio_devices.py index 67b8eb3..36f85bf 100644 --- a/sendspin/audio_devices.py +++ b/sendspin/audio_devices.py @@ -13,6 +13,8 @@ logger = logging.getLogger(__name__) +_PORTAUDIO_NO_OUTPUT_DEVICE_MATCHING = "No output device matching" + SOUNDDEVICE_DTYPE_MAP: dict[int, str] = { 16: "int16", 24: "int24", @@ -325,10 +327,12 @@ def _try_alsa_device(name: str) -> AudioDevice | None: try: sounddevice.check_output_settings(device=name) except sounddevice.PortAudioError as err: - # PortAudio doesn't recognize this device name (e.g. hw:CARD=...,DEV=...). + # sounddevice exposes this PortAudio lookup failure only in the + # error message, without a stable code to distinguish it from + # actual device-open failures. # Validate it exists in ALSA before accepting it with safe defaults. err_msg = str(err.args[0]) if err.args else "" - if "No output device matching" not in err_msg: + if _PORTAUDIO_NO_OUTPUT_DEVICE_MATCHING not in err_msg: return None if not _alsa_device_exists(name): return None diff --git a/tests/test_audio_devices.py b/tests/test_audio_devices.py index aa751de..2edc1c9 100644 --- a/tests/test_audio_devices.py +++ b/tests/test_audio_devices.py @@ -81,7 +81,7 @@ def test_try_alsa_device_accepts_hw_card_format_when_portaudio_reports_no_match( sounddevice, "check_output_settings", side_effect=sounddevice.PortAudioError( - "No output device matching 'hw:CARD=sndrpihifiberry,DEV=0'", + f"{_mod._PORTAUDIO_NO_OUTPUT_DEVICE_MATCHING} 'hw:CARD=sndrpihifiberry,DEV=0'", -1, "", ),