Files

100 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""把 HRIR 截到 N tap(末尾带淡出防爆音), 输出到 hrir/taps-<N>/。
为什么: 每路 HRTF 卷积的成本 ≈ tap 数。SADIE-II H4 在 96k 是 4800 tap,
折到 1024 tap 等于每路少 4.7 倍乘加 —— 12 路卷积器都吃这个。
代价: 砍掉的是脉冲响应尾部(房间/耳廓的晚期反射), 声音会比原来"干"一点。
用法: python3 work/裁HRIR.py <tap数> [源目录, 默认 hrir/current]
只落文件 + 出指标, 不碰正在跑的音频。
"""
import os
import struct
import sys
from typing import Any
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def read_wav(path: str) -> tuple[list[float], int, int]:
"""读 WAV(float32 / int16), 返回 (样本, 帧率, 声道)."""
with open(path, "rb") as fh:
blob = fh.read()
if blob[:4] != b"RIFF" or blob[8:12] != b"WAVE":
raise ValueError("不是 WAV: " + path)
pos, rate, ch, bits, fmt, data = 12, 48000, 1, 32, 3, b""
while pos + 8 <= len(blob):
cid = blob[pos:pos + 4]
csz = struct.unpack("<I", blob[pos + 4:pos + 8])[0]
body = blob[pos + 8:pos + 8 + csz]
if cid == b"fmt ":
fmt = struct.unpack("<H", body[0:2])[0]
ch = struct.unpack("<H", body[2:4])[0]
rate = struct.unpack("<I", body[4:8])[0]
bits = struct.unpack("<H", body[14:16])[0]
elif cid == b"data":
data = body
pos += 8 + csz + (csz & 1)
if bits == 32:
n = len(data) // 4
vals = list(struct.unpack("<%df" % n, data[:n * 4]))
elif bits == 16:
n = len(data) // 2
vals = [v / 32768.0 for v in struct.unpack("<%dh" % n, data[:n * 2])]
else:
raise ValueError("不支持的位深 %d" % bits)
return vals, rate, ch
def write_wav(path: str, x: list[float], rate: int, ch: int) -> None:
"""写 32bit float WAV(与项目里既有 IR 一致)."""
raw = struct.pack("<%df" % len(x), *x)
hdr = b"RIFF" + struct.pack("<I", 36 + len(raw)) + b"WAVE"
fmt = b"fmt " + struct.pack("<IHHIIHH", 16, 3, ch, rate, rate * ch * 4, ch * 4, 32)
dat = b"data" + struct.pack("<I", len(raw)) + raw
with open(path, "wb") as fh:
fh.write(hdr + fmt + dat)
def fade_tail(x: list[float], rate: int, ms: float = 3.0) -> list[float]:
"""末尾淡出, 免得硬切出一个咔嗒."""
k = min(int(rate * ms / 1000.0), len(x) // 4)
if k <= 0:
return x
out = list(x)
for i in range(k):
out[len(x) - k + i] *= 1.0 - (i + 1) / k
return out
def main() -> int:
if len(sys.argv) < 2:
print(__doc__)
return 2
taps = int(sys.argv[1])
src = sys.argv[2] if len(sys.argv) > 2 else os.path.join(ROOT, "hrir", "current")
dst = os.path.join(ROOT, "hrir", "taps-%d" % taps)
os.makedirs(dst, exist_ok=True)
files = sorted(f for f in os.listdir(src) if f.endswith(".wav"))
if not files:
print("! 源目录没有 wav: " + src)
return 1
print("源 %s%s" % (src, dst))
print("%-14s %8s %8s %10s" % ("文件", "原tap", "新tap", "峰值dBFS"))
peak_all = 0.0
for f in files:
x, rate, ch = read_wav(os.path.join(src, f))
n0 = len(x) // ch
cut = x[: taps * ch]
cut = fade_tail(cut, rate)
write_wav(os.path.join(dst, f), cut, rate, ch)
pk = max((abs(v) for v in cut), default=0.0)
peak_all = max(peak_all, pk)
print("%-14s %8d %8d %10.2f" % (f, n0, min(n0, taps), 20 * __import__("math").log10(pk or 1e-9)))
print("完成: %d 个文件, 整体峰值 %.2f dBFS, 目录 %s" % (len(files), 20 * __import__("math").log10(peak_all or 1e-9), dst))
return 0
if __name__ == "__main__":
raise SystemExit(main())