cruelfox 发表于 2021-2-28 21:14

【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不可能在每一个音频采样数据到来后立即完成处理&mdash;&mdash;通常算法需要以一定长度的帧(frame)为单位进行处理。环形缓冲区起到 FIFO 的作用,在算法处理期间保证未处理的数据有地方存储。AEC算法之前并没有用环形缓冲区,或许是因为 i2s 软件驱动里面带的缓冲已经够用了。</p>

<p>&nbsp;</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, &amp;bytes_read, portMAX_DELAY);
      for (int i = 0; i &lt; 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>&nbsp;</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>&nbsp;</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(&amp;dumpPCMTask, "dump", 4 * 1024, NULL, 8, NULL, 1);
                                break;
                case 21:        // stop recording
                                dump_enabled=0;
                                break;

        }
}</code></pre>

<p>  抱怨一下:新增命令词要改工程的配置文件,然而改了以后make,&nbsp;整个工程都被重新编译了,极不方便。</p>

<p>  dumpPCMTask 的堆栈大小第一次设的 2kB 不够用,ESP32 出现异常重起了。改为 4kB 就好用了。</p>

<p>&nbsp;</p>

<p>  录了一段麦克风原始音频,取出 SD 卡后在电脑上查看录音文件:<br />
  播放此文件效果良好,没有发生破音、间断等异常情况。成功。</p>

annysky2012 发表于 2021-2-28 21:21

<p>感谢分享~~赞</p>

Jacktang 发表于 2021-2-28 22:10

<p>乐鑫的这个&nbsp;Korvo 板搭载百度鸿鹄语音芯片</p>

<p>期待更多的测评分享</p>

cruelfox 发表于 2021-2-28 23:30

Jacktang 发表于 2021-2-28 22:10
乐鑫的这个&nbsp;Korvo 板搭载百度鸿鹄语音芯片

期待更多的测评分享

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

dql2016 发表于 2021-3-1 09:11

<table cellpadding="0" cellspacing="0">
        <tbody>
                <tr>
                        <td>
                        <p>感谢分享</p>
                        </td>
                </tr>
        </tbody>
</table>

东莞市惠海半导体 发表于 2021-3-1 09:40

<p>文章非常有用,让我受益匪浅!</p>

freebsder 发表于 2021-3-1 22:01

<p>谢谢分享!</p>

liujing0146 发表于 2021-8-1 13:09

<p>请教一个基础问题:</p>

<p>在语音控制里面创建任务</p>

<p>case 20:</p>

<p>xTaskCreatePinnedToCore()</p>

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

<p>&nbsp;</p>

<p>&nbsp;</p>

cruelfox 发表于 2021-8-1 16:30

liujing0146 发表于 2021-8-1 13:09
请教一个基础问题:

在语音控制里面创建任务

case 20:

xTaskCreatePinnedToCore()

然后在dump ...

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

liujing0146 发表于 2021-8-4 10:06

<div class="quote">
<blockquote><font size="2"><a href="forum.php?mod=redirect&amp;goto=findpost&amp;pid=3081521&amp;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()&nbsp; &nbsp;</p>

<p>还有初始化函数codec_init()、rb_init()</p>

<p>都看不到源代码,是封装在哪个库里面吗?</p>

<p>还有个问题请教一下,vTaskDelete(NULL);&nbsp; 这个删除自身任务,放在死循环while&nbsp;(1)外面,什么时候会运行到?&nbsp; 谢谢!</p>

cruelfox 发表于 2021-8-4 10:33

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

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

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

liujing0146 发表于 2021-9-7 12:05

<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]
查看完整版本: 【ESP32-Korvo测评】(5)麦克风音频流抓取的实现