A subharmonic synthesiser and polymetric sequencer with Teensy 4 and audio library

Status
Not open for further replies.

kallikak

Active member
Hello.

I thought I'd share a project I've been working on for the last 2 months - A subharmonic synthesiser and polymetric sequencer with Teensy 4 and audio library inspired by Euclidean Rhythms, and the Moog Subharmonicon (and standard subtractive synthesis).

Euclidean rhythms use the Euclidean algorithm to describe how to best allocate some number of beats within a bar of arbitrary size. This connection has only recently come to light - see http://cgm.cs.mcgill.ca/~godfried/publications/banff.pdf.

Additionally, the Moog Subharmonicon is a very recent synthesizer that uses polyrhythms to selectively trigger across two 4-note sequencers. Very interesting musical patterns are created by the changing interleaving when the rhythms share no small common divisors.

I came across these two things at about the same time and thought the double sequencer idea could well be joined with a Euclidean rhythm generator to make an interesting synthesizer. Using a Teensy 4.0 and the Audio shield and library, I created "The Euclidean" - a 4-voice subharmonic synthesizer and polymetric sequencer.
Here's some pics from while I was building it, and the final prototype (outside of its case still)

Euclid1.jpg
Euclid2.jpg
Euclid3.jpg
Euclid4.jpg

Brief specs are:
  • 2 primary oscillators supporting various waveshapes, each with an associated sub-oscillator with frequency determined by dividing the primary frequency by an integer from 2 to 16
  • frequency modulation, pulse width modulation, quantisation
  • AD envelope
  • LFO (with multiple waveshapes)
  • Resonant low-pass filter with (positive and negative) envelope and LFO modulation
  • Chorus, Flanger, Delay effects
  • Three independent "Euclidean Rhythm" generators
  • Two independent 4 note sequencers - each driving one of the oscillators, and driven by various combinations of rhythm events.
  • +/-24 semitone steps for each sequencer
  • MIDI and Serial integration
(Full specs are at the end of this post.)

Here's some videos of it running.

Demo of presets
Making a patch

MIDI keyboard integration

This is still very much a work in progress, but I'm really happy having reached a milestone where it is so usable and fun. :)

Ken

Specifications:
  • Two independent oscillator units.
    • 2 unison primary voices (detunable - light, medium, heavy)
    • waveshapes: Sine, Triangle, Square, Sawtooth, Variable Width Pulse
      - The Triangle, Square and Sawtooth waves are bandwidth limited to minimise aliasing.
    • 2 unison sub-voices with frequency determined by dividing the primary frequency by an integer from 2 to 16
    • sub oscillator waveshapes: Sine, Triangle, Square, Sawtooth
    • independent level control of each oscillator (plus toggle for mute)
    • Optionally quantise the frequency to any one of:
      - 12 note chromatic scale,
      - 8 note diatonic major or minor scale,
      - 5 note pentatonic major or minor scale,
      - 8 note just intonation major or minor scale,
      - 8 note Pythagorean major or minor scale.
    • Transposable +/- 24 semitones via a panel control, or arbitrarily via a MIDI keyboard.
  • LFO
    • Optionally modulate any of:
      - oscillator frequency,
      - pulse width,
      - Filter cut-off
    • waveshapes: Sine, Triangle, Square, Sawtooth, Reverse Sawtooth, Sample and Hold
  • Low-pass resonant filter
    • adjustable cut-off frequency
    • adjustable resonance
    • positive and negative adjustable envelope response
  • Envelope
    • There are three independent Attack/Decay envelopes, one for each oscillator group, and one for the filter.
    • Although independent, their settings are common
    • When being played manually, the envelope becomes ASR
  • Effect presets (slow/medium/fast)
    • Chorus
    • Flanger
    • Delay
    • Chorus + Delay
  • Rhythm
    • Three independent "Euclidean Rhythm" generators
    • set the number of main beats, and the bar length for each
    • the maximum bar length is 32
    • the tempo ranges from 30bpm to 1200bpm
  • Sequencers
    • Two independent 4 note sequencers
    • Specify offset for each step from -24 to +24 semitones
    • Apply to main frequency, sub-frequency divisor or both
    • When applied to sub-frequency divider, the offset is divided by 3 (so from -8 to +8)
    • Sequencer 1 drives oscillator 1, and sequencer 2 drives oscillator 2
    • The sequencers can be set to progress on a beat from any of the 3 rhythms, offbeats, or both.
    • The sequencer play mode has three states:
      - Run - progress as specified according to the rhythm
      - Hold - play and hold a single step (useful for individual oscillator tuning)
      - Pause - repeat the current step until pressed again (useful for joint oscillator tuning)
  • Presets
    • Up to 64 presets can be saved on an SD card
  • MIDI
    • If any sequencer is inactive (either because stopped or not set to advance) then the corresponding oscillator (and sub-oscillator) will play according to an incoming MIDI note.
    • If both sequencers are active, played MIDI notes transpose the sequences.
    • <COMING SOON> incoming/outgoing MIDI clock
  • Serial connection
    • A serial connection allows some control from a computer
    • Presets can be queried and restored from backup using serial commands
 
