Stable I2S callback??

robiwan
Posts: 29
Joined: Sat Dec 07, 2024 11:36 am

Stable I2S callback??

Postby robiwan » Tue Jan 07, 2025 5:57 pm

I'm trying to setup a callback mechanism for I2S where I play f.i. 128 samples @48000 Hz sample rate (i.e. ~2666 us per callback). I also use the "on_sent" callback to take a timestamp (with esp_timer_get_time), and then I've written out stats once each second.

But the result is quite disheartening:

Code: Select all

I (5332) i2s_cb: Min interval  (us): 0
I (5332) i2s_cb: Max interval  (us): 4610
I (5332) i2s_cb: Avg interval  (us): 2451
I (6282) i2s_cb: Min interval  (us): 0
I (6282) i2s_cb: Max interval  (us): 4609
I (6282) i2s_cb: Avg interval  (us): 2464
I (7222) i2s_cb: Min interval  (us): 0
I (7222) i2s_cb: Max interval  (us): 4609
I (7222) i2s_cb: Avg interval  (us): 2464
I (8172) i2s_cb: Min interval  (us): 0
I (8172) i2s_cb: Max interval  (us): 4610
I (8172) i2s_cb: Avg interval  (us): 2451
Basic code is:

Code: Select all

static bool IRAM_ATTR sent_callback(i2s_chan_handle_t handle,
                                    i2s_event_data_t* event,
                                    void* user_ctx)
{
    int64_t* p_ts = (int64_t*)(user_ctx);
    *p_ts         = (s_config.timestamp_fn)(); // This points to the esp_timer_get_time function...
    return false;
}

static void i2s_pdm_tx_task(void* args)
{
    const int buf_size_bytes =
        s_config.buffer_size * (s_config.stereo ? 2 : 1) * sizeof(int16_t);
    int16_t* w_buf = (int16_t*)calloc(1, buf_size_bytes);
    assert(w_buf);
    i2s_chan_handle_t tx_chan =
        i2s_init_pdm_tx(s_config.sample_rate, s_config.stereo);

    size_t w_bytes = 0;
    int64_t timestamp;
    int n                           = 0;
    i2s_event_callbacks_t callbacks = {NULL};
    callbacks.on_sent               = sent_callback;
    ESP_ERROR_CHECK(
        i2s_channel_register_event_callback(tx_chan, &callbacks, &timestamp));

    // Preload first data
    (s_config.i2s_callback_fn)(
        s_config.args, timestamp, w_buf, s_config.buffer_size);
    ESP_ERROR_CHECK(
        i2s_channel_preload_data(tx_chan, w_buf, buf_size_bytes, &w_bytes));

    ESP_ERROR_CHECK(i2s_channel_enable(tx_chan));

    while (1) {
        // Call the audio callback function
        (s_config.i2s_callback_fn)(
            s_config.args, timestamp, w_buf, s_config.buffer_size);
        // Write buffer to I2S DMA
        if (i2s_channel_write(tx_chan, w_buf, buf_size_bytes, &w_bytes, 1000) !=
                ESP_OK ||
            w_bytes != buf_size_bytes) {
            ESP_LOGE(TAG, "Write Task: i2s write failed");
            break;
        }
    }
    free(w_buf);
    vTaskDelete(NULL);
}
Any ideas on what goes wrong? I would suspect that the ISR callback function would be called quite close to when the DMA transmit is completed...?
Last edited by robiwan on Wed Jan 08, 2025 6:46 am, edited 1 time in total.

robiwan
Posts: 29
Joined: Sat Dec 07, 2024 11:36 am

Re: Stable I2S callback??

Postby robiwan » Tue Jan 07, 2025 8:45 pm

Ok, the number of DMA buffers and size seems to be set in i2s_alloc_dma_desc (i2s_common.c), although I cannot see where it is called from so I don't understand how it ends up being called with num=6 and bufsize=480. As I'm using 128 samples (16 bit) in the callback I hardcoded num=2 and bufsize=256:

Code: Select all

