Soft-patchable synth - Proof of Concept

h4yn0nnym0u5e

Well-known member
Well lookee here ... it's yet another Teensy-based synth ... or might be, if I ever finish it. But it says "Blog" in the category heading, so this may serve to stimulate discussion if nothing else, and if I'm moved to do so I can report progress as it occurs.

For nearly 4 years now I've been tinkering with Dynamic Audio Objects, that can be created and destroyed at run-time, unlike the existing Teensyduino which can't destroy them without Bad Things happening. I'm not much of an application maker, though, so I've never really done much that's useful with them, but it occurred to me that I should, as a demo to self as much as anything. Hence the following.


In Real Life
AudioPatcher Mk I.jpg


In CAD
1741449294118.png



The hardware consists of:
  • Teensy 4.1 with 8MB PSRAM; SD card slot accessible through the back of the case
  • Teensy USB host cable
  • Audio adaptor
  • 3.5" ILI9341 TFT screen with XPT2046 touch screen
  • M5stack I²C controls
    • 8angle: 8 pots, 8 RGB LEDS, 1 switch
    • 8encoder: 8 encoders with momentary switches, 8 RGB LEDS, 1 switch
  • miscellaneous connectors, some yet to be wired up...
The software provides 6 screens, selected by the left-most encoder:

Object placement
00006.png
Place patchcords
00005.png
Edit objects (settings and position)
00004.png
Modify objects' MIDI settings
00003.png
Delete objects or patchcords
00002.png
Filing: save and load patches
00001.png


A few objects are hard-coded into the system at compile time; these are effectively the hardware definition, so in the screen dumps above you see the I²S input on the left, a control object for the SGTL5000, and an I²S output object at the right. They're movable, but can't be deleted. Everything else is created at run-time.

"All very well", I hear you say, "but what about polyphony? That's just a single voice!". Well, yes and no. Objects can be marked as needing to be instantiated per-note, as you can see in the detail dump here - the coloured dots at the top right mark those objects.

00009-crop.png


The first note you play uses the objects already available, but subsequent notes have a new set of objects and their associated patchcords created at note-on time. Any patchcord coming from a single-instance object is cloned - this might be for a global LFO, for example. Those going out must be connected to a mixer, and there must be an unused input on that mixer, otherwise voice creation fails - you've reached the limit of the available polyphony. You can see the latter above: chor connects to the first input of mixS, which defaults to 8 inputs, so this is an 8-note synth. [I haven't implemented it yet, but mixS is a new stereo mixer in the Dynamic Audio Objects, and can be constructed with more than 8 inputs, so I could provide a facility to increase the polyphony].

Some time after note-off occurs, the voice will become inactive, at which point its objects will be destroyed, unless of course they're the "already available" ones - those are only destroyed during editing, or when you load a different patch from the filesystem.

MIDI commands are accepted from either the PC or via the Host port. Much remains to be done on the MIDI settings side, but the plan is to have all useful settings configurable to any available Continuous Controller, with various mapping curves available, e.g. exponential for envelope times, linear for envelope sustain level, etc. All messages are broadcast to every object, so a single CC can do multiple things if you want. In the future...

Much remains to be done:
  • full implementation of all [relevant] audio objects, complete with internal and MIDI settings
  • corner cases like
    • loading arbitrary waveforms into AudioSynthWaveform objects
    • implementing AudioSynthWavetable objects using samples loaded from SD
  • make better use of the M5stack LEDs
  • fix the annoying ticking due to display activity (probably hardware, that)
  • MIDI patch selection, SysEx etc.
  • figuring out and implementing a decent sample playback UI
  • lots of niggly bugs
  • documentation :(
  • stretch goal ... there's no reason this can't be made multi-timbral :cool: just a Small matter Of Programming
Anyone who wants to look at some truly horrible code can find stuff at https://github.com/h4yn0nnym0u5e/AudioPatcher. The structure is awful and there aren't enough comments, and so far there's no documentation on how to build it yourself. You have been warned. If you're nuts enough to want to build it, you need Dynamic Audio Objects and the corresponding cores changes, my M5stack library, and @MarkT's excellent Hammond vibrato / chorus.

If anyone has made a synth they're particularly proud of (I know you're out there...), and wants to drop me a copy of their design as a single voice (i.e. something I can import into the Design Tool), along with an indication of which objects are global and per-voice, plus maybe some settings that they like, I'd love to drop it into this and see if it works OK. I've looked at a few I know of, but disentangling the 1-voice version from the multi-voice one has so far proven too much to bother with :).
 
got a bit fed up with soldering and breaking things so I’ve went down the M5Stack for my prototype

Dare say I’ll be in the M5Stack library later this week.

Did you 3d print the case?
 
Dare say I’ll be in the M5Stack library later this week.
Good luck! Do ask if you need help.
Did you 3d print the case?
Yes, one of my earlier and not-so-fine efforts, though with a bit of tape it gets the job done for now.

The project as a whole has stalled for a while now. I got a bit stuck on the UI for loading SoundFont and Dexed voices, plus I’ve been distracted by another project I’m helping with.
 
I’ve not printed anything before so I’ll protype it on a board then look at the enclosure. Or I’ll break something and try yet another path 🙈
 
