Music Players Bug排障和个人推荐
配置
Laptop: HP Omen 16-wf0000
CPU: Intel i5 13500HX
GPU: NVIDIA RTX4060 Laptop 8G
Kernel: Linux CachyOS Kernel
System: EndeavourOS Linux
Bootloader: systemd-boot
WM: Hyprland前言
本篇不是干货。
首先先说本人对于音乐播放器的需求。大概在2023年左右彻底放弃了用流媒体听歌,只有在试听的时候会上utb Music,在看评论的时候上网易云,虽说现在也基本转战rateyourmusic或者干脆不看乐评管自己听了。音源从Soulseek(现在是Nicotine+)上下44.1k甚至96k和192k的.flac文件,有的时候运气好还能扒到DSP文件。
按下不表,所以我个人对于本地音乐软件的需求是:
Last.fm支持,现在还得加一个ListenBrainz支持
对不同的采样频率支持无问题
无缝切歌,对于一张完整的专辑来说无缝基本可以说是必不可少的功能了
ReplayGain支持,有扫描自然是更好
目前来说在我尝试过的十几个音乐播放器当中,Quod Libet是有且仅有我想要的这些所有功能的,虽然我一直对于它后端的GStreamer有着奇怪的洁癖,但是鉴pw-top里面显示输出的采样率和歌曲文件本身是完美匹配的,所以我也没有太在意,能用就行。
但是滚动发行版魅力时刻了。大概在14号左右的一次-Syu,甚至更早的时候,wireplumber,或者GStreamer本身,或者ALSA等等,导致GStreamer这边出现了问题。
rg "2026-01-14" /var/log/pacman.log | grep -E "upgraded (pipewire|gstreamer|libpulse|alsa)"
(pipewire|gstreamer|libpulse|alsa)"
[2026-01-14T20:17:35+0800] [ALPM] upgraded alsa-ucm-conf (1.2.15.1-1 -> 1.2.15.2-1)
[2026-01-14T20:17:35+0800] [ALPM] upgraded alsa-lib (1.2.15.1-1 -> 1.2.15.2-1)
[2026-01-14T20:17:35+0800] [ALPM] upgraded alsa-utils (1.2.15.1-1 -> 1.2.15.2-1)
[2026-01-14T20:17:51+0800] [ALPM] upgraded gstreamer (1.26.10-1 -> 1.26.10-3)
……
[2026-01-03T17:07:54+0800] [ALPM] upgraded libwireplumber (0.5.12-1 -> 0.5.13-1)
[2026-01-03T17:07:54+0800] [ALPM] upgraded wireplumber (0.5.12-1 -> 0.5.13-1)这个是事后抓到的时候才下的定论,但是问题初次出现的时候,并不清楚是什么导致的,所以回到当时初发状况的时候。
1. 问题
我本人是有边玩游戏边听歌的习惯的,尤其是和朋友一起玩的时候,在Quod Libet里面会把音量拉到大约一半或者更小的值,让歌充当背景,不会盖过朋友的语音或者是游戏本身的声音,尤其是玩ATM9或者Terraria或者无人深空这种相对Chill的游戏的时候。问题就出在大约14号那个时候,15号考完的试,当天晚上和朋友一起玩Terraria。本来好好的没啥问题,结果就在我切歌的时候,不知道音量被自动切到了最大。当时吓一跳,但是以为是自己误触到了什么按键,调回去就没再管它了。但是之后问题不停触发不停复现,虽然不影响听但是在打游戏的时候,这种状况频出非常恼人,尤其是在下地牢打的时候。非常干扰。
2.排障
最开始认为是WirePlumber的问题,毕竟之前也在音频上遇到过奇奇怪怪的问题,尤其是刚刚滚动更新过,大概率是旧的状态文件和新版本不兼容了,可能导致其在切歌的时候新建了一个流式管道,导致音量被重置到了100。
先清理WirePlumber的本地状态。保险起见,先把服务停了再清理状态。
systemctl --user stop wireplumber pipewire pipewire-pulse
rm -rf ~/.local/state/wireplumber
systemctl --user start wireplumber pipewire pipewire-pulse问题似乎暂时解决了,连续切了好几次歌都没出问题。当时以为就是个小问题,就没有继续管他,管自己高兴玩Terraria去了。
第二次问题复现还是玩Terraria,在18号晚上。问题稳定复现,但并不是每次切歌都会出现,问题的出现似乎带有奇怪的随机性。本来怀疑是不是更新之后匹配本地不同比特率文件的时候出现的,测试过后发现和比特率毛线关系都没有。
直接pw-mon抓底层日志pw-mon > pw_debug.log方便筛选和后来查看。在终端激活命令之后,立马在Quod Libet开始切歌等到问题复现的时候切回终端。
grep -n -B 5 -A 100 'application.name = "Quod Libet"' ~/pw_debug.log > ~/ql_filtered.log
完整pw_debug十万多条长,等全部看完我电脑自己把问题先修好了。筛选出Quod Libet字段前后100行并且导出为ql_filtered.log查找病症。
added:
id: 79
permissions: r-xm-
type: PipeWire:Interface:Port (version 3)
direction: "output"
params:
* id:3 (Spa:Enum:ParamId:EnumFormat)
audio/dsp
format : (Id) F32P
* id:6 (Spa:Enum:ParamId:Meta)
Object: size 56, type Spa:Pod:Object:Param:Meta (262149), id Spa:Enum:ParamId:Meta (6)
Prop: key Spa:Pod:Object:Param:Meta:type (1), flags 00000000
Id 1 (Spa:Pointer:Meta:Header)
Prop: key Spa:Pod:Object:Param:Meta:size (2), flags 00000000
Int 32
* id:7 (Spa:Enum:ParamId:IO)
Object: size 56, type Spa:Pod:Object:Param:IO (262150), id Spa:Enum:ParamId:IO (7)
Prop: key Spa:Pod:Object:Param:IO:id (1), flags 00000000
Id 1 (Spa:Enum:IO:Buffers)
Prop: key Spa:Pod:Object:Param:IO:size (2), flags 00000000
Int 8
* id:7 (Spa:Enum:ParamId:IO)
Object: size 56, type Spa:Pod:Object:Param:IO (262150), id Spa:Enum:ParamId:IO (7)
Prop: key Spa:Pod:Object:Param:IO:id (1), flags 00000000
Id 10 (Spa:Enum:IO:AsyncBuffers)
Prop: key Spa:Pod:Object:Param:IO:size (2), flags 00000000
Int 8
* id:4 (Spa:Enum:ParamId:Format)
audio/dsp
format : (Id) F32P
* id:5 (Spa:Enum:ParamId:Buffers)
Object: size 152, type Spa:Pod:Object:Param:Buffers (262148), id Spa:Enum:ParamId:Buffers (5)
Prop: key Spa:Pod:Object:Param:Buffers:buffers (1), flags 00000000
Choice: type Spa:Enum:Choice:Range, flags 00000000 28 4
Int 1
Int 1
Int 32
Prop: key Spa:Pod:Object:Param:Buffers:blocks (2), flags 00000000
Int 1
Prop: key Spa:Pod:Object:Param:Buffers:BlockInfo:size (3), flags 00000000
Choice: type Spa:Enum:Choice:Range, flags 00000000 28 4
Int 32768
Int 64
Int 2147483647
Prop: key Spa:Pod:Object:Param:Buffers:BlockInfo:stride (4), flags 00000000
Int 4
* id:15 (Spa:Enum:ParamId:Latency)
Object: size 176, type Spa:Pod:Object:Param:Latency (262155), id Spa:Enum:ParamId:Latency (15)
Prop: key Spa:Pod:Object:Param:Latency:direction (1), flags 00000000
Id 0 (Spa:Enum:Direction:Input)
Prop: key Spa:Pod:Object:Param:Latency:minQuantum (2), flags 00000000
Float 1.000000
Prop: key Spa:Pod:Object:Param:Latency:maxQuantum (3), flags 00000000
Float 1.000000
Prop: key Spa:Pod:Object:Param:Latency:minRate (4), flags 00000000
Int 512
Prop: key Spa:Pod:Object:Param:Latency:maxRate (5), flags 00000000
Int 512
Prop: key Spa:Pod:Object:Param:Latency:minNs (6), flags 00000000
Long 0
Prop: key Spa:Pod:Object:Param:Latency:maxNs (7), flags 00000000
Long 0
* id:15 (Spa:Enum:ParamId:Latency)
Object: size 176, type Spa:Pod:Object:Param:Latency (262155), id Spa:Enum:ParamId:Latency (15)
Prop: key Spa:Pod:Object:Param:Latency:direction (1), flags 00000000
Id 1 (Spa:Enum:Direction:Output)
Prop: key Spa:Pod:Object:Param:Latency:minQuantum (2), flags 00000000
Float 0.000000
Prop: key Spa:Pod:Object:Param:Latency:maxQuantum (3), flags 00000000
Float 0.000000
Prop: key Spa:Pod:Object:Param:Latency:minRate (4), flags 00000000
Int 0
Prop: key Spa:Pod:Object:Param:Latency:maxRate (5), flags 00000000
Int 0
Prop: key Spa:Pod:Object:Param:Latency:minNs (6), flags 00000000
Long 0
Prop: key Spa:Pod:Object:Param:Latency:maxNs (7), flags 00000000
Long 0
* id:17 (Spa:Enum:ParamId:Tag)
Object: size 336, type Spa:Pod:Object:Param:Tag (262157), id Spa:Enum:ParamId:Tag (17)
Prop: key Spa:Pod:Object:Param:Tag:direction (1), flags 00000000
Id 1 (Spa:Enum:Direction:Output)
Prop: key Spa:Pod:Object:Param:Tag:info (2), flags 00000004
Struct: size 288
Int 5
String "media.role"
String "Music"
String "media.name"
String "'Monkey Trick' by 'the Jesus Lizard'"
String "media.class"
String "Stream/Output/Audio"
String "media.title"
String "Monkey Trick"
String "media.artist"
String "the Jesus Lizard"
properties:
format.dsp = "32 bit float mono audio"
audio.channel = "FL"
port.group = "stream.0"
port.id = "0"
port.direction = "out"
object.path = "Quod Libet:output_0"
port.name = "output_FL"
port.alias = "Quod Libet:output_FL"
node.id = "76"
object.id = "79"
object.serial = "23069"
changed:
id: 76
permissions: rwxm-
type: PipeWire:Interface:Node (version 3)
params:
id:3 (Spa:Enum:ParamId:EnumFormat)
audio/raw
format : (Id) S16LE
rate : (Int) 44100
channels : (Int) 2
position : (Array) < FL, FR >
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 104, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:id (1), flags 00000000
Id 65539 (Spa:Pod:Object:Param:Props:volume)
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Volume"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Range, flags 00000000 28 4
Float 1.000000
Float 0.000000
Float 10.000000
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 104, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:id (1), flags 00000000
Id 65540 (Spa:Pod:Object:Param:Props:mute)
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Mute"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Enum, flags 00000000 28 4
Bool false
Bool false
Bool true
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 136, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:id (1), flags 00000000
Id 65544 (Spa:Pod:Object:Param:Props:channelVolumes)
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Channel Volumes"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Range, flags 00000000 28 4
Float 1.000000
Float 0.000000
Float 10.000000
Prop: key Spa:Pod:Object:Param:PropInfo:container (5), flags 00000000
Id 13 (Spa:Array)
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 112, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:id (1), flags 00000000
Id 65547 (Spa:Pod:Object:Param:Props:channelMap)
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Channel Map"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Id 0 (Spa:)
Prop: key Spa:Pod:Object:Param:PropInfo:container (5), flags 00000000
Id 13 (Spa:Array)
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 112, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:id (1), flags 00000000
Id 65548 (Spa:Pod:Object:Param:Props:monitorMute)
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Monitor Mute"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Enum, flags 00000000 28 4
Bool false
Bool false
Bool true
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 136, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:id (1), flags 00000000
Id 65549 (Spa:Pod:Object:Param:Props:monitorVolumes)
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Monitor Volumes"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Range, flags 00000000 28 4
Float 1.000000
Float 0.000000
Float 10.000000
Prop: key Spa:Pod:Object:Param:PropInfo:container (5), flags 00000000
Id 13 (Spa:Array)
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 112, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:id (1), flags 00000000
Id 65551 (Spa:Pod:Object:Param:Props:softMute)
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Soft Mute"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Enum, flags 00000000 28 4
Bool false
Bool false
Bool true
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 136, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:id (1), flags 00000000
Id 65552 (Spa:Pod:Object:Param:Props:softVolumes)
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Soft Volumes"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Range, flags 00000000 28 4
Float 1.000000
Float 0.000000
Float 10.000000
Prop: key Spa:Pod:Object:Param:PropInfo:container (5), flags 00000000
Id 13 (Spa:Array)
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 160, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:name (2), flags 00000000
String "monitor.channel-volumes"
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Monitor channel volume"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Enum, flags 00000000 28 4
Bool false
Bool false
Bool true
Prop: key Spa:Pod:Object:Param:PropInfo:params (6), flags 00000000
Bool true
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 160, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:name (2), flags 00000000
String "channelmix.disable"
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Disable Channel mixing"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Enum, flags 00000000 28 4
Bool false
Bool false
Bool true
Prop: key Spa:Pod:Object:Param:PropInfo:params (6), flags 00000000
Bool true
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 160, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:name (2), flags 00000000
String "channelmix.min-volume"
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Minimum volume level"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Range, flags 00000000 28 4
Float 0.000000
Float 0.000000
Float 10.000000
Prop: key Spa:Pod:Object:Param:PropInfo:params (6), flags 00000000
Bool true
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 160, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:name (2), flags 00000000
String "channelmix.max-volume"
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Maximum volume level"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Range, flags 00000000 28 4
Float 10.000000
Float 0.000000
Float 10.000000
Prop: key Spa:Pod:Object:Param:PropInfo:params (6), flags 00000000
Bool true
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 160, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:name (2), flags 00000000
String "channelmix.normalize"
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Normalize Volumes"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Enum, flags 00000000 28 4
Bool false
Bool false
Bool true
Prop: key Spa:Pod:Object:Param:PropInfo:params (6), flags 00000000
Bool true
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 160, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:name (2), flags 00000000
String "channelmix.mix-lfe"
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Mix LFE into channels"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Enum, flags 00000000 28 4
Bool true
Bool true
Bool false
Prop: key Spa:Pod:Object:Param:PropInfo:params (6), flags 00000000
Bool true
id:1 (Spa:Enum:ParamId:PropInfo)
Object: size 152, type Spa:Pod:Object:Param:PropInfo (262145), id Spa:Enum:ParamId:PropInfo (1)
Prop: key Spa:Pod:Object:Param:PropInfo:name (2), flags 00000000
String "channelmix.upmix"
Prop: key Spa:Pod:Object:Param:PropInfo:description (7), flags 00000000
String "Enable upmixing"
Prop: key Spa:Pod:Object:Param:PropInfo:type (3), flags 00000000
Choice: type Spa:Enum:Choice:Enum, flags 00000000 28 4
Bool true
Bool true
Bool false
Prop: key Spa:Pod:Object:Param:PropInfo:params (6), flags 00000000
Bool true日志片段。其中最可疑的就add: id: 79changed: id: 76这几段。
日志显示切歌时,PipeWire 里的端口 (79/81) 被移除并重新添加了。这意味着 Quod Libet 使用的 GStreamer 后端在每一首歌结束时,都会彻底销毁当前的音频流,然后为下一首歌建立一个全新的音频流。对于WirePlumber来说,它看到的不是软件名称一样,而是上一个音频流被销毁来了个新的音频流。所以WirePlumber将Quod Libet切歌时的事件当做了一个全新的事件来处理,按照默认规则将其默认音量设置为了100%。它实际上发出了一PropInfo变更信号,重新协商了属性,导致id为65539的音量属性被重置回了默认Float 1.000000。
2.解决方案
最开始我想的是修改Quod Libet的输出管道配置,强迫其使用相对来说更稳定的PulseAudio Sink,或者明确状态让WirePlumber能针对软件名字同步音频流。
先试试PulseAudio Sink,在Quod Libet的参数里面加上:
pulsesink client-name=QuodLibet stream-properties="props,media.role=Music"
重启Quod Libet,问题复现,说明即便是使用了PulseAudio Sink也无法解决。其实我估计看到这里的都在等待一个针对Quod Libet的可修复方案,但是我觉得光是一个pw-mon抓不出来最根本的原因,而且我也懒得再花费精力去看WirePlumber那一堆文档了。更何况本身我对Quod Libet后端所使用的GStreamer就有一点赛博洁癖,所以想来想去,干脆直接换一个不使用GStreamer的后端播放器好了。但是在这之前,至少先把导致的具体原因先搞清楚。
2.5 bug诱发的具体原因?
回到前文展示的1-14号更新:GStreamer, ALSA的lib, utils和ucm-conf。GStreamer的主版本号完全没变过,只有pkgrel变了,说明GStreamer完全没有新的代码更改,完全是因为这个包Arch Linux官方的维护者因为ALSA上游包更新了所以手动触发了一次重新编译。关键更新在于ALSA的三个组件alsa-lib, alsa-utilsalsa-ucm-conf。去GitHub找他们的commit和对应的代码更改:



从上到下依次是alsa-lib, alsa-utilsalsa-ucm-conf。alsa-lib和alsa-utils的更新大多是修修补补,不太涉及核心的设备初始化逻辑。唯alsa-ucm-conf中:
先2.里面的代码变动。

(顺便还抓到一个bug:他们把Comment写成omment了)
在这个commit里面,几乎所有的文件(例ucm2/USB-Audio/AllenAndHeath/Zedi10.confucm2/USB-Audio/Arturia/Minifuse-12.conf等)。他们原先的逻辑是:先定义两个静态的全局变量,分别是播放通道数=2和录音通道数=2,然后通过Include加载一个通用的静direct.conf脚本。这种方式说实话,很像早期的C语言Coding,依赖一个所谓的全局状态direct.conf必须假定外部已经定义好了那两个变量,否则就会出错,逻辑相对隐式,容易乱。
新版代码则先调用一directm.conf,然后调用了一个类似于函数的东西并且将ID和两个Channel作为参数传递进去,封装性相比原来来说更好,明确告诉系统要实例化一个DirectUseCase和对应参数。
但是问题出现在这directm.conf文件内。具体怎么回事呢,我直接贴出源代码:
#
# Define direct use case (no channel split or routing changes) macros
#
#
# Macro DirectUseCase
#
# Arguments:
# Id - Use Case identifier (e.g. "Direct" or "Direct1")
# [CardName] - optional, default is global CardName
# [Comment] - optional, default is compibed from identifier and CardName
# [PlaybackChannels] - optional, playback channels or/and
# [CaptureChannels] - optional, capture channels
# [PlaybackRate] - optional, in Hz
# [CaptureRate] - optional, in Hz
#
DefineMacro.DirectUseCase {
If.cardname {
Condition {
Type String
Empty "${var:-__CardName}"
}
True.Define.__CardName "${CardName}"
}
If.comment {
Condition {
Type String
Empty "${var:-__Comment}"
}
True.Define.__Comment "${var:__Id} ${var:__CardName}"
}
SectionUseCase."${var:__Id}" {
Comment "Direct ${var:__CardName}"
Config.SectionDevice."Direct" {
Comment "Direct ${CardName}"
Value {
If.p {
Condition {
Type String
Empty "${var:-__PlaybackChannels}"
}
False {
PlaybackPriority 1000
PlaybackChannels "${var:__PlaybackChannels}"
PlaybackPCM "hw:${CardId}"
}
}
If.c {
Condition {
Type String
Empty "${var:-__CaptureChannels}"
}
False {
CapturePriority 1000
CaptureChannels "${var:__CaptureChannels}"
CapturePCM "hw:${CardId}"
}
}
}
If.prate {
Condition {
Type String
Empty "${var:-__PlaybackRate}"
}
False.Value.PlaybackRate "${var:-__PlaybackRate}"
}
If.crate {
Condition {
Type String
Empty "${var:-__CaptureRate}"
}
False.Value.CaptureRate "${var:-__CaptureRate}"
}
}
}
}```
它只做了两件事:
1. 定义优先级
2. 直接把裸的PCM设hw:${CardID}给暴露了出来。
而且整个文件里面根本没有定义PlaybackMixerElem。翻commit看他们已经删掉的direct-verb.conf文件和direct.conf文件,如下:
# direct-verb.conf
SectionDevice."Direct" {
Comment "Direct ${CardName}"
Value {
If.p {
Condition {
Type String
Empty "${var:-DirectPlaybackChannels}"
}
False {
PlaybackPriority 1000
PlaybackChannels "${var:DirectPlaybackChannels}"
PlaybackPCM "hw:${CardId}"
}
}
If.c {
Condition {
Type String
Empty "${var:-DirectCaptureChannels}"
}
False {
CapturePriority 1000
CaptureChannels "${var:DirectCaptureChannels}"
CapturePCM "hw:${CardId}"
}
}
}
}direct.conf
If.direct {
Condition {
Type String
Empty "${var:-DirectCardName}"
}
True.Define.DirectCardName "${CardName}"
}
SectionUseCase."Direct" {
Comment "Direct ${var:DirectCardName}"
File "/common/direct-verb.conf"
}奇怪的地方是direct.confdirectm.conf在核心逻辑上几乎是一比一还原的,照理来说应该不会出现这样那样的问题。猜想有两个可能,一个应该是因为参数传递的锅,导致UCM的哈希值在传递过程中发生细微改变等等,另外一个可能性是两者使用的SectionUseCase的定义方式不一样,旧版直接定死了为"Direct",而新版则是定义为了一${var:__Id}的变量。这种情况下,HiFi.conf内可能有比如说类似Macro.direct.DirectUseCase "HiFi"这样的东西,导致我自己的iBasso DC04Pro在系统内UCM Profile的名字变成了类似DIRECT HiFi之类的东西。
如果单纯只是改个名的话,那WirePlumber就记住这个字符串名字了,下次就不炸音量了。但是偏偏GStreamer的行为是断开当前音频流,重建新的音频流。所以恶性循环发生了:
Hi-Res 切换导致连接断开 -> 因为配置结构从静态文件变成了动态宏,底层的状态机可能在“关闭-重开”的过程中表现得不如以前稳定,或者因为名字变了,导致某些持久化的状态没有被正确加载 -> 面对一个“名字变了”且“频繁重连”的节点,WirePlumber 的 Lua 脚本可能判定为**“这是一个不稳定的新流”,因此每次重连都触发default-node-volume规则,而不restore-stream规则。
为了验证猜想,我随便找了HiFi.conf,这个位于[ucm2/HDA/HiFi.conf](https://github.com/alsa-project/alsa-ucm-conf/blob/27dc3eec31ec3205b116d49fd7d9f5fef209a5c2/ucm2/HDA/HiFi.conf#L4)
# Use case Configuration for sof-hda-dsp
SectionVerb {
EnableSequence [
disdevall ""
]
Value.TQ "HiFi"
}
If.analog {
Condition {
Type ControlExists
Control "name='Master Playback Switch'"
}
True {
If.acp {
Condition {
Type String
Empty "${var:AcpCardId}"
}
True.Include.analog.File "/HDA/HiFi-analog.conf"
False.Include {
acp.File "/HDA/HiFi-acp.conf"
analog.File "/HDA/HiFi-analog.conf"
}
}
}
}
If.hdmi {
Condition { Type String Empty "" }
True.Include.hdmi.File "/HDA/Hdmi.conf"
}这个不是我iBasso所使用的配置文件sof-hda-dsp通常是英特尔笔记本内置声卡的驱动,但是它也能作为侧面印证:
Value.TQ "HiFi"
EnableSequence {
disdevall ""
}
# 暴力重置了每当这个 Profile 被激活时,为了防止状态冲突,它会先一刀切,把相关的所有设备都关掉或者重置一遍。爆了。音量丢失在这个时候几乎是必然的,基本上可以认为他们为了代码格式的规范而丢失了运行时的稳定性。
所以解决方案是等官方修复:trol:,本来想去提个issue但是想想算了。
3.音乐播放器评析和推荐
注:由于很多音乐播放器我只是用过一小段时间,印象不深,所以只会有寥寥几句的点评。或者是吐槽,会有冒犯性内容。不喜欢自己退。
排障讲完了,这东西得等ALSA官方修复才行,因为底层架构对于我这种普通用户来说过于复杂,况且滚动更新版也不好在本地对这些进行修改,上游一推送做的更改全部前功尽弃。所以暂时先绕过GSteamer,换个音乐播放器。
回到我开头所说的需求,大概说一下我用过的本地音乐播放器。注:这里所说的音乐播放器全部都是个人使用体验,并且仅限本地文件播放器,不包括在线流媒体播放器。
DeaDBeeF
首选。这东西对我来说就很像音乐播放器界的Arch Linux,定制化强到让我在刚开始使用Linux的时候一度对其感到不知所措然后放弃了,但是一旦配置好了之后,它在体验上就属于最贴近Foobar2000的那一档:可定制化,轻量,媒体管理器,多样、硬核且深入的Preferences管理等等。即使不去用他的远古Cpp做一个皮肤或,用他默认的Design Mode也可以设计出非常不错的界面,并且它有和GTK3的联动,效果就长这样:

支持ALSA / PipeWire / PulseAudio甚至是Null和OSS等远古老机器的输出,支持ReplayGain,支持DSD文件播放等等。就目前来说,DeaDBeeF的确是我能找到最牛批的播放器了,前提是愿意花一点时间折腾。我是跟着这个YouTube教学走了一小部分GUI界面设置。
同样,它和其他绝大多数播放器不一样,比如Quod Libet或者Clementine,它并不使用一个通用的后端比如GStreamer或者mpv。它有一个自研的音频播放核心。在解码层,它直接调libflac, libwavpack或ffmpeg等底层库来解码;在输出层则直接通过独立的输出插件直接与音频服务器对话,这大概也就是为什么DeaDBeeF支持这么多输出协议的原因:手搓仙人。
唯一美中不足的是,它本体只有last.fm支持,没有ListenBrainz支持。但这也不是什么大问题,直接在系统内装个rescrobbled就行。首次启用需要先在终端运行一次rescrobbled,然后去.config/rescrobbled/config.toml里面手动填入last.fm API和Key,或者如果它弹出了Auth界面那是最好,我的情况就是没弹出折腾了半天才弄好,按下不表。弄好之后配合WhiteList就可以精准记录DeaDBeeF内放的歌了。
Quod Libet
我知道ALSA这个更新“揭露”了GStreamer的一些问题,但是说实话。如果没有和我一样边打游戏边听歌的需求的话,Quod Libet对于那些不想折腾的人来说是一个很不错的选择

预设好的、带波形图的界面、完整的媒体库管理和分组、ReplayGain扫描、Lastfm和ListenBrainz扫描,以及它拥有非常完善的插件系统,想要什么插件或者不需要什么插件,直接去Plugins里面启用或者禁用就行。缺点就是GStreamer,对于某些人来说可能太重,太不稳定。但是It's worth giving a shot.
Clementine / Strawberry
印象不深,尤其是前者。我刚开始用Linux那会用的就是Strawberry,但是后来当我发现它不支持ReplayGain标签的时候我就果断放弃了,不知道现在支持了没。时间太久,我忘记它是不支持ReplayGain还是不支持Last.fm还是二者皆有,而且当时播放总是会有奇怪的嗞嗞声,不知道哪里的问题。
Audacious
使用Quod Libet之前用的。基本上Audacious很像Quod Libet,为数不多的不同就是QL比Audacious多了一个ReplayGain扫描与写入。其它的见仁见智吧,我不是很喜欢Qt界面以及在Hyprland下偶发的bug,具体表现为莫名其妙把我默认播放器接管了而且根本找不到GUI界面在哪,默认音量还贼鬼大,所以最终还是Skip了。
Elisa
包装好的玩具。没有.cue支持。
Lollypop
包装好的白女玩具。没有.cue支持。
用上面这俩我还不如直接用Spotify去。
Tauon Music Box
用过一小段时间,为数不多的印象是界面太花里胡哨并且UI缩放比例似乎有问题,并且我要的核心功能还是比较缺乏,至少说是不完善。Pass了。
Cantata
没折腾明白。主要是没有那个闲功夫去折腾mpd,不过就Client界面来看应该是个功能相当完善的播放器,其实可以试试看。
Resonance
野鸡。听都他妈没听过,被一个英文营销号忽悠了,Import Music Tags能花几百年,不知道是不是在偷偷拿我电脑挖矿。