Custom decimation filters¶
The Decimator supports 1-, 2-, or 3-stage decimation pipelines configured via custom filters.
This flexibility allows applications to tailor the filter chain to their specific latency and computational requirements.
Custom filters must comply with implementation requirements for each stage. See Filter stage constraints for details on stage-specific constraints.
This document explains how to design and deploy custom decimation filters for 1-, 2-, or 3-stage configurations.
Designing a custom filter¶
A filter design script is provided in python/filter_design/design_filter.py.
The script contains functions to generate the filters currently provided as part
of lib_mic_array and save them as .pkl files. Using these functions as
a guide, the script can be extended to generate custom filters tailored to the
application’s needs.
Note that in Decimator,
the filters are implemented
using fixed-point arithmetic, which requires the coefficients to be presented
in a specific format.
The helper scripts python/stage1.py and python/stage2.py generate
the correctly quantized and interleaved coefficient arrays required by the
library.
After generating a .pkl file, the helper scripts stage1.py and
stage2.py can be used to format the first and second stage filters,
respectively, into the fixed-point C arrays required by the library.
Alternatively, the combined.py script can process both the first and second
stage filters in one step. When executed, stage1.py, stage2.py, and
combined.py print the filter coefficients as C-style arrays, along with
filter-related defines such as tap count, decimation factor, etc., as
#define macros on stdout.
The scripts can also be run with the --file-prefix <prefix> (or
-fp <prefix>) option. In this mode, all arrays and defines are written into
a header file (<prefix>.h), which can be included in the application.
Running python combined.py --help from the python directory shows full
usage instructions.
From the python directory, the workflow is typically:
python filter_design/design_filter.py # generates the
# .pkl files
python combined.py <custom_filter.pkl> -fp <prefix> # converts the
# .pkl file to C
# arrays for stage
# 1 and 2, and
# writes to
# <prefix>.h
Using custom filters¶
When using the Decimator provided by the
library, the mic_array_init_custom_filter() function is used to
initialize a mic array instance with a custom decimation filter.
The mic_array_conf_t structure is populated with the decimator and
PDM RX configurations before calling
mic_array_init_custom_filter(). In particular, the application must:
Allocate all filter coefficient buffers, filter state buffers, and PDM RX buffers referenced by the decimator and PDM RX configurations, and ensure they persist for the lifetime of the mic array instance (until
mic_array_start()returns).Populate the decimator pipeline configuration (
mic_array_decimator_conf_t) and the PDM RX configuration (pdm_rx_conf_t) structures.
Note
When designing custom filters as described in
Designing a custom filter, the filter coefficient arrays and associated
configuration parameters can be written to a header file by running
combined.py. This file is included in the application, and the decimation
filter coefficients and other parameters in
mic_array_filter_conf_t are populated from it.
The application must still allocate persistent runtime buffers for the
filter delay line (mic_array_filter_conf_t) and for the
PDM RX input and output buffers
(pdm_rx_conf_t).
Below is a snippet from the app_custom_filter source code
showing how the mic_array_conf_t structure is populated:
#include "good_2_stage_filter.h" // Autogenerated by running 'python combined.py filter_design/good_2_stage_filter_int.pkl -fp good_2_stage_filter'
void init_mic_conf(mic_array_conf_t &mic_array_conf, mic_array_filter_conf_t (&filter_conf)[2], unsigned *channel_map)
{
static int32_t stg1_filter_state[APP_MIC_COUNT][8];
static int32_t stg2_filter_state[APP_MIC_COUNT][GOOD_2_STAGE_FILTER_STG2_TAP_COUNT];
memset(&mic_array_conf, 0, sizeof(mic_array_conf_t));
//decimator
mic_array_conf.decimator_conf.filter_conf = &filter_conf[0];
mic_array_conf.decimator_conf.num_filter_stages = 2;
// filter stage 1
filter_conf[0].coef = (int32_t*)good_2_stage_filter_stg1_coef;
filter_conf[0].num_taps = GOOD_2_STAGE_FILTER_STG1_TAP_COUNT;
filter_conf[0].decimation_factor = GOOD_2_STAGE_FILTER_STG1_DECIMATION_FACTOR;
filter_conf[0].state = (int32_t*)stg1_filter_state;
filter_conf[0].shr = GOOD_2_STAGE_FILTER_STG1_SHR;
filter_conf[0].state_words_per_channel = filter_conf[0].num_taps/32; // works on 1-bit samples
// filter stage 2
filter_conf[1].coef = (int32_t*)good_2_stage_filter_stg2_coef;
filter_conf[1].num_taps = GOOD_2_STAGE_FILTER_STG2_TAP_COUNT;
filter_conf[1].decimation_factor = GOOD_2_STAGE_FILTER_STG2_DECIMATION_FACTOR;
filter_conf[1].state = (int32_t*)stg2_filter_state;
filter_conf[1].shr = GOOD_2_STAGE_FILTER_STG2_SHR;
filter_conf[1].state_words_per_channel = GOOD_2_STAGE_FILTER_STG2_TAP_COUNT;
// pdm rx
static uint32_t pdmrx_out_block[APP_MIC_COUNT][GOOD_2_STAGE_FILTER_STG2_DECIMATION_FACTOR];
static uint32_t __attribute__((aligned(8))) pdmrx_out_block_double_buf[2][APP_MIC_COUNT * GOOD_2_STAGE_FILTER_STG2_DECIMATION_FACTOR];
mic_array_conf.pdmrx_conf.pdm_out_words_per_channel = GOOD_2_STAGE_FILTER_STG2_DECIMATION_FACTOR;
mic_array_conf.pdmrx_conf.pdm_out_block = (uint32_t*)pdmrx_out_block;
mic_array_conf.pdmrx_conf.pdm_in_double_buf = (uint32_t*)pdmrx_out_block_double_buf;
mic_array_conf.pdmrx_conf.channel_map = channel_map;
}
Once mic_array_conf_t is populated, mic array can be initialised by calling mic_array_init_custom_filter()
and started by calling mic_array_start():
unsigned channel_map[2] = {0,1};
mic_array_conf_t mic_array_conf;
mic_array_filter_conf_t filter_conf[2];
init_mic_conf(mic_array_conf, filter_conf, channel_map);
mic_array_init_custom_filter(&pdm_res, &mic_array_conf);
par {
mic_array_start((chanend_t) c_audio_frames);
<... other tasks ...>
}
Note
Apart from calling mic_array_init_custom_filter() instead of
mic_array_init(), all other aspects of using the custom filter API, such
as including the mic array in an application, declaring resources, and overriding
build-time default configuration, are exactly the same as in the default usage
model described in Using lib_mic_array.
Filter stage constraints¶
Stage 1 (mandatory)
The first stage decimator has fixed constraints that cannot be changed:
Tap count:
256(fixed)Decimation factor:
32(fixed)Implementation: Must be compatible with
fir_1x16_bit()as described in Filter implementation (Stage 1)
Only the filter coefficients may be customized. The coefficients must be quantized to 16-bit precision
and formatted appropriately for the VPU implementation. Use the Python helper script python/stage1.py
to convert floating-point coefficients to the required format.
Stage 2 (optional)
If a second stage is included, it must meet these requirements:
Implementation: Must be compatible with the 32-bit FIR filter from lib_xcore_math, specifically
xs3_filter_fir_s32()as described in Filter implementation (Stage 2)Tap count: Configurable (no fixed constraint)
Decimation factor: Configurable integer value
Use the Python helper script python/stage2.py to convert floating-point coefficients to the required format.
Stage 3 (optional)
A third stage, if included, must also be compatible with the 32-bit FIR filter from lib_xcore_math. It has the same flexibility as stage 2:
Tap count: Configurable
Decimation factor: Configurable integer value
Single-stage decimator configuration
Single-stage decimation is a special case in which additional decimation is
expected to be performed in the application. This is useful when downstream
decimation requirements are not directly represented by the integer-factor FIR
stages used by Decimator (for example,
rational-factor resampling).
When only stage 1 is used, the decimation factor is fixed at 32. If a
different final output sample rate is required, the application must perform
the remaining decimation after receiving the stage-1 output from the mic array.
Because further decimation is expected downstream, single-stage operation has the following additional constraints:
The DCOE filter is forcibly disabled, and
MIC_ARRAY_CONFIG_USE_DC_ELIMINATIONis ignored. If required, the application must apply DC elimination after all decimation is complete.The number of output words per channel from PDM RX (
pdm_rx_conf_t.pdm_out_words_per_channel) must matchMIC_ARRAY_CONFIG_SAMPLES_PER_FRAME.pdm_rx_conf_t.pdm_out_words_per_channelmust not exceedmic_array::MicArray::MAX_PDM_OUT_WORDS_PER_CHANNEL.
These two conditions are checked at runtime. If either condition is violated, the mic array initialization asserts.