The display ticking is almost certainly SPI contention with audio DMA rather than a hardware layout problem
It’s almost certainly not… I’ve experienced DMA contention with audio, and it’s not just “annoying ticking”, it’s horribly garbled. This ticking is only really perceptible when all synth voices are silent, and is synchronised with the free memory and CPU usage monitor you can see at the bottom right. I also get a burst of noise when the edit area changes, of course.

I note your linked write up basically copies my original post, and fails to link either to this thread or my repository. None of the links in the write up enable the reader to implement this concept.
 
Here's some test code:
C++:
/**************************************************************************
 Teensy 4.1 basic DMA thrash test
 MicroDexed setup uses PCM5102; add a control object
 if your audio hardware requires it.
 **************************************************************************/
#include <Audio.h>

// GUItool: begin automatically generated code
AudioSynthWaveform       wavM;           //xy=140,202
AudioSynthWaveform       wav1;           //xy=281,167
AudioSynthWaveformModulated wav2;   //xy=312,213
AudioOutputI2S           i2sOut;         //xy=459,191

AudioConnection          patchCord1{wavM, 0, wav2, 0};
AudioConnection          patchCord2{wav1, 0, i2sOut, 0};
AudioConnection          patchCord3{wav2, 0, i2sOut, 1};

// GUItool: end automatically generated code

//======================================================================
#define FREQ1 440.0f
#define FREQ2 660.0f
#define FREQM   0.1f

/*
 * MicroDexed has a PCM5102, which is muted by default
 * The pin is labelled 5102MUTE, but actually needs to be
 * high to UNmute.
 */
void unMute(bool unmute = true)
{
#define MUTE_PIN 34 
  pinMode(MUTE_PIN,OUTPUT);
  digitalWrite(MUTE_PIN,unmute);
}

//============================================================
// Infrastructure for DMA thrashing
#include <DMAChannel.h>
DMAChannel dma{false}; // allocate but DON'T begin()

#define MEM_SIZE 2'000
DMAMEM uint32_t g_int1[MEM_SIZE];
DMAMEM uint32_t g_int2[MEM_SIZE];

uint32_t thrashTime;

void thrashDMA(int cycles);
static void dma_isr(void);
//============================================================
void setup()
{
  pinMode(LED_BUILTIN,OUTPUT);
  AudioMemory(400);
  Serial.begin(9600);
 
  unMute();
    
  wav1.begin(0.01f,FREQ1,WAVEFORM_SINE);
  wav2.begin(0.01f,FREQ2,WAVEFORM_SINE);
  wav2.frequencyModulation(1.0f);
  //wavM.begin(1.0f,FREQM,WAVEFORM_SINE);

  // Use jmarsh update with second parameter set
  // to true to make this channel pre-emptible:
#define noJMARSH_DMA
#if defined(JMARSH_DMA)
  dma.begin(false, true); // stops clicking: audio pre-empts memory copy
#else
  dma.begin(); // start the DMA channel - standard cores implementation
#endif // defined(JMARSH_DMA)

  dma.attachInterrupt(dma_isr);
}


//---------------------------------------------------------------
volatile int ISRcount;
void loop()
{
  static int count;
  count++;

  elapsedMillis em = 0;

  if (em < 100)
    delay(100 - em);

  thrashDMA(2); // set to 1 cycle to stop clicking
  if (em < 200);
    delay(200 - em);

  Serial.printf("%u: ISRcount: %d; loop count: %d; thrash time: %uus\n",
                millis(), ISRcount, count, thrashTime);
}


static void dma_isr(void) {
  static int count;

  ISRcount ++;//= 10;
 
  if (++count == 10'000) {
    count = 0;
    digitalToggleFast(LED_BUILTIN);
  }
  dma.clearInterrupt();
  asm volatile ("dsb");
}

void thrashDMA(int cycles)
{
  int tmp = ISRcount;
  dma.triggerContinuously();
  dma.interruptAtCompletion();
  dma.sourceBuffer(g_int1, sizeof g_int1);
  dma.destinationBuffer(g_int2, sizeof g_int2);
  dma.triggerAtCompletionOf(dma);
  dma.enable();
  //delayMicroseconds(us);
  dma.triggerManual();
  elapsedMicros eu = 0;
  while (ISRcount - tmp < cycles )
    if (eu > 50'000) break;
  thrashTime = eu;   
  dma.disable();
}
//===============================================================
extern "C"
void startup_late_hook(void)
{
  while (!Serial)
    ;
}

As it stands, it only produces an annoying ticking! But that's just because of the way it's written to show the issue - a more irregular and frequent use of DMA would make the audio corruption worse, of course.
  • tested on a MicroDexed synth, which uses a PCM5102; you'll want to add any required control object for your hardware
  • change the parameter for the thrashDMA() call in loop() to 1 to make the clicking stop - the DMA then doesn't run long enough to starve the audio. Conversely, increase to make it worse.
  • pull in the changes discussed on this thread, and define the JMARSH_DMA macro at line 66 to allow the DMA memory copy to be pre-empted by the audio DMA; you can now increase the thrashDMA cycle count freely, as it no longer interferes
  • I tried using the ILI9341_t3n async updates to reproduce the issue, but it didn't; not sure why
  • reverting to an older version of the Audio library (without your recent "recover after stalling" changes) irretrievably halts audio output, as you might expect
 
Back
Top