VPA (Voice Process Algorithm)移植指南*
1.概述*
本文主要讲解如何部署自己的算法在 SDK 上,适用于第三方算法公司阅读。
- vpa 是 GX830X芯片SDK 专门用来处理音频算法的子系统。
- 可以在 vpa 中部署各种音频算法,包括前处理和神经网络模型。
2.vpa 目录结构*
- vpa 目录结构如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
. ├── algorithm_package │ └── bypass │ ├── bypass.mk │ └── vpa_process.c ├── base │ ├── base.mk │ ├── vpa_ae.c │ ├── vpa_context.c │ └── vpa_helper.c ├── include │ ├── vpa_ae.h │ ├── vpa_context.h │ ├── vpa_helper.h │ └── vpa_process.h └── vpa.mk 4 directories, 11 files
2.1.algorithm_package 介绍*
-
algorithm_package 下面的每一个目录都是一个独立的算法包,各算法包之间不允许相互调用。
重要
algorithm_package 下的每一个子目录是一个独立的算法包,方便用户进行算法版本管理,不推荐直接基于 algorithm_package 进行算法开发。
2.1.2.bypass 算法包介绍*
-
该算法包实现了对 mic 、 ref 拷贝到 output 通道中,并根据应用层的配置来决定是否交织output通道中的数据
重要
该算法包仅仅用于示范,不推荐直接基于 bypass 目录进行算法开发。
2.2.base 目录介绍*
- 该目录包含了 3 个c源码文件:
- vpa_ae.c: 实现了 FFT、IFFT 相关 API 接口。
- vpa_context.c:根据 app 的参数完成 context 的初始化。
- vpa_helper.c:提供了与 context 相关的 api 接口供开发者使用。
3.如何构建自己的算法包*
algorithm_package 下面的每一个目录都是一个独立的算法包,各算法包之间不允许相互调用。本节我们参考 bypass 来构建属于自己的算法包。
- 拷贝 bypass 算法目录,重命名为需要移植的算法, 例如改为 gx_lib,并将 bypass.mk 重命名为 gx_lib.mk,且将 gx_lib.mk 中的 BYPASS 改为 GX_LIB,
-
在 vpa.mk 中增加如下代码:
gx_lib.mk 1 2 3
include $(VPA_DIR)algorithm_package/gx_lib/gx_lib.mk VPA_OBJS += $(GX_LIB_OBJS) VPA_INCS += $(GX_LIB_INCS)
-
之后就可以在 gx_lib 中进行算法移植开发了。
-
构建完 gx_lib 后,在 apps 对应的 app 中 app.mk 加上下面两句话,就可以编译到 algorithm_package/gx_lib 算法包了
app.mk 1 2
BUILD_VPA_ENABLE := 1 BUILD_VPA_GX_LIB_ENABLE := 1
4.如何移植自己的算法*
- 供 apps 调用的几个算法 api 接口见 vpa_process.h 三个接口,这三个接口均在 vpa_porcess.c 中实现。
-
vpa_process.c 中必须包含如下三个接口的实现:
vpa_process.c 1 2 3 4 5 6 7 8 9 10 11 12
int32_t vpa_init(VPA_CONTEXT_HEADER *context_header, void *priv) { ... // 省略1一亿行代码 } int32_t vpa_done(void) { ... // 省略1一亿行代码 } int32_t vpa_process_active(VPA_CONTEXT *context) { ... // 省略1一亿行代码 }
4.1.算法初始化:vpa_init*
- 算法需要初始化的代码放在 vpa_init 接口中,这个接口会在上电后调用一次
- 示例代码如下:
vpa_process.c -->> vpa_init(VPA_CONTEXT_HEADER *context_header, void *priv) 1 2 3 4 5 6 7 8
int32_t vpa_init(VPA_CONTEXT_HEADER *context_header, void *priv) { printf(LOG_TAG" [%s]\n", __func__); vpa_top_init(context_header); gx_snpu_init(); return 0; }
4.2.算法的处理:vpa_process_active*
- 算法处理的入口函数在 vpa_process.c 中的 vpa_process_active,所有的算法都是在这个函数中进行处理,这个函数会在初始化后,根据配置的context的时长回调。
- 示例代码如下:
vpa_process.c -->> vpa_process_active(VPA_CONTEXT *context) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
int32_t vpa_process_active(VPA_CONTEXT *context) { uint64_t start_us = gx_get_time_us(); // 刷 cache,使 cache 内数据无效 vpa_process_invalidate_mic_buffer(context); vpa_process_invalidate_ref_buffer(context); // 将 mic、ref 拷贝到 output vpa_copy_mic_to_output(context, 0, CIB_MIC0, CIB_OUT0); vpa_copy_mic_to_output(context, 0, CIB_MIC0, CIB_OUT0 + 1); if (context->ctx_header->audio_io_config.ref_num > 0) vpa_copy_ref_to_output(context, 0, CIB_REF0, CIB_OUT0 + 2); // 对 output 进行交织 vpa_interleave_output_channels(context); // 刷 cache,将 cache 数据写回 内存 vpa_process_writeback_out_buffer(context); uint64_t end_us = gx_get_time_us(); vpa_top_run(start_us, end_us); return 0; }
5.context*
vpa 使用 context(处理上下文)来存放每个处理过程的输入、输出和中间过程数据,这个数据结构在 vpa_context.h 中定义.
5.1.context 结构介绍*
- context 有两部分内容:VPA_CONTEXT 和 VPA_CONTEXT_HEADER
- VPA_CONTEXT_HEADER 包含了一些不常变化的信息
vpa_context.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
typedef struct { uint32_t mic_num; // 1 - 2 uint32_t ref_num; // 0 - 1 uint32_t out_num; // 0 - 1 uint32_t out_interlaced_num; // 0: non-interlace, >0 Interweave the previous out_interlaced_num uint32_t frame_length; // 10ms, 16ms uint32_t sample_rate; // 16000 uint32_t sample_width; // 16, 32 uint32_t pcm_frame_num_per_context; // the FRAME num in a CONTEXT uint32_t context_num_per_channel_for_mic_ref; // the pcm_frame_num_per_context * context_num_per_channel_for_mic_ref frame num in a mic、ref CHANNEL uint32_t context_num_per_channel_for_out; // the pcm_frame_num_per_context * context_num_per_channel_for_out frame num in a output CHANNEL } AUDIO_IO_CONFIG; typedef struct { uint32_t version; AUDIO_IO_CONFIG audio_io_config; // CTX buffer for GLOBAL uint32_t ctx_num; // Context number void *ctx_buffer; // Context Buffer header point uint32_t ctx_size; // Context size // OUT buffer for CLOBAL void *out_buffer; uint32_t out_buffer_size; // Bytes // MIC buffer for GLOBAL void *mic_buffer; uint32_t mic_buffer_size; // Bytes // REF buffer for GLOBAL void *ref_buffer; uint32_t ref_buffer_size; // Bytes // TX buffer for GLOBAL void *tx_buffer; uint32_t tx_buffer_size; // Bytes } VPA_CONTEXT_HEADER;
- VSP_CONTEXT 则包含每一个单元数据的内容,其定义如下:
vpa_context.h 1 2 3 4 5 6 7 8 9
typedef struct { VPA_CONTEXT_HEADER *ctx_header; uint32_t frame_index; // FRAME index of the first frame in CONTEXT uint32_t ctx_index; // CONTEXT index from 0 - (2^32 - 1) uint32_t vad:3; uint32_t kws:13; uint32_t reserve1:16; uint32_t reserve2; } VPA_CONTEXT;
5.1.context 使用注意事项*
- 为了减少运算开销,context 采用了非常原始和直接的方式存放数据。而 context 中存放的内容与实际的算法实现和产品需求紧密相关。这一定程度上增加了 context 与各模块的耦合性,提高了 vpa 的开发难度,各模块的开发者必须了解其他模块如何处理Context的,并且根据整体的需求添加或删减字段。
- context 都存放在内存中,而 AE 和 CPU 都是通过 cache 访问内存的,所以在处理新的 context 前要清除 cache ,而处理完 Context 后要将 cache 写回到内存中
- VPA_CONTEXT_HEADER 包含了一些不常变化的信息
6.算力查看*
-
我们提供了实时查看算力的功能
- 首先需要在 vpa_init 接口中调用 vpa_top_init(context_header) 初始化 top 功能
- 其次在需要关注的算法接口前后调用相关接口接口,如下示例,最终 vpa_top_run 会输出对应的算力百分比。
uint64_t start_us = gx_get_time_us(); ...//关注的算法接口 uint64_t end_us = gx_get_time_us(); vpa_top_run(start_us, end_us);
7.内存分配接口使用*
- 建议开发者首先使用 vpa_malloc 和 vpa_free 接口,该接口申请的内存是从 psram 中申请。