你好,我是吴老师。
这份是给你自己读的——不用等我讲,你一段一段往下看,看到要动手的地方就真去动手。
这一课的重点不是"听懂",是"做出来"。 你要亲手把一个只会响的板子,一步一步变成一台能放歌、能选歌、能调音量的音乐盒。
读完之后,你应该能:让板子放 TF 卡里的歌;给音乐盒加上"显示歌名、调音量、上一首下一首、随机播放";说清一首歌是怎么从卡里跑到扬声器的。
用时:90 分钟(大部分时间在动手)。准备:板子、TF 卡(FAT32)、数据线、Skill 已接入的 WorkBuddy。
一、今天做一个"掌上音乐盒"
先看成品——这台音乐盒做完是这样用的:
屏幕上列着一排歌名
↓
手指点哪首 → 就放哪首
底部一排按钮 → 音量− / 上一首 / 播放暂停 / 下一首 / 音量+
播放时 → 顶部显示歌名 + 进度条,RGB 灯跟着音乐呼吸
先别急着做。第一步是把歌准备好。
第一步:把歌拷进 TF 卡
我给你准备了 5 首示例歌曲,都是用代码合成的,没有版权问题,随便用、随便改:
| 歌名 | 风格 | 时长 |
|---|---|---|
| 01 小星星变奏曲 | 温柔,最经典 | 27 秒 |
| 02 芯片冒险 | 快节奏,8-bit 游戏风 | 14 秒 |
| 03 电子钟表舞曲 | 规整四拍,像电子表报时 | 16 秒 |
| 04 安静的雨夜 | 慢速,适合睡前听 | 27 秒 |
| 05 生日快乐 | 大家都熟 | 14 秒 |
(格式:44.1kHz 立体声 16 位 MP3 —— 和板子的配置一致,拷进去就能放)
怎么放进去:把 TF 卡插到电脑(或读卡器),把这 5 个 .mp3 拷到卡的根目录,
拔下来插回板子。
为什么要自己准备歌,不能上网找?
网上的流行歌都是商业版权音乐——你能自己听,但不能交给别人、更不能放进公开的课程仓库。 所以我干脆用代码合成了一批:旋律就是一张"音符表",你想改哪首就改哪首。
想自己造歌?看
assets/music/README.md——跑一个脚本就能生成, 用的是你第 11 课就学过的「旋律 = 一串频率 + 时长」。
准备好之后,回答一个问题(做的时候留意):
一首歌是怎么从卡里跑到扬声器的?中间经过了几个地方? (先猜一猜,第二节你就会看到答案。)
二、第一步:先让它能放歌
请使用课程的 esp32s3-touchboard-hardware-dev Skill,
烧录 assets/codes/demo01_play_song,让板子上电自动播放 TF 卡里的歌。
播放前先检查声卡(ES8311)和功放初始化方式。
烧进去之后,上电先别动,观察 30 秒,然后边玩边记:
| 我做的 / 我观察的 | 我看到的 |
|---|---|
| 刚上电,板子自己做了什么? | |
| 屏幕上显示了什么?(歌名?进度条?) | |
| 按一下 BOOT | |
| 再按一下 BOOT | |
| RGB 灯在播放时什么样?暂停后呢? | |
| 一首放完了会怎样? |
能放歌了。恭喜——最难的一步已经过去了。
顺便搞清楚:这首歌是怎么跑到扬声器里的
现在回头看第一节那个问题。一首歌要走 4 步:
| 步骤 | 谁在干 | 哪一课学的 |
|---|---|---|
| ① 读文件 | SD_MMC 把 MP3 文件从卡里读出来 | 第 14 课 |
| ② 解 码 | 解码库把 MP3 还原成声音点(PCM) | 本课 |
| ③ 送声音 | 一小段一小段送进 I2S | 第 13 课 |
| ④ 出 声 | I2S → ES8311 → 扬声器 | 第 11 课 |
四步里有三步你早就学过了,只有第②步"解码"是新的。
为什么要"解码"?
因为 MP3 是"压缩"过的——体积可能只有原始声音的 1/10。
打个比方:一箱苹果要寄外地,直接寄又大又贵;聪明做法是榨成汁寄一小瓶,到了兑水还原。
- 卡里的 MP3 = "浓缩汁"(小)
- 解码 = "兑水还原"(还原成声音点)
- 还原后走 I2S 送到扬声器
⚠️ 但要说清楚:MP3 是有损压缩——压缩时把一部分声音信息直接扔掉了 (专挑人耳听不出来的扔:被大声音盖住的小声音、20kHz 以上的高频……)。 扔掉的部分是加不回来的,所以"还原"出来不等于和原版一模一样。 大多数情况下听不太出来,但反复压缩、或者用好音响听,就能听出差别。
三、第二步:把歌名显示出来
现在能放歌了,但你根本不知道在放哪首。
第二步:让屏幕上显示歌名。
别急着问 AI——先自己写一版提示词。
想一想,要让 AI 帮你做这件事,你得交代清楚哪几件事? (比如:显示什么?显示在哪?用什么字体?歌名太长怎么办?)
在下面写一版:
| 我要让 AI 知道的事 | 我打算怎么说 |
|---|---|
| 显示什么 | |
| 显示在屏幕哪里 | |
| 用什么字体(中文?英文?) | |
| 要不要进度条? | |
| 还有什么要交代的? |
写完了,再点开看看我写的版本——对比一下,你想漏了什么?
点开看:参考提示词(先自己写完再看)
请在播放器上加:屏幕上显示当前歌曲的文件名(不要扩展名),
再加一个播放进度条。
用课程的中文字库,中英文都要能显示。
对比一下:你的版本里有没有提到"用课程的中文字库"? 如果不提,AI 可能用别的方案,中文就会显示成方块(第 9 课那个坑)。
这就是写提示词的关键:把你知道、但 AI 不知道的事说清楚—— 比如"这块板有专门的课程字库"。
烧进去看看——看到歌名了吗?
这里有个小坑,提前告诉你:歌名太长会超出屏幕。 屏幕只有 240 像素宽,一个汉字 16 像素、一个字母 6 像素—— 算一算,一行最多放得下几个字?
所以程序里要把太长的名字截断,末尾加个"…"。 如果显示成一团乱或者跑到屏幕外,就是这个没处理。
做完这一步,你的音乐盒已经"看得见"了。
顺便说个"必须一直转"的东西
你可能在代码里看到一个奇怪的函数:audio.loop(),它在主循环里一遍一遍地转。
主循环里有一句 audio.loop(),每转一圈都要跑一次,一次都不能少。
为什么? 因为一首歌几十 MB,板子内存装不下,不可能一次解完。
audio.loop() 每转一圈只解一小段、送一小段——边解码边播放。
就像水龙头:一边进水(解码)、一边出水(播放), 而不是"先把缸接满、再倒出去"。
它一停,歌就卡住——传送带的电机不能停。
四、第三步:加上音量调节
歌在放了,名字也看到了。但声音太大/太小怎么办?——现在只能改代码重烧。
第三步:加上音量调节。
先自己写提示词。 这次要想的是:
- 用什么来调?(BOOT 键?屏幕上的按钮?两个都要?)
- 调大调小分几档?屏幕上要不要显示当前音量?
- 调完之后,歌是继续放还是重头开始?
在下面写一版,再点开对照:
| 我要让 AI 知道的事 | 我打算怎么说 |
|---|---|
| 用什么操作来调 | |
| 音量范围 / 分几档 | |
| 屏幕上显示什么 |
点开看:参考提示词(先自己写完再看)
请给播放器加上音量调节:用 BOOT 键或者屏幕上的按钮,
把音量调大/调小,屏幕上显示当前音量。
对比一下:你有没有说清"调完之后歌不能停"? 你要是不说,AI 可能写成"改音量就重新开始播"——那不是你想要的。
凡是你在意的细节,都要写进提示词。AI 不会替你猜。
它背后其实只做了一件事:告诉声卡"现在音量调到几档"。
🔍 想一想:改音量的时候,歌会不会停一下?
你调音量时歌是继续放的——因为音量是"配置", 走的是第 13 课学的 I2C,而声音数据在 I2S 上照常跑,两条路互不干扰。
这就是第 13 课那句"I2C 管事儿,I2S 送货"在真实作品里的样子。
五、第四步:加上"上一首 / 下一首 / 暂停"
一首放完了会自己跳下一首。但放到一半想换歌呢?
第四步:加上一排按钮。
先自己写。 这一步要交代的事情比较多,想一想:
- 要哪几个按钮?分别叫什么?
- 屏幕只有 240×320,按钮要多大才好点?(想想第 8 课:手指尖接触面积约 8~10 像素)
- 按钮排在哪?横排还是竖排?
- 点下去有没有反馈(变色)?
| 我要让 AI 知道的事 | 我打算怎么说 |
|---|---|
| 要几个按钮、分别是什么 | |
| 按钮多大、排在哪 | |
| 点下去有什么反馈 | |
| 屏幕尺寸(要不要告诉它?) |
点开看:参考提示词(先自己写完再看)
请在屏幕底部加一排按钮:音量−、上一首、播放暂停、下一首、音量+
(屏幕分辨率 240×320 竖屏,按钮要够大,手指点得准)。
手指点哪个就做哪个。
对比一下:
- 你告诉 AI 屏幕是多大的了吗?不说的话,它可能按别的尺寸排版,按钮就跑出屏幕了;
- 你说"按钮要够大,手指点得准"了吗?这句话看着像废话, 但它能把 AI 引到"按手指来设计按钮尺寸"(第 8 课学过:接触面积约 8~10 像素)。
提示词不是越短越好——把约束条件说清楚,AI 才不会跑偏。
屏幕上点一下、歌就换——这就是第 8 课学的"按钮是判断出来的": 程序判断你手指的坐标落在哪个按钮的格子里,就执行对应的动作。
| 你点的按钮 | 背后调的函数 |
|---|---|
| 播放 / 暂停 | audio.pauseResume() |
| 上一首 / 下一首 | 换一个文件,再 audio.connecttoFS(...) |
| 音量 + / − | audio.setVolume() |
做完这一步,它已经真的像一台音乐盒了。
六、第五步:加上"随机播放"(新增,不是替换)
现在是按顺序一首首放。想要随机呢?
⚠️ 注意这一步的要求:是新增一个模式,不是把原来的顺序播放换掉。
也就是说,做完之后你的音乐盒应该有两种模式: 顺序播放和随机播放,而且能在两种之间切换。
第五步:新增一个"播放模式"按钮。
先自己写。 这一步最容易写漏——想清楚两件事:
- 是"加一个新功能"还是"把旧的换掉"?这两句话 AI 理解得完全不一样;
- 随机挑的时候,会不会挑到正在放的这首?
| 我要让 AI 知道的事 | 我打算怎么说 |
|---|---|
| 是新增还是替换? | |
| 怎么切换两种模式? | |
| 随机挑要注意什么? | |
| 有哪些东西不许动? |
点开看:参考提示词(先自己写完再看)
请给播放器【新增】一个"播放模式"功能,不要改掉原来的顺序播放:
1. 在屏幕上加一个"模式"按钮,点一下在【顺序播放】和【随机播放】之间切换;
2. 按钮上要显示当前是哪一种模式(比如写"顺序"或"随机");
3. 顺序模式 = 原来那样往下放;
随机模式 = 下一首随机挑,但别挑到正在放的那一首;
4. 其余功能(音量、上一首下一首、进度条)都不动。
对比一下这三个点(都很容易漏):
漏了会怎样 所以要说 AI 把顺序播放换掉了 明确说"新增,不要改掉原来的" 随机连续挑到同一首,听起来像卡了 明确说"别挑到正在放的那一首" AI 顺手改了别的地方,把好的搞坏了 明确说"其余功能都不动" 最后一条最重要:改程序时加一句"别的都不许动", 能避免 AI 把已经跑通的部分改坏——这是保护你的作品。
做法:让程序多记一件事——"现在是什么模式"。
程序里新增一个标记:现在到底是【顺序】还是【随机】
选下一首的时候,看这个标记:
如果是【随机】 → 随便挑一首(别挑到正在放的那首)
如果是【顺序】 → 还按原来那样,往下走一首
"模式"按钮要做的,就是把那个标记翻个面(顺序 ↔ 随机)。
💡 注意你刚才做了什么: 你没有动"怎么把歌放出来"(解码、I2S、音量、进度条全没碰), 只是多了两条"选哪首歌"的路。
这就是好代码的样子——一个功能一小块: "放歌"和"选歌"是两件独立的事,加新功能不用拆老功能。
七、你还能加什么
上面五步做完,你的音乐盒已经能听、能看、能点了。 下面这些都是你能自己加的(挑一个,照着同样的套路做):
| 想法 | 怎么做 | 会用到 |
|---|---|---|
| 单曲循环 | 播完不跳下一首,还放这首 | 改 nextTrack 的判断 |
| 播放列表翻页 | 歌太多一屏放不下,加"上一页/下一页" | 第 8 课的触摸判断 |
| 灯效更炫 | 改成随音量大小呼吸(现在只是彩虹) | 要读当前音量 |
| 记住上次听到哪首 | 关机前把歌名写进 TF 卡,下次开机接着放 | 第 14 课的写文件 |
| 显示歌词 | 找一份歌词文件,按时间显示对应的行 | 第 9 课的中文显示 |
| 语音点歌 | 说"放第三首" | 第 12 课麦克风 + 第 24 课语音识别 |
做任何一个之前,先问自己两个问题:
- 这件事属于哪一小块?(选歌?显示?交互?)
- 我能不能只改那一小块,别的不动?
这两个问题问顺了,你就能改任何程序。
八、故障排查:没声音怎么办
| 现象 | 先检查 |
|---|---|
| 屏幕正常但没声音 | 看屏幕提示:是"没找到 SD"还是"声卡没起来"? |
| 一直不动、不播放 | TF 卡里真有 .mp3 吗?放在根目录了吗? |
| 放几秒就停 / 卡顿 | 换一首歌试试(有些歌码率太高) |
| 触摸点了没反应 | 触摸初始化了吗?坐标旋转对吗? |
| 有杂音 | 换 USB 供电(别用充电宝);音量别拉满 |
| 歌名显示成乱码 | 中文字库加载了吗? |
排查的顺序:先看屏幕上的提示(它在告诉你哪一步失败了),再看文件,最后才怀疑代码。
九、自检小测
先自己想,再点开对答案。
1. 一首歌从 TF 卡到扬声器,经过哪 4 步?
① 读文件 SD_MMC 把 MP3 文件读出来
② 解 码 解码库把它还原成声音点(PCM)
③ 送声音 audio.loop() 一小段一小段送进 I2S
④ 出 声 I2S → ES8311 声卡 → 扬声器
四步里只有第②步"解码"是本课新学的,其余在第 11/13/14 课都学过。
2. MP3 为什么比原始声音小?解码在干什么?
因为 MP3 被压缩过——它把一部分声音信息直接扔掉了 (专挑人耳听不出来的扔:被大声音盖住的小声音、20kHz 以上的高频等), 所以体积可能只有原来的 1/10。
解码就是把剩下的部分按规则展开,还原成能送进扬声器的声音点。
⚠️ 扔掉的部分是加不回来的,这叫"有损压缩"。 大多数情况下听不太出来,但不等于和原版一模一样。
3. 加音量的时候,歌为什么不会停一下?
因为音量和声音走的是两条不同的线。
- 音量是"配置",走 I2C(偶尔说一次)
- 声音数据在 I2S 上照常跑(一刻不停)
两条路互不干扰,所以调音量不影响正在播的歌。
这正是第 13 课那句"I2C 管事儿,I2S 送货"。
4. `audio.loop()` 为什么必须在主循环里一直转?
因为歌不能停。
一首歌几十 MB,板子内存装不下,不可能一次解完——
audio.loop() 每转一圈只解一小段、送一小段,边解码边播放。
它一停,就没人往扬声器送数据了,歌就卡住。 就像传送带的电机不能停。
5. 为什么要让解码和画屏"分时间"?
因为 CPU 只有一双手。
解码和画屏都要占 CPU 时间。如果画屏每圈都花很久,解码就被挤得没机会跑——歌就卡了。
所以真实的程序是这样安排的:
每一圈:
loop() 里的解码 ← 每圈都做,一次不能少
每隔 250 毫秒才画一次屏 ← 攒着做,几百圈才轮到一次
屏幕晚 0.25 秒更新你看不出来,但歌晚 0.02 秒就"咔"一声了。
6. 加"随机播放"时,你只改动了哪一小块?这说明什么?
只改了"下一首选哪首"那一步:
原来:下一首 = 当前这首 + 1
现在:下一首 = 随便挑一首
就这么一处。 解码、I2S、音量、界面全都没动。
这说明好代码是分块的——"选歌"和"放歌"是两件独立的事。 改一个功能只动一小块,这就是你能不能自己改造别人程序的关键。
十、最后
这一课你亲手做出了一台音乐盒——从"能放歌"到"能点、能调、能随机", 每一步都是你自己加上去的。
第 3 章(声音与存储)到这里就结束了。 从第 11 课的"让板子响一声",到现在这台音乐盒,你已经会用: 扬声器、麦克风、I2S 总线、TF 卡、WAV、MP3——
下一章(第 17 课起),板子要出门上网了。