import logging import queue import numpy as np import pyaudio import opuslib from xiaozhi.services.protocols.typing import AudioConfig import time import sys from xiaozhi.services.audio.stream import MyAudio from xiaozhi.xiaoai import XiaoAi logger = logging.getLogger("AudioCodec") class AudioCodec: """音频编解码器类,处理音频的录制和播放""" def __init__(self): """初始化音频编解码器""" self.audio = None self.input_stream = None self.output_stream = None self.opus_encoder = None self.opus_decoder = None self.audio_decode_queue = queue.Queue() self._is_closing = False self._initialize_audio() def _initialize_audio(self): """初始化音频设备和编解码器""" try: self.audio = MyAudio() if XiaoAi.mode == "xiaoai" else pyaudio.PyAudio() # 初始化音频输入流 self.input_stream = self.audio.open( format=pyaudio.paInt16, channels=AudioConfig.CHANNELS, rate=AudioConfig.SAMPLE_RATE, input=True, frames_per_buffer=AudioConfig.FRAME_SIZE, ) # 初始化音频输出流 self.output_stream = self.audio.open( format=pyaudio.paInt16, channels=AudioConfig.CHANNELS, rate=AudioConfig.SAMPLE_RATE, output=True, frames_per_buffer=AudioConfig.FRAME_SIZE, ) # 初始化Opus编码器 self.opus_encoder = opuslib.Encoder( fs=AudioConfig.SAMPLE_RATE, channels=AudioConfig.CHANNELS, application=opuslib.APPLICATION_AUDIO, ) # 初始化Opus解码器 self.opus_decoder = opuslib.Decoder( fs=AudioConfig.SAMPLE_RATE, channels=AudioConfig.CHANNELS ) logger.info("音频设备和编解码器初始化成功") except Exception as e: logger.error(f"初始化音频设备失败: {e}") raise def read_audio(self): """读取音频输入数据并编码""" try: data = self.input_stream.read( AudioConfig.FRAME_SIZE, exception_on_overflow=False ) if not data: return None return self.opus_encoder.encode(data, AudioConfig.FRAME_SIZE) except Exception as e: logger.error(f"读取音频输入时出错: {e}") return None def write_audio(self, opus_data): """将编码的音频数据添加到播放队列""" self.audio_decode_queue.put(opus_data) def play_audio(self): """处理并播放队列中的音频数据""" try: # 批量处理多个音频包以减少处理延迟 batch_size = min(10, self.audio_decode_queue.qsize()) if batch_size == 0: return False # 创建缓冲区存储解码后的数据 buffer = bytearray() for _ in range(batch_size): if self.audio_decode_queue.empty(): break opus_data = self.audio_decode_queue.get_nowait() try: pcm_data = self.opus_decoder.decode( opus_data, AudioConfig.FRAME_SIZE, decode_fec=False ) buffer.extend(pcm_data) except Exception as e: logger.error(f"解码音频数据时出错: {e}") # 只有在有数据时才处理和播放 if len(buffer) > 0: # 转换为numpy数组 pcm_array = np.frombuffer(buffer, dtype=np.int16) # 播放音频 try: if self.output_stream and self.output_stream.is_active(): self.output_stream.write(pcm_array.tobytes()) return True else: # MAC 特定:如果流不活跃,尝试重新初始化 self._reinitialize_output_stream() if self.output_stream and self.output_stream.is_active(): self.output_stream.write(pcm_array.tobytes()) return True except OSError as e: if "Stream closed" in str(e) or "Internal PortAudio error" in str( e ): logger.error(f"播放音频时出错: {e}") self._reinitialize_output_stream() else: logger.error(f"播放音频时出错: {e}") except queue.Empty: pass except Exception as e: logger.error(f"播放音频时出错: {e}") self._reinitialize_output_stream() return False def has_pending_audio(self): """检查是否还有待播放的音频数据""" return not self.audio_decode_queue.empty() def wait_for_audio_complete(self, timeout=5.0): # 等待音频队列清空 attempt = 0 max_attempts = 15 while not self.audio_decode_queue.empty() and attempt < max_attempts: time.sleep(0.1) attempt += 1 # 在关闭前清空任何剩余数据 while not self.audio_decode_queue.empty(): try: self.audio_decode_queue.get_nowait() except queue.Empty: break def clear_audio_queue(self): """清空音频队列""" while not self.audio_decode_queue.empty(): try: self.audio_decode_queue.get_nowait() except queue.Empty: break def start_streams(self): """启动音频流""" if not self.input_stream.is_active(): self.input_stream.start_stream() if not self.output_stream.is_active(): self.output_stream.start_stream() def stop_streams(self): """停止音频流""" if self.input_stream and self.input_stream.is_active(): self.input_stream.stop_stream() if self.output_stream and self.output_stream.is_active(): self.output_stream.stop_stream() def _reinitialize_output_stream(self): """重新初始化音频输出流""" if self._is_closing: # 如果正在关闭,不要重新初始化 return try: if self.output_stream: try: if self.output_stream.is_active(): self.output_stream.stop_stream() self.output_stream.close() except Exception as e: # logger.warning(f"关闭旧输出流时出错: {e}") pass # 在 MAC 上添加短暂延迟 if sys.platform in ("darwin", "linux"): time.sleep(0.1) self.output_stream = self.audio.open( format=pyaudio.paInt16, channels=AudioConfig.CHANNELS, rate=AudioConfig.SAMPLE_RATE, output=True, frames_per_buffer=AudioConfig.FRAME_SIZE, ) logger.info("音频输出流重新初始化成功") except Exception as e: logger.error(f"重新初始化音频输出流失败: {e}") raise def close(self): """关闭音频编解码器,确保资源正确释放""" if self._is_closing: # 防止重复关闭 return self._is_closing = True logger.info("开始关闭音频编解码器...") try: # 等待并清理剩余音频数据 self.wait_for_audio_complete() # 关闭输入流 if self.input_stream: logger.debug("正在关闭输入流...") try: if self.input_stream.is_active(): self.input_stream.stop_stream() self.input_stream.close() except Exception as e: logger.error(f"关闭输入流时出错: {e}") self.input_stream = None # 关闭输出流 if self.output_stream: logger.debug("正在关闭输出流...") try: if self.output_stream.is_active(): self.output_stream.stop_stream() self.output_stream.close() except Exception as e: logger.error(f"关闭输出流时出错: {e}") self.output_stream = None # 关闭 PyAudio 实例 if self.audio: logger.debug("正在终止 PyAudio...") try: self.audio.terminate() except Exception as e: logger.error(f"终止 PyAudio 时出错: {e}") self.audio = None # 清理编解码器 self.opus_encoder = None self.opus_decoder = None logger.info("音频编解码器关闭完成") except Exception as e: logger.error(f"关闭音频编解码器时发生错误: {e}") finally: self._is_closing = False def __del__(self): """析构函数,确保资源被释放""" self.close()