Collaplex Cinema Spatial
耳机上的电影院空间音频 —— 用真人测量的头相关传输函数(HRTF)把多声道音轨渲染成双耳信号, 在普通耳机上还原电影院 C 位附近的环绕包围感;立体声源会先经矩阵上混再渲染,因此浏览器与音乐播放器 同样具备空间感。
- 版本:1.1.1 · 许可:MIT · 平台:Linux / PipeWire
- 全链路 96 kHz / 24 bit,纯脚本与数据,无编译,
Architecture: all - 开源地址:http://8.136.202.225:3000/edgevoid/cinema-spatial
1. 概述
系统在 PipeWire 中建立两个虚拟声卡:程序把音频送入虚拟声卡,虚拟声卡内部完成空间渲染, 再输出到真实回放设备。虚拟声卡是系统级默认输出,因此对任何播放器均生效。
| 虚拟声卡 | 用途 |
|---|---|
cinema_spatial_up_sink |
立体声上混版(默认,推荐)—— 立体声源经矩阵上混成 5.1 再渲染 |
cinema_spatial_sink |
5.1 直通版 —— 多声道片源直接渲染,不做上混 |
输出端为 cinema_spatial_up_out / cinema_spatial_out,通过 node.target 指向选定的物理设备。
多声道源 ──┐
├─> [矩阵上混 5.1] ─> 6 路 ─> [逐路 HRTF 卷积] ─> 双耳混音 ─> [房间混响] ─> 物理输出
立体声源 ──┘ (SADIE-II H4 真人 HRIR)
1.1 基本原理
- HRTF(头相关传输函数):同一方向的声音到达两耳时会被人头与耳廓滤波,产生耳间时间差、 强度差与频谱染色,听觉系统据此判断方位。将这些滤波记为脉冲响应(IR),与声道信号卷积, 即可在耳机上重建方位线索。
- 虚拟声卡:借助 PipeWire 的
filter-chain模块建立Audio/Sink,渲染全部在声卡内部完成, 对上层播放器透明。
2. 核心技术
2.1 镜像法房间声学追踪(image-source)
0–80 ms 早期反射按镜像法精确求解:声源在房间三维空间周期展开,每条镜像源对应一条真实的 "声源 → 若干次反射 → 听者"路径。当前房间模型下共 8473 条镜面路径,逐条计算:
- 传播时延
- 按距离的球面扩散(1/r)
- 逐频带吸声系数(碰墙次数累乘)
- 空气吸收
- 左右耳分别求距离,得到耳间时间差(ITD)
2.2 Sabine 统计尾音
精确求解至 1.4 s 需约 240 万个镜像源(阶数 66),计算上不可行。因此 80 ms 之后改为统计模型: 按逐频带 Sabine RT60 生成指数衰减噪声尾音,两耳部分相干系数 0.4,并在 80 ms 交叉点做能量对齐。
2.3 真实房间脉冲响应卷积
早期反射与统计尾音混合后输出 96 kHz / 32 bit 双耳房间 IR(reverb/房间混响IR-96k.wav,
时长 2.00 s),在滤波链中以卷积级接入。
- 归一化方式:能量归一(Σir² = 1)。此方式下湿量语义清晰:湿量 G 时,湿路电平 = 干路 + 20log₁₀G。
- 湿量是生成期常量,修改后需重建链路。
与算法混响的区别:本项目不使用 Freeverb / Schroeder 反馈网络一类算法混响, 而是"先求解声传播、再做卷积"的房间物理模型。
2.4 HRTF 双耳化
采用 SADIE II 数据集真人受试者 H4 的 96K / 24bit / 512tap 版本:
2818 个测量方向,仰角覆盖 ±90°,测距 1.2 m。每条 HRIR 截取 4800 tap @ 96 kHz(约 50 ms)。
2.5 房间与听音位模型
| 参数 | 取值 |
|---|---|
| 房间尺寸 | 20 × 12 × 7 m(V = 1680 m³,S = 928 m²) |
| 银幕 | 10.5 × 5.2 m,底高 1.0 m,穿孔幕吸声系数 0.45 |
| 吸声系数 | 墙板 0.80 / 吊顶 0.85 / 地毯 0.45 |
| 听音位 | 中轴距银幕 8 m,耳高 1.2 m,双耳间距 0.175 m |
| 声速 | 343 m/s |
全部参数存于 声学追踪引擎/config/房间.json,可由配置改写。
3. 安装
3.1 deb 包
sudo dpkg -i collaplex-cinema-spatial_1.1.1_all.deb
# 依赖: pipewire libmysofa1 mpv
安装内容:可执行命令写入 /usr/bin/,数据与 Web 控制台写入 /usr/share/cinema-spatial/,
配置写入 /usr/share/pipewire/pipewire.conf.d/。用户级同名配置优先级高于系统级,
因此放在 ~/.config/pipewire/pipewire.conf.d/ 的同名文件会覆盖系统配置。
生效:systemctl --user restart pipewire pipewire-pulse(或注销重新登录)。
3.2 从源码
# 1. 获取 HRTF 数据集(不随仓库分发)
mkdir -p sofa && cd sofa
curl -L -O https://zenodo.org/records/12092466/files/H4_HRIR_SOFA.zip
unzip H4_HRIR_SOFA.zip && cd ..
# 2. 从 SOFA 提取脉冲响应(12 个 = 6 方向 × 左右耳)
bash 换HRTF.sh "$PWD/sofa/H4_HRIR_SOFA/H4_HRIR_SOFA/H4_96K_24bit_512tap_FIR_SOFA.sofa" H4-96k -16
ln -sfn H4-96k hrir/current
# 3. 生成 PipeWire 配置并生效
python3 生成配置.py
systemctl --user restart pipewire pipewire-pulse
4. 使用
4.1 命令行
| 命令 | 作用 |
|---|---|
空间音频 开 |
全局切换到立体声上混版(推荐) |
空间音频 开5.1 |
全局切换到 5.1 直通版 |
空间音频 关 |
切回物理设备,链路停用 |
空间音频 状态 |
查看两个虚拟声卡与当前默认输出 |
空间音频 诊断 |
输出目标 / profile / route 硬件增益基数 / 音量账 / 连线状态 |
空间音频 数字 | 模拟 |
切换输出的数字(S/PDIF)或模拟通道 |
空间音频 设备 [关键词] |
列出或切换输出设备 |
空间音频 混响 [开|关|湿量] |
房间混响开关与湿量(不带参数表示翻转) |
空间音频 参数 [hrtf|taps|rate] |
查看/修改采样参数(采样率、HRTF 模型、HRIR 长度) |
空间音频 单声道 [自动|开|关] |
单声道输出合并(见 §6.4) |
空间音频 重建 |
按当前参数与配置重新生成链路 |
mpv-影院 影片 |
仅对 mpv 生效的 HRTF 播放包装,不改变全局设置(适合 A/B 对比) |
也可以在桌面环境的"声音设置"中直接选择 电影院空间音频。
输出通道的选择:实测耳机的数字输出(S/PDIF)音质明显优于模拟输出(该通道以 96 kHz / 24 bit 运行), 推荐使用。
空间音频 数字会自动完成"设置重建目标 + 保留 profile + 校验连线", 比在 GNOME 面板上点击更可靠 —— 后者会改变 profile,使配置中的目标节点名失效。
4.2 Web 控制台
见 §7。菜单中也有「Collaplex 音效」快捷方式。
5. 参数与配置
5.1 状态文件
| 文件 | 内容 |
|---|---|
~/.local/state/cinema-spatial/volume |
音量持久化值 |
~/.local/state/cinema-spatial/params.json |
采样率 / HRTF 模型 / HRIR 长度 |
~/.local/state/cinema-spatial/reverb.json |
混响开关与湿量 |
~/.local/state/cinema-spatial/mono.json |
单声道模式({} 表示自动) |
~/.local/state/cinema-spatial/loudness.json |
响度统一开关与目标值 |
5.2 采样参数
- 面板「采样,各参数调整选项」中的三个下拉可直接修改:采样率 / HRTF 模型 / HRIR 长度, 改完自动重建(约 10 s)。
- 命令:
空间音频 参数(查看现值与可选项)、空间音频 参数 hrtf H4-48k、参数 taps 1024、参数 rate 48000。 - IR 目录选择优先级:
hrir/taps-<N>> 模型名 >hrir/current软链;模型与长度截断二者互斥。 - 长度截断:
work/裁HRIR.py <N>生成hrir/taps-<N>/(末尾 3 ms 淡出,可选 512 / 1024 / 2048)。 - 采样率写入用户级
~/.config/pipewire/pipewire.conf.d/91-clock.conf,同名覆盖系统配置,无需 sudo。 - 三者均为生成期常量,修改后必须重建链路。
- 实测结论:开启混响时 HRIR 长度不是主要 CPU 成本 —— 2 秒房间 IR 的等效长度是 HRIR 的 40 倍, 截断 HRIR 既省不下多少 CPU,又会损失 HRTF 尾部使声音变干。因此默认保持 H4-96k / 4800 tap。
5.3 混响
- 命令:
空间音频 混响 [开|关|湿量];面板提供开关与湿量滑块。 - 关闭时整个混响级不生成,不占用 CPU。
- 湿量为生成期常量,修改后需重建。
5.4 单声道输出
单声道蓝牙音响 / 耳机仅提供 1 个 MONO 端口,而播放节点默认声明 2 声道 [ FL FR ],
声道数不匹配会导致链接建立失败、设备掉线。系统提供三态开关(默认自动):
| 取值 | 行为 |
|---|---|
| 自动 | 读取目标设备的 audio.channels,为 1 时自动合并 |
| 开 | 强制合并 |
| 关 | 强制不合并 |
合并实现:链路中增加一级 monoOut 混音器,左右声道各乘 −6 dB 后相加(避免相关信号叠加溢出 6 dB),
图输出改为单端口,播放节点改为 audio.channels = 1 audio.position = [ MONO ]。
5.5 输出设备
音频状态.py sinks列出全部物理输出(USB → PCI → 显卡 Pro/HDMI),并标记当前目标。空间音频 设备 [关键词]按node.name或描述匹配,重写配置中的node.target。- 设备切换优先采用运行期重连:播放节点本质是 PipeWire stream,其输出端口可在运行期改连, 因此切换设备无需重建链路、无需重启 PipeWire,蓝牙设备不会被中断。 仅当新设备声道数与链路当前输出不一致时(如单声道设备)才回落为"重建 + 等待目标就绪"。
- 自动探测会跳过显卡
pro-output/ HDMI 一类在默认状态下不发声的输出。
5.6 音量与响度
- 扬声器电平:面板竖滑块 → 物理输出音量(即实际听到的音量)。写入为闭环自校正 (按实测比例计算后写入、回读、残差修正,最多 3 轮),因为硬件增益基数因设备而异, 不可写死(本机实测数字通道 1.003;部分设备会返回 0.001 的无效基数)。
- 输入增益:链的输入电平,同时是削波余量旋钮,修改立即生效、无需重建。 注意:键盘媒体键调节的正是这一级(虚拟声卡是应用默认输出)。
- 响度统一:按硬件增益基数补偿,使两条输出通道响度一致。
6. 信号链与架构
6.1 时钟
主时钟统一为 96 kHz(default.clock.rate = 96000),以保证 96 kHz HRIR 不被降采样。
6.2 增益记账
矩阵上混产生的 6 路信号在混音器中相加,整链净增益约 +10.5 dB。因此:
- 增益必须按整链校准,只按单路 IR 峰值校准会在真实内容上削波。
- 链路上每一级音量(虚拟声卡、滤波链输出、物理设备、耳机自身)串联共享动态余量, 修改任意一级都必须重新记账。
- 校准素材必须接近满刻度,且峰值因子需与真实内容相当(影视内容的瞬态峰值可比稳态测试信号高 10 dB 以上)。
6.3 约束
node.passive = true必须与node.target同时设置,否则输出端不连接任何设备。- 混响路径:
mixL/R → 房间 IR 卷积(revL/revR) → 湿混音(wetL/wetR) → 输出;关闭时不生成该级。 - 卷积使用
type=time(时域,保真);默认的type=freq会损失高频。 convolver按当前时钟率重采样脉冲响应,时钟未提升到 96 kHz 时换用高采样率 IR 无意义。
7. Web 控制台(Collaplex 音效 · :8788)
页面布局对应设计稿 ~/桌面/collaplex web设计稿.drawio:顶栏 / 原始电平与处理后电平 /
三个信号开关 / 音量滑块 / 数字·模拟输出选择 / 采样参数 / 响度统一 / 5.1 直通·混响·单声道·设备选择 /
状态信息 / 核心技术说明。
bash web/启动.sh # 启动(默认端口 8788,重复执行不会起第二个)
python3 web/webui.py --port 8788 --bind 127.0.0.1 # 或直接运行后端
访问 http://127.0.0.1:8788/。后端零依赖(标准库),接口列表:
| 接口 | 作用 |
|---|---|
/api/state |
全量状态(设备、链路、参数、音量、混响、单声道) |
/api/meters |
两路电平(原始 / 处理后) |
/api/mode |
开 / 开 5.1 / 关 |
/api/volume |
输入增益 |
/api/speaker |
扬声器电平(闭环自校正) |
/api/route |
数字 / 模拟 |
/api/switch |
三个信号开关 |
/api/loudness |
响度统一 |
/api/device |
输出设备 |
/api/reverb |
混响开关与湿量 |
/api/mono |
单声道模式 |
/api/fix |
检测并修复(设备名失效时重建) |
7.1 电平表的采集口径
| 表 | 采集源 |
|---|---|
| 原始电平 | cinema_spatial_up_sink:monitor_FL/FR —— 进入链路的音频 |
| 处理后电平 | cinema_spatial_up_out:output_FL/FR —— 链路输出的处理后信号 |
采集进程以 --target 0 启动(禁止自动连线)并使用唯一节点名,
再由程序按端口 id 显式连线。如此可确保:
- 采集目标始终是上述信号本身,绝不回退到麦克风;
- 两个采集进程同名导致的端口寻址混淆不会发生;
- 目标信号不在图中时保持空表,不进行任何降级采集。
电平表采用每表自动量程:按最近 2 秒的最大峰值向上取整到 6 dB 作为顶线,轴标签标注真实 dBFS 值。
8. 已知问题与工程结论
以下均为实测结论,供后续维护参考。
8.1 PipeWire 模块与配置
- 滤波链没有运行时热加载。
pw-cli load-module在本机对任意模块(含libpipewire-module-null-sink) 均返回Error: "Could not load module",多种写法与全路径均已尝试。 → 修改滤波链配置必须重启 PipeWire。 - 重启 PipeWire 会连带重启 WirePlumber(后者是其客户端,退出后由 systemd 拉起), 因此蓝牙 / USB 设备需要数秒重新出现。判目标失效前必须轮询等待(当前实现最多等待 20 秒), 不能在固定延时后直接判定,否则会把输出目标错误地切换到其它设备。
pw-link -l的输出格式为:端口名独占一行,其后以|-> <对端>表示去向,|<- <对端>表示来源。 解析时必须按行配对。- 同名节点不可用名字寻址。多个
pw-record进程的node.name相同、application.process.id为None,用pw-record:input_FL之类的名字会连到其它进程的端口上。涉及多实例时必须按端口 id 寻址。 - 判定节点是否存在不能用
pw-cli info(对不存在的名字同样返回成功),应使用pw-dump按node.name精确比对。 - 重建不要连续触发:短时间内反复重启 PipeWire 会使 WirePlumber 崩溃(GLib 断言 → core-dump),
表现为监听端口无法建立新连接。当前 CLI 在重建后会检查
wireplumber是否存活并自动拉起。
8.2 卷积素材与归一化
- 任何卷积素材(房间 IR / HRIR)都必须按能量归一,不能按峰值归一。 以房间混响为例:2 秒密集尾音按峰值归一时,卷积的能量增益可达 +11 ~ +13 dB, 即使湿量仅 0.05(−26 dB)仍会把整链推高约 11 dB,导致削波。改为能量归一后, 实测整链净增益由 +11 dB 降至 +0.4 dB,0 dBFS 输入时输出为 −1.17 dBFS(余量 1.17 dB)。
- 不同 SOFA 数据集的电平差异极大(同一数据集内,假头 D1 峰值 −11.6 dB、真人 H4 −3.1 dB), 更换模型必须重新量测峰值,目标压至约 −12 dB。
sofalizer按 SOFA 自身采样率输出:SOFA 为 44.1 kHz 时输出会降到 44.1 kHz。 需在链尾aresample拉回,或使用原生高采样率数据集。
8.3 设备与路由
- USB 音频设备重启后 profile 会变化(
iec958-stereo↔analog-stereo), 设备node.name的后缀随之改变,配置中的node.target失效,WirePlumber 会回落到其它输出 (实测曾落到 HDMI,表现为无声或声音从显示器输出)。空间音频 开自带自检与重建。 - 虚拟声卡音量不会自动保留:滤波链每次重建都是全新节点,音量回到默认值,会导致整链削波。
当前实现于
capture.props写入默认音量,并在每次切换命令中补写。 - 链路输出是 stream,当目标设备消失时 WirePlumber 会按系统默认将其改连到其它设备
(实测曾被改连到显卡
pro-output-3)。重建会按配置中的目标拉回。 如需彻底固定,可对播放节点设置node.autoconnect = false,仅由本工具显式连线。 - 运行时构造的节点存活期有限(例如通过
pw-cli create-node建立的 null sink 无法在重启后存活), 因此不可作为长期目标设备。设备失效时应回退到自动探测。
8.4 其它
- ffmpeg 滤镜表达式中的
,与|必须用单引号包裹,否则会被当作分隔符解析。 adelay+join合成多声道素材不可靠(曾静默产出 0.25 秒单声道文件),建议改用aevalsrc。- 测试素材必须匹配真实内容的峰值因子:稳态粉噪无法暴露影视瞬态的削波风险,校准应使用真实片源 或至少混入冲击型素材。
9. 实测数据
9.1 房间混响逐频带 RT60
| 频段 | Sabine 理论 | 早期反射实测 | 最终 IR 实测 |
|---|---|---|---|
| 125 Hz | 1.363 s | 1.542 s | 1.338 s |
| 250 Hz | 0.722 s | 0.779 s | 0.749 s |
| 500 Hz | 0.451 s | 0.639 s | 0.524 s |
| 1 kHz | 0.364 s | 0.871 s | 0.460 s |
| 2 kHz | 0.331 s | 1.153 s | 0.427 s |
| 4 kHz | 0.332 s | 0.809 s | 0.362 s |
测量工具:work/测链路冲激响应.py(馈入脉冲 → 录制 monitor → 计算包络与 T60)。
运行中的两个 filter-chain 模块内 revL / revR / wetL / wetR 均已加载(pw-cli info 实测),
处理后峰值 −22.2 dBFS(未削波),设备噪声地板 −56 dBFS。
9.2 削波问题定位(能量归一前后)
| 状态 | 输入 | 输出 | 链净增益 |
|---|---|---|---|
| 关闭混响 | −6.64 dBFS | −6.4 dBFS | +0.3 dB |
| 混响 0.05(峰值归一,修复前) | −6.64 dBFS | — | 约 +11 dB |
| 混响 0.05(能量归一,修复后) | −6.64 dBFS | −6.2 dBFS | +0.4 dB |
| 0 dBFS 输入(修复后) | −1.04 dBFS | −1.17 dBFS | 余量 1.17 dB |
9.3 电平表显示验证
| 表 | 墨点数 | 墨点重心 y | 结论 |
|---|---|---|---|
| 原始 | 45353 → 44985 | 41.4 → 41.4 | 变化正常 |
| 处理后 | 34950 → 36016 | 47.4 → 46.0 | 变化正常 |
10. 文件说明
| 文件 | 作用 |
|---|---|
空间音频 |
主命令(开关、诊断、参数、设备、混响、单声道) |
音频状态.py |
状态后端:设备枚举、profile、route、硬件增益基数、连线校验 |
生成配置.py |
生成 PipeWire filter-chain 配置 |
mpv-影院 |
mpv 专用的 HRTF 播放包装 |
换HRTF.sh |
从 SOFA 数据集提取脉冲响应 |
打包deb.sh |
打包 collaplex-cinema-spatial |
发布release.sh |
构建并发布到 Gitea Release |
web/ |
Collaplex 音效 Web 控制台(webui.py + index.html + 启动.sh,零依赖) |
reverb/ |
房间脉冲响应(由 声学追踪引擎/ 生成) |
hrir/ |
HRIR 数据与 current 软链 |
声学追踪引擎/ |
房间声学追踪与 IR 生成器(镜像法 + Sabine 尾音) |
work/ |
开发期验证工具(含 裁HRIR.py、测链路冲激响应.py) |
验收测试.sh |
安装后自动验证 |
demo.sh / 响度校准.sh / 方向测试.sh / 对比分析.py / 排查.sh |
开发期工具 |
11. 版本记录
1.1.1
- README 重写为正式文档,并同步至当前功能状态
1.1.0
- 新增单声道输出支持(三态:自动 / 开 / 关),解决单声道设备因声道数不匹配导致的掉线
- 新增运行期重连:切换输出设备不再重启 PipeWire,蓝牙设备不再被中断
- 修复"重启后目标失效":改为轮询等待目标设备就绪(最多 20 秒),并在自动探测中跳过显卡 / HDMI 输出
- 修复削波:房间 IR 改为能量归一(此前按峰值归一致使整链 +11 dB)
- 电平表改为显式连线采集链路输出,杜绝回退到麦克风
- 面板新增"核心技术"说明区
- 打包修复:
control版本号跟随构建版本(此前恒为 1.0.0,导致 dpkg 无法升级); 补入音频状态.py与web/控制台
1.0.x
- 首个公开发布:双虚拟声卡、SADIE-II HRTF 渲染、deb 打包
- 修复"响度怪":详见
响度怪-根因与修复.md;新增空间音频 诊断, 音量改为持久化,重建时保留 profile 并校验连线
12. 数据来源与许可
HRTF 数据采用 SADIE II
(University of York,Zenodo DOI 10.5281/zenodo.12092466),
使用其中真人受试者 H4 的 96K / 24bit / 512tap 版本。引用方式见数据集页面。
数据集体积较大,不随本仓库分发,请从 Zenodo 自行下载。
本项目以 MIT 许可发布。