给自己的 PC 修了 6 个小时的 bug,观后感:

🖕😡 微软视窗,我阐述你的梦。

我的电脑连着一台蓝牙音箱,只要在浏览器里随手暂停一个视频,整个系统当场原地失聪。

这个弱智 bug 折磨了我整整 6 年。

昨晚终于把雷德蒙德这帮草台班子的裤衩给扒了。解法极其抽象:在后台常驻一个 0 音量无限循环音频,且必须向视窗注册系统媒体传输控件。

傻X咖喱系统

年初才骂完 微软边缘 这个咖喱浏览器,这次轮到他们 整套咖喱系统的左右脑在互搏

下面是完整的坐牢破案过程。不想看哥们破防请直接跳解法 →


导火索:一只被冤枉了 6 年的透明水母

先介绍一下本案中最大的受害者:我桌上摆着一台尊贵的哈曼卡顿·声音棍·第四代HK SoundSticks 4,即「哈曼卡顿 水晶4」

长得像未来水母的透明音响,底下那颗大低音炮跟个晶莹剔透的大果冻似的,灯效还会优雅地呼吸。当年咬牙两千多块买回来的,音质是真的顶未必,摆在桌上排面直接拉满。

昨晚在写我的 Godot 游戏,写累了想在后台开个脱口秀听听,给自己鼓鼓劲。很合理吧?熬大夜的傻子程序员都这么干。

打开浏览器,播放,听了两句不好笑,随手点了一下暂停

切回游戏——

完全静默

脱口秀没声、游戏没声、系统提示音没声、拖动音量条那个“噔”的一声也没了。显卡风扇呼呼转,两千多块的哈曼卡顿·声音棍·第四代宛如被物理拔掉了发声器官。

这个 bug,从 6 年前的视窗 10 时代就开始折磨我了。

6 年来,我换过蓝牙驱动、换过英特尔网卡、重装过系统,甚至一度怀疑我的这台哈曼卡顿·声音棍·第四代质量不行——两千多块钱的顶级水母啊!我一度怀疑是不是买到华强北假货了,天天在网上搜售后。

每次哑火,我就得像巴甫洛夫的狗一样,切去浏览器随便点个视频,播一秒再暂停——跟做法事一样,声音才会回来。

直到昨晚我才知道:我的哈曼卡顿·声音棍·第四代冰清玉洁,纯纯是微型软件(Microsoft)脑瘫。

晚上十点,哥们心态彻底爆炸,叫上「克劳德·编码」Claude Code,决定把视窗按在地上往死里解剖。


视窗:发生了某些事情,但我们认为一切正常

抓出来的第一组诊断数据就让我脑溢血:

在视窗的狗眼里,一切好得很。

  • 音量合成器里,游戏进程的音量条在疯狂蹦迪
  • 音频终结点Audio Endpoint,典中典微软机翻:正常、启用、未静音
  • 蓝牙硬件状态:工作正常、已连接
  • 底层追踪:数据流以 44100 帧/秒 的恒定速率向设备推进,一帧没丢
  • 视窗自带的小故障侦测GlitchDetection零错误,零告警

我把哑火状态与正常发声状态的十六进制内存对齐对比——连一个 bit 的差别都没有。

1
2
3
4
[视窗操作系统报告]
✓ 您的设备已准备就绪。
✓ 我们正在以 44100Hz 欢快地为您播放音频。
✗ 但您听不到声音是您耳朵的个人问题,请重新启动您的计算机。

薛定谔的音频。

这就是它能潜伏 6 年的原因:因为在系统自己看来,它健康得像头牛。 日志干干净净,系统自我感觉良好,典中典之“全责装死,已为您解决”。

我那台两千多块的哈曼卡顿·声音棍·第四代,就这么替微型软件背了整整 6 年黑锅。


破案:送外卖的与关防盗门的

破案线索极其弱智。

哑火的时候,我随手按了一下哈曼卡顿·声音棍·第四代顶上的物理音量键——声音棍的灯亮了,视窗这边的音量条纹丝不动。反过来拖电脑音量条,声音棍也毫无反应。

平时这俩是联动的。现在:互不理睬,直接失联。

在蓝牙规范里,音频实际上跑在两条独立的管道上:

  1. 高级音频分发配置文件A2DP —— 运送声音二进制数据的【拉货卡车】
  2. 音频/视频远程控制配置文件AVRCP —— 传输音量同步、播放暂停指令的【对讲机】

真相大白:

