查看: 965|回复: 1

[入门] Arduino学习(九): 写一个Arduino扩展库

[复制链接]
在之前的博文: Arduino学习(五) 蜂鸣器实验 中,我们学习了使用无源蜂鸣器可以发出不同频率的声音,据此,Arduino可以用来播放音乐了。



本篇的目标:是写一个扩展库,实现以下功能:

1, 把任意曲谱写成一个字符串,比如,歌曲“小蜜蜂”的简谱是:“5 3 3  4 2 2 ”

2, 扩展库可以读取曲谱字符串,播放音乐

3, 这个扩展库库要求能跨平台编译使用,能在Arduino, 51单片机,Windows中均可编译并使用。



本例中,将学习到C 和 C++混合编程,跨平台的模块设计等技巧



一、曲谱的表达方式

下面是歌曲“小蜜蜂”的简谱(节选)

截图201902201726018013.png

首先,要设计一个字符串表达方式,用一串字符表达曲谱

简谱中:曲谱由多个音符组成,每个音符有音高、音长。

设计为:曲谱为一串字符串。 每个音符由表达音高的字符串 和 表达音长的字符串共同组成。

简谱中:音高用 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语言数组表达如下:

[AppleScript] 纯文本查看 复制代码
//数组:键盘与频率对应表 

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 ,  编写头文件如下:
[AppleScript] 纯文本查看 复制代码
#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函数,一定要写成这样

[AppleScript] 纯文本查看 复制代码
#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

[AppleScript] 纯文本查看 复制代码
#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语言函数。
[AppleScript] 纯文本查看 复制代码
#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 主程序

[AppleScript] 纯文本查看 复制代码
#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类

[AppleScript] 纯文本查看 复制代码
//写一个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 主程序可以改成这样:

[AppleScript] 纯文本查看 复制代码
#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.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 代码如下:
[AppleScript] 纯文本查看 复制代码
#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


与music_arduino.cpp一样, music_windows.c 模块实现了music_open(), music_close(),wait()等待,play_tone()放音, stop_tone()停止放音等五个与平台相关的函数
略有不同的是,music_arduino.cpp是C++编程, music_windows.c是C编程

写一个Windows 测试程序如下:

[AppleScript] 纯文本查看 复制代码
#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写扩展库

经过一定的设计,模块是可以跨平台重用的。
---------------------
作者:JoStudio
来源:CSDN
原文:https://blog.csdn.net/c80486/article/details/52964993

gada888  版主

发表于 2019-2-21 18:47:54

收藏了。好贴
回复

使用道具 举报

高级模式
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2020 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail