Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 589244d4c3 | |||
| 9d3265aa28 |
@@ -0,0 +1,11 @@
|
||||
# 96kHz 主时钟 —— 2026-09-12 换 SADIE-II H4 96K/512tap 脉冲响应
|
||||
#
|
||||
# 为什么必须强制: PipeWire 的 convolver 会按"当前时钟率"重采样 IR。
|
||||
# 时钟若是 48k, 96k/512tap 的 IR 会被降采样成等效 256tap —— 等于白换。
|
||||
#
|
||||
# 代价: 所有内容重采样到 96k。48k 内容(影片/多数音频)是整数比, 几乎无损;
|
||||
# 44.1k 音乐略有精度损失。CPU 实测富余(48k 时 busy 仅 13us), 故强制。
|
||||
context.properties = {
|
||||
default.clock.rate = 96000
|
||||
default.clock.allowed-rates = [ 96000 ]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# 打 release + 上传 deb 资产到 Gitea (2026-09-12)
|
||||
set -eu
|
||||
cd "/home/lou/桌面/工作区/实验/空间音频"
|
||||
DEB="$HOME/桌面/collaplex-cinema-spatial_1.0.0_all.deb"
|
||||
API="http://8.136.202.225:3000/api/v1/repos/edgevoid/cinema-spatial"
|
||||
AUTH="edgevoid:147427"
|
||||
|
||||
echo "=== 1. 重新打包(含最新脚本) ==="
|
||||
rm -f "$DEB"
|
||||
bash 打包deb.sh 2>&1 | tail -4
|
||||
LOCAL_SIZE=$(stat -c%s "$DEB")
|
||||
LOCAL_MD5=$(md5sum "$DEB" | cut -d' ' -f1)
|
||||
echo "本地: $LOCAL_SIZE 字节 md5=$LOCAL_MD5"
|
||||
|
||||
echo
|
||||
echo "=== 2. 创建 release v1.0.0 ==="
|
||||
python3 - <<'PYEOF' > /tmp/rel_body.json
|
||||
import json
|
||||
body = """## collaplex-cinema-spatial 1.0.0
|
||||
|
||||
\u8033\u673a\u4e0a\u7684\u7535\u5f71\u9662\u7a7a\u95f4\u97f3\u9891 \u2014\u2014 PipeWire filter-chain \u865a\u62df\u58f0\u5361 + SADIE-II \u771f\u4eba HRTF\uff0c\u5168\u94fe\u8def 96kHz/24bit\u3002
|
||||
|
||||
### \u5b89\u88c5
|
||||
|
||||
```
|
||||
sudo dpkg -i collaplex-cinema-spatial_1.0.0_all.deb
|
||||
systemctl --user restart pipewire pipewire-pulse wireplumber
|
||||
```
|
||||
|
||||
\u4f9d\u8d56: pipewire libmysofa1 mpv
|
||||
|
||||
### \u7528\u6cd5
|
||||
|
||||
```
|
||||
\u7a7a\u95f4\u97f3\u9891 \u5f00 # \u5168\u5c40\u5207\u5230\u7acb\u4f53\u58f0\u4e0a\u6df7\u7248(\u63a8\u8350)
|
||||
\u7a7a\u95f4\u97f3\u9891 \u5f005.1 # \u5168\u5c40\u5207\u5230 5.1 \u76f4\u901a\u7248
|
||||
\u7a7a\u95f4\u97f3\u9891 \u5173 # \u5207\u56de\u7269\u7406\u8bbe\u5907
|
||||
\u7a7a\u95f4\u97f3\u9891 \u72b6\u6001 # \u67e5\u770b\u72b6\u6001
|
||||
mpv-\u5f71\u9662 \u7535\u5f71.mkv # \u53ea\u7ed9 mpv \u7528
|
||||
```
|
||||
|
||||
### \u8bf4\u660e
|
||||
|
||||
- HRTF \u7528 SADIE-II \u771f\u4eba\u53d7\u8bd5\u8005 H4\uff0c96K/24bit/512tap
|
||||
- \u7acb\u4f53\u58f0\u6e90\u81ea\u52a8\u77e9\u9635\u4e0a\u6df7\uff0c\u4e0d\u9700\u8981\u591a\u58f0\u9053\u7247\u6e90
|
||||
- \u82e5\u65e0\u58f0: python3 /usr/share/cinema-spatial/\u751f\u6210\u914d\u7f6e.py \u91cd\u65b0\u63a2\u6d4b\u8f93\u51fa\u8bbe\u5907
|
||||
"""
|
||||
print(json.dumps({"tag_name": "v1.0.0", "name": "v1.0.0", "body": body}, ensure_ascii=False))
|
||||
PYEOF
|
||||
|
||||
RELEASE_ID=$(curl -s -X POST "$API/releases" -u "$AUTH" -H "Content-Type: application/json" \
|
||||
--data-binary @/tmp/rel_body.json \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id','')); " 2>/dev/null)
|
||||
echo "release id: $RELEASE_ID"
|
||||
|
||||
echo
|
||||
echo "=== 3. 上传 deb 资产 ==="
|
||||
curl -s -X POST "$API/releases/$RELEASE_ID/assets?name=collaplex-cinema-spatial_1.0.0_all.deb" \
|
||||
-u "$AUTH" -H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$DEB" -o /tmp/upl.json -w "上传 HTTP %{http_code}\n"
|
||||
|
||||
echo
|
||||
echo "=== 4. ★ 逐字节核对 ==="
|
||||
curl -s "$API/releases/$RELEASE_ID" -u "$AUTH" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
print('release:', d.get('tag_name'), '|', d.get('name'))
|
||||
for a in d.get('assets', []):
|
||||
print(' 资产:', a['name'], '|', a['size'], '字节')
|
||||
print(' 下载:', a.get('browser_download_url',''))
|
||||
"
|
||||
echo "本地字节: $LOCAL_SIZE 本地md5: $LOCAL_MD5"
|
||||
@@ -32,7 +32,7 @@ echo "=== 5. PipeWire 配置 (用安装路径生成) ==="
|
||||
CINEMA_SPATIAL_IR="${ROOT}/hrir" \
|
||||
CINEMA_SPATIAL_OUT="$BUILD/usr/share/pipewire/pipewire.conf.d/90-cinema-spatial.conf" \
|
||||
python3 "$SRC/生成配置.py"
|
||||
cp "$HOME/.config/pipewire/pipewire.conf.d/91-clock.conf" \
|
||||
cp "$SRC/91-clock.conf" \
|
||||
"$BUILD/usr/share/pipewire/pipewire.conf.d/"
|
||||
|
||||
echo "=== 6. control ==="
|
||||
|
||||
@@ -16,6 +16,7 @@ _ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
# IR 目录: 环境变量优先(打包/安装场景指 /usr/share/cinema-spatial/hrir),
|
||||
# 否则用项目内的软链 hrir/current (开发时换 HRTF 只改软链)。
|
||||
IR = os.environ.get("CINEMA_SPATIAL_IR") or os.path.join(_ROOT, "hrir", "current")
|
||||
|
||||
OUT = os.environ.get("CINEMA_SPATIAL_OUT") or os.path.expanduser(
|
||||
"~/.config/pipewire/pipewire.conf.d/90-cinema-spatial.conf"
|
||||
)
|
||||
@@ -68,6 +69,15 @@ def default_sink_name() -> str:
|
||||
return first
|
||||
|
||||
|
||||
# node.target —— filter-chain 的输出端必须送到"真实设备":
|
||||
# - @DEFAULT_SINK@ 这类 pactl 占位符 PipeWire 不认(会当成设备名, 结果连到别的设备)
|
||||
# - 完全不写则 WirePlumber 可能连错(实测连到 HDMI)
|
||||
# 所以默认探测物理输出(优先 USB), 换设备后重跑本脚本即可。
|
||||
# 也可 export CINEMA_SPATIAL_TARGET=<node.name> 强制指定。
|
||||
_TARGET = os.environ.get("CINEMA_SPATIAL_TARGET") or default_sink_name()
|
||||
TARGET_PROP = f'node.target = "{_TARGET}" ' if _TARGET else ""
|
||||
|
||||
|
||||
def hrir_block(indent: str) -> list[str]:
|
||||
"""12 个卷积器 + 6 个分发 + 左右耳混音。输入端口: cp<dir>:In"""
|
||||
L: list[str] = []
|
||||
@@ -107,14 +117,14 @@ def sink_51() -> list[str]:
|
||||
' outputs = [ "mixL:Out" "mixR:Out" ]', " }",
|
||||
" capture.props = { node.name = cinema_spatial_sink media.class = Audio/Sink "
|
||||
"audio.channels = 6 audio.position = [ FL FR FC LFE BL BR ] }",
|
||||
f' playback.props = {{ node.name = cinema_spatial_out node.passive = true node.target = "{default_sink_name()}" '
|
||||
f' playback.props = {{ node.name = cinema_spatial_out {TARGET_PROP}'
|
||||
"audio.channels = 2 audio.position = [ FL FR ] }",
|
||||
" }", " }"]
|
||||
return L
|
||||
|
||||
|
||||
def sink_upmix() -> list[str]:
|
||||
tgt = default_sink_name()
|
||||
|
||||
L = [" { name = libpipewire-module-filter-chain", " flags = [ nofail ]", " args = {",
|
||||
' node.description = "电影院空间音频 (立体声上混)"',
|
||||
' media.name = "Cinema Spatial Upmix"', " filter.graph = {", " nodes = [",
|
||||
@@ -157,7 +167,7 @@ def sink_upmix() -> list[str]:
|
||||
' outputs = [ "mixL:Out" "mixR:Out" ]', " }",
|
||||
" capture.props = { node.name = cinema_spatial_up_sink media.class = Audio/Sink "
|
||||
"audio.channels = 2 audio.position = [ FL FR ] }",
|
||||
f' playback.props = {{ node.name = cinema_spatial_up_out node.passive = true node.target = "{tgt}" '
|
||||
f' playback.props = {{ node.name = cinema_spatial_up_out {TARGET_PROP}'
|
||||
"audio.channels = 2 audio.position = [ FL FR ] }",
|
||||
" }", " }"]
|
||||
return L
|
||||
@@ -172,7 +182,7 @@ def main() -> int:
|
||||
with open(OUT, "w", encoding="utf-8") as fh:
|
||||
fh.write("\n".join(body))
|
||||
print(f"已生成 {OUT} ({len(body)} 行)")
|
||||
print(f"输出目标: {default_sink_name()}")
|
||||
print(f"输出目标: {TARGET_PROP.replace('node.target = ', '').strip() or '自动(WirePlumber 路由到默认输出)'}")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
# 电影院空间音频 开关 2026-09-12
|
||||
# 用法: 空间音频 开 | 开5.1 | 关 | 状态
|
||||
# 开 = 立体声上混版(全局推荐; 浏览器/音乐/在线视频等立体声源也会有环绕)
|
||||
# 开5.1 = 5.1 直通版(本地多声道片源; 也可以用 mpv-影院)
|
||||
#
|
||||
# ★ 为什么"开"的时候要做设备自检:
|
||||
# filter-chain 的输出端必须显式指向真实设备(node.target)。但 USB 耳机重启/重插后
|
||||
# profile 可能变(如 iec958-stereo <-> analog-stereo), 设备名随之改变, 配置里的
|
||||
# 旧名字就失效 -> WirePlumber 回落到其它设备(实测落到 HDMI), 声音就跑了。
|
||||
# 所以每次"开"都检查一遍: 目标不存在就重新探测并重建配置。
|
||||
set -u
|
||||
STATE="${XDG_RUNTIME_DIR:-/tmp}/cinema-spatial.prev"
|
||||
CONF="${HOME}/.config/pipewire/pipewire.conf.d/90-cinema-spatial.conf"
|
||||
for g in "${HOME}/桌面/工作区/实验/空间音频/生成配置.py" "/usr/share/cinema-spatial/生成配置.py"; do
|
||||
[ -f "$g" ] && GEN="$g" && break
|
||||
done
|
||||
GEN="${GEN:-}"
|
||||
|
||||
num_of() { awk '{for (i = 1; i <= NF; i++) if ($i ~ /^[0-9]+\.$/) {print substr($i, 1, length($i) - 1); exit}}'; }
|
||||
id_of() { wpctl status 2>/dev/null | awk -v n="$1" '$0 ~ n {print; exit}' | num_of; }
|
||||
cur_id() { wpctl status 2>/dev/null | sed -n '/Sinks:/,/Sources:/p' | grep ' \* ' | num_of; }
|
||||
nick() { wpctl inspect "$1" 2>/dev/null | grep -oP 'node\.nick = "\K[^"]+' | head -1; }
|
||||
|
||||
# 物理输出设备名: 优先 USB, 跳过虚拟声卡与 HDMI/DP 专业输出
|
||||
phys_sink() {
|
||||
pw-dump 2>/dev/null | python3 -c '
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
c = []
|
||||
for o in d:
|
||||
if o.get("type") != "PipeWire:Interface:Node":
|
||||
continue
|
||||
p = o.get("info", {}).get("props", {})
|
||||
if p.get("media.class") != "Audio/Sink":
|
||||
continue
|
||||
n = p.get("node.name", "")
|
||||
if not n or "cinema_spatial" in n or "pro-output" in n:
|
||||
continue
|
||||
c.append(n)
|
||||
print((sorted(c, key=lambda x: (0 if "usb" in x else 1, x)) or [""])[0])
|
||||
' 2>/dev/null
|
||||
}
|
||||
|
||||
conf_target() { grep -oP 'node\.target = "\K[^"]+' "$CONF" 2>/dev/null | head -1; }
|
||||
|
||||
# 设备是否真的存在(按 node.name 精确匹配)
|
||||
# ★ 别用 pw-cli info: 它对不存在的名字也返回成功, 会误判成"设备还在"
|
||||
target_alive() {
|
||||
local t; t=$(conf_target)
|
||||
[ -n "$t" ] || return 1
|
||||
pw-dump 2>/dev/null | python3 -c '
|
||||
import sys, json
|
||||
t = sys.argv[1]
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
for o in d:
|
||||
p = o.get("info", {}).get("props", {})
|
||||
if p.get("node.name") == t:
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
' "$t"
|
||||
}
|
||||
|
||||
# 目标失效 -> 重新探测并重建配置
|
||||
rebuild_if_stale() {
|
||||
target_alive && return 0
|
||||
[ -n "$GEN" ] || { echo " ! 配置里的输出设备已失效, 且找不到 生成配置.py, 无法自动修复"; return 1; }
|
||||
echo " · 输出设备已变化, 正在重新探测并重建配置..."
|
||||
python3 "$GEN" >/dev/null 2>&1 || { echo " ! 重建失败"; return 1; }
|
||||
systemctl --user restart pipewire pipewire-pulse wireplumber >/dev/null 2>&1
|
||||
sleep 4
|
||||
target_alive && echo " · 已修复, 输出指向: $(conf_target)"
|
||||
}
|
||||
|
||||
case "${1:-状态}" in
|
||||
开|on) VNAME=cinema_spatial_up_sink; LABEL="立体声上混" ;;
|
||||
开5.1|on51) VNAME=cinema_spatial_sink; LABEL="5.1 直通" ;;
|
||||
关|off) VNAME=""; LABEL="关" ;;
|
||||
状态|"") VNAME=""; LABEL="状态" ;;
|
||||
*) echo "用法: 空间音频 {开|开5.1|关|状态}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
if [ -n "$VNAME" ]; then
|
||||
rebuild_if_stale
|
||||
v=$(id_of "$VNAME")
|
||||
if [ -z "$v" ]; then
|
||||
echo "虚拟声卡 $VNAME 没加载。检查 $CONF"
|
||||
exit 1
|
||||
fi
|
||||
cur=$(cur_id)
|
||||
[ -n "$cur" ] && echo "$cur" > "$STATE"
|
||||
wpctl set-default "$v"
|
||||
# 耳机自身的音量若被压过, 这里不动它(用户自己的选择)
|
||||
echo "已切到「电影院空间音频 ($LABEL)」(id $v)"
|
||||
echo " 开之前是「$(nick "${cur:-0}")」"
|
||||
echo " 输出设备 : $(conf_target)"
|
||||
elif [ "$LABEL" = "关" ]; then
|
||||
prev=$(cat "$STATE" 2>/dev/null || true)
|
||||
if [ -n "${prev:-}" ] && wpctl inspect "$prev" >/dev/null 2>&1; then
|
||||
wpctl set-default "$prev"
|
||||
echo "已切回「$(nick "$prev")」(id $prev)"
|
||||
else
|
||||
fb=$(wpctl status 2>/dev/null | sed -n '/Sinks:/,/Sources:/p' | grep '\.' | head -1 | num_of)
|
||||
wpctl set-default "$fb"
|
||||
echo "切换回第一个物理输出「$(nick "$fb")」(id $fb)"
|
||||
fi
|
||||
else
|
||||
echo "虚拟声卡:"
|
||||
echo " 5.1 直通 : id $(id_of cinema_spatial_sink || true)"
|
||||
echo " 立体声上混: id $(id_of cinema_spatial_up_sink || true)"
|
||||
echo "当前默认 : $(wpctl inspect @DEFAULT_AUDIO_SINK@ 2>/dev/null | grep -oP 'node\.description = "\K[^"]+' | head -1)"
|
||||
t=$(conf_target)
|
||||
echo "输出设备 : ${t:-<未设置>}"
|
||||
if target_alive; then echo " ✓ 设备存在"; else echo " ✗ 设备已失效(下次'开'会自动重建)"; fi
|
||||
echo "物理输出 : $(phys_sink)"
|
||||
fi
|
||||
Reference in New Issue
Block a user