Allpass/Delay calculations in "next" function causing server exits

I’m trying to implement the following algorithm in SC.

Obviously it’s not possible to do everything directly in SC, because it needs four separate feedback loops. I decided to try to implement what I’m calling a “Barr Section” in С++ - two all passes, a delay, then a simple one-pole filter (not included in the diagram) and a gain control, all inside a single feedback loop. Then I could potentially stack four of them (or more) into a reverb.

I encountered two major issues:

  1. How to make a more modular design? I was initially thinking of writing a few classes - Allpass, Delay, LPF - so that I could also easily create other similar “sections”. The problem for me was that allpasses, delays etc. all need buffers, and despite reading this thread and looking through the attached, very complicated examples, I could not for the life of me work out how to do this.

  2. More importantly: it’s not working. I’ve isolated the issue (afaict) to the 2 allpass and 1 delay lines in the “next” function. If the next function is reduced to output[i] = input[i], it works. Similarly, the lpf and gain control worked fine. But the whole “next” function crashes the server. I can’t really work out what I’ve done wrong, since other allpass- and delay-related plugins I’ve written with quite similar code have worked fine. This is where I would really love some attentive feedback from people who know what they’re doing.

// PluginBarrSec.cpp
// Jordan White (jordanwhitede@gmail.com)

#include "SC_PlugIn.hpp"
#include "BarrSec.hpp"

static InterfaceTable* ft;

namespace BarrSec {

BarrSec::BarrSec() {
    mCalcFunc = make_calc_function<BarrSec, &BarrSec::next>(); 
    next(1);
    lpfPrevOutput = 0.0f;
    feedbackSignal = 0.0f;
    writePhase0 = 0;
    writePhase1 = 0;
    writePhase2 = 0;
    maxDelay0 = 1.0;
    maxDelay1 = 1.0;
    maxDelay2 = 1.0;
    bufSize0 = NEXTPOWEROFTWO((float)sampleRate() * maxDelay0);
    bufSize1 = NEXTPOWEROFTWO((float)sampleRate() * maxDelay1);
    bufSize2 = NEXTPOWEROFTWO((float)sampleRate() * maxDelay2);

    
    // buffers
    apDelayBuffer0 = (float *)RTAlloc(mWorld, bufSize0 * sizeof(float)); 
    apHistoryBuffer0 = (float *)RTAlloc(mWorld, bufSize0 * sizeof(float));
    
    apDelayBuffer1 = (float *)RTAlloc(mWorld, bufSize1 * sizeof(float));
    apHistoryBuffer1 = (float *)RTAlloc(mWorld, bufSize1 * sizeof(float));
    
    delayBuffer = (float *)RTAlloc(mWorld, bufSize2 * sizeof(float));

    // wrap phase using bit masking
    mask0 = bufSize0 - 1;
    mask1 = bufSize1 - 1;
    mask2 = bufSize2 - 1;

    // check that RTAlloc succeeded
    if (apDelayBuffer0 == NULL || apHistoryBuffer0 == NULL || apDelayBuffer1 == NULL || apHistoryBuffer1 == NULL || delayBuffer == NULL) {
        mCalcFunc = make_calc_function<BarrSec, &BarrSec::clear>();

        // clear outputs just in case
        clear(1);

        // what does this do?
        if (mWorld->mVerbosity -2) {
            Print("Failed to allocate memory for BarrSec Ugen.\n");
        }
        return;
    }
        
    // initialise buffers, filled with zeroes
    memset(apDelayBuffer0, 0, bufSize0 * sizeof(float));
    memset(apHistoryBuffer0, 0, bufSize0 * sizeof(float));
    memset(apDelayBuffer1, 0, bufSize1 * sizeof(float));
    memset(apHistoryBuffer1, 0, bufSize1 * sizeof(float));
    memset(delayBuffer, 0, bufSize2 * sizeof(float));

    next(1);

} // BarrSec::BarrSec

void BarrSec::clear(int inNumSamples) {
    ClearUnitOutputs(this, inNumSamples);
}

    BarrSec::~BarrSec(){
        RTFree(mWorld, apDelayBuffer0);
        RTFree(mWorld, apHistoryBuffer0);
        RTFree(mWorld, apDelayBuffer1);
        RTFree(mWorld, apHistoryBuffer1);
        RTFree(mWorld, delayBuffer);

    }

void BarrSec::next(int nSamples) { 

    // Audio rate input
    const float* input = in(0);

    // Control rate parameter - potentially change this
    float apTime0 = in0(1);

    float apTime1 = in0(2);

    float delayTime = in0(3); // slope?

    float coef = in0(4);

    float ffreq = in0(5); // slope?

    float feedback = in0(6); // slope?

    // Output buffer
    float* output = out(0);

    // cap delay times 
    if (apTime0 > maxDelay0) {
        apTime0 = maxDelay0;
        Print("apTime0 capped at 0.5s");
    }

    if (apTime1 > maxDelay1) {
        apTime1 = maxDelay1;
        Print("apTime1 capped at 0.5s");
    }

    if (delayTime > maxDelay2) {
        delayTime = maxDelay2;
        Print("delayTime capped at 0.5s");
    }

    // calclulate delay phases

    int apPhaseOffset0 = apTime0 * (float)sampleRate();
    int apPhaseOffset1 = apTime1 * (float)sampleRate();
    int delayPhaseOffset = delayTime * (float)sampleRate();


    // calculate low-pass coeff
    float alpha = expf(-2.0f * M_PI * ffreq / sampleRate());

    for (int i = 0; i < nSamples; ++i) {
 
        

        // input + fb 
        const float sig = input[i] + feedbackSignal;
        
        // ap 0
        apHistoryBuffer0[writePhase0] = sig; 
        const int historyPhase0 = writePhase0 - apPhaseOffset0;
        const int apPhase0 = writePhase0 - apPhaseOffset0;
        const float filtered0 = ((-1.0 * coef) * sig) +
                    apHistoryBuffer0[historyPhase0 & mask0] + 
                    (coef * apDelayBuffer0[apPhase0 & mask0]);
        apDelayBuffer0[writePhase0] = filtered0;
        writePhase0 = (writePhase0 + 1) & mask0;

        // ap 1
        apHistoryBuffer1[writePhase1] = filtered0; 
        const int historyPhase1 = writePhase1 - apPhaseOffset1;
        const int apPhase1 = writePhase1 - apPhaseOffset1;
        const float filtered1 = ((-1.0 * coef) * sig) +
                    apHistoryBuffer1[historyPhase1 & mask1] + 
                    (coef * apDelayBuffer1[apPhase1 & mask1]);
        apDelayBuffer1[writePhase1] = filtered1;
        writePhase1 = (writePhase1 + 1) & mask1;

        // delay 

        const int delayPhase = writePhase2 - delayPhaseOffset;
        const float delayed = delayBuffer[delayPhase & mask2]; // read delayed signal
        
        delayBuffer[writePhase2] = filtered1; // write current input to buffer
        delayBuffer[writePhase2] = input[i];
        writePhase2 = (writePhase2 + 1) & mask2;

        // lpf
        const float filtered2 = (1.0f - alpha) * delayed + alpha * lpfPrevOutput; // 1 pole LPF 
        lpfPrevOutput = filtered2; // previous sample. does this apply even in context of a signal chain? 

        // fb control
        const float fbOutput = filtered2 * feedback; 

        output[i] = zapgremlins(fbOutput);

        feedbackSignal = output[i];
    }
}

} // namespace BarrSec

PluginLoad(BarrSecUGens) {
    // Plugin magic
    ft = inTable;
    registerUnit<BarrSec::BarrSec>(ft, "BarrSec", false);
}

// PluginBarrSec.hpp
// Jordan White (jordanwhitede@gmail.com)

#pragma once

#include "SC_PlugIn.hpp"

namespace BarrSec {

class BarrSec : public SCUnit {
public:
    BarrSec();
    ~BarrSec();

private:
    // Calc function
    void next(int nSamples);
    void clear(int inNumSamples);
    // Member variables
    int writePhase0;
    int writePhase1;
    int writePhase2;
    float* apDelayBuffer0;
    float* apHistoryBuffer0;
    float* apDelayBuffer1;
    float* apHistoryBuffer1;
    float* delayBuffer;
    int bufSize0;
    int bufSize1;
    int bufSize2;
    int mask0;
    int mask1;
    int mask2;
    float maxDelay0;
    float maxDelay1;
    float maxDelay2;
    float preOutputLPIn;
    float lpfPrevOutput;
    float freq_past;
    float feedbackInput;
    float feedbackSignal;
};

} // namespace BarrSec

More generally: I’ve mentioned on the forum before that I think that the documentation around writing UGens that allocate memory is lacking. If someone were happy to help me develop my knowledge on this topic then I would be very keen to help rewrite the documentation in this area, because I think it might help other people in my position (learning c++ on the fly without much background knowledge) in the future.

