博客 / 詳情

返回

《ESP32-S3使用指南—IDF版 V1.6》第四十二章 錄音機實驗

第四十二章錄音機實驗

1)實驗平台:正點原子DNESP32S3開發板

2)章節摘自【正點原子】ESP32-S3使用指南—IDF版 V1.6

3)購買鏈接:https://detail.tmall.com/item.htm?&id=768499342659

4)全套實驗源碼+手冊+視頻下載地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32S3.html

5)正點原子官方B站:https://space.bilibili.com/394620890

6)正點原子DNESP32S3開發板技術交流羣:132780729

上一章,我們實現了一個簡單的音樂播放器,本章我們將在上一章的基礎上,繼續用ES8388實現一個簡單的錄音機,錄製WAV格式的錄音。
本章分為如下幾個小節:
42.1 ES8388錄音簡介
42.2 硬件設計
42.3 程序設計
42.4 下載驗證

42.1 ES8388錄音簡介

本章涉及的知識點基本上在上一章都有介紹。本章要實現WAV錄音,還是和上一章一樣,要了解:WAV文件格式、ES8388和I²S。WAV文件格式,我們在上一章已經做了詳細介紹了,這裏就不作介紹了。
正點原子DNESP32S3開發板將板載的一個MIC分別接入到了ES8388的2個差分輸入通道(LIP/LIN和RIP/RIN,原理圖見:圖42.2.3)。代碼上,我們採用立體聲WAV錄音,不過,左右聲道的音源都是一樣的,錄音出來的WAV文件,聽起來就是個單聲道效果。
關於ES8388的驅動與上一章是一樣的,區別在於ES8388的工作狀態不一樣,在本章錄音實驗中ES8388設置為開啓ADC,上一章節則是設置為開啓DAC,讀者想了解可以參考第42章的介紹,或者參考本例程源代碼和ES8388的pdf數據手冊理解。

42.2 硬件設計

42.2.1例程功能

本章實驗功能簡介:開機後,先初始化各外設,然後檢測字庫是否存在,如果檢測無問題,則開始循環播放SD卡MUSIC文件夾裏面的歌曲(必須在SD卡根目錄建立一個MUSIC文件夾,並存放歌曲在裏面),在SPILCD上顯示歌曲名字、播放時間、歌曲總時間、歌曲總數目、當前歌曲的編號等信息。KEY0用於選擇下一曲,KEY2用於選擇上一曲,KEY3用來控制暫停/繼續播放。LED閃爍,提示程序運行狀態。

42.2.2硬件資源

本實驗,大家需要準備1個microSD/SD卡(在裏面新建一個MUSIC文件夾,並存放一些歌曲在MUSIC文件夾下)和一個耳機(非必備),分別插入SD卡接口和耳機接口,然後下載本實驗就可以實現錄音機的效果。實驗用到的硬件資源如下:
1.LED燈
LED -IO0
2.獨立按鍵
KEY0(XL9555) - IO1_7
KEY1(XL9555) - IO1_6
KEY2(XL9555) - IO1_5
KEY3(XL9555) - IO1_4
3.XL9555
IIC_SDA-IO41
IIC_SCL-IO42
4.SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在P5端口,使用跳線帽將IO_SET和LCD_DC相連)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555)
5.SD
CS-IO2
SCK-IO12
MOSI-IO11
MISO-IO13
6.ES8388音頻CODEC芯片(IIC端口0)
IIC_SDA-IO41
IIC_SCL-IO42
I2S_BCK_IO-IO46
I2S_WS_IO-IO9
I2S_DO_IO-IO10
I2S_DI_IO-IO14
IS2_MCLK_IO-IO3
7.開發闆闆載的咪頭或自備麥克風輸入
8.喇叭或耳機
錄音機實驗與上一章(音樂播放器實驗)用到的硬件資源基本一樣,我們這裏就不重複介紹原理圖了,有差異的是這次我們用到板載的咪頭用於信號輸入,也可以通過3.5mm的音頻接口通過LINE_IN接入麥克風輸入錄音音源。

42.2.3原理圖

本實驗相關的原理圖同上一章節。

42.3 程序設計

42.3.1 程序流程圖

程序流程圖能幫助我們更好的理解一個工程的功能和實現的過程,對學習和設計工程有很好的主導作用。下面看看本實驗的程序流程圖:

圖42.3.1.1錄音實驗程序流程圖

42.3.2 錄音實驗函數解析

本章實驗所使用ESP32-S3的API函數在上一章節已經講述過了,在此不再贅述。

