小言_互联网的博客

HaaS EDU场景式应用学习 - 复古八音盒

244人阅读  评论(0)

HaaS EDU场景式应用整体介绍

 

1、实验介绍

Chiptune是不少80,90后的童年回忆,说Chiptune的名字应该很多人比较陌生,不过它有另外一个名字:8-bit。所谓的所谓的Chiptune也就是由老式家用电脑、录像游戏机和街机的芯片(也就是所谓的CHIP)发出的声音而写作的曲子。严格说来其实Chiptune不仅仅只有8bit,不过都是追求复古颗粒感的低比特率。

本实验中,我们也来实现一款复古“八音”盒。

2、涉及知识点

  • 乐谱编码
  • PWM与蜂鸣器

 

3、软硬件环境准备

3.1、硬件

开发用电脑一台

HAAS EDU K1 开发板一块 (开发板内置蜂鸣器)

USB2TypeC 数据线一根

3.2、软件

"复古八音盒"功能已经包含在edu_demo应用中,并且包含在发布版本中。

3.2.1、固件版本

固件版本:V1.0.0

3.2.2、代码路径


  
  1. git clone https: //gitee.com/alios-things/AliOS-Things.git -b dev_3.1.0_haas
  2. cd AliOS-Things /application/example /edu_demo/k1_apps/musicbox

3.2.3、编译

进入代码的顶层目录如AliOS-Things进行编译。直接编译application/example/目录下的edu_demo应用。

两种方法进行编译

命令行方式


  
  1. aos make distclean
  2. aos make edu_demo@haaseduk1 -c config
  3. aos make

AliOS Studio IDE方式

3.2.4、烧录

见开发环境章节

 

4、蜂鸣器

蜂鸣器是一种非常简单的发声器件,和播放播放使用的扬声器不同,蜂鸣器只能播放较为简单的频率。

从驱动原理上区分,蜂鸣器可以分为无源蜂鸣器和有源蜂鸣器。这里的“源”,指的就是有无驱动源。无源蜂鸣器,顾名思义,就是没有自己的内置驱动源。只有为音圈接入交变电流后,其内部的电磁铁与永磁铁相吸或相斥而推动振膜发声,而接入直流电后,只能持续推动振膜而无法产生声音,只能在接通或断开时产生声音。而有源驱动器相反,只要接入直流电,其内部的驱动源会以一个固定的频率驱动振膜,直接发声。

在本实验中,推荐大家使用无源蜂鸣器,因为它只由PWM驱动,声音会更清脆纯净。使用有源蜂鸣器时,也能实现类似的效果,不过由于叠加了有源蜂鸣器自己的震动频率,声音会略显嘈杂。

4.1、驱动电路

蜂鸣器的 1端 连接到VCC,2端 连接到三极管。这里的三极管由PWM0驱动,来决定蜂鸣器的 2端 是否和GND连通,进而引发一次振荡。通过不断翻转IO口,即可以驱动蜂鸣器发声。

 

4.2、驱动代码

为了实现IO口按特定频率翻转,我们可以使用PWM(脉冲宽度调制)功能。关于PWM的详细介绍可以参看z第三章资源PWM部分。

在本实验中,我们实现了tone和noTone两个方法。其中,tone方法用于驱动蜂鸣器发出特定频率的声音,也就是“音调”。noTone方法用于关闭蜂鸣器。

值得注意的是,在tone方法中,pwm的占空比固定设置为0.5,这代表在一个震动周期内,蜂鸣器的振膜总是一半时间在上,一半时间在下。在这里改变占空比并不会改变蜂鸣器的功率,所以音量大小不会改变。


  
  1. // application/example/edu_demo/k1_apps/musicbox/musicbox.c
  2. void tone(uint16_t port, uint16_t frequency, uint16_t duration)
  3. {
  4.     pwm_dev_t pwm = {port, { 0.5, frequency}, NULL}; // 设定pwm 频率为设定频率
  5.     if (frequency > 0)                              // 频率值合法才会初始化pwm
  6.     {
  7.         hal_pwm_init(&pwm);
  8.         hal_pwm_start(&pwm);
  9.     }
  10.     if (duration != 0)                 
  11.     {
  12.         aos_msleep(duration);
  13.     }
  14.     if (frequency > 0 && duration > 0)      // 如果设定了 duration,则在该延时后停止播放
  15.     {
  16.         hal_pwm_stop(&pwm);
  17.         hal_pwm_finalize(&pwm);
  18.     }
  19. }
  20. void noTone(uint16_t port)
  21. {
  22.     pwm_dev_t pwm = {port, { 0.5, 1}, NULL}; // 关闭对应端口的pwm输出
  23.     hal_pwm_stop(&pwm);
  24.     hal_pwm_finalize(&pwm);
  25. }

 

