我做了一个本地音乐播放器,致敬小时候的千千静听:技术、UI 与情怀实录
目录
- 缘起:为什么我不想要任何在线音乐软件
- 目标拆解:本地播放器到底要做什么
- 技术选型:浏览器够用,不必上 Electron
- 核心实现一:扫描音乐库与 ID3 标签
- 核心实现二:
.lrc歌词逐行同步 - UI 定制:本地播放器最大的快乐
- 三个让我卡了一阵的坑
- 结尾:本地软件的"安静"是一种奢侈
1. 缘起:为什么我不想要任何在线音乐软件
我有几千首歌躺在硬盘里,每首基本都配好了 .lrc 歌词文件和封面图。这是多年攒下来的"音乐资产"。
但我打开任何一款主流播放器,体验都让我皱眉:
- 启动先弹广告/推荐位,我要找的"播放本地音乐"入口藏得老深;
- 强推在线曲库、登录账号、VIP 提示,每次都要点"跳过";
- UI 风格统一是统一了,但不是我喜欢的样子——我想让封面占满半屏,它偏要列表占大头;
- 隐私顾虑:我的本地文件路径、播放习惯,谁知道有没有被上报。
我怀念小时候的千千静听——打开就能放,安静又听话。既然现在的播放器都做不到,那就自己做一个。
2. 目标拆解:本地播放器到底要做什么
和写任何工具一样,我先把"做一个播放器"拆成可验收清单:
| 诉求(人话) | 拆解后(可验收) |
|---|---|
| 能放歌 | 选择文件夹 → 列出所有音频文件 → 点击播放 |
| 显示信息 | 解析 ID3 标签:标题、艺术家、专辑、封面图 |
| 显示歌词 | 解析同名 .lrc 文件,按时间戳逐行高亮、自动滚动 |
| 顺序/随机/循环 | 三种播放模式可切换,列表结束有明确行为 |
| 进度可控 | 拖动进度条、上一首/下一首、音量调节 |
| UI 我说了算 | 整体视觉风格、封面尺寸、列表布局可自定义 |
这张表决定了播放器的骨架。功能不多,但每一项都做到位——这是我对所有"自己用的工具"的一贯要求。
3. 技术选型:浏览器够用,不必上 Electron
一开始我犹豫过要不要上 Electron,但仔细想需求——我不需要系统级文件监听、不需要托盘、不需要原生解码,浏览器的能力完全够用:
| 能力 | 浏览器方案 |
|---|---|
| 选文件夹 | <input type="file" webkitdirectory> |
| 读音频 | URL.createObjectURL(file) + <audio> |
| 读 ID3 标签 | jsmediatags 库 |
解析 .lrc |
自己写一个 30 行的解析器 |
| 播放控制 | 原生 HTMLAudioElement API |
整个播放器就是一个 HTML 文件,双击就能开。没有安装包、没有更新提示、没有后台进程——这本身就是对"千千静听精神"的一种继承。
心得:选型时先问"我到底需要什么",而不是"什么技术更高级"。本地小工具的最高境界是双击即用、关掉即走。
4. 核心实现一:扫描音乐库与 ID3 标签
选完文件夹后,第一步是把音频文件筛出来,并读取每首歌的元数据:
const audioFiles = [...files].filter(f =>
/\.(mp3|flac|wav|m4a|ogg)$/i.test(f.name)
);
for (const file of audioFiles) {
const tags = await readTags(file); // 用 jsmediatags 解析
tracks.push({
file,
title: tags.title || file.name.replace(/\.\w+$/, ''),
artist: tags.artist || '未知艺术家',
album: tags.album || '未知专辑',
cover: tags.picture ? blobFromTag(tags.picture) : null,
});
}
几个细节值得说:
- ID3 标签不一定准。很多老歌的标签是空的或者乱码,所以文件名永远作为兜底,不能完全依赖标签;
- 封面图从 ID3 里取。
jsmediatags给的是二进制数据,要转成Blob再URL.createObjectURL才能给<img>用; - 没有封面时给占位图。统一视觉的前提是"无封面也有样子",不要让某首歌塌成空白。
5. 核心实现二:.lrc 歌词逐行同步
这是整个播放器我最喜欢的部分。.lrc 文件长这样:
[ti:歌曲名]
[ar:歌手]
[00:12.50]第一行歌词
[00:18.20]第二行歌词
[00:24.80]第三行歌词
解析器其实非常短:
function parseLrc(text) {
return text.split('\n')
.map(line => {
const m = line.match(/\[(\d+):(\d+\.\d+)\](.*)/);
if (!m) return null;
return { time: +m[1] * 60 + +m[2], text: m[3].trim() };
})
.filter(Boolean)
.sort((a, b) => a.time - b.time);
}
同步显示的逻辑:监听 audio.timeupdate,找到当前时间对应的最后一行,高亮它并滚动到视图中央。
audio.addEventListener('timeupdate', () => {
const t = audio.currentTime;
const idx = lrc.findIndex((line, i) =>
line.time <= t && (i === lrc.length - 1 || lrc[i + 1].time > t)
);
highlightLine(idx);
scrollToLine(idx);
});
心得:歌词同步的关键不是"解析",而是"滚动得舒服"——用 scrollIntoView({ behavior: 'smooth', block: 'center' }) 让当前行缓缓滑到中间,比硬跳转观感好十倍。这种细节,是本地播放器才愿意花心思打磨的地方。
6. UI 定制:本地播放器最大的快乐
本地播放器最大的特权,是 UI 完全我说了算。我给自己定了几个商业播放器不会这么做的设计选择:
- 封面占左半屏,满铺。商业播放器为了塞推荐位不敢这么干,我没有这个包袱;
- 歌词占右半屏,逐行渐变高亮。当前行最亮,前后两行半亮,其余暗淡——像聚光灯打在舞台上;
- 列表是抽屉,不是主界面。想切歌时滑出来,平时隐去,让封面和歌词独占视野;
- 配色随封面变化。用 Canvas 提取封面主色,动态调整背景渐变——每张专辑都有了自己的氛围。
这些设计在商业产品里几乎不可能出现——它们太"个人化"了。但本地播放器的意义,恰恰就是让软件贴合你的审美,而不是让你迁就软件的审美。
7. 三个让我卡了一阵的坑
坑一:<audio> 跨格式支持差异。 Chrome 不支持 FLAC?实际上新版 Chrome 已经支持,但某些 .m4a 编码仍会失败。我的处理是播放失败时降级提示,并在扫描阶段标注"可能不兼容"的格式,不让用户点了没反应才报错。
坑二:.lrc 时间戳精度不一。 有的歌词文件是 [00:12.50](两位小数),有的是 [00:12.500](三位),还有的是 [00:12](没小数)。正则要写成 \d+\.\d+ 兼容小数位不固定的情况,否则部分歌词行解析不出来。
坑三:大音乐库的卡顿。 几千首歌一次性读 ID3 标签会卡死界面。改成惰性加载——列表先显示文件名,标签在滚动到可视区域时再读。这个优化让几千首歌的库也丝滑。
8. 结尾:本地软件的"安静"是一种奢侈
做完这个播放器,我对"本地软件"有了一种新的理解:
- 它不抢你注意力。没有推荐位、没有红点、没有推送,打开就是为了放歌;
- 它尊重你的数据。文件、歌词、封面都在你的硬盘里,软件只是"播放它们",不是"管理它们";
- 它尊重你的审美。UI 是你自己的,不是产品经理 A/B 测出来的。
在这个所有软件都在抢用户时长的时代,一个"打开就做事、关掉就消失"的本地工具,反而成了一种奢侈。
如果你也有一堆本地音乐、也怀念千千静听那种安静,我强烈建议你自己写一个。技术门槛比想象中低,一个 HTML 文件就够;真正难的是克制住加功能的冲动——不做推荐、不做社交、不做"发现",就做一个老老实实的播放器。
工具小而静,价值往往不在多,而在"它永远在那里,永远不打扰你"。
- 点赞
- 收藏
- 关注作者
评论(0)