42.3.3 錄音實驗驅動解析

在IDF版的31_recoding例程中,作者在31_recoding\components\BSP路徑下新增了一個I2S文件夾和一個ES8388文件夾,分別用於存放i2s.c、i2s.h和es8388.c以及es8388.h這四個文件。其中,i2s.h和es8388.h文件負責聲明I2S以及ES8388相關的函數和變量,而i2s.c和es8388.c文件則實現了I2S以及ES8388的驅動代碼。下面,我們將詳細解析這四個文件的實現內容。
1,recorder驅動
錄這裏我們只講解核心代碼,詳細的源碼請大家參考光盤本實驗對應源碼,RECORDER的驅動主要包括兩個文件:recorder.c和recorder.h。
音樂播放器實驗中我們已經學過配置ES8388的方法,我們在recoder.c編寫函數配置ES8388工作在PCM錄音模式,我們編寫代碼如下:

/**
* @brief      進入PCM 錄音模式
* @param      無
* @retval     無
*/
voidrecoder_enter_rec_mode(void)
{
    es8388_adda_cfg(0, 1);           /* 開啓ADC */
    es8388_input_cfg(0);              /* 開啓輸入通道(通道1,MIC所在通道) */
    es8388_mic_gain(8);               /*MIC增益設置為最大 */
    es8388_alc_ctrl(3, 4, 4);        /* 開啓立體聲ALC控制,以提高錄音音量 */
    es8388_output_cfg(0, 0);         /* 關閉通道1和2的輸出 */
    es8388_spkvol_set(0);             /* 關閉喇叭. */
    es8388_sai_cfg(0, 3);             /* 飛利浦標準,16位數據長度 */
   
    /* 初始化I2S*/
    i2s_set_samplerate_bits_sample(SAMPLE_RATE,
                                         I2S_BITS_PER_SAMPLE_16BIT);
    i2s_trx_start();                  /* 開啓I2S */
    recoder_remindmsg_show(0);
}

該函數就是用我們前面介紹的方法,激活ES8388的PCM模式,本章,我們使用的是44.1Khz採樣率,16位單聲道線性PCM模式。
由於最後要把錄音寫入到文件,這裏需要準備wav的文件頭,為方便,我們定義了一個__WaveHeader結構體來定義文件頭的數據字節,這個結構體包含了前面提到的wav文件的數據結構塊:

Typedef struct
{
    ChunkRIFF riff;               /*riff塊 */
    ChunkFMT fmt;                 /*fmt塊 */
    //ChunkFACT fact;             /*fact塊 線性PCM,沒有這個結構體 */
    ChunkDATA data;               /*data塊 */
}__WaveHeader;

我們定義一個recoder_wav_init()函數方便初始化文件信息,代碼如下:

voidrecoder_wav_init(__WaveHeader *wavhead)
{
    wavhead->riff.ChunkID= 0x46464952;    /* RIFF" */
    wavhead->riff.ChunkSize= 0;             /* 還未確定,最後需要計算 */
    wavhead->riff.Format= 0x45564157;     /*"WAVE" */
    wavhead->fmt.ChunkID= 0x20746D66;     /* "fmt" */
wavhead->fmt.ChunkSize= 16;             /* 大小為16個字節 */
/*0x01,表示PCM; 0x00,表示IMA ADPCM */
    wavhead->fmt.AudioFormat= 0x01;
    wavhead->fmt.NumOfChannels= 2;          /* 雙聲道 */
    wavhead->fmt.SampleRate= SAMPLE_RATE; /* 採樣速率 */
wavhead->fmt.ByteRate= wavhead->fmt.SampleRate* 4;
/* 字節速率=採樣率*通道數*(ADC位數/8) */
    wavhead->fmt.BlockAlign= 4;             /* 塊大小=通道數*(ADC位數/8) */
    wavhead->fmt.BitsPerSample= 16;         /* 16位PCM*/
    wavhead->data.ChunkID= 0x61746164;     /*"data" */
    wavhead->data.ChunkSize= 0;             /* 數據大小,還需要計算 */
}

錄音完成我們還要重新計算錄音文件的大小寫入文件頭,以保證音頻文件能正常被解析。我們把這些數據直接按順序寫入文件即可完成錄音操作,結合文件操作和按鍵功能定義,我們用wav_recoder()函數實現錄音過程,代碼如下:

/**
* @brief      WAV錄音
* @param      無
* @retval     無
*/
voidwav_recorder(void)
{
    uint8_t res;
    uint8_t key;
    uint8_t rval = 0;
    uint32_t bw;
   
    __WaveHeader *wavhead= 0;
   
    /* 目錄 */
    FF_DIR recdir;
   
    /* 錄音文件 */
    FIL *f_rec;
   
    /* 數據緩存指針 */
    uint8_t *pdatabuf;
   
    /* 文件名稱 */
    uint8_t *pname = 0;
   
    /* 錄音時間 */
    uint32_t recsec = 0;
   
    /* 計時器 */
    uint8_t timecnt = 0;
    uint16_t bytes_read = 0;
    /* 打開錄音文件夾 */
    while (f_opendir(&recdir, "0:/RECORDER"))
    {
        lcd_show_string(30, 230, 240, 16, 16, "RECORDERfolder error!", RED);
        vTaskDelay(200);
        
        /* 清除顯示 */
        lcd_fill(30, 230, 240, 246, WHITE);
        vTaskDelay(200);
        
        /* 創建該目錄 */
        f_mkdir("0:/RECORDER");
    }
    /* 錄音存儲區 */
    pdatabuf =malloc(1024 * 10);
   
    /* 開闢FIL字節的內存區域 */
    f_rec = (FIL*)malloc(sizeof(FIL));
   
    /* 開闢__WaveHeader字節的內存區域 */
    wavhead = (__WaveHeader*)malloc(sizeof(__WaveHeader));
   
    /* 申請30個字節內存,文件名類似"0:RECORDER/REC00001.wav"*/
    pname =malloc(30);
    if (!f_rec || !wavhead|| !pname || !pdatabuf)
    {
        /* 任意一項失敗, 則失敗 */
        rval = 1;
    }
    if (rval == 0)
    {
        /* 進入錄音模式,此時耳機可以聽到咪頭採集到的音頻 */
        recoder_enter_rec_mode();
        
        /* pname沒有任何文件名 */
        pname[0] = 0;
        while (rval == 0)
        {
            key =xl9555_key_scan(0);
            switch (key)
            {
                /* STOP&SAVE*/
                caseKEY2_PRES:
               
                    /* 有錄音 */
                    if (g_rec_sta& 0x80)
                    {
                        /* 關閉錄音 */
                        g_rec_sta = 0;
                        
                        /* 整個文件的大小-8; */
                        wavhead->riff.ChunkSize= g_wav_size + 36;
                        
                        /* 數據大小 */
                        wavhead->data.ChunkSize= g_wav_size;
                        
                        /* 偏移到文件頭. */
                        f_lseek(f_rec, 0);
                        
                        /* 寫入頭數據 */
                        f_write(f_rec,
                                 (const void *)wavhead,
                                  sizeof(__WaveHeader),
                                  &bw);
                                
                        f_close(f_rec);
                        g_wav_size = 0;
                    }
                    g_rec_sta = 0;
                    recsec = 0;
                    
                    /* 關閉DS0 */
                    LED(1);
                    
                    /* 清除顯示,清除之前顯示的錄音文件名 */
                    lcd_fill(30,
                               190,
                               lcd_self.width,
                               lcd_self.height,
                               WHITE);
                    break;
                /* REC/PAUSE */
                caseKEY0_PRES:
               
                    /* 如果是暫停,繼續錄音 */
                    if (g_rec_sta& 0x01)
                    {
                        /* 取消暫停 */
                        g_rec_sta &= 0xFE;
                    }
                    
                    /* 已經在錄音了,暫停 */
                    else if (g_rec_sta& 0x80)
                    {
                        /* 暫停 */
                        g_rec_sta |= 0x01;
                    }
                    
                    /* 還沒開始錄音 */
                    else
                    {
                        recsec = 0;
                        
                        /* 得到新的名字 */
                        recoder_new_pathname(pname);
                        text_show_string(30,
                                            190,
                                           lcd_self.width,
                                            16,
                                            "錄製:",
                                            16,
                                            0,
                                            RED);
                        
                        /* 顯示當前錄音文件名字 */
                        text_show_string(30 + 40,
                                            190,
                                           lcd_self.width,
                                            16,
                                            (char *)pname + 11,
                                            16,
                                            0,
                                            RED);
                        
                        /* 初始化wav數據 */
                        recoder_wav_init(wavhead);
                        
                        /* 打開文件 */
                        res =f_open(f_rec,
                                       (const TCHAR*)pname,
                                       FA_CREATE_ALWAYS |
                                       FA_WRITE);
                        
                        /* 文件創建失敗 */
                        if (res)
                        {
                            /* 創建文件失敗,不能錄音 */
                            g_rec_sta = 0;
                           
                            /* 提示是否存在SD卡 */
                            rval = 0xFE;
                        }
                        else
                        {
                            /* 寫入頭數據 */
                            res =f_write(f_rec,
                                            (const void *)wavhead,
                                             sizeof(__WaveHeader),
                                            (UINT*)&bw);
                                         
                            recoder_msg_show(0, 0);
                           
                            /* 開始錄音 */
                            g_rec_sta |= 0x80;
                        }
                    }
                    if (g_rec_sta& 0x01)
                    {
                        /* 提示正在暫停 */
                        LED(0);
                    }
                    else
                    {
                        LED(1);
                    }
                    break;
                /* 播放最近一段錄音 */
                caseKEY3_PRES:
               
                    /* 沒有在錄音 */
                    if (g_rec_sta!= 0x80)
                    {
                        /* 如果按鍵被按下,且pname不為空 */
                        if (pname[0])
                        {
                            text_show_string(30,
                            190,
                            lcd_self.width, 16, "播放:", 16, 0, RED);
                           
                            /* 顯示當播放的文件名字 */
                            text_show_string(30 + 40,
                                               190,
                                               lcd_self.width,
                                               16,
                                               (char *)pname + 11,
                                               16,
                                               0,
                                               RED);
                           
                            /* 進入播放模式 */
                           recoder_enter_play_mode();
                           
                            /* 播放pname */
                            audio_play_song(pname);
                           
                            /* 清除顯示,清除之前顯示的錄音文件名 */
                            lcd_fill(30,
                                       190,
                                       lcd_self.width,
                                       lcd_self.height,
                                       WHITE);
                           
                            /* 重新進入錄音模式 */
                           recoder_enter_rec_mode();
                        }
                    }
                    break;
            }
            if ((g_rec_sta& 0x80) == 0x80)
            {
                if ((g_rec_sta& 0x01) == 0x00)
                {
                    bytes_read =i2s_rx_read((uint8_t *)pdatabuf, 1024 * 10);
                    
                    /* 寫入文件 */
                    res =f_write(f_rec, pdatabuf,bytes_read, (UINT*)&bw);
                    if (res)
                    {
                        printf("writeerror:%d\r\n", res);
                    }
                    /* WAV數據大小增加 */
                    g_wav_size +=bytes_read;
                }
            }
            else
            {
                vTaskDelay(1);
            }
            timecnt++;
            if ((timecnt% 20) == 0)
            {
                /* LED閃爍 */
                LED_TOGGLE();
            }
            /* 錄音時間顯示 */
            if (recsec!= (g_wav_size / wavhead->fmt.ByteRate))
            {
                /* 錄音時間 */
                recsec =g_wav_size / wavhead->fmt.ByteRate;
               
                /* 顯示碼率 */
                recoder_msg_show(recsec,
                                    wavhead->fmt.SampleRate*
                                    wavhead->fmt.NumOfChannels*
                                    wavhead->fmt.BitsPerSample);
            }
        }
    }
    /* 釋放內存 */
    free(pdatabuf);
   
    /* 釋放內存 */
    free(f_rec);
   
    /* 釋放內存 */
    free(wavhead);
   
    /* 釋放內存 */
free(pname);
}

