#include "max4466_mic.h"
#include <math.h>
/*
=========================================================
MAX4466 Microphone — реализация
=========================================================
*/
/* ====================================================
BEGIN
==================================================== */
bool Max4466Mic::begin(const Max4466Config &config) {
_config = config;
/* Валидация и зажим параметров */
if (_config.sample_count == 0) _config.sample_count = 256;
if (_config.db_avg_window_s < 1) _config.db_avg_window_s = 1;
if (_config.db_avg_window_s > DB_AVG_MAX) _config.db_avg_window_s = DB_AVG_MAX;
if (_config.db_trend_window_min < 2) _config.db_trend_window_min = 2;
if (_config.db_trend_window_min > DB_TREND_MAX) _config.db_trend_window_min = DB_TREND_MAX;
if (_config.db_ref_mv <= 0.0f) _config.db_ref_mv = 1.0f;
analogReadResolution(12);
pinMode(_config.adc_pin, INPUT);
_last_peak_reset_ms = millis();
/*
Аналоговый датчик не подтверждает наличие —
считаем инициализацию всегда успешной.
*/
_online = true;
return true;
}
/* ====================================================
UPDATE
==================================================== */
void Max4466Mic::update() {
uint32_t now_ms = millis();
if (now_ms - _last_read_ms < _config.read_interval_ms) {
return;
}
_last_read_ms = now_ms;
/* Снимаем RMS */
float rms_mv = _measure_rms_mv();
/*
Если RMS слишком мал — скорее всего пин не подключён
или питание отсутствует. Но мы не уходим в offline —
просто применяем noise floor.
*/
_rms_mv = rms_mv;
float db = _rms_to_db(rms_mv);
_db_raw = db;
_update_filtered_db(db);
/* Пик */
if (db > _db_peak) {
_db_peak = db;
}
/* Сброс пика по таймеру */
if (_config.peak_reset_ms > 0 &&
(now_ms - _last_peak_reset_ms) >= _config.peak_reset_ms) {
_db_peak = db;
_last_peak_reset_ms = now_ms;
}
_has_valid_data = true;
_last_success_read_ms = now_ms;
_tick_db_avg(now_ms);
_tick_trend(now_ms);
}
/* ====================================================
СТАТУС
==================================================== */
bool Max4466Mic::is_stale() const {
if (!_has_valid_data) return true;
return (millis() - _last_success_read_ms) > _config.stale_after_ms;
}
void Max4466Mic::reset_peak() {
_db_peak = _db_filtered;
_last_peak_reset_ms = millis();
}
/* ====================================================
ЗАМЕР RMS
==================================================== */
float Max4466Mic::_measure_rms_mv() {
/*
Алгоритм:
1. Снять N отсчётов
2. Вычислить DC-смещение (среднее)
3. Вычислить дисперсию (RMS переменной составляющей)
MAX4466 имеет выход с DC-смещением ~VCC/2 (≈ 1650 мВ при 3.3В).
Нас интересует только переменная составляющая — амплитуда звука.
*/
uint32_t sum = 0;
uint16_t n = _config.sample_count;
/* Буфер на стеке — не больше 512 отсчётов во избежание переполнения */
if (n > 512) n = 512;
uint16_t buf[512];
for (uint16_t i = 0; i < n; i++) {
buf[i] = (uint16_t)analogRead(_config.adc_pin);
sum += buf[i];
if (_config.sample_interval_us > 0) {
delayMicroseconds(_config.sample_interval_us);
}
}
/* DC offset */
float dc = (float)sum / (float)n;
/* RMS переменной составляющей */
float sum_sq = 0.0f;
for (uint16_t i = 0; i < n; i++) {
float diff = (float)buf[i] - dc;
sum_sq += diff * diff;
}
float rms_lsb = sqrtf(sum_sq / (float)n);
/* Перевод LSB → мВ */
float mv_per_lsb = _config.adc_vref_mv / (float)_config.adc_max_value;
return rms_lsb * mv_per_lsb;
}
/* ====================================================
RMS → dB
==================================================== */
float Max4466Mic::_rms_to_db(float rms_mv) const {
/*
dB = 20 * log10(rms_mv / ref_mv)
ref_mv = db_ref_mv из конфига.
Если rms_mv ниже noise floor в абсолютных единицах —
возвращаем db_noise_floor, чтобы не было -inf и артефактов.
*/
if (rms_mv < 1e-6f) {
return _config.db_noise_floor;
}
float db = 20.0f * log10f(rms_mv / _config.db_ref_mv);
if (db < _config.db_noise_floor) {
db = _config.db_noise_floor;
}
return db;
}
/* ====================================================
EMA-ФИЛЬТР dB
==================================================== */
void Max4466Mic::_update_filtered_db(float db) {
if (!_has_valid_data) {
_db_filtered = db;
return;
}
_db_filtered =
(_db_filtered * (1.0f - _config.db_ema_alpha)) +
(db * _config.db_ema_alpha);
}
/* ====================================================
СЕКУНДНОЕ УСРЕДНЕНИЕ
==================================================== */
void Max4466Mic::_tick_db_avg(uint32_t now_ms) {
if (_db_avg_last_ms == 0) { _db_avg_last_ms = now_ms; return; }
if (now_ms - _db_avg_last_ms < 1000) return;
_db_avg_last_ms = now_ms;
uint8_t win = _config.db_avg_window_s;
_db_avg_buf[_db_avg_idx] = _db_filtered;
_db_avg_idx = (_db_avg_idx + 1) % win;
if (_db_avg_count < win) _db_avg_count++;
float sum = 0.0f;
for (uint8_t i = 0; i < _db_avg_count; i++) sum += _db_avg_buf[i];
_db_avg = sum / (float)_db_avg_count;
}
/* ====================================================
ТРЕНД ПО МИНУТАМ
==================================================== */
void Max4466Mic::_tick_trend(uint32_t now_ms) {
if (_trend_last_ms == 0) { _trend_last_ms = now_ms; return; }
if (now_ms - _trend_last_ms < 60000u) return;
_trend_last_ms = now_ms;
uint8_t win = _config.db_trend_window_min;
_trend_buf[_trend_idx] = _db_avg;
_trend_idx = (_trend_idx + 1) % win;
if (_trend_count < win) _trend_count++;
_compute_trend();
}
void Max4466Mic::_compute_trend() {
uint8_t n = _trend_count;
if (n < 2) { _trend_slope = 0; _trend_std_dev = 0.0f; return; }
uint8_t win = _config.db_trend_window_min;
float sx = 0, sy = 0, sxx = 0, sxy = 0, mean_y = 0;
for (uint8_t i = 0; i < n; i++) {
uint8_t idx = (uint8_t)((_trend_idx - n + i + win) % win);
float x = (float)i, y = _trend_buf[idx];
sx += x;
sy += y;
sxx += x * x;
sxy += x * y;
mean_y += y;
}
float fn = (float)n;
float denom = fn * sxx - sx * sx;
if (fabsf(denom) < 1e-6f) {
_trend_slope = 0;
} else {
/*
Порог наклона: 0.5 dB/мин.
Изменение меньше этого считается «постоянным».
*/
float slope = (fn * sxy - sx * sy) / denom;
_trend_slope = (slope > 0.5f) ? 1 : (slope < -0.5f) ? -1 : 0;
}
mean_y /= fn;
float var = 0.0f;
for (uint8_t i = 0; i < n; i++) {
uint8_t idx = (uint8_t)((_trend_idx - n + i + win) % win);
float d = _trend_buf[idx] - mean_y;
var += d * d;
}
_trend_std_dev = sqrtf(var / fn);
}
const char* Max4466Mic::get_noise_dynamics() const {
if (_trend_count < 2) return "constant";
if (_trend_std_dev > 3.0f && _trend_slope == 0) return "variable";
if (_trend_slope > 0) return "increasing";
if (_trend_slope < 0) return "decreasing";
return "constant";
}
/* ====================================================
NOISE LEVEL 0..10
==================================================== */
uint8_t Max4466Mic::get_noise_level() const {
if (!_has_valid_data) return 0;
float range = _config.db_scale_range_db;
if (range < 1.0f) range = 1.0f;
float normalized = (_db_avg - _config.db_noise_floor) / range;
if (normalized < 0.0f) normalized = 0.0f;
if (normalized > 1.0f) normalized = 1.0f;
return (uint8_t)(normalized * 10.0f + 0.5f);
}
/* ====================================================
JSON
==================================================== */
String Max4466Mic::get_state_json() const {
String json = "{";
json += "\"online\":" + String(_online ? "true" : "false") + ",";
json += "\"current_noise\":" + String((int)(_db_filtered + 0.5f)) + ",";
json += "\"noise_level\":" + String(get_noise_level()) + ",";
json += "\"noise_level_dbi\":" + String((int)(_db_avg + 0.5f)) + ",";
json += "\"noise_dynamics\":\"" + String(get_noise_dynamics()) + "\"";
json += "}";
return json;
}