Thanks in advance for any help!

I’m on my phone, but one problem I’ve noticed is that you’re accidentally calling next(1) twice in the constructor! The first call happens before the buffers have been allocated, which would explain the crashes.

What do you find complicated about memory allocation? It’s not really different from malloc/realloc/free in C. Now, if you want to use the RT allocator in standard C++ containers, you currently have to roll your own allocator. But personally I’ve never found a compelling reason to do so. 99% of Ugens just allocate in the constructor and free in the destructor. In my experience you rarely need to reallocate. (This is different from Pd where block size and sample rate can change at runtime, so buffers may need to grow/shrink.)

EDIT: your allocations and deallocations look completely fine! There isn’t much more to it.

1 Like

BTW, have you tried to run the code in a debugger?

You just need to attach to the running Server process from your editor/IDE and then create a Synth with the offending Ugen. When the Server crashes, you can get a stack trace and see the exact source of the crash.

1 Like

@Spacechild1 - thanks! removing the first next(1) fixed everything.

I’ll check out a debugger for future projects, thanks!

Re: memory management - maybe there’s not much more to it, I might be making it a bigger deal than it is. But I don’t have a strong C/C++ background so I thought it might be a bit trickier.

eg, if I wanted to make the code more modular and write classes outside the BarrSec function for the APF, Delay and OnePole, should they each get their own buffer allocation etc. and a destructor? Or should buffer allocation still take place in the next() function?

Nice!

I’ll check out a debugger for future projects, thanks!

+1. A debugger would have immediately pointed you to that extra next(1) call.

eg, if I wanted to make the code more modular and write classes outside the BarrSec function for the APF, Delay and OnePole, should they each get their own buffer allocation etc. and a destructor?

That’s a good question! One problem is that the RT memory functions are stateful, e.g. they operate on a World instance. I see three general solutions:

  1. allocate all memory in the top-level constructor and pass the buffers to the individual class instances, e.g. with some init function.

  2. allocate the buffers in the child classes; pass the World* in the constructor and keep it as a member. This way you can automatically deallocate in the destructor. This provides the best encapsulation, but it wastes a little bit of memory. You’d need some extra method to check for allocation failure, though.

  3. allocate/deallocate the buffers in the child classes via init and free methods that take a World* argument. No need to store World* as a member, but you can’t deallocate automatically in the destructor.

Personally, I would tend towards 1.

should buffer allocation still take place in the next() function?

You mean the constructor right? You typically don’t allocate RT memory in the next() function. (I mean, you can, it’s still RT safe, but it still uses unnecessary CPU-cycles, so it’s better to pre-allocate the memory in the constructor. Side note: if a buffer doesn’t need to persist between next() calls, you can just allocate it on the stack with alloca.)

1 Like

Thanks, I’ll take a while to digest this :slight_smile: and yes I meant the constructor…my bad

So, the plugin compiles, the allpasses, delay and onepole all seem to be working fine. For some reason the feedback isn’t working. Anyone got any ideas what I’m doing wrong here?

Here’s the next() function, I assume that’s where the issue is:

// c++ code
for (int i = 0; i < nSamples; ++i) {    

        // input + fb 
        const float sig = input[i] + feedbackSignal;
        
        // ap 0
        apHistoryBuffer0[writePhase0] = sig; 
        const int historyPhase0 = writePhase0 - apPhaseOffset0;
        const int apPhase0 = writePhase0 - apPhaseOffset0;
        const float filtered0 = ((-1.0 * coef) * sig) +
                    apHistoryBuffer0[historyPhase0 & mask0] + 
                    (coef * apDelayBuffer0[apPhase0 & mask0]);
        apDelayBuffer0[writePhase0] = filtered0;
        writePhase0 = (writePhase0 + 1) & mask0;

        // ap 1
        apHistoryBuffer1[writePhase1] = filtered0; 
        const int historyPhase1 = writePhase1 - apPhaseOffset1;
        const int apPhase1 = writePhase1 - apPhaseOffset1;
        const float filtered1 = ((-1.0 * coef) * sig) +
                    apHistoryBuffer1[historyPhase1 & mask1] + 
                    (coef * apDelayBuffer1[apPhase1 & mask1]);
        apDelayBuffer1[writePhase1] = filtered1;
        writePhase1 = (writePhase1 + 1) & mask1;

        // delay 

        const int delayPhase = writePhase2 - delayPhaseOffset;
        const float delayed = delayBuffer[delayPhase & mask2]; // read delayed signal
        
        delayBuffer[writePhase2] = filtered1; // write current input to buffer
        delayBuffer[writePhase2] = input[i];
        writePhase2 = (writePhase2 + 1) & mask2;

        // lpf
        const float filtered2 = (1.0f - alpha) * delayed + alpha * lpfPrevOutput; // 1 pole LPF 
        lpfPrevOutput = filtered2; // previous sample for one pole

        output[i] = zapgremlins(filtered2); 

        // fb control
        const float fbOutput = filtered2 * feedback; 

        feedbackSignal = fbOutput;
    }