esp_err_t i2s_alloc_dma_desc(i2s_chan_handle_t handle, uint32_t num, uint32_t bufsize)
{
    I2S_NULL_POINTER_CHECK(TAG, handle);
    esp_err_t ret = ESP_OK;
    ESP_RETURN_ON_FALSE(bufsize <= I2S_DMA_BUFFER_MAX_SIZE, ESP_ERR_INVALID_ARG, TAG, "dma buffer can't be bigger than %d", I2S_DMA_BUFFER_MAX_SIZE);
    num = 2;
    bufsize = 256;
    handle->dma.desc_num = num;
    handle->dma.buf_size = bufsize;
    printf("-----------> num=%lu, bufsize=%lu\n", num, bufsize);
    ...
then, I get very stable timestamps.

Question 1: How can I specify DMA num + bufsize without hardcoding stuff?

Here's the interval stats for one second execution:

Code: Select all

I (3402) i2s_cb: Min interval  (us): 2457
I (3402) i2s_cb: Max interval  (us): 2458
I (3402) i2s_cb: Avg interval  (us): 2457
Nice. Albeit wrong. 128/48000 should be 2666 us. Playing a 440 Hz sine wave I can see on a frequency analyzer that the actual tone played is ~479 Hz, and 440 * 2666 / 2457 ~= 477. Hmm... the initialization is like so:

Code: Select all

static i2s_chan_handle_t i2s_init_pdm_tx(int sample_rate, bool stereo)
{
    i2s_chan_handle_t tx_chan;  // I2S tx channel handler
    /* Setp 1: Determine the I2S channel configuration and allocate TX channel
     * only The default configuration can be generated by the helper macro, it
     * only requires the I2S controller id and I2S role, but note that PDM
     * channel can only be registered on I2S_NUM_0 */
    i2s_chan_config_t tx_chan_cfg =
        I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
    tx_chan_cfg.auto_clear    = true;
    tx_chan_cfg.intr_priority = 7;
    ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_chan, NULL));

    /* Step 2: Setting the configurations of PDM TX mode and initialize the TX
     * channel The slot configuration and clock configuration can be generated
     * by the macros These two helper macros is defined in 'i2s_pdm.h' which can
     * only be used in PDM TX mode. They can help to specify the slot and clock
     * configurations for initialization or re-configuring */
    i2s_pdm_tx_config_t pdm_tx_cfg = {
        .clk_cfg = I2S_PDM_TX_CLK_DEFAULT_CONFIG(sample_rate),
        /* The data bit-width of PDM mode is fixed to 16 */
        .slot_cfg = I2S_PDM_TX_SLOT_DEFAULT_CONFIG(
            I2S_DATA_BIT_WIDTH_16BIT,
            (stereo ? I2S_SLOT_MODE_STEREO : I2S_SLOT_MODE_MONO)),
        .gpio_cfg =
            {
                .clk  = PDM_TX_CLK_IO,
                .dout = PDM_TX_DOUT_IO,
                .invert_flags =
                    {
                        .clk_inv = false,
                    },
            },
    };
    pdm_tx_cfg.slot_cfg.hp_en = false;
    ESP_ERROR_CHECK(i2s_channel_init_pdm_tx_mode(tx_chan, &pdm_tx_cfg));

    return tx_chan;
}
Question 2: Am I missing something here??

MicroController
Posts: 1955
Joined: Mon Oct 17, 2022 7:38 pm
Location: Europe, Germany

Re: Stable I2S callback??

Postby MicroController » Tue Jan 07, 2025 10:49 pm

robiwan wrote:
Tue Jan 07, 2025 5:57 pm
Any ideas on what goes wrong?
No, not at all.
Your code doesn't show how or what times/intervals you're actually trying to measure.
And the log output, which is fully unrelated to the code, indicates some kind of problem because...?

I understand that you want some sort of I2S callback but not use the driver's callback mechanism. Did I get that part right?

robiwan
Posts: 29
Joined: Sat Dec 07, 2024 11:36 am

Re: Stable I2S callback??

Postby robiwan » Wed Jan 08, 2025 5:55 am

Suffice to say, the "callback" part is described in the i2s_pdm_tx_task function I posted, and the stats are gathered/measured in that callback (which I did not post), but here it is for completeness:

Code: Select all

