Sunday, October 3, 2021

What Triggers Audio Processing?

Using the Teensy or Tympan for audio processing can be very exciting.  It's really fun to open up the example programs, compile them, and listen.  It's also pretty easy to look at the example code to see how the algorithm blocks are created and connected together.  Great!  But, what if you want to make your own algorithm?  That's when start to look a bit more critically at the examples.  Your first thought will likely be: "Wait.  How does any of this audio processing actually get called? How does this crazy structure work?!?"   Yes.  That's a good question.  Let's talk about it.

So Much is Hidden.  The essential problem is that nearly all of the audio plumbing is hidden so that its complexity doesn't scare people off.  For example, look at an extremely minimal audio processing example in the image below.  It instantiates the audio objects and creates the audio connections.  Then, you've got the traditional Arduino setup() and loop() functions.  Note that the loop() function is empty.  This program looks like it does nothing.  Yet, audio does flow.  The audio is made louder by the gain block applying 6 dB of gain.  But how?!?  I see no functions that call into the audio blocks!

What You Didn't Know That You Programmed.  The screenshot above shows the code that you know that you programmed.  There is also a whole bunch of code that you included, however, that you didn't know that you were invoking.  In effect, you programmed some very complex activities and you didn't even know it.

Going Down the Rabbit Hole.  The flow chart below tries to expose some of the hidden code.  This is a map that helps explain some of the hidden underground parts of the Teensy Audio Library (and Tympan Library). 

1) Code Shown in the Arduino Window.  The blocks in blue are the pieces of code that you know that you wrote.  This is the code shown in the Arduino IDE.  Here, you instantiate the audio classes and the audio connections.   Here, you write the Arduino setup() and loop() functions.  This is the part that we can all see and (usually) understand.  For the audio processing, the hidden magic gets invoked behind the scenes by the audio classes.  In particular, the AudioOutputI2S class is the most magical.

2) Audio Class Constructors.  As a bit of background, "I2S" is a communication system built into the Teensy processor that is purposely design to pass sound data (that's the 'S' in I2S) between the processor and the audio input/output hardware.  So, the AudioOutputI2S class handles the passing of audio data out from the processor to the audio output.  If you were to open the AudioOutputI2S class, you would see that its constructor calls its begin() method.  Looking in begin(), you'll see that it configures the I2S bus (which is logical) but it also configures the DMA and it attaches an interrupt to the DMA.  Huh?

DMA. Direct Memory Access is a special way of using memory.  You know how you can drive your car and listen to a podcast at the same time?  Your brain is able to handle certain tasks autonomously in the background without disturbing the foreground thoughts?  The processor has some of the same capabilities.  The processor can allow for certain regions of its built-in memory to be directly accessed by external devices.  In this case, DMA is configured so that the audio output system can read audio data directly from the processors memory without the processor having to respond to a request for each and every sample.  That's DMA.  It happens in the background.

3) Firing an Interrupt (ISR).  When the DMA is set up, it's pointed to a small region of memory.  The region holds a fixed number of audio samples.  Once the I2S bus is commanded to begin pumping data, it starts pulling data from the DMA.  Again, this happens in the background.  As the DMA empties, it'll get low on samples that remain.  In the DMA setup, the DMA has been configured to call a function (an interrupt service routine, ISR) to replenish the data in the DMA.  In the AudioOutputI2S begin() method, a specific function was attached as the ISR.  You didn't know it, but it was.  The ISR is right there in AudioOutputI2S.

Interrupting All Other Activities.  When the ISR is requested, the processor now has to take notice.  It is called an "interrupt" service routine because it interrupts whatever else is happening.  Whatever else the processor is doing (such as looping around in your Arduino loop() function) will be paused while the processor goes off and executes the ISR.  This interruption is done so that we can ensure there is fresh audio data placed into the DMA before the DMA fully empties and the audio stalls.

4) Execute the ISR.  Looking in AudioOutputISR, we see that the ISR copies previously-processed audio data into the DMA.  This keeps the DMA fed so that there are no hiccups in the audio.  Great.  The next thing that the ISR does is command update_all(), which starts the audio processing chain so that processed audio will be available the next time the DMA is running low.  This update_all() is the key.

Doing the Audio Processing.  The update_all() method lives in AudioStream.  AudioStream is the root (parent) class of every audio processing class that you might have instantiated in your Arduino code.  AudioStream has some static data members that, in effect, act as a central location for tracking every audio processing object that needs to be called.  Update_all() simply loops through the list of your audio objects and calls each one's update() method.  

Each Class's Update().  If you open up any audio class, you'll find that there is an update() method.  This is where all the audio processing is done.  Inside the update(), it pulls blocks of audio from its upstream connections and pushes audio blocks out to its downstream connections.  These upstream and downstream connections are known because of you created them in your Arduino code via those AudioConnection lines.

Summary.  So, that's how the audio processing happens on Teensy (and Tympan).  There is the code that you explicitly wrote and then there is all the code that comes along with the libraries, such as the class  AudioOutputI2S.  AudioOutputI2S sets up the I2S bus for passing data to the audio hardware and it sets up the DMA for feeding data to the I2S bus.  Whenever the DMA gets low on data, it fires an ISR.  The ISR refills the DMA and it launches the cascade of update()for all your audio objects.  Because it is interrupt driven, it all happens in the background...and that's why your Arduino code looks so empty.

Phew.  Good work, everyone.

1 comment:

  1. Hello.. Please can you help me.. I believe that the Arduino Nano RP2040 Connect could be as a USB Audio Device.. I however lack the ability to make the knowhow to bridge between the example PDM > PCM sketch and USB Audio device.. I just want to listen to the best quality USB audio that the board can produce as an example..

    ReplyDelete