init
This commit is contained in:
125
audio_player.py
Normal file
125
audio_player.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user