"""Воспроизведение звука с возможностью быстро прервать.""" from __future__ import annotations from collections.abc import Callable import numpy as np import sounddevice as sd # Как часто проверяем, не пора ли остановиться _BLOCK_SECONDS = 0.05 class AudioPlayer: """Не потокобезопасен: все методы вызываются из одного потока воспроизведения.""" def __init__(self) -> None: self._stream: sd.OutputStream | None = None def play( self, audio: np.ndarray, sample_rate: int, should_continue: Callable[[], bool] ) -> None: """Блокирует до конца фрагмента; если should_continue() вернул False — обрывает звук.""" stream = self._open(sample_rate) samples = np.asarray(audio, dtype=np.float32).reshape(-1, 1) block = max(1, int(sample_rate * _BLOCK_SECONDS)) for start in range(0, len(samples), block): if not should_continue(): self.abort() return stream.write(samples[start : start + block]) def finish(self) -> None: """Дожидается, пока доиграет буфер, и освобождает устройство.""" if self._stream is not None: stream, self._stream = self._stream, None stream.stop() stream.close() def abort(self) -> None: """Немедленно глушит звук и освобождает устройство.""" if self._stream is not None: stream, self._stream = self._stream, None stream.abort() stream.close() def _open(self, sample_rate: int) -> sd.OutputStream: if self._stream is not None and self._stream.samplerate != sample_rate: self.finish() if self._stream is None: self._stream = sd.OutputStream(samplerate=sample_rate, channels=1, dtype="float32") self._stream.start() return self._stream