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:
@@ -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):
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user