Last edited:
That's inspirational - and very professionally constructed too. I don't think I've seen those push-switches with the LED(s) poking through
since the 1980's!
 
Yes, really a very nice project, certainly worth being featured on the blog. I'm curious about how the band-limited waveforms were written and integrated to work with the rest of the audio library. Can you elaborate? Thanks.
 
Yes, really a very nice project, certainly worth being featured on the blog. I'm curious about how the band-limited waveforms were written and integrated to work with the rest of the audio library. Can you elaborate? Thanks.

Yeah - the aliasing is a pain.

At the moment I generate wavetables for sawtooth, square and triangle waves using a modification of some code from the forum (see below), but I intend to refine things much more.

Specifically, the aliasing on the Pulse wave is quite bad, but it can't be solved in the same way (it would need many wavetables corresponding to different pulse widths). Instead I am working on generating the pulse wave using a sawtooth and an offset reversed sawtooth. This is quite straightforward, even though the wavetable needs to be updated each time the pulse width is varied. What is trickier is to handle the modulation of the pulse width. I will look into that too, but having that work alias-free at LFO frequencies is not at the top of my priorities at the moment.

I am also inclined to try out a PolyBLEP or similar solution. My initial quick effort was not as successful as the wavetable approach, but I expect when I get time to do it properly it will be the better option.

Ken

Code:
//  Modified version of code created by Gustavo Silveira on 1/26/17.
//  Copyright © 2017 Gustavo Silveira. All rights reserved.
//  Sawtooth wavetables generator

#include <fstream>
#include <sstream>
#include <iostream>
#include <string>
#include <math.h>

#define NYQUIST_REDUCTION_FACTOR 0.6

using namespace std;

float scaleBetween(float unscaledNum, float minAllowed, float maxAllowed, float min, float max) 
{
    return (maxAllowed - minAllowed) * (unscaledNum - min) / (max - min) + minAllowed;
}

float midiNoteToFreq(int n)
{
    return 440.0 * pow(2, 1.0 * (n - 69) / 12);
}

const int32_t sampleRate = 44100;
const int16_t nyquist = NYQUIST_REDUCTION_FACTOR * (sampleRate >> 1);

const int tableSize = 256; // the size of your wavetable
const int numberOfTables = 45;
//const int notesInTables = nyquist/numberOfTables;
const float PI = 3.141592653589793238462643;

int16_t resolution = 32767; //how big the biggest value will be

typedef enum { SQUARE, TRIANGLE, SAWTOOTH } waveshape;

void makeOne(waveshape shape)
{   
    //float phase = PI; //Uncoment this if you want to flip the wave phase
    float phase = 0;
    float angularFreq = 2 * PI / tableSize;
    float table[numberOfTables][tableSize] = {{0},{0}};

    ofstream myFile;
    int t, i, n, m, z = 0;
    
    float maxAmp = 1;
    float minValue = 0; //those will be used for normalizing the scale
    float maxValue = 0;
    
    int step;
    switch (shape)
    {
        case SQUARE:
            step = 2;
            myFile.open("squareWave.h");
            myFile << "const int16_t squareWavetable[45][257] = {";
            break;
        case TRIANGLE:
            step = 2;
            myFile.open("triangleWave.h");
            myFile << "const int16_t triangleWavetable[45][257] = {";
            break;
        case SAWTOOTH:
            step = 1;
            myFile.open("sawtoothWave.h");
            myFile << "const int16_t sawtoothWavetable[45][257] = {";
            break;
    }
     
    for (t = 0; t < numberOfTables; t++) 
    {    
        int numberOfHarmonics = round(nyquist / midiNoteToFreq(z)); //skips every three notes        
        for (n = 0; n < tableSize; n++) 
        {            
            for(m = 0; m < numberOfHarmonics; m += step) 
            {
                switch (shape)
                {
                    case SQUARE:
                        table[t][n] += maxAmp / (m + 1) * sin(angularFreq * (m + 1) * n + phase);
                        break;
                    case TRIANGLE:
                        table[t][n] += (m % 2 ? 1 : -1) * maxAmp / ((m + 1) * (m + 1)) * sin(angularFreq * (m + 1) * n + phase);
                        break;
                    case SAWTOOTH:
                        table[t][n] += maxAmp / (m + 1) * sin(angularFreq * (m + 1) * n + phase);
                        break;
                }
            }
            
            if (table[t][n] > maxValue) maxValue = table[t][n];
            if (table[t][n] < minValue) minValue = table[t][n];
        }
        z += 3;
    }
    
    cout <<"maxValue = ";
    cout << maxValue;
    cout <<" | minValue = ";
    cout << minValue << "\n";
    
    for (t = 0; t < numberOfTables; t++)
    {
        myFile << "{";
        
        for (i = 0; i < tableSize; i++)  // normalize the table
        {
            myFile << round(scaleBetween(table[t][i], -resolution, resolution, minValue, maxValue));
            myFile << ",";
        }
        myFile << "0}, \n"; // the table needs this last value
    }
    myFile << "};";
    myFile.close();
}

