More DSP building blocks

2022-04-15T18:22:17.414295

I keep coming back to the idea that most projects are really built up from a composition of simple ideas and functions. As I continue to think about building software from a functional programming paradigm (but without really using functional programming, just stealing some of the good bits), the more powerful this approach feels particularly at the early stages of a project. Small functions, separation of concerns, limiting side effects. And then composing your applications by tying them all together.

As I continue my wagon circling of DSP, it's obvious to note that most audio programming is built up from a few simple operations.

  • adding a value to samples is adding a DC offset
  • multiplying a sample by a value is amplitude modulation
  • delay a sample in a ring buffer is the basis for many effects
  1. buffers shorter than 1ms act like IIR filters
  2. buffers between 1ms - 12ms act like comb filters, the haas effect, FIR filters, and more
  3. buffers longer than 12ms act like the traditional delay effect

And it is basically the combination of these things that is responsible for (maybe) all the traditional effects that popular music uses.

// c++ styled pseudo-code ring buffer for delaying samples

// initialize your buffer that will hold your Write Head indexes
// and the one that will hold the delayed samples
// you have an index and the number of samples you want to delay
std::vector<int> writeHeadBuffer;
std::vector<float> ringBuffer
int index, delaySize;

// to start, the buffers are resized and the index is set to 0
writeHeadBuffer.resize(blockSize);
ringBuffer.resize(blockSize);
index = 0;

// the write head buffer is then passed a block of samples
// this is the start of the processing
for (auto s = 0; s < numberOfSamples; ++ s)
{
    index = (index + 1) % delaySize;
    writeHeadBuffer[s] = index;
    // so you can imagine if the delaySize was 5 samples
    // this buffer would be filled with [1,2,3,4,5,1,2,3,4,5,etc]
}

// now as we process our original block of samples, we iterate through
for (auto s = 0; s < numberOfSamples; ++s)
{
    // we will write the newest sample to the ringBuffer
    // and read the oldest sample from the ringBuffer to our output samples

    // this will give us the write head index we want
    const auto writeIndex = writeHeadBuffer[s];
    // then we use that index to write our sample to our ringBuffer
    ringBuffer[writeIndex] = samples[s];

    // and get the oldest sample
    const auto readIndex = (writeIndex + 1) % delaySize;
    samples[s] += ringBuffer[readIndex];
}