《ESP32-P4开发指南—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 程序流程图


图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用于播放最近一次的录音。


图44.4.1 录音机实验界面


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


图44.4.2 录音进行中


       在录音的时候按下KEY0则执行暂停/继续录音的切换,通过LED1指示录音暂停。通过按下KEY1,可以停止当前录音,并保存录音文件。在完成一次录音文件保存之后,我们可以通过按KEY2按键,来实现播放这个录音文件(即播放最近一次的录音文件),实现试听。

       我们可以把录音完成的wav文件放到电脑上,可以通过一些播放软件播放并查看详细的音频编码信息,本例程使用的是KMPlayer播放,查看到的信息如下图所示:


图44.4.3 录音文件属性


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


请使用浏览器的分享功能分享到微信等