KORG LOGUESDK KORG LOGUESDK  LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK KORG LOGUESDK 

MKOUSSA MKOUSSA MKOUSSA MKOUSSA MKOUSSA MKOUSSA MKOUSSA MKOUSSA MKOUSSA MKOUSSA MKOUSSA MKOUSSA MKOUSSA MKOUSSA 

Building Reverse Echo

Missy Elliott Reference Here
By: MKoussa

Having finished my first effect, I was immediately ready to start working on another. Even before I had figured out all the issues with Stutter, I had begun with what would become my second fx, Reverse Echo. The inspiration came from, you guessed it, the very next DOTEC article! (Seriously, it's a great walk through).

The mission was to build their StopFX (download), which is "like stopping a record". In the tutorial, they explain how you can access memory, and how the 'delfx' and 'revfx' have more memory available than the 'modfx' part on the NTS-1.

Here is the StopFX from the DOTEC article with comments sourced from the article:

    
#include "userdelfx.h"
#include "float_math.h"
#include "buffer_ops.h"

#define BUFFER_LEN 48000 * 10
static __sdram float s_delay_ram[BUFFER_LEN];

static float rate, p, slope;
static uint32_t z, z2, prev, next;
static uint8_t isStop;

void DELFX_INIT(uint32_t platform, uint32_t api)
{
  buf_clr_f32(s_delay_ram, BUFFER_LEN);
  z = 0;
  z2 = 0;
  prev = 0;
  next = 0;
  p = 0.f;
  slope = 0.f;
  isStop = 0;
}

void DELFX_PROCESS(float *xn, uint32_t frames)
{
  // Copy to sampling buffer
  for(uint32_t i = 0; i < frames * 2; i++){
    s_delay_ram[z++] = xn[i];
    if(z > BUFFER_LEN - 1) z = 0;
  }

  // Reset when knob is turned all the way to the left
  if(rate < 0.1f){
    isStop = 0;
    rate = 0.f;
  }
  else{
    //Determine the beginning stop point of the knob
    //This is disabled when the knob is turning, since “isStop” is “1”.
    if(!isStop){
      isStop = 1;
      rate = 0.f;
      p = (z - frames * 2.f) / 2.f;
      if(p < 0.f) p = (BUFFER_LEN - 2.f * p) / 2.f;
    } // this is not the closing bracket for “else”, but for the “if” statement just before

    for(uint32_t i = 0; i < frames; i++){

      uint32_t length_mono = BUFFER_LEN / 2;

      // Get the before-after index of the playback position, and the interpolating coefficient
      prev = (uint32_t)p;
      slope = p - prev;
      next = prev + 1;
      if(next > length_mono - 1) next = 0;

      // Get the sound of the playback position
      float s1L = s_delay_ram[prev * 2];
      float s2L = s_delay_ram[next * 2];
      float s1R = s_delay_ram[prev * 2 + 1];
      float s2R = s_delay_ram[next * 2 + 1];

      float currentL = 0.f,currentR = 0.f;
      currentL = s1L + (s2L - s1L)*slope;
      currentR = s1R + (s2R - s1R)*slope;

      // Overwrite as output.
      xn[i * 2] = currentL;
      xn[i * 2 + 1] = currentR;

      // Advance the playback point according to the “rate” amount
      p += 1.f - rate;
      if(p > (float)length_mono - 1.f) p = 0.f;
    }// “for” ends here
  }// “else” ends here
}

void DELFX_PARAM(uint8_t index, int32_t value)
{
  const float valf = q31_to_f32(value);
  switch (index) {
    case k_user_delfx_param_time:
      rate = valf;
      break;
    case k_user_delfx_param_depth:
      break;
    default:
      break;
  }
}
    

Seeing as how sound was literally just a bunch of decimal numbers in an array, the very first thing I did was to see if I could build a very simple delay effect, where I would record the audio and immediately play it back.

While working out how to accomplish this, I wondered if instead of just playing back the sample, if I could play it back in reverse by just iterating over the array backwards. To accomplish this, I took the maximum length of the array and subtracted whatever the iteration count was. So, for example, if the maximum was 100, and the current iteration count was 1, it would save the incoming audio to the 1 position in the array while playing the 100 - 1 position in the array (maximum - iteration count). This way new audio was always being saved in the right position and the reverse was playing back, regardless of the echoRate setting.

Here's a code example of what I mean:

    
#include "userdelfx.h"
#include "buffer_ops.h"
#define BUFFER_LEN 48000 * 2
static __sdram float s_delay_ram[BUFFER_LEN];

static uint32_t echoRate;

void DELFX_INIT(uint32_t platform, uint32_t api)
{
  buf_clr_f32(s_delay_ram, BUFFER_LEN);
  echoRate = 0;
}

void DELFX_PROCESS(float *xn, uint32_t frames)
{
  for(uint32_t i = 0; i < frames; i++)
  {
    //Save incoming audio
    s_delay_ram[echoRate * 2]     = xn[i * 2];
    s_delay_ram[echoRate * 2 + 1] = xn[i * 2 + 1];

    //Play back the saved audio, but from end to beginning
    xn[i * 2]     = s_delay_ram[BUFFER_LEN - (echoRate * 2)];
    xn[i * 2 + 1] = s_delay_ram[BUFFER_LEN - (echoRate * 2 + 1)];

    echoRate++;
  }
  if(echoRate > BUFFER_LEN / 2)
  {
    echoRate = 0;
  }
}

void DELFX_PARAM(uint8_t index, int32_t value)
{
  const float valf = q31_to_f32(value);
  switch (index) {
    case k_user_delfx_param_time:
      echoRate = (valf * (BUFFER_LEN / 2)) - 1;
      break;
    case k_user_delfx_param_depth:
      break;
    default:
     break;
  }
}
    
    

It worked and wow, how cool! I was playing back chunks of audio backwards!

Awesome!

Well, until I realized this wasn't really useful. I mean, surely someone, somewhere would find it useful, but just for myself, it wasn't enough.

The main issue was it was fully wet, all the time. There was no way to mix the incoming audio with the audio playback. Second, it felt kind of odd to just reverse and playback a single chunk of audio, abruptly resetting in the middle of playback.

I tackled the second problem first, as I really didn't know where to start with wet/dry, and figured that would come to me in time.

At this point, there was a lot of experimentation of what I could do to add to the effect. Eventually, I landed on mixing in the existing looping audio with the new incoming audio to make a sort of trailing effect. Every new sample became whatever was already sampled mixed with the new audio. As I was combining these sounds in a very simple manner (sum and divide by 2), I knew that the math would work itself out so that the audio loop, over time, would eventually go to 0 without more input.

Here's the very first check in:

    
#include "userdelfx.h"
#include "buffer_ops.h"

#define BUFFER_LEN 48000 * 2
static __sdram float s_delay_ram[BUFFER_LEN];

static uint8_t depth;
static uint8_t repeat;
static uint32_t echoRate;

void DELFX_INIT(uint32_t platform, uint32_t api)
{
  buf_clr_f32(s_delay_ram, BUFFER_LEN);
  repeat = 0;
  depth = 0;
  echoRate = 0;
}

void DELFX_PROCESS(float *xn, uint32_t frames)
{
  for(uint32_t i = 0; i < frames; i++)
  {
    s_delay_ram[echoRate * 2]     = (xn[i * 2] + s_delay_ram[echoRate * 2]) / 2;
    s_delay_ram[echoRate * 2 + 1] = (xn[i * 2 + 1] + s_delay_ram[echoRate * 2 + 1]) / 2;

    xn[i * 2]     = (s_delay_ram[BUFFER_LEN - (echoRate * 2)] + xn[i * 2]) / 2;
    xn[i * 2 + 1] = (s_delay_ram[BUFFER_LEN - (echoRate * 2 + 1)] + xn[i * 2 + 1]) / 2;

    echoRate++;
  }
  if(echoRate > BUFFER_LEN / 2)
  {
    echoRate = 0;
  }
}

void DELFX_PARAM(uint8_t index, int32_t value)
{
  const float valf = q31_to_f32(value);
    switch (index) {
    case k_user_delfx_param_time:
      echoRate = (valf * (BUFFER_LEN / 2)) - 1;
      break;
    case k_user_delfx_param_depth:
      depth = (uint8_t)(valf * 10);
      break;
    default:
      break;
  }
}
    
    

We're really cookin' now. This felt like a real, bonafide effect.

Next, I needed to add the wet/dry. I needed to somehow both increase/decrease the volume of the raw input, as well as the audio sample, simultaniously and in opposite directions. As in, one needs to get smaller while the other gets larger and vice-versa. This turned out to be, for the most part, pretty straitforward.

The solution I went with was to first multiply the sample by the wet/dry value (which is 0 - 1), so that when it was all the way wet, the sample would be multiplied by 1 and be at full volume. Second, I multiplied the source audio by 1 minus the same wet/dry value. This way, when you turned the wet/dry up, the number would get smaller and smaller, until full wetness where the source audio is multiplied by 0, effectively making it silent.

Finally, just like Stutter, this effect too suffered from clicks and scratches when the knobs when turned. Having already solved this issue, I essentially used the same mixture of logic and atomic processes to ensure that there would no longer be any clicking and you could twist all the knobs carefree.

Here's where we're at now, version 2.2:

    
#include "userdelfx.h"
#include "buffer_ops.h"
#include <atomic>

#define BUFFER_LEN 48000 * 4
#define BUFFER_LEN_HALF 48000

static __sdram float s_delay_ram[BUFFER_LEN];

static bool depthChange, wetDryChange;

static float depth, depthMath, wetDryMath, depthVal, wetDry, wetDryVal;

static uint32_t echoCount, echoMax;
static std::atomic<uint32_t> echoMaxVal(0);

void DELFX_INIT(uint32_t platform, uint32_t api)
{
  buf_clr_f32(s_delay_ram, BUFFER_LEN);

  depth = 0.0f;
  echoCount = 0;
  wetDry = 0.0f;
  echoMax = 0;
  //
  depthMath = 0.99f;
  wetDryMath = 1.0f;
  //
  depthVal = 0.0f;
  echoMaxVal = 0;
  wetDryVal = 0.0f;
}

void DELFX_PROCESS(float *xn, uint32_t frames)
{
  for(uint32_t i = 0; i < frames; i++)
  {
    if(echoCount > echoMax)
    {
      echoCount = 0;
      echoMax = echoMaxVal;
    }
    s_delay_ram[echoCount * 2]     = (xn[i * 2]     + (s_delay_ram[echoCount * 2]     * depth)) / depthMath;
    s_delay_ram[echoCount * 2 + 1] = (xn[i * 2 + 1] + (s_delay_ram[echoCount * 2 + 1] * depth)) / depthMath;

    if(wetDry > 0)
    {
      xn[i * 2]     = (xn[i * 2]     * wetDryMath) + (s_delay_ram[(echoMax * 2)     - (echoCount * 2)]     * wetDry);
      xn[i * 2 + 1] = (xn[i * 2 + 1] * wetDryMath) + (s_delay_ram[(echoMax * 2 + 1) - (echoCount * 2 + 1)] * wetDry);
    }
    else
    {
      xn[i * 2]     = xn[i * 2];
      xn[i * 2 + 1] = xn[i * 2 + 1];
    }

    echoCount++;
    if(depthChange)
    {
        depth = depthVal;
        depthMath = 0.99f + depth;
        depthChange = false;
    }

    if(wetDryChange)
    {
        wetDry = wetDryVal;
        wetDryMath = 1.0f - wetDry;
        wetDryChange = false;
    }
  }
}

void DELFX_PARAM(uint8_t index, int32_t value)
{
  const float valf = q31_to_f32(value);
  switch (index)
  {
    case k_user_delfx_param_time:
      echoMaxVal = ((uint32_t)(valf * BUFFER_LEN_HALF)) + 48000;
      break;
    case k_user_delfx_param_depth:
      depthVal = valf;
      depthChange = true;
      break;
    case k_user_delfx_param_shift_depth:
      wetDryVal = valf;
      wetDryChange = true;
      break;
    default:
      break;
  }
}
    
    

And just like that, Reverse Echo was done. Or, atleast, done enough for me to feel comfortable enough to move onto something else for a time.

This effect, much more so than Stutter, really solidified my excitement and interest in making effects and, to a larger extent, DSP in general. There's so much incredible algorithms that have been discovered within DSP that yield so many cool and unique sounds. This really felt like the beginning of a lifetime journey.

All good things in time!

What's Next For Reverse Echo?

Just like Stutter, I'd really like to incorporate a BPM function. If anything, compile two versions, one with and one without BPM syncing. Most delay effects get better when they are BPM synced. Additionally, there does appear to be some volume reduction when using the effect. I know that this has something to do with my math, I just haven't sat down and spent the time trying to figure it out. Once I build in BPM sync, I will also resolve this issue.

Of course, there is the ultimate issue: clicking when starting a new loop. I've tried a number of weird things to resolve this and have yet to find anything that effectively works. I might end up doing some sort of bitcrushing effect, where I sum a couple of samples and divide them. I did this in the Distortion effect and while it's pretty noticeable, I think in this situation, it wouldn't be very noticeable; at least a lot less than the clicking!

LOGUESDK LOGUESDK LOGUESDK LOGUESDK LOGUESDK LOGUESDK LOGUESDK LOGUESDK LOGUESDK LOGUESDK LOGUESDK LOGUESDK LOGUESDK LOGUESDK 

L1GUESDK L2GUESDK L3GUESDK L4GUESDK L5GUESDK L6GUESDK L7GUESDK L8GUESDK L9GUESDK L10GUESDK L11GUESDK L12GUESDK L13GUESDK L14GUESDK L15GUESDK L16GUESDK L17GUESDK L18GUESDK L19GUESDK L20GUESDK L21GUESDK L22GUESDK L23GUESDK L24GUESDK L25GUESDK L26GUESDK L27GUESDK L28GUESDK