归一化接在核心前面: 全局音频 -> 虚拟声卡 -> 响度归一化 -> HRTF(湿量0.3) -> 数字输出; 安装脚本单脚本可复现(含残留DSP清理)

This commit is contained in:
lou
2026-09-19 18:16:43 +08:00
parent 1b93498096
commit 0dc55ddcb5
4 changed files with 79 additions and 107 deletions
+1 -1
View File
@@ -11,6 +11,6 @@ export LN_GATE_DB=-50
export LN_MAX_BOOST=20
export LN_MAX_CUT=20
export LN_SLEW_DB_S=6
export LN_RATE=48000
export LN_RATE=96000
export LN_IDLE_LIMIT=999999
exec /usr/bin/python3 "__DSP__"
+26 -6
View File
@@ -1,18 +1,38 @@
# Collaplex 虚拟声卡(纯转发) —— 全局音频 -> 虚拟声卡 -> HRTF 独立实例 -> 数字输出
# 占位符: __HRTF_IN__(接入 HRTF 的 sink 名), 由 脚本/安装.sh 渲染
# Collaplex 虚拟声卡 + 响度归一化 —— 本文件由 脚本/安装.sh 生成(占位符: __HRTF_IN__ / __WRAPPER__)
# 链路: 全局音频(应用输出) -> collaplex_vsink(虚拟声卡) -> 响度归一化(pipe DSP) -> 数字输出(S/PDIF)
context.modules = [
{ name = libpipewire-module-loopback
{ name = libpipewire-module-filter-chain
flags = [ nofail ]
args = {
node.description = "Collaplex 虚拟声卡"
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.channels = 2
audio.position = [ FL FR ]
}
playback.props = {
node.name = collaplex_vsink_out
audio.channels = 2
audio.position = [ FL FR ]
node.target = "__HRTF_IN__"
}
-41
View File
@@ -1,41 +0,0 @@
# 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__"
}
}
}
]
+52 -59
View File
@@ -1,81 +1,74 @@
#!/bin/bash
# Collaplex 音效 — 把链路固化装到 PipeWire(用户级)
#
# 链路: 全局音频(应用输出) -> collaplex_vsink(虚拟声卡) -> collaplex_hrtf_in(双耳 HRTF + 房间混响, 湿量 $WET) -> 数字输出
# 链路: 全局音频(应用输出) -> collaplex_vsink(虚拟声卡)
# -> 响度归一化(pipe DSP) -> collaplex_hrtf_in(双耳 HRTF + 房间混响) -> 数字输出
#
# 装两个 filter/loopback 实例(各自独立, 靠 node.target 串成一条链):
# ~/.config/pipewire/pipewire.conf.d/10-collaplex-vsink.conf 虚拟声卡(纯转发)
# ~/.config/pipewire/pipewire.conf.d/20-collaplex-hrtf.conf HRTF + 混响(独立实例, 有接入/输出)
# 用法: bash 脚本/安装.sh 湿量可覆盖: WET=0.15 bash 脚本/安装.sh
# 装什么:
# $HOME/.local/bin/collaplex-loudness-norm pipe 插件入口(指向本项目 dsp/)
# $HOME/.config/pipewire/pipewire.conf.d/10-...conf 虚拟声卡 + 响度归一化
# $HOME/.config/pipewire/pipewire.conf.d/20-...conf HRTF 核心(接入 + 输出)
#
# 湿量: WET=0.3 环境变量覆盖(默认 0.3)
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CONF_DIR="$HOME/.config/pipewire/pipewire.conf.d"
WRAPPER="$HOME/.local/bin/collaplex-loudness-norm"
WET="${WET:-0.3}"
HRIR="$ROOT/hrir"
REVERB="$ROOT/reverb/房间混响IR-96k.wav"
HRTF_IN="collaplex_hrtf_in"
echo "== Collaplex 音效 - 固化安装 =="
echo "项目: $ROOT"
[ -f "$REVERB" ] || { echo "!! 缺房间 IR: $REVERB"; exit 1; }
[ -f "$HRIR/FL_L.wav" ] || { echo "!! 缺 HRIR: $HRIR"; exit 1; }
# ---- 1. 探测数字输出(S/PDIF) ----
TARGET="$(pw-dump | /usr/bin/python3 -c '
import json,sys
best=""
# ---- 1. 探测数字输出 ----
TARGET=$(pw-dump | /usr/bin/python3 -c '
import json, sys
for n in json.load(sys.stdin):
if n.get("type")!="PipeWire:Interface:Node":
if n.get("type") != "PipeWire:Interface:Node":
continue
p=(n.get("info") or {}).get("props") or {}
nm=p.get("node.name") or ""
if p.get("media.class")=="Audio/Sink" and ("iec958" in nm or "digital" in nm.lower()):
best=nm
print(best)
')"
[ -n "$TARGET" ] || { echo "!! 找不到数字输出设备(pw-dump 里没有 iec958/digital sink)"; exit 1; }
p = (n.get("info") or {}).get("props") or {}
if p.get("media.class") == "Audio/Sink":
nm = p.get("node.name") or ""
if "iec958" in nm or "digital" in nm.lower():
print(nm)
break
')
[ -n "$TARGET" ] || { echo "找不到数字输出设备(iec958)"; exit 1; }
echo "数字输出: $TARGET"
echo "HRTF 湿量: $WET"
# ---- 2. 渲染并安装两份配置 ----
# ---- 2. 部署 pipe 插件入口(必须 ASCII 路径) ----
mkdir -p "$(dirname "$WRAPPER")"
sed -e "s|__DSP__|$ROOT/dsp/loudness_norm.py|" "$ROOT/bin/collaplex-loudness-norm" > "$WRAPPER"
chmod 755 "$WRAPPER"
echo "已装入口: $WRAPPER"
# ---- 3. 渲染两份配置 ----
mkdir -p "$CONF_DIR"
sed -e "s|__HRTF_IN__|$HRTF_IN|" \
sed -e "s|__WRAPPER__|$WRAPPER|" -e "s|__HRTF_IN__|$HRTF_IN|" \
"$ROOT/pipewire/10-collaplex-vsink.conf" > "$CONF_DIR/10-collaplex-vsink.conf"
sed -e "s|__HRIR__|$HRIR|" -e "s|__REVERB__|$REVERB|" \
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 "已装配置:"
echo " $CONF_DIR/10-collaplex-vsink.conf"
echo " $CONF_DIR/20-collaplex-hrtf.conf"
echo "已装配置: 10-collaplex-vsink.conf(湿量不用) / 20-collaplex-hrtf.conf(湿量 $WET)"
# ---- 3. 重新加载 ----
systemctl --user restart pipewire pipewire-pulse wireplumber
sleep 7
# ---- 4. 自检 ----
echo "---- 自检 ----"
MISS=0
for n in collaplex_vsink collaplex_vsink_out "$HRTF_IN" collaplex_hrtf_out; do
if pw-dump | grep -q "\"node.name\": \"$n\""; then
echo "$n"
else
echo "$n 缺失"; MISS=1
fi
# ---- 4. 重启前清掉残留 DSP(占住共享槽位会让新实例起不来) ----
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 -9 "${p#/proc/}" 2>/dev/null || true;;
esac
done
DEF="$(pw-dump | /usr/bin/python3 -c '
import json,sys
for n in json.load(sys.stdin):
if n.get("type")=="PipeWire:Interface:Metadata":
continue
' 2>/dev/null; wpctl inspect @DEFAULT_SINK@ 2>/dev/null | grep -m1 "node.name" | cut -d= -f2 | tr -d " \"")"
echo " 默认输出: ${DEF:-未知}"
sleep 1
if [ "$MISS" = "0" ]; then
echo "完成。桌面/浏览器的声音现在都会走: 虚拟声卡 -> HRTF -> 数字输出。"
echo "单元测试: pw-play --target collaplex_hrtf_in 某个.wav (只听 HRTF 这一级)"
else
echo "!! 有节点没起来, 看: journalctl --user -u pipewire -n 50"
exit 1
fi
# ---- 5. 重载 ----
systemctl --user restart pipewire pipewire-pulse wireplumber
sleep 8
# ---- 6. 自检 ----
echo "=== 自检 ==="
echo -n "节点: "; pw-dump | grep -o '"node.name": "collaplex[^"]*"' | sort -u | tr '\n' ' '; echo
n=0
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*) n=$((n+1));; esac
done
echo "DSP 进程数: $n (应为 2)"
echo -n "默认输出: "; wpctl inspect @DEFAULT_SINK@ 2>/dev/null | grep -m1 "node.name" | sed 's/.*= //'