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

125 lines
4.0 KiB
Python

"""中低频沙沙定位: 播 200Hz 纯音, 改总音量, 录输出看谐波/非谐波成分。"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
import urllib.request
import wave
import numpy as np
from scipy.io import wavfile
PROJ = "/home/lou/桌面/工作区/实验/collaplex音效"
RATE = 96000
FREQ = 200.0
SRC = "/tmp/thd_tone.wav"
SINK = ""
API = "http://127.0.0.1:8789"
def find_digital_sink() -> str:
raw = subprocess.run(["pw-dump"], capture_output=True, text=True).stdout
for node in json.loads(raw):
props = (node.get("info") or {}).get("props") or {}
name = str(props.get("node.name", ""))
if props.get("media.class") == "Audio/Sink" and "iec958" in name:
return name
return ""
def write_tone() -> None:
secs = 5.0
n = int(RATE * secs)
t = np.arange(n) / RATE
x = (0.1 * np.sin(2 * np.pi * FREQ * t)) # 峰值 -20 dBFS
ramp = np.minimum(1.0, np.minimum(np.arange(n) / 2000.0, (n - np.arange(n)) / 2000.0))
x = x * ramp
data = (np.clip(x, -1, 1) * 32767).astype("<i2")
with wave.open(SRC, "wb") as fh:
fh.setnchannels(2)
fh.setsampwidth(2)
fh.setframerate(RATE)
fh.writeframes(np.repeat(data, 2).tobytes())
def set_volume(db: float) -> None:
req = urllib.request.Request(API + "/api/volume", data=json.dumps({"db": db}).encode(),
headers={"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=10).read()
def capture(path: str) -> None:
player = subprocess.Popen(["pw-play", "--target", "collaplex_vsink", SRC],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(0.6)
rec = subprocess.Popen(["timeout", "3", "pw-record", "--target", SINK,
"-P", "{ stream.capture.sink = true }",
"--rate", str(RATE), "--channels", "2", "--format", "f32", path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
rec.wait(timeout=12)
player.terminate()
player.wait(timeout=5)
def analyse(path: str, label: str) -> None:
try:
_rate, data = wavfile.read(path)
except (OSError, ValueError):
print(" %-12s 读不到录音" % label)
return
x = data.astype(np.float64)
if x.ndim > 1:
x = x[:, 0]
if x.size < 8192:
print(" %-12s 录音太短" % label)
return
x = x[4096:4096 + 65536]
x = x - float(np.mean(x))
win = np.hanning(x.size)
spec = np.abs(np.fft.rfft(x * win)) ** 2
freqs = np.fft.rfftfreq(x.size, 1.0 / RATE)
def band_energy(f0: float, f1: float) -> float:
mask = (freqs >= f0) & (freqs < f1)
return float(np.sum(spec[mask]))
def tone_energy(f: float, half: float = 12.0) -> float:
return band_energy(f - half, f + half)
fund = tone_energy(FREQ)
harmonics = sum(tone_energy(FREQ * k) for k in range(2, 9))
total = float(np.sum(spec)) + 1e-30
rest = max(total - fund - harmonics, 0.0)
to_db = lambda v: 10.0 * np.log10(v + 1e-30) # noqa: E731
print(" %-12s 基频 %7.1f dB | 谐波(2~8次) %7.1f dB 相对基频 %+6.1f dB | "
"其余(含噪声) %7.1f dB 相对基频 %+6.1f dB" % (
label, to_db(fund), to_db(harmonics), to_db(harmonics) - to_db(fund),
to_db(rest), to_db(rest) - to_db(fund)))
def main() -> None:
global SINK
SINK = find_digital_sink()
if not SINK:
print("找不到数字输出")
return
write_tone()
print("素材: 200 Hz 纯音(峰值 -20 dBFS), 录音后分析 200k 分量之外的成分")
print("判据: 谐波/其余成分相对基频越低越干净; 若 +6~+12 dB 时显著抬起, 就是限幅失真")
print()
for db in (0.0, 6.0, 12.0):
set_volume(db)
time.sleep(0.5)
path = "/tmp/thd_%d.wav" % int(db)
capture(path)
analyse(path, "总音量 %+d dB" % int(db))
set_volume(6.0)
if __name__ == "__main__":
main()