int main(int argc, const char * argv[]) 
{
    makeOne(SQUARE);
    makeOne(TRIANGLE);
    makeOne(SAWTOOTH);
    return 0;    
}
 
Ah, that's the same method (and code) as I've used!

I've said several times and it's worth repeating, if someone who knows what their doing could have a go at making the waveforms in the Audio Lib band-limited, they would be doing lots of people a big favour.
 
I agree. It would be great to have the aliasing issues actually addressed in the library, but the wavetables are easy and a big improvement.

Despite saying above that it wasn't a priority, I extended my code today to handle a bandwidth limited Pulse waveform with support for low frequency modulation of the pulse width. It would be pretty easy to wrap it up as a library object - AudioSynthBandLimitedWaveform - as an interim option perhaps?
 
I found another (almost) aliasing issue in the Audio lib, the tonesweep generation only changes frequency between audio blocks, generating lots of
sidebands (very obvious on an FFT display, the numerous side bands are spaced apart by 344Hz corresponding to the 2.9ms block size.

I may look into if this can be easily remedied, as well as into the generation of sawtooth/triangle/square waves with low aliasing.
 
i've been looking at aliasing issue, and the BLIT / BLSF approach seems to be viable for square/sawtooth waveforms,
but gets rather heavy in resource usage at higher frequencies (where explicit harmonic summing gets cheaper).

Currently I've got Python code using scipy.signal, but I'm hopefully I can get something C++ together once I'm happy with
performance settings and how phase modulation can work with it.

The tonesweep issue I mentioned was a simple fix: https://github.com/PaulStoffregen/Audio/compare/master...MarkTillotson:tonesweep_improvement

The spectra for it are illustrated here (again a Python emulation):
tonesweep.png
green and black are mid-sweep 1024 point FFT, for broken and fixed respectively, blue and red for whole sweep, and
spectrograms for broken and fixed showing the frequency smearing.
 
I've added some extra functionality to my synth - I used the spare memory to add some basic 808 style drum sounds (bass, 3 toms, snare, and open and closed hi-hat). After trying out a few variations, I've ended up with the following approach where they are controlled by presses on the otherwise unused bottom row from the parameter control matrix:
  • Choose one of a predefined set of a 3 instrument kits, where each instrument is associated with the beats from one of the 3 rhythm generators
  • Choose a mode for the kit:
    • Off - do not play
    • Sel - play according to the beat selection buttons across both sequencers
    • R1 - play according to the first rhythm setting
    • 123 - play according to all 3 rhythm settings (regardless of the beat selection button states)
  • Choose an instrument for off beats
  • Choose a mode for playing off beats
    • Off - do not play
    • Sel - play according to the off beat selection buttons across both sequencers
    • R1 - play offbeats as determined by the first rhythm setting
    • 123 - play offbeats as determined by all 3 rhythm settings
    • All - simply play on every beat

To make this work better I have added an offset parameter to each rhythm generator. This is something I was considering doing originally anyway, but it's absence was a more significant issue for drum beat generation.

A couple of other features are:
  • The drum sounds go through the filter and as such also can be modulated, but they do not go through the amplitude envelope.
  • The volume of the drums can be controlled independently of the oscillators, and there is an accent on the first beat of each rhythm.

Here's a link to a quick demo to give an idea of how it works and how it blends with the synthesiser components.

EUCLID percussion demo

Ken
 
Just realised there is an error in the triangle calculation in the code I posted earlier.

The triangle wave only has the odd harmonics, so need to make a small change as follows:

Code:
...
        case TRIANGLE:
            if (m % 2 == 0)
	        table[t][n] += ((m >> 1) % 2 ? 1 : -1) * maxAmp / ((m + 1) * (m + 1)) * sin(angularFreq * (m + 1) * n + phase);
            break;
...
 
Last edited:
regarding aliasing, it is quite a simple thing to sample a waveform at each c note 0 - 8 using a free commercial DAW and vst type synth, clip them in something like audacity, and re-import them using the wavetable tool. Takes up 12.1k of space per voice and is very efficient as well. With the release of the 4.1, the amount of available memory makes this approach very tenable, and leaves processing resources available for other tasks. https://www.earlevel.com/main/category/digital-audio/oscillators/wavetable-oscillators/ This site has some good articles on using wavetables to avoid aliasing, as well as generating your own arbitrary bandlimited wavetables, if you're into doing it programatically.
 
Last edited:
I've progressed from the prototype stage to what I think is a shareable version. Putting together a webpage now with details.

It fits nicely in the enclosure I chose, and it's nice to have the connections robust instead of dangling wires. :)

1. Euclidean - side view.jpg
3. Euclidean - rear view.jpg

The LCD display is working well and showing some good detail - especially for setting the rhythm and the running beats.

4. Euclidean - top view running-small.jpg
6. Euclidean - LCD rhythm.jpg

All together I use 2 genuine boards and 2 boards that are just panels. These fit a Pactec PT-8 enclosure, but there are extra holes that align so that the boards can be stacked vertically. The lower board has a prototyping area since the Teensy 4.0 can be replaced with a Teensy 4.1 meaning there are many spare pins for expansion/experimentation. The control board has a number of SMT components in place which simplifies construction.

7. Euclidean - PCBs.jpg

Here's a short demo showing a fairly straightforward polyrhythm and sequence and how you can interact with it musically via a MIDI keyboard:

https://youtu.be/Z7MHLqkzVcU

Ken
 
This is really nice work and forms a very usable all-in-one instrument. You mentioned effects, will the delay sync to the tempo with divisions (1/4, 1/8, 1/16...)?
 
This is really nice work and forms a very usable all-in-one instrument. You mentioned effects, will the delay sync to the tempo with divisions (1/4, 1/8, 1/16...)?

If you look at the encoder matrix at the top left, there is a select encoder to select a row and then 4 encoders to change values. The upper value in each case is changed by rotation, and the lower value by clicking. Generally the lower values cycle through a few values like wave shape, or simple on/off settings.

So in my initial setup, the effects are handled by cycling through 4 presets - bypass, slow, medium and fast. For delay, slow is 1/2 the beat speed, medium at the beat speed and fast double the beat speed. If this results in a delay over 1 second I repeatedly halve the value until it is under. (The slowest tempo is 30bpm, so slow delay on slowest tempo would be a 4s delay if I didn't do this.) The feedback also reduces with the speed.

However, in the latest incarnation I changed this, and now the select encoder is clickable and acts like a shift key (the LED flashes to indicate shift) and the other encoders only rotate. That way I can handle more values more easily - plus clicking rotary controls is always a bit dodgy because of the possibility of unintended rotation. I haven't finished the code for this update yet but it won't take long and will make smoother adjustments possible, and to the effects in particular.

Ken
 
If you look at the encoder matrix at the top left, there is a select encoder to select a row and then 4 encoders to change values. The upper value in each case is changed by rotation, and the lower value by clicking. Generally the lower values cycle through a few values like wave shape, or simple on/off settings.

So in my initial setup, the effects are handled by cycling through 4 presets - bypass, slow, medium and fast. For delay, slow is 1/2 the beat speed, medium at the beat speed and fast double the beat speed. If this results in a delay over 1 second I repeatedly halve the value until it is under. (The slowest tempo is 30bpm, so slow delay on slowest tempo would be a 4s delay if I didn't do this.) The feedback also reduces with the speed.

However, in the latest incarnation I changed this, and now the select encoder is clickable and acts like a shift key (the LED flashes to indicate shift) and the other encoders only rotate. That way I can handle more values more easily - plus clicking rotary controls is always a bit dodgy because of the possibility of unintended rotation. I haven't finished the code for this update yet but it won't take long and will make smoother adjustments possible, and to the effects in particular.

Ken

sweet project man!!! it looks like you used the same multiplexer that i did, any chance you could take a look at my wiring and help with my issues? https://forum.pjrc.com/threads/62957-Teensy-4-0-Audio-Project-Multiplexer-Wiring-Help
 
I've put together some documentation for this now (having a few "early adopters" have a go at building one certainly puts a bit of pressure on to sort stuff out. :) )

A User Guide describing the theoretical basis for the device, the specifications, and the available controls is here: The EUCLIDEAN - User Guide

A Build Guide for the kit version using the boards and panels shown in my earlier post is here: The EUCLIDEAN - Build Guide

Finally, the source code and compiled firmware files are here: https://github.com/kallikak/Euclidean

I'm by no means a professional at this (entirely self taught when it comes to hardware), but prior to sending any boards out I did do a complete build from scratch and it went without a hitch, so I'm pretty confident everything is ok. (It's the build I photographed for the Build Guide.)

The User Guide includes short samples of the 10 presets included in the firmware (ROM presets). Given that these are all playing without any interaction with the controls means they give a good overview of what you can do with the synthesizer as a baseline(*), and how the Euclidean differs from other devices.

Ken

(*) No pun intended! :D
 
Status
Not open for further replies.
Back
Top