How to create an analog delay?

Hi everyone :slight_smile:, I’m new to creating VST,
I’ve been banging my head for a long time, I can’t figure out how to create the effect of analog delays, I mean when the delay time is changed, like here
https://www.youtube.com/watch?v=avp0D5dOahk&t=4s

Could someone be able to help me out?

This is my circular buffer code.

class CircularBuffer {
    public:
        CircularBuffer() {
            readIndex = 0;
            writeIndex = 0;
        }

        void init(double size) {
            resize(size);
        }

        float read(int shiftLocations) {
            readIndex = writeIndex - shiftLocations;
            checkReadIndex();

            return buffer[readIndex];
        }

        void write(float data) {
            buffer[writeIndex] = data;

            writeIndex = writeIndex + 1;
            checkWriteIndex();
        }

    private:
        std::vector<float> buffer;
        int size, readIndex, writeIndex;

        void checkReadIndex() {
            if (readIndex < 0) readIndex = size + readIndex;
        }

        void checkWriteIndex() {
            if (writeIndex >= size) writeIndex = writeIndex - size;
        }

        void resize(int size) {
            this->size = size;
            buffer.resize(size, 0);
        }
};

Your delay time takes integer values. If you want it to change continuously, it needs to take fractional values between the integer ones. For that, you need an interpolation function that computes values between samples using adjacent samples. If you’re using the juce::dsp classes, there’s a DelayLine, with options for interpolation. I’d use Lagrange3rd in your case.

Here’s a starting point in case you’re not using the dsp classes. It’s a good idea to make your buffer size a power of 2, so you can use a bitmask for wrapping. You don’t need to store the read index, as it’s computed from the write index and the delay time. I’d also use a single function for write and read, to enforce an order: in your code, calling write before read would produce an unintended shift of 1 sample.

The read function here starts separating the integer and fractional parts of the read position, then it computes the interpolated output using a 4-point Hermite.

int nextPowerOf2 (float x)
{
    return 1 << std::max (0, int (std::ceil (std::log2 (x))));
}

class Delay
{
public:
    float process (float data, float delayInSamples)
    {
        index = index + 1 & mask;
        buffer[index] = data;
        return read (index - delayInSamples);
    }

    void resize (int newSize)
    {
        newSize = nextPowerOf2 (newSize);
        mask = newSize - 1;
        buffer.resize (newSize, 0);
    }

private:
    float read (float position) const
    {
        int i = int (std::floor (position));
        float f = position - i;

        // 4-point Hermite
        float xm1 = buffer[i - 1 & mask],
              x0  = buffer[i     & mask],
              x1  = buffer[i + 1 & mask],
              x2  = buffer[i + 2 & mask],
              a   = (x1 - xm1) * 0.5f,
              b   = x0 - x1,
              c   = a + b,
              d   = (x2 - x0) * 0.5f;

        return x0 + f * (a - f * (c + (1 - f) * (b + c + d)));
    }

    std::vector<float> buffer;
    int mask{}, index{};
};

This works sample-by-sample and on a single channel. It would need to be extended to work on multiple channels, to use blocks or vectorization, to have a single iterator controlling multiple lines, etc.

1 Like