实验介绍

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

涉及知识点

乐谱编码
PWM与蜂鸣器

开发环境准备

硬件

开发用电脑一台
HAAS EDU K1 开发板一块
USB2TypeC 数据线一根

软件

AliOS Things开发环境搭建

开发环境的搭建请参考 @ref HaaS_EDU_K1_Quick_Start (搭建开发环境章节),其中详细的介绍了AliOS Things 3.3的IDE集成开发环境的搭建流程。

HaaS EDU K1 DEMO 代码下载

HaaS EDU K1 DEMO 的代码下载请参考 @ref HaaS_EDU_K1_Quick_Start (创建工程章节),其中,
选择解决方案: 基于教育开发板的示例
选择开发板: haaseduk1 board configure

代码编译、烧录

参考 @ref HaaS_EDU_K1_Quick_Start (3.1 编译工程章节),点击 ✅ 即可完成编译固件。
参考 @ref HaaS_EDU_K1_Quick_Start (3.2 烧录镜像章节),点击 "⚡️" 即可完成烧录固件。

蜂鸣器

蜂鸣器是一种非常简单的发声器件,和播放播放使用的扬声器不同,蜂鸣器只能播放较为简单的频率。
从驱动原理上区分,蜂鸣器可以分为无源蜂鸣器和有源蜂鸣器。这里的“源”,指的就是有无驱动源。无源蜂鸣器,顾名思义,就是没有自己的内置驱动源。只有为音圈接入交变电流后,其内部的电磁铁与永磁铁相吸或相斥而推动振膜发声,而接入直流电后,只能持续推动振膜而无法产生声音,只能在接通或断开时产生声音。而有源驱动器相反,只要接入直流电,其内部的驱动源会以一个固定的频率驱动振膜,直接发声。
在本实验中,推荐大家使用无源蜂鸣器,因为它只由PWM驱动,声音会更清脆纯净。使用有源蜂鸣器时,也能实现类似的效果,不过由于叠加了有源蜂鸣器自己的震动频率,声音会略显嘈杂。

驱动电路

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

驱动代码

为了实现IO口按特定频率翻转,我们可以使用PWM(脉冲宽度调制)功能。关于PWM的详细介绍可以参看z第三章资源PWM部分。
在本实验中,我们实现了tone和noTone两个方法。其中,tone方法用于驱动蜂鸣器发出特定频率的声音,也就是“音调”。noTone方法用于关闭蜂鸣器。
值得注意的是,在tone方法中,pwm的占空比固定设置为0.5,这代表在一个震动周期内,蜂鸣器的振膜总是一半时间在上,一半时间在下。在这里改变占空比并不会改变蜂鸣器的功率,所以音量大小不会改变。

// solutions/eduk1_demo/k1_apps/musicbox/musicbox.cvoid tone(uint16_t port, uint16_t frequency, uint16_t duration)
{pwm_dev_t pwm = {port, {0.5, frequency}, NULL};   // 设定pwm 频率为设定频率if (frequency > 0)                               // 频率值合法才会初始化pwm{hal_pwm_init(&pwm);hal_pwm_start(&pwm);}if (duration != 0)                    {aos_msleep(duration);}if (frequency > 0 && duration > 0)     // 如果设定了 duration,则在该延时后停止播放{hal_pwm_stop(&pwm);hal_pwm_finalize(&pwm);}
}void noTone(uint16_t port)
{pwm_dev_t pwm = {port, {0.5, 1}, NULL};   // 关闭对应端口的pwm输出hal_pwm_stop(&pwm);hal_pwm_finalize(&pwm);
}

从音调到音乐

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

定义音调

目前我们只能指定发声的频率,却不知道频率怎么对应音调。而遵循音调,才能拼接出音乐。如果把蜂鸣器看作我们要驱动的器件,那么频率与音调的对应关系就是通讯协议,而音乐就是理想的器件输出。
我们采用目前对常用的音乐律式——十二平均律。采用维基百科的定义,可以计算如下:
将主音设为a1(440Hz),来计算所有音的频率,结果如下(为计算过程更清晰,分数不进行约分):

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

// solutions/eduk1_demo/k1_apps/musicbox/pitches.h#define NOTE_B0  31
#define NOTE_C1  33
#define NOTE_CS1 35
#define NOTE_D1  37
#define NOTE_DS1 39
... ...
#define NOTE_B7  3951
#define NOTE_C8  4186
#define NOTE_CS8 4435
#define NOTE_D8  4699
#define NOTE_DS8 4978

这样,我们就可以采用tone方法来发出对应的音调。

tone(0, NOTE_B7, 100)
// 使用pwm0对应的蜂鸣器播放 NOTE_B7 持续100ms

生成乐谱

