Vibe Coding 大赏:音乐馆

在线音乐播放器满地都是,我为什么还要自己写一个?

原因特别简单:我受不了“随机播放”了。

三百首歌的播放列表,点开随机播放,听二十几首就能撞到重复的——这不是错觉,是数学定律。真随机有“生日悖论”,23 首之内出现重复的概率就超过一半。你以为你开了随机,实际上体感感觉很多歌都重复放,想听的歌又一直放不到。

于是我干脆自己造了一个,做完之后它长这样:

MQ Music 音乐馆 - 桌面端主界面


一、介绍

这个项目不是从零开始写的,它基于 HeoMusic(Apache-2.0 协议)魔改而来。

HeoMusic 为张洪大佬写的一个播放器,本身就是一个很优秀的静态播放器了(基于 APlayer + MetingJS),开箱即用、配置简单。我保留的是它的静态——一个纯静态、零构建、丢到任何静态托管就能跑的音乐播放器;改掉的是几乎全部的实现细节。

上游的 index.html 只有 1 KB、config.js 是 570 字节的示例文件;我这边 index.html 18 KB、config.js 16 KB,player.js 写到 2400 行、modern.css 1907 行。基本上除了“这是个音乐播放器”这件事没变,其他都大改了一遍。

致谢:感谢 @张洪Heo 和 HeoMusic 的贡献者们,没有这个项目打底,我大概会一直停留在“想做一个播放器”的阶段。


二、功能与界面

2.1 主界面:沉浸式的一屏

打开就是一屏,没有多余的东西。背景为封面的模糊图,封面为主,侧边栏为辅,控制台在封面下方有进度条和控制按钮等。

2.2 歌词:逐行高亮

右侧面板默认就是歌词。当前行高亮加粗,上下行渐隐,唱到哪亮到哪:

逐行高亮歌词

支持逐行 + 逐字两种歌词格式,桌面端自动滚动到当前行居中。

2.3 播放列表:可搜可点

「列表」页签是完整歌单,带序号、缩略图、歌名、歌手,顶部还有一个列表内搜索框:

播放列表

我目前的歌单是 340 首(网易云歌单)+ 50 首本地音乐混合播放。本地音乐可以直接填直链(mp3 / flac),也可以单独配封面和歌词文件。

2.4 在线搜索点播

想听什么直接搜,搜到就能点播,不用切到网易云再复制链接:

在线搜索

搜出来的结果会直接插进当前播放队列,听完继续回到原来的顺序。

鸣谢:由GD音乐台提供的API

2.5 音效

点击“滑块”图标,会弹出音效菜单:

音效菜单

关闭 / 3D环绕 / 沉浸 / 现场感 / 迷幻 / 流行 / 重低音,由 Web Audio API 实时处理。

当时弄这个音效整了挺久的,主要问题在于CORS跨域的问题,想要整音效,首先得整到音乐文件,而音乐文件得从网易云CDN爬取,这就麻烦了,网易云的有些CDN没有CORS跨域支持,就无法下载,当时折腾了挺久。

2.6 账户:播放记录云端同步

游客模式可以完整使用(数据存在浏览器 localStorage),但如果你想换台设备继续听,可以注册个账户:

账户面板

登录之后播放次数会同步到云端,换浏览器、换手机都不丢。这个数据同时也是下面「平均随机」算法的依据——听得越多,越不容易被重复抽到。

2.7 移动端交互

手机上的布局不是把桌面版压扁,而是重新做了一套:

移动端主界面

底部导航四个入口(播放 / 歌词 / 列表 / 搜索),歌词可以全屏展开:

移动端全屏歌词


三、两个设计

3.1 「平均随机」

这是整个项目的起点,也是我最满意的一块。

问题在于:真随机 ≠ 用户想要的随机。用户要的不是“每次独立均匀抽样”,而是“这一轮里别重复,长期下来每首歌播放次数差不多”。

所以我用的是加权无放回抽样(Gumbel-top):

每首歌的 key = -ln(random) / (PRIOR + α × 播放次数)
按 key 降序排列 = 本轮播放队列

这一个公式同时给了三个性质:

  1. 轮内零重复——排列天然保证,根治生日悖论
  2. 播放次数少的歌期望排前——长期收敛到“人人平等”
  3. 新歌不会被无限播——新歌按”已播 4 次”起步参与排序(贝叶斯平滑),每播一次让一步,播满 4 次后就和普通歌平权。解决了“新歌计数为 0,权重无限大,被反复抽中”的问题

两个参数都放在 config.js 里可以调:shufflePrior(越大,新歌特权越小)、shuffleAlpha(0 = 纯均匀洗牌,1 = 线性偏置,推荐 1)。

