Files
2026-09-11 19:47:15 +03:00

89 lines
3.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()