100 lines
3.7 KiB
Python
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())
|