Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 18 of 18

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

  1. #1
    Junior Member
    Join Date
    Jun 2019
    Posts
    15

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

    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/pu...ions/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)

    Click image for larger version. 

Name:	Euclid1.jpg 
Views:	30 
Size:	195.8 KB 
ID:	20966
    Click image for larger version. 

Name:	Euclid2.jpg 
Views:	30 
Size:	182.9 KB 
ID:	20967
    Click image for larger version. 

Name:	Euclid3.jpg 
Views:	38 
Size:	110.2 KB 
ID:	20968
    Click image for larger version. 

Name:	Euclid4.jpg 
Views:	43 
Size:	189.9 KB 
ID:	20969

    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
    https://youtu.be/v3G87oQD6Rg
    Making a patch
    https://youtu.be/EpbZltwllng

    MIDI keyboard integration
    https://youtu.be/SEoN5bkz9Ko

    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 by kallikak; 07-15-2020 at 07:47 AM. Reason: typos

  2. #2
    Senior Member
    Join Date
    Jul 2020
    Posts
    388
    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!

  3. #3
    Senior Member
    Join Date
    Apr 2019
    Posts
    110
    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.

  4. #4
    Junior Member
    Join Date
    Jun 2019
    Posts
    15
    Quote Originally Posted by UHF View Post
    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;    
    }

  5. #5
    Senior Member
    Join Date
    Apr 2019
    Posts
    110
    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.

  6. #6
    Junior Member
    Join Date
    Jun 2019
    Posts
    15
    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?

  7. #7
    Senior Member
    Join Date
    Jul 2020
    Posts
    388
    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.

  8. #8
    Senior Member
    Join Date
    Jul 2020
    Posts
    388
    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/Au...ep_improvement

    The spectra for it are illustrated here (again a Python emulation):
    Click image for larger version. 

Name:	tonesweep.png 
Views:	6 
Size:	187.9 KB 
ID:	21008
    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.

  9. #9
    Junior Member
    Join Date
    Jun 2019
    Posts
    15
    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

  10. #10
    Junior Member
    Join Date
    Jun 2019
    Posts
    15
    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 by kallikak; 08-07-2020 at 10:01 AM.

  11. #11
    Senior Member
    Join Date
    Dec 2019
    Posts
    129
    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/catego...e-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 by RABB17; 08-07-2020 at 06:06 PM.

  12. #12
    Senior Member
    Join Date
    Dec 2019
    Posts
    129
    synth looks amazing though! very nice work

  13. #13
    Junior Member
    Join Date
    Jun 2019
    Posts
    15
    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. :-)

    Click image for larger version. 

Name:	1. Euclidean - side view.jpg 
Views:	9 
Size:	176.5 KB 
ID:	21673
    Click image for larger version. 

Name:	3. Euclidean - rear view.jpg 
Views:	12 
Size:	67.1 KB 
ID:	21674

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

    Click image for larger version. 

Name:	4. Euclidean - top view running-small.jpg 
Views:	10 
Size:	171.8 KB 
ID:	21675
    Click image for larger version. 

Name:	6. Euclidean - LCD rhythm.jpg 
Views:	12 
Size:	118.0 KB 
ID:	21676

    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.

    Click image for larger version. 

Name:	7. Euclidean - PCBs.jpg 
Views:	16 
Size:	163.3 KB 
ID:	21677

    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

  14. #14
    Senior Member
    Join Date
    Apr 2019
    Posts
    110
    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...)?

  15. #15
    Senior Member houtson's Avatar
    Join Date
    Aug 2015
    Location
    Scotland
    Posts
    130
    Hi Ken, really nice work and sounds great!

  16. #16
    Junior Member
    Join Date
    Jun 2019
    Posts
    15
    Quote Originally Posted by UHF View Post
    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

  17. #17
    Quote Originally Posted by kallikak View Post
    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...er-Wiring-Help

  18. #18
    Junior Member
    Join Date
    Jun 2019
    Posts
    15
    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!

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •