Trinket USB MIDI Multi-Pot Mozzi Synthesis – Part 2

This is a first go at getting polyphony running on my Trinket M0 USB Mozzi synth.  It is very much a work in progress though as I’ve just not managed to get the right balance of the different aspects of the system running together yet.  But I thought I’d pause and report what I have so far.

2021-03-27 15.53.18

Warning! I strongly recommend using old or second hand equipment for your experiments.  I am not responsible for any damage to expensive instruments!

These are the key tutorials for the main concepts used in this project:

If you are new to microcontrollers, see the Getting Started pages.

Parts list

  • Adafruit Trinket M0 (or probably any other Adafruit M0 board)
  • 1x 10uF non-polar capacitor (optional)
  • 2x 1kΩ resistors (optional)
  • 4x 10kΩ potentiometers
  • 1x 220uF capacitor (optional)
  • 3.5mm jack socket (optional) and amplification
  • Breadboard and jumper wires

The Circuit

TrinketUSBMIDIMultiPotSynth2_bb

This is using exactly the same circuit as for the Trinket USB MIDI Multi-Pot Mozzi Synthesis, but I seem to have solved my USB “noise” issue.

First of all please remember, I am not an electronics person, so remember my warning and seek some knowledgeable advise yourself if you start experimenting!

So, I properly grounded the USB connector of the USB MIDI keyboard.  By that I mean having the USB outer casing, the breadboard and the negative from the USB power supply all linked to a common ground/earth point that I know was connected to the mains earth (in my case via a 1MΩ resistor, similar to what you’d find in an anti-static hook-up).  I was being careful not to create ground loops here, but experimented with how to connect the respective bits back to ground and seem to have found something that works for me.

This greatly reduced the audible noise, but it was pretty much eliminated completely by also adding a 220uF electrolytic capacitor across the GND and USB connections for the Trinket M0.  Be really careful how you add this of course – it needs to be as close to the Trinket’s pins as possible (and connected to USB not 3V/VCC due to how I’m powering things), but naturally mustn’t touch any of the circuitry nearly.

Mine seemed to fit quite nicely just underneath the USB connector as shown below.

2021-03-27 15.53.42

At this point, everything else was pretty much as described in the Trinket USB MIDI Multi-Pot Mozzi Synthesis.

Warning reminder: The “top” +ve rail of the breadboard is running at 5V and MUST NOT BE CONNECTED TO THE REST OF THE CIRCUIT which is all at 3V.

The Code

The starting point is the code as described in Trinket USB MIDI Multi-Pot Mozzi Synthesis – complete with the option to build for a USB host or a USB device.  The aim is to get the Trinket as a “stand alone” USB MIDI synthesizer as much as possible, so I’m building it to act as a USB host for a MIDI device.

The basic principle is the same as almost any of the other projects on this site for adding polyphony: maintain a list of “output oscillators” and when a noteOn is received, find a free “slot” and play that corresponding frequency.

Of course with this being Mozzi, it is slightly more complicated, but the basic principle still stands.

I am maintaining independent oscillators for the carrier frequency and the FM modulation.  There is only one oscillator serving all notes when it comes to modulating the FM intensity. I also need to maintain an independent envelope generator for each note too.

#define NUM_OSC 4

Oscil<COS2048_NUM_CELLS, AUDIO_RATE> aCarrier[NUM_OSC];
Oscil<COS2048_NUM_CELLS, AUDIO_RATE> aModulator[NUM_OSC];
Oscil<COS2048_NUM_CELLS, CONTROL_RATE> kIntensityMod(COS2048_DATA);

ADSR <CONTROL_RATE, AUDIO_RATE> envelope[NUM_OSC];

Some of the functions now need to be updated to either set a parameter for all oscillators or update a single oscillator.  setWavetable() will update the wave table for all notes – as for a polyphonic synth, we want all notes starting with the same basic waveform.  setFreqs() will update the carrier and modulation frequency for a single oscillator depending on which note has just been played.

As before, all handling of pots and updating of control parameters happens in the updateControl() function, and the final summing of sounding oscillators happens in the updateAudio() function.

