Arduino扩展库(Library)就是别人写好的,可重用的函数或类。
在之前的博文: Arduino学习(五) 蜂鸣器实验 中,我们学习了使用无源蜂鸣器可以发出不同频率的声音,据此,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元素是一个八度..., 这个数组涵盖了钢琴键盘所有的音
三、跨平台的考虑
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_ */
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);
return 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语言函数。
#include "music.h"
//以下五个函数与平台相关,是在其它模块文件中实现的
//打开音乐设备
extern int music_open(MusicData *data, int pin);
//关闭音乐设备
extern void music_close(MusicData *data);
//播放一个音
extern void play_tone(MusicData *data);
//停止播放一个音
extern void stop_tone(MusicData *data);
//等待一段时间, 时间单位毫秒
extern void wait(int milliSeconds);
//数组:键盘与频率对应表
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中的位置
#define IS_TUNE(c) ( (c >= 'A' && c <='G') || (c == 'b') || (c=='#') )
#define IS_NUMBER(c) (c >= '0' && c <='9')
//处理曲谱中 1=XX 的文字
static void process_tune_string(MusicData *data) {
char c;
//调用本函数时,当前位置data->index应是一个 "=" 号
if (data->str[data->index] != '=') return;
c = data->str[++data->index]; //取'='号后的第一个字符
if ( IS_TUNE(c) ) { //如果是曲调字符
//分析后续的曲调字符
do {
if (c >='A' && c <= 'G') {
data->tune = (c - 'C') * 2;
if (c >= 'F') data->tune--; //E和F之间是半音,故E之后均要减1
if (c <= 'B') data->tune++; //B和C之间是半音,故B之前均要加1
} else if (c == '#')
data->tune++;
else if (c == 'b')
data->tune--;
c = data->str[++data->index]; //取下一个字符
} while( IS_TUNE(c) );
} else if ( IS_NUMBER(c) ) { //如果是数字字符,则表示速度
//分析后续的速度字符
data->speed = 0;
do {
data->speed = data->speed * 10 + (c - '0');
c = data->str[++data->index]; //取下一个字符
} while( IS_NUMBER(c) );
if (data->speed <= 0) data->speed = 70; //如果速度未定义,则设为默认速率
}
}
//读取下一个音符,成功返回1,失败返回0
static int read_tone(MusicData *data) {
char c, found;
if ( !data->str ) return 0;
found = 0; //当前音有否找到
data->key = -1; //设当前音的键名为-1,即不存在
data->duration = 128;//设默认音长为一拍(用128表示)
//逐个扫描字符, 直到找到一个音,或到达字符串尾
while (data->index < data->len && found == 0) {
c = data->str[data->index]; //取当前字符
data->index++; //读取位置向前一个字符
//根据当前字符,进行相应处理
switch(c) {
case '1':
//如果碰到'1=XX', 则是曲调或速度定义
if (data->index < data->len-1 && data->str[data->index] == '=' ) {
process_tune_string(data);//处理曲谱中 1=XX 的文字
continue;
}
case '0':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
if ( data->key >= 0 ) { //当前有一个音
found = 1; //此时碰到下一个音,则表示当前音已读完
data->index--; //回退一个字符
} else {
if (c == '0') {
data->key = 0;
} else {
data->key = (c - '1') * 2 + CENTER_C + data->tune; //设置当前音
if (c >= '4') data->key--; //3和4之间是半音,故4之后的音均要减1
}
}
break;
case '^':
if (data->key > 0) data->key += 12; //音高升八度,12个半音
break;
case 'v':
if (data->key > 0) data->key -= 12; //音高降八度,12个半音
break;
case 'b':
data->key++; //升半音
break;
case '#':
data->key--; //降半音
break;
case '_':
data->duration /= 2; //音长减半
break;
case '-':
data->duration += 128; //音长增加一拍
break;
default:
continue;
}
}
if (data->key >= 0)
return 1;
else
return 0;
}
/**
* 播放音乐
*/
int music_play(MusicData *data, const char *music_str) {
//初始化数据
data->str = music_str;//指向曲谱字符串
data->len = strlen(music_str); //曲谱字符串长度
data->tune = 0; //0为C大调
data->speed = 70; //默认每分钟70拍
data->index = 0; //读取位置:从字符串头部开始
data->key = -1; //当前音符的键名:-1表示当前键名未定义
data->duration = 128; //为少用浮点数,以128表示一拍,64表示0.5拍...
//读一个音,放一个音
while( read_tone(data) == 1) {
data->frequency = key_frequency[data->key]; //得到当前音的频率
play_tone(data); //播放一个音
//等待一段时间:根据duration、音乐速度计算出毫秒数
wait( 60000 / data->speed * data->duration / 128);
stop_tone(data); //停止一个音
data->key = -1;
}
return 0;
}
其中:
编写 read_tone() 函数,这个函数解析字符串,扫描字符,读出一个音符,稍微有点复杂
7, 然后,编写 MusicPlayer Arduino 主程序
#include "music.h" //音乐库头文件
MusicData data; //声明一个音乐数据结构体变量
//曲谱
const char * music_str = "1=C 5_ 3_ 3 | 4_ 2_ 2 | 1_ 2_ 3_ 4_ | 5_ 5_ 5 |";
int pin = 3; //管脚D3连接到无源蜂鸣器
void setup() {
music_open( &data, pin); //打开音乐设备
}
void loop() {
music_play( &data, music_str); //播放音乐
delay( 5000 );
}
运行,放出音乐了
8,写一个C++类,封装C语言函数
在music.h 最后, 在#endif 前,添加以下代码,创建一个Music类
//写一个Music类 ,封装C语言函数
#ifdef __cplusplus
class Music {
private:
MusicData data;
public:
Music(int pin) { music_open(&data, pin); };
~Music() { music_close(&data); };
int play(char *music_str) { return music_play(&data, music_str); };
};
#endif
有了C++类,则MusicPlayer Arduino 主程序可以改成这样:
#include "music.h" //音乐库头文件
int pin = 3; //管脚D3连接到无源蜂鸣器
Music music(pin); //定义一个Music对象, 并初始化
//曲谱
const char * music_str = "1=C 5_ 3_ 3 | 4_ 2_ 2 | 1_ 2_ 3_ 4_ | 5_ 5_ 5 |";
void setup() {
}
void loop() {
music.play(music_str); //播放音乐
delay( 5000 );
}
9,做成Arduino扩展库
(1) 创建库
在Arduino安装目录下有一个 libraries 子目录,这个目录是扩展库目录。
打开 libraries 目录, 新建一个名为 Music的子目录,将 music.c, music.h, music_arduino.cpp 三个文件移动到该目录中。
现在,重启 Arduino IDE. 点菜单“项目”--“Include Library”,你应该看到有菜单尾部增加了一项“Music”,这表明,Music库已创建了。
(2)使用 Music库
创建一个新的Arduino项目,点菜单“项目”--“Include Library”--"Music",
则Arduino IDE将在当前项目程序头部增加一句 : #include
这时,你就可以使用Music类了。范例程序见上。
(3)创建examples
在Arduino扩展库Music目录下,创建名为 examples的子目录。
在examples目录下,创建一个名为 MusicExample的子目录。
在MusicExample目录下,创建一个名为 MusicExample.ino 的文件,文件内容填入上述的范例程序。
重启Arduino, 在点菜单“文件”--“示例”,你应该看到有菜单尾部增加了一项“Music”-“MusicExample", 点这个菜单项,将立即生成一个范例程序
五、Windows程序开发
music.c, music.h 是纯C函数,可以跨平台。只需要写一个 music_window.c, 写出与平台相关的几个函数即可。
在Windows中,我们用Win32 MIDI API放音,用C语言编程,music_windows.c 代码如下:
#include "music.h"
//如果是在Windows平台中
#if defined(__CYGWIN32__) || defined(WIN32)|| defined(_WIN32)
#include //windows的头文件
//在Windows中,使用MIDI API放音
//See also: http://www.giordanobenicchi.it/midi-tech/lowmidi.htm
static HMIDIOUT midi_handle = 0; //全局变量:MIDI设备句柄
//在Windows中打开音乐设备
int music_open(MusicData *data, int pin) {
unsigned long result=0;
if ( midiOutGetNumDevs() > 0 ) {//查看有否MIDI设备
result = midiOutOpen(&midi_handle, MIDI_MAPPER, 0, 0, CALLBACK_NULL);//打开MIDI
if (result != MMSYSERR_NOERROR) midi_handle = 0;
return result == 0;
}
return 0;
}
//在Windows中关闭音乐设备
void music_close(MusicData *data) {
if (midi_handle)
midiOutClose(midi_handle);
midi_handle = 0;
}
#define MUSIC_CENTER_C 37 //在Music库中,中央C的键值是37
#define MIDI_CENTER_C 0x3C //在MIDI中,中央C的键值是0x3C
//在Windows中播放一个音
void play_tone(MusicData *data) {
int key;
int volume; //音量值,取值范围为0-100
if (midi_handle == 0 ) return;
//将Music键值换算为MIDI的键值
key = data->key - MUSIC_CENTER_C + MIDI_CENTER_C;
volume = 60; //默认音量值
//使用midiOutShortMsg()放音
midiOutShortMsg(midi_handle, (volume << 16) + (key << 8) + 0x90 );
}
//在Windows中停止播放一个音
void stop_tone(MusicData *data) {
int key;
key = data->key - MUSIC_CENTER_C + MIDI_CENTER_C; //将Music键值换算为MIDI的键值
midiOutShortMsg(midi_handle, (key << 8) + 0x90);
}
//等待一段时间, 时间单位毫秒
void wait(int milliSeconds) {
Sleep(milliSeconds);
}
#endif
略有不同的是,music_arduino.cpp是C++编程, music_windows.c是C编程
写一个Windows 测试程序如下:
#include "music.h"
int main() {
//曲谱
const char * music_str = "1=C 1=120 5_ 3_ 3 | 4_ 2_ 2 | 1_ 2_ 3_ 4_ | 5_ 5_ 5 |";
MusicData data;
music_open(&data, 0);//打开音乐设备, pin取值为0即可
music_play(&data, music_str);//播放音乐
music_close(&data);//关闭音乐设备
}
其实,程序与Arduino程序基本是一样的。
我用VC, MingW均进行过编译,OK。 编译时注意要链接winmm库(内含MIDI API)
运行效果良好, 毕竟通过声卡MIDI 放出来的音,音效不错。
六、51单片机程序开发
只需要写一个 music_51.c, 实现了music_open(), music_close(),wait()等待,play_tone()放音, stop_tone()停止放音等五个函数
51没有tone()函数,必须采用中断,自己写一个频率函数,驱动蜂鸣器。
相关代码,随后再提供。。。
七、小结
用C / C++ 为Arduino写扩展库
经过一定的设计,模块是可以跨平台重用的。