《DNESP32P4开发指南_V1.0》第四十四章 录音实验

第四十四章 录音实验

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

image002

图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 录音模式
 * @param      	无
 * @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用于播放最近一次的录音。

image003

图44.4.1 录音机实验界面

此时,我们按下KEY0就开始录音了,此时看到屏幕显示录音文件的名字以及录音时长,如下图所示:

image005

图44.4.2 录音进行中

在录音的时候按下KEY0则执行暂停/继续录音的切换,通过LED1指示录音暂停。通过按下KEY1,可以停止当前录音,并保存录音文件。在完成一次录音文件保存之后,我们可以通过按KEY2按键,来实现播放这个录音文件(即播放最近一次的录音文件),实现试听。
我们可以把录音完成的wav文件放到电脑上,可以通过一些播放软件播放并查看详细的音频编码信息,本例程使用的是KMPlayer播放,查看到的信息如下图所示:

image007

图44.4.3 录音文件属性

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

posted @ 2026-08-25 09:22  正点原子  阅读(6)  评论(0)    收藏  举报