【ESP32-Korvo测评】(5)麦克风音频流抓取的实现
[复制链接]
在 Korvo 开发板上,语音识别需要的声音信号来自三颗 MEMS 模拟麦克风。经过板载 ADC 采样成 PCM 音频数据流之后,从 I2S 接口传入 ESP32. 与音频回放调用相似,程序通过 i2s_read() 函数获取来自 ADC 的 PCM 码流(每调用一次读取确定的一段长度)。但是从麦克风采集的音频信号并不直接用作语音识别的原始数据,而是要经过几步处理:
这个图是我根据例子中 recsrc.c 的代码画出来的,对应于 Korvo 开发板。在第一步,音频数据从 I2S 接口获取之后,交给 AEC (回声消除)算法处理函数进行3通道回声消除处理,回声消除后的音频写入 aec_rb 这个环形缓冲区 (ring buffer). 第二步,MASE (麦克风阵列语音增强) 任务从 aec_rb 获取3通道音频,由 MASE 算法处理后得到单声道的音频数据,写入 mase_rb 环形缓冲区。第三步,AGC (自动增益控制) 任务从 mase_rb 获取音频,对信号进行增益调整后写入 agc_rb 环形缓冲区。最后,语音识别算法再从 agc_rb 缓冲区中取音频进行识别计算。
为什么要用到几个环形缓冲区呢?这是因为语音处理是需要时间的,CPU不可能在每一个音频采样数据到来后立即完成处理——通常算法需要以一定长度的帧(frame)为单位进行处理。环形缓冲区起到 FIFO 的作用,在算法处理期间保证未处理的数据有地方存储。AEC算法之前并没有用环形缓冲区,或许是因为 i2s 软件驱动里面带的缓冲已经够用了。
为了后续的评测工作,我需要将音频数据实时地保存到 SD 卡上,以便评估音频质量。与前面做过的回放操作相比,就刚好是反过来了。不过写文件操作还不能影响已有的信号处理过程,因此我需要用单独的任务来进行文件操作,并使用另外独立的环形缓冲区。
原来的 ADC 音频获取代码片段是这样:
#elif defined CONFIG_ESP32_KORVO_V1_1_BOARD
i2s_read(I2S_NUM_1, rsp_in, 4 *AEC_FRAME_BYTES, &bytes_read, portMAX_DELAY);
for (int i = 0; i < AEC_FRAME_BYTES / 2; i++) {
aec_ref[i] = rsp_in[4 * i + 0];
aec_rec[i] = rsp_in[4 * i + 1];
aec_rec[i + AEC_FRAME_BYTES / 2] = rsp_in[4 * i + 3];
if (nch == 3)
{
aec_rec[i + AEC_FRAME_BYTES] = rsp_in[4 * i + 2];
}
}
aec_process(aec_handle, aec_rec, aec_ref, aec_out);
rb_write(rec_rb, aec_out, AEC_FRAME_BYTES * nch, portMAX_DELAY);
#endif
i2s_read() 调用读取长度是 AEC_FRAME_BYTES, 而存放读取数据的 rsp_in 是 AEC_FRAME_BYTES*I2S_CHANNEL_NUM 字节数的动态申请内存。这里通道数为4, 对应3只麦克风和一个回放参考通道。PCM 是 16-bit 16kHz 格式,因此每个帧采样长度是 AEC_FRAME_BYTES/2. 程序用一个循环把数据复制并重新排布,以满足 aec_process() 函数的数据格式要求。
现在想取出一个麦克风通道的原始音频数据,存入 SD 卡的文件中,就只需要将 aec_rec 的一部分写入文件就可以了。为了不增加过多的延迟,我将这部分数据写到一个环形缓冲区:在 aec_process() 之前,
if(dump_enabled)
rb_write(dump_rb, aec_rec, AEC_FRAME_BYTES, portMAX_DELAY);
另外编写一个写文件用的任务:
void dumpPCMTask(void *arg)
{
if(mount_sdcard())
{
static uint8_t fc;
FILE *fdump;
char fname[20];
uint8_t *buf = malloc(AEC_FRAME_BYTES);
fc++;
sprintf(fname, "/sdcard/rec%d.pcm",fc);
fdump=fopen(fname,"w");
if(fdump)
{
rb_reset(dump_rb);
dump_enabled=1;
while(dump_enabled)
{
rb_read(dump_rb, buf, AEC_FRAME_BYTES, 100);
fwrite(buf, AEC_FRAME_BYTES, 1, fdump);
}
fclose(fdump);
printf("End of dump: %s\n",fname);
}
free(buf);
esp_vfs_fat_sdmmc_unmount();
}
else
printf("Error: SD card not mounted!\n");
vTaskDelete(NULL);
}
启动和停止录音我就用两个语音命令来控制。把例子里默认的 speech_commands_action.c 编辑一下:
extern uint8_t dump_enabled;
extern void dumpPCMTask(void *);
void speech_commands_action(int command_id)
{
printf("Commands ID: %d.\n", command_id);
switch(command_id)
{
case 20: // start recording
xTaskCreatePinnedToCore(&dumpPCMTask, "dump", 4 * 1024, NULL, 8, NULL, 1);
break;
case 21: // stop recording
dump_enabled=0;
break;
}
}
抱怨一下:新增命令词要改工程的配置文件,然而改了以后make, 整个工程都被重新编译了,极不方便。
dumpPCMTask 的堆栈大小第一次设的 2kB 不够用,ESP32 出现异常重起了。改为 4kB 就好用了。
录了一段麦克风原始音频,取出 SD 卡后在电脑上查看录音文件:
播放此文件效果良好,没有发生破音、间断等异常情况。成功。
|