超级版主
 
- 积分
- 5418
- 金钱
- 5418
- 注册时间
- 2019-5-8
- 在线时间
- 1404 小时
|
|
第四十四章 录音实验
1)实验平台:正点原子DNESP32P4开发板
2)章节摘自【正点原子】ESP32-P4开发指南— V1.0
3)购买链接:https://detail.tmall.com/item.htm?id=873309579825
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32P4.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子DNESP32S3开发板技术交流群:132780729
上一章,我们实现了一个简单的音乐播放器,本章我们将在上一章的基础上,继续用ES8388实现一个简单的录音机,录制WAV格式的录音。
本章分为如下几个部分:
44.1 I2S录音简介
44.2 硬件设计
44.3 软件设计
44.4 下载验证
44.1 I2S录音简介
本章涉及的知识点基本上在上一章都有介绍。本章要实现WAV录音,还是和上一章一样,要了解:WAV文件格式、ES8388和I2S接口。这些知识,我们在上一章已经做了详细介绍了,这里就不作介绍了。
44.2 硬件设计
44.2.1 程序功能
本章实验功能简介:开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,再检测SD卡根目录是否存在RECORDER文件夹,如果不存在则创建,如果创建失败,则报错。在找到SD卡的RECORDER文件夹后,即进入录音模式(包括配置ES8388和I2S等),此时可以在耳机(或喇叭)听到采集到的音频。KEY0用于开始/暂停录音,KEY1用于保存并停止录音,KEY2用于播放最近一次的录音。
当我们按下KEY0的时候,可以在屏幕上看到录音文件的名字、码率以及录音时间等,然后通过KEY1可以保存该文件,同时停止录音(文件名和时间也都将清零),在完成一段录音后,我们可以通过按KEY2按键,来试听刚刚的录音。LED0用于提示程序正在运行。
44.2.2 硬件资源
本实验,大家需要准备1个TF卡和一个耳机(非必备),分别插入SD卡接口和耳机接口,然后下载本实验就可以实现录音机的效果。实验用到的硬件资源如下:
1)LED灯
LED 0 - IO51
2)ES8388音频CODEC芯片,通过I2S驱动
3)I2S音频接口
I2S_BCK_IO - IO47
I2S_WS_IO - IO48
I2S_DO_IO - IO49
I2S_DI_IO - IO50
I2S_MCK_IO - IO46
4)XL9555
IIC_INT - IO36
IIC_SDA - IO33
IIC_SCL - IO32
EXIO_8 - KEY0
EXIO_9 - KEY1
EXIO_10 - KEY2
5)RGBLCD/MIPILCD(引脚太多,不罗列出来)
6)SPIFFS
7)SD卡
CMD - IO44
CLK - IO43
D0 - IO39
D1 - IO40
D2 - IO41
D3 - IO42
44.2.3 原理图
ES8388原理图已在43.2.3小节中详细阐述,为避免重复,此处不再赘述。
44.3 程序设计
44.3.1 I2S的IDF驱动
I2S相关函数,笔者已经在上一章节中讲解了,此处不再赘述。
44.3.2 程序流程图
图44.3.2.1 录音实验程序流程图
44.3.3 程序解析
在35_recoding例程是基于35_music实验编写的,所以笔者着重讲解有区别的文件驱动。
1,recorder驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码,main/APP/ AUDIO文件夹下recorder.c和recorder.h。
音乐播放器实验中,我们已经学习过配置ES8388的方法。本实验中,我们来学习ES8388的录音模式,进入PCM 录音模式函数如下:- /**
- * @brief 进入PCM 录音模式
- * [url=home.php?mod=space&uid=271674]@param[/url] 无
- * @retval 无
- */
- void recoder_enter_rec_mode(void)
- {
- myi2s_init();
- 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_i2s_cfg(0, 3); /* 飞利浦标准,16位数据长度 */
- i2s_set_samplerate_bits_sample(I2S_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; /* wav头 */
复制代码 我们定义一个recoder_wav_init函数方便初始化文件信息,代码如下:
- /**
- * @brief 初始化WAV头
- * @param wavhead : wav文件头指针
- * @retval 无
- */
- 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; /* PCM格式块大小为16个字节 */
- wavhead->fmt.AudioFormat = 0x01; /* 0x01,表示PCM; 0x00,表示IMA ADPCM */
- wavhead->fmt.NumOfChannels = 2; /* 双声道 */
- wavhead->fmt.SampleRate = 16000; /* 采样速率为16000Hz */
- wavhead->fmt.BitsPerSample = 16; /* 16位PCM */
- /* 块对齐=通道数*每个样本的字节数 */
- wavhead->fmt.BlockAlign = wavhead->fmt.NumOfChannels
- * (wavhead->fmt.BitsPerSample / 8);
- /* 字节速率=采样率*块对齐 */
- wavhead->fmt.ByteRate = wavhead->fmt.SampleRate * wavhead->fmt.BlockAlign;
- wavhead->data.ChunkID = 0x61746164; /* "data" */
- wavhead->data.ChunkSize = 0; /* 数据大小,最后需要计算 */
- }
复制代码 录音完成我们还要重新计算录音文件的大小写入文件头,以保证音频文件能正常被解析。我们把这些数据直接按顺序写入文件即可完成录音操作,结合文件操作和按键功能定义,我们用wav_recoder函数实现录音过程,代码如下:
- /**
- * @brief WAV录音任务
- * @param arg:传入任务句柄
- * @retval 无
- */
- static void recorder_task(void * arg)
- {
- QueueSetMemberHandle_t activate_member = NULL;
- UINT bw;
- size_t bytes_read = 0; /* 读取写入录音文件大小 */
- uint8_t file_write_res = 0; /* 上一次写入完成标志 */
- uint8_t rval = 0; /* 获取TF状态 */
- FF_DIR recdir; /* 目录 */
- FIL *f_rec = NULL; /* 录音文件 */
- static uint8_t *pdatabuf = NULL; /* 数据缓存指针 */
- static uint8_t *pdatabuf1 = NULL; /* 数据缓存指针 */
- uint8_t *pname = NULL; /* 文件名称存储buf */
- FSIZE_t recorder_file_read_pos = 0; /* 记录当前录音文件读取位置 */
- uint32_t g_wav_size_tatol = 0; /* wav数据大小(字节数,不包括文件头!!) */
- /* 资源申请内存 */
- pdatabuf = malloc(REC_RX_BUF_SIZE); /* 存储录音数据 */
- pdatabuf1 = malloc(REC_RX_BUF_SIZE); /* 存储录音数据 */
- f_rec = (FIL*)malloc(sizeof(FIL)); /* 文件指针 */
- wavhead = (__WaveHeader *)malloc(sizeof(__WaveHeader)); /* wav头部 */
- pname = malloc(255); /* 存储文件名称buf */
- if (!f_rec || !wavhead || !pname || !pdatabuf || !pdatabuf1)
- {
- goto exit;
- }
-
- memset(pdatabuf,0,REC_RX_BUF_SIZE);
- memset(pdatabuf1,0,REC_RX_BUF_SIZE);
- current_buffer = pdatabuf;
- next_buffer = pdatabuf1;
- /* 打开文件夹,若没有,则自动创建 */
- while (f_opendir(&recdir, "0:/RECORDER"))
- {
- f_mkdir("0:/RECORDER"); /* 创建该目录 */
- }
- xTaskNotifyGive(arg);
- /* 创建互斥锁,防止数据存储干扰 */
- i2s_mutex = xSemaphoreCreateRecursiveMutex();
- xSemaphoreGiveRecursive(i2s_mutex);
- recoder_enter_rec_mode(); /* 进入录音模式 */
- pname[0] = 0; /* pname没有任何文件名 */
- while (rval == 0)
- {
- /* 等待队列集中的队列接收到消息 */
- activate_member = xQueueSelectFromSet(xQueueSet, 10);
- if (activate_member == key0_xSemaphore)
- {
- xSemaphoreTake(activate_member, portMAX_DELAY);
- if (g_rec_sta & 0x01 && recorder_file_read_pos != 0
- && file_write_res == 1) /* 暂停录音 */
- {
- g_rec_sta &= 0xFE; /* 恢复录音 */
- }
- else if (g_rec_sta & 0x80 && file_write_res == 1) /* 正在录音 */
- {
- g_rec_sta |= 0x01; /* 暂停录音 */
- recorder_file_read_pos = f_tell(f_rec);/* 记录当前文件写到位置 */
- }
- else /* 未开始录音 */
- {
- recsec = 0;
- recoder_new_pathname(pname);
- text_show_string(30, 190, lcddev.width, 16, "录制:", 16, 0, RED);
- text_show_string(30 + 40, 190, lcddev.width, 16,
- (char *)pname + 11, 16, 0, RED);
- recoder_wav_init(wavhead); /* 初始化wav头 */
- i2s_set_samplerate_bits_sample(wavhead->fmt.SampleRate,
- I2S_BITS_PER_SAMPLE_16BIT);
- i2s_trx_start(); /* 开启I2S */
- rval =f_open(f_rec,(const TCHAR *)pname,FA_CREATE_ALWAYS
- | FA_WRITE);
- /* 打开文件失败 */
- if (rval != FR_OK)
- {
- g_rec_sta = 0; /* 关闭录音 */
- rval = 0xFE; /* 提示SD卡问题 */
- }
- else
- {
- f_lseek(f_rec, sizeof(__WaveHeader)); /* 偏移到数据存储地址 */
- recoder_msg_show(0, 0); /* 提示录音时长 */
- g_rec_sta |= 0x80; /* 开始录音 */
- }
- }
- }
- else if (activate_member == key1_xSemaphore)
- {
- xSemaphoreTake(activate_member, portMAX_DELAY);
- if ((g_rec_sta & 0x80 )&& (file_write_res == 1)
- &&(g_wav_size_tatol == g_wav_size)) /* 有录音且上一次录音写入完成 */
- {
- xSemaphoreTakeRecursive(i2s_mutex, portMAX_DELAY);
- f_lseek(f_rec, 0); /* 偏移到文件头 */
- g_rec_sta = 0;
- wavhead->riff.ChunkSize = g_wav_size + 36; /* 整个文件的大小-8; */
- wavhead->data.ChunkSize = g_wav_size; /* 数据大小 */
- f_write(f_rec, (const void *)wavhead, sizeof(__WaveHeader),
- (UINT *)&bw); /* 写入头数据 */
- f_close(f_rec);
- g_wav_size_tatol = 0;
- g_wav_size = 0;
- xSemaphoreGiveRecursive(i2s_mutex);
- }
- g_rec_sta = 0;
- recsec = 0;
- lcd_fill(30, 190, lcddev.width, lcddev.height, WHITE); /* 清除显示 */
- }
- else if (activate_member == key2_xSemaphore)
- {
- xSemaphoreTake(activate_member, portMAX_DELAY);
- if (g_rec_sta == 0 && file_write_res == 1)
- {
- text_show_string(30, 190, lcddev.width, 16, "播放:", 16, 0, RED);
- text_show_string(30 + 40, 190, lcddev.width, 16,
- (char *)pname + 11, 16, 0, RED);
- recoder_enter_play_mode(); /* 进入播放模式 */
- audio_play_song(pname); /* 播放录音 */
- lcd_fill(30, 190, lcddev.width, lcddev.height, WHITE)
- recoder_enter_rec_mode(); /* 回到录音模式 */
- g_rec_sta = 0;
- bytes_read = 0;
- memset(current_buffer,0,REC_RX_BUF_SIZE);
- }
- }
- /* 处理录音中数据写入与状态显示 */
- if (g_rec_sta & 0x80 && !(g_rec_sta & 0x01))
- {
- xSemaphoreTakeRecursive(i2s_mutex, portMAX_DELAY);
- file_write_res = 0;
- bytes_read = i2s_rx_read(current_buffer, REC_RX_BUF_SIZE);
- g_wav_size_tatol += bytes_read;
- if (bytes_read == REC_RX_BUF_SIZE)
- {
- if (recorder_file_read_pos != 0)
- {
- f_lseek(f_rec, recorder_file_read_pos);
- recorder_file_read_pos = 0;
- }
- /* 写入到文件当中 */
- rval = f_write(f_rec, current_buffer, REC_RX_BUF_SIZE,
- (UINT*)&bw);
- if (rval != FR_OK)
- {
- printf("write error:%d\r\n", rval);
- }
- else
- {
- file_write_res = 1;
- g_wav_size += bytes_read;
- /* 切换缓冲区 */
- uint8_t *temp = current_buffer;
- current_buffer = next_buffer;
- next_buffer = temp;
- }
- }
- xSemaphoreGiveRecursive(i2s_mutex);
- }
- }
- exit:
- /* 清理资源 */
- free(pdatabuf);
- free(f_rec);
- free(wavhead);
- free(pname);
- /* 删除任务 */
- vTaskDelete(NULL);
- }
- /**
- * @brief WAV录音
- * @param 无
- * @retval 无
- */
- void wav_recorder(void)
- {
- uint8_t key = 0;
- uint8_t timecnt = 0; /* 计时器 */
- /* 创建录音线程 */
- BaseType_t task_created = xTaskCreatePinnedToCore(recorder_task,
- "recorder_task",
- 4096,
- xTaskGetCurrentTaskHandle(),
- 10, NULL, 0);
- assert(task_created == pdTRUE);
- /* 等待录音线程的通知继续 */
- ulTaskNotifyTake(false, portMAX_DELAY);
- /* 创建队列集 */
- xQueueSet = xQueueCreateSet(3);
- /* 创建信号量 */
- key0_xSemaphore = xSemaphoreCreateBinary();
- key1_xSemaphore = xSemaphoreCreateBinary();
- key2_xSemaphore = xSemaphoreCreateBinary();
- /* 把信号量加入到队列集 */
- xQueueAddToSet(key0_xSemaphore, xQueueSet);
- xQueueAddToSet(key1_xSemaphore, xQueueSet);
- xQueueAddToSet(key2_xSemaphore, xQueueSet);
- while (1)
- {
- key = xl9555_key_scan(0);
- switch (key)
- {
- case KEY0_PRES:
- xSemaphoreGive(key0_xSemaphore);
- break;
- case KEY1_PRES:
- xSemaphoreGive(key1_xSemaphore);
- break;
- case KEY2_PRES:
- xSemaphoreGive(key2_xSemaphore);
- break;
- default:
- break;
- }
- uint32_t current_rec_time = g_wav_size / wavhead->fmt.ByteRate;
- timecnt++;
- if (timecnt % 20 == 0)
- {
- LED0_TOGGLE(); /* 闪烁提示 */
- }
- if (recsec != current_rec_time)
- {
- recsec = current_rec_time;
- recoder_msg_show(recsec, wavhead->fmt.SampleRate
- * wavhead->fmt.NumOfChannels * wavhead->fmt.BitsPerSample);
- }
- vTaskDelay(10);
- }
- }
复制代码 上述代码实现了一个基于FreeRTOS的WAV格式录音功能。通过recorder_task任务管理录音数据的采集、存储和播放。录音数据通过I2S接口从硬件采集,并使用缓冲区进行数据存储。录音文件采用WAV格式,包含头部信息,并存储在SD卡的指定目录下。用户可以通过按键控制录音的开始、暂停和停止,同时显示录音时长。
在wav_recorder函数中,创建并启动录音任务,初始化相关资源,并使用队列和信号量机制响应按键事件。任务通过读取I2S接口的数据、切换缓冲区并写入文件,实时更新录音状态和时长显示。
2,CMakeLists.txt文件
本例程的CMakeLists.txt跟音乐播放器实验是一致的,代码如下:
- set(src_dirs
- LED
- KEY
- MYIIC
- XL9555
- LCD
- ES8388
- MYI2S
- SDMMC)
- set(include_dirs
- LED
- KEY
- MYIIC
- XL9555
- LCD
- ES8388
- MYI2S
- SDMMC)
- set(requires
- driver
- esp_lcd
- esp_common
- 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)
复制代码 3,main.c代码
在main.c里面编写如下代码。
- void app_main(void)
- {
- esp_err_t ret;
- uint8_t key = 0;
- ret = nvs_flash_init(); /* 初始化NVS */
- if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
- {
- ESP_ERROR_CHECK(nvs_flash_erase());
- ESP_ERROR_CHECK(nvs_flash_init());
- }
- led_init(); /* LED初始化 */
- key_init(); /* KEY初始化 */
- myiic_init(); /* IIC0初始化 */
- xl9555_init(); /* XL9555初始化 */
- es8388_init(); /* es8388初始化 */
- lcd_init(); /* LCD屏初始化 */
- xl9555_pin_write(SPK_EN_IO,1); /* 关闭喇叭 */
- while (sdmmc_init()) /* 检测不到SD卡 */
- {
- lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
- vTaskDelay(pdMS_TO_TICKS(500));
- lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);
- vTaskDelay(pdMS_TO_TICKS(500));
- }
- while (fonts_init()) /* 检查字库 */
- {
- lcd_clear(WHITE); /* 清屏 */
- lcd_show_string(30, 30, 200, 16, 16, "ESP32-P3", 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(pdMS_TO_TICKS(200));
- }
- lcd_show_string(30, 50, 200, 16, 16, "Font Update Success! ", RED);
- vTaskDelay(pdMS_TO_TICKS(1500));
- lcd_clear(WHITE); /* 清屏 */
- }
- ret = exfuns_init(); /* 为fatfs相关变量申请内存 */
- vTaskDelay(pdMS_TO_TICKS(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(); /* 录音 */
- }
- }
复制代码 该函数就相对简单了,在初始化各个外设后,通过wav_recorder函数,开始音频播放,到这里本实验的代码基本就编写完成了。。
44.4 下载验证
在代码编译成功之后,我们下载代码到正点原子DNESP32P4开发板上,先初始化各外设,然后检测字库是否存在,如果检测无问题,再检测SD卡根目录是否存在RECORDER文件夹,如果不存在则创建,如果创建失败,则报错。在找到SD卡的RECORDER文件夹后,即进入录音模式(包括配置ES8388和I2S等),此时可以在耳机(或喇叭)听到采集到的音频。KEY0用于开始/暂停录音,KEY1用于保存并停止录音,KEY2用于播放最近一次的录音。
图44.4.1 录音机实验界面
此时,我们按下KEY0就开始录音了,此时看到屏幕显示录音文件的名字以及录音时长,如下图所示:
图44.4.2 录音进行中
在录音的时候按下KEY0则执行暂停/继续录音的切换,通过LED1指示录音暂停。通过按下KEY1,可以停止当前录音,并保存录音文件。在完成一次录音文件保存之后,我们可以通过按KEY2按键,来实现播放这个录音文件(即播放最近一次的录音文件),实现试听。
我们可以把录音完成的wav文件放到电脑上,可以通过一些播放软件播放并查看详细的音频编码信息,本例程使用的是KMPlayer播放,查看到的信息如下图所示:
图44.4.3 录音文件属性
这和我们程序设计时的效果一样,通过电脑端的播放器可以直接播放我们所录的音频。经实测,效果还是非常不错的。 |
|