明珠的个人博客

是谁告诉你,你是赤裸的?

0%

单片机模拟钢琴播放音乐

简介

使用单片机的蜂鸣器模拟钢琴声进而播放出完整乐曲,2点要素:1.单片机使用无源蜂鸣器2.如何识别乐谱,并转化为单片机可识别的信号,无源蜂鸣器可以实现不同频率的音调,而音乐,最重要的是搞懂音调节拍,音调我们知道就是不同频率的高低→蜂鸣器改变频率,节拍就是时长→单片机的定时器可以实现毫秒级定时。万事俱备,开工。

硬件条件

蜂鸣器是一种将电信号转化为声音信号的器件,常用来产生设备的按键音、报警音等提示音。
按照驱动方式可以分为有源蜂鸣器和无源蜂鸣器。
有源蜂鸣器:内部自带震荡源,将正负极接上直流电压即可持续发生,频率固定;
无源蜂鸣器:内部不带震荡源,需要控制器提供震荡脉冲才可发声,调整提供震荡脉冲的频率,可发出不同频率的声音。
我们想要控制频率来实现歌曲的音调,所以选用的是无源蜂鸣器。

由于蜂鸣器的工作电流一般比较大,以致于单片机的I/O 口是无法直接驱动的*(但AVR可以驱动小功率蜂鸣器),所以要利用放大电路来驱动,一般使用三极管来放大电流就可以了。

上图是普中51单片机开发板的原理图,J7连接的是芯片的一个I/O口。可以看到,当 J7 端子有一个高电平进来时,PNP 三极管TP1截止,蜂鸣器(BZ1)不得电,当 J7 端子有一个低电平进来时,PNP 三极管TP1 导通,蜂鸣器得电,如果 J7 端子有一个一定频率的脉冲信号(高低电平不断翻转)时,这个无源蜂鸣器发出声音。
硬件基础完毕,接下来是软件编程。

软件条件

想要编程,就得知道音乐是怎么产生的。
首先,我们拿到一张谱子,然后看谱,当我们识别完乐谱后,按照乐谱用手操纵乐器从头到尾按顺序演奏完毕。
所以首先识谱,分辨出音调和节拍。
不过音乐的基本知识就不为难自己了,我是个音乐白痴。。所以我就这么理解了…
隐约记得音乐课上似乎讲过do、re、mi、fa、sol、la、si、do…
OK,百度了下,钢琴键盘与五线谱、简谱音高对照表,本次使用简谱。

钢琴按键音调

从上图看到,最上面一排是钢琴键盘,左边的音名那里,我们看到钢琴键盘上是CDEFGABC,然后小写字母再循环一遍,然后小写字母加上标再循环一遍,分别叫做大字组,小字组,小字一组,小字二组,以此类推,每组分别是7个白键+5个黑键。其中,大写字母C和小写字母c、c1、c2等相差8度,而钢琴的黑键和白键相差半音,乐谱符号“#”代表升音,“b”代表降音。

引用百度的回答:在音乐中,一个音就叫一度,音阶1234567中,1到2就是两度(有大二度,小二度之分),1到3就是三度(大三度、小三度),。。。。1到7就是七度,而从中音1到高音1就是一个八度。 那么我们就可以说高音1比中音1高一个八度。这就是“一个音比另一个音高八度”的意思。

从上图中可以看出,其实就是7个音调,然后按照音量高低被命名为不同组(大字组、小字组…),加上黑白键相差的高/低半音,一组可以有7+5=12个音调,这12个音调有个人工规律,叫十二平均律,音调对应频率的关系为等比数列,推导如下:

条件1:相隔纯八度的两个音,它们的频率比值是1比2,不是其他的比值
条件2:十二平均律的原理就是将相隔纯八度的两音之间,划分为十二份,每相邻的两个音,其比值要相同,才能称之为平均,注意,是比值,而不是差值
要满足条件1与2,则这个比值只能是2开12次方

我们知道,要做比较必定有基准。国际通用的标准音定义a1=440HZ,以C调为例,其音调和频率关系如下:

根据公式T(周期)=1/f(频率),频率我们已经确定了,就可以使用单片机的定时器做出不同频率的音调了,接下来就是节拍(时长)的实现了。

节拍

乐谱的左上方有写“1=C 4/4”,其中“1=C”意思就是乐谱是C调,乐谱里面的1234567(do、re、mi、fa、sol、la、si)相对应的不是ABCDEFG而是CDEFGAB!而如果这里规定是F调的话,那么就说明1唱F,2就要唱G,3要唱A,……7要唱E,就是所谓的要相应的左移或者右移,以此类推。

“4/4”叫作拍号,图中从下往上读,意思是以四分音符为一拍,每小节四拍。每拍的时间应该多长,在音乐上一般用BPM(beats per minute,每分钟多少拍)来标识。演奏时其实不用真的去追求严格意义上的拍长,感觉对了就行了。就像炒菜一样,淡一点就是少放点盐,不用真的去称出精准的 1.34 克来~

在五线谱中,会注明每分钟的拍数,

OK,现在已经有了一拍的时间长度了,不过肯定是不够用的,如果想表示半拍,或者1/4拍,或者 2 拍、4拍,那该怎么办?

在给出答案前,我们得先学习一个新的东西:音符时值。

音符时值是个相对时长的单位,它的参考系是拍长,我们先来了解一下各种音符时值的表示符号:

  • 全音符 = 2 * 二分音符 :一个空心圆
  • 二分音符 = 2 * 四份音符 :空心圆带一条杠
  • 四分音符 = 2 * 八分音符号 :实心圆带一条杠
  • 八分音符 = 2 * 十六分音符 : 符尾一条线
  • 十六分音符 = 2 * 三十二分音符:符尾两条线

