feat(audio): AEC NLMS adaptatif (mode aec 2)
Repo State / repo-state (push) Failing after 12m17s

This commit is contained in:
clement
2026-06-19 22:53:12 +02:00
parent baed6f4290
commit 178931899a
3 changed files with 116 additions and 19 deletions
+100 -12
View File
@@ -49,12 +49,49 @@ static TaskHandle_t s_task = NULL;
static volatile int s_play_gain = 8; /* downlink : PCM mobile -> ecouteur */ static volatile int s_play_gain = 8; /* downlink : PCM mobile -> ecouteur */
static volatile int s_mic_gain = 12; /* uplink : micro -> mobile (filtre) */ static volatile int s_mic_gain = 12; /* uplink : micro -> mobile (filtre) */
static volatile bool s_mic_filter = true; /* passe-bande voix sur le micro */ static volatile bool s_mic_filter = true; /* passe-bande voix sur le micro */
static volatile bool s_aec_on = true; /* suppression d'echo (mic ducking) */ /* Mode anti-echo : 0=off, 1=suppression (ducking), 2=NLMS adaptatif. */
static volatile int s_aec_mode = 1;
/* Enveloppe du signal downlink (ce qu'on joue dans l'ecouteur) = reference /* Enveloppe du signal downlink (ce qu'on joue dans l'ecouteur) = reference
* d'echo, mise a jour par la lecture SCO, lue par la capture micro. */ * d'echo, mise a jour par la lecture SCO, lue par la capture micro. */
static volatile float s_dl_env = 0.0f; static volatile float s_dl_env = 0.0f;
/* --- AEC NLMS (mode 2) --- */
#define AEC_L 128 /* longueur du filtre (8 ms @ 16k) */
static float s_aec_w[AEC_L]; /* coeffs du filtre adaptatif */
static float s_aec_xh[AEC_L]; /* historique de la reference (circulaire) */
static int s_aec_xi = 0;
static volatile float s_aec_mu = 0.30f; /* pas d'adaptation NLMS */
static volatile int s_aec_delay = 320; /* retard de boucle (samples) — reglable */
static RingbufHandle_t s_aec_ref_rb = NULL; /* reference (downlink) -> AEC */
static float aec_nlms_step(float d, float x)
{
s_aec_xh[s_aec_xi] = x;
float y = 0.0f, norm = 1e-3f;
int idx = s_aec_xi;
for (int k = 0; k < AEC_L; k++) {
float xv = s_aec_xh[idx];
y += s_aec_w[k] * xv;
norm += xv * xv;
idx = (idx - 1) & (AEC_L - 1);
}
float e = d - y;
/* Double-parole : si le micro depasse nettement l'echo estime -> voix
* proche -> on GELE l'adaptation (sinon le filtre diverge). */
float ad = d < 0 ? -d : d, ay = y < 0 ? -y : y;
if (ad < 2.0f * ay + 200.0f) {
float mu = s_aec_mu / norm;
idx = s_aec_xi;
for (int k = 0; k < AEC_L; k++) {
s_aec_w[k] += mu * e * s_aec_xh[idx];
idx = (idx - 1) & (AEC_L - 1);
}
}
s_aec_xi = (s_aec_xi + 1) & (AEC_L - 1);
return e;
}
static inline int16_t sat16(int32_t v) static inline int16_t sat16(int32_t v)
{ {
if (v > 32767) return 32767; if (v > 32767) return 32767;
@@ -117,10 +154,13 @@ void audio_router_load_gains(void)
if (nvs_get_i32(h, "gspk", &v) == ESP_OK) s_play_gain = (int)v; if (nvs_get_i32(h, "gspk", &v) == ESP_OK) s_play_gain = (int)v;
if (nvs_get_i32(h, "gmic", &v) == ESP_OK) s_mic_gain = (int)v; if (nvs_get_i32(h, "gmic", &v) == ESP_OK) s_mic_gain = (int)v;
if (nvs_get_i32(h, "filt", &v) == ESP_OK) s_mic_filter = (v != 0); if (nvs_get_i32(h, "filt", &v) == ESP_OK) s_mic_filter = (v != 0);
if (nvs_get_i32(h, "aec", &v) == ESP_OK) s_aec_on = (v != 0); if (nvs_get_i32(h, "aec", &v) == ESP_OK) s_aec_mode = (int)v;
if (nvs_get_i32(h, "aecd", &v) == ESP_OK) s_aec_delay = (int)v;
if (nvs_get_i32(h, "aecmu",&v) == ESP_OK) s_aec_mu = (float)v / 100.0f;
nvs_close(h); nvs_close(h);
ESP_LOGI(TAG, "gains NVS: gspk=%d gmic=%d filt=%d aec=%d", ESP_LOGI(TAG, "gains NVS: gspk=%d gmic=%d filt=%d aec=%d aecd=%d aecmu=%.2f",
s_play_gain, s_mic_gain, (int)s_mic_filter, (int)s_aec_on); s_play_gain, s_mic_gain, (int)s_mic_filter, s_aec_mode,
s_aec_delay, s_aec_mu);
} }
void audio_router_set_play_gain(int g) void audio_router_set_play_gain(int g)
@@ -135,10 +175,22 @@ void audio_router_set_mic_filter(bool on)
{ {
s_mic_filter = on; ar_nvs_set_i32("filt", on ? 1 : 0); s_mic_filter = on; ar_nvs_set_i32("filt", on ? 1 : 0);
} }
void audio_router_set_aec(bool on) void audio_router_set_aec(int mode) /* 0=off 1=suppress 2=NLMS */
{ {
s_aec_on = on; ar_nvs_set_i32("aec", on ? 1 : 0); if (mode < 0 || mode > 2) return;
s_aec_mode = mode; ar_nvs_set_i32("aec", mode);
} }
void audio_router_set_aec_delay(int samples)
{
if (samples < 0 || samples > 4000) return;
s_aec_delay = samples; ar_nvs_set_i32("aecd", samples);
}
void audio_router_set_aec_mu(int mu_pct) /* mu en % (ex. 30 -> 0.30) */
{
if (mu_pct < 1 || mu_pct > 100) return;
s_aec_mu = (float)mu_pct / 100.0f; ar_nvs_set_i32("aecmu", mu_pct);
}
int audio_router_get_aec(void) { return s_aec_mode; }
int audio_router_get_play_gain(void) { return s_play_gain; } int audio_router_get_play_gain(void) { return s_play_gain; }
int audio_router_get_mic_gain(void) { return s_mic_gain; } int audio_router_get_mic_gain(void) { return s_mic_gain; }
@@ -197,6 +249,13 @@ static void tone_task(void *a)
if ((float)dl_peak > env) env = (float)dl_peak; if ((float)dl_peak > env) env = (float)dl_peak;
s_dl_env = env; s_dl_env = env;
/* Reference AEC : pousser le signal downlink joue (pre-gain),
* draine 1:1 par la capture micro -> alignement reference/micro. */
if (s_aec_mode == 2 && s_aec_ref_rb) {
xRingbufferSend(s_aec_ref_rb, sco_acc,
(size_t)target * sizeof(int16_t), 0);
}
/* Frame complete : mono -> stereo (CVSD double pour 8k->16k) */ /* Frame complete : mono -> stereo (CVSD double pour 8k->16k) */
int frames = 0; int frames = 0;
for (int i = 0; i < target && frames < FRAME; i++) { for (int i = 0; i < target && frames < FRAME; i++) {
@@ -308,11 +367,27 @@ static void sco_mic_task(void *a)
int n = hal_i2s_capture_read_frame(mono, FRAME, &e); int n = hal_i2s_capture_read_frame(mono, FRAME, &e);
if (n <= 0) { vTaskDelay(pdMS_TO_TICKS(5)); continue; } if (n <= 0) { vTaskDelay(pdMS_TO_TICKS(5)); continue; }
s_dbg_cap++; s_dbg_cap++;
/* 1) Filtre passe-bande voix (HP 300 Hz + LP 3400 Hz) + pic micro. */
/* 0) NLMS (mode 2) : recupere la frame de reference (downlink joue,
* alignee par le ring + le retard) et annule l'echo sur le micro brut. */
static float refbuf[FRAME];
if (s_aec_mode == 2 && s_aec_ref_rb) {
size_t got = 0;
int16_t *rp = (int16_t *)xRingbufferReceiveUpTo(s_aec_ref_rb, &got,
pdMS_TO_TICKS(25), (size_t)n * sizeof(int16_t));
int rn = rp ? (int)(got / sizeof(int16_t)) : 0;
for (int i = 0; i < n; i++) refbuf[i] = (i < rn) ? (float)rp[i] : 0.0f;
if (rp) vRingbufferReturnItem(s_aec_ref_rb, rp);
}
/* 1) (NLMS) + filtre passe-bande voix (HP 300 Hz + LP 3400 Hz) + pic. */
static float fbuf[FRAME]; static float fbuf[FRAME];
float ne_peak = 0.0f; float ne_peak = 0.0f;
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
float x = (float)mono[i]; float x = (float)mono[i];
if (s_aec_mode == 2) {
x = aec_nlms_step(x, refbuf[i]); /* annulation d'echo */
}
if (s_mic_filter) { if (s_mic_filter) {
x = biquad_run(&s_hp, x); x = biquad_run(&s_hp, x);
x = biquad_run(&s_lp, x); x = biquad_run(&s_lp, x);
@@ -321,13 +396,12 @@ static void sco_mic_task(void *a)
float a = x < 0 ? -x : x; float a = x < 0 ? -x : x;
if (a > ne_peak) ne_peak = a; if (a > ne_peak) ne_peak = a;
} }
/* 2) Suppression d'echo : si le correspondant parle (downlink actif) et /* 2) Suppression residuelle (modes 1 et 2) : duck le micro quand le
* que le micro n'est pas dominant (=echo, pas de voix proche), on duck * correspondant parle et que le micro n'est pas dominant. */
* le micro. Attaque rapide, relachement lent (commutation de voix). */
static float s_supp = 1.0f; static float s_supp = 1.0f;
float target = 1.0f; float target = 1.0f;
if (s_aec_on && s_dl_env > 800.0f && ne_peak < s_dl_env * 0.6f) { if (s_aec_mode >= 1 && s_dl_env > 800.0f && ne_peak < s_dl_env * 0.6f) {
target = 0.12f; /* atténuation forte de l'echo */ target = (s_aec_mode == 2) ? 0.4f : 0.12f; /* plus doux avec NLMS */
} }
if (target < s_supp) s_supp += (target - s_supp) * 0.6f; /* attaque rapide */ if (target < s_supp) s_supp += (target - s_supp) * 0.6f; /* attaque rapide */
else s_supp += (target - s_supp) * 0.05f; /* relâchement lent */ else s_supp += (target - s_supp) * 0.05f; /* relâchement lent */
@@ -356,6 +430,20 @@ void audio_router_sco_begin(bool msbc)
mic_dsp_init(); /* (re)calcule les coeffs du filtre + reset l'etat */ mic_dsp_init(); /* (re)calcule les coeffs du filtre + reset l'etat */
if (!s_sco_play_rb) s_sco_play_rb = xRingbufferCreate(SCO_RB_SIZE, RINGBUF_TYPE_BYTEBUF); if (!s_sco_play_rb) s_sco_play_rb = xRingbufferCreate(SCO_RB_SIZE, RINGBUF_TYPE_BYTEBUF);
if (!s_sco_cap_rb) s_sco_cap_rb = xRingbufferCreate(SCO_RB_SIZE, RINGBUF_TYPE_BYTEBUF); if (!s_sco_cap_rb) s_sco_cap_rb = xRingbufferCreate(SCO_RB_SIZE, RINGBUF_TYPE_BYTEBUF);
/* Ring de reference AEC + reset du filtre NLMS + preremplissage du retard. */
if (!s_aec_ref_rb) s_aec_ref_rb = xRingbufferCreate(8192, RINGBUF_TYPE_BYTEBUF);
memset(s_aec_w, 0, sizeof(s_aec_w));
memset(s_aec_xh, 0, sizeof(s_aec_xh));
s_aec_xi = 0;
if (s_aec_ref_rb) {
static int16_t zeros[512] = {0};
int d = s_aec_delay;
while (d > 0) {
int chunk = d > 512 ? 512 : d;
xRingbufferSend(s_aec_ref_rb, zeros, (size_t)chunk * sizeof(int16_t), 0);
d -= chunk;
}
}
hal_i2s_capture_begin(); hal_i2s_capture_begin();
hal_audio_pa_set(true); hal_audio_pa_set(true);
audio_router_init(); /* s'assure que tone_task existe */ audio_router_init(); /* s'assure que tone_task existe */
@@ -14,7 +14,10 @@ void audio_router_init(void);
void audio_router_set_play_gain(int g); /* downlink : ecouteur */ void audio_router_set_play_gain(int g); /* downlink : ecouteur */
void audio_router_set_mic_gain(int g); /* uplink : micro -> mobile */ void audio_router_set_mic_gain(int g); /* uplink : micro -> mobile */
void audio_router_set_mic_filter(bool on); void audio_router_set_mic_filter(bool on);
void audio_router_set_aec(bool on); /* suppression d'echo (mic ducking) */ void audio_router_set_aec(int mode); /* 0=off 1=suppression 2=NLMS */
void audio_router_set_aec_delay(int samples);
void audio_router_set_aec_mu(int mu_pct);
int audio_router_get_aec(void);
int audio_router_get_play_gain(void); int audio_router_get_play_gain(void);
int audio_router_get_mic_gain(void); int audio_router_get_mic_gain(void);
+12 -6
View File
@@ -27,15 +27,21 @@ static void handle(char *l)
int on = atoi(l + 4); int on = atoi(l + 4);
audio_router_set_mic_filter(on != 0); audio_router_set_mic_filter(on != 0);
ESP_LOGI(TAG, "filtre micro = %s", on ? "ON" : "OFF"); ESP_LOGI(TAG, "filtre micro = %s", on ? "ON" : "OFF");
} else if (strncmp(l, "aecd", 4) == 0) {
audio_router_set_aec_delay(atoi(l + 4));
ESP_LOGI(TAG, "AEC retard = %d samples", atoi(l + 4));
} else if (strncmp(l, "aecmu", 5) == 0) {
audio_router_set_aec_mu(atoi(l + 5));
ESP_LOGI(TAG, "AEC mu = %d%%", atoi(l + 5));
} else if (strncmp(l, "aec", 3) == 0) { } else if (strncmp(l, "aec", 3) == 0) {
int on = atoi(l + 3); audio_router_set_aec(atoi(l + 3));
audio_router_set_aec(on != 0); ESP_LOGI(TAG, "anti-echo = %d (0=off 1=suppress 2=NLMS)", audio_router_get_aec());
ESP_LOGI(TAG, "annulation d'echo = %s", on ? "ON" : "OFF");
} else if (strncmp(l, "status", 6) == 0) { } else if (strncmp(l, "status", 6) == 0) {
ESP_LOGI(TAG, "gspk=%d gmic=%d", ESP_LOGI(TAG, "gspk=%d gmic=%d aec=%d",
audio_router_get_play_gain(), audio_router_get_mic_gain()); audio_router_get_play_gain(), audio_router_get_mic_gain(),
audio_router_get_aec());
} else { } else {
ESP_LOGI(TAG, "cmds: gmic N | gspk N | filt 0/1 | aec 0/1 | status"); ESP_LOGI(TAG, "cmds: gmic N|gspk N|filt 0/1|aec 0/1/2|aecd N|aecmu N|status");
} }
} }