v1.1.1: README 重写为正式文档(概述/核心技术/信号链/参数/已知问题/实测数据/版本记录); 打包版本跟随 VER

This commit is contained in:
edgevoid
2026-09-13 16:43:36 +08:00
parent a7d0e88bec
commit f1a1199ae3
3 changed files with 346 additions and 326 deletions
+344 -324
View File
@@ -1,307 +1,349 @@
# Cinema Spatial —— 耳机上的电影院空间音频
# Collaplex Cinema Spatial
把多声道音轨(5.1/7.1)用 HRTF 渲染成双耳,耳机上得到接近电影院 C 位的环绕包围感。
立体声源会先经矩阵上混(杜比 Pro Logic 原理)再渲染,所以浏览器音乐播放器同样有空间感。
**耳机上的电影院空间音频** —— 用真人测量的头相关传输函数(HRTF)把多声道音轨渲染成双耳信号,
在普通耳机上还原电影院 C 位附近的环绕包围感;立体声源会先经矩阵上混再渲染,因此浏览器音乐播放器
同样具备空间感。
> ★ 2026-09-13 修复「响度怪」(先点模拟/数字输出导致响度不同):根因、证据与改动见
> [`响度怪-根因与修复.md`](响度怪-根因与修复.md)。新增 `空间音频 诊断`,音量改为持久化,重建会保 profile 并校验连线。
- 版本:**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 路 ─> [每路与人头脉冲响应卷积] ─> 双耳混音 ─> 耳机
立体声源 ──┘ (SADIE-II 真人 HRTF)
├─> [矩阵上混 5.1] ─> 6 路 ─> [逐路 HRTF 卷积] ─> 双耳混音 ─> [房间混响] ─> 物理输出
立体声源 ──┘ (SADIE-II H4 真人 HRIR)
```
- **HRTF**Head-Related Transfer Function,头相关传输函数):同一方向的声音到达两耳时,
会被人头、耳廓滤波,产生不同的时间差、强度差和频谱染色 —— 大脑靠这些线索判断方位。
把这些滤波做成脉冲响应(IR),与声道信号卷积,就能在耳机上"伪造"出方位。
- **虚拟声卡**:用 PipeWire 的 `filter-chain` 模块建两个 `Audio/Sink`,程序把声音送进来,
内部渲染完再输出到真实设备。全局生效,任何播放器都能用。
### 1.1 基本原理
## 特性
- **HRTF(头相关传输函数)**:同一方向的声音到达两耳时会被人头与耳廓滤波,产生耳间时间差、
强度差与频谱染色,听觉系统据此判断方位。将这些滤波记为脉冲响应(IR),与声道信号卷积,
即可在耳机上重建方位线索。
- **虚拟声卡**:借助 PipeWire 的 `filter-chain` 模块建立 `Audio/Sink`,渲染全部在声卡内部完成,
对上层播放器透明。
- 全链路 **96kHz / 24bit**HRTF 采用 SADIE-II `96K/24bit/512tap` 真人数据)
- 立体声源自动矩阵上混,不需要多声道片源
- 提供 deb 包,系统级安装
- 纯脚本 + 数据,无编译,`Architecture: all`
---
## 安装
## 2. 核心技术
### 方式一:deb 包
### 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 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 包
```sh
sudo dpkg -i collaplex-cinema-spatial_1.0.0_all.deb
sudo dpkg -i collaplex-cinema-spatial_1.1.1_all.deb
# 依赖: pipewire libmysofa1 mpv
```
配置装在 `/usr/share/pipewire/pipewire.conf.d/`,对所有用户生效。
**生效**`systemctl --user restart pipewire pipewire-pulse wireplumber`(或注销重登)
安装内容:可执行命令写入 `/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 从源码
```sh
# 1. 下载 HRTF 数据集(不随仓库分发)
# 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 方向 x 左右耳)
# 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 配置
# 3. 生成 PipeWire 配置并生效
python3 生成配置.py
systemctl --user restart pipewire pipewire-pulse wireplumber
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 直通·混响·单声道·设备选择 /
状态信息 / 核心技术说明。
```sh
空间音频 开 # 全局切到立体声上混版(推荐)
空间音频 开5.1 # 全局切到 5.1 直通版
空间音频 关 # 切回物理设备
空间音频 状态 # 查看两个虚拟声卡和当前默认
空间音频 诊断 # 输出目标 / profile / route 硬件增益基数 / 音量账 / 连线状态
空间音频 数字 # 切数字输出(S/PDIF) —— 实测音质更好, 推荐
空间音频 模拟 # 切模拟输出
mpv-影院 电影.mkv # 只给 mpv 用, 不依赖全局设置(适合 A/B 对比)
bash web/启动.sh # 启动(默认端口 8788,重复执行不会起第二个)
python3 web/webui.py --port 8788 --bind 127.0.0.1 # 或直接运行后端
```
也可以直接在桌面环境的"声音设置"里选 **电影院空间音频**
访问 <http://127.0.0.1:8788/>。后端零依赖(标准库),接口列表:
> ★ **输出 route 怎么选**:实测耳机的「数字输出(S/PDIF)」音质**明显优于**「模拟输出」
> (数字那条跑 96 kHz / 24-bit),**推荐数字**。用 `空间音频 数字` 切比在 GNOME 面板点更稳——
> 面板点会改 profile 从而让配置里的目标节点名失效,这条命令把"重建目标 + 保 profile + 验连线"都包住了。
## ★ 反复重建会搞崩 WirePlumber2026-09-13 血亏)
症状:面板「处理后电平」恒为 **-120 dBFS**,但 `pw-link` 连线对、设备 1.00 未静音、
`pw-top` 里设备 WAIT/BUSY 正常 —— **链路看着全对**
真凶两条,都已治:
1. **`wireplumber` 连续快速重启 PipeWire 后会崩**GLib 断言 → core-dump)。它一死,monitor 的新连接
建不起来 → 任何从 monitor 录电平的实现都读静音。判据 `systemctl --user is-active wireplumber`
`systemctl --user restart wireplumber`(只重启会话管理器,PipeWire 本体不动,~1s 恢复)。
**已写进 CLI `regen_chain` 自愈**:重建后查 is-active,不活就拉起。
2. **录制进程认死旧节点 id**:每次重建节点 id 都会漂(实测物理输出 84 → 166),内层永不退出的
`pw-record` 会一直从已消失的目标录静音。已改:电平 worker **每 15s 主动重开**录制。
**教训**:重建别连发;重建后既要验连线,也要验会话管理器活着。
## ★ "重启后目标失效"的根因与修复(2026-09-13
现象(老板报):接单声道蓝牙音响后日志出现
`! 重启后目标失效(配置里是 bluez_output...), 回退到自动探测` → 之后**蓝牙音响一直连不上**。
根因链:
1. 改混响/参数/单声道/设备都要重建滤波链 → 滤波链是**配置里的模块**,**没有运行时热加载** → 必须重启 PipeWire
2. WirePlumber 是 PipeWire 客户端 → 会跟着退出并被 systemd 拉起(**避免不了**
3. 它一重启,蓝牙/USB 设备要 3~8 秒(蓝牙有时 10+ 秒)才重新出现
4. 原代码 `sleep 4` 就判 `target_alive` → 设备还没回来 → 判"失效" → **回退把目标换成别的设备**
→ 蓝牙音响再也连不上;而且回退还可能落到显卡 `pro-output`(死输出)
修法:新增 **`wait_target <秒>`(轮询等目标回来,最多 20 秒)**,三处重启后都用它 ✓
(设备在就立刻返回,放宽无代价);回退探测跳过 `pro-output`/HDMI ✓
实测:重建 2~7 秒、目标存活 ✓、连线正常 ✓
**已确认、别再试**:运行时热加载不通 —— `pw-cli load-module libpipewire-module-null-sink '{}'`
返回 `Error: "Could not load module"`(无参/空对象/全路径都试过)→ 重建就得重启 PipeWire,省不掉。
**下一步可选**(未做):设备切换其实可以**不重建**——用 `pw-link` 运行期把链输出重连到新设备
conf 里的 target 只影响下次重启),那样切蓝牙设备就不会掉线。**2026-09-13 已实现**(见下节)。
## 处理后电平:只读真实信号,绝不经过麦克风(2026-09-13 老板要求)
老板原话:「处理后电平的话**不要用麦克风捕捉,要原始信号**」。
**病根**(实测抓到):电平采集原来用 `pw-record --target <数字 id>` 录**物理设备**的 monitor
`pw-record` 自己找 target 时:
- 自动连线会**挑错端口**(实测 post 两个输入都从 `output_FR` 来 —— 双声道表其实只拿了右声道)
- target 一旦解析不到 → **静默回退默认录制源 = 麦克风** ✗✗
- 而且两个 `pw-record` 进程**同名**`node.name` 都是 `pw-record``application.process.id` 还是 `None`
→ 用 `pw-record:input_FL` 这种**名字寻址会打到另一个进程的端口上**(踩过:日志出现 `连 0/2` + 拆掉 2 条 ✗)
**改法**(三个要点):
| | |
| 接口 | 作用 |
|---|---|
| `--target 0` | 声明"不链接节点"→ **完全不自动连线**,麦克风回退这条路被物理掐死 |
| `-P '{ node.name = cinema-meter-raw ... }'` | 录制进程起**唯一名字**,不再有同名歧义 |
| `link_meter()` 按**端口 id** 显式连线 | `pw-link <源端口id> <录制端口id>`,源码里只连该表该看的信号 |
| `/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` | 检测并修复(设备名失效时重建) |
**采集口语义**(一 raw 一 post,天然成对):
### 7.1 电平表的采集口径
- `raw``cinema_spatial_up_sink:monitor_FL/FR`(被捕捉的原始音频)
- `post``cinema_spatial_up_out:output_FL/FR`**真正的处理后信号**
**验证**(硬证据):
```
电平[raw] 2/2 条 ← cinema_spatial_up_sink(只此信号, 无麦克风)
电平[post] 2/2 条 ← cinema_spatial_up_out(只此信号, 无麦克风)
cinema-meter-raw:input_FL ← cinema_spatial_up_sink:monitor_FL
cinema-meter-post:input_FL ← cinema_spatial_up_out:output_FL
麦克风(alsa_input)连线数: 0
```
★ 目标信号不在图里时**宁可不录**(空表),也不让 pw-record 去抓麦克风。
## 切换设备不再重启(运行期重连)—— 2026-09-13
`空间音频 设备 <关键词>` 与面板设备下拉现在**优先走运行期重连**(CLI 里的 `relink_to`):
链的 playback 节点本质是 PipeWire **stream**,输出端口可以**运行期改连** → 切设备不必重建、
**不必重启 PipeWire** → 蓝牙设备不会被带掉 ✓
- 前提:新设备声道数 == 链当前输出声道数(单声道设备要改图 → 自动回落到重建)
- 顺序:拆旧连线 → 建新连线 → 校验;建不上 → 回落"重建 + wait_target 20"
- 同时把新目标写进 conf(供下次重启使用)
- **实测**:切到临时 2ch null 声卡 **1 秒**完成、日志"运行期重连成功(没重启 PipeWire)"、
**WirePlumber 启动时刻不变** ✓✓;切回内置模拟 0 秒 ✓
- 排查提示:`pw-link -l` 的格式是 **来源在上一行、`|-> 去向` 在下一行**(反向连接显示 `|<-`
★ 已知遗留:链输出是 stream,**目标设备消失时 WirePlumber 会按系统默认把它挪到别的设备**
(实测一度被挪到显卡 `pro-output-3` 的 AUX0 = 死输出)。重建会按 conf 里的 target 拉回 ✓。
若想根治,可给 playback 节点加 `node.autoconnect = false`,只由本工具显式连线。
## 单声道设备(单声道蓝牙音响/耳机)—— 2026-09-13
现象(老板实测):接单声道蓝牙音响 → **直接断连 → 回退守卫滚回 → 之后一直连不上**
根因:链的 playback 节点硬编码 `audio.channels = 2 audio.position = [ FL FR ]`,而单声道设备只有 1 个
`MONO` 端口 → 链接建不起来 → 设备掉线 → 回退。**不是蓝牙的问题,是声道数不匹配。**
三态开关(默认 自动):
| 操作 | 效果 |
| 表 | 采集源 |
|---|---|
| `空间音频 单声道` / 面板「单声道」按钮(点一下循环 自动→开→关) | 看/改当前 |
| `空间音频 单声道 开` | 链里加一级 `monoOut` 混音器(L/R 各 −6 dB 相加,避免相关信号 +6 dB 溢出),graph 输出单端口,playback 节点改 `audio.channels = 1 audio.position = [ MONO ]` |
| `空间音频 单声道 自动` | `pw-dump` 读目标设备的 `audio.channels`,是 1 就自动合并 |
| 原始电平 | `cinema_spatial_up_sink:monitor_FL/FR` —— 进入链路的音频 |
| 处理后电平 | `cinema_spatial_up_out:output_FL/FR` —— 链路输出的处理后信号 |
状态文件 `~/.local/state/cinema-spatial/mono.json``{}` = 自动)。
实测:`开` → 两条链的运行节点都是 `audio.channels=1 [ MONO ]` ✓;`自动` → 回到 `2 [ FL FR ]`
采集进程以 `--target 0` 启动(**禁止自动连线**)并使用唯一节点名,
再由程序按**端口 id** 显式连线。如此可确保:
★ 附带发现(解释"一提一点点就削波"的另一半):**键盘媒体键调的是虚拟声卡音量 = 链的输入增益**(它是应用默认输出),
不是"扬声器电平"。也就是说媒体键拧的就是**削波余量旋钮** —— 面板的「输入增益」= 同一个量,
把它调到 0.24~0.30 就能给最响的瞬态留出余量
- 采集目标始终是上述信号本身,**绝不回退到麦克风**;
- 两个采集进程同名导致的端口寻址混淆不会发生;
- 目标信号不在图中时**保持空表**,不进行任何降级采集
## ★★ 削波真凶:混响 IR 只按峰值归一(2026-09-13
电平表采用**每表自动量程**:按最近 2 秒的最大峰值向上取整到 6 dB 作为顶线,轴标签标注真实 dBFS 值。
老板报"扬声器音量开得很低,但一提一点点就直接削波"。**测量**(60 秒采最大峰值):
---
| | 最大峰值 | 触顶(≥−0.5dBFS)窗数 |
|---|---|---|
| 原始(虚拟声卡捕捉) | +1.02 dBFS | 4790 |
| **处理后** | **+12.02 dBFS** | 5759 |
## 8. 已知问题与工程结论
**超满刻度 12dB** —— 这是硬削波。根因在**我自己加的混响**:
以下均为实测结论,供后续维护参考。
- `镜像法IR.py` 原先只做**峰值归一**(把峰值压到 −6 dBFS),**没做能量归一**。
- 混响是"密集尾音",卷积的能量增益 ≈ `sqrt(Σ ir²)`2 秒 96k 的尾音这个值是 **+11~13 dB**。
- 于是湿路即使只给 0.05(−26 dB),也把整链顶高 **+11 dB** → 削波。
- 低音量时听不出(增益失真与电平无关),**一拧大就暴露** —— 正是老板描述的症状。
### 8.1 PipeWire 模块与配置
**修法**:按**能量归一**(Σir²=1)。之后语义变干净:湿量 G 时湿路电平 = 干路 + 20log₁₀G。
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.id`
`None`,用 `pw-record:input_FL` 之类的名字会连到其它进程的端口上。涉及多实例时必须按**端口 id** 寻址。
5. **判定节点是否存在不能用 `pw-cli info`**(对不存在的名字同样返回成功),应使用 `pw-dump`
`node.name` 精确比对。
6. **重建不要连续触发**:短时间内反复重启 PipeWire 会使 WirePlumber 崩溃(GLib 断言 → core-dump),
表现为监听端口无法建立新连接。当前 CLI 在重建后会检查 `wireplumber` 是否存活并自动拉起。
**控制变量验证**(放已知电平信号,物理音量压到 0.02 听不见,读完换算回 1.000):
### 8.2 卷积素材与归一化
| | 输入 | 输出 | 链净增益 |
|---|---|---|---|
| 关混响 | 6.64 dBFS | 6.4 dBFS | +0.3 dB |
| 开混响 0.05(新 IR | 6.64 dBFS | 6.2 dBFS | **+0.4 dB**(旧 IR 是 +11 dB ✗) |
| 0 dBFS 输入(单独测) | 1.04 dBFS | 1.17 dBFS | 余量 **1.17 dB** ✓ |
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` 拉回,或使用原生高采样率数据集。
**附带**:新增「输入增益」滑块(= 链的输入电平 = 削波余量旋钮,`/api/volume`**改完立即生效不用重建**)。
老板的音源本身峰值到 **+1.02 dBFS**0dBFS 母带 + inter-sample peak),所以最响的瞬态还差 ~1 dB:
把输入增益从 0.30 降到 0.24 就干净(代价 = 整体低 2 dB)。
### 8.3 设备与路由
**教训**:任何卷积素材(混响 IR / HRIR / 房间响应)**按峰值归一都是错的** —— 必须按能量归一
而且必须用**已知电平的信号**做闭环验证(这次就是靠"放 6dBFS 粉噪比 raw/post"一次定位的)。
1. **USB 音频设备重启后 profile 会变化**`iec958-stereo``analog-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. **测试素材必须匹配真实内容的峰值因子**:稳态粉噪无法暴露影视瞬态的削波风险,校准应使用真实片源
或至少混入冲击型素材。
老板报"这个原始音频信号调的是截获的音频信号不是扬声器电平"。原实现写的是**虚拟声卡音量**
(= 链的输入增益 → 它确实在调"被捕捉的信号"→ 报得对)。已改成写**扬声器电平**:
---
- 滑块 → `/api/speaker`**物理输出音量**(你听到的音量);工具提示改成「扬声器电平(物理输出 —— 你听到的音量)」
- **链的输入增益固定 0.300 不动**:这样「原始电平」表是干净的声源监视(不受音量影响 = 老板要的语义),
也避免把链内部电平抬高 10.5 dB(12 路卷积叠加有削波风险)
- 实测:设 0.500 → 物理输出 **0.500** ✓,而虚拟声卡捕捉电平 **Δ0.000**
-**硬件增益基数不能写死表**:实测这台数字路基数 1.003(我旧表写 1.0,差 10%),
且 Ryzen 卡会报一路 **0.001** 的假基数(照算会补 1114 倍)。→ 改成**闭环自校正**:
按实测比例(物理÷目标)算,写一次后读真实音量、量残差再修,最多 3 轮。现在"要多少就是多少"。
- 统一响度开着时写的是**目标**(物理 = 基数 × 目标),否则直接写该 route 的音量 —— 两种模式都不会被下次 apply 抹掉。
## 9. 实测数据
## 电平表"处理后不滚动"的根因与修复(2026-09-13
### 9.1 房间混响逐频带 RT60
老板报"处理后电平不滚动"。**数据侧是活的**API 每 0.5s 都在变、120/120 窗有值、两个 pw-record 都活着),
真凶在**显示**
1. 两张表共用固定量程 **0…-60 dBFS**。处理后信号比原始低约 22 dB(虚拟音量 -10.5 + HRTF 约 -16),
落在 -48 上下 —— 幅度只剩 2~3 dB 抖动,**肉眼看就是一条贴底死线**。
→ 改**每表自动量程**:按**最近 2 秒**的最大峰值向上取整到 6 dB 定顶线(显示 48 dB 动态范围,
轴标签写真实 dBFS)。★ 注意必须用"最近 2 秒"而不是整 12 秒:历史最响的一瞬 -10 dBFS 会把顶线
钉在 -6,安静段照样贴底。
2. 顺带挖出潜伏 bug`hold.raw = drawMeter(...)` 把**返回值(数字)**存回 hold,
下一帧 `peakHold.v` 就是 `undefined`**峰值保持线一直画不出来**
→ 改成传持久对象 `hold.raw = hold.raw || {v:-60}`,由 drawMeter 内部更新 `.v`
**验证方式(只看像素,不看 DOM/日志)**CDP 取 canvas 像素,隔 8 秒再取一次比像素和 + 墨点重心:
| 表 | 墨点 | 墨点重心 y | 像素和 |
|---|---|---|---|
| 原始 | 45353 → 44985 | 41.4 → 41.4 | 有变化 ✓ |
| 处理后 | 34950 → 36016 | 47.4 → 46.0 | 有变化 ✓ |
## 采样参数可编辑(2026-09-13
- 面板「采样,各参数调整选项」里三个下拉直接改:**采样率 / HRTF 模型 / HRIR 长度**,改完自动重建(约 10s)
- 命令:`空间音频 参数`(看现值 + 可选项)、`空间音频 参数 hrtf H4-48k``参数 taps 1024``参数 rate 48000`
- 状态文件 `~/.local/state/cinema-spatial/params.json``生成配置.py` 按它选 IR 目录
(优先 `hrir/taps-<N>` > 模型名 > `hrir/current` 软链。模型与截断二选一,设一个会清掉另一个)
- tap 截断:`work/裁HRIR.py <N>``hrir/taps-<N>/`(末尾 3ms 淡出防咔嗒;512/1024/2048 随便裁)
- 采样率:写**用户级** `~/.config/pipewire/pipewire.conf.d/91-clock.conf`
同名覆盖 deb 装在 `/usr/share/pipewire/pipewire.conf.d/` 的那份(**不用 sudo**
- ★ 三者都是生成期常量 → **改完必须重建链路**;面板下拉已自动触发重建
- ★ 实测结论(控制变量):**开着混响时 HRTF tap 数不是主成本**
—— 2 秒的房间 IR 等效每声道 19.2 万 tap,比 HRIR 的 4800 tap 大 40 倍。
所以裁 HRIR1024 tap)省不到多少 CPU,却会砍掉 HRTF 尾部让声音变干 →
**默认保持 H4-96k / 4800 tap**,除非以后 CPU 告急才考虑裁。
- 顶栏同时重排:标题 + 模式徽标 / 状态摘要(窄屏自动落到第二行整行)/ `开 · 开 5.1 · 关` 分段控件(当前模式高亮)+ 主按钮
## 房间混响(真实房间脉冲响应卷积,2026-09-13)
用**自己的房间模型**算真实 IR,不用廉价算法混响:生成器 `声学追踪引擎/src/镜像法IR.py`
(房间 20×12×7m / 材质逐带吸声 / 空气吸收 / 双耳间距 0.175m 全来自 `config/房间.json`)。
- 方法:**镜像法早期反射 + Sabine 尾音混合**。纯镜像法阶数有限→时长有限(阶 18 只到 ~250ms),
所以在 80ms 交叉点把早期场淡出、按逐带 Sabine RT60 生成的指数衰减噪声尾音(两耳相干 0.4)淡入。
- 产物:`reverb/房间混响IR-96k.wav`2.00s / 96kHz / 双声道 / 峰值 -6dBFS / 29.1ms 前直达声已切)
- 落地:`生成配置.py` 的混响级(`revL/revR` 卷积 + `wetL/wetR` 干湿混合,干=1.0、湿=可调);
**关的时候整级不生成**(不白吃 CPU)。湿量是生成期常量 → 改湿量必须重建。
- 命令:`空间音频 混响 [开|关|湿量]`(不带参数=翻转;约 10s 重建)、`空间音频 重建`
- 面板:混响开关 + 湿量滑块(湿量放在「采样参数」区,保持画稿那行三块结构不变)
- ★ 改湿量后必须重建:`生成配置.py` 只认生成期常量
## 输出设备选择(2026-09-13
- `音频状态.py sinks` → 全部物理输出(USB → PCI → 显卡 Pro/HDMI,带当前目标标记)
- `空间音频 设备 [关键词]` → 按 node.name / 描述匹配 → 重写 conf 的 `node.target` → 重建 → **校验** → 实装
- 失败守卫:重启后目标若失效,自动滚回自动探测的设备(实测撞到过:pw-cli 造的 null sink 活不过重启)
- 面板「设备选择」下拉吃同一份数据
## 混响级实测(2026-09-13
| 频段 | Sabine | 早期(镜像法) | 最终 IR 实测 |
| 频段 | Sabine 理论 | 早期反射实测 | 最终 IR 实测 |
|---|---|---|---|
| 125 Hz | 1.363 s | 1.542 s | 1.338 s |
| 250 Hz | 0.722 s | 0.779 s | 0.749 s |
@@ -310,99 +352,77 @@ cinema-meter-post:input_FL ← cinema_spatial_up_out:output_FL
| 2 kHz | 0.331 s | 1.153 s | 0.427 s |
| 4 kHz | 0.332 s | 0.809 s | 0.362 s |
- 运行中两个 filter-chain 模块里 `revL/revR/wetL/wetR` 全部已加载(`pw-cli info` 实测)✓
- 处理后峰值 -22.2 dBFS(**没削波**);设备噪声地板 -56 dBFS
- 测量工具:`work/测链路冲激响应.py`(喂脉冲→录 monitor→算包络/T60;注意 `pw-link -l` 的来源在目标端口下一行、挑设备要读 conf 的 node.target 别按 pw-dump 顺序猜)
测量工具:`work/测链路冲激响应.py`(馈入脉冲 → 录制 monitor → 计算包络与 T60)。
运行中的两个 filter-chain 模块内 `revL / revR / wetL / wetR` 均已加载(`pw-cli info` 实测),
处理后峰值 −22.2 dBFS(未削波),设备噪声地板 −56 dBFS。
## Collaplex 音效(Web 控制台 / :8788)—— 已按设计稿重做(2026-09-13
### 9.2 削波问题定位(能量归一前后
布局照 `~/桌面/collaplex web设计稿.drawio` 实现:顶栏 / 原始电平+处理后电平 / 三个信号开关(竖排) /
音量滑块 / 数字·模拟两条输出线 / 采样参数 / 响度统一开关+滑块 / 5.1直通·混响·设备选择 / 状态信息。
| 状态 | 输入 | 输出 | 链净增益 |
|---|---|---|---|
| 关闭混响 | 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 |
- 后端 `web/webui.py``/api/state` `/api/meters` `/api/mode` `/api/volume` `/api/route` `/api/switch` `/api/loudness` `/api/fix` `/api/device`
- **电平表只认数字节点 ID** —— `pw-record --target <数字ID>` 才接 monitor
给**名字**不解析会静默回退麦克风,`<名>.monitor` 本机根本不存在(详见技能 linux-audio-routing
- 已接线并实测:开/关/5.1、数字·模拟 route、原始音量、三个信号开关(虚拟声卡那路是**真断开连线**)、
响度统一(按硬件增益基数补 +0.94 dB)、按当前参数重建
- **待接线**:混响开关(需定方案:房间 IR 卷积 / 轻量算法混响)、设备选择(要改 conf 的 `node.target` 再重建)、
采样参数编辑(要重新生成配置)
- 改设计稿之前的旧面板留档:`web/index.html.设计稿前备份`
### 9.3 电平表显示验证
### 旧说明(保留)
| 表 | 墨点数 | 墨点重心 y | 结论 |
|---|---|---|---|
| 原始 | 45353 → 44985 | 41.4 → 41.4 | 变化正常 |
| 处理后 | 34950 → 36016 | 47.4 → 46.0 | 变化正常 |
浏览器里看状态、切模式、调音量、**一键修复"失声"**(设备名失效时自动重建)。
---
```sh
bash web/启动.sh # 启动(默认端口 8788,重复执行不会起第二个)
python3 web/webui.py --port 8788 --bind 127.0.0.1 # 也可以直接跑
```
打开 <http://127.0.0.1:8788/>。菜单里也有「Collaplex 音效」快捷方式。
界面会实时显示:默认输出、链路采样率、HRTF 模型、**配置输出设备 vs 当前物理输出(不一致会标红)**、
输出端实际连到哪个设备、以及两个虚拟声卡和设备的音量。检测到设备名失效时直接点修复即可。
## 文件说明
## 10. 文件说明
| 文件 | 作用 |
|---|---|
| `空间音频` | 开关命令 |
| `mpv-影院` | mpv 专用的 HRTF 播放包装 |
| `空间音频` | 主命令(开关、诊断、参数、设备、混响、单声道) |
| `音频状态.py` | 状态后端:设备枚举、profile、route、硬件增益基数、连线校验 |
| `生成配置.py` | 生成 PipeWire filter-chain 配置 |
| `换HRTF.sh` | 从 SOFA 提取脉冲响应(可换任意 HRTF 模型)|
| `提取HRIR.sh` | 早期版本(已被 `换HRTF.sh` 取代)|
| `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` | 安装后自动验证 |
| `web/` | Collaplex 音效 Web 控制台(`webui.py` 后端 + `index.html` 前端 + `启动.sh`),零依赖 |
| `demo.sh` / `响度校准.sh` / `方向测试.sh` | 开发期验证工具 |
| `对比分析.py` / `排查.sh` | 开发期排查工具 |
| `demo.sh` / `响度校准.sh` / `方向测试.sh` / `对比分析.py` / `排查.sh` | 开发期工具 |
## 踩过的坑(均为实测)
---
1. **增益必须按"整链"算**:矩阵上混出来的 6 路会在混音器里相加,实测**整链净增益约 +10.5dB**。
只按单路脉冲响应峰值校准,一听真实内容就削波爆音。
2. **校准素材必须接近满刻度**:自造测试信号(峰值 -12dB)与真实母带(≈0dBFS)差 10dB 以上,
用它校出的增益必然炸。
3. **不同 SOFA 的电平差异极大**:同一数据集内,D1(假头)IR 峰值 -11.6dB、H4(真人)-3.1dB。
**换模型必须重新量峰值**,目标压在 -12dB 左右。
4. **`sofalizer` 按 SOFA 自身的采样率输出**:不管输入 48k 还是 96k,只要 SOFA 是 44.1k
输出就掉到 44.1k —— 这是"采样率偏低"的根因。要么在滤镜链尾 `aresample` 拉回,
要么用原生高采样率的数据集。
5. **`convolver` 按当前时钟率重采样脉冲响应**:主时钟还是 48k 的话,96k/512tap 的 IR
会被降成等效 256tap,等于白换。必须把 `default.clock.rate` 一起提到 96k。
6. **`node.passive = true` 必须配 `node.target`**:否则 filter-chain 的输出端不连任何设备,
声音凭空消失。
7. **ffmpeg 滤镜表达式里的 `,` 和 `|`** 要用单引号包住,否则会被当分隔符解析。
8. **`adelay` + `join` 合成多声道素材不可靠**:曾静默产出 0.25 秒的单声道文件。
改用 `aevalsrc` 直接合成。
9. **`type=time` 是时域卷积(保真)**;默认的 `type=freq` 走 FFT,会削高频。
10. **★ 测试素材必须匹配真实内容的"峰值因子"**:用平稳的粉噪做压力测试,看着余量充足,
一放电影里爆炸/撞击那种高峰值因子瞬态就削 —— 同样听感响度下,瞬态峰值能再高出 10dB 以上。
**校准要用真实片源,或至少混入冲击型素材。**
11. **★ 每一层音量都吃动态余量,改一处就要记账**:虚拟声卡音量与设备(耳机)音量
**串联在同一条链上**。曾把设备音量从 0.66 提到 1.00(+3.6dB)却没算进余量账,
结果一有瞬态就削。**设备自身的出厂音量是预留余量,不要顺手拉满。**
12. **★ USB 音频设备重启后 profile 会变**(`iec958-stereo``analog-stereo`),
设备 `node.name` 随之后缀改变 → 配置里记的 `node.target` 失效 → WirePlumber 回落到其它输出
(实测落到 HDMI,表现为"没声音"或"声音从显示器出来")。
本项目的 `空间音频 开` 已带自检:设备名失效时自动重新探测并重建配置。
12. **★★ 虚拟声卡的音量不会被保留**(2026-09-13,爆音反复的根因):
filter-chain 每次重建(重载 PipeWire、开机、切设备)都是**全新的节点**,
没有历史音量 → 回到默认 **1.0** → 整链直接削波。
用户看到的现象是"我明明调低了,怎么又炸了"——**而且调低的那一刻音量可能刚被重置**。
**修法两处**:① `capture.props` 里写 `node.volume = 0.25`(重建时的默认值)
② 切换命令每次补写一遍(兜底)。
13. **判定设备是否存在不能用 `pw-cli info`**:它对不存在的名字也返回成功。要用 `pw-dump`
`node.name` 精确比对。
## 11. 版本记录
## 数据来源
### 1.1.1
- README 重写为正式文档,并同步至当前功能状态
HRTF 采用 **[SADIE II](https://www.york.ac.uk/sadie-project/database.html)**
### 1.1.0
- 新增单声道输出支持(三态:自动 / 开 / 关),解决单声道设备因声道数不匹配导致的掉线
- 新增运行期重连:切换输出设备不再重启 PipeWire,蓝牙设备不再被中断
- 修复"重启后目标失效":改为轮询等待目标设备就绪(最多 20 秒),并在自动探测中跳过显卡 / HDMI 输出
- 修复削波:房间 IR 改为能量归一(此前按峰值归一致使整链 +11 dB)
- 电平表改为显式连线采集链路输出,杜绝回退到麦克风
- 面板新增"核心技术"说明区
- 打包修复:`control` 版本号跟随构建版本(此前恒为 1.0.0,导致 dpkg 无法升级);
补入 `音频状态.py``web/` 控制台
### 1.0.x
- 首个公开发布:双虚拟声卡、SADIE-II HRTF 渲染、deb 打包
- 修复"响度怪":详见 [`响度怪-根因与修复.md`](响度怪-根因与修复.md);新增 `空间音频 诊断`
音量改为持久化,重建时保留 profile 并校验连线
---
## 12. 数据来源与许可
HRTF 数据采用 **[SADIE II](https://www.york.ac.uk/sadie-project/database.html)**
University of YorkZenodo DOI [10.5281/zenodo.12092466](https://doi.org/10.5281/zenodo.12092466)),
使用其中真人受试者 **H4**`96K/24bit/512tap` 版本。
使用其中真人受试者 **H4**`96K / 24bit / 512tap` 版本。引用方式见数据集页面。
引用方式见数据集页面。数据集**不随本仓库分发**,请从 Zenodo 自行下载。
数据集体积较大,**不随本仓库分发**,请从 Zenodo 自行下载。
## License
MIT
本项目以 **MIT** 许可发布。
+1 -1
View File
@@ -14,7 +14,7 @@ bash 打包deb.sh 2>&1 | tail -3
echo "本地: $(stat -c%s "$DEB") 字节"
echo "=== 建 release v$VER ==="
BODY="collaplex-cinema-spatial $VER - 耳机电影院空间音频 (PipeWire filter-chain + SADIE-II HRTF, 96kHz/24bit)\n\n核心技术: 镜像法房间声学追踪(0-80ms 早期反射, 8473 条镜面路径) + Sabine 统计尾音(80ms-2s) -> 96kHz 真实房间 IR 双耳卷积; 湿量按能量归一(湿量 G 时湿路 = 干路 + 20log10 G)。不是 Freeverb/Schroeder 类算法混响。\n\n本版新增/修复:\n- 单声道设备支持: 单声道蓝牙音响/耳机连不上(链输出是 2 声道 FL/FR, 单声道设备只有 1 个 MONO 端口) -> 新增单声道合并(自动/开/关), 默认按目标设备声道数自动判定\n- 切换设备不再重启 PipeWire: 新增运行期重连(链输出本质是 stream, 端口可运行期改连) -> 蓝牙设备不再被重启带掉\n- 修复'重启后目标失效': 原来固定 sleep 4 就判死, 而蓝牙/USB 设备要 3~8 秒重连 -> 改为轮询等待(最多 20 秒), 回退探测跳过显卡/HDMI\n- 修复削波: 房间混响 IR 原来只按峰值归一, 能量增益 +11dB, 处理后峰值冲到 +12dBFS -> 改为能量归一(sigma ir^2 = 1)\n- 电平表采集: 处理后电平改为按端口 id 显式连线读取链输出, 杜绝 pw-record 静默回退到麦克风\n- 面板新增'核心技术'说明区; 新增输入增益(削波余量)滑块\n- 打包: control 版本号改为跟随 VER(旧版恒为 1.0.0 致 dpkg 无法升级), 补入 音频状态.py 与 web/ 面板\n\n安装: sudo dpkg -i ${PKG}_${VER}_all.deb; systemctl --user restart pipewire pipewire-pulse"
BODY="collaplex-cinema-spatial $VER - 耳机电影院空间音频 (PipeWire filter-chain + SADIE-II HRTF, 96kHz/24bit)\n\n核心技术: 镜像法房间声学追踪(0-80ms 早期反射, 8473 条镜面路径) + Sabine 统计尾音(80ms-2s) -> 96kHz 真实房间 IR 双耳卷积; 湿量按能量归一(湿量 G 时湿路 = 干路 + 20log10 G)。不是 Freeverb/Schroeder 类算法混响。\n\n本版 1.1.1: README 重写为正式文档(核心技术/信号链/参数/已知问题/实测数据/版本记录), 未改动功能代码。\n\n自 1.0.x 以来的主要变化:\n- 单声道设备支持: 单声道蓝牙音响/耳机连不上(链输出是 2 声道 FL/FR, 单声道设备只有 1 个 MONO 端口) -> 新增单声道合并(自动/开/关), 默认按目标设备声道数自动判定\n- 切换设备不再重启 PipeWire: 新增运行期重连(链输出本质是 stream, 端口可运行期改连) -> 蓝牙设备不再被重启带掉\n- 修复'重启后目标失效': 原来固定 sleep 4 就判死, 而蓝牙/USB 设备要 3~8 秒重连 -> 改为轮询等待(最多 20 秒), 回退探测跳过显卡/HDMI\n- 修复削波: 房间混响 IR 原来只按峰值归一, 能量增益 +11dB, 处理后峰值冲到 +12dBFS -> 改为能量归一(sigma ir^2 = 1)\n- 电平表采集: 处理后电平改为按端口 id 显式连线读取链输出, 杜绝 pw-record 静默回退到麦克风\n- 面板新增'核心技术'说明区; 新增输入增益(削波余量)滑块\n- 打包: control 版本号改为跟随 VER(旧版恒为 1.0.0 致 dpkg 无法升级), 补入 音频状态.py 与 web/ 面板\n\n安装: sudo dpkg -i ${PKG}_${VER}_all.deb; systemctl --user restart pipewire pipewire-pulse"
RID=$(curl -s -X POST "$API/releases" -u edgevoid:147427 -H "Content-Type: application/json" -d "{\"tag_name\":\"v$VER\",\"name\":\"v$VER\",\"body\":\"$BODY\"}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))")
echo "release id: $RID"
[ -n "$RID" ] || exit 1
+1 -1
View File
@@ -4,7 +4,7 @@
set -eu
SRC="/home/lou/桌面/工作区/实验/空间音频"
PKG="collaplex-cinema-spatial"
VER="1.1.0"
VER="1.1.1"
BUILD="/tmp/${PKG}-deb"
ROOT="/usr/share/cinema-spatial"
SOFA_SRC="$SRC/sofa/H4_HRIR_SOFA/H4_HRIR_SOFA/H4_96K_24bit_512tap_FIR_SOFA.sofa"