[本机音频 Transport]:完成真实设备检查和sounddevice输入输出,包含device-check、RawInputStream和播放错误恢复

This commit is contained in:
mkbk
2026-06-17 19:37:21 +08:00
parent e877cca00f
commit ac97daa1e7
5 changed files with 288 additions and 27 deletions
+6
View File
@@ -71,6 +71,12 @@ class CliAcceptanceTests(unittest.TestCase):
self.assertFalse(data["ok"])
self.assertTrue(data["missing_files"])
def test_device_check_reports_sounddevice_status(self) -> None:
with patch("owner_voice_pet.cli.sounddevice_device_report", return_value={"ok": True, "devices": []}):
code, data = self.call("device-check")
self.assertEqual(code, 0)
self.assertTrue(data["ok"])
if __name__ == "__main__":
unittest.main()
+76
View File
@@ -65,6 +65,82 @@ class TransportTests(unittest.TestCase):
self.assertIsInstance(health.input_available, bool)
self.assertIsInstance(health.output_available, bool)
def test_sounddevice_transport_reads_from_raw_input_stream(self) -> None:
fake = FakeSoundDevice()
transport = SoundDeviceAudioTransport(sounddevice_module=fake)
transport.start_input(sample_rate=16000, channels=1)
frames = transport.read_frames(10)
transport.stop()
self.assertEqual(len(frames), 1)
self.assertEqual(frames[0].pcm, b"\x01\x00\x02\x00")
self.assertEqual(frames[0].sample_rate, 16000)
def test_sounddevice_transport_plays_pcm_to_raw_output_stream(self) -> None:
fake = FakeSoundDevice()
transport = SoundDeviceAudioTransport(sounddevice_module=fake)
result = transport.play_pcm(AudioSegment(b"\x00\x00\x01\x00", 16000, 1, 0, 20))
self.assertTrue(result.played)
self.assertEqual(fake.output_writes, [b"\x00\x00\x01\x00"])
def test_sounddevice_device_report_uses_query_devices(self) -> None:
fake = FakeSoundDevice()
report = SoundDeviceAudioTransport(sounddevice_module=fake).device_report()
self.assertTrue(report["ok"])
self.assertEqual(len(report["devices"]), 2)
class FakeInputStream:
def __init__(self, callback, **kwargs) -> None:
self.callback = callback
self.kwargs = kwargs
self.started = False
self.closed = False
def start(self) -> None:
self.started = True
self.callback(b"\x01\x00\x02\x00", 2, None, "")
def stop(self) -> None:
self.started = False
def close(self) -> None:
self.closed = True
class FakeOutputStream:
def __init__(self, owner, **kwargs) -> None:
self.owner = owner
self.kwargs = kwargs
def __enter__(self):
return self
def __exit__(self, *args) -> None:
return None
def write(self, data: bytes) -> None:
self.owner.output_writes.append(bytes(data))
class FakeSoundDevice:
def __init__(self) -> None:
self.output_writes: list[bytes] = []
def RawInputStream(self, **kwargs):
return FakeInputStream(**kwargs)
def RawOutputStream(self, **kwargs):
return FakeOutputStream(self, **kwargs)
def query_devices(self):
return [
{"name": "Mic", "max_input_channels": 1, "max_output_channels": 0, "default_samplerate": 16000},
{"name": "Speaker", "max_input_channels": 0, "max_output_channels": 2, "default_samplerate": 48000},
]
if __name__ == "__main__":
unittest.main()