Files
collaplex-audio/测试/test_echo.py
T

93 lines
3.1 KiB
Python

"""回声诊断: 播单脉冲, 录数字输出, 看是平滑混响衰减还是离散回声。
离散回声 = 主峰之后出现孤立的、比周围明显高的峰(延迟 20ms 以上);
平滑混响 = 包络单调下降。分湿量 0 / 0.3 各测一次, 好判断回声是不是混响造成的。
"""
from __future__ import annotations
import json
import subprocess
import time
import urllib.request
from pathlib import Path
import numpy as np
from scipy.io import wavfile
RATE = 96000
MAT = Path("/tmp/echo_mat.wav")
REC = Path("/tmp/echo_rec.wav")
API = "http://127.0.0.1:8789"
def api(path: str, payload: dict[str, object] | None = None) -> dict[str, object]:
"""调面板 API。"""
headers = {"Content-Type": "application/json"}
data = json.dumps(payload).encode() if payload else None
req = urllib.request.Request(API + path, data=data, headers=headers)
with urllib.request.urlopen(req, timeout=5) as r:
return json.loads(r.read())
def db(x: float) -> float:
"""线性转 dBFS。"""
return 20 * np.log10(max(float(x), 1e-7))
def find_monitor() -> str:
"""找数字输出的 monitor 名字。"""
out = subprocess.run(["pw-dump"], capture_output=True, text=True, timeout=20).stdout
for line in out.splitlines():
if '"node.name"' in line and "iec958" in line:
return line.split('"')[3] + ".monitor"
return ""
def main() -> None:
"""跑诊断。"""
n = int(RATE * 3.5)
x = np.zeros(n, dtype=np.float32)
x[int(RATE * 1.0)] = 0.5
wavfile.write(MAT, RATE, (x * 32767).astype(np.int16))
mon = find_monitor()
print("录音设备:", mon or "(没找到, 用默认)")
args = ["pw-record", "--rate", str(RATE), "--channels", "2", "--format", "s16", str(REC)]
if mon:
args[1:1] = ["--target", mon]
# 静音 Chrome(它会往默认输出送声音, 污染测量), 完事恢复
subprocess.run(["wpctl", "set-mute", "90", "1"], capture_output=True, timeout=10)
for wet in (0.0, 0.3):
api("/api/wet", {"wet": wet})
time.sleep(0.3)
REC.unlink(missing_ok=True)
rec = subprocess.Popen(args)
time.sleep(0.6)
subprocess.run(["pw-play", "--target", "collaplex_vsink", str(MAT)], timeout=30)
time.sleep(0.8)
rec.terminate()
rec.wait(timeout=5)
_r, data = wavfile.read(REC)
mono = data[:, 0].astype(np.float64) / 32768.0
pk = int(np.argmax(np.abs(mono)))
print(f"\n湿量 {wet:.2f}: 主峰 @ {pk / RATE * 1000:.1f} ms, {db(abs(mono[pk])):.1f} dBFS")
seg: list[float] = []
for k in range(1, 61):
a = pk + int(RATE * 0.02 * (k - 1))
b = pk + int(RATE * 0.02 * k)
if b > len(mono):
break
seg.append(db(np.max(np.abs(mono[a:b]))))
print(" 主峰后每 20ms 包络(dBFS):")
print(" ", " ".join(f"{v:.0f}" for v in seg[:40]))
api("/api/wet", {"wet": 0.3})
subprocess.run(["wpctl", "set-mute", "90", "0"], capture_output=True, timeout=10)
print("\n(湿量恢复 0.3, Chrome 已取消静音)")
main()