static void i2s_callback(void* args, int64_t timestamp, void* data, int size)
{
    sine_t* p = (sine_t*)(args);
    if (p->calculations < 10) {
        if (p->counter == 0) {
            p->first_time     = timestamp;
            p->min_interval   = INT64_MAX;
            p->max_interval   = 0;
            p->total_interval = 0;
        } else if (p->counter > 0) {
            // Calculate interval since the last message
            int64_t interval = timestamp - p->last_time;

            if (interval < p->min_interval)
                p->min_interval = interval;
            if (interval > p->max_interval)
                p->max_interval = interval;
            p->total_interval += interval;
        }
        p->last_time = timestamp;
        p->counter++;
        if (p->counter * size >= 48000) {
            int intervals_count  = p->counter - 1;
            int64_t avg_interval = p->total_interval / intervals_count;
            ESP_LOGI("i2s_cb", "Min interval  (us): %" PRId64, p->min_interval);
            ESP_LOGI("i2s_cb", "Max interval  (us): %" PRId64, p->max_interval);
            ESP_LOGI("i2s_cb", "Avg interval  (us): %" PRId64, avg_interval);
            p->calculations++;
        }
    }

    const float factor = 6.2831853072f / p->periodicity;
    int16_t* buf       = (int16_t*)(data);
    for (int i = 0; i < size; ++i) {
        const float v = sinf(p->pos * factor) * 16383.0f;
        buf[i]        = (int16_t)(v);
        p->pos        = (p->pos + 1) % p->periodicity;
    }
}

static int64_t IRAM_ATTR i2s_timestamp_callback(void)
{
    return esp_timer_get_time();
}
This being said, what do you mean with:
I understand that you want some sort of I2S callback but not use the driver's callback mechanism. Did I get that part right?
As far as I have seen, there is no callback mechanism. You fill a buffer, then call i2s_channel_write, and repeat.

Where is this driver callback mechanism you speak of? And how can I make sure that the DMA buffer sizes are of correct size?

TIA

robiwan
Posts: 29
Joined: Sat Dec 07, 2024 11:36 am

Re: Stable I2S callback??

Postby robiwan » Wed Jan 08, 2025 7:07 am

MicroController wrote:
Tue Jan 07, 2025 10:49 pm
And the log output, which is fully unrelated to the code, indicates some kind of problem because...?
Because of what I wrote in the follow-up post. The timing is completely off. It is ~2457 us where it should be ~2666 us (128/48000 s).

MicroController
Posts: 1955
Joined: Mon Oct 17, 2022 7:38 pm
Location: Europe, Germany

Re: Stable I2S callback??

Postby MicroController » Wed Jan 08, 2025 7:34 am

robiwan wrote:
Wed Jan 08, 2025 5:55 am
As far as I have seen, there is no callback mechanism. You fill a buffer, then call i2s_channel_write, and repeat.

Where is this driver callback mechanism you speak of?
Uhm...

Code: Select all

i2s_channel_register_event_callback(tx_chan, &callbacks, &timestamp)

robiwan
Posts: 29
Joined: Sat Dec 07, 2024 11:36 am

Re: Stable I2S callback??

Postby robiwan » Wed Jan 08, 2025 8:02 am

MicroController wrote:
Wed Jan 08, 2025 7:34 am
Uhm...

Code: Select all

i2s_channel_register_event_callback(tx_chan, &callbacks, &timestamp)
Yes, but that is the ISR level callback. Is there any example showing how to fill audio buffers using this callback? I haven't seen any.

MicroController
Posts: 1955
Joined: Mon Oct 17, 2022 7:38 pm
Location: Europe, Germany

Re: Stable I2S callback??

Postby MicroController » Wed Jan 08, 2025 8:18 am

To send or receive data asynchronously, callbacks can be registered by i2s_channel_register_event_callback(). Users are able to access the DMA buffer directly in the callback function instead of transmitting or receiving by the two blocking functions.

robiwan
Posts: 29
Joined: Sat Dec 07, 2024 11:36 am

Re: Stable I2S callback??

Postby robiwan » Wed Jan 08, 2025 8:21 am

Yes! I AM a noob wrt ESP32 :) So DMA num + size is in the i2s_chan_config_t, can't believe I missed that :D

Ok, so now I am good with the callback thingies That will just work.

robiwan
Posts: 29
Joined: Sat Dec 07, 2024 11:36 am

Re: Stable I2S callback??

Postby robiwan » Wed Jan 08, 2025 4:00 pm

MicroController wrote:
Wed Jan 08, 2025 8:18 am
To send or receive data asynchronously, callbacks can be registered by i2s_channel_register_event_callback(). Users are able to access the DMA buffer directly in the callback function instead of transmitting or receiving by the two blocking functions.
Yes, got this working, but I cannot even call math functions (like sinf), let alone any logging (total and absolute crash). Yeah I know, that's not something to be done in an audio callback anyway, but for now I need to be able to log stuff during development, so I'll go with the task based loop with the callback in the loop.

But still, the timing is off, with an interval between timestamps (taken in the I2S callback) of ~2457 us when it should be ~2666 us.

This I have no idea how to attack.

Who is online

Users browsing this forum: MicroController and 96 guests