42.3.4 CMakeLists.txt文件

打開本實驗BSP下的CMakeLists.txt文件,其內容如下所示:

set(src_dirs
            IIC
            LCD
            LED
            SDIO
            SPI
            XL9555
            ES8388
            I2S)
set(include_dirs
            IIC
            LCD
            LED
            SDIO
            SPI
            XL9555
            ES8388
            I2S)
set(requires
            driver
            fatfs)
idf_component_register(SRC_DIRS${src_dirs}
INCLUDE_DIRS ${include_dirs}REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)

上述的紅色I2C、ES8388驅動需要由開發者自行添加,以確保錄音驅動能夠順利集成到構建系統中。這一步驟是必不可少的,它確保了錄音驅動的正確性和可用性,為後續的開發工作提供了堅實的基礎。
打開本實驗main文件下的CMakeLists.txt文件,其內容如下所示:

idf_component_register(
    SRC_DIRS
        "."
        "app"
    INCLUDE_DIRS
        "."
        "app")

上述的紅色app驅動需要由開發者自行添加,在此便不做贅述了。

42.3.5 實驗應用代碼

打開main/main.c文件,該文件定義了工程入口函數,名為app_main。該函數代碼如下。

i2c_obj_ti2c0_master;
/**
* @brief      程序入口
* @param      無
* @retval     無
*/
voidapp_main(void)
{
    esp_err_t ret;
    uint8_t key = 0;
    /* 初始化NVS*/
    ret =nvs_flash_init();
if (ret ==ESP_ERR_NVS_NO_FREE_PAGES ||
        ret ==ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret =nvs_flash_init();
    }
    /* 初始化LED*/
    led_init();
   
    /* 初始化IIC0*/
    i2c0_master =iic_init(I2C_NUM_0);
   
    /* 初始化SPI*/
    spi2_init();
   
    /* 初始化IO擴展芯片 */  
    xl9555_init(i2c0_master);
   
    /* 初始化LCD*/
    lcd_init();
    /* ES8388初始化 */
    es8388_init(i2c0_master);
   
    /* 設置耳機音量 */
    es8388_hpvol_set(25);
   
    /* 設置喇叭音量 */
    es8388_spkvol_set(25);
   
    /* 打開喇叭 */
    xl9555_pin_write(SPK_EN_IO,0);
   
    /* I2S初始化 */
    i2s_init();
    /* 檢測不到SD卡 */
    while (sd_spi_init())
    {
        lcd_show_string(30, 110, 200, 16, 16, "SDCard Error!", RED);
        vTaskDelay(500);
        lcd_show_string(30, 130, 200, 16, 16, "PleaseCheck! ", RED);
        vTaskDelay(500);
    }
    /* 檢查字庫 */
    while (fonts_init())
    {
        /* 清屏 */
        lcd_clear(WHITE);
        lcd_show_string(30, 30, 200, 16, 16, "ESP32-S3", RED);
        
        /* 更新字庫 */
        key =fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);
        /* 更新失敗 */
        while (key)
        {
            lcd_show_string(30, 50, 200, 16, 16, "FontUpdate Failed!", RED);
            vTaskDelay(200);
            lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
            vTaskDelay(200);
        }
        lcd_show_string(30, 50, 200, 16, 16, "FontUpdate Success!   ", RED);
        vTaskDelay(1500);
        
        /* 清屏 */
        lcd_clear(WHITE);
    }
   
    /* 為fatfs相關變量申請內存 */
    ret =exfuns_init();
   
    /* 實驗信息顯示延時 */
    vTaskDelay(500);
    text_show_string(30, 50, 200, 16, "正點原子ESP32開發板", 16, 0, RED);
    text_show_string(30, 70, 200, 16, "WAV錄音機 實驗", 16, 0, RED);
    text_show_string(30, 90, 200, 16, "正點原子@ALIENTEK", 16, 0, RED);
    while (1)
    {
        /* 錄音 */
        wav_recorder();
    }
}

