diff --git a/README.md b/README.md index d3cd593..8f848b2 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,27 @@ collaplex-audio stop # 连链路一起 **敏感判据 = 通道探针**: 把一个声道指向"左声道全零、右声道为真数据"的双声道文件, 再配 `channel = 1`; 若该声道输出**没有**塌掉, 就说明真的按索引取到了第 2 声道 (实测左右差仅 1.1 dB; 读错声道会掉 40 dB 以上)。 +- **HRIR 的频响校正要用"同相合并响应", 不是标准 DF-EQ** —— 标准漫反射场均衡按各方向 + **功率平均**取参考(假设各方向互不相关), 这套 HRIR 按它算只需要压 1.5 dB; 但真实内容 + (人声/低频居中, L ≈ R)两耳信号是**相加**的, 同相合并响应实测低频高 **7.5 dB**, 这才 + 是用户实际听到的量。按同相合并设计后低频压 7.2 dB, 端到端复测低频落到 0(见下)。 + +## IR 校正(2026-09-20) + +两处校正都改**文件**、不改 conf(数据干净、conf 简洁、任何用到这些 IR 的地方都受益), +工具 `工具/均衡IR.py` 可重跑, 原文件一律备份为 `.orig.wav`。曲线由 `工具/分析均衡曲线.py` +量出来。都用一个**共用的** lowshelf, 所以 HRTF 的方向线索(ILD/ITD)不受影响。 + +| 对象 | 问题 | 校正 | 效果 | +|---|---|---|---| +| 12 条 HRIR | 同相输入时低频比 1 kHz 高 7.5 dB | lowshelf 500 Hz **−7.2 dB** Q=0.707 | 端到端低频 **+7.5 → −0.1 dB** | +| 房间 IR | 低频比中频高 24.5 dB, 湿路灌低频 | lowshelf 300 Hz **−21 dB** Q=0.70 | 低频 **+24.5 → +5.8 dB**(留一点厚度) | + +配套改动: +- `LN_MAX_BOOST` **20 → 12**(默认值) —— 20 dB 的提升上限会把任何轻内容顶穿满刻度, + 是"软限幅常年介入"的直接原因。 +- 房间 IR 被压低频后峰值掉 3.7 dB, 湿路 `gain` 相应 **0.4 → 0.61** 补偿, 这样听感差异 + 纯粹来自"低频变干净"而不是"混响变小"。 ## 实测(2026-09-19) @@ -214,6 +235,21 @@ collaplex-audio stop # 连链路一起 (只改电平的对照实验)、`测试/验证混响声道.py`(混响声道配置检查)、 `测试/诊断低频沙沙.py`(逐级旁路, 注: 其频响口径受电平影响, 已由前两个脚本取代)。 +### 修复后复测(同素材同脚本) + +| 指标 | 修复前 | 修复后 | +|---|---|---| +| HRTF + EQ 的低频(20~200 Hz 相对 1 kHz) | +7.5 dB | **−0.1 dB** | +| 再加混响 | +13.6 dB | **+0.2 dB** | +| 整链(经归一化)低频 | +12.7 dB | **+0.1 dB** | +| 整链峰值因子 | **2.0 dB** | **12.2 dB** | +| **超软限幅阈值样本** | **50.49%** | **0.00%** | +| 整链峰值 / RMS | −0.1 / −2.1 dBFS | **−6.2 / −18.4 dBFS** | + +做完的四件事(全部落地): ① `LN_MAX_BOOST` 20 → 12; ② HRIR 同相合并响应均衡 −7.2 dB; +③ 房间 IR 低频校平 −21 dB + 湿路 gain 0.4 → 0.61 补偿; ④ EQ 微笑曲线(20~63 Hz +2.5 dB、 +高频 +3 dB)改回平直。低频过量收回 ~13 dB 之后, 响度目标回到 −14 dB 也不会再顶穿。 + ## 试听 ```sh diff --git a/bin/collaplex-loudness-norm b/bin/collaplex-loudness-norm index 00c5e43..37b3b22 100644 --- a/bin/collaplex-loudness-norm +++ b/bin/collaplex-loudness-norm @@ -8,7 +8,7 @@ # 音频绕过、零日志)。脚本内部再去 exec 中文路径的 DSP 是没问题的。 export LN_TARGET_DB=-12 export LN_GATE_DB=-50 -export LN_MAX_BOOST=20 +export LN_MAX_BOOST=12 export LN_MAX_CUT=20 export LN_SLEW_DB_S=6 export LN_RATE=96000 diff --git a/hrir/BL_L.wav b/hrir/BL_L.wav index df2b424..1a26e01 100644 Binary files a/hrir/BL_L.wav and b/hrir/BL_L.wav differ diff --git a/hrir/BL_L.wav.pre-eq.wav b/hrir/BL_L.wav.pre-eq.wav new file mode 100644 index 0000000..df2b424 Binary files /dev/null and b/hrir/BL_L.wav.pre-eq.wav differ diff --git a/hrir/BL_R.wav b/hrir/BL_R.wav index aebb680..9a1dd32 100644 Binary files a/hrir/BL_R.wav and b/hrir/BL_R.wav differ diff --git a/hrir/BL_R.wav.pre-eq.wav b/hrir/BL_R.wav.pre-eq.wav new file mode 100644 index 0000000..aebb680 Binary files /dev/null and b/hrir/BL_R.wav.pre-eq.wav differ diff --git a/hrir/BR_L.wav b/hrir/BR_L.wav index d1ab9d2..c9cd35a 100644 Binary files a/hrir/BR_L.wav and b/hrir/BR_L.wav differ diff --git a/hrir/BR_L.wav.pre-eq.wav b/hrir/BR_L.wav.pre-eq.wav new file mode 100644 index 0000000..d1ab9d2 Binary files /dev/null and b/hrir/BR_L.wav.pre-eq.wav differ diff --git a/hrir/BR_R.wav b/hrir/BR_R.wav index e1e9c2b..0c1c82e 100644 Binary files a/hrir/BR_R.wav and b/hrir/BR_R.wav differ diff --git a/hrir/BR_R.wav.pre-eq.wav b/hrir/BR_R.wav.pre-eq.wav new file mode 100644 index 0000000..e1e9c2b Binary files /dev/null and b/hrir/BR_R.wav.pre-eq.wav differ diff --git a/hrir/FC_L.wav b/hrir/FC_L.wav index 4b4dcab..97d1c5f 100644 Binary files a/hrir/FC_L.wav and b/hrir/FC_L.wav differ diff --git a/hrir/FC_L.wav.pre-eq.wav b/hrir/FC_L.wav.pre-eq.wav new file mode 100644 index 0000000..4b4dcab Binary files /dev/null and b/hrir/FC_L.wav.pre-eq.wav differ diff --git a/hrir/FC_R.wav b/hrir/FC_R.wav index dfd0ead..85ad407 100644 Binary files a/hrir/FC_R.wav and b/hrir/FC_R.wav differ diff --git a/hrir/FC_R.wav.pre-eq.wav b/hrir/FC_R.wav.pre-eq.wav new file mode 100644 index 0000000..dfd0ead Binary files /dev/null and b/hrir/FC_R.wav.pre-eq.wav differ diff --git a/hrir/FL_L.wav b/hrir/FL_L.wav index 04e3a44..96f58fa 100644 Binary files a/hrir/FL_L.wav and b/hrir/FL_L.wav differ diff --git a/hrir/FL_L.wav.pre-eq.wav b/hrir/FL_L.wav.pre-eq.wav new file mode 100644 index 0000000..04e3a44 Binary files /dev/null and b/hrir/FL_L.wav.pre-eq.wav differ diff --git a/hrir/FL_R.wav b/hrir/FL_R.wav index 7b2e3f9..44d3988 100644 Binary files a/hrir/FL_R.wav and b/hrir/FL_R.wav differ diff --git a/hrir/FL_R.wav.pre-eq.wav b/hrir/FL_R.wav.pre-eq.wav new file mode 100644 index 0000000..7b2e3f9 Binary files /dev/null and b/hrir/FL_R.wav.pre-eq.wav differ diff --git a/hrir/FR_L.wav b/hrir/FR_L.wav index 8db87f2..84c6996 100644 Binary files a/hrir/FR_L.wav and b/hrir/FR_L.wav differ diff --git a/hrir/FR_L.wav.pre-eq.wav b/hrir/FR_L.wav.pre-eq.wav new file mode 100644 index 0000000..8db87f2 Binary files /dev/null and b/hrir/FR_L.wav.pre-eq.wav differ diff --git a/hrir/FR_R.wav b/hrir/FR_R.wav index 3673a6d..de5ebf1 100644 Binary files a/hrir/FR_R.wav and b/hrir/FR_R.wav differ diff --git a/hrir/FR_R.wav.pre-eq.wav b/hrir/FR_R.wav.pre-eq.wav new file mode 100644 index 0000000..3673a6d Binary files /dev/null and b/hrir/FR_R.wav.pre-eq.wav differ diff --git a/hrir/LFE_L.wav b/hrir/LFE_L.wav index ba7b611..d615ca2 100644 Binary files a/hrir/LFE_L.wav and b/hrir/LFE_L.wav differ diff --git a/hrir/LFE_L.wav.pre-eq.wav b/hrir/LFE_L.wav.pre-eq.wav new file mode 100644 index 0000000..ba7b611 Binary files /dev/null and b/hrir/LFE_L.wav.pre-eq.wav differ diff --git a/hrir/LFE_R.wav b/hrir/LFE_R.wav index ba7b611..d615ca2 100644 Binary files a/hrir/LFE_R.wav and b/hrir/LFE_R.wav differ diff --git a/hrir/LFE_R.wav.pre-eq.wav b/hrir/LFE_R.wav.pre-eq.wav new file mode 100644 index 0000000..ba7b611 Binary files /dev/null and b/hrir/LFE_R.wav.pre-eq.wav differ diff --git a/pipewire/20-collaplex-hrtf.conf b/pipewire/20-collaplex-hrtf.conf index 99426ac..c6fcd9c 100644 --- a/pipewire/20-collaplex-hrtf.conf +++ b/pipewire/20-collaplex-hrtf.conf @@ -20,8 +20,8 @@ context.modules = [ { type = builtin label = convolver name = cFRR config = { filename = "__HRIR__/FR_R.wav" } } { type = builtin label = mixer name = hL control = { "Gain 1" = 1.0 "Gain 2" = 1.0 } } { type = builtin label = mixer name = hR control = { "Gain 1" = 1.0 "Gain 2" = 1.0 } } - { type = builtin label = convolver name = revL config = { filename = "__REVERB__" channel = 0 gain = 0.4 } } - { type = builtin label = convolver name = revR config = { filename = "__REVERB__" channel = 1 gain = 0.4 } } + { type = builtin label = convolver name = revL config = { filename = "__REVERB__" channel = 0 gain = 0.61 } } + { type = builtin label = convolver name = revR config = { filename = "__REVERB__" channel = 1 gain = 0.61 } } { type = builtin label = pipe name = wetgL config = { command = "__WET_WRAPPER__" } } { type = builtin label = pipe name = wetgR config = { command = "__WET_WRAPPER__" } } { type = builtin label = mixer name = wetL control = { "Gain 1" = 1.0 "Gain 2" = 1.0 } } diff --git a/reverb/房间混响IR-96k.wav b/reverb/房间混响IR-96k.wav index cee8526..286347d 100644 Binary files a/reverb/房间混响IR-96k.wav and b/reverb/房间混响IR-96k.wav differ diff --git a/reverb/房间混响IR-96k.wav.pre-eq.wav b/reverb/房间混响IR-96k.wav.pre-eq.wav new file mode 100644 index 0000000..cee8526 Binary files /dev/null and b/reverb/房间混响IR-96k.wav.pre-eq.wav differ diff --git a/工具/分析均衡曲线.py b/工具/分析均衡曲线.py new file mode 100644 index 0000000..b888c20 --- /dev/null +++ b/工具/分析均衡曲线.py @@ -0,0 +1,153 @@ +"""分析出一条"该补多少"的曲线, 供设计均衡滤波器用。 + +两件事: + ① HRIR 漫反射场均衡(DF-EQ): 这套 HRIR 是原始测量数据, 没做过漫反射场均衡, + 表现为"对侧耳低频比自身 1 kHz 高 7~8 dB、所有 IR 高频高 4~5 dB" -> 链路低频过量。 + 标准做法: 用所有方向 IR 的**功率平均**当参考, 取 1/sqrt(P_avg) 作为均衡曲线。 + ② 房间 IR 的频响校平: 混响 IR 低频(20~200 Hz)比中高频高 20~25 dB, 湿路灌低频。 + +只做分析、打印曲线, 不写任何文件。 +""" +from __future__ import annotations + +import glob +import os +import warnings + +import numpy as np +from scipy.io import wavfile + +warnings.filterwarnings("ignore") + +RATE = 96000 +PROJ = "/home/lou/桌面/工作区/实验/collaplex音效" +N = 1 << 15 # FFT 长度 -> 频率分辨率 2.93 Hz + +BANDS: list[float] = [] +_f = 20.0 +while _f <= 20000.0: + BANDS.append(round(_f, 2)) + _f *= 2 ** (1 / 6) # 1/6 倍频程 + + +def read_mono(path: str) -> np.ndarray: + _sr, data = wavfile.read(path) + a = np.asarray(data, dtype=np.float64) + if a.ndim > 1: + a = a[:, 0] + return a + + +def smooth_log(freq: np.ndarray, curve: np.ndarray, frac: float = 1 / 6) -> np.ndarray: + """在 log 频率轴上按 1/frac 倍频程做移动平均(曲线要平滑, 不然是把噪声也均衡了)。""" + out = np.empty_like(curve) + for i, fc in enumerate(freq): + if fc <= 0: + out[i] = curve[i] + continue + m = (freq >= fc * 2 ** (-frac / 2)) & (freq <= fc * 2 ** (frac / 2)) + out[i] = float(np.mean(curve[m])) if bool(np.any(m)) else curve[i] + return out + + +def main() -> None: + freq = np.fft.rfftfreq(N, 1.0 / RATE) + files = sorted(glob.glob(os.path.join(PROJ, "hrir", "*.wav"))) + + power = np.zeros(N // 2 + 1) + for path in files: + a = read_mono(path) + spec = np.fft.rfft(a, N) + power += np.abs(spec) ** 2 + power /= len(files) + + # 漫反射场均衡曲线: 1/sqrt(功率平均), 归一到 1 kHz + eq = 1.0 / np.sqrt(power + 1e-30) + eq = smooth_log(freq, eq, 1 / 6) + ref_mask = (freq >= 900) & (freq <= 1120) + eq /= float(np.mean(eq[ref_mask])) if bool(np.any(ref_mask)) else 1.0 + eq_db = 20 * np.log10(eq + 1e-30) + + print(f"HRIR 文件 {len(files)} 条, 功率平均后取 1/sqrt(P) 得均衡曲线") + print("① HRIR 所需均衡(相对 1 kHz, dB) —— 负值 = 要压掉") + line: list[str] = [] + for fc in BANDS: + if fc > 12000: + break + m = (freq >= fc * 2 ** (-1 / 12)) & (freq <= fc * 2 ** (1 / 12)) + v = float(np.mean(eq_db[m])) if bool(np.any(m)) else 0.0 + line.append(f"{fc:>7.0f}Hz:{v:+5.1f}") + for i in range(0, len(line), 5): + print(" " + " ".join(line[i:i + 5])) + + print() + for lo, hi, tag in ((20, 200, "低频 20-200"), (200, 1000, "中低 200-1k"), + (1000, 3000, "中高 1k-3k"), (3000, 12000, "高频 3k-12k")): + m = (freq >= lo) & (freq <= hi) + print(f" 均值 {tag:>14}: {float(np.mean(eq_db[m])):+6.2f} dB") + + # ---- ② 房间 IR 的频响校平需求 ---- + ir = read_mono(os.path.join(PROJ, "reverb", "房间混响IR-96k.wav")) + n_ir = 1 << 18 # 2.73s IR -> 用大 FFT + spec_ir = np.abs(np.fft.rfft(ir, n_ir)) ** 2 + f_ir = np.fft.rfftfreq(n_ir, 1.0 / RATE) + # 用 1/6 倍频程平滑 + sm = np.zeros_like(spec_ir) + for i in range(len(f_ir)): + fc = f_ir[i] + if fc <= 0: + continue + m = (f_ir >= fc * 2 ** (-1 / 12)) & (f_ir <= fc * 2 ** (1 / 12)) + sm[i] = float(np.mean(spec_ir[m])) if bool(np.any(m)) else spec_ir[i] + ir_db = 10 * np.log10(sm + 1e-30) + ref_m = (f_ir >= 700) & (f_ir <= 1500) + ir_db -= float(np.mean(ir_db[ref_m])) + + print() + print("② 房间 IR 自己的频响(相对 0.7-1.5 kHz 均值, dB) —— 正值 = 该频段过强") + line = [] + for fc in BANDS: + if fc > 12000: + break + m = (f_ir >= fc * 2 ** (-1 / 12)) & (f_ir <= fc * 2 ** (1 / 12)) + v = float(np.mean(ir_db[m])) if bool(np.any(m)) else 0.0 + line.append(f"{fc:>7.0f}Hz:{v:+6.1f}") + for i in range(0, len(line), 5): + print(" " + " ".join(line[i:i + 5])) + for lo, hi, tag in ((20, 200, "低频 20-200"), (200, 1000, "中低 200-1k"), + (1000, 3000, "中高 1k-3k"), (3000, 12000, "高频 3k-12k")): + m = (f_ir >= lo) & (f_ir <= hi) + print(f" 均值 {tag:>14}: {float(np.mean(ir_db[m])):+6.2f} dB") + + # ---- ③ 同相合并响应: 实际内容大多是相关的(人声/低频居中), 两耳信号是**相加** ---- + # 功率平均(①)假设各方向互不相关, 会低估低频; 同相合并才是复核实测的那个口径。 + def merge_power(a_name: str, b_name: str) -> np.ndarray: + xa = np.fft.rfft(read_mono(os.path.join(PROJ, "hrir", a_name)), N) + xb = np.fft.rfft(read_mono(os.path.join(PROJ, "hrir", b_name)), N) + return np.abs(xa + xb) ** 2 + + p_merge = (merge_power("FL_L.wav", "FR_L.wav") + merge_power("FL_R.wav", "FR_R.wav")) / 2.0 + eq_m = 1.0 / np.sqrt(p_merge + 1e-30) + eq_m = smooth_log(freq, eq_m, 1 / 6) + eq_m /= float(np.mean(eq_m[ref_mask])) if bool(np.any(ref_mask)) else 1.0 + eq_m_db = 20 * np.log10(eq_m + 1e-30) + + print() + print("③ 同相合并响应所需的均衡(左耳 = FL_L+FR_L, 右耳 = FL_R+FR_R 取平均)") + line = [] + for fc in BANDS: + if fc > 12000: + break + m = (freq >= fc * 2 ** (-1 / 12)) & (freq <= fc * 2 ** (1 / 12)) + v = float(np.mean(eq_m_db[m])) if bool(np.any(m)) else 0.0 + line.append(f"{fc:>7.0f}Hz:{v:+5.1f}") + for i in range(0, len(line), 5): + print(" " + " ".join(line[i:i + 5])) + for lo, hi, tag in ((20, 200, "低频 20-200"), (200, 1000, "中低 200-1k"), + (1000, 3000, "中高 1k-3k"), (3000, 12000, "高频 3k-12k")): + m = (freq >= lo) & (freq <= hi) + print(f" 均值 {tag:>14}: {float(np.mean(eq_m_db[m])):+6.2f} dB") + + +if __name__ == "__main__": + main() diff --git a/工具/均衡IR.py b/工具/均衡IR.py new file mode 100644 index 0000000..68e1fe8 --- /dev/null +++ b/工具/均衡IR.py @@ -0,0 +1,137 @@ +"""给 IR 做频响校正 —— HRIR 漫反射场均衡 / 房间 IR 低频校平。 + +用法: + python 工具/均衡IR.py 分析 只看曲线, 不写文件 + python 工具/均衡IR.py hrir 给 12 条 HRIR 做均衡(备份 .orig.wav) + python 工具/均衡IR.py reverb 给房间 IR 做低频校平(备份 .orig.wav) + python 工具/均衡IR.py 全部 + +为什么用 biquad 而不是频域均衡: + 低频搁架需要很长的 FIR 才能表达(42ms 的 FIR 只有 23Hz 分辨率), 而 IIR 搁架几个 + 参数就够、且**因果**、不会把 IR 推出非因果的"前兆"。曲线本身是平滑的搁架型, + biquad 的表征能力完全够。 + +★ 为什么 HRIR 要用"同相合并响应"而不是标准 DF-EQ: + 标准 DF-EQ 用各方向**功率平均**做参考(假设各方向不相关), 实测只要求压 1.5 dB; + 但真实内容(人声/低频居中, L≈R)两耳信号是**相加**的 —— 同相合并响应实测低频 + 高 7.5 dB, 这才是用户听到的那个量。所以这里按同相合并来设计。 +""" +from __future__ import annotations + +import math +import os +import shutil +import sys +import warnings + +import numpy as np +from scipy.io import wavfile +from scipy.signal import sosfilt, sosfreqz + +warnings.filterwarnings("ignore") + +RATE = 96000 +PROJ = "/home/lou/桌面/工作区/实验/collaplex音效" +HRIR_DIR = os.path.join(PROJ, "hrir") +REVERB = os.path.join(PROJ, "reverb", "房间混响IR-96k.wav") + +# 校正参数(由 工具/分析均衡曲线.py 的曲线量出来) +HRIR_SHELF = (500.0, -7.2, 0.707) # freq, gain_dB, Q —— 整族 HRIR 共用同一条, +# 方向线索(ILD/ITD)因此不受影响 +REVERB_SHELF = (300.0, -21.0, 0.70) # 房间 IR 低频比中频高 24.5 dB, 压到约 +5 dB, +# 留一点厚度(真实房间的低频混响本就偏强) + + +def lowshelf(freq: float, gain_db: float, q: float, fs: int) -> list[float]: + """RBJ cookbook 的低频搁架。""" + a = 10.0 ** (gain_db / 40.0) + w0 = 2.0 * math.pi * freq / fs + alpha = math.sin(w0) / (2.0 * q) + cos_w0 = math.cos(w0) + sqrt_a = math.sqrt(a) + b0 = a * ((a + 1) - (a - 1) * cos_w0 + 2 * sqrt_a * alpha) + b1 = 2 * a * ((a - 1) - (a + 1) * cos_w0) + b2 = a * ((a + 1) - (a - 1) * cos_w0 - 2 * sqrt_a * alpha) + a0 = (a + 1) + (a - 1) * cos_w0 + 2 * sqrt_a * alpha + a1 = -2 * ((a - 1) + (a + 1) * cos_w0) + a2 = (a + 1) + (a - 1) * cos_w0 - 2 * sqrt_a * alpha + return [b0 / a0, b1 / a0, b2 / a0, 1.0, a1 / a0, a2 / a0] + + +def band_report(freq: np.ndarray, db: np.ndarray, label: str) -> None: + out: list[str] = [] + for lo, hi, tag in ((20, 200, "20-200"), (200, 1000, "200-1k"), + (1000, 3000, "1k-3k"), (3000, 12000, "3k-12k")): + m = (freq >= lo) & (freq <= hi) + out.append(f"{tag}:{float(np.mean(db[m])):+5.1f}") + print(f" {label:<12} " + " ".join(out)) + + +def process(path: str, shelf: tuple[float, float, float], label: str, + n_fft: int, write: bool) -> None: + sr, data = wavfile.read(path) + a = np.asarray(data, dtype=np.float64) + stereo = a.ndim > 1 + if not stereo: + a = a[:, None] + + n0, n_ch = a.shape + spec_before = np.abs(np.fft.rfft(a[:, 0], n_fft)) ** 2 + freq = np.fft.rfftfreq(n_fft, 1.0 / sr) + + sos = np.array([lowshelf(shelf[0], shelf[1], shelf[2], sr)], dtype=np.float64) + y = np.empty_like(a) + for ch in range(n_ch): + y[:, ch] = sosfilt(sos, a[:, ch]) + + spec_after = np.abs(np.fft.rfft(y[:, 0], n_fft)) ** 2 + w_curve, h = sosfreqz(sos, worN=n_fft, fs=sr) + curve_db = 20 * np.log10(np.abs(h) + 1e-12) + curve_freq = np.asarray(w_curve, dtype=np.float64) + + print(f"[{label}] {os.path.basename(path)} {n0} 样 x {n_ch} 声道") + print(f" 搁架: freq={shelf[0]:.0f}Hz gain={shelf[1]:+.1f}dB Q={shelf[2]}") + band_report(curve_freq, curve_db, "施加的曲线") + b_db = 10 * np.log10(spec_before + 1e-30) + a_db = 10 * np.log10(spec_after + 1e-30) + ref = (freq >= 900) & (freq <= 1100) + s = float(np.mean(a_db[ref] - b_db[ref])) + band_report(freq, b_db - float(np.mean(b_db[ref])), "改前") + band_report(freq, a_db - float(np.mean(a_db[ref])) - s, "改后") + + peak_b = float(np.max(np.abs(a))) + peak_a = float(np.max(np.abs(y))) + print(f" 峰值 {peak_b:.5f} -> {peak_a:.5f} ({20 * math.log10(peak_a / peak_b + 1e-12):+.2f} dB)") + + if write: + # ★ 用独立的备份名。reverb 的 `.orig.wav` 是"剪直达声之前"的版本(那一步建的), + # 这里存的是"频响校正之前"的版本, 两者不能混用同一个名字 —— 否则重跑一步 + # 会把另一步的基线覆盖掉。重跑顺序: 先 重做混响IR.py, 再 均衡IR.py。 + bak = path + ".pre-eq.wav" + if not os.path.exists(bak): + shutil.copy2(path, bak) + print(f" 已备份 -> {os.path.basename(bak)}") + wavfile.write(path, sr, y.astype(np.float32)) + print(" 已写回") + + +def main() -> None: + mode = sys.argv[1] if len(sys.argv) > 1 else "分析" + write = mode != "分析" + + if mode in ("分析", "hrir", "全部"): + print("=== HRIR 漫反射场均衡(同相合并响应口径) ===") + for name in sorted(os.listdir(HRIR_DIR)): + if name.endswith(".wav") and not name.endswith(".orig.wav"): + process(os.path.join(HRIR_DIR, name), HRIR_SHELF, "HRIR", + 1 << 15, write and mode in ("hrir", "全部")) + print() + + if mode in ("分析", "reverb", "全部"): + print("=== 房间 IR 低频校平 ===") + process(REVERB, REVERB_SHELF, "房间IR", 1 << 18, + write and mode in ("reverb", "全部")) + + +if __name__ == "__main__": + main()