[queued] Loop pedal with procedurally generated beats.

I just finnished a school assignment in a Human-Computer Interaction course, where I am looking at new ways to make instrument practice more engaging.

I made a loop pedal that:
  • Generates random rythms based on pre-made distribution templates, using a "picking without replacement" algortihm.
  • Generates random drum sounds from synthesis (using the Teensy audio library).
  • Allows the user to record a chord progression to jam along with.

Here is a video demonstration:

The full source code (along with much more information) can be found on my github:
https://github.com/MariusIrgens/LoopPedal

All comments are welcome!
 
I Forgot to add a picture! Sorry.

276585523-f668eea8-f432-497c-9add-428f51fd7747.png
 
Very cool - that would make practice ( if nothing else ) more productive and enjoyable.

Are any other the other inline devices seen in the video required?
 
If by devices you mean the other pedal in the background, its just an Electro-Harmonix Cathedral Reverb. The guitar runs through it before the loop pedal input, and it is not required, just there to make the sound a bit more interesting. It is possible to have fx on the input both pre and post loop pedal, and it is possible to also have fx on the drums post loop pedal.

The only thing required to build the loop pedal and make it work is:
Teensy 4.0
Audio Adapter Board
SD card (128 mb should be more than enough)

The potentiometers, rotary encoder, LED lights and foot switches can be omitted or handled differently. Right now they just call interrupts in the interactionManager class.
 
Great to see and hear the finished project!

I noted your comment in the ReadMe "I wanted to be able to record multiple loops on top of each other, but since the SD card can not be read from and written to simultaneously, this unfortunately could not be implemented. If anyone knows how to fix this, please let me know." The main issue you'd encounter is that the AudioPlaySdRaw class you use does SD reads inside the audio update interrupt, which causes mayhem when you also try to do SD writes from your recording loop in the foreground code. There are a number of ways around this: you could use the AudioPlayQueue object for playback, instead of AudioPlaySdRaw, as this would let you have full control of interleaving SD card reads and writes. It's "simply" a matter of having enough audio queued up for the times the SD card takes a few extra milliseconds to do a read or write operation. Another route would to be to use something like my buffered playback / record objects, which are intended for just this sort of use.

I couldn't quite make out why you needed to make your own envelope class, though I am aware that the standard one does have some shortcomings. It'd be interesting to know what you changed, or what new features you implemented.
 
Thank you! I will definitely try to update my looper code with the queue object. I was actually looking into that originally, but was struggling to make it work properly. Would you happen to have a link to some example code implementing it?

The envelope in the audio library seemed to only be able to modulate specific parameters. I wanted an envelope I could route to any parameter I wanted - pitch, frequency, filter cutoff, etc. I also wanted to be able to change the curve shape, and some other things.
 
I'm not aware of an example using AudioPlayQueue for SD card playback. Here's a starting point, though I realise there are some issues with expanding it to a more complex design:
Code:
/*
 * Simple demo of playing audio from SD card, without use of the 
 * AudioPlaySdWav class (which accesses the card under interrupt).
 */
#include <Audio.h>

// GUItool: begin automatically generated code
AudioPlayQueue           queue;         //xy=313,217
AudioOutputI2S           i2sOut;           //xy=492,218

AudioConnection          patchCord1(queue, 0, i2sOut, 0);
AudioConnection          patchCord2(queue, 0, i2sOut, 1);

AudioControlSGTL5000     audioShield;    //xy=479,279
// GUItool: end automatically generated code

#define LOW_WATER 10 // re-load queue when we only have this many blocks in it
#define LOAD_SIZE 20 // load this many blocks to queue

//=============================================================
bool reload(File& f, AudioPlayQueue& q)
{
  bool result = true;
  size_t sampleCount = LOAD_SIZE * AUDIO_BLOCK_SAMPLES;
  int16_t buffer[sampleCount]; // enough space for a full reload
  size_t got = f.read(buffer, sampleCount * sizeof *buffer); // get some data: got is byte count...
  got /= sizeof *buffer; // ...and now sample count

  for (int i=0; i < LOAD_SIZE && got > 0; i++)
  {
    if (got >= AUDIO_BLOCK_SAMPLES) // we have a full block...
      got -= AUDIO_BLOCK_SAMPLES;
    else // not a full block: file must be exhausted
    {
      result = false; // say playback's finished
      memset(buffer + i*AUDIO_BLOCK_SAMPLES + got,0,(AUDIO_BLOCK_SAMPLES - got)*sizeof *buffer); // silence the end and...
      got = 0;
    }
    q.play(buffer + i*AUDIO_BLOCK_SAMPLES, AUDIO_BLOCK_SAMPLES);     // ...transmit to audio
  }

  return result;
}


//=============================================================
void setup() 
{
  AudioMemory(50);  // plenty of space for audio buffering
  
  audioShield.enable();
  audioShield.volume(0.1);

  SD.begin(BUILTIN_SDCARD); // change to suit your hardware
}


//=============================================================
enum {starting, playing, waiting} state;
uint32_t waitStarted;
File audio;
const char* fname = "sweep100-1k.wav"; // change this to a file you have!

void loop() 
{
  switch (state)
  {
    case starting:
      audio = SD.open(fname);
      state = playing;
      break;

    case playing:
     [COLOR="#FF0000"] if (AudioMemoryUsage() < LOW_WATER) // not ideal[/COLOR]
      {
        bool fileOK = reload(audio,queue);
        if (!fileOK)
        {
          audio.close();
          waitStarted = millis();
          state = waiting;
        }
      }
      break;

    case waiting:
      if (millis() - waitStarted > 1000)
        state = starting;
      break;
  }
}
The main issue is that there's no way of telling how full the queue actually is. For this demo I've just assumed it's pretty much the only user of audio blocks, so if the overall usage drops below LOW_WATER it's time for a re-load. A better implementation would be to use AudioPlayQueue::setMaxBuffers() in setup(), then if play() returns a non-zero value when you try to re-load the queue, your code has to try again later. You could either keep the loaded audio buffered, or seek() the audio file back to the first non-queued sample. Or add a blocksQueued() function to the AudioPlayQueue class.

The envelope can modulate anything that has a modulation input - just connect an AudioSynthWaveformDc object to the envelope input, and its output to the modulation input. You might need to add a mixer if you want bi-polar modulation. Having said that, when you start wanting to make it curved using a filter, you might as well create your own object which does exactly what you want!
 
Last edited:
Thanks! I will see if I can implement this at some point. I just need to finish my master thesis first.

Do you know, if I use a Teensy 4.1 with multiple SD cards, if it would manage to read and write to them simultaneously? I would like to add sampled drumsound playback. Or, if I would load samples into the Teensy RAM, would it be able to play them back polyphonically, while also recording/playing the loop with the SD card?
 
You can read / write multiple SD cards, but as the code is blocking it won't get you any extra speed. The on-board SD uses a fast SDIO interface so that's the best one to use; a decent card will get about 20MB/s transfer speed, but that drops significantly if you're accessing multiple files. I reckon you should be able to play 16 mono while recording 2 stereo files ... a diligent forum search (actually, use Google, the forum's built-in search is rubbish) will reveal how I know this.

Teensy RAM would top out at a total of 11s sample time maximum - probably significantly less once your code uses some. If you fit PSRAM to a Teensy 4.1 you have about 90s to play with. Playing polyphonically from there will be no problem, it's just copying memory around. Use AudioProcessorUsage() (see https://www.pjrc.com/teensy/td_libs_AudioProcessorUsage.html) to track the CPU load.
 
OK, so theoretically, I should not have any problems playing at least 8 mono files, a loop, and even record on top of a loop, as long as I work a bit more on the code?
 
Should be OK, with a bit of work, I’d think.

If you have or can borrow one, an oscilloscope is really handy to look at when and how long SD reads and writes are by putting spare outputs high and low at times of interest. Much easier than trying to do it with serial output!
 
Back
Top