"""Player — воспроизведение с мгновенной остановкой. Клавиша «Замолчи» = player.stop(): sounddevice.stop() прерывает вывод в течение ~миллисекунд, очередь очищается. Воспроизведение идёт в фоновом потоке; is_playing отражает состояние. last_start_latency — сколько времени занял вызов sd.play() (открытие потока вывода до первого блока): замеряется для диагностики латентности ответа. """ from __future__ import annotations import threading import time from pathlib import Path from typing import Optional, Union import numpy as np import sounddevice as sd import soundfile as sf class Player: def __init__(self, device: Optional[int] = None, volume: float = 1.0) -> None: self._device = device self.volume = volume # 0.0 .. 2.0 self._lock = threading.Lock() self._playing = False self._thread: Optional[threading.Thread] = None self.last_start_latency = 0.0 # сек: длительность вызова sd.play() @property def is_playing(self) -> bool: return self._playing def play(self, data: np.ndarray, samplerate: int, blocking: bool = False, lead_silence_sec: float = 0.1) -> None: """Воспроизвести float32 mono/stereo. Не блокирует (если blocking=False). lead_silence_sec — тишина в начале: защита от «съедания» первого слова при открытии потока вывода (недозаполненный буфер на некоторых ALSA/Pulse). """ audio = np.asarray(data, dtype=np.float32) if audio.ndim == 1: audio = audio[:, np.newaxis] # (frames,) → (frames, 1) if lead_silence_sec > 0: pad = np.zeros((int(lead_silence_sec * samplerate), audio.shape[1]), dtype=np.float32) audio = np.concatenate([pad, audio]) gain = float(np.clip(self.volume, 0.0, 2.0)) if gain != 1.0: audio = np.clip(audio * gain, -1.0, 1.0) with self._lock: # Новая команда play отменяет предыдущую (накладывать нельзя) sd.stop() def _run() -> None: try: t0 = time.perf_counter() sd.play(audio, samplerate, device=self._device) self.last_start_latency = time.perf_counter() - t0 sd.wait() # вернётся сразу после stop() или конца аудио finally: self._playing = False self._playing = True if blocking: _run() else: self._thread = threading.Thread(target=_run, daemon=True) self._thread.start() def play_file(self, path: Union[str, Path], blocking: bool = False) -> None: """Воспроизвести wav/flac/ogg с диска.""" path = Path(path) data, sr = sf.read(str(path), dtype="float32", always_2d=False) self.play(data, sr, blocking=blocking) def stop(self) -> None: """Мгновенно заглушить воспроизведение (клавиша «Замолчи»).""" with self._lock: sd.stop() self._playing = False def wait(self) -> None: """Дождаться конца текущего воспроизведения.""" t = self._thread if t is not None and t.is_alive(): t.join()