Files
edgevoid 4bc381604b v1.3.6: 电平表缩放加"平均+变速" —— 消除量程一跳一跳
老板: "我觉的应该给缩放加点平均和变速算法, 这样就不会出现一下出现一下拉满了"

根因: 之前纯按瞬时极值(最近2秒的 max/min)定量程, 单帧瞬态就能把量程猛地张开/
收缩, 观感"一时全空一时拉满"。

两层平滑(思路同压缩器 attack/release):
① 平均: 峰值/谷值各走 EMA —— 峰值涨得快(.28)落得慢(.08), 谷值反过来
         -> 单帧毛刺不再定调
② 变速: 目标量程走**非对称** EMA —— 张开快(.30, 大声音进来立刻扩不削顶),
        收缩慢(.07, 安静段不急着缩), 避免波形忽大忽小
- 中心走 EMA(.12) 保证"居中"不跳
- 首帧直接就位, 不让开机时慢慢爬
- 显示仍 6dB 步进

模拟实测(安静 -> 一帧 -6dBFS 突发 -> 中等 -> 安静, 帧内跨度 30dB):
最大单帧量程跳变仅 6dB; 突发帧量程 54->60(而不是跳到 90+); 之后 60->66->60 缓慢回落。
2026-09-14 07:59:42 +08:00

35 KiB
Raw Permalink Blame History

Collaplex Cinema Spatial

耳机上的电影院空间音频 —— 用真人测量的头相关传输函数(HRTF)把多声道音轨渲染成双耳信号, 在普通耳机上还原电影院 C 位附近的环绕包围感;立体声源会先经矩阵上混再渲染,因此浏览器与音乐播放器 同样具备空间感。


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

