4607| 1
|
[入门] Arduino学习(九): 写一个Arduino扩展库 |
在之前的博文: 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语言数组表达如下: [mw_shl_code=applescript,true]//数组:键盘与频率对应表 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中的位置 [/mw_shl_code] 数组中每个元素是一个频率值,第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 , 编写头文件如下: [mw_shl_code=applescript,true]#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_ */[/mw_shl_code] 这一个C语言头文件, 其中: 1, 定义了一个结构体 MusicData, 用于记录音乐状态:曲谱、曲调、速度、当前音符、音长 2, 定义了三个函数: music_open()打开(初始化)音乐设备, music_close()关闭音乐设备, music_play()用于放音, 注意:为了在C++编译器中使用C函数,一定要写成这样 [mw_shl_code=applescript,true]#ifdef __cplusplus extern "C" { #endif ... C函数声明 ... #ifdef __cplusplus } #endif[/mw_shl_code] 5, 然后,编写 music_arduino.cpp 模块 这个模块是编写与Arduino相关的函数: music_open(), music_close(),wait()等待,play_tone()放音, stop_tone()停止放音等五个与平台相关的函数 因为Arduino是C++的,所以这个模块要采用C++来写, 文件扩展名为.cpp [mw_shl_code=applescript,true]#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 [/mw_shl_code] 其中: #ifdef ARDUINO 表示在Arduino开发环境中。 ARDUINO 这个宏是 Arduino IDE的预定义宏。如果不在Arduino环境中编译,这个模块相当于空代码 本模块中的五个函数均与平台相关,每个平台均要实现这五个函数,并独立放在一个模块文件中,这样可以方便维护和扩展平台。 music_arduino.cpp 模块是C++的,由于需要被C语言调用,因此,所有的函数均要加上了 extern "C" 的声明。 6, 然后,编写 music.c 模块 这个模块编写与平台无关的所有C语言函数。 [mw_shl_code=applescript,true]#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; } [/mw_shl_code] 其中: 编写 read_tone() 函数,这个函数解析字符串,扫描字符,读出一个音符,稍微有点复杂 7, 然后,编写 MusicPlayer Arduino 主程序 [mw_shl_code=applescript,true]#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 ); }[/mw_shl_code] 运行,放出音乐了 8,写一个C++类,封装C语言函数 在music.h 最后, 在#endif 前,添加以下代码,创建一个Music类 [mw_shl_code=applescript,true]//写一个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 [/mw_shl_code] 有了C++类,则MusicPlayer Arduino 主程序可以改成这样: [mw_shl_code=applescript,true]#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 ); } [/mw_shl_code] 调用是不是更简明一点 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.h> 这时,你就可以使用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 代码如下: [mw_shl_code=applescript,true]#include "music.h" //如果是在Windows平台中 #if defined(__CYGWIN32__) || defined(WIN32)|| defined(_WIN32) #include <windows.h> //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[/mw_shl_code] 与music_arduino.cpp一样, music_windows.c 模块实现了music_open(), music_close(),wait()等待,play_tone()放音, stop_tone()停止放音等五个与平台相关的函数 略有不同的是,music_arduino.cpp是C++编程, music_windows.c是C编程 写一个Windows 测试程序如下: [mw_shl_code=applescript,true]#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);//关闭音乐设备 }[/mw_shl_code] 其实,程序与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写扩展库 经过一定的设计,模块是可以跨平台重用的。 --------------------- 作者:JoStudio 来源:CSDN 原文:https://blog.csdn.net/c80486/article/details/52964993 |
© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed