- 离线渲染.py: pan上混5.1 → sofalizer(H4 SOFA) → 房间IR(afir) → loudnorm → 96k FLAC
视频自动取音轨; 已是5.1的片源不重上混; 实测约 4.4 倍速
两道锁: (1)渲染锁 —— 一轮任务期间拒绝新任务, 不排队堆积
(2)文件就绪检查 —— 相隔1.2s两次stat大小/时间不变且ffprobe能读, 未落盘的跳过
三个入口: 命令行 / 面板「离线渲染」区(拖拽或贴路径, 实时进度+日志+取消) / 右键「打开方式」
- 音频守护.py + config/组件配置单.json: 30s 核对 DSP进程数与槽位新鲜度/链输出连线/
默认设备/虚拟声卡优先级/中间级音量/节点份数/面板/物理设备静音, 扫到就修
(能温和就温和, 链路级才重建), 同类问题 90s 冷却, 连续 3 次修不好转只报警
- 组件配置单.md: 链路拓扑 + 组件表 + 六个必须 + 自愈表
- 面板: 控制滑块从图表卡片独立成「控制」卡片; 数字/模拟输出音量条拉长到各占一半
(合计 = 虚拟声卡开关卡片总高); 新增离线渲染区
- 修: DSP 孤儿抢共享槽位导致重建后无声(claim_slot 抢不到槽位即退出 + 重建前收旧 DSP)
- 修: 打包会把正在用的 PipeWire 配置移开(prune_duplicate_confs 只在自己写到生效目录时清理)
- 修: 离线渲染的 SOFA/混响IR 改为多路径探测(开发目录与 deb 包内布局不同)
- .gitignore: 保留 reverb/ 房间IR(离线渲染要卷积的素材)
30 KiB
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/。
★★ 同名配置只留一份 —— 两份并存 = 没声音(2026-09-13 实测) PipeWire 对
conf.d/*.conf不做"用户级覆盖系统级",两份都加载。装完 deb 之后, 若~/.config/pipewire/pipewire.conf.d/里还有一份开发时生成的同名配置, 就会建出两套同名节点(实测 6 个节点各 2 份)—— 音频进哪一份由调度决定, 常常进了没有下游消费者的那一份 → 整条链静默。 同一时刻还有 4 个归一化 DSP 进程抢同一对槽位锁、两条链都往同一个物理设备推流。 自检:python3 音频状态.py dup(空 = 正常)/空间音频 诊断。 修法:把另一处同名文件改名.disabled-dup后重启 pipewire。本机定用户级为唯一真源, 系统级那两份已改名/usr/share/pipewire/pipewire.conf.d/{90-cinema-spatial,91-clock}.conf.disabled-dup。
生效: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; ★ 它不会覆盖系统级那份,两份会被同时加载 → 生成器写完后会自动把别处的同名 conf 改名.disabled-dup(见生成配置.py的prune_duplicate_confs())。 - 三者均为生成期常量,修改后必须重建链路。
- 实测结论:开启混响时 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 的无效基数)。
- 输入增益:链的输入电平,同时是削波余量旋钮,修改立即生效、无需重建。
注意:键盘媒体键调节的正是这一级(虚拟声卡是应用默认输出)。
★ 它与「响度归一化」是同一段链上的两个旋钮:你压小输入,DSP 就把增益补回来,
净效果趋近不变;补到
MAX_BOOST = +20 dB上限(面板推子"顶格")之后再压小,才会真的变小声。 要更大声请调「扬声器电平」,不要去压输入增益。 - 统一响度音量(「响度统一」那一行的横滑块):与「扬声器电平」是同一个量,
走同一个端点
/api/speaker。 ★ 2026-09-13 修:它原本走/api/loudness{value}直写内部target,而竖滑块走/api/speaker按硬件基数换算后也写同一个target→ 两个滑块互相覆盖 (现场复现:拖扬声器到 0.60,target 存成 0.5354,横滑块立刻显示 0.5354 看着像"跳")。 现在两根滑块同源、同量纲(0~1.5),显示的数字永远一致。 - 响度统一:按硬件增益基数补偿,使两条输出通道响度一致。
★ 物理音量只有
/api/speaker一个写入入口(/api/loudness已不再接受value)。
5.7 电平表的口径与"顶满"
- 「原始电平」= 本块输入峰值;「处理后电平」= DSP 上报的输出峰值(共享区
+28字段,真实值, 受PEAK_CEIL = 0.985限制)。数字上的"峰值"取最近 1.5 秒,峰值线另按 12 dB/s 回落。 - ★ 2026-09-13 修:旧版"处理后" = 块能量开方(RMS) + 增益(快慢时标混用)→ 瞬态时会算出 >0 dBFS 的 不可能值,看着"顶满";旧版数字取整个 12 秒窗口最大值 → 一次冲顶会"顶"很久。
- ★ 上混图 6 路相加做了能量归一(每路 1/√6 = 0.4082,净增益 −1.8 dB)。改动在
生成配置.py(改 conf 无效,重建会覆盖)。改前每路 =1 → 净增益 +6 dB,正好把 DSP 的峰值保护吃掉 → 起播瞬间顶穿满刻度(实测 +7.83 dBFS)。 - ★ 起播淡入:长静音(>24 块≈1s)后重新起播的前 4 块(≈170ms)线性爬升,压掉卷积器冷启动过冲。
参数
LN_FADE_BLOCKS/LN_FADE_GAP_BLOCKS。
5.8 重建之后静音?先数 DSP 进程
正常情况 DSP 进程必须正好 2 个(左右声道各一个 响度归一化/loudness_norm.py):
ps -eo pid,etimes,cmd | grep -E "[l]oudness_norm"
多于 2 个 = 有孤儿在抢 /dev/shm/collaplex-loudness 的两个槽位 → pipe 插件的
同步管道卡死 → 整链静音(链路看着全对:节点在、连线在、音量正常)。
★ 2026-09-13 修(老板报"没声音了"):
claim_slot()原来在两个槽都被占时return 0(假装抢到)→ 第 3、4 个实例 一起写槽0,数据被劈开。现改为打印一行并自我退出。- CLI 新增
kill_old_dsp():所有重启 PipeWire 的地方统一走restart_pipewire(), 先收掉旧 DSP 再重启。因为 DSP 的父进程是 pipewire 主进程,重建时pdeathsig不触发,不主动收就会每次攒一对。
验证:连续重建 3 次,每次都打印"清掉 2 个旧 DSP 实例",进程数恒为 2 ✓
5.9 默认设备为什么不会被 HDMI 抢走
虚拟声卡(cinema_spatial_up_sink / cinema_spatial_sink)带 priority.session = 2000,
高于所有物理输出(本机 HDMI 1196 / USB 1108 / 板载 1009)—— 见 SINK_PRIO。
★ 2026-09-13 修:原来虚拟声卡没有这个属性(None),所以每次 WirePlumber
"重新选择默认节点"(显示器唤醒、分辨率变化、设备插拔、服务重启)都被 HDMI 抢走;
从 GNOME 手动选回来,下一个事件又抢走("点都点不回来")。
验证:wpctl set-default <HDMI> 后 systemctl --user restart wireplumber,
默认应自动回到 cinema_spatial_up_sink。
想改用 HDMI 输出时手动 wpctl set-default <HDMI> 即可(但下一次重选会回到虚拟声卡)。
5.10 菜单音量桥(系统音量键 → 扬声器电平)
系统音量键 / GNOME 顶栏滑块 / 系统设置里的"输出音量"改的都是 @DEFAULT_AUDIO_SINK@,
也就是虚拟声卡音量 = 链的输入增益 —— 那一级会被响度归一化自动补回来,按键几乎没效果,
压太低还会把 DSP 顶到 +20 dB 上限。所以面板内置一个音量桥(随面板启动,200ms 轮询):
- 把虚拟声卡的改动读走,按比例折算到物理输出(即「扬声器电平」,你听到的音量)
- 再把虚拟声卡复位到参考值(= 「输入增益」滑块的值,存
~/.local/state/cinema-spatial/volume) - 静音键 → 物理输出归 0 并记住原值;解除 → 恢复
实测:按音量+ 物理 0.24→0.26→0.27,按− 回落,静音→0.000、解除恢复, 虚拟声卡始终稳在参考值(削波余量不被吃)。
⚠ 桥随面板运行(面板关掉,音量键就恢复成"改输入增益"的老行为)。 面板从菜单「Collaplex 音效」打开:http://127.0.0.1:8788/ ⚠ 桥只能有一份(就是面板里这个)—— 不要另外再跑一个 CLI 版,两份会互相抢物理音量。
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 |
响度统一 / 归一化目标(target_db)★ 不再接受 value:物理音量统一走 /api/speaker |
/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 |
离线渲染.py |
离线批量渲染:音频/视频 → 空间音频 FLAC(含渲染锁 + 文件就绪检查) |
离线渲染-打开.sh |
文件管理器右键「打开方式」入口(包内装为 /usr/bin/collaplex-render) |
音频守护.py |
组件看门狗:30 秒一轮核对期望状态并自愈 |
组件配置单.md / config/组件配置单.json |
链路组件的期望状态(人读版 / 机器可读版) |
桌面项/ |
包内版桌面项模板(打包时原样装进 /usr/share/applications/) |
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.3.0
- 离线渲染(
离线渲染.py):音频 / 视频 → 空间音频 FLAC(96 kHz 双耳)。链路与实时虚拟声卡同参数:pan上混 5.1 →sofalizer(同一个 H4 SOFA)→ 房间 IR 卷积(afir)→loudnorm。视频自动取音轨;已是 5.1 的片源不重上混- 两道锁:渲染锁(一轮任务期间不接受新任务,直接拒绝而非排队堆积);文件就绪检查(相隔 1.2 秒两次 stat 的大小/时间不变 + ffprobe 能读出音轨,防止把还在下载 / 写入的文件扔进渲染)
- 三个入口:命令行 / 面板「离线渲染」区(拖拽或贴路径,实时进度条 + 日志 + 取消)/ 文件管理器右键「打开方式」
- 实测:8 秒立体声 1.8 s 完成(≈4.4 倍速)
- 组件看门狗(
音频守护.py+config/组件配置单.json):30 秒一轮核对整条链的期望状态(DSP 进程数与槽位新鲜度 / 连线 / 默认设备 / 虚拟声卡优先级 / 中间级音量 / 节点份数 / 面板 / 物理设备静音),扫到问题就修 —— 能温和就温和(改音量、切默认、重连),只有链路级问题才重建;同类问题 90 秒冷却,连续 3 次修不好转只报警 - 面板:控制滑块从图表卡片里独立成「控制」卡片(图表归图表、控件归控件);数字 / 模拟输出音量条拉长到各占一半,合计等于虚拟声卡开关卡片的总高
- 修复:DSP 孤儿抢共享槽位导致重建后无声(
claim_slot()抢不到槽位即自我了断 + 重建前收掉旧 DSP) - 修复:打包动作会把正在用的 PipeWire 配置移开(
prune_duplicate_confs()在 OUT 落到"非生效目录"时不再清理,另加CINEMA_SPATIAL_KEEP_CONFS=1双保险)
1.2.0
- 全系统响度归一化(filter-chain
pipe插件挂自研 DSP):各软件推进来的响度拉平到同一目标,目标可调(−40 ~ −6 dBFS),6 dB/s 限速 + 峰值保护,左右声道共享增益不漂声像 - 控制台拆 4 路电平(原始 L/R + 处理后 L/R),新增增益历史曲线
- 架构:归一化做成独立 filter-chain 实例串在上混图之前(塞进 40+ 节点大图会收到恒定 −43 dBFS 空数据、音频绕过)
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 许可发布。