import os import subprocess import threading import time import serial import serial.tools.list_ports import logging import tempfile from PyQt5.QtCore import QObject, pyqtSignal # 配置日志记录 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger("FFmpeg_Audio") # PCM 音频参数 PCM_SAMPLE_RATE = 8000 # 8kHz (默认模式,可通过AT+CPCMFRM=1设置为16kHz) PCM_CHANNELS = 1 # 单声道 FFMPEG_PATH = "D:\\ffmpeg\\ffmpeg.exe" # FFmpeg可执行文件路径 FFPLAY_PATH = "D:\\ffmpeg\\ffplay.exe" # FFPlay可执行文件路径 class FFmpegAudio(QObject): status_changed = pyqtSignal(str) # 状态变化信号 def __init__(self): super().__init__() self.audio_port = None self.port_name = None self.is_running = False self.call_active = False self.terminating = False # FFmpeg进程 self.ffmpeg_input_process = None # 从串口读取到扬声器 self.ffmpeg_output_process = None # 从麦克风到串口 # 管理线程 self.monitor_thread = None # 临时文件 self.temp_dir = tempfile.mkdtemp() logger.info(f"创建临时目录: {self.temp_dir}") 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 _cleanup_resources(self): """清理所有资源(在关闭或错误时调用)""" logger.info("清理音频资源...") # 停止FFmpeg进程 try: if self.ffmpeg_input_process: logger.info("正在停止FFmpeg输入进程...") self.ffmpeg_input_process.terminate() self.ffmpeg_input_process.wait(timeout=1) self.ffmpeg_input_process = None except Exception as e: logger.error(f"停止FFmpeg输入进程出错: {str(e)}") try: if self.ffmpeg_output_process: logger.info("正在停止FFmpeg输出进程...") self.ffmpeg_output_process.terminate() self.ffmpeg_output_process.wait(timeout=1) self.ffmpeg_output_process = None except Exception as e: logger.error(f"停止FFmpeg输出进程出错: {str(e)}") # 关闭音频端口 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 logger.info("音频资源清理完成") 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._cleanup_resources() # 等待监控线程结束 if self.monitor_thread and self.monitor_thread.is_alive(): self.monitor_thread.join(timeout=1) self.monitor_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: # 当状态从非活动变为活动时,确保FFmpeg进程在运行 self._ensure_ffmpeg_running() else: # 状态从活动变为非活动时,停止FFmpeg进程 logger.info("通话状态变为非活动,准备关闭音频处理") # 启动单独的关闭线程,避免在当前线程中执行可能阻塞的操作 threading.Thread(target=self._delayed_shutdown, daemon=True).start() def _delayed_shutdown(self): """延迟关闭处理""" try: # 等待短暂时间确保所有挂起的操作完成 time.sleep(0.5) if not self.call_active and not self.terminating: logger.info("执行延迟关闭音频处理") self._cleanup_resources() except Exception as e: logger.error(f"延迟关闭出错: {str(e)}") def _ensure_ffmpeg_running(self): """确保FFmpeg进程在运行""" if not self.is_running or not self.audio_port or not self.audio_port.is_open: logger.warning("音频处理未启动或端口未打开,无法确保FFmpeg运行") return try: # 检查并启动FFmpeg进程 if not self.ffmpeg_input_process or self.ffmpeg_input_process.poll() is not None: self._start_ffmpeg_input() if not self.ffmpeg_output_process or self.ffmpeg_output_process.poll() is not None: self._start_ffmpeg_output() except Exception as e: logger.error(f"确保FFmpeg运行时出错: {str(e)}") def _start_ffmpeg_input(self): """启动从串口到扬声器的FFmpeg进程""" if not self.audio_port or not self.audio_port.is_open: logger.error("音频端口未打开,无法启动FFmpeg输入进程") return False try: # 配置FFmpeg命令行 # 从串口读取PCM数据并播放到扬声器 input_pipe_path = os.path.join(self.temp_dir, "input_pipe.pcm") # 检查并确保管道创建 if os.path.exists(input_pipe_path): try: os.remove(input_pipe_path) except: pass # 创建命令行 cmd = [ FFPLAY_PATH, "-f", "s16le", # 16位有符号整数,小端序 "-ar", str(PCM_SAMPLE_RATE), # 采样率 "-ac", str(PCM_CHANNELS), # 通道数 "-i", "pipe:0", # 从标准输入读取 "-loglevel", "warning", # 只显示警告和错误 "-af", "volume=5", # 增加音量 "-nodisp" # 不显示视频窗口 ] logger.info(f"启动FFmpeg输入进程: {' '.join(cmd)}") # 启动FFmpeg进程 self.ffmpeg_input_process = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, bufsize=0 # 无缓冲 ) # 启动从串口读取数据的线程 threading.Thread( target=self._read_from_serial_to_ffmpeg, daemon=True ).start() logger.info("FFmpeg输入进程启动成功") return True except Exception as e: logger.error(f"启动FFmpeg输入进程失败: {str(e)}") return False def _read_from_serial_to_ffmpeg(self): """从串口读取数据并发送到FFmpeg""" buffer_size = 320 # 每次读取的字节数 (160个16位样本) # 用于统计的变量 bytes_read = 0 last_log_time = time.time() frames_sent = 0 logger.info("[读取] 开始从串口读取PCM数据到FFmpeg") while self.is_running and self.call_active 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.ffmpeg_input_process or self.ffmpeg_input_process.poll() is not None: logger.warning("[读取] FFmpeg输入进程已结束,停止读取") break # 读取数据 if self.audio_port.in_waiting > 0: data = self.audio_port.read(min(buffer_size, self.audio_port.in_waiting)) if data: bytes_read += len(data) frames_sent += 1 # 发送到FFmpeg try: self.ffmpeg_input_process.stdin.write(data) self.ffmpeg_input_process.stdin.flush() except Exception as e: logger.error(f"[读取] 发送数据到FFmpeg出错: {str(e)}") break # 输出统计信息 current_time = time.time() if current_time - last_log_time > 5.0: # 每5秒记录一次 logger.info(f"[读取] 已读取 {bytes_read/1024:.2f} KB PCM数据,发送 {frames_sent} 帧") last_log_time = current_time # 检查是否有长时间未收到数据 if self.audio_port.in_waiting == 0: time.sleep(0.01) # 短暂休眠 except Exception as e: logger.error(f"[读取] 从串口读取数据出错: {str(e)}") time.sleep(0.1) # 关闭FFmpeg输入 try: if self.ffmpeg_input_process and self.ffmpeg_input_process.stdin: self.ffmpeg_input_process.stdin.close() except: pass logger.info(f"[读取] 停止从串口读取,总计读取 {bytes_read/1024:.2f} KB PCM数据") def _start_ffmpeg_output(self): """启动从麦克风到串口的FFmpeg进程""" if not self.audio_port or not self.audio_port.is_open: logger.error("音频端口未打开,无法启动FFmpeg输出进程") return False try: # 配置FFmpeg命令行 # 从麦克风录制PCM数据并发送到串口 cmd = [ FFMPEG_PATH, "-f", "dshow", # DirectShow输入 "-i", "audio=@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\\wave_{DFDF5B7D-7597-4E7C-84D6-CFF1F7379E35}", # 默认麦克风 "-ar", str(PCM_SAMPLE_RATE), # 采样率 "-ac", str(PCM_CHANNELS), # 通道数 "-loglevel", "warning", # 只显示警告和错误 "-af", "volume=4,highpass=f=200,lowpass=f=3000,compand=0.3:0.8:-90/-60:-60/-40:-40/-30:-20/-20:0/-10:0.2", # 音频处理 "-f", "s16le", # 16位有符号整数,小端序 "pipe:1" # 输出到标准输出 ] logger.info(f"启动FFmpeg输出进程: {' '.join(cmd)}") # 启动FFmpeg进程 self.ffmpeg_output_process = subprocess.Popen( cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0 # 无缓冲 ) # 启动将FFmpeg输出写入串口的线程 threading.Thread( target=self._write_from_ffmpeg_to_serial, daemon=True ).start() logger.info("FFmpeg输出进程启动成功") return True except Exception as e: logger.error(f"启动FFmpeg输出进程失败: {str(e)}") return False def _write_from_ffmpeg_to_serial(self): """从FFmpeg读取数据并写入串口""" buffer_size = 320 # 每次读取/写入的字节数 # 用于统计的变量 bytes_written = 0 last_log_time = time.time() frames_sent = 0 logger.info("[发送] 开始从FFmpeg发送PCM数据到串口") while self.is_running and self.call_active 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.ffmpeg_output_process or self.ffmpeg_output_process.poll() is not None: logger.warning("[发送] FFmpeg输出进程已结束,停止写入") break # 读取数据 data = self.ffmpeg_output_process.stdout.read(buffer_size) if data: # 写入串口 self.audio_port.write(data) self.audio_port.flush() bytes_written += len(data) frames_sent += 1 # 输出统计信息 current_time = time.time() if current_time - last_log_time > 5.0: # 每5秒记录一次 logger.info(f"[发送] 已发送 {bytes_written/1024:.2f} KB PCM数据,发送 {frames_sent} 帧") last_log_time = current_time # 短暂休眠,避免CPU占用过高 time.sleep(0.01) except Exception as e: logger.error(f"[发送] 写入数据到串口出错: {str(e)}") time.sleep(0.1) logger.info(f"[发送] 停止写入串口,总计发送 {bytes_written/1024:.2f} KB PCM数据") 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: # 检查FFmpeg是否存在 if not os.path.exists(FFMPEG_PATH) or not os.path.exists(FFPLAY_PATH): logger.error(f"找不到FFmpeg可执行文件: {FFMPEG_PATH} 或 {FFPLAY_PATH}") self.status_changed.emit("找不到FFmpeg可执行文件") return False # 设置运行标志 self.is_running = True # 启动监控线程 self.monitor_thread = threading.Thread(target=self._monitor_thread, daemon=True) self.monitor_thread.name = "FFmpegMonitorThread" self.monitor_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 _monitor_thread(self): """监控线程,确保FFmpeg进程正常运行""" logger.info("启动FFmpeg监控线程") while self.is_running and not self.terminating: try: # 如果通话活动且FFmpeg进程需要启动 if self.call_active: self._ensure_ffmpeg_running() # 检查进程状态 if self.ffmpeg_input_process and self.ffmpeg_input_process.poll() is not None: logger.warning(f"FFmpeg输入进程已退出,状态码: {self.ffmpeg_input_process.poll()}") if self.call_active and not self.terminating: logger.info("尝试重启FFmpeg输入进程") self._start_ffmpeg_input() if self.ffmpeg_output_process and self.ffmpeg_output_process.poll() is not None: logger.warning(f"FFmpeg输出进程已退出,状态码: {self.ffmpeg_output_process.poll()}") if self.call_active and not self.terminating: logger.info("尝试重启FFmpeg输出进程") self._start_ffmpeg_output() # 短暂休眠 time.sleep(1.0) except Exception as e: logger.error(f"监控线程出错: {str(e)}") time.sleep(1.0) logger.info("FFmpeg监控线程退出") # 单独测试功能 if __name__ == "__main__": from PyQt5.QtWidgets import QApplication import sys app = QApplication(sys.argv) # 测试FFmpeg音频功能 audio = FFmpegAudio() 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)