接下来,我们就可以开始谱曲了,这里我们选用一首非常简单的儿歌——《两只老虎》,来为大家演示如何谱曲。
我们的tone方法有两个需要关注的参数:frequency决定了播放的音调,duration决定了该音调播放的时长,也就是节拍。因此我们在读简谱时,也需要关注这两个参数。
关于简谱的一些基础知识,感兴趣的同学可以参考wikipedia-简谱。本实验只会使用到非常简单的方法,因此也可以直接往下阅读。

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

音符

音符用数字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

八度

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

自然大调

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};
//   真         奇         怪

拍号和音长

左上角的 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};

结构体定义

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

// solutions/eduk1_demo/k1_apps/musicbox/musicbox.ctypedef struct
{char *name;                    // 音乐的名字    int *notes;                 // 音符数组int *noteDurations;          // 节拍数组unsigned int noteLength; // 音符数量unsigned int musicTime;      // 音乐总时长 由播放器处理 用于界面显示 用户不需要关心
} music_t;                      // 音乐结构体typedef struct
{music_t **music_list;          // 音乐列表unsigned int music_list_len; // 音乐列表的长度int cur_music_index;          // 当前第几首音乐unsigned int cur_music_note;  // 当前音乐的第几个音符unsigned int cur_music_time;   // 当前的播放时长 由播放器处理 用于界面显示 用户不需要关心unsigned int isPlaying;         // 音乐是否播放/暂停 由播放器处理 用户不需要关心
} player_t;static music_t liang_zhi_lao_hu = {"liang_zhi_lao_hu", liang_zhi_lao_hu_Notes, liang_zhi_lao_hu_NoteDurations, 34
};music_t *music_list[] = {&liang_zhi_lao_hu_Notes,        // 将音乐插入到音乐列表中
};player_t musicbox_player = {music_list, 1, 0, 0, 0, 0};  // 初始化音乐播放器

实现播放音乐

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

绘制播放器

作为一位有理想有追求的开发者,仅仅能播放音乐肯定没法满足我们的创造欲。所以我们再来实现一个播放器,可以做到 暂停/播放, 上一首/下一首, 还能显示歌曲名和进度条。
实现这些需要的信息,我们在结构体中都已经完成了相关的定义,只需要根据按键操作完成对应的音乐播放控制即可。

