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 Stutter

The first FX
By: MKoussa

Like many, my first forray into working with the Logue-SDK was through the DOTEC articles. In it, they walk you through setting up your environment and eventually, downloading their bitcrusher Mod FX to demo.

It was so straightforward and yet, so effective at, well, immitating the crushing of the bits! And with only a single control, it was easy to comprehend what it was doing. Look for yourself!

This is the entire bitcrusher effect! (I google translated the Japanese)

    
      #include "usermodfx.h"

      static float rate, lastSampleL, lastSampleR;
      static int32_t count;

      //初期化処理
      //Initialization process
      void MODFX_INIT(uint32_t platform, uint32_t api)
      {
        lastSampleL = 0.f;
        lastSampleR = 0.f;
        count = 0;
      }

      void MODFX_PROCESS(const float *main_xn,
                               float *main_yn,
                         const float *sub_xn,
                               float *sub_yn,
                            uint32_t frames)
      {
        //エフェクト処理のループ
        //Effect processing loop
        for (uint32_t i = 0; i < frames; i++)
        {
          //LとRの音を用意する
          //Prepare the L and R sounds
          const float inL = main_xn[i * 2];
          const float inR = main_xn[i * 2 + 1];

          //大きくなるほどサンプルが粗くなる
          //The larger the value, the coarser the sample.
          uint32_t skip = rate * 64;

          //countが0の時だけlastSampleを更新する
          //Update lastSample only when count is 0
          if (count == 0)
          {
            lastSampleL = inL;
            lastSampleR = inR;
          }

          //lastSampleの音を継続する
          //Continue the sound of lastSample
          main_yn[i * 2] = lastSampleL;
          main_yn[i * 2 + 1] = lastSampleR;
          count++;

          //skipを超えたらcountを0にリセット
          //If skip is exceeded, count is reset to 0.
          if (count > (int)skip)
          count = 0;
        }
      }

      void MODFX_PARAM(uint8_t index, int32_t value)
      {
        //固定小数点q31フォーマットをfloatに変換
        //Convert fixed-point q31 format to float
        const float valf = q31_to_f32(value);

        switch (index)
        {
          //timeツマミを回した時にrateに値を代入
          //Assign a value to rate when you turn the time knob
          case k_user_modfx_param_time:
            rate = valf;
            break;
          default:
            break;
        }
      }
    
    

This bitcrusher effect was the basis that I built Stutter from.

Literally all it does is take a sample and replay that sample X amount of times (X being set by the A knob) and then gets a new sample. That's it. I encourage you to try it out to see for yourself how awesome something so simple can sound! For me, it was extremely encouraging.

Truth be told, my mind was filled with so many different ideas that I, still to this day, haven't even finished the DOTEC Articles. I absolutely mean to, especially because they build a vocoder, but alas, all good things in time!

Once I got the bitcrusher setup and I was able to successfully make it, the first thing I explored was changing the '64' in this chunk of code

    
      //大きくなるほどサンプルが粗くなる
      //The larger the value, the coarser the sample.
      uint32_t skip = rate * 64;
    

I knew this would instantly and radically change how things would sound and I was correct. Try it for yourself!

It then dawned on me that, considering the effect changing the '64' had on the sound, it would be a perfect candidate for the B knob. So I set about looking at the other demo projects and eventually piecing together what I needed. After a couple iterations, I had it (mostly) working. I could twist both knobs and get radically different sounds. Sounds that became weirdly rhythmic.

Here's my initial commit:


      void MODFX_PROCESS(const float *main_xn,
                               float *main_yn,
                         const float *sub_xn,
                               float *sub_yn,
                            uint32_t frames)
      {
        for(uint32_t i = 0; i < frames; i++)
        {
          if(repeat == 0)
          {
              _loopSamplesL[rate] = main_xn[i * 2];
              _loopSamplesR[rate] = main_xn[i * 2 + 1];
          }
          main_yn[i * 2]     = _loopSamplesL[rate];
          main_yn[i * 2 + 1] = _loopSamplesR[rate];

          rate++;
          if(rate >= sampleNum)
          {
            rate = 0;
            repeat++;
          }
          if(repeat > depth)
          {
            repeat = 0;
          }
        }
      }

There was, however, a major flaw in my rudimentary design.

Whenever I would twist the knobs while sound was running through the effect, I would get a horrendous glitchy machine sound. Now, most times, I would welcome this with open arms but, in this case... It was very Not Good. Twisting the knobs while sound was playing was obviously a base function that any effect should have. You can't realistically ask your users to stop what they're doing to change a setting! It needed to be fixed.

My solution ended up being a mix between logic and C++ functionality.

First, I configured any knob change to change a boolean to let it be known that the value has changed and it needs to be updated. This was to solve for the variable being changed while it was trying to be used in counting logic. This would lead to variables being out of range and trying to retrieve values from memory locations that either weren't set or didn't exist. When this happens, it's called 'undefined behavior'.

    
      void MODFX_PARAM(uint8_t index, int32_t value)
      {
        //Convert fixed point q31 format to float
        const float valf = q31_to_f32(value);
        switch (index)
        {
          //A knob
          case k_user_modfx_param_time:
            rateVal = (uint16_t)(valf * 1023);
            rateChange = true;
            break;
          //B Knob
          case k_user_modfx_param_depth:
            depthVal = (uint16_t)(valf * 50);
            depthChange = true;
            break;
          default:
            break;
        }
      }
    
    

Now, in the main method loop that does the actual magic, I only update the variable when the rateChange or depthChange boolean is true.

    
      ...
      if(rate > rateLength)
      {
        rate = 0;
        repeat++;
        if(rateChange)
        {
            rateLength = rateVal;
            rateChange = false;
        }
        if(depthChange)
        {
            depth = depthVal;
            depthChange = false;
        }
        if(repeat > depth)
        {
            repeat = 0;
        }
      }
    
    

This, I had hoped, would be enough. There didn't appear to be any opportunity for any variable to get incorrectly, or not, set. But, alas, there was one more thing I needed to do to fully ensure there would no longer be any glitches.

In programming, there is this whole thing called 'race conditions', where, basically, multiple things are trying to do something to the same thing. Essentially, I was still having issues where when a knob was being twisted at just the right time when variables were being set or changed, there could sometimes be situations where the CPU didn't know which instruction to do first, so, instead, it threw up its preverbial hands and said "frick it, you get an annoying beep".

The solution? Use Atomic operations.

    
      ...
      static std::atomic<uint16_t> rateVal(0);
      static std::atomic<uint16_t> depthVal(0);
    
    

With this in place, now whenever the value is changed from the user twisting the knobs, the CPU knows which instruction takes priority and sets or changes accordingly. Finally, no more glitching! Stutter began to really come alive now. Being able to seamlessly change both parameters opened up all sorts of cool uses cases for the effect. One could set an LFO to both knobs and get all sorts of quasi-rhythmic jams going. It was at this point I felt I could say that Stutter was 'done enough' and ready for people to use in a meaningful way. It went from a novel idea, to a prototype and finally to a useful product.

And that's it! Altogether, I'd say I spent about 20 to 30 hours building Stutter. Mostly because it was, quite literally, all new to me, and it took a bit to translate the code into sound in my mind. This experience is partly why I provide the source code for my stuff. I could have only reached this place of understanding because of this demo. We all benefit when we work together.

What's Next For Stutter?

One idea I've been kicking around is a BPM synced version. There exists in the Logue-SDK a way to get the BPM and I think it would be really cool to be able to sync either the rate or depth (or both!) somehow to the BPM. Possibly there could be a way to toggle the BPM sync off and on, and when on, the rate and/or depth could be either multiples or fractions of the BPM. Kind of like a BPM linked delay effect. It'd probably need to be changed to a delfx at that point as I don't think the modfx let's you use the shift-B functionality but... I have never tried it!

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