第二十四章 音频录制实验
本章将介绍如何使用Kendryte K210的I2S功能,并将麦克风输入的声音数据以wav格式保存在SD卡中。通过学习本章内容,读者将掌握利用SDK编程技术实现wav音频文件的编码并保存的方法。 本章分为如下几个小节: 24.1 I2S录音简介 24.2 硬件设计 24.3 程序设计 24.4 运行验证
24.1 WAV和I2S简介 本章涉及的知识点基本上在上一章都有介绍。本章要实现WAV录音,还是和上一章一样,要了解:WAV文件格式和I2S接口。WAV文件格式和Kendryte K210的I2S功能,我们在上一章已经做了详细介绍了,这里就不作介绍了。 24.2 硬件设计 24.2.1 例程功能 1. 开机后,先初始化各外设,然后检测SD卡是否存在,如果不存在,则报错。在找到SD卡的RECORDER文件夹后,即进入录音模式(配置I2S和开启中断),此时LCD模块显示进入音频录制实验以及控制信息。KEY0用于开始录音,KEY1用于暂停或开启录音,KEY2用于保存并停止录音。 当我们按下KEY0的时候,可以在屏幕上看到录音时间,然后通过KEY2可以保存该文件,同时停止录音(文件名和时间也都将清零),按下KEY0开始录音的途中我们也可以通过KEY1暂停录音,再次按下KEY1会继续录音,直至按下KEY2可以保存该文件并停止录音。 24.2.2 硬件资源 1. 数字麦克风 IIS_SDIN - IO30 IIS_BCK - IO32 IIS_LRCK - IO33 24.2.3 原理图 本章实验内容,需要获取板载数字麦克风的音频数据,然后使用WAV编码后保存到文件系统中。 DNK210开发板上的数字麦克风的连接原理图,如下所示: 图24.2.3.1 数字功放NS4168连接原理图 关于该数字麦克风的使用方法,可参考MSM261S4030H0R的数据手册——《MSM261S4030H0R.pdf》,读者可在A盘à硬件资料à芯片资料下找到这份文档。 24.3 程序设计 24.3.1 mic驱动代码 本实验是通过I2S将数据输入到数字功放实现音频的播放,数字功放也需要相应的驱动代码进行配置,驱动源码包括两个文件:mic.c和mic.h,我们先介绍mic.h文件的内容。 /*****************************HARDWARE-PIN*********************************/ /* 硬件IO口,与原理图对应 */ #define PIN_SPK_CTRL (21) #define PIN_MIC_SDIN (30) #define PIN_MIC_BCK (32) #define PIN_MIC_WS (33) /*****************************SOFTWARE-GPIO********************************/ /* 软件GPIO口,与程序对应 */ #define SPK_CTRL_GPIONUM (0) /*****************************FUNC-GPIO************************************/ /* GPIO口的功能,绑定到硬件IO口 */ #define FUNC_SPK_CTRL (FUNC_GPIO0 + SPK_CTRL_GPIONUM) #define FUNC_MIC_WS FUNC_I2S0_WS #define FUNC_MIC_SDIN FUNC_I2S0_IN_D1 #define FUNC_MIC_BCK FUNC_I2S0_SCLK 这个是驱动引脚功能的绑定的宏,除了数据引脚不同外,其他和扬声器的引脚是相同的,也就是说DNK210开发板的音频播放和音频录制两个功能不可以同时使用,只能分步进行。 下面看mic.c文件代码。 /** * @param 无 * @retval 返回值 : 无 */ void mic_i2s_hardware_init(void) { /* I2S 初始化 */ gpio_init(); /* 使能GPIO的时钟 */ /* mic */ fpioa_set_function(PIN_MIC_WS, FUNC_MIC_WS); fpioa_set_function(PIN_MIC_SDIN, FUNC_MIC_SDIN); fpioa_set_function(PIN_MIC_BCK, FUNC_MIC_BCK); fpioa_set_function(PIN_SPK_CTRL, FUNC_SPK_CTRL); gpio_set_drive_mode(SPK_CTRL_GPIONUM, GPIO_DM_OUTPUT); /*输出模式*/ gpio_set_pin(SPK_CTRL_GPIONUM, GPIO_PV_LOW); /*输出为低,使能麦克风输入*/ } 麦克风的初始化代码和扬声器的也相识,不同的是数据引脚绑定的I2S输入功能,用于数据的接收,控制引脚拉低,使能数字功放输入。 24.3.2 recoder代码 recoder程序文件主要用于实现音频录制功能,驱动源码包括两个文件:recoder.c和recoder.h,我们先看下recoder.h文件。 #define REC_I2S_RX_DMA_BUF_SIZE 4096 /* 定义RX DMA 数组大小 */ #define REC_I2S_RX_FIFO_SIZE 10 /* 定义接收FIFO大小 */ #define MIC_GAIN 5 /* 麦克风增益值,可以根据实际调大录音的音量 */ #define REC_SAMPLERATE 16000 /* 采样率,44.1Khz */ 这四个宏分别是设置音频存放的缓存区大小、接收FIFO的数量、麦克风增益以及设置采样率。 下面看看recoder.c里面的几个函数,代码如下: /** * @brief 进入PCM 录音模式 * @param 无 * @retval 无 */ void recoder_enter_rec_mode(void) { /* I2S设备0初始化为接收模式 */ i2s_init(I2S_DEVICE_0, I2S_RECEIVER, 0x0C); /* 通道参数设置 */ i2s_rx_channel_config( I2S_DEVICE_0, /* I2S设备0 */ I2S_CHANNEL_1, /* 通道1 */ RESOLUTION_16_BIT, /* 接收数据16bit */ SCLK_CYCLES_32, /* 单个数据时钟为32 */ TRIGGER_LEVEL_4, /* FIFO深度为4 */ STANDARD_MODE); /* 标准模式 */ /* 设置采样率 */ i2s_set_sample_rate(I2S_DEVICE_0, REC_SAMPLERATE); /* 设置DMA中断回调 */ dmac_set_irq(DMAC_CHANNEL1, i2s_receive_dma_cb, NULL, 4); } 该函数用于初始化I2S外设,并将I2S配置为接收模式,设置采样率和DMA中断回调函数,我们使用的采样率为44.1Khz。 /** * @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; /* 大小为16个字节 */ wavhead->fmt.AudioFormat = 0X01; /* 0X01,表示PCM;0X01,表示IMA ADPCM */ wavhead->fmt.NumOfChannels = 2; /* 双声道 */ wavhead->fmt.SampleRate = REC_SAMPLERATE; /* 采样速率 */ 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; /* 数据大小,还需要计算 */ } recoder_wav_init()函数方便初始化wav文件信息。 /** * @brief WAV录音 * @param 无 * @retval 无 */ void wav_recorder(void) { uint8_t res, i; uint8_t key; uint8_t rval = 0; uint32_t bw; char datashow[15]; __WaveHeader *wavhead = 0; DIR recdir; /* 目录 */ FIL *f_rec = 0; /* 录音文件 */ uint8_t *pdatabuf; /* 数据缓存指针 */ uint8_t *pname = 0; uint32_t recsec = 0; /* 录音时间 */ while (f_opendir(&recdir, "0:/RECORDER")) /* 打开录音文件夹 */ { msleep(200); f_mkdir("0:/RECORDER"); /* 创建该目录 */ } /* 申请内存 */ for (i = 0; i < REC_I2S_RX_FIFO_SIZE; i++) { p_i2s_recfifo_buf = iomem_malloc(REC_I2S_RX_DMA_BUF_SIZE); /* I2S录音FIFO内存申请 */ if (p_i2s_recfifo_buf == NULL) { break; /* 申请失败 */ } } p_i2s_recbuf1 = iomem_malloc(REC_I2S_RX_DMA_BUF_SIZE / 2); p_i2s_recbuf2 = iomem_malloc(REC_I2S_RX_DMA_BUF_SIZE / 2); /* I2S录音内存申请 */ rx_buf = iomem_malloc(REC_I2S_RX_DMA_BUF_SIZE / 2); f_rec = (FIL *)iomem_malloc(sizeof(FIL)); /* 开辟FIL字节的内存区域 */ wavhead = (__WaveHeader *)iomem_malloc(sizeof(__WaveHeader)); /* 开辟__WaveHeader字节的内存区域 */ pname = iomem_malloc(30); /* 申请30个字节内存,类似"0:RECORDER/REC00001.wav" */ if (!p_i2s_recbuf2 || !f_rec || !wavhead || !pname)rval = 1; /* 任意一项失败, 则失败 */ if (rval == 0) { recoder_enter_rec_mode(); /* 进入录音模式 */ pname[0] = 0; /* pname没有任何文件名 */ while (rval == 0) { key = key_scan(0); switch (key) { case KEY0_PRES: /* 开始录音 */ recsec = 0; recoder_new_pathname(pname); /* 得到新的名字 */ recoder_wav_init(wavhead); /* 初始化wav数据 */ res = f_open(f_rec, (const TCHAR *)pname, FA_CREATE_ALWAYS | FA_WRITE); if (res) /* 文件创建失败 */ { g_rec_sta = 0; /* 创建文件失败,不能录音 */ rval = 0XFE; /* 提示是否存在SD卡 */ } else { res = f_write(f_rec, (const void *)wavhead, sizeof(__WaveHeader), &bw); /* 写入头数据 */ g_rec_sta |= 0X80; /* 开始录音 */ /* I2S通过DMA接收数据,保存到rx_buf中 */ i2s_receive_data_dma(I2S_DEVICE_0, p_i2s_recbuf1, REC_I2S_RX_DMA_BUF_SIZE, DMAC_CHANNEL1); } break; case KEY1_PRES: /*REC/PAUSE */ if (g_rec_sta & 0X01) /* 原来是暂停,继续录音 */ { g_rec_sta &= 0XFE; /* 取消暂停 */ /* I2S通过DMA接收数据,保存到rx_buf中 */ i2s_receive_data_dma(I2S_DEVICE_0, p_i2s_recbuf1, REC_I2S_RX_DMA_BUF_SIZE, DMAC_CHANNEL1); } else if (g_rec_sta & 0X80) /* 已经在录音了,暂停 */ { g_rec_sta |= 0X01; /* 暂停 */ } else /* 还没开始录音 */ { recsec = 0; recoder_new_pathname(pname); /* 得到新的名字 */ recoder_wav_init(wavhead); /* 初始化wav数据 */ res = f_open(f_rec, (const TCHAR *)pname, FA_CREATE_ALWAYS | FA_WRITE); if (res) /* 文件创建失败 */ { g_rec_sta = 0; /* 创建文件失败,不能录音 */ rval = 0XFE; /* 提示是否存在SD卡 */ } else { res = f_write(f_rec, (const void *)wavhead, sizeof(__WaveHeader), &bw); /* 写入头数据 */ g_rec_sta |= 0X80; /* 开始录音 */ /* I2S通过DMA接收数据,保存到rx_buf中 */ i2s_receive_data_dma(I2S_DEVICE_0, p_i2s_recbuf1, REC_I2S_RX_DMA_BUF_SIZE, DMAC_CHANNEL1); } } key = 0; break; case KEY2_PRES: /* STOP&SAVE */ if (g_rec_sta & 0X80) /* 有录音 */ { g_rec_sta = 0; /* 关闭录音 */ wavhead->riff.ChunkSize = g_wav_size + 36; /* 整个文件的大小-8; */ 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; g_index = 0; key = 0; break; } if (recoder_i2s_fifo_read(&pdatabuf))/*读取一次数据,读到数据了,写入文件*/ { res = f_write(f_rec, pdatabuf, REC_I2S_RX_DMA_BUF_SIZE, &bw); /* 写入文件 */ if (res) { printf("write error:%d\r\n", res); } g_wav_size += REC_I2S_RX_DMA_BUF_SIZE; /* WAV数据大小增加 */ } else { msleep(1); } if (recsec != (g_wav_size / wavhead->fmt.ByteRate)) /* 录音时间显示 */ { recsec = g_wav_size / wavhead->fmt.ByteRate; /* 录音时间 */ sprintf((char *)datashow, "time:%02d:%02d", (uint16_t)(recsec / 60), (uint16_t)(recsec % 60)); for (size_t i = 0; i < 320 * 16; i++) { lcd_gram = 0xFFFF; } draw_string_rgb565_image(lcd_gram, 320, 240, 10, 0, (char *)datashow, BLUE); lcd_draw_picture(0, 70, 320, 16, (uint16_t *)lcd_gram); } } } for (i = 0; i < REC_I2S_RX_FIFO_SIZE; i++) { iomem_free(p_i2s_recfifo_buf); /* SAI录音FIFO内存释放 */ } iomem_free(p_i2s_recbuf1); /* 释放内存 */ iomem_free(p_i2s_recbuf2); /* 释放内存 */ iomem_free(f_rec); /* 释放内存 */ iomem_free(wavhead); /* 释放内存 */ iomem_free(pname); /* 释放内存 */ } wav_recorder函数是我们实现录音功能的主要函数,它首先是申请数个缓存区,然后将I2S按顺序存入这些缓存区中,再一个一个写入到SD卡保存,我们通过相应的按键控制录音的开始、暂停与继续、停止并保存录音文件等操作,录音完成我们还要重新计算录音文件的大小并写入wav文件头,以保证音频文件能正常被解析。 24.3.3 main.c代码 main.c中的代码如下所示: int main(void) { FRESULT res; FATFS fs; sysctl_pll_set_freq(SYSCTL_PLL0, 800000000); sysctl_pll_set_freq(SYSCTL_PLL1, 400000000); sysctl_pll_set_freq(SYSCTL_PLL2, 45158400); sysctl_set_power_mode(SYSCTL_POWER_BANK6, SYSCTL_POWER_V18); sysctl_set_power_mode(SYSCTL_POWER_BANK7, SYSCTL_POWER_V18); sysctl_set_spi0_dvp_data(1); plic_init(); sysctl_enable_irq(); dmac_init(); key_init(); /* 按键初始化 */ mic_i2s_hardware_init(); /* 麦克风初始化 */ lcd_init(); /* 初始化LCD */ lcd_set_direction(DIR_YX_LRUD); lcd_draw_string(10, 10, "RECODER ", RED); lcd_draw_string(10, 30, "KEY0:START ", RED); lcd_draw_string(10, 50, "KEY1:REC/PAUSE KEY2:STOP&SAVE", RED); /* 初始化SD卡*/ if (sd_init() != 0) { printf("SD card initialization failed!\n"); while (1); } printf("SD card initialization succeed!\n"); /* Filesystem mount SD card */ res = f_mount(&fs, _T("0:"), 1); if (res != FR_OK) { printf("SD card mount failed! Error code: %d\n", res); while (1); } printf("SD card mount succeed!\n"); while (1) { wav_recorder(); /* 开始录音 */ } } main函数代码具体流程大致是:首先完成系统级和用户级初始化工作,完成LCD、按键、麦克风、SD卡等初始化,然后挂载SD卡,挂载成功后进入录音函数,LCD模块显示按键控制相关信息。 24.4 运行验证 将DNK210开发板连接到电脑主机,通过VSCode将固件烧录到开发板中,程序启动后进入录音模式,我们按下KEY0控制DNK210开发板开始录音,可以看到LCD显示录音时间信息,LCD显示的内容如图24.4.1所示: 图24.4.1音频录制中运行效果图 录音完成后我们可以将SD卡插入读取器在电脑中播放录音。
|