first commit

This commit is contained in:
workD12
2026-09-11 19:47:15 +03:00
commit 32754a1fe2
56 changed files with 3836 additions and 0 deletions
+89
View File
@@ -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()