Arduino学习(九): 写一个Arduino扩展库:音乐播放库,并实现跨平台

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元素是一个八度..., 这个数组涵盖了钢琴键盘所有的音

其中 第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);
  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一样, music_windows.c 模块实现了music_open(), music_close(),wait()等待,play_tone()放音, stop_tone()停止放音等五个与平台相关的函数

略有不同的是,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写扩展库

经过一定的设计,模块是可以跨平台重用的。





你可能感兴趣的:(arduino,c)