在 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 卡后在电脑上查看录音文件:
播放此文件效果良好,没有发生破音、间断等异常情况。成功。
引用: Jacktang 发表于 2021-2-28 22:10 乐鑫的这个 Korvo 板搭载百度鸿鹄语音芯片 期待更多的测评分享
不是的。 语音算法全是在ESP32上软件实现。
请教一个基础问题:
在语音控制里面创建任务
case 20:
xTaskCreatePinnedToCore()
然后在dumpPCMTask任务最有自己关闭任务,下次如果还想录音,是不是板子要重启了?
引用: liujing0146 发表于 2021-8-1 13:09 请教一个基础问题: 在语音控制里面创建任务 case 20: xTaskCreatePinnedToCore() 然后在dump ...
不用重启。 创建录音任务是由语音识别任务做的,停止录音以后可以再次创建任务。
cruelfox 发表于 2021-8-1 16:30 不用重启。 创建录音任务是由语音识别任务做的,停止录音以后可以再次创建任务。
谢谢解答
esp-skainet-master 代码我整个下载了,用VScode IDF插件打开,编译不了,只能看到两个顶层的C代码
处理函数aec_create()、mase_process()、ns_process()、esp_agc_process()
还有初始化函数codec_init()、rb_init()
都看不到源代码,是封装在哪个库里面吗?
还有个问题请教一下,vTaskDelete(NULL); 这个删除自身任务,放在死循环while (1)外面,什么时候会运行到? 谢谢!
引用: liujing0146 发表于 2021-8-4 10:06 cruelfox 发表于 2021-8-1 16:30 不用重启。 创建录音任务是由语音识别任务做的,停止录音以后可以再次创 ...
那些函数源文件在 esp-skainet 的某个 components 目录里面,你找找。 可能得手动添加路径才能被VSCode找到。我不用VSCode.
第二个问题,要是在死循环后面,就不会执行到。编译优化都给扔掉了。
首先感谢楼主的帖子
我的项目需要处理一个声音信号,片上处理不是很理想,所以想保存到TF卡用电脑分析数据。
折腾一天半,终于读出来了,学了挂载SD卡、文件操作。
其实第一天就读出来了,我用的I2S麦克风,I2S输入的数据是24位有效值,在ESP32内存中是32位INT存储,然后用freertos的队列发给保存任务,用 fwrite()写入文件,写入文件的是32位的int,用Cool Edit打开选 单通道 32位,后一步选成24位结果4个字节的数据全部用3个字节处理乱套了,播放出来的全部是杂音。