Some new audio objects (STK Instruments / Effects, Bytebeat, Slew Limiter, etc)

mattkuebrich

Active member
For the past few months, I've been working on some new Teensy audio objects and posting them on GitHub. It's been a bit of an exercise to learn C++ and a way to fill in some missing functions that I've needed when building my DIY synths. I'm excited to share them, although I should say I'm not a great coder. I'd love to hear if anyone finds these useful or has any questions, issues, or ways to make them better.

Many of these were adapted from other systems and projects, and I've tried to give credit and licenses if needed. I've written documentation that follows the same format as the existing objects in the Design Tool and provided example sketches for each object.

The last one I finished is a wrapper for the 29 polyphonic, physically modeled and FM instruments and 8 effects from The Synthesis ToolKit in C++ (STK). Another fun one is the Bytebeat object. It has an example sketch that ports all of the equations from the Equation Composer Eurorack module.

Here's all 16 objects I've completed so far:
Bernoulli Gate
Bytebeat
Chaos Noise
Comparator
DC Block
Dust
FM Drum
Function Shaper
Gate to Trigger
Readout
Quantizer
Sample and Hold
Shift Register / Rungler
Slew Limiter
STK Instrmnt / STK Effect

My GitHub page for all of these:
https://github.com/MattKuebrich/teensy-audio-objects
 
Last edited:
This looks like a very interesting suite of objects - it’d be nice to think that at some point there would be a Teensyduino update that curates these and the many other contributions into a major overhaul of the Audio library!

I’ve not had a chance to audition any of them, but did take a quick glance at some of the code, resulting in a few (by no means thorough) comments:
  • you’ve adopted the common practice of an early return from update() if an incoming block is not available. Even Paul does this, but it’s not always correct, as the documentation says a NULL block should be interpreted as silent, i.e. a block of zero-value samples. DC block, Comparator and Function Shaper are examples where the early return is clearly incorrect
  • Ring Modulator appears to be a less efficient duplicate of the existing AudioEffectMultiply
  • the Comparator modes are #defined as very common words, which is liable to cause users issues. You then don’t even use the defined symbols in your code! You should be using an enum for these, defined inside the AudioEffectComparator class so you have to use AudioEffectComparator::LESS etc. to refer to them
  • the Comparator update is very inefficient, checking the mode for every sample
  • In AudioEffectFunctionShaper there doesn’t appear to be a guard against the shaper function pointer being NULL. In fact, you don’t initialise it during construction, so it could be any random value! I’d suggest you initialise it to nullptr, and treat that as a pass-through
 
Last edited:
This looks like a very interesting suite of objects - it’d be nice to think that at some point there would be a Teensyduino update that curates these and the many other contributions into a major overhaul of the Audio library!

I’ve not had a chance to audition any of them, but did take a quick glance at some of the code, resulting in a few (by no means thorough) comments:
  • you’ve adopted the common practice of an early return from update() if an incoming block is not available. Even Paul does this, but it’s not always correct, as the documentation says a NULL block should be interpreted as silent, i.e. a block of zero-value samples. DC block, Comparator and Function Shaper are examples where the early return is clearly incorrect
  • Ring Modulator appears to be a less efficient duplicate of the existing AudioEffectMultiply
  • the Comparator modes are #defined as very common words, which is liable to cause users issues. You then don’t even use the defined symbols in your code! You should be using an enum for these, defined inside the AudioEffectComparator class so you have to use AudioEffectComparator::LESS etc. to refer to them
  • the Comparator update is very inefficient, checking the mode for every sample
  • In AudioEffectFunctionShaper there doesn’t appear to be a guard against the shaper function pointer being NULL. In fact, you don’t initialise it during construction, so it could be any random value! I’d suggest you initialise it to nullptr, and treat that as a pass-through
Thanks so much for the feedback, this is all super helpful. I'm "learning along the way" so thanks for bearing with me. I'd also love to see more objects in the official library and of course would love to contribute whatever I can.

As far as the "early return" (in say, Function Shaper), would I do something like this instead? Very unsure if that's correct. If not, what would be the proper way to do it? What would be the downside of the early return?

Code:
if (!block) {
    block = allocate();
    if (!block) {
      transmit(block); // ?
      release(block); // ?
      return;
    }
    for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) {
      block->data[i] = 0;
    }
  }

Gosh you're totally right about my ring modulator being basically the same as AudioEffectMultiply. I just removed that one.

Thanks for the tips on the comparator. I do think that having to use "AudioEffectComparator::LESS" in a sketch is kinda wonky. That initially confused me while using the AudioEffectDigitalCombine object, which does it that way. Maybe I'll do COMPARE_LESS or something, in the same wave the waveform object uses WAVEFORM_SINE.
 
Last edited:
Here's something that should work (it's untested...):
C++:
void AudioEffectFunctionShaper::update(void) {
    audio_block_t *block;

    block = receiveWritable();

    if (nullptr == block) // null block received, treat as silence:
    {
      block = allocate(); // get block to write to
      if (nullptr != block) // allocated OK...
          memset(block->data,0,sizeof block->data); // ...fill with silence
    }

    if (nullptr != block)
    {
        if (nullptr != shaper) // guard against unset shaper function
        {
            for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) {
                // normal shaper / limiter etc. here
            }
        }
        transmit(block);
        release(block);
    }
    else
    {
        if (nullptr != shaper) // guard against unset shaper function
        {
            for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) // ensure shaper keeps up-to-date state
                r = shaper(0.0f, r, a, b, c);
        }
        // nothing to transmit, we don't have a block
    }
}
An allocated block has stale audio in it, so you have to zero it yourself. If we can't get an output block, we still need to run the shaper function as it may store internal state. I've guarded against it being unset, though. The backwards-looking comparisons against nullptr are like that because (a) comparison to nullptr is the "correct" C++ way, which !block is not, and (b) if you mess up and accidentally type = instead of ==, you get a compilation error, not just a warning that you might miss.

An early return is a Bad Thing because in a complex function it's easy to lose track of bits of state that you should have updated but returned too early (this is especially true when you come back to maintain your code months or years later). A case in point is the existing AudioEffectEnvelope, which just returns if there's no audio available. Wrong. It has time-dependent internal state, so it needs to continue to track the DAHDSR progress. I've fixed this over a year ago, but the pull request is still pending...

I know AudioEffectComparator::LESS looks clunky, but ... tough. It guarantees you won't get a name clash. Ideally the AudioSynthWaveform objects would be re-written to do it properly, but they'd probably have to support the old macro symbols for a few years at least, to avoid breaking old code. In an all-new object there's really no excuse for not doing it right, to my mind.
 
Sorry for the delay. Thanks so much for the thorough response! I’ve tried the code you’ve provided - it seems to work and fails gracefully if the shaper function isn't set. I've updated functionshaper, comparator and dcblock based on your comments, along with a few others that were also using macros instead of enum.
 
This is great! I plan on incorporating some of these into a groovebox I'm making. I've just recently gotten dx7 / dexed emulation from the Micro Dexed Touch project ported over, for the most part. I'm posting videos of the progress on my personal YouTube channel.
 
This is great! I plan on incorporating some of these into a groovebox I'm making. I've just recently gotten dx7 / dexed emulation from the Micro Dexed Touch project ported over, for the most part. I'm posting videos of the progress on my personal YouTube channel.

Thanks! Let me know if you have any problems / questions with anything. Your xr-1 groovebox looks amazing and am enjoying the jams you've posted! I'll be following your progress.

On a related note, I've been screwing around with FM as too. The FM instruments in STK (of which I did a Teensy wrapper) are based on the Yamaha TX81Z and I spent some time seeing if I could turn that into a more general TX81Z emulator (object) for Teensy. But I found even the original STK FM instruments don't sound that much like the TX81Z presets they were based on, so I've abandoned that for the moment.

On an unrelated note, I love your cover of Arpanet's Wireless Internet on YouTube. That's also a favorite of mine!
 
Thanks! Let me know if you have any problems / questions with anything. Your xr-1 groovebox looks amazing and am enjoying the jams you've posted! I'll be following your progress.

On a related note, I've been screwing around with FM as too. The FM instruments in STK (of which I did a Teensy wrapper) are based on the Yamaha TX81Z and I spent some time seeing if I could turn that into a more general TX81Z emulator (object) for Teensy. But I found even the original STK FM instruments don't sound that much like the TX81Z presets they were based on, so I've abandoned that for the moment.

On an unrelated note, I love your cover of Arpanet's Wireless Internet on YouTube. That's also a favorite of mine!
Thanks, yeah I'm currently trying to get it to a good enough place to send some units out to some people so I can start getting feedback and improving it, I still want to add live recording, sample recording, and some other stuff, but just gotta prioritize things. I used to have a TX81Z, would love an emulator based on that, but I guess the Dexed / DX-7 emulation is basically that as well since I think the DX-7 and TX81Z have the same sound engine from what I gather. Yeah I love Arpanet / Dopplereffekt, got to see Dopplereffekt live earlier this year when I demoed the xr-1 at Machina Bristronica. I'll let you know how it goes, I think people will want more drum engine-y type functionality, so I'm interested in trying out the FM drum of yours to see if it'd be a good fit. I might incorporate the Teensy port of the MI Braids as well.
 
@mattkuebrich Would be so sweet if you or someone made a variant of Teensy's Delay effect with an input for synchronizing the delay time to a clock signal. Not sure how difficult it would be, I assume you'd need to know the clock division interval time in milliseconds, and you'd probably need to expose a method to update that interval if/when the clock changes or something.
 
@mattkuebrich Would be so sweet if you or someone made a variant of Teensy's Delay effect with an input for synchronizing the delay time to a clock signal. Not sure how difficult it would be, I assume you'd need to know the clock division interval time in milliseconds, and you'd probably need to expose a method to update that interval if/when the clock changes or something.

Seems like you're making good headway with Pio in the other thread. I was going to mention before I saw Pio's progress - if you’re wanting to change the delay time on the fly, it might be better to start with houtson's delay10tap object or h4yn0nnym0u5e's modulated delay object from this thread. delay10tap is less clicky when you adjust the delay time, say with a pot, than the library delay. Haven't tried h4yn0nnym0u5e's object but could be possible to use that for what you want without messing with the actual audio object code, not sure.
 
Back
Top