1. 项目思路拆解:从“堆积木”到“写乐谱程序”
1.1 为什么选《天空之城》做Scratch音乐编程课
第一次在课堂上带学生做音乐项目,我其实犹豫了很久。流行歌版权是个问题,儿歌又太简单,纯音阶练习学生五分钟就腻。后来定成《天空之城》,原因有三个。
第一,这首曲子的主题旋律辨识度极高,慢速、抒情、音域适中,非常适合初学者用Scratch的“演奏音符”积木还原。第二,它的结构并不复杂,但又不是从头到尾一条直线——有前奏、主旋律、间奏,还有重复和变化,这就逼着你必须想清楚“怎么组织代码”,而不是一股脑把音符从上往下排。第三,它天然适合讲两个编程概念:消息广播和链表。
说实话,很多Scratch音乐课的问题是“太像录音机”。学生把简谱抄进去,用几十个播放音符积木堆完,播放一遍,结束。作品能响,但换一首曲子就得全部重写,项目也谈不上任何复用性。这次我换了个思路:把乐谱当作数据,把程序当作“读数据的演奏机器”。音符和节拍放进列表里,角色只负责按顺序取数、播放;多个声部之间靠广播消息协调;乐句与乐句之间用链表结构串起来,方便插曲和反复。这才是“音乐编程”和“用Scratch播放音乐”的本质区别。
1.2 音乐编程要解决的三个核心问题
用Scratch做音乐,绕不开三个问题:音符怎么表示、节拍怎么控制、多个声部怎么同步。
音符在Scratch里其实很简单。“演奏音符(60)(0.5)拍”这个积木,前面的数字是MIDI音高编号,60对应中央C,后面的数字是持续拍数。但真正难的是把简谱转成音高编号,还要保证时值换算不出错。
节拍是个更隐蔽的坑。Scratch的“等待()秒”积木和“演奏()拍”积木是两套时间系统:前者依赖程序运行的实际秒数,后者依赖软件内部的节拍器速度。如果不统一,就会出现“音符还没播完就开始播下一个”或者“每个音间隔忽长忽短”的问题。我见过太多学生的作品,谱子是对的,听起来却乱七八糟,十有八九就是节拍参数不对。
第三个问题是声部同步。《天空之城》如果只弹单音旋律,效果其实很干。加上一个简单的分解和弦伴奏,立刻就有层次感了。但在Scratch里,两个角色同时开始容易,同时结束难,中途切段落更难。这时候就需要广播消息来当“指挥”,让所有声部听着同一个“口令”行动。
1.3 为什么选“消息”和“链表”作为核心知识点
这个项目放在Scratch进阶课程里,目的不只是做出一首曲子,而是借音乐场景讲两个真正重要的编程概念。
消息,对应的是Scratch里的“广播”和“当接收到”。它的核心价值是解耦——发送消息的人不需要知道谁会接收,接收的人也不需要知道消息从哪里来。在音乐项目里,一个“指挥者”角色不需要知道现在有几个声部在演奏,只需要发出“开始”“暂停”“进入B段”“结束”这几条广播,所有声部听到后各自执行。这在代码组织上是一次思路升级:从“顺序执行”变成“事件驱动”。
链表,则是对Scratch列表的进一步抽象。Scratch原生没有指针,但我们可以用两个列表模拟:一个存数据,一个存“下一个节点的编号”。这样就能实现真正的链式结构,而不是简单的数组遍历。在这个项目里,我把整首曲子的段落拆成一个链表:前奏节点指向主旋律节点,主旋律节点指向间奏节点,间奏节点又可以指回主旋律节点实现反复。想插入一段新的乐句,只需要改一个指针值,不需要动其他数据。
这两个概念放在一起还有一个巧妙的配合:消息负责“什么时候演奏”,链表负责“演奏什么内容”。一个控制流程,一个组织数据,程序结构就清爽了。
2. 核心机制详解:音符、节拍、消息、链表到底怎么配合
2.1 音符与节拍:先算清楚再动手编程
很多教程直接给一个Scratch音高对照表,让用户照着填。但如果不理解背后的对应关系,一旦遇到谱子上没有标注MIDI号的情况,还是会卡住。
Scratch的“演奏音符()()拍”使用的是MIDI音高编号,中央C(简谱1)是60,每升高半音加1。所以简谱的do、re、mi对应60、62、64,高音do是72。如果要升一个八度,编号加12。这个规则非常稳定,只需要记住一个基准点,剩下的都可以推算出来。
节拍的计算则要仔细。Scratch底部有一个“每分钟节拍数”的设定,默认是60,也就是每拍1秒。如果曲子要以90的BPM演奏,那一拍就是60除以90,约0.667秒。四分音符记1拍,八分音符记0.5拍,二分音符记2拍。我做了一张速查表,方便学生对照:
| 音符时值 | 拍数 | 90BPM下的时长(秒) |
|---|---|---|
| 全音符 | 4 | 2.67 |
| 二分音符 | 2 | 1.33 |
| 四分音符 | 1 | 0.67 |
| 八分音符 | 0.5 | 0.33 |
| 十六分音符 | 0.25 | 0.17 |
实际做的时候,我会把BPM设成90,然后用“播放音符”积木的“拍”参数直接控制时值,不再额外加“等待()秒”。这样整个程序的节奏会以Scratch节拍器为准,不会因为电脑卡顿而漂移。
2.2 消息广播:用“指挥者”角色统一调度
在Scratch里,广播消息可以理解成“喊一嗓子”。一个角色喊“全体注意,开始演奏”,所有能听到这个消息的角色都会立刻执行各自对应的脚本。这个机制不需要发送者和接收者之间有直接关联,所以特别适合音乐合奏。
我的项目里设了一个“指挥者”角色,它的任务不是演奏,而是发广播。整个流程是这样的:舞台出现“开始”按钮,点击后指挥者广播“前奏开始”;前奏播完,广播“主旋律开始”;主旋律完成,广播“间奏开始”;间奏结束,广播“主旋律反复”。每个乐器角色只需要写好“当接收到XX消息”对应的乐句,剩下的交给指挥者。
使用消息机制最大的好处是,加一个声部非常容易。比如我在项目里加了一个“低音”角色,它不需要修改主旋律角色的任何代码,只需要自己写三段响应脚本——接收到“前奏开始”就弹低音,接收到“主旋律开始”就弹和弦根音,如此类推。这种可扩展性,用“顺序执行”是做不到的。
2.3 链表结构:用两个列表模拟真正的链式存储
Scratch的“列表”本质上是数组,元素按顺序存储,按下标访问。但链表不一样,它每个节点除了存数据,还存着“下一个节点在哪里”。这样数据不一定要在物理空间上连续排列,插入和删除也更灵活。
在Scratch里模拟链表,我用两个列表。第一个列表叫“节点数据”,存每个节点的内容,比如段落名称、起始音符在乐谱中的位置、重复次数。第二个列表叫“下一节点”,存的是下一个节点的编号。如果某个节点是最后一个,它的“下一节点”填0,表示结束。
举个具体例子。我的《天空之城》项目总共有5个节点:
| 节点编号 | 节点数据(段落名) | 下一节点 |
|---|---|---|
| 1 | 前奏 | 2 |
| 2 | 主旋律A | 3 |
| 3 | 间奏 | 4 |
| 4 | 主旋律A(反复) | 5 |
| 5 | 结尾 | 0 |
指挥者角色拿到的是一个“当前节点编号”变量,初始为1。每次演奏完一段,它就去查“下一节点”列表找到下一个编号,更新当前节点,然后继续。这个过程在数据结构课本里叫“链表遍历”,在Scratch里就是“变量读取列表值再写回变量”,但背后的逻辑是完全一致的。
2.4 消息与链表的结合点:事件驱动的乐句循环
更有意思的是,消息机制和链表并非各管各的,它们可以配合起来形成事件驱动的演奏循环。
我设计的指挥者脚本是“死循环”加“条件判断”的结构。角色先读链表当前节点,判断节点类型:如果节点数据是“前奏”,就广播“前奏开始”,等待3秒让声部播完,然后通过指针跳到下一个节点。如果节点是“主旋律A”,就广播“主旋律A开始”,但这次等待的时间需要通过计算得出,因为主旋律比前奏长。这个等待时间是从乐谱总长度的数据里算出来的,保证广播不会过早发出。
这个设计解决了一个常见问题:如果只靠“等待()秒”,时间都是写死的,换一首歌全部要重新调。但把段落长度也变成数据存进链表之后,指挥者的逻辑就不用改了,它只需要读数据、广播、等待、跳转。这就是数据结构让程序变得“通用”的实际价值。
3. 实操过程:从简谱到完整Scratch项目
3.1 乐谱准备:从《天空之城》主题中提取音符数据
正式开始写代码之前,我先带学生把简化版《天空之城》主题的简谱整理出来。我们用的是C大调简化版,主旋律开头大致是:
| 小节 | 简谱音符(小节内) | 时值说明 |
|---|---|---|
| 1 | 6 7 1 3 | 八分、八分、八分、四分 |
| 2 | 7 1 3 7 | 八分、八分、四分、八分 |
| 3 | 6 7 1 3 | 八分、八分、八分、四分 |
| 4 | 7 1 3 7 | 八分、八分、四分、八分 |
在C大调里,简谱的1对应MIDI号60,那么6对应69、7对应71。高音1是72,高音3是76。把这些转成MIDI号数组就是:
69, 71, 72, 76, 71, 72, 76, 71, 69, 71, 72, 76, 71, 72, 76, 71
时值数组对应的是:
0.5, 0.5, 0.5, 1, 0.5, 0.5, 1, 0.5, 0.5, 0.5, 0.5, 1, 0.5, 0.5, 1, 0.5
这两个数组放列表里,一个是音高表,一个是节拍表。演奏时循环从第1项开始,每轮读取一个音高和一个节拍,用“播放音符”积木演奏,直到读完整张表。
3.2 建立乐谱数据表:音高列表和时值列表
在Scratch的“变量”分类里新建三个列表:“音高表”“节拍表”“当前指针”。前两个是全局的,给演奏角色用;第三个是演奏角色私有的,防止和其他角色冲突。
音高表建立的时候,不要一个一个手动输入,太容易错。我习惯先在纸上把简谱列好,然后批量输入。Scratch的列表编辑器支持一次粘贴多行数据,每行一个数字,比在积木里一个个加要快得多。节拍表也一样,我按八分音符0.5拍、四分音符1拍的规则填好。
这里有一个关键细节:音高表数量和节拍表数量必须完全一致。学生最容易犯的错就是中间漏了一个音符,结果音高和节拍错位,演奏出来节奏完全对不上。我在项目里专门加了一段初始化逻辑,让角色播放前检查两个列表的长度是否相等,不相等就停下来报错。
3.3 演奏主旋律角色:按列表数据播放音符
主旋律角色的代码结构非常简单,核心就三步:读取当前指针指向的音高,读取对应位置的节拍,播放后让指针加1。
用Scratch积木描述大概是:
当按下空格键 将 [当前指针 v] 设为 [1] 重复 (音高表 的项目数) 次 播放音符 (音高表 的第 (当前指针) 项) (节拍表 的第 (当前指针) 项) 拍 将 [当前指针 v] 增加 (1)这个循环最大的特点是“数据驱动”——角色本身不关心乐谱内容是什么,它只负责按编号取数、播放、移动指针。换成任何一首歌,只要重新填充列表内容,角色代码一行都不用改。
但要提醒的是,“播放音符”积木本身不会阻塞程序,它只是发起一个发声任务,程序会立刻继续执行下一条指令。如果循环里不加任何等待,所有音符会在极短时间内触发,结果就是“嗡嗡”一声混响,完全听不出旋律。正确做法是利用“播放音符()拍”的拍数参数,让它自然占用对应的时间;如果使用“演奏音符()()秒”积木,则需要手动加“等待()秒”,而且时长必须和音符一致。
3.4 指挥者角色:用链表遍历控制整首乐曲的段落切换
有了主旋律,接下来就是整首歌的段落调度。我的做法是做一个单独的“指挥者”角色,它不发声,只负责发广播。
先在项目里建立两个全局列表:“段落名称”和“下一节点”,并填入第一节里那组数据。再建立一个私有变量“当前段落”,初始为1。
指挥者角色的积木逻辑:
当点击绿旗 将 [当前段落 v] 设为 [1] 重复直到 <(当前段落) = [0]> 如果 <(段落名称 的第 (当前段落) 项) = [前奏]> 那么 广播 [前奏开始 v] end 如果 <(段落名称 的第 (当前段落) 项) = [主旋律A]> 那么 广播 [主旋律A开始 v] end 如果 <(段落名称 的第 (当前段落) 项) = [间奏]> 那么 广播 [间奏开始 v] end 如果 <(段落名称 的第 (当前段落) 项) = [结尾]> 那么 广播 [结尾开始 v] end 等待 [段落时长 v] 秒 将 [当前段落 v] 设为 (下一节点 的第 (当前段落) 项) end这里的“段落时长”也是一个列表,每一项存的是该段落预计需要多少秒。这个值怎么算?我先把段落里所有音符的拍数加起来,换算成秒数,再多加0.3秒的余量。比如主旋律A一共12拍,BPM=90时一拍0.667秒,12拍就是8秒,那“段落时长”里就填8.3。
这样设计之后,如果我想让《天空之城》在间奏之后重复一遍主旋律再进结尾,我只需要把“下一节点”列表里间奏那一项的“3”改成“4”,让间奏指回主旋律A节点,而不是直接把一整段代码复制一遍。这就是链表相对数组的直观优势。
3.5 多声部合奏:让每个角色监听不同的消息
指挥者发广播只是第一步,真正让音乐有立体感的是多个声部同时响应。我在项目中加了三个演奏角色:主旋律、分解和弦、低音。
分解和弦角色接收“主旋律A开始”后,它不会去读主旋律的音高表,而是读取自己的“和弦音高表”和“和弦节拍表”。我给它预设的是一组“1-3-5-3”的分解和弦循环,每小节重复一次,和主旋律形成对位。
低音角色更简单,它只弹每小节的根音,一拍一个,时值都用1拍。三个角色都通过广播消息触发,所以它们之间的代码是彼此独立的。主旋律角色不需要知道低音角色存在,低音角色也不需要关心主旋律在哪个小节。只要广播消息的时间点正确,整个合奏就能对齐。
这个结构的扩展性非常好。以后我想加一个打击乐声部,只需要新增一个角色,监听同样的广播消息,然后用“击打”类积木演奏节奏。不需要修改任何现有角色的代码。
4. 常见问题与排查技巧实录
4.1 高频踩坑点:音符、节拍、广播的典型故障
做这个项目时,学生和学员问得最多的问题集中在几个地方。我把高频故障整理成一张速查表,方便遇到问题直接查:
| 现象 | 可能原因 | 排查思路与解法 |
|---|---|---|
| 所有音符挤在一起,听不清旋律 | 循环里没有等待,或用了“播放音符()秒”但没加“等待” | 改用“播放音符()拍”并确保BPM正确 |
| 高低音不对 | MIDI号换算错误,或简谱识别错误 | 回到中央C=60的基准重新推算,逐个对照 |
| 节拍忽快忽慢 | 等待秒数和拍数混用,电脑卡顿导致漂移 | 统一用拍数控制,不在循环里混用两套计时 |
| 广播接收方不执行 | 广播消息名称和接收积木里的消息名称不完全一致 | 检查消息名称是否“精确相同”,包括大小写和空格 |
| 主旋律播完了但伴奏还没结束 | 各声部段落总时长不同,指挥者等待时间不够 | 给“段落时长”增加余量,或按最慢声部计算 |
| 段落顺序错乱 | 链表“下一节点”数据填写错误 | 检查每个节点的下一节点编号,尤其注意是否有指向自己的死循环 |
4.2 调音与节奏校准的实用技巧
音乐项目的调试比普通Scratch项目难一点,因为你没法“单步运行”听效果。我自己摸索出一个办法:把“演奏速度”临时调到很慢,比如把BPM降到30,让每个音符都拖得很长,这样就能听清楚音高对不对、先后顺序对不对。确认无误后再把BPM调回90。
另一个技巧是把演奏角色“按段落拆开测试”。指挥者暂时不给段落时长,改成手动按“1”“2”“3”键分别广播“前奏开始”“主旋律A开始”“间奏开始”。这样可以在开发阶段只测试某一个声部的某一段,等每段都正确了,再接通指挥者做整体联调。这比每次从头播到尾、发现问题还得听半分钟强得多。
4.3 表演模式:让作品从“自己听”变成“给别人看”
很多学生做完之后只是点绿旗播放一遍,演示效果很平淡。我给项目加了一个“表演模式”:舞台上放三个按钮,分别控制“开始演奏”“暂停/继续”“停止”。
暂停和继续的实现很有意思,也是消息机制的典型应用。指挥者广播“暂停”后,所有声部角色进入一个“当接收到暂停,等待直到接收到继续”的状态。这里用到了Scratch的“等待直到”积木,条件写“接收到继续”。注意暂停前要先把当前段落保存到变量里,继续时从这个段落重新开始,而不是从头播。
演出时还可以配合“外观”积木做一些视觉效果。比如低音角色播放时切换成深色造型,主旋律角色切换成亮色造型,让观众眼睛看到的声音来源和耳朵听到的一致。这虽然不影响程序逻辑,但对课堂展示来说效果提升巨大。
4.4 进一步扩展:把链表升级成可编辑乐谱
项目做完之后,我建议学有余力的学生做一个“歌词编辑器”功能:在舞台上动态修改链表里的段落名称和下一节点指向,甚至把某一整个节点里面的音符数据换掉。这样作品就变成一个简易的“音乐播放器”,不只能放《天空之城》,还能放其他任何收入列表的曲子。
我在实际操作中的体会是,学生一旦理解了“数据与程序分离”的思路,会开始主动设计自己的音乐作品,而不再满足于照着谱子堆积木。这也是这个项目最值得肯定的地方——它不只是在教Scratch,而是在借Scratch传达“用数据结构组织程序”的思维。最后再分享一个小技巧:如果想让《天空之城》的结尾更有余韵,可以把“结尾”节点的下一节点设置为0之前,先广播一次“渐弱”消息,让主旋律角色播放一个时值4拍的长音,同时把音量在循环里逐渐调小到0。这个效果用Scratch的“将音量设为”积木加循环就能实现,却能让作品听起来非常完整,值得一试。