Initial commit: RecorderStudio:PyQt5 高保真录音软件,无损 WAV / WASAPI 独占、BS.1770 响度与 ffmpeg 交叉验证
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""界面回归检查之二:对话框的后台任务链路(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("对话框后台任务链路校验全部通过")
|
||||
Reference in New Issue
Block a user