A Simple Delay Line Class

DSP, Plugin and Host development discussion.
Post Reply New Topic
RELATED
PRODUCTS

Post

I haven't had a chance to test this code thoroughly yet but thought I'd post it anyway. It's a simple delay line class that can be used as a building block for delay based effects such as choruses, flangers, delays, etc.

There are two overloaded subscript operator methods. One is for reading the other for writing. The reading version takes a floating point index. This allows you to modulate reads using fractional indexes. It uses linear interpolation to calculate the results.

I don't like the int cast or the use of the modulo operator. If I could somehow restrict the delay line length to a power of two, I could use a mask instead of modulo. I'm not sure how to get around the int cast. I don't want to switch to a fixed point scheme, so I think I'm stuck with it.

Code: Select all

#ifndef DELAY_LINE_H
#define DELAY_LINE_H

#include <cassert>
#include <cstring>

template<int Length>
class DelayLine
{
private:
    float data[Length];

public:
    DelayLine()
    {
        Clear();
    }

    float operator[](float index) const
    {
        assert((index >= 0.0f) && (index < Length));

        int x1 = (int)index;
        int x2 = (x1 + 1) % Length;
        float y1 = data[x1];
        float y2 = data[x2];
        float f = index - x1;
    
        return (y2 - y1) * f + y1;
    }

    float &operator[](int index)
    {        
        assert((index >= 0) && (index < Length));

        return data[index];
    }

    void Clear()
    {
        memset(data, 0, sizeof(data));
    }

    int GetLength() const
    {
        return Length;
    }
};

#endif

Post

Thanks :tu:

Is the template argument just a way of avoiding a call to the constructor or some initialize() method? Never seen that, but clever.

Post

asomers wrote:Is the template argument just a way of avoiding a call to the constructor or some initialize() method? Never seen that, but clever.
It allows you to determine the delay line length at compile time, and it frees you from having to manage the memory for the delay line's buffer (as you would if you allocated the buffer on the heap).

It's a trade off, though, because you don't have the ability to resize it. The idea is that you create a delay line large enough to handle any size you may want to deal with.

Post

Thanks for sharing,

About the modulo, what you can do is that seeing as the template length is resolved at compile time replace the modulo with:

Code: Select all

bool isPowerOf2(int v){
return !(v & (v - 1)) && v;
}



...

// Instead of int x2 = (x1 + 1) % Length;
if (isPowerOf2(Length)) {
  int x2 = (x1 + 1) & (Length-1); 
}
else {
  int x2 = (x1<Length)?(x1 + 1):0 ; 
}
The isPowerOf2 and if/else statement is resolved at compile time so only the most efficient will be compiled depending on the 'Length' parameter

Sam

Post

sambean wrote: The isPowerOf2 and if/else statement is resolved at compile time so only the most efficient will be compiled depending on the 'Length' parameter
Nice!! :tu:

Post

Leslie Sanford wrote:It allows you to determine the delay line length at compile time
hey thanks Leslie. i have a similiar class that takes the (maximum) delayline length as constructor argument and then dynamically allocates the memory. how does this template based approach work? do i have to declare my delayline something like:

DelayLine<8192> myDelayLine;

for a delayline of length 8192 in the header of my - say - flanger? sorry, but i have never seen that technique before and don't use templates that much.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

Robin from www.rs-met.com wrote:how does this template based approach work? do i have to declare my delayline something like:

DelayLine<8192> myDelayLine;

for a delayline of length 8192 in the header of my - say - flanger?
Yup, that's the correct syntax.
sorry, but i have never seen that technique before and don't use templates that much.
Chris Walton mentioned using "non-types" as template arguments here awhile back and got me intrigued. I did some research and have become kind of hooked on them. You can do some pretty wild things with this that takes you into the area of "metaprogramming". I'm trying to use some of these techniques when they are helpful but not get too carried away to the point where the code is hard to read and/or maintain.

Post

to give you an idea, here's a simple example of using recursive templates with non-type parameters to compute pow with a fixed exponent. It will be automatically inlined by the compiler.

Code: Select all

template<unsinged int N> 
inline float pow(const float x)
{
    return x * pow<N-1>(x); // general case
}

template<> 
inline float pow<0>(const float x)
{
    return 1; // end case
}
you can go further and if you notice that:

Code: Select all

x^2N   = x^N * x^N
x^2N+1 = x^N * x^N * x
then you can write

Code: Select all

template<unsinged int N> 
inline float pow(const float x)
{
    if(N%2) // odd
    {
       const float xNdiv2 = pow< (N-1)/2 >(x);
       return x * xNdiv2 * xNdiv2 ;
    }
    else // even
    {
       const float xNdiv2 = pow< N/2 >(x);
       return xNdiv2 * xNdiv2 ;
    }
}

template<> 
inline float pow<0>(const float x)
{
    return 1;
}

template<> 
inline float pow<1>(const float x)
{
    return x;
}

Post

