I2S for dummies

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
Post Reply
User avatar
mathieu
Posts: 88
Joined: Fri Nov 10, 2017 9:57 pm

I2S for dummies

Post by mathieu » Wed Oct 20, 2021 6:55 pm

For someone not really C-fluent, is there some documentation out there which explains in some detail the design/implementation of machine.I2S? I'm thinking of issues such as:
  • What really happens under the surface doing asyncio.StreamWriter.drain()?
  • Best practices re: when and how often to write data to an output buffer of a given size
Thanks for any pointers.

User avatar
Mike Teachman
Posts: 155
Joined: Mon Jun 13, 2016 3:19 pm
Location: Victoria, BC, Canada

Re: I2S for dummies

Post by Mike Teachman » Thu Oct 21, 2021 5:20 am

The top of each machine_i2s.c C file has some comments that might help to explain the implementation for each port. There is also a repo I authored with examples for the supported ports.
https://github.com/miketeachman/micropy ... s-examples

If you can frame up some specific questions I can add answers to a FAQ in the examples repo.

User avatar
mathieu
Posts: 88
Joined: Fri Nov 10, 2017 9:57 pm

Re: I2S for dummies

Post by mathieu » Thu Oct 21, 2021 8:45 am

Thanks Mike, I just read the source comments and still have a few questions. A FAQ would indeed be super useful.

Say I am in the ESP32 port, writing the contents of a bytearray buffer buf to an machine.I2S instance audio_out using asyncio:

Code: Select all

swriter = asyncio.StreamWriter(audio_out)
swriter.write(buf)
await swriter.drain()
I think (but I'm not sure) that what is happening here is: quickly copy the contents of buf to an "app buffer" belonging to the I2S instance; some kind of async process automatically feeds the contents of the app buffer to a memory region which is read automatically through DMA (or the DMA-accessed memory the same thing as the app buffer?) and transmitted out to the I2S device; finally, swriter.drain() returns when the app buffer is almost empty (define "almost"? is there are there low and high thresholds controlling the data flow?).

Is the above description correct?

Should I use an app buffer (ibuf argument in the I2S constructor I assume?) of the same length as my user-facing buf? Can I have buf larger than ibuf? Is it better to have buf be shorter, e.g., half of ibuf? How do i make this decision?

Is data typically sent to the I2S device synchronously with playback or can you do "bursts" of data at a higher rate?

Thanks again.

User avatar
Mike Teachman
Posts: 155
Joined: Mon Jun 13, 2016 3:19 pm
Location: Victoria, BC, Canada

Re: I2S for dummies

Post by Mike Teachman » Thu Oct 21, 2021 3:46 pm

Your description is good. The async process is run by the MicroPython uasyncio scheduler. It polls the I2S instance to determine when space is available in the internal buffer (ibuf). The polling happens in 'yield core._io_queue.queue_write(self.s)'. When space is available, all, or portions, of the user buffer are copied into the ibuf. If the ibuf becomes full during the copy, the uasyncio scheduler will resume polling, to wait until ibuf space becomes available. During that polling time other uasyncio tasks will be run. In the case of the ESP32 the DMA capable memory is the ibuf. The drain() method returns when the user buffer is completely emptied into ibuf.

In general the user facing buffer will be smaller than ibuf. The ibuf size depends on a number of factors, that I'll write about in the FAQ. It's interesting to think of ibuf as a large bucket of water, with a hole in the bottom that drains the bucket. The water streaming out of the bottom is analogous to the flow of audio samples going into the I2S hardware. That flow must be constant and at a fixed rate. The user facing buffer is like a small bucket that is used to fill the large bucket. The uasyncio scheduler is like a person using the small bucket, taking audio samples from a Wav file "lake" and filling the large bucket. If the large bucket becomes empty, the water stream stops, and audio stops playing. If the large bucket becomes full, the person might go do another task, and come back a bit later to see if there is more room in the large bucket. That is sort of how polling works in the uasyncio scheduler. When they return, if there is space in the large bucket, they will pour some more water (samples) into the large bucket.

I hope that helps for now. I'll make the example more complete in the I2S examples repo, complete with bucket graphics !

User avatar
mathieu
Posts: 88
Joined: Fri Nov 10, 2017 9:57 pm

Re: I2S for dummies

Post by mathieu » Fri Oct 22, 2021 6:53 am

Thanks, that's very clear.

This means that, with bits=16, format=I2S.MONO, rate = 16000, and ibuf = N, I should typically expect a delay up to N/2/16000 seconds between swriter.write(buf) and the corresponding audio being played, right?

User avatar
Mike Teachman
Posts: 155
Joined: Mon Jun 13, 2016 3:19 pm
Location: Victoria, BC, Canada

Re: I2S for dummies

Post by Mike Teachman » Thu Oct 28, 2021 12:54 am

The sound should start pretty much right after the first write() method completes. The ibuf does not need to be full for sound to be heard. But, after the final write() method sound will continue until the ibuf is emptied of sample data. That delay can be calculated by the formula you listed.

Post Reply