《ESP32-S3使用指南—IDF版 V1.6》第四十二章 录音机实验

第四十二章 录音机实验

上一章,我们实现了一个简单的音乐播放器,本章我们将在上一章的基础上,继续用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

  1. XL9555

IIC_SDA-IO41

IIC_SCL-IO42

  1. SPILCD

CS-IO21

SCK-IO12

SDA-IO11

DC-IO40(在P5端口,使用跳线帽将IO_SET和LCD_DC相连)

PWR- IO1_3(XL9555)

RST- IO1_2(XL9555)

  1. SD

CS-IO2

SCK-IO12

MOSI-IO11

MISO-IO13

  1. 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

  1. 开发板板载的咪头或自备麦克风输入

  2. 喇叭或耳机

录音机实验与上一章(音乐播放器实验)用到的硬件资源基本一样,我们这里就不重复介绍原理图了,有差异的是这次我们用到板载的咪头用于信号输入,也可以通过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录音模式,我们编写代码如下:

scss 复制代码
/**

* @brief      进入PCM 录音模式

* [@param](http://www.openedv.com/home.php?mod=space&uid=271674)      无

* @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文件的数据结构块:

arduino 复制代码
Typedef struct

{

    ChunkRIFF riff;               /*riff块 */

    ChunkFMT fmt;                 /*fmt块 */

    //ChunkFACT fact;             /*fact块 线性PCM,没有这个结构体 */

    ChunkDATA data;               /*data块 */

}__WaveHeader;

我们定义一个recoder_wav_init()函数方便初始化文件信息,代码如下:

ini 复制代码
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()函数实现录音过程,代码如下:

scss 复制代码
/**

* @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文件,其内容如下所示:

scss 复制代码
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文件,其内容如下所示:

erlang 复制代码
idf_component_register(

    SRC_DIRS

        "."

        "app"

    INCLUDE_DIRS

        "."

        "app")

上述的红色app驱动需要由开发者自行添加,在此便不做赘述了。

42.3.5 实验应用代码

打开main/main.c文件,该文件定义了工程入口函数,名为app_main。该函数代码如下。

scss 复制代码
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 录音文件属性

这和我们程序设计时的效果一样,通过电脑端的播放器可以直接播放我们所录的音频。经实测,效果还是非常不错的。

相关推荐
radient3 小时前
属于Agent的课本 - RAG
人工智能·后端·程序员
京东云开发者1 天前
把算法焊死在模型上系列-后端眼中的RAG平台架构
程序员
京东云开发者1 天前
探索无限可能:生成式推荐的演进、前沿与挑战【AI业务应用方向】
程序员
SimonKing1 天前
Mybatis-Plus的竞争对手来了,试试 MyBatis-Flex
java·后端·程序员
文心快码BaiduComate1 天前
Comate Zulu实测:不会编程也能做软件?AI程序员现状令人震惊
java·程序员·前端框架
大模型教程1 天前
告别数据隐私焦虑!用FastGPT免费私有化部署AI个人知识管理系统!
程序员·llm·agent
DyLatte1 天前
迷失自我与忘记初心
程序员
大模型教程1 天前
非常适合初学者的大模型应用开发教程,快速掌握 LLM 开发技能
程序员·llm·agent
可爱的鸡仔1 天前
STM32--------ADC转换
stm32·单片机·嵌入式