可以看到main函數與音樂播放器實驗十分類似,封裝好了APP,main函數會精簡很多。

42.4 下載驗證

在代碼編譯成功之後,我們下載代碼到正點原子DNESP32S3開發板上,先初始化各外設,然後檢測字庫是否存在,如果檢測無問題,再檢測SD卡根目錄是否存在RECORDER文件夾,如果不存在則創建,如果創建失敗,則報錯。在找到SD卡的RECORDER文件夾後,即進入錄音模式(包括配置ES8388和I²S等),此時可以在耳機(或喇叭)聽到採集到的音頻。KEY0用於開始/暫停錄音,KEY2用於保存並停止錄音,KEY3用於播放最近一次的錄音。

圖42.4.1 錄音機實驗界面
此時,我們按下KEY0就開始錄音了,此時看到屏幕顯示錄音文件的名字以及錄音時長,如圖42.4.2所示:

圖42.4.2 錄音進行中
在錄音的時候按下KEY0則執行暫停/繼續錄音的切換,通過LED指示錄音暫停。通過按下KEY2,可以停止當前錄音,並保存錄音文件。在完成一次錄音文件保存之後,我們可以通過按KEY3按鍵,來實現播放這個錄音文件(即播放最近一次的錄音文件),實現試聽。
我們可以把錄音完成的wav文件放到電腦上,可以通過一些播放軟件播放並查看詳細的音頻編碼信息,本例程使用的是KMPlayer播放,查看到的信息如圖42.4.3所示:

圖42.4.3 錄音文件屬性
這和我們程序設計時的效果一樣,通過電腦端的播放器可以直接播放我們所錄的音頻。經實測,效果還是非常不錯的。

user avatar justin_lu 頭像 zeebj 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.