Skip to content

Commit aba76a0

Browse files
refactor(application): 优化信号处理器设置逻辑,支持跨平台处理
- 移除了macOS特定的信号处理器设置,统一为所有平台设置SIGINT和SIGTERM信号处理器。 - 在系统托盘中添加占位图标以避免警告信息。 - 在音频测试中增加线程安全的状态更新方法,确保UI在多线程环境下的稳定性。
1 parent a3e28e2 commit aba76a0

File tree

3 files changed

+143
-69
lines changed

3 files changed

+143
-69
lines changed

src/application.py

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
import json
3-
import platform
43
import signal
54
import sys
65
import threading
@@ -19,48 +18,48 @@
1918

2019
logger = get_logger(__name__)
2120

22-
# 检查是否为 macOS 系统
23-
if platform.system() == "Darwin":
2421

25-
def setup_signal_handler(sig, handler, description):
26-
"""
27-
统一的信号处理器设置函数.
28-
"""
29-
try:
30-
signal.signal(sig, handler)
31-
except (AttributeError, ValueError) as e:
32-
print(f"注意: 无法设置{description}处理器: {e}")
33-
34-
def handle_sigint(signum, frame):
35-
app = Application.get_instance()
36-
if not app:
37-
sys.exit(0)
38-
39-
# 使用app的主循环,更稳定且跨线程安全
40-
loop = app._main_loop
41-
if loop and not loop.is_closed():
42-
# 直接创建task在指定的循环中
43-
def create_shutdown_task():
44-
try:
45-
if loop.is_running():
46-
asyncio.run_coroutine_threadsafe(app.shutdown(), loop)
47-
else:
48-
loop.create_task(app.shutdown())
49-
except Exception as e:
50-
print(f"创建shutdown任务失败: {e}")
51-
sys.exit(0)
52-
53-
loop.call_soon_threadsafe(create_shutdown_task)
54-
else:
55-
# 主循环未就绪或已关闭,直接退出
56-
sys.exit(0)
22+
def setup_signal_handler(sig, handler, description):
23+
"""
24+
统一的信号处理器设置函数(跨平台尽力而为).
25+
"""
26+
try:
27+
signal.signal(sig, handler)
28+
except (AttributeError, ValueError) as e:
29+
print(f"注意: 无法设置{description}处理器: {e}")
30+
31+
32+
def handle_sigint(signum, frame):
33+
app = Application.get_instance()
34+
if not app:
35+
sys.exit(0)
36+
37+
# 使用app的主循环,更稳定且跨线程安全
38+
loop = app._main_loop
39+
if loop and not loop.is_closed():
40+
# 直接创建task在指定的循环中
41+
def create_shutdown_task():
42+
try:
43+
if loop.is_running():
44+
asyncio.run_coroutine_threadsafe(app.shutdown(), loop)
45+
else:
46+
loop.create_task(app.shutdown())
47+
except Exception as e:
48+
print(f"创建shutdown任务失败: {e}")
49+
sys.exit(0)
50+
51+
loop.call_soon_threadsafe(create_shutdown_task)
52+
else:
53+
# 主循环未就绪或已关闭,直接退出
54+
sys.exit(0)
5755

58-
# 设置信号处理器
59-
setup_signal_handler(signal.SIGTRAP, signal.SIG_IGN, "SIGTRAP")
60-
setup_signal_handler(signal.SIGINT, handle_sigint, "SIGINT")
6156

62-
else:
63-
logger.debug("非 macOS 系统,跳过信号处理器设置")
57+
# 设置信号处理器:所有平台尽量设置 SIGINT;支持则设置 SIGTERM;若存在 SIGTRAP 则忽略它
58+
setup_signal_handler(signal.SIGINT, handle_sigint, "SIGINT")
59+
if hasattr(signal, "SIGTERM"):
60+
setup_signal_handler(signal.SIGTERM, handle_sigint, "SIGTERM")
61+
if hasattr(signal, "SIGTRAP"):
62+
setup_signal_handler(signal.SIGTRAP, signal.SIG_IGN, "SIGTRAP")
6463

6564
setup_opus()
6665

src/views/components/system_tray.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ def _setup_tray(self):
5555
self.tray_icon = QSystemTrayIcon()
5656
self.tray_icon.setContextMenu(self.tray_menu)
5757

58+
# 在显示前设置一个占位图标,避免 QSystemTrayIcon::setVisible: No Icon set 警告
59+
try:
60+
# 使用一个纯色圆点作为初始占位
61+
pixmap = QPixmap(16, 16)
62+
pixmap.fill(QColor(0, 0, 0, 0))
63+
painter = QPainter(pixmap)
64+
painter.setRenderHint(QPainter.Antialiasing)
65+
painter.setBrush(QBrush(QColor(0, 180, 0)))
66+
painter.setPen(QColor(0, 0, 0, 0))
67+
painter.drawEllipse(2, 2, 12, 12)
68+
painter.end()
69+
self.tray_icon.setIcon(QIcon(pixmap))
70+
except Exception:
71+
pass
72+
5873
# 连接托盘图标的事件
5974
self.tray_icon.activated.connect(self._on_tray_activated)
6075