080 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 双耳房间 IRreverb/房间混响IR-96k.wav, 时长 2.00 s),在滤波链中以卷积级接入。

  • 归一化方式:能量归一(Σir² = 1。此方式下湿量语义清晰:湿量 G 时,湿路电平 = 干路 + 20log₁₀G。
  • 湿量是生成期常量,修改后需重建链路。

与算法混响的区别:本项目不使用 Freeverb / Schroeder 反馈网络一类算法混响, 而是"先求解声传播、再做卷积"的房间物理模型。

2.4 HRTF 双耳化

采用 SADIE II 数据集真人受试者 H496K / 24bit / 512tap 版本: 2818 个测量方向,仰角覆盖 ±90°,测距 1.2 m。每条 HRIR 截取 4800 tap @ 96 kHz(约 50 ms)。

2.5 房间与听音位模型

参数 取值
房间尺寸 20 × 12 × 7 mV = 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(见 生成配置.pyprune_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 轮询):

  1. 把虚拟声卡的改动读走,按比例折算到物理输出(即「扬声器电平」,你听到的音量)
  2. 再把虚拟声卡复位到参考值= 「输入增益」滑块的值,存 ~/.local/state/cinema-spatial/volume
  3. 静音键 → 物理输出归 0 并记住原值;解除 → 恢复

实测:按音量+ 物理 0.24→0.26→0.27,按− 回落,静音→0.000、解除恢复, 虚拟声卡始终稳在参考值(削波余量不被吃)。

⚠ 桥随面板运行(面板关掉,音量键就恢复成"改输入增益"的老行为)。 面板从菜单「Collaplex 音效」打开:http://127.0.0.1:8788/ ⚠ 桥只能有一份(就是面板里这个)—— 不要另外再跑一个 CLI 版,两份会互相抢物理音量。


6. 信号链与架构

6.1 时钟

主时钟统一为 96 kHzdefault.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 模块与配置

  1. 滤波链没有运行时热加载pw-cli load-module 在本机对任意模块(含 libpipewire-module-null-sink 均返回 Error: "Could not load module",多种写法与全路径均已尝试。 → 修改滤波链配置必须重启 PipeWire
  2. 重启 PipeWire 会连带重启 WirePlumber(后者是其客户端,退出后由 systemd 拉起), 因此蓝牙 / USB 设备需要数秒重新出现。判目标失效前必须轮询等待(当前实现最多等待 20 秒), 不能在固定延时后直接判定,否则会把输出目标错误地切换到其它设备。
  3. pw-link -l 的输出格式为:端口名独占一行,其后以 |-> <对端> 表示去向,|<- <对端> 表示来源。 解析时必须按行配对。
  4. 同名节点不可用名字寻址。多个 pw-record 进程的 node.name 相同、application.process.idNone,用 pw-record:input_FL 之类的名字会连到其它进程的端口上。涉及多实例时必须按端口 id 寻址。
  5. 判定节点是否存在不能用 pw-cli info(对不存在的名字同样返回成功),应使用 pw-dumpnode.name 精确比对。
  6. 重建不要连续触发:短时间内反复重启 PipeWire 会使 WirePlumber 崩溃(GLib 断言 → core-dump), 表现为监听端口无法建立新连接。当前 CLI 在重建后会检查 wireplumber 是否存活并自动拉起。

8.2 卷积素材与归一化

  1. 任何卷积素材(房间 IR / HRIR)都必须按能量归一,不能按峰值归一。 以房间混响为例:2 秒密集尾音按峰值归一时,卷积的能量增益可达 +11 ~ +13 dB, 即使湿量仅 0.05(−26 dB)仍会把整链推高约 11 dB,导致削波。改为能量归一后, 实测整链净增益由 +11 dB 降至 +0.4 dB0 dBFS 输入时输出为 1.17 dBFS(余量 1.17 dB)。
  2. 不同 SOFA 数据集的电平差异极大(同一数据集内,假头 D1 峰值 −11.6 dB、真人 H4 −3.1 dB), 更换模型必须重新量测峰值,目标压至约 −12 dB。
  3. sofalizer 按 SOFA 自身采样率输出SOFA 为 44.1 kHz 时输出会降到 44.1 kHz。 需在链尾 aresample 拉回,或使用原生高采样率数据集。

8.3 设备与路由

  1. USB 音频设备重启后 profile 会变化iec958-stereoanalog-stereo), 设备 node.name 的后缀随之改变,配置中的 node.target 失效,WirePlumber 会回落到其它输出 (实测曾落到 HDMI,表现为无声或声音从显示器输出)。空间音频 开 自带自检与重建。
  2. 虚拟声卡音量不会自动保留:滤波链每次重建都是全新节点,音量回到默认值,会导致整链削波。 当前实现于 capture.props 写入默认音量,并在每次切换命令中补写。
  3. 链路输出是 stream,当目标设备消失时 WirePlumber 会按系统默认将其改连到其它设备 (实测曾被改连到显卡 pro-output-3)。重建会按配置中的目标拉回。 如需彻底固定,可对播放节点设置 node.autoconnect = false,仅由本工具显式连线。
  4. 运行时构造的节点存活期有限(例如通过 pw-cli create-node 建立的 null sink 无法在重启后存活), 因此不可作为长期目标设备。设备失效时应回退到自动探测。

8.4 其它

  1. ffmpeg 滤镜表达式中的 ,| 必须用单引号包裹,否则会被当作分隔符解析。
  2. adelay + join 合成多声道素材不可靠(曾静默产出 0.25 秒单声道文件),建议改用 aevalsrc
  3. 测试素材必须匹配真实内容的峰值因子:稳态粉噪无法暴露影视瞬态的削波风险,校准应使用真实片源 或至少混入冲击型素材。

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.6

  • 电平表缩放加「平均 + 变速」(老板:"给缩放加点平均和变速算法,这样就不会出现一下出现一下拉满了")—— 纯按瞬时极值定量程,遇到单帧瞬态就猛地张开/收缩,观感一跳一跳(一时全空一时拉满)。现在两层平滑,思路同压缩器的 attack/release
    • ① 平均:峰值/谷值先各走 EMA(峰值涨得快 0.28 / 落得慢 0.08,谷值反过来),不让单帧毛刺定调
    • ② 变速:目标量程再走非对称 EMA —— 张开快(0.30,大声音进来立刻扩,不削顶)收缩慢(0.07,安静段不急着缩,免得波形忽大忽小)
    • 中心同样走 EMA(0.12),保证"波形居中"不跳
    • 首帧直接就位(不让开机时量程慢慢爬)
  • 模拟测试(安静 → 一帧 −6dBFS 突发 → 中等 → 安静,帧内跨度 30dB):最大单帧量程跳变仅 6 dB,突发帧量程 54→60 而非跳到 90+,之后 60→66→60 缓慢回落

1.3.5

  • 两个电平表波形"放大" —— 1.3.4 只把波峰压到中线(确实不再被压住),但波形只占上半区、下半区全空,显得又小又浪费(老板:"你只改了一半,现在确实压低了,波形再放大一点就好看了")。现在量程跟着信号的实际幅度走量程 = 信号跨度 × 1.45(上下各留一半余量 → 波形垂直居中且占约 2/3),跨度下限 18 dB(信号平直时不放大噪声)、上限 72 dB,6 dB 步进不逐帧抖。CDP 实测四档电平(跨度 10~24 dB)波形占画布 42%~60%,上下留白对称
  • 附带修一个自己引入的 bugmn 初值曾取 fallback(-60),把底线拖到 −66 导致波形反而更小(21%);谷值现在只认真实数据

1.3.4

  • 两个电平表(原始 / 处理后)波形改为垂直居中 —— 原来顶线 = 「最近 2 秒峰值 + 3 dB」,于是波峰永远贴在画布最上沿,看着像被上面那行标题压住(老板原话"太靠上波形被盖住了")。现改为 顶线 = 峰值 + SPAN/2,波峰正好落在画布垂直中线,上下各留半屏;顶线允许超过 0 dBFS(那只是绘图范围,多出来的是留白),0 dBFS 单独画一条亮基准线保住刻度参照。CDP 注入三档电平(−6 / −12 / −30 dBFS)实测波峰偏离中线均为 0 px
  • 上一版(1.3.3)误把动态量程加在了「响度归一化」的增益曲线上 —— 老板要的是这两个电平表。归一化那边的动态量程保留(同样是"波峰居中"的需求),两处现在都居中

1.3.3

  • 响度归一化曲线改为动态量程:原来量程固定 ±20 dB,而增益常年偏一边(实测 +4.6 dB 整条压在上半区)。现在按最近 120 个样本的 min/max 定中心与半幅,让曲线的波峰自动落在图表垂直正中;两处细节:① 半幅下限 2 dB(曲线接近平直时别把噪声放大成锯齿),② 中心/半幅都用一阶低通(每帧挪 12%)平滑,避免曲线"呼吸"抖动。左侧量程标签同步改成跟随数据的真实 dB 值(不再固定 ±20/0),0 dB 基准线只在可见范围内才画
  • CDP 实测三个场景(波峰+12/波谷+2、波峰−4/波谷−16、几乎平直+4.6)中值偏离画布正中线均为 0 px

1.3.2

  • 面板补 favicon(内联 SVG:深底 + 五根声波柱)—— 之前没有图标,浏览器标签页显示默认破图标
  • 修复:响度归一化卡片里的曲线被压 —— 卡片高 184px 装不下(标题 22 + 推子 22 + 文字行 20 + 间距 ≈ 70px),而 #ln 只扣了 46px → canvas 溢出、上沿被「目标/判据」那行文字盖住。改:卡片 210px、#ln 扣 74px、网格 430→456(把这 26px 补给上面两个电平表,免得它们被压矮)。CDP 量尺实测无重叠#ln 126px
  • 纠正一个错误假设物理输出音量是一个「档位」,不是"越大越响" —— 响度由 DSP 归一化保证(实测 0.35 就很好)。1.3.1 曾错误地把它"对齐"到虚拟声卡(1.0),结果进削波保护、动态被压扁:面板波形顶满而听感反而更小(老 bug 复现)。已撤掉三处相关逻辑,看门狗现在只兜「静音 / 近乎归零(<0.02)」,绝不改正常档位

1.3.1

  • 修复:断电隔夜重启后"又没声音" —— 物理输出的音量会从 WirePlumber 存档恢复成旧值(实测 0.35,而虚拟声卡是 1.0),而菜单音量桥只"监视虚拟声卡变化"、虚拟没变就永远不动物理 → 声音凭空小 9 dB。两处兜底:① 桥启动时把物理输出对齐一次;② 看门狗新增第 9 项检查「物理输出音量 ≠ 虚拟声卡」并自动纠回(实测 22 秒纠回)
  • 修复:隔夜无人看守 —— 看门狗不做开机自启(既有约定),重启后链路就没有守护。现在 web/启动.sh 起来时顺带把看门狗拉起(已在跑则不重复起);看门狗用 setsid 脱离会话,挂在 systemd --user 下,Hermes 关了它照样跑

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 无法升级); 补入 音频状态.pyweb/ 控制台

1.0.x

  • 首个公开发布:双虚拟声卡、SADIE-II HRTF 渲染、deb 打包
  • 修复"响度怪":详见 响度怪-根因与修复.md;新增 空间音频 诊断, 音量改为持久化,重建时保留 profile 并校验连线

12. 数据来源与许可

HRTF 数据采用 SADIE II University of YorkZenodo DOI 10.5281/zenodo.12092466), 使用其中真人受试者 H496K / 24bit / 512tap 版本。引用方式见数据集页面。

数据集体积较大,不随本仓库分发,请从 Zenodo 自行下载。

本项目以 MIT 许可发布。