From 0dc55ddcb56906ffd4e1f80bc952c0a29d295698 Mon Sep 17 00:00:00 2001 From: lou Date: Sat, 19 Sep 2026 18:16:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BD=92=E4=B8=80=E5=8C=96=E6=8E=A5=E5=9C=A8?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E5=89=8D=E9=9D=A2:=20=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=20->=20=E8=99=9A=E6=8B=9F=E5=A3=B0=E5=8D=A1?= =?UTF-8?q?=20->=20=E5=93=8D=E5=BA=A6=E5=BD=92=E4=B8=80=E5=8C=96=20->=20HR?= =?UTF-8?q?TF(=E6=B9=BF=E9=87=8F0.3)=20->=20=E6=95=B0=E5=AD=97=E8=BE=93?= =?UTF-8?q?=E5=87=BA;=20=E5=AE=89=E8=A3=85=E8=84=9A=E6=9C=AC=E5=8D=95?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E5=8F=AF=E5=A4=8D=E7=8E=B0(=E5=90=AB?= =?UTF-8?q?=E6=AE=8B=E7=95=99DSP=E6=B8=85=E7=90=86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/collaplex-loudness-norm | 2 +- pipewire/10-collaplex-vsink.conf | 32 ++++++-- pipewire/10b-collaplex-loudness.conf | 41 ---------- 脚本/安装.sh | 111 +++++++++++++-------------- 4 files changed, 79 insertions(+), 107 deletions(-) delete mode 100644 pipewire/10b-collaplex-loudness.conf diff --git a/bin/collaplex-loudness-norm b/bin/collaplex-loudness-norm index c66fe53..8097ad6 100644 --- a/bin/collaplex-loudness-norm +++ b/bin/collaplex-loudness-norm @@ -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__" diff --git a/pipewire/10-collaplex-vsink.conf b/pipewire/10-collaplex-vsink.conf index b8853f5..1bc8f56 100644 --- a/pipewire/10-collaplex-vsink.conf +++ b/pipewire/10-collaplex-vsink.conf @@ -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__" } diff --git a/pipewire/10b-collaplex-loudness.conf b/pipewire/10b-collaplex-loudness.conf deleted file mode 100644 index eb426b2..0000000 --- a/pipewire/10b-collaplex-loudness.conf +++ /dev/null @@ -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__" - } - } - } -] diff --git a/脚本/安装.sh b/脚本/安装.sh index 81a5063..defb936 100755 --- a/脚本/安装.sh +++ b/脚本/安装.sh @@ -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/.*= //'