// 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 #include #include #include #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; }