src/views/settings/components/audio/audio_widget.py

Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import numpy as np
66
import sounddevice as sd
7-
from PyQt5.QtCore import pyqtSignal
7+
from PyQt5.QtCore import Q_ARG, QMetaObject, Qt, pyqtSignal
88
from PyQt5.QtWidgets import (
99
QComboBox,
1010
QLabel,
@@ -316,15 +316,15 @@ def _do_input_test(self, device_id):
316316
sample_rate = int(input_device['sample_rate'])
317317
duration = 3 # 录音时长3秒
318318

319-
self._append_status(f"开始录音测试 (设备: {device_id}, 采样率: {sample_rate}Hz)")
320-
self._append_status("请对着麦克风说话,比如数数字: 1、2、3...")
319+
self._append_status_threadsafe(f"开始录音测试 (设备: {device_id}, 采样率: {sample_rate}Hz)")
320+
self._append_status_threadsafe("请对着麦克风说话,比如数数字: 1、2、3...")
321321

322322
# 倒计时提示
323323
for i in range(3, 0, -1):
324-
self._append_status(f"{i}秒后开始录音...")
324+
self._append_status_threadsafe(f"{i}秒后开始录音...")
325325
time.sleep(1)
326326

327-
self._append_status("正在录音,请说话... (3秒)")
327+
self._append_status_threadsafe("正在录音,请说话... (3秒)")
328328

329329
# 录音
330330
recording = sd.rec(
@@ -336,7 +336,7 @@ def _do_input_test(self, device_id):
336336
)
337337
sd.wait()
338338

339-
self._append_status("录音完成,正在分析...")
339+
self._append_status_threadsafe("录音完成,正在分析...")
340340

341341
# 分析录音质量
342342
max_amplitude = np.max(np.abs(recording))
@@ -354,27 +354,27 @@ def _do_input_test(self, device_id):
354354

355355
# 测试结果分析
356356
if max_amplitude < 0.001:
357-
self._append_status("[失败] 未检测到音频信号")
358-
self._append_status("请检查: 1) 麦克风连接 2) 系统音量 3) 麦克风权限")
357+
self._append_status_threadsafe("[失败] 未检测到音频信号")
358+
self._append_status_threadsafe("请检查: 1) 麦克风连接 2) 系统音量 3) 麦克风权限")
359359
elif max_amplitude > 0.8:
360-
self._append_status("[警告] 音频信号过载")
361-
self._append_status("建议降低麦克风增益或音量设置")
360+
self._append_status_threadsafe("[警告] 音频信号过载")
361+
self._append_status_threadsafe("建议降低麦克风增益或音量设置")
362362
elif activity_ratio < 0.1:
363-
self._append_status("[警告] 检测到音频但语音活动较少")
364-
self._append_status("请确保对着麦克风说话,或检查麦克风灵敏度")
363+
self._append_status_threadsafe("[警告] 检测到音频但语音活动较少")
364+
self._append_status_threadsafe("请确保对着麦克风说话,或检查麦克风灵敏度")
365365
else:
366-
self._append_status("[成功] 录音测试通过")
367-
self._append_status(f"音质数据: 最大音量={max_amplitude:.1%}, 平均音量={rms:.1%}, 活跃度={activity_ratio:.1%}")
368-
self._append_status("麦克风工作正常")
366+
self._append_status_threadsafe("[成功] 录音测试通过")
367+
self._append_status_threadsafe(f"音质数据: 最大音量={max_amplitude:.1%}, 平均音量={rms:.1%}, 活跃度={activity_ratio:.1%}")
368+
self._append_status_threadsafe("麦克风工作正常")
369369

370370
except Exception as e:
371371
self.logger.error(f"录音测试失败: {e}", exc_info=True)
372-
self._append_status(f"[错误] 录音测试失败: {str(e)}")
372+
self._append_status_threadsafe(f"[错误] 录音测试失败: {str(e)}")
373373
if "Permission denied" in str(e) or "access" in str(e).lower():
374-
self._append_status("可能是权限问题,请检查系统麦克风权限设置")
374+
self._append_status_threadsafe("可能是权限问题,请检查系统麦克风权限设置")
375375
finally:
376-
# 重置UI状态
377-
self._reset_input_test_ui()
376+
# 重置UI状态(切回主线程)
377+
self._reset_input_ui_threadsafe()
378378

379379
def _test_output_device(self):
380380
"""
@@ -418,15 +418,15 @@ def _do_output_test(self, device_id):
418418
duration = 2.0 # 播放时长
419419
frequency = 440 # 440Hz A音
420420

421-
self._append_status(f"开始播放测试 (设备: {device_id}, 采样率: {sample_rate}Hz)")
422-
self._append_status("请准备好耳机/扬声器,即将播放测试音...")
421+
self._append_status_threadsafe(f"开始播放测试 (设备: {device_id}, 采样率: {sample_rate}Hz)")
422+
self._append_status_threadsafe("请准备好耳机/扬声器,即将播放测试音...")
423423

424424
# 倒计时提示
425425
for i in range(3, 0, -1):
426-
self._append_status(f"{i}秒后开始播放...")
426+
self._append_status_threadsafe(f"{i}秒后开始播放...")
427427
time.sleep(1)
428428

429-
self._append_status(f"正在播放 {frequency}Hz 测试音 ({duration}秒)...")
429+
self._append_status_threadsafe(f"正在播放 {frequency}Hz 测试音 ({duration}秒)...")
430430

431431
# 生成测试音频 (正弦波)
432432
t = np.linspace(0, duration, int(sample_rate * duration))
@@ -442,16 +442,16 @@ def _do_output_test(self, device_id):
442442
sd.play(audio, samplerate=sample_rate, device=device_id)
443443
sd.wait()
444444

445-
self._append_status("播放完成")
446-
self._append_status("测试说明: 如果听到清晰的测试音,说明扬声器/耳机工作正常")
447-
self._append_status("如果没听到声音,请检查音量设置或选择其他输出设备")
445+
self._append_status_threadsafe("播放完成")
446+
self._append_status_threadsafe("测试说明: 如果听到清晰的测试音,说明扬声器/耳机工作正常")
447+
self._append_status_threadsafe("如果没听到声音,请检查音量设置或选择其他输出设备")
448448

449449
except Exception as e:
450450
self.logger.error(f"播放测试失败: {e}", exc_info=True)
451-
self._append_status(f"[错误] 播放测试失败: {str(e)}")
451+
self._append_status_threadsafe(f"[错误] 播放测试失败: {str(e)}")
452452
finally:
453-
# 重置UI状态
454-
self._reset_output_test_ui()
453+
# 重置UI状态(切回主线程)
454+
self._reset_output_ui_threadsafe()
455455

456456
def _reset_input_test_ui(self):
457457
"""
@@ -461,6 +461,27 @@ def _reset_input_test_ui(self):
461461
self.ui_controls["test_input_btn"].setEnabled(True)
462462
self.ui_controls["test_input_btn"].setText("测试录音")
463463

464+
def _reset_input_ui_threadsafe(self):
465+
try:
466+
self.testing_input = False
467+
btn = self.ui_controls.get("test_input_btn")
468+
if not btn:
469+
return
470+
QMetaObject.invokeMethod(
471+
btn,
472+
"setEnabled",
473+
Qt.QueuedConnection,
474+
Q_ARG(bool, True),
475+
)
476+
QMetaObject.invokeMethod(
477+
btn,
478+
"setText",
479+
Qt.QueuedConnection,
480+
Q_ARG(str, "测试录音"),
481+
)
482+
except Exception as e:
483+
self.logger.error(f"线程安全重置输入测试UI失败: {e}")
484+
464485
def _reset_output_test_ui(self):
465486
"""
466487
重置输出测试UI状态.
@@ -469,6 +490,27 @@ def _reset_output_test_ui(self):
469490
self.ui_controls["test_output_btn"].setEnabled(True)
470491
self.ui_controls["test_output_btn"].setText("测试播放")
471492

493+
def _reset_output_ui_threadsafe(self):
494+
try:
495+
self.testing_output = False
496+
btn = self.ui_controls.get("test_output_btn")
497+
if not btn:
498+
return
499+
QMetaObject.invokeMethod(
500+
btn,
501+
"setEnabled",
502+
Qt.QueuedConnection,
503+
Q_ARG(bool, True),
504+
)
505+
QMetaObject.invokeMethod(
506+
btn,
507+
"setText",
508+
Qt.QueuedConnection,
509+
Q_ARG(str, "测试播放"),
510+
)
511+
except Exception as e:
512+
self.logger.error(f"线程安全重置输出测试UI失败: {e}")
513+
472514
def _append_status(self, message):
473515
"""
474516
添加状态信息.
@@ -485,6 +527,24 @@ def _append_status(self, message):
485527
except Exception as e:
486528
self.logger.error(f"添加状态信息失败: {e}", exc_info=True)
487529

530+
def _append_status_threadsafe(self, message):
531+
"""
532+
后台线程安全地将状态文本追加到 QTextEdit(切回主线程)。
533+
"""
534+
try:
535+
if not self.ui_controls["status_text"]:
536+
return
537+
current_time = time.strftime("%H:%M:%S")
538+
formatted_message = f"[{current_time}] {message}"
539+
QMetaObject.invokeMethod(
540+
self.ui_controls["status_text"],
541+
"append",
542+
Qt.QueuedConnection,
543+
Q_ARG(str, formatted_message),
544+
)
545+
except Exception as e:
546+
self.logger.error(f"线程安全追加状态失败: {e}", exc_info=True)
547+
488548
def _load_config_values(self):
489549
"""
490550
从配置文件加载值到UI控件.

0 commit comments

Comments
 (0)