495 lines
18 KiB
C
495 lines
18 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 "nvs.h"
|
|
|
|
#include <string.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <math.h>
|
|
|
|
#ifndef M_PI
|
|
#define M_PI 3.14159265358979323846
|
|
#endif
|
|
|
|
#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 (reglable a la volee, avec saturation) --- */
|
|
static volatile int s_play_gain = 8; /* downlink : PCM mobile -> ecouteur */
|
|
static volatile int s_mic_gain = 12; /* uplink : micro -> mobile (filtre) */
|
|
static volatile bool s_mic_filter = true; /* passe-bande voix sur le micro */
|
|
/* 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
|
|
* d'echo, mise a jour par la lecture SCO, lue par la capture micro. */
|
|
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)
|
|
{
|
|
if (v > 32767) return 32767;
|
|
if (v < -32768) return -32768;
|
|
return (int16_t)v;
|
|
}
|
|
|
|
/* --- Biquad (RBJ) : passe-haut + passe-bas en serie sur le micro --- */
|
|
typedef struct { float b0, b1, b2, a1, a2; float z1, z2; } biquad_t;
|
|
static biquad_t s_hp, s_lp; /* HP 300 Hz, LP 3400 Hz @ 16 kHz */
|
|
|
|
static void biquad_set(biquad_t *f, float fc, float fs, float q, bool highpass)
|
|
{
|
|
float w0 = 2.0f * (float)M_PI * fc / fs;
|
|
float c = cosf(w0), s = sinf(w0);
|
|
float alpha = s / (2.0f * q);
|
|
float a0;
|
|
if (highpass) {
|
|
f->b0 = (1.0f + c) / 2.0f; f->b1 = -(1.0f + c); f->b2 = (1.0f + c) / 2.0f;
|
|
} else {
|
|
f->b0 = (1.0f - c) / 2.0f; f->b1 = (1.0f - c); f->b2 = (1.0f - c) / 2.0f;
|
|
}
|
|
a0 = 1.0f + alpha;
|
|
f->b0 /= a0; f->b1 /= a0; f->b2 /= a0;
|
|
f->a1 = (-2.0f * c) / a0; f->a2 = (1.0f - alpha) / a0;
|
|
f->z1 = f->z2 = 0.0f;
|
|
}
|
|
|
|
static inline float biquad_run(biquad_t *f, float x) /* direct form II transposed */
|
|
{
|
|
float y = f->b0 * x + f->z1;
|
|
f->z1 = f->b1 * x - f->a1 * y + f->z2;
|
|
f->z2 = f->b2 * x - f->a2 * y;
|
|
return y;
|
|
}
|
|
|
|
static void mic_dsp_init(void)
|
|
{
|
|
biquad_set(&s_hp, 300.0f, 16000.0f, 0.707f, true);
|
|
biquad_set(&s_lp, 3400.0f, 16000.0f, 0.707f, false);
|
|
}
|
|
|
|
/* --- Persistance NVS des gains/filtre --- */
|
|
#define AR_NVS_NS "audio"
|
|
static void ar_nvs_set_i32(const char *key, int32_t v)
|
|
{
|
|
nvs_handle_t h;
|
|
if (nvs_open(AR_NVS_NS, NVS_READWRITE, &h) == ESP_OK) {
|
|
nvs_set_i32(h, key, v);
|
|
nvs_commit(h);
|
|
nvs_close(h);
|
|
}
|
|
}
|
|
|
|
void audio_router_load_gains(void)
|
|
{
|
|
nvs_handle_t h;
|
|
if (nvs_open(AR_NVS_NS, NVS_READONLY, &h) != ESP_OK) return;
|
|
int32_t 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, "filt", &v) == ESP_OK) s_mic_filter = (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);
|
|
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, s_aec_mode,
|
|
s_aec_delay, s_aec_mu);
|
|
}
|
|
|
|
void audio_router_set_play_gain(int g)
|
|
{
|
|
if (g >= 0 && g <= 32) { s_play_gain = g; ar_nvs_set_i32("gspk", g); }
|
|
}
|
|
void audio_router_set_mic_gain(int g)
|
|
{
|
|
if (g >= 0 && g <= 32) { s_mic_gain = g; ar_nvs_set_i32("gmic", g); }
|
|
}
|
|
void audio_router_set_mic_filter(bool on)
|
|
{
|
|
s_mic_filter = on; ar_nvs_set_i32("filt", on ? 1 : 0);
|
|
}
|
|
void audio_router_set_aec(int mode) /* 0=off 1=suppress 2=NLMS */
|
|
{
|
|
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_mic_gain(void) { return s_mic_gain; }
|
|
|
|
/* --- 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) {
|
|
/* Enveloppe downlink (reference d'echo) : pic de la frame, avec
|
|
* decroissance lente. Utilisee par la suppression d'echo micro. */
|
|
int dl_peak = 0;
|
|
for (int i = 0; i < target; i++) {
|
|
int a = sco_acc[i] < 0 ? -sco_acc[i] : sco_acc[i];
|
|
if (a > dl_peak) dl_peak = a;
|
|
}
|
|
float env = s_dl_env * 0.85f;
|
|
if ((float)dl_peak > env) env = (float)dl_peak;
|
|
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) */
|
|
int frames = 0;
|
|
for (int i = 0; i < target && frames < FRAME; i++) {
|
|
int16_t v = sat16((int32_t)sco_acc[i] * s_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++;
|
|
|
|
/* 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];
|
|
float ne_peak = 0.0f;
|
|
for (int i = 0; i < n; 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) {
|
|
x = biquad_run(&s_hp, x);
|
|
x = biquad_run(&s_lp, x);
|
|
}
|
|
fbuf[i] = x;
|
|
float a = x < 0 ? -x : x;
|
|
if (a > ne_peak) ne_peak = a;
|
|
}
|
|
/* 2) Suppression residuelle (modes 1 et 2) : duck le micro quand le
|
|
* correspondant parle et que le micro n'est pas dominant. */
|
|
static float s_supp = 1.0f;
|
|
float target = 1.0f;
|
|
if (s_aec_mode >= 1 && s_dl_env > 800.0f && ne_peak < s_dl_env * 0.6f) {
|
|
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 */
|
|
else s_supp += (target - s_supp) * 0.05f; /* relâchement lent */
|
|
/* 3) Gain + suppression. */
|
|
for (int i = 0; i < n; i++) {
|
|
mono[i] = sat16((int32_t)(fbuf[i] * s_supp * (float)s_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;
|
|
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_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_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;
|
|
}
|