first commit
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user