feat: 开放小爱音箱接入小智 AI 演示源代码

This commit is contained in:
Del Wang
2025-04-09 21:10:42 +08:00
parent d170fadbcc
commit 6237c46441
25 changed files with 3983 additions and 0 deletions
@@ -0,0 +1,271 @@
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()