fix(audio): stop passing explicit index when zero devices enumerate (#891)

* fix(audio): stop passing explicit index when zero devices enumerate

With zero audio devices, _resolve_valid_input_device returned the saved preferred_index, so _open_capture_stream opened input_device_index=<stale> — a device that cannot exist — and Pa_OpenStream aborted the whole process (SIGSEGV) before the clean IOError->ERROR path could run. Zero enumerated devices now returns None so PyAudio falls back to the system default and fails catchably; a negative count (enumeration failure) still passes the preferred index through.

* test(audio): cover capture path omitting device index

* test(audio): cover saved-device zero-enumeration capture path

* test(audio): annotate new resolver tests
This commit is contained in:
Jatin K Malik
2026-09-29 22:26:16 -07:00
committed by GitHub
parent a89966a744
commit 77def71354
2 changed files with 66 additions and 4 deletions
@@ -498,8 +498,14 @@ def _resolve_valid_input_device(audio, preferred_index: Optional[int] = None) ->
# MagicMock-based tests or misbehaving drivers can yield non-int counts.
return preferred_index
if device_count <= 0:
# No enumeration available; let PyAudio fall back to system default.
if device_count == 0:
# Enumeration worked and reported zero devices: no index can be valid,
# and PortAudio aborts the process when asked to open one. Returning
# None omits the explicit index so PyAudio uses the system default and
# surfaces a catchable error instead.
return None
if device_count < 0:
# Enumeration failed; the preferred device may still exist by index.
return preferred_index
for i in range(device_count):
+58 -2
View File
@@ -396,11 +396,21 @@ class TestResolveValidInputDevice:
audio.get_device_count.side_effect = OSError("driver dead")
assert _resolve_valid_input_device(audio, preferred_index=3) == 3
def test_zero_device_count_returns_preferred(self):
def test_zero_device_count_returns_none(self) -> None:
audio = MagicMock()
audio.get_default_input_device_info.side_effect = IOError("nope")
audio.get_device_count.return_value = 0
assert _resolve_valid_input_device(audio, preferred_index=7) == 7
# Zero enumerated devices means no index can exist — the caller must
# fall back to the system default rather than open an explicit index.
assert _resolve_valid_input_device(audio, preferred_index=7) is None
def test_negative_device_count_returns_preferred(self) -> None:
audio = MagicMock()
audio.get_default_input_device_info.side_effect = IOError("nope")
audio.get_device_count.return_value = -1
# Enumeration failure (host API error sentinel) cannot tell whether
# the preferred index exists, so it passes through.
assert _resolve_valid_input_device(audio, preferred_index=3) == 3
def test_non_dict_info_is_treated_as_valid(self):
audio = MagicMock()
@@ -422,6 +432,52 @@ class TestResolveValidInputDevice:
audio.get_device_info_by_index.side_effect = info
assert _resolve_valid_input_device(audio, preferred_index=None) == 1
def test_capture_stream_omits_index_when_resolver_returns_none(self) -> None:
mock_pa_mod = MagicMock()
mock_pa_mod.paInt16 = 8
audio = MagicMock()
audio.get_default_input_device_info.return_value = {
"name": "Default Mic",
"index": 0,
"maxInputChannels": 1,
"defaultSampleRate": 16000,
}
with patch.dict("sys.modules", {"pyaudio": mock_pa_mod}):
channels, rate, stream = rm._open_capture_stream(audio, device_index=None)
assert stream is audio.open.return_value
audio.open.assert_called_once()
assert "input_device_index" not in audio.open.call_args.kwargs
assert (channels, rate) == (1, 16000)
def test_saved_device_zero_enumeration_opens_default(self) -> None:
# End-to-end check of the fix: a stale saved index must not reach
# PortAudio when enumeration reports zero devices.
mock_pa_mod = MagicMock()
mock_pa_mod.paInt16 = 8
audio = MagicMock()
audio.get_default_input_device_info.side_effect = IOError("no default")
audio.get_device_count.return_value = 0
resolved = _resolve_valid_input_device(audio, preferred_index=7)
assert resolved is None
audio.get_default_input_device_info.side_effect = None
audio.get_default_input_device_info.return_value = {
"name": "Default Mic",
"index": 0,
"maxInputChannels": 1,
"defaultSampleRate": 16000,
}
with patch.dict("sys.modules", {"pyaudio": mock_pa_mod}):
rm._open_capture_stream(audio, device_index=resolved)
audio.open.assert_called_once()
assert "input_device_index" not in audio.open.call_args.kwargs
class TestDetectPywhispercppGpuBackend:
"""Coverage for the runtime GPU library detector."""