顺带还有两件小事:手动点播的歌会从本轮队列移除(同一轮不会二次抽到),歌单增删改序后播放计数也不丢(用 ne:{id} / lo:{url} 这种稳定标识,而不是数组下标)。

3.2 用户数据云写入

账户同步这块踩过一个坑。

最初的协议是”全量替换”:本地计数变了就把整份数据 PUT 上去。问题是服务端实现是 DELETE 全部 + INSERT 全部——一次同步写大约 2N 行(N = 该用户的计数行数)。我实测是 384 行,也就是一次同步写 768 行。

而我用的Cloudflare D1 写入额度比较少,10 万行/天——折算下来全站所有用户加起来,每天只能同步大约 131 次。两个真实用户各听 65 首就能把额度打穿。

后来改成了增量合并 + 写入节流:

播放 → 本地计数 +1(localStorage 永远不丢)
→ 变化的 key 记入 pending(落盘持久化)
→ 节流:30 秒防抖 + 60 秒最小间隔 + 3 分钟最长等待保底
→ POST 只发变化项,服务端 UPSERT 取 MAX(云端, 客户端)

写行数从 768 降到 13 行,约 768 倍降幅,读请求也归零了(MAX 天然幂等,多设备互不覆盖,不再需要”先读再写回”)。

再加一个 countPlayAfterSec(默认 20 秒)——实际播放满 20 秒才计入播放次数。快速切歌、试听不算数,既减少了写入,也让洗牌权重,不被”点开就跳过”的歌污染。

代价是上报有 30~60 秒延迟,但 pending 是落盘的,崩溃、强杀、关页都有兜底补传,数据不会丢。


四、自己部署一个

整个前端是纯静态、零构建的——没有 npm install,没有打包步骤,把文件丢上去就能跑。

4.1 Fork 我的仓库

Fork 666mingqing6/Music 到你自己的账号。

4.2 改 config.js

config.js 是全站唯一的配置入口,所有东西都在这个文件里:

var userId = "12675886878";      // 你的网易云歌单 ID
var userServer = "netease"; // 服务商:netease / tencent / kugou ...
var userType = "playlist"; // playlist / song / album / artist

// Meting API 基地址(自建 Cloudflare Worker)
var metingApiBase = "https://meting-api.646474.xyz";

// 随机播放参数
var shufflePrior = 4; // 新歌冷启动平滑底数
var shuffleAlpha = 1; // 播放次数偏置强度

// 播放满多少秒才计入播放次数
var countPlayAfterSec = 20;

// 本地音乐(可选,支持 mp3 / flac 直链)
var localMusic = [
{
name: '歌名',
artist: '歌手',
url: 'https://example.com/song.mp3',
cover: 'https://example.com/cover.jpg',
lrc: 'https://example.com/song.lrc'
}
];

至少要改的:

  • userId — 换成你自己的歌单 ID。从歌单分享链接里取,比如 https://music.163.com/playlist?id=12675886878,ID 就是 12675886878
  • metingApiBase — 可以换成你自己的 API 地址(见下一步)
  • localMusic 可以留空数组,只用在线歌单也完全没问题。

4.3 后端 API

音乐数据(歌单、搜索、歌词、封面)需要一个 Meting API。我把自建的那份也开源了:666mingqing6/meting-api,Cloudflare Workers + D1,部署即可。

想省事也可以直接用别人的公共 Meting API,但稳定性、可用性都不保证

4.4 部署到 Cloudflare Pages

改完 config.js,剩下就是选个静态托管平台。Cloudflare Pages 最省事,而且免费:

  1. 登录 Cloudflare Dashboard → Workers & Pages → Create → Pages → Connect to Git
  2. 选中你 fork 的仓库
  3. 其他啥也不用管
  4. 部署,等它跑完即可。

之后每次 git push 都会自动重新部署

当然,因为它就是个纯静态站,**Vercel、Netlify、GitHub Pages……**都能部署,看你习惯哪个。


五、结语

回头看,这个项目真正花时间的部分,几乎都不是”写功能”。

写一个能播歌的页面用AI可能一两小时就够了。剩下的时间都花在:音效的 CORS 深坑来回改了六版、随机算法从”均匀随机”推到加权洗牌、同步协议为了不把云配额打穿重构了三次、以及一堆”点开就跳过算不算播放”这种只有真用起来才会发现的问题。

这可能就是 Vibe Coding 的问题:让 AI 把第一版写出来很快,但把它变成一个真正能用、经得起日常使用的东西,还是得靠一次次踩坑和打磨。

不过至少现在,我打开自己的音乐馆,按下播放,我就可以愉快地去,边听边享受,去干别的事了,只需要开着标签页就行。

这就够了。