[MCU] 【ESP32-Korvo测评】(5)麦克风音频流抓取的实现

cruelfox   2021-2-28 21:14 楼主

  在 Korvo 开发板上,语音识别需要的声音信号来自三颗 MEMS 模拟麦克风。经过板载 ADC 采样成 PCM 音频数据流之后,从 I2S 接口传入 ESP32. 与音频回放调用相似,程序通过 i2s_read() 函数获取来自 ADC 的 PCM 码流(每调用一次读取确定的一段长度)。但是从麦克风采集的音频信号并不直接用作语音识别的原始数据,而是要经过几步处理:

flow.PNG

  这个图是我根据例子中 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 卡后在电脑上查看录音文件:
wave.PNG   播放此文件效果良好,没有发生破音、间断等异常情况。成功。

回复评论 (11)

感谢分享~~赞

没有什么不可以,我就是我,不一样的烟火! 
点赞  2021-2-28 21:21

乐鑫的这个 Korvo 板搭载百度鸿鹄语音芯片

期待更多的测评分享

点赞  2021-2-28 22:10
引用: Jacktang 发表于 2021-2-28 22:10 乐鑫的这个 Korvo 板搭载百度鸿鹄语音芯片 期待更多的测评分享

不是的。 语音算法全是在ESP32上软件实现。

点赞  2021-2-28 23:30

感谢分享

点赞  2021-3-1 09:11

文章非常有用,让我受益匪浅!

点赞  2021-3-1 09:40

谢谢分享!

默认摸鱼,再摸鱼。2022、9、28
点赞  2021-3-1 22:01

请教一个基础问题:

在语音控制里面创建任务

case 20:

xTaskCreatePinnedToCore()

然后在dumpPCMTask任务最有自己关闭任务,下次如果还想录音,是不是板子要重启了?

 

 

点赞  2021-8-1 13:09
引用: liujing0146 发表于 2021-8-1 13:09 请教一个基础问题: 在语音控制里面创建任务 case 20: xTaskCreatePinnedToCore() 然后在dump ...

不用重启。 创建录音任务是由语音识别任务做的,停止录音以后可以再次创建任务。

点赞  2021-8-1 16:30
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)外面,什么时候会运行到?  谢谢!

点赞  2021-8-4 10:06
引用: liujing0146 发表于 2021-8-4 10:06 cruelfox 发表于 2021-8-1 16:30 不用重启。 创建录音任务是由语音识别任务做的,停止录音以后可以再次创 ...

那些函数源文件在 esp-skainet 的某个 components 目录里面,你找找。 可能得手动添加路径才能被VSCode找到。我不用VSCode.

第二个问题,要是在死循环后面,就不会执行到。编译优化都给扔掉了。

点赞 (1) 2021-8-4 10:33

首先感谢楼主的帖子

我的项目需要处理一个声音信号,片上处理不是很理想,所以想保存到TF卡用电脑分析数据。

折腾一天半,终于读出来了,学了挂载SD卡、文件操作。

其实第一天就读出来了,我用的I2S麦克风,I2S输入的数据是24位有效值,在ESP32内存中是32位INT存储,然后用freertos的队列发给保存任务,用 fwrite()写入文件,写入文件的是32位的int,用Cool Edit打开选 单通道 32位,后一步选成24位结果4个字节的数据全部用3个字节处理乱套了,播放出来的全部是杂音。

点赞  2021-9-7 12:05
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复