天天看點

HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援

實驗介紹

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

HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援

涉及知識點

樂譜編碼

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驅動,聲音會更清脆純淨。使用有源蜂鳴器時,也能實作類似的效果,不過由于疊加了有源蜂鳴器自己的震動頻率,聲音會略顯嘈雜。

驅動電路

HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援

蜂鳴器的 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),來計算所有音的頻率,結果如下(為計算過程更清晰,分數不進行約分):

HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援

這樣就得到了頻率與音調的關系,我們将它記錄在頭檔案中。

// 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-簡譜

。本實驗隻會使用到非常簡單的方法,是以也可以直接往下閱讀。

HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援

以《兩隻老虎》這張簡譜為例。

音符

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

八度

如果是高一個八度,就會在數字上方加上一點。如果是低一個八度,就會數字下方加上一點。在中間的那一個八度就什麼也不用加。如果要再高一個八度,就在上方垂直加上兩點(如:

HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援

);要再低一個八度,就在下方垂直加上兩點(如:

HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援

),如此類推。

自然大調

HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
NOTE_G7 NOTE_G6 NOTE_G5 NOTE_G3 NOTE_G2 NOTE_G1

自然小調

HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援
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

開發者釘釘群和公衆号見下圖,開發者釘釘群每天都有技術支援同學值班。

HaaS EDU場景式應用學習 - 複古八音盒實驗介紹涉及知識點開發環境準備蜂鳴器從音調到音樂實作播放音樂繪制播放器開發者支援