vibe code the initial impl

This commit is contained in:
2026-10-13 19:47:46 -05:00
parent 7da1c2119d
commit 7862a09858
39 changed files with 2867 additions and 157 deletions
+19 -3
View File
@@ -1,3 +1,19 @@
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES esp_driver_gpio esp_driver_i2s led_strip)
idf_component_register(
SRCS
main.c
audio_in.c
fingerprint.c
matcher.c
detector.c
calibration.c
config_store.c
processing.c
console.c
notifier.c
led.c
INCLUDE_DIRS "."
REQUIRES
esp_driver_i2s esp_driver_gpio esp_driver_rmt
esp_timer esp_system esp_event esp_netif esp_wifi
esp_http_client esp-tls mbedtls nvs_flash console
)
+110
View File
@@ -0,0 +1,110 @@
menu "Door Buzzer Detector"
menu "Hardware"
config BUZZ_I2S_BCLK_GPIO
int "I2S BCLK (mic SCK) GPIO"
default 3
range 0 48
config BUZZ_I2S_WS_GPIO
int "I2S WS (mic WS/LRCL) GPIO"
default 4
range 0 48
config BUZZ_I2S_DATA_GPIO
int "I2S data-in (mic SD) GPIO"
default 5
range 0 48
choice BUZZ_I2S_SLOT
prompt "I2S slot that carries the mic"
default BUZZ_I2S_SLOT_LEFT
help
INMP441: L/R pin tied to GND -> left slot, tied to VDD -> right slot.
config BUZZ_I2S_SLOT_LEFT
bool "Left"
config BUZZ_I2S_SLOT_RIGHT
bool "Right"
endchoice
config BUZZ_AUDIO_SHIFT
int "Right shift applied to the 32-bit I2S sample"
default 14
range 8 20
help
The INMP441 delivers 24-bit data left-justified in a 32-bit slot, so
a shift of 16 keeps the top 16 bits (unity gain). A smaller shift adds
digital gain (14 = 4x) which helps with this quiet mic; the result is
clamped to int16. Fingerprints are level-independent, but the RMS gate
is not.
config BUZZ_BOOT_BUTTON_GPIO
int "User button GPIO (BOOT)"
default 0
range 0 48
config BUZZ_RGB_LED_ENABLE
bool "Drive the on-board WS2812 status LED"
default y
config BUZZ_RGB_LED_GPIO
int "RGB LED GPIO"
default 48
range 0 48
depends on BUZZ_RGB_LED_ENABLE
endmenu
menu "Detector defaults (runtime-adjustable, persisted in NVS)"
config BUZZ_THRESHOLD_MILLI
int "Cosine-similarity threshold x1000"
default 800
range 100 999
config BUZZ_MIN_RMS
int "Loudness gate: minimum frame RMS (int16 units)"
default 200
range 0 32767
config BUZZ_WIN_N
int "Sliding window length N (frames, 32 ms each)"
default 15
range 1 32
config BUZZ_WIN_M
int "Matches required M within the window"
default 10
range 1 32
config BUZZ_COOLDOWN_MS
int "Cooldown after an alert (milliseconds)"
default 5
range 0 86400000
config BUZZ_NOISE_MULT_X10
int "Learn/adapt gate: noise floor multiplier x10"
default 40
range 10 1000
config BUZZ_ADAPT_WEIGHT_PCT
int "Adapt: weight (percent) of the newly learned template"
default 50
range 0 100
config BUZZ_MONITOR_MS
int "Minimum interval between 'monitor' lines (ms)"
default 100
range 10 60000
endmenu
menu "Wi-Fi"
config BUZZ_WIFI_SSID
string "Wi-Fi SSID"
default ""
config BUZZ_WIFI_PASSWORD
string "Wi-Fi password"
default ""
endmenu
menu "ntfy notifications"
config BUZZ_NTFY_SERVER
string "ntfy server URL"
default "https://ntfy.sh"
config BUZZ_NTFY_TOPIC
string "Alert topic (anyone who knows it can read it: make it unguessable)"
default "change-me-door-buzzer"
config BUZZ_NTFY_HB_TOPIC
string "Heartbeat topic"
default "change-me-door-buzzer-heartbeat"
config BUZZ_HEARTBEAT_HOURS
int "Heartbeat interval in hours (0 = disabled)"
default 0
range 0 720
endmenu
endmenu
+175
View File
@@ -0,0 +1,175 @@
// I2S capture: continuous 16 kHz mono read, builds 50%-overlapping frames
// (previous hop + current hop) and hands them to the processing task through a
// fixed pool of frame slots. No allocation after init.
#include <string.h>
#include "driver/i2s_std.h"
#include "esp_log.h"
#include "esp_task_wdt.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "audio_in.h"
#include "board_config.h"
static const char *TAG = "audio";
#define NUM_SLOTS 8
#define SETTLE_HOPS 8 // discard the first hops: mic start-up transient
#define AUDIO_TASK_CORE 1
#define AUDIO_TASK_PRIO 10
#define AUDIO_TASK_STACK 4096
static int16_t s_slots[NUM_SLOTS][FRAME_LEN];
static QueueHandle_t s_free_q;
static QueueHandle_t s_ready_q;
static i2s_chan_handle_t s_rx;
static int32_t s_raw[HOP_LEN];
static int16_t s_hop_a[HOP_LEN], s_hop_b[HOP_LEN];
static volatile uint32_t s_captured, s_dropped;
static uint32_t s_seq;
esp_err_t audio_in_init(void)
{
s_free_q = xQueueCreate(NUM_SLOTS, sizeof(uint8_t));
s_ready_q = xQueueCreate(NUM_SLOTS, sizeof(uint8_t));
if (!s_free_q || !s_ready_q) {
return ESP_ERR_NO_MEM;
}
for (uint8_t i = 0; i < NUM_SLOTS; i++) {
xQueueSend(s_free_q, &i, 0);
}
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
chan_cfg.dma_desc_num = 6;
chan_cfg.dma_frame_num = 256; // 6 x 256 x 4 B -> ~96 ms of DMA buffering
esp_err_t err = i2s_new_channel(&chan_cfg, NULL, &s_rx);
if (err != ESP_OK) {
ESP_LOGE(TAG, "i2s_new_channel: %s", esp_err_to_name(err));
return err;
}
// INMP441: Philips format, 24-bit sample left-justified in a 32-bit slot.
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(FS_HZ),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = I2S_BCLK_GPIO,
.ws = I2S_WS_GPIO,
.dout = I2S_GPIO_UNUSED,
.din = I2S_DATA_GPIO,
.invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false },
},
};
#if CONFIG_BUZZ_I2S_SLOT_RIGHT
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_RIGHT;
#else
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT;
#endif
err = i2s_channel_init_std_mode(s_rx, &std_cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "i2s_channel_init_std_mode: %s", esp_err_to_name(err));
return err;
}
err = i2s_channel_enable(s_rx);
if (err != ESP_OK) {
ESP_LOGE(TAG, "i2s_channel_enable: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "I2S RX up: %d Hz, BCLK=%d WS=%d DIN=%d, shift=%d",
FS_HZ, CONFIG_BUZZ_I2S_BCLK_GPIO, CONFIG_BUZZ_I2S_WS_GPIO,
CONFIG_BUZZ_I2S_DATA_GPIO, CONFIG_BUZZ_AUDIO_SHIFT);
return ESP_OK;
}
// 32-bit slot -> int16. The 24-bit sample sits in bits [31:8]; shifting right by 16
// would keep its top 16 bits (unity gain). CONFIG_BUZZ_AUDIO_SHIFT < 16 adds gain.
// Arithmetic shift keeps the sign; clamp protects against overflow with gain.
static inline int16_t to_i16(int32_t raw)
{
int32_t v = raw >> CONFIG_BUZZ_AUDIO_SHIFT;
if (v > INT16_MAX) v = INT16_MAX;
if (v < INT16_MIN) v = INT16_MIN;
return (int16_t)v;
}
static void audio_task(void *arg)
{
ESP_ERROR_CHECK(esp_task_wdt_add(NULL));
int16_t *prev = s_hop_a, *cur = s_hop_b;
uint32_t settle = SETTLE_HOPS;
bool have_prev = false;
uint32_t last_warn_ms = 0;
for (;;) {
esp_task_wdt_reset(); // if I2S stops delivering data the WDT reboots us
size_t need = sizeof(s_raw), total = 0;
while (total < need) {
size_t got = 0;
esp_err_t err = i2s_channel_read(s_rx, (uint8_t *)s_raw + total, need - total, &got, 200);
total += got;
if (err == ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "I2S read timeout (mic wired?)");
} else if (err != ESP_OK) {
ESP_LOGE(TAG, "i2s_channel_read: %s", esp_err_to_name(err));
vTaskDelay(pdMS_TO_TICKS(10));
}
}
for (int i = 0; i < HOP_LEN; i++) {
cur[i] = to_i16(s_raw[i]);
}
if (settle > 0) {
settle--;
} else if (have_prev) {
uint8_t idx;
if (xQueueReceive(s_free_q, &idx, 0) == pdTRUE) {
memcpy(&s_slots[idx][0], prev, HOP_LEN * sizeof(int16_t));
memcpy(&s_slots[idx][HOP_LEN], cur, HOP_LEN * sizeof(int16_t));
if (xQueueSend(s_ready_q, &idx, 0) != pdTRUE) {
xQueueSend(s_free_q, &idx, 0);
s_dropped++;
} else {
s_captured++;
}
} else {
s_dropped++;
uint32_t now = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
if (now - last_warn_ms > 2000) {
last_warn_ms = now;
ESP_LOGW(TAG, "processing behind: dropped %lu frames so far", (unsigned long)s_dropped);
}
}
}
have_prev = true;
int16_t *t = prev; prev = cur; cur = t;
}
}
esp_err_t audio_in_start(void)
{
BaseType_t ok = xTaskCreatePinnedToCore(audio_task, "audio", AUDIO_TASK_STACK, NULL,
AUDIO_TASK_PRIO, NULL, AUDIO_TASK_CORE);
return ok == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
}
bool audio_in_get_frame(audio_frame_t *f, TickType_t wait)
{
uint8_t idx;
if (xQueueReceive(s_ready_q, &idx, wait) != pdTRUE) {
return false;
}
f->idx = idx;
f->pcm = s_slots[idx];
f->seq = s_seq++;
return true;
}
void audio_in_release_frame(uint8_t idx)
{
xQueueSend(s_free_q, &idx, 0);
}
uint32_t audio_in_frames_captured(void) { return s_captured; }
uint32_t audio_in_frames_dropped(void) { return s_dropped; }
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "feature_params.h"
typedef struct {
const int16_t *pcm; // FRAME_LEN samples, valid until audio_in_release_frame()
uint8_t idx; // slot index to hand back
uint32_t seq; // monotonically increasing frame number
} audio_frame_t;
esp_err_t audio_in_init(void); // creates the I2S RX channel and the frame pool
esp_err_t audio_in_start(void); // starts the capture task (pinned to core 1)
bool audio_in_get_frame(audio_frame_t *f, TickType_t wait);
void audio_in_release_frame(uint8_t idx);
uint32_t audio_in_frames_captured(void);
uint32_t audio_in_frames_dropped(void); // processing fell behind
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "driver/gpio.h"
#include "sdkconfig.h"
// Pin map (defaults 3/4/5/48 set in Kconfig, see "Door Buzzer Detector > Hardware").
#define I2S_BCLK_GPIO ((gpio_num_t)CONFIG_BUZZ_I2S_BCLK_GPIO)
#define I2S_WS_GPIO ((gpio_num_t)CONFIG_BUZZ_I2S_WS_GPIO)
#define I2S_DATA_GPIO ((gpio_num_t)CONFIG_BUZZ_I2S_DATA_GPIO)
#define RGB_LED_GPIO ((gpio_num_t)CONFIG_BUZZ_RGB_LED_GPIO)
#define BOOT_BTN_GPIO ((gpio_num_t)CONFIG_BUZZ_BOOT_BUTTON_GPIO)
+354
View File
@@ -0,0 +1,354 @@
// learn / adapt / selfcheck / test / background / autothresh.
// A session is started by a console task, fed by the processing task via
// calibration_on_frame(), and finished (mode -> NORMAL) by the processing task, which
// then signals the waiting console task. Session data is guarded by s_mx.
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "calibration.h"
#include "config_store.h"
#include "fingerprint.h"
#include "matcher.h"
#define LEARN_SECS 60
#define TEST_SECS 10
#define BG_SECS 60
#define SECS_TO_FRAMES(s) ((uint32_t)(s) * FS_HZ / HOP_LEN)
#define NOISE_FRAMES (FS_HZ / HOP_LEN) // first second -> noise floor
#define MIN_LEARN_FRAMES 20
#define MAX_SCORES 2048
_Static_assert(BG_SECS * FS_HZ / HOP_LEN <= MAX_SCORES, "score buffer too small for background run");
static SemaphoreHandle_t s_mx;
static SemaphoreHandle_t s_done;
static volatile app_mode_t s_mode = MODE_NORMAL;
static struct {
uint32_t frame_idx, total_frames;
float min_rms, threshold, noise_mult;
// learn / adapt
float noise[NOISE_FRAMES];
float gate;
float fp_sum[NUM_BANDS];
uint32_t count;
// test / background
float scores[MAX_SCORES];
uint32_t n_scores;
uint32_t n_gated, n_above, n_above_gated;
} S;
static score_stats_t s_bg, s_buzz;
static int cmp_f(const void *a, const void *b)
{
float x = *(const float *)a, y = *(const float *)b;
return (x > y) - (x < y);
}
static float pct(const float *sorted, uint32_t n, float p)
{
// Linear interpolation, same as numpy.percentile default.
float pos = p / 100.0f * (float)(n - 1);
uint32_t lo = (uint32_t)pos;
uint32_t hi = lo + 1 < n ? lo + 1 : lo;
float fr = pos - (float)lo;
return sorted[lo] * (1.0f - fr) + sorted[hi] * fr;
}
static void compute_stats(float *a, uint32_t n, score_stats_t *o)
{
memset(o, 0, sizeof(*o));
if (n == 0) return;
qsort(a, n, sizeof(float), cmp_f);
double sum = 0;
for (uint32_t i = 0; i < n; i++) sum += a[i];
o->valid = true;
o->n = n;
o->min = a[0];
o->max = a[n - 1];
o->mean = (float)(sum / n);
o->p5 = pct(a, n, 5);
o->p50 = pct(a, n, 50);
o->p95 = pct(a, n, 95);
o->p99 = pct(a, n, 99);
}
static void print_stats(const char *label, const score_stats_t *s)
{
printf("%s (n=%lu): min=%.3f p5=%.3f p50=%.3f mean=%.3f p95=%.3f p99=%.3f max=%.3f\n",
label, (unsigned long)s->n, s->min, s->p5, s->p50, s->mean, s->p95, s->p99, s->max);
}
esp_err_t calibration_init(void)
{
s_mx = xSemaphoreCreateMutex();
s_done = xSemaphoreCreateBinary();
return (s_mx && s_done) ? ESP_OK : ESP_ERR_NO_MEM;
}
app_mode_t calibration_get_mode(void) { return s_mode; }
const char *calibration_mode_name(app_mode_t m)
{
switch (m) {
case MODE_NORMAL: return "NORMAL";
case MODE_LEARNING: return "LEARNING";
case MODE_ADAPTING: return "ADAPTING";
case MODE_BACKGROUND_CAL: return "BACKGROUND_CAL";
case MODE_TEST_CAL: return "TEST_CAL";
}
return "?";
}
bool calibration_get_background(score_stats_t *o) { *o = s_bg; return s_bg.valid; }
bool calibration_get_buzz(score_stats_t *o) { *o = s_buzz; return s_buzz.valid; }
// ---- frame feed (processing task) -------------------------------------------
void calibration_on_frame(const float fp[NUM_BANDS], bool fp_valid, float rms, float score)
{
xSemaphoreTake(s_mx, portMAX_DELAY);
app_mode_t m = s_mode;
if (m == MODE_NORMAL) {
xSemaphoreGive(s_mx);
return;
}
uint32_t i = S.frame_idx++;
switch (m) {
case MODE_LEARNING:
case MODE_ADAPTING:
if (i < NOISE_FRAMES) {
S.noise[i] = rms;
if (i == NOISE_FRAMES - 1) {
float tmp[NOISE_FRAMES];
memcpy(tmp, S.noise, sizeof(tmp));
qsort(tmp, NOISE_FRAMES, sizeof(float), cmp_f);
float floor_rms = tmp[NOISE_FRAMES / 2]; // median
float g = floor_rms * S.noise_mult;
S.gate = g > S.min_rms ? g : S.min_rms; // never below the detector gate
}
} else if (fp_valid && rms >= S.gate) {
for (int b = 0; b < NUM_BANDS; b++) S.fp_sum[b] += fp[b];
S.count++;
}
break;
case MODE_TEST_CAL:
if (fp_valid && rms >= S.min_rms && S.n_scores < MAX_SCORES) {
S.scores[S.n_scores++] = score;
}
break;
case MODE_BACKGROUND_CAL:
if (S.n_scores < MAX_SCORES) S.scores[S.n_scores++] = fp_valid ? score : 0.0f;
if (rms >= S.min_rms) S.n_gated++;
if (score >= S.threshold) {
S.n_above++;
if (rms >= S.min_rms) S.n_above_gated++;
}
break;
default:
break;
}
if (S.frame_idx >= S.total_frames) {
s_mode = MODE_NORMAL;
xSemaphoreGive(s_done);
}
xSemaphoreGive(s_mx);
}
// ---- session control (console / button tasks) --------------------------------
static esp_err_t begin(app_mode_t m, uint32_t secs)
{
app_config_t cfg;
config_get(&cfg);
xSemaphoreTake(s_mx, portMAX_DELAY);
if (s_mode != MODE_NORMAL) {
xSemaphoreGive(s_mx);
printf("busy: a %s session is already running\n", calibration_mode_name(s_mode));
return ESP_ERR_INVALID_STATE;
}
memset(&S, 0, sizeof(S));
S.total_frames = SECS_TO_FRAMES(secs);
S.min_rms = cfg.min_rms;
S.threshold = cfg.threshold;
S.noise_mult = cfg.noise_mult;
xSemaphoreTake(s_done, 0);
s_mode = m; // last: processing task starts feeding after this
xSemaphoreGive(s_mx);
return ESP_OK;
}
static esp_err_t wait_done(uint32_t secs)
{
for (uint32_t t = 1; t <= secs + 10; t++) {
if (xSemaphoreTake(s_done, pdMS_TO_TICKS(1000)) == pdTRUE) {
printf("\n");
return ESP_OK;
}
printf(" %lus", (unsigned long)t);
fflush(stdout);
}
xSemaphoreTake(s_mx, portMAX_DELAY);
s_mode = MODE_NORMAL;
xSemaphoreGive(s_mx);
printf("\nsession timed out: no audio frames arriving?\n");
return ESP_ERR_TIMEOUT;
}
esp_err_t calibration_run_learn(bool adapt, float w_new)
{
if (adapt && !matcher_has_template()) {
printf("adapt needs an existing template; use `learn` first\n");
return ESP_ERR_INVALID_STATE;
}
if (adapt && !(w_new >= 0.0f && w_new <= 1.0f)) {
printf("adapt weight must be 0..1\n");
return ESP_ERR_INVALID_ARG;
}
esp_err_t err = begin(adapt ? MODE_ADAPTING : MODE_LEARNING, LEARN_SECS);
if (err != ESP_OK) return err;
printf("%s: %d s. Stay QUIET for the first second, then press the buzzer a few times.\n",
adapt ? "ADAPT" : "LEARN", LEARN_SECS);
err = wait_done(LEARN_SECS);
if (err != ESP_OK) return err;
xSemaphoreTake(s_mx, portMAX_DELAY);
uint32_t count = S.count;
float gate = S.gate;
float v[NUM_BANDS];
for (int b = 0; b < NUM_BANDS; b++) v[b] = count ? S.fp_sum[b] / (float)count : 0.0f;
xSemaphoreGive(s_mx);
printf("noise-gate RMS %.1f, loud frames collected: %lu\n", gate, (unsigned long)count);
if (count < MIN_LEARN_FRAMES) {
printf("REFUSED: need >= %d loud frames. Press the buzzer longer / closer, or lower noise_mult/min_rms.\n",
MIN_LEARN_FRAMES);
return ESP_FAIL;
}
// |mean of unit vectors| = average cosine of the frames to their own mean direction.
float s2 = 0;
for (int b = 0; b < NUM_BANDS; b++) s2 += v[b] * v[b];
printf("cohesion of collected frames: %.3f (close to 1 = consistent sound)\n", sqrtf(s2));
if (!features_l2_normalize(v, NUM_BANDS)) {
printf("REFUSED: collected fingerprint is degenerate (flat)\n");
return ESP_FAIL;
}
if (adapt) {
float cur[NUM_BANDS];
matcher_get_template(cur);
printf("similarity new vs current template: %.3f\n", features_dot(v, cur));
for (int b = 0; b < NUM_BANDS; b++) v[b] = w_new * v[b] + (1.0f - w_new) * cur[b];
if (!features_l2_normalize(v, NUM_BANDS)) {
printf("REFUSED: blended template degenerate\n");
return ESP_FAIL;
}
}
err = matcher_set_template(v, count);
if (err != ESP_OK) {
printf("saving template failed: %s\n", esp_err_to_name(err));
return err;
}
printf("%s done: template saved to NVS (source now: %s)%s\n", adapt ? "ADAPT" : "LEARN",
matcher_source_name(), adapt ? "" : ". Run `selfcheck` to verify.");
return ESP_OK;
}
esp_err_t calibration_run_test(bool verdict)
{
if (!matcher_has_template()) {
printf("no template: run make_template.py or `learn` first\n");
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = begin(MODE_TEST_CAL, TEST_SECS);
if (err != ESP_OK) return err;
printf("%s: %d s. Press the real buzzer now (several presses).\n", verdict ? "SELFCHECK" : "TEST", TEST_SECS);
err = wait_done(TEST_SECS);
if (err != ESP_OK) return err;
xSemaphoreTake(s_mx, portMAX_DELAY);
uint32_t n = S.n_scores;
float thr = S.threshold, gate = S.min_rms;
score_stats_t st;
compute_stats(S.scores, n, &st);
xSemaphoreGive(s_mx);
printf("loud frames (RMS >= %.0f): %lu\n", gate, (unsigned long)n);
if (n < MIN_LEARN_FRAMES) {
printf("WARNING: only %lu loud frames; too few for reliable stats (press longer or lower min_rms).\n",
(unsigned long)n);
if (n == 0) return ESP_FAIL;
} else {
s_buzz = st; // used by autothresh
}
print_stats("score vs active template", &st);
if (verdict && n >= MIN_LEARN_FRAMES) {
if (st.p5 >= thr) {
printf("VERDICT: GOOD match. 95%% of loud frames clear threshold %.2f.\n", thr);
} else if (st.p50 >= thr) {
printf("VERDICT: WEAK match. The median frame clears %.2f but the tail does not.\n"
" Try `adapt` (blend with live mic) or `learn`, then `selfcheck` again.\n", thr);
} else {
printf("VERDICT: POOR match (median %.2f < threshold %.2f).\n"
" Was that really the buzzer? If yes: `learn` on this mic, or see README about FP_DETREND\n"
" (compile-time, feature_params.h) if mic tilt is the issue.\n", st.p50, thr);
}
}
return ESP_OK;
}
esp_err_t calibration_run_background(void)
{
if (!matcher_has_template()) {
printf("no template: run make_template.py or `learn` first\n");
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = begin(MODE_BACKGROUND_CAL, BG_SECS);
if (err != ESP_OK) return err;
printf("BACKGROUND: %d s of normal ambient sound (do NOT press the buzzer).\n", BG_SECS);
err = wait_done(BG_SECS);
if (err != ESP_OK) return err;
xSemaphoreTake(s_mx, portMAX_DELAY);
uint32_t n = S.n_scores, gated = S.n_gated, above = S.n_above, above_g = S.n_above_gated;
float thr = S.threshold;
compute_stats(S.scores, n, &s_bg);
xSemaphoreGive(s_mx);
print_stats("background score vs active template", &s_bg);
printf("frames passing the loudness gate: %lu / %lu\n", (unsigned long)gated, (unsigned long)n);
printf("frames with score >= threshold %.2f: %lu (of which also loud enough to match: %lu)\n",
thr, (unsigned long)above, (unsigned long)above_g);
return ESP_OK;
}
esp_err_t calibration_autothresh(void)
{
if (!s_bg.valid || !s_buzz.valid) {
printf("need both: `background` and (`test` or `selfcheck`) with >= %d loud frames first\n", MIN_LEARN_FRAMES);
return ESP_ERR_INVALID_STATE;
}
float lo = s_bg.p99, hi = s_buzz.p5;
float thr = 0.5f * (lo + hi);
if (thr < 0.50f) thr = 0.50f;
if (thr > 0.98f) thr = 0.98f;
printf("background p99 = %.3f, buzzer p5 = %.3f -> threshold %.3f\n", lo, hi, thr);
if (hi <= lo) {
printf("WARNING: distributions OVERLAP. Expect false alarms or missed buzzes. "
"Improve the template (`learn`), move the mic, or revisit FP_DETREND.\n");
}
char msg[96], val[24];
snprintf(val, sizeof(val), "%.4f", thr);
if (config_set_param("threshold", val, msg, sizeof(msg)) != ESP_OK) {
printf("could not set threshold: %s\n", msg);
return ESP_FAIL;
}
printf("threshold set and saved.\n");
return ESP_OK;
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "feature_params.h"
typedef enum {
MODE_NORMAL = 0,
MODE_LEARNING,
MODE_ADAPTING,
MODE_BACKGROUND_CAL,
MODE_TEST_CAL, // also used by `selfcheck`
} app_mode_t;
typedef struct {
bool valid;
uint32_t n;
float min, p5, p50, mean, p95, p99, max;
} score_stats_t;
esp_err_t calibration_init(void);
app_mode_t calibration_get_mode(void); // lock-free read of an aligned word
const char *calibration_mode_name(app_mode_t m);
// Called by the processing task for every frame while mode != NORMAL.
void calibration_on_frame(const float fp[NUM_BANDS], bool fp_valid, float rms, float score);
// Blocking sessions, called from console / button tasks. The detector is paused
// (no alerts) while a session runs. All return ESP_OK on success.
esp_err_t calibration_run_learn(bool adapt, float weight_new); // 60 s
esp_err_t calibration_run_test(bool selfcheck_verdict); // 10 s
esp_err_t calibration_run_background(void); // 60 s
esp_err_t calibration_autothresh(void);
bool calibration_get_background(score_stats_t *out);
bool calibration_get_buzz(score_stats_t *out);
+190
View File
@@ -0,0 +1,190 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "config_store.h"
static const char *TAG = "config";
#define NVS_NS "buzz"
#define CFG_KEY "cfg"
#define CFG_MAGIC 0xB0220002u
#define CFG_MAGIC_PRE_COOLDOWN_MS 0xB0220001u
typedef struct { uint32_t magic; app_config_t cfg; } cfg_blob_t;
typedef enum { PT_FLOAT, PT_UINT } ptype_t;
typedef struct {
const char *name;
ptype_t type;
size_t off;
double min, max;
const char *help;
} param_t;
#define P(n, t, mn, mx, h) { #n, t, offsetof(app_config_t, n), mn, mx, h }
static const param_t s_params[] = {
P(threshold, PT_FLOAT, 0.10, 0.999, "cosine threshold for a matching frame"),
P(min_rms, PT_FLOAT, 0.0, 32767.0, "loudness gate (int16 RMS)"),
P(win_n, PT_UINT, 1, 32, "sliding window length N (frames)"),
P(win_m, PT_UINT, 1, 32, "matches M needed in window (<= N)"),
P(cooldown_ms, PT_UINT, 0, 86400000, "cooldown after alert (ms)"),
P(noise_mult, PT_FLOAT, 1.0, 100.0, "learn/adapt gate = noise floor x this"),
P(adapt_weight, PT_FLOAT, 0.0, 1.0, "weight of new template in adapt"),
P(monitor_ms, PT_UINT, 10, 60000, "min interval between monitor lines (ms)"),
P(heartbeat_h, PT_UINT, 0, 720, "heartbeat period (h), 0 = off"),
};
#define NPARAMS (sizeof(s_params) / sizeof(s_params[0]))
static app_config_t s_cfg;
static SemaphoreHandle_t s_mx;
static void load_defaults(app_config_t *c)
{
c->threshold = CONFIG_BUZZ_THRESHOLD_MILLI / 1000.0f;
c->min_rms = (float)CONFIG_BUZZ_MIN_RMS;
c->win_n = CONFIG_BUZZ_WIN_N;
c->win_m = CONFIG_BUZZ_WIN_M;
c->cooldown_ms = CONFIG_BUZZ_COOLDOWN_MS;
c->noise_mult = CONFIG_BUZZ_NOISE_MULT_X10 / 10.0f;
c->adapt_weight = CONFIG_BUZZ_ADAPT_WEIGHT_PCT / 100.0f;
c->monitor_ms = CONFIG_BUZZ_MONITOR_MS;
c->heartbeat_h = CONFIG_BUZZ_HEARTBEAT_HOURS;
if (c->win_m > c->win_n) c->win_m = c->win_n;
}
static esp_err_t save_locked(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_open: %s", esp_err_to_name(err));
return err;
}
cfg_blob_t blob = { .magic = CFG_MAGIC, .cfg = s_cfg };
err = nvs_set_blob(h, CFG_KEY, &blob, sizeof(blob));
if (err == ESP_OK) err = nvs_commit(h);
if (err != ESP_OK) ESP_LOGE(TAG, "save failed: %s", esp_err_to_name(err));
nvs_close(h);
return err;
}
esp_err_t config_init(void)
{
s_mx = xSemaphoreCreateMutex();
if (!s_mx) return ESP_ERR_NO_MEM;
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGW(TAG, "erasing NVS (%s)", esp_err_to_name(err));
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_flash_init: %s", esp_err_to_name(err));
return err;
}
load_defaults(&s_cfg);
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
cfg_blob_t blob;
size_t len = sizeof(blob);
if (nvs_get_blob(h, CFG_KEY, &blob, &len) == ESP_OK && len == sizeof(blob)) {
if (blob.magic == CFG_MAGIC) {
s_cfg = blob.cfg;
ESP_LOGI(TAG, "loaded saved config from NVS");
} else if (blob.magic == CFG_MAGIC_PRE_COOLDOWN_MS) {
s_cfg = blob.cfg;
s_cfg.cooldown_ms = CONFIG_BUZZ_COOLDOWN_MS;
ESP_LOGI(TAG, "migrating saved config to millisecond cooldown");
err = save_locked();
if (err != ESP_OK) {
nvs_close(h);
return err;
}
} else {
ESP_LOGI(TAG, "no compatible saved config, using Kconfig defaults");
}
} else {
ESP_LOGI(TAG, "no compatible saved config, using Kconfig defaults");
}
nvs_close(h);
}
return ESP_OK;
}
void config_get(app_config_t *out)
{
xSemaphoreTake(s_mx, portMAX_DELAY);
*out = s_cfg;
xSemaphoreGive(s_mx);
}
esp_err_t config_set_param(const char *name, const char *value, char *err, size_t err_len)
{
for (size_t i = 0; i < NPARAMS; i++) {
const param_t *p = &s_params[i];
if (strcmp(p->name, name) != 0) continue;
char *end = NULL;
errno = 0;
double v = strtod(value, &end);
if (end == value || *end != '\0' || errno != 0) {
snprintf(err, err_len, "'%s' is not a number", value);
return ESP_ERR_INVALID_ARG;
}
if (v < p->min || v > p->max) {
snprintf(err, err_len, "%s must be in [%g, %g]", p->name, p->min, p->max);
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_mx, portMAX_DELAY);
app_config_t next = s_cfg;
void *dst = (uint8_t *)&next + p->off;
if (p->type == PT_FLOAT) *(float *)dst = (float)v;
else *(uint32_t *)dst = (uint32_t)(v + 0.5);
if (next.win_m > next.win_n) {
xSemaphoreGive(s_mx);
snprintf(err, err_len, "win_m (%lu) must be <= win_n (%lu)",
(unsigned long)next.win_m, (unsigned long)next.win_n);
return ESP_ERR_INVALID_ARG;
}
s_cfg = next;
esp_err_t e = save_locked();
xSemaphoreGive(s_mx);
if (e != ESP_OK) snprintf(err, err_len, "applied but NVS save failed");
return e;
}
snprintf(err, err_len, "unknown parameter '%s' (see `get`)", name);
return ESP_ERR_NOT_FOUND;
}
esp_err_t config_reset_defaults(void)
{
xSemaphoreTake(s_mx, portMAX_DELAY);
load_defaults(&s_cfg);
esp_err_t e = save_locked();
xSemaphoreGive(s_mx);
return e;
}
int config_param_count(void) { return (int)NPARAMS; }
const char *config_param_name(int i) { return s_params[i].name; }
void config_param_format(int i, char *buf, size_t len)
{
const param_t *p = &s_params[i];
app_config_t c;
config_get(&c);
const void *src = (const uint8_t *)&c + p->off;
if (p->type == PT_FLOAT) {
snprintf(buf, len, "%-13s = %-9.4g (%g..%g) %s", p->name, *(const float *)src, p->min, p->max, p->help);
} else {
snprintf(buf, len, "%-13s = %-9lu (%g..%g) %s", p->name, (unsigned long)*(const uint32_t *)src, p->min, p->max, p->help);
}
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
// All runtime-tunable parameters in one struct. Defaults come from Kconfig, the
// saved copy (NVS) wins when present and layout-compatible.
typedef struct {
float threshold; // cosine similarity needed for a frame to "match"
float min_rms; // loudness gate (int16 RMS); quieter frames never match
uint32_t win_n; // sliding window length (frames), 1..32
uint32_t win_m; // matches needed inside the window, 1..win_n
uint32_t cooldown_ms; // quiet period after an alert
float noise_mult; // learn/adapt gate = noise floor * this
float adapt_weight; // weight of the NEW template in `adapt` (0..1)
uint32_t monitor_ms; // min interval between `monitor` lines
uint32_t heartbeat_h; // heartbeat period in hours, 0 = off
} app_config_t;
esp_err_t config_init(void); // nvs_flash_init + load
void config_get(app_config_t *out); // thread-safe copy
esp_err_t config_set_param(const char *name, const char *value, char *err, size_t err_len);
esp_err_t config_reset_defaults(void);
int config_param_count(void);
const char *config_param_name(int i);
void config_param_format(int i, char *buf, size_t len); // "0.800 (range 0.1..0.999) help"
+227
View File
@@ -0,0 +1,227 @@
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_console.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "sdkconfig.h"
#include "audio_in.h"
#include "calibration.h"
#include "config_store.h"
#include "console.h"
#include "fingerprint.h"
#include "matcher.h"
#include "notifier.h"
#include "processing.h"
#include "test_vector.h"
static int cmd_selftest(int argc, char **argv)
{
#if !TEST_VECTOR_VALID
printf("no test vector compiled in. Run tools/make_template.py and rebuild.\n");
return 0;
#else
static features_ctx_t ctx; // own scratch: never shares with the processing task
float fp[NUM_BANDS], rms = 0;
bool ok = features_compute(&ctx, TEST_VECTOR_PCM, fp, &rms);
float maxerr = 0;
for (int b = 0; b < NUM_BANDS; b++) {
float e = fabsf(fp[b] - TEST_VECTOR_FP[b]);
if (!(e <= maxerr)) maxerr = e; // NaN-safe
}
float rms_rel = fabsf(rms - TEST_VECTOR_RMS) / fmaxf(TEST_VECTOR_RMS, 1.0f);
printf("fingerprint max abs error: %.3e (tol 1e-3)\n", maxerr);
printf("rms: firmware %.3f, expected %.3f (rel err %.2e)\n", rms, TEST_VECTOR_RMS, rms_rel);
bool pass = ok && maxerr < 1e-3f && rms_rel < 1e-3f;
printf("selftest: %s\n", pass ? "PASS" : "FAIL");
if (!pass) printf("Parity broken: feature_params.h changed since make_template.py ran? Re-run it and rebuild.\n");
return 0;
#endif
}
static int cmd_selfcheck(int argc, char **argv) { calibration_run_test(true); return 0; }
static int cmd_test(int argc, char **argv) { calibration_run_test(false); return 0; }
static int cmd_background(int argc, char **argv) { calibration_run_background(); return 0; }
static int cmd_autothresh(int argc, char **argv) { calibration_autothresh(); return 0; }
static int cmd_learn(int argc, char **argv)
{
if (argc >= 2 && strcmp(argv[1], "reset") == 0) {
esp_err_t e = matcher_reset();
printf("learn reset: %s. Active template: %s\n", e == ESP_OK ? "NVS template cleared" : esp_err_to_name(e),
matcher_source_name());
return 0;
}
calibration_run_learn(false, 0.0f);
return 0;
}
static int cmd_adapt(int argc, char **argv)
{
app_config_t cfg;
config_get(&cfg);
float w = cfg.adapt_weight;
if (argc >= 2) {
char *end;
w = strtof(argv[1], &end);
if (*end != '\0' || !(w >= 0.0f && w <= 1.0f)) {
printf("usage: adapt [weight 0..1] (weight of the NEW template)\n");
return 0;
}
}
calibration_run_learn(true, w);
return 0;
}
static int cmd_monitor(int argc, char **argv)
{
if (argc < 2 || (strcmp(argv[1], "on") && strcmp(argv[1], "off"))) {
printf("usage: monitor on|off (currently %s)\n", processing_get_monitor() ? "on" : "off");
return 0;
}
processing_set_monitor(strcmp(argv[1], "on") == 0);
printf("monitor %s\n", argv[1]);
return 0;
}
static int cmd_set(int argc, char **argv)
{
if (argc != 3) {
printf("usage: set <param> <value> (see `get`)\n");
return 0;
}
char err[96];
if (config_set_param(argv[1], argv[2], err, sizeof(err)) == ESP_OK) {
printf("ok, saved\n");
} else {
printf("error: %s\n", err);
}
return 0;
}
static int cmd_get(int argc, char **argv)
{
char line[200];
for (int i = 0; i < config_param_count(); i++) {
if (argc >= 2 && strcmp(argv[1], config_param_name(i)) != 0) continue;
config_param_format(i, line, sizeof(line));
printf("%s\n", line);
}
return 0;
}
static int cmd_status(int argc, char **argv)
{
proc_stats_t ps;
processing_get_stats(&ps);
notifier_stats_t ns;
notifier_get_stats(&ns);
app_config_t cfg;
config_get(&cfg);
float sug;
printf("uptime: %llu s\n", (unsigned long long)(esp_timer_get_time() / 1000000));
printf("mode / state: %s / %s\n", calibration_mode_name(calibration_get_mode()), detector_state_name(ps.state));
printf("template: %s, %lu frames%s\n", matcher_source_name(), (unsigned long)matcher_frame_count(),
matcher_has_template() ? "" : " <-- run tools/make_template.py + rebuild, or `learn`");
if (matcher_suggested_threshold(&sug)) printf(" template.h suggests threshold %.3f\n", sug);
printf("detector: threshold %.3f, min_rms %.0f, window %lu/%lu, cooldown %lu ms\n", cfg.threshold, cfg.min_rms,
(unsigned long)cfg.win_m, (unsigned long)cfg.win_n, (unsigned long)cfg.cooldown_ms);
printf("last frame: rms %.1f, score %+.3f, match %d\n", ps.last_rms, ps.last_score, ps.last_match);
printf("triggers: %lu (last at uptime %lu s)\n", (unsigned long)ps.triggers, (unsigned long)ps.last_trigger_s);
printf("frames: processed %lu, captured %lu, dropped %lu\n", (unsigned long)ps.frames,
(unsigned long)audio_in_frames_captured(), (unsigned long)audio_in_frames_dropped());
printf("proc time: avg %lu us, max %lu us (budget %d us per frame)\n", (unsigned long)ps.proc_avg_us,
(unsigned long)ps.proc_max_us, HOP_LEN * 1000000 / FS_HZ);
printf("wifi: %s%s%s\n", ns.wifi_configured ? (ns.wifi_connected ? "connected " : "DISCONNECTED") : "not configured",
ns.wifi_connected ? ns.ip : "", "");
printf("notifications: ok %lu, failed %lu, dropped %lu, topic %s%s\n", (unsigned long)ns.sent_ok,
(unsigned long)ns.sent_failed, (unsigned long)ns.dropped, CONFIG_BUZZ_NTFY_TOPIC,
cfg.heartbeat_h ? " (heartbeat on)" : "");
printf("heap free: %lu bytes\n", (unsigned long)esp_get_free_heap_size());
return 0;
}
static int cmd_fingerprint(int argc, char **argv)
{
float live[NUM_BANDS], tpl[NUM_BANDS];
processing_get_last_fp(live);
matcher_get_template(tpl);
const int *e = features_band_edges();
printf("band bins live template (%s)\n", matcher_source_name());
for (int b = 0; b < NUM_BANDS; b++) {
printf("%3d %3d-%-3d %+.4f %+.4f\n", b, e[b], e[b + 1] - 1, live[b], tpl[b]);
}
return 0;
}
static int cmd_alert(int argc, char **argv)
{
printf(notifier_send(NOTIF_TEST, "Test notification") ? "queued\n" : "queue full\n");
return 0;
}
static int cmd_reboot(int argc, char **argv)
{
printf("rebooting...\n");
fflush(stdout);
esp_restart();
return 0;
}
static void reg(const char *name, const char *help, const char *hint, esp_console_cmd_func_t fn)
{
const esp_console_cmd_t c = { .command = name, .help = help, .hint = hint, .func = fn };
esp_err_t err = esp_console_cmd_register(&c);
if (err != ESP_OK) ESP_LOGE("console", "register %s: %s", name, esp_err_to_name(err));
}
esp_err_t console_start(void)
{
esp_console_repl_t *repl = NULL;
esp_console_repl_config_t rc = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
rc.prompt = "buzz> ";
rc.max_cmdline_length = 128;
rc.task_stack_size = 8192;
esp_err_t err;
#if CONFIG_ESP_CONSOLE_UART_DEFAULT || CONFIG_ESP_CONSOLE_UART_CUSTOM
esp_console_dev_uart_config_t hw = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
err = esp_console_new_repl_uart(&hw, &rc, &repl);
#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG
esp_console_dev_usb_serial_jtag_config_t hw = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT();
err = esp_console_new_repl_usb_serial_jtag(&hw, &rc, &repl);
#elif CONFIG_ESP_CONSOLE_USB_CDC
esp_console_dev_usb_cdc_config_t hw = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT();
err = esp_console_new_repl_usb_cdc(&hw, &rc, &repl);
#else
#error "Unsupported console transport"
#endif
if (err != ESP_OK) {
ESP_LOGE("console", "REPL create: %s", esp_err_to_name(err));
return err;
}
esp_console_register_help_command();
reg("selftest", "Run firmware feature pipeline on the built-in test vector (C/Python parity)", NULL, cmd_selftest);
reg("selfcheck", "10 s: press the real buzzer; score vs ACTIVE template + verdict", NULL, cmd_selfcheck);
reg("learn", "60 s: learn template from the real mic (`learn reset` = clear NVS template)", "[reset]", cmd_learn);
reg("adapt", "60 s: blend newly heard buzzer into the active template", "[weight 0..1]", cmd_adapt);
reg("background", "60 s ambient score measurement vs active template", NULL, cmd_background);
reg("test", "10 s: press the buzzer; score distribution of loud frames", NULL, cmd_test);
reg("autothresh", "set threshold between background p99 and buzzer p5", NULL, cmd_autothresh);
reg("monitor", "per-frame live line on/off", "on|off", cmd_monitor);
reg("set", "set a parameter (persisted)", "<param> <value>", cmd_set);
reg("get", "show parameters", "[param]", cmd_get);
reg("status", "template source, detector state, timing, Wi-Fi", NULL, cmd_status);
reg("fingerprint", "print live fingerprint and active template", NULL, cmd_fingerprint);
reg("alert", "send a test notification", NULL, cmd_alert);
reg("reboot", "restart", NULL, cmd_reboot);
err = esp_console_start_repl(repl);
if (err != ESP_OK) ESP_LOGE("console", "start_repl: %s", esp_err_to_name(err));
return err;
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
#include "esp_err.h"
esp_err_t console_start(void); // esp_console REPL on the configured console device
+51
View File
@@ -0,0 +1,51 @@
#include <string.h>
#include "detector.h"
void detector_init(detector_t *d) { memset(d, 0, sizeof(*d)); d->state = DET_IDLE; }
void detector_reset(detector_t *d) { d->state = DET_IDLE; d->hist = 0; }
bool detector_is_match(float rms, float score, float min_rms, float threshold)
{
return rms >= min_rms && score >= threshold;
}
bool detector_update(detector_t *d, bool match, uint32_t now_ms, const detector_cfg_t *cfg)
{
uint32_t n = cfg->win_n < 1 ? 1 : (cfg->win_n > 32 ? 32 : cfg->win_n);
uint32_t m = cfg->win_m < 1 ? 1 : (cfg->win_m > n ? n : cfg->win_m);
switch (d->state) {
case DET_IDLE: {
d->hist = (d->hist << 1) | (match ? 1u : 0u);
uint32_t mask = (n == 32) ? 0xFFFFFFFFu : ((1u << n) - 1u);
if ((uint32_t)__builtin_popcount(d->hist & mask) >= m) {
d->state = DET_TRIGGERED;
d->triggers++;
return true;
}
break;
}
case DET_TRIGGERED:
d->state = DET_COOLDOWN;
d->cooldown_start_ms = now_ms;
d->hist = 0;
break;
case DET_COOLDOWN:
if ((uint32_t)(now_ms - d->cooldown_start_ms) >= cfg->cooldown_ms) {
d->state = DET_IDLE;
d->hist = 0;
}
break;
}
return false;
}
const char *detector_state_name(det_state_t s)
{
switch (s) {
case DET_IDLE: return "IDLE";
case DET_TRIGGERED: return "TRIGGERED";
case DET_COOLDOWN: return "COOLDOWN";
}
return "?";
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
// Pure state machine: no ESP-IDF dependencies, can be compiled and tested on a PC.
#include <stdbool.h>
#include <stdint.h>
typedef enum { DET_IDLE = 0, DET_TRIGGERED, DET_COOLDOWN } det_state_t;
typedef struct {
uint32_t win_n; // window length in frames (clamped to 1..32)
uint32_t win_m; // matches required (clamped to 1..win_n)
uint32_t cooldown_ms;
} detector_cfg_t;
typedef struct {
det_state_t state;
uint32_t hist; // bit 0 = newest frame, 1 = matched
uint32_t cooldown_start_ms;
uint32_t triggers;
} detector_t;
void detector_init(detector_t *d);
void detector_reset(detector_t *d); // back to IDLE with empty history
// Loudness gate AND score >= threshold.
bool detector_is_match(float rms, float score, float min_rms, float threshold);
// Feed one frame. Returns true exactly once per trigger (IDLE -> TRIGGERED).
// TRIGGERED lasts one frame, then COOLDOWN until cooldown_ms elapsed, then IDLE
// with a cleared history (the buzzer's tail cannot re-trigger).
bool detector_update(detector_t *d, bool match, uint32_t now_ms, const detector_cfg_t *cfg);
const char *detector_state_name(det_state_t s);
+24
View File
@@ -0,0 +1,24 @@
// feature_params.h -- SINGLE SOURCE OF TRUTH for the audio feature pipeline.
//
// This file contains ONLY #define lines (plus comments and an include guard).
// tools/make_template.py parses it with a regex, so firmware and Python can
// never silently disagree. Keep every value a plain numeric literal.
//
// Band edges (implemented identically in fingerprint.c and make_template.py):
// f_b = F_MIN_HZ * (F_MAX_HZ / F_MIN_HZ) ^ (b / NUM_BANDS), b = 0..NUM_BANDS
// bin_b = floor(f_b * FRAME_LEN / FS_HZ + 0.5) (round half up, double math)
// if bin_b < bin_(b-1) + 1 then bin_b = bin_(b-1) + 1 (>= 1 bin per band)
// band b covers FFT bins [bin_b, bin_(b+1)). bin_NUM_BANDS must be <= FRAME_LEN/2.
#ifndef FEATURE_PARAMS_H
#define FEATURE_PARAMS_H
#define FS_HZ 16000 // sample rate of the whole pipeline
#define FRAME_LEN 1024 // samples per analysis frame (power of two)
#define HOP_LEN 512 // hop = 32 ms at 16 kHz
#define NUM_BANDS 32 // log-spaced bands in the fingerprint
#define F_MIN_HZ 100 // lower edge of band 0
#define F_MAX_HZ 7500 // nominal upper edge of the last band
#define LOG_EPS 1e-10f // added to band power before log (power is of samples scaled to +-1.0)
#define FP_DETREND 1 // 1: remove linear tilt across band index, 0: off
#endif
+141
View File
@@ -0,0 +1,141 @@
// Spectral fingerprint. MUST stay numerically identical to tools/make_template.py.
#include <math.h>
#include <string.h>
#include "esp_log.h"
#include "dsps_fft2r.h"
#include "fingerprint.h"
static const char *TAG = "features";
static float s_window[FRAME_LEN];
static int s_edges[NUM_BANDS + 1];
static float s_inv_width[NUM_BANDS];
static float s_sxx; // sum of squared centred band indices (detrend)
static bool s_ready;
esp_err_t features_init(void)
{
if (s_ready) {
return ESP_OK;
}
// Periodic Hann: w[n] = 0.5 - 0.5 cos(2 pi n / N)
for (int n = 0; n < FRAME_LEN; n++) {
s_window[n] = 0.5f - 0.5f * cosf(2.0f * (float)M_PI * (float)n / (float)FRAME_LEN);
}
// Log-spaced band edges, see feature_params.h. Double math + floor(x+0.5) so
// C and numpy can never disagree on rounding of exact .5 values.
const double ratio = (double)F_MAX_HZ / (double)F_MIN_HZ;
for (int b = 0; b <= NUM_BANDS; b++) {
double f = (double)F_MIN_HZ * pow(ratio, (double)b / (double)NUM_BANDS);
int bin = (int)floor(f * (double)FRAME_LEN / (double)FS_HZ + 0.5);
if (b > 0 && bin < s_edges[b - 1] + 1) {
bin = s_edges[b - 1] + 1; // guarantee >= 1 bin per band
}
s_edges[b] = bin;
}
if (s_edges[NUM_BANDS] > FRAME_LEN / 2) {
ESP_LOGE(TAG, "band edges exceed Nyquist (%d > %d): lower NUM_BANDS or raise F_MIN_HZ",
s_edges[NUM_BANDS], FRAME_LEN / 2);
return ESP_ERR_INVALID_ARG;
}
for (int b = 0; b < NUM_BANDS; b++) {
s_inv_width[b] = 1.0f / (float)(s_edges[b + 1] - s_edges[b]);
}
s_sxx = 0.0f;
for (int b = 0; b < NUM_BANDS; b++) {
float xc = (float)b - 0.5f * (float)(NUM_BANDS - 1);
s_sxx += xc * xc;
}
esp_err_t err = dsps_fft2r_init_fc32(NULL, FRAME_LEN);
if (err != ESP_OK) {
ESP_LOGE(TAG, "dsps_fft2r_init_fc32: %s", esp_err_to_name(err));
return err;
}
s_ready = true;
return ESP_OK;
}
const int *features_band_edges(void) { return s_edges; }
bool features_l2_normalize(float *v, int n)
{
float s = 0.0f;
for (int i = 0; i < n; i++) s += v[i] * v[i];
float nrm = sqrtf(s);
if (!(nrm > 1e-3f)) { // flat or NaN (silence leaves ~1e-5 float residue)
memset(v, 0, (size_t)n * sizeof(float));
return false;
}
float inv = 1.0f / nrm;
for (int i = 0; i < n; i++) v[i] *= inv;
return true;
}
float features_dot(const float *a, const float *b)
{
float s = 0.0f;
for (int i = 0; i < NUM_BANDS; i++) s += a[i] * b[i];
if (s > 1.0f) s = 1.0f;
if (s < -1.0f) s = -1.0f;
return s;
}
bool features_compute(features_ctx_t *ctx, const int16_t *pcm, float fp[NUM_BANDS], float *rms_out)
{
// 1) DC removal per frame (exact integer sum, then float).
int32_t isum = 0;
for (int i = 0; i < FRAME_LEN; i++) isum += pcm[i];
const float mean = (float)isum / (float)FRAME_LEN;
// 2) RMS of the DC-free frame (int16 units) + windowed, +-1.0-scaled FFT input.
float sq = 0.0f;
for (int i = 0; i < FRAME_LEN; i++) {
float d = (float)pcm[i] - mean;
sq += d * d;
ctx->fft[2 * i] = d * (1.0f / 32768.0f) * s_window[i];
ctx->fft[2 * i + 1] = 0.0f;
}
const float rms = sqrtf(sq / (float)FRAME_LEN);
if (rms_out) *rms_out = rms;
// 3) FFT (decimation-in-frequency + bit reversal) -> bins 0..FRAME_LEN/2-1.
if (dsps_fft2r_fc32(ctx->fft, FRAME_LEN) != ESP_OK ||
dsps_bit_rev_fc32(ctx->fft, FRAME_LEN) != ESP_OK) {
memset(fp, 0, NUM_BANDS * sizeof(float));
return false;
}
// 4) Mean power per band -> natural log (eps guards log(0)).
float mean_log = 0.0f;
for (int b = 0; b < NUM_BANDS; b++) {
float acc = 0.0f;
for (int k = s_edges[b]; k < s_edges[b + 1]; k++) {
float re = ctx->fft[2 * k], im = ctx->fft[2 * k + 1];
acc += re * re + im * im;
}
fp[b] = logf(acc * s_inv_width[b] + LOG_EPS);
mean_log += fp[b];
}
// 5) Subtract the mean across bands: removes overall level (gain, distance).
mean_log /= (float)NUM_BANDS;
for (int b = 0; b < NUM_BANDS; b++) fp[b] -= mean_log;
#if FP_DETREND
// 6) Remove the least-squares line over band index. A mic's frequency-response
// tilt looks like a linear slope in log-spectrum, so this cancels it. Vector is
// already zero-mean, so the fit is slope = <v, xc> / <xc, xc>.
float sxv = 0.0f;
for (int b = 0; b < NUM_BANDS; b++) {
sxv += fp[b] * ((float)b - 0.5f * (float)(NUM_BANDS - 1));
}
const float slope = sxv / s_sxx;
for (int b = 0; b < NUM_BANDS; b++) {
fp[b] -= slope * ((float)b - 0.5f * (float)(NUM_BANDS - 1));
}
#endif
// 7) L2 normalise so cosine similarity is a plain dot product.
return features_l2_normalize(fp, NUM_BANDS);
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "feature_params.h"
// Scratch for one FFT. Each caller thread owns its own context (processing task,
// selftest) so features_compute() needs no locking. 16-byte aligned for the S3 FFT.
typedef struct {
float fft[2 * FRAME_LEN] __attribute__((aligned(16)));
} features_ctx_t;
esp_err_t features_init(void); // window, band edges, FFT tables (idempotent)
// pcm: FRAME_LEN int16 samples. Outputs a unit-norm fingerprint and the DC-free RMS
// (int16 units). Returns false if the fingerprint is degenerate (silence / flat):
// fp is then all zeros and scores 0.
bool features_compute(features_ctx_t *ctx, const int16_t *pcm, float fp[NUM_BANDS], float *rms_out);
const int *features_band_edges(void); // NUM_BANDS + 1 FFT-bin edges
bool features_l2_normalize(float *v, int n); // false (and zeros) if norm ~ 0
float features_dot(const float *a, const float *b); // cosine for unit vectors
+3 -1
View File
@@ -1,2 +1,4 @@
dependencies:
espressif/led_strip: "^3.0.0"
idf: ">=5.2"
espressif/esp-dsp: ">=1.4.0"
espressif/led_strip: ">=3.0.0"
+41
View File
@@ -0,0 +1,41 @@
#include "esp_log.h"
#include "led.h"
#include "sdkconfig.h"
#if CONFIG_BUZZ_RGB_LED_ENABLE
#include "driver/rmt_types.h"
#include "led_strip.h"
#include "board_config.h"
static led_strip_handle_t s_strip;
esp_err_t led_init(void)
{
led_strip_config_t strip_cfg = {
.strip_gpio_num = RGB_LED_GPIO,
.max_leds = 1,
};
led_strip_rmt_config_t rmt_cfg = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = 10 * 1000 * 1000,
};
esp_err_t err = led_strip_new_rmt_device(&strip_cfg, &rmt_cfg, &s_strip);
if (err != ESP_OK) {
ESP_LOGW("led", "led_strip init failed: %s (LED disabled)", esp_err_to_name(err));
s_strip = NULL;
return err;
}
led_strip_clear(s_strip);
return ESP_OK;
}
void led_set(uint8_t r, uint8_t g, uint8_t b)
{
if (!s_strip) return;
led_strip_set_pixel(s_strip, 0, r, g, b);
led_strip_refresh(s_strip);
}
#else
esp_err_t led_init(void) { return ESP_OK; }
void led_set(uint8_t r, uint8_t g, uint8_t b) { (void)r; (void)g; (void)b; }
#endif
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <stdint.h>
#include "esp_err.h"
esp_err_t led_init(void);
void led_set(uint8_t r, uint8_t g, uint8_t b); // no-op if the LED is disabled in Kconfig
+59 -120
View File
@@ -1,139 +1,78 @@
#include <math.h>
#include <stdint.h>
// Door buzzer detector: spectral-template matching on an I2S MEMS mic, ntfy push alerts.
#include <stdio.h>
#include "driver/gpio.h"
#include "driver/i2s_std.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "led_strip.h"
#include "freertos/task.h"
#include "audio_in.h"
#include "board_config.h"
#include "calibration.h"
#include "config_store.h"
#include "console.h"
#include "led.h"
#include "matcher.h"
#include "notifier.h"
#include "processing.h"
#define I2S_BCLK_GPIO GPIO_NUM_3
#define I2S_WS_GPIO GPIO_NUM_4
#define I2S_DATA_GPIO GPIO_NUM_5
#define RGB_LED_GPIO GPIO_NUM_48
static const char *TAG = "main";
#define SAMPLE_RATE_HZ 16000
#define SAMPLES_PER_READ 256
#define GREEN_NOISE_RMS 100000.0f
#define RED_NOISE_RMS 1000000.0f
#define LED_BRIGHTNESS 64
static const char *TAG = "buzz_detector";
static led_strip_handle_t init_rgb_led(void)
// BOOT button (GPIO0, active low). A short press (30 ms .. 1 s) starts `learn`.
static void button_task(void *arg)
{
const led_strip_config_t strip_config = {
.strip_gpio_num = RGB_LED_GPIO,
.max_leds = 1,
.led_model = LED_MODEL_WS2812,
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB,
.flags = {
.invert_out = false,
},
gpio_config_t io = {
.pin_bit_mask = 1ULL << BOOT_BTN_GPIO,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
};
const led_strip_rmt_config_t rmt_config = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = 10 * 1000 * 1000,
.mem_block_symbols = 64,
.flags = {
.with_dma = false,
},
};
led_strip_handle_t strip = NULL;
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &strip));
ESP_ERROR_CHECK(led_strip_clear(strip));
return strip;
}
static void update_rgb_led(led_strip_handle_t strip, float rms)
{
float red = 0.0f;
float green = 0.0f;
if (rms < GREEN_NOISE_RMS) {
green = LED_BRIGHTNESS * rms / GREEN_NOISE_RMS;
} else {
float red_fraction = (rms - GREEN_NOISE_RMS) /
(RED_NOISE_RMS - GREEN_NOISE_RMS);
if (red_fraction > 1.0f) {
red_fraction = 1.0f;
if (gpio_config(&io) != ESP_OK) {
ESP_LOGE(TAG, "button gpio_config failed");
vTaskDelete(NULL);
}
int64_t down_at = 0;
for (;;) {
vTaskDelay(pdMS_TO_TICKS(20));
bool down = gpio_get_level(BOOT_BTN_GPIO) == 0;
if (down && down_at == 0) {
down_at = esp_timer_get_time();
} else if (!down && down_at != 0) {
int64_t held_ms = (esp_timer_get_time() - down_at) / 1000;
down_at = 0;
if (held_ms >= 30 && held_ms <= 1000) {
printf("\n[BOOT button] starting learn\n");
calibration_run_learn(false, 0.0f); // blocks this task only
}
}
red = LED_BRIGHTNESS * red_fraction;
green = LED_BRIGHTNESS * (1.0f - red_fraction);
}
ESP_ERROR_CHECK(led_strip_set_pixel(strip, 0, (uint8_t)red,
(uint8_t)green, 0));
ESP_ERROR_CHECK(led_strip_refresh(strip));
}
static i2s_chan_handle_t init_microphone(void)
static void check(esp_err_t err, const char *what)
{
i2s_chan_handle_t rx_channel = NULL;
const i2s_chan_config_t channel_config =
I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&channel_config, NULL, &rx_channel));
i2s_std_config_t standard_config = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE_HZ),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = I2S_BCLK_GPIO,
.ws = I2S_WS_GPIO,
.dout = I2S_GPIO_UNUSED,
.din = I2S_DATA_GPIO,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
standard_config.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT;
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_channel, &standard_config));
ESP_ERROR_CHECK(i2s_channel_enable(rx_channel));
return rx_channel;
if (err != ESP_OK) {
ESP_LOGE(TAG, "%s failed: %s", what, esp_err_to_name(err));
}
}
void app_main(void)
{
led_strip_handle_t rgb_led = init_rgb_led();
i2s_chan_handle_t microphone = init_microphone();
int32_t samples[SAMPLES_PER_READ];
float smoothed_rms = 0.0f;
uint32_t log_counter = 0;
ESP_ERROR_CHECK(config_init()); // also initialises NVS
check(led_init(), "led_init");
ESP_ERROR_CHECK(calibration_init());
ESP_ERROR_CHECK(matcher_init());
ESP_ERROR_CHECK(audio_in_init());
check(notifier_init(), "notifier_init");
ESP_ERROR_CHECK(processing_start());
ESP_ERROR_CHECK(audio_in_start());
ESP_LOGI(TAG, "INMP441 ready on BCLK=%d, WS=%d, SD=%d; RGB LED on GPIO %d",
I2S_BCLK_GPIO, I2S_WS_GPIO, I2S_DATA_GPIO, RGB_LED_GPIO);
while (true) {
size_t bytes_read = 0;
ESP_ERROR_CHECK(i2s_channel_read(microphone, samples, sizeof(samples),
&bytes_read, portMAX_DELAY));
const size_t sample_count = bytes_read / sizeof(samples[0]);
if (sample_count == 0) {
continue;
}
int64_t sum_squares = 0;
for (size_t i = 0; i < sample_count; ++i) {
// INMP441 provides 24-bit signed samples in the upper bits of each 32-bit slot.
const int32_t sample = samples[i] >> 8;
sum_squares += (int64_t)sample * sample;
}
const float rms = sqrtf((float)sum_squares / sample_count);
smoothed_rms += 0.2f * (rms - smoothed_rms);
update_rgb_led(rgb_led, smoothed_rms);
if (++log_counter == 16) {
ESP_LOGI(TAG, "sound RMS: %.0f", smoothed_rms);
log_counter = 0;
}
if (xTaskCreate(button_task, "button", 4096, NULL, 3, NULL) != pdPASS) {
ESP_LOGE(TAG, "button task not created");
}
printf("\nDoor buzzer detector ready. Type `help`.\n");
if (!matcher_has_template()) {
printf("NEEDS TEMPLATE. Either:\n"
" 1) python tools/make_template.py --buzz your_recording.wav then rebuild/flash, or\n"
" 2) type `learn` (or short-press BOOT) and press the buzzer during the 60 s session.\n");
}
check(console_start(), "console_start");
}
+161
View File
@@ -0,0 +1,161 @@
#include <math.h>
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "nvs.h"
#include "fingerprint.h"
#include "matcher.h"
#include "template.h"
_Static_assert(TEMPLATE_NUM_BANDS == NUM_BANDS,
"template.h was generated with a different NUM_BANDS: rerun tools/make_template.py");
static const char *TAG = "matcher";
#define NVS_NS "buzz"
#define TPL_KEY "tpl"
#define TPL_MAGIC 0x42555A54u // 'BUZT'
typedef struct {
uint32_t magic;
uint32_t num_bands;
uint32_t frame_count;
float fp[NUM_BANDS];
} tpl_blob_t;
static SemaphoreHandle_t s_mx;
static float s_tpl[NUM_BANDS];
static tpl_source_t s_src = TPL_NONE;
static uint32_t s_count;
static bool valid_unit_vector(const float *v)
{
float s = 0;
for (int i = 0; i < NUM_BANDS; i++) {
if (!isfinite(v[i])) return false;
s += v[i] * v[i];
}
return fabsf(sqrtf(s) - 1.0f) < 0.01f;
}
static bool load_nvs(void)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) return false;
tpl_blob_t b;
size_t len = sizeof(b);
esp_err_t e = nvs_get_blob(h, TPL_KEY, &b, &len);
nvs_close(h);
if (e != ESP_OK || len != sizeof(b) || b.magic != TPL_MAGIC ||
b.num_bands != NUM_BANDS || !valid_unit_vector(b.fp)) {
return false;
}
memcpy(s_tpl, b.fp, sizeof(s_tpl));
s_count = b.frame_count;
s_src = TPL_NVS;
return true;
}
static void load_builtin_or_none(void)
{
#if TEMPLATE_VALID
memcpy(s_tpl, TEMPLATE_FP, sizeof(s_tpl));
if (valid_unit_vector(s_tpl)) {
s_count = TEMPLATE_FRAME_COUNT;
s_src = TPL_BUILTIN;
return;
}
ESP_LOGE(TAG, "compiled-in template is not a unit vector, ignoring it");
#endif
memset(s_tpl, 0, sizeof(s_tpl));
s_count = 0;
s_src = TPL_NONE;
}
esp_err_t matcher_init(void)
{
s_mx = xSemaphoreCreateMutex();
if (!s_mx) return ESP_ERR_NO_MEM;
if (!load_nvs()) load_builtin_or_none();
ESP_LOGI(TAG, "template source: %s (%lu frames)", matcher_source_name(), (unsigned long)s_count);
return ESP_OK;
}
bool matcher_has_template(void) { return s_src != TPL_NONE; }
tpl_source_t matcher_source(void) { return s_src; }
uint32_t matcher_frame_count(void) { return s_count; }
const char *matcher_source_name(void)
{
switch (s_src) {
case TPL_NVS: return "learned on-device (NVS)";
case TPL_BUILTIN: return "compiled-in template.h";
default: return "NONE (needs template)";
}
}
bool matcher_suggested_threshold(float *thr)
{
#if TEMPLATE_HAS_SUGGESTED_THRESHOLD
*thr = TEMPLATE_SUGGESTED_THRESHOLD;
return true;
#else
(void)thr;
return false;
#endif
}
float matcher_score(const float fp[NUM_BANDS])
{
float s = 0.0f;
xSemaphoreTake(s_mx, portMAX_DELAY);
if (s_src != TPL_NONE) s = features_dot(fp, s_tpl);
xSemaphoreGive(s_mx);
return s;
}
void matcher_get_template(float out[NUM_BANDS])
{
xSemaphoreTake(s_mx, portMAX_DELAY);
memcpy(out, s_tpl, sizeof(s_tpl));
xSemaphoreGive(s_mx);
}
esp_err_t matcher_set_template(const float fp[NUM_BANDS], uint32_t frame_count)
{
if (!valid_unit_vector(fp)) return ESP_ERR_INVALID_ARG;
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
tpl_blob_t b = { .magic = TPL_MAGIC, .num_bands = NUM_BANDS, .frame_count = frame_count };
memcpy(b.fp, fp, sizeof(b.fp));
err = nvs_set_blob(h, TPL_KEY, &b, sizeof(b));
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
if (err != ESP_OK) {
ESP_LOGE(TAG, "saving template: %s", esp_err_to_name(err));
return err;
}
xSemaphoreTake(s_mx, portMAX_DELAY);
memcpy(s_tpl, fp, sizeof(s_tpl));
s_count = frame_count;
s_src = TPL_NVS;
xSemaphoreGive(s_mx);
return ESP_OK;
}
esp_err_t matcher_reset(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_erase_key(h, TPL_KEY);
if (err == ESP_ERR_NVS_NOT_FOUND) err = ESP_OK;
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
if (err != ESP_OK) return err;
xSemaphoreTake(s_mx, portMAX_DELAY);
load_builtin_or_none();
xSemaphoreGive(s_mx);
return ESP_OK;
}
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "feature_params.h"
typedef enum { TPL_NONE = 0, TPL_NVS, TPL_BUILTIN } tpl_source_t;
esp_err_t matcher_init(void); // priority: NVS-learned > compiled-in template.h > none
bool matcher_has_template(void);
tpl_source_t matcher_source(void);
const char *matcher_source_name(void);
uint32_t matcher_frame_count(void);
bool matcher_suggested_threshold(float *thr); // from template.h, if the tool computed one
float matcher_score(const float fp[NUM_BANDS]); // cosine vs active template (0 if none)
void matcher_get_template(float out[NUM_BANDS]);
esp_err_t matcher_set_template(const float fp[NUM_BANDS], uint32_t frame_count); // -> NVS, source = NVS
esp_err_t matcher_reset(void); // erase NVS template, fall back to built-in
+200
View File
@@ -0,0 +1,200 @@
// Wi-Fi station with exponential-backoff reconnect + ntfy.sh HTTPS notifications.
// Sending happens in its own task fed by a queue so networking can never block audio.
#include <stdio.h>
#include <string.h>
#include "esp_crt_bundle.h"
#include "esp_event.h"
#include "esp_http_client.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "config_store.h"
#include "notifier.h"
static const char *TAG = "notify";
#define WIFI_CONNECTED_BIT BIT0
#define SEND_ATTEMPTS 4
#define NET_TASK_STACK 8192
#define NET_TASK_PRIO 5
#define BACKOFF_MIN_MS 1000
#define BACKOFF_MAX_MS 60000
typedef struct {
notif_kind_t kind;
char msg[96];
} notif_msg_t;
static QueueHandle_t s_q;
static EventGroupHandle_t s_eg;
static esp_timer_handle_t s_reconnect_timer;
static uint32_t s_backoff_ms = BACKOFF_MIN_MS;
static notifier_stats_t s_stats;
static bool s_wifi_configured;
static void reconnect_cb(void *arg)
{
esp_err_t err = esp_wifi_connect();
if (err != ESP_OK && err != ESP_ERR_WIFI_CONN) {
ESP_LOGW(TAG, "esp_wifi_connect: %s", esp_err_to_name(err));
}
}
static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
xEventGroupClearBits(s_eg, WIFI_CONNECTED_BIT);
s_stats.wifi_connected = false;
ESP_LOGW(TAG, "Wi-Fi disconnected, retry in %lu ms", (unsigned long)s_backoff_ms);
esp_timer_stop(s_reconnect_timer); // ok if not running
esp_timer_start_once(s_reconnect_timer, (uint64_t)s_backoff_ms * 1000ULL);
s_backoff_ms = s_backoff_ms * 2 > BACKOFF_MAX_MS ? BACKOFF_MAX_MS : s_backoff_ms * 2;
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *ev = (ip_event_got_ip_t *)data;
snprintf(s_stats.ip, sizeof(s_stats.ip), IPSTR, IP2STR(&ev->ip_info.ip));
s_stats.wifi_connected = true;
s_backoff_ms = BACKOFF_MIN_MS;
xEventGroupSetBits(s_eg, WIFI_CONNECTED_BIT);
ESP_LOGI(TAG, "Wi-Fi up, IP %s", s_stats.ip);
}
}
static esp_err_t post_ntfy(const char *topic, const char *title, const char *prio, const char *tags, const char *body)
{
char url[192];
size_t sl = strlen(CONFIG_BUZZ_NTFY_SERVER);
while (sl > 0 && CONFIG_BUZZ_NTFY_SERVER[sl - 1] == '/') sl--;
snprintf(url, sizeof(url), "%.*s/%s", (int)sl, CONFIG_BUZZ_NTFY_SERVER, topic);
esp_http_client_config_t cfg = {
.url = url,
.method = HTTP_METHOD_POST,
.timeout_ms = 10000,
.crt_bundle_attach = esp_crt_bundle_attach,
};
esp_http_client_handle_t c = esp_http_client_init(&cfg);
if (!c) return ESP_ERR_NO_MEM;
esp_http_client_set_header(c, "Title", title);
esp_http_client_set_header(c, "Priority", prio);
esp_http_client_set_header(c, "Tags", tags);
esp_http_client_set_post_field(c, body, (int)strlen(body));
esp_err_t err = esp_http_client_perform(c);
if (err == ESP_OK) {
int st = esp_http_client_get_status_code(c);
if (st != 200) {
ESP_LOGW(TAG, "ntfy HTTP status %d", st);
err = ESP_FAIL;
}
} else {
ESP_LOGW(TAG, "HTTP perform: %s", esp_err_to_name(err));
}
esp_http_client_cleanup(c);
return err;
}
// Retry a few times with backoff while Wi-Fi is down / the request fails, then drop.
static bool deliver(const char *topic, const char *title, const char *prio, const char *tags, const char *body)
{
uint32_t wait_ms = 2000;
for (int attempt = 1; attempt <= SEND_ATTEMPTS; attempt++) {
EventBits_t bits = xEventGroupWaitBits(s_eg, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, pdMS_TO_TICKS(15000));
if (bits & WIFI_CONNECTED_BIT) {
if (post_ntfy(topic, title, prio, tags, body) == ESP_OK) {
ESP_LOGI(TAG, "sent '%s' to %s (attempt %d)", body, topic, attempt);
return true;
}
} else {
ESP_LOGW(TAG, "Wi-Fi down (attempt %d/%d)", attempt, SEND_ATTEMPTS);
}
vTaskDelay(pdMS_TO_TICKS(wait_ms));
wait_ms *= 2;
}
ESP_LOGE(TAG, "giving up on '%s', dropped", body);
return false;
}
static void net_task(void *arg)
{
int64_t last_hb_us = esp_timer_get_time();
notif_msg_t m;
for (;;) {
if (xQueueReceive(s_q, &m, pdMS_TO_TICKS(30000)) == pdTRUE) {
bool ok = deliver(CONFIG_BUZZ_NTFY_TOPIC, "Door buzzer",
m.kind == NOTIF_ALERT ? "high" : "default",
m.kind == NOTIF_ALERT ? "bell" : "wrench", m.msg);
if (ok) s_stats.sent_ok++; else s_stats.sent_failed++;
last_hb_us = esp_timer_get_time(); // an alert proves liveness; restart the heartbeat timer
}
app_config_t cfg;
config_get(&cfg);
if (cfg.heartbeat_h > 0 &&
(esp_timer_get_time() - last_hb_us) >= (int64_t)cfg.heartbeat_h * 3600LL * 1000000LL) {
last_hb_us = esp_timer_get_time();
if (deliver(CONFIG_BUZZ_NTFY_HB_TOPIC, "Door buzzer heartbeat", "min", "green_heart", "alive")) {
s_stats.sent_ok++;
} else {
s_stats.sent_failed++;
}
}
}
}
bool notifier_send(notif_kind_t kind, const char *msg)
{
notif_msg_t m = { .kind = kind };
snprintf(m.msg, sizeof(m.msg), "%s", msg);
if (xQueueSend(s_q, &m, 0) != pdTRUE) {
s_stats.dropped++;
ESP_LOGW(TAG, "notification queue full, dropped");
return false;
}
return true;
}
void notifier_get_stats(notifier_stats_t *out)
{
*out = s_stats;
out->wifi_configured = s_wifi_configured;
}
esp_err_t notifier_init(void)
{
s_q = xQueueCreate(4, sizeof(notif_msg_t));
s_eg = xEventGroupCreate();
if (!s_q || !s_eg) return ESP_ERR_NO_MEM;
s_wifi_configured = strlen(CONFIG_BUZZ_WIFI_SSID) > 0;
if (s_wifi_configured) {
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
const esp_timer_create_args_t targs = { .callback = reconnect_cb, .name = "wifi_retry" };
ESP_ERROR_CHECK(esp_timer_create(&targs, &s_reconnect_timer));
wifi_init_config_t icfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&icfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL));
wifi_config_t wc = { 0 };
strlcpy((char *)wc.sta.ssid, CONFIG_BUZZ_WIFI_SSID, sizeof(wc.sta.ssid));
strlcpy((char *)wc.sta.password, CONFIG_BUZZ_WIFI_PASSWORD, sizeof(wc.sta.password));
wc.sta.threshold.authmode = strlen(CONFIG_BUZZ_WIFI_PASSWORD) ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN;
wc.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH;
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wc));
ESP_ERROR_CHECK(esp_wifi_start());
} else {
ESP_LOGW(TAG, "no Wi-Fi SSID configured (menuconfig): notifications will be dropped");
}
BaseType_t ok = xTaskCreatePinnedToCore(net_task, "net", NET_TASK_STACK, NULL, NET_TASK_PRIO, NULL, 0);
return ok == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
typedef enum { NOTIF_ALERT = 0, NOTIF_TEST } notif_kind_t;
typedef struct {
bool wifi_connected;
char ip[16];
uint32_t sent_ok, sent_failed, dropped;
bool wifi_configured;
} notifier_stats_t;
esp_err_t notifier_init(void); // Wi-Fi + network task
bool notifier_send(notif_kind_t kind, const char *msg); // non-blocking enqueue, false if queue full
void notifier_get_stats(notifier_stats_t *out);
+143
View File
@@ -0,0 +1,143 @@
// features -> score -> detector, or calibration while a session is active.
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_task_wdt.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "audio_in.h"
#include "calibration.h"
#include "config_store.h"
#include "detector.h"
#include "fingerprint.h"
#include "led.h"
#include "matcher.h"
#include "notifier.h"
#include "processing.h"
static const char *TAG = "proc";
#define PROC_TASK_CORE 1
#define PROC_TASK_PRIO 8
#define PROC_TASK_STACK 6144
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
static proc_stats_t s_stats;
static float s_last_fp[NUM_BANDS];
static volatile bool s_monitor;
static features_ctx_t s_ctx;
void processing_get_stats(proc_stats_t *out)
{
portENTER_CRITICAL(&s_lock);
*out = s_stats;
portEXIT_CRITICAL(&s_lock);
}
void processing_get_last_fp(float fp[NUM_BANDS])
{
portENTER_CRITICAL(&s_lock);
memcpy(fp, s_last_fp, sizeof(s_last_fp));
portEXIT_CRITICAL(&s_lock);
}
void processing_set_monitor(bool on) { s_monitor = on; }
bool processing_get_monitor(void) { return s_monitor; }
static void update_led(app_mode_t mode, det_state_t st, bool has_tpl)
{
static int last = -1;
int code;
if (!has_tpl) code = 1;
else if (mode != MODE_NORMAL) code = 2;
else if (st != DET_IDLE) code = 3;
else code = 0;
if (code == last) return;
last = code;
switch (code) {
case 1: led_set(20, 0, 0); break; // red: needs template
case 2: led_set(20, 12, 0); break; // amber: calibration session
case 3: led_set(20, 0, 20); break; // magenta: alert / cooldown
default: led_set(0, 0, 6); break; // dim blue: listening
}
}
static void processing_task(void *arg)
{
ESP_ERROR_CHECK(esp_task_wdt_add(NULL));
detector_t det;
detector_init(&det);
float fp[NUM_BANDS];
uint32_t last_mon_ms = 0;
for (;;) {
esp_task_wdt_reset();
audio_frame_t fr;
if (!audio_in_get_frame(&fr, pdMS_TO_TICKS(1000))) {
ESP_LOGW(TAG, "no audio frame for 1 s");
continue;
}
int64_t t0 = esp_timer_get_time();
float rms = 0.0f;
bool valid = features_compute(&s_ctx, fr.pcm, fp, &rms);
const bool has_tpl = matcher_has_template();
const float score = (valid && has_tpl) ? matcher_score(fp) : 0.0f;
uint32_t dt_us = (uint32_t)(esp_timer_get_time() - t0);
audio_in_release_frame(fr.idx); // pool slot back as early as possible
const uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000);
app_config_t cfg;
config_get(&cfg);
const app_mode_t mode = calibration_get_mode();
const bool match = has_tpl && detector_is_match(rms, score, cfg.min_rms, cfg.threshold);
bool trig = false;
if (mode == MODE_NORMAL) {
if (has_tpl) {
detector_cfg_t dc = { cfg.win_n, cfg.win_m, cfg.cooldown_ms };
trig = detector_update(&det, match, now_ms, &dc);
} else {
detector_reset(&det);
}
} else {
calibration_on_frame(fp, valid, rms, score); // detector paused: no alerts
detector_reset(&det);
}
if (trig) {
ESP_LOGW(TAG, "BUZZER DETECTED (rms %.0f, score %.3f)", rms, score);
notifier_send(NOTIF_ALERT, "Buzzer detected");
}
portENTER_CRITICAL(&s_lock);
s_stats.frames++;
s_stats.proc_avg_us = s_stats.frames == 1 ? dt_us : (s_stats.proc_avg_us * 15u + dt_us) / 16u;
if (dt_us > s_stats.proc_max_us) s_stats.proc_max_us = dt_us;
s_stats.last_rms = rms;
s_stats.last_score = score;
s_stats.last_match = match;
s_stats.state = det.state;
s_stats.triggers = det.triggers;
if (trig) s_stats.last_trigger_s = now_ms / 1000 ? now_ms / 1000 : 1;
memcpy(s_last_fp, fp, sizeof(s_last_fp));
portEXIT_CRITICAL(&s_lock);
update_led(mode, det.state, has_tpl);
if (s_monitor && (uint32_t)(now_ms - last_mon_ms) >= cfg.monitor_ms) {
last_mon_ms = now_ms;
printf("[%8lu ms] rms=%7.1f score=%+.3f %s %s\n", (unsigned long)now_ms, rms, score,
match ? "MATCH" : " - ",
mode == MODE_NORMAL ? detector_state_name(det.state) : calibration_mode_name(mode));
}
}
}
esp_err_t processing_start(void)
{
esp_err_t err = features_init();
if (err != ESP_OK) return err;
BaseType_t ok = xTaskCreatePinnedToCore(processing_task, "proc", PROC_TASK_STACK, NULL,
PROC_TASK_PRIO, NULL, PROC_TASK_CORE);
return ok == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "detector.h"
#include "feature_params.h"
typedef struct {
uint32_t frames;
uint32_t proc_avg_us; // exponential moving average of features+match time
uint32_t proc_max_us;
float last_rms, last_score;
bool last_match;
det_state_t state;
uint32_t triggers;
uint32_t last_trigger_s; // uptime seconds of the last trigger, 0 = never
} proc_stats_t;
esp_err_t processing_start(void);
void processing_get_stats(proc_stats_t *out);
void processing_get_last_fp(float fp[NUM_BANDS]);
void processing_set_monitor(bool on);
bool processing_get_monitor(void);
+21
View File
@@ -0,0 +1,21 @@
// GENERATED by tools/make_template.py -- do not edit.
// Source recording: many_buzz.wav
// Parameters: FS_HZ=16000 FRAME_LEN=1024 HOP_LEN=512 NUM_BANDS=32 F_MIN_HZ=100 F_MAX_HZ=7500 LOG_EPS=1e-10 FP_DETREND=1
#ifndef TEMPLATE_H
#define TEMPLATE_H
#include "feature_params.h"
#define TEMPLATE_VALID 1
#define TEMPLATE_NUM_BANDS 32
#define TEMPLATE_FRAME_COUNT 1605
#define TEMPLATE_HAS_SUGGESTED_THRESHOLD 0
static const float TEMPLATE_FP[NUM_BANDS] = {
-1.234297492e-01f, -1.172692978e-01f, -1.647167516e-01f, -2.107734516e-01f,
-1.697949230e-01f, -1.590963513e-01f, -1.411206649e-01f, -4.264512523e-02f,
1.186895466e-01f, 5.534144070e-01f, 4.097405769e-01f, 2.584858953e-01f,
2.150118383e-01f, 7.485003583e-02f, -1.082601745e-01f, -1.384175877e-01f,
-9.168937132e-02f, 9.338026730e-02f, 2.869415823e-02f, 2.120377562e-02f,
-5.454016039e-03f, 1.042970822e-01f, 6.318512667e-02f, -1.156468943e-01f,
1.076363226e-01f, 8.577662649e-02f, -2.341709754e-02f, 5.675516909e-03f,
-1.403284712e-01f, -2.986392837e-01f, -1.027432535e-01f, 1.340128842e-02f,
};
#endif
+86
View File
@@ -0,0 +1,86 @@
// GENERATED by tools/make_template.py -- do not edit.
// One int16 frame from the middle of the buzzer (frame #1355) and the Python-computed
// fingerprint/RMS the firmware `selftest` must reproduce within 1e-3.
#ifndef TEST_VECTOR_H
#define TEST_VECTOR_H
#include <stdint.h>
#include "feature_params.h"
#define TEST_VECTOR_VALID 1
#define TEST_VECTOR_RMS 2076.644521f
static const int16_t TEST_VECTOR_PCM[FRAME_LEN] = {
-946, -1702, -2426, -2012, -1726, -1055, -1290, -1011, -900, -1042, -1005, -863, -339, 267, 493, 656,
1057, 835, 1041, 1258, 1529, 2141, 1751, 2357, 2697, 3020, 3065, 2443, 2244, 1888, 2155, 2081,
1913, 698, -227, -777, -1053, -939, -1549, -2176, -2786, -2572, -2326, -2590, -3312, -3801, -3829, -3531,
-2763, -3285, -3392, -3490, -2180, -1060, -313, 285, 777, 1535, 1521, 2483, 2476, 3045, 3019, 3019,
3037, 3148, 3833, 4197, 3638, 3379, 3587, 3239, 2708, 971, 975, 248, 390, 3, -428, -982,
-2180, -1992, -2688, -2702, -2828, -2740, -2665, -3422, -3985, -3687, -3615, -2860, -3001, -2621, -2034, -1543,
-709, -1014, -781, -441, 769, 1358, 2244, 2226, 2164, 2159, 2610, 3668, 3800, 3033, 3213, 3712,
4244, 3943, 3241, 3210, 2070, 1926, 1422, 1302, 261, -685, -1026, -1419, -1793, -3110, -3016, -3436,
-3220, -3412, -3374, -3168, -3624, -4175, -4202, -3021, -2252, -2002, -2192, -1027, -347, 310, 104, 362,
1024, 1578, 2666, 2916, 3447, 2990, 3264, 3409, 3135, 2726, 2513, 2828, 2955, 2445, 2141, 1814,
1481, 424, -78, -394, -450, -656, -1299, -1327, -1953, -2112, -2976, -3092, -2889, -2478, -2426, -2043,
-1736, -1587, -1935, -2470, -1813, -1668, -1009, -889, -150, -333, 120, 594, 1053, 1740, 1188, 2057,
1778, 2014, 1774, 1555, 1634, 1585, 1865, 2073, 1806, 1670, 1799, 1710, 1775, 1059, 894, -78,
-486, -592, -269, -190, -962, -1294, -1863, -1644, -1530, -1691, -1792, -2158, -1735, -1357, -1459, -2165,
-2874, -2062, -1085, -268, -403, -902, -1154, -721, 155, 732, 1174, 1026, 1404, 1421, 1739, 1599,
985, 978, 1104, 2264, 2049, 1954, 1533, 1634, 1367, 1315, 1278, 584, 5, -719, 98, 99,
88, -729, -941, -1221, -1311, -1253, -1368, -1419, -1579, -1228, -1051, -991, -2154, -2078, -1786, -1166,
-929, -1083, -705, -866, -951, -902, -594, 31, 475, 537, 893, 983, 751, 1162, 1168, 1795,
1937, 1809, 2563, 2669, 3197, 2692, 2360, 2003, 2005, 2242, 2144, 1529, 214, -311, -876, -785,
-1090, -1692, -2341, -2585, -2170, -2231, -2709, -3558, -3742, -3758, -3035, -2985, -3465, -3599, -3154, -1615,
-884, -75, 169, 972, 1194, 1728, 2343, 2365, 2896, 2677, 2926, 2736, 3262, 3838, 3881, 3174,
3531, 3313, 3093, 1714, 821, 799, 124, 423, -305, -312, -1574, -2018, -2184, -2714, -2621, -2761,
-2579, -2835, -3778, -3789, -3563, -3181, -2637, -2897, -2059, -1932, -950, -828, -879, -722, 47, 1035,
1707, 2344, 2010, 2167, 2183, 3115, 3855, 3397, 2871, 3421, 3873, 4285, 3420, 3285, 2649, 1887,
1755, 1272, 1057, -423, -732, -1326, -1398, -2516, -3234, -3075, -3377, -3079, -3506, -3085, -3313, -3725,
-4221, -3512, -2495, -1947, -2085, -1623, -503, -24, 390, 29, 807, 1227, 2210, 2795, 3080, 3164,
2840, 3370, 3238, 2845, 2400, 2474, 2903, 2721, 2169, 1865, 1593, 1042, 93, -68, -460, -340,
-973, -1068, -1358, -1834, -2315, -2970, -2674, -2412, -2107, -2127, -1632, -1580, -1424, -2228, -2102, -1691,
-1366, -945, -691, -207, -465, 410, 482, 1438, 1187, 1330, 1893, 1600, 1897, 1347, 1449, 1351,
1582, 1861, 1947, 1546, 1762, 1662, 1799, 1435, 979, 672, -264, -290, -359, 140, -355, -878,
-1397, -1710, -1353, -1407, -1449, -1744, -1897, -1323, -1236, -1534, -2518, -2549, -1535, -671, -195, -691,
-1057, -1162, -445, 261, 875, 1022, 1010, 1390, 1275, 1703, 1067, 913, 804, 1544, 2231, 1968,
1881, 1512, 1688, 1244, 1546, 958, 521, -323, -244, 374, 201, -97, -830, -878, -1347, -1173,
-1386, -1305, -1571, -1459, -1204, -940, -1530, -2386, -2038, -1801, -1114, -1249, -1053, -905, -1173, -1145,
-985, -473, 135, 328, 544, 893, 755, 839, 1105, 1265, 1967, 1676, 2179, 2666, 2902, 3083,
2434, 2278, 1916, 2220, 2208, 2101, 967, 12, -504, -836, -692, -1290, -1885, -2495, -2363, -2114,
-2331, -3052, -3580, -3674, -3438, -2672, -3197, -3424, -3567, -2430, -1235, -536, 140, 534, 1367, 1330,
2236, 2317, 2768, 2914, 2751, 2799, 2825, 3545, 3999, 3539, 3158, 3450, 3109, 2652, 962, 859,
227, 217, 15, -431, -849, -2168, -2117, -2705, -2770, -2785, -2794, -2676, -3366, -4086, -3684, -3590,
-2883, -2934, -2663, -1970, -1588, -720, -1044, -827, -588, 654, 1269, 2183, 2208, 2034, 2086, 2470,
3553, 3808, 3102, 3169, 3690, 4147, 4035, 3280, 3291, 2085, 1895, 1469, 1390, 471, -612, -843,
-1398, -1618, -3045, -2973, -3312, -3203, -3305, -3359, -2982, -3414, -3965, -4169, -2995, -2201, -1835, -2065,
-984, -264, 317, 222, 303, 1015, 1481, 2617, 2838, 3347, 2925, 3085, 3312, 2979, 2561, 2320,
2617, 2843, 2359, 2025, 1733, 1479, 555, -22, -298, -439, -453, -1090, -1029, -1648, -1807, -2643,
-2840, -2604, -2204, -2104, -1823, -1495, -1426, -1695, -2393, -1793, -1665, -1053, -971, -325, -386, -67,
491, 761, 1563, 956, 1771, 1624, 1788, 1693, 1376, 1505, 1447, 1785, 1986, 1785, 1587, 1852,
1716, 1874, 1217, 1104, 295, -290, -317, -129, 91, -733, -1045, -1637, -1541, -1333, -1457, -1520,
-1922, -1667, -1255, -1399, -2013, -2788, -2152, -1167, -397, -387, -914, -1192, -913, -91, 466, 1013,
946, 1308, 1326, 1454, 1470, 868, 916, 933, 2086, 2044, 1987, 1646, 1669, 1541, 1338, 1434,
770, 355, -439, 201, 261, 274, -436, -727, -1005, -1204, -1103, -1302, -1219, -1509, -1183, -1103,
-949, -2082, -2250, -1984, -1432, -1007, -1229, -865, -1020, -1131, -1081, -815, -224, 270, 405, 682,
827, 604, 986, 1053, 1628, 1950, 1754, 2503, 2613, 3083, 2760, 2347, 2083, 2055, 2321, 2244,
1744, 432, -153, -711, -725, -859, -1520, -2110, -2502, -2145, -2113, -2537, -3378, -3631, -3740, -3130,
-2868, -3375, -3432, -3257, -1764, -1048, -113, 192, 940, 1237, 1589, 2344, 2301, 2926, 2664, 2808,
2686, 3133, 3806, 3998, 3205, 3420, 3281, 3081, 1897, 763, 817, 42, 442, -243, -227, -1495,
-2116, -2207, -2775, -2608, -2795, -2542, -2734, -3651, -3816, -3572, -3324, -2738, -3075, -2205, -1888, -1061,
-827, -1004, -760, -105, 1007, 1581, 2382, 2044, 2165, 2147, 2984, 3765, 3412, 2895, 3423, 3868,
4263, 3583, 3349, 2883, 1943, 1816, 1382, 1233, -142, -664, -1148, -1286, -2163, -3058, -2932, -3291,
-3006, -3450, -3043, -3136, -3548, -4173, -3611, -2507, -1905, -2007, -1755, -585, -118, 368, 33, 702,
1077, 1964, 2706, 3013, 3225, 2795, 3307, 3152, 2801, 2380, 2374, 2699, 2571, 2116, 1890, 1595,
1073, 58, -169, -592, -449, -955, -1166, -1421, -1876, -2247, -3003, -2803, -2569, -2175, -2135, -1623,
-1546, -1380, -2178, -2211, -1791, -1494, -1014, -817, -218, -500, 303, 385, 1273, 1202, 1148, 1754,
1413, 1771, 1263, 1398, 1298, 1469, 1749, 1864, 1502, 1681, 1694, 1793, 1546, 966, 741, -192,
-288, -376, 84, -235, -811, -1224, -1642, -1318, -1356, -1395, -1567, -1805, -1252, -1150, -1411, -2347,
};
static const float TEST_VECTOR_FP[NUM_BANDS] = {
-3.645497329e-01f, -1.889563834e-01f, -2.027908220e-01f, -9.830562242e-02f,
-1.154377913e-01f, -6.263310393e-02f, -5.827089101e-02f, -3.816777800e-02f,
1.257844590e-01f, 5.240575307e-01f, 3.891325996e-01f, 2.516123367e-01f,
2.173896044e-01f, 1.050724342e-01f, -8.207634210e-02f, -9.264745520e-02f,
-5.232892854e-02f, 1.067979427e-01f, 4.857097334e-02f, 4.391817445e-02f,
-2.416819206e-03f, 1.032181760e-01f, 6.795309402e-02f, -1.334582865e-01f,
8.268648484e-02f, 6.559289181e-02f, -5.534441780e-02f, -1.353428886e-02f,
-1.429585141e-01f, -2.783958093e-01f, -1.231069169e-01f, -2.640679836e-02f,
};
#endif