Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Gauthier Guinet <43207538+Gguinet@users.noreply.github.com>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Quentin <torroba.q@gmail.com>
Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com>
Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: angelapopopo <angele.lenglemetz@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-03-23 18:45:21 +01:00 committed by GitHub
parent 5103019b01
commit eb580209d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
180 changed files with 11136 additions and 1030 deletions

View file

@ -0,0 +1,206 @@
from __future__ import annotations
import io
import struct
from unittest.mock import MagicMock, patch
import wave
import pytest
try:
import sounddevice as sd
except OSError:
pytest.skip("PortAudio library not available", allow_module_level=True)
from vibe.core.audio_player.audio_player import AudioPlayer
from vibe.core.audio_player.audio_player_port import (
AlreadyPlayingError,
AudioBackendUnavailableError,
AudioFormat,
NoAudioOutputDeviceError,
UnsupportedAudioFormatError,
)
def _make_wav_bytes(
n_frames: int = 1024, sample_rate: int = 48_000, channels: int = 1
) -> bytes:
buf = io.BytesIO()
with wave.open(buf, "wb") as wf:
wf.setnchannels(channels)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes(
struct.pack(f"<{n_frames * channels}h", *([1000] * n_frames * channels))
)
return buf.getvalue()
def _get_callback(mock_stream_cls: MagicMock):
return mock_stream_cls.call_args.kwargs["callback"]
def _get_finished_callback(mock_stream_cls: MagicMock):
return mock_stream_cls.call_args.kwargs["finished_callback"]
class TestAudioPlayerInitialState:
def test_not_playing(self) -> None:
player = AudioPlayer()
assert player.is_playing is False
class TestPlayback:
@patch("vibe.core.audio_player.audio_player.sd.RawOutputStream")
def test_play_sets_playing_state(self, mock_stream_cls: MagicMock) -> None:
player = AudioPlayer()
player.play(_make_wav_bytes(), AudioFormat.WAV)
assert player.is_playing is True
mock_stream_cls.return_value.start.assert_called_once()
@patch("vibe.core.audio_player.audio_player.sd.RawOutputStream")
def test_play_when_already_playing_raises(self, mock_stream_cls: MagicMock) -> None:
player = AudioPlayer()
player.play(_make_wav_bytes(), AudioFormat.WAV)
with pytest.raises(AlreadyPlayingError):
player.play(_make_wav_bytes(), AudioFormat.WAV)
@patch("vibe.core.audio_player.audio_player.sd.RawOutputStream")
def test_callback_feeds_audio_data(self, mock_stream_cls: MagicMock) -> None:
wav_data = _make_wav_bytes(n_frames=512)
player = AudioPlayer()
player.play(wav_data, AudioFormat.WAV)
callback = _get_callback(mock_stream_cls)
outdata = bytearray(512 * 2) # 512 frames * 2 bytes per sample
callback(outdata, 512, {}, sd.CallbackFlags())
assert outdata != bytearray(512 * 2)
@patch("vibe.core.audio_player.audio_player.sd.RawOutputStream")
def test_callback_pads_silence_at_end(self, mock_stream_cls: MagicMock) -> None:
wav_data = _make_wav_bytes(n_frames=256)
player = AudioPlayer()
player.play(wav_data, AudioFormat.WAV)
callback = _get_callback(mock_stream_cls)
# First callback consumes all 256 frames
outdata1 = bytearray(256 * 2)
callback(outdata1, 256, {}, sd.CallbackFlags())
# Second callback should raise CallbackStop (no data left)
outdata2 = bytearray(256 * 2)
with pytest.raises(sd.CallbackStop):
callback(outdata2, 256, {}, sd.CallbackFlags())
@patch("vibe.core.audio_player.audio_player.sd.RawOutputStream")
def test_on_finished_called_after_natural_completion(
self, mock_stream_cls: MagicMock
) -> None:
finished = []
player = AudioPlayer()
player.play(
_make_wav_bytes(),
AudioFormat.WAV,
on_finished=lambda: finished.append(True),
)
finished_callback = _get_finished_callback(mock_stream_cls)
finished_callback()
assert player.is_playing is False
assert len(finished) == 1
@patch("vibe.core.audio_player.audio_player.sd.RawOutputStream")
def test_can_play_multiple_times(self, mock_stream_cls: MagicMock) -> None:
player = AudioPlayer()
player.play(_make_wav_bytes(), AudioFormat.WAV)
finished_callback = _get_finished_callback(mock_stream_cls)
finished_callback()
assert player.is_playing is False
player.play(_make_wav_bytes(), AudioFormat.WAV)
assert player.is_playing is True
@patch("vibe.core.audio_player.audio_player.sd.RawOutputStream")
def test_creates_stream_with_correct_params(
self, mock_stream_cls: MagicMock
) -> None:
wav_data = _make_wav_bytes(sample_rate=24_000, channels=1)
player = AudioPlayer()
player.play(wav_data, AudioFormat.WAV)
call_kwargs = mock_stream_cls.call_args.kwargs
assert call_kwargs["samplerate"] == 24_000
assert call_kwargs["channels"] == 1
assert call_kwargs["dtype"] == "int16"
class TestStop:
@patch("vibe.core.audio_player.audio_player.sd.RawOutputStream")
def test_stop_closes_stream(self, mock_stream_cls: MagicMock) -> None:
player = AudioPlayer()
player.play(_make_wav_bytes(), AudioFormat.WAV)
player.stop()
mock_stream_cls.return_value.close.assert_called_once()
@patch("vibe.core.audio_player.audio_player.sd.RawOutputStream")
def test_finished_callback_resets_state(self, mock_stream_cls: MagicMock) -> None:
player = AudioPlayer()
player.play(_make_wav_bytes(), AudioFormat.WAV)
finished_callback = _get_finished_callback(mock_stream_cls)
finished_callback()
assert player.is_playing is False
def test_stop_when_not_playing_is_noop(self) -> None:
player = AudioPlayer()
player.stop()
assert player.is_playing is False
@patch("vibe.core.audio_player.audio_player.sd.RawOutputStream")
def test_stop_triggers_on_finished_via_callback(
self, mock_stream_cls: MagicMock
) -> None:
finished = []
player = AudioPlayer()
player.play(
_make_wav_bytes(),
AudioFormat.WAV,
on_finished=lambda: finished.append(True),
)
# Simulate sounddevice calling finished_callback after stop
finished_callback = _get_finished_callback(mock_stream_cls)
finished_callback()
assert len(finished) == 1
class TestUnsupportedFormat:
def test_unsupported_format_raises(self) -> None:
player = AudioPlayer()
with pytest.raises(UnsupportedAudioFormatError):
player.play(b"fake data", "mp3") # type: ignore[arg-type]
class TestGuardAudioOutput:
def test_raises_when_no_sounddevice(self) -> None:
with patch("vibe.core.audio_player.audio_player.sd", None):
player = AudioPlayer()
with pytest.raises(AudioBackendUnavailableError):
player.play(_make_wav_bytes(), AudioFormat.WAV)
@patch("vibe.core.audio_player.audio_player.sd.RawOutputStream")
def test_raises_when_no_output_device(self, mock_stream_cls: MagicMock) -> None:
with patch(
"vibe.core.audio_player.audio_player.sd.query_devices",
side_effect=sd.PortAudioError(-1),
):
player = AudioPlayer()
with pytest.raises(NoAudioOutputDeviceError):
player.play(_make_wav_bytes(), AudioFormat.WAV)
assert player.is_playing is False
mock_stream_cls.assert_not_called()