Files
lou c9e9a5ac28 混响立体声丢失: 重做 IR 的工具把双声道压成了单声道 -> conf 改用 convolver 的 channel 按声道取
- 工具/重做混响IR.py 原来写 `x = x[:, 0]`, 只取左声道、把右声道丢掉, 写出去的 IR 成了
  单声道; 而 20-conf 里 revL/revR 读的是同一个文件 -> 两路湿声完全同源, 混响糊在正中央、
  没有宽度(原 IR 本身是双声道真立体声, 左右相关 0.884)
- 改为逐声道处理 + 双声道写回; 重新生成 reverb/房间混响IR-96k.wav (285888 x 2, 相关 0.9492)
- 20-conf: revL 用 channel = 0 / revR 用 channel = 1 —— PipeWire 的 convolver 支持该键,
  从多声道 IR 文件里按索引取声道。实测(通道探针: 左零右真的文件 + channel = 1)右耳尾巴
  不塌, 左右差仅 1.1 dB; 若读错声道会掉 40 dB 以上
- 新增取证脚本: 扫频测频响(Farina 反卷积, 确定性信号才测得准)、电平与失真诊断(只改电平的
  对照实验)、验证混响声道(文件级 + 物理级检查)、诊断低频沙沙(逐级旁路)
- 扫频脚本修了一个 bug: 湿量只在第一轮写, 后面几轮沿用被改过的值, 导致 B/C 两轮测的是同一配置
- README: 坑表加两条(双声道 IR + 验证判据的坑) + 2026-09-20 低频/沙沙诊断实测段
2026-09-20 12:55:51 +08:00

92 lines
3.5 KiB
Python

"""重做房间混响 IR: 剪掉直达声, 只留混响尾。
原 IR 前 20ms 是静音、主峰在 30ms(= 10.3m 的传播延迟), 前两次反射(30/40ms)与直达声
同量级。它和零延迟的干声相加时, 湿声整体晚 30ms 且很响 -> 听感就是明显的回声。
处理:
1. 从主峰前一点点开始裁(去掉纯静音段)
2. 前 55ms 余弦渐入 —— 压掉直达声和过强的早期反射
3. 之后原样保留(混响尾)
★ 声道: 原 IR 是**双声道立体声**(左右相关 0.884), 处理必须逐声道做、保持双声道输出。
2026-09-19 版这里写了 `x = x[:, 0]` —— 把右声道丢掉了, 输出的 IR 变成单声道,
而 filter-chain 里 revL/revR 读同一个文件 -> 两路湿声 100% 同相: 混响糊在正中间、
低频同相叠加 +6 dB。现在改成: 双声道各自处理后原样写回, conf 里用
`channel = 0` / `channel = 1` 分别取左右。
产出 reverb/房间混响IR-96k.wav(原文件备份为 .orig.wav), 并打印标定用的增益。
"""
from __future__ import annotations
import shutil
from pathlib import Path
import numpy as np
from scipy.io import wavfile
IR = Path("reverb/房间混响IR-96k.wav")
BAK = Path("reverb/房间混响IR-96k.orig.wav")
FADE_MS = 55.0 # 渐入长度
TRIM_MS = 22.0 # 裁掉的纯静音段
TARGET_PEAK = 0.1493 # 原 IR 峰值(保持同量级, 免得到时候电平对不上)
def main() -> None:
"""处理 IR, 逐声道, 保持双声道。"""
if not BAK.exists():
shutil.copy2(IR, BAK)
print(f"已备份当前 IR -> {BAK}")
# ★ 一律从原始备份读: 当前 IR 可能已经被上一版降成单声道了
rate, data = wavfile.read(BAK)
x = np.asarray(data, dtype=np.float64)
if x.ndim == 1:
x = x[:, None]
n_ch = int(x.shape[1])
n0 = int(x.shape[0])
if n_ch > 1:
corr0 = float(np.corrcoef(x[:, 0], x[:, 1])[0, 1])
print(f"源 IR: {n0} 样 x {n_ch} 声道, 左右相关 {corr0:.4f}")
else:
print(f"源 IR: {n0} 样 x {n_ch} 声道")
peak0 = float(np.abs(x).max())
rms0 = float(np.sqrt((x ** 2).mean()))
trim = int(rate * TRIM_MS / 1000)
y = x[trim:].copy()
# 前 FADE_MS 余弦渐入(升余弦的前半段) —— 逐声道同一个窗
fade = int(rate * FADE_MS / 1000)
if fade < len(y):
t = np.arange(fade) / fade
w = 0.5 * (1 - np.cos(np.pi * t))
y[:fade] *= w[:, None]
peak1 = float(np.abs(y).max())
rms1 = float(np.sqrt((y ** 2).mean()))
print(f"原 IR: 峰值 {peak0:.4f}, RMS {rms0:.6f}")
print(f"新 IR: {len(y)} 样, 峰值 {peak1:.4f}, RMS {rms1:.6f}")
print(f"峰值比 {peak1 / peak0:.4f} ({20 * np.log10(peak1 / peak0):+.2f} dB)")
print(f"RMS 比 {rms1 / rms0:.4f} ({20 * np.log10(rms1 / rms0):+.2f} dB)")
# 保持峰值量级与原版一致(后面 gain 仍按 0.126 用, 电平不会跑)
y *= TARGET_PEAK / peak1
wavfile.write(IR, rate, y.astype(np.float32))
print(f"已写入 {IR}")
if n_ch > 1:
corr = float(np.corrcoef(y[:, 0], y[:, 1])[0, 1])
print(f"\n保持双声道: 处理后左右相关 = {corr:.4f}")
print(f" 左 RMS {np.sqrt((y[:, 0] ** 2).mean()):.6f} / 右 RMS {np.sqrt((y[:, 1] ** 2).mean()):.6f}")
print("\n新 IR 每 10ms 包络(左声道):")
z = np.abs(y[:, 0])
for k in range(0, 14):
a, b = int(rate * 0.01 * k), int(rate * 0.01 * (k + 1))
if b > len(z):
break
print(f" {k * 10:4d} ms {20 * np.log10(max(z[a:b].max(), 1e-7)):7.1f} dB")
main()