Arduino音乐制作入门:从蜂鸣器原理到《小星星》实战
1. 从零开始:为什么用Arduino做音乐是个好主意?
你可能觉得音乐制作是专业软件和昂贵设备的事,而Arduino只是个玩灯、玩小车的单片机开发板。但恰恰是这种“跨界”的玩法,让音乐创作的门槛降到了前所未有的低点,也带来了意想不到的乐趣。我自己最早接触这个,是因为想给一个手工项目加段简单的开机音效,又不想用现成的MP3模块那么“重”,结果一入坑就发现了一片新天地。
用Arduino做音乐,核心在于“简单”和“直接”。它绕开了复杂的数字音频工作站(DAW)界面和乐理知识,让你直接通过代码去“指挥”一个蜂鸣器或无源喇叭发声。每一个音符的频率、时长、节奏都由你定义,这本质上是在用编程思维解构音乐。对于初学者,这比学习一个复杂的合成器插件要直观得多;对于开发者,这为硬件项目添加动态音频反馈提供了极其轻量级的方案。从简单的《生日快乐》歌,到模拟8-bit游戏音效,再到配合传感器创作的交互式音乐装置,Arduino都能胜任。
更重要的是,它成本极低。一块最基础的Arduino Uno板子,加上一个几块钱的压电蜂鸣器或无源喇叭,就能组成一个完整的音乐发声系统。网络上有海量的现成代码和库,你甚至不需要懂乐理,复制粘贴就能让板子唱起歌来。这完美契合了“超简单”这个标题所承诺的——没有复杂的接线,没有深奥的理论,只有立刻能听到结果的快乐。
2. 核心发声原理:数字引脚如何“唱出”音符?
要让Arduino发出音乐,首先得明白它怎么让一个喇叭响起来。我们常用的发声元件有两种:有源蜂鸣器和无源蜂鸣器(或无源喇叭)。这里有个关键区别,直接决定了你能不能做音乐。
有源蜂鸣器内部自带振荡电路,通电就响,只能发出固定频率的声音,通常用于报警提示音。它就像个只会喊一个调门的哨子,不适合做音乐。而无源蜂鸣器或无源喇叭,内部没有振荡源,它的发声完全依赖于你给它的电信号。你给它一个特定频率的方波,它就振动发出那个频率的声音。这就像你的声带,气流(电流)通过时,控制声带(振膜)以不同频率振动,就能发出不同音高(音符)的声音。
Arduino让无源蜂鸣器唱歌的核心指令,是tone()函数。这个函数的原理是脉冲宽度调制(PWM)的一种应用。举个例子,中央C(Do)的频率是262Hz。tone(pin, 262)这个命令,会让Arduino在指定的数字引脚(pin)上,以每秒262次的频率,循环输出高电平和低电平,形成一个262Hz的方波信号。这个快速变化的电信号驱动蜂鸣器的振膜以相同频率振动,我们就听到了262Hz的音高,也就是中央C。
注意:
tone()函数会干扰使用PWM的引脚(3, 5, 6, 9, 10, 11),在Arduino Uno上,它使用定时器中断。在同一时间,只能在一个引脚上播放一个音调。如果需要和弦,就需要更复杂的方案,比如使用多个定时器或专门的音频库。
那么,音符和频率怎么对应呢?这有一套国际标准。以最常用的A4(La,中央A)为基准,频率是440Hz。其他音符的频率可以通过公式计算(12平均律),但作为初学者,我们直接查表就行。下面是一个常用中音区音符的频率对应表,你可以把它存下来,写歌时直接调用:
| 音符名 (中音) | 频率 (Hz) | 简谱对应 |
|---|---|---|
| C (Do) | 262 | 1 |
| D (Re) | 293 | 2 |
| E (Mi) | 329 | 3 |
| F (Fa) | 349 | 4 |
| G (Sol) | 392 | 5 |
| A (La) | 440 | 6 |
| B (Si) | 494 | 7 |
| C (高音Do) | 523 | 1(上面加点) |
有了频率,还需要控制时长。这就是delay()函数出场的时候了。tone()负责“唱什么音高”,delay()负责“这个音唱多久”。在播放一个音符后,用delay(n)维持n毫秒,然后调用noTone()停止发声,或者紧接着播放下一个音符,这样就形成了旋律。
3. 硬件准备与连接:三样东西就能开唱
动手之前,我们把需要的家伙事儿捋一捋。目标是“超简单”,所以配件力求最少。
1. 主控板:Arduino Uno这是最经典、资源最丰富的型号,兼容性无敌。当然,你用Nano、Leonardo等其他ATmega328P核心的板子也完全没问题,代码通用。不建议初学者一开始就用ESP32,虽然性能更强,但引脚定义和库的兼容性可能会带来不必要的麻烦。
2. 发声元件:无源蜂鸣器或无源喇叭这是关键!务必确认你买的是“无源”的。通常,无源蜂鸣器底部是密封的,没有电路板;或者直接买一个小型(0.5W-1W)的8欧姆无源喇叭,效果更好。价格都在5元以内。
3. 连接线:杜邦线若干公对公的杜邦线,用来连接。
接线步骤(简单到只需三步):
- 将无源蜂鸣器的正极(通常标有“+”或红色线)连接到Arduino的任意一个数字引脚,比如我们选用D8。
- 将蜂鸣器的负极(通常标有“-”或黑色线)连接到Arduino的GND(接地)引脚。
- 通过USB线将Arduino连接到电脑。
整个连接示意图就是:Arduino D8引脚 -> 蜂鸣器正极 -> 蜂鸣器负极 -> Arduino GND引脚。不需要电阻,因为tone()函数输出的电流很小,直接驱动蜂鸣器是安全的。但如果使用功率稍大的喇叭,为了保险起见,可以在正极串联一个100欧姆的电阻。
硬件搭建完毕,是不是简单得有点意外?真正的魔法都在接下来的代码里。
4. 第一段旋律:用代码“写”一首《小星星》
理论说再多,不如听个响。我们现在就来让Arduino演奏最经典的《小星星》第一句:“一闪一闪亮晶晶,满天都是小星星”。对应的简谱是:1 1 5 5 6 6 5 – 。
在编程之前,我们需要做点准备工作:把音符和频率、节奏对应起来。为了让代码更清晰,我们通常会用两个数组来分别存储音符和节拍。
// 定义《小星星》第一句的音符序列(对应简谱 1 1 5 5 6 6 5) int melody[] = { NOTE_C4, NOTE_C4, NOTE_G4, NOTE_G4, NOTE_A4, NOTE_A4, NOTE_G4 }; // 定义每个音符对应的节拍:4代表四分音符,2代表二分音符等。 // 这里我们假设四分音符的时长为300毫秒 int noteDurations[] = { 4, 4, 4, 4, 4, 4, 2 };你可能会问,NOTE_C4这些常量是哪来的?这里就引出一个超级好用的“外挂”:Arduino的pitches.h头文件。这个文件里定义了从低音到高音所有常用音符的频率常量,我们不需要自己记忆频率数字,直接使用NOTE_C4(中央C)、NOTE_G4(Sol)这样的名字即可,大大提升了代码的可读性。你需要自己创建这个文件。
如何创建并使用pitches.h文件:
- 在Arduino IDE中,点击“文件” -> “新建”,会创建一个新的空白标签页。
- 将下面提供的完整
pitches.h文件内容复制粘贴进去。 - 点击“文件” -> “保存”,文件名必须严格输入
pitches.h,然后保存到和你当前项目(.ino文件)同一个文件夹下。 - 回到主程序标签页,在代码开头通过
#include "pitches.h"引入它。
以下是pitches.h文件的完整内容,你可以直接使用:
/************************************************* * Public Constants *************************************************/ #define NOTE_B0 31 #define NOTE_C1 33 #define NOTE_CS1 35 #define NOTE_D1 37 #define NOTE_DS1 39 #define NOTE_E1 41 #define NOTE_F1 44 #define NOTE_FS1 46 #define NOTE_G1 49 #define NOTE_GS1 52 #define NOTE_A1 55 #define NOTE_AS1 58 #define NOTE_B1 62 #define NOTE_C2 65 #define NOTE_CS2 69 #define NOTE_D2 73 #define NOTE_DS2 78 #define NOTE_E2 82 #define NOTE_F2 87 #define NOTE_FS2 93 #define NOTE_G2 98 #define NOTE_GS2 104 #define NOTE_A2 110 #define NOTE_AS2 117 #define NOTE_B2 123 #define NOTE_C3 131 #define NOTE_CS3 139 #define NOTE_D3 147 #define NOTE_DS3 156 #define NOTE_E3 165 #define NOTE_F3 175 #define NOTE_FS3 185 #define NOTE_G3 196 #define NOTE_GS3 208 #define NOTE_A3 220 #define NOTE_AS3 233 #define NOTE_B3 247 #define NOTE_C4 262 #define NOTE_CS4 277 #define NOTE_D4 294 #define NOTE_DS4 311 #define NOTE_E4 330 #define NOTE_F4 349 #define NOTE_FS4 370 #define NOTE_G4 392 #define NOTE_GS4 415 #define NOTE_A4 440 #define NOTE_AS4 466 #define NOTE_B4 494 #define NOTE_C5 523 #define NOTE_CS5 554 #define NOTE_D5 587 #define NOTE_DS5 622 #define NOTE_E5 659 #define NOTE_F5 698 #define NOTE_FS5 740 #define NOTE_G5 784 #define NOTE_GS5 831 #define NOTE_A5 880 #define NOTE_AS5 932 #define NOTE_B5 988 #define NOTE_C6 1047 #define NOTE_CS6 1109 #define NOTE_D6 1175 #define NOTE_DS6 1245 #define NOTE_E6 1319 #define NOTE_F6 1397 #define NOTE_FS6 1480 #define NOTE_G6 1568 #define NOTE_GS6 1661 #define NOTE_A6 1760 #define NOTE_AS6 1865 #define NOTE_B6 1976 #define NOTE_C7 2093 #define NOTE_CS7 2217 #define NOTE_D7 2349 #define NOTE_DS7 2489 #define NOTE_E7 2637 #define NOTE_F7 2794 #define NOTE_FS7 2960 #define NOTE_G7 3136 #define NOTE_GS7 3322 #define NOTE_A7 3520 #define NOTE_AS7 3729 #define NOTE_B7 3951 #define NOTE_C8 4186 #define NOTE_CS8 4435 #define NOTE_D8 4699 #define NOTE_DS8 4978准备好头文件后,完整的《小星星》演奏代码如下:
#include "pitches.h" // 引入我们定义好的音符频率头文件 // 定义连接蜂鸣器的引脚 const int buzzerPin = 8; // 《小星星》第一句的音符序列 int melody[] = { NOTE_C4, NOTE_C4, NOTE_G4, NOTE_G4, NOTE_A4, NOTE_A4, NOTE_G4 }; // 每个音符的节拍:4为四分音符,2为二分音符 int noteDurations[] = { 4, 4, 4, 4, 4, 4, 2 }; void setup() { // 初始化串口,便于调试(可选) Serial.begin(9600); Serial.println("准备播放《小星星》..."); } void loop() { // 计算音符的总数 int noteCount = sizeof(melody) / sizeof(melody[0]); // 循环播放每一个音符 for (int thisNote = 0; thisNote < noteCount; thisNote++) { // 计算当前音符的持续时间(毫秒) // 假设以四分音符为基准,时长为300ms,那么二分音符就是600ms (300*2),八分音符就是150ms (300/2) int noteDuration = 1000 / noteDurations[thisNote]; // 这里用1000ms/节拍数,得到基准时长 // 为了让节奏更舒服,我们可以将基准时长乘以一个系数,比如300 noteDuration = 300 * (1000 / noteDurations[thisNote]) / 1000; // 简化后等效于 300 / noteDurations[thisNote] // 更清晰的写法:直接定义四分音符时长 int quarterNote = 300; // 四分音符=300ms noteDuration = quarterNote * 4 / noteDurations[thisNote]; // 公式:时长 = (基准时长 * 4) / 节拍数 // 播放当前音符 tone(buzzerPin, melody[thisNote], noteDuration); // 为了区分连续的音符,在每个音符播放后增加一个短暂的停顿(通常为持续时间的20%-30%) int pauseBetweenNotes = noteDuration * 1.3; delay(pauseBetweenNotes); // 停止当前音符的播放 noTone(buzzerPin); } // 整首曲子播放完后,等待一段时间再重新开始(或者只播放一次) delay(2000); // 等待2秒 // 如果想让曲子只播放一次,可以把loop()里的内容移到setup()里,或者在这里加一个 while(1); 死循环。 }将代码上传到Arduino,你应该立刻就能听到清脆的《小星星》旋律了。这段代码里有两个关键点值得细说:节拍的计算和音符间的停顿。
节拍计算逻辑:我们定义quarterNote = 300,意思是四分音符的时长为300毫秒。那么,一个二分音符(节拍值为2)的时长就应该是300 * 4 / 2 = 600ms。一个八分音符(节拍值为8)的时长就是300 * 4 / 8 = 150ms。这个公式noteDuration = quarterNote * 4 / noteDurations[thisNote]是通用的。
音符间停顿:如果每个音符播完立刻播下一个,旋律会显得粘连不清。因此,我们在delay(pauseBetweenNotes)时,停顿时长略大于音符本身时长(这里是1.3倍),这就在音符之间创造了一个微小的间隙,让旋律更有颗粒感,更像真实的演奏。
5. 进阶玩法:从乐谱到代码的转换秘籍
会了《小星星》,你肯定想弹奏自己喜欢的曲子。这就需要把简谱或五线谱“翻译”成Arduino能懂的数组。这个过程像解谜,也像编程,一旦掌握就一通百通。
第一步:确定调性和音符映射。大部分简单的流行歌曲或儿歌,都可以用C大调来编写,这样音符直接对应pitches.h中的NOTE_C4,NOTE_D4等,最简单。比如C大调的中音“Do”就是NOTE_C4,“Re”是NOTE_D4,以此类推。高八度就在数字上加1(如NOTE_C5),低八度就减1(如NOTE_C3)。
第二步:将简谱数字化。以《欢乐颂》片段(简谱:3 3 4 5 | 5 4 3 2 | 1 1 2 3 | 3 . 2 2 –)为例,我们先忽略附点和连音线。
- 音符:3(Mi), 4(Fa), 5(Sol), 2(Re), 1(Do)。在C大调中,对应
NOTE_E4,NOTE_F4,NOTE_G4,NOTE_D4,NOTE_C4。 - 节拍:我们假设每小节四拍。这段旋律的节拍大致是:四分音符、四分音符、四分音符、四分音符 | 四分音符、四分音符、四分音符、二分音符 | 四分音符、四分音符、四分音符、四分音符 | 附点四分音符、八分音符、二分音符。为了简化,我们可以先全部按四分音符处理(节拍值=4),附点音符可以近似为“四分音符+八分音符”(节拍值4+2=6,但需要拆成两个音符)。作为进阶,我们可以更精确。
第三步:处理特殊节奏(附点、连音、休止符)。这是让音乐听起来“对味”的关键。
- 附点音符:例如“3.”(附点四分音符),其时长是原音符的1.5倍。在代码里,我们可以将其拆解为:先播放一个标准时长的音符,再播放一个时长为原时长一半的音符,但这两个音符的音高相同,且中间没有停顿。或者更简单,直接设置这个音符的
noteDuration = quarterNote * 4 / 4 * 1.5(即节拍值按4/1.5 ≈ 2.666来处理,但数组里不好存小数)。更实用的方法是:在节拍数组里,用不同的整数值来代表不同时长,比如4代表四分音符,6代表附点四分音符,然后在计算时长时做判断。 - 休止符:音乐中的停顿。实现方法很简单,不调用
tone(),直接调用delay(restDuration)即可。restDuration的计算方式和音符时长一样。 - 连音线:连接两个相同音高的音符,使其听起来像一个更长的音符。在代码中,直接让第一个音符的时长等于两个音符时长之和,然后跳过第二个音符的播放即可。
下面是一个更完善的《欢乐颂》片段示例,包含了休止符和更精确的节奏处理思路:
#include "pitches.h" const int buzzerPin = 8; // 欢乐颂片段:3 3 4 5 | 5 4 3 2 | 1 1 2 3 | 3 . 2 2 - // 节拍简化:4,4,4,4, |4,4,4,2, |4,4,4,4, |6,2,2 (6代表附点四分音符,占1.5拍) int melody[] = { NOTE_E4, NOTE_E4, NOTE_F4, NOTE_G4, NOTE_G4, NOTE_F4, NOTE_E4, NOTE_D4, NOTE_C4, NOTE_C4, NOTE_D4, NOTE_E4, NOTE_E4, NOTE_D4, NOTE_D4, NOTE_REST // NOTE_REST 代表休止符,我们可以用一个负数或0频率表示 }; // 节拍数组:4=四分音符,2=二分音符,6=附点四分音符(1.5拍) int noteDurations[] = { 4, 4, 4, 4, 4, 4, 4, 2, 4, 4, 4, 4, 6, 8, 2, 4 // 3.(6) 2(8) 2-(2) 最后一拍休止(4) }; void setup() { pinMode(buzzerPin, OUTPUT); } void loop() { int quarterNote = 300; // 基准速度,四分音符=300ms for (int i = 0; i < sizeof(melody)/sizeof(melody[0]); i++) { int noteDuration; // 处理附点音符(节拍值6) if (noteDurations[i] == 6) { noteDuration = quarterNote * 4 / 4 * 1.5; // 附点四分音符 = 1.5 * 四分音符 } else { noteDuration = quarterNote * 4 / noteDurations[i]; } // 判断是否是休止符(这里用频率0表示) if (melody[i] != 0) { tone(buzzerPin, melody[i], noteDuration); } // 无论是播放音符还是休止,都需要等待这个节拍的时长 // tone()函数本身是非阻塞的,但指定了持续时间参数后,它会阻塞对应时长。为了统一控制,我们更常用不带时长参数的tone(),然后自己delay。 // 更推荐的做法: // if (melody[i] != NOTE_REST) { // tone(buzzerPin, melody[i]); // } // delay(noteDuration); // noTone(buzzerPin); // delay(noteDuration * 0.1); // 音符间短间隔 } delay(2000); // 循环间隔 }通过这样的转换练习,你会发现任何乐谱本质上都是一系列“频率-时长”对的序列。你可以用Excel或文本编辑器来辅助整理这些数组,效率会高很多。
6. 突破单音:实现多声部与和弦的探索
只用tone()函数,我们只能演奏单旋律。真正的音乐往往有和声。如何在资源有限的Arduino上实现哪怕简单的多声部呢?这里有几种思路,难度和效果逐级递增。
方案一:快速交替模拟(伪和弦)Arduino不能同时在一个引脚上输出两个频率。但人耳有听觉暂留特性,如果我们在两个音高之间快速切换,切换速度快到一定程度(比如每秒上百次),听起来就会像两个音同时在响。这可以通过在loop()中不使用delay(),而是用millis()进行非阻塞计时来实现,在两个音高间快速轮询。这种方法实现起来复杂,且效果带有明显的“电子颤音”感,不算理想。
方案二:使用多个蜂鸣器和引脚这是实现真正同时发声的最直接方法。将两个或多个无源蜂鸣器连接到不同的数字引脚(如D8和D9)。然后在代码中,对每个引脚分别使用tone()函数。因为tone()使用的是定时器中断,在Arduino Uno上,不同的定时器可以驱动不同的引脚同时发声。例如,引脚3和11使用定时器2,引脚9和10使用定时器1,引脚5和6使用定时器0。合理分配引脚,可以实现二声部。
#include "pitches.h" const int buzzerPin1 = 9; // 使用定时器1 const int buzzerPin2 = 10; // 使用定时器1的另一个通道,可以同时发声? // 注意:实际上,tone()函数在同一时间只能在一个引脚上活动。 // 要实现真正的多引脚同时发声,需要更底层的定时器操作,或者使用专门的库。 // 一个取巧的办法是快速轮流播放,模拟同时。重要提示:标准的
tone()库在某一时刻只能在一个引脚上产生音调。尽管有多个定时器,但库本身的设计限制了并发。要实现硬件级的双音同时输出,需要直接操作定时器寄存器,这对初学者门槛较高。
方案三:使用PWM模拟合成音(高级)通过直接控制定时器,产生占空比可变的PWM波,可以合成出更复杂的波形(如方波、锯齿波),理论上可以叠加多个频率分量。这是制作8-bit音乐的核心技术,但需要深厚的单片机知识和信号处理基础。
方案四:使用专用音频芯片或模块当项目对音质和多声部有更高要求时,最务实的选择是外挂一个专门处理音频的芯片,比如经典的VS1053、VS1003解码模块,或者更简单的WTV020、DFPlayer等语音模块。这些模块可以通过SPI、SD卡或串口接收指令,播放高质量的MP3/WAV文件或者MIDI音乐,完全解放Arduino的主控资源,效果也专业得多。这属于“升级装备”的路线,超出了“超简单”的范畴,但却是项目进阶的必经之路。
对于入门者,我建议先深耕单旋律的演奏,把它玩透。尝试用不同的节奏、速度、音高来编曲,配合LED制造视觉同步效果,乐趣已经足够多了。当你觉得单音限制了你时,就意味着你该向上述的进阶方案迈进了。
7. 项目实战:制作一个交互式音乐盒
掌握了单旋律编程后,我们可以做一个有趣的小项目:一个通过旋钮(电位器)控制音高,通过按钮切换曲目的交互式音乐盒。这个项目综合了输入(模拟/数字)和输出(声音),能让你更全面地理解Arduino的交互能力。
所需材料清单:
- Arduino Uno x1
- 无源蜂鸣器 x1
- 10kΩ电位器 x1
- 按键开关 x2
- 220Ω电阻 x2(用于按键下拉)
- 面包板和杜邦线若干
电路连接详解:
- 蜂鸣器:正极接D8,负极接GND。
- 电位器:两侧引脚分别接5V和GND,中间引脚(滑动端)接模拟输入引脚A0。旋转电位器,A0读取的电压值会在0-1023之间变化。
- 按键1(切换曲目):一端接5V,另一端接数字引脚D2,同时在D2和GND之间连接一个220Ω的下拉电阻,确保按键未按下时引脚为稳定的低电平。
- 按键2(播放/停止):接法同按键1,连接至数字引脚D3。
代码逻辑与实现:这个项目的代码结构比单纯播放旋律要复杂一些,我们需要处理状态切换和非阻塞逻辑,避免使用delay()导致界面卡死。
#include "pitches.h" // 引脚定义 const int buzzerPin = 8; const int potPin = A0; const int songButtonPin = 2; const int playButtonPin = 3; // 状态变量 int currentSong = 0; // 当前曲目索引 bool isPlaying = false; unsigned long lastNoteTime = 0; int noteIndex = 0; // 定义两首歌曲的旋律和节拍 // 歌曲1: 《小星星》片段 int melody1[] = {NOTE_C4, NOTE_C4, NOTE_G4, NOTE_G4, NOTE_A4, NOTE_A4, NOTE_G4}; int rhythm1[] = {4, 4, 4, 4, 4, 4, 2}; // 节拍 // 歌曲2: 《欢乐颂》片段 int melody2[] = {NOTE_E4, NOTE_E4, NOTE_F4, NOTE_G4, NOTE_G4, NOTE_F4, NOTE_E4, NOTE_D4}; int rhythm2[] = {4, 4, 4, 4, 4, 4, 4, 2}; // 当前播放歌曲的指针 int* currentMelody = melody1; int* currentRhythm = rhythm1; int songLength = sizeof(melody1) / sizeof(melody1[0]); // 计算音符时长函数 int getNoteDuration(int rhythmValue, int tempo) { // tempo为四分音符的毫秒数,例如300 return tempo * 4 / rhythmValue; } void setup() { pinMode(buzzerPin, OUTPUT); pinMode(songButtonPin, INPUT_PULLUP); // 启用内部上拉电阻,这样按键另一端接地即可,省去外部下拉电阻 pinMode(playButtonPin, INPUT_PULLUP); Serial.begin(9600); } void loop() { // 1. 检查曲目切换按钮(防抖处理) if (digitalRead(songButtonPin) == LOW) { delay(50); // 简单防抖延时 if (digitalRead(songButtonPin) == LOW) { currentSong = (currentSong + 1) % 2; // 在0和1之间切换 switch(currentSong) { case 0: currentMelody = melody1; currentRhythm = rhythm1; songLength = sizeof(melody1)/sizeof(melody1[0]); Serial.println("切换到歌曲:小星星"); break; case 1: currentMelody = melody2; currentRhythm = rhythm2; songLength = sizeof(melody2)/sizeof(melody2[0]); Serial.println("切换到歌曲:欢乐颂"); break; } // 切换曲目时停止当前播放并重置索引 isPlaying = false; noteIndex = 0; noTone(buzzerPin); while(digitalRead(songButtonPin) == LOW); // 等待按键释放 } } // 2. 检查播放/停止按钮 if (digitalRead(playButtonPin) == LOW) { delay(50); if (digitalRead(playButtonPin) == LOW) { isPlaying = !isPlaying; // 切换播放状态 if (!isPlaying) { noTone(buzzerPin); // 停止播放 noteIndex = 0; // 可选:停止后重置到曲目开头 } Serial.println(isPlaying ? "开始播放" : "停止播放"); while(digitalRead(playButtonPin) == LOW); } } // 3. 如果正在播放,则执行播放逻辑(非阻塞) if (isPlaying) { // 通过电位器读取实时速度,映射到100-500ms(速度可调范围) int potValue = analogRead(potPin); int tempo = map(potValue, 0, 1023, 100, 500); // 四分音符时长,值越小速度越快 // 判断是否到了播放下一个音符的时间 if (millis() - lastNoteTime >= getNoteDuration(currentRhythm[noteIndex], tempo)) { // 播放当前音符 tone(buzzerPin, currentMelody[noteIndex]); // 准备下一个音符 noteIndex++; if (noteIndex >= songLength) { noteIndex = 0; // 播放完一遍,循环 // 如果想播放一次就停止,可以在这里设置 isPlaying = false; } lastNoteTime = millis(); // 更新上次播放时间 } } else { // 如果不播放,确保没有声音 noTone(buzzerPin); } // 4. 在音符持续期内,不需要做任何事,loop()快速循环以检测按钮 // 注意:这里的tone()没有指定持续时间,所以声音会一直响,直到我们播放下一个音符或停止。 // 实际效果是每个音符的时长由 rhythm 和 tempo 决定,通过 noTone 和下一个 tone 之间的间隔来体现停顿。 // 更精确的控制需要更复杂的状态机,但当前方案对于简单演示已足够。 }项目要点与调试心得:
- 非阻塞设计:整个
loop()循环必须快速执行,不能有长延时。我们使用millis()来计时,判断何时播放下一个音符,这样在等待期间,程序依然能检测按钮是否被按下。 - 按键防抖:机械按键在按下和弹起时会产生瞬间的抖动,导致一次按下被误判为多次。代码中简单的
delay(50)后再判断,是一种基础的软件防抖方法。对于要求高的场合,可以使用更稳定的状态机防抖逻辑。 - 状态管理:
isPlaying、currentSong、noteIndex这些变量构成了项目的状态机。清晰的状态划分是编写复杂交互逻辑的基础。 - 电位器映射:
map(potValue, 0, 1023, 100, 500)函数将模拟输入值线性映射到我们想要的 tempo(速度)范围。你可以调整后两个参数来改变速度的可调范围。 - 音长控制:本例中,一个音符的播放时长,等于从它开始播放 (
tone()) 到下一个音符开始播放之间的时间间隔。这种方式简单,但音符之间没有静音间隙。如果你需要更精确的节奏(包括休止符),就需要引入更精细的定时,比如记录每个音符的“开始时间”和“应持续时长”,到时后调用noTone(),并插入一段休止时间。
上传代码后,旋转电位器,你会发现音乐的速度随之改变;按下一个按钮切换曲目,另一个按钮控制播放和停止。一个简单的交互式音乐盒就诞生了。你可以在此基础上增加更多功能,比如用光敏电阻控制音量(通过PWM的占空比模拟),或者用超声波传感器根据距离改变音高,创造出属于你自己的“空气乐器”。