Skip to content

File syn_vad.c

File List > audio > syn_vad.c

Go to the documentation of this file

#if __has_include("syn_config.h")
#include "syn_config.h"
#endif

#if !defined(SYN_USE_VAD) || SYN_USE_VAD

#include "../port/syn_port_system.h"
#include "syn_vad.h"

#include <string.h>

/* ── Internal Constants ─────────────────────────────────────────────────── */

#define SYN_VAD_MIN_NOISE_FLOOR 50U        
#define SYN_VAD_DEFAULT_INITIAL_NOISE 200U 
/* ── Sensitivity Configuration Helper ───────────────────────────────────── */

static void vad_apply_sensitivity(SYN_VAD *vad, SYN_VAD_Sensitivity sensitivity)
{
    vad->cfg.sensitivity = sensitivity;
    switch (sensitivity) {
    case SYN_VAD_SENSITIVITY_SENSITIVE:
        vad->energy_threshold_multiplier = 2U; /* 2x above noise floor */
        vad->min_zcr_threshold = 2U;
        vad->max_zcr_threshold = 120U;
        break;
    case SYN_VAD_SENSITIVITY_AGGRESSIVE:
        vad->energy_threshold_multiplier = 8U; /* 8x above noise floor */
        vad->min_zcr_threshold = 5U;
        vad->max_zcr_threshold = 90U;
        break;
    case SYN_VAD_SENSITIVITY_NORMAL:
    default:
        vad->energy_threshold_multiplier = 4U; /* 4x above noise floor */
        vad->min_zcr_threshold = 3U;
        vad->max_zcr_threshold = 100U;
        break;
    }
}

/* ── Public API Implementation ───────────────────────────────────────────── */

SYN_Status syn_vad_init(SYN_VAD *vad, const SYN_VAD_Config *cfg)
{
    if (vad == NULL) {
        return SYN_INVALID_PARAM;
    }

    (void)memset(vad, 0, sizeof(*vad));

    if (cfg != NULL) {
        vad->cfg = *cfg;
    } else {
        vad->cfg.sample_rate_hz = 16000U;
        vad->cfg.frame_length = SYN_VAD_DEFAULT_FRAME_LEN;
        vad->cfg.attack_frames = SYN_VAD_DEFAULT_ATTACK_FRAMES;
        vad->cfg.hangover_frames = SYN_VAD_DEFAULT_HANGOVER_FRAMES;
        vad->cfg.sensitivity = SYN_VAD_SENSITIVITY_NORMAL;
        vad->cfg.initial_noise_floor = SYN_VAD_DEFAULT_INITIAL_NOISE;
    }

    if (vad->cfg.frame_length == 0U) {
        vad->cfg.frame_length = SYN_VAD_DEFAULT_FRAME_LEN;
    }
    if (vad->cfg.attack_frames == 0U) {
        vad->cfg.attack_frames = 1U;
    }
    if (vad->cfg.initial_noise_floor == 0U) {
        vad->cfg.initial_noise_floor = SYN_VAD_DEFAULT_INITIAL_NOISE;
    }

    vad->noise_floor = vad->cfg.initial_noise_floor;
    vad_apply_sensitivity(vad, vad->cfg.sensitivity);
    vad->state = SYN_VAD_STATE_SILENCE;
    return SYN_OK;
}

SYN_Status syn_vad_reset(SYN_VAD *vad)
{
    if (vad == NULL) {
        return SYN_INVALID_PARAM;
    }

    vad->state = SYN_VAD_STATE_SILENCE;
    vad->attack_counter = 0U;
    vad->hangover_counter = 0U;
    vad->noise_floor = vad->cfg.initial_noise_floor;
    return SYN_OK;
}

SYN_VAD_State syn_vad_process_frame(SYN_VAD *vad, const int16_t *samples, size_t num_samples,
                                    SYN_VAD_Features *out_features)
{
    if (vad == NULL || samples == NULL || num_samples == 0U) {
        return SYN_VAD_STATE_SILENCE;
    }

    uint64_t energy_acc = 0U;
    uint64_t hf_energy_acc = 0U;
    uint16_t zcr_count = 0U;
    int16_t prev_sample = samples[0];

    for (size_t i = 0U; i < num_samples; i++) {
        int16_t s = samples[i];
        int32_t s32 = (int32_t)s;
        energy_acc += (uint64_t)(s32 * s32);

        int32_t diff = (int32_t)s - (int32_t)prev_sample;
        hf_energy_acc += (uint64_t)(diff * diff);

        if ((s >= 0 && prev_sample < 0) || (s < 0 && prev_sample >= 0)) {
            zcr_count++;
        }
        prev_sample = s;
    }

    uint32_t frame_energy = (uint32_t)(energy_acc / num_samples);
    uint32_t hf_energy = (uint32_t)(hf_energy_acc / num_samples);

    /* Instantaneous Speech Trigger Evaluation */
    uint32_t threshold = vad->noise_floor * vad->energy_threshold_multiplier;
    bool instant_speech = false;

    if (frame_energy > threshold && zcr_count >= vad->min_zcr_threshold &&
        zcr_count <= vad->max_zcr_threshold) {
        instant_speech = true;
    }

    if (instant_speech) {
        if (vad->attack_counter < vad->cfg.attack_frames) {
            vad->attack_counter++;
        }
        if (vad->attack_counter >= vad->cfg.attack_frames) {
            vad->state = SYN_VAD_STATE_SPEECH;
            vad->hangover_counter = vad->cfg.hangover_frames;
        }
    } else {
        vad->attack_counter = 0U;

        /* Update background noise floor using Exponential Moving Average (alpha = 1/32) */
        vad->noise_floor = (vad->noise_floor * 31U + frame_energy) / 32U;
        if (vad->noise_floor < SYN_VAD_MIN_NOISE_FLOOR) {
            vad->noise_floor = SYN_VAD_MIN_NOISE_FLOOR;
        }

        if (vad->hangover_counter > 0U) {
            vad->hangover_counter--;
            vad->state = SYN_VAD_STATE_SPEECH;
        } else {
            vad->state = SYN_VAD_STATE_SILENCE;
        }
    }

    if (out_features != NULL) {
        out_features->energy = frame_energy;
        out_features->zcr = zcr_count;
        out_features->hf_energy = hf_energy;
        out_features->noise_floor = vad->noise_floor;
        out_features->is_speech_instant = instant_speech;
    }

    return vad->state;
}

SYN_VAD_State syn_vad_get_state(const SYN_VAD *vad)
{
    return (vad != NULL) ? vad->state : SYN_VAD_STATE_SILENCE;
}

SYN_Status syn_vad_set_sensitivity(SYN_VAD *vad, SYN_VAD_Sensitivity sensitivity)
{
    if (vad == NULL) {
        return SYN_INVALID_PARAM;
    }
    vad_apply_sensitivity(vad, sensitivity);
    return SYN_OK;
}

#endif /* !defined(SYN_USE_VAD) || SYN_USE_VAD */