Leslie Sanford wrote: Yup, that's the correct syntax.
aha, neat indeed. ...metaprogramming...interesting

@mdsp: thanks for your example. ...but i didn't mean to imply to not to know about / use templates at all. it's just that i use them only on rare occasions.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

Based on sambean's suggestion I made the following changes to the DelayLine class:

Code: Select all

#ifndef DELAY_LINE_H
#define DELAY_LINE_H

#include <cassert>
#include <cstring>

template<unsigned int Length>
class DelayLine
{
private:
    static const bool IsPowerOfTwo;

    float data[Length];

public:
    DelayLine()
    {
        Clear();
    }

    float operator[](float index) const
    {
        assert((index >= 0.0f) && (index < Length));

        unsigned int x1 = (unsigned int)index;
        unsigned int x2 = IsPowerOfTwo ? 
                          (x1 + 1) & (Length - 1) : 
                          (x1 + 1) % Length;        
        float y1 = data[x1];
        float y2 = data[x2];
        float f = index - x1;
    
        return (y2 - y1) * f + y1;
    }

    float &operator[](unsigned int index)
    {
        assert(index < Length);

        return data[index];
    }

    void Clear()
    {
        memset(data, 0, sizeof(data));
    }

    int GetLength() const
    {
        return Length;
    }
};

template<int Length>
const bool DelayLine<Length>::IsPowerOfTwo = 
                      !(Length & (Length - 1)) && Length;

#endif
First, I changed the integer indexes from int to unsigned int. This takes care of having to deal with an index less than zero. I also changed the Length to type unsigned int.

Second, I added a member constant that indicates whether the Length is a power of two (thanks to sambean for the algorithm). In the overloaded subscript operator for reading fractional values, I have an if/else statement that uses a bit mask if the Length is a power of two else it uses modulo. Because all of the values are constants, both Length and IsPowerOfTwo, the branch should be compiled away by any decent compiler.

I think that's about it.

[EDIT] It occurs to me that I could write an iterator class modeled as a Random Access Iterator for writing values to the delay line. It would take care of wrapping itself so that wouldn't have to be done externally. Hmm, I wonder if I could have an iterator for fractional reads as well. [/EDIT]

Post

A completely new version:

Code: Select all

#ifndef DELAY_LINE_H
#define DELAY_LINE_H

#include <cassert>
#include <cstring>
#include <cstddef>

template<typename T, size_t Length>
class DelayLine
{
private:
    static const bool IsPowerOfTwo;

    T buffer[Length];
    size_t index;

public:
    DelayLine() :
    index(0)
    {
        Clear();
    }

    T &operator*()
    {
        return buffer[index];
    }
    
    DelayLine &operator++()
    {
        index = IsPowerOfTwo ? 
                (index + 1) & (Length - 1) :
                (index + 1) % Length;

        return *this;
    }

    DelayLine operator++(int)
    {
        DelayLine temp = *this;

        index = IsPowerOfTwo ? 
                (index + 1) & (Length - 1) :
                (index + 1) % Length;

        return temp;
    }

    T Modulate(T offset)
    {
        assert(offset >= 0);

        T i = index - offset;

        while(i < 0.0f)
        {
            i += Length;
        }

        size_t x1 = (size_t)i;
        size_t x2 = IsPowerOfTwo ? 
                    (x1 + 1) & (Length - 1) : 
                    (x1 + 1) % Length;        
        T y1 = buffer[x1];
        T y2 = buffer[x2];
        T f = i - x1;
    
        return (y2 - y1) * f + y1;
    }

    void Clear()
    {
        memset(buffer, 0, sizeof(buffer));
    }

    size_t GetLength() const
    {
        return Length;
    }
};

template<typename T, int Length>
const bool DelayLine<T, Length>::IsPowerOfTwo = 
              !(Length & (Length - 1)) && Length;
Typical usage:

Code: Select all

// Somewhere in the code, probably in a "Chorus" class 
// declaration:
DelayLine<float, 8192> delayLine;

// Typical LFO type object.
Lfo lfo;

// Somewhere else in the code, probably in the Chorus's "Process"
// method:

// Delay time in samples.
float delayTime = 100;

// Feedback scaler.
float feedbackLevel = 0.75f

// Audio input.
const float *input = inputs[0];

// Audio output.
float *output = outputs[0];

// Buffer from the lfo filled with its output.
const float *lfoBuffer = lfo.GetOutput();

// Modulation offset in samples.
float offset.

// Delay output;
float delay;

// The "inner loop"
for(int i = 0; i < sampleFrames; i++)
{
    offset = delayTime * pow(2.0f, *lfoBuffer);

    delay = delayLine.Modulate(offset);

    *delayLine = *input + delay * feedbackLevel;

    // Mix hard coded to scale delay by 50%.
    *output = *input + delay * 0.5f;

    delayLine++;
    input++;
    output++;
    lfoBuffer++;    
}
Untested, so I've probably forgot something. But you get the idea.

Post Reply

Return to “DSP and Plugin Development”