# audio_player.py import pyaudio import threading import queue from typing import Optional _DEFAULTS = dict( format=pyaudio.paInt16, channels=2, rate=44100, chunk=1024, ) class _AudioPlayer: def __init__(self, **kw): # 用外界传入或缺省值 self.format = kw.get('format', _DEFAULTS['format']) self.channels = kw.get('channels', _DEFAULTS['channels']) self.rate = kw.get('rate', _DEFAULTS['rate']) self.chunk = kw.get('chunk', _DEFAULTS['chunk']) self._pa = pyaudio.PyAudio() self._stream = None self._q = queue.Queue() self._played = threading.Event() self._total_feed = 0 self._total_played = 0 self._lock = threading.Lock() self._thread = threading.Thread(target=self._worker, daemon=True) self._thread.start() # ---- 内部工作线程 ---- def _worker(self): while True: data = self._q.get() if data is None: break if self._stream is None: self._stream = self._pa.open( format=self.format, channels=self.channels, rate=self.rate, output=True, frames_per_buffer=self.chunk ) self._stream.write(data) with self._lock: self._total_played += len(data) if self._total_played >= self._total_feed: self._played.set() # ---- 对外接口 ---- def feed(self, data: bytes): if not isinstance(data, bytes): raise TypeError("feed() 需要 bytes") with self._lock: self._total_feed += len(data) self._played.clear() self._q.put(data) def wait(self): self._played.wait() def clear(self): """清空待播放队列并重置相关状态""" with self._lock: # 清空队列 while not self._q.empty(): try: self._q.get_nowait() except queue.Empty: break # 重置计数状态 unplayed = self._total_feed - self._total_played self._total_feed = self._total_played # 剩余未播放的都被清空了 if self._total_played >= self._total_feed: self._played.set() # ---- 清理 ---- def close(self): self._q.put(None) self._thread.join(timeout=1) if self._stream: self._stream.close() self._pa.terminate() # 模块级变量 _player: Optional[_AudioPlayer] = None def init(**kw): """一次性初始化播放器(必须在首次使用前调用)""" global _player if _player is not None: raise RuntimeError("player 已经初始化过了") _player = _AudioPlayer(**kw) def feed(data: bytes): if _player is None: raise RuntimeError("请先调用 audio_player.init(...)") _player.feed(data) def wait(): if _player is None: raise RuntimeError("请先调用 audio_player.init(...)") _player.wait() def clear(): """清空待播放的所有数据""" if _player is None: raise RuntimeError("请先调用 audio_player.init(...)") _player.clear() def close(): global _player if _player is not None: _player.close() _player = None