Collaplex 音效

耳机方向的空间音效链路(PipeWire)。

链路

全局音频(应用输出) → collaplex_vsink(虚拟声卡) → 响度归一化(pipe DSP)
                   → collaplex_hrtf_in(双耳 HRTF + 房间混响, 湿量 0.3)
                   → EQ(三段) + 总音量放大(+6 dB)
                   → 数字输出(S/PDIF)

安装

bash 脚本/安装.sh                # 默认: 混响湿量 0.3, 总音量 +6 dB
VOL_DB=9 bash 脚本/安装.sh       # 总音量改成 +9 dB
WET=0.4 bash 脚本/安装.sh        # 混响湿量改成 0.4

脚本:探测数字输出 → 部署 pipe 插件入口 → 渲染两份配置 → 清掉残留 DSP(占住共享槽位会让新实例起不来) → 重载 PipeWire → 自检(4 个节点 + 2 个 DSP 进程)。

调 EQ

pipewire/20-collaplex-hrtf.confparam_eq 节点(左右各一个, 两处一起改):

filters1 = [ { type = bq_lowshelf freq = 120  gain = 0.0 }          # 低频架
             { type = bq_peaking  freq = 1000 gain = 0.0 q = 1.0 } ] # 中频峰
filters2 = [ { type = bq_highshelf freq = 6000 gain = 0.0 } ]        # 高频架

gain 单位 dB(正=提升), 改完重跑 安装.sh

★ 坑: param_eq 的端口名带编号 —— In 1 / Out 1(走 filters1)、In 2 / Out 2(走 filters2)。 写成 :In / :Out 会报 unknown input port 并让整张图加载失败(模块静默降级成直通, 节点都不建)。

控制面板

bash 脚本/面板.sh            # http://127.0.0.1:8789
  • 电平归一化(响度目标)、总音量、32 段 EQ 全部是滑块:滚轮调节、Shift+滚轮微调(0.1 dB)、双击归零
  • EQ 是 1/3 倍频程 32 段(20 Hz … 25 kHz),每段 ±15 dB,实时生效(DSP 从共享内存读参数,不用重装)
  • 参数落点:/dev/shm/collaplex-eq(前端写、DSP 读,版本号保证不成半更新);响度目标写在 ~/.local/state/cinema-spatial/loudness.json(归一化 DSP 每秒重读)

关于"放大"与削波(2026-09-19 修)

总音量级的输出不能硬削(原实现是 np.clip(out, -1.0, 1.0)): 硬削把波形削成平顶, 听感就是"炸麦"; 而被削掉的峰值不增加响度, 所以出现"声音没变大、却削峰了"。

改用软限幅 dsp/eq.py: soft_limit(): 上限 -1 dBFS, 幅值在 (上限−拐点) 以内完全透明, 超出部分用 tanh 平滑渐近上限 —— 不产生平顶, 波形其余部分不动, 响度真实提升。

实测(峰值 -12 dBFS 素材, 总音量 +12 dB): 输出峰值 -1.01 dBFS, 贴顶样本自 64.6% → 0%

第二处放大 bug(2026-09-19 修)

20-collaplex-hrtf.conf 里残留了一级固定 +6 dB(volL/volR mixer)和一段用不上的 param_eq —— 它和面板的总音量叠了两级, 把归一化守住的 3 dBFS 顶到 +3 dBFS 再送进 EQ, 软限幅被迫一直工作, 听感就是"响度上不去、动态还被压"。已删除, 音量只由面板控制

逐级实测排除法: 归一化输出峰值 ≤ 3.17 dBFS(从不超 0, 清白); HRIR 峰值 17 dB、房间混响 IR 16.5 dB, 都不可能推高峰值 → 元凶就是那句残留增益。

修后端到端(峰值 −12 dBFS 素材):

总音量 出 RMS 提升 出峰值
+0 dB 22.97 dBFS 12.28 dBFS
+6 dB 16.98 dBFS +5.99 dB 6.35 dBFS
+12 dB 11.77 dBFS +11.20 dB 1.11 dBFS

中低频沙沙声的根因(2026-09-19 修)

reverb/房间混响IR-96k.wav未归一化的原始 IR: 它的频响在 100~200 Hz 比中高频高约 20 dB (|H| = 8.9 / 10.6, 而 8 kHz 只有 0.26)。湿路径(×0.3)因此在中低频 +10 dB, 与干信号相干相加后 整个 HRTF 级 +19 dB —— 归一化守住的 −14 dBFS 被顶到 +5 dBFS, EQ 的软限幅于是一直在工作, 生成 3 次谐波与宽带失真, 听感就是中低频沙沙