void musicbox_task()
{while (1){// 清除上一次绘画的残留OLED_Clear();// 获取当前音乐的指针music_t *cur_music = musicbox_player.music_list[musicbox_player.cur_music_index];// 获取当前音乐的名字并且绘制char show_song_name[14] = {0};sprintf(show_song_name, "%-13.13s", cur_music->name);OLED_Show_String(14, 4, show_song_name, 16, 1);// 如果当前播放器并未被暂停(正在播放)if (musicbox_player.isPlaying){// 如果还没播放完if (musicbox_player.cur_music_note < cur_music->noteLength){int noteDuration = 1000 / cur_music->noteDurations[musicbox_player.cur_music_note];noteDuration = (noteDuration < 0) ? (-noteDuration * 1.5) : noteDuration;printf("note[%d] = %d\t delay %d ms\n", musicbox_player.cur_music_note, cur_music->noteDurations[musicbox_player.cur_music_note], noteDuration);int note = cur_music->notes[musicbox_player.cur_music_note];tone(0, note, noteDuration);aos_msleep((int)(noteDuration * NOTE_SPACE_RATIO));musicbox_player.cur_music_time += (noteDuration + (int)(noteDuration * NOTE_SPACE_RATIO));musicbox_player.cur_music_note++;}// 如果播放完 切换到下一首else{noTone(0);aos_msleep(1000);next_song();  // musicbox_player.cur_music_index++ 播放器的指向下一首音乐}OLED_Icon_Draw(54, 36, &icon_pause_24_24, 1);    // 播放器处于播放状态时 绘制暂停图标}else{OLED_Icon_Draw(54, 36, &icon_resume_24_24, 1);    // 播放器处于暂停状态时 绘制播放图标aos_msleep(500);}// 绘制一条直线代表进度条 直线的长度是 99.0(可绘画区域的最大长度) * (musicbox_player.cur_music_time(播放器记录的的当前音乐播放时长) / cur_music->musicTime(这首歌的总时长))OLED_DrawLine(16, 27, (int)(16 + 99.0 * (musicbox_player.cur_music_time * 1.0 / cur_music->musicTime)), 27, 1);   // 绘制上一首和下一首的图标OLED_Icon_Draw(94, 36, &icon_next_song_24_24, 1);OLED_Icon_Draw(14, 36, &icon_previous_song_24_24, 1);// 将绘制的信息显示在屏幕上OLED_Refresh_GRAM();}
}

开发者支持

HaaS官方:https://haas.iot.aliyun.com/
HaaS技术社区:https://blog.csdn.net/HaaSTech
开发者钉钉群和公众号见下图,开发者钉钉群每天都有技术支持同学值班。

HaaS EDU场景式应用学习 - 复古八音盒相关推荐

  1. HaaS EDU场景式应用学习 - 分歧争端机

    实验介绍 分歧争端机使用的场景是当出现分歧时,分别摇一摇HaaS EDU K1来摇出一个0到100之间的数字,通过比较谁摇出的数字大来解决分歧的一种方法.本质是通过摇一摇这个动作来随机产生一个数字.这 ...

  2. HaaS EDU场景式应用学习 - 首页信息屏

    实验介绍 本章主要介绍首页信息屏的实现,作为第一页,首页信息实现了类手机的界面,其中包含了丰富的内容. 1)产品名称 2) 版本信息 3) 系统状态,系统时间,WIFI连接,蓝牙连接. 4) IP地址 ...

  3. 扇贝编程python是干嘛的-产品观察 | 以对话式互动学习撬动转化,扇贝编程瞄准职教市场...

    原标题:产品观察 | 以对话式互动学习撬动转化,扇贝编程瞄准职教市场 成人编程教育是职业教育行业最火热的赛道之一,除了行业内的垂直创业公司,越来越多教育公司也在把业务线延展至这一领域,以便深度结合业务 ...

  4. 业界首个面向NLP场景深度迁移学习框架

    机器之心发布 机器之心编辑部 阿里云正式开源了深度迁移学习框架 EasyTransfer,本文详细介绍了 EasyTransfer 框架的核心功能 近日,阿里云正式开源了深度迁移学习框架 EasyTr ...

  5. 阿里云开源EasyTransfer:业界首个面向NLP场景深度迁移学习框架

    阿里云开源EasyTransfer:业界首个面向NLP场景深度迁移学习框架 原文链接:https://zhuanlan.zhihu.com/p/267392773 阿里云正式开源了深度迁移学习框架 E ...

  6. 场景式营销——新商业时代的生存法则

    现在,人们已经进入一个全新的商业购物时代--场景式营销时代. 从某种程度上说,"场景式营销",即针对目标消费者所处时间.地点展开的营销活动.作为特定的营销方式,场景式营销早已存在于 ...

  7. CVPR 2022 Oral | 人大高瓴AI学院提出:面向动态视音场景的问答学习机制

    点击下方卡片,关注"CVer"公众号 AI/CV重磅干货,第一时间送达 标    题:CVPR2022 Oral | 人大高瓴AI学院提出面向动态视音场景的问答学习任务 作    ...

  8. 学习金字塔:输出式主动学习

    什么是主动学习? 学习金字塔告诉我们,通过主动学习后,知识的留存率最高. 什么是主动学习?就是说.写.教.做的输出式学习. 说:给学弟学妹们分享些技术,给企业/学校作培训,上学术/工业顶会演讲,都可以 ...

  9. 【Autogluon】傻瓜式深度学习框架

    Autogluon傻瓜式深度学习 提示:这里简述项目相关背景: 安装教程可参考: 教程: https://blog.csdn.net/lichangzhen2008/article/details/1 ...

  10. 《场景式500主题会话10000单词完全掌握》[PDF]

    <场景式500主题会话10000单词完全掌握>[PDF] 目录: 001身体部位 002身体动作 003头部与面部 004面部表情 005皮肤毛发 006五脏六腑 007血液循环 008容 ...

最新文章

  1. 我是如何在阿里巴巴面试中壮烈牺牲的?(内含面试题)
  2. 【人物】徐小平:既然做老大,你就得让兄弟们有肉吃
  3. DevExpress的TreeList实现自定义节点NodeCell的背景颜色和前景色
  4. 在网络中同时使用kfold和使用Dropout(基于Iris数据集)
  5. 普通机器学习模型的提升
  6. mysql的cost
  7. vue导出excel并修改表头样式
  8. 语言-英语-美国英语:美国英语
  9. 上班两年干了些啥?该思考人生
  10. 机器学习:决策树算法案例(西瓜数据集3.0)
  11. python乌龟吃鱼
  12. 天下武功,无坚不破,唯快不破
  13. 来自首次Ray聚会的记录
  14. 探索 TDengine在《图码联侦》项目中的应用可行性及实践研究(new)
  15. Mysql使用Key/Value方式存储动态扩展字段、对象与HashMap的相互转化
  16. 大学生职业生涯发展与规划
  17. SEAndroid安全机制框架分析
  18. getopt.h和getopt(),getopt_long()等函数
  19. 计算机操作测试题及答案,计算机操作系统期末考试题及答案-
  20. Android双波浪自定义控件(DoubleWaveView)

热门文章

  1. Redis与数据库的数据一致性
  2. php新浪博客模板,supersite php模板
  3. ps中100%比例放大图片显示的快捷键
  4. SAS 学习笔记 (一) — SAS简介
  5. AI制作ICON流程
  6. Excel小技巧-获取列数
  7. MacOS - MacBook - 推荐工具收集
  8. 你必须了解的支撑研究蛋白质组学的3大技术
  9. 日语 敬体 简体 作文 对话
  10. 3dmax如何删除多余的时间帧