v2.6.0 (#524)
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:
parent
5103019b01
commit
eb580209d4
180 changed files with 11136 additions and 1030 deletions
206
tests/audio_player/test_audio_player.py
Normal file
206
tests/audio_player/test_audio_player.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue