用Arduino单片机制作一个简单的音乐播放器

Arduino单片机上有多个数字IO针脚,可以输出数字信号,用于驱动发声器件,从而让它发出想要的声音。蜂鸣器是一种常见的发声器件,通电后可以发出声音。因此,单片机可以通过数字输出控制蜂鸣器发出指定的声音。另外,Arduino支持串口的通信方式,可以从电脑上接收数据,根据收到的数据确定所需发的声音。本文说明用上述方式如何通过Arduino单片机实现一个简单的音乐播放器。

一、项目实现的工具

(一)无源蜂鸣器

蜂鸣器有分有源蜂鸣器和无源蜂鸣器两种。其中,无源蜂鸣器内部不带震荡源,要通过震荡的信号来令其发出声音。音调随震荡频率的不同而不同[1]

(二)Arduino单片机

Arduino单片机通过串口从电脑端获取音乐信息,然后根据音乐信息控制无源蜂鸣器的震荡频率,产生音乐效果。

(三)可调节电阻

用于控制无源蜂鸣器的电压大小,从而调节音量。

(四)树莓派电脑,运行基于Debian Linux的Raspberry Pi OS

一方面,编写Arduino程序并将其烧录进单片机;另一方面,将存储的音乐信息通过串口发送给Arduino。

(五)信号线

主要用于通电和传输信号

二、电路及程序设计

(一)电路

在该项目中,使用Arduino单片机的第7个IO针脚作为向蜂鸣器输出信号的针脚。注意Arduino单片机的输出是推挽输出,即高电平输出。

无源蜂鸣器有三个针脚,即电源(VCC),接地(GND)以及信号(I/O)。蜂鸣器在接通电源时,其随信号的高电平输入进行震荡,发出声音。而电源的电压决定了发声的响度。

电路大致如下

所以通过可调电阻,控制蜂鸣器的输入电压大小,从而调节音量大小。

(二)程序

在介绍程序之前,先简单介绍一些音乐的基本知识。我们通常的音乐是用八度音阶表示的。任何一个音符,和高八度的音相比,其音调(发声体震荡频率)相差两倍。每个八度区里有12个半音,所以每个半音的音调相差倍。也就是说,如果简谱中的的音调是Hz,那么的音调是Hz,而的音调是的音调是。详细说明,见[2]

本项目在Arduino程序中,用一个字节作为一个音符的信号。规定当该字节值为0x80时,代表的音调为1024Hz,可以理解为简谱中的音调是1024Hz,然后字节值每相差1,就代表相差一个半音。因此,0x7F代表Hz,即;0x81代表Hz,即

另外,把0x00作为休止符。这里规定,每一个音符的时长为半秒。

在此,贴上Arduino里的C++程序

cpp 复制代码
/*
Let's define the hex of the tone

define a do: 1024Hz
0x80: do3
0x81: #do3
0x82: re3
0x7F: si2

and each byte is for a note of 0.5s. So if you need do3 for 1s, then need 2 0x80

*/

const int buzzerPin = 7;
byte baseNote = 0x80;
int baseTone = 1024;
byte note;
void setup() {
  // put your setup code here, to run once:
  pinMode(buzzerPin, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:
  if ((Serial.available() > 0) && (note = Serial.read()) && (note != 0x00)){
    Serial.print('Sing:');
    Serial.print(note);
    Serial.print(' ');
    //Now convert it into frequency of the tone
    int toneFreq;
    if (baseNote > note){ //received note is under base note
      int noteDiff = baseNote - note;
      toneFreq = (int)((float)baseTone / (pow(2.0, noteDiff/12.0))); //between do and #do is 2^(1/12)
    }
    else{//received note is above base note
      int noteDiff = note - baseNote;
      toneFreq = (int)((float)baseTone * (pow(2.0, noteDiff/12.0)));
    }
    Serial.print(toneFreq);
    Serial.print(' ');
    tone(buzzerPin, toneFreq);
  }
  else{
    noTone(buzzerPin);
    Serial.print('0');
  }
  delay(500);
  
}

程序中,tone(buzzerPin, toneFreq)表式在buzzerPin数字输出中产生频率为toneFreqHz的震荡;而noTone(buzzerPin)表示让数字输出停止震荡。

因此,程序通过串口接收音符字节信息,然后根据该信息激活数字输出,让无源蜂鸣器发出音乐。

另外,在树莓派电脑中,运行一个python程序,通过读取指定格式的乐谱,产生音符数据,并将其通过串口输入到Arduino中。这里,先对乐谱的格式做一个规定。

这里,令乐谱为一个txt文件,该txt文件里每一个音符用一个1-3个字符的字符串组成,而音符和音符之间用空格分开。对于每一个音符,第一个字符是1-7之间的数字,意义和简谱的数字部分一样;第二的字符表明该音符在哪一个八度上,标准的八度号为3,所以如果没有该字符就默认为3处理;第三个字符可以不存在,也可以是+或-,表明是升调还是降调。所以,假定乐谱文件如下:

cpp 复制代码
13 33 23 52 0 43+ 63-

则对应的简谱是

所以,把乐谱txt文件转换为Arduino读取的音乐数据的python程序如下

python 复制代码
"""
This file gives the function to get music from file
base 0x80 is do3(13)
so do2 is 12 semitones lower than do3, do3+ is 1 semitone higher than do3, do3- is 1 semitone lower than do3
"""

def loadMusicFile(filename):
	with open(filename,'r') as file:
		content = file.read()
		return content.split(' ')
		
def convertFromMusicToArduino(filename):
	notes = loadMusicFile(filename)
	print(notes)
	musicData = []
	for n in notes:
		if len(n) == 0:
			continue;
		dataThis = 0x80
		if n[0] == '1':
			dataThis += 0
		elif n[0] == '2':
			dataThis += 2
		elif n[0] == '3':
			dataThis += 4
		elif n[0] == '4':
			dataThis += 5
		elif n[0] == '5':
			dataThis += 7
		elif n[0] == '6':
			dataThis += 9
		elif n[0] == '7':
			dataThis += 11
		else:
			dataThis = 0x00
		if len(n) > 1 and dataThis != 0x00:
			dataThis += (int(n[1]) - 3) * 0x0c
		if len(n) > 2 and dataThis != 0x00:
			if n[2] == '+':
				dataThis += 0x01
			elif n[2] == '-':
				dataThis -= 0x01
		musicData.append(dataThis)
	return musicData

注意0x0c表明十进制里的12。

然后,运行以下python程序,把音频文件转成Arduino支持的格式后,通过串口传入Arduino。python的串口连接的方式见[3]

python 复制代码
import serial
import os
from time import sleep
from getmusicFromFile import convertFromMusicToArduino

serialportName = '/dev/ttyACM1'
bps = 9600

ser = serial.Serial(serialportName, int(bps), timeout=0.5, parity=serial.PARITY_NONE, stopbits=1)

data = convertFromMusicToArduino('song.txt')

data_bytes = bytes(data)
print(data_bytes)

if (ser.isOpen()):
	print("Serial opened")
	sleep(1.8)
	ser.write(data_bytes)
	sleep(10)
	print(ser.read(30))
	ser.close()
print("done")

注意,在ser.isOpen()后,在传输数据前有一个sleep(1.8)。这个等待,是要让串口准备好后再传输数据,否则会传输失败,Arduino有可能未能收到数据。

三、实验效果

该实验,用歌曲"友谊地久天长"的第一小段作为乐谱,文件song.txt如下

diff 复制代码
52 52 13 13 0 13 13 0 33 33 23 23 23 13 23 0 33 33 13 13 0 13 33 33 53 53 63 63 63 0 0 0 63 63 53 53 53 33 33 33 13 13 23 23 23 13 23 23 33 23 13 13 13 62 62 62 52 52 13 13 13

运行效果见视频,播放的正是"友谊地久天长"的第一段。注意当我转动可调电阻时音量的变化。

友谊地久天长

参考资料

[1]有源蜂鸣器与无源蜂鸣器的驱动方式详解(精华版)_有源蜂鸣器和无源蜂鸣器的电路图-CSDN博客

[2]八度音阶和频率的关系_音阶与频率的关系-CSDN博客

[3]​​​​​​​用 Python 玩转串口(基于 pySerial)_python打开串口-CSDN博客

相关推荐
Light602 分钟前
低代码牵手 AI 接口:开启智能化开发新征程
人工智能·python·深度学习·低代码·链表·线性回归
墨绿色的摆渡人3 分钟前
用 Python 从零开始创建神经网络(六):优化(Optimization)介绍
人工智能·python·深度学习·神经网络
Winston Wood17 分钟前
一文了解Android中的AudioFlinger
android·音频
CoderBob17 分钟前
【EmbeddedGUI】脏矩阵设计说明
c语言·单片机
小han的日常32 分钟前
pycharm分支提交操作
python·pycharm
明月清风徐徐1 小时前
Scrapy爬取豆瓣电影Top250排行榜
python·selenium·scrapy
theLuckyLong1 小时前
SpringBoot后端解决跨域问题
spring boot·后端·python
ROC_bird..1 小时前
STL - vector的使用和模拟实现
开发语言·c++
机器视觉知识推荐、就业指导1 小时前
C++中的栈(Stack)和堆(Heap)
c++
Yongqiang Cheng1 小时前
Python operator.itemgetter(item) and operator.itemgetter(*items)
python·operator·itemgetter