Arduino学习(九):写⼀个Arduino扩展库:⾳乐播放库,并
樊胜美是谁实现跨平台
Arduino扩展库(Library)就是别⼈写好的,可重⽤的函数或类。
在之前的博⽂: 中,我们学习了使⽤⽆源蜂鸣器可以发出不同频率的声⾳,据此,Arduino可以⽤来播放⾳乐了。
本篇的⽬标:是写⼀个扩展库,实现以下功能:
1, 把任意曲谱写成⼀个字符串,⽐如,歌曲“⼩蜜蜂”的简谱是:“5 3 3  4 2 2 ”雨落长安
2, 扩展库可以读取曲谱字符串,播放⾳乐
3, 这个扩展库库要求能跨平台编译使⽤,能在Arduino, 51单⽚机,Windows中均可编译并使⽤。
本例中,将学习到C 和 C++混合编程,跨平台的模块设计等技巧
⼀、曲谱的表达⽅式
下⾯是歌曲“⼩蜜蜂”的简谱(节选)
⾸先,要设计⼀个字符串表达⽅式,⽤⼀串字符表达曲谱
简谱中:曲谱由多个⾳符组成,每个⾳符有⾳⾼、⾳长。
设计为:曲谱为⼀串字符串。 每个⾳符由表达⾳⾼的字符串 和 表达⾳长的字符串共同组成。
简谱中:⾳⾼⽤ 1,2,...7 表⽰,⾼⼋度的⾳在上⽅加⼀个点,低⼋度的⾳在下⽅加⼀个点.  另外 0  表⽰停顿(⽆⾳)
设计为: ⾳⾼⽤ 1,2,...7 表⽰,0  表⽰停顿(⽆⾳), ⾼⼋度的⾳接⼀个字符 ‘^’ , 低⼋度接⼀个字符 ‘v’ (⼩写v)  ⽐如: 5^ 表⽰⾼⼋度⾳的 5 (So),  5vv 表⽰低⼗六度的5(So)
对于升降半⾳,升半⾳在⾳符后加 #,  降半⾳在⾳符后加 b (⼩写b)
简谱中:不⾜⼀拍的⾳长由下划线表⽰,⼆分⾳符⼀个下划线,四分⾳符⼆个下划线。超过⼀拍的⾳长⽤ “-”表⽰,每个"-"为加⼀拍
设计为:不⾜⼀拍的⾳接⼀个或多个下划线符号,⽐如: 5_ 表⽰ 半拍的5(So),  5__表⽰ 四分⾳符的5(So)
超过⼀拍的⾳,接⼀个或多个“-”号,⽐如: 5- 表⽰⼆拍的5(So), 5--- 表⽰四拍的5
简谱中曲调表达形式为: 1=C,  意思为 1 是 C⾳, 即C⼤调或A⼩调.  可⽤的调式为: C,  D, E, F, G, A, B,  可以升降调,如:#G 表⽰升G⼤调,  bF表⽰降F⼤调
设计为:与简谱完全⼀致, 要求“1”后紧接⼀个“=”(等号),再加曲调字符
简谱中⾳乐速度表达形式为: 1=88, 意思是 每分钟88拍
设计为:与简谱完全⼀致, 要求“1”后紧接⼀个“=”(等号),再加数字
按照上述设计, 则上图中的“⼩蜜蜂”的简谱, ⽤⼀串字符串表达为:
1=C | 5_ 3_  3  |  4_  2_  2    | 1_ 2_ 3_ 4_  | 5_ 5_  5    |
为了容易看懂,我在其中增加了⼀些空格和 “|” 分隔符, 还是⽐较直观的吧,  可以⽅便⼿⼯写曲谱。
接下来,要编程,让计算机读取曲谱、放⾳出来。
⼆、⾳符与频率
1,⾳乐中规定了⼀个⼋度 有 C,D,  E, F, G, A, B 七个⾳符,在C⼤调中,唱名分别为:  1(do) 2(re) 3(me) 4(fa) 5(so) 6(la) 7(si)
E和F之间、B和C之间没有半⾳, 其它两个⾳之间均有半⾳。
因此,⼀个⼋度有⼗⼆个⾳,表达为 C , C#, D, D#, E, F, F#, G, G#, A, A#, B
2, 每个⾳对应⼀个频率,⽤⼀个C语⾔数组表达如下:
//数组:键盘与频率对应表
const int key_frequency[] = {
0,
//C, C+, D, D+, E, F, F+, G, G+, A, A+, B, B+
65, 69, 73, 78, 82, 87, 92, 98, 103, 110, 116, 123,
131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247,
262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988,
1046, 1109, 1175, 1244, 1318, 1397, 1480, 1568, 1661, 1760, 1865, 1926,
2089, 2160, 2288, 2422, 2565, 2716, 2877, 3047, 3226, 3417, 3618, 3832,
4058, 4297, 4551, 4819, 5104, 5405, 5724, 6061, 6419, 6798, 7166, 7625
};
#define CENTER_C      37  //中央C在数组key_frequency中的位置
数组中每个元素是⼀个频率值,第0元素是预留的, 第1-12元素是⼀个⼋度(⼗⼆个⾳), 第13-24元素是⼀个⼋度..., 这个数组涵盖了钢琴键盘所有的⾳
其中 第37元素 是中央C,频率值为523 Hz,即钢琴键盘最中央的C键,就是C⼤调的do.  宏定义为 CENTER_C
三、跨平台的考虑
1,编程语⾔的选择:
跨平台编程⼀般只能采⽤C。 由于要Arduino, 51单⽚机,Windows三个平台,51单⽚机只⽀持C
Arduino, Windows⽀持C++, 可以写⼀个C++类,封装C语⾔函数,⽅便调⽤。
⼀套源码,在不同的平台上均可编译执⾏,即算是实现了跨平台。
2, 不同平台的放⾳机制不同
Arduino中,可以使⽤ tone()函数播放指定频率的⾳。
51单⽚机中,要⾃⼰写⼀个中断程序,产⽣脉冲信号,驱动⽆源蜂鸣器发⾳。
Windows中,可以使⽤ Win32 API中的MIDI相关函数,放出指定的⾳符
因此,要写⼀个放⾳函数  play_tone( tone ),是与平台相关的,每个平台都要分别实现这个函数
同样的,与平台相关的函数还有:初始化设备、关闭设备、时间等待、等等。下例中,我把平台相关的函数均放在独⽴模块(⽂件)中。
四、Arduino程序开发
我们在Arduino IDE中进⾏开发
1, 打开Arduino IDE, 新建⼀个项⽬,存盘为 MusicPlayer, 然后直接关闭项⽬。
2,  在电脑中到 Arduino的项⽬存盘⽬录, 打开其中的⼦⽬录MusicPlayer, 在该⽬录下⼿⼯创建三个空⽂件, 名为 music.h, music.c, music_arduino.cpp
3,  关闭项⽬后,再⽤Arduino IDE 重新打开 MusicPlayer 项⽬,则此时可以看到, Arduino IDE将同时打开了新建的三个⽂件 music.h, music.c,music_arduino.cpp
4, 然后,选择 music.h ,  编写头⽂件如下:
#ifndef MUSIC_H_
#define MUSIC_H_
#ifdef __cplusplus
extern "C" {
#endif
//⾳乐数据结构体,记录各种状态
typedef struct MusicData {
char *str;  //曲谱字符串
int  len;  //曲谱字符串的长度
int  tune;  //曲调
int  speed; //⾳乐速度
int  index; //当前位置
int  key;  //当前⾳的键名
int  duration; //当前⾳的⾳长
岁月神偷主题曲int  frequency; //当前⾳的频率
int  pin;      //连接蜂鸣器的管脚
} MusicData;
/**
* 打开⾳乐设备
*/
int music_open(MusicData *data, int pin) ;
/**
* 播放⾳乐
*/
int music_play(MusicData *data, const char *music_str);
/**
*关闭⾳乐设备
*/
void music_close(MusicData *data);
#ifdef __cplusplus
}
#endif
#endif /* MUSIC_H_ */
这⼀个C语⾔头⽂件, 其中:
1, 定义了⼀个结构体 MusicData, ⽤于记录⾳乐状态:曲谱、曲调、速度、当前⾳符、⾳长
2, 定义了三个函数: music_open()打开(初始化)⾳乐设备,  music_close()关闭⾳乐设备,    music_play()⽤于放⾳,注意:为了在C++编译器中使⽤C函数,⼀定要写成这样
#ifdef __cplusplus
extern "C" {
#endif
... C函数声明 ...
#ifdef __cplusplus
}
#endif
5, 然后,编写 music_arduino.cpp 模块
这个模块是编写与Arduino相关的函数: music_open(), music_close(),wait()等待,play_tone()放⾳, stop_tone()停⽌放⾳等五个与平台相关的函数
因为Arduino是C++的,所以这个模块要采⽤C++来写, ⽂件扩展名为.cpp
#include "music.h"
//如果是在Arduino平台中
#ifdef ARDUINO
#include "Arduino.h" //Arduino的头⽂件
//在Arduino中打开⾳乐设备
extern "C" int music_open(MusicData *data, int pin) {
data->pin = pin;
pinMode(pin, OUTPUT);
tokyo dogsreturn 0;
}
//在Arduino中关闭⾳乐设备
extern "C" void music_close(MusicData *data) {
姚明明道歉}
/
/在Arduino中停⽌播放⼀个⾳
extern "C" void stop_tone(MusicData *data) {
noTone(data->pin);
}
//在Arduino中播放⼀个⾳
extern "C" void play_tone(MusicData *data) {
白裂痕if (data->key > 0)
tone(data->pin, data->frequency);
else
noTone(data->pin);
}
/
/等待⼀段时间, 时间单位毫秒
extern "C" void wait(int milliSeconds) {
delay(milliSeconds);
}
#endif
其中:
#ifdef ARDUINO  表⽰在Arduino开发环境中。 ARDUINO 这个宏是 Arduino IDE的预定义宏。如果不在Arduino环境中编译,这个模块相当于空代码
本模块中的五个函数均与平台相关,每个平台均要实现这五个函数,并独⽴放在⼀个模块⽂件中,这样可以⽅便维护和扩展平台。
music_arduino.cpp 模块是C++的,由于需要被C语⾔调⽤,因此,所有的函数均要加上了 extern "C" 的声明。
6, 然后,编写 music.c 模块
这个模块编写与平台⽆关的所有C语⾔函数。