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 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/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..3ad5f39fcdfd 100644 --- a/src/audio/mfcc/CMakeLists.txt +++ b/src/audio/mfcc/CMakeLists.txt @@ -1,10 +1,28 @@ # 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) 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/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..6c7f598110a9 100644 --- a/src/audio/mfcc/README.md +++ b/src/audio/mfcc/README.md @@ -1,25 +1,99 @@ -# 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. + +### 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 ```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 --> AGC8[Soft AGC int8] + VAD --> AGC8 + 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 +``` + +### 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 ``` -## Configuration and Scripts +### 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/llext/CMakeLists.txt b/src/audio/mfcc/llext/CMakeLists.txt index 688342751d75..5799e5cdb416 100644 --- a/src/audio/mfcc/llext/CMakeLists.txt +++ b/src/audio/mfcc/llext/CMakeLists.txt @@ -8,5 +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/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_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 3890671165fd..5af5a4b3179d 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,16 +543,52 @@ static void mfcc_prepare_output(struct mfcc_state *state, int num_ceps) if (num_ceps <= 0) return; - /* 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) { + switch (state->out_mode) { +#if CONFIG_COMP_MFCC_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. + * 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; + } + 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. + */ 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; @@ -519,9 +601,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 +624,15 @@ 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; - } + size_t sample_size = mfcc_out_sample_size(state); 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 +655,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 +699,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 +747,21 @@ static int mfcc_output_legacy(struct processing_module *mod, struct mfcc_comp_da } } - /* Write pending feature data (always int32) */ + /* Write pending feature data (int8 or int32 per output mode) */ if (state->out_remain > 0 && avail > 0) { - data_bytes = state->out_remain * sizeof(int32_t); - to_write = MIN(data_bytes, avail) & ~(size_t)3; + 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) + 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..6d4e132e974c 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 @@ -209,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; @@ -383,6 +396,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 +475,79 @@ 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; + + /* 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) ? + (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 + + if (config->enable_pcan) { + state->out_mode = MFCC_OUT_MEL8_PCAN; + } 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; +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 +614,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_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 885339004fc0..43b69f869aae 100644 --- a/src/include/sof/audio/mfcc/mfcc_comp.h +++ b/src/include/sof/audio/mfcc/mfcc_comp.h @@ -13,6 +13,10 @@ #include #include #include +#if CONFIG_COMP_MFCC_PCAN +#include +#endif +#include #include #include #include @@ -39,6 +43,14 @@ /** \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 */ + MFCC_OUT_MEL8_AGC, /**< Soft AGC Mel log spectra, int8 Q1.7 */ +}; + /** * \brief Data header prepended to every MFCC output frame. * @@ -99,6 +111,11 @@ 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_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 */ @@ -117,13 +134,14 @@ 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 */ 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 */ @@ -151,6 +169,13 @@ 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 || 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) { if (ptr >= buffer->end_addr) 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/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..a326502470cd --- /dev/null +++ b/src/include/sof/math/pcan.h @@ -0,0 +1,156 @@ +/* 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 */ + +/* 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. + */ +struct pcan_config { + 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) */ + 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. + * + * \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(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(int32_t strength, int32_t 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/include/user/mfcc.h b/src/include/user/mfcc.h index 286ee4f5e985..80604cfd2729 100644 --- a/src/include/user/mfcc.h +++ b/src/include/user/mfcc.h @@ -56,7 +56,12 @@ 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 */ + 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*/ @@ -79,7 +84,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 +99,9 @@ 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 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__ */ 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/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 414ddf482f93..ed7b2fec5e58 100644 --- a/src/math/auditory/mel_filterbank_32.c +++ b/src/math/auditory/mel_filterbank_32.c @@ -9,10 +9,24 @@ #include #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 +81,58 @@ 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 > UINT32_MAX) + s_comp = UINT32_MAX; - /* 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 */ + } } } +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/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..e7b1f7f4eb0f --- /dev/null +++ b/src/math/pcan/pcan.c @@ -0,0 +1,241 @@ +// 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 +#include + +/* ln(2) in Q5.27: round(ln(2) * 2^27) */ +#define PCAN_LN2_Q27 93032640 + +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) +{ + 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(int32_t strength, int32_t 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; +} +EXPORT_SYMBOL(pcan_populate_state); + +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, + PCAN_INPUT_CORRECTION_BITS); + 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); 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 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/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 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..eb7873e542aa --- /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(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(26214, 6400, 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(31130, 10240, 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 = 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; + 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(31130, 10240, 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 = 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; + 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..cca40819399c --- /dev/null +++ b/test/ztest/unit/math/advanced/pcan/testcase.yaml @@ -0,0 +1,16 @@ +# 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 + - pcan + platform_allow: native_sim + integration_platforms: + - native_sim + build_only: false 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" +}