44
55import numpy as np
66import sounddevice as sd
7- from PyQt5 .QtCore import pyqtSignal
7+ from PyQt5 .QtCore import Q_ARG , QMetaObject , Qt , pyqtSignal
88from 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