你在浏览器点暂停 → 视窗的系统媒体传送控件SMTC通过对讲机吼了一句“暂停!” → 哈曼卡顿·声音棍·第四代老老实实把大门焊死了 → 但开货车那哥们(视窗音频引擎)根本不知道这事,继续以每秒 44100 帧的速度,往焊死的大门上疯狂卸货。

送外卖的骑手在门口一秒扔 44100 份盒饭,客户早就出国旅游把防盗门锁死了。微型软件外卖后台显示:配送中,客户签收率 100%,骑手本月全勤。

雷德蒙德的两套状态机左右脑互搏,同部门俩同事上班谁也不跟谁说话。这就是万亿美元市值的工业奇迹。


18 个假说,全军覆没

从昨晚十点到凌晨四点,我和克劳德·编码提出了整整 18 个假说,然后看着它们一个接一个被抬进停尸房:

蓝牙低功耗模式、USB 选择性挂起、免提配置文件抢占、通信衰减、杜比全景声Dolby Atmos、音频增强、独占模式、硬件卸载、英伟达那一坨没有用的高清晰度多媒体接口HDMI音频终结点……

全灭。

中间最癫狂的一段:我用视窗音频会话应用程序编程接口WASAPI,写了八种姿势往引擎里硬灌音频流:
轮询、事件驱动、换 AudioCategory、新建流、重用流、真波形、纯静音,最后甚至把Chromium的源码扒下来照着参数抄。

我把测试增益拉到终结点电平 0.41(平时听歌才 0.1),功放推到冒烟——依然一个屁都放不出来

两千多块的哈曼卡顿·声音棍·第四代,被雷德蒙德这帮印度哥们直接干成了一个两千多块、只会发光的亚克力桌面摆件。造型确实顶级,就是他妈的不响。

令人抓狂。

那浏览器凭什么能响?

搞死我 18 个假说的关键在于:为什么浏览器一播视频就能把声音救回来?

我顺藤摸瓜测了一圈:网易云可以、罐子播放器PotPlayer可以、火狐Firefox也可以。

它们的唯一共同点不是“播放了声音”,而是:

它们都向系统注册了「系统媒体传送控件」(就是你按 Win+A 弹出的那个媒体播控面板)。

只有这帮正规军开始播放,视窗才会拿起对讲机喊一句“正在播放”,音箱收到后才会把大门重新打开。

声音根本不是关键,出入证才是。

我用注册了媒体控件的组件,播放了一段 0 分贝、全空的纯静音音频——

哈曼卡顿·声音棍·第四代 当场诈尸,系统声音瞬间全通。

前面写底层 WASAPI 把功放推到冒烟全是在对着铁门骂街;放一段“什么都没有的虚无”,水母活了。


解法

既然音箱一收到“暂停”就会把门焊死,那永远别让它收到暂停不就完了?

写一个无 UI 控制台程序,后台常驻一个永远在播放、音量为 0、无限循环的幽灵媒体会话。只要这玩意一直播,视窗就永远不会向音箱发关闭指令。

核心代码(.NET 10 / WinRT):

1
2
<!-- 必须 targeting windows10,否则调用不了 Windows.Media 命名空间 -->
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using Windows.Media.Core;
using Windows.Media.Playback;

var player = new MediaPlayer();
player.AudioCategory = MediaPlayerAudioCategory.Media;
player.Volume = 0.0; // 永远静音
player.IsLoopingEnabled = true; // 永无止境

// 必须配置元数据,否则视窗会在控制中心大字展示你的本地文件路径,极其丢人
var item = new MediaPlaybackItem(MediaSource.CreateFromUri(
new Uri("file:///D:/silent.wav"))); // 一个纯静音 wav
var props = item.GetDisplayProperties();
props.Type = Windows.Media.MediaPlaybackType.Music;
props.MusicProperties.Title = "AudioLinkKeeper";
props.MusicProperties.Artist = "保持蓝牙链路";
item.ApplyDisplayProperties(props);
player.Source = item;

// ⚠️ 避坑 1:千万别把所有命令都设为 Never。
// 视窗会判定该会话不可控,从而直接从 SMTC 面板剔除,
// 一旦被剔除,视窗立刻不再同步播放状态,当场失效。只禁用上下首即可。
var cm = player.CommandManager;
cm.IsEnabled = true;
cm.NextBehavior.EnablingRule = MediaCommandEnablingRule.Never;
cm.PreviousBehavior.EnablingRule = MediaCommandEnablingRule.Never;

player.Play();

