From 423993ce543670dc6a690e783b3d96165eb3b96d Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Wed, 14 Oct 2026 17:29:27 +0100 Subject: [PATCH 01/10] submanifests: tflm: add TensorFlow Lite Micro and ML submanifest Add west submanifest for TensorFlow Lite Micro (TFLM) to integrate ML inference directly into the SOF build tree and replace obsolete out-of-tree tensorflow-clone.sh script. Co-developed-by: Liam Girdwood Signed-off-by: Liam Girdwood Co-developed-by: Naveen Manohar Signed-off-by: Naveen Manohar Signed-off-by: Seppo Ingalsuo --- scripts/tensorflow-clone.sh | 75 ------------------------------------- submanifests/tflm.yml | 33 ++++++++++++++++ 2 files changed, 33 insertions(+), 75 deletions(-) delete mode 100755 scripts/tensorflow-clone.sh create mode 100644 submanifests/tflm.yml diff --git a/scripts/tensorflow-clone.sh b/scripts/tensorflow-clone.sh deleted file mode 100755 index d3f67d0a835e..000000000000 --- a/scripts/tensorflow-clone.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: BSD-3-Clause -# Copyright(c) 2025 Intel Corporation. All rights reserved. - -# fail immediately on any errors -set -e - -# Array of TFLM Git repository URLs. Add or remove repositories as needed. -declare -a REPOS=( - "https://github.com/thesofproject/nnlib-hifi4" - "https://github.com/tensorflow/tflite-micro" - "https://github.com/thesofproject/flatbuffers" - "https://github.com/google/gemmlowp" - "https://github.com/google/ruy" -) - -# Commit ID to check for (optional). If specified, the script will update -# the repository if this commit ID is not found. Leave empty to skip. -declare -a COMMIT_ID=( - "cdedfb1a1044eb774915de21b63a1b6aa93276f6" - "e86d97b6237f88ab5925c0b41e3e3589a1560d86" - "f5acabf4e1a3fcba024081bb1871a2ed59aa1c28" - "719139ce755a0f31cbf1c37f7f98adcc7fc9f425" - "d37128311b445e758136b8602d1bbd2a755e115d" -) - -# Directory where repositories will be cloned/updated. -BASE_DIR="$HOME/work/sof" # Or any other desired location - -# Function to check if a commit ID exists in a repository -check_commit() { - local repo_dir="$1" - local commit_id="$2" - - if [ -z "$commit_id" ]; then - return 0 # Skip check if no commit ID is provided - fi - - if ! git -C "$repo_dir" rev-parse --quiet --verify "$commit_id" >/dev/null 2>&1; then - return 1 # Commit ID not found - else - return 0 # Commit ID found - fi -} - - -# Function to update the repository -update_repo() { - local repo_dir="$1" - echo "Updating repository: $repo_dir" - git -C "$repo_dir" fetch --all - git -C "$repo_dir" pull -} - -# Main script logic -mkdir -p "$BASE_DIR" - -for ((i = 0; i < ${#REPOS[@]}; i++)); do - echo "Counter: $i, Value: ${REPOS[i]}" - repo_url=${REPOS[i]} - - repo_name=$(basename "$repo_url" .git) # Extract repo name - repo_dir="$BASE_DIR/$repo_name" - - if [ ! -d "$repo_dir" ]; then - echo "Cloning repository: $repo_url" - git clone "$repo_url" "$repo_dir" || { echo "git clone failed for $repo_url"; exit 1; } - elif ! check_commit "$repo_dir" "${COMMIT_ID[i]}"; then - update_repo "$repo_dir" - else - echo "Repository $repo_name is up to date." - fi -done - -echo "All repositories processed." diff --git a/submanifests/tflm.yml b/submanifests/tflm.yml new file mode 100644 index 000000000000..a55ce39ff33a --- /dev/null +++ b/submanifests/tflm.yml @@ -0,0 +1,33 @@ +--- +# TensorFlow Lite Micro (TFLM) and ML dependencies for Sound Open Firmware +# +# These projects provide the TFLM runtime, mathematical kernels, and neural network +# acceleration libraries used by MicroWakeWord (mww), TensorFlow Classifier (tflmcly), +# MFCC, and PCAN audio processing components. + +manifest: + projects: + - name: tflite-micro + url: https://github.com/tensorflow/tflite-micro + revision: e86d97b6237f88ab5925c0b41e3e3589a1560d86 + path: tflite-micro + + - name: flatbuffers + url: https://github.com/thesofproject/flatbuffers + revision: f5acabf4e1a3fcba024081bb1871a2ed59aa1c28 + path: flatbuffers + + - name: nnlib-hifi4 + url: https://github.com/thesofproject/nnlib-hifi4 + revision: cdedfb1a1044eb774915de21b63a1b6aa93276f6 + path: nnlib-hifi4 + + - name: gemmlowp + url: https://github.com/google/gemmlowp + revision: 719139ce755a0f31cbf1c37f7f98adcc7fc9f425 + path: gemmlowp + + - name: ruy + url: https://github.com/google/ruy + revision: d37128311b445e758136b8602d1bbd2a755e115d + path: ruy From ee49cce05bdeed0d4d3dc613cee6ed09c21a79c2 Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Fri, 9 Oct 2026 21:56:45 +0100 Subject: [PATCH 02/10] math: audio: add PCAN (Per-Channel AGC Normalization) support Add PCAN (Per-Channel AGC Normalization) fixed-point math library for normalizing mel filterbank energies. PCAN estimates per-channel noise background, applies non-linear wide-dynamic-range AGC compression with fixed-point LUT gain lookup, and delivers scaled features for streaming keyword-spotting networks. Include ztest test suite with MATLAB reference models covering gain lookup, noise estimation, dynamic shrinking, stream processing, and corner cases. Co-developed-by: Liam Girdwood Signed-off-by: Liam Girdwood Signed-off-by: Seppo Ingalsuo --- src/include/sof/math/auditory.h | 4 + src/include/sof/math/pcan.h | 135 ++++++++++ src/math/CMakeLists.txt | 4 + src/math/Kconfig | 12 + src/math/auditory/mel_filterbank_32.c | 77 +++++- src/math/pcan/CMakeLists.txt | 39 +++ src/math/pcan/README.md | 105 ++++++++ src/math/pcan/pcan.c | 246 ++++++++++++++++++ test/cmocka/src/math/auditory/CMakeLists.txt | 1 + .../unit/math/advanced/pcan/CMakeLists.txt | 61 +++++ .../math/advanced/pcan/pcan_gain_lookup.m | 77 ++++++ .../math/advanced/pcan/pcan_noise_estimate.m | 37 +++ .../unit/math/advanced/pcan/pcan_process.m | 55 ++++ .../unit/math/advanced/pcan/pcan_shrink.m | 41 +++ .../ztest/unit/math/advanced/pcan/pcan_test.c | 173 ++++++++++++ .../advanced/pcan/pcan_wide_dynamic_func.m | 56 ++++ test/ztest/unit/math/advanced/pcan/prj.conf | 2 + test/ztest/unit/math/advanced/pcan/ref_pcan.m | 212 +++++++++++++++ .../math/advanced/pcan/ref_pcan_corners.h | 28 ++ .../unit/math/advanced/pcan/ref_pcan_func.h | 47 ++++ .../unit/math/advanced/pcan/ref_pcan_lut.h | 45 ++++ .../unit/math/advanced/pcan/ref_pcan_stream.h | 132 ++++++++++ .../unit/math/advanced/pcan/testcase.yaml | 14 + 23 files changed, 1589 insertions(+), 14 deletions(-) create mode 100644 src/include/sof/math/pcan.h create mode 100644 src/math/pcan/CMakeLists.txt create mode 100644 src/math/pcan/README.md create mode 100644 src/math/pcan/pcan.c create mode 100644 test/ztest/unit/math/advanced/pcan/CMakeLists.txt create mode 100644 test/ztest/unit/math/advanced/pcan/pcan_gain_lookup.m create mode 100644 test/ztest/unit/math/advanced/pcan/pcan_noise_estimate.m create mode 100644 test/ztest/unit/math/advanced/pcan/pcan_process.m create mode 100644 test/ztest/unit/math/advanced/pcan/pcan_shrink.m create mode 100644 test/ztest/unit/math/advanced/pcan/pcan_test.c create mode 100644 test/ztest/unit/math/advanced/pcan/pcan_wide_dynamic_func.m create mode 100644 test/ztest/unit/math/advanced/pcan/prj.conf create mode 100644 test/ztest/unit/math/advanced/pcan/ref_pcan.m create mode 100644 test/ztest/unit/math/advanced/pcan/ref_pcan_corners.h create mode 100644 test/ztest/unit/math/advanced/pcan/ref_pcan_func.h create mode 100644 test/ztest/unit/math/advanced/pcan/ref_pcan_lut.h create mode 100644 test/ztest/unit/math/advanced/pcan/ref_pcan_stream.h create mode 100644 test/ztest/unit/math/advanced/pcan/testcase.yaml diff --git a/src/include/sof/math/auditory.h b/src/include/sof/math/auditory.h index a68f0468d591..32c07803c613 100644 --- a/src/include/sof/math/auditory.h +++ b/src/include/sof/math/auditory.h @@ -111,4 +111,8 @@ void psy_apply_mel_filterbank_16(struct psy_mel_filterbank *mel_fb, struct icomp void psy_apply_mel_filterbank_32(struct psy_mel_filterbank *mel_fb, struct icomplex32 *fft_out, int32_t *power_spectra, int32_t *mel_log, int bitshift); +void psy_apply_mel_filterbank_with_linear_32(struct psy_mel_filterbank *mel_fb, struct icomplex32 *fft_out, + int32_t *power_spectra, int32_t *mel_log, + uint32_t *mel_linear, int bitshift); + #endif /* __SOF_MATH_AUDITORY_H__ */ diff --git a/src/include/sof/math/pcan.h b/src/include/sof/math/pcan.h new file mode 100644 index 000000000000..494e193026b4 --- /dev/null +++ b/src/include/sof/math/pcan.h @@ -0,0 +1,135 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. All rights reserved. + * + * Author: Antigravity AI & SOF Team + */ + +/** + * \file include/sof/math/pcan.h + * \brief Per-Channel AGC Normalization (PCAN) library interface + * + * Wraps and integrates Google's upstream TFLite Micro microfrontend + * library (under Apache-2.0 with patent grant protection). + */ + +#ifndef __SOF_MATH_PCAN_H__ +#define __SOF_MATH_PCAN_H__ + +#include +#include +#include + +#include "tensorflow/lite/experimental/microfrontend/lib/bits.h" +#include "tensorflow/lite/experimental/microfrontend/lib/pcan_gain_control.h" +#include "tensorflow/lite/experimental/microfrontend/lib/pcan_gain_control_util.h" +#include "tensorflow/lite/experimental/microfrontend/lib/noise_reduction.h" +#include "tensorflow/lite/experimental/microfrontend/lib/noise_reduction_util.h" +#include "tensorflow/lite/experimental/microfrontend/lib/log_scale.h" + +#define PCAN_SNR_BITS kPcanSnrBits /* 12 */ +#define PCAN_OUTPUT_BITS kPcanOutputBits /* 6 */ +#define PCAN_SMOOTHING_COEF_BITS kNoiseReductionBits /* 14 */ +#define PCAN_WIDE_DYNAMIC_BITS kWideDynamicFunctionBits /* 32 */ +#define PCAN_LUT_SIZE kWideDynamicFunctionLUTSize /* 125 */ + +/** + * \brief SOF PCAN configuration structure. + */ +struct pcan_config { + float strength; /**< Exponent alpha (e.g. 0.95) */ + float offset; /**< Additive offset delta (e.g. 80.0) */ + int32_t gain_bits; /**< Gain bit shift (e.g. 21) */ + uint16_t smoothing_coef; /**< IIR smoothing coefficient in Q14 (e.g. 819 for 0.05) */ + uint16_t smoothing_bits; /**< Bit shift for noise estimate (e.g. 10) */ + int32_t input_correction_bits; /**< Input correction bits shift (e.g. 0) */ + bool enable_pcan; /**< Enable flag */ +}; + +/** + * \brief SOF PCAN runtime state wrapper. + */ +struct pcan_state { + struct PcanGainControlState g_pcan; /**< Google upstream PCAN state */ + struct NoiseReductionState g_noise; /**< Google upstream Noise Reduction state */ + struct LogScaleState g_log_scale; /**< Google upstream LogScale state */ + uint32_t *noise_estimate; /**< Pointer to noise estimate buffer */ + int16_t *gain_lut; /**< Pointer to gain LUT */ + int num_channels; /**< Number of channels */ + int32_t snr_shift; /**< SNR bit shift */ + uint16_t smoothing_bits; /**< Smoothing bits */ + bool enable_pcan; /**< PCAN enabled */ + bool allocated_noise; /**< Internally allocated noise buffer */ + bool allocated_lut; /**< Internally allocated gain LUT buffer */ +}; + +/** + * \brief Evaluate Google's WideDynamicFunction. + */ +static inline int16_t pcan_wide_dynamic_function(uint32_t x, const int16_t *lut) +{ + return WideDynamicFunction(x, lut); +} + +/** + * \brief Evaluate Google's PcanShrink. + */ +static inline uint32_t pcan_shrink(uint32_t x) +{ + return PcanShrink(x); +} + +/** + * \brief Compute single point in continuous PCAN gain curve. + */ +int16_t pcan_gain_lookup_function(float strength, float offset, int32_t gain_bits, + int32_t input_bits, uint32_t x); + +/** + * \brief Fill 125-entry gain lookup table for WideDynamicFunction. + */ +int pcan_compute_lut(float strength, float offset, int32_t gain_bits, + int32_t input_bits, int16_t *gain_lut); + +/** + * \brief Initialize PCAN state with supplied buffers or allocate them. + */ +int pcan_populate_state(const struct pcan_config *config, struct pcan_state *state, + uint32_t *noise_estimate_buffer, int16_t *gain_lut_buffer, + int num_channels, uint16_t smoothing_bits, + int32_t input_correction_bits); + +/** + * \brief Free allocated resources in PCAN state. + */ +void pcan_free_state(struct pcan_state *state); + +/** + * \brief Reset PCAN temporal state. + */ +void pcan_reset(struct pcan_state *state); + +/** + * \brief Apply noise reduction and update noise estimate using Google's NoiseReductionApply. + */ +static inline void pcan_noise_reduction(struct pcan_state *state, uint32_t *signal) +{ + if (state && state->enable_pcan) + NoiseReductionApply(&state->g_noise, signal); +} + +/** + * \brief Apply PCAN gain control and compression using Google's PcanGainControlApply. + */ +static inline void pcan_apply(struct pcan_state *state, uint32_t *signal) +{ + if (state && state->enable_pcan) + PcanGainControlApply(&state->g_pcan, signal); +} + +/** + * \brief Apply Google's fixed-point logarithm and scale to signal in-place. + */ +void pcan_log_scale(struct pcan_state *state, uint32_t *signal); + +#endif /* __SOF_MATH_PCAN_H__ */ diff --git a/src/math/CMakeLists.txt b/src/math/CMakeLists.txt index a52263006295..4f950725e3d5 100644 --- a/src/math/CMakeLists.txt +++ b/src/math/CMakeLists.txt @@ -83,6 +83,10 @@ if(CONFIG_MATH_AUDITORY) add_subdirectory(auditory) endif() +if(CONFIG_MATH_PCAN) + add_subdirectory(pcan) +endif() + if(CONFIG_MATH_DCT) list(APPEND base_files dct.c) endif() diff --git a/src/math/Kconfig b/src/math/Kconfig index 1feaab8f03d1..b55a6b0b34ce 100644 --- a/src/math/Kconfig +++ b/src/math/Kconfig @@ -279,6 +279,7 @@ config MATH_16BIT_MEL_FILTERBANK config MATH_32BIT_MEL_FILTERBANK bool "icomplex32 data support" default n + select SQRT_FIXED help This option enables function psy_apply_mel_filterbank_32(). It takes as input an array of icomplex32 @@ -287,6 +288,17 @@ config MATH_32BIT_MEL_FILTERBANK endmenu +config MATH_PCAN + bool "PCAN (Per-Channel AGC Normalization) library" + default n + select BINARY_LOGARITHM_FIXED + select MATH_EXP + select NATURAL_LOGARITHM_FIXED + help + Select this to build PCAN (Per-Channel Automatic Gain Control Normalization) + library. PCAN performs dynamic per-channel gain control and piecewise polynomial + root compression across time to suppress stationary noise and enhance acoustic transients. + config MATH_DCT bool "DCT transform library" default n diff --git a/src/math/auditory/mel_filterbank_32.c b/src/math/auditory/mel_filterbank_32.c index 414ddf482f93..18244cf64990 100644 --- a/src/math/auditory/mel_filterbank_32.c +++ b/src/math/auditory/mel_filterbank_32.c @@ -9,10 +9,23 @@ #include #include #include +#include #include -void psy_apply_mel_filterbank_32(struct psy_mel_filterbank *fb, struct icomplex32 *fft_out, - int32_t *power_spectra, int32_t *mel_log, int bitshift) +static inline uint32_t mel_sqrt32(uint32_t num) +{ + if (num == 0) + return 0; + + /* sofm_sqrt_int32 treats input as Q2.30, returning sqrt(n)*2^15. + * Scale down by 2^15 with rounding to obtain integer sqrt(num). + */ + return (uint32_t)((sofm_sqrt_int32((int32_t)num) + (1 << 14)) >> 15); +} + +void psy_apply_mel_filterbank_with_linear_32(struct psy_mel_filterbank *fb, struct icomplex32 *fft_out, + int32_t *power_spectra, int32_t *mel_log, + uint32_t *mel_linear, int bitshift) { int64_t pmax; int64_t p; @@ -67,20 +80,56 @@ void psy_apply_mel_filterbank_32(struct psy_mel_filterbank *fb, struct icomplex3 */ log_arg = sat_int32(Q_SHIFT_RND(p, 45, 25)); log_arg = MAX(log_arg, AUDITORY_EPS_Q31); - log = base2_logarithm((uint32_t)log_arg); - log -= AUDITORY_LOG2_2P25_Q16; - /* Compensate Mel triangles scale */ - log += fb->scale_log2; + if (mel_linear) { + /* Compensate dynamic lshift and FFT bitshift so mel_linear reflects + * true acoustic magnitude calibrated to Google microfrontend range. + */ + uint32_t s = mel_sqrt32((uint32_t)log_arg); + /* Total power shift applied was: lshift + 2 * bitshift */ + int neg_shift = -((int32_t)lshift + 2 * bitshift); + int int_shift = neg_shift >> 1; + int frac_shift = neg_shift & 1; - /* Subtract the applied lshift for power spectra - * log2(x * 2^(-n)) = log2(x) - n. Note that the bitshift need to be subtracted - * as doubled because it was applied in linear domain, from log(x * 2^(-2 * n)) - */ - log -= ((int32_t)lshift + 2 * bitshift) << 16; + uint64_t s_comp = s; + if (frac_shift) + s_comp = (s_comp * 46341U) >> 15; /* 46341 / 32768 ~= sqrt(2) */ + + if (int_shift > 0) + s_comp <<= int_shift; + else if (int_shift < 0) + s_comp >>= -int_shift; + + /* Scale to Google microfrontend range: 25826 / 32768 ~= 0.788 */ + s_comp = (s_comp * 25826U) >> 15; + if (s_comp > 65535U) + s_comp = 65535U; - /* Scale for desired log, output as Q9.23 */ - log = Q_MULTSR_32X32((int64_t)log, fb->log_mult, 16, 29, 23); - mel_log[i] = log; /* Q9.23 */ + mel_linear[i] = (uint32_t)s_comp; + } + + if (mel_log) { + log = base2_logarithm((uint32_t)log_arg); + log -= AUDITORY_LOG2_2P25_Q16; + + /* Compensate Mel triangles scale */ + log += fb->scale_log2; + + /* Subtract the applied lshift for power spectra + * log2(x * 2^(-n)) = log2(x) - n. Note that the bitshift need to be subtracted + * as doubled because it was applied in linear domain, from log(x * 2^(-2 * n)) + */ + log -= ((int32_t)lshift + 2 * bitshift) << 16; + + /* Scale for desired log, output as Q9.23 */ + log = Q_MULTSR_32X32((int64_t)log, fb->log_mult, 16, 29, 23); + mel_log[i] = log; /* Q9.23 */ + } } } + +void psy_apply_mel_filterbank_32(struct psy_mel_filterbank *fb, struct icomplex32 *fft_out, + int32_t *power_spectra, int32_t *mel_log, int bitshift) +{ + psy_apply_mel_filterbank_with_linear_32(fb, fft_out, power_spectra, mel_log, NULL, bitshift); +} diff --git a/src/math/pcan/CMakeLists.txt b/src/math/pcan/CMakeLists.txt new file mode 100644 index 000000000000..aafb9ca07640 --- /dev/null +++ b/src/math/pcan/CMakeLists.txt @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: BSD-3-Clause + +if(NOT DEFINED TFLM_PATH) + if(DEFINED SOF_ROOT_SOURCE_DIRECTORY AND EXISTS "${SOF_ROOT_SOURCE_DIRECTORY}/../tflite-micro") + set(TFLM_PATH "${SOF_ROOT_SOURCE_DIRECTORY}/../tflite-micro") + elseif(DEFINED sof_top_dir AND EXISTS "${sof_top_dir}/../tflite-micro") + set(TFLM_PATH "${sof_top_dir}/../tflite-micro") + elseif(EXISTS "${PROJECT_SOURCE_DIR}/../tflite-micro") + set(TFLM_PATH "${PROJECT_SOURCE_DIR}/../tflite-micro") + elseif(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../../../../tflite-micro") + set(TFLM_PATH "${CMAKE_CURRENT_LIST_DIR}/../../../../tflite-micro") + endif() +endif() + +set(MICROFRONTEND_DIR ${TFLM_PATH}/tensorflow/lite/experimental/microfrontend/lib) + +set(base_files + pcan.c + ${MICROFRONTEND_DIR}/pcan_gain_control.c + ${MICROFRONTEND_DIR}/noise_reduction.c + ${MICROFRONTEND_DIR}/noise_reduction_util.c + ${MICROFRONTEND_DIR}/log_scale.c + ${MICROFRONTEND_DIR}/log_lut.c +) + +is_zephyr(zephyr) +if(zephyr) ### Zephyr ### + + zephyr_include_directories(${TFLM_PATH}) + zephyr_library_sources( + ${base_files} + ) + +else() ### library, e.g. testbench or plugin ### + + target_include_directories(sof PRIVATE ${TFLM_PATH}) + add_local_sources(sof ${base_files}) + +endif() diff --git a/src/math/pcan/README.md b/src/math/pcan/README.md new file mode 100644 index 000000000000..d038b733fa54 --- /dev/null +++ b/src/math/pcan/README.md @@ -0,0 +1,105 @@ +# Per-Channel AGC Normalization (PCAN) Math Library + +This directory contains the **PCAN (Per-Channel Automatic Gain Control Normalization)** library for Sound Open Firmware (SOF), integrating Google's upstream `microfrontend` library from [TFLite Micro](https://github.com/tensorflow/tflite-micro/tree/main/tensorflow/lite/experimental/microfrontend/lib) under the **Apache 2.0 license** with express patent grant protection. + +--- + +## 1. Overview & Mathematical Formulation + +PCAN applies per-channel adaptive dynamic gain control and root dynamic range compression across time to linear filterbank energies (e.g. Mel spectrogram bins), suppressing stationary background noise while enhancing transient acoustic events (speech onsets, keywords). + +``` + +-------------------------------------------------------------+ + | Input Filterbank Energy Matrix | + | E_in[channel, frame] | + +-------------------------------------------------------------+ + | + v ++---------------------------------------------------------------------------------------------+ +| PCAN Processing Stages | +| | +| 1. Temporal Noise Estimation (IIR Smoothing): | +| E_est[k] = ((E_in[k] << smoothing_bits)*alpha_s + E_est[k]*(1 - alpha_s)) >> 14 | +| | +| 2. Piecewise Quadratic Octave Gain Lookup: | +| gain[k] = WideDynamicFunction(E_est[k], gain_lut) | +| - Fast single-cycle CLZ/MSB (AE_NSAU on Tensilica HiFi) | +| - 10-bit fractional index quadratic interpolation | +| | +| 3. Dynamic Gain & Root Polynomial Compression: | +| SNR[k] = ((uint64_t)E_in[k] * gain[k]) >> snr_shift | +| E_out[k] = PcanShrink(SNR[k]) | +| - If SNR[k] < 8192 (2 << 12): SNR[k]^2 >> 20 | +| - If SNR[k] >= 8192: (SNR[k] >> 6) - 64 | ++---------------------------------------------------------------------------------------------+ + | + v + +-------------------------------------------------------------+ + | PCAN-Normalized Feature Tensor | + | E_out[channel, frame] | + +-------------------------------------------------------------+ +``` + +--- + +## 2. Dependencies + +The PCAN math library requires the following tools and repositories: + +| Dependency | Required Version | Description | +|---|---|---| +| **West Manifest (`west.yml`)** | $\ge 0.13$ | Pulls `tflite-micro` workspace dependency via `west update` | +| **TFLite-Micro** | Git revision `e86d97b6` | Upstream Google microfrontend C source files (`pcan_gain_control.c`, `noise_reduction.c`, `bits.h`) | +| **CMake** | $\ge 3.13$ | SOF build and configuration system | +| **Host Toolchain** | GCC $\ge 9.0$ / Clang $\ge 11.0$ | For building host testbench and CMocka unit tests | +| **DSP Toolchains** | Zephyr SDK / Xtensa XCC / Clang | For building target firmware with HiFi3 / HiFi4 / HiFi5 intrinsics | +| **CMocka** | Built with SOF | Unit testing framework for bit-exact validation | +| **GNU Octave** | $\ge 5.0$ | Reference modeling and golden test vector generation | + +--- + +## 3. Build & Test Instructions + +### 3.1 Synchronize Workspace via West +Ensure `tflite-micro` is pulled and synchronized in your workspace: +```bash +cd /path/to/sof-workspace +west update +``` + +### 3.2 Build and Run CMocka Unit Tests (Host) +Configure and run the 5-phase PCAN unit test suite on the host: +```bash +# Configure unit test build +cmake -S sof -B build_ut -DBUILD_UNIT_TESTS=ON -DBUILD_UNIT_TESTS_HOST=ON -DINIT_CONFIG=unit_test_defconfig + +# Build PCAN unit test binary +cmake --build build_ut --target pcan + +# Run PCAN test executable directly +./build_ut/test/cmocka/src/math/pcan/pcan + +# Or run via CTest with full math regression +ctest --test-dir build_ut -R "pcan|auditory|dct|matrix|window|fft" --output-on-failure +``` + +### 3.3 Regenerate Octave Golden Reference Vectors +To re-generate or verify golden test headers from the Octave reference suite: +```bash +cd test/cmocka/src/math/pcan +octave-cli --eval "ref_pcan" +``` +This updates `ref_pcan_lut.h`, `ref_pcan_func.h`, `ref_pcan_stream.h`, and `ref_pcan_corners.h`. + +### 3.4 Build Target Firmware (Zephyr / SOF) +When building SOF for a target DSP platform (e.g. Intel ACE15 / Meteor Lake / Arrow Lake): +```bash +west build -b intel_adsp_ace15_mtpm app +``` + +--- + +## 4. Kconfig & CMake Options + +- `CONFIG_MATH_PCAN`: Enables building the PCAN math library and links Google's microfrontend C source files. +- `CONFIG_COMP_MFCC`: Automatically selects `CONFIG_MATH_PCAN` when MFCC feature extraction is enabled. diff --git a/src/math/pcan/pcan.c b/src/math/pcan/pcan.c new file mode 100644 index 000000000000..eb2fccb6b1c5 --- /dev/null +++ b/src/math/pcan/pcan.c @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. All rights reserved. +// +// Author: Antigravity AI & SOF Team + +#include +#include +#include +#include +#include +#include + +/* ln(2) in Q5.27: round(ln(2) * 2^27) */ +#define PCAN_LN2_Q27 93032640 + +/* Convert PCAN config floats to fixed-point (init-time only). */ +static inline int32_t pcan_strength_q15(float strength) +{ + return (int32_t)(strength * 32768.0f + 0.5f); +} + +static inline int32_t pcan_offset_q7(float offset) +{ + return (int32_t)(offset * 128.0f + 0.5f); +} + +int16_t pcan_gain_lookup_function(float strength, float offset, int32_t gain_bits, + int32_t input_bits, uint32_t x) +{ + const int32_t strength_q15 = pcan_strength_q15(strength); + const int32_t offset_q7 = pcan_offset_q7(offset); + uint64_t u64; + uint32_t u32; + uint32_t ln_u_q27; + int32_t ln_actual_q27; + int64_t s_ln; + int32_t exp_arg_q27; + int32_t exp_arg_ceiling; + int32_t gain_q20; + int32_t gain; + + /* u = x + offset * 2^input_bits, matches upstream's (x_as_float + offset) in fixed-point. */ + u64 = (uint64_t)x + (((uint64_t)offset_q7 << input_bits) >> 7); + if (u64 == 0) + u64 = 1; + u32 = (u64 > 0xFFFFFFFFu) ? 0xFFFFFFFFu : (uint32_t)u64; + + /* ln(u) as UQ5.27. */ + ln_u_q27 = ln_int32(u32); + + /* ln(x_as_float + offset) = ln(u) - input_bits * ln(2), in Q5.27. */ + ln_actual_q27 = (int32_t)ln_u_q27 - input_bits * PCAN_LN2_Q27; + + /* strength * ln(actual): Q1.15 * Q5.27 -> Q6.42, shift down 15 -> Q5.27. */ + s_ln = ((int64_t)strength_q15 * (int64_t)ln_actual_q27) >> 15; + + /* exp_arg = gain_bits * ln(2) - strength * ln(actual), Q5.27. */ + exp_arg_q27 = gain_bits * PCAN_LN2_Q27 - (int32_t)s_ln; + + /* exp(10.3972) = 32767.5, above this the int16 clamp always fires. + * Q5.27: round(10.3972 * 2^27) = 1395385005. + */ + exp_arg_ceiling = 1395385005; + if (exp_arg_q27 >= exp_arg_ceiling) + return INT16_MAX; + if (exp_arg_q27 <= -SOFM_EXP_FIXED_INPUT_MAX) + return 0; + + /* sofm_exp_fixed returns Q12.20 for input Q5.27 in [-16, +7.6246]. Above the + * upper input bound the output saturates. Handle the intermediate range + * [7.6246, 10.3972] with an explicit split so we still get an accurate value. + */ + if (exp_arg_q27 > SOFM_EXP_FIXED_INPUT_MAX) { + /* exp(a + b) = exp(a) * exp(b); pick b = 5.5 in Q5.27. */ + const int32_t half_q27 = 738197504; /* round(5.5 * 2^27) */ + const int32_t half_exp_q20 = 256590991; /* round(exp(5.5) * 2^20) */ + int64_t full_q40; + + gain_q20 = sofm_exp_fixed(exp_arg_q27 - half_q27); + /* Q12.20 * Q12.20 -> Q24.40. Do the >> 20 to Q(?)20 as int64. */ + full_q40 = (int64_t)gain_q20 * half_exp_q20; + /* Round to integer directly to avoid intermediate int32 overflow. */ + gain = (int32_t)((full_q40 + ((int64_t)1 << 39)) >> 40); + if (gain > INT16_MAX) + return INT16_MAX; + if (gain < 0) + return 0; + return (int16_t)gain; + } + + gain_q20 = sofm_exp_fixed(exp_arg_q27); + + /* Round to nearest integer (matching upstream: gain_as_float + 0.5f, then cast). */ + gain = (gain_q20 + (1 << 19)) >> 20; + if (gain > INT16_MAX) + return INT16_MAX; + if (gain < 0) + return 0; + return (int16_t)gain; +} + +int pcan_compute_lut(float strength, float offset, int32_t gain_bits, + int32_t input_bits, int16_t *gain_lut) +{ + int interval; + + if (!gain_lut) + return -EINVAL; + + memset(gain_lut, 0, PCAN_LUT_SIZE * sizeof(int16_t)); + + /* Layout matches upstream PcanGainControlPopulateState(): entries 0 and 1 + * hold the low-x samples, then for intervals 2..kWideDynamicFunctionBits + * the triplet (y0, a1, a2) is stored at (4 * interval - 6, -5, -4). + */ + gain_lut[0] = pcan_gain_lookup_function(strength, offset, gain_bits, input_bits, 0); + gain_lut[1] = pcan_gain_lookup_function(strength, offset, gain_bits, input_bits, 1); + + for (interval = 2; interval <= kWideDynamicFunctionBits; ++interval) { + const uint32_t x0 = (uint32_t)1 << (interval - 1); + const uint32_t x1 = x0 + (x0 >> 1); + const uint32_t x2 = (interval == kWideDynamicFunctionBits) ? + x0 + (x0 - 1) : 2 * x0; + const int16_t y0 = pcan_gain_lookup_function(strength, offset, gain_bits, + input_bits, x0); + const int16_t y1 = pcan_gain_lookup_function(strength, offset, gain_bits, + input_bits, x1); + const int16_t y2 = pcan_gain_lookup_function(strength, offset, gain_bits, + input_bits, x2); + const int32_t diff1 = (int32_t)y1 - y0; + const int32_t diff2 = (int32_t)y2 - y0; + const int32_t a1 = 4 * diff1 - diff2; + const int32_t a2 = diff2 - a1; + + gain_lut[4 * interval - 6] = y0; + gain_lut[4 * interval - 5] = (int16_t)a1; + gain_lut[4 * interval - 4] = (int16_t)a2; + } + + return 0; +} + +int pcan_populate_state(const struct pcan_config *config, struct pcan_state *state, + uint32_t *noise_estimate_buffer, int16_t *gain_lut_buffer, + int num_channels, uint16_t smoothing_bits, + int32_t input_correction_bits) +{ + int ret; + + if (!config || !state || num_channels <= 0) + return -EINVAL; + + memset(state, 0, sizeof(*state)); + state->enable_pcan = config->enable_pcan; + if (!state->enable_pcan) + return 0; + + state->num_channels = num_channels; + state->smoothing_bits = smoothing_bits; + state->snr_shift = config->gain_bits - input_correction_bits - PCAN_SNR_BITS; + if (state->snr_shift < 0) + return -EINVAL; + + if (noise_estimate_buffer) { + state->noise_estimate = noise_estimate_buffer; + } else { + state->noise_estimate = malloc(num_channels * sizeof(uint32_t)); + if (!state->noise_estimate) + return -ENOMEM; + state->allocated_noise = true; + } + memset(state->noise_estimate, 0, num_channels * sizeof(uint32_t)); + + if (gain_lut_buffer) { + state->gain_lut = gain_lut_buffer; + } else { + state->gain_lut = malloc(PCAN_LUT_SIZE * sizeof(int16_t)); + if (!state->gain_lut) { + pcan_free_state(state); + return -ENOMEM; + } + state->allocated_lut = true; + } + + ret = pcan_compute_lut(config->strength, config->offset, config->gain_bits, + (int32_t)smoothing_bits - input_correction_bits, + state->gain_lut); + if (ret < 0) { + pcan_free_state(state); + return ret; + } + + /* Mirror the SOF state into the upstream struct consumed by PcanGainControlApply(). */ + state->g_pcan.enable_pcan = 1; + state->g_pcan.noise_estimate = state->noise_estimate; + state->g_pcan.num_channels = num_channels; + state->g_pcan.gain_lut = state->gain_lut; + state->g_pcan.snr_shift = state->snr_shift; + + state->g_noise.estimate = state->noise_estimate; + state->g_noise.num_channels = num_channels; + state->g_noise.smoothing_bits = smoothing_bits; + state->g_noise.even_smoothing = 410; /* 0.025 * (1 << 14) */ + state->g_noise.odd_smoothing = 983; /* 0.06 * (1 << 14) */ + state->g_noise.min_signal_remaining = 819; /* 0.05 * (1 << 14) */ + + state->g_log_scale.enable_log = 1; + state->g_log_scale.scale_shift = 6; + + return 0; +} + +void pcan_free_state(struct pcan_state *state) +{ + if (!state) + return; + + if (state->allocated_noise) + free(state->noise_estimate); + if (state->allocated_lut) + free(state->gain_lut); + + memset(state, 0, sizeof(*state)); +} + +void pcan_reset(struct pcan_state *state) +{ + if (!state || !state->noise_estimate) + return; + + memset(state->noise_estimate, 0, state->num_channels * sizeof(uint32_t)); +} + +void pcan_log_scale(struct pcan_state *state, uint32_t *signal) +{ + int i; + uint16_t *scaled; + + if (!state || !state->enable_pcan) + return; + + scaled = LogScaleApply(&state->g_log_scale, signal, state->num_channels, 3); + for (i = state->num_channels - 1; i >= 0; --i) + signal[i] = scaled[i]; +} diff --git a/test/cmocka/src/math/auditory/CMakeLists.txt b/test/cmocka/src/math/auditory/CMakeLists.txt index fe200e6b15eb..04b5feec01d2 100644 --- a/test/cmocka/src/math/auditory/CMakeLists.txt +++ b/test/cmocka/src/math/auditory/CMakeLists.txt @@ -9,4 +9,5 @@ cmocka_test(auditory ${PROJECT_SOURCE_DIR}/src/math/base2log.c ${PROJECT_SOURCE_DIR}/src/math/decibels.c ${PROJECT_SOURCE_DIR}/src/math/numbers.c + ${PROJECT_SOURCE_DIR}/src/math/sqrt_int32.c ) diff --git a/test/ztest/unit/math/advanced/pcan/CMakeLists.txt b/test/ztest/unit/math/advanced/pcan/CMakeLists.txt new file mode 100644 index 000000000000..f3bcb5af4910 --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/CMakeLists.txt @@ -0,0 +1,61 @@ +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_math_advanced_pcan) + +set(SOF_ROOT "${PROJECT_SOURCE_DIR}/../../../../../..") + +# Set SOF top directory for UUID registry generation +set(sof_top_dir ${SOF_ROOT}) + +# Include SOF CMake utilities for proper SOF source compilation +include(${SOF_ROOT}/scripts/cmake/misc.cmake) +include(${SOF_ROOT}/scripts/cmake/uuid-registry.cmake) + +# Locate the tflite-micro checkout (west-managed in the SOF workspace) +if(NOT DEFINED TFLM_PATH) + if(EXISTS "${SOF_ROOT}/../tflite-micro") + set(TFLM_PATH "${SOF_ROOT}/../tflite-micro") + elseif(EXISTS "${SOF_ROOT}/../../tflite-micro") + set(TFLM_PATH "${SOF_ROOT}/../../tflite-micro") + endif() +endif() + +if(NOT DEFINED TFLM_PATH OR NOT EXISTS "${TFLM_PATH}") + message(FATAL_ERROR + "tflite-micro not found; expected next to the SOF workspace. " + "Set TFLM_PATH manually or run 'west update' to fetch it.") +endif() + +set(MICROFRONTEND_DIR ${TFLM_PATH}/tensorflow/lite/experimental/microfrontend/lib) + +target_include_directories(app PRIVATE + ${SOF_ROOT}/zephyr/include + ${SOF_ROOT}/src/include + ${SOF_ROOT}/src/platform/posix/include + ${SOF_ROOT}/test/cmocka/include + ${TFLM_PATH} + ${PROJECT_BINARY_DIR}/include/generated +) + +target_compile_definitions(app PRIVATE + -DCONFIG_ZEPHYR_POSIX=1 + -DCONFIG_LIBRARY=1 + -DUNIT_TEST=1 +) + +target_sources(app PRIVATE + pcan_test.c + ${SOF_ROOT}/src/math/pcan/pcan.c + ${SOF_ROOT}/src/math/log_e.c + ${SOF_ROOT}/src/math/base2log.c + ${SOF_ROOT}/src/math/exp_fcn.c + ${SOF_ROOT}/src/math/exp_fcn_hifi.c + ${MICROFRONTEND_DIR}/pcan_gain_control.c + ${MICROFRONTEND_DIR}/noise_reduction.c + ${MICROFRONTEND_DIR}/noise_reduction_util.c + ${MICROFRONTEND_DIR}/log_scale.c + ${MICROFRONTEND_DIR}/log_lut.c +) + +sof_append_relative_path_definitions(app) diff --git a/test/ztest/unit/math/advanced/pcan/pcan_gain_lookup.m b/test/ztest/unit/math/advanced/pcan/pcan_gain_lookup.m new file mode 100644 index 000000000000..68fc83f246b6 --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/pcan_gain_lookup.m @@ -0,0 +1,77 @@ +% pcan_gain_lookup - Compute PCAN continuous gain and generate LUT +% +% SPDX-License-Identifier: BSD-3-Clause +% Copyright(c) 2026 Intel Corporation. All rights reserved. + +function [gain_lut, y_lut] = pcan_gain_lookup(config, input_bits) +% Inputs: +% config.strength - Exponent alpha (e.g., 0.95) +% config.offset - Additive constant delta (e.g., 80.0) +% config.gain_bits - Scale factor exponent (e.g., 21) +% input_bits - smoothing_bits - input_correction_bits +% +% Outputs: +% gain_lut - 125-entry int16 lookup table for WideDynamicFunction +% y_lut - Raw function values at octave evaluation points + + if nargin < 2 + input_bits = 10; + end + + strength = config.strength; + offset = config.offset; + gain_bits = config.gain_bits; + + kWideDynamicFunctionBits = 32; + kWideDynamicFunctionLUTSize = 4 * kWideDynamicFunctionBits - 3; % 125 + + gain_lut = zeros(kWideDynamicFunctionLUTSize, 1, 'int16'); + + % Evaluate point x in gain function + function y = eval_gain(x_val) + x_float = double(x_val) / double(bitshift(uint64(1), input_bits)); + g_float = double(bitshift(uint64(1), gain_bits)) * ((x_float + offset) ^ (-strength)); + if g_float > 32767 + y = int16(32767); + else + y = int16(round(g_float)); + end + end + + gain_lut(1) = eval_gain(0); + gain_lut(2) = eval_gain(1); + + % Intervals 2 through 32 + % In C: lut is offset by -6 so that interval 2 writes to lut[4*2]=lut[8] -> offset 2 in array + for interval = 2:kWideDynamicFunctionBits + x0 = bitshift(uint64(1), interval - 1); + x1 = x0 + bitshift(x0, -1); + if interval == kWideDynamicFunctionBits + x2 = x0 + (x0 - 1); + else + x2 = 2 * x0; + end + + y0 = int32(eval_gain(x0)); + y1 = int32(eval_gain(x1)); + y2 = int32(eval_gain(x2)); + + diff1 = y1 - y0; + diff2 = y2 - y0; + a1 = 4 * diff1 - diff2; + a2 = diff2 - a1; + + % Map to 1-based index in gain_lut: + % In C: index is 4 * interval - 6 (0-based) -> +1 for 1-based + idx = 4 * interval - 5; + + gain_lut(idx) = int16(y0); + gain_lut(idx + 1) = int16(a1); + gain_lut(idx + 2) = int16(a2); + if interval < kWideDynamicFunctionBits + gain_lut(idx + 3) = int16(0); + end + end + + y_lut = gain_lut; +end diff --git a/test/ztest/unit/math/advanced/pcan/pcan_noise_estimate.m b/test/ztest/unit/math/advanced/pcan/pcan_noise_estimate.m new file mode 100644 index 000000000000..b9f340de52df --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/pcan_noise_estimate.m @@ -0,0 +1,37 @@ +% pcan_noise_estimate - Octave fixed-point model of PCAN temporal IIR noise smoothing +% +% SPDX-License-Identifier: BSD-3-Clause +% Copyright(c) 2026 Intel Corporation. All rights reserved. + +function [estimate_out] = pcan_noise_estimate(estimate_in, signal_in, smoothing_coef, smoothing_bits, coef_bits) +% Inputs: +% estimate_in - Previous noise estimate vector (uint32) +% signal_in - Current frame input energy vector (uint32) +% smoothing_coef - IIR smoothing factor in Q(coef_bits) (default 819 for 0.05 in Q14) +% smoothing_bits - Scale shift for input energy (default 10) +% coef_bits - Number of fractional bits in smoothing_coef (default 14) +% +% Output: +% estimate_out - Updated noise estimate vector (uint32) + + if nargin < 3 + smoothing_coef = 819; % ~0.05 in Q14 (16384 * 0.05 = 819.2) + end + if nargin < 4 + smoothing_bits = 10; + end + if nargin < 5 + coef_bits = 14; + end + + one_minus_coef = bitshift(1, coef_bits) - smoothing_coef; + num_channels = length(signal_in); + estimate_out = zeros(num_channels, 1, 'uint32'); + + for i = 1:num_channels + sig_scaled = bitshift(uint64(signal_in(i)), smoothing_bits); + est_prev = uint64(estimate_in(i)); + est_new = bitshift((sig_scaled * uint64(smoothing_coef)) + (est_prev * uint64(one_minus_coef)), -coef_bits); + estimate_out(i) = uint32(est_new); + end +end diff --git a/test/ztest/unit/math/advanced/pcan/pcan_process.m b/test/ztest/unit/math/advanced/pcan/pcan_process.m new file mode 100644 index 000000000000..d853c117da36 --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/pcan_process.m @@ -0,0 +1,55 @@ +% pcan_process - Octave model of full PCAN frame processing +% +% SPDX-License-Identifier: BSD-3-Clause +% Copyright(c) 2026 Intel Corporation. All rights reserved. + +function [output_frames, final_noise_estimate] = pcan_process(input_frames, config, smoothing_bits, input_correction_bits) +% Inputs: +% input_frames - [num_channels x num_frames] matrix of uint32 filterbank energies +% config.strength - Exponent alpha (e.g. 0.95) +% config.offset - Additive constant delta (e.g. 80.0) +% config.gain_bits - Gain bit shift (e.g. 21) +% config.smoothing_coef - Smoothing factor in Q14 (e.g. 819) +% smoothing_bits - Smoothing bit shift (e.g. 10) +% input_correction_bits - Input correction shift (e.g. 0) +% +% Outputs: +% output_frames - [num_channels x num_frames] matrix of uint32 PCAN normalized outputs +% final_noise_estimate - [num_channels x 1] final noise estimate state + + if nargin < 3 + smoothing_bits = 10; + end + if nargin < 4 + input_correction_bits = 0; + end + + [num_channels, num_frames] = size(input_frames); + input_bits = smoothing_bits - input_correction_bits; + kPcanSnrBits = 12; + snr_shift = config.gain_bits - input_correction_bits - kPcanSnrBits; + + [gain_lut, ~] = pcan_gain_lookup(config, input_bits); + + noise_estimate = zeros(num_channels, 1, 'uint32'); + output_frames = zeros(num_channels, num_frames, 'uint32'); + + for f = 1:num_frames + sig_in = input_frames(:, f); + + % 1. Update temporal noise estimate + noise_estimate = pcan_noise_estimate(noise_estimate, sig_in, config.smoothing_coef, smoothing_bits, 14); + + % 2. Apply PCAN gain control per channel + frame_out = zeros(num_channels, 1, 'uint32'); + for c = 1:num_channels + gain = pcan_wide_dynamic_func(noise_estimate(c), gain_lut); + snr = bitshift(uint64(sig_in(c)) * uint64(uint16(gain)), -snr_shift); + frame_out(c) = pcan_shrink(uint32(snr), 12, 6); + end + + output_frames(:, f) = frame_out; + end + + final_noise_estimate = noise_estimate; +end diff --git a/test/ztest/unit/math/advanced/pcan/pcan_shrink.m b/test/ztest/unit/math/advanced/pcan/pcan_shrink.m new file mode 100644 index 000000000000..2ebd98736cc9 --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/pcan_shrink.m @@ -0,0 +1,41 @@ +% pcan_shrink - Octave fixed-point model of PcanShrink piecewise compression +% +% SPDX-License-Identifier: BSD-3-Clause +% Copyright(c) 2026 Intel Corporation. All rights reserved. + +function y = pcan_shrink(x, snr_bits, output_bits) +% Inputs: +% x - 32-bit unsigned SNR input value (scalar or vector) +% snr_bits - Number of fractional bits in SNR (default 12) +% output_bits - Number of fractional bits in output (default 6) +% +% Output: +% y - 32-bit unsigned compressed output value + + if nargin < 2 + snr_bits = 12; + end + if nargin < 3 + output_bits = 6; + end + + threshold = bitshift(uint64(2), snr_bits); % 2 << 12 = 8192 + quadratic_shift = -(2 + 2 * snr_bits - output_bits); % -20 + linear_shift = -(snr_bits - output_bits); % -6 + linear_offset = uint64(bitshift(1, output_bits)); % 64 + + y = zeros(size(x), 'uint32'); + + for k = 1:numel(x) + val = uint64(x(k)); + if val < threshold + % Quadratic compression: x^2 / 4 + prod_val = val * val; + res = bitshift(prod_val, quadratic_shift); + else + % Linear compression: x - 1 + res = bitshift(val, linear_shift) - linear_offset; + end + y(k) = uint32(res); + end +end diff --git a/test/ztest/unit/math/advanced/pcan/pcan_test.c b/test/ztest/unit/math/advanced/pcan/pcan_test.c new file mode 100644 index 000000000000..0dfc6d189acc --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/pcan_test.c @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. All rights reserved. +// +// These contents may have been developed with support from one or more Intel-operated +// generative artificial intelligence solutions. +// +// Converted from cmocka to Ztest +// Original: test/cmocka/src/math/pcan/pcan_test.c + +#include +#include +#include +#include + +#include + +#include "ref_pcan_lut.h" +#include "ref_pcan_func.h" +#include "ref_pcan_stream.h" +#include "ref_pcan_corners.h" + +/* The SOF PCAN core computes the gain LUT in fixed-point (ln_int32 + + * sofm_exp_fixed) while the Octave reference uses double-precision powf. + * The two agree to within a couple of LSBs on the sampled y-values, but the + * quadratic slope terms a1 = 4*(y1-y0) - (y2-y0) and a2 = -a1 + (y2-y0) + * amplify per-sample rounding by up to ~6x, so allow a bigger LUT tolerance. + */ +#define PCAN_TEST_LSB_TOL 2 +#define PCAN_TEST_LUT_TOL 6 + +#define zassert_int_near(actual, expected, tol) \ + do { \ + long long _a = (long long)(actual); \ + long long _e = (long long)(expected); \ + long long _d = _a - _e; \ + \ + if (_d < 0) \ + _d = -_d; \ + zassert_true(_d <= (long long)(tol), \ + "value %lld deviates from reference %lld by %lld (tol %lld)", \ + _a, _e, _d, (long long)(tol)); \ + } while (0) + +ZTEST(pcan_suite, test_pcan_lut_generation) +{ + int16_t lut[PCAN_LUT_SIZE]; + int ret; + int i; + + ret = pcan_compute_lut(0.95f, 80.0f, 21, 10, lut); + zassert_equal(ret, 0); + + for (i = 0; i < PCAN_TEST_LUT1_SIZE; i++) + zassert_int_near(lut[i], ref_pcan_lut1[i], PCAN_TEST_LUT_TOL); + + ret = pcan_compute_lut(0.8f, 50.0f, 18, 12, lut); + zassert_equal(ret, 0); + + for (i = 0; i < PCAN_TEST_LUT2_SIZE; i++) + zassert_int_near(lut[i], ref_pcan_lut2[i], PCAN_TEST_LUT_TOL); +} + +ZTEST(pcan_suite, test_pcan_wide_dynamic_function) +{ + int16_t lut[PCAN_LUT_SIZE]; + int16_t out; + int ret; + int i; + + ret = pcan_compute_lut(0.95f, 80.0f, 21, 10, lut); + zassert_equal(ret, 0); + + for (i = 0; i < PCAN_TEST_WDF_NUM_POINTS; i++) { + out = pcan_wide_dynamic_function(ref_pcan_wdf_inputs[i], lut); + zassert_int_near(out, ref_pcan_wdf_outputs[i], PCAN_TEST_LSB_TOL); + } +} + +ZTEST(pcan_suite, test_pcan_shrink) +{ + uint32_t out; + int i; + + for (i = 0; i < PCAN_TEST_SHRINK_NUM_POINTS; i++) { + out = pcan_shrink(ref_pcan_shrink_inputs[i]); + zassert_equal(out, ref_pcan_shrink_outputs[i]); + } +} + +ZTEST(pcan_suite, test_pcan_streaming) +{ + struct pcan_config cfg; + struct pcan_state pstate; + uint32_t channel_data[PCAN_STREAM_NUM_CHANNELS]; + int ret; + int f; + int c; + + cfg.strength = 0.95f; + cfg.offset = 80.0f; + cfg.gain_bits = 21; + cfg.smoothing_coef = PCAN_STREAM_SMOOTHING_COEF; + cfg.smoothing_bits = PCAN_STREAM_SMOOTHING_BITS; + cfg.input_correction_bits = PCAN_STREAM_INPUT_CORRECTION_BITS; + cfg.enable_pcan = true; + + ret = pcan_populate_state(&cfg, &pstate, NULL, NULL, + PCAN_STREAM_NUM_CHANNELS, + PCAN_STREAM_SMOOTHING_BITS, + PCAN_STREAM_INPUT_CORRECTION_BITS); + zassert_equal(ret, 0); + + for (f = 0; f < PCAN_STREAM_NUM_FRAMES; f++) { + for (c = 0; c < PCAN_STREAM_NUM_CHANNELS; c++) + channel_data[c] = ref_pcan_stream_inputs[f * PCAN_STREAM_NUM_CHANNELS + c]; + + pcan_noise_reduction(&pstate, channel_data); + pcan_apply(&pstate, channel_data); + pcan_log_scale(&pstate, channel_data); + } + + for (c = 0; c < PCAN_STREAM_NUM_CHANNELS; c++) + zassert_true(pstate.noise_estimate[c] > 0, "noise estimate should be positive"); + + pcan_reset(&pstate); + for (c = 0; c < PCAN_STREAM_NUM_CHANNELS; c++) + zassert_equal(pstate.noise_estimate[c], 0); + + pcan_free_state(&pstate); +} + +ZTEST(pcan_suite, test_pcan_corner_cases) +{ + int16_t lut[PCAN_LUT_SIZE]; + struct pcan_config bad_cfg; + struct pcan_state bad_state; + int16_t wdf_out; + uint32_t shrink_out; + int ret; + int i; + + ret = pcan_compute_lut(0.95f, 80.0f, 21, 10, lut); + zassert_equal(ret, 0); + + for (i = 0; i < PCAN_CORNERS_NUM_POINTS; i++) { + wdf_out = pcan_wide_dynamic_function(ref_pcan_corner_inputs[i], lut); + zassert_int_near(wdf_out, ref_pcan_corner_wdf_outputs[i], PCAN_TEST_LSB_TOL); + + shrink_out = pcan_shrink(ref_pcan_corner_inputs[i]); + zassert_equal(shrink_out, ref_pcan_corner_shrink_outputs[i]); + } + + /* gain_bits too small -> negative snr_shift */ + bad_cfg.strength = 0.95f; + bad_cfg.offset = 80.0f; + bad_cfg.gain_bits = 5; + bad_cfg.smoothing_coef = 819; + bad_cfg.smoothing_bits = 10; + bad_cfg.input_correction_bits = 0; + bad_cfg.enable_pcan = true; + + ret = pcan_populate_state(&bad_cfg, &bad_state, NULL, NULL, 16, 10, 0); + zassert_true(ret < 0); + + ret = pcan_populate_state(NULL, &bad_state, NULL, NULL, 16, 10, 0); + zassert_true(ret < 0); + + ret = pcan_populate_state(&bad_cfg, &bad_state, NULL, NULL, 0, 10, 0); + zassert_true(ret < 0); +} + +ZTEST_SUITE(pcan_suite, NULL, NULL, NULL, NULL, NULL); diff --git a/test/ztest/unit/math/advanced/pcan/pcan_wide_dynamic_func.m b/test/ztest/unit/math/advanced/pcan/pcan_wide_dynamic_func.m new file mode 100644 index 000000000000..1ecf32041efb --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/pcan_wide_dynamic_func.m @@ -0,0 +1,56 @@ +% pcan_wide_dynamic_func - Octave fixed-point model of WideDynamicFunction +% +% SPDX-License-Identifier: BSD-3-Clause +% Copyright(c) 2026 Intel Corporation. All rights reserved. + +function y = pcan_wide_dynamic_func(x, gain_lut) +% Inputs: +% x - 32-bit unsigned input value (scalar or vector) +% gain_lut - 125-entry int16 lookup table from pcan_gain_lookup +% +% Output: +% y - 16-bit signed interpolated gain factor (same shape as x) + + y = zeros(size(x), 'int16'); + + for k = 1:numel(x) + val = uint32(x(k)); + if val <= 2 + % Directly return lut[0], lut[1], or lut[2] + y(k) = gain_lut(val + 1); + else + % Compute MSB (1 to 32) + clz_val = 0; + tmp = val; + for b = 31:-1:0 + if bitand(tmp, bitshift(uint32(1), b)) ~= 0 + break; + end + clz_val = clz_val + 1; + end + interval = 32 - clz_val; + + % In C: lut pointer is offset by (4 * interval - 6) + % 1-based index for lut[0]: + idx = 4 * interval - 5; + l0 = int32(gain_lut(idx)); + l1 = int32(gain_lut(idx + 1)); + l2 = int32(gain_lut(idx + 2)); + + if interval < 11 + frac = bitand(bitshift(val, 11 - interval), uint32(1023)); % 0x3FF + else + frac = bitand(bitshift(val, -(interval - 11)), uint32(1023)); + end + frac_i32 = int32(frac); + + res = bitshift(l2 * frac_i32, -5); + res = res + bitshift(l1, 5); + res = res * frac_i32; + res = bitshift(res + 16384, -15); % (1 << 14) = 16384 + res = res + l0; + + y(k) = int16(res); + end + end +end diff --git a/test/ztest/unit/math/advanced/pcan/prj.conf b/test/ztest/unit/math/advanced/pcan/prj.conf new file mode 100644 index 000000000000..d34c7781cd0a --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/prj.conf @@ -0,0 +1,2 @@ +CONFIG_ZTEST=y +CONFIG_SOF_FULL_ZEPHYR_APPLICATION=n diff --git a/test/ztest/unit/math/advanced/pcan/ref_pcan.m b/test/ztest/unit/math/advanced/pcan/ref_pcan.m new file mode 100644 index 000000000000..865d52757840 --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/ref_pcan.m @@ -0,0 +1,212 @@ +% ref_pcan - Generate C header files for PCAN library unit tests +% +% SPDX-License-Identifier: BSD-3-Clause +% Copyright(c) 2026 Intel Corporation. All rights reserved. + +function ref_pcan() + % Add path to cmocka helper export functions + path(path(), '../../../m'); + + try + opt.describe = export_get_git_describe(); + catch + opt.describe = 'SOF PCAN Reference Generator'; + end + + %% Phase 1: Export Golden LUTs for default and custom configurations + export_ref_lut(opt); + + %% Phase 2 & 3: Export Golden values for WideDynamicFunction and PcanShrink + export_ref_functions(opt); + + %% Phase 4: Export Streaming Multi-Channel Multi-Frame Test Vectors + export_ref_streaming(opt); + + %% Phase 5: Export Corner & Edge Cases + export_ref_corner_cases(opt); + + fprintf(1, 'All PCAN test headers exported successfully.\n'); +end + +function export_ref_lut(opt) + header_fn = 'ref_pcan_lut.h'; + fh = export_headerfile_open(header_fn); + export_comment(fh, sprintf('Generated by ref_pcan.m (%s)', opt.describe)); + + % Config 1: Default TFLM configuration + cfg1.strength = 0.95; + cfg1.offset = 80.0; + cfg1.gain_bits = 21; + input_bits1 = 10; + [lut1, ~] = pcan_gain_lookup(cfg1, input_bits1); + + export_ndefine(fh, 'PCAN_TEST_LUT1_SIZE', length(lut1)); + export_ndefine(fh, 'PCAN_TEST_LUT1_GAIN_BITS', cfg1.gain_bits); + export_ndefine(fh, 'PCAN_TEST_LUT1_INPUT_BITS', input_bits1); + export_vector(fh, 16, 'ref_pcan_lut1', lut1); + + % Config 2: Custom configuration (strength=0.8, offset=50.0, gain_bits=18) + cfg2.strength = 0.8; + cfg2.offset = 50.0; + cfg2.gain_bits = 18; + input_bits2 = 12; + [lut2, ~] = pcan_gain_lookup(cfg2, input_bits2); + + export_ndefine(fh, 'PCAN_TEST_LUT2_SIZE', length(lut2)); + export_ndefine(fh, 'PCAN_TEST_LUT2_GAIN_BITS', cfg2.gain_bits); + export_ndefine(fh, 'PCAN_TEST_LUT2_INPUT_BITS', input_bits2); + export_vector(fh, 16, 'ref_pcan_lut2', lut2); + + fclose(fh); + fprintf(1, 'Exported %s.\n', header_fn); +end + +function export_ref_functions(opt) + header_fn = 'ref_pcan_func.h'; + fh = export_headerfile_open(header_fn); + export_comment(fh, sprintf('Generated by ref_pcan.m (%s)', opt.describe)); + + cfg.strength = 0.95; + cfg.offset = 80.0; + cfg.gain_bits = 21; + input_bits = 10; + [lut, ~] = pcan_gain_lookup(cfg, input_bits); + + % 1. WideDynamicFunction Test Vector: Test various points across all 32 intervals + wdf_inputs = uint32([ ... + 0, 1, 2, 3, 4, 5, 7, 8, 15, 16, 31, 32, 63, 64, 127, 128, ... + 255, 256, 511, 512, 1023, 1024, 2047, 2048, 4095, 4096, 8191, 8192, ... + 16383, 16384, 32767, 32768, 65535, 65536, 131071, 131072, ... + 262143, 262144, 524287, 524288, 1048575, 1048576, 2097151, 2097152, ... + 4194303, 4194304, 8388607, 8388608, 16777215, 16777216, ... + 33554431, 33554432, 67108863, 67108864, 134217727, 134217728, ... + 268435455, 268435456, 536870911, 536870912, 1073741823, 1073741824, ... + 2147483647, 2147483648, 4294967295 ... + ]); + wdf_outputs = pcan_wide_dynamic_func(wdf_inputs, lut); + + export_ndefine(fh, 'PCAN_TEST_WDF_NUM_POINTS', length(wdf_inputs)); + export_uint32_vector(fh, 'ref_pcan_wdf_inputs', wdf_inputs); + export_vector(fh, 16, 'ref_pcan_wdf_outputs', wdf_outputs); + + % 2. PcanShrink Test Vector: Sweep below, around (8192), and above threshold + shrink_inputs = uint32([ ... + 0, 1, 10, 100, 500, 1000, 2000, 4000, 8000, 8190, 8191, 8192, 8193, 8194, ... + 10000, 16384, 32768, 65536, 100000, 500000, 1000000, 10000000 ... + ]); + shrink_outputs = pcan_shrink(shrink_inputs, 12, 6); + + export_ndefine(fh, 'PCAN_TEST_SHRINK_NUM_POINTS', length(shrink_inputs)); + export_uint32_vector(fh, 'ref_pcan_shrink_inputs', shrink_inputs); + export_uint32_vector(fh, 'ref_pcan_shrink_outputs', shrink_outputs); + + fclose(fh); + fprintf(1, 'Exported %s.\n', header_fn); +end + +function export_ref_streaming(opt) + header_fn = 'ref_pcan_stream.h'; + fh = export_headerfile_open(header_fn); + export_comment(fh, sprintf('Generated by ref_pcan.m (%s)', opt.describe)); + + cfg.strength = 0.95; + cfg.offset = 80.0; + cfg.gain_bits = 21; + cfg.smoothing_coef = 819; % 0.05 in Q14 + smoothing_bits = 10; + input_correction_bits = 0; + + num_channels = 16; + num_frames = 20; + + % Create deterministic synthetic test signal: + % Baseline background noise + mid-stream bursts + rng(42); + input_matrix = zeros(num_channels, num_frames, 'uint32'); + for f = 1:num_frames + for c = 1:num_channels + noise = 50 + mod(c * 17 + f * 23, 100); + if f >= 5 && f <= 12 && c >= 4 && c <= 10 + speech_burst = 2000 + (c * 150); + else + speech_burst = 0; + end + input_matrix(c, f) = uint32(noise + speech_burst); + end + end + + [output_matrix, final_noise] = pcan_process(input_matrix, cfg, smoothing_bits, input_correction_bits); + + export_ndefine(fh, 'PCAN_STREAM_NUM_CHANNELS', num_channels); + export_ndefine(fh, 'PCAN_STREAM_NUM_FRAMES', num_frames); + export_ndefine(fh, 'PCAN_STREAM_SMOOTHING_COEF', cfg.smoothing_coef); + export_ndefine(fh, 'PCAN_STREAM_SMOOTHING_BITS', smoothing_bits); + export_ndefine(fh, 'PCAN_STREAM_INPUT_CORRECTION_BITS', input_correction_bits); + + input_linear = reshape(input_matrix, [], 1); + output_linear = reshape(output_matrix, [], 1); + + export_uint32_vector(fh, 'ref_pcan_stream_inputs', input_linear); + export_uint32_vector(fh, 'ref_pcan_stream_outputs', output_linear); + export_uint32_vector(fh, 'ref_pcan_stream_final_noise', final_noise); + + fclose(fh); + fprintf(1, 'Exported %s.\n', header_fn); +end + +function export_ref_corner_cases(opt) + header_fn = 'ref_pcan_corners.h'; + fh = export_headerfile_open(header_fn); + export_comment(fh, sprintf('Generated by ref_pcan.m (%s)', opt.describe)); + + cfg.strength = 0.95; + cfg.offset = 80.0; + cfg.gain_bits = 21; + input_bits = 10; + [lut, ~] = pcan_gain_lookup(cfg, input_bits); + + % Corner case points: zeros, boundaries, transitions, overflows + corner_inputs = uint32([ ... + 0, ... + 1, ... + 2, ... + 3, ... + 4, ... + 2047, 2048, 2049, ... + 8191, 8192, 8193, ... + 65535, 65536, 65537, ... + 1073741823, 1073741824, 1073741825, ... + 2147483647, 2147483648, 2147483649, ... + 4294967294, 4294967295 ... + ]); + + wdf_corner_outs = pcan_wide_dynamic_func(corner_inputs, lut); + shrink_corner_outs = pcan_shrink(corner_inputs, 12, 6); + + export_ndefine(fh, 'PCAN_CORNERS_NUM_POINTS', length(corner_inputs)); + export_uint32_vector(fh, 'ref_pcan_corner_inputs', corner_inputs); + export_vector(fh, 16, 'ref_pcan_corner_wdf_outputs', wdf_corner_outs); + export_uint32_vector(fh, 'ref_pcan_corner_shrink_outputs', shrink_corner_outs); + + fclose(fh); + fprintf(1, 'Exported %s.\n', header_fn); +end + +function export_uint32_vector(fh, vname, data) + columns = 6; + rows = ceil(length(data) / columns); + fprintf(fh, '\nstatic const uint32_t %s[%d] = {\n', vname, length(data)); + i = 1; + for j = 1:rows + fprintf(fh, '\t%11uU,', data(i)); + i = i + 1; + for k = 2:columns + if i <= length(data) + fprintf(fh, ' %11uU,', data(i)); + i = i + 1; + end + end + fprintf(fh, '\n'); + end + fprintf(fh, '};\n'); +end diff --git a/test/ztest/unit/math/advanced/pcan/ref_pcan_corners.h b/test/ztest/unit/math/advanced/pcan/ref_pcan_corners.h new file mode 100644 index 000000000000..8fa1852b4222 --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/ref_pcan_corners.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +/* Generated by ref_pcan.m (v1.9-rc1-7901-g9500fa321) */ + +#define PCAN_CORNERS_NUM_POINTS 22 + +static const uint32_t ref_pcan_corner_inputs[22] = { + 0U, 1U, 2U, 3U, 4U, 2047U, + 2048U, 2049U, 8191U, 8192U, 8193U, 65535U, + 65536U, 65537U, 1073741823U, 1073741824U, 1073741825U, 2147483647U, + 2147483648U, 2147483649U, 4294967294U, 4294967295U, +}; + +static const int16_t ref_pcan_corner_wdf_outputs[22] = { + 32636, 32635, 32635, 32635, 32634, 31879, 31879, 31879, 29812, 29811, + 29811, 18676, 18672, 18672, 4, 4, 4, 2, 2, 2, + 1, 1, +}; + +static const uint32_t ref_pcan_corner_shrink_outputs[22] = { + 0U, 0U, 0U, 0U, 0U, 3U, + 4U, 4U, 63U, 64U, 64U, 959U, + 960U, 960U, 16777151U, 16777152U, 16777152U, 33554367U, + 33554368U, 33554368U, 67108799U, 67108799U, +}; diff --git a/test/ztest/unit/math/advanced/pcan/ref_pcan_func.h b/test/ztest/unit/math/advanced/pcan/ref_pcan_func.h new file mode 100644 index 000000000000..f86dfb415d6f --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/ref_pcan_func.h @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +/* Generated by ref_pcan.m (v1.9-rc1-7901-g9500fa321) */ + +#define PCAN_TEST_WDF_NUM_POINTS 65 + +static const uint32_t ref_pcan_wdf_inputs[65] = { + 0U, 1U, 2U, 3U, 4U, 5U, + 7U, 8U, 15U, 16U, 31U, 32U, + 63U, 64U, 127U, 128U, 255U, 256U, + 511U, 512U, 1023U, 1024U, 2047U, 2048U, + 4095U, 4096U, 8191U, 8192U, 16383U, 16384U, + 32767U, 32768U, 65535U, 65536U, 131071U, 131072U, + 262143U, 262144U, 524287U, 524288U, 1048575U, 1048576U, + 2097151U, 2097152U, 4194303U, 4194304U, 8388607U, 8388608U, + 16777215U, 16777216U, 33554431U, 33554432U, 67108863U, 67108864U, + 134217727U, 134217728U, 268435455U, 268435456U, 536870911U, 536870912U, + 1073741823U, 1073741824U, 2147483647U, 2147483648U, 4294967295U, +}; + +static const int16_t ref_pcan_wdf_outputs[65] = { + 32636, 32635, 32635, 32635, 32634, 32634, 32634, 32633, 32630, 32630, + 32624, 32624, 32612, 32612, 32587, 32587, 32539, 32539, 32443, 32443, + 32253, 32253, 31879, 31879, 31159, 31158, 29812, 29811, 27448, 27446, + 23710, 23707, 18676, 18672, 13169, 13166, 8351, 8348, 4876, 4874, + 2698, 2697, 1446, 1446, 762, 762, 398, 398, 207, 207, + 107, 107, 56, 56, 29, 29, 15, 15, 8, 8, + 4, 4, 2, 2, 1, +}; +#define PCAN_TEST_SHRINK_NUM_POINTS 22 + +static const uint32_t ref_pcan_shrink_inputs[22] = { + 0U, 1U, 10U, 100U, 500U, 1000U, + 2000U, 4000U, 8000U, 8190U, 8191U, 8192U, + 8193U, 8194U, 10000U, 16384U, 32768U, 65536U, + 100000U, 500000U, 1000000U, 10000000U, +}; + +static const uint32_t ref_pcan_shrink_outputs[22] = { + 0U, 0U, 0U, 0U, 0U, 0U, + 3U, 15U, 61U, 63U, 63U, 64U, + 64U, 64U, 92U, 192U, 448U, 960U, + 1498U, 7748U, 15561U, 156186U, +}; diff --git a/test/ztest/unit/math/advanced/pcan/ref_pcan_lut.h b/test/ztest/unit/math/advanced/pcan/ref_pcan_lut.h new file mode 100644 index 000000000000..6736041146cc --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/ref_pcan_lut.h @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +/* Generated by ref_pcan.m (v1.9-rc1-7901-g9500fa321) */ + +#define PCAN_TEST_LUT1_SIZE 125 +#define PCAN_TEST_LUT1_GAIN_BITS 21 +#define PCAN_TEST_LUT1_INPUT_BITS 10 + +static const int16_t ref_pcan_lut1[125] = { + 32636, 32635, 32635, 1, -2, 0, 32634, 1, -2, 0, + 32633, -5, 2, 0, 32630, -6, 0, 0, 32624, -12, + 0, 0, 32612, -23, -2, 0, 32587, -48, 0, 0, + 32539, -96, 0, 0, 32443, -190, 0, 0, 32253, -378, + 4, 0, 31879, -739, 18, 0, 31158, -1409, 62, 0, + 29811, -2567, 202, 0, 27446, -4301, 562, 0, 23707, -6265, + 1230, 0, 18672, -7458, 1952, 0, 13166, -7030, 2212, 0, + 8348, -5342, 1868, 0, 4874, -3459, 1282, 0, 2697, -2025, + 774, 0, 1446, -1120, 436, 0, 762, -596, 232, 0, + 398, -313, 122, 0, 207, -164, 64, 0, 107, -85, + 34, 0, 56, -45, 18, 0, 29, -22, 8, 0, + 15, -13, 6, 0, 8, -8, 4, 0, 4, -2, + 0, 0, 2, -3, 2, +}; +#define PCAN_TEST_LUT2_SIZE 125 +#define PCAN_TEST_LUT2_GAIN_BITS 18 +#define PCAN_TEST_LUT2_INPUT_BITS 12 + +static const int16_t ref_pcan_lut2[125] = { + 11465, 11465, 11465, 0, 0, 0, 11465, -3, 2, 0, + 11464, 0, 0, 0, 11464, 1, -2, 0, 11463, 1, + -2, 0, 11462, -5, 2, 0, 11459, -6, 0, 0, + 11453, -9, -2, 0, 11442, -25, 2, 0, 11419, -47, + 2, 0, 11374, -91, 2, 0, 11285, -178, 4, 0, + 11111, -341, 10, 0, 10780, -637, 38, 0, 10181, -1116, + 116, 0, 9181, -1749, 286, 0, 7718, -2315, 526, 0, + 5929, -2478, 700, 0, 4151, -2156, 696, 0, 2691, -1588, + 552, 0, 1655, -1047, 378, 0, 986, -647, 238, 0, + 577, -386, 144, 0, 335, -226, 84, 0, 193, -130, + 48, 0, 111, -77, 30, 0, 64, -45, 18, 0, + 37, -24, 8, 0, 21, -15, 6, 0, 12, -7, + 2, 0, 7, -5, 2, +}; diff --git a/test/ztest/unit/math/advanced/pcan/ref_pcan_stream.h b/test/ztest/unit/math/advanced/pcan/ref_pcan_stream.h new file mode 100644 index 000000000000..1960909baa6f --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/ref_pcan_stream.h @@ -0,0 +1,132 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +/* Generated by ref_pcan.m (v1.9-rc1-7901-g9500fa321) */ + +#define PCAN_STREAM_NUM_CHANNELS 16 +#define PCAN_STREAM_NUM_FRAMES 20 +#define PCAN_STREAM_SMOOTHING_COEF 819 +#define PCAN_STREAM_SMOOTHING_BITS 10 +#define PCAN_STREAM_INPUT_CORRECTION_BITS 0 + +static const uint32_t ref_pcan_stream_inputs[320] = { + 90U, 107U, 124U, 141U, 58U, 75U, + 92U, 109U, 126U, 143U, 60U, 77U, + 94U, 111U, 128U, 145U, 113U, 130U, + 147U, 64U, 81U, 98U, 115U, 132U, + 149U, 66U, 83U, 100U, 117U, 134U, + 51U, 68U, 136U, 53U, 70U, 87U, + 104U, 121U, 138U, 55U, 72U, 89U, + 106U, 123U, 140U, 57U, 74U, 91U, + 59U, 76U, 93U, 110U, 127U, 144U, + 61U, 78U, 95U, 112U, 129U, 146U, + 63U, 80U, 97U, 114U, 82U, 99U, + 116U, 2733U, 2800U, 2967U, 3134U, 3301U, + 3468U, 3635U, 52U, 69U, 86U, 103U, + 120U, 137U, 105U, 122U, 139U, 2656U, + 2823U, 2990U, 3157U, 3324U, 3491U, 3558U, + 75U, 92U, 109U, 126U, 143U, 60U, + 128U, 145U, 62U, 2679U, 2846U, 3013U, + 3180U, 3347U, 3414U, 3581U, 98U, 115U, + 132U, 149U, 66U, 83U, 51U, 68U, + 85U, 2702U, 2869U, 3036U, 3103U, 3270U, + 3437U, 3604U, 121U, 138U, 55U, 72U, + 89U, 106U, 74U, 91U, 108U, 2725U, + 2892U, 2959U, 3126U, 3293U, 3460U, 3627U, + 144U, 61U, 78U, 95U, 112U, 129U, + 97U, 114U, 131U, 2748U, 2815U, 2982U, + 3149U, 3316U, 3483U, 3550U, 67U, 84U, + 101U, 118U, 135U, 52U, 120U, 137U, + 54U, 2671U, 2838U, 3005U, 3172U, 3339U, + 3406U, 3573U, 90U, 107U, 124U, 141U, + 58U, 75U, 143U, 60U, 77U, 2694U, + 2861U, 3028U, 3195U, 3262U, 3429U, 3596U, + 113U, 130U, 147U, 64U, 81U, 98U, + 66U, 83U, 100U, 117U, 134U, 51U, + 68U, 85U, 102U, 119U, 136U, 53U, + 70U, 87U, 104U, 121U, 89U, 106U, + 123U, 140U, 57U, 74U, 91U, 108U, + 125U, 142U, 59U, 76U, 93U, 110U, + 127U, 144U, 112U, 129U, 146U, 63U, + 80U, 97U, 114U, 131U, 148U, 65U, + 82U, 99U, 116U, 133U, 50U, 67U, + 135U, 52U, 69U, 86U, 103U, 120U, + 137U, 54U, 71U, 88U, 105U, 122U, + 139U, 56U, 73U, 90U, 58U, 75U, + 92U, 109U, 126U, 143U, 60U, 77U, + 94U, 111U, 128U, 145U, 62U, 79U, + 96U, 113U, 81U, 98U, 115U, 132U, + 149U, 66U, 83U, 100U, 117U, 134U, + 51U, 68U, 85U, 102U, 119U, 136U, + 104U, 121U, 138U, 55U, 72U, 89U, + 106U, 123U, 140U, 57U, 74U, 91U, + 108U, 125U, 142U, 59U, 127U, 144U, + 61U, 78U, 95U, 112U, 129U, 146U, + 63U, 80U, 97U, 114U, 131U, 148U, + 65U, 82U, +}; + +static const uint32_t ref_pcan_stream_outputs[320] = { + 28U, 39U, 51U, 65U, 12U, 19U, + 29U, 40U, 53U, 67U, 13U, 21U, + 30U, 42U, 54U, 68U, 39U, 50U, + 62U, 12U, 21U, 30U, 40U, 52U, + 64U, 13U, 22U, 31U, 42U, 53U, + 8U, 14U, 50U, 8U, 13U, 21U, + 32U, 41U, 51U, 8U, 14U, 22U, + 33U, 42U, 52U, 9U, 16U, 23U, + 9U, 15U, 21U, 31U, 42U, 52U, + 9U, 16U, 22U, 32U, 44U, 53U, + 10U, 17U, 25U, 33U, 16U, 24U, + 31U, 922U, 936U, 949U, 979U, 1006U, + 1013U, 1036U, 6U, 11U, 17U, 26U, + 35U, 43U, 25U, 33U, 40U, 574U, + 597U, 604U, 617U, 628U, 631U, 628U, + 13U, 18U, 26U, 35U, 45U, 8U, + 34U, 43U, 7U, 421U, 432U, 435U, + 442U, 450U, 444U, 456U, 21U, 27U, + 35U, 44U, 9U, 14U, 5U, 9U, + 14U, 336U, 346U, 350U, 345U, 351U, + 355U, 361U, 30U, 36U, 6U, 10U, + 16U, 22U, 10U, 15U, 21U, 283U, + 288U, 280U, 285U, 287U, 289U, 292U, + 39U, 6U, 11U, 16U, 23U, 30U, + 17U, 23U, 29U, 240U, 234U, 236U, + 240U, 242U, 244U, 241U, 8U, 12U, + 18U, 24U, 32U, 4U, 25U, 31U, + 4U, 199U, 203U, 205U, 209U, 213U, + 210U, 215U, 14U, 19U, 26U, 32U, + 5U, 9U, 33U, 5U, 9U, 176U, + 181U, 185U, 189U, 185U, 189U, 192U, + 21U, 26U, 34U, 6U, 11U, 16U, + 6U, 10U, 15U, 0U, 0U, 0U, + 0U, 0U, 0U, 0U, 28U, 4U, + 7U, 11U, 17U, 23U, 12U, 17U, + 22U, 0U, 0U, 0U, 0U, 0U, + 0U, 0U, 5U, 8U, 13U, 17U, + 24U, 30U, 18U, 23U, 29U, 0U, + 0U, 0U, 0U, 0U, 0U, 0U, + 10U, 14U, 19U, 24U, 3U, 6U, + 25U, 3U, 6U, 0U, 0U, 0U, + 0U, 0U, 0U, 0U, 16U, 21U, + 26U, 4U, 7U, 11U, 4U, 7U, + 11U, 0U, 0U, 0U, 0U, 0U, + 0U, 0U, 22U, 28U, 5U, 8U, + 13U, 17U, 9U, 13U, 17U, 0U, + 1U, 0U, 0U, 0U, 0U, 0U, + 3U, 6U, 9U, 13U, 19U, 24U, + 14U, 19U, 23U, 0U, 0U, 0U, + 0U, 0U, 0U, 0U, 7U, 10U, + 15U, 19U, 26U, 4U, 20U, 25U, + 4U, 0U, 0U, 0U, 0U, 0U, + 0U, 0U, 12U, 16U, 21U, 26U, + 5U, 8U, +}; + +static const uint32_t ref_pcan_stream_final_noise[16] = { + 65089U, 66549U, 66741U, 658990U, 693317U, 727673U, + 763723U, 799477U, 833963U, 863004U, 61280U, 65959U, + 67716U, 69176U, 63373U, 64115U, +}; diff --git a/test/ztest/unit/math/advanced/pcan/testcase.yaml b/test/ztest/unit/math/advanced/pcan/testcase.yaml new file mode 100644 index 000000000000..064bb7debe9c --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/testcase.yaml @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# Copyright(c) 2026 Intel Corporation. All rights reserved. +# +# PCAN (Per-Channel AGC Normalization) unit tests for Ztest framework +# + +tests: + sof.unit.math.advanced.pcan: + tags: math advanced pcan mfcc microwakeword + platform_allow: native_sim + integration_platforms: + - native_sim + build_only: false From 4d46fad7adb8e0f093060c167084a856e418bd21 Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Mon, 9 Nov 2026 17:19:58 +0200 Subject: [PATCH 03/10] github: testbench: fetch TFLM microfrontend sources The testbench build enables MFCC with PCAN, which compiles the PCAN gain control, noise reduction and log scale sources from the TFLM microfrontend library. The testbench workflow only checks out the SOF repository, so these sources are missing and the build fails. Add a step that fetches only the microfrontend library directory of tflite-micro next to sof/, at the revision pinned in submanifests/tflm.yml, using a shallow sparse checkout. Signed-off-by: Seppo Ingalsuo --- .github/workflows/testbench.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/testbench.yml b/.github/workflows/testbench.yml index 82e26470ff17..03f02f505f8c 100644 --- a/.github/workflows/testbench.yml +++ b/.github/workflows/testbench.yml @@ -47,6 +47,19 @@ jobs: with: path: sof + # MFCC with PCAN needs the TFLM microfrontend sources next to sof/, + # use the revision pinned in the west submanifest. + - name: Checkout TFLM microfrontend + run: | + rev=$(awk '/name: tflite-micro/ {f=1} f && /revision:/ {print $2; exit}' \ + sof/submanifests/tflm.yml) + git init tflite-micro + cd tflite-micro + git remote add origin https://github.com/tensorflow/tflite-micro + git sparse-checkout set tensorflow/lite/experimental/microfrontend/lib + git fetch --depth 1 --filter=blob:none origin "$rev" + git checkout FETCH_HEAD + # note: libasound-dev needed for testbench build which still # requires system ALSA headers to be present - name: apt get From 274cb471a85deabe263a4db83eefea7981e88fdd Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Fri, 9 Oct 2026 21:56:45 +0100 Subject: [PATCH 04/10] audio: mfcc: add 8-bit PCAN feature extraction support Extend MFCC (Mel-Frequency Cepstral Coefficients) audio component with 8-bit Per-Channel AGC Normalization (PCAN) feature extraction: - Add CONFIG_COMP_MFCC_PCAN Kconfig option and CMake configuration. - Implement 8-bit quantized PCAN feature extraction paths alongside standard 16-bit cepstral outputs. - Add Topology2 component definition macros for 40-mel and 80-mel PCAN compressed feature extraction. - Lower high-frequency PCAN tracing to debug level to prevent IPC trace buffer saturation during continuous listening. Co-developed-by: Liam Girdwood Signed-off-by: Liam Girdwood Signed-off-by: Seppo Ingalsuo --- src/arch/host/configs/library_defconfig | 1 + src/audio/mfcc/CMakeLists.txt | 18 +++ src/audio/mfcc/Kconfig | 23 +++ src/audio/mfcc/README.md | 89 ++++++++-- src/audio/mfcc/mfcc.c | 72 ++++++++- src/audio/mfcc/mfcc_common.c | 152 ++++++++++++------ src/audio/mfcc/mfcc_setup.c | 72 +++++++++ src/audio/mfcc/tune/setup_mfcc.m | 127 +++++++++++++-- src/include/sof/audio/mfcc/mfcc_comp.h | 11 +- src/include/sof/audio/mfcc/mfcc_vad.h | 8 +- src/include/sof/math/pcan.h | 7 + src/include/user/mfcc.h | 10 +- src/math/auditory/mel_filterbank_32.c | 4 +- src/math/pcan/pcan.c | 3 +- .../components/mfcc/ceps13_compress_dtx.conf | 8 +- .../include/components/mfcc/default.conf | 8 +- .../include/components/mfcc/mel40.conf | 24 +++ .../include/components/mfcc/mel40_10ms.conf | 24 +++ .../components/mfcc/mel40_10ms_compress.conf | 25 +++ .../mfcc/mel40_10ms_pcan_compress.conf | 26 +++ .../components/mfcc/mel40_compress.conf | 24 +++ .../include/components/mfcc/mel80.conf | 16 +- .../components/mfcc/mel80_compress.conf | 16 +- .../components/mfcc/mel80_compress_dtx.conf | 16 +- .../components/mfcc/mel80_pcan_compress.conf | 24 +++ 25 files changed, 676 insertions(+), 132 deletions(-) create mode 100644 tools/topology/topology2/include/components/mfcc/mel40.conf create mode 100644 tools/topology/topology2/include/components/mfcc/mel40_10ms.conf create mode 100644 tools/topology/topology2/include/components/mfcc/mel40_10ms_compress.conf create mode 100644 tools/topology/topology2/include/components/mfcc/mel40_10ms_pcan_compress.conf create mode 100644 tools/topology/topology2/include/components/mfcc/mel40_compress.conf create mode 100644 tools/topology/topology2/include/components/mfcc/mel80_pcan_compress.conf diff --git a/src/arch/host/configs/library_defconfig b/src/arch/host/configs/library_defconfig index 1279dd26c924..e6a6042302a9 100644 --- a/src/arch/host/configs/library_defconfig +++ b/src/arch/host/configs/library_defconfig @@ -11,6 +11,7 @@ CONFIG_COMP_IIR=y CONFIG_COMP_IGO_NR=y CONFIG_COMP_LEVEL_MULTIPLIER=y CONFIG_COMP_MFCC=y +CONFIG_COMP_MFCC_PCAN=y CONFIG_COMP_MODULE_ADAPTER=y CONFIG_COMP_MULTIBAND_DRC=y CONFIG_COMP_MUX=y diff --git a/src/audio/mfcc/CMakeLists.txt b/src/audio/mfcc/CMakeLists.txt index 274c7aa05eb8..0744ae5e0373 100644 --- a/src/audio/mfcc/CMakeLists.txt +++ b/src/audio/mfcc/CMakeLists.txt @@ -1,5 +1,23 @@ # SPDX-License-Identifier: BSD-3-Clause +if(CONFIG_COMP_MFCC_PCAN) + if(NOT DEFINED TFLM_PATH) + if(DEFINED SOF_ROOT_SOURCE_DIRECTORY AND EXISTS "${SOF_ROOT_SOURCE_DIRECTORY}/../tflite-micro") + set(TFLM_PATH "${SOF_ROOT_SOURCE_DIRECTORY}/../tflite-micro") + elseif(DEFINED sof_top_dir AND EXISTS "${sof_top_dir}/../tflite-micro") + set(TFLM_PATH "${sof_top_dir}/../tflite-micro") + elseif(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../../../../tflite-micro") + set(TFLM_PATH "${CMAKE_CURRENT_LIST_DIR}/../../../../tflite-micro") + endif() + endif() + is_zephyr(zephyr) + if(zephyr) + zephyr_include_directories(${TFLM_PATH}) + else() + target_include_directories(sof PRIVATE ${TFLM_PATH}) + endif() +endif() + if(CONFIG_COMP_MFCC STREQUAL "m" AND DEFINED CONFIG_LLEXT) add_subdirectory(llext ${PROJECT_BINARY_DIR}/mfcc_llext) add_dependencies(app mfcc) diff --git a/src/audio/mfcc/Kconfig b/src/audio/mfcc/Kconfig index f56cadb40de2..1faa74998e72 100644 --- a/src/audio/mfcc/Kconfig +++ b/src/audio/mfcc/Kconfig @@ -24,3 +24,26 @@ config COMP_MFCC The characteristic of the audio features are defined in the binary control blob. Directory tools/tune/mfcc contains a tool to create the configurations. + +if COMP_MFCC + +config COMP_MFCC_PCAN + bool "Enable PCAN (Per-Channel AGC Normalization) in MFCC" + default n + select MATH_PCAN + help + Select to build MFCC with PCAN (Per-Channel Automatic Gain Control + Normalization) support on the Mel filterbank energies. PCAN performs + dynamic per-channel gain control and piecewise polynomial root + compression to suppress stationary noise and enhance transients, + producing normalized features suitable for direct int8 quantization + downstream (e.g. microWakeWord). The topology PCAN configuration + fields are ignored when this option is disabled. + +config COMP_MFCC_DEBUG_TRACE + bool "Verbose per-process debug traces in MFCC" + default n + help + Enable MFCC per-process debug prints in the hot path. + +endif # COMP_MFCC diff --git a/src/audio/mfcc/README.md b/src/audio/mfcc/README.md index 31f9c331e545..e2f348231f24 100644 --- a/src/audio/mfcc/README.md +++ b/src/audio/mfcc/README.md @@ -1,25 +1,84 @@ -# MFCC Feature Extraction Architecture +# MFCC & PCAN Feature Extraction Architecture -This directory contains the Mel-Frequency Cepstral Coefficients (MFCC) feature extractor. +This directory contains the **Mel-Frequency Cepstral Coefficients (MFCC)** feature extractor and integrated **PCAN (Per-Channel AGC Normalization)** audio pre-processing component for Sound Open Firmware (SOF). -## Overview +--- -MFCC extracts audio features commonly used as inputs for machine learning models, such as wake-word detection or speech recognition. +## 1. Overview -## Architecture Diagram +The MFCC module converts raw streaming time-domain PCM audio into compact acoustic feature representations (e.g. 13-bin cepstral coefficients, 40-bin or 80-bin Mel spectrograms) suitable for on-device machine learning models such as wake-word classifiers (TFLM, microWakeWord) and speech recognition engines (Whisper). + +When enabled, **PCAN** applies per-channel adaptive dynamic gain control and piecewise root dynamic range compression across time to suppress stationary background noise and normalize channel energy before feature quantization. + +--- + +## 2. Architecture & Data Flow ```mermaid graph LR - In[Audio Frame] --> Win[Windowing] - Win --> FFT[FFT] - FFT --> Mel[Mel Filterbank] - Mel --> DCT[DCT] - DCT --> Out[MFCC Output Features] + In[Audio PCM Frame] --> Win[Windowing: Hamming/Hann] + Win --> FFT[FFT: 512-pt] + FFT --> Mel[Mel Filterbank: 40/80 Bins] + Mel --> PCAN[PCAN Gain Normalization] + PCAN --> Log[Log Scaling / dB] + Log --> VAD[VAD / DTX Silence Gating] + Log --> DCT[DCT: Cepstral Matrix] + DCT --> Lifter[Cepstral Lifter] + Lifter --> Out[MFCC Output Features] +``` + +--- + +## 3. Dependencies + +| Dependency | Purpose | +|---|---| +| **West Manifest (`west.yml`)** | Manages workspace dependencies including `tflite-micro` | +| **`tflite-micro`** | Provides Google's upstream Apache-2.0 `microfrontend` PCAN library (`pcan_gain_control.c`, `noise_reduction.c`) | +| **GNU Octave ($\ge 5.0$)** | Required for topology configuration generation (`tune/setup_mfcc.m`) and reference vector export | +| **CMake ($\ge 3.13$)** | SOF build system | +| **Zephyr SDK / Xtensa XCC** | DSP cross-compilation toolchains for Cadence HiFi3 / HiFi4 SIMD kernels | +| **CMocka** | Host unit testing framework | + +--- + +## 4. Build & Tuning Instructions + +### 4.1 Synchronize Dependencies via West +```bash +west update ``` -## Configuration and Scripts +### 4.2 Build Host Unit Tests +```bash +# Configure unit tests +cmake -S sof -B build_ut -DBUILD_UNIT_TESTS=ON -DBUILD_UNIT_TESTS_HOST=ON -DINIT_CONFIG=unit_test_defconfig + +# Build all math and MFCC tests +cmake --build build_ut --target pcan +ctest --test-dir build_ut -R "pcan|auditory|dct|matrix|window|fft" --output-on-failure +``` + +### 4.3 Build Target DSP Firmware +```bash +west build -b intel_adsp_ace15_mtpm app +``` + +### 4.4 Generate Topology Configurations (Octave) +The tuning script generates binary configuration blobs and ALSA topology configurations: +```bash +cd src/audio/mfcc/tune +octave-cli --eval "setup_mfcc" +``` +Available presets generated: +- `ceps13_compress_dtx.conf`: 13-bin cepstral features with VAD and DTX silence suppression. +- `mel80_compress.conf`: 80-bin linear Mel spectrogram features. +- `mel80_compress_dtx.conf`: 80-bin Mel spectrogram with DTX gating. +- `mel80_pcan_compress.conf`: 80-bin Mel spectrogram with Google PCAN dynamic AGC normalization. + +--- + +## 5. Configuration Options -- **Kconfig**: Enables the MFCC component (`COMP_MFCC`) which selects necessary math libraries (`MATH_FFT`, `MATH_DCT`, `MATH_16BIT_MEL_FILTERBANK`, etc.). Depends on `COMP_MODULE_ADAPTER`. -- **CMakeLists.txt**: Compiles generic, common, and HIFI implementations (`mfcc_hifi3.c`, `mfcc_hifi4.c`). Provides support for Zephyr loadable extensions (`llext`). -- **mfcc.toml**: Specifies the topology configuration for the MFCC module (UUID, affinity, memory parameters, and pin formats). -- **Topology (.conf)**: Derived from `tools/topology/topology2/include/components/mfcc.conf`, configuring a `mfcc` widget object of type `effect` with UUID `73:a7:10:db:a4:1a:ea:4c:a2:1f:2d:57:a5:c9:82:eb`. +- **`CONFIG_COMP_MFCC`**: Enables the MFCC component and automatically selects required math libraries (`CONFIG_MATH_FFT`, `CONFIG_MATH_AUDITORY`, `CONFIG_MATH_DCT`, `CONFIG_MATH_PCAN`, `CONFIG_MATH_WINDOW`). +- **`CONFIG_COMP_MODULE_ADAPTER`**: Module adapter infrastructure for SOF processing components. diff --git a/src/audio/mfcc/mfcc.c b/src/audio/mfcc/mfcc.c index 88d5d4863d31..a71948e8d8b3 100644 --- a/src/audio/mfcc/mfcc.c +++ b/src/audio/mfcc/mfcc.c @@ -33,11 +33,19 @@ #include #include #include +#if CONFIG_COMP_MFCC_DEBUG_TRACE +#include +#endif LOG_MODULE_REGISTER(mfcc, CONFIG_SOF_LOG_LEVEL); SOF_DEFINE_REG_UUID(mfcc); +#if CONFIG_COMP_MFCC_DEBUG_TRACE +static uint32_t last_mfcc_cycle; +static uint32_t mfcc_call_count; +#endif + /** \brief Source/sink API based source copy function map. */ struct mfcc_source_func_map { uint8_t source_fmt; @@ -162,6 +170,13 @@ static int mfcc_process(struct processing_module *mod, size_t source_avail; int frames; int num_ceps; +#if CONFIG_COMP_MFCC_DEBUG_TRACE + uint32_t now = k_cycle_get_32(); + uint32_t delta_cycles = now - last_mfcc_cycle; + + last_mfcc_cycle = now; + mfcc_call_count++; +#endif comp_dbg(dev, "start"); @@ -172,23 +187,47 @@ static int mfcc_process(struct processing_module *mod, * can continue while the previous period is drained. */ pending = state->header_pending || state->out_remain > 0; - if (cd->config->compress_output && pending) - return mfcc_process_output(mod, cd, sources, sinks, 0, 0); + if (cd->config->compress_output && pending) { + int ret = mfcc_process_output(mod, cd, sources, sinks, 0, 0); + +#if CONFIG_COMP_MFCC_DEBUG_TRACE + comp_info(dev, "[MFCC proc %u] delta=%u us (pending retry, ret=%d)", + mfcc_call_count, k_cyc_to_us_near32(delta_cycles), ret); +#endif + return ret; + } source_avail = source_get_data_frames_available(sources[0]); frames = MIN(source_avail, cd->max_frames); - if (!frames) + if (!frames) { +#if CONFIG_COMP_MFCC_DEBUG_TRACE + comp_info(dev, "[MFCC proc %u] delta=%u us (no frames, avail=%zu)", + mfcc_call_count, k_cyc_to_us_near32(delta_cycles), source_avail); +#endif return 0; + } /* Copy input audio from source to MFCC internal circular buffer */ cd->source_func(sources[0], &state->buf, &state->emph, frames, state->source_channel); /* Run STFT and Mel/DCT processing */ num_ceps = mfcc_stft_process(mod, cd); - if (num_ceps < 0) + if (num_ceps < 0) { +#if CONFIG_COMP_MFCC_DEBUG_TRACE + comp_info(dev, "[MFCC proc %u] delta=%u us stft error %d", + mfcc_call_count, k_cyc_to_us_near32(delta_cycles), num_ceps); +#endif return num_ceps; + } + + int ret = mfcc_process_output(mod, cd, sources, sinks, num_ceps, frames); - return mfcc_process_output(mod, cd, sources, sinks, num_ceps, frames); +#if CONFIG_COMP_MFCC_DEBUG_TRACE + comp_info(dev, "[MFCC proc %u] delta=%u us avail=%zu frames=%d ceps=%d ret=%d", + mfcc_call_count, k_cyc_to_us_near32(delta_cycles), + source_avail, frames, num_ceps, ret); +#endif + return ret; } /** @@ -241,12 +280,29 @@ static int mfcc_prepare(struct processing_module *mod, /* Initialize MFCC, max_frames is set to dev->frames + 4 */ if (cd->config && data_size > 0) { - ret = mfcc_setup(mod, dev->frames + 4, audio_stream_get_rate(&sourceb->stream), + uint32_t src_rate = audio_stream_get_rate(&sourceb->stream); + int max_frames; + + if (dev->frames < cd->config->frame_shift) + dev->frames = cd->config->frame_shift; + + max_frames = dev->frames + 4; + + ret = mfcc_setup(mod, max_frames, src_rate, audio_stream_get_channels(&sourceb->stream)); if (ret < 0) { comp_err(dev, "setup failed."); return ret; } + + /* Set DP scheduling period from FFT hop cadence; sink rate is + * not yet propagated at prepare time so module_adapter cannot + * derive it from mod->sinks[]. + */ + if (src_rate && cd->state.fft.fft_hop_size) + dev->period = (uint32_t)(1000000ULL * + cd->state.fft.fft_hop_size / + src_rate); } else { comp_err(dev, "configuration is missing."); return -EINVAL; @@ -306,6 +362,10 @@ static int mfcc_reset(struct processing_module *mod) /* Reset to similar state as init() */ cd->source_func = NULL; +#if CONFIG_COMP_MFCC_DEBUG_TRACE + last_mfcc_cycle = 0; + mfcc_call_count = 0; +#endif return 0; } diff --git a/src/audio/mfcc/mfcc_common.c b/src/audio/mfcc/mfcc_common.c index 3890671165fd..e0f3fa6476d7 100644 --- a/src/audio/mfcc/mfcc_common.c +++ b/src/audio/mfcc/mfcc_common.c @@ -13,6 +13,9 @@ #include #include #include +#if CONFIG_COMP_MFCC_PCAN +#include +#endif #include #include #include @@ -352,8 +355,51 @@ int mfcc_stft_process(struct processing_module *mod, struct mfcc_comp_data *cd) * to add the missing "gain". */ mel_scale_shift = input_shift - fft->fft_plan->len; + +#if CONFIG_COMP_MFCC_PCAN + if (state->pcan.enable_pcan) { + psy_apply_mel_filterbank_with_linear_32(&state->melfb, fft->fft_out, + state->power_spectra, state->mel_log_32, + state->mel_linear, mel_scale_shift); + pcan_noise_reduction(&state->pcan, state->mel_linear); + pcan_apply(&state->pcan, state->mel_linear); + pcan_log_scale(&state->pcan, state->mel_linear); + + /* Throttled data-flow trace: linear Mel energy and noise + * estimate after PCAN to confirm the detector input is + * not pinned to the floor. + */ + { + static uint32_t pcan_dbg; + int nb = state->pcan.num_channels; + + if ((pcan_dbg++ % 32) == 1) { + uint32_t mel_max = 0; + uint64_t mel_sum = 0; + uint32_t ne0 = state->pcan.noise_estimate ? + state->pcan.noise_estimate[0] : 0; + int b; + + for (b = 0; b < nb; b++) { + uint32_t v = state->mel_linear[b]; + + mel_sum += v; + mel_max = MAX(mel_max, v); + } + if (mel_max > 0 || (pcan_dbg % 4096) == 1) + comp_dbg(dev, + "pcan[%u] bins=%d mel_max=%u mel_sum=%u noise0=%u", + pcan_dbg, nb, mel_max, (uint32_t)mel_sum, ne0); + } + } + } else { + psy_apply_mel_filterbank_32(&state->melfb, fft->fft_out, state->power_spectra, + state->mel_log_32, mel_scale_shift); + } +#else psy_apply_mel_filterbank_32(&state->melfb, fft->fft_out, state->power_spectra, state->mel_log_32, mel_scale_shift); +#endif if (state->mel_only) { /* In Mel-only mode output Mel log spectra directly */ @@ -419,7 +465,7 @@ int mfcc_stft_process(struct processing_module *mod, struct mfcc_comp_data *cd) /* Use hop counter for frame numbering (independent of VAD enable) */ state->header.frame_number = state->hop_count; - /* Run VAD on the mel log spectrum (available in both modes) */ + /* Run VAD on the scaled mel log spectrum (available in both modes) */ if (config->enable_vad) { mfcc_vad_update(&cd->vad, state->mel_log_32); @@ -497,6 +543,37 @@ static void mfcc_prepare_output(struct mfcc_state *state, int num_ceps) if (num_ceps <= 0) return; +#if CONFIG_COMP_MFCC_PCAN + if (state->pcan.enable_pcan) { + int8_t *out8 = (int8_t *)state->out_stage; + + /* In PCAN mode mel_linear[] is indexed by k < num_ceps. + * num_ceps must not exceed the number of Mel bins or we + * read past the end of the Mel filterbank array. + */ + if (num_ceps > state->melfb.mel_bins) + return; + + for (k = 0; k < num_ceps; k++) { + /* Map PCAN log-scaled output (~0..666) to int8 [-128..127] */ + int32_t val = (int32_t)state->mel_linear[k]; + + val = ((val * 256) + 333) / 666 - 128; + if (val > 127) + val = 127; + else if (val < -128) + val = -128; + + out8[k] = (int8_t)val; + } + + state->out_data_ptr = state->out_stage; + state->out_remain = num_ceps; + state->header_pending = true; + return; + } +#endif + /* Copy into out_stage so the next STFT hop may freely reuse * mel_log_32 / cepstral_coef while this frame is still pending. */ @@ -519,9 +596,8 @@ static void mfcc_prepare_output(struct mfcc_state *state, int num_ceps) * * Writes the pending data header (if any) followed by the pending * int32_t Mel/cepstral payload into the sink as a contiguous blob, - * without zero padding. When VAD + DTX are enabled, suppresses silence - * frames after a configurable trailing-silence count and optionally - * sends periodic keep-alive silence updates. + * without zero padding. Every prepared hop is committed. VAD must not + * drop frames: the downstream streaming detector runs continuously. * * \param[in,out] mod Processing module. * \param[in,out] cd MFCC component data. @@ -543,50 +619,19 @@ static int mfcc_output_compress(struct processing_module *mod, struct mfcc_comp_ uint8_t *dst; int ret; bool pending; - bool send_periodic; pending = state->header_pending || state->out_remain > 0; if (!pending) return 0; - if (num_ceps > 0 && cd->config->enable_vad && !cd->vad.is_speech) { - state->vad_silence_count++; - /* With DTX enabled, send trailing silence frames - * (configurable count) then suppress. After trailing - * frames, optionally send periodic silence updates - * at the configured interval. This gives the host - * enough silence to detect end-of-speech while - * keeping alive updates during long silence. - * Without DTX, output every frame regardless of VAD. - */ - if (cd->config->enable_dtx && - state->vad_silence_count > state->dtx_trailing_silence) { - send_periodic = false; - - /* Check periodic silence frame send */ - if (state->dtx_silence_interval > 0) { - state->dtx_silence_counter++; - if (state->dtx_silence_counter >= - state->dtx_silence_interval) { - state->dtx_silence_counter = 0; - send_periodic = true; - } - } - - if (!send_periodic) { - /* Suppress this frame */ - state->header_pending = false; - state->out_remain = 0; - return 0; - } - } - } else { - state->vad_silence_count = 0; - state->dtx_silence_counter = 0; - } +#if CONFIG_COMP_MFCC_PCAN + size_t sample_size = state->pcan.enable_pcan ? sizeof(int8_t) : sizeof(int32_t); +#else + size_t sample_size = sizeof(int32_t); +#endif out_bytes = (state->header_pending ? sizeof(state->header) : 0) + - state->out_remain * sizeof(int32_t); + state->out_remain * sample_size; if (out_bytes == 0) return 0; @@ -609,7 +654,7 @@ static int mfcc_output_compress(struct processing_module *mod, struct mfcc_comp_ if (state->out_remain > 0) { mfcc_sink_write_bytes(&dst, sink_start, sink_buf_size, (uint8_t *)state->out_data_ptr, - state->out_remain * sizeof(int32_t)); + state->out_remain * sample_size); } ret = sink_commit_buffer(sinks[0], commit_bytes); @@ -653,11 +698,10 @@ static int mfcc_output_legacy(struct processing_module *mod, struct mfcc_comp_da void *sink_start; size_t sink_buf_size; uint8_t *dst; - int n32; int ret; /* The MFCC sink is treated as an opaque byte container: the period - * carries an MFCC blob (header + int32 features), not PCM audio. + * carries an MFCC blob (header + features), not PCM audio. * Sizing the commit as sink_frame_bytes * frames keeps the period * size matched to whatever the sink advertises (S16_LE / S24_4LE / * S32_LE), so no format-specific conversion is needed. Any payload @@ -702,17 +746,25 @@ static int mfcc_output_legacy(struct processing_module *mod, struct mfcc_comp_da } } - /* Write pending feature data (always int32) */ + /* Write pending feature data (int8 in PCAN mode, int32 otherwise) */ if (state->out_remain > 0 && avail > 0) { - data_bytes = state->out_remain * sizeof(int32_t); - to_write = MIN(data_bytes, avail) & ~(size_t)3; +#if CONFIG_COMP_MFCC_PCAN + size_t sample_size = state->pcan.enable_pcan ? sizeof(int8_t) : sizeof(int32_t); +#else + size_t sample_size = sizeof(int32_t); +#endif + data_bytes = state->out_remain * sample_size; + to_write = MIN(data_bytes, avail); + if (sample_size > 1) + to_write &= ~(sample_size - 1); if (to_write > 0) { mfcc_sink_write_bytes(&dst, sink_start, sink_buf_size, (uint8_t *)state->out_data_ptr, to_write); - n32 = to_write / sizeof(int32_t); - state->out_data_ptr += n32; - state->out_remain -= n32; + int n = to_write / sample_size; + + state->out_data_ptr = (void *)((uint8_t *)state->out_data_ptr + to_write); + state->out_remain -= n; } } diff --git a/src/audio/mfcc/mfcc_setup.c b/src/audio/mfcc/mfcc_setup.c index 1d062b2a5206..933bfb47a4b5 100644 --- a/src/audio/mfcc/mfcc_setup.c +++ b/src/audio/mfcc/mfcc_setup.c @@ -10,6 +10,9 @@ #include #include #include +#if CONFIG_COMP_MFCC_PCAN +#include +#endif #include #include #include @@ -383,6 +386,9 @@ int mfcc_setup(struct processing_module *mod, int max_frames, int sample_rate, i /* Use FFT buffer as scratch for later computed data */ state->power_spectra = (int32_t *)&fft->fft_buf[0]; state->mel_log_32 = &state->power_spectra[fft->half_fft_size]; +#if CONFIG_COMP_MFCC_PCAN + state->mel_linear = (uint32_t *)&state->mel_log_32[config->num_mel_bins]; +#endif /* Check that mel_log_32 fits in the remaining fft_buf scratch space */ mel_log_32_space = (int)(fft->fft_buffer_size / sizeof(int32_t)) - fft->half_fft_size; @@ -459,9 +465,71 @@ int mfcc_setup(struct processing_module *mod, int max_frames, int sample_rate, i } } + if (config->enable_pcan) { +#if CONFIG_COMP_MFCC_PCAN + struct pcan_config pcfg; + uint32_t *pcan_noise; + int16_t *pcan_lut; + + pcfg.strength = (config->pcan_strength > 0) ? + ((float)config->pcan_strength / 32768.0f) : 0.95f; + pcfg.offset = (config->pcan_offset > 0) ? + ((float)config->pcan_offset / 128.0f) : 80.0f; + pcfg.gain_bits = (config->pcan_gain_bits > 0) ? + config->pcan_gain_bits : 21; + pcfg.smoothing_coef = (config->pcan_smoothing_coef > 0) ? + (uint16_t)config->pcan_smoothing_coef : 819; + pcfg.smoothing_bits = 10; + pcfg.input_correction_bits = PCAN_INPUT_CORRECTION_BITS; + pcfg.enable_pcan = true; + + pcan_noise = mod_zalloc(mod, config->num_mel_bins * sizeof(uint32_t)); + if (!pcan_noise) { + comp_err(dev, "Failed PCAN noise estimate alloc"); + ret = -ENOMEM; + goto free_vad; + } + + pcan_lut = mod_zalloc(mod, PCAN_LUT_SIZE * sizeof(int16_t)); + if (!pcan_lut) { + comp_err(dev, "Failed PCAN gain LUT alloc"); + mod_free(mod, pcan_noise); + ret = -ENOMEM; + goto free_vad; + } + + ret = pcan_populate_state(&pcfg, &state->pcan, pcan_noise, pcan_lut, + config->num_mel_bins, 10, + PCAN_INPUT_CORRECTION_BITS); + if (ret < 0) { + comp_err(dev, "Failed PCAN state init"); + mod_free(mod, pcan_noise); + mod_free(mod, pcan_lut); + goto free_vad; + } + } else { + state->pcan.enable_pcan = false; + state->pcan.noise_estimate = NULL; + state->pcan.gain_lut = NULL; + } +#else + comp_err(dev, "enable_pcan set but CONFIG_COMP_MFCC_PCAN is not selected"); + ret = -EINVAL; + goto free_vad; + } +#endif + comp_dbg(dev, "done"); return 0; +free_vad: + if (config->enable_vad) { + mod_free(mod, cd->vad.noise_floor); + mod_free(mod, cd->vad.weights); + cd->vad.noise_floor = NULL; + cd->vad.weights = NULL; + } + free_out_stage: mod_free(mod, state->out_stage); @@ -528,6 +596,10 @@ void mfcc_free_buffers(struct processing_module *mod) mfcc_free_and_null(mod, (void **)&cd->state.dct.matrix); mfcc_free_and_null(mod, (void **)&cd->state.lifter.matrix); mfcc_free_and_null(mod, (void **)&cd->state.out_stage); +#if CONFIG_COMP_MFCC_PCAN + mfcc_free_and_null(mod, (void **)&cd->state.pcan.noise_estimate); + mfcc_free_and_null(mod, (void **)&cd->state.pcan.gain_lut); +#endif mfcc_free_and_null(mod, (void **)&cd->vad.noise_floor); mfcc_free_and_null(mod, (void **)&cd->vad.weights); } diff --git a/src/audio/mfcc/tune/setup_mfcc.m b/src/audio/mfcc/tune/setup_mfcc.m index dbf69587a74f..9ebc952e734c 100644 --- a/src/audio/mfcc/tune/setup_mfcc.m +++ b/src/audio/mfcc/tune/setup_mfcc.m @@ -31,6 +31,55 @@ function setup_mfcc() setup.tplg_fn = 'mel80_compress.conf'; export_mfcc_setup(gen_cfg, setup); + % Blob for 40-bin/20ms-hop mel spectrogram, matching TFLM micro_speech's + % front-end shape (TFLM_FEATURE_SIZE=40, TFLM_FEATURE_STRIDE_MS=20, + % TFLM_FEATURE_DURATION_MS=30) for interim wake-word sanity-checking. + setup = get_mel_spectrogram_config(); + setup.frame_length = 30.0; % 480 samples at 16 kHz + setup.frame_shift = 20.0; % 320 samples at 16 kHz + setup.num_mel_bins = 40; + setup.tplg_fn = 'mel40.conf'; + export_mfcc_setup(gen_cfg, setup); + + % Same 40-bin/20ms-hop mel spectrogram with compress PCM output for the + % on-device TFLM wake-word path (KPB -> SRC -> MFCC -> tflmcly). + setup = get_mel_spectrogram_config(); + setup.frame_length = 30.0; + setup.frame_shift = 20.0; + setup.num_mel_bins = 40; + setup.compress_output = true; + setup.tplg_fn = 'mel40_compress.conf'; + export_mfcc_setup(gen_cfg, setup); + + % Blob for 40-bin/10ms-hop mel spectrogram, matching microWakeWord's + % MixConv front-end shape (40 features per 10ms stride over a 30ms + % window, 125 Hz to 7500 Hz bandwidth) -- see src/audio/microwakeword. + setup = get_mel_spectrogram_config(); + setup.frame_length = 30.0; % 480 samples at 16 kHz + setup.frame_shift = 10.0; % 160 samples at 16 kHz + setup.num_mel_bins = 40; + setup.low_freq = 125; + setup.high_freq = 7500; + setup.tplg_fn = 'mel40_10ms.conf'; + export_mfcc_setup(gen_cfg, setup); + + % Same 40-bin/10ms-hop mel spectrogram with compress PCM output for + % microWakeWord compress stream capture. + setup = get_mel_spectrogram_config(); + setup.frame_length = 30.0; + setup.frame_shift = 10.0; + setup.num_mel_bins = 40; + setup.low_freq = 125; + setup.high_freq = 7500; + setup.compress_output = true; + setup.update_controls = false; + setup.enable_vad = false; + setup.enable_dtx = false; + setup.dtx_trailing_silence_hops = 0; + setup.dtx_silence_hops_interval = 0; + setup.tplg_fn = 'mel40_10ms_compress.conf'; + export_mfcc_setup(gen_cfg, setup); + % Blob for mel spectrogram with compress PCM output and DTX setup = get_mel_spectrogram_config(); setup.compress_output = true; @@ -51,6 +100,39 @@ function setup_mfcc() setup.tplg_fn = 'ceps13_compress_dtx.conf'; export_mfcc_setup(gen_cfg, setup); + % Mel spectrogram with PCAN normalization and compress PCM output + setup = get_mel_spectrogram_config(); + setup.enable_pcan = true; + setup.compress_output = true; + setup.tplg_fn = 'mel80_pcan_compress.conf'; + export_mfcc_setup(gen_cfg, setup); + + % 40 mel bins, 10 ms hop PCAN for microWakeWord (no Slaney norm, Google-matched) + setup = get_mel_spectrogram_config(); + setup.frame_length = 30.0; + setup.frame_shift = 10.0; + setup.num_mel_bins = 40; + setup.low_freq = 125; + setup.high_freq = 7500; + setup.norm = 'none'; + setup.mel_offset = 0; + setup.mel_scale = 1.0; + setup.top_db = 0; + setup.dynamic_mmax = false; + setup.enable_pcan = true; + setup.pcan_strength = 0.95; + setup.pcan_offset = 80.0; + setup.pcan_gain_bits = 21; + setup.pcan_smoothing_coef = 819; + setup.compress_output = true; + setup.update_controls = false; + setup.enable_vad = false; + setup.enable_dtx = false; + setup.dtx_trailing_silence_hops = 0; + setup.dtx_silence_hops_interval = 0; + setup.tplg_fn = 'mel40_10ms_pcan_compress.conf'; + export_mfcc_setup(gen_cfg, setup); + end function cfg = get_mfcc_default_config() @@ -94,6 +176,11 @@ function setup_mfcc() cfg.dtx_silence_hops_interval = 0; cfg.update_controls = false; cfg.compress_output = false; + cfg.enable_pcan = false; + cfg.pcan_strength = 0.95; + cfg.pcan_offset = 80.0; + cfg.pcan_gain_bits = 21; + cfg.pcan_smoothing_coef = 819; end function cfg = get_mel_spectrogram_config() @@ -113,7 +200,7 @@ function setup_mfcc() cfg.num_mel_bins = 80; cfg.preemphasis_coefficient = 0; cfg.raw_energy = false; - cfg.remove_dc_offset = false; + cfg.remove_dc_offset = true; cfg.round_to_power_of_two = true; cfg.sample_frequency = 16000; cfg.snip_edges = true; @@ -123,20 +210,25 @@ function setup_mfcc() cfg.vtln_low = 0; cfg.vtln_warp = 1.0; cfg.window_type = 'hann'; - cfg.mel_log = 'log10'; + cfg.mel_log = 'log'; % Set to 'db' for librosa, set to 'log10' for matlab cfg.pmin = 1e-10; - cfg.top_db = 8; % applied for log10, would be 80 dB clamp for decibels as 10*log10() - cfg.mel_offset = 4.0; % For whisper like Mel scale and normalize - cfg.mel_scale = 0.25; % For whisper like Mel scale and normalize - cfg.mmax_init = 0; % Initial value max Mel value, data clamp is mmax - top_db - cfg.mmax_coef = 0; % Dynamic max Mel value decay coefficient (zero lock to found max) - cfg.dynamic_mmax = true; - cfg.enable_vad = true; + cfg.top_db = 80.0; + cfg.mel_offset = 4.0; % Whisper: (mel + 4.0) * 0.25 + cfg.mel_scale = 0.25; + cfg.mmax_init = 0; + cfg.mmax_coef = 0.005; % Whisper mmax tracking: slow decay + cfg.dynamic_mmax = false; + cfg.enable_vad = false; cfg.enable_dtx = false; cfg.dtx_trailing_silence_hops = 0; cfg.dtx_silence_hops_interval = 0; - cfg.update_controls = true; + cfg.update_controls = false; cfg.compress_output = false; + cfg.enable_pcan = false; + cfg.pcan_strength = 0.95; + cfg.pcan_offset = 80.0; + cfg.pcan_gain_bits = 21; + cfg.pcan_smoothing_coef = 819; end function export_mfcc_setup(gen_cfg, cfg) @@ -173,8 +265,9 @@ function export_mfcc_setup(gen_cfg, cfg) v = cfg.dtx_trailing_silence_hops; [b8, j] = add_w16b(v, b8, j); % DTX trailing silence hops v = cfg.dtx_silence_hops_interval; [b8, j] = add_w16b(v, b8, j); % DTX silence frame interval +v = cfg.pcan_smoothing_coef; [b8, j] = add_w32b(v, b8, j); % PCAN smoothing coef in Q14 % Reserved -for i = 1:5 +for i = 1:4 [b8, j] = add_w32b(0, b8, j); end @@ -200,10 +293,9 @@ function export_mfcc_setup(gen_cfg, cfg) v = 0; [b8, j] = add_w16b(v, b8, j); % vtln_high Qx.y TBD v = 0; [b8, j] = add_w16b(v, b8, j); % vtln_low Qx.y TBD v = 0; [b8, j] = add_w16b(v, b8, j); % vtln_warp Qx.y TBD -% reserved16[3] -for i = 1:3 - [b8, j] = add_w16b(0, b8, j); -end +v = q_convert(cfg.pcan_strength, 15); [b8, j] = add_w16b(v, b8, j); % PCAN strength in Q1.15 +v = q_convert(cfg.pcan_offset, 7); [b8, j] = add_w16b(v, b8, j); % PCAN offset in Q8.7 +v = cfg.pcan_gain_bits; [b8, j] = add_w16b(v, b8, j); % PCAN gain bits v = cfg.htk_compat; [b8, j] = add_w8b(v, b8, j); % bool v = cfg.raw_energy; [b8, j] = add_w8b(v, b8, j); % bool v = cfg.remove_dc_offset; [b8, j] = add_w8b(v, b8, j); % bool @@ -216,8 +308,9 @@ function export_mfcc_setup(gen_cfg, cfg) v = cfg.enable_dtx; [b8, j] = add_w8b(v, b8, j); % bool v = cfg.update_controls; [b8, j] = add_w8b(v, b8, j); % bool v = cfg.compress_output; [b8, j] = add_w8b(v, b8, j); % bool -% reserved_bool[4] -for i = 1:4 +v = cfg.enable_pcan; [b8, j] = add_w8b(v, b8, j); % bool +% reserved_bool[3] +for i = 1:3 [b8, j] = add_w8b(0, b8, j); end diff --git a/src/include/sof/audio/mfcc/mfcc_comp.h b/src/include/sof/audio/mfcc/mfcc_comp.h index 885339004fc0..ad4e2afdf33c 100644 --- a/src/include/sof/audio/mfcc/mfcc_comp.h +++ b/src/include/sof/audio/mfcc/mfcc_comp.h @@ -13,6 +13,9 @@ #include #include #include +#if CONFIG_COMP_MFCC_PCAN +#include +#endif #include #include #include @@ -99,6 +102,10 @@ struct mfcc_state { struct mfcc_fft fft; /**< FFT related */ struct dct_plan_16 dct; /**< DCT related */ struct psy_mel_filterbank melfb; /**< Mel filter bank */ +#if CONFIG_COMP_MFCC_PCAN + struct pcan_state pcan; /**< PCAN state */ + uint32_t *mel_linear; /**< Linear Mel band magnitudes for PCAN */ +#endif struct mfcc_cepstral_lifter lifter; /**< Cepstral lifter coefficients */ struct mat_matrix_16b *mel_spectra; /**< Pointer to scratch */ struct mat_matrix_16b *cepstral_coef; /**< Pointer to scratch */ @@ -122,8 +129,8 @@ struct mfcc_state { bool header_pending; /**< True when data header not yet written for current output */ struct mfcc_data_header header; /**< Data header for current output frame */ size_t sample_buffers_size; /**< bytes */ - int32_t *out_data_ptr; /**< Read pointer into staging data for multi-period output */ - int out_remain; /**< Remaining int32_t samples to write to sink from staging */ + void *out_data_ptr; /**< Read pointer into staging data for multi-period output */ + int out_remain; /**< Remaining samples to write to sink from staging */ int32_t *out_stage; /**< Dedicated staging buffer for pending output, decoupled from STFT scratch */ int out_stage_size; /**< Capacity of out_stage in int32_t samples */ uint32_t hop_count; /**< FFT hop counter, increments every processed hop */ diff --git a/src/include/sof/audio/mfcc/mfcc_vad.h b/src/include/sof/audio/mfcc/mfcc_vad.h index 6873343d334e..048ea475eba1 100644 --- a/src/include/sof/audio/mfcc/mfcc_vad.h +++ b/src/include/sof/audio/mfcc/mfcc_vad.h @@ -29,9 +29,9 @@ struct processing_module; #define MFCC_VAD_NOISE_INIT_FRAMES 100 /** - * \brief Slow noise floor rise coefficient in Q1.15 (0.003 * 2^15). + * \brief Slow noise floor rise coefficient in Q1.15 (0.001 * 2^15, tau ~10 s). */ -#define MFCC_VAD_NOISE_RISE_ALPHA 98 +#define MFCC_VAD_NOISE_RISE_ALPHA 33 /** * \brief Fast noise floor rise coefficient in Q1.15 (0.020 * 2^15). @@ -39,9 +39,9 @@ struct processing_module; #define MFCC_VAD_NOISE_RISE_ALPHA_FAST 655 /** - * \brief Energy threshold for speech detection in Q9.23 (0.30 * 2^23). + * \brief Energy threshold for speech detection in Q9.23 (0.20 * 2^23). */ -#define MFCC_VAD_ENERGY_THRESHOLD 2516582 +#define MFCC_VAD_ENERGY_THRESHOLD 1677722 /** * \brief Hangover frame count to keep VAD active after last speech detection. diff --git a/src/include/sof/math/pcan.h b/src/include/sof/math/pcan.h index 494e193026b4..463ae29eb5b3 100644 --- a/src/include/sof/math/pcan.h +++ b/src/include/sof/math/pcan.h @@ -33,6 +33,13 @@ #define PCAN_WIDE_DYNAMIC_BITS kWideDynamicFunctionBits /* 32 */ #define PCAN_LUT_SIZE kWideDynamicFunctionLUTSize /* 125 */ +/* Google derives this as log2(fft_size) - 1 - kFilterbankBits / 2 because its + * fixed-point FFT keeps a 1/N scale. SOF compensates the FFT length in the Mel + * filter bank and scales mel_linear to Google's 512-point range, so it is 3 for + * every FFT size. + */ +#define PCAN_INPUT_CORRECTION_BITS 3 + /** * \brief SOF PCAN configuration structure. */ diff --git a/src/include/user/mfcc.h b/src/include/user/mfcc.h index 286ee4f5e985..e8793811509b 100644 --- a/src/include/user/mfcc.h +++ b/src/include/user/mfcc.h @@ -56,7 +56,8 @@ struct sof_mfcc_config { int16_t mmax_coef; /**< Q1.15 decay coefficient for dynamic mmax, a small value for slow */ uint16_t dtx_trailing_silence_hops; /**< DTX: number of silence hops to send after speech, 0 = send first only */ uint16_t dtx_silence_hops_interval; /**< DTX: send silence frame every Nth hop during VAD=0, 0 = disable */ - uint32_t reserved[5]; + uint32_t pcan_smoothing_coef; /**< Q14 smoothing coef for PCAN, e.g. 819 for 0.05 */ + uint32_t reserved[4]; int32_t sample_frequency; /**< Hz. e.g. 16000 */ int32_t pmin; /**< Q1.31 linear power, limit minimum Mel energy, e.g. 1e-9 */ enum sof_mfcc_mel_log_type mel_log; /**< Use MEL_LOG_IS_LOG, LOG10 or DB*/ @@ -79,7 +80,9 @@ struct sof_mfcc_config { int16_t vtln_high; /**< Reserved, no support */ int16_t vtln_low; /**< Reserved, no support */ int16_t vtln_warp; /**< Reserved, no support */ - int16_t reserved16[3]; /**< Reserved for future 16-bit fields, set to 0 */ + int16_t pcan_strength; /**< Q1.15 strength alpha, e.g. 31130 for 0.95 */ + int16_t pcan_offset; /**< Q8.7 offset delta, e.g. 10240 for 80.0 */ + int16_t pcan_gain_bits; /**< Gain bits scale, e.g. 21 */ bool htk_compat; /**< Must be false */ bool raw_energy; /**< Reserved, no support */ bool remove_dc_offset; /**< Reserved, no support */ @@ -92,7 +95,8 @@ struct sof_mfcc_config { bool enable_dtx; /**< Discontinuous transmission: suppress silence after trailing frames */ bool update_controls; /**< Update controls with VAD decision */ bool compress_output; /**< Use compress PCM output: variable size, no zero padding */ - bool reserved_bool[4]; /* Reserved for future boolean flags, set to false (0) */ + bool enable_pcan; /**< Enable PCAN normalization on Mel filterbank energies */ + bool reserved_bool[3]; /* Reserved for future boolean flags, set to false (0) */ } __attribute__((packed)); #endif /* __USER_MFCC_H__ */ diff --git a/src/math/auditory/mel_filterbank_32.c b/src/math/auditory/mel_filterbank_32.c index 18244cf64990..c496973f64a1 100644 --- a/src/math/auditory/mel_filterbank_32.c +++ b/src/math/auditory/mel_filterbank_32.c @@ -102,8 +102,8 @@ void psy_apply_mel_filterbank_with_linear_32(struct psy_mel_filterbank *fb, stru /* Scale to Google microfrontend range: 25826 / 32768 ~= 0.788 */ s_comp = (s_comp * 25826U) >> 15; - if (s_comp > 65535U) - s_comp = 65535U; + if (s_comp > UINT32_MAX) + s_comp = UINT32_MAX; mel_linear[i] = (uint32_t)s_comp; } diff --git a/src/math/pcan/pcan.c b/src/math/pcan/pcan.c index eb2fccb6b1c5..a438b4e612bb 100644 --- a/src/math/pcan/pcan.c +++ b/src/math/pcan/pcan.c @@ -240,7 +240,8 @@ void pcan_log_scale(struct pcan_state *state, uint32_t *signal) if (!state || !state->enable_pcan) return; - scaled = LogScaleApply(&state->g_log_scale, signal, state->num_channels, 3); + scaled = LogScaleApply(&state->g_log_scale, signal, state->num_channels, + PCAN_INPUT_CORRECTION_BITS); for (i = state->num_channels - 1; i >= 0; --i) signal[i] = scaled[i]; } diff --git a/tools/topology/topology2/include/components/mfcc/ceps13_compress_dtx.conf b/tools/topology/topology2/include/components/mfcc/ceps13_compress_dtx.conf index 7056b9e7cb4b..306d0bca002a 100644 --- a/tools/topology/topology2/include/components/mfcc/ceps13_compress_dtx.conf +++ b/tools/topology/topology2/include/components/mfcc/ceps13_compress_dtx.conf @@ -1,4 +1,4 @@ -# Exported MFCC configuration 26-May-2026 +# Exported MFCC configuration 16-Sep-2026 # cd src/audio/mfcc/tune; octave setup_mfcc.m Object.Base.data."mfcc_config" { bytes " @@ -8,7 +8,7 @@ Object.Base.data."mfcc_config" { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x74,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x14,0x00,0xf4,0x01, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x33,0x03,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x80,0x3e,0x00,0x00, 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, @@ -17,8 +17,8 @@ Object.Base.data."mfcc_config" { 0xc3,0x35,0x00,0x2c,0x00,0x00,0x00,0x00, 0x90,0x01,0xa0,0x00,0x00,0x00,0x14,0x00, 0x0d,0x00,0x17,0x00,0x00,0x00,0x00,0x64, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01, + 0x00,0x00,0x00,0x00,0x00,0x00,0x9a,0x79, + 0x00,0x28,0x15,0x00,0x00,0x01,0x01,0x01, 0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x01, 0x00,0x00,0x00,0x00" } diff --git a/tools/topology/topology2/include/components/mfcc/default.conf b/tools/topology/topology2/include/components/mfcc/default.conf index 0ac19fa71d04..297c4ec13581 100644 --- a/tools/topology/topology2/include/components/mfcc/default.conf +++ b/tools/topology/topology2/include/components/mfcc/default.conf @@ -1,4 +1,4 @@ -# Exported MFCC configuration 26-May-2026 +# Exported MFCC configuration 16-Sep-2026 # cd src/audio/mfcc/tune; octave setup_mfcc.m Object.Base.data."mfcc_config" { bytes " @@ -8,7 +8,7 @@ Object.Base.data."mfcc_config" { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x74,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x33,0x03,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x80,0x3e,0x00,0x00, 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, @@ -17,8 +17,8 @@ Object.Base.data."mfcc_config" { 0xc3,0x35,0x00,0x2c,0x00,0x00,0x00,0x00, 0x90,0x01,0xa0,0x00,0x00,0x00,0x14,0x00, 0x0d,0x00,0x17,0x00,0x00,0x00,0x00,0x64, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01, + 0x00,0x00,0x00,0x00,0x00,0x00,0x9a,0x79, + 0x00,0x28,0x15,0x00,0x00,0x01,0x01,0x01, 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00" } diff --git a/tools/topology/topology2/include/components/mfcc/mel40.conf b/tools/topology/topology2/include/components/mfcc/mel40.conf new file mode 100644 index 000000000000..abede8b58913 --- /dev/null +++ b/tools/topology/topology2/include/components/mfcc/mel40.conf @@ -0,0 +1,24 @@ +# Exported MFCC configuration 16-Sep-2026 +# cd src/audio/mfcc/tune; octave setup_mfcc.m +Object.Base.data."mfcc_config" { + bytes " + 0x53,0x4f,0x46,0x34,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x01,0xd0,0x01,0x03, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x00,0x02,0x00,0x04, + 0x00,0x00,0xa4,0x00,0x00,0x00,0x00,0x00, + 0x33,0x03,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x80,0x3e,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xe0,0x01,0x40,0x01,0x40,0x1f,0x00,0x00, + 0x00,0x00,0x28,0x00,0x00,0x00,0x00,0x28, + 0x00,0x00,0x00,0x00,0x00,0x00,0x9a,0x79, + 0x00,0x28,0x15,0x00,0x00,0x00,0x01,0x01, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00" +} diff --git a/tools/topology/topology2/include/components/mfcc/mel40_10ms.conf b/tools/topology/topology2/include/components/mfcc/mel40_10ms.conf new file mode 100644 index 000000000000..bf2bdd39bb5f --- /dev/null +++ b/tools/topology/topology2/include/components/mfcc/mel40_10ms.conf @@ -0,0 +1,24 @@ +# Exported MFCC configuration 11-Sep-2026 +# cd src/audio/mfcc/tune; octave setup_mfcc.m +Object.Base.data."mfcc_config" { + bytes " + 0x53,0x4f,0x46,0x34,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x01,0xd0,0x01,0x03, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x00,0x02,0x00,0x04, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x80,0x3e,0x00,0x00, + 0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xe0,0x01,0xa0,0x00,0x4c,0x1d,0x7d,0x00, + 0x00,0x00,0x28,0x00,0x00,0x00,0x00,0x04, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01, + 0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x00, + 0x00,0x00,0x00,0x00" +} diff --git a/tools/topology/topology2/include/components/mfcc/mel40_10ms_compress.conf b/tools/topology/topology2/include/components/mfcc/mel40_10ms_compress.conf new file mode 100644 index 000000000000..453a1cb7e412 --- /dev/null +++ b/tools/topology/topology2/include/components/mfcc/mel40_10ms_compress.conf @@ -0,0 +1,25 @@ +# Exported MFCC configuration 16-Sep-2026 +# cd src/audio/mfcc/tune; octave setup_mfcc.m +# enable_vad=0 enable_dtx=0: streaming MWW must see every hop, and VAD is not run. +Object.Base.data."mfcc_config" { + bytes " + 0x53,0x4f,0x46,0x34,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x01,0xd0,0x01,0x03, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x00,0x02,0x00,0x04, + 0x00,0x00,0xa4,0x00,0x00,0x00,0x00,0x00, + 0x33,0x03,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x80,0x3e,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xe0,0x01,0xa0,0x00,0x4c,0x1d,0x7d,0x00, + 0x00,0x00,0x28,0x00,0x00,0x00,0x00,0x28, + 0x00,0x00,0x00,0x00,0x00,0x00,0x9a,0x79, + 0x00,0x28,0x15,0x00,0x00,0x00,0x01,0x01, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x01, + 0x00,0x00,0x00,0x00" +} diff --git a/tools/topology/topology2/include/components/mfcc/mel40_10ms_pcan_compress.conf b/tools/topology/topology2/include/components/mfcc/mel40_10ms_pcan_compress.conf new file mode 100644 index 000000000000..8ab315ed1ac4 --- /dev/null +++ b/tools/topology/topology2/include/components/mfcc/mel40_10ms_pcan_compress.conf @@ -0,0 +1,26 @@ +# Exported MFCC configuration 16-Sep-2026 +# cd src/audio/mfcc/tune; octave setup_mfcc.m +# enable_vad=1 enable_dtx=0 update_controls=0: VAD runs to populate energy/vad_flag +# for MWW gating; DTX off so every hop is still emitted; no kcontrol notifications. +Object.Base.data."mfcc_config" { + bytes " + 0x53,0x4f,0x46,0x34,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x01,0xd0,0x01,0x03, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x00,0x00,0x00,0x10, + 0x00,0x00,0xa4,0x00,0x00,0x00,0x00,0x00, + 0x33,0x03,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x80,0x3e,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xe0,0x01,0xa0,0x00,0x4c,0x1d,0x7d,0x00, + 0x00,0x00,0x28,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x9a,0x79, + 0x00,0x28,0x15,0x00,0x00,0x00,0x01,0x01, + 0x01,0x00,0x00,0x00,0x01,0x00,0x00,0x01, + 0x01,0x00,0x00,0x00" +} diff --git a/tools/topology/topology2/include/components/mfcc/mel40_compress.conf b/tools/topology/topology2/include/components/mfcc/mel40_compress.conf new file mode 100644 index 000000000000..c4881ac0060f --- /dev/null +++ b/tools/topology/topology2/include/components/mfcc/mel40_compress.conf @@ -0,0 +1,24 @@ +# Exported MFCC configuration 16-Sep-2026 +# cd src/audio/mfcc/tune; octave setup_mfcc.m +Object.Base.data."mfcc_config" { + bytes " + 0x53,0x4f,0x46,0x34,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x01,0xd0,0x01,0x03, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x00,0x02,0x00,0x04, + 0x00,0x00,0xa4,0x00,0x00,0x00,0x00,0x00, + 0x33,0x03,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x80,0x3e,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xe0,0x01,0x40,0x01,0x40,0x1f,0x00,0x00, + 0x00,0x00,0x28,0x00,0x00,0x00,0x00,0x28, + 0x00,0x00,0x00,0x00,0x00,0x00,0x9a,0x79, + 0x00,0x28,0x15,0x00,0x00,0x00,0x01,0x01, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x01, + 0x00,0x00,0x00,0x00" +} diff --git a/tools/topology/topology2/include/components/mfcc/mel80.conf b/tools/topology/topology2/include/components/mfcc/mel80.conf index b18baadd459b..16b9d12fabed 100644 --- a/tools/topology/topology2/include/components/mfcc/mel80.conf +++ b/tools/topology/topology2/include/components/mfcc/mel80.conf @@ -1,4 +1,4 @@ -# Exported MFCC configuration 26-May-2026 +# Exported MFCC configuration 16-Sep-2026 # cd src/audio/mfcc/tune; octave setup_mfcc.m Object.Base.data."mfcc_config" { bytes " @@ -7,18 +7,18 @@ Object.Base.data."mfcc_config" { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x74,0x00,0x00,0x00,0x00,0x02,0x00,0x04, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0xa4,0x00,0x00,0x00,0x00,0x00, + 0x33,0x03,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x80,0x3e,0x00,0x00, - 0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x90,0x01,0xa0,0x00,0x40,0x1f,0x00,0x00, - 0x00,0x00,0x50,0x00,0x00,0x00,0x00,0x04, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01, - 0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x00, + 0x00,0x00,0x50,0x00,0x00,0x00,0x00,0x28, + 0x00,0x00,0x00,0x00,0x00,0x00,0x9a,0x79, + 0x00,0x28,0x15,0x00,0x00,0x00,0x01,0x01, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00" } diff --git a/tools/topology/topology2/include/components/mfcc/mel80_compress.conf b/tools/topology/topology2/include/components/mfcc/mel80_compress.conf index f26f2af6980c..e104b3579e75 100644 --- a/tools/topology/topology2/include/components/mfcc/mel80_compress.conf +++ b/tools/topology/topology2/include/components/mfcc/mel80_compress.conf @@ -1,4 +1,4 @@ -# Exported MFCC configuration 26-May-2026 +# Exported MFCC configuration 16-Sep-2026 # cd src/audio/mfcc/tune; octave setup_mfcc.m Object.Base.data."mfcc_config" { bytes " @@ -7,18 +7,18 @@ Object.Base.data."mfcc_config" { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x74,0x00,0x00,0x00,0x00,0x02,0x00,0x04, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0xa4,0x00,0x00,0x00,0x00,0x00, + 0x33,0x03,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x80,0x3e,0x00,0x00, - 0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x90,0x01,0xa0,0x00,0x40,0x1f,0x00,0x00, - 0x00,0x00,0x50,0x00,0x00,0x00,0x00,0x04, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01, - 0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01, + 0x00,0x00,0x50,0x00,0x00,0x00,0x00,0x28, + 0x00,0x00,0x00,0x00,0x00,0x00,0x9a,0x79, + 0x00,0x28,0x15,0x00,0x00,0x00,0x01,0x01, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x01, 0x00,0x00,0x00,0x00" } diff --git a/tools/topology/topology2/include/components/mfcc/mel80_compress_dtx.conf b/tools/topology/topology2/include/components/mfcc/mel80_compress_dtx.conf index d225811ca4d1..c3e53b28c1fb 100644 --- a/tools/topology/topology2/include/components/mfcc/mel80_compress_dtx.conf +++ b/tools/topology/topology2/include/components/mfcc/mel80_compress_dtx.conf @@ -1,4 +1,4 @@ -# Exported MFCC configuration 26-May-2026 +# Exported MFCC configuration 16-Sep-2026 # cd src/audio/mfcc/tune; octave setup_mfcc.m Object.Base.data."mfcc_config" { bytes " @@ -7,18 +7,18 @@ Object.Base.data."mfcc_config" { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x74,0x00,0x00,0x00,0x00,0x02,0x00,0x04, - 0x00,0x00,0x00,0x00,0x14,0x00,0xf4,0x01, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0xa4,0x00,0x14,0x00,0xf4,0x01, + 0x33,0x03,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x80,0x3e,0x00,0x00, - 0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x90,0x01,0xa0,0x00,0x40,0x1f,0x00,0x00, - 0x00,0x00,0x50,0x00,0x00,0x00,0x00,0x04, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01, - 0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01, + 0x00,0x00,0x50,0x00,0x00,0x00,0x00,0x28, + 0x00,0x00,0x00,0x00,0x00,0x00,0x9a,0x79, + 0x00,0x28,0x15,0x00,0x00,0x00,0x01,0x01, + 0x01,0x00,0x00,0x00,0x00,0x01,0x00,0x01, 0x00,0x00,0x00,0x00" } diff --git a/tools/topology/topology2/include/components/mfcc/mel80_pcan_compress.conf b/tools/topology/topology2/include/components/mfcc/mel80_pcan_compress.conf new file mode 100644 index 000000000000..4892bf431b6c --- /dev/null +++ b/tools/topology/topology2/include/components/mfcc/mel80_pcan_compress.conf @@ -0,0 +1,24 @@ +# Exported MFCC configuration 16-Sep-2026 +# cd src/audio/mfcc/tune; octave setup_mfcc.m +Object.Base.data."mfcc_config" { + bytes " + 0x53,0x4f,0x46,0x34,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x01,0xd0,0x01,0x03, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x74,0x00,0x00,0x00,0x00,0x02,0x00,0x04, + 0x00,0x00,0xa4,0x00,0x00,0x00,0x00,0x00, + 0x33,0x03,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x80,0x3e,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x90,0x01,0xa0,0x00,0x40,0x1f,0x00,0x00, + 0x00,0x00,0x50,0x00,0x00,0x00,0x00,0x28, + 0x00,0x00,0x00,0x00,0x00,0x00,0x9a,0x79, + 0x00,0x28,0x15,0x00,0x00,0x00,0x01,0x01, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x01, + 0x01,0x00,0x00,0x00" +} From acc5fe33e2db494afa54e60a4b4d86c0411ba3b7 Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Mon, 2 Nov 2026 19:09:50 +0200 Subject: [PATCH 05/10] audio: mfcc: select output data type at setup Add an output mode to the MFCC state that is set once in setup from the configuration blob: cepstral coefficients, int32 Mel or int8 PCAN Mel. The output preparation and the output sample size now use it instead of checking the PCAN enable flag under the Kconfig option in several places. This prepares adding more output data types. The output is bit-exact with the previous version for the cepstral, Mel and PCAN benchmark topologies in testbench. Signed-off-by: Seppo Ingalsuo --- src/audio/mfcc/mfcc_common.c | 39 +++++++++++--------------- src/audio/mfcc/mfcc_setup.c | 5 ++++ src/include/sof/audio/mfcc/mfcc_comp.h | 14 +++++++++ 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/audio/mfcc/mfcc_common.c b/src/audio/mfcc/mfcc_common.c index e0f3fa6476d7..ff9a6fd9eac9 100644 --- a/src/audio/mfcc/mfcc_common.c +++ b/src/audio/mfcc/mfcc_common.c @@ -543,8 +543,10 @@ static void mfcc_prepare_output(struct mfcc_state *state, int num_ceps) if (num_ceps <= 0) return; + switch (state->out_mode) { #if CONFIG_COMP_MFCC_PCAN - if (state->pcan.enable_pcan) { + case MFCC_OUT_MEL8_PCAN: + { int8_t *out8 = (int8_t *)state->out_stage; /* In PCAN mode mel_linear[] is indexed by k < num_ceps. @@ -566,24 +568,23 @@ static void mfcc_prepare_output(struct mfcc_state *state, int num_ceps) out8[k] = (int8_t)val; } - - state->out_data_ptr = state->out_stage; - state->out_remain = num_ceps; - state->header_pending = true; - return; + break; } #endif - - /* Copy into out_stage so the next STFT hop may freely reuse - * mel_log_32 / cepstral_coef while this frame is still pending. - */ - if (state->mel_only) { + case MFCC_OUT_MEL32: + /* Copy into out_stage so the next STFT hop may freely reuse + * mel_log_32 / cepstral_coef while this frame is still pending. + */ for (k = 0; k < num_ceps; k++) state->out_stage[k] = state->mel_log_32[k]; - } else { + break; + case MFCC_OUT_CEPS: /* Widen int16 Q9.7 cepstral coefficients to int32 Q9.23. */ for (k = 0; k < num_ceps; k++) state->out_stage[k] = (int32_t)state->cepstral_coef->data[k] << 16; + break; + default: + return; } state->out_data_ptr = state->out_stage; @@ -624,11 +625,7 @@ static int mfcc_output_compress(struct processing_module *mod, struct mfcc_comp_ if (!pending) return 0; -#if CONFIG_COMP_MFCC_PCAN - size_t sample_size = state->pcan.enable_pcan ? sizeof(int8_t) : sizeof(int32_t); -#else - size_t sample_size = sizeof(int32_t); -#endif + size_t sample_size = mfcc_out_sample_size(state); out_bytes = (state->header_pending ? sizeof(state->header) : 0) + state->out_remain * sample_size; @@ -746,13 +743,9 @@ static int mfcc_output_legacy(struct processing_module *mod, struct mfcc_comp_da } } - /* Write pending feature data (int8 in PCAN mode, int32 otherwise) */ + /* Write pending feature data (int8 or int32 per output mode) */ if (state->out_remain > 0 && avail > 0) { -#if CONFIG_COMP_MFCC_PCAN - size_t sample_size = state->pcan.enable_pcan ? sizeof(int8_t) : sizeof(int32_t); -#else - size_t sample_size = sizeof(int32_t); -#endif + size_t sample_size = mfcc_out_sample_size(state); data_bytes = state->out_remain * sample_size; to_write = MIN(data_bytes, avail); if (sample_size > 1) diff --git a/src/audio/mfcc/mfcc_setup.c b/src/audio/mfcc/mfcc_setup.c index 933bfb47a4b5..e7a42c51d4ee 100644 --- a/src/audio/mfcc/mfcc_setup.c +++ b/src/audio/mfcc/mfcc_setup.c @@ -519,6 +519,11 @@ int mfcc_setup(struct processing_module *mod, int max_frames, int sample_rate, i } #endif + if (config->enable_pcan) + state->out_mode = MFCC_OUT_MEL8_PCAN; + else + state->out_mode = state->mel_only ? MFCC_OUT_MEL32 : MFCC_OUT_CEPS; + comp_dbg(dev, "done"); return 0; diff --git a/src/include/sof/audio/mfcc/mfcc_comp.h b/src/include/sof/audio/mfcc/mfcc_comp.h index ad4e2afdf33c..b0c69cbef1d2 100644 --- a/src/include/sof/audio/mfcc/mfcc_comp.h +++ b/src/include/sof/audio/mfcc/mfcc_comp.h @@ -42,6 +42,13 @@ /** \brief Switch control index for VAD notification to user space */ #define MFCC_CTRL_INDEX_VAD 0 +/** \brief Output data type, selected at setup from the configuration blob. */ +enum mfcc_out_mode { + MFCC_OUT_CEPS = 0, /**< Cepstral coefficients, int16 Q9.7 widened to int32 */ + MFCC_OUT_MEL32, /**< Mel log spectra, int32 Q9.23 */ + MFCC_OUT_MEL8_PCAN, /**< PCAN normalized Mel energies, int8 */ +}; + /** * \brief Data header prepended to every MFCC output frame. * @@ -124,6 +131,7 @@ struct mfcc_state { int high_freq; int sample_rate; bool mel_only; /**< When true, output Mel spectra instead of cepstral coefficients */ + enum mfcc_out_mode out_mode; /**< Output data type */ bool waiting_fill; /**< booleans */ bool prev_samples_valid; bool header_pending; /**< True when data header not yet written for current output */ @@ -158,6 +166,12 @@ static inline int mfcc_buffer_samples_without_wrap(struct mfcc_buffer *buffer, i return buffer->end_addr - ptr; } +/** \brief Size in bytes of one output value for the current output mode. */ +static inline size_t mfcc_out_sample_size(const struct mfcc_state *state) +{ + return state->out_mode == MFCC_OUT_MEL8_PCAN ? sizeof(int8_t) : sizeof(int32_t); +} + static inline int16_t *mfcc_buffer_wrap(struct mfcc_buffer *buffer, int16_t *ptr) { if (ptr >= buffer->end_addr) From 8fcc6416aa9479022335ebefc8b7dd1c2880c65e Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Mon, 2 Nov 2026 19:19:28 +0200 Subject: [PATCH 06/10] audio: mfcc: add 8-bit soft AGC Mel output mode Add AGC8, an int8 Mel output mode that is a light alternative to PCAN. A single gain is added to all Q9.23 Mel log values of a hop. It attacks instantly to keep the hop peak at or below +1.0 and releases towards a target, slower when the VAD reports speech. The values are then clamped and converted to int8 Q1.7. It needs no per-channel noise estimate and no tflite-micro code, and is always built, so one MFCC build supports cepstral, int32 Mel, PCAN and AGC8 output. The algorithm is moved from the removed microwakeword int32 input path and is bit-exact with it. AGC8 is enabled with the new enable_agc8 blob flag and tuned with agc_target, agc_floor, agc_release and agc_release_speech, which take the previously reserved blob space. Zero values select the defaults, so the blob size and existing blobs are unchanged. Setup rejects a blob that enables both PCAN and AGC8, or either of them with cepstral output. Signed-off-by: Seppo Ingalsuo --- src/audio/mfcc/CMakeLists.txt | 2 +- src/audio/mfcc/README.md | 15 ++++++ src/audio/mfcc/llext/CMakeLists.txt | 1 + src/audio/mfcc/mfcc_agc8.c | 71 ++++++++++++++++++++++++++ src/audio/mfcc/mfcc_common.c | 4 ++ src/audio/mfcc/mfcc_setup.c | 18 ++++++- src/include/sof/audio/mfcc/mfcc_agc8.h | 68 ++++++++++++++++++++++++ src/include/sof/audio/mfcc/mfcc_comp.h | 6 ++- src/include/user/mfcc.h | 9 +++- 9 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 src/audio/mfcc/mfcc_agc8.c create mode 100644 src/include/sof/audio/mfcc/mfcc_agc8.h diff --git a/src/audio/mfcc/CMakeLists.txt b/src/audio/mfcc/CMakeLists.txt index 0744ae5e0373..3ad5f39fcdfd 100644 --- a/src/audio/mfcc/CMakeLists.txt +++ b/src/audio/mfcc/CMakeLists.txt @@ -22,7 +22,7 @@ if(CONFIG_COMP_MFCC STREQUAL "m" AND DEFINED CONFIG_LLEXT) add_subdirectory(llext ${PROJECT_BINARY_DIR}/mfcc_llext) add_dependencies(app mfcc) else() - add_local_sources(sof mfcc.c mfcc_setup.c mfcc_common.c mfcc_generic.c mfcc_hifi4.c mfcc_hifi3.c mfcc_vad.c) + add_local_sources(sof mfcc.c mfcc_setup.c mfcc_common.c mfcc_generic.c mfcc_hifi4.c mfcc_hifi3.c mfcc_vad.c mfcc_agc8.c) if(CONFIG_IPC_MAJOR_4) add_local_sources(sof mfcc_ipc4.c) endif() diff --git a/src/audio/mfcc/README.md b/src/audio/mfcc/README.md index e2f348231f24..6c7f598110a9 100644 --- a/src/audio/mfcc/README.md +++ b/src/audio/mfcc/README.md @@ -10,6 +10,19 @@ The MFCC module converts raw streaming time-domain PCM audio into compact acoust When enabled, **PCAN** applies per-channel adaptive dynamic gain control and piecewise root dynamic range compression across time to suppress stationary background noise and normalize channel energy before feature quantization. +### Output modes + +One build supports all output modes, selected at runtime by the configuration blob: + +| Mode | Blob settings | Output per hop | +|---|---|---| +| Cepstral coefficients | `num_ceps > 0` | int16 Q9.7 widened to int32 | +| Mel log spectra | `num_ceps == 0` | int32 Q9.23 | +| PCAN Mel | `num_ceps == 0`, `enable_pcan` (needs `CONFIG_COMP_MFCC_PCAN`) | int8 | +| Soft AGC Mel (AGC8) | `num_ceps == 0`, `enable_agc8` | int8 Q1.7 | + +AGC8 is a light alternative to PCAN without the per-channel noise estimate and the `tflite-micro` dependency. A single gain is added to all Q9.23 Mel log values of a hop. The gain attacks instantly to keep the hop peak at or below +1.0, and releases towards a target with a slower step when the VAD reports speech. The values are clamped to -1.0 .. +1.0 and converted to int8 Q1.7. The blob fields `agc_target`, `agc_floor` (Q8.7) and `agc_release`, `agc_release_speech` (Q9.23 per hop) set the parameters, zero selects the default. + --- ## 2. Architecture & Data Flow @@ -22,6 +35,8 @@ graph LR Mel --> PCAN[PCAN Gain Normalization] PCAN --> Log[Log Scaling / dB] Log --> VAD[VAD / DTX Silence Gating] + Log --> AGC8[Soft AGC int8] + VAD --> AGC8 Log --> DCT[DCT: Cepstral Matrix] DCT --> Lifter[Cepstral Lifter] Lifter --> Out[MFCC Output Features] diff --git a/src/audio/mfcc/llext/CMakeLists.txt b/src/audio/mfcc/llext/CMakeLists.txt index 688342751d75..0032f469c1ef 100644 --- a/src/audio/mfcc/llext/CMakeLists.txt +++ b/src/audio/mfcc/llext/CMakeLists.txt @@ -8,5 +8,6 @@ sof_llext_build("mfcc" ../mfcc_generic.c ../mfcc_hifi3.c ../mfcc_hifi4.c + ../mfcc_agc8.c LIB openmodules ) diff --git a/src/audio/mfcc/mfcc_agc8.c b/src/audio/mfcc/mfcc_agc8.c new file mode 100644 index 000000000000..90f1b19dc936 --- /dev/null +++ b/src/audio/mfcc/mfcc_agc8.c @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// Author: Seppo Ingalsuo + +/** + * \file mfcc_agc8.c + * \brief Soft AGC and int8 quantization of MFCC Mel log spectra. + */ + +#include +#include +#include +#include + +#define MFCC_AGC8_CLIP_MAX_Q23 (1 << 23) +#define MFCC_AGC8_CLIP_MIN_Q23 (-(1 << 23)) + +void mfcc_agc8_init(struct mfcc_agc8 *agc, const struct sof_mfcc_config *config) +{ + /* Blob target and floor are Q8.7, release steps are Q9.23 */ + agc->target = config->agc_target ? (int32_t)config->agc_target << 16 : + MFCC_AGC8_TARGET_Q23; + agc->floor = config->agc_floor ? (int32_t)config->agc_floor << 16 : + MFCC_AGC8_FLOOR_Q23; + agc->release = config->agc_release ? config->agc_release : + MFCC_AGC8_RELEASE_Q23; + agc->release_speech = config->agc_release_speech ? config->agc_release_speech : + MFCC_AGC8_RELEASE_SPEECH_Q23; + agc->gain = agc->target; +} + +void mfcc_agc8_process(struct mfcc_agc8 *agc, const int32_t *mel, int8_t *out, int n, + bool speech) +{ + int32_t peak = mel[0]; + int32_t gain; + int64_t v; + int i; + + for (i = 1; i < n; i++) { + if (mel[i] > peak) + peak = mel[i]; + } + + /* Attack: instantly keep the hop peak at or below +1.0 */ + if (agc->gain > (int64_t)MFCC_AGC8_CLIP_MAX_Q23 - peak) + agc->gain = MFCC_AGC8_CLIP_MAX_Q23 - peak; + if (agc->gain < agc->floor) + agc->gain = agc->floor; + + gain = agc->gain; + for (i = 0; i < n; i++) { + v = (int64_t)mel[i] + gain; + if (v > MFCC_AGC8_CLIP_MAX_Q23) + v = MFCC_AGC8_CLIP_MAX_Q23; + else if (v < MFCC_AGC8_CLIP_MIN_Q23) + v = MFCC_AGC8_CLIP_MIN_Q23; + + v = Q_SHIFT_RND(v, 23, 7); + out[i] = (int8_t)(v > INT8_MAX ? INT8_MAX : v); + } + + /* Release towards target, with a slower step during speech */ + if (agc->gain < agc->target) { + agc->gain += speech ? agc->release_speech : agc->release; + if (agc->gain > agc->target) + agc->gain = agc->target; + } +} diff --git a/src/audio/mfcc/mfcc_common.c b/src/audio/mfcc/mfcc_common.c index ff9a6fd9eac9..5af5a4b3179d 100644 --- a/src/audio/mfcc/mfcc_common.c +++ b/src/audio/mfcc/mfcc_common.c @@ -571,6 +571,10 @@ static void mfcc_prepare_output(struct mfcc_state *state, int num_ceps) break; } #endif + case MFCC_OUT_MEL8_AGC: + mfcc_agc8_process(&state->agc8, state->mel_log_32, (int8_t *)state->out_stage, + num_ceps, state->header.vad_flag != 0); + break; case MFCC_OUT_MEL32: /* Copy into out_stage so the next STFT hop may freely reuse * mel_log_32 / cepstral_coef while this frame is still pending. diff --git a/src/audio/mfcc/mfcc_setup.c b/src/audio/mfcc/mfcc_setup.c index e7a42c51d4ee..a8b14ffa5aa9 100644 --- a/src/audio/mfcc/mfcc_setup.c +++ b/src/audio/mfcc/mfcc_setup.c @@ -212,6 +212,16 @@ int mfcc_setup(struct processing_module *mod, int max_frames, int sample_rate, i return -EINVAL; } + if (config->enable_pcan && config->enable_agc8) { + comp_err(dev, "enable_pcan and enable_agc8 are exclusive"); + return -EINVAL; + } + + if ((config->enable_pcan || config->enable_agc8) && config->num_ceps > 0) { + comp_err(dev, "int8 Mel output requires num_ceps 0"); + return -EINVAL; + } + cd->max_frames = max_frames; state->sample_rate = sample_rate; state->low_freq = config->low_freq; @@ -519,10 +529,14 @@ int mfcc_setup(struct processing_module *mod, int max_frames, int sample_rate, i } #endif - if (config->enable_pcan) + if (config->enable_pcan) { state->out_mode = MFCC_OUT_MEL8_PCAN; - else + } else if (config->enable_agc8) { + mfcc_agc8_init(&state->agc8, config); + state->out_mode = MFCC_OUT_MEL8_AGC; + } else { state->out_mode = state->mel_only ? MFCC_OUT_MEL32 : MFCC_OUT_CEPS; + } comp_dbg(dev, "done"); return 0; diff --git a/src/include/sof/audio/mfcc/mfcc_agc8.h b/src/include/sof/audio/mfcc/mfcc_agc8.h new file mode 100644 index 000000000000..62962ae8902f --- /dev/null +++ b/src/include/sof/audio/mfcc/mfcc_agc8.h @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + * + * Author: Seppo Ingalsuo + */ + +/** + * \file mfcc_agc8.h + * \brief Soft AGC and int8 quantization of MFCC Mel log spectra. + * + * A light alternative to PCAN for int8 Mel feature output. A single gain + * is added to all Q9.23 Mel log values of a hop. The gain attacks + * instantly so that the hop peak stays at or below +1.0 and releases + * slowly towards the target, slower during speech. The result is clamped + * to -1.0 .. +1.0 and converted to int8 Q1.7. + */ + +#ifndef __SOF_AUDIO_MFCC_MFCC_AGC8_H__ +#define __SOF_AUDIO_MFCC_MFCC_AGC8_H__ + +#include +#include + +struct sof_mfcc_config; + +/** \brief Default gain target in Q9.23, 0.25 */ +#define MFCC_AGC8_TARGET_Q23 2097152 +/** \brief Default gain floor in Q9.23, -2.0 */ +#define MFCC_AGC8_FLOOR_Q23 -16777216 +/** \brief Default release step per hop in Q9.23 during silence */ +#define MFCC_AGC8_RELEASE_Q23 4194 +/** \brief Default release step per hop in Q9.23 during speech */ +#define MFCC_AGC8_RELEASE_SPEECH_Q23 419 + +/** \brief Soft AGC state */ +struct mfcc_agc8 { + int32_t gain; /**< Current gain in Q9.23 */ + int32_t target; /**< Gain target in Q9.23 */ + int32_t floor; /**< Minimum gain in Q9.23 */ + int32_t release; /**< Release step per hop in Q9.23 during silence */ + int32_t release_speech; /**< Release step per hop in Q9.23 during speech */ +}; + +/** + * \brief Initialize soft AGC parameters from the configuration blob. + * + * Zero valued blob parameters select the defaults. The gain is set to + * the target. + * + * \param[out] agc Soft AGC state. + * \param[in] config MFCC configuration blob. + */ +void mfcc_agc8_init(struct mfcc_agc8 *agc, const struct sof_mfcc_config *config); + +/** + * \brief Apply soft AGC to one hop of Mel log values and convert to int8. + * + * \param[in,out] agc Soft AGC state. + * \param[in] mel Mel log values in Q9.23. + * \param[out] out int8 Q1.7 output values. + * \param[in] n Number of Mel values. + * \param[in] speech True when VAD reports speech for this hop. + */ +void mfcc_agc8_process(struct mfcc_agc8 *agc, const int32_t *mel, int8_t *out, int n, + bool speech); + +#endif /* __SOF_AUDIO_MFCC_MFCC_AGC8_H__ */ diff --git a/src/include/sof/audio/mfcc/mfcc_comp.h b/src/include/sof/audio/mfcc/mfcc_comp.h index b0c69cbef1d2..43b69f869aae 100644 --- a/src/include/sof/audio/mfcc/mfcc_comp.h +++ b/src/include/sof/audio/mfcc/mfcc_comp.h @@ -16,6 +16,7 @@ #if CONFIG_COMP_MFCC_PCAN #include #endif +#include #include #include #include @@ -47,6 +48,7 @@ enum mfcc_out_mode { MFCC_OUT_CEPS = 0, /**< Cepstral coefficients, int16 Q9.7 widened to int32 */ MFCC_OUT_MEL32, /**< Mel log spectra, int32 Q9.23 */ MFCC_OUT_MEL8_PCAN, /**< PCAN normalized Mel energies, int8 */ + MFCC_OUT_MEL8_AGC, /**< Soft AGC Mel log spectra, int8 Q1.7 */ }; /** @@ -113,6 +115,7 @@ struct mfcc_state { struct pcan_state pcan; /**< PCAN state */ uint32_t *mel_linear; /**< Linear Mel band magnitudes for PCAN */ #endif + struct mfcc_agc8 agc8; /**< Soft AGC state for int8 Mel output */ struct mfcc_cepstral_lifter lifter; /**< Cepstral lifter coefficients */ struct mat_matrix_16b *mel_spectra; /**< Pointer to scratch */ struct mat_matrix_16b *cepstral_coef; /**< Pointer to scratch */ @@ -169,7 +172,8 @@ static inline int mfcc_buffer_samples_without_wrap(struct mfcc_buffer *buffer, i /** \brief Size in bytes of one output value for the current output mode. */ static inline size_t mfcc_out_sample_size(const struct mfcc_state *state) { - return state->out_mode == MFCC_OUT_MEL8_PCAN ? sizeof(int8_t) : sizeof(int32_t); + return (state->out_mode == MFCC_OUT_MEL8_PCAN || state->out_mode == MFCC_OUT_MEL8_AGC) ? + sizeof(int8_t) : sizeof(int32_t); } static inline int16_t *mfcc_buffer_wrap(struct mfcc_buffer *buffer, int16_t *ptr) diff --git a/src/include/user/mfcc.h b/src/include/user/mfcc.h index e8793811509b..80604cfd2729 100644 --- a/src/include/user/mfcc.h +++ b/src/include/user/mfcc.h @@ -57,7 +57,11 @@ struct sof_mfcc_config { uint16_t dtx_trailing_silence_hops; /**< DTX: number of silence hops to send after speech, 0 = send first only */ uint16_t dtx_silence_hops_interval; /**< DTX: send silence frame every Nth hop during VAD=0, 0 = disable */ uint32_t pcan_smoothing_coef; /**< Q14 smoothing coef for PCAN, e.g. 819 for 0.05 */ - uint32_t reserved[4]; + int16_t agc_target; /**< Q8.7 soft AGC gain target, 0 = default 0.25 */ + int16_t agc_floor; /**< Q8.7 soft AGC minimum gain, 0 = default -2.0 */ + int16_t agc_release; /**< Q9.23 soft AGC release step per hop, 0 = default 4194 */ + int16_t agc_release_speech; /**< Q9.23 release step per hop in speech, 0 = default 419 */ + uint32_t reserved[2]; int32_t sample_frequency; /**< Hz. e.g. 16000 */ int32_t pmin; /**< Q1.31 linear power, limit minimum Mel energy, e.g. 1e-9 */ enum sof_mfcc_mel_log_type mel_log; /**< Use MEL_LOG_IS_LOG, LOG10 or DB*/ @@ -96,7 +100,8 @@ struct sof_mfcc_config { bool update_controls; /**< Update controls with VAD decision */ bool compress_output; /**< Use compress PCM output: variable size, no zero padding */ bool enable_pcan; /**< Enable PCAN normalization on Mel filterbank energies */ - bool reserved_bool[3]; /* Reserved for future boolean flags, set to false (0) */ + bool enable_agc8; /**< Enable soft AGC and int8 Mel output, exclusive with PCAN */ + bool reserved_bool[2]; /* Reserved for future boolean flags, set to false (0) */ } __attribute__((packed)); #endif /* __USER_MFCC_H__ */ From 41d79184289141bea37ab042e890b881de9e7772 Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Tue, 3 Nov 2026 14:41:48 +0200 Subject: [PATCH 07/10] test: ztest: pcan: use YAML list for testcase tags Twister rejects the space separated tags string in the PCAN test case description, so the test could not be run. Use a YAML list like the other math unit tests. Signed-off-by: Seppo Ingalsuo --- test/ztest/unit/math/advanced/pcan/testcase.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/ztest/unit/math/advanced/pcan/testcase.yaml b/test/ztest/unit/math/advanced/pcan/testcase.yaml index 064bb7debe9c..cca40819399c 100644 --- a/test/ztest/unit/math/advanced/pcan/testcase.yaml +++ b/test/ztest/unit/math/advanced/pcan/testcase.yaml @@ -7,7 +7,9 @@ tests: sof.unit.math.advanced.pcan: - tags: math advanced pcan mfcc microwakeword + tags: + - math + - pcan platform_allow: native_sim integration_platforms: - native_sim From a77e4178f9ec95d00f01cdec3388301915ad4169 Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Tue, 3 Nov 2026 14:41:48 +0200 Subject: [PATCH 08/10] math: pcan: use fixed-point strength and offset in configuration The PCAN strength and offset were passed as floats only to be converted back to Q1.15 and Q8.7 inside the gain lookup function. The MFCC component in turn converted its Q1.15 and Q8.7 blob values to float to call the library. Change the PCAN configuration and the LUT computation API to take the fixed-point values directly. This removes the last floating point use from the MFCC and PCAN code. The output is bit-exact with the previous version. Signed-off-by: Seppo Ingalsuo --- src/audio/mfcc/mfcc_setup.c | 7 +++--- src/include/sof/math/pcan.h | 22 +++++++++++++++---- src/math/pcan/pcan.c | 17 ++------------ .../ztest/unit/math/advanced/pcan/pcan_test.c | 16 +++++++------- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/audio/mfcc/mfcc_setup.c b/src/audio/mfcc/mfcc_setup.c index a8b14ffa5aa9..6d4e132e974c 100644 --- a/src/audio/mfcc/mfcc_setup.c +++ b/src/audio/mfcc/mfcc_setup.c @@ -481,10 +481,9 @@ int mfcc_setup(struct processing_module *mod, int max_frames, int sample_rate, i uint32_t *pcan_noise; int16_t *pcan_lut; - pcfg.strength = (config->pcan_strength > 0) ? - ((float)config->pcan_strength / 32768.0f) : 0.95f; - pcfg.offset = (config->pcan_offset > 0) ? - ((float)config->pcan_offset / 128.0f) : 80.0f; + /* Q1.15 and Q8.7, defaults 0.95 and 80.0 */ + pcfg.strength = (config->pcan_strength > 0) ? config->pcan_strength : 31130; + pcfg.offset = (config->pcan_offset > 0) ? config->pcan_offset : 10240; pcfg.gain_bits = (config->pcan_gain_bits > 0) ? config->pcan_gain_bits : 21; pcfg.smoothing_coef = (config->pcan_smoothing_coef > 0) ? diff --git a/src/include/sof/math/pcan.h b/src/include/sof/math/pcan.h index 463ae29eb5b3..a326502470cd 100644 --- a/src/include/sof/math/pcan.h +++ b/src/include/sof/math/pcan.h @@ -44,8 +44,8 @@ * \brief SOF PCAN configuration structure. */ struct pcan_config { - float strength; /**< Exponent alpha (e.g. 0.95) */ - float offset; /**< Additive offset delta (e.g. 80.0) */ + int32_t strength; /**< Exponent alpha in Q1.15 (e.g. 31130 for 0.95) */ + int32_t offset; /**< Additive offset delta in Q8.7 (e.g. 10240 for 80.0) */ int32_t gain_bits; /**< Gain bit shift (e.g. 21) */ uint16_t smoothing_coef; /**< IIR smoothing coefficient in Q14 (e.g. 819 for 0.05) */ uint16_t smoothing_bits; /**< Bit shift for noise estimate (e.g. 10) */ @@ -88,14 +88,28 @@ static inline uint32_t pcan_shrink(uint32_t x) /** * \brief Compute single point in continuous PCAN gain curve. + * + * \param strength Exponent alpha in Q1.15. + * \param offset Additive offset delta in Q8.7. + * \param gain_bits Number of fractional bits in the returned gain. + * \param input_bits Number of fractional bits in the input energy x. + * \param x Smoothed noise energy estimate. + * \return Gain value with gain_bits fractional bits. */ -int16_t pcan_gain_lookup_function(float strength, float offset, int32_t gain_bits, +int16_t pcan_gain_lookup_function(int32_t strength, int32_t offset, int32_t gain_bits, int32_t input_bits, uint32_t x); /** * \brief Fill 125-entry gain lookup table for WideDynamicFunction. + * + * \param strength Exponent alpha in Q1.15. + * \param offset Additive offset delta in Q8.7. + * \param gain_bits Number of fractional bits in the gain values. + * \param input_bits Number of fractional bits in the input energy. + * \param gain_lut Output table of PCAN_LUT_SIZE entries. + * \return 0 on success, -EINVAL if gain_lut is NULL. */ -int pcan_compute_lut(float strength, float offset, int32_t gain_bits, +int pcan_compute_lut(int32_t strength, int32_t offset, int32_t gain_bits, int32_t input_bits, int16_t *gain_lut); /** diff --git a/src/math/pcan/pcan.c b/src/math/pcan/pcan.c index a438b4e612bb..e212a83e51fe 100644 --- a/src/math/pcan/pcan.c +++ b/src/math/pcan/pcan.c @@ -14,22 +14,9 @@ /* ln(2) in Q5.27: round(ln(2) * 2^27) */ #define PCAN_LN2_Q27 93032640 -/* Convert PCAN config floats to fixed-point (init-time only). */ -static inline int32_t pcan_strength_q15(float strength) -{ - return (int32_t)(strength * 32768.0f + 0.5f); -} - -static inline int32_t pcan_offset_q7(float offset) -{ - return (int32_t)(offset * 128.0f + 0.5f); -} - -int16_t pcan_gain_lookup_function(float strength, float offset, int32_t gain_bits, +int16_t pcan_gain_lookup_function(int32_t strength_q15, int32_t offset_q7, int32_t gain_bits, int32_t input_bits, uint32_t x) { - const int32_t strength_q15 = pcan_strength_q15(strength); - const int32_t offset_q7 = pcan_offset_q7(offset); uint64_t u64; uint32_t u32; uint32_t ln_u_q27; @@ -100,7 +87,7 @@ int16_t pcan_gain_lookup_function(float strength, float offset, int32_t gain_bit return (int16_t)gain; } -int pcan_compute_lut(float strength, float offset, int32_t gain_bits, +int pcan_compute_lut(int32_t strength, int32_t offset, int32_t gain_bits, int32_t input_bits, int16_t *gain_lut) { int interval; diff --git a/test/ztest/unit/math/advanced/pcan/pcan_test.c b/test/ztest/unit/math/advanced/pcan/pcan_test.c index 0dfc6d189acc..eb7873e542aa 100644 --- a/test/ztest/unit/math/advanced/pcan/pcan_test.c +++ b/test/ztest/unit/math/advanced/pcan/pcan_test.c @@ -48,13 +48,13 @@ ZTEST(pcan_suite, test_pcan_lut_generation) int ret; int i; - ret = pcan_compute_lut(0.95f, 80.0f, 21, 10, lut); + ret = pcan_compute_lut(31130, 10240, 21, 10, lut); zassert_equal(ret, 0); for (i = 0; i < PCAN_TEST_LUT1_SIZE; i++) zassert_int_near(lut[i], ref_pcan_lut1[i], PCAN_TEST_LUT_TOL); - ret = pcan_compute_lut(0.8f, 50.0f, 18, 12, lut); + ret = pcan_compute_lut(26214, 6400, 18, 12, lut); zassert_equal(ret, 0); for (i = 0; i < PCAN_TEST_LUT2_SIZE; i++) @@ -68,7 +68,7 @@ ZTEST(pcan_suite, test_pcan_wide_dynamic_function) int ret; int i; - ret = pcan_compute_lut(0.95f, 80.0f, 21, 10, lut); + ret = pcan_compute_lut(31130, 10240, 21, 10, lut); zassert_equal(ret, 0); for (i = 0; i < PCAN_TEST_WDF_NUM_POINTS; i++) { @@ -97,8 +97,8 @@ ZTEST(pcan_suite, test_pcan_streaming) int f; int c; - cfg.strength = 0.95f; - cfg.offset = 80.0f; + cfg.strength = 31130; /* 0.95 in Q1.15 */ + cfg.offset = 10240; /* 80.0 in Q8.7 */ cfg.gain_bits = 21; cfg.smoothing_coef = PCAN_STREAM_SMOOTHING_COEF; cfg.smoothing_bits = PCAN_STREAM_SMOOTHING_BITS; @@ -140,7 +140,7 @@ ZTEST(pcan_suite, test_pcan_corner_cases) int ret; int i; - ret = pcan_compute_lut(0.95f, 80.0f, 21, 10, lut); + ret = pcan_compute_lut(31130, 10240, 21, 10, lut); zassert_equal(ret, 0); for (i = 0; i < PCAN_CORNERS_NUM_POINTS; i++) { @@ -152,8 +152,8 @@ ZTEST(pcan_suite, test_pcan_corner_cases) } /* gain_bits too small -> negative snr_shift */ - bad_cfg.strength = 0.95f; - bad_cfg.offset = 80.0f; + bad_cfg.strength = 31130; /* 0.95 in Q1.15 */ + bad_cfg.offset = 10240; /* 80.0 in Q8.7 */ bad_cfg.gain_bits = 5; bad_cfg.smoothing_coef = 819; bad_cfg.smoothing_bits = 10; From 9bbcd25f1726dc45d95e6b95ac2c6e486bf83e39 Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Mon, 2 Nov 2026 19:23:03 +0200 Subject: [PATCH 09/10] test: ztest: add MFCC AGC8 unit test Add native_sim unit tests for the MFCC soft AGC int8 output: default and blob parameter init, Q1.7 conversion and rounding, attack with saturation, the gain floor, the silence and speech release steps and the release stop at the target gain. Signed-off-by: Seppo Ingalsuo --- .../ztest/unit/audio/mfcc_agc8/CMakeLists.txt | 23 +++ test/ztest/unit/audio/mfcc_agc8/prj.conf | 2 + .../unit/audio/mfcc_agc8/test_mfcc_agc8.c | 135 ++++++++++++++++++ test/ztest/unit/audio/mfcc_agc8/testcase.yaml | 16 +++ 4 files changed, 176 insertions(+) create mode 100644 test/ztest/unit/audio/mfcc_agc8/CMakeLists.txt create mode 100644 test/ztest/unit/audio/mfcc_agc8/prj.conf create mode 100644 test/ztest/unit/audio/mfcc_agc8/test_mfcc_agc8.c create mode 100644 test/ztest/unit/audio/mfcc_agc8/testcase.yaml diff --git a/test/ztest/unit/audio/mfcc_agc8/CMakeLists.txt b/test/ztest/unit/audio/mfcc_agc8/CMakeLists.txt new file mode 100644 index 000000000000..8ef61b317830 --- /dev/null +++ b/test/ztest/unit/audio/mfcc_agc8/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_audio_mfcc_agc8) + +set(SOF_ROOT "${PROJECT_SOURCE_DIR}/../../../../..") + +target_include_directories(app PRIVATE + ${SOF_ROOT}/zephyr/include + ${SOF_ROOT}/src/include + ${SOF_ROOT}/src/platform/posix/include +) + +target_compile_definitions(app PRIVATE + -DCONFIG_ZEPHYR_POSIX=1 + -DCONFIG_LIBRARY=1 + -DUNIT_TEST=1 +) + +target_sources(app PRIVATE + test_mfcc_agc8.c + ${SOF_ROOT}/src/audio/mfcc/mfcc_agc8.c +) diff --git a/test/ztest/unit/audio/mfcc_agc8/prj.conf b/test/ztest/unit/audio/mfcc_agc8/prj.conf new file mode 100644 index 000000000000..d34c7781cd0a --- /dev/null +++ b/test/ztest/unit/audio/mfcc_agc8/prj.conf @@ -0,0 +1,2 @@ +CONFIG_ZTEST=y +CONFIG_SOF_FULL_ZEPHYR_APPLICATION=n diff --git a/test/ztest/unit/audio/mfcc_agc8/test_mfcc_agc8.c b/test/ztest/unit/audio/mfcc_agc8/test_mfcc_agc8.c new file mode 100644 index 000000000000..f4ca4e6e05af --- /dev/null +++ b/test/ztest/unit/audio/mfcc_agc8/test_mfcc_agc8.c @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// Author: Seppo Ingalsuo + +#include +#include +#include +#include + +#define NBINS 40 +#define ONE_Q23 (1 << 23) + +static struct mfcc_agc8 agc; +static int32_t mel[NBINS]; +static int8_t out[NBINS]; + +static void fill(int32_t value, int32_t peak) +{ + int i; + + for (i = 0; i < NBINS; i++) + mel[i] = value; + mel[NBINS / 2] = peak; +} + +static void agc_default_init(void) +{ + struct sof_mfcc_config config; + + memset(&config, 0, sizeof(config)); + mfcc_agc8_init(&agc, &config); +} + +ZTEST(mfcc_agc8_suite, test_init_defaults) +{ + agc_default_init(); + zassert_equal(agc.target, MFCC_AGC8_TARGET_Q23); + zassert_equal(agc.floor, MFCC_AGC8_FLOOR_Q23); + zassert_equal(agc.release, MFCC_AGC8_RELEASE_Q23); + zassert_equal(agc.release_speech, MFCC_AGC8_RELEASE_SPEECH_Q23); + zassert_equal(agc.gain, MFCC_AGC8_TARGET_Q23); +} + +ZTEST(mfcc_agc8_suite, test_init_blob) +{ + struct sof_mfcc_config config; + + memset(&config, 0, sizeof(config)); + config.agc_target = 64; /* 0.5 in Q8.7 */ + config.agc_floor = -128; /* -1.0 in Q8.7 */ + config.agc_release = 1000; + config.agc_release_speech = 100; + mfcc_agc8_init(&agc, &config); + zassert_equal(agc.target, ONE_Q23 / 2); + zassert_equal(agc.floor, -ONE_Q23); + zassert_equal(agc.release, 1000); + zassert_equal(agc.release_speech, 100); + zassert_equal(agc.gain, ONE_Q23 / 2); +} + +ZTEST(mfcc_agc8_suite, test_quantize_no_attack) +{ + /* Peak 0.5 + gain 0.25 stays below +1.0, no attack */ + agc_default_init(); + fill(0, ONE_Q23 / 2); + mfcc_agc8_process(&agc, mel, out, NBINS, false); + zassert_equal(agc.gain, MFCC_AGC8_TARGET_Q23); + zassert_equal(out[0], 32, "0.25 should be 32 in Q1.7, got %d", out[0]); + zassert_equal(out[NBINS / 2], 96, "0.75 should be 96 in Q1.7, got %d", out[NBINS / 2]); +} + +ZTEST(mfcc_agc8_suite, test_attack_and_saturation) +{ + /* Peak 3.0 limits gain to -2.0, peak maps to +1.0 and saturates to 127 */ + agc_default_init(); + fill(-2 * ONE_Q23, 3 * ONE_Q23); + mfcc_agc8_process(&agc, mel, out, NBINS, false); + zassert_equal(out[NBINS / 2], 127); + zassert_equal(out[0], -128, "-4.0 should clamp to -128, got %d", out[0]); + zassert_equal(agc.gain, -2 * ONE_Q23 + MFCC_AGC8_RELEASE_Q23); +} + +ZTEST(mfcc_agc8_suite, test_floor) +{ + /* Peak 5.0 would need gain -4.0, floor keeps it at -2.0 */ + agc_default_init(); + fill(0, 5 * ONE_Q23); + mfcc_agc8_process(&agc, mel, out, NBINS, true); + zassert_equal(out[NBINS / 2], 127); + zassert_equal(out[0], -128); + zassert_equal(agc.gain, MFCC_AGC8_FLOOR_Q23 + MFCC_AGC8_RELEASE_SPEECH_Q23); +} + +ZTEST(mfcc_agc8_suite, test_release_rates) +{ + int32_t gain; + + agc_default_init(); + fill(0, ONE_Q23); + mfcc_agc8_process(&agc, mel, out, NBINS, false); + gain = agc.gain; + zassert_equal(gain, MFCC_AGC8_RELEASE_Q23); + + /* Quiet hops: speech releases with the slow step, silence with the normal step */ + fill(-ONE_Q23, -ONE_Q23); + mfcc_agc8_process(&agc, mel, out, NBINS, true); + zassert_equal(agc.gain, gain + MFCC_AGC8_RELEASE_SPEECH_Q23); + gain = agc.gain; + mfcc_agc8_process(&agc, mel, out, NBINS, false); + zassert_equal(agc.gain, gain + MFCC_AGC8_RELEASE_Q23); +} + +ZTEST(mfcc_agc8_suite, test_release_stops_at_target) +{ + agc_default_init(); + agc.gain = MFCC_AGC8_TARGET_Q23 - 1; + fill(-ONE_Q23, -ONE_Q23); + mfcc_agc8_process(&agc, mel, out, NBINS, false); + zassert_equal(agc.gain, MFCC_AGC8_TARGET_Q23); +} + +ZTEST(mfcc_agc8_suite, test_rounding) +{ + /* Gain 0.25: -0.25 + 1/256 rounds to -31 in Q1.7, -0.25 - 1/256 to -32 */ + agc_default_init(); + fill(-ONE_Q23 / 2 + (ONE_Q23 >> 8), 0); + mel[1] = -ONE_Q23 / 2 - (ONE_Q23 >> 8); + mfcc_agc8_process(&agc, mel, out, NBINS, false); + zassert_equal(out[0], -31, "got %d", out[0]); + zassert_equal(out[1], -32, "got %d", out[1]); +} + +ZTEST_SUITE(mfcc_agc8_suite, NULL, NULL, NULL, NULL, NULL); diff --git a/test/ztest/unit/audio/mfcc_agc8/testcase.yaml b/test/ztest/unit/audio/mfcc_agc8/testcase.yaml new file mode 100644 index 000000000000..ba1ca3bbb02d --- /dev/null +++ b/test/ztest/unit/audio/mfcc_agc8/testcase.yaml @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# Copyright(c) 2026 Intel Corporation. All rights reserved. +# +# MFCC soft AGC int8 output (AGC8) unit tests for Ztest framework +# + +tests: + sof.unit.audio.mfcc_agc8: + tags: + - audio + - mfcc + platform_allow: native_sim + integration_platforms: + - native_sim + harness: ztest From 1d985a250ea66adf52c9f5656d9e8d81444ebc5a Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Mon, 9 Nov 2026 14:41:07 +0200 Subject: [PATCH 10/10] math: export functions used by MFCC for LLEXT builds The MFCC component calls FFT, DCT, matrix, auditory, window and PCAN functions from the math library, but none of them were exported. With CONFIG_COMP_MFCC=m the module could not resolve these symbols when it was loaded. Export them, along with the two TFLM microfrontend functions that the PCAN inline helpers call, from SOF code since the TFLM sources are not modified. Also add the missing mfcc_vad.c and mfcc_ipc4.c to the MFCC LLEXT build so the module contains the same code as the built-in version. Signed-off-by: Seppo Ingalsuo --- src/audio/mfcc/llext/CMakeLists.txt | 2 ++ src/math/auditory/auditory.c | 4 ++++ src/math/auditory/mel_filterbank_32.c | 3 +++ src/math/dct.c | 2 ++ src/math/fft/fft_32.c | 2 ++ src/math/fft/fft_32_hifi3.c | 2 ++ src/math/fft/fft_common.c | 3 +++ src/math/matrix.c | 3 +++ src/math/pcan/pcan.c | 7 +++++++ src/math/window.c | 6 ++++++ 10 files changed, 34 insertions(+) diff --git a/src/audio/mfcc/llext/CMakeLists.txt b/src/audio/mfcc/llext/CMakeLists.txt index 0032f469c1ef..5799e5cdb416 100644 --- a/src/audio/mfcc/llext/CMakeLists.txt +++ b/src/audio/mfcc/llext/CMakeLists.txt @@ -8,6 +8,8 @@ sof_llext_build("mfcc" ../mfcc_generic.c ../mfcc_hifi3.c ../mfcc_hifi4.c + ../mfcc_vad.c ../mfcc_agc8.c + ../mfcc_ipc4.c LIB openmodules ) diff --git a/src/math/auditory/auditory.c b/src/math/auditory/auditory.c index 0abd10de9308..8359bbdff760 100644 --- a/src/math/auditory/auditory.c +++ b/src/math/auditory/auditory.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,7 @@ int16_t psy_hz_to_mel(int16_t hz) mel = Q_MULTSR_32X32((int64_t)log, LOG_MULT_HZMEL_Q20, 26, 20, 2); return mel; } +EXPORT_SYMBOL(psy_hz_to_mel); /** * Convert Mel to Hz @@ -88,6 +90,7 @@ int16_t psy_mel_to_hz(int16_t mel) hz = Q_MULTSR_32X32((int64_t)exp, 700, 20, 0, 0); return hz; } +EXPORT_SYMBOL(psy_mel_to_hz); int mod_psy_get_mel_filterbank(struct processing_module *mod, struct psy_mel_filterbank *fb) { @@ -239,6 +242,7 @@ int mod_psy_get_mel_filterbank(struct processing_module *mod, struct psy_mel_fil fb->scratch_data2, sizeof(int16_t) * fb->data_length); return 0; } +EXPORT_SYMBOL(mod_psy_get_mel_filterbank); int mod_psy_free_mel_filterbank(struct processing_module *mod, struct psy_mel_filterbank *mel_fb) { diff --git a/src/math/auditory/mel_filterbank_32.c b/src/math/auditory/mel_filterbank_32.c index c496973f64a1..ed7b2fec5e58 100644 --- a/src/math/auditory/mel_filterbank_32.c +++ b/src/math/auditory/mel_filterbank_32.c @@ -10,6 +10,7 @@ #include #include #include +#include #include static inline uint32_t mel_sqrt32(uint32_t num) @@ -127,9 +128,11 @@ void psy_apply_mel_filterbank_with_linear_32(struct psy_mel_filterbank *fb, stru } } } +EXPORT_SYMBOL(psy_apply_mel_filterbank_with_linear_32); void psy_apply_mel_filterbank_32(struct psy_mel_filterbank *fb, struct icomplex32 *fft_out, int32_t *power_spectra, int32_t *mel_log, int bitshift) { psy_apply_mel_filterbank_with_linear_32(fb, fft_out, power_spectra, mel_log, NULL, bitshift); } +EXPORT_SYMBOL(psy_apply_mel_filterbank_32); diff --git a/src/math/dct.c b/src/math/dct.c index e4bbe5623dd8..0b5d5b96704f 100644 --- a/src/math/dct.c +++ b/src/math/dct.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -78,6 +79,7 @@ int mod_dct_initialize_16(struct processing_module *mod, struct dct_plan_16 *dct return 0; } +EXPORT_SYMBOL(mod_dct_initialize_16); int mod_dct_free_16(struct processing_module *mod, struct dct_plan_16 *dct) { diff --git a/src/math/fft/fft_32.c b/src/math/fft/fft_32.c index a6f8b2aa1ab3..fa68879317ba 100644 --- a/src/math/fft/fft_32.c +++ b/src/math/fft/fft_32.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #ifdef FFT_GENERIC @@ -93,5 +94,6 @@ void fft_execute_32(struct fft_plan *plan, bool ifft) } } } +EXPORT_SYMBOL(fft_execute_32); #endif /* FFT_GENERIC */ diff --git a/src/math/fft/fft_32_hifi3.c b/src/math/fft/fft_32_hifi3.c index 1d00dacd1b7f..a078187e1742 100644 --- a/src/math/fft/fft_32_hifi3.c +++ b/src/math/fft/fft_32_hifi3.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #ifdef FFT_HIFI3 @@ -156,4 +157,5 @@ void fft_execute_32(struct fft_plan *plan, bool ifft) } } } +EXPORT_SYMBOL(fft_execute_32); #endif diff --git a/src/math/fft/fft_common.c b/src/math/fft/fft_common.c index 589571881443..715bb9736815 100644 --- a/src/math/fft/fft_common.c +++ b/src/math/fft/fft_common.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "fft_common.h" @@ -103,6 +104,7 @@ struct fft_plan *mod_fft_plan_new(struct processing_module *mod, void *inb, fft_plan_init_bit_reverse(plan->bit_reverse_idx, plan->size, plan->len); return plan; } +EXPORT_SYMBOL(mod_fft_plan_new); void mod_fft_plan_free(struct processing_module *mod, struct fft_plan *plan) { @@ -112,3 +114,4 @@ void mod_fft_plan_free(struct processing_module *mod, struct fft_plan *plan) mod_free(mod, plan->bit_reverse_idx); mod_free(mod, plan); } +EXPORT_SYMBOL(mod_fft_plan_free); diff --git a/src/math/matrix.c b/src/math/matrix.c index e4c4c1ecbfff..f3d08a71f2d1 100644 --- a/src/math/matrix.c +++ b/src/math/matrix.c @@ -5,6 +5,7 @@ // Author: Seppo Ingalsuo #include +#include #include #include @@ -57,6 +58,7 @@ int mat_multiply(struct mat_matrix_16b *a, struct mat_matrix_16b *b, struct mat_ } return 0; } +EXPORT_SYMBOL(mat_multiply); int mat_multiply_elementwise(struct mat_matrix_16b *a, struct mat_matrix_16b *b, struct mat_matrix_16b *c) @@ -94,3 +96,4 @@ int mat_multiply_elementwise(struct mat_matrix_16b *a, struct mat_matrix_16b *b, return 0; } +EXPORT_SYMBOL(mat_multiply_elementwise); diff --git a/src/math/pcan/pcan.c b/src/math/pcan/pcan.c index e212a83e51fe..e7b1f7f4eb0f 100644 --- a/src/math/pcan/pcan.c +++ b/src/math/pcan/pcan.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -197,6 +198,7 @@ int pcan_populate_state(const struct pcan_config *config, struct pcan_state *sta return 0; } +EXPORT_SYMBOL(pcan_populate_state); void pcan_free_state(struct pcan_state *state) { @@ -232,3 +234,8 @@ void pcan_log_scale(struct pcan_state *state, uint32_t *signal) for (i = state->num_channels - 1; i >= 0; --i) signal[i] = scaled[i]; } +EXPORT_SYMBOL(pcan_log_scale); + +/* the TFLM microfrontend sources can't carry SOF symbol exports */ +EXPORT_SYMBOL(NoiseReductionApply); +EXPORT_SYMBOL(PcanGainControlApply); diff --git a/src/math/window.c b/src/math/window.c index 39739ef24be3..d43db99cc790 100644 --- a/src/math/window.c +++ b/src/math/window.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -41,6 +42,7 @@ void win_rectangular_16b(int16_t *win, int length) for (i = 0; i < length; i++) win[i] = WIN_ONE_Q15; } +EXPORT_SYMBOL(win_rectangular_16b); void win_rectangular_32b(int32_t *win, int length) { @@ -76,6 +78,7 @@ void win_blackman_16b(int16_t win[], int length, int16_t a0) win[n] = sat_int16(val); } } +EXPORT_SYMBOL(win_blackman_16b); void win_blackman_32b(int32_t win[], int length, int32_t a0) { @@ -118,6 +121,7 @@ void win_hann_16b(int16_t win[], int length) win[n] = sat_int16(val); } } +EXPORT_SYMBOL(win_hann_16b); void win_hann_32b(int32_t win[], int length) { @@ -152,6 +156,7 @@ void win_hamming_16b(int16_t win[], int length) win[n] = sat_int16(Q_SHIFT_RND(val, 30, 15)); } } +EXPORT_SYMBOL(win_hamming_16b); void win_hamming_32b(int32_t win[], int length) { @@ -195,3 +200,4 @@ void win_povey_16b(int16_t win[], int length) win[n] = sat_int16(Q_SHIFT_RND(x4, 20, 15)); } } +EXPORT_SYMBOL(win_povey_16b);