第四十二章录音机实验
上一章,我们实现了一个简单的音乐播放器,本章我们将在上一章的基础上,继续用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录音模式,我们编写代码如下: /** * @retval 无 */ void recoder_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()函数方便初始化文件信息,代码如下: void recoder_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 无 */ void wav_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; /* 计时器 */ uint16_t bytes_read = 0; /* 打开录音文件夹 */ while (f_opendir(&recdir, "0:/RECORDER")) { lcd_show_string(30, 230, 240, 16, 16, "RECORDER folder 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 */ case KEY2_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 */ case KEY0_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; /* 播放最近一段录音 */ case KEY3_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("write error:%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_t i2c0_master; /** * @brief 程序入口 * @param 无 * @retval 无 */ void app_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, "SD Card Error!", RED); vTaskDelay(500); lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", 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, "Font Update Failed!", RED); vTaskDelay(200); lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE); vTaskDelay(200); } lcd_show_string(30, 50, 200, 16, 16, "Font Update 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 录音文件属性 这和我们程序设计时的效果一样,通过电脑端的播放器可以直接播放我们所录的音频。经实测,效果还是非常不错的。
|