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文件。

按下不表,所以我个人对于本地音乐软件的需求是:

  1. Last.fm支持,现在还得加一个ListenBrainz支持

  2. 对不同的采样频率支持无问题

  3. 无缝切歌,对于一张完整的专辑来说无缝基本可以说是必不可少的功能了

  4. 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中:

  1. common: introduce DirectUseCase macro

  2. USB-Audio: update to use new DirectUseCase macro

  3. common: remove direct.conf and direct-verb.conf files

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通常是英特尔笔记本内置声卡的驱动,但是它也能作为侧面印证:

  1. 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界面设置。

Tutorial on YouTube

同样,它和其他绝大多数播放器不一样,比如Quod Libet或者Clementine,它并不使用一个通用的后端比如GStreamer或者mpv。它有一个自研的音频播放核心。在解码层,它直接调libflac, libwavpackffmpeg等底层库来解码;在输出层则直接通过独立的输出插件直接与音频服务器对话,这大概也就是为什么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能花几百年,不知道是不是在偷偷拿我电脑挖矿。


Music Players Bug排障和个人推荐
http://localhost:50392/archives/wei-ming-ming-wen-zhang-DtP4ejC3
作者
Cynsm
发布于
2026年01月20日
许可协议