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