以上都是二分关系,那如果是 1.5 倍关系怎么搞?音乐家们使用附点音符的概念来解决这个问题。

这个点表示增加原来时长的一半,而减少时长的线是加在音符下面的,叫减时线,每增加一条减时线,时值减少二分之一。

这样音乐的时长问题就解决了,从上面再看一遍,时长是2倍关系,和二进制一样,太好了,大道至简,殊途同归。

程序

基本思想是把12个音调的频率做成一张表,按照简谱在程序中调用对应频率,并乘以相应音符时值,这样一首曲子就出来了。
下面是来自B站江协科技的程序,可以作为固定模板使用,只需要更改SPEEDunsigned char code Music[]部分即可,SPEED就是节拍,另一个是我们从简谱上得出的音符和时值,程序其它部分不用改动。嗯,音乐完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#include <REGX52.H>
#include "Delay.h"
#include "Timer0.h"

//蜂鸣器端口定义
sbit Buzzer=P1^5;

//播放速度,值为四分音符的时长(ms)
#define SPEED 500

//音符与索引对应表,P:休止符,L:低音,M:中音,H:高音,下划线:升半音符号#
#define P 0
#define L1 1
#define L1_ 2
#define L2 3
#define L2_ 4
#define L3 5
#define L4 6
#define L4_ 7
#define L5 8
#define L5_ 9
#define L6 10
#define L6_ 11
#define L7 12
#define M1 13
#define M1_ 14
#define M2 15
#define M2_ 16
#define M3 17
#define M4 18
#define M4_ 19
#define M5 20
#define M5_ 21
#define M6 22
#define M6_ 23
#define M7 24
#define H1 25
#define H1_ 26
#define H2 27
#define H2_ 28
#define H3 29
#define H4 30
#define H4_ 31
#define H5 32
#define H5_ 33
#define H6 34
#define H6_ 35
#define H7 36

//索引与频率对照表
unsigned int FreqTable[]={
0,
63628,63731,63835,63928,64021,64103,64185,64260,64331,64400,64463,64528,
64580,64633,64684,64732,64777,64820,64860,64898,64934,64968,65000,65030,
65058,65085,65110,65134,65157,65178,65198,65217,65235,65252,65268,65283,
};

//乐谱
unsigned char code Music[]={
//音符,时值,

//1
P, 4,
P, 4,
P, 4,
M6, 2,
M7, 2,

H1, 4+2,
M7, 2,
H1, 4,
H3, 4,

M7, 4+4+4,
M3, 2,
M3, 2,

//2
M6, 4+2,
M5, 2,
M6, 4,
H1, 4,

M5, 4+4+4,
M3, 4,

M4, 4+2,
M3, 2,
M4, 4,
H1, 4,

//3
M3, 4+4,
P, 2,
H1, 2,
H1, 2,
H1, 2,

M7, 4+2,
M4_,2,
M4_,4,
M7, 4,

M7, 8,
P, 4,
M6, 2,
M7, 2,

//4
H1, 4+2,
M7, 2,
H1, 4,
H3, 4,

M7, 4+4+4,
M3, 2,
M3, 2,

M6, 4+2,
M5, 2,
M6, 4,
H1, 4,

//5
M5, 4+4+4,
M2, 2,
M3, 2,

M4, 4,
H1, 2,
M7, 2+2,
H1, 2+4,

H2, 2,
H2, 2,
H3, 2,
H1, 2+4+4,

//6
H1, 2,
M7, 2,
M6, 2,
M6, 2,
M7, 4,
M5_,4,


M6, 4+4+4,
H1, 2,
H2, 2,

H3, 4+2,
H2, 2,
H3, 4,
H5, 4,

//7
H2, 4+4+4,
M5, 2,
M5, 2,

H1, 4+2,
M7, 2,
H1, 4,
H3, 4,

H3, 4+4+4+4,

//8
M6, 2,
M7, 2,
H1, 4,
M7, 4,
H2, 2,
H2, 2,

H1, 4+2,
M5, 2+4+4,

H4, 4,
H3, 4,
H3, 4,
H1, 4,

//9
H3, 4+4+4,
H3, 4,

H6, 4+4,
H5, 4,
H5, 4,

H3, 2,
H2, 2,
H1, 4+4,
P, 2,
H1, 2,

//10
H2, 4,
H1, 2,
H2, 2,
H2, 4,
H5, 4,

H3, 4+4+4,
H3, 4,

H6, 4+4,
H5, 4+4,

//11
H3, 2,
H2, 2,
H1, 4+4,
P, 2,
H1, 2,

H2, 4,
H1, 2,
H2, 2+4,
M7, 4,

M6, 4+4+4,
P, 4,

0xFF //终止标志
};

unsigned char FreqSelect,MusicSelect;

void main()
{
Timer0Init();
while(1)
{
if(Music[MusicSelect]!=0xFF) //如果不是停止标志位
{
FreqSelect=Music[MusicSelect]; //选择音符对应的频率
MusicSelect++;
Delay(SPEED/4*Music[MusicSelect]); //选择音符对应的时值
MusicSelect++;
TR0=0;
Delay(5); //音符间短暂停顿
TR0=1;
}
else //如果是停止标志位
{
TR0=0;
while(1);
}
}
}

void Timer0_Routine() interrupt 1
{
if(FreqTable[FreqSelect]) //如果不是休止符
{
/*取对应频率值的重装载值到定时器*/
TL0 = FreqTable[FreqSelect]%256; //设置定时初值
TH0 = FreqTable[FreqSelect]/256; //设置定时初值
Buzzer=!Buzzer; //翻转蜂鸣器IO口
}
}