Files
buzz_detector/main/audio_in.c
T
2026-10-13 19:47:46 -05:00

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; }