Old 01-19-2020, 11:31 PM   #1
SaulT
Human being with feelings
 
Join Date: Oct 2013
Location: Seattle, WA
Posts: 778
Default A Basic Delay Plugin Explained

I got a request from a fellow user about how to make a basic delay plugin. I figured I would make it a public post so it was open for feedback if anyone else wanted to chime in. This first post will have the full source and the second post will break it down by sections and explain what is happening where.

The purpose is a basic short delay that can create echoes through feedback. I'm not going to claim to be the best teacher or explain the best and I can easily talk too much, but hopefully the user who asked will benefit anyways.

++changelog

2020/01/20 ... incorporated Ashcat's moving the feedback into the function itself, added db2ratio()

Also, I should point out that I put the default values to those exact settings because I liked how it sounded on Otaku Gang's "Ten Crack Commandments (Star Wars Remix)" which is the track I tend to test my plugins out on. And now you know.

Code:
desc:simple delay plugin

slider1:350<1,500,1>Delay (ms)
slider2:-15<-90,0,0.1>Wet (dB)
slider3:-45<-90,0,0.1>Feedback (dB)

@init

buf0 = 100000;
buf1 = 200000;

function db2ratio(db) ( 10^(db * 0.05); );

function delay_set(buffer,ms,fbGain)
 instance(buf,size,frac,pos)
(
  buf = buffer;
  size = srate * ms * 0.001;
  frac = size - floor(size);
  size = floor(size);
  feedbackGain = db2ratio(fbGain);
);

function delay(in)
 instance(pos,buf,old,out,size,frac,feedbackGain)
(
  out = buf[pos];
  out = out*(1-frac) + old*frac;
  old = buf[pos];
  buf[pos] = in + (out * feedbackGain);
  pos += 1;
  pos >= size ? pos -= size;
  out;
);

@slider

delay0.delay_set(buf0,slider1,slider3);
delay1.delay_set(buf1,slider1,slider3);

wetGain = db2ratio(slider2);


@sample

spl0 += delay0.delay(spl0) * wetGain;
spl1 += delay1.delay(spl1) * wetGain;

Last edited by SaulT; Yesterday at 09:04 PM.
SaulT is online now   Reply With Quote
Old 01-19-2020, 11:32 PM   #2
SaulT
Human being with feelings
 
Join Date: Oct 2013
Location: Seattle, WA
Posts: 778
Default

Code:
desc:simple delay plugin

slider1:350<1,500,1>Delay (ms)
slider2:-15<-90,0,0.1>Wet (dB)
slider3:-90<-90,0,0.1>Feedback (dB)
What controls do we want? This is a delay so it seems pretty straightforward - we want to select how long the delay is, and milliseconds is a fairly intuitive pick for units. I'm picking a simple volume control for how much delay to blend back in, in decibels. Similarly, I'm picking decibels for feedback. Audio people should know decibels, it's one of the basic units of measure. 90 decibels of attenuation is basically silence for all intents and purposes, so that's a good minimum.

Code:
@init

buf0 = 100000;
buf1 = 200000;
Okay, so I could have said something like "buf0 = 0; buf1 = buf0 + srate;" but instead I'm picking a predetermined range. Why is buf0 100k? Because I'm thinking of the worst case scenario - someone is in a 96 kHz samplerate and they pick a 1 second delay. We want our plugins to work anywhere from 22.05 kHz to 96 kHz samplerates, and what if we are trying to create a reverb with dozens of delay lines? We want to think ahead and plan for these things.

Code:
function delay_set(buffer,ms)
 instance(buf,size,frac,pos)
(
  buf = buffer;
  size = srate * ms * 0.001;
  frac = size - floor(size);
  size = floor(size);
);
Every time we change a slider we are changing something about the delay. One of the values of using functions is that instead of putting all of this in the @slider section we can put all of the variable tweaking in a separate function. Secondary benefit? By putting it in a function we can reuse the code for multiple delay lines. Remember, we are coding for two channels, left and right, right? Now, what if we wanted to add support for more channels? It's relatively simple, we create delay2 and so forth and call delay2.delay_set(...). It isn't full blown object oriented code, but using functions to create objects makes our code simpler and quicker to write!

Code:
 instance(buf,size,frac,pos)
when a variable calls this function it becomes an object and JS assigns these variables to that object. So delay0 will now have delay0.buf, delay0.size, etc.

Code:
  size = srate * ms * 0.001;
  frac = size - floor(size);
  size = floor(size);
Remember that we get milliseconds by dividing the samplerate (samples per second) by 1000. This is the same as multiplying by 0.001. The tricky part of this code is that if we want a precise delay often we will end up with a fractional value. For example, at 44100 kHz samplerate if we want a 35 ms delay then the exact value of that delay is 0.035 * 44100 = 1543.5 samples. We can't have half a byte of buffer so we use a technique called linear interpolation to get that fractional delay amount. I love interpolation, but for a basic delay plugin we really don't need anything fancier than linear interpolation.

Code:
function delay(in)
 instance(pos,buf,old,out,size,frac)
(
  out = buf[pos];
  out = out*(1-frac) + old*frac;
  old = buf[pos];
  buf[pos] = in;
  pos += 1;
  pos >= size ? pos -= size;
  out;
);
The heart of our plugin here. We are using a circular buffer to create our delay. This means that we are cycling through a buffer with a position variable keeping track of our place. So we pull out the current value, we linear interpolate the precise value, and put it aside for a second. Our new sample is put into the buffer, we move the position variable up one, and if it is greater than the size it goes back to the beginning. The last value we list in a function is what is returned, in javascript or whatnot we would specifically say return out;, here the return is assumed.

Code:
@slider

delay0.delay_set(buf0,slider1);
delay1.delay_set(buf1,slider1);

wetGain = 10^(slider2/20);
feedbackGain = 10^(slider3/20);
See how little code we need here? We update the values of our delays and we calculate our decibel multipliers. Remember, decibels are just ratios and volume adjustment is just a multiply.

Code:
@sample

spl0 += delay0.delay(spl0 + old0 * feedbackGain) * wetGain;
spl1 += delay1.delay(spl1 + old1 * feedbackGain) * wetGain;

old0 = spl0;
old1 = spl1;
This part of the code is executed every sample, right? So each sample we are putting the current sample and some of the previous output into our buffer, then make sure we save the current output for use next sample. Hopefully this is clear enough.

...

And, umm, that's basically it. Now, there are a lot of places you could go with this. You can take this technique with the circular buffers and make reverb functions, or you could put a filter on the delayed output, all kinds of things.

Remember that Reaper lists all of the current variables in the "development environment" window. You can check to make sure they look right. A common mistake I make when I'm writing is I'll forget to put a function's variable inside the instance() command. If I was to see the pos variable just hanging out but not delay0.pos and delay1.pos, then I'll know I forgot to put it in the instance().

I could have written the plugin without the functions, but it's such a valuable and important part of JS that I really needed to make sure that I illustrated it. Using functions makes your code a lot simpler to write, easier to read, and much more scalable.

Last edited by SaulT; Yesterday at 12:23 AM.
SaulT is online now   Reply With Quote
Old Yesterday, 05:19 AM   #3
lgz
Human being with feelings
 
Join Date: Jan 2020
Posts: 4
Default best delay code

It's great that I saw it!! This post is really helpful. I learned a lot in the interpretation of each line of code!
lgz is offline   Reply With Quote
Old Yesterday, 09:27 AM   #4
nofish
Human being with feelings
 
nofish's Avatar
 
Join Date: Oct 2007
Location: home is where the heart is
Posts: 9,281
Default

I appreciate your '... explained' posts (also this one), thanks.
nofish is offline   Reply With Quote
Old Yesterday, 10:14 AM   #5
ashcat_lt
Human being with feelings
 
Join Date: Dec 2012
Posts: 4,532
Default

db2ratio is such a ubiquitous function, and with all your talk about how useful functions are, you left it out. I’m not sure it would make the @slider section a lot tidier, but if we’re illustrating how functions save us from writing redundant code...

But wait! One big thing and then a slightly smaller thing.

1) We don’t want to feed the mix signal back through the delay at the end there. The feedback split should come before the mixer. In fact, it should probably happen in the delay function itself.

Code:
function delay(in)
 instance(pos,buf,old,out,size,frac)
(
  out = buf[pos];
  out = out*(1-frac) + old*frac;
  old = buf[pos];
  buf[pos] = in + (out * feedbackGain);
  pos += 1;
  pos >= size ? pos -= size;
  out;
);
Then in @sample you just send it the input and remove all references to old0 and old1.


The other thing I wanted to mention is more about the fundamental nature of this very simple type of delay. Basically, it’s gonna glitch hard and strange when you adjust the delay time. Consider what happens when we have a long time and then shorten it. We basically leave a bunch of buffer slots full of stuff, it never actually plays out so we kind of jump back to even further back then we already were, and then if we switch back to a longer time we’ll end up hearing stuff that maybe happened quite a while ago. We expect weird things when simple digital delays are modulated, and I understand that this example wasn’t meant to address that, but just pointing it out for those who might have higher expectations.

Last edited by ashcat_lt; Yesterday at 10:42 AM.
ashcat_lt is online now   Reply With Quote
Old Yesterday, 08:59 PM   #6
SaulT
Human being with feelings
 
Join Date: Oct 2013
Location: Seattle, WA
Posts: 778
Default

Quote:
1) We don’t want to feed the mix signal back through the delay at the end there. The feedback split should come before the mixer. In fact, it should probably happen in the delay function itself.
And this, friends, is why I made this a public post. I went through an iteration or two and for some reason the code just seemed weird to me. Ashcat is of course correct and this is the more proper way to do it. I'll update the code above with a changelog.

...also, for completeness sake, how about I add that db2ratio code.

A very good point, the delay is not going to smoothly modulate necessarily and this function is definitely not programmed to accommodate changes in delay length!
SaulT is online now   Reply With Quote
Old Today, 12:31 AM   #7
Tale
Human being with feelings
 
Tale's Avatar
 
Join Date: Jul 2008
Location: The Netherlands
Posts: 3,022
Default

Quote:
Originally Posted by SaulT View Post
Okay, so I could have said something like "buf0 = 0; buf1 = buf0 + srate;" but instead I'm picking a predetermined range. Why is buf0 100k? Because I'm thinking of the worst case scenario - someone is in a 96 kHz samplerate and they pick a 1 second delay. We want our plugins to work anywhere from 22.05 kHz to 96 kHz samplerates, and what if we are trying to create a reverb with dozens of delay lines? We want to think ahead and plan for these things.
You could also have stored the samples interleaved (i.e. 0, 1, 0, 1, 0, 1, ...). That way you don't have to worry about buf0 and buf1 overlapping, even beyond 96 kHz.
Tale is offline   Reply With Quote
Reply

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT -7. The time now is 07:53 AM.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2020, vBulletin Solutions Inc.