void updateAudio() {
long output=0;
long fmmod = aSmoothIntensity.next(fm_intensity);
for (int i=0; i<NUM_OSC; i++) {
long modulation = fmmod * aModulator[i].next();
output += (envelope[i].next() * aCarrier[i].phMod(modulation));
}
output = output >> 3;
return MonoOutput::from16Bit((uint16_t)output);
}

Now as we’re adding together a number of signals some scaling is required.  Note that I’m not shifting the envelope.next() * aCarrier[].phMod(modulation) result, so this will have a full 16-bit range (multiplying two signed 8-bit numbers together could give a maximum of +/- 32767), so I’m performing all the arithmetic as a “long” (32-bit) value.

However, adding together (in this case four) 16-bit values could theoretically result in any number between 0 and +/- 131071, which I believe needs an 18-bit number (my binary maths is getting rusty now).  More oscillators will give an even bigger number, so some scaling is required to get them back to a range suitable for Mozzi.

One thing that has come in the Mozzi library since I last had a proper look is the from8Bit() and from16Bit() helper functions which will return a value in the appropriate native scaling required for your platform. In my case, a bit shift of 3 gets me to a 16-bit value (I might get away with 2, but have played safe); then MonoOutput::From16Bit will scale it as required by the Mozzi outputAudio function.

As the number of oscillators used changes, the amount to shift to get back to 16-bits will change, but I’m not worrying about that right now.

A key issue with all this is how to balance the time spent in the various parts of the system:

  • loop() calls the main Mozzi audioHook() function which performs most of the Mozzi processing.
  • updateControl() has to handle the potentiometers, USB host stack, MIDI handling, and updating of the CONTROL_RATE oscillators and envelop generators.
  • updateAudio() has to produce the final sample for Mozzi to send out to the audio output.

I spent quite some time tweaking the various settings and structure of updateControl and still haven’t really managed to get it right.  If CONTROL_RATE is too slow, it misses MIDI notes.  If CONTROL_RATE is too quick, I get noise in the output.  If I adjust the handling within the updateControl function, I find there are limits anyway as the intensity modulation and envelopes have to be updated every time (at “CONTROL_RATE”) anyway.

So, I’m not there yet, but thought I’d write up where I’ve got to so far.

Find it on GitHub here.

Advanced Discussion: Mozzi Threads of Execution

I’ve been down several “rabbit holes” whilst attempting to get the right balance of the three threads of execution.  My initial guide was this “under the hood” page on the Mozzi site, but as I dig deeper into the code I’ve realised this is no longer accurate as there was a change back in 2018 to move the main control loops out of a timer interrupt and into the audioHook.  This doesn’t seem to have caught up with the documentation though, so here is what I know so far.

When things all get consistent again, I’ll probably remove this.

Here is the “pseudo code” representation of what is going on (mostly in MozziGuts.Cpp) for the SAMD21 architecture.

Arduino's loop() function calls: audioHook()

audioHook():
IF audio output ring buffer isn't full THEN
advanceControlLoop()
bufferAudioOutput (updateAudio()):
This adds samples to the ring buffer

advanceControlLoop():
IF control counter has expired (initially set to AUDIO_RATE/CONTROL_RATE) THEN
updateControl()

SAMD21 TC5_Handler() running @ sample rate (32768Hz):
samd21AudioOutput -> defaultAudioOutput:
Read a value from the audio ring buffer
audioOutput:
analogWrite to the DAC

So in reality, both updateControl and updateAudio are actually run from the audioHook, running “as fast as possible” from the Arduino’s loop() function.  But updateAudio will keep running until the ring buffer is full at which point it stalls until the interrupt handler has had a chance to empty it.

This is shown below – recall this is just my current understanding (until the official documentation catches up with the latest code changes).

Mozzi-Under-The-Hood

One consequence of this though, is that, as far as I can see, CONTROL_RATE isn’t actually called on a regular, periodic CONTROL_RATE frequency. It is called every (AUDIO_RATE/CONTROL_RATE) runs of the audioHook, providing that the ring buffer is being emptied ok.

