【ESP32-Korvo测评】(5)麦克风音频流抓取的实现
<p> 在 Korvo 开发板上,语音识别需要的声音信号来自三颗 MEMS 模拟麦克风。经过板载 ADC 采样成 PCM 音频数据流之后,从 I2S 接口传入 ESP32. 与音频回放调用相似,程序通过 <span style="color:#c0392b;"><strong>i2s_read()</strong></span> 函数获取来自 ADC 的 PCM 码流(每调用一次读取确定的一段长度)。但是从麦克风采集的音频信号并不直接用作语音识别的原始数据,而是要经过几步处理:</p><p></p>
<p> 这个图是我根据例子中 recsrc.c 的代码画出来的,对应于 Korvo 开发板。在第一步,音频数据从 I2S 接口获取之后,交给 AEC (回声消除)算法处理函数进行3通道回声消除处理,回声消除后的音频写入 aec_rb 这个环形缓冲区 (ring buffer). 第二步,MASE (麦克风阵列语音增强) 任务从 aec_rb 获取3通道音频,由 MASE 算法处理后得到单声道的音频数据,写入 mase_rb 环形缓冲区。第三步,AGC (自动增益控制) 任务从 mase_rb 获取音频,对信号进行增益调整后写入 agc_rb 环形缓冲区。最后,语音识别算法再从 agc_rb 缓冲区中取音频进行识别计算。<br />
为什么要用到几个环形缓冲区呢?这是因为语音处理是需要时间的,CPU不可能在每一个音频采样数据到来后立即完成处理——通常算法需要以一定长度的帧(frame)为单位进行处理。环形缓冲区起到 FIFO 的作用,在算法处理期间保证未处理的数据有地方存储。AEC算法之前并没有用环形缓冲区,或许是因为 i2s 软件驱动里面带的缓冲已经够用了。</p>
<p> </p>
<p> 为了后续的评测工作,我需要将音频数据实时地保存到 SD 卡上,以便评估音频质量。与前面做过的回放操作相比,就刚好是反过来了。不过写文件操作还不能影响已有的信号处理过程,因此我需要用单独的任务来进行文件操作,并使用另外独立的环形缓冲区。<br />
原来的 ADC 音频获取代码片段是这样:</p>
<pre>
<code class="language-cpp">#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 = rsp_in;
aec_rec = rsp_in;
aec_rec = rsp_in;
if (nch == 3)
{
aec_rec = rsp_in;
}
}
aec_process(aec_handle, aec_rec, aec_ref, aec_out);
rb_write(rec_rb, aec_out, AEC_FRAME_BYTES * nch, portMAX_DELAY);
#endif</code></pre>
<p> 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() 函数的数据格式要求。</p>
<p> </p>
<p> 现在想取出一个麦克风通道的原始音频数据,存入 SD 卡的文件中,就只需要将 aec_rec 的一部分写入文件就可以了。为了不增加过多的延迟,我将这部分数据写到一个环形缓冲区:在 aec_process() 之前,</p>
<pre>
<code class="language-cpp"> if(dump_enabled)
rb_write(dump_rb, aec_rec, AEC_FRAME_BYTES, portMAX_DELAY);</code></pre>
<p> 另外编写一个写文件用的任务:</p>
<pre>
<code class="language-cpp">void dumpPCMTask(void *arg)
{
if(mount_sdcard())
{
static uint8_t fc;
FILE *fdump;
char fname;
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);
}</code></pre>
<p> </p>
<p> 启动和停止录音我就用两个语音命令来控制。把例子里默认的 speech_commands_action.c 编辑一下:</p>
<pre>
<code class="language-cpp">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;
}
}</code></pre>
<p> 抱怨一下:新增命令词要改工程的配置文件,然而改了以后make, 整个工程都被重新编译了,极不方便。</p>
<p> dumpPCMTask 的堆栈大小第一次设的 2kB 不够用,ESP32 出现异常重起了。改为 4kB 就好用了。</p>
<p> </p>
<p> 录了一段麦克风原始音频,取出 SD 卡后在电脑上查看录音文件:<br />
播放此文件效果良好,没有发生破音、间断等异常情况。成功。</p>
<p>感谢分享~~赞</p>
<p>乐鑫的这个 Korvo 板搭载百度鸿鹄语音芯片</p>
<p>期待更多的测评分享</p>
Jacktang 发表于 2021-2-28 22:10
乐鑫的这个 Korvo 板搭载百度鸿鹄语音芯片
期待更多的测评分享
<p>不是的。 语音算法全是在ESP32上软件实现。</p>
<table cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td>
<p>感谢分享</p>
</td>
</tr>
</tbody>
</table>
<p>文章非常有用,让我受益匪浅!</p>
<p>谢谢分享!</p>
<p>请教一个基础问题:</p>
<p>在语音控制里面创建任务</p>
<p>case 20:</p>
<p>xTaskCreatePinnedToCore()</p>
<p>然后在dumpPCMTask任务最有自己关闭任务,下次如果还想录音,是不是板子要重启了?</p>
<p> </p>
<p> </p>
liujing0146 发表于 2021-8-1 13:09
请教一个基础问题:
在语音控制里面创建任务
case 20:
xTaskCreatePinnedToCore()
然后在dump ...
<p>不用重启。 创建录音任务是由语音识别任务做的,停止录音以后可以再次创建任务。</p>
<div class="quote">
<blockquote><font size="2"><a href="forum.php?mod=redirect&goto=findpost&pid=3081521&ptid=1157722" target="_blank"><font color="#999999">cruelfox 发表于 2021-8-1 16:30</font></a></font> 不用重启。 创建录音任务是由语音识别任务做的,停止录音以后可以再次创建任务。</blockquote>
</div>
<p>谢谢解答</p>
<p>esp-skainet-master 代码我整个下载了,用VScode IDF插件打开,编译不了,只能看到两个顶层的C代码</p>
<p>处理函数aec_create()、mase_process()、ns_process()、esp_agc_process() </p>
<p>还有初始化函数codec_init()、rb_init()</p>
<p>都看不到源代码,是封装在哪个库里面吗?</p>
<p>还有个问题请教一下,vTaskDelete(NULL); 这个删除自身任务,放在死循环while (1)外面,什么时候会运行到? 谢谢!</p>
liujing0146 发表于 2021-8-4 10:06
cruelfox 发表于 2021-8-1 16:30 不用重启。 创建录音任务是由语音识别任务做的,停止录音以后可以再次创 ...
<p>那些函数源文件在 esp-skainet 的某个 components 目录里面,你找找。 可能得手动添加路径才能被VSCode找到。我不用VSCode.</p>
<p>第二个问题,要是在死循环后面,就不会执行到。编译优化都给扔掉了。</p>
<p>首先感谢楼主的帖子</p>
<p>我的项目需要处理一个声音信号,片上处理不是很理想,所以想保存到TF卡用电脑分析数据。</p>
<p>折腾一天半,终于读出来了,学了挂载SD卡、文件操作。</p>
<p>其实第一天就读出来了,我用的I2S麦克风,I2S输入的数据是24位有效值,在ESP32内存中是32位INT存储,然后用freertos的队列发给保存任务,用 fwrite()写入文件,写入文件的是32位的int,用Cool Edit打开选 单通道 32位,后一步选成24位结果4个字节的数据全部用3个字节处理乱套了,播放出来的全部是杂音。</p>
页:
[1]