HRTF 独立实例: 接入(collaplex_hrtf_in) -> 双耳 HRTF + 房间混响(湿量0.3) -> 数字输出; 虚拟声卡退回纯转发

This commit is contained in:
lou
2026-09-19 18:06:23 +08:00
parent f1a3c80fd4
commit 0840c7d398
18 changed files with 156 additions and 55 deletions
+1
View File
@@ -12,4 +12,5 @@ export LN_MAX_BOOST=20
export LN_MAX_CUT=20
export LN_SLEW_DB_S=6
export LN_RATE=48000
export LN_IDLE_LIMIT=999999
exec /usr/bin/python3 "__DSP__"
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+7 -26
View File
@@ -1,38 +1,19 @@
# Collaplex 虚拟声卡 + 响度归一化 —— 本文件由 脚本/安装.sh 生成(占位符: __TARGET__ / __WRAPPER__)
# 链路: 全局音频(应用输出) -> collaplex_vsink(虚拟声卡) -> 响度归一化(pipe DSP) -> 数字输出(S/PDIF)
# Collaplex 虚拟声卡(纯转发) —— 全局音频 -> 虚拟声卡 -> 数字输出
# 与 20-collaplex-hrtf.conf(HRTF 独立实例)互不干扰:
# 想全系统走空间音效, 把下面的 node.target 改成 "collaplex_hrtf_in" 即可串起来。
context.modules = [
{ name = libpipewire-module-filter-chain
flags = [ nofail ]
{ name = libpipewire-module-loopback
args = {
node.description = "Collaplex 虚拟声卡 (响度归一化)"
media.name = "Collaplex Virtual Sink"
filter.graph = {
nodes = [
{ type = builtin label = copy name = c1L }
{ type = builtin label = copy name = c1R }
{ type = builtin label = pipe name = lnL
config = { command = "__WRAPPER__" } }
{ type = builtin label = pipe name = lnR
config = { command = "__WRAPPER__" } }
{ type = builtin label = copy name = c2L }
{ type = builtin label = copy name = c2R }
]
links = [
{ output = "c1L:Out" input = "lnL:In" }
{ output = "c1R:Out" input = "lnR:In" }
{ output = "lnL:Out" input = "c2L:In" }
{ output = "lnR:Out" input = "c2R:In" }
]
inputs = [ "c1L:In" "c1R:In" ]
outputs = [ "c2L:Out" "c2R:Out" ]
}
node.description = "Collaplex 虚拟声卡"
capture.props = {
media.class = Audio/Sink
node.name = collaplex_vsink
audio.channels = 2
audio.position = [ FL FR ]
}
playback.props = {
node.name = collaplex_vsink_out
audio.channels = 2
audio.position = [ FL FR ]
node.target = "__TARGET__"
}
+41
View File
@@ -0,0 +1,41 @@
# Collaplex 虚拟声卡 + 响度归一化 —— 本文件由 脚本/安装.sh 生成(占位符: __TARGET__ / __WRAPPER__)
# 链路: 全局音频(应用输出) -> collaplex_vsink(虚拟声卡) -> 响度归一化(pipe DSP) -> 数字输出(S/PDIF)
context.modules = [
{ name = libpipewire-module-filter-chain
flags = [ nofail ]
args = {
node.description = "Collaplex 虚拟声卡 (响度归一化)"
media.name = "Collaplex Virtual Sink"
filter.graph = {
nodes = [
{ type = builtin label = copy name = c1L }
{ type = builtin label = copy name = c1R }
{ type = builtin label = pipe name = lnL
config = { command = "__WRAPPER__" } }
{ type = builtin label = pipe name = lnR
config = { command = "__WRAPPER__" } }
{ type = builtin label = copy name = c2L }
{ type = builtin label = copy name = c2R }
]
links = [
{ output = "c1L:Out" input = "lnL:In" }
{ output = "c1R:Out" input = "lnR:In" }
{ output = "lnL:Out" input = "c2L:In" }
{ output = "lnR:Out" input = "c2R:In" }
]
inputs = [ "c1L:In" "c1R:In" ]
outputs = [ "c2L:Out" "c2R:Out" ]
}
capture.props = {
media.class = Audio/Sink
node.name = collaplex_vsink
audio.position = [ FL FR ]
}
playback.props = {
node.name = collaplex_vsink_out
audio.position = [ FL FR ]
node.target = "__TARGET__"
}
}
}
]
+62
View File
@@ -0,0 +1,62 @@
# Collaplex HRTF(双耳虚拟环绕)+ 房间混响 —— 本文件由 脚本/安装.sh 生成
# 占位符: __HRIR__ 脉冲响应目录 / __REVERB__ 房间 IR / __TARGET__ 数字输出 / __WET__ 湿量
# 链路: 全局音频 -> 虚拟声卡 -> 响度归一化 -> [本图] 双耳 HRTF -> 房间混响(湿量 __WET__) -> 数字输出
#
# HRTF: 左耳 = L·h(FL→L) + R·h(FR→L), 右耳 = L·h(FL→R) + R·h(FR→R)
# 混响: 干路直通(Gain 1 = 1.0) + 湿路经房间 IR(Gain 2 = 湿量) —— 按能量归一, 湿量 G 时湿路电平 = 干路 + 20log10(G)
context.modules = [
{ name = libpipewire-module-filter-chain
flags = [ nofail ]
args = {
node.description = "Collaplex HRTF (双耳虚拟环绕)"
media.name = "Collaplex HRTF"
filter.graph = {
nodes = [
{ type = builtin label = copy name = iL }
{ type = builtin label = copy name = iR }
{ type = builtin label = convolver name = cFLL config = { filename = "__HRIR__/FL_L.wav" } }
{ type = builtin label = convolver name = cFLR config = { filename = "__HRIR__/FL_R.wav" } }
{ type = builtin label = convolver name = cFRL config = { filename = "__HRIR__/FR_L.wav" } }
{ type = builtin label = convolver name = cFRR config = { filename = "__HRIR__/FR_R.wav" } }
{ type = builtin label = mixer name = hL control = { "Gain 1" = 1.0 "Gain 2" = 1.0 } }
{ type = builtin label = mixer name = hR control = { "Gain 1" = 1.0 "Gain 2" = 1.0 } }
{ type = builtin label = convolver name = revL config = { filename = "__REVERB__" } }
{ type = builtin label = convolver name = revR config = { filename = "__REVERB__" } }
{ type = builtin label = mixer name = wetL control = { "Gain 1" = 1.0 "Gain 2" = __WET__ } }
{ type = builtin label = mixer name = wetR control = { "Gain 1" = 1.0 "Gain 2" = __WET__ } }
]
links = [
{ output = "iL:Out" input = "cFLL:In" }
{ output = "iL:Out" input = "cFLR:In" }
{ output = "iR:Out" input = "cFRL:In" }
{ output = "iR:Out" input = "cFRR:In" }
{ output = "cFLL:Out" input = "hL:In 1" }
{ output = "cFRL:Out" input = "hL:In 2" }
{ output = "cFLR:Out" input = "hR:In 1" }
{ output = "cFRR:Out" input = "hR:In 2" }
{ output = "hL:Out" input = "wetL:In 1" }
{ output = "hL:Out" input = "revL:In" }
{ output = "revL:Out" input = "wetL:In 2" }
{ output = "hR:Out" input = "wetR:In 1" }
{ output = "hR:Out" input = "revR:In" }
{ output = "revR:Out" input = "wetR:In 2" }
]
inputs = [ "iL:In" "iR:In" ]
outputs = [ "wetL:Out" "wetR:Out" ]
}
capture.props = {
media.class = Audio/Sink
node.name = collaplex_hrtf_in
node.description = "Collaplex HRTF 接入"
audio.channels = 2
audio.position = [ FL FR ]
}
playback.props = {
node.name = collaplex_hrtf_out
audio.channels = 2
audio.position = [ FL FR ]
node.target = "__TARGET__"
}
}
}
]
Binary file not shown.
+45 -29
View File
@@ -1,20 +1,26 @@
#!/bin/bash
# Collaplex 音效 — 把当前链路固化装到 PipeWire(用户级)
# Collaplex 音效 — 把链路固化装到 PipeWire(用户级)
#
# 链路: 全局音频(应用输出) -> collaplex_vsink(虚拟声卡) -> 响度归一化(pipe DSP) -> 数字输出
# 链路: 全局音频(应用输出) -> collaplex_vsink(虚拟声卡) -> 响度归一化(pipe DSP)
# -> collaplex_hrtf_in(HRTF 双耳虚拟环绕 + 房间混响, 湿量 $WET) -> 数字输出(S/PDIF)
#
# 装什么:
# $HOME/.config/pipewire/pipewire.conf.d/10-collaplex-vsink.conf 链路定义(由本脚本渲染)
# $HOME/.config/pipewire/pipewire.conf.d/10-collaplex-vsink.conf 虚拟声卡 + 响度归一化
# $HOME/.config/pipewire/pipewire.conf.d/20-collaplex-hrtf.conf HRTF + 房间混响
# $HOME/.local/bin/collaplex-loudness-norm pipe 插件入口(ASCII 路径)
# 设备名换了 / 项目挪了位置 -> 重跑本脚本即可(这就是"固化"的意义: 定义在项目里, 部署可复现)
#
# 设备名换了 / 项目挪了位置 / 想改湿量 -> 重跑本脚本(可变部分由脚本渲染, 定义在项目里)
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CONF_DST="$HOME/.config/pipewire/pipewire.conf.d/10-collaplex-vsink.conf"
CONF_DIR="$HOME/.config/pipewire/pipewire.conf.d"
WRAP_DST="$HOME/.local/bin/collaplex-loudness-norm"
PY=/usr/bin/python3
WET="${WET:-0.3}" # 混响湿量(湿度"3" = 0.3); export WET=0.5 可覆盖
HRTF_IN="collaplex_hrtf_in"
echo "项目根: $ROOT"
echo "项目根 : $ROOT"
echo "混响湿量: $WET"
# ---- 1. 探测数字输出设备(跳过 HDMI/显卡) ----
TARGET="$("$PY" - <<'EOF'
@@ -29,7 +35,7 @@ for n in d:
continue
nm = p.get("node.name") or ""
if "hdmi" in nm.lower() or "gb206" in nm.lower() or "radeon" in nm.lower():
continue # 显卡 HDMI, 不是我们要的数字输出
continue
sinks.append(nm)
dig = [s for s in sinks if "iec958" in s or "spdif" in s.lower()]
print(dig[0] if dig else "")
@@ -42,11 +48,15 @@ if [ -z "$TARGET" ]; then
fi
echo "数字输出: $TARGET"
# ---- 2. 渲染并安装链路定义 ----
mkdir -p "$(dirname "$CONF_DST")" "$(dirname "$WRAP_DST")"
sed -e "s|__TARGET__|$TARGET|" -e "s|__WRAPPER__|$WRAP_DST|" \
"$ROOT/pipewire/10-collaplex-vsink.conf" > "$CONF_DST"
echo "已装配置: $CONF_DST"
# ---- 2. 渲染两级链路定义 ----
mkdir -p "$CONF_DIR" "$(dirname "$WRAP_DST")"
sed -e "s|__HRTF_IN__|$HRTF_IN|" -e "s|__WRAPPER__|$WRAP_DST|" \
"$ROOT/pipewire/10-collaplex-vsink.conf" > "$CONF_DIR/10-collaplex-vsink.conf"
echo "已装配置: $CONF_DIR/10-collaplex-vsink.conf"
sed -e "s|__HRIR__|$ROOT/hrir|" -e "s|__REVERB__|$ROOT/reverb/房间混响IR-96k.wav|" \
-e "s|__TARGET__|$TARGET|" -e "s|__WET__|$WET|" \
"$ROOT/pipewire/20-collaplex-hrtf.conf" > "$CONF_DIR/20-collaplex-hrtf.conf"
echo "已装配置: $CONF_DIR/20-collaplex-hrtf.conf"
# ---- 3. 渲染并安装 pipe 插件入口(指向项目内的 DSP) ----
sed -e "s|__DSP__|$ROOT/dsp/loudness_norm.py|" \
@@ -55,26 +65,34 @@ chmod 755 "$WRAP_DST"
echo "已装入口: $WRAP_DST -> $ROOT/dsp/loudness_norm.py"
# ---- 4. 让 PipeWire 重新加载 ----
# 重启前先收掉残留的 DSP: 空闲退出的旧实例会占住 /dev/shm 的槽位, 新的就起不来(静音)。
for p in /proc/[0-9]*; do
c=$(tr '\0' ' ' < "$p/cmdline" 2>/dev/null) || continue
case "$c" in "/usr/bin/python3 "*dsp/loudness_norm.py*) kill "${p#/proc/}" 2>/dev/null || true;; esac
done
systemctl --user restart pipewire pipewire-pulse wireplumber
sleep 6
sleep 7
# ---- 5. 自检 ----
echo
echo "=== 自检 ==="
N=$("$PY" - <<'EOF'
import json, subprocess
"$PY" - "$HRTF_IN" <<'EOF'
import json, subprocess, sys
want = ["collaplex_vsink", "collaplex_vsink_out", "collaplex_hrtf_in", "collaplex_hrtf_out"]
d = json.loads(subprocess.run(["pw-dump"], capture_output=True, text=True).stdout)
n = [x for x in d if x.get("type") == "PipeWire:Interface:Node"
and (x.get("info", {}).get("props", {}) or {}).get("node.name") == "collaplex_vsink"]
print(len(n))
have = {}
for n in d:
if n.get("type") == "PipeWire:Interface:Node":
nm = (n.get("info", {}).get("props", {}) or {}).get("node.name")
if nm:
have[nm] = n["id"]
for w in want:
print(" %s %s%s" % ("✓" if w in have else "✗", w, (" (id %d)" % have[w]) if w in have else ""))
EOF
)
[ "$N" = "1" ] && echo " ✓ 虚拟声卡节点 collaplex_vsink 在" || echo " ✗ 虚拟声卡节点缺失(个数=$N)"
DP=$(for p in /proc/[0-9]*; do c=$(tr '\0' ' ' < "$p/cmdline" 2>/dev/null) || continue; case "$c" in *dsp/loudness_norm.py*) echo "$p";; esac; done | wc -l)
DP=$(for p in /proc/[0-9]*; do c=$(tr '\0' ' ' < "$p/cmdline" 2>/dev/null) || continue; case "$c" in "/usr/bin/python3 "*dsp/loudness_norm.py*) echo "$p";; esac; done | wc -l)
[ "$DP" = "2" ] && echo " ✓ 响度归一化 DSP 进程 2 个(左右各一)" || echo " ✗ DSP 进程数=$DP(应为 2)"
ID=$("$PY" - <<'EOF'
ID="$("$PY" - <<'EOF'
import json, subprocess
d = json.loads(subprocess.run(["pw-dump"], capture_output=True, text=True).stdout)
for x in d:
@@ -82,12 +100,10 @@ for x in d:
if (x.get("info", {}).get("props", {}) or {}).get("node.name") == "collaplex_vsink":
print(x["id"])
EOF
)
if [ -n "$ID" ]; then
wpctl set-default "$ID" >/dev/null 2>&1 || true
echo " ✓ 默认输出已设为 collaplex_vsink(id $ID)"
fi
)"
[ -n "$ID" ] && { wpctl set-default "$ID" >/dev/null 2>&1 || true; echo " ✓ 默认输出 = collaplex_vsink(id $ID)"; }
echo
echo "开关/目标: python3 \"$ROOT/dsp/状态.py\" # 看实时推子"
echo "实时状态: python3 \"$ROOT/dsp/状态.py\""
echo "改湿量: WET=0.5 bash 脚本/安装.sh (重建后生效)"
echo "改目标电平: echo '{\"on\": true, \"target_db\": -16}' > ~/.local/state/cinema-spatial/loudness.json"