修法: 给房间混响的 convolver 加线性增益 gain = 0.126(18 dB)。

★ 坑: convolvergain线性倍数, 不是 dB —— 写 -18.0 会变成"反相 ×18", 电平反而更高。

修后实测: EQ 入峰值 +5.16 → 15.57 dBFS; 600 Hz 谐波 9.9 → 28.9 dB; 软限幅不再触发。

已修(2026-09-19): 归一化的增益原来是逐块更新的, 块间台阶产生约 −29 dB 的谐波。 现已改为块内逐样本线性插值(上一块增益 → 本块增益的斜坡), 谐波(相对基频) +0/+6 dB 由 12.1 / 15.9 dB 降到 90.1 / 70.6 dB

限幅阈值定太低的坑(2026-09-19 修)

EQ 输出级的软限幅原来设 ceiling = 0.891 / knee = 0.25, 介入点低到 3.9 dBFS。 后果: 纯音峰值因子只有 3 dB, 峰值落在 −11 dBFS —— 测不出问题; 而音乐峰值因子 10~14 dB, 归一化到 14 dBFS RMS 后峰值就在 4 ~ 0 dBFS, 正好长期落在限幅区里被压 → 中低频谐波 → 沙沙。 所以才会"纯音测干净、一放音乐就犯"。

现改为 ceiling = 0.99 / knee = 0.06(介入点 0.63 dBFS): 平时完全透明, 只在真要削波时介入。 实测(高峰值因子素材): 常规段增益精确 +6.00 dB; 仅瞬态尖峰(入 2.21 dBFS)被压到 0.09 dBFS。

教训: 峰值相关的失真纯音测不出来, 必须用高峰值因子素材 —— 见 测试/test_limit.py

空间数据来源

用途 数据 来源
双耳 HRTF SADIE II 受试者 H4, 96 kHz / 24 bit / 512 tap, 每条截取 4800 tap(约 50 ms) University of York 音频实验室, Zenodo DOI 10.5281/zenodo.12092466
房间混响 自家房间模型程序化生成的双耳 IR 本项目 reverb/
  • hrir/ 的 12 条 = H4 的 6 个方向 × 左右耳(BL / BR / FC / FL / FR / LFE); 链路实际用前左 + 前右共 4 条(FL_L / FL_R / FR_L / FR_R)做双耳化。
  • SADIE II 为真人受试者测量: 2818 个方向, 仰角 ±90°, 测距 1.2 m。
  • 房间混响 IR 是未归一化的原始响应 —— 所以混响 convolver 必须带 gain = 0.126 标定 (见下文"中低频沙沙声的根因")。

实测(2026-09-19)

指标 数据
响度归一化 输入差 20.00 dB → 输出端差 0.00 dB(推子 6.28 / +13.72)
HRTF 双耳化 出口左右相关系数 0.679(直通 = 1.000)
混响 脉冲尾巴拖到 1.2 s(湿量 0.3)
总音量 出口电平 21.5 → 16.2 dBFS(+5.3 dB)

试听

pw-play --target collaplex_hrtf_in 某个.wav    # 只走 HRTF 核心(单曲)

全系统:默认输出就是 collaplex_vsink,Chrome/播放器/游戏直接出声即走整条链。

文件

路径 作用
dsp/loudness_norm.py 响度归一化 DSP(pipe 插件载荷, 左右各一进程)
dsp/状态.py 读共享内存看实时推子/电平
bin/collaplex-loudness-norm pipe 插件入口模板(必须是 ASCII 路径)
pipewire/10-collaplex-vsink.conf 虚拟声卡 + 响度归一化
pipewire/20-collaplex-hrtf.conf HRTF 核心 + 混响 + EQ + 总音量
hrir/ reverb/ HRIR(12 个)与房间 IR
脚本/安装.sh 渲染 + 部署 + 重置 DSP + 自检
S
Description
Collaplex 音效 第二代: 全局音频的响度归一化 + 双耳 HRTF(SADIE II / H4) + 32 段 EQ + 房间混响, 带 62Hz 推送控制面板
Readme 6.9 MiB
Languages
Python 80.5%
HTML 12.6%
Shell 6.9%