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:
-
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.
-
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!