《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 录音文件属性

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

相关推荐
程序员cxuan14 小时前
腾讯又来一王炸,开源版 WorkBuddy 太夯了!
人工智能·后端·程序员
G.E.M.小白15 小时前
【Linux嵌入式蜂鸣器驱动开发】原理图分析、寄存器寻址、完整驱动+应用+Makefile
嵌入式·imx6ull·linux驱动开发·蜂鸣器驱动
阿宸科普15 小时前
液冷机组冷却设备,接触器怎么选?三大系列参数与工程校核要点
产品
用户8106294808115 小时前
热压机控制系统设计:PLC 状态机、PID 调节与压力闭环控制
嵌入式
穆梓兰煊15 小时前
WebAssembly AI 推理学习路线图
程序员
洞察万事局15 小时前
液冷机组冷却设备,接触器怎么选?一个电气设计的一线记录
产品
用户8106294808115 小时前
两台4600吨液压机如何协同工作?聊聊自动化产线的任务调度与节拍控制
嵌入式
穆梓东海15 小时前
公司从 400 多人缩减到 100 多人,我开始重新思考程序员的未来
程序员
newerp15 小时前
Execution Trace 深入实战:可视化调度与系统停顿分析
后端·程序员·go
勿信日志15 小时前
症状不指向根因:六个把我骗过的 bug
程序员