引言
做一个专属于音乐的显示设备,这个念头在我脑海里已经盘旋了好几年。
最初的构想是一个挂在墙上的 Hi-Fi 音乐画框——类似三星 The Frame 电视的形态,但完全为音乐体验而设计。一张标准 12 英寸黑胶唱片封套大约是 31 厘米见方;我设想的设备稍大一些,做成长方形,大概 31 × 45 厘米,足够在彩色电子墨水屏上完整展示专辑封面,同时留出空间放置播放控制与曲目信息。连上无线音箱,以无损音质播放音乐;不放歌的时候,它就是墙上一幅安静的艺术品。某种意义上,这是对黑胶时代视觉仪式感的一种数字回归。
那个梦想至今仍在。但真正在一周内被做出来的,是一个更小、更粗糙、完全由挫败感催生的东西。
缘起:杜比全景声与 AirPlay 的落差
几个月前,我入手了两台 Sonos Era 300。纯粹是因为:Lily Allen 的新专辑采用了杜比全景声混音,我在MacBook Pro上听到的专辑的立体感,我也想让我在法国的家里真正感受空间音频的沉浸感,而 Era 300 正是当时市面上少数支持 Dolby Atmos 的无线音箱之一。

Sonos的音箱本身的表现没有任何可以挑剔的地方。真正让我困扰的,是围绕它的软件生态。
在此之前,我使用初代 HomePod 已经有好几年。HomePod 播放 Apple Music 的无损内容一直是无缝的——这很可能是因为 HomePod 并非通过标准 AirPlay 音频流接收数据,而是直接从 Apple 服务器拉取原始音频。带着同样的期待,我买下了 两台Sonos Era 300。
然而一个月后,我才意识到一个关键的技术事实:AirPlay 2 协议在传输音频时,会统一将所有内容编码为 AAC-LC 256 kbps——这是一种有损编码格式。也就是说,无论你的音源是 Apple Music 的无损 ALAC 还是 Tidal 的 Hi-Res FLAC,只要经过 AirPlay 传输到 Sonos,最终到达音箱的都是压缩后的有损音频。
Apple Music (ALAC 无损)
├─→ HomePod: 直接从 Apple 服务器拉取 → 无损 ✓
└─→ AirPlay 2 → AAC-LC 256kbps (有损) → Sonos ✗
Sonos App 原生串流 → Apple Music/Tidal → 无损 ✓想要真正发挥这些高端音箱的无损能力,唯一的途径是使用 Sonos 自家的 App,通过其原生集成的流媒体服务(Apple Music、Tidal 等)直接串流,完全绕过 AirPlay。
而 Sonos App,恰恰是这整个体验中最令人头疼的环节。
从便携到固定:控制逻辑的断裂

其实我并非 Sonos 生态的新用户。在 Era 300 之前,我已经使用 Sonos Roam 大约三四年了。Roam 的体验一直是可以接受的,原因很简单——它是一个便携音箱,你随身携带,物理按键永远触手可及。调音量、切歌,手指一按就行。App 有 bug?无所谓,反正你也不需要打开它。
Era 300 彻底改变了这个前提。这是两台固定放置的音箱,摆在房间另一头的架子上。没有方便触及的物理按钮。从此,每一次交互——调音量、切歌、暂停——都变成了「掏出手机 → 打开 Sonos App → 等待连接 → 执行操作」的四步流程。
而 Sonos App 的表现,用过的人大概都深有体会。音量控制会莫名失灵。使用 Sonos 原生串流时,iOS 系统的音量按键和控制中心对它完全无效——你必须在 App 内操作。语音控制同样不稳定:一半时间不响应,另一半时间响应了但做的不是你要求的事。
2026 年了,控制一台音箱不应该是这样的体验。
音乐的「不可见」问题
但真正让我耿耿于怀的,是一个更根本的问题。
我一直很喜欢 Apple Music 渲染「正在播放」界面的方式——专辑封面背后那层柔和的、色彩斑斓的模糊光晕。它让音乐产生了一种可感知的「存在感」。你瞥一眼手机屏幕,整个界面都在随着当前曲目的情绪而流动。
但当你锁屏的那一刻,这一切就消失了。音乐变得彻底不可见。花了几千块的 Sonos 音箱——拥有出色工业设计的音箱——就那样静静坐在架子上,像一个沉默的黑盒子。你不知道它在放什么,除非你拿起手机去查看。
这个问题在 Sonos 社区中其实已经被讨论了超过十年。论坛帖子和 Reddit 上的诉求始终如一:用户渴望一个不需要「主动查看」的环境显示器——一个用余光就能感知到当前播放信息的存在。旧的 iPad 太卡、电视太浪费、智能显示屏同步太差、树莓派 DIY 方案成本高且维护复杂。
在了解这些方案的过程中,我注意到了 Tidbyt——一款小型 LED 像素显示屏,可以显示 Sonos「正在播放」信息以及各种小组件。这个产品形态立刻吸引了我:一个独立的、小巧的、始终亮着的环境显示器,正是我想要的东西。
但 Tidbyt 的架构让我望而却步。它完全依赖云端:硬件本身只是一个 ESP32 控制器,所有渲染逻辑、数据拉取都在他们的服务器上完成。没有本地处理能力,没有自定义空间,也没有离线运行的可能。更关键的是,Tidbyt 最近被 Modal Labs 收购后已经停产,社区正在紧急搭建「Tronbyt」等救援项目,试图在官方服务器关闭后延续现有设备的生命——这是云端硬件产品的经典死亡螺旋。
我想要的,是一个完全本地运行、可以自由扩展、不依赖任何公司服务器的方案。
于是我买了一台 Ulanzi TC001。这是一款售价200人民币左右的 LED 矩阵时钟,内置 ESP32-WROOM-32 芯片,8×32 像素的 WS2812B LED 矩阵,还带有光线传感器和蜂鸣器。硬件足够了,剩下的就是软件。
一周、四个版本:架构演进的全过程
翻开这个项目的 git 历史,时间线几乎令人难以置信:从零到一个完整可用的系统,不到一周;其间经历了四次重大的架构改写。这种开发节奏,大概只有在你挠到一个困扰了自己很久的痒处时才会出现。

