Files
RTC_BL_PHONE/components/audio_router/audio_router.c
T
clement 8f722db9f4
Repo State / repo-state (push) Failing after 17s
feat(bt): gain numerique SCO downlink+uplink
2026-06-19 15:01:44 +02:00

276 lines
9.4 KiB
C

/*
* audio_router.c — aiguillage des tonalités vers l'I2S (écouteur).
* Adapté de plip_voice/main/tones.c : la génération de sinus est déléguée à
* tone_gen ; l'écriture I2S passe par hal_i2s_spk_handle().
* Cadences France Télécom (440 Hz) : DIAL continu, BUSY 500/500, RINGBACK 1500/3500.
*/
#include "audio_router.h"
#include "tone_gen.h"
#include "hal_i2s.h"
#include "board_config.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/ringbuf.h"
#include "driver/i2s_std.h"
#include "esp_log.h"
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#define TAG "audio_router"
typedef enum { TONE_NONE, TONE_DIAL, TONE_BUSY, TONE_RINGBACK, TONE_SCO } tone_mode_t;
#define SCO_RB_SIZE 4096 /* ~128 ms @ 16 kHz mono 16-bit */
static RingbufHandle_t s_sco_play_rb = NULL; /* PCM entrant -> écouteur (TX) */
static RingbufHandle_t s_sco_cap_rb = NULL; /* micro -> PCM sortant */
static volatile bool s_sco_active = false;
static volatile bool s_sco_msbc = true;
static TaskHandle_t s_sco_mic_task = NULL;
#define SR PLIP_SAMPLE_RATE /* 16000 — doit correspondre à hal_i2s */
#define FRAME 320 /* 20 ms @ 16 kHz */
#define TONE_AMPLITUDE 14000.0f
#define TONE_FREQ_HZ 440.0f
static volatile tone_mode_t s_mode = TONE_NONE;
static volatile bool s_tone_idle = true;
static TaskHandle_t s_task = NULL;
/* --- Gain numerique SCO (avec saturation) --- */
#define SCO_PLAY_GAIN 4 /* downlink : PCM mobile -> ecouteur */
#define SCO_MIC_GAIN 4 /* uplink : micro -> mobile */
static inline int16_t sat16(int32_t v)
{
if (v > 32767) return 32767;
if (v < -32768) return -32768;
return (int16_t)v;
}
/* --- Compteurs de diagnostic SCO (temporaires) --- */
static volatile uint32_t s_dbg_in_bytes = 0; /* PCM recu du BT (feed_playback) */
static volatile uint32_t s_dbg_cap = 0; /* frames micro capturees */
static void tone_task(void *a)
{
(void)a;
int16_t buf[FRAME * 2];
uint32_t ph = 0;
int64_t t = 0; /* ms écoulées dans l'état courant */
uint32_t sco_it = 0, sco_play = 0, sco_under = 0;
static int16_t sco_acc[FRAME]; /* accumulateur mono SCO */
int sco_acc_n = 0;
for (;;) {
tone_mode_t m = s_mode;
if (m == TONE_NONE) {
s_tone_idle = true;
vTaskDelay(pdMS_TO_TICKS(20));
t = 0;
ph = 0;
sco_acc_n = 0;
continue;
}
/* --- Mode SCO : pont PCM entrant → I2S TX (mono→stéréo) --- */
if (m == TONE_SCO) {
/* Le PCM SCO arrive en petits morceaux. On ACCUMULE une frame mono
* complete (target samples) avant d'ecrire l'I2S, sinon ecrire des
* bouts completes de silence rend l'audio robotique. En underrun
* reel, auto_clear de l'I2S envoie du silence tout seul. */
int target = s_sco_msbc ? FRAME : (FRAME / 2); /* mono samples/frame */
size_t need_bytes = (size_t)(target - sco_acc_n) * sizeof(int16_t);
size_t got_bytes = 0;
int16_t *in = (int16_t *)xRingbufferReceiveUpTo(s_sco_play_rb, &got_bytes,
pdMS_TO_TICKS(20), need_bytes);
if (in) {
int ns = (int)(got_bytes / sizeof(int16_t));
if (ns > target - sco_acc_n) ns = target - sco_acc_n;
memcpy(&sco_acc[sco_acc_n], in, (size_t)ns * sizeof(int16_t));
sco_acc_n += ns;
vRingbufferReturnItem(s_sco_play_rb, in);
}
if (sco_acc_n >= target) {
/* Frame complete : mono -> stereo (CVSD double pour 8k->16k) */
int frames = 0;
for (int i = 0; i < target && frames < FRAME; i++) {
int16_t v = sat16((int32_t)sco_acc[i] * SCO_PLAY_GAIN);
buf[frames * 2] = v; buf[frames * 2 + 1] = v; frames++;
if (!s_sco_msbc && frames < FRAME) {
buf[frames * 2] = v; buf[frames * 2 + 1] = v; frames++;
}
}
while (frames < FRAME) { buf[frames*2]=0; buf[frames*2+1]=0; frames++; }
size_t written = 0;
s_tone_idle = false;
esp_err_t wr = i2s_channel_write(hal_i2s_spk_handle(), buf, sizeof(buf),
&written, pdMS_TO_TICKS(50));
sco_acc_n = 0;
sco_play++;
if (++sco_it % 100 == 0) {
ESP_LOGI(TAG, "SCO diag: in=%u play=%u under=%u cap=%u wrote=%u wr=%s",
(unsigned)s_dbg_in_bytes, (unsigned)sco_play,
(unsigned)sco_under, (unsigned)s_dbg_cap,
(unsigned)written, esp_err_to_name(wr));
}
} else {
sco_under++; /* pas encore une frame complete */
}
continue;
}
bool on = true;
if (m == TONE_BUSY) {
on = (t % 1000) < 500;
} else if (m == TONE_RINGBACK) {
on = (t % 5000) < 1500;
}
/* TONE_DIAL : toujours on */
if (on) {
tone_gen_fill_stereo(buf, FRAME, &ph, TONE_FREQ_HZ, TONE_AMPLITUDE, SR);
} else {
memset(buf, 0, sizeof(buf));
ph += FRAME; /* garder la phase continue */
}
size_t written = 0;
s_tone_idle = false;
i2s_channel_write(hal_i2s_spk_handle(), buf, sizeof(buf), &written,
pdMS_TO_TICKS(50));
t += 20;
}
}
void audio_router_init(void)
{
if (!s_task) {
xTaskCreate(tone_task, "tones", 4096, NULL, 5, &s_task);
ESP_LOGI(TAG, "tone task created");
}
}
void audio_router_dialtone_start(void)
{
ESP_LOGI(TAG, "dial tone start");
hal_audio_pa_set(true);
audio_router_init();
s_mode = TONE_DIAL;
}
void audio_router_busy_start(void)
{
ESP_LOGI(TAG, "busy tone start");
hal_audio_pa_set(true);
audio_router_init();
s_mode = TONE_BUSY;
}
void audio_router_ringback_start(void)
{
ESP_LOGI(TAG, "ringback tone start");
hal_audio_pa_set(true);
audio_router_init();
s_mode = TONE_RINGBACK;
}
void audio_router_tones_stop(void)
{
ESP_LOGI(TAG, "tones stop");
s_mode = TONE_NONE;
const int max_wait_ms = 200;
int waited_ms = 0;
while (!s_tone_idle && waited_ms < max_wait_ms) {
vTaskDelay(pdMS_TO_TICKS(5));
waited_ms += 5;
}
if (waited_ms >= max_wait_ms) {
ESP_LOGW(TAG, "tones_stop: timeout waiting for tone task idle");
}
}
/* ------------------------------------------------------------------ */
/* SCO bridge — Phase 4 HFP */
/* ------------------------------------------------------------------ */
static void sco_mic_task(void *a)
{
(void)a;
int16_t mono[FRAME];
while (s_sco_active) {
int64_t e = 0;
int n = hal_i2s_capture_read_frame(mono, FRAME, &e);
if (n <= 0) { vTaskDelay(pdMS_TO_TICKS(5)); continue; }
s_dbg_cap++;
for (int i = 0; i < n; i++) mono[i] = sat16((int32_t)mono[i] * SCO_MIC_GAIN);
if (s_sco_msbc) {
/* 16 kHz direct : envoyer tel quel */
xRingbufferSend(s_sco_cap_rb, mono, (size_t)(n * (int)sizeof(int16_t)), 0);
} else {
/* 16k -> 8k : décimer 1 sur 2 */
int16_t dec[FRAME / 2];
int m = 0;
for (int i = 0; i < n; i += 2) dec[m++] = mono[i];
xRingbufferSend(s_sco_cap_rb, dec, (size_t)(m * (int)sizeof(int16_t)), 0);
}
}
s_sco_mic_task = NULL;
vTaskDelete(NULL);
}
void audio_router_sco_begin(bool msbc)
{
s_sco_msbc = msbc;
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);
hal_i2s_capture_begin();
hal_audio_pa_set(true);
audio_router_init(); /* s'assure que tone_task existe */
s_sco_active = true;
s_mode = TONE_SCO;
if (!s_sco_mic_task) {
xTaskCreatePinnedToCore(sco_mic_task, "sco_mic", 4096, NULL, 6, &s_sco_mic_task, 1);
}
ESP_LOGI(TAG, "SCO bridge ON (%s)", msbc ? "mSBC 16k" : "CVSD 8k");
}
void audio_router_sco_end(void)
{
s_mode = TONE_NONE; /* tone_task quitte la branche SCO d'abord */
s_sco_active = false; /* signale l'arret a sco_mic_task */
/* attendre que sco_mic_task se termine reellement (elle met s_sco_mic_task=NULL) */
int waited = 0;
while (s_sco_mic_task != NULL && waited < 200) {
vTaskDelay(pdMS_TO_TICKS(5));
waited += 5;
}
/* laisser tone_task finir une eventuelle ecriture I2S en cours avant de couper la PA */
vTaskDelay(pdMS_TO_TICKS(25));
hal_audio_pa_set(false);
hal_i2s_capture_end();
ESP_LOGI(TAG, "SCO bridge OFF");
}
void audio_router_sco_feed_playback(const uint8_t *pcm, uint32_t len)
{
if (s_sco_play_rb && s_sco_active) {
s_dbg_in_bytes += len;
xRingbufferSend(s_sco_play_rb, pcm, len, 0);
}
}
uint32_t audio_router_sco_take_capture(uint8_t *pcm, uint32_t len)
{
if (!s_sco_cap_rb) return 0;
size_t got = 0;
uint8_t *d = (uint8_t *)xRingbufferReceiveUpTo(s_sco_cap_rb, &got, 0, len);
if (d) {
memcpy(pcm, d, got);
vRingbufferReturnItem(s_sco_cap_rb, d);
return (uint32_t)got;
}
return 0;
}