audioplayer.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. ###############################################################################
  2. # 实时音频播放模块 - 监听 WAV 文件夹并播放新音频
  3. ###############################################################################
  4. import os
  5. import time
  6. import wave
  7. import numpy as np
  8. import threading
  9. import queue
  10. from pathlib import Path
  11. from logger import logger
  12. class RealtimeAudioPlayer:
  13. """实时监控文件夹并播放新到达的 WAV 音频"""
  14. def __init__(self, watch_dir, sample_rate=16000):
  15. """
  16. 初始化音频播放器
  17. Args:
  18. watch_dir: 监听的文件夹路径
  19. sample_rate: 采样率(默认16000Hz)
  20. """
  21. self.watch_dir = watch_dir
  22. self.sample_rate = sample_rate
  23. # 确保监听目录存在
  24. os.makedirs(watch_dir, exist_ok=True)
  25. # 状态管理
  26. self.is_running = False
  27. self.is_playing = False
  28. self.current_audio = None
  29. # 音频数据队列
  30. self.audio_queue = queue.Queue(maxsize=50)
  31. # 已处理的文件
  32. self.processed_files = set()
  33. # 线程控制
  34. self.watch_thread = None
  35. self.play_thread = None
  36. self.stop_event = threading.Event()
  37. # 回调函数
  38. self.on_audio = None
  39. logger.info(f"🎵 实时音频播放器已初始化")
  40. logger.info(f" 监听目录: {watch_dir}")
  41. logger.info(f" 采样率: {sample_rate}Hz")
  42. def set_callbacks(self, on_audio=None):
  43. """
  44. 设置回调函数
  45. Args:
  46. on_audio: 音频数据回调 (audio_data: np.ndarray)
  47. """
  48. self.on_audio = on_audio
  49. def start(self):
  50. """启动监听和播放"""
  51. if self.is_running:
  52. logger.warning("⚠️ 音频播放器已在运行")
  53. return
  54. logger.info("🎵 启动实时音频播放器...")
  55. self.is_running = True
  56. self.stop_event.clear()
  57. # 启动监听线程
  58. self.watch_thread = threading.Thread(target=self._watch_loop, daemon=True)
  59. self.watch_thread.start()
  60. # 启动播放线程
  61. self.play_thread = threading.Thread(target=self._play_loop, daemon=True)
  62. self.play_thread.start()
  63. logger.info("✅ 实时音频播放器已启动")
  64. def stop(self):
  65. """停止播放"""
  66. if not self.is_running:
  67. return
  68. logger.info("🛑 停止实时音频播放器...")
  69. self.is_running = False
  70. self.stop_event.set()
  71. if self.watch_thread:
  72. self.watch_thread.join(timeout=2)
  73. if self.play_thread:
  74. self.play_thread.join(timeout=2)
  75. logger.info("✅ 实时音频播放器已停止")
  76. def _watch_loop(self):
  77. """监听文件夹循环"""
  78. logger.info("👁️ 开始监听音频文件...")
  79. while not self.stop_event.is_set():
  80. try:
  81. # 查找所有 WAV 文件
  82. wav_files = list(Path(self.watch_dir).glob('*.wav'))
  83. # 过滤未处理的文件
  84. new_wavs = [f for f in wav_files if f.name not in self.processed_files]
  85. if new_wavs:
  86. # 按修改时间排序,处理最新的
  87. new_wavs.sort(key=lambda p: p.stat().st_mtime)
  88. for wav_path in new_wavs:
  89. if self.stop_event.is_set():
  90. break
  91. logger.info(f"📥 检测到新音频文件: {wav_path.name}")
  92. self._load_audio(wav_path)
  93. self.processed_files.add(wav_path.name)
  94. # 等待一段时间再检查
  95. time.sleep(0.5)
  96. except Exception as e:
  97. logger.error(f"❌ 监听音频文件出错: {e}")
  98. time.sleep(1)
  99. def _load_audio(self, wav_path):
  100. """
  101. 加载 WAV 文件到队列
  102. Args:
  103. wav_path: WAV 文件路径
  104. """
  105. try:
  106. # 读取 WAV 文件
  107. with wave.open(str(wav_path), 'rb') as wf:
  108. n_channels = wf.getnchannels()
  109. sample_width = wf.getsampwidth()
  110. framerate = wf.getframerate()
  111. n_frames = wf.getnframes()
  112. # 读取所有音频数据
  113. audio_data = wf.readframes(n_frames)
  114. # 转换为 numpy 数组
  115. if sample_width == 2:
  116. audio_array = np.frombuffer(audio_data, dtype=np.int16)
  117. elif sample_width == 1:
  118. audio_array = np.frombuffer(audio_data, dtype=np.uint8)
  119. else:
  120. logger.warning(f"⚠️ 不支持的采样宽度: {sample_width}")
  121. return
  122. # 如果是立体声,转换为单声道
  123. if n_channels == 2:
  124. audio_array = audio_array.reshape(-1, 2).mean(axis=1).astype(np.int16)
  125. # 重采样(如果需要)
  126. if framerate != self.sample_rate:
  127. logger.info(f"🔄 重采样: {framerate}Hz -> {self.sample_rate}Hz")
  128. audio_array = self._resample(audio_array, framerate, self.sample_rate)
  129. # 添加到队列
  130. logger.info(f"📤 音频已加入队列: {wav_path.name}, 时长: {len(audio_array)/self.sample_rate:.2f}s")
  131. self.audio_queue.put(audio_array)
  132. except Exception as e:
  133. logger.error(f"❌ 加载音频文件失败 {wav_path}: {e}")
  134. def _resample(self, audio_data, orig_sr, target_sr):
  135. """
  136. 简单的线性插值重采样
  137. Args:
  138. audio_data: 音频数据
  139. orig_sr: 原始采样率
  140. target_sr: 目标采样率
  141. """
  142. ratio = target_sr / orig_sr
  143. new_length = int(len(audio_data) * ratio)
  144. new_data = np.zeros(new_length, dtype=np.int16)
  145. for i in range(new_length):
  146. orig_idx = i / ratio
  147. idx = int(orig_idx)
  148. frac = orig_idx - idx
  149. if idx + 1 < len(audio_data):
  150. new_data[i] = int(audio_data[idx] * (1 - frac) + audio_data[idx + 1] * frac)
  151. else:
  152. new_data[i] = audio_data[idx]
  153. return new_data
  154. def _play_loop(self):
  155. """播放音频数据循环"""
  156. logger.info("🎵 开始播放音频...")
  157. while not self.stop_event.is_set():
  158. try:
  159. # 从队列获取音频数据
  160. if not self.audio_queue.empty():
  161. audio_data = self.audio_queue.get(timeout=0.1)
  162. self.is_playing = True
  163. logger.info(f"🔊 正在播放音频,长度: {len(audio_data)/self.sample_rate:.2f}s")
  164. # 通过回调发送音频数据
  165. if self.on_audio:
  166. # 计算每帧的大小(20ms)
  167. chunk_size = self.sample_rate // 50 # 16000 / 50 = 320 samples per 20ms
  168. # 按帧发送
  169. for i in range(0, len(audio_data), chunk_size):
  170. if self.stop_event.is_set():
  171. break
  172. chunk = audio_data[i:i + chunk_size]
  173. # 如果最后一个 chunk 不足,补零
  174. if len(chunk) < chunk_size:
  175. chunk = np.pad(chunk, (0, chunk_size - len(chunk)), 'constant')
  176. self.on_audio(chunk)
  177. # 控制播放速度
  178. time.sleep(0.02) # 20ms per chunk
  179. self.is_playing = False
  180. logger.info("✅ 音频播放完成")
  181. else:
  182. time.sleep(0.01)
  183. except queue.Empty:
  184. continue
  185. except Exception as e:
  186. logger.error(f"❌ 播放音频出错: {e}")
  187. self.is_playing = False
  188. time.sleep(0.1)