V1:基于 AWTRIX 3 的初始方案
第一个版本使用了 AWTRIX 3——一个成熟的开源 LED 矩阵固件。我在 NAS 上编写了一个 Python 脚本,通过 SoCo 库轮询 Sonos 获取当前播放曲目,从专辑封面提取一个主色调,然后通过 HTTP API 向运行 AWTRIX 固件的 Ulanzi 推送滚动文字。
它能跑。但效果离「环境显示器」相去甚远——AWTRIX 内置的跑马灯在每次循环之间会留下一大段空白,颜色只有单一的主色调,扁平且毫无生气。整体感觉更像一个极客玩具,而不是一个你愿意长时间放在桌面上的设备。
V1.5:NAS 充当 GPU 的疯狂尝试
不满足于 AWTRIX 有限的显示能力,我走上了一条更为激进的路线:让 NAS 承担全部渲染工作。
我用 Python 写了一个完整的渲染器,逐像素计算弹跳滚动位置、从五色加权调色板生成逐字符渐变色,然后以 5 fps 的频率向 AWTRIX 推送完整的像素帧。代码量迅速膨胀到 5000 行以上。我还添加了一个网页版的实时像素镜像面板——在浏览器里看,渐变效果确实很漂亮。
但在实际的 LED 矩阵上,问题暴露无遗。ESP32 的 CPU 在每次写入 WS2812B 灯带时会被阻塞约 7.7 毫秒(bit-banging 协议的固有限制),在这个时间窗口内无法处理任何网络请求。HTTP 响应时间因此从 20 毫秒到 1.5 秒以上不等,完全不可预测。
我随后尝试从 HTTP 切换到 MQTT 传输,期望利用 MQTT 的非阻塞发布(publish 耗时约 0.1 毫秒)来绕过这个瓶颈。然而测试发现,即使网络传输不再是问题,ESP32 端的消息处理速率也被限制在约每秒 12 条——帧率上限始终突破不了 10 fps。
问题的根源不在传输协议,而在架构本身。我把 NAS 变成了一个给 256 像素屏幕用的远程 GPU,这条路从一开始就是错的。
V2:状态机重构,但治标不治本
第三次尝试引入了规范的状态机来管理页面轮转和滚动逻辑,试图让代码更加可维护。但它的底层仍然是 AWTRIX,仍然是 NAS 端渲染。边缘情况的补丁以比我修复 bug 更快的速度累积:弹跳滚动与页面切换的冲突、莫名出现的 5 像素边距、HTTP 超时导致的画面撕裂。每修一个 bug,都会引入一个新的。
V3:扔掉一切,从零开始
3 月 13 日中午前后,我做了一个改变整个项目走向的决定——彻底放弃 AWTRIX,从零编写自定义 ESP32 固件。
核心洞察其实很简单:不要再试图让一个设备同时做两件事,而是明确分工。
- NAS 负责「决定显示什么」:曲名、艺术家、调色板颜色、播放进度、BPM、能量值。它通过 MQTT 每秒发布一条轻量的 JSON 消息,约 100 字节。
- ESP32 负责「决定怎么显示」:文字滚动、渐变光环动画、色彩光斑漂移、歌曲过渡效果——全部在本地以 30 fps 实时计算和渲染。
结果是立竿见影的。动画帧率从 5 fps 提升到 30 fps,提升了六倍。网络流量下降了 80%。即使 MQTT 消息偶尔延迟,显示器的渲染也完全不受影响——因为它不再依赖网络来驱动每一帧。
回过头来看,我在 V1 到 V2 三个版本上花费的补丁时间,几乎等于从头编写 V3 自定义固件所用的时间。这次重写解决了之前所有累积的架构问题。我将这套设计称为「State-Sync」(状态同步)架构,它成为了后续一切功能的基石。
在 256 像素上复现 Apple Music 的色彩感
这个项目中最令我满意的部分之一,是调色板提取算法的实现。
目标很明确:让 8×32 的 LED 矩阵呈现出类似 Apple Music「正在播放」界面那种随专辑封面情绪而流动的色彩感。传统的 k-means 聚类或 LAB 色彩空间转换虽然效果尚可,但对于需要在歌曲切换时实时计算的场景来说太过昂贵。
我最初实现了一种桶频率分析算法,灵感来源于 iTunes 11 时代的配色方案。具体而言:对专辑封面的左侧边缘进行采样(情绪色彩通常集中在这个区域),将 RGB 值按每通道 5 bit 进行分桶(共 32,768 个桶),过滤掉「无聊」的颜色——接近纯黑、纯白或纯灰的色值——并强制相邻色之间的最小色相距离为 0.06,以确保最终调色板中的颜色足够鲜明且有区分度。
整个计算在 32×32 降采样图像上完成,耗时不到 1 毫秒。
在 ESP32 端,固件利用这组调色板渲染了最多五个柔和的色彩光斑(blob),它们以利萨如(Lissajous)轨迹在文字背后缓慢漂移,脉动频率与当前曲目的 BPM 同步,亮度随频谱能量波动。文字本身从这片流动的色彩场中采样颜色,使得每个字符都带有微妙的动态色彩变化。
在一个只有 256 像素的 LED 矩阵上,这个效果好得超出了它本该有的水平。
一开始,我以为 256 像素就是不可逾越的硬件限制。后来才意识到,真正的瓶颈从来不是硬件——而是架构。
后来我用 CIELAB 色彩空间的 K-means 聚类重写了整个调色板算法——感知均匀性带来了质的飞跃。但这是下篇的故事了。
Last.fm:数据主权的失落与夺回
我曾是 Last.fm 的付费用户,痴迷于记录每一次收听——Apple Music 元数据、iTunes 导入的第三方音乐、完整的收听历史。对我来说,这些播放记录就是数据,而我深深在乎自己的数据。
后来 Apple 推出了 Replay 功能,基本覆盖了我对收听统计的需求。但真正让我放弃 Last.fm 的,是数据管理的灾难。每次切换 App Store 地区(偶尔需要下载其他区域的应用),Last.fm iOS 客户端的播放记录都会凭空消失。切换回来后尝试重新导入,得到的是令人抓狂的数据差异:重复条目、缺失时段、错位的时间戳。精心维护的数据变得不可信赖——而不可信赖的数据,比没有数据更糟糕。
2024 年,Sonos 推送了那次灾难性的 App 大更新,Last.fm 的 scrobble 功能对几乎所有用户都失效了。Reddit 上充斥着相关报告,Sonos 客服甚至公开确认没有修复计划。

