i2s timing questions

sb_espressif
Posts: 28
Joined: Fri Dec 08, 2023 3:04 am

i2s timing questions

Postby sb_espressif » Wed Jan 17, 2024 12:39 pm

Hi, I'm trying to wrap my head around a problem and am not even really sure how to formulate my questions, but here's a shot:

I have a task whose job it is to read input from a gpio. Then I have a second task whose job it is to play audio. I want the 'gpio task's' work to affect that of the playback task: so when, say, a user moves a slider, they hear audio playback in response. The gpio task affects the audio play back in realtime (lets say it adjusts pitch of the audio).

What I'm having trouble picturing is how to manage the timing between these two things. Some timing facts I know:
  • The GPIO task runs at 100hz - so I have new data from it every 10 ms.
  • The i2s channel is configured to run at 44.1 KHz
Right now both tasks just use dumb while(1) loops, and I have a queue onto which the GPIO task throws its data, and the audio task pulls from it and periodically sends samples to i2s, both from within these while loops. But this doesn't really seem so great to me - the audio playback is choppy and I can tell I have timing issues with this approach.

I think what I want to do is:
  • Measure my GPIO
  • Indicate this measurement somewhere (queue? What other mechanisms should I consider for this?)
  • When the audio task sees that new information is available, it grabs it, processes it, then sends out an audio buffer over i2s. This audio buffer should last juuuuust long enough so that it's finishing up right as a new GPIO value comes available. <---this is the part I don't understand how to manage.
  • When the audio task is near the end of its playback, it indicates its ready for new GPIO data?
I'm still learning esp-idf/freertos, so a lot of words are new to me (semaphores, queues, event managers), and I'm not really sure how to think about putting these parts together.

I'm curious if anyone could give me any pointers? I'm happy to do more learning, just not sure what direction to look in.

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

Re: i2s timing questions

Postby MicroController » Wed Jan 17, 2024 1:58 pm

It sounds like the I2S should be the high-priority, time-defining part in the process. I'd start from there: Let one task take/generate/process a block of input audio data, then push it to I2S. This processing must of course be faster than I2S will shift the data out and should block after the 'processing' of a block until the I2S can accept this new block of data, then repeat processing with the next block.
Before starting to process a new block, to determine how the audio data is to be processed/generated, the task just looks at the current state of the user input. There may be no need for a queue or any other inter-task communication object because all that matters is the one single state of user input at the instant a new block starts to be processed, and the audio-generating task should not have to wait for anything but the audio sink.
Using double-buffering, i.e. preparing one block while another block is being transferred by I2S, the worst-case lag/response time to user input is 2x the block's length, which you can choose accordingly. The time the task spends waiting/blocked between the end of processing a block and I2S becoming ready to accept it is a kind of time buffer which helps in compensating any jitter/fluctuations in processing time.

micron
Posts: 8
Joined: Thu Nov 09, 2023 7:09 pm

Re: i2s timing questions

Postby micron » Thu Jan 18, 2024 12:44 am

What is the source of the audio?

I have a project where I have an I2S microphone and an I2S amplifier. Between them is a storage area on a flash chip. Never mind the microphone part for now, so looking just at playback, I have a "recorder" task and an "amplifier" task. The amplifier task is higher priority than the recorder task.

The amplifier task has a queue associated with it. The recorder task will read a chunk of 4K samples from flash into a dynamically allocated buffer, and pass it in a queue message to the amplifier task that will then do the actual I2S output.

The recorder task will do this in a loop as fast as it can - as long as there is space available in the amplifier queue. When the I2S callback indicates completion of a chunk, the amplifier task frees the dynamic buffer that it was passed.

I guess you could also use static buffers to pass in each queue entry if you have the available RAM; but I think that could become more complicated trying to keep track of what buffers are free. With dynamic buffers it's... allocate - fill - send - forget! The receiving task is responsible to free it.

Watching it on a logic analyzer, I see the flash being accessed for as many entries as are in the amplifier queue very quickly, then a steady, evenly paced access, as entries in the amplifier queue become available. The intent is to prevent the I2S output being starved. I started with an 8 entry queue, but this appeared to be over-kill, so I found that 4 was enough.

At first, the audio was stored on a separate data-flash, and I could attain a sample rate of 96K, no problem. Then I added an option to use a raw partition in the esp32 system flash, and was only able to get to about 24K, even then it still sometimes get garbled if there's other system activity. If I want to be really safe, I'll use 16K sample rate.

I hope this helps.

***Edit***

Oh, by the way, using queues to send a message from one task to another is a technique from the earliest real-time kernels - so you could use a queue to send messages from the gpio task to your audio task telling when to play or stop. You can define what's the format of messages in the queue, then create a C struct for it.

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

Re: i2s timing questions

Postby MicroController » Thu Jan 18, 2024 8:43 am

micron wrote:
Thu Jan 18, 2024 12:44 am
The recorder task will read a chunk of 4K samples from flash into a dynamically allocated buffer, and pass it in a queue message to the amplifier task that will then do the actual I2S output.
You may want to look into FreeRTOS's stream buffers.

Who is online

Users browsing this forum: No registered users and 67 guests