基于Wokwi的智能炫彩音乐盒


基于Wokwi的智能炫彩音乐盒

(Arduino项目开发)
---
目录

摘要

​ 近年来,Arduino的开发在硬件和软件方面都取得了重大进展,这使得人们能够比过去更加容易根据自己的想法创建交互式项目和模型,促进了技术的创新。Wokwi、tinkercad等在线平台也为Arduino项目的学习和开发降低了成本、提供了方便,对初学者走入该开发领域起到了重要的鼓励作用。本项目基于Wokwi平台成果开发出了一个智能炫彩音乐盒,该音乐盒能够灵活地在自动播放和手动弹奏这两种模式之间切换,自动播放时,音乐盒能够流畅且清晰地播放程序预设好的乐曲,且支持用户手动调整音乐的节奏,手动弹奏时,用户能够通过按下不同的琴键令音乐盒发出不同音调的声音,给予用户充足的创作自由。另外,任何一种模式在发出不同音调的声音时,音乐盒对根据声音的音调、持续时长、节奏等特点,产生不同效果的绚丽灯光,为用户提供视觉、听觉以及触觉的多重享受。本项目利用NeoPixel Compatible LED Ring、Pushbutton、Buzzer等简单的Arduino组件完成了智能炫彩音乐盒的组装,实现了声音灯光相辅相成的复杂功能,并结合了视频的形式对设计结果进行介绍,让开发者从中体验到了电路开发的无限乐趣。

1. 相关介绍

在这里插入图片描述

图-1 Arduino-Wokwi介绍

1.1 Arduino介绍

​ Arduino是一种强大的数字系统设计工具,在电子和工程领域越来越受欢迎。虽然Arduino被业余爱好者和专业人士广泛使用,但许多人可能没有听说过它,或者可能不完全理解它是什么。

​ Arduino是一个开源电子平台,专为构建和编程电子设备而设计。它由可编程微控制器或计算机芯片组成,可用于控制LED、电机、传感器等电子元件。Arduino有各种尺寸和结构的开发板,可以使用各种编程语言进行编程,包括C语言和C++。人们可以利用丰富的Arduino电子元件构建各种电子项目,例如机器人、无人机、游戏、乐器等等,还可以将Arduino连接到其他系统和接口,例如蓝牙、WIFI和物联网(IoT)。许多业余爱好者、艺术家和设计师经常使用Arduino来创建互动艺术装置、可穿戴智能设备以及其他创新项目。

​ Arduino的运作基于微控制器板,相当于一个小型计算机通过在单个芯片上运行事先编写好的程序来控制开发板及其组件。Arduino开发板有不同的尺寸和结构,但大多数都有相同的基本组件,包括微控制器、输入/输出引脚、电源调节器、USB端口和复位按钮。对应的Arduino IDE允许人们编译和上传程序到开发板中,从而让它可以与组件交互并执行各种任务。

​ Arduino在业余爱好者、初学者和专业人士中受欢迎的原因有许多。首先,它很容易学习和使用,即使初学者并没有电子知识的基础和程序编写的经验,也能够很快速地入门。其次,它具备通用性、灵活性以及可扩展性,人们可以根据自己的意愿自由地向项目添加新的组件和功能。再次,Arduino开发的成本低廉,电子元件的获取也容易,人们可以从世界各地的许多供应商购买获得Arduino开发板和各类组件,也可以借助Wokwi和tinkercad等平台免费地进行自己的项目开发。最后,Arduino有一个庞大且活跃的用户和开源社区,开发者们慷慨地共享项目支持、教程和项目示例。

​ 而在进行Arduino开发时,人们也应该先了解一些它的局限性。首先,与一些更加先进的微控制器或者计算机相比,它的处理能力和内存是有限的,这意味着人们可能无法在Arduino板上实现复杂的算法和数据处理任务。其次,每个开发板都只具备数量有限的引脚,会对人们可以使用的组件的数量和类型带来限制。最后,Arduino可能不适合工业、安全等领域的应用,因为它是以教育和实验为目的设计的。

1.2 Wokwi平台介绍

​ Wokwi是一个模拟和测试Arduino项目的在线平台。

​ 用户可以在Wokwi网站上免费注册,并在电路模拟界面以拖拽组件的方式搭建自己的项目。用户也可以上传已经编写好的Arduino程序并在平台上模拟所上传的项目,以查看项目的运作,且不需要任何物理组件。同时,用户可以使用虚拟组件(如LED、按钮和传感器)进行项目的测试,并实时查看输出信息和效果。Wokwi还有一个内置的代码编辑器,支持多种编程语言的语法高亮显示和代码补全,包括C++、Python和JavaScript。最后,用户可以与其他用户分享自己的项目,并在平台上与他们进行开发协作。

​ 对于初学者来说,Wokwi是学习Arduino和电子知识的一个优秀工具。由于Wokwi能够模拟物理组件的各类行为,初学者可以从中学习到电路是如何工作的、应该如何阅读原理图、以及如何调试他们的项目。甚至教师可以在该平台上布置对应的作业和练习,并使用平台内置的评估系统自动为学生作业和练习评分。另外,Wokwi还支持大部分主流的Arduino IDE,这意味着初学者可以在使用Wokwi的同时熟悉专业的开发环境。

​ Wokwi致力于推广开源的软硬件,它拥有一个活跃的Arduino开发者社区,也鼓励其社区成员共享自己的项目和知识。人们通过可以加入Wokwi Discord服务器来与其他成员进行联系,提出自己遇到的问题,并获得有关自己项目的反馈。人们也可以参加Wokwi平台发布的挑战和竞赛,融入Arduino社区的充满创造力的氛围。

2. 项目需求分析

在这里插入图片描述

图-2 项目功能框架

2.1 项目目标

​ 本项目属于Arduino领域,旨在发明一个基于Wokwi的智能炫彩音乐盒。该智能音乐盒能够实现现实生活中音乐盒所具备的基本功能,即播放预设好的音乐的功能。除此之外,该音乐盒还配备多个琴键,每个琴键能够发出不同声调的声音,以满足用户自我创作音乐的需求。另外,该智能音乐盒还能根据所播放音乐的不同音调展现绚丽的灯光效果,以求为用户提供视觉、听觉与触觉的多重炫彩盛宴。在不播放音乐的时候,也能充当简单的灯具以提供照明功能。

2.2 功能需求

2.2.1 灯光效果

​ 绚丽的灯光效果将模拟火焰扩散的形式来展示。

​ 当音乐盒不发出声音时,需要显示较为柔和且简单的灯光,以渲染安静舒适的氛围。

​ 当用户弹奏音乐盒上的琴键或者音乐盒自动播放音乐时,音乐盒能根据音调的不同展现出不同的灯效,具体来说,当音调高昂时,则展现出规模更大、变化更加丰富的灯效,从而表达高昂激烈的音乐情绪,当音调低沉时,则展现出规模相对较小、变化相对缓慢的灯效,以表达舒缓细腻的情绪。
​ 同时,当音调发生变换时,灯光也需要随着音调及时改变,并且具备流畅的变换过程而不产生突兀的变换效果。
​ 另外,灯光的变换也需要能够体现短暂发声和持续发声的区别,具体来说,随着同一个声音发声的时间延长,灯光将以火势蔓延的效果扩大自身的规模。

2.2.2 音乐自动播放

​ 音乐盒需要具备一个控制自动播放/手动弹奏的开关,当打开自动播放音乐的功能时,音乐盒能够播放程序预设好的乐曲,并根据所播放乐曲的音调以及音乐特点(如每个发音的时长)展示对应的灯光效果。
​ 预设的乐曲可以通过程序灵活更换,乐曲的更换只需要按顺序输入目标乐曲的简谱上的数字,以满足不同用户的音乐品味需求。
​ 另外,音乐的节奏也可以灵活设置。具体来说,智能音乐盒配备一个节奏控制器,通过对节奏控制器进行不同方向的滑动可以达到加快/放慢音乐节奏的效果,以使音乐可以根据用户的需求表现出欢快/舒缓的效果。
​ 音乐自动播放模式将音乐以及音乐的节奏以灯光的特效展现在用户眼中。

2.2.3 弹奏模式

​ 当开关设置为手动弹奏模式时,音乐盒检测到用户按下了哪个琴键,并根据用户所按下的琴键发出对应的不同音调的声音(每个键发出的声音清晰,容易与其他键区分),且能够展现出不同音调所对应的灯光特效。

​ 从左到右的琴键对应音调由低到高,且低沉的音调对应规模小、变化小的灯光特效(火焰效果的半径更小、颜色类别相对较少),高昂的音调对应规模大、变化大的灯光特效(火焰效果的半径更大、颜色类别相对更多、变化更加丰富)。同时,声音发出的时长由用户按下琴键的时长来决定,灯光的火焰效果也由声音持续的时长决定,随着声音持续时长增加,火焰的半径也随之增加,且半径增加的过程模拟的是火焰蔓延的过程,从而提升用户的视觉观感。

​ 弹奏模式将用户的听觉以及触觉呈现为绚丽的视觉表达。

2.3 技术需求

表-1 硬件需求
功能硬件
Arduino项目基本功能搭建Arduino Mega开发板、杜邦线若干条
灯光效果NeoPixel Compatible LED Ring(WS2812)若干个
音乐自动播放Slide switch、Slide Potentiometer、Buzzer
弹奏模式Pushbutton若干个

​ 项目功能实现所需的技术硬件如表-1所示,考虑到实际硬件的成本以及本人制作所擅长的方式,最终将借助Wokwi平台模拟实现项目的目标功能。

2.4 环境需求

​ 音乐盒设计在室内,能够在温度范围为10-30摄氏度的环境中稳定运行。同时,音乐盒应能够防止静电干扰和静电放电。

2.5 用户需求

2.5.1 灯光效果
  • 用户希望音乐盒能够呈现出绚丽的、具有视觉冲击力的灯光效果。
  • 用户希望在八音盒不发声时,灯光相对柔和简单,营造安静舒适的氛围。
  • 用户希望灯光效果能够平滑流畅地进行变化,并根据正在播放的音乐的音调反映不同的情绪。高音时,灯光的规模较大,变化较多,以表达强烈的音乐情感;低音时,灯光的规模较小,变化较慢,以表达舒缓细腻的情感。
  • 用户希望灯光随着音乐的音调而变化,并且有一个平稳的过渡过程,而没有突兀的变化。
  • 用户希望灯光能够反映出短暂声音和持续声音的区别,随着持续声音的持续,灯光的规模会扩大,模拟火焰的蔓延过程。
2.5.2 音乐自动播放
  • 用户希望音乐盒具有控制自动播放/手动弹奏模式的开关。
  • 开启自动播放功能后,用户希望音乐盒能够播放预先编入设备的预设音乐,并根据音乐的音色以及每个发音的长短等特征显示相应的灯光效果。
  • 用户希望能够通过程序输入目标音乐的简谱,以方便地改变预设的音乐,以满足自身的音乐品味。
  • 用户希望通过使用节奏控制器随心所欲地改变音乐的节奏,即根据自身的需要加快或减慢音乐的播放速度。
  • 用户期望音乐自动播放模式能展现出绚丽的灯光效果,从视觉方式呈现音乐和音乐的节奏。
2.5.3 弹奏模式
  • 当开关设置为弹奏模式时,用户希望音乐盒能够检测到自身按下了哪些琴键,并根据按下的琴键发出不同音调的声音,并显示出对应的灯光效果。
  • 用户期望琴键从左到右对应从低到高的音调,低音调对应较小规模以及变化较缓的灯光效果,而高音调对应较大的规模和变化较大的灯光效果。
  • 用户期望声音的持续时间由按下琴键的时间决定,灯光的效果由声音的持续时间决定,灯光的规模随着声音持续时间的增加而增大,规模扩大的方式则模拟火焰的蔓延过程。
  • 用户期望弹奏模式能够以华丽的视觉效果来表现他们的听觉和触觉。
2.5.4 音质
  • 用户希望音乐盒能够在产生高质量的声音同时尽可能减少失真度以及噪音。
  • 用户希望每个键发出的声音清晰、音调准确,容易与其他键区分。
2.5.5 用户操作
  • 用户希望音乐盒具备用户友好的设计,以便用户能够清晰易懂地对其进行控制。
  • 用户期望音乐盒允许在自动播放和弹奏模式之间轻松切换,同时能够方便地自主调整音乐的节奏。

2.6 项目计划

​ 项目计划安排如图-3所示。

在这里插入图片描述

图-3 项目计划安排

3. 组件分析介绍

3.1 Arduino Mega开发板

在这里插入图片描述

图-4 Arduino Mega开发板

​ Arduino Mega是一个功能强大的开发板,提供比标准Arduino Uno开发板更多的输入/输出引脚、处理能力和内存容量,适用于机器人、自动化等领域和其他需要多个传感器和执行器的高级项目。Arduino Mega开发板的规格参数如表-2所示。

表-2 Arduino Mega开发板规格参数
类型规格参数
微控制器ATmega2560
工作电压5V
推荐输入电压7-12V
限制输入电压6-20V
数字输入/输出引脚54个(其中15个提供PWM输出)
模拟输入引脚16个
每个输入/输出引脚的直流电流20mA
3.3V引脚的直流电流50mA
时钟频率16MHz

​ Arduino Mega开发板具有更多的输入输出引脚、更好的可拓展性、更多的内存以及更多的硬件串行端口,因此它能够适用于更加广泛的项目。

​ 具体来说,首先,Mega总共有54个数字输入/输出引脚,超过标准Arduino Uno开发板引脚数量的两倍,因此该板适用于需要多个传感器、执行器和其他组件的项目。其次,Mega可以通过称为shields的附加板进行扩展,而shields允许开发者在项目中添加某些特定的功能,例如WIFI、电机、LCD显示器等等。再次,Mega具有256kb的闪存和8kb的SRAM,比标准的Arduino Uno板更多,这允许开发者在电路板上存储和运行更复杂的程序。最后,Mega有4个硬件串行端口,允许开发者同时与多台设备通信,这对于需要与GPS模块、蓝牙模块和其他串行设备通信的项目非常有用。

3.2 NeoPixel Compatible LED Ring

在这里插入图片描述

图-5 NeoPixel Compatible LED Ring

​ NeoPixel Compatible LED Ring是一个圆形板,周围有一系列可单独寻址的RGB LED,排列成一个环。它与流行的NeoPixel库兼容,方便开发者使用Arduino或其他微控制器平台更加轻松地控制和定制LED。NeoPixel Compatible LED Ring的规格参数如表-3所示。

表-3 NeoPixel Compatible LED Ring规格参数
类型规格参数
LED数量不同尺寸的LED Ring有不同数量的LED
LED类型LED与WS2812兼容(意味着它们可以单独寻址)
工作电压5V
能量功耗取决于LED的数量和亮度,一般每个全亮度LED消耗60mA
通信协议基于OneWire协议的单个数据线

​ NeoPixel Compatible LED Ring具有可单独寻址、低功耗、易控制、持久耐用、可定制化等特点。

​ 具体来说,第一,LED Ring允许开发者单独控制每个LED,同时,NeoPixel库也提供了广泛LED控制函数,如设置颜色、亮度和动画速度等,这意味着开发者可以创建更加复杂的光照模式或动画效果。第二,LED Ring的设计非常节能,在全亮度下,每个LED的最大功耗仅为60mA,这使能够它适应电池供电情况的应用。第三,LED Ring可以使用各种微控制器平台方便地进行控制,包括Arduino、树莓派等,另外,NeoPixel库也提供了一个易于使用的接口来控制LED,并在网上提供了丰富的示例和教程。第四,LED Ring由高品质材料制成,设计持久耐用,额定使用寿命可达50000小时,并且电路板耐冲击、抗振动。最后,LED Ring是高度可定制的,具有广泛的动画和模式可用,换句话说,NeoPixel库为用户自定义动画和模式的创建提供各类高效的函数,用户还可以修改硬件以满足自身的特定需求。

​ NeoPixel Compatible LED Ring被广泛应用在装饰照明、可穿戴设备、机器人以及游戏等领域。

3.3 Slide switch

在这里插入图片描述

图-6 Slide switch

​ Slide switch是一种用于控制电路中电流流动的电气开关,它由一个来回移动的小杠杆或滑块组成,用于打开或关闭电路。

​ Slide switch的关键组成部分包括触点、制动器以及外壳。触点是当开关闭合时彼此接触的金属块,允许电流的流动,而当开关打开时,触点是分开的,电流无法流过。制动器是开关的可移动部分,通过滑动或转动来打开或关闭开关,根据不同开关的设计,它可能是杠杆、滑块或摇杆。外壳用于固定触点和制动器,它通常由塑料或金属制成,可以安装在电路板或面包板上。

​ Slide switch的主要有4种,单刀单掷开关、单刀双掷开关、双刀单掷开关以及双刀双掷开关,在电子产品、机器人、自动化等领域都有着广泛的应用。

3.4 Slide Potentiometer

在这里插入图片描述

图-7 Slide Potentiometer

​ Slide Potentiometer是一种电子元件,由沿轨道移动的滑动触点组成,用于改变电路中的电阻。

​ Slide Potentiometer的关键组成部分包括电阻元件、滑动触点、接线端。电阻元件是提供可变电阻的元件,通常由碳或金属薄膜沉积在陶瓷基材或塑料薄膜上而制成。滑动触点是沿着电阻元件移动的元件,用于控制电路的电阻,通常由铜或黄铜等导电材料制成。接线端是用于连接Slide Potentiometer和电路的触点,通常由金属制成。

​ Slide Potentiometer主要有线性、对数型以及双组型3种,在音像、照明、机器人以及工业控制等领域有着广泛的应用。

3.5 Buzzer

在这里插入图片描述

图-8 Buzzer

​ Buzzer是一种将电信号转换成声波的声学换能器,是一种电声装置,用于在电子电路和设备中产生声音或警报。

​ Buzzer的关键组成部分包括声圈、隔膜以及外壳。音圈是一个线圈,悬浮在永磁体的磁场中,一个变化的电信号被发送到音圈时会导致音圈来回移动,从而产生声波。隔膜是附着在音圈上的一层薄薄的金属或塑料薄膜,它会随着音圈的运动而来回振动,从而放大和塑造声波。外壳用于保护内部组件,主要由塑料和金属等材料制成。

​ Buzzer主要有磁性Buzzer、压电Buzzer、静电Buzzer这几种,在警报系统、电子设备、机动车等领域有着广泛的应用。

3.6 Pushbutton

在这里插入图片描述

图-9 Pushbutton

​ Pushbutton是一种通过按下按钮或柱塞来激活的开关。

​ Pushbutton的关键组成部分包括一个安装在外壳上的按钮,与一组电触点相连,当按下按钮时,触点闭合,允许电流流通,而当按钮没被按下时,同侧的接口并不连通。

​ Pushbutton主要有瞬时按钮、自锁按钮、切换按钮、带灯按钮这几种,在电子产品、工业制造、交通运输以及安全系统等领域都有广泛的应用,是一种及其重要且基础的元件。

3.7 数字模拟信号控制

img
图-10 数字信号(Analog Signal)和模拟信号(Digital Signal)

​ 模拟信号和数字信号是电子通信系统中用于传输信息的两种信号。

​ 模拟信号是连续的信号,可以表示某种物理量,如声音、光照和温度。模拟信号可以在一定范围内取任意值,并且随时间平稳变化。人对着麦克风说话的声音就是一种模拟信号,麦克风将声波转换成振幅和频率变化的电信号,然后可以通过电路或无线电波传输。在Arduino中有10位模拟,模拟信号使用模拟输入表示,能够读取0到5伏的电压值。这些模拟输入可用于读取来自传感器的模拟信号,如温度传感器、光传感器和加速度传感器。例如,温度传感器可能会输出与环境温度相对应的模拟电压信号,Arduino可以使用其模拟输入读取该信号并将其转换为可由微控制器处理的数字值。

​ 数字信号是用0和1序列表示信息的离散信号,可用于表示数值、文本、图像和其他类型的数据。与模拟信号不同,数字信号只能取特定的值,并且它们以离散的步长产生变化。计算机键盘的输入是一种模拟信号,当一个键被按下时,它向计算机发送一个数字信号,表示相应的字符。Arduino中的数字信号用数字输入/输出表示。数字输入用于读取开关或按钮的状态,而数字输出用于控制LED、电机或其他执行器的状态。例如,一个按钮可以连接到Arduino的数字输入端,当按钮被按下时,数字输入端会读取一个高电平值“1”,表示按钮被按下。类似地,一个LED可以连接到一个数字输出,当输出设为高电平值“1”时,LED亮,当输出设为低电平值“0”时,LED熄灭。

​ 在Arduino中,使用模拟输入读取模拟信号,使用数字输入读取数字信号,并使用数字输出进行控制,这些信号用于与物理世界交互,并控制各种设备和传感器。

4. 技术说明

4.1 电路设计

​ 智能炫彩音乐盒的系统电路设计如图-11所示。

在这里插入图片描述

图-11 智能炫彩音乐盒电路设计

是由21个NeoPixel Compatible LED Ring组合而成的LED显示屏,用于根据音乐盒发出的不同声音显示不同的灯光效果,该LED显示屏每个环的LED灯珠数量不同,由内到外,每个环所具有的LED灯珠数量逐渐增加,最里层只有1个LED灯珠,而最外层有160个LED灯珠。最外层的LED环的GND接口接到了开发板的GND.2引脚,VCC接口接到了开发板的5V引脚,DIN数据接口接到了开发板的3号引脚。

是Buzzer,用于根据用户按下不同琴键(Pushbutton)时接收到的不同信号发出不同音调的声音。Buzzer的1号接口与开发板的GND.1引脚相连,2号接口与开发板的8号引脚相接。

是Slide Potentiometer,通过改变自身的阻值来改变它对应接口的模拟信号数值,最终配合delay()函数用于控制自动播放模式时音乐的节奏。Slide Potentiometer的VCC接口接到开发板的5V引脚,GND接口接到开发板的GND.3引脚,SIG信号接口接到开发板的A1引脚。

是Slide switch,用于自动播放/手动弹奏模式的切换。Slide switch的1号接口接到开发板的GND.3引脚,2号接口接到开发板的A0引脚,3号接口接到开发板的5V引脚。

是Pushbutton,当不同的按钮被按下时,通过按钮自身状态的变化改变电路传递到Buzzer的信号,从而在弹奏模式时,让Buzzer发出不同音调的声音以及LED显示屏显示不同效果的灯效。Pushbutton一共有8个,每个Pushbutton的1.l接口都被连接到了开发板的GND.1引脚,而从左到右8个Pushbutton的2.l接口分别被连接到了开发板的{12,11,10,9,7,6,5,4}号引脚。

则是Arduino Mega开发板,是整个系统的“大脑”,用于控制上述组件 的行为。

4.2 代码实现

4.2.1 FastLED库

​ 为了让由NeoPixel Compatible LED Ring组合而成的LED显示屏实现更加绚丽的灯光特效,FastLED库的使用是一大关键。

​ FastLED是一个开源的库,旨在为Arduino项目有关LED的编程带来更多的便利,它提供了一个易于使用的界面,允许开发者控制LED组件中单个LED的颜色和亮度以及彩虹褪色、调色板循环等特殊效果。一般的FastLED库使用步骤包括:

  • 基本设置:要使用FastLED库,开发者需要将其导入Arduino项目并定义LED组件中的LED数量。然后,开发者可以使用一系列代码命令来控制LED组件中单个LED或LED组的颜色和亮度。
  • 使用预先制作好的效果:FastLED库包含了在LED组件上生成特殊效果的预设函数,如颜色淡出、彩虹漩涡和火焰动画等等。这些函数可以很方便地进行扩展或修改,以满足开发者的项目需求。
  • 自定义效果:FastLED库还允许用户通过修改预设的函数或自己写一个全新的函数来创建自定义的效果。具体来说,开发者可以通过自己写的函数调整LED组件的时序、颜色模式、亮度以及其他相关属性。
  • 与音乐同步:FastLED可用于需要与音乐同步的LED项目。例如,开发者可以使用麦克风或声音传感器来检测音乐或声音级别,并相应地调整LED的属性。
  • 控制多个LED组件:FastLED允许开发者使用单个Arduino板一次控制多个LED组件,通过定义每个LED组件中LED的数量并使用阵列来存储灯条数据,开发者甚至可以创建规模巨大且变化复杂的LED灯光效果。
  • 高级功能:FastLED库还包含了某些能实现高级功能的函数,如伽马校正、色温校正和抖动等。这些功能有助于提高LED组件的色彩精度和整体视觉质量。

​ 通过使用FastLED,开发者可以将视觉效果惊人的Arduino项目带入生活中。

4.2.2 代码分析

​ 在智能炫彩音乐盒的系统电路搭建完成后,需要利用逻辑完整的代码来控制各个组件的行为,从而让音乐盒能够实现需求定义的功能。下面将分开逐个步骤对代码的细节进行分析。

​ 在脚本文件开始的部分,首先需要导入FastLED库,保证后续能够正常进行LED组件的设置和调用,同时,还要用宏定义定义一个TIMING变量,该变量用于开启/关闭代码调试模式,当TIMING的值为1,说明调试模式开启,反之则调试模式关闭。调试模式开启时,在系统运行时,会在控制台输出相应的调试信息以及各组件的信号等。该部分的代码和注释如下:

#include <FastLED.h> // 导入FastLED库 保证后续LED组件的正常设置和调用
#define TIMING 0 // 定义用于调试设置的`TIMING`变量

​ 接下来,需要对有关于发声部分的功能组件进行全局设置。需要设置的内容主要包括(1)用于发声的Buzzer和用于调节音乐节奏的Slide Potentiometer的引脚;(2)按下不同琴键(Pushbutton)时,Buzzer发出声音的频率;(3)各个琴键的引脚以及音高(用数组来进行存储)。该部分的具体代码和注释如下:

// 发声部分设置
#define SPEAKER_PIN 8 // 定义Buzzer的引脚
#define RHYTHM_PIN A1 // 定义Slide Potentiometer的引脚
// 定义各个音调的声音频率
#define C5  523
#define D5  587
#define E5  659
#define F5  698
#define G5  784
#define A5  880
#define B5  988
#define C6  1047
const int numTones = 8; // 定义琴键个数(一共8个)
const uint8_t buttonPins[numTones] = {12, 11, 10, 9, 7, 6, 5, 4}; // 定义各个琴键的引脚
const int buttonTones[numTones] = {C5, D5, E5, F5, G5, A5, B5, C6}; // 定义每个琴键的音高
uint8_t key = 8; // 记录被按下琴键的状态 方便后续灯光效果的显示和切换

​ 完成了发声部分的全局设置后,需要进行灯光部分功能组件的全局设置。需要设置的内容主要包括(1)LED显示屏的引脚、LED数量、LED兼容类型、RGB排列顺序、LEDs对象的创建等FastLED库使用的基础设置;(2)由于本项目的灯光效果主要模拟火焰蔓延的形式,因此还需要定义火焰的宽度、高度;(3)heat数组,用于记录灯光颜色的需要进行变化的效果;(4)调色板变量,用于灯光颜色的设置;(5)两个用于记录状态改变的变量。该部分的代码和注释如下:

// 灯光部分设置
#define LED_PIN 3 // 定义LED显示屏引脚
#define NUM_LEDS 1630 // 定义LED数量
#define LED_TYPE WS2812 // 定义LED兼容类型
#define COLOR_ORDER GRB // 灯珠内RGB排列顺序
CRGB leds[NUM_LEDS]; // LEDs对象的创建
const uint8_t led_count[] = {160, 145, 142, 135, 125, 118, 110, 102, 95, 86, 78, 70, 62, 53, 45, 37, 29, 21, 12, 4, 1}; // 每个环中LED灯珠的数量
#define NUM_RINGS (sizeof(led_count) / sizeof(led_count[0])) // LED环的数量
#define FIRE_WIDTH 64 // 火焰的宽度
#define FIRE_HEIGHT NUM_RINGS // 火焰的高度
static uint8_t heat[FIRE_WIDTH][FIRE_HEIGHT]; // heat数组 用于记录灯光颜色的需要进行变化的效果
CRGBPalette256 currentPalette; // 调色板变量 用于灯光颜色的设置
bool fade = false; // 状态变量 用于记录当前是否需要让当前的灯光效果褪去
uint8_t fadeCnt = 0; // 计数器 用于记录在多少个单位时间后 灯光效果开始褪去

​ 在完成灯光部分的全局设置后,还需要进行关于自动播放功能组件的全局设置。需要设置的主要内容包括(1)Slide Switch的引脚定义;(2)记录是否需要自动播放、当前自动播放到了第几个音符的变量;(3)需要自动播放的音乐的简谱;(4)记录当前的简谱需要演奏多少个音符的变量。该部分的代码和注释如下:

// 自动化开关设置
#define AUTO_BUTTON A0 // 定义Slide Switch的引脚
int auto_status; // 记录是否要进行自动播放 由于需要接收到Slide Switch的信号值 这里的数据类型要用到int
int auto_play_step = 0; // 记录当前演奏到了第几个音符
const uint8_t simple_spectrum[] = {5, 8, 7, 5, 4, 5, 5, 4, 5, 8, 8, 5, 5, 5,
                                   5, 5, 5, 2, 3, 2, 1, 5, 6, 1, 1, 7, 1, 2,
                                   3, 0, 5, 5, 5, 2, 3, 2, 1, 5, 6, 1, 1, 7,
                                   1, 2, 1, 0, 1, 6, 1, 7, 1, 2, 3, 0, 1, 6,
                                   6, 6, 5, 4, 3, 0, 5, 8, 7, 6, 5, 3, 2, 1,
                                   1, 6, 1, 7, 1, 2, 1, 0, 5, 8, 7, 8, 5, 4,
                                   3, 0, 5, 8, 7, 6, 5, 4}; // 需要自动播放的音乐的简谱
#define steps (sizeof(simple_spectrum) / sizeof(simple_spectrum[0])) // 计算并记录总共需要演奏多少个音符

​ 完成了所有的全局设置后,需要在setup()函数中完成对各个组件的初始化操作,该部分的代码和注释如下:

void setup() {
    Serial.begin(115200); // 定义数据输出频率

    // LED 初始化
    FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS); // LED 初始化操作
    uint8_t i = 0;
    do {
        currentPalette[i] = CRGB(i, i, i);
    } while ( ++ i); // 设置调色板 令显示屏初始时显示白色火焰的灯光效果

    // 琴键以及Buzzer初始化
    for (uint8_t i = 0; i < numTones; i++)
        pinMode(buttonPins[i], INPUT_PULLUP);
    pinMode(SPEAKER_PIN, OUTPUT);

    pinMode(AUTO_BUTTON, INPUT_PULLUP); // Slide Switch初始化
    pinMode(RHYTHM_PIN, INPUT_PULLUP);  // Slide Potentiometer初始化
}

​ 在setup()函数后,就是程序的关键部分——loop()函数的实现,因为loop()函数是定义各组件行为的关键函数。为了实现所设计的功能,音乐盒的loop()函数的主要逻辑为(1)根据当前琴键的状态处理heat数组,并记录开始处理前、处理完成后这两个时刻,用于在调试模式时输出heat数组的处理时长;(2)对于每个LED环,用do-while循环根据处理后的heat数组中的相应值设置每个LED的颜色,并记录设置完成后的时刻,用于调试模式时输出灯光设置的处理时长;(3)根据当前Slide Switch的状态(决定了当前是自动播放模式还是弹奏模式)更新被按下的琴键、需要发出的音调等变量,如果是自动播放模式,则根据设定的简谱以及当前演奏到的音符(演奏到某个音符相当于某个对应的琴键被按下)的索引来更新,且需要根据Slide Potentiometer的模拟信号值设置声音的持续时长,以控制音乐的节奏,如果是手动弹奏模式,则根据当前被按下的琴键来进行更新;(4)根据琴键的状态来设置灯效以及发出声音;(5)根据调试变量TIMING的值来决定是否输出调试信息。该部分的代码和注释如下:

void loop() {
    // micros()获取当前时刻 用于测试Fire函数的时间
    unsigned long t1 = micros(); // 开始处理前时刻
    if (key == 8) Fire(random8() / 2 + 128); // Fire函数处理heat数组
    unsigned long t2 = micros(); // 处理完成后时刻

    CRGB *led = leds;

    // 对于每个LED环 用循环根据heat数组中的相应值设置每个LED的颜色
    uint8_t ring = 0; // 当前设置到第几个环
    do {
        uint8_t count = led_count[ring]; // 将count设置为当前环中的LED数量
        uint16_t td = FIRE_WIDTH * 255 / count; // 用td表示灯光从一个当前像素需要扩散到的像素的数量
        uint16_t t = 0; // 对于每个LED灯珠 使用t计算其在heat数组中相应的索引
        for (uint8_t i = 0; i < count ; i++) { // 遍历当前环中的每个LED 
            uint8_t h = heat[t >> 8][FIRE_HEIGHT - 1 - ring]; // 从heat数组中检索相应的值
            *led ++ = currentPalette[h]; // 使用调色板变量基于该值和当前调色板设置LED的颜色
            t += td;
        }
    } while ( ++ ring < NUM_RINGS);
    unsigned long t3 = micros(); // 获取设置完每个灯珠颜色后的时刻

    FastLED.show(); // 灯光显示

    int pitch = 0; // 需要弹奏的音调(Buzzer需要发出的频率)
    bool push = false; // 记录当前是否有按钮被按下

    auto_status = analogRead(AUTO_BUTTON); // 接收当前Slide Switch的模拟信号
    if (auto_status > 500) { // 如果Slide Switch的模拟信号值大于500 则为自动播放模式
        int rhythm_status = analogRead(RHYTHM_PIN); // 接收Slide Potentiometer的模拟信号
        delay(rhythm_status); // 设置每个音符的持续时长
        // 找出当前演奏需要按下第几个琴键
        key = simple_spectrum[auto_play_step] - 1;
        if (key != 255) { // 如果的音符不是空
            pitch = buttonTones[key]; // 找出当前需要演奏第几个琴键并赋值给变量key
            push = true; // 更新布尔变量push的状态
        }
        auto_play_step = (auto_play_step + 1) % steps; // 更新到下一个需要演奏的音符的索引
    } else { // 如果当前时弹奏模式
        for (uint8_t i = 0; i < numTones; i ++ ) { // 利用for循环检查每个琴键是否被按下
            if (digitalRead(buttonPins[i]) == LOW) { // 如果琴键被按下
                pitch = buttonTones[i]; // 将与该按钮相关的频率存储在pitch变量中
                key = i; // 用key记录被按下的琴键的索引
                push = true; // 更新push的状态
            }
        }
    }

    // 如果当前没有琴键被按下 则隔一定时间后恢复初始状态
    if (!push) {
        // 经过25个单位时间后 灯光效果褪去
        if (fadeCnt < 25) fadeCnt ++ ;
        else {
            fadeCnt = 0;
            key = 8;
        }
    }

    // 如果有琴键被按下
    if (key != 8) {
        // 被按下的同时先清除原本已有的灯效
        if (!fade) {
            for (int i = 0; i < FIRE_WIDTH; i ++ )
                for (int j = 0; j < FIRE_HEIGHT; j ++ )
                    heat[i][j] = 0;
        }
        MusicAndLight(pitch, key); // 根据pitch和key的值发出声音和显示灯效
        fade = true;
    } else { // 如果没有任何琴键被按下
        // 先清除原有的灯效
        if (fade) {
            for (int i = 0; i < FIRE_WIDTH; i ++ )
                for (int j = 0; j < FIRE_HEIGHT; j ++ )
                    heat[i][j] = 0;
        }
        // 再恢复成初始的白色火焰灯光状态
        uint8_t i = 0;
        do {
            currentPalette[i] = CRGB(i, i, i);
        } while ( ++ i);
        fade = false;
    }

    // 如果 TIMING 设置为true 则测量 Fire() 函数和映射过程所花费的时间 并且以每 64 帧将平均时间打印到串行控制台
    if (TIMING) {
        static unsigned long t2_sum, t3_sum;
        t2_sum += t2 - t1;
        t3_sum += t3 - t2;
        static byte frame;
        if (!(++frame % 64)) {
            Serial.print(F("fire "));
            Serial.print(t2_sum / 64);
            Serial.print(F("us\tmap "));
            Serial.print(t3_sum / 64);
            Serial.print(F(" us\t"));
            Serial.println(FastLED.getFPS());
            t2_sum = t3_sum = 0;
        }
    }
}

loop()函数中,对heat数组进行预处理是灯光特效的关键,Fire()则是这个步骤的关键函数,该函数主要实现的逻辑包括(1)使用双重循环利用heat数组中每个单元的新值来更新heat数组;(2)内嵌的循环需要处理火焰的冷却、火焰的扩散(蔓延)、火焰的闪烁等效果。用代码以及注释展示如下:

// 参数activity决定颜色变化的活跃程度 数值越高表示颜色变化越活跃
void Fire(uint8_t activity) {
    // 双重循环 利用heat数组中每个单元的新值更新heat数组
	for (uint8_t h = 0; h < FIRE_WIDTH; h ++ ) {
		// 通过从中减去0到31之间的随机值来冷却heat数组的每个单元格 模拟火随着时间冷却的效果
		for( uint8_t i = 1; i < FIRE_HEIGHT - 1; i ++ )
			heat[h][i] = qsub8(heat[h][i], random8(32));

		// 计算从heat数组的每个单元到相邻单元的热扩散
		for(uint8_t k = FIRE_HEIGHT - 1; k >= 1; k -- ) {
            // 取当前单元的热值、它左边、右边、上面 共四个单元格的热值的平均值
            // 模拟每个单元的热量向上转移 并向周围的细胞轻微扩散
			uint8_t hleft = (h + FIRE_WIDTH - 1) % FIRE_WIDTH;
			uint8_t hright = (h + 1) % FIRE_WIDTH;
			heat[h][k] = (heat[h][k]
						+ heat[hleft][k - 1]
						+ heat[h][k - 1]
						+ heat[hright][k - 1] ) / 4;
		}
		
        // 根据activity参数值在heat数组的第一行随机添加或减去0到63之间的值
        // 模拟新的热量被添加到火的底部 也是产生闪烁效果的原因
		if(random8() < activity)
			heat[h][0] = qadd8(heat[h][0], random8(64));
        else
			heat[h][0] = qsub8(heat[h][0], random8(64));
	}
}

​ 考虑到记录音调的变量pitch的值在按下每个不同的琴键时都会不同,因此,可以利用琴键对应pitch值唯一性的特点来对灯光的特性进行设置。因此loop()函数中所调用的MusicAndLight()函数就是用来实现这个需求的。该函数的主要实现的逻辑包括(1)根据传入的pitch变量调用函数令Buzzer发出对应的声音;(2)根据传入的key变量(用于记录哪个按键被按下)利用switch-case结构为发出不同音调时的灯光显示分配不同的调色板;(3)调用Fire()函数根据数值映射到[0, 255]的pitch变量更新改变调色板后的heat数组,从而改变灯光的显示效果。

​ 值得注意的是,Fire()函数的参数activity的值越大,灯光效果的变化越活跃,而当Buzzer发出的声音的音调越高,pitch的值越大,因此,将映射后的pitch变量作为参数传入Fire()函数以更新heat数组,就能实现音调越高,灯光规模越大、变化越丰富的效果。MusicAndLight()函数的代码以及注释展示如下:

void MusicAndLight(int pitch, uint8_t key) {
    // 根据传入的pitch变量调用函数令Buzzer发出对应的声音
	if (pitch) tone(SPEAKER_PIN, pitch);
	else noTone(SPEAKER_PIN);

	uint8_t i = 0;
    // 根据传入的key变量不同音调时的灯光显示分配不同的调色板
	switch (key) {
		case 0: // 按下第一个琴键时
			do {
			currentPalette[i] = HeatColor(i);
			} while ( ++ i); // 调色板设置为 HeatColor
			break;
		case 1: // 按下第二个琴键时
			do {
				currentPalette[i] = ColorFromPalette(OceanColors_p, i, 255);
			} while ( ++ i); // 调色板设置为 OceanColors_p
			break;
		case 2: // 按下第三个琴键时
			do {
				currentPalette[i] = ColorFromPalette(PartyColors_p, i, 255);
			} while ( ++ i); // 调色板设置为 PartyColors_p
			break;
		case 3: // 按下第四个琴键时
			do {
				currentPalette[i] = ColorFromPalette(ForestColors_p, i, 255);
			} while ( ++ i); // 调色板设置为 ForestColors_p
			break;
		case 4: // 按下第五个琴键时
			do {
				currentPalette[i] = ColorFromPalette(LavaColors_p, i, 255);
			} while ( ++ i); // 调色板设置为 LavaColors_p
			break;
		case 5: // 按下第六个琴键时
			do {
				currentPalette[i] = ColorFromPalette(CloudColors_p, i, 255);
			} while ( ++ i); // 调色板设置为 CloudColors_p
			break;
		case 6: // 按下第七个琴键时
			do {
				currentPalette[i] = ColorFromPalette(RainbowStripeColors_p, i, 255);
			} while ( ++ i); // 调色板设置为 RainbowStripeColors_p
			break;
		case 7: // 按下第八个琴键时
			do {
				currentPalette[i] = ColorFromPalette(RainbowColors_p, i, 255);
			} while ( ++ i); // 调色板设置为 RainbowColors_p
		break;
	}
	int mappedPitch = map(pitch, 0, 1048, 0, 255); // 将pitch的值映射到区间[0, 255]
	Fire(mappedPitch); // 根据映射后的pitch处理heat数组
}

​ 以上是所有代码的详细分析,在完成以上代码的编译以及上传到开发板的操作后,智能炫彩音乐盒已经能够实现所有设计所需的功能。

5. 成果展示

​ 项目的灯光效果如图-12所示,不同的灯光代表当前发出不同声调的声音,(从上往下、从左往右看)第一个图表示的是没有任何琴键被按下时的初始状态,第二到第九个图分别是音调从低到高时,不同音调所对应的灯光效果。可以看出,当音调较低时,火焰灯效的半径越小、颜色越单调,而当音调较高时,火焰灯效的半径越大、颜色越丰富。

在这里插入图片描述

图-12 灯光效果成果展示

​ 但由于该项目的亮点在于其声音效果以及丝滑绚丽的灯效变换动态,图片并不能很好地展示智能炫彩音乐盒的真正效果,因此最终的成果还需要参考“系统构造介绍.mp4”以及“系统运行.mp4”。

6. 总结和讨论

​ 本项目的目标是使用Arduino组件构建一个智能炫彩音乐盒,它可以在自动播放和手动弹奏模式之间灵活切换,自动播放模式时,还能根据用户的需求灵活调节音乐节奏。该项目使用了NeoPixel Compatible LED Ring、Pushbutton、Buzzer等简单的Arduino组件实现了一组复杂的功能,包括根据用户的行为展现悦耳的音乐、不同的音调、与声音相辅相成的灯光特效以及灵活的节奏,成功构建了一个能为用户提供结合视觉、听觉和触觉美好体验的智能音乐盒。

6.1 项目关键点

​ 本项目的关键点包括:

  • Arduino组件的有效应用,包括NeoPixel Compatible LED Ring、Pushbutton、Buzzer。通过使用这些组件,我成功依据自己的想法创建了优秀的音效和炫酷的灯光,并让两者相互映衬,带来了丰富的感官体验。
  • 赋予了音乐盒灵活性,允许用户在自动播放和手动弹奏模式之间灵活切换,为用户同时提供了享受音乐和创作音乐的机会,增加了项目的多功能性以及趣味性。
  • 节奏可调节,让用户有机会尝试并手动调节适合他们偏好的节奏,换句话说,音乐盒的这一方面为用户提供了大量的操作自由,允许用户根据自己的音乐品味来控制音乐盒。
  • 根据用户的弹奏行为或发声音调的不同来产生不同色调和灯效的功能,它为用户提供了一种独特的视觉、听觉和触觉享受,为用户带来更加身临其境的体验。

​ 这些关键点体现了本项目的创作深度和创新性,我认为这些功能元素能使这款智能炫彩音乐盒脱颖而出。

6.2 讨论

​ 虽然项目已经暂时完成了初始设计好的功能,但仍然具有巨大的改善迭代空间。其中一个可行的想法是添加更多额外的操作模式或乐器,允许用户创建更复杂的音乐作品。另外,还可以考虑对灯光特效进行更深入的调整,从而实现更具视觉动感的光效。最后,还可以实现一些实用的改进,如添加文档或改进代码,以改善用户体验,并使项目更易于初学者使用。

7. 代码

​ 电路搭建的json代码如下:

{
  "version": 1,
  "author": "stevesigma + sutaburosu",
  "editor": "wokwi",
  "parts": [
    { "type": "wokwi-arduino-mega", "id": "mega", "top": 1178.96, "left": 128.83, "attrs": {} },
    { "type": "wokwi-neopixel", "id": "pixel1", "top": 620, "left": 620, "attrs": {} },
    {
      "type": "wokwi-led-ring",
      "id": "ring4",
      "top": 590,
      "left": 590,
      "attrs": { "pixels": "4", "background": "black", "pixelSpacing": "2.2" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring12",
      "top": 560,
      "left": 560,
      "attrs": { "pixels": "12", "background": "black", "pixelSpacing": "1.65" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring21",
      "top": 530,
      "left": 530,
      "attrs": { "pixels": "21", "background": "black", "pixelSpacing": "1.17" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring29",
      "top": 500,
      "left": 500,
      "attrs": { "pixels": "29", "background": "black", "pixelSpacing": "1.18" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring37",
      "top": 470,
      "left": 470,
      "attrs": { "pixels": "37", "background": "black", "pixelSpacing": "1.19" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring45",
      "top": 440,
      "left": 440,
      "attrs": { "pixels": "45", "background": "black", "pixelSpacing": "1.2" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring53",
      "top": 410,
      "left": 410,
      "attrs": { "pixels": "53", "background": "black", "pixelSpacing": "1.22" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring62",
      "top": 380,
      "left": 380,
      "attrs": { "pixels": "62", "background": "black", "pixelSpacing": "1.12" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring70",
      "top": 350,
      "left": 350,
      "attrs": { "pixels": "70", "background": "black", "pixelSpacing": "1.13" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring78",
      "top": 320,
      "left": 320,
      "attrs": { "pixels": "78", "background": "black", "pixelSpacing": "1.137" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring86",
      "top": 290,
      "left": 290,
      "attrs": { "pixels": "86", "background": "black", "pixelSpacing": "1.148" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring95",
      "top": 260,
      "left": 260,
      "attrs": { "pixels": "95", "background": "black", "pixelSpacing": "1.095" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring102",
      "top": 230,
      "left": 230,
      "attrs": { "pixels": "102", "background": "black", "pixelSpacing": "1.162" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring110",
      "top": 200,
      "left": 200,
      "attrs": { "pixels": "110", "background": "black", "pixelSpacing": "1.17" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring118",
      "top": 170,
      "left": 170,
      "attrs": { "pixels": "118", "background": "black", "pixelSpacing": "1.18" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring125",
      "top": 140,
      "left": 140,
      "attrs": { "pixels": "125", "background": "black", "pixelSpacing": "1.23" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring135",
      "top": 110,
      "left": 110,
      "attrs": { "pixels": "135", "background": "black", "pixelSpacing": "1.14" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring142",
      "top": 80,
      "left": 80,
      "attrs": { "pixels": "142", "background": "black", "pixelSpacing": "1.19" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring145",
      "top": 50,
      "left": 50,
      "attrs": { "pixels": "145", "background": "black", "pixelSpacing": "1.405" }
    },
    {
      "type": "wokwi-led-ring",
      "id": "ring160",
      "top": 20,
      "left": 20,
      "attrs": { "pixels": "160", "background": "black", "pixelSpacing": "1.11" }
    },
    {
      "type": "wokwi-pushbutton",
      "id": "btn1",
      "top": 1410,
      "left": 100,
      "rotate": 90,
      "attrs": { "color": "red", "key": "1" }
    },
    {
      "type": "wokwi-pushbutton",
      "id": "btn2",
      "top": 1410,
      "left": 160,
      "rotate": 90,
      "attrs": { "color": "orange", "key": "2" }
    },
    {
      "type": "wokwi-pushbutton",
      "id": "btn3",
      "top": 1410,
      "left": 220,
      "rotate": 90,
      "attrs": { "color": "yellow", "key": "3" }
    },
    {
      "type": "wokwi-pushbutton",
      "id": "btn4",
      "top": 1410,
      "left": 280,
      "rotate": 90,
      "attrs": { "color": "green", "key": "4" }
    },
    {
      "type": "wokwi-pushbutton",
      "id": "btn5",
      "top": 1410,
      "left": 340,
      "rotate": 90,
      "attrs": { "color": "skyblue", "key": "5" }
    },
    {
      "type": "wokwi-pushbutton",
      "id": "btn6",
      "top": 1410,
      "left": 400,
      "rotate": 90,
      "attrs": { "color": "blue", "key": "6" }
    },
    {
      "type": "wokwi-pushbutton",
      "id": "btn7",
      "top": 1410,
      "left": 460,
      "rotate": 90,
      "attrs": { "color": "purple", "key": "7" }
    },
    {
      "type": "wokwi-pushbutton",
      "id": "btn8",
      "top": 1410,
      "left": 520,
      "rotate": 90,
      "attrs": { "color": "pink", "key": "8" }
    },
    {
      "type": "wokwi-buzzer",
      "id": "bz1",
      "top": 1303.05,
      "left": 650,
      "rotate": 90,
      "attrs": { "volume": "0.1" }
    },
    {
      "type": "wokwi-slide-switch",
      "id": "sw1",
      "top": 1373.78,
      "left": 50.47,
      "rotate": 270,
      "attrs": {}
    },
    {
      "type": "wokwi-slide-potentiometer",
      "id": "pot1",
      "top": 1091.28,
      "left": 28.36,
      "attrs": { "travelLength": "30" }
    }
  ],
  "connections": [
    [ "mega:GND.2", "ring160:GND", "black", [ "*", "*", "v150" ] ],
    [ "mega:3", "ring160:DIN", "green", [ "*", "*", "v30" ] ],
    [ "mega:5V", "ring160:VCC", "red", [ "*", "*", "v160" ] ],
    [ "ring160:DOUT", "ring145:DIN", "", [ "*" ] ],
    [ "ring145:DOUT", "ring142:DIN", "", [ "*" ] ],
    [ "ring142:DOUT", "ring135:DIN", "", [ "*" ] ],
    [ "ring135:DOUT", "ring125:DIN", "", [ "*" ] ],
    [ "ring125:DOUT", "ring118:DIN", "", [ "*" ] ],
    [ "ring118:DOUT", "ring110:DIN", "", [ "*" ] ],
    [ "ring110:DOUT", "ring102:DIN", "", [ "*" ] ],
    [ "ring102:DOUT", "ring95:DIN", "", [ "*" ] ],
    [ "ring95:DOUT", "ring86:DIN", "", [ "*" ] ],
    [ "ring86:DOUT", "ring78:DIN", "", [ "*" ] ],
    [ "ring78:DOUT", "ring70:DIN", "", [ "*" ] ],
    [ "ring70:DOUT", "ring62:DIN", "", [ "*" ] ],
    [ "ring62:DOUT", "ring53:DIN", "", [ "*" ] ],
    [ "ring53:DOUT", "ring45:DIN", "", [ "*" ] ],
    [ "ring45:DOUT", "ring37:DIN", "", [ "*" ] ],
    [ "ring37:DOUT", "ring29:DIN", "", [ "*" ] ],
    [ "ring29:DOUT", "ring21:DIN", "", [ "*" ] ],
    [ "ring21:DOUT", "ring12:DIN", "", [ "*" ] ],
    [ "ring12:DOUT", "ring4:DIN", "", [ "*" ] ],
    [ "ring4:DOUT", "pixel1:DIN", "", [ "*" ] ],
    [ "ring160:GND", "ring145:GND", "", [ "*" ] ],
    [ "ring145:GND", "ring142:GND", "", [ "*" ] ],
    [ "ring142:GND", "ring135:GND", "", [ "*" ] ],
    [ "ring135:GND", "ring125:GND", "", [ "*" ] ],
    [ "ring125:GND", "ring118:GND", "", [ "*" ] ],
    [ "ring118:GND", "ring110:GND", "", [ "*" ] ],
    [ "ring110:GND", "ring102:GND", "", [ "*" ] ],
    [ "ring102:GND", "ring95:GND", "", [ "*" ] ],
    [ "ring95:GND", "ring86:GND", "", [ "*" ] ],
    [ "ring86:GND", "ring78:GND", "", [ "*" ] ],
    [ "ring78:GND", "ring70:GND", "", [ "*" ] ],
    [ "ring70:GND", "ring62:GND", "", [ "*" ] ],
    [ "ring62:GND", "ring53:GND", "", [ "*" ] ],
    [ "ring53:GND", "ring45:GND", "", [ "*" ] ],
    [ "ring45:GND", "ring37:GND", "", [ "*" ] ],
    [ "ring37:GND", "ring29:GND", "", [ "*" ] ],
    [ "ring29:GND", "ring21:GND", "", [ "*" ] ],
    [ "ring21:GND", "ring12:GND", "", [ "*" ] ],
    [ "ring12:GND", "ring4:GND", "", [ "*" ] ],
    [ "ring4:GND", "pixel1:VSS", "", [ "*" ] ],
    [ "ring160:VCC", "ring145:VCC", "", [ "*" ] ],
    [ "ring145:VCC", "ring142:VCC", "", [ "*" ] ],
    [ "ring142:VCC", "ring135:VCC", "", [ "*" ] ],
    [ "ring135:VCC", "ring125:VCC", "", [ "*" ] ],
    [ "ring125:VCC", "ring118:VCC", "", [ "*" ] ],
    [ "ring118:VCC", "ring110:VCC", "", [ "*" ] ],
    [ "ring110:VCC", "ring102:VCC", "", [ "*" ] ],
    [ "ring102:VCC", "ring95:VCC", "", [ "*" ] ],
    [ "ring95:VCC", "ring86:VCC", "", [ "*" ] ],
    [ "ring86:VCC", "ring78:VCC", "", [ "*" ] ],
    [ "ring78:VCC", "ring70:VCC", "", [ "*" ] ],
    [ "ring70:VCC", "ring62:VCC", "", [ "*" ] ],
    [ "ring62:VCC", "ring53:VCC", "", [ "*" ] ],
    [ "ring53:VCC", "ring45:VCC", "", [ "*" ] ],
    [ "ring45:VCC", "ring37:VCC", "", [ "*" ] ],
    [ "ring37:VCC", "ring29:VCC", "", [ "*" ] ],
    [ "ring29:VCC", "ring21:VCC", "", [ "*" ] ],
    [ "ring21:VCC", "ring12:VCC", "", [ "*" ] ],
    [ "ring12:VCC", "ring4:VCC", "", [ "*" ] ],
    [ "ring4:VCC", "pixel1:VDD", "", [ "*" ] ],
    [ "btn1:2.l", "mega:12", "green", [ "v0" ] ],
    [ "btn2:2.l", "mega:11", "green", [ "v0" ] ],
    [ "btn3:2.l", "mega:10", "green", [ "v0" ] ],
    [ "btn4:2.l", "mega:9", "green", [ "v-214.34", "h-75.32" ] ],
    [ "btn5:2.l", "mega:7", "green", [ "v-166.89", "h-96.52" ] ],
    [ "btn6:2.l", "mega:6", "green", [ "v-188.72", "h-163.25" ] ],
    [ "btn7:2.l", "mega:5", "green", [ "v-163.84", "h-55.76" ] ],
    [ "btn8:2.l", "mega:4", "green", [ "v-147.38", "h-200.91" ] ],
    [ "btn1:1.l", "mega:GND.1", "green", [ "v0" ] ],
    [ "btn2:1.l", "mega:GND.1", "green", [ "v0" ] ],
    [ "btn3:1.l", "mega:GND.1", "green", [ "v-272.15", "h-68.49" ] ],
    [ "btn4:1.l", "mega:GND.1", "green", [ "v-234.68", "h-135.96" ] ],
    [ "btn5:1.l", "mega:GND.1", "green", [ "v-201.75", "h-160.07" ] ],
    [ "btn6:1.l", "mega:GND.1", "green", [ "v-182.91", "h-207.43" ] ],
    [ "btn7:1.l", "mega:GND.1", "green", [ "v-159.96", "h-294.57" ] ],
    [ "btn8:1.l", "mega:GND.1", "green", [ "v-154.15", "h-230.56" ] ],
    [ "bz1:1", "mega:GND.1", "green", [ "h0" ] ],
    [ "bz1:2", "mega:8", "green", [ "h0" ] ],
    [ "sw1:1", "mega:GND.3", "green", [ "h0" ] ],
    [ "sw1:3", "mega:5V", "green", [ "h0" ] ],
    [ "sw1:2", "mega:A0", "green", [ "h0" ] ],
    [ "pot1:VCC", "mega:5V", "red", [ "v0" ] ],
    [ "pot1:GND", "mega:GND.3", "black", [ "v0" ] ],
    [ "pot1:SIG", "mega:A1", "green", [ "v0" ] ]
  ],
  "dependencies": {}
}

​ 逻辑功能实现的C++代码如下:

#include <FastLED.h>
#define TIMING 0

// 钢琴设置
#define SPEAKER_PIN 8 // 定义扬声器的引脚
#define RHYTHM_PIN A1 // 定义节奏控制器的引脚
#define C5  523 // 定义各个音高
#define D5  587
#define E5  659
#define F5  698
#define G5  784
#define A5  880
#define B5  988
#define C6  1047
const int numTones = 8; // 一共 8 个琴键
const uint8_t buttonPins[numTones] = {12, 11, 10, 9, 7, 6, 5, 4}; // 定义各个琴键的引脚
const int buttonTones[numTones] = {C5, D5, E5, F5, G5, A5, B5, C6}; // 定义每个琴键的音高
uint8_t key = 8; // 被按下的琴键

// LED 设置
#define LED_PIN 3 // 定义 LED 的引脚
#define NUM_LEDS 1630 // 定义 LED 灯珠的数量
#define LED_TYPE WS2812 // 定义 LED 的类型 如果换为 LED 矩阵则为 MAX7219
#define COLOR_ORDER GRB // 灯珠内 RGB 的排列顺序
CRGB leds[NUM_LEDS]; // 建立 LED 对象
const uint8_t led_count[] = {160, 145, 142, 135, 125, 118, 110, 102, 95, 86, 78, 70, 62, 53, 45, 37, 29, 21, 12, 4, 1}; // 每个环中 LED 灯珠的数量
#define NUM_RINGS (sizeof(led_count) / sizeof(led_count[0])) // 圆环的数量
#define FIRE_WIDTH 64 // 火焰的宽度
#define FIRE_HEIGHT NUM_RINGS // 火焰的高度
static uint8_t heat[FIRE_WIDTH][FIRE_HEIGHT]; // heat 数组 记录颜色
CRGBPalette256 currentPalette;
bool fade = false;
uint8_t fadeCnt = 0;

// 自动化开关设置
#define AUTO_BUTTON A0
int auto_status; // 记录是否要进行自动播放
int auto_play_step = 0; // 记录当前演奏到了第几个音符
const uint8_t simple_spectrum[] = {5, 8, 7, 5, 4, 5, 5, 4, 5, 8, 8, 5, 5, 5,
                                   5, 5, 5, 2, 3, 2, 1, 5, 6, 1, 1, 7, 1, 2,
                                   3, 0, 5, 5, 5, 2, 3, 2, 1, 5, 6, 1, 1, 7,
                                   1, 2, 1, 0, 1, 6, 1, 7, 1, 2, 3, 0, 1, 6,
                                   6, 6, 5, 4, 3, 0, 5, 8, 7, 6, 5, 3, 2, 1,
                                   1, 6, 1, 7, 1, 2, 1, 0, 5, 8, 7, 8, 5, 4,
                                   3, 0, 5, 8, 7, 6, 5, 4}; // 需要自动播放的音乐的简谱
#define steps (sizeof(simple_spectrum) / sizeof(simple_spectrum[0])) // 计算并记录总共需要演奏多少个音符

void setup() {
    Serial.begin(115200);

    // LED 初始化
    FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS); // LED 初始化操作
    uint8_t i = 0;
    do {
        currentPalette[i] = CRGB(i, i, i);
    } while ( ++ i);

    // 琴键初始化
    for (uint8_t i = 0; i < numTones; i++) {
        pinMode(buttonPins[i], INPUT_PULLUP);
    }
    pinMode(SPEAKER_PIN, OUTPUT);

    // 自动化开关初始化
    pinMode(AUTO_BUTTON, INPUT_PULLUP);

    // 节奏控制器的初始化
    pinMode(RHYTHM_PIN, INPUT_PULLUP);
}

void loop() {
    // micros() 获取当前的时间 从而测试 Fire 函数的时间
    unsigned long t1 = micros();
    if (key == 8) Fire(random8() / 2 + 128); // 处理 heat 数组
    unsigned long t2 = micros();

    CRGB *led = leds;

    // 对于每个 LED 环 循环内的代码根据 heat 数组中的相应值设置每个 LED 的颜色
    uint8_t ring = 0;
    do {
        uint8_t count = led_count[ring]; // 将 count 设置为当前环中的LED数量
        uint16_t td = FIRE_WIDTH * 255 / count; // 用 td 表示每个 LED 的 heat 数组的元素数量
        uint16_t t = 0; // 对于每个LED 使用 t 计算 heat 数组中相应的索引
        // 遍历当前环中的每个 LED 
        for (uint8_t i = 0; i < count ; i++) { 
            uint8_t h = heat[t >> 8][FIRE_HEIGHT - 1 - ring]; // 从 heat 数组中检索相应的值
            *led ++ = currentPalette[h]; // 使用 ColorFromPalette() 函数基于该值和当前调色板设置LED的颜色
            t += td;
        }
    } while ( ++ ring < NUM_RINGS);
    unsigned long t3 = micros(); // t3 设置为当前时间 使用 micros() 来测量基于 heat 数组设置 LED 颜色所需的时间

    FastLED.show();

    int pitch = 0; // 音高
    bool push = false; // 是否有按钮被按下

    // 自动化处理
    auto_status = analogRead(AUTO_BUTTON);
    if (auto_status > 500) {
        int rhythm_status = analogRead(RHYTHM_PIN);
        delay(rhythm_status);
        // 找出当前演奏需要按下第几个琴键
        key = simple_spectrum[auto_play_step] - 1;
        if (key != 255) {
            pitch = buttonTones[key];
            push = true;
        }
        auto_play_step = (auto_play_step + 1) % steps;
    } else {
        // 处理琴键被按下时的事件
        for (uint8_t i = 0; i < numTones; i ++ ) {
            if (digitalRead(buttonPins[i]) == LOW) {
                pitch = buttonTones[i];
                key = i;
                push = true;
            }
        }
    }

    // 如果当前没有琴键被按下 则隔一定时间后恢复初始状态
    if (!push) {
        if (fadeCnt < 25) fadeCnt ++ ;
        else {
            fadeCnt = 0;
            key = 8;
        }
    }

    // 如果有琴键被按下
    if (key != 8) {
        // 被按下的同时先清除原本已有的灯效
        if (!fade) {
            for (int i = 0; i < FIRE_WIDTH; i ++ )
                for (int j = 0; j < FIRE_HEIGHT; j ++ )
                    heat[i][j] = 0;
        }
        MusicAndLight(pitch, key);
        fade = true;
    } else { // 如果没有任何琴键被按下
        // 先清除原有的灯效
        if (fade) {
            for (int i = 0; i < FIRE_WIDTH; i ++ )
                for (int j = 0; j < FIRE_HEIGHT; j ++ )
                    heat[i][j] = 0;
        }
        // 再恢复成初始状态
        uint8_t i = 0;
        do {
            currentPalette[i] = CRGB(i, i, i);
        } while ( ++ i);
        fade = false;
    }

    // 如果 TIMING 设置为true 则测量 Fire() 函数和映射过程所花费的时间 并且以每 64 帧将平均时间打印到串行控制台
    if (TIMING) {
        static unsigned long t2_sum, t3_sum;
        t2_sum += t2 - t1;
        t3_sum += t3 - t2;
        static byte frame;
        if (!(++frame % 64)) {
            Serial.print(F("fire "));
            Serial.print(t2_sum / 64);
            Serial.print(F("us\tmap "));
            Serial.print(t3_sum / 64);
            Serial.print(F(" us\t"));
            Serial.println(FastLED.getFPS());
            t2_sum = t3_sum = 0;
        }
    }
}

// activity 决定颜色变化的活跃程度 数值越高 表示颜色变化越活跃
void Fire(uint8_t activity) {
    // 使用双个循环用 heat 数组中每个单元的新值更新 heat 数组
    for (uint8_t h = 0; h < FIRE_WIDTH; h++) {
        // 通过从中减去 0 到 31 之间的随机值来冷却 heat 数组的每个单元格 模拟火随着时间冷却的效果
        for( uint8_t i = 1; i < FIRE_HEIGHT - 1; i++) {
            heat[h][i] = qsub8(heat[h][i], random8(32));
        }

        // 计算从 heat 数组的每个单元到相邻单元的热扩散
        for( uint8_t k = FIRE_HEIGHT - 1; k >= 1; k -- ) {
            // 取当前单元的热值、它左边、右边、上面 共四个单元格的热值的平均值
            // 模拟每个单元的热量向上转移 并向周围的细胞轻微扩散
            uint8_t hleft = (h + FIRE_WIDTH - 1) % FIRE_WIDTH;
            uint8_t hright = (h + 1) % FIRE_WIDTH;
            heat[h][k] = (heat[h][k]
                        + heat[hleft][k - 1]
                        + heat[h][k - 1]
                        + heat[hright][k - 1] ) / 4;
        }

        // 根据 activity 参数在 heat 数组的第一行随机添加或减去 0 到 63 之间的值
        // 模拟新的热量被添加到火的底部 也是产生闪烁效果的原因
        if(random8() < activity) {
            heat[h][0] = qadd8(heat[h][0], random8(64));
        } else {
            heat[h][0] = qsub8(heat[h][0], random8(64));
        }
    }
}

// 当琴键被按下时 发出对应的声音并改变灯效
void MusicAndLight(int pitch, uint8_t key) {
    if (pitch) tone(SPEAKER_PIN, pitch);
    else noTone(SPEAKER_PIN);

    uint8_t i = 0;
    switch (key) {
        case 0:
            do {
              currentPalette[i] = HeatColor(i);
            } while ( ++ i);
            break;
        case 1:
            do {
              currentPalette[i] = ColorFromPalette(OceanColors_p, i, 255);
            } while ( ++ i);
            break;
        case 2:
            do {
              currentPalette[i] = ColorFromPalette(PartyColors_p, i, 255);
            } while ( ++ i);
            break;
        case 3:
            do {
              currentPalette[i] = ColorFromPalette(ForestColors_p, i, 255);
            } while ( ++ i);
            break;
        case 4:
            do {
              currentPalette[i] = ColorFromPalette(LavaColors_p, i, 255);
            } while ( ++ i);
            break;
        case 5:
            do {
              currentPalette[i] = ColorFromPalette(CloudColors_p, i, 255);
            } while ( ++ i);
            break;
        case 6:
            do {
              currentPalette[i] = ColorFromPalette(RainbowStripeColors_p, i, 255);
            } while ( ++ i);
            break;
        case 7:
            do {
              currentPalette[i] = ColorFromPalette(RainbowColors_p, i, 255);
            } while ( ++ i);
            break;
    }
    int mappedPitch = map(pitch, 0, 1048, 0, 255);
    Fire(mappedPitch); // 处理 heat 数组
}
  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值