176 lines
5.9 KiB
C
176 lines
5.9 KiB
C
// I2S capture: continuous 16 kHz mono read, builds 50%-overlapping frames
|
|
// (previous hop + current hop) and hands them to the processing task through a
|
|
// fixed pool of frame slots. No allocation after init.
|
|
#include <string.h>
|
|
#include "driver/i2s_std.h"
|
|
#include "esp_log.h"
|
|
#include "esp_task_wdt.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/queue.h"
|
|
#include "freertos/task.h"
|
|
#include "audio_in.h"
|
|
#include "board_config.h"
|
|
|
|
static const char *TAG = "audio";
|
|
|
|
#define NUM_SLOTS 8
|
|
#define SETTLE_HOPS 8 // discard the first hops: mic start-up transient
|
|
#define AUDIO_TASK_CORE 1
|
|
#define AUDIO_TASK_PRIO 10
|
|
#define AUDIO_TASK_STACK 4096
|
|
|
|
static int16_t s_slots[NUM_SLOTS][FRAME_LEN];
|
|
static QueueHandle_t s_free_q;
|
|
static QueueHandle_t s_ready_q;
|
|
static i2s_chan_handle_t s_rx;
|
|
static int32_t s_raw[HOP_LEN];
|
|
static int16_t s_hop_a[HOP_LEN], s_hop_b[HOP_LEN];
|
|
static volatile uint32_t s_captured, s_dropped;
|
|
static uint32_t s_seq;
|
|
|
|
esp_err_t audio_in_init(void)
|
|
{
|
|
s_free_q = xQueueCreate(NUM_SLOTS, sizeof(uint8_t));
|
|
s_ready_q = xQueueCreate(NUM_SLOTS, sizeof(uint8_t));
|
|
if (!s_free_q || !s_ready_q) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
for (uint8_t i = 0; i < NUM_SLOTS; i++) {
|
|
xQueueSend(s_free_q, &i, 0);
|
|
}
|
|
|
|
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
|
|
chan_cfg.dma_desc_num = 6;
|
|
chan_cfg.dma_frame_num = 256; // 6 x 256 x 4 B -> ~96 ms of DMA buffering
|
|
esp_err_t err = i2s_new_channel(&chan_cfg, NULL, &s_rx);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "i2s_new_channel: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
// INMP441: Philips format, 24-bit sample left-justified in a 32-bit slot.
|
|
i2s_std_config_t std_cfg = {
|
|
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(FS_HZ),
|
|
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO),
|
|
.gpio_cfg = {
|
|
.mclk = I2S_GPIO_UNUSED,
|
|
.bclk = I2S_BCLK_GPIO,
|
|
.ws = I2S_WS_GPIO,
|
|
.dout = I2S_GPIO_UNUSED,
|
|
.din = I2S_DATA_GPIO,
|
|
.invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false },
|
|
},
|
|
};
|
|
#if CONFIG_BUZZ_I2S_SLOT_RIGHT
|
|
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_RIGHT;
|
|
#else
|
|
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT;
|
|
#endif
|
|
err = i2s_channel_init_std_mode(s_rx, &std_cfg);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "i2s_channel_init_std_mode: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
err = i2s_channel_enable(s_rx);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "i2s_channel_enable: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
ESP_LOGI(TAG, "I2S RX up: %d Hz, BCLK=%d WS=%d DIN=%d, shift=%d",
|
|
FS_HZ, CONFIG_BUZZ_I2S_BCLK_GPIO, CONFIG_BUZZ_I2S_WS_GPIO,
|
|
CONFIG_BUZZ_I2S_DATA_GPIO, CONFIG_BUZZ_AUDIO_SHIFT);
|
|
return ESP_OK;
|
|
}
|
|
|
|
// 32-bit slot -> int16. The 24-bit sample sits in bits [31:8]; shifting right by 16
|
|
// would keep its top 16 bits (unity gain). CONFIG_BUZZ_AUDIO_SHIFT < 16 adds gain.
|
|
// Arithmetic shift keeps the sign; clamp protects against overflow with gain.
|
|
static inline int16_t to_i16(int32_t raw)
|
|
{
|
|
int32_t v = raw >> CONFIG_BUZZ_AUDIO_SHIFT;
|
|
if (v > INT16_MAX) v = INT16_MAX;
|
|
if (v < INT16_MIN) v = INT16_MIN;
|
|
return (int16_t)v;
|
|
}
|
|
|
|
static void audio_task(void *arg)
|
|
{
|
|
ESP_ERROR_CHECK(esp_task_wdt_add(NULL));
|
|
int16_t *prev = s_hop_a, *cur = s_hop_b;
|
|
uint32_t settle = SETTLE_HOPS;
|
|
bool have_prev = false;
|
|
uint32_t last_warn_ms = 0;
|
|
|
|
for (;;) {
|
|
esp_task_wdt_reset(); // if I2S stops delivering data the WDT reboots us
|
|
|
|
size_t need = sizeof(s_raw), total = 0;
|
|
while (total < need) {
|
|
size_t got = 0;
|
|
esp_err_t err = i2s_channel_read(s_rx, (uint8_t *)s_raw + total, need - total, &got, 200);
|
|
total += got;
|
|
if (err == ESP_ERR_TIMEOUT) {
|
|
ESP_LOGW(TAG, "I2S read timeout (mic wired?)");
|
|
} else if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "i2s_channel_read: %s", esp_err_to_name(err));
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
}
|
|
}
|
|
for (int i = 0; i < HOP_LEN; i++) {
|
|
cur[i] = to_i16(s_raw[i]);
|
|
}
|
|
|
|
if (settle > 0) {
|
|
settle--;
|
|
} else if (have_prev) {
|
|
uint8_t idx;
|
|
if (xQueueReceive(s_free_q, &idx, 0) == pdTRUE) {
|
|
memcpy(&s_slots[idx][0], prev, HOP_LEN * sizeof(int16_t));
|
|
memcpy(&s_slots[idx][HOP_LEN], cur, HOP_LEN * sizeof(int16_t));
|
|
if (xQueueSend(s_ready_q, &idx, 0) != pdTRUE) {
|
|
xQueueSend(s_free_q, &idx, 0);
|
|
s_dropped++;
|
|
} else {
|
|
s_captured++;
|
|
}
|
|
} else {
|
|
s_dropped++;
|
|
uint32_t now = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
|
if (now - last_warn_ms > 2000) {
|
|
last_warn_ms = now;
|
|
ESP_LOGW(TAG, "processing behind: dropped %lu frames so far", (unsigned long)s_dropped);
|
|
}
|
|
}
|
|
}
|
|
have_prev = true;
|
|
int16_t *t = prev; prev = cur; cur = t;
|
|
}
|
|
}
|
|
|
|
esp_err_t audio_in_start(void)
|
|
{
|
|
BaseType_t ok = xTaskCreatePinnedToCore(audio_task, "audio", AUDIO_TASK_STACK, NULL,
|
|
AUDIO_TASK_PRIO, NULL, AUDIO_TASK_CORE);
|
|
return ok == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
bool audio_in_get_frame(audio_frame_t *f, TickType_t wait)
|
|
{
|
|
uint8_t idx;
|
|
if (xQueueReceive(s_ready_q, &idx, wait) != pdTRUE) {
|
|
return false;
|
|
}
|
|
f->idx = idx;
|
|
f->pcm = s_slots[idx];
|
|
f->seq = s_seq++;
|
|
return true;
|
|
}
|
|
|
|
void audio_in_release_frame(uint8_t idx)
|
|
{
|
|
xQueueSend(s_free_q, &idx, 0);
|
|
}
|
|
|
|
uint32_t audio_in_frames_captured(void) { return s_captured; }
|
|
uint32_t audio_in_frames_dropped(void) { return s_dropped; }
|