Files
open-xiaoai/packages/server-python/xiaozhi/services/audio/codec.py
T

272 lines
9.3 KiB
Python
Raw Normal View History

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()