在这个项目中重建 scrobble 功能,对我来说有一种「夺回数据主权」的意味。因为系统直接通过本地 UPnP 协议轮询音箱的播放状态,它可以完全独立于 Sonos 的云端基础设施来检测播放并发送 scrobble 数据。无论 Sonos 未来怎样更新(或搞坏)他们的 App,这套系统都不受影响。
这只是开始
写到这里,你看到的是这个项目的「骨架」:一个从挫败感出发、经历四次架构迭代、最终找到正确设计范式的过程。State-Sync 架构给了我一个干净的基础——NAS 决定「什么」,ESP32 决定「怎么」。色彩光斑赋予了 256 像素超出预期的视觉表现力。Last.fm 的重建让播放数据回到了我自己手中。
但这只是基础。
当架构稳定之后,我开始在两个方向上同时推进:把 256 像素的表现力榨到极限,同时直面一个越来越无法回避的事实——256 像素,终究是不够的。
在下篇中,我会讲述为什么 8 个字符的显示宽度逼得我去删歌词里的逗号,如何把一个 Sonos 专属的显示器变成支持所有 AirPlay 设备的通用音乐伴侣,如何用 Whisper 和动态规划让歌词逐字同步,如何用频谱分析让色彩光斑跟着节拍呼吸——以及,为什么做完这一切之后,我反而更加确信需要一个全新的硬件。
