你好,我是吴老师。这份是给你自己读的——不用等我讲,看不懂就整段复制给 AI,加一句"请一次只解释一个概念,讲完问我一个小问题"。
读完之后,你应该能:
- 说出"歌单在板子里到底是什么"(一个数组);
- 明白"现在放到第几首"是靠一个下标记住的;
- 说清"点一首歌/切一首歌"在程序里是怎么做到先停再放的。
用时:约 60 分钟。 准备:烧录
demo01_music_player的板子 + 一张拷了.mp3的 TF 卡 + 一根 USB 线。
一、从"按顺序放"到"能点的歌单"
第 16 课我们做了个掌上音乐盒,把 TF 卡里的一首歌"读→解码→喂 I2S→出声"放了出来。可是它只会按顺序一首首往下放,你想听中间那首,得等它唱到。
这次升级:TF 卡里放一大堆歌,板子把所有歌列成一份歌单,你想听哪首就点哪首。
先看现象:上电后屏幕自动列出歌单并开始唱第一首,你手指点任意一首,它立刻切过去。
二、我观察到的现象
| 现象 | 勾选 |
|---|---|
| 上电后自动把所有歌列成歌单,开始唱第一首 | |
| 点歌单某一行,立刻切到那一首 | |
| 点 NEXT / PREV 能在上一首、下一首间切换 | |
| 点 VOL+ / VOL- 声音变大变小 | |
| 一首唱完,自动接着唱下一首 | |
| 进度条在走,显示"第几秒/总长多少" |
三、从"文件"到"歌单"的几站
| 环节 | 谁在干 | 哪课学过 |
|---|---|---|
| 扫 描 | scanDirectory() 递归翻 SD 文件夹,收所有 .mp3 | 第 14 课 |
| 排歌单 | 文件名存进数组 tracks[],sortTracks() 排序 | 本课重点 |
| 放一首 | 下标 currentTrack 指着"第几首",selectTrack() 先停再放 | 第 16 课 |
| 解码+喂声 | ESP32-audioI2S 的 Audio 库边解码边喂 I2S | 第 16 课 |
| 出 声 | ES8311 声卡 + 扬声器 | 第 11–13 课 |
跟第 16 课最大的不同:第 16 课是"点一首歌",这一课是"先扫出全部歌名做成数组,再反复从数组里取出一个下标来放"。
四、歌单,就是一排"有编号的抽屉"
| 概念 | 像什么 | 在代码里 |
|---|---|---|
| 歌单 | 有编号的抽屉柜 | 数组 tracks[]:tracks[0] 第 1 首、tracks[1] 第 2 首…… |
| 现在第几首 | 一支手指 | 下标 currentTrack:一个整数 |
| 切歌 | 手指挪一下 | 下标 +1 / 下标 −1 |
⚠️ 抽屉只是比喻,板子里没抽屉。
tracks[]存的是一串文件路径字符串,currentTrack只是一个整数。记住"下标"这个词:它就是"第几个"的整数,用来在数组里定位。
一句话:歌单 = 数组(存歌名)+ 下标(记住现在第几首);切歌 = 让下标前移/后移。
五、两个代码名,够了
1. scanDirectory(SD_MMC, "/", 0)——把 TF 卡里的歌都扫出来
scanDirectory:递归翻 TF 卡文件夹。
从根目录 "/" 开始,遇到子文件夹再往下一层(最多 4 层);
遇到结尾是 ".mp3" 的,就把路径存进数组 tracks[]。
最后 sortTracks() 按字母排队,就成了屏幕上的歌单。
屏幕上的那一列"第 1 首、第 2 首……",不是画出来的装饰,是把 tracks[] 一行行打印出来。 你改了 TF 卡里的歌,数组就变,屏幕歌单就跟着变。歌单在板子里,就是这一个数组。
2. selectTrack(index)——把播放头挪到第 index 首,然后放它
selectTrack(index):
先 audio.stopSong() // 停掉正在放的那首
currentTrack = index // 播放头挪到"第 index 首"
audio.connecttoFS(SD_MMC, tracks[index]) // 把第 index 首接进音乐管家
你点歌单某一行、或按 NEXT/PREV,最后都落到 selectTrack(someIndex),只是那个下标程序帮你算:下一首 = (currentTrack + 1) % trackCount;上一首 = (currentTrack - 1 + trackCount) % trackCount。那个 %(取余)就是"到最后一首就绕回第一首"。
最该记住的:歌单是 tracks[] 数组;播放位置是 currentTrack 下标。点歌/切歌 = 先 stopSong(),再把 currentTrack 改成一个新下标,最后 connecttoFS() 放那一首。
六、用手指点歌:这块板子真的能"点"
再留意这些细节,全都是"唱一首歌"之外的小机关:
| 现象 | 勾选 |
|---|---|
| 点歌单某一行,顶部歌名和进度条立刻换成那首 | |
| 点 NEXT 换下一首,点 PREV 换回上一首 | |
| 点 VOL+ 屏幕"VOL n"变大,VOL- 变小 | |
| 点 PAUSE 按键变 PLAY,声音停住;再点继续 | |
| 一首唱完,不点按钮,它自己跳到下一首 | |
| 正在放的那一行底色高亮 |
为什么切歌不跟上一首搅在一起? 因为 selectTrack() 先 audio.stopSong() 把旧歌停干净再换新的——像 CD 机换碟,先停再放。
一首唱完怎么自动接下一首? 靠 audio_eof_mp3 这个"播完回调":放完就把 trackFinished 置真,主循环一看到就自动下标+1。
七、改一个条件:随机 / 超过 64 首
思想实验,不用真改代码:
| 我改了什么 | 我预测 | 实际 |
|---|---|---|
| 把"下一首"改成"随机挑一首" | 把顺序的"下标+1"改成随机取下标;可能随机到同一首,要"直到不等于当前首" | |
| TF 卡里放 100 首,超过 64 首 | MAX_TRACKS = 64 是上限,超出扫不进歌单,屏幕上只显示前 64 首 |
要"随机",只是把"下标+1"换个取下标的方式;要"多放几百首",只是把数组上限调大。 核心的"数组 + 下标"一点没变——这也是把歌单做成数组的好处。
八、一条完整的"点一首歌"链路
TF 卡里的一堆 MP3 → scanDirectory 递归扫描出文件名
→ 文件名存进数组 tracks[] 并 sortTracks 排队
→ 屏幕把 tracks 列成歌单
→ 手指点某一行 / 按 NEXT 或 PREV → 算出一个下标 index
→ selectTrack:先 stopSong 停掉当前
→ currentTrack = index, connecttoFS 放这一首
→ Audio 解码 + 小段小段喂 I2S → 扬声器出声
→ drawProgress 用播放时间/总长 画进度条
→ 一首放完 audio_eof_mp3 → 自动下标+1 放下一首
真正要记住的只有一句:歌单 = 存文件名的数组 tracks[];播放位置 = 下标 currentTrack;点歌/切歌 = 让下标指着"第几首",先停再放,唱完自动下标+1。
九、过关检查
- 板子怎么知道"一共有哪些歌"?"扫描"把什么变成了什么?
- 歌名存进数组后,靠什么记住"现在放到第几首"?
- "下一首"在程序里是"下标 +几"?为什么要"取模"?
selectTrack()接新歌之前为什么要先stopSong()?- 一首唱完,板子怎么知道"该自动接下一首"?
- 进度条怎么知道"现在第几秒、总长多少"?
留下三样东西:你 TF 卡里歌的总数、你点"下一首"后停在的那个下标(第几首)、"歌单在板子里是什么"的一句话回答。
十、还能玩什么
| 作品方向 | 描述给 AI 的效果 | 再想想 |
|---|---|---|
| 随机播放 | 打乱顺序随机挑一首 | 顺序是"下标+1",随机是另一种下标取法? |
| 单曲/整单循环 | 单曲循环,或整单播完再从头 | 取模已让最后一首绕回第一首,单曲循环改哪? |
| 图片封面 | 每首歌配一张封面图 | 歌名存数组,图片也能存吗? |
| 滚动歌词 | 边放边把歌词滚出来 | 进度条在读播放时间,歌词怎么跟着时间走? |
| 收藏/歌单编辑 | 收藏、删歌、建多个列表 | "一个数组"记一首,那"很多个列表"记什么? |
下节课预告:学会"把一堆文件整理成可点的歌单、用下标在歌单里挪"后,第 39 课让板子不光放声音,还能自己看图讲完整的故事——做一本"我的电子绘本"。你会发现,"声音会放"和"画面会换"是同一套"点一下、翻到下一页"的本领。