849 lines
38 KiB
Python
849 lines
38 KiB
Python
import serial
|
|
import serial.tools.list_ports
|
|
import sounddevice as sd
|
|
import numpy as np
|
|
import threading
|
|
import queue
|
|
import time
|
|
import re
|
|
import sys
|
|
import logging
|
|
import struct
|
|
from PyQt5.QtCore import QObject, pyqtSignal, QTimer
|
|
|
|
# 配置日志记录
|
|
logging.basicConfig(level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
logger = logging.getLogger("PCM_Audio")
|
|
|
|
# PCM 音频参数 (SIM7600CE-T 使用的标准 PCM 格式)
|
|
# 按照文档说明: "USB audio PCM data format is 8K sample rate, 16 bit linear"
|
|
# 可以通过AT+CPCMFRM=1设置为16K采样率,但请确保模块和音频处理使用相同的采样率
|
|
PCM_SAMPLE_RATE = 8000 # 8kHz (默认模式,可通过AT+CPCMFRM=1设置为16kHz)
|
|
PCM_CHANNELS = 1 # 单声道
|
|
PCM_DTYPE = np.int16 # 16-bit 线性PCM
|
|
CHUNK_SIZE = 160 # 每次读取的样本数 (20ms @ 8kHz,更小的块大小可降低延迟)
|
|
BUFFER_SIZE = 8 # 增加缓冲区大小,提高音频稳定性
|
|
|
|
class PCMAudio(QObject):
|
|
status_changed = pyqtSignal(str) # 状态变化信号
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.audio_port = None
|
|
self.audio_thread = None
|
|
self.play_thread = None
|
|
self.record_thread = None
|
|
self.terminating = False # 新增终止标志
|
|
self.is_running = False
|
|
self.call_active = False
|
|
self.port_name = None # 存储当前使用的端口名称
|
|
self.shutdown_requested = False # 替代QTimer的关闭请求标志
|
|
|
|
# 音频数据队列
|
|
self.play_queue = queue.Queue(maxsize=BUFFER_SIZE) # 播放队列
|
|
self.record_queue = queue.Queue(maxsize=BUFFER_SIZE) # 录音队列
|
|
|
|
# 音频流
|
|
self.output_stream = None
|
|
self.input_stream = None
|
|
|
|
def find_audio_port(self):
|
|
"""查找SIM7600CE的Audio端口 (通常是Audio 9001端口)"""
|
|
logger.info("正在查找SIM7600CE Audio端口...")
|
|
self.status_changed.emit("正在查找音频端口...")
|
|
|
|
ports = list(serial.tools.list_ports.comports())
|
|
for port in ports:
|
|
# 检查描述或设备ID中是否包含"Audio"和"9001"
|
|
if ('audio' in port.description.lower() or
|
|
'audio' in port.device.lower() or
|
|
'9001' in port.description):
|
|
logger.info(f"找到疑似音频端口: {port.device} - {port.description}")
|
|
self.status_changed.emit(f"找到音频端口: {port.device}")
|
|
return port.device
|
|
|
|
logger.warning("未找到SIM7600CE音频端口! 请确保设备已连接且驱动已安装。")
|
|
self.status_changed.emit("未找到音频端口, 通话将没有音频")
|
|
return None
|
|
|
|
def open_audio_port(self, port=None):
|
|
"""打开SIM7600CE的Audio端口"""
|
|
# 重置终止标志
|
|
self.terminating = False
|
|
|
|
# 先关闭之前可能打开的端口
|
|
if self.audio_port and self.audio_port.is_open:
|
|
try:
|
|
self.audio_port.close()
|
|
logger.info("关闭先前打开的音频端口")
|
|
except Exception as e:
|
|
logger.error(f"关闭先前端口时出错: {str(e)}")
|
|
self.audio_port = None
|
|
|
|
if port:
|
|
audio_port_name = port
|
|
else:
|
|
audio_port_name = self.find_audio_port()
|
|
|
|
if not audio_port_name:
|
|
logger.error("无法打开音频端口: 未找到端口")
|
|
self.status_changed.emit("无法打开音频端口")
|
|
return False
|
|
|
|
try:
|
|
# 使用更高的波特率921600以确保音频数据传输顺畅
|
|
self.audio_port = serial.Serial(
|
|
port=audio_port_name,
|
|
baudrate=921600, # 提高波特率到921600
|
|
bytesize=serial.EIGHTBITS,
|
|
parity=serial.PARITY_NONE,
|
|
stopbits=serial.STOPBITS_ONE,
|
|
timeout=0.1, # 非阻塞读取
|
|
rtscts=True, # 启用硬件流控制
|
|
write_timeout=0.5 # 设置写入超时
|
|
)
|
|
self.port_name = audio_port_name # 存储端口名称
|
|
logger.info(f"成功打开音频端口: {audio_port_name}, 波特率: 921600")
|
|
self.status_changed.emit(f"音频端口已打开: {audio_port_name}")
|
|
|
|
# 清空可能已有的数据
|
|
self.audio_port.reset_input_buffer()
|
|
self.audio_port.reset_output_buffer()
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"打开音频端口失败: {str(e)}")
|
|
self.status_changed.emit(f"打开音频端口失败: {str(e)[:50]}")
|
|
return False
|
|
|
|
def start_audio_processing(self):
|
|
"""启动音频处理"""
|
|
if not self.audio_port:
|
|
logger.error("未打开音频端口,无法启动音频处理")
|
|
self.status_changed.emit("未打开音频端口,无法启动音频处理")
|
|
return False
|
|
|
|
if self.is_running:
|
|
logger.warning("音频处理已经在运行")
|
|
return True
|
|
|
|
# 重置终止标志
|
|
self.terminating = False
|
|
|
|
# 初始化音频设备
|
|
try:
|
|
# 获取默认设备信息
|
|
devices = sd.query_devices()
|
|
default_output = sd.default.device[1]
|
|
default_input = sd.default.device[0]
|
|
|
|
logger.info(f"使用默认输出设备: {devices[default_output]['name']}")
|
|
logger.info(f"使用默认输入设备: {devices[default_input]['name']}")
|
|
self.status_changed.emit(f"使用音频设备: {devices[default_output]['name']}")
|
|
|
|
# 清空旧数据
|
|
if self.audio_port and self.audio_port.is_open:
|
|
self.audio_port.reset_input_buffer()
|
|
self.audio_port.reset_output_buffer()
|
|
|
|
# 清空队列
|
|
self._clear_audio_queues()
|
|
|
|
# 打开音频流
|
|
self.output_stream = sd.OutputStream(
|
|
samplerate=PCM_SAMPLE_RATE,
|
|
channels=PCM_CHANNELS,
|
|
dtype=PCM_DTYPE,
|
|
callback=self._audio_output_callback,
|
|
blocksize=CHUNK_SIZE,
|
|
latency='low' # 设置低延迟
|
|
)
|
|
|
|
self.input_stream = sd.InputStream(
|
|
samplerate=PCM_SAMPLE_RATE,
|
|
channels=PCM_CHANNELS,
|
|
dtype=PCM_DTYPE,
|
|
callback=self._audio_input_callback,
|
|
blocksize=CHUNK_SIZE,
|
|
latency='low' # 设置低延迟
|
|
)
|
|
|
|
# 启动音频流
|
|
self.output_stream.start()
|
|
self.input_stream.start()
|
|
|
|
# 设置运行标志
|
|
self.is_running = True
|
|
|
|
# 启动处理线程
|
|
self.audio_thread = threading.Thread(target=self._audio_port_thread, daemon=True)
|
|
self.audio_thread.name = "PCMAudioPortThread"
|
|
self.audio_thread.start()
|
|
|
|
# 启动播放线程
|
|
self.play_thread = threading.Thread(target=self._play_thread, daemon=True)
|
|
self.play_thread.name = "PCMAudioPlayThread"
|
|
self.play_thread.start()
|
|
|
|
# 启动录音线程
|
|
self.record_thread = threading.Thread(target=self._record_thread, daemon=True)
|
|
self.record_thread.name = "PCMAudioRecordThread"
|
|
self.record_thread.start()
|
|
|
|
logger.info("音频处理已启动")
|
|
self.status_changed.emit("音频处理已启动")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"启动音频处理失败: {str(e)}")
|
|
self.status_changed.emit(f"启动音频处理失败: {str(e)[:50]}")
|
|
self._cleanup_resources()
|
|
return False
|
|
|
|
def _clear_audio_queues(self):
|
|
"""清空音频队列"""
|
|
# 清空播放队列
|
|
while not self.play_queue.empty():
|
|
try:
|
|
self.play_queue.get_nowait()
|
|
except Exception as e:
|
|
logger.error(f"清空播放队列出错: {str(e)}")
|
|
break
|
|
|
|
# 清空录音队列
|
|
while not self.record_queue.empty():
|
|
try:
|
|
self.record_queue.get_nowait()
|
|
except Exception as e:
|
|
logger.error(f"清空录音队列出错: {str(e)}")
|
|
break
|
|
|
|
logger.info("已清空音频队列")
|
|
|
|
def _cleanup_resources(self):
|
|
"""清理所有资源(在关闭或错误时调用)"""
|
|
# 停止和关闭音频流
|
|
if self.output_stream:
|
|
try:
|
|
self.output_stream.stop()
|
|
self.output_stream.close()
|
|
except Exception as e:
|
|
logger.error(f"关闭输出流出错: {str(e)}")
|
|
self.output_stream = None
|
|
|
|
if self.input_stream:
|
|
try:
|
|
self.input_stream.stop()
|
|
self.input_stream.close()
|
|
except Exception as e:
|
|
logger.error(f"关闭输入流出错: {str(e)}")
|
|
self.input_stream = None
|
|
|
|
# 清空队列
|
|
self._clear_audio_queues()
|
|
|
|
# 关闭音频端口
|
|
if self.audio_port and self.audio_port.is_open:
|
|
try:
|
|
self.audio_port.reset_input_buffer()
|
|
self.audio_port.reset_output_buffer()
|
|
self.audio_port.close()
|
|
logger.info(f"已关闭音频端口: {self.port_name}")
|
|
except Exception as e:
|
|
logger.error(f"关闭音频端口出错: {str(e)}")
|
|
self.audio_port = None
|
|
|
|
# 重置状态
|
|
self.is_running = False
|
|
self.call_active = False
|
|
|
|
def stop_audio_processing(self):
|
|
"""停止音频处理"""
|
|
if not self.is_running:
|
|
logger.info("音频处理已经停止,无需再次停止")
|
|
return
|
|
|
|
logger.info("正在停止音频处理...")
|
|
self.status_changed.emit("正在停止音频处理...")
|
|
|
|
# 设置终止标志,通知所有线程停止
|
|
self.terminating = True
|
|
self.call_active = False
|
|
self.is_running = False
|
|
self.shutdown_requested = False # 取消可能的关闭请求
|
|
|
|
# 立即清理所有资源,不再等待线程正常结束
|
|
self._cleanup_resources()
|
|
|
|
# 只在资源清理后尝试关闭线程
|
|
# 等待线程结束 - 使用更短的超时以防止阻塞
|
|
threads_to_wait = []
|
|
|
|
if self.audio_thread and self.audio_thread.is_alive():
|
|
logger.info("等待音频端口线程结束...")
|
|
threads_to_wait.append(('音频端口线程', self.audio_thread))
|
|
|
|
if self.play_thread and self.play_thread.is_alive():
|
|
logger.info("等待播放线程结束...")
|
|
threads_to_wait.append(('播放线程', self.play_thread))
|
|
|
|
if self.record_thread and self.record_thread.is_alive():
|
|
logger.info("等待录音线程结束...")
|
|
threads_to_wait.append(('录音线程', self.record_thread))
|
|
|
|
# 等待所有线程结束,每个线程最多等待0.5秒
|
|
for thread_name, thread in threads_to_wait:
|
|
thread.join(timeout=0.5)
|
|
if thread.is_alive():
|
|
logger.warning(f"{thread_name}未能正常结束")
|
|
|
|
# 重置线程变量
|
|
self.audio_thread = None
|
|
self.play_thread = None
|
|
self.record_thread = None
|
|
|
|
logger.info("音频处理已停止")
|
|
self.status_changed.emit("音频处理已停止")
|
|
|
|
def set_call_active(self, active):
|
|
"""设置通话状态"""
|
|
prev_state = self.call_active
|
|
self.call_active = active
|
|
|
|
if prev_state != active: # 只有状态改变时才记录和通知
|
|
logger.info(f"通话状态: {'活动' if active else '非活动'}")
|
|
self.status_changed.emit(f"通话音频状态: {'活动' if active else '非活动'}")
|
|
|
|
if active:
|
|
# 当状态从非活动变为活动时,清空缓冲区
|
|
if self.audio_port and self.audio_port.is_open:
|
|
try:
|
|
self.audio_port.reset_input_buffer()
|
|
self.audio_port.reset_output_buffer()
|
|
except Exception as e:
|
|
logger.error(f"重置音频缓冲区出错: {str(e)}")
|
|
|
|
# 清空音频队列
|
|
self._clear_audio_queues()
|
|
else:
|
|
# 状态从活动变为非活动时,开始直接关闭处理,不使用延迟机制
|
|
logger.info("通话状态变为非活动,准备关闭音频处理")
|
|
self.shutdown_requested = True # 设置关闭请求标志,用于代替QTimer
|
|
|
|
# 启动单独的关闭线程,避免在当前线程中执行可能阻塞的操作
|
|
shutdown_thread = threading.Thread(target=self._delayed_shutdown_thread, daemon=True)
|
|
shutdown_thread.start()
|
|
|
|
def _delayed_shutdown_thread(self):
|
|
"""在单独线程中执行延迟关闭,避免阻塞主线程"""
|
|
try:
|
|
# 等待短暂时间,确保所有挂起的操作都有时间完成
|
|
time.sleep(0.5)
|
|
|
|
# 检查是否仍然需要关闭
|
|
if not self.call_active and self.shutdown_requested:
|
|
logger.info("执行延迟关闭音频处理")
|
|
# 执行停止处理,但不在线程中调用self.stop_audio_processing,而是发送信号
|
|
self.status_changed.emit("音频处理关闭中...")
|
|
|
|
# 设置所有状态标志
|
|
self.terminating = True
|
|
|
|
# 等待安全时间后强制清理资源
|
|
time.sleep(0.5)
|
|
logger.info("关闭音频流和端口")
|
|
|
|
# 关闭和清理资源
|
|
if self.output_stream:
|
|
try:
|
|
self.output_stream.stop()
|
|
self.output_stream.close()
|
|
self.output_stream = None
|
|
except Exception as e:
|
|
logger.error(f"关闭输出流出错: {str(e)}")
|
|
|
|
if self.input_stream:
|
|
try:
|
|
self.input_stream.stop()
|
|
self.input_stream.close()
|
|
self.input_stream = None
|
|
except Exception as e:
|
|
logger.error(f"关闭输入流出错: {str(e)}")
|
|
|
|
# 重置状态
|
|
self.is_running = False
|
|
self.shutdown_requested = False
|
|
logger.info("延迟关闭完成")
|
|
self.status_changed.emit("音频处理已关闭")
|
|
except Exception as e:
|
|
logger.error(f"延迟关闭线程出错: {str(e)}")
|
|
|
|
def _audio_output_callback(self, outdata, frames, time, status):
|
|
"""音频输出回调(从队列获取PCM数据并输出到扬声器)"""
|
|
if status:
|
|
logger.warning(f"音频输出状态: {status}")
|
|
|
|
if not self.call_active or self.terminating:
|
|
# 如果没有通话或正在终止,输出静音
|
|
outdata.fill(0)
|
|
return
|
|
|
|
try:
|
|
if not self.play_queue.empty():
|
|
# 从队列获取PCM数据
|
|
data = self.play_queue.get_nowait()
|
|
# 确保数据长度匹配
|
|
if len(data) < frames:
|
|
# 数据不足,补零
|
|
padding = np.zeros((frames - len(data), PCM_CHANNELS), dtype=PCM_DTYPE)
|
|
data = np.vstack((data, padding))
|
|
elif len(data) > frames:
|
|
# 数据过多,截断
|
|
data = data[:frames]
|
|
# 复制到输出缓冲区
|
|
outdata[:] = data
|
|
else:
|
|
# 队列为空,输出静音
|
|
outdata.fill(0)
|
|
except Exception as e:
|
|
logger.error(f"音频输出错误: {str(e)}")
|
|
outdata.fill(0)
|
|
|
|
def _audio_input_callback(self, indata, frames, time, status):
|
|
"""音频输入回调(从麦克风获取PCM数据并发送到队列)"""
|
|
if status:
|
|
logger.warning(f"音频输入状态: {status}")
|
|
|
|
if not self.call_active or self.terminating:
|
|
# 如果没有通话或正在终止,不处理输入
|
|
return
|
|
|
|
try:
|
|
# 将麦克风数据放入录音队列
|
|
if not self.record_queue.full():
|
|
self.record_queue.put_nowait(indata.copy())
|
|
except Exception as e:
|
|
logger.error(f"音频输入错误: {str(e)}")
|
|
|
|
def _audio_port_thread(self):
|
|
"""音频端口处理线程(读取PCM数据 - 模块到扬声器)"""
|
|
# PCM数据解析缓冲区
|
|
buffer = bytearray()
|
|
bytes_per_frame = CHUNK_SIZE * PCM_CHANNELS * 2 # 16-bit = 2 bytes
|
|
last_log_time = 0
|
|
frame_count = 0
|
|
last_buffer_check_time = 0
|
|
processed_frames_total = 0
|
|
last_data_received_time = time.time()
|
|
silent_frames_count = 0
|
|
frame_sync_attempts = 0
|
|
recovered_frames = 0
|
|
|
|
# 设置调试计数器
|
|
debug_frame_counter = 0
|
|
debug_signal_detection = False
|
|
signal_level_history = []
|
|
max_signal_level = 0
|
|
|
|
# 设置基准音量和增益值 - 增加接收增益确保清晰听到对方声音
|
|
base_gain = 5.0 # 更高的基准增益,确保足够听到对方声音
|
|
noise_threshold = 30 # 降低噪声阈值以确保不会过滤掉有效信号
|
|
|
|
# 设置静音帧阈值,超过该数量未收到有效数据帧时发出警告
|
|
SILENT_FRAMES_THRESHOLD = 50
|
|
|
|
logger.info("音频端口处理线程已启动")
|
|
logger.info(f"PCM参数: 采样率={PCM_SAMPLE_RATE}Hz, 通道数={PCM_CHANNELS}, 每帧字节数={bytes_per_frame}")
|
|
logger.info(f"音频输出设置: 基准增益={base_gain}x,噪声阈值={noise_threshold}")
|
|
|
|
# 发送模式测试数据
|
|
try:
|
|
# 向模块发送一些测试数据,验证发送通道
|
|
if self.audio_port and self.audio_port.is_open:
|
|
test_data = np.zeros((CHUNK_SIZE, PCM_CHANNELS), dtype=np.int16)
|
|
test_data[:10, 0] = 16000 # 前10个样本设置为16000,形成短脉冲
|
|
test_bytes = test_data.tobytes()
|
|
self.audio_port.write(test_bytes)
|
|
logger.info(f"已发送测试音频数据: {len(test_bytes)}字节")
|
|
except Exception as e:
|
|
logger.error(f"发送测试数据出错: {str(e)}")
|
|
|
|
while self.is_running and not self.terminating:
|
|
try:
|
|
if not self.audio_port or not self.audio_port.is_open:
|
|
time.sleep(0.1)
|
|
continue
|
|
|
|
# 如果不在通话状态,快速检查并继续循环
|
|
if not self.call_active:
|
|
# 清空缓冲区并睡眠
|
|
if buffer:
|
|
buffer = bytearray()
|
|
time.sleep(0.1)
|
|
continue
|
|
|
|
# 读取串口数据
|
|
try:
|
|
available = self.audio_port.in_waiting
|
|
if available > 0:
|
|
# 读取所有可用数据
|
|
data = self.audio_port.read(available)
|
|
if data:
|
|
# 更新最后接收数据时间
|
|
last_data_received_time = time.time()
|
|
silent_frames_count = 0 # 重置静音帧计数
|
|
|
|
# 添加到缓冲区
|
|
buffer.extend(data)
|
|
|
|
# 每1000帧记录一次调试信息
|
|
debug_frame_counter += 1
|
|
if debug_frame_counter >= 1000:
|
|
# 记录详细状态信息
|
|
logger.info(f"[读取] 音频缓冲区: {len(buffer)}字节, 可用数据: {available}字节")
|
|
logger.info(f"[读取] 已处理总帧数: {processed_frames_total}, 缓冲区状态: {len(buffer)/bytes_per_frame:.1f}帧")
|
|
if signal_level_history:
|
|
avg_level = sum(signal_level_history) / len(signal_level_history)
|
|
logger.info(f"[读取] 平均信号电平: {avg_level:.2f}, 最大信号电平: {max_signal_level:.2f}")
|
|
if avg_level > 0:
|
|
logger.info(f"[读取] 检测到音频信号,增益设置为{base_gain}x")
|
|
debug_frame_counter = 0
|
|
else:
|
|
# 长时间未收到数据,可能需要检查通信状态
|
|
current_time = time.time()
|
|
if current_time - last_data_received_time > 0.5: # 半秒未收到数据
|
|
silent_frames_count += 1
|
|
if silent_frames_count > SILENT_FRAMES_THRESHOLD and self.call_active:
|
|
logger.warning("[读取] 长时间未收到音频数据,检查通信状态")
|
|
silent_frames_count = 0 # 重置计数,避免重复警告
|
|
|
|
# 尝试重置串口缓冲区,重新启动数据流
|
|
if self.audio_port and self.audio_port.is_open:
|
|
try:
|
|
# 先发送一些数据,可能帮助触发接收
|
|
test_data = np.zeros((CHUNK_SIZE, PCM_CHANNELS), dtype=np.int16)
|
|
test_data[:10, 0] = 16000 # 前10个样本设置为16000
|
|
test_bytes = test_data.tobytes()
|
|
self.audio_port.write(test_bytes)
|
|
|
|
# 重置输入缓冲区
|
|
self.audio_port.reset_input_buffer()
|
|
logger.info("[读取] 已重置音频输入缓冲区并发送测试数据")
|
|
except Exception as e:
|
|
logger.error(f"[读取] 重置音频缓冲区出错: {str(e)}")
|
|
|
|
# 定期检查缓冲区大小,避免缓冲区无限增长
|
|
current_time = time.time()
|
|
if current_time - last_buffer_check_time > 1.0: # 每秒检查一次
|
|
# 检查缓冲区大小
|
|
buffer_size = len(buffer)
|
|
|
|
# 如果缓冲区不是帧大小的整数倍,尝试帧同步
|
|
remainder = buffer_size % bytes_per_frame
|
|
if remainder != 0 and buffer_size > bytes_per_frame:
|
|
# 尝试通过查找头部模式同步帧
|
|
frame_sync_attempts += 1
|
|
if frame_sync_attempts % 10 == 0: # 每10次尝试记录一次
|
|
logger.warning(f"[读取] 帧同步尝试: {frame_sync_attempts}次, 缓冲区大小: {buffer_size}字节, 余数: {remainder}字节")
|
|
|
|
# 丢弃余数字节或补齐帧
|
|
if remainder < bytes_per_frame / 2:
|
|
# 余数小于半帧,丢弃余数
|
|
buffer = buffer[:-remainder]
|
|
else:
|
|
# 余数大于半帧,补齐为完整帧(用0填充)
|
|
padding_size = bytes_per_frame - remainder
|
|
buffer.extend(bytes(padding_size))
|
|
recovered_frames += 1
|
|
|
|
buffer_frames = len(buffer) / bytes_per_frame
|
|
if buffer_size > bytes_per_frame * 30: # 如果缓冲区积累太多数据
|
|
logger.warning(f"[读取] 缓冲区积累过多数据 ({buffer_size} 字节, {buffer_frames:.1f} 帧), 保留最后10帧数据")
|
|
# 只保留最后部分数据
|
|
buffer = buffer[-bytes_per_frame * 10:]
|
|
|
|
# 如果音频缓冲区长时间为空并且通话活动,记录警告
|
|
if buffer_size == 0 and self.call_active and processed_frames_total > 0:
|
|
logger.warning("[读取] 音频缓冲区为空但通话仍在进行,可能缺少音频数据")
|
|
|
|
last_buffer_check_time = current_time
|
|
|
|
# 当缓冲区数据足够时处理
|
|
processed = 0
|
|
while len(buffer) >= bytes_per_frame and self.call_active and not self.terminating:
|
|
# 提取一帧数据
|
|
frame_data = buffer[:bytes_per_frame]
|
|
buffer = buffer[bytes_per_frame:]
|
|
processed += 1
|
|
processed_frames_total += 1
|
|
|
|
try:
|
|
# 将SIM7600CE的PCM数据转换为音频数据
|
|
pcm_data = np.frombuffer(frame_data, dtype=np.int16).reshape(-1, PCM_CHANNELS)
|
|
|
|
# 计算信号电平用于自动增益和检测有效信号
|
|
signal_level = np.abs(pcm_data).mean()
|
|
|
|
# 在首次接收到高于阈值的信号时记录
|
|
if signal_level > noise_threshold and not signal_level_history:
|
|
logger.info(f"[读取] 首次检测到信号: 电平={signal_level:.2f}")
|
|
|
|
# 过滤掉异常值,确保数据有效
|
|
if signal_level < 32000: # 有效PCM数据不应超过此值
|
|
# 更新信号历史
|
|
signal_level_history.append(signal_level)
|
|
if len(signal_level_history) > 50: # 保留50帧的历史
|
|
signal_level_history.pop(0)
|
|
|
|
# 记录最大信号电平(用于调试)
|
|
if signal_level > max_signal_level:
|
|
max_signal_level = signal_level
|
|
if not debug_signal_detection and signal_level > 100:
|
|
logger.info(f"[读取] 检测到新的最大信号电平: {max_signal_level:.2f}")
|
|
debug_signal_detection = True
|
|
|
|
# 噪声消除 - 如果信号电平低于噪声阈值,视为噪声
|
|
if signal_level < noise_threshold:
|
|
# 对于非常低的信号(噪声),应用非常小的增益
|
|
# 但仍保留一部分,以保持连续性
|
|
pcm_data = pcm_data * 0.05 # 保留5%的信号
|
|
else:
|
|
# 为了确保足够的音量,使用较高的基准增益
|
|
# 让对方的声音更加清晰
|
|
pcm_data = np.clip(pcm_data * base_gain, -32700, 32700).astype(np.int16)
|
|
|
|
# 放入播放队列
|
|
if not self.play_queue.full() and not self.terminating:
|
|
self.play_queue.put_nowait(pcm_data)
|
|
frame_count += 1
|
|
else:
|
|
# 信号电平异常,可能是帧同步问题
|
|
logger.warning(f"[读取] 异常信号电平: {signal_level}, 可能帧同步问题")
|
|
|
|
# 每隔一段时间记录一次性能日志
|
|
current_time = time.time()
|
|
if current_time - last_log_time > 5.0: # 每5秒记录一次
|
|
avg_signal = np.mean(signal_level_history) if signal_level_history else 0
|
|
logger.info(f"[读取] 已处理 {frame_count} 帧PCM数据,平均信号电平: {avg_signal:.2f}")
|
|
last_log_time = current_time
|
|
frame_count = 0
|
|
|
|
except Exception as e:
|
|
logger.error(f"[读取] 处理PCM数据帧错误: {str(e)}")
|
|
# 出错时清空缓冲区,避免继续处理错误数据
|
|
buffer = bytearray()
|
|
break
|
|
except Exception as e:
|
|
logger.error(f"[读取] 读取音频端口数据出错: {str(e)}")
|
|
time.sleep(0.1)
|
|
|
|
# 如果当前没有更多数据可读,短暂休眠避免CPU占用
|
|
if available == 0:
|
|
time.sleep(0.01) # 10ms延迟,提供更好的响应性
|
|
else:
|
|
# 有数据处理时使用更短的延迟
|
|
time.sleep(0.001)
|
|
|
|
except Exception as e:
|
|
logger.error(f"[读取] 音频端口处理错误: {str(e)}")
|
|
time.sleep(0.1)
|
|
|
|
# 线程结束前清空缓冲区及统计数据
|
|
buffer = bytearray()
|
|
signal_level_history = []
|
|
logger.info(f"[读取] 音频端口处理线程已退出,总处理帧数: {processed_frames_total}, 恢复帧: {recovered_frames}")
|
|
|
|
def _play_thread(self):
|
|
"""播放线程(处理PCM数据队列)"""
|
|
logger.info("播放线程已启动")
|
|
|
|
while self.is_running and not self.terminating:
|
|
try:
|
|
# 线程主要工作在回调中完成,这里只需要保持线程运行
|
|
time.sleep(0.1)
|
|
except Exception as e:
|
|
logger.error(f"播放线程错误: {str(e)}")
|
|
time.sleep(0.1)
|
|
|
|
logger.info("播放线程已退出")
|
|
|
|
def _record_thread(self):
|
|
"""录音线程(发送PCM数据到串口 - 麦克风到模块)"""
|
|
logger.info("[发送] 录音线程已启动")
|
|
|
|
# 记录最近的数据包大小,用于调试
|
|
packet_sizes = []
|
|
last_log_time = 0
|
|
sent_packets_count = 0
|
|
total_bytes_sent = 0
|
|
last_packet_sent_time = time.time()
|
|
|
|
# 引入随机数生成器用于加入测试音频
|
|
np.random.seed()
|
|
|
|
# 采样率和块大小
|
|
sample_rate = PCM_SAMPLE_RATE # 8kHz
|
|
chunk_size = CHUNK_SIZE # 160个样本,即20ms@8kHz
|
|
|
|
# 初始化降噪参数
|
|
noise_floor = 80 # 噪声阈值 - 降低以确保捕获更多人声
|
|
voice_gain = 4.0 # 人声增益 - 增加以确保声音传输清晰
|
|
noise_gate_enabled = True # 启用噪声门控
|
|
|
|
logger.info(f"[发送] 麦克风设置: 启用噪声门控={noise_gate_enabled}, 噪声阈值={noise_floor}, 人声增益={voice_gain}x")
|
|
|
|
# 创建测试音频信号(1kHz正弦波)用于向模块发送
|
|
test_audio_enabled = False # 默认关闭测试音频
|
|
test_tone_freq = 1000 # 1kHz
|
|
test_tone_samples = np.arange(chunk_size)
|
|
test_tone = (16000 * np.sin(2 * np.pi * test_tone_freq * test_tone_samples / sample_rate)).astype(np.int16)
|
|
test_tone = test_tone.reshape(-1, PCM_CHANNELS)
|
|
|
|
# 强制发送计时器,确保即使麦克风无输入,仍定期发送数据包
|
|
force_send_interval = 0.020 # 20ms,确保平滑音频
|
|
zero_frame = np.zeros((chunk_size, PCM_CHANNELS), dtype=np.int16)
|
|
|
|
# 加入启动时的初始测试音频
|
|
try:
|
|
if self.audio_port and self.audio_port.is_open:
|
|
# 发送测试音频波形序列
|
|
for i in range(5): # 发送5帧测试音频
|
|
self.audio_port.write(test_tone.tobytes())
|
|
sent_packets_count += 1
|
|
time.sleep(0.02) # 20ms间隔
|
|
logger.info(f"[发送] 已发送初始测试音频: 5帧")
|
|
except Exception as e:
|
|
logger.error(f"[发送] 发送初始测试音频出错: {str(e)}")
|
|
|
|
while self.is_running and not self.terminating:
|
|
try:
|
|
if not self.call_active or not self.audio_port or not self.audio_port.is_open or self.terminating:
|
|
time.sleep(0.1)
|
|
continue
|
|
|
|
current_time = time.time()
|
|
|
|
# 是否应该强制发送(超过定期发送间隔)
|
|
should_force_send = (current_time - last_packet_sent_time) > force_send_interval
|
|
|
|
# 从录音队列获取数据
|
|
try:
|
|
# 使用短超时,避免长时间阻塞
|
|
pcm_data = None
|
|
try:
|
|
pcm_data = self.record_queue.get(timeout=0.01)
|
|
except queue.Empty:
|
|
# 队列为空,如果需要强制发送则生成静音帧
|
|
if should_force_send:
|
|
if test_audio_enabled:
|
|
# 使用测试音频而不是静音
|
|
pcm_data = test_tone.copy()
|
|
logger.debug("[发送] 生成测试音频帧发送")
|
|
else:
|
|
# 使用静音帧
|
|
pcm_data = zero_frame.copy()
|
|
logger.debug("[发送] 生成静音帧发送")
|
|
else:
|
|
continue
|
|
|
|
# 如果还没有数据,跳过当前循环
|
|
if pcm_data is None:
|
|
continue
|
|
|
|
# 计算当前音量级别
|
|
volume_level = np.abs(pcm_data).mean()
|
|
|
|
# 偶尔发送测试音频以确保通信通道开放
|
|
if sent_packets_count % 1000 == 0: # 每1000个包发送一次测试音频
|
|
# 临时替换为测试音频
|
|
pcm_data = test_tone.copy()
|
|
logger.info(f"[发送] 发送测试音频帧: #{sent_packets_count}")
|
|
|
|
# 应用噪声门控和增益处理
|
|
if noise_gate_enabled:
|
|
if volume_level < noise_floor:
|
|
# 低于阈值的信号视为背景噪音,强烈抑制但不完全消除
|
|
# 这有助于减少背景噪音传输到对方
|
|
pcm_data = pcm_data * 0.02 # 只保留2%原始信号
|
|
else:
|
|
# 高于阈值的信号应用更高增益提升人声清晰度
|
|
# 确保声音传输到对方足够清晰
|
|
pcm_data = np.clip(pcm_data * voice_gain, -32700, 32700).astype(np.int16)
|
|
else:
|
|
# 如果不启用噪声门控,仍然应用增益
|
|
pcm_data = np.clip(pcm_data * voice_gain, -32700, 32700).astype(np.int16)
|
|
|
|
# 将PCM数据转换为字节发送到串口(确保使用小端字节序)
|
|
bytes_data = pcm_data.astype(np.int16).tobytes()
|
|
|
|
# 更新发送计时
|
|
last_packet_sent_time = current_time
|
|
|
|
# 记录数据包大小用于调试
|
|
packet_sizes.append(len(bytes_data))
|
|
if len(packet_sizes) > 20:
|
|
packet_sizes.pop(0)
|
|
|
|
# 每5秒记录一次发送数据统计
|
|
if current_time - last_log_time > 5.0:
|
|
if packet_sizes:
|
|
avg_size = sum(packet_sizes) / len(packet_sizes)
|
|
logger.info(f"[发送] 音频发送: 平均数据包大小 {avg_size:.2f} 字节, 已发送 {sent_packets_count} 个数据包 ({total_bytes_sent/1024:.2f} KB)")
|
|
last_log_time = current_time
|
|
|
|
# 检查连接和终止状态
|
|
if self.audio_port and self.audio_port.is_open and not self.terminating:
|
|
# 确保立即发送数据
|
|
bytes_sent = self.audio_port.write(bytes_data)
|
|
sent_packets_count += 1
|
|
total_bytes_sent += bytes_sent
|
|
|
|
# 调试:检查发送的字节数
|
|
if bytes_sent != len(bytes_data):
|
|
logger.warning(f"[发送] 音频数据发送不完整: {bytes_sent}/{len(bytes_data)}字节")
|
|
|
|
# 确保数据立即发送
|
|
self.audio_port.flush()
|
|
|
|
except Exception as e:
|
|
logger.error(f"[发送] 发送PCM数据错误: {str(e)}")
|
|
time.sleep(0.01)
|
|
|
|
except Exception as e:
|
|
logger.error(f"[发送] 录音线程错误: {str(e)}")
|
|
time.sleep(0.1)
|
|
|
|
# 清理记录数据
|
|
packet_sizes = []
|
|
logger.info(f"[发送] 录音线程已退出,总发送数据包: {sent_packets_count}, 总发送字节: {total_bytes_sent/1024:.2f} KB")
|
|
|
|
# 单独测试功能
|
|
if __name__ == "__main__":
|
|
from PyQt5.QtWidgets import QApplication
|
|
import sys
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
# 测试音频功能
|
|
audio = PCMAudio()
|
|
|
|
port = audio.find_audio_port()
|
|
if port:
|
|
print(f"找到音频端口: {port}")
|
|
if audio.open_audio_port(port):
|
|
print("成功打开音频端口")
|
|
audio.start_audio_processing()
|
|
print("按Enter键模拟通话开始...")
|
|
input()
|
|
audio.set_call_active(True)
|
|
print("通话已开始,现在可以说话...按Enter键结束通话")
|
|
input()
|
|
audio.set_call_active(False)
|
|
print("通话已结束")
|
|
# 等待延迟关闭完成
|
|
time.sleep(4)
|
|
sys.exit(0)
|
|
else:
|
|
print("未找到音频端口")
|
|
sys.exit(1) |