// 看门狗:防止手贱在控制中心按了暂停
while (true)
{
await Task.Delay(1000);
var st = player.PlaybackSession.PlaybackState;
if (st == MediaPlaybackState.Playing) continue;

// ⚠️ 避坑 3:状态为 None 时,MediaPlayer 这个对象本身已经变成尸体了,
// 你对着它调一万次 Play() 都不会有任何反应,必须 Dispose 掉整个重建。
if (st == MediaPlaybackState.None)
{
player.Dispose();
player = BuildPlayer(); // 把上面那一坨初始化重新走一遍
}
else
{
player.Play(); // 只是被按了暂停,Play 一下就能爬起来
}
}

⚠️ 避坑 2:千万别想着“监听哑火再补播”。我试过:补播静音结束后,会话状态变回“已停止”,又触发了一次暂停指令,再次精准把音箱干哑——自己一脚踩在自己的狗尾巴上。必须常驻,永不停止。

完整源码我扔 GitHub 上了,WTFPL,你他妈想干啥就干啥:

→ dragon-fish/AudioLinkKeeper ←

README 里有英文版的完整说明,包括我实测过的所有失败方案对照表——万一有哪个倒霉蛋跟我一样被这玩意折磨,搜到了能少走 6 年弯路。


⚠️ 避坑 3 值得单独说两句,因为它离谱得有点艺术性。

我最早那版看门狗就是上面注释掉的天真写法:发现不在播放,就调 Play()。跑了 57 分钟之后,我发现控制中心里的「保持蓝牙链路」没了

进程还活着,看门狗还在每秒兢兢业业地检查,日志刷了几百行「会话不在播放,重新 Play」——但那个 MediaPlayer 对象早就变成尸体了PlaybackStateNonePlay() 调下去没有任何反应,也不报错。

也就是说,我写的这个修复程序,得了跟它要修的 bug 一模一样的病

  • 进程活着、循环在跑、日志在刷 → 看起来一切正常
  • 实际上早就什么都没在做了 → 保活功能已经死了 57 分钟

我他 🐴 属实是被雷德蒙德给传染了。


它现在像个幽灵一样挂在我的控制中心里:一个永远在播放虚无、永远 0 分贝的滚木播放器。

代价是快捷控制栏里常驻一个狗皮膏药。但比起两千多块的哈曼卡顿·声音棍·第四代随机失声 6 年,这笔账血赚。


后记:名场面集锦

  • 排查中发现,英伟达的 HDMI 音频终结点在两个月里自己假死抖动了 1450 次。问题是:我的显示器压根没有扬声器。 它在空气里自嗨了 1450 次。
  • 视窗自带的 Audio/Informational 事件追踪每秒产生 20000+ 条日志,512MB 环形缓冲区只够存 3 分钟
  • 克劳德·编码中途写了个抓取脚本,启动耗时 90 秒,旨在抓取启动前 20 秒的日志。等它跑起来,想抓的日志早就被刷入虚空了。
  • 360 把我们自己编译的修复程序当场报毒拦截。一个开机自启、0音量死循环、拦截不到任何声波的后台进程,确实挺像专业窃听木马的😅。
  • 视窗终端Windows Terminal无法打开任何带中文路径的 file:// 超链接。斜杠、反斜杠、URL 转义全灭。微型软件自家终端打不开自家文件系统上的中文。
  • 全程我那台两千多块的哈曼卡顿·声音棍·第四代就在旁边优雅地亮着呼吸灯,蓝牙未断,硬件未休眠,它从头到尾冰清玉洁,就是不出声。它比我还冤。
  • 修复程序上线 57 分钟后自己悄悄死了,进程还在、循环还在、日志还在刷,就是不干活了。跟它要修的那个 bug 是同一种死法。 我现在怀疑这病会传染。

总结

这个 bug 必须集齐高级音频分发配置文件 + 音频/视频远程控制配置文件 + 系统媒体传送控件才会完美触发。

踩坑的用户大概率都和我一样:怀疑音箱、怀疑驱动、重连设备、玄学重启,一忍就是 6 年

最讽刺的是:尊贵的哈曼卡顿·声音棍·第四代替微软背了 6 年黑锅。水晶4 对不起,是我错怪你了,你没有任何问题,是雷德蒙德的脑瘫工程师有问题。

我真的很想穿上我的大皮靴,狠狠地踢爆这帮印↗度↘工程师的屁股。不想好好做蓝牙音频可以不做,滚去给系统塞更多的 领航员Copilot 垃圾吧。

我再说一次:

🖕😡 微软视窗,我阐述你的梦。