實驗介紹
Chiptune是不少80,90後的童年回憶,說Chiptune的名字應該很多人比較陌生,不過它有另外一個名字:8-bit。所謂的所謂的Chiptune也就是由老式家用電腦、錄像遊戲機和街機的晶片(也就是所謂的CHIP)發出的聲音而寫作的曲子。嚴格說來其實Chiptune不僅僅隻有8bit,不過都是追求複古顆粒感的低比特率。本實驗中,我們也來實作一款複古“八音”盒。

涉及知識點
樂譜編碼
PWM與蜂鳴器
開發環境準備
硬體
開發用電腦一台
HAAS EDU K1 開發闆一塊
USB2TypeC 資料線一根
- 1
- 2
- 3
軟體
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.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);
}
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
從音調到音樂
完成了蜂鳴器的驅動,可以讓蜂鳴器發出我們想要頻率的聲音了。接下來,我們需要做的就是把這些頻率組合起來,形成音樂。
定義音調
目前我們隻能指定發聲的頻率,卻不知道頻率怎麼對應音調。而遵循音調,才能拼接出音樂。如果把蜂鳴器看作我們要驅動的器件,那麼頻率與音調的對應關系就是通訊協定,而音樂就是理想的器件輸出。
我們采用目前對常用的音樂律式——
十二平均律。采用維基百科的定義,可以計算如下:
将主音設為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 |
數字 | |||||||
代碼 | NOTE_C4 | NOTE_D4 | NOTE_E4 | NOTE_F4 | NOTE_G4 | NOTE_A4 | NOTE_B4 |
如果 左上角的定義 1 = D,那麼就從D開始重新标注,如下表:
1 = D | |||||||
---|---|---|---|---|---|---|---|
八度
如果是高一個八度,就會在數字上方加上一點。如果是低一個八度,就會數字下方加上一點。在中間的那一個八度就什麼也不用加。如果要再高一個八度,就在上方垂直加上兩點(如:
);要再低一個八度,就在下方垂直加上兩點(如:
),如此類推。
自然大調
| | | | | | ||
NOTE_G7 | NOTE_G6 | NOTE_G5 | NOTE_G3 | NOTE_G2 | NOTE_G1 |
自然小調
| | | | | | ||
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.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,
34
};
music_t *music_list[] = {
&liang_zhi_lao_hu_Notes, // 将音樂插入到音樂清單中
};
player_t musicbox_player = {music_list, 1, 0, 0, 0, 0}; // 初始化音樂播放器
- 28
- 29
- 30
- 31
- 32
- 33
實作播放音樂
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();
}
}
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
開發者支援
HaaS官方:
https://haas.iot.aliyun.com/HaaS技術社群:
https://blog.csdn.net/HaaSTech開發者釘釘群和公衆号見下圖,開發者釘釘群每天都有技術支援同學值班。