In the end, I found that a CONTROL_RATE of 2048 (which is a lot higher than any of my Arduino projects) seems to give a good chance that the MIDI handling can function without missing notes.  It still misses the odd note, so I’m still not there yet.  But with an AUDIO_RATE of 32768 this means I have the CONTROL_RATE running once every 16 calls to the audioHook().

Using the Trinket’s built-in LED for some timings, I’ve made some approximate measurements:

  • audioHook period = 3.25uS minimum, so the loop() is running with a frequency of around 300kHz (at its fastest).
  • updateAudio time to run = 7.5uS minimum execution time, plus 10-20uS “down time”, which gives it a frequency of something like (according to my scope) 30-32kHz.
  • updateControl time to run = 400uS minimum, with a typical “not running” time of around 720uS.

Taking these values as a guide it means that when the control thread is running, the updateAudio execution (period approx 31uS) is paused for at least 10-15 runs.  Presumably if the ring buffer is acting properly, this should give plenty of time to catchup in the “slack” time between control update runs… but this also might mean that the “control rate” might sometimes catch up with itself (recall it is running at 1/16 of the AUDIO_RATE), so that might be a source of some of the audio issues – I guess it depends if this ever “stalls” the filling of the audio buffer.

What I really need though is a logic analyser trace of the three threads interacting to really see how long they are taking, but I don’t have enough IO on a Trinket to try that.  I might have to get an ItsyBitsy M0 🙂

So, the “todo” list:

  • Get some proper timing measurements of the three threads of execution under different circumstances (after buying a new board!).
  • Graph out the relative execution times of the three threads.
  • Examine the call chain from MIDI.read() through the USH2-MIDI code into the Arduino MIDI library and the USB Host Library to see if there are any areas where USB/MIDI data might get lost.  I need to re-test, but I didn’t seem to have MIDI loss issues when acting as a USB device rather than host.
  • Work out how often I need to call UsbH.Task().  I’m not convinced it needs to be at CONTROL_RATE.
  • Try some non-power of two CONTROL_RATES.  1024 was losing data, but maybe 2048 is just too quick.
  • Find a way to log if the audio ring buffer ever stalls due to overrunning control updates.
  • Look into writing a mozziAnalogRead() function for an asynchronous ADC read for the SAMD21 for better performance.
  • Consider switching some of the USB/MIDI or control handling to another thread of execution somehow (although I’m not sure if this is even possible).

Closing Thoughts

I’m slowly getting to understand a bit what makes Mozzi tick, but don’t really have a feel for how much is possible on the SAMD21 based boards yet. Mozzi is under active development all the time, so it is interesting to watch it all evolve, even as I’m playing myself!

But the idea of some complex Mozzi polyphony is tantalisingly close!

Kevin

2 thoughts on “Trinket USB MIDI Multi-Pot Mozzi Synthesis – Part 2

  1. Hi! Nice blog you have here! I am currently struggling with bitshift operations for the output as I add oscillators and LPFs, but getting there I think.
    Why don’t you read midi signal on updateAudio instead?? I did that so my synth don’t miss any of the incoming midi signals.

    Like

    1. The thing with bitshifts (apologies if you already know this) is that each shift to the right is the same as a divide by two. When you are combining signals, a simple addition means your two 0 to 255 level signals coming together become a 0 to 512 peak level signal worst case. The average case will still be lower of course. A single bit shift right will divide this by two again though to return it to a 0 to 255 signal, but you will lose some resolution.

      If you are multiplying signals then your worst case is 255*255 so you will have to divide by 256, i.e. bitshift by 8 to get it back to the 0 to 255 range. There are options in more recently Mozzi to keep higher resolution in the calculations and return to a “native” bit depth only at the last minute which makes things a lot simpler moving between architectures.

      With regards to audioRate, the idea is to keep this as regular as possible to keep the audio samples coming. I guess it was more critical when this used to be called from different timers, but as a general rule, the audio sample processing needs to happen a lot quicker than the MIDI handling, hence why I consider it as probably more of a control thing. But I guess it depends on the trade-offs you want to take – more responsive MIDI vs more accurate sample playing. If we do too much in the audio update there is a chance that the sample buffer will stall producing clicks or pops or other blips in the audio output.

      Kevin

      Like

Leave a comment