| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990 |
- /*
- * SPDX-FileCopyrightText: 2026 DatanoiseTV
- *
- * SPDX-License-Identifier: Apache-2.0
- */
- /*
- * I2S audio driver for AES67 on ESP32-P4.
- *
- * Uses ESP-IDF I2S standard mode with DMA for audio capture and playback.
- * Ring buffers bridge between I2S DMA transfers and the RTP engine.
- * DMA buffers reside in internal SRAM; ring buffers may use PSRAM.
- */
- #include "aes67_audio.h"
- #include <string.h>
- #include "esp_log.h"
- #include "esp_heap_caps.h"
- #include "freertos/FreeRTOS.h"
- #include "freertos/task.h"
- #include "driver/i2s_std.h"
- #include "driver/i2s_common.h"
- #include "esp_timer.h"
- #include "esp_cache.h"
- #include "hal/i2s_ll.h"
- #include "hal/hal_utils.h"
- #include "soc/hp_sys_clkrst_struct.h"
- static const char *TAG = "aes67_audio";
- /* Kept for future capture (RX) task */
- /* I2S DMA: 2 descriptors at 16 frames (0.33ms) each.
- * ISR fires at 3000Hz. DMA ring latency = 0.33ms.
- * Stable at all ptimes (0.125ms to 4ms) with 100% utilization.
- * DMA_FRAME_NUM=6 (8000Hz ISR) causes 0.03% loss at the edge. */
- #define DMA_DESC_NUM 2
- #define DMA_FRAME_NUM 16
- struct aes67_audio_ctx {
- i2s_chan_handle_t tx_chan;
- i2s_chan_handle_t rx_chan;
- aes67_audio_config_t config;
- /* Ring buffers (PSRAM when use_psram is set) */
- int32_t *capture_buf;
- int32_t *playback_buf;
- uint32_t buf_size_frames;
- volatile uint32_t capture_wr;
- volatile uint32_t capture_rd;
- volatile uint32_t playback_wr;
- volatile uint32_t playback_rd;
- /* DMA staging buffers (always internal SRAM) */
- uint8_t *dma_rx_buf;
- uint8_t *dma_tx_buf;
- uint32_t dma_buf_size_bytes;
- /* Pre-allocated int16 conversion buffer for direct_write.
- * Avoids heap_caps_malloc/free on every write call. */
- int16_t *i2s_conv_buf;
- uint32_t i2s_conv_buf_samples;
- /* DMA completion semaphore - signaled by on_sent ISR callback */
- SemaphoreHandle_t dma_sem;
- /* Staging ring buffer: RTP RX writes int16 data here,
- * DMA ISR reads from here to fill DMA descriptors directly. */
- int16_t *staging_buf;
- uint32_t staging_size; /* Total size in int16 samples */
- volatile uint32_t staging_wr;
- volatile uint32_t staging_rd;
- volatile uint32_t staging_underruns; /* ISR couldn't fill full descriptor */
- volatile uint32_t staging_isr_count; /* Total ISR calls */
- volatile bool staging_ready; /* ISR reads only when true */
- /* Frame callback */
- aes67_audio_frame_cb_t frame_cb;
- void *frame_cb_user_data;
- /* I/O task */
- TaskHandle_t io_task;
- bool running;
- bool use_psram;
- /* Samples per frame period: sample_rate * packet_time_us / 1000000 */
- uint32_t frame_size;
- };
- /* --- Helpers --------------------------------------------------------------- */
- /* Convert word_length (bytes per sample) to I2S bit width enum.
- * Currently unused since we force 16-bit I2S for ES8311 compat,
- * but kept for future use with other codecs. */
- __attribute__((unused))
- static i2s_data_bit_width_t word_length_to_bits(uint8_t word_length)
- {
- switch (word_length) {
- case 2: return I2S_DATA_BIT_WIDTH_16BIT;
- case 3: return I2S_DATA_BIT_WIDTH_24BIT;
- case 4: return I2S_DATA_BIT_WIDTH_32BIT;
- default: return I2S_DATA_BIT_WIDTH_24BIT;
- }
- }
- /* In ESP32-P4 Philips I2S mode with 32-bit slot width, the DMA data
- * is already left-justified (MSB-first) in the 32-bit word. This means
- * the I2S DMA format matches our internal left-justified int32 format
- * directly. No bit shifting is needed regardless of the configured
- * data bit width (16/24/32), because the I2S hardware handles the
- * slot-to-wire packing. */
- /* Available frames in a ring buffer (single-producer / single-consumer safe) */
- static inline uint32_t ring_available(uint32_t wr, uint32_t rd, uint32_t capacity)
- {
- return (wr - rd) % capacity;
- }
- /* Free frames in a ring buffer */
- static inline uint32_t ring_free(uint32_t wr, uint32_t rd, uint32_t capacity)
- {
- /* Reserve one frame to distinguish full from empty */
- return capacity - 1 - ring_available(wr, rd, capacity);
- }
- /* --- DMA <-> Ring Buffer Conversion --------------------------------------- */
- /* Copy DMA buffer (32-bit slots) directly to capture ring buffer.
- * Both DMA and ring buffer use left-justified int32 format. */
- __attribute__((unused))
- static void dma_to_capture_ring(struct aes67_audio_ctx *ctx,
- const uint8_t *dma_buf,
- uint32_t frame_count)
- {
- const uint8_t ch = ctx->config.channels;
- const uint32_t cap = ctx->buf_size_frames;
- const int32_t *src = (const int32_t *)dma_buf;
- uint32_t wr = ctx->capture_wr;
- for (uint32_t f = 0; f < frame_count; f++) {
- uint32_t ring_idx = (wr % cap) * ch;
- for (uint8_t c = 0; c < ch; c++) {
- ctx->capture_buf[ring_idx + c] = src[f * ch + c];
- }
- wr++;
- }
- ctx->capture_wr = wr;
- }
- /* Read from playback ring buffer and copy to DMA TX buffer.
- * Both use left-justified int32 format - direct copy, no shifting. */
- __attribute__((unused))
- static void playback_ring_to_dma(struct aes67_audio_ctx *ctx,
- uint8_t *dma_buf,
- uint32_t frame_count)
- {
- const uint8_t ch = ctx->config.channels;
- const uint32_t cap = ctx->buf_size_frames;
- int32_t *dst = (int32_t *)dma_buf;
- uint32_t rd = ctx->playback_rd;
- uint32_t avail = ring_available(ctx->playback_wr, rd, cap);
- for (uint32_t f = 0; f < frame_count; f++) {
- if (f < avail) {
- uint32_t ring_idx = (rd % cap) * ch;
- for (uint8_t c = 0; c < ch; c++) {
- dst[f * ch + c] = ctx->playback_buf[ring_idx + c];
- }
- rd++;
- } else {
- for (uint8_t c = 0; c < ch; c++) {
- dst[f * ch + c] = 0;
- }
- }
- }
- ctx->playback_rd = rd;
- }
- /* --- DMA callbacks -------------------------------------------------------- */
- /* --- StreamBuffer-based ISR playback --- */
- /* The Ethernet hook writes variable-size packets (52-192 frames depending
- * on source ptime). The DMA ISR reads exactly 1536 bytes per fire
- * (one descriptor). A byte-stream buffer naturally handles variable
- * packet sizes by concatenating them until the ISR has a full descriptor. */
- static StreamBufferHandle_t s_isr_stream_buf = NULL;
- /* ISR diagnostics */
- static volatile uint32_t s_isr_fires = 0;
- static volatile uint32_t s_isr_full = 0;
- static volatile uint32_t s_isr_empty = 0;
- /* Write audio data to the stream buffer (called from Ethernet hook).
- * Handles any packet size -- multiple small packets accumulate until
- * the ISR reads a full DMA descriptor. */
- void aes67_audio_slot_write(const void *data, uint32_t len)
- {
- if (s_isr_stream_buf && len > 0) {
- xStreamBufferSend(s_isr_stream_buf, data, len, 0);
- }
- }
- /* Last frame for sample-hold on underrun (less audible than silence) */
- static int32_t s_last_frame[2] = {0, 0}; /* Stereo max */
- /* ISR callback: reads from the stream buffer to fill one DMA descriptor.
- * On underrun, uses sample-hold (repeat last frame) instead of silence
- * to mask the ~8% EMAC packet loss with minimal artifacts. */
- static IRAM_ATTR bool on_i2s_tx_sent_sbuf(i2s_chan_handle_t handle,
- i2s_event_data_t *event,
- void *user_ctx)
- {
- BaseType_t woken = pdFALSE;
- s_isr_fires++;
- if (s_isr_stream_buf) {
- size_t received = xStreamBufferReceiveFromISR(s_isr_stream_buf,
- event->dma_buf,
- event->size,
- &woken);
- if (received >= event->size) {
- s_isr_full++;
- /* Save last frame for sample-hold on next underrun */
- int32_t *samples = (int32_t *)event->dma_buf;
- uint32_t last_idx = (event->size / sizeof(int32_t)) - 2;
- s_last_frame[0] = samples[last_idx];
- s_last_frame[1] = samples[last_idx + 1];
- } else {
- s_isr_empty++;
- /* Sample-hold: fill with last known frame instead of silence.
- * Decays toward zero to avoid DC offset on stream stop. */
- int32_t *dst = (int32_t *)event->dma_buf;
- uint32_t n_samples = event->size / sizeof(int32_t);
- /* Fill any partial data gap */
- uint32_t start = received / sizeof(int32_t);
- for (uint32_t i = start; i < n_samples; i += 2) {
- dst[i] = s_last_frame[0];
- dst[i + 1] = s_last_frame[1];
- }
- /* Decay toward zero (fade over ~10 underruns = 10ms) */
- s_last_frame[0] = (s_last_frame[0] * 7) / 8;
- s_last_frame[1] = (s_last_frame[1] * 7) / 8;
- }
- } else {
- s_isr_empty++;
- memset(event->dma_buf, 0, event->size);
- }
- esp_cache_msync(event->dma_buf, event->size, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
- return woken == pdTRUE;
- }
- /* Log ISR stats -- only on first lock and on underruns */
- void aes67_audio_log_isr_stats(void)
- {
- static bool first_100_logged = false;
- static uint32_t prev_empty_total = 0;
- uint32_t fires = s_isr_fires;
- uint32_t full = s_isr_full;
- uint32_t empty = s_isr_empty;
- s_isr_fires = 0;
- s_isr_full = 0;
- s_isr_empty = 0;
- if (fires == 0) return;
- uint32_t pct = full * 100 / fires;
- /* Log once when first reaching 100% */
- if (!first_100_logged && pct == 100) {
- first_100_logged = true;
- ESP_LOGI("dma_isr", "locked: %lu Hz, 100%% utilization", (unsigned long)(fires / 2));
- }
- /* Log whenever there are underruns (after initial lock) */
- if (first_100_logged && empty > 0) {
- prev_empty_total += empty;
- ESP_LOGW("dma_isr", "underrun: %lu empty in %lu fires (%lu%%), total=%lu",
- (unsigned long)empty, (unsigned long)fires,
- (unsigned long)pct, (unsigned long)prev_empty_total);
- }
- }
- /* Initialize stream buffer for ISR playback */
- void aes67_audio_set_isr_stream_buf(aes67_audio_handle_t handle,
- StreamBufferHandle_t sbuf)
- {
- (void)handle;
- s_isr_stream_buf = xStreamBufferCreate(65536, 1);
- ESP_LOGI("aes67_audio", "ISR playback stream buffer: 64KB");
- }
- /* --- I2S RX Capture ISR (I2S -> AES67 TX source) --- */
- /* Capture stream buffer: RX DMA ISR writes here, TIC task reads */
- static StreamBufferHandle_t s_capture_stream_buf = NULL;
- /* RX DMA ISR: fires when a DMA descriptor finishes capturing.
- * Copies captured int32 samples to the capture stream buffer.
- * The TIC task reads from this buffer to feed RTP source streams. */
- static IRAM_ATTR bool on_i2s_rx_recv(i2s_chan_handle_t handle,
- i2s_event_data_t *event,
- void *user_ctx)
- {
- if (s_capture_stream_buf && event->size > 0) {
- /* Invalidate CPU cache: DMA wrote to memory, CPU cache may be stale */
- esp_cache_msync(event->dma_buf, event->size, ESP_CACHE_MSYNC_FLAG_DIR_M2C);
- BaseType_t woken = pdFALSE;
- xStreamBufferSendFromISR(s_capture_stream_buf, event->dma_buf,
- event->size, &woken);
- return woken == pdTRUE;
- }
- return false;
- }
- /* Get the capture stream buffer (called by TIC task to read captured audio) */
- StreamBufferHandle_t aes67_audio_get_capture_buf(aes67_audio_handle_t handle)
- {
- (void)handle;
- return s_capture_stream_buf;
- }
- /* Initialize capture stream buffer for I2S RX -> RTP TX path */
- void aes67_audio_init_capture(aes67_audio_handle_t handle)
- {
- (void)handle;
- s_capture_stream_buf = xStreamBufferCreate(65536, 1);
- ESP_LOGI("aes67_audio", "ISR capture stream buffer: 64KB");
- }
- /* Legacy staging ISR (unused, kept for reference) */
- __attribute__((unused))
- static IRAM_ATTR bool on_i2s_tx_sent(i2s_chan_handle_t handle,
- i2s_event_data_t *event,
- void *user_ctx)
- {
- struct aes67_audio_ctx *ctx = (struct aes67_audio_ctx *)user_ctx;
- int16_t *dma_buf = (int16_t *)event->dma_buf;
- uint32_t dma_samples = event->size / sizeof(int16_t);
- /* Don't read from staging until it has enough data */
- if (!ctx->staging_ready) {
- memset(dma_buf, 0, event->size);
- esp_cache_msync(event->dma_buf, event->size, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
- ctx->staging_isr_count++;
- return false;
- }
- /* Copy from staging ring to DMA buffer */
- uint32_t rd = ctx->staging_rd;
- uint32_t wr = ctx->staging_wr;
- uint32_t cap = ctx->staging_size;
- uint32_t avail = (wr >= rd) ? (wr - rd) : (cap - rd + wr);
- uint32_t to_copy = (avail < dma_samples) ? avail : dma_samples;
- uint32_t first = cap - (rd % cap);
- if (first > to_copy) first = to_copy;
- /* Fast copy from staging ring (internal SRAM, no cache issues) */
- memcpy(dma_buf, &ctx->staging_buf[rd % cap], first * sizeof(int16_t));
- if (to_copy > first) {
- memcpy(dma_buf + first, ctx->staging_buf,
- (to_copy - first) * sizeof(int16_t));
- }
- /* Zero-fill if not enough data (silence on underrun) */
- if (to_copy < dma_samples) {
- memset(dma_buf + to_copy, 0, (dma_samples - to_copy) * sizeof(int16_t));
- }
- ctx->staging_rd = (rd + to_copy) % cap;
- ctx->staging_isr_count++;
- if (to_copy < dma_samples) {
- ctx->staging_underruns++;
- }
- /* Flush CPU cache to memory so DMA controller sees the new data.
- * ESP32-P4 routes internal SRAM through L1 cache -- without this
- * sync, DMA reads stale data and produces crackling. */
- esp_cache_msync(event->dma_buf, event->size, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
- /* Notify playback task to refill staging buffer */
- BaseType_t woken = pdFALSE;
- xSemaphoreGiveFromISR(ctx->dma_sem, &woken);
- return woken == pdTRUE;
- }
- /* --- Public API ----------------------------------------------------------- */
- esp_err_t aes67_audio_init(const aes67_audio_config_t *audio_config,
- const aes67_i2s_pins_t *pins,
- bool use_psram,
- uint16_t ring_buf_ms,
- aes67_audio_handle_t *handle)
- {
- if (!audio_config || !pins || !handle) {
- return ESP_ERR_INVALID_ARG;
- }
- struct aes67_audio_ctx *ctx = calloc(1, sizeof(*ctx));
- if (!ctx) {
- ESP_LOGE(TAG, "Failed to allocate audio context");
- return ESP_ERR_NO_MEM;
- }
- memcpy(&ctx->config, audio_config, sizeof(aes67_audio_config_t));
- ctx->use_psram = use_psram;
- /* Compute frame size: samples per packet period */
- ctx->frame_size = (uint32_t)audio_config->sample_rate *
- audio_config->packet_time_us / 1000000;
- if (ctx->frame_size == 0) {
- ESP_LOGE(TAG, "Invalid sample_rate/packet_time combination");
- free(ctx);
- return ESP_ERR_INVALID_ARG;
- }
- ESP_LOGI(TAG, "Init: %lu Hz, %u ch, %u bytes/sample, frame_size=%lu",
- (unsigned long)audio_config->sample_rate,
- audio_config->channels,
- audio_config->word_length,
- (unsigned long)ctx->frame_size);
- /* --- Allocate ring buffers --- */
- uint32_t ring_frames = (audio_config->sample_rate * ring_buf_ms) / 1000;
- if (ring_frames < ctx->frame_size * 4) {
- ring_frames = ctx->frame_size * 4;
- }
- /* Add 1 for the sentinel frame (full/empty disambiguation) */
- ring_frames += 1;
- ctx->buf_size_frames = ring_frames;
- size_t ring_bytes = ring_frames * audio_config->channels * sizeof(int32_t);
- uint32_t alloc_caps = use_psram ? MALLOC_CAP_SPIRAM : MALLOC_CAP_DEFAULT;
- ctx->capture_buf = heap_caps_calloc(1, ring_bytes, alloc_caps);
- ctx->playback_buf = heap_caps_calloc(1, ring_bytes, alloc_caps);
- if (!ctx->capture_buf || !ctx->playback_buf) {
- ESP_LOGE(TAG, "Failed to allocate ring buffers (%u bytes each)", (unsigned)ring_bytes);
- goto err_free_bufs;
- }
- /* --- Allocate DMA staging buffers (internal SRAM) --- */
- ctx->dma_buf_size_bytes = ctx->frame_size * audio_config->channels * sizeof(int32_t);
- ctx->dma_rx_buf = heap_caps_calloc(1, ctx->dma_buf_size_bytes,
- MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
- ctx->dma_tx_buf = heap_caps_calloc(1, ctx->dma_buf_size_bytes,
- MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
- if (!ctx->dma_rx_buf || !ctx->dma_tx_buf) {
- ESP_LOGE(TAG, "Failed to allocate DMA buffers (%lu bytes each)",
- (unsigned long)ctx->dma_buf_size_bytes);
- goto err_free_bufs;
- }
- /* --- Configure I2S channels --- */
- /* Official ESP-IDF es8311 example for ESP32-P4 uses I2S_NUM_0 */
- i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
- chan_cfg.dma_desc_num = DMA_DESC_NUM;
- chan_cfg.dma_frame_num = DMA_FRAME_NUM;
- /* When using ISR callback, auto_clear doesn't matter because the ISR
- * fills every descriptor (with data or silence). Set false for
- * i2s_channel_write compatibility in non-ISR modes. */
- chan_cfg.auto_clear = (s_isr_stream_buf != NULL);
- esp_err_t ret = i2s_new_channel(&chan_cfg, &ctx->tx_chan, &ctx->rx_chan);
- if (ret != ESP_OK) {
- ESP_LOGE(TAG, "i2s_new_channel failed: %s", esp_err_to_name(ret));
- goto err_free_bufs;
- }
- /* Map word_length to I2S bit depth */
- i2s_slot_mode_t slot_mode = I2S_SLOT_MODE_STEREO;
- /* 32-bit I2S: int32 maps directly to DMA, no conversion needed.
- * ES8311 supports 32-bit (SDP_IN_WL = 100). */
- i2s_std_config_t std_cfg = {
- .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(audio_config->sample_rate),
- .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, slot_mode),
- };
- /* Use APLL clock source for precise audio frequency control.
- * APLL has SDM (Sigma-Delta Modulator) for sub-ppm tuning.
- * ESP32-P4 Ethernet uses MPLL, so no APLL conflict. */
- std_cfg.clk_cfg.clk_src = I2S_CLK_SRC_APLL;
- std_cfg.clk_cfg.mclk_multiple = I2S_MCLK_MULTIPLE_384;
- std_cfg.gpio_cfg = (i2s_std_gpio_config_t){
- .mclk = pins->mck_gpio,
- .bclk = pins->bck_gpio,
- .ws = pins->ws_gpio,
- .dout = pins->dout_gpio,
- .din = pins->din_gpio,
- .invert_flags = {
- .mclk_inv = false,
- .bclk_inv = false,
- .ws_inv = false,
- },
- };
- ret = i2s_channel_init_std_mode(ctx->tx_chan, &std_cfg);
- if (ret != ESP_OK) {
- ESP_LOGE(TAG, "i2s_channel_init_std_mode TX failed: %s", esp_err_to_name(ret));
- goto err_del_channel;
- }
- ret = i2s_channel_init_std_mode(ctx->rx_chan, &std_cfg);
- if (ret != ESP_OK) {
- ESP_LOGE(TAG, "i2s_channel_init_std_mode RX failed: %s", esp_err_to_name(ret));
- goto err_del_channel;
- }
- ctx->running = false;
- ctx->capture_wr = 0;
- ctx->capture_rd = 0;
- ctx->playback_wr = 0;
- ctx->playback_rd = 0;
- /* Staging ring: RTP RX writes int16, DMA ISR reads.
- * 10 packets of headroom to absorb network jitter and
- * phase differences between packet arrival and DMA consumption. */
- ctx->staging_size = ctx->frame_size * 10 * audio_config->channels;
- ctx->staging_buf = heap_caps_calloc(ctx->staging_size, sizeof(int16_t),
- MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
- if (!ctx->staging_buf) {
- ESP_LOGE(TAG, "Failed to allocate staging ring");
- goto err_del_channel;
- }
- ctx->staging_wr = 0;
- ctx->staging_rd = 0;
- ctx->staging_ready = false;
- /* Binary semaphore: DMA ISR signals when descriptor consumed */
- ctx->dma_sem = xSemaphoreCreateBinary();
- if (!ctx->dma_sem) {
- ESP_LOGE(TAG, "Failed to create DMA semaphore");
- goto err_del_channel;
- }
- /* Pre-allocate int16 conversion buffer for direct_write.
- * Size for max batch: 25 packets worth of frames. */
- ctx->i2s_conv_buf_samples = ctx->frame_size * 25 * audio_config->channels;
- ctx->i2s_conv_buf = heap_caps_malloc(
- ctx->i2s_conv_buf_samples * sizeof(int16_t),
- MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
- if (!ctx->i2s_conv_buf) {
- ESP_LOGE(TAG, "Failed to allocate I2S conversion buffer (%lu bytes)",
- (unsigned long)(ctx->i2s_conv_buf_samples * sizeof(int16_t)));
- goto err_del_channel;
- }
- *handle = ctx;
- ESP_LOGI(TAG, "Audio driver initialized (ring=%lu frames, dma=%lu bytes)",
- (unsigned long)ring_frames, (unsigned long)ctx->dma_buf_size_bytes);
- return ESP_OK;
- err_del_channel:
- i2s_del_channel(ctx->tx_chan);
- i2s_del_channel(ctx->rx_chan);
- err_free_bufs:
- heap_caps_free(ctx->capture_buf);
- heap_caps_free(ctx->playback_buf);
- heap_caps_free(ctx->dma_rx_buf);
- heap_caps_free(ctx->dma_tx_buf);
- free(ctx);
- return ESP_ERR_NO_MEM;
- }
- esp_err_t aes67_audio_start(aes67_audio_handle_t handle)
- {
- if (!handle) {
- return ESP_ERR_INVALID_ARG;
- }
- if (handle->running) {
- ESP_LOGW(TAG, "Audio already running");
- return ESP_OK;
- }
- /* Register DMA ISR callback for direct stream buffer -> DMA transfer.
- * This bypasses i2s_channel_write and the playback task entirely. */
- /* Register TX DMA ISR (playback: stream buffer -> I2S) */
- if ((s_isr_stream_buf != NULL)) {
- i2s_event_callbacks_t tx_cbs = {
- .on_recv = NULL,
- .on_recv_q_ovf = NULL,
- .on_sent = on_i2s_tx_sent_sbuf,
- .on_send_q_ovf = NULL,
- };
- esp_err_t cb_ret = i2s_channel_register_event_callback(
- handle->tx_chan, &tx_cbs, handle);
- if (cb_ret != ESP_OK) {
- ESP_LOGW(TAG, "Failed to register TX ISR: %s", esp_err_to_name(cb_ret));
- } else {
- ESP_LOGI(TAG, "TX DMA ISR registered (playback)");
- }
- }
- /* Register RX DMA ISR (capture: I2S -> stream buffer) */
- if (s_capture_stream_buf != NULL) {
- i2s_event_callbacks_t rx_cbs = {
- .on_recv = on_i2s_rx_recv,
- .on_recv_q_ovf = NULL,
- .on_sent = NULL,
- .on_send_q_ovf = NULL,
- };
- esp_err_t cb_ret = i2s_channel_register_event_callback(
- handle->rx_chan, &rx_cbs, handle);
- if (cb_ret != ESP_OK) {
- ESP_LOGW(TAG, "Failed to register RX ISR: %s", esp_err_to_name(cb_ret));
- } else {
- ESP_LOGI(TAG, "RX DMA ISR registered (capture)");
- }
- }
- /* Enable both TX and RX channels. The ES8311 codec requires both
- * to be active (matching the official ESP-IDF es8311 example). */
- esp_err_t ret = i2s_channel_enable(handle->tx_chan);
- if (ret != ESP_OK) {
- ESP_LOGE(TAG, "Failed to enable TX channel: %s", esp_err_to_name(ret));
- return ret;
- }
- ret = i2s_channel_enable(handle->rx_chan);
- if (ret != ESP_OK) {
- ESP_LOGW(TAG, "Failed to enable RX channel: %s", esp_err_to_name(ret));
- /* Non-fatal: TX still works */
- }
- handle->running = true;
- /* Read and cache the clock divider set by i2s_channel_init_std_mode.
- * This gives us the correct base values for adaptive clock recovery. */
- extern void aes67_audio_read_clock_divider(aes67_audio_handle_t h);
- aes67_audio_read_clock_divider(handle);
- ESP_LOGI(TAG, "Audio started (DMA: %d desc x %d frames, DMA-paced)",
- DMA_DESC_NUM, DMA_FRAME_NUM);
- return ESP_OK;
- }
- esp_err_t aes67_audio_stop(aes67_audio_handle_t handle)
- {
- if (!handle) {
- return ESP_ERR_INVALID_ARG;
- }
- if (!handle->running) {
- return ESP_OK;
- }
- handle->running = false;
- /* Disable I2S channels */
- i2s_channel_disable(handle->tx_chan);
- i2s_channel_disable(handle->rx_chan);
- ESP_LOGI(TAG, "Audio stopped");
- return ESP_OK;
- }
- esp_err_t aes67_audio_destroy(aes67_audio_handle_t handle)
- {
- if (!handle) {
- return ESP_ERR_INVALID_ARG;
- }
- /* Stop if still running */
- if (handle->running) {
- aes67_audio_stop(handle);
- }
- /* Delete I2S channels */
- i2s_del_channel(handle->tx_chan);
- i2s_del_channel(handle->rx_chan);
- /* Free all buffers */
- heap_caps_free(handle->capture_buf);
- heap_caps_free(handle->playback_buf);
- heap_caps_free(handle->dma_rx_buf);
- heap_caps_free(handle->dma_tx_buf);
- heap_caps_free(handle->i2s_conv_buf);
- free(handle);
- ESP_LOGI(TAG, "Audio driver destroyed");
- return ESP_OK;
- }
- esp_err_t aes67_audio_register_frame_cb(aes67_audio_handle_t handle,
- aes67_audio_frame_cb_t cb,
- void *user_data)
- {
- if (!handle) {
- return ESP_ERR_INVALID_ARG;
- }
- handle->frame_cb = cb;
- handle->frame_cb_user_data = user_data;
- return ESP_OK;
- }
- esp_err_t aes67_audio_write_playback(aes67_audio_handle_t handle,
- const void *samples, uint32_t frame_count)
- {
- if (!handle || !samples) {
- return ESP_ERR_INVALID_ARG;
- }
- const int32_t *src = (const int32_t *)samples;
- const uint8_t ch = handle->config.channels;
- const uint32_t cap = handle->buf_size_frames;
- uint32_t free_frames = ring_free(handle->playback_wr, handle->playback_rd, cap);
- if (frame_count > free_frames) {
- /* Not enough room. Just overwrite - the I2S read task will
- * pick up the latest data. Don't touch playback_rd here as
- * that would race with the I2S I/O task reading it. */
- frame_count = free_frames;
- if (frame_count == 0) return ESP_OK;
- }
- uint32_t wr = handle->playback_wr;
- for (uint32_t f = 0; f < frame_count; f++) {
- uint32_t ring_idx = (wr % cap) * ch;
- for (uint8_t c = 0; c < ch; c++) {
- handle->playback_buf[ring_idx + c] = src[f * ch + c];
- }
- wr++;
- }
- handle->playback_wr = wr;
- return ESP_OK;
- }
- esp_err_t aes67_audio_read_capture(aes67_audio_handle_t handle,
- void *samples, uint32_t frame_count)
- {
- if (!handle || !samples) {
- return ESP_ERR_INVALID_ARG;
- }
- int32_t *dst = (int32_t *)samples;
- const uint8_t ch = handle->config.channels;
- const uint32_t cap = handle->buf_size_frames;
- uint32_t avail = ring_available(handle->capture_wr, handle->capture_rd, cap);
- if (frame_count > avail) {
- return ESP_ERR_NOT_FOUND;
- }
- uint32_t rd = handle->capture_rd;
- for (uint32_t f = 0; f < frame_count; f++) {
- uint32_t ring_idx = (rd % cap) * ch;
- for (uint8_t c = 0; c < ch; c++) {
- dst[f * ch + c] = handle->capture_buf[ring_idx + c];
- }
- rd++;
- }
- handle->capture_rd = rd;
- return ESP_OK;
- }
- esp_err_t aes67_audio_direct_write(aes67_audio_handle_t handle,
- const int32_t *samples, uint32_t frame_count)
- {
- if (!handle || !samples || frame_count == 0) {
- return ESP_ERR_INVALID_ARG;
- }
- uint32_t total_samples = frame_count * handle->config.channels;
- /* Clamp to pre-allocated buffer capacity */
- if (total_samples > handle->i2s_conv_buf_samples) {
- total_samples = handle->i2s_conv_buf_samples;
- frame_count = total_samples / handle->config.channels;
- }
- /* With 24-bit I2S (32-bit slots), write int32 directly to DMA.
- * The L24 conversion already produces left-justified int32.
- * No truncation needed -- full 24-bit quality preserved. */
- size_t bytes_written = 0;
- size_t bytes_to_write = total_samples * sizeof(int32_t);
- int64_t t0 = esp_timer_get_time();
- esp_err_t ret = i2s_channel_write(handle->tx_chan, samples, bytes_to_write,
- &bytes_written, portMAX_DELAY);
- int64_t t1 = esp_timer_get_time();
- /* Measure i2s_channel_write timing to find the throughput bottleneck */
- static int64_t total_write_us = 0;
- static int64_t total_between_us = 0;
- static int64_t last_end_us = 0;
- if (last_end_us > 0) {
- total_between_us += (t0 - last_end_us);
- }
- last_end_us = t1;
- total_write_us += (t1 - t0);
- /* Log first few writes with sample values for debugging format issues */
- static uint32_t write_log_count = 0;
- if (write_log_count < 3) {
- write_log_count++;
- ESP_LOGI(TAG, "I2S write #%lu: %u/%u bytes, frames=%lu, ret=%s, "
- "write=%lldus, between=%lldus | "
- "int32[0..3]: %+ld %+ld %+ld %+ld -> "
- "int16[0..3]: %+d %+d %+d %+d",
- (unsigned long)write_log_count,
- (unsigned)bytes_written, (unsigned)bytes_to_write,
- (unsigned long)frame_count, esp_err_to_name(ret),
- (long long)(t1 - t0),
- (long long)(last_end_us > 0 ? (t0 - (last_end_us - (t1 - t0))) : 0),
- (long)samples[0], (long)samples[1],
- (long)samples[2], (long)samples[3],
- (long)samples[0], (long)samples[1],
- (long)samples[2], (long)samples[3]);
- }
- return ret;
- }
- esp_err_t aes67_audio_get_config(aes67_audio_handle_t handle,
- aes67_audio_config_t *config)
- {
- if (!handle || !config) {
- return ESP_ERR_INVALID_ARG;
- }
- memcpy(config, &handle->config, sizeof(aes67_audio_config_t));
- return ESP_OK;
- }
- esp_err_t aes67_audio_get_buffer_levels(aes67_audio_handle_t handle,
- uint32_t *capture_frames,
- uint32_t *playback_frames)
- {
- if (!handle) {
- return ESP_ERR_INVALID_ARG;
- }
- if (capture_frames) {
- *capture_frames = ring_available(handle->capture_wr, handle->capture_rd,
- handle->buf_size_frames);
- }
- if (playback_frames) {
- *playback_frames = ring_available(handle->playback_wr, handle->playback_rd,
- handle->buf_size_frames);
- }
- return ESP_OK;
- }
- /* Write int16 stereo samples to the staging ring buffer.
- * Called by the playback task. The DMA ISR reads from this ring. */
- esp_err_t aes67_audio_staging_write(aes67_audio_handle_t handle,
- const int16_t *samples, uint32_t sample_count)
- {
- if (!handle || !samples) return ESP_ERR_INVALID_ARG;
- uint32_t cap = handle->staging_size;
- uint32_t wr = handle->staging_wr;
- uint32_t rd = handle->staging_rd;
- uint32_t avail = (wr >= rd) ? (wr - rd) : (cap - rd + wr);
- uint32_t free_samples = cap - 1 - avail; /* -1 for sentinel */
- if (sample_count > free_samples) {
- sample_count = free_samples;
- }
- if (sample_count == 0) return ESP_OK;
- uint32_t pos = wr % cap;
- uint32_t first = cap - pos;
- if (first > sample_count) first = sample_count;
- memcpy(&handle->staging_buf[pos], samples, first * sizeof(int16_t));
- if (sample_count > first) {
- memcpy(handle->staging_buf, samples + first,
- (sample_count - first) * sizeof(int16_t));
- }
- handle->staging_wr = (wr + sample_count) % cap;
- /* Enable ISR reads once staging is at least half full (20ms).
- * More headroom = more tolerance for network jitter. */
- if (!handle->staging_ready) {
- uint32_t new_wr = handle->staging_wr;
- uint32_t cur_rd = handle->staging_rd;
- uint32_t filled = (new_wr >= cur_rd) ? (new_wr - cur_rd) : (cap - cur_rd + new_wr);
- if (filled >= cap / 2) {
- handle->staging_ready = true;
- }
- }
- return ESP_OK;
- }
- void aes67_audio_get_staging_stats(aes67_audio_handle_t handle,
- uint32_t *isr_count, uint32_t *underruns)
- {
- if (handle) {
- if (isr_count) *isr_count = handle->staging_isr_count;
- if (underruns) *underruns = handle->staging_underruns;
- }
- }
- /* Cached base divider values (read from registers after I2S init) */
- static uint32_t base_div_n = 0;
- static uint32_t base_div_x = 0;
- static uint32_t base_div_y = 0;
- static uint32_t base_div_z = 0;
- static uint32_t base_div_yn1 = 0;
- static uint32_t base_numerator = 0;
- static uint32_t base_denominator = 0;
- /* Read and cache the I2S0 TX clock divider from hardware registers.
- * Must be called AFTER i2s_channel_init_std_mode(). */
- void aes67_audio_read_clock_divider(aes67_audio_handle_t handle)
- {
- (void)handle;
- base_div_n = HP_SYS_CLKRST.peri_clk_ctrl13.reg_i2s0_tx_div_n;
- base_div_x = HP_SYS_CLKRST.peri_clk_ctrl13.reg_i2s0_tx_div_x;
- base_div_y = HP_SYS_CLKRST.peri_clk_ctrl14.reg_i2s0_tx_div_y;
- base_div_z = HP_SYS_CLKRST.peri_clk_ctrl14.reg_i2s0_tx_div_z;
- base_div_yn1 = HP_SYS_CLKRST.peri_clk_ctrl14.reg_i2s0_tx_div_yn1;
- /* Reverse the x,y,z,yn1 -> numerator,denominator conversion */
- if (base_div_z > 0) {
- base_denominator = (base_div_x + 1) * base_div_z + base_div_y;
- base_numerator = base_div_yn1 ? (base_denominator - base_div_z) : base_div_z;
- }
- ESP_LOGI("aes67_audio", "I2S0 TX clk: N=%lu, x=%lu, y=%lu, z=%lu, yn1=%lu "
- "-> numer=%lu denom=%lu (divider=N+%lu/%lu)",
- (unsigned long)base_div_n, (unsigned long)base_div_x,
- (unsigned long)base_div_y, (unsigned long)base_div_z,
- (unsigned long)base_div_yn1,
- (unsigned long)base_numerator, (unsigned long)base_denominator,
- (unsigned long)base_numerator, (unsigned long)base_denominator);
- }
- /* Adjust the I2S TX MCLK divider at runtime to fine-tune sample rate.
- * This is used for adaptive clock recovery: speed up or slow down the
- * I2S clock to match the network source rate.
- * ppm_adj: parts-per-million adjustment. +100 = 0.01% faster. */
- void aes67_audio_adjust_clock_ppm(aes67_audio_handle_t handle, int32_t ppm_adj)
- {
- if (!handle || base_denominator == 0) return;
- /* Adjust numerator relative to the cached base values.
- * Increasing numerator = larger divider = slower clock.
- * ppm_adj > 0 means "source is faster, speed up I2S" = decrease numerator. */
- int64_t adj = (int64_t)ppm_adj * (int64_t)base_denominator / 1000000;
- int32_t new_numer = (int32_t)base_numerator - (int32_t)adj;
- if (new_numer < 1) new_numer = 1;
- if (new_numer > (int32_t)base_denominator - 1) new_numer = base_denominator - 1;
- /* Convert numerator/denominator back to x,y,z,yn1 format */
- uint32_t numer = (uint32_t)new_numer;
- uint32_t denom = base_denominator;
- uint32_t div_yn1 = numer * 2 > denom;
- uint32_t div_z = div_yn1 ? denom - numer : numer;
- uint32_t div_x = div_z ? (denom / div_z - 1) : 0;
- uint32_t div_y = div_z ? (denom % div_z) : 0;
- /* Write to I2S0 TX clock divider registers.
- * Workaround: set small div first, then target. */
- HAL_FORCE_MODIFY_U32_REG_FIELD(HP_SYS_CLKRST.peri_clk_ctrl13,
- reg_i2s0_tx_div_n, 2);
- HP_SYS_CLKRST.peri_clk_ctrl14.reg_i2s0_tx_div_yn1 = 0;
- HP_SYS_CLKRST.peri_clk_ctrl14.reg_i2s0_tx_div_y = 1;
- HP_SYS_CLKRST.peri_clk_ctrl14.reg_i2s0_tx_div_z = 0;
- HP_SYS_CLKRST.peri_clk_ctrl13.reg_i2s0_tx_div_x = 0;
- /* Set target */
- HP_SYS_CLKRST.peri_clk_ctrl14.reg_i2s0_tx_div_yn1 = div_yn1;
- HP_SYS_CLKRST.peri_clk_ctrl14.reg_i2s0_tx_div_z = div_z;
- HP_SYS_CLKRST.peri_clk_ctrl14.reg_i2s0_tx_div_y = div_y;
- HP_SYS_CLKRST.peri_clk_ctrl13.reg_i2s0_tx_div_x = div_x;
- HAL_FORCE_MODIFY_U32_REG_FIELD(HP_SYS_CLKRST.peri_clk_ctrl13,
- reg_i2s0_tx_div_n, base_div_n);
- }
- SemaphoreHandle_t aes67_audio_get_dma_sem(aes67_audio_handle_t handle)
- {
- return handle ? handle->dma_sem : NULL;
- }
- i2s_chan_handle_t aes67_audio_get_tx_chan(aes67_audio_handle_t handle)
- {
- return handle ? handle->tx_chan : NULL;
- }
|