博客音乐播放器:3D 频谱地形与 Meting 接入
一直想要个能看的音乐页,不是塞个 APlayer 列表就完事那种。后来看到 团子和蛋糕 的音乐页面,正是我想要的效果,就照着这个搬过来、接上自己的歌单。
三层分工
MusicManager 是挂在 Layout 上的全局单例,负责拉歌单、管 <audio> 元素、做 Web Audio 频谱分析,是真正的”大脑”。MusicVisualizer 只管可视化:Three.js 那块 InstancedMesh 方块、着色器、OrbitControls,音频数据从 Manager 拿,驱动地形起伏。music.astro 就是个页面壳,把可视化挂上去。
为什么要做成全局单例、而不是只放在 /music/ 页里?因为 Firefly 的页面切换走的是 Swup 软导航——切页面不整页重载、不重跑脚本。播放器想跨页面连续播,就必须活在被保留的那一层(Layout)里。
数据源用 Meting API,配置只有一行 meting.api,歌单 id 写死在配置里。接口自己掌控的好处是灵活,代价是 cookie 和可用性得自己维护。
频谱怎么变成地形起伏
链路本身不长:
const source = audioCtx.createMediaElementSource(audioEl); // 把 <audio> 接进 Web Audiosource.connect(analyser);analyser.connect(audioCtx.destination); // 这根不能省// 每帧analyser.getByteFrequencyData(freqData); // 0–255 的实时频点能量freqData 是一串 0–255 的频点能量,地形就是拿它去驱动 InstancedMesh 里每个方块的高度——方块数量固定,每帧只改矩阵、不重建几何体,所以铺几万个也不卡。
两个通用但容易忽略的点:
analyser.connect(destination)这根线必须接。分析器不是”旁路监听”,它是串联在链路上的——接了分析器却没接输出,声音直接没了。createMediaElementSource对同一个<audio>元素只能调一次,重复调用会抛InvalidStateError。所以初始化那里要有锁,否则 Swup 反复进出页面就会炸。
配置里两个要注意的细节
meting.api 一定要写全 https://。 漏了协议头,浏览器把请求当成相对路径拼到当前页域名上,变成 x1anyu.cn/music/meting.yufish.cn/...,直接 404,歌单就拉不到。
<audio> 必须设 crossOrigin="anonymous"。 否则跨域音频会被浏览器判定为”污染”,Web Audio 的 analyser 读到的永远是 0,地形一动不动。这个属性要在元素创建时就设上,等 src 赋值之后再补是无效的。
一点小问题
地形不动。 MusicVisualizer 要靠 mgr.getAudio() 拿 audio 元素去做 createMediaElementSource,但 MusicManager 的公开 API 里压根没有 getAudio——拿到的是 null,代码就走进了”等一会儿再试”的 setTimeout 死循环,分析器从头到尾没连上。补上 getAudio: function () { return audio; } 之后地形立刻活了。
这个 bug 的表现很有迷惑性:歌照放、进度条照走、UI 一切正常,只有地形是死的。因为播放本身根本不经过 Web Audio,只有可视化依赖它。所以排查时很容易往”着色器写错了""数据没传到”那边想,实际是源头就没接上。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!











