What Is Wrong With My Delay Line?

I’ve been having a mental breakdown with this class.

#ifndef DELAY_LINE_H
#define DELAY_LINE_H

#include <vector>

class DelayLine {
public:
    DelayLine(int M, float g, int maxBlockSize);

    void write(const float* input, int blockSize);
    float* read(int delay, int blockSize);
    void process(float* block, int blockSize);

private:
    std::vector<float> buffer;
    std::vector<float> readBuffer;

    int bufferSize = 0;
    int writePosition = 0;

    int M = 0;       // delay length
    float g = 0.0f;  // feedback gain
};

#endif // DELAY_LINE_H

#include "DelayLine.h"
#include <cstring>
#include <cassert>

DelayLine::DelayLine(int M, float g, int maxBlockSize)
    : M(M), g(g)
{
    bufferSize = M + maxBlockSize + 1;
    buffer.resize(bufferSize, 0.0f);
    readBuffer.resize(maxBlockSize, 0.0f);
    writePosition = 0;
}

void DelayLine::write(const float* input, int blockSize) {
    for (int i = 0; i < blockSize; ++i) {
        int readPosition = writePosition - M;
        if (readPosition < 0) readPosition += bufferSize;

        float feedback = g * buffer[readPosition];
        buffer[writePosition] = input[i] + feedback;

        writePosition++;
        if (writePosition >= bufferSize) writePosition -= bufferSize;
    }
}

float* DelayLine::read(int tau, int blockSize) {
    assert(tau >= 0 && tau < bufferSize);

    int readPosition = writePosition - tau;
    if (readPosition < 0) readPosition += bufferSize;

    for (int i = 0; i < blockSize; ++i) {
        int index = readPosition + i;
        if (index >= bufferSize) index -= bufferSize;
        readBuffer[i] = buffer[index];
    }

    return readBuffer.data();
}

void DelayLine::process(float* block, int blockSize) {
    write(block, blockSize);
    float* delayed = read(M, blockSize);
    std::memcpy(block, delayed, sizeof(float) * blockSize);
}

I give each channel in the audio buffer its own delay line here.

void V6AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    // Use this method as the place to do any pre-playback
    // initialisation that you need..

    for (int ch = 0; ch < getTotalNumOutputChannels(); ++ch) {
        delayLines.emplace_back(std::make_unique<DelayLine>(23, 0.0f, samplesPerBlock));
        //RRSFilters.emplace_back(std::make_unique<RRSFilter>(95, 0.00024414062f, samplesPerBlock));
    }
}

And this is my process block.

    for (int ch = 0; ch < buffer.getNumChannels(); ++ch) {
        float* channelData = buffer.getWritePointer(ch);
        delayLines[ch]->process(channelData, buffer.getNumSamples()); // In-place processing
        //RRSFilters[ch]->process(channelData);
    }

I’ve been going through hell because there is goddamned jitter when I play the audio. So I have. to ask if I’m doing something wrong.

Why do you have two buffers? A delay line is usually just one buffer.

Why are read and write separate methods? You should be reading at the same time as writing.

What is tau?

It looks like you’re manually reimplementing % - have you considered using size_t and % to wrap your counters?

Here’s a more straightforward version

class DelayLine {
public:
    float process(float input) {
        auto output = buffer[read];
        buffer[write] = input;
        read = (read + 1) % buffer.size();
        write = (write + 1) % buffer.size();
        return output;
    }
private:
   std::vector<float> buffer;
   size_t read = 0;
   size_t write = 0;
}

This is processing in individual samples though?

Sure, but it sounds like you’re struggling with fundamentals. Try getting something that works before optimizing, writing unit tests, then verifying your optimizations work…

Either way, the data structures stay the same for block processing. You do not need a second buffer.

Load an impulse, which is a sound with a single value being 1.0 and the rest 0.0 and feed it into your delay line. Set a breakpoint after the first write and look at the contents of the delay line’s buffer.

You should see a 1.0 followed by zeros until the delay length, then a sample that is g, then more zeros, then a sample that is g*g, and so on. If you’re seeing that, the write part works (although you might want to verify wrapping around works too). If write is OK, do the same for read.

In other words, use a simple test signal and inspect what is actually happening.

If you’re just trying to have delay functional in your project so you can move on to the next phase of your project, the juce::dsp::DelayLine class is pretty easy to get working! But also, coming up with your own design and flushing out the bugs yourself is really rewarding.