Chiptune是不少80,90后的童年回忆,说Chiptune的名字应该很多人比较陌生,不过它有另外一个名字:8-bit。所谓的所谓的Chiptune也就是由老式家用电脑、录像游戏机和街机的芯片(也就是所谓的CHIP)发出的声音而写作的曲子。严格说来其实Chiptune不仅仅只有8bit,不过都是追求复古颗粒感的低比特率。本实验中,我们也来实现一款复古“八音”盒。
涉及知识点乐谱编码
PWM与蜂鸣器
开发用电脑一台 HAAS EDU K1 开发板一块 USB2TypeC 数据线一根123软件 AliOS Things开发环境搭建
开发环境的搭建请参考 @ref HaaS_EDU_K1_Quick_Start (搭建开发环境章节),其中详细的介绍了AliOS Things 3.3的IDE集成开发环境的搭建流程。1HaaS EDU K1 DEMO 代码下载
HaaS EDU K1 DEMO 的代码下载请参考 @ref HaaS_EDU_K1_Quick_Start (创建工程章节),其中, 选择解决方案: 基于教育开发板的示例 选择开发板: haaseduk1 board configure123代码编译、烧录
参考 @ref HaaS_EDU_K1_Quick_Start (3.1 编译工程章节),点击 ? 即可完成编译固件。 参考 @ref HaaS_EDU_K1_Quick_Start (3.2 烧录镜像章节),点击 "??" 即可完成烧录固件。12蜂鸣器
蜂鸣器是一种非常简单的发声器件,和播放播放使用的扬声器不同,蜂鸣器只能播放较为简单的频率。
从驱动原理上区分,蜂鸣器可以分为无源蜂鸣器和有源蜂鸣器。这里的“源”,指的就是有无驱动源。无源蜂鸣器,顾名思义,就是没有自己的内置驱动源。只有为音圈接入交变电流后,其内部的电磁铁与永磁铁相吸或相斥而推动振膜发声,而接入直流电后,只能持续推动振膜而无法产生声音,只能在接通或断开时产生声音。而有源驱动器相反,只要接入直流电,其内部的驱动源会以一个固定的频率驱动振膜,直接发声。
在本实验中,推荐大家使用无源蜂鸣器,因为它只由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.c void 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);123456789101112131415161718192021222324252627从音调到音乐
完成了蜂鸣器的驱动,可以让蜂鸣器发出我们想要频率的声音了。接下来,我们需要做的就是把这些频率组合起来,形成音乐。
定义音调目前我们只能指定发声的频率,却不知道频率怎么对应音调。而遵循音调,才能拼接出音乐。如果把蜂鸣器看作我们要驱动的器件,那么频率与音调的对应关系就是通讯协议,而音乐就是理想的器件输出。
我们采用目前对常用的音乐律式——十二平均律。采用维基百科的定义,可以计算如下:
将主音设为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 497812345678910111213
这样,我们就可以采用tone方法来发出对应的音调。
tone(0, NOTE_B7, 100) // 使用pwm0对应的蜂鸣器播放 NOTE_B7 持续100ms12生成乐谱
接下来,我们就可以开始谱曲了,这里我们选用一首非常简单的儿歌——《两只老虎》,来为大家演示如何谱曲。
我们的tone方法有两个需要关注的参数:frequency决定了播放的音调,duration决定了该音调播放的时长,也就是节拍。因此我们在读简谱时,也需要关注这两个参数。
关于简谱的一些基础知识,感兴趣的同学可以参考wikipedia-简谱。本实验只会使用到非常简单的方法,因此也可以直接往下阅读。
以《两只老虎》这张简谱为例。
音符音符用数字1至7表示。这7个数字就等于大调的自然音阶。
左上角的 1 = C 表示调号,代表这张简谱使用C大调,加上音名,就会是这样:
如果 左上角的定义 1 = D,那么就从D开始重新标注,如下表:
1 = D音阶DEFGABC唱名doremifasollaSi数字1234567代码NOTE_D4NOTE_E4NOTE_F4NOTE_G4NOTE_A4NOTE_B4NOTE_C4 八度如果是高一个八度,就会在数字上方加上一点。如果是低一个八度,就会数字下方加上一点。在中间的那一个八度就什么也不用加。如果要再高一个八度,就在上方垂直加上两点(如:);要再低一个八度,就在下方垂直加上两点(如:),如此类推。
自然大调 1 = C自然大调数字5代码NOTE_G7NOTE_G6NOTE_G5NOTE_G4NOTE_G3NOTE_G2NOTE_G1 自然小调 1 = C自然小调数字5代码NOTE_GS7NOTE_GS6NOTE_GS5NOTE_GS4NOTE_GS3NOTE_GS2NOTE_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}; // 真 奇 怪12345678910111213拍号和音长
左上角的 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};1234567结构体定义
接下来,我们将得到的乐谱信息填入结构体当中。
// solutions/eduk1_demo/k1_apps/musicbox/musicbox.c typedef 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, music_t *music_list[] = { liang_zhi_lao_hu_Notes, // 将音乐插入到音乐列表中 player_t musicbox_player = {music_list, 1, 0, 0, 0, 0}; // 初始化音乐播放器123456789101112131415161718192021222324252627282930313233实现播放音乐
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++;123456789101112131415161718192021绘制播放器
作为一位有理想有追求的开发者,仅仅能播放音乐肯定没法满足我们的创造欲。所以我们再来实现一个播放器,可以做到 暂停/播放, 上一首/下一首, 还能显示歌曲名和进度条。
实现这些需要的信息,我们在结构体中都已经完成了相关的定义,只需要根据按键操作完成对应的音乐播放控制即可。
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();12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455开发者支持
HaaS官方:https://haas.iot.aliyun.com/
HaaS技术社区:https://blog.csdn.net/HaaSTech
开发者钉钉群和公众号见下图,开发者钉钉群每天都有技术支持同学值班。
TigerGraph宣布,将在11月19日在线举办Graph+AI World 2020 中国峰会。本次峰会...
怎么申请免费公司 邮箱 ?其实目前市面很多号称是免费的邮箱,要申请 企业邮箱 ...
11月25日,由深信服旗下云计算品牌信服云发起的中国IT正传系列沙龙在全国陆续开展...
Shellcode转储 Shellcode还将扫描游戏进程和Windows进程lsass.exe,以查找可疑的...
01 数据 数据几乎渗透到我们生活的每一个角落,从我们在手机中留下的数字足迹,...
哪些 云服务器 较便宜?其实 云服务器 的价格通常是透明的,企业可以根据给定月...
随着网上商务的增多,网上虚拟产品成为网上建站不可或缺的,如域名, 虚拟主机 ...
前言 有时候你在本地写了一个web项目,地址是http:localhost:8080/XXX,但是你只...
在 JavaScript 中,我们只能继承单个对象。每个对象只能有一个 [[Prototype]]。...
域名 转移密码多少?域名要想进行转移,需要获得转移密码。而转移密码,需要向原...