"""界面回归检查之二:对话框的后台任务链路(QThread + 信号)真跑一遍。 自检覆盖了 process_file / export_audio 这些纯函数,但没有覆盖 "Qt 工作线程 → 信号 → 界面回填"这段管线。这里直接构造对话框、 触发 _run()、泵事件循环直到完成,再检查产物是否真的生成。 """ import os import sys import time os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT) import numpy as np from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) app = QApplication([]) from recorder.gui import QSS # noqa: E402 app.setStyleSheet(QSS) from recorder import dsp # noqa: E402 from recorder.wavfile import WavReader, write_wav # noqa: E402 from recorder.window import ExportDialog, ProcessDialog # noqa: E402 fails = [] def check(name, ok, detail=""): print((" ✓ " if ok else " ✗ ") + name + ((" " + detail) if detail else "")) if not ok: fails.append(name) def pump(seconds=1.0, until=None): """泵事件循环,直到 until() 为真或超时。""" t0 = time.time() while time.time() - t0 < seconds: app.processEvents() time.sleep(0.02) if until is not None and until(): return True return until() if until else True work = os.path.join(ROOT, "dev", "shots", "dlg") os.makedirs(work, exist_ok=True) # 造一段"两头静音 + 中间正弦 + 轻微直流偏置"的测试素材 sr = 48000 rng = np.random.default_rng(3) sil = rng.standard_normal((sr // 2, 2)) * dsp.db_to_lin(-85) + 0.001 tone = np.stack([np.sin(2 * np.pi * 440 * np.arange(sr) / sr) * 0.25] * 2, axis=1) src = os.path.join(work, "dialog_src.wav") write_wav(src, np.concatenate([sil, tone, sil]).astype(np.float32), sr, bit_depth="24") print("── 后期处理对话框 " + "─" * 48) pd = ProcessDialog(src, None) pd.show() app.processEvents() pd.trim.setChecked(True) pd.norm.setCurrentIndex(1) # 峰值归一化 pd.depth.setCurrentIndex(pd.depth.findData("16")) # 16 位输出 pd._run() ok = pump(60, until=lambda: pd.progress.value() == 100 and "完成" in pd.log.toPlainText()) out = os.path.splitext(src)[0] + "_processed.wav" check("后期处理任务完成并回填界面", ok and pd.run_btn.isEnabled(), pd.log.toPlainText().strip().splitlines()[-1] if pd.log.toPlainText() else "") check("生成了处理后的文件", os.path.exists(out), f"{os.path.getsize(out) if os.path.exists(out) else 0} 字节") check("处理完成后「打开输出目录」按钮可用", pd.open_btn.isEnabled()) if os.path.exists(out): with WavReader(out) as r: info = r.info() data = r.read() check("输出为 16 位且用时变短(首尾静音已裁)", info["bits"] == 16 and 1.1 < info["duration"] < 1.5, f"{info['format']},{info['duration']:.3f}s(原始 2.000s)") check("峰值被归一化到 -1 dBFS 附近", abs(float(dsp.dbfs(dsp.peak(data, axis=0))[0]) + 1.0) < 0.3, f"{float(dsp.dbfs(dsp.peak(data, axis=0))[0]):+.2f} dBFS") check("直流偏移已去除", abs(float(np.mean(data))) < 1e-3, f"{float(np.mean(data)):.2e}") pd.close() print("\n── 导出对话框 " + "─" * 50) ed = ExportDialog(out, None) ed.show() app.processEvents() check("导出预设列表完整", ed.preset.count() >= 8, "、".join(ed.preset.itemData(i) for i in range(ed.preset.count()))) ed.preset.setCurrentIndex(1) # WAV 24 位 ed._run() done = pump(60, until=lambda: "成功" in ed.log.toPlainText() or "失败" in ed.log.toPlainText()) check("导出任务完成并回填界面", done and "成功" in ed.log.toPlainText(), ed.log.toPlainText().strip()) exported = None for i in range(ed.preset.count()): from recorder import post p = post.default_export_path(out, ed.preset.itemData(i)) if os.path.exists(p): exported = p check("导出文件已生成且不与源文件同名", exported is not None and os.path.abspath(exported) != os.path.abspath(out), os.path.basename(exported) if exported else "无") # 找不到 ffmpeg 的场景:必须有清晰提示而不是崩溃 from recorder import post as _post if _post.find_ffmpeg() is None: ed.preset.setCurrentIndex(ed.preset.findData("flac")) ed._run() pump(20, until=lambda: "失败" in ed.log.toPlainText()) check("缺少 ffmpeg 时导出失败并给出提示", "ffmpeg" in ed.log.toPlainText(), ed.log.toPlainText().strip()[-60:]) else: ed.preset.setCurrentIndex(ed.preset.findData("flac")) ed._run() flac_ok = pump(120, until=lambda: "成功" in ed.log.toPlainText() or "失败" in ed.log.toPlainText()) check("导出 FLAC 成功", flac_ok and "成功" in ed.log.toPlainText(), ed.log.toPlainText().strip()[-60:]) ed.close() print("\n" + "=" * 62) if fails: print(f"失败 {len(fails)} 项:" + "、".join(fails)) sys.exit(1) print("对话框后台任务链路校验全部通过")