Understanding DSP code, specifically FillBuffer()

Hi,

I’ve been poking around the code posted on github, mainly concentrating on the clouds module hoping to learn a few things.

Things are going ok, there is one sticking point however, I can not figure out how FillBuffer() is called, sorry if this is a really dumb question, but any help understanding this would be appreciated.

FillBuffer is passed as an argument to the codec initialization routine. In drivers/codec.cc you’ll see that it is invoked in Codec::Fill, which is itself invoked whenever 50% of a DMA transfer on the I2S peripheral is completed.

Thanks for the quick reply!

Ok, I’ll look into this, think that’s enough to get me started.

Thanks again.

Apologies for necrobumping but I’ve been digging into some of this myself and this thread seemed like a good place to ask. FillBuffer() makes sense, but what I’m struggling with is making sense of the various buffers used (taking Clouds as an example).

Specifically, I’m having trouble drawing the connections between the workspace buffers allocated by GranularProcessor and the DMA buffers initialized by Codec — my understanding is that they are different buffers but at some point the contents of the workspace buffers will need to be transferred to the DMA tx/rx buffers and sent out to the codec. Where does this actually happen?

The workspace buffers in GranularProcessor are for recording audio, they don’t have anything to do with reading from/writing to the codec.

In short:

  • The DMA controller handles transfers from/to the codec, and this happens in the following buffers: Codec:: tx_dma_buffer_ and ```Codex:: rx_dma_buffer_````.
  • Whenever we start transmitting/receiving one half of the buffer, we call whatever function is pointed at FillBufferCallback and give it a pointer to the other half of the buffer, so that it is processed. That’s classic double buffering. Practically, FillBufferCallback points to clouds.cc's FillBuffer.
  • FillBuffer does a couple of things (handling the VU-meter, reading CVs), then passes these pointers to GranularProcessor::Process for the actual cloudsing.
  • At this point, the samples go through a fairly involved list of steps (stereo to mono mixing, mixing with the feedback signal, possibly some downsampling when the quality setting is set to half-sample-rate, granularization, diffusion, pitch-shifting, filtering, reverb). But we don’t really care about the original DMA buffers – we immediately read from them and convert the data to floats; and in the end whatever floats we have are converted back to integers and stuffed into the DMA buffers.

If the “buffer” you’re concerned with is the granularization buffer, look into GranularProcessor::ProcessGranular. You’ll see that the input samples are first recorded in the granular buffer (buffer_16_[i].WriteFade), and then, depending on the playback mode, various parts of this buffer are read in complicated ways (typically player_.Play(buffer_16_, parameters_, &output[0].l, size);).

3 Likes

Wish I had found this thread sooner! I was trying to wrap my head around exactly this. My case is a bit different, since I’m trying to use the onboard DAC, but it’s mostly the same.

I had managed to cobble together a “Hello DMA” project based on the small handful of tutorials I was able to find that fit the bill, and then went about wrapping up the DMA/DAC parts similar to the Clouds’ Codec class.

One key difference is I have been trying to use the DMA’s circular mode, though it appears to me that there’s no way to tell which address the DMA is currently reading from. DMA_GetCurrDataCounter() always returns 64 for me, which is the same as the buffer size I have set. Am I missing something, or am I simply better off using a double buffer instead?

Also, is there a reason for the codec Init() and Start() being separate functions? They seem to be called in direct succession.

Thanks for opening up all this code and packaging up the build environment, it’s really proving to be a valuable learning resource!

Sorry, I’ve never used DMA_GetCurrDataCounter(). Has the transfer been initiated?

The code is written in such a way that you can call Stop() and then Start() again with another callback. I think at some point I made use of this to use a different callback for the factory testing mode.