5、从音调到音乐

完成了蜂鸣器的驱动,可以让蜂鸣器发出我们想要频率的声音了。接下来,我们需要做的就是把这些频率组合起来,形成音乐。

5.1、定义音调

目前我们只能指定发声的频率,却不知道频率怎么对应音调。而遵循音调,才能拼接出音乐。如果把蜂鸣器看作我们要驱动的器件,那么频率与音调的对应关系就是通讯协议,而音乐就是理想的器件输出。

我们采用目前对常用的音乐律式——十二平均律。采用维基百科的定义,可以计算如下:

将主音设为a1(440Hz),来计算所有音的频率,结果如下 (为计算过程更清晰,分数不进行约分)

音程名称

间隔半音数

十二平均律的倍数

频率

纯一度(A1)

0

{\displaystyle 2^{0}=1\,}

{\displaystyle 440\times 1=440\,}

增一度/小二度(A♯1/B♭1)

1

{\displaystyle {\sqrt[{12}]{2}}=2^{\frac {1}{12}}\approx 1.0594630943592952645618252949463}

{\displaystyle 440\times 2^{\frac {1}{12}}\approx 466.1637615180899164072031297762}

大二度(B1)

2

{\displaystyle {\sqrt[{6}]{2}}=2^{\frac {2}{12}}\approx 1.1224620483093729814335330496792}

{\displaystyle 440\times 2^{\frac {2}{12}}\approx 493.8833012561241118307545418586}

小三度(C)

3

{\displaystyle {\sqrt[{4}]{2}}=2^{\frac {3}{12}}\approx 1.1892071150027210667174999705605}

{\displaystyle 440\times 2^{\frac {3}{12}}\approx 523.2511306011972693556999870466}

大三度(C♯)

4

{\displaystyle {\sqrt[{3}]{2}}=2^{\frac {4}{12}}\approx 1.2599210498948731647672106072782}

{\displaystyle 440\times 2^{\frac {4}{12}}\approx 554.3652619537441924975726672023}

纯四度(D)

5

{\displaystyle {\sqrt[{12}]{32}}=2^{\frac {5}{12}}\approx 1.3348398541700343648308318811845}

{\displaystyle 440\times 2^{\frac {5}{12}}\approx 587.3295358348151205255660277209}

