Files

117 lines
3.9 KiB
Python

"""湿度滑块验证: 播脉冲, 测输出里的混响尾巴能量随湿量变化。"""
from __future__ import annotations
import json
import subprocess
import time
import urllib.request
import wave
import numpy as np
from scipy.io import wavfile
RATE = 96000
IMPULSE = "/tmp/imp.wav"
REC = "/tmp/imp_rec.wav"
API = "http://127.0.0.1:8789"
def sink() -> str:
"""找数字输出设备的 node.name(含 iec958 的那个)。"""
try:
out = subprocess.run(["pw-dump"], capture_output=True, text=True, timeout=20).stdout
nodes = json.loads(out)
except (OSError, ValueError) as exc:
print(" (pw-dump 失败: %s)" % exc)
return ""
for node in nodes:
props = (node.get("info") or {}).get("props") or {}
if props.get("media.class") != "Audio/Sink":
continue
name = str(props.get("node.name", ""))
if "iec958" in name:
return name
print(" (没找到 iec958 设备, 现有 sink 已打印在上方)")
return ""
def post(path: str, body: dict[str, float]) -> None:
req = urllib.request.Request(API + path, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=10).read()
def make_impulse() -> None:
"""0.1 秒脉冲 + 1.9 秒静音 = 2 秒素材。
加静音尾巴是因为 pw-play 起播和 pw-record 启动都有几百毫秒延迟,
素材太短会在录音真正开始前就播完(录到全零)。
"""
n = int(RATE * 3.0)
x = np.zeros(n)
m = int(RATE * 0.1)
t = np.arange(m) / RATE
start = int(RATE * 1.0) # 脉冲放第 1 秒: 等录音真正跑起来再响
x[start:start + m] = 0.5 * np.sin(2 * np.pi * 1000 * t) * np.hanning(m)
data = (x * 32767).astype("<i2")
with wave.open(IMPULSE, "wb") as fh:
fh.setnchannels(2)
fh.setsampwidth(2)
fh.setframerate(RATE)
fh.writeframes(np.repeat(data, 2).tobytes())
def read_rec() -> np.ndarray:
"""读回录音文件(取左声道)。"""
try:
_rate, data = wavfile.read(REC)
except OSError:
return np.zeros(1, dtype=np.float64)
arr = np.asarray(data, dtype=np.float64)
return arr[:, 0] if arr.ndim > 1 else arr
def tail_db(rec: np.ndarray) -> float:
"""从录音里的峰值(脉冲)之后 0.2~1.2 秒算能量 = 混响尾巴。
用峰值自校准, 免得依赖"录音开始时脉冲正好在第 N 秒"。
"""
if rec.size < RATE:
return -120.0
peak_idx = int(np.argmax(np.abs(rec)))
start = peak_idx + int(0.2 * RATE)
end = min(peak_idx + int(1.2 * RATE), rec.size)
if end <= start:
return -120.0
rms = float(np.sqrt(np.mean(rec[start:end] ** 2)))
return 20.0 * np.log10(rms + 1e-12)
def main() -> None:
dev = sink()
if not dev:
print("找不到数字输出设备, 退出")
return
print(" 设备: %s" % dev.split(".")[-3])
make_impulse()
for wet in (0.0, 0.3, 0.6, 1.0):
post("/api/wet", {"wet": wet})
time.sleep(0.3)
# 先起录音再播放: pw-record 启动有延迟, 反过来会漏掉脉冲
play = subprocess.Popen(["pw-play", "--target", "collaplex_vsink", IMPULSE],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(0.3)
subprocess.run(["timeout", "3.5", "pw-record", "--target", dev,
"-P", "{ stream.capture.sink = true }", "--rate", str(RATE),
"--channels", "2", "--format", "f32", REC],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
play.wait(timeout=6)
recorded = read_rec()
print(" 湿量 %.2f -> 混响尾巴 %6.1f dBFS" % (wet, tail_db(recorded)))
post("/api/wet", {"wet": 0.3})
print(" (已恢复湿量 0.3)")
if __name__ == "__main__":
main()