图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 录音文件属性
这和我们程序设计时的效果一样,通过电脑端的播放器可以直接播放我们所录的音频。经实测,效果还是非常不错的。