增四度/减五度(D#/E♭)

6

{\displaystyle {\sqrt {2}}=2^{\frac {6}{12}}\approx 1.4142135623730950488016887242097}

{\displaystyle 440\times 2^{\frac {6}{12}}\approx 622.2539674441618214727430386522}

纯五度(E)

7

{\displaystyle {\sqrt[{12}]{128}}=2^{\frac {7}{12}}\approx 1.4983070768766814987992807320298}

{\displaystyle 440\times 2^{\frac {7}{12}}\approx 659.2551138257398594716835220930}

小六度(F)

8

{\displaystyle {\sqrt[{3}]{4}}=2^{\frac {8}{12}}\approx 1.5874010519681994747517056392723}

{\displaystyle 440\times 2^{\frac {8}{12}}\approx 698.4564628660077688907504812795}

大六度(F#)

9

{\displaystyle {\sqrt[{4}]{8}}=2^{\frac {9}{12}}\approx 1.6817928305074290860622509524664}

{\displaystyle 440\times 2^{\frac {9}{12}}\approx 739.9888454232687978673904190852}

小七度(G)

10

{\displaystyle {\sqrt[{6}]{32}}=2^{\frac {10}{12}}\approx 1.781797436280678609480452411181}

{\displaystyle 440\times 2^{\frac {10}{12}}\approx 783.9908719634985881713990609195}

大七度(G#)

11

{\displaystyle {\sqrt[{12}]{2048}}=2^{\frac {11}{12}}\approx 1.8877486253633869932838263133351}

{\displaystyle 440\times 2^{\frac {11}{12}}\approx 830.6093951598902770448835778670}

纯八度(A)

12

{\displaystyle 2^{1}=2\,}

{\displaystyle 440\times 2=880\,}

 

这样就得到了频率与音调的关系,我们将它记录在头文件中。


  
  1. // application/example/edu_demo/k1_apps/musicbox/pitches.h
  2. #define NOTE_B0  31
  3. #define NOTE_C1  33
  4. #define NOTE_CS1 35
  5. #define NOTE_D1  37
  6. #define NOTE_DS1 39
  7. ... ...
  8. #define NOTE_B7  3951
  9. #define NOTE_C8  4186
  10. #define NOTE_CS8 4435
  11. #define NOTE_D8  4699
  12. #define NOTE_DS8 4978
  13. 这样,我们就可以采用tone方法来发出对应的音调。
  14. tone( 0, NOTE_B7, 100)
  15. // 使用pwm0对应的蜂鸣器播放 NOTE_B7 持续100ms

5.2、生成乐谱

接下来,我们就可以开始谱曲了,这里我们选用一首非常简单的儿歌——《两只老虎》,来为大家演示如何谱曲。

我们的tone方法有两个需要关注的参数:frequency决定了播放的音调,duration决定了该音调播放的时长,也就是节拍。因此我们在读简谱时,也需要关注这两个参数。

关于简谱的一些基础知识,感兴趣的同学可以参考wikipedia-简谱。本实验只会使用到非常简单的方法,因此也可以直接往下阅读。

以《两只老虎》这张简谱为例。

 

5.2.1、音符

音符用数字1至7表示。这7个数字就等于大调的自然音阶。

左上角的 1 = C 表示调号,代表这张简谱使用C大调,加上音名,就会是这样:

1 = C

             

音阶

C

D

E

F

G

A

B

唱名

do

re

mi

fa

sol

la

Si

数字

1

2

3

4

5

6

7

代码

NOTE_C4

NOTE_D4

NOTE_E4

NOTE_F4

NOTE_G4

NOTE_A4

NOTE_B4

如果 左上角的定义 1 = D,那么就从D开始重新标注,如下表:

1 = D

             

音阶

D

E

F

G

A

B

C

唱名

do

re

mi

fa

sol

la

Si

数字

1

2

3

4

5

6

7

代码

NOTE_D4

NOTE_E4

NOTE_F4

NOTE_G4

NOTE_A4

NOTE_B4

NOTE_C4

 

5.2.2、八度

如果是高一个八度,就会在数字上方加上一点。如果是低一个八度,就会数字下方加上一点。在中间的那一个八度就什么也不用加。如果要再高一个八度,就在上方垂直加上两点(如:);要再低一个八度,就在下方垂直加上两点(如:),如此类推。

1 = C

           

自然大调

数字

5

代码

NOTE_G7

NOTE_G6

NOTE_G5

NOTE_G4

NOTE_G3

NOTE_G2

NOTE_G1

 

1 = C

           

自然小调

数字

5

代码

NOTE_GS7

NOTE_GS6

NOTE_GS5

NOTE_GS4

NOTE_GS3

NOTE_GS2

NOTE_GS1

 

了解了音符和八度后,我们可以开始填写音调数组,这个数组里的每个元素对应 tone 方法的 frequency 参数。

static int liang_zhi_lao_hu_Notes[] = {

    NOTE_C4, NOTE_D4, NOTE_E4, NOTE_C4, NOTE_C4, NOTE_D4, NOTE_E4, NOTE_C4,

//  两       只       老        虎       两        只       老       虎

    NOTE_E4, NOTE_F4, NOTE_G4, NOTE_E4, NOTE_F4, NOTE_G4,

//  跑       得       快        跑       得        快

    NOTE_G4, NOTE_A4, NOTE_G4, NOTE_F4, NOTE_E4, NOTE_C4,

//  一       只       没        有       眼        睛   

    NOTE_G4, NOTE_A4, NOTE_G4, NOTE_F4, NOTE_E4, NOTE_C4,

//  一       只       没        有       尾        巴

    NOTE_D4, NOTE_G3, NOTE_C4, 0,

//  真       奇       怪

    NOTE_D4, NOTE_G3, NOTE_C4, 0};

//  真       奇       怪

 

5.2.3、拍号和音长

左上角的 2/4 表示拍号。这里的4代表4分音符为一拍,2代表每一个小节里共有两拍。

通常只有数字的是四分音符。数字下加一条横线,就可令四分音符的长度减半,即成为八分音符;两条横线可令八分音符的长度减半,即成为十六分音符,以此类推;数字后方的横线延长音符,每加一条横线延长一个四分音符的长度。

因此我们可以得到节拍数组,这个数组里的每个元素对应 tone 方法的 duration 参数。

static int liang_zhi_lao_hu_NoteDurations[] = {

    8, 8, 8, 8, 8, 8, 8, 8,

    8, 8, 4, 8, 8, 4,

    16, 16, 16, 16, 4, 4,

    16, 16, 16, 16, 4, 4,

    8, 8, 4, 4,

    8, 8, 4, 4};

 

5.2.4、结构体定义

接下来,我们将得到的乐谱信息填入结构体当中。


  
  1. // application/example/edu_demo/k1_apps/musicbox/musicbox.c
  2. typedef struct
  3. {
  4.     char *name;                 // 音乐的名字   
  5.     int *notes;                 // 音符数组
  6.     int *noteDurations;         // 节拍数组
  7.     unsigned int noteLength;    // 音符数量
  8.     unsigned int musicTime;     // 音乐总时长 由播放器处理 用于界面显示 用户不需要关心
  9. } music_t;                      // 音乐结构体
  10. typedef struct
  11. {
  12.     music_t **music_list;           // 音乐列表
  13.     unsigned int music_list_len;    // 音乐列表的长度
  14.     int cur_music_index;            // 当前第几首音乐
  15.     unsigned int cur_music_note;    // 当前音乐的第几个音符
  16.     unsigned int cur_music_time;    // 当前的播放时长 由播放器处理 用于界面显示 用户不需要关心
  17.     unsigned int isPlaying;         // 音乐是否播放/暂停 由播放器处理 用户不需要关心
  18. } player_t;
  19. static music_t liang_zhi_lao_hu = {
  20.     "liang_zhi_lao_hu",
  21.     liang_zhi_lao_hu_Notes,
  22.     liang_zhi_lao_hu_NoteDurations,
  23.     34
  24. };
  25. music_t *music_list[] = {
  26.     &liang_zhi_lao_hu_Notes,        // 将音乐插入到音乐列表中
  27. };
  28. player_t musicbox_player = {music_list, 1, 0, 0, 0, 0}; // 初始化音乐播放器

 

6、实现播放音乐


  
  1. while ( 1)
  2. {
  3.     // 如果当前音调下标小于这首音乐的总音调 即尚未播放完
  4.     if (musicbox_player.cur_music_note < cur_music->noteLength)
  5.     {
  6.         // 通过节拍计算出当前音符需要的延时 1000ms / n分音符
  7.         int noteDuration = 1000 / cur_music->noteDurations[musicbox_player.cur_music_note];
  8.         // 对于附点音符 我们用读数来标记 加有一个附点后音符的音长比其原来的音长增加了一半,即原音长的1.5倍。
  9.         noteDuration = (noteDuration < 0) ? (-noteDuration * 1.5) : noteDuration;
  10.         // 得到当前的音调
  11.         int note = cur_music->notes[musicbox_player.cur_music_note];
  12.         // 使用 tone 方法播放音调
  13.         tone( 0, note, noteDuration);
  14.         // 延时一段时间 让音调转换更清晰
  15.         aos_msleep(( int)(noteDuration * NOTE_SPACE_RATIO));
  16.         // 计算当前的播放时间
  17.         musicbox_player.cur_music_time += (noteDuration + ( int)(noteDuration * NOTE_SPACE_RATIO));
  18.         // 准备播放下一个音调
  19.         musicbox_player.cur_music_note++;
  20.     }
  21. }

 

7、绘制播放器

作为一位有理想有追求的开发者,仅仅能播放音乐肯定没法满足我们的创造欲。所以我们再来实现一个播放器,可以做到 暂停/播放, 上一首/下一首, 还能显示歌曲名和进度条。

实现这些需要的信息,我们在结构体中都已经完成了相关的定义,只需要根据按键操作完成对应的音乐播放控制即可。


  
  1. void musicbox_task()
  2. {
  3.     while ( 1)
  4.     {
  5.         // 清除上一次绘画的残留
  6.         OLED_Clear();
  7.         // 获取当前音乐的指针
  8.         music_t *cur_music = musicbox_player.music_list[musicbox_player.cur_music_index];
  9.         // 获取当前音乐的名字并且绘制
  10.         char show_song_name[ 14] = { 0};
  11.         sprintf(show_song_name, "%-13.13s", cur_music->name);
  12.         OLED_Show_String( 14, 4, show_song_name, 16, 1);
  13.         // 如果当前播放器并未被暂停(正在播放)
  14.         if (musicbox_player.isPlaying)
  15.         {
  16.             // 如果还没播放完
  17.             if (musicbox_player.cur_music_note < cur_music->noteLength)
  18.             {
  19.                 int noteDuration = 1000 / cur_music->noteDurations[musicbox_player.cur_music_note];
  20.                 noteDuration = (noteDuration < 0) ? (-noteDuration * 1.5) : noteDuration;
  21.                 printf( "note[%d] = %d\t delay %d ms\n", musicbox_player.cur_music_note, cur_music->noteDurations[musicbox_player.cur_music_note], noteDuration);
  22.                 int note = cur_music->notes[musicbox_player.cur_music_note];
  23.                 tone( 0, note, noteDuration);
  24.                 aos_msleep(( int)(noteDuration * NOTE_SPACE_RATIO));
  25.                 musicbox_player.cur_music_time += (noteDuration + ( int)(noteDuration * NOTE_SPACE_RATIO));
  26.                 musicbox_player.cur_music_note++;
  27.             }
  28.             // 如果播放完 切换到下一首
  29.             else
  30.             {
  31.                 noTone( 0);
  32.                 aos_msleep( 1000);
  33.                 next_song();    // musicbox_player.cur_music_index++ 播放器的指向下一首音乐
  34.             }
  35.             OLED_Icon_Draw( 54, 36, &icon_pause_24_24, 1);   // 播放器处于播放状态时 绘制暂停图标
  36.         }
  37.         else
  38.         {
  39.             OLED_Icon_Draw( 54, 36, &icon_resume_24_24, 1);  // 播放器处于暂停状态时 绘制播放图标
  40.             aos_msleep( 500);
  41.         }
  42.        
  43.         // 绘制一条直线代表进度条 直线的长度是 99.0(可绘画区域的最大长度) * (musicbox_player.cur_music_time(播放器记录的的当前音乐播放时长) / cur_music->musicTime(这首歌的总时长))
  44.         OLED_DrawLine( 16, 27, ( int)( 16 + 99.0 * (musicbox_player.cur_music_time * 1.0 / cur_music->musicTime)), 27, 1);
  45.         // 绘制上一首和下一首的图标
  46.         OLED_Icon_Draw( 94, 36, &icon_next_song_24_24, 1);
  47.         OLED_Icon_Draw( 14, 36, &icon_previous_song_24_24, 1);
  48.        
  49.         // 将绘制的信息显示在屏幕上
  50.         OLED_Refresh_GRAM();
  51.     }
  52. }

 

8、开发者技术支持

如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号

更多技术与解决方案介绍,请访问阿里云AIoT首页https://iot.aliyun.com/


转载:https://blog.csdn.net/HaaSTech/article/details/114214685
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场