I assume “feedback” is a coefficient, but I don’t see what its value is. If it is 0, then you won’t get feedback. I also wonder why fbOutput is a variable at all. Why not just assign the value directly to feedbackSignal?

I would just do:

feedbackSignal = output[i]*feedbackCoef

unless you want gremlins in the feedback for some reason.

Sam

Thanks Sam. I should have included the whole function and not just the for loop…“feedback” is indeed the feedback coefficient, declared here as an input control. I don’t know if that leads to any other problems?

void BarrSec::next(int nSamples) { 

    // Audio rate input
    const float* input = in(0);

    // Control rate parameter - potentially change this
    float apTime0 = in0(1);

    float apTime1 = in0(2);

    float delayTime = in0(3); // slope?

    float coef = in0(4);

    float ffreq = in0(5); // slope?

    float feedback = in0(6); // slope?

    // Output buffer
    float* output = out(0);

    // cap delay times 
    if (apTime0 > maxDelay) {
        apTime0 = maxDelay;
        Print("apTime0 capped at 0.5s");
    }

    if (apTime1 > maxDelay) {
        apTime1 = maxDelay;
        Print("apTime1 capped at 0.5s");
    }

    if (delayTime > maxDelay) {
        delayTime = maxDelay;
        Print("delayTime capped at 0.5s");
    }

    // calclulate delay phases

    int apPhaseOffset0 = apTime0 * (float)sampleRate();
    int apPhaseOffset1 = apTime1 * (float)sampleRate();
    int delayPhaseOffset = delayTime * (float)sampleRate();


    // calculate low-pass coeff
    float alpha = expf(-2.0f * M_PI * ffreq / sampleRate());

    // 1. feedback in. 2. process allpass. 3. process allpass. 4. process delay. 5. lpf. 6. write out and in.
    for (int i = 0; i < nSamples; ++i) {
 
        

        // input + fb 
        const float sig = input[i] + feedbackSignal;
        
        // ap 0
        apHistoryBuffer0[writePhase0] = sig; 
        const int historyPhase0 = writePhase0 - apPhaseOffset0;
        const int apPhase0 = writePhase0 - apPhaseOffset0;
        const float filtered0 = ((-1.0 * coef) * sig) +
                    apHistoryBuffer0[historyPhase0 & mask0] + 
                    (coef * apDelayBuffer0[apPhase0 & mask0]);
        apDelayBuffer0[writePhase0] = filtered0;
        writePhase0 = (writePhase0 + 1) & mask0;

        // ap 1
        apHistoryBuffer1[writePhase1] = filtered0; 
        const int historyPhase1 = writePhase1 - apPhaseOffset1;
        const int apPhase1 = writePhase1 - apPhaseOffset1;
        const float filtered1 = ((-1.0 * coef) * sig) +
                    apHistoryBuffer1[historyPhase1 & mask1] + 
                    (coef * apDelayBuffer1[apPhase1 & mask1]);
        apDelayBuffer1[writePhase1] = filtered1;
        writePhase1 = (writePhase1 + 1) & mask1;

        // delay 

        const int delayPhase = writePhase2 - delayPhaseOffset;
        const float delayed = delayBuffer[delayPhase & mask2]; // read delayed signal
        
        delayBuffer[writePhase2] = filtered1; // write current input to buffer
        delayBuffer[writePhase2] = input[i];
        writePhase2 = (writePhase2 + 1) & mask2;

        // lpf
        const float filtered2 = (1.0f - alpha) * delayed + alpha * lpfPrevOutput; // 1 pole LPF 
        lpfPrevOutput = filtered2; // previous sample for one pole

        output[i] = zapgremlins(filtered2); 

        // fb control
//        const float fbOutput = filtered2 * feedback; 

        feedbackSignal = output[i] * feedback;
    }
}

So turns out this line was the problem, have removed it and it works basically as expected as far as I can tell.