New Teensy 4.1 DIY Synthesizer

Can I ask about the screen do you feel it’s a much option and advise it going forward for others?

Also the encoder work you’ve done is that part of a common library or just custom code? I liked the acceleration option very nice.
 
The 3.5-inch Touch Display is a Waveshare ST7796S. It's high quality and has a built-in frame with a protective glass cover. This makes it very suitable for installation in devices.

Screenshot 2026-03-09 185742.png


Link : https://www.amazon.de/Waveshare-3-5...pcontext&ref_=fplfs&psc=1&smid=A3U321I9X7C9XA

The encoder query is handled by an MSP23017 chip and Adafruit_MCP23X17.h Lib.
I'm using the Teensy 4.1's I2C bus for control. I wrote a custom function to process the encoder data.

Link : https://forum.pjrc.com/index.php?threads/new-teensy-4-1-diy-synthesizer.63255/post-366769
 
The 3.5-inch Touch Display is a Waveshare ST7796S. It's high quality and has a built-in frame with a protective glass cover. This makes it very suitable for installation in devices.

View attachment 39121

Link : https://www.amazon.de/Waveshare-3-5...pcontext&ref_=fplfs&psc=1&smid=A3U321I9X7C9XA

The encoder query is handled by an MSP23017 chip and Adafruit_MCP23X17.h Lib.
I'm using the Teensy 4.1's I2C bus for control. I wrote a custom function to process the encoder data.

Link : https://forum.pjrc.com/index.php?threads/new-teensy-4-1-diy-synthesizer.63255/post-366769
Thanks for that. I have a few displays I bought from various sites currently doesn’t look to bad on those but I do like the mounting options on those.

I did have encoders in the smaller version of my code so will revert back to look in to what I used and update where needed.

Thanks for posting
 
The trick with my encoder query is that I trigger a hardware interrupt on the Teensy4.1 with every change and then read the 16-bit GPIO data register from the MCP23017 only once. From this, I can extract the encoder number and direction of movement. This reduces access to the slow I2C bus (400 kHz).

C:
#include <Arduino.h>
#include <Adafruit_MCP23X17.h>

// Encoders variable
volatile uint16_t current_GPIO1 = 0;
uint16_t old_GPIO1 = 0;

// Encoder values  for 10x Menu Pages with 7x Encoder
int16_t Enc_Pos[10][7] = {
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0}};

// U10 MCP23017 Encoder Pin
#define enc1_A 256      // GPB0 
#define enc1_B 512      // GPB1
#define enc2_A 1024     // GPB2
#define enc2_B 2048     // GPB3
#define enc3_A 4096     // GPB4
#define enc3_B 8192     // GPB5
#define enc4_A 16384    // GPB6
#define enc4_B 32768    // GPA7
#define enc5_A 128      // GPA0
#define enc5_B 64       // GPA1
#define enc6_A 32       // GPA2
#define enc6_B 16       // GPA3
#define enc7_A 8        // GPA4
#define enc7_B 4        // GPA5
#define on 1
#define off 0

uint16_t currentClkState[7] = {0,0,0,0,0,0,0};
uint8_t Enc_B[7] = {0,0,0,0,0,0,0};
uint16_t lastClkState[7] = {0,0,0,0,0,0,0};
unsigned long lastInterruptTime = 0;
int accelerationFactor = 1;


// Encoder interrupt ----------------------------------------------
void Encoder_Interrupt()
{
    enc_action = true;
    current_GPIO1 = mcp1.readGPIOAB();
}


// read Encoder ------------------------------------------------
void read_encoder(void)
{

    if (enc_action)
    {
        uint8_t encID = 0;
        unsigned long interruptTime = millis();
        unsigned long timeDiff = interruptTime - lastInterruptTime;

        enc_action = false;

        // Encoder Acceleration
        // If the rotation is fast (< 50 ms), accelerate
        if (timeDiff < 50)
        {
            accelerationFactor = 3;
        }
        else
        {
            accelerationFactor = 1;
        }

        // Check MCP23017 GPIO for activity
        uint16_t changesBits = old_GPIO1 ^ current_GPIO1;

        // Encoder1 Clk Pin status
        if (changesBits == enc1_A)
        {
            currentClkState[0] = bitRead(current_GPIO1, 8);
            encID = 0;
        } // Encode1 DT Pin status
        else if (changesBits == enc1_B)
        {
            Enc_B[0] = bitRead(current_GPIO1, 9);
            encID = 0;
        }
        // Encoder1 Clk Pin status
        else if (changesBits == enc2_A)
        {
            currentClkState[1] = bitRead(current_GPIO1, 10);
            encID = 1;
        } // Encode2 DT Pin status
        else if (changesBits == enc2_B)
        {
            Enc_B[1] = bitRead(current_GPIO1, 11);
            encID = 1;
        }
        // Encoder3 Clk Pin status
        else if (changesBits == enc3_A)
        {
            currentClkState[2] = bitRead(current_GPIO1, 12);
            encID = 2;
        } // Encode3 DT Pin status
        else if (changesBits == enc3_B)
        {
            Enc_B[2] = bitRead(current_GPIO1, 13);
            encID = 2;
        }
        // Encoder4 Clk Pin status
        else if (changesBits == enc4_A)
        {
            currentClkState[3] = bitRead(current_GPIO1, 14);
            encID = 3;
        } // Encode4 DT Pin status
        else if (changesBits == enc4_B)
        {
            Enc_B[3] = bitRead(current_GPIO1, 15);
            encID = 3;
        }
        // Encoder5 Clk Pin status
        else if (changesBits == enc5_A)
        {
            currentClkState[4] = bitRead(current_GPIO1, 7);
            encID = 4;
        } // Encode5 DT Pin status
        else if (changesBits == enc5_B)
        {
            Enc_B[4] = bitRead(current_GPIO1, 6);
            encID = 4;
        }

        // save encoder values
        if (currentClkState[encID] != lastClkState[encID])
        {
            if (Enc_B[encID] != currentClkState[encID])
            {
                Enc_Pos[Menu_page][encID] += accelerationFactor;
                if (Enc_Pos[Menu_page][encID] >= 127)
                {
                    Enc_Pos[Menu_page][encID] = 127;
                }
            }
            else
            {
                Enc_Pos[Menu_page][encID] -= accelerationFactor;
                if (Enc_Pos[Menu_page][encID] <= 0)
                {
                    Enc_Pos[Menu_page][encID] = 0;
                }
            }

            // Menu page ---------
            if (Menu_page == 1)
            {
                if (encID == 0)
                {
                    tft.clearChangedArea();
                    tft.setFont(OpenSans_14);
                    tft.setTextColor(ST7735_WHITE);
                    tft.setCursor(50,50);
                    tft.fillRect(50,50,40,12,ST7735_RED);
                    tft.print(Enc_Pos[1][0]); // Encoder 1 value
                    tft.updateScreenAsync(false, true, true);

                }
                else if (encID == 1)
                {
                    cur_env1_attack = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 2)
                {
                    cur_env1_decay = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 3)
                {
                    cur_env1_sustain = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 4)
                {
                    cur_env1_release = Enc_Pos[Menu_page][encID];
                }

            }
         
            if (Menu_page == 4)
            {

                if (encID == 0)
                {
                    cur_env1_delay = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 1)
                {
                    cur_env1_attack = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 2)
                {
                    cur_env1_decay = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 3)
                {
                    cur_env1_sustain = Enc_Pos[Menu_page][encID];
                }
                else if (encID == 4)
                {
                    cur_env1_release = Enc_Pos[Menu_page][encID];
                }
                boolean asyncUpdate = true;
                draw_envelope_line(asyncUpdate, curve, cur_env1_delay, cur_env1_attack, cur_env1_decay,
                    cur_env1_sustain, cur_env1_release, ST7735_GREEN);
            }
        }
        lastClkState[encID] = currentClkState[encID];
        old_GPIO1 = current_GPIO1;
        lastInterruptTime = interruptTime;
    }


// Setup -------------------------------------------------------
void setup(void)
{
    Serial.begin(9600);

// init Key/Encoder IO Expander
    mcp1.begin_I2C(0x22, &Wire1);
    // init Wire1 for Encoders
    Wire1.begin();
    Wire1.setClock(400000UL); // I2C speed 400KHz

    // init MPC23017Expander for Encoders
    mcp1.setupInterrupts(true, false, LOW);
    for (size_t i = 0; i < 16; i++)
    {
        mcp1.pinMode(i, INPUT_PULLUP);
        mcp1.setupInterruptPin(i, CHANGE);
       
    }
    mcp1.getCapturedInterrupt();
    mcp1.readGPIOAB();
    mcp1.clearInterrupts();
    delay(10);

    // init Encoder Interrupt
    attachInterrupt(digitalPinToInterrupt(PIN_ENC_INT), Encoder_Interrupt, FALLING);
}

void loop()
{
    
        read_encoder();    
   
}

}

I'm not curious at all ;) But what are you currently developing :unsure:
 
Last edited:
I'm not curious at all ;) But what are you currently developing :unsure:
Moving back on to the hardware for my clone of a clone

Started of simple, grew arms and legs or voices and filters. But nailed the supersaw and got some good filters. Ported on to my MicroDexed for now as that’s a solid build.

It’s now possible to load presets from the hardware, working on the final tweaks of that though.

Next sliders and pots (which I have done many times and failed. But that was my soldering

https://forum.pjrc.com/index.php?threads/jteensy8000-clone-of-a-clone-with-extra-filters.77776/
 
This is my first version of Jeannie - Polyphonic DIY Synthsizer. The case for Jeannie are made by Erica Synths

Link : https://github.com/rolfdegen/Jeannie-Open-source-Synthesizer
I seen it and you’ve done a lot of work. Well done it’s great to see. I take it you’ll prototype and then go for another kit?

This one for me will be a one off I think as the restrictions have been hard, hence the left over extras. But really surprised at what can be achieved
 
During the development of Jeannie 2, it became apparent that the graphical user interface and the audio engine required more CPU resources than anticipated, pushing the limits of a Teensy 4.1. To gain more flexibility for developing audio synthesis and graphical representations, I decided to split the audio engine and the GUI across two CPUs. The audio engine runs on a Teensy 4.1 due to its large sample memory of 16-32 MB, while the GUI runs on a smaller Teensy 4.0.

Jeannie 2 Blogdiagram

Screenshot 2026-04-26 212118.png
 
A few more details...
Teensy 4.1 handles audio synthesis and MIDI.
Teensy 4.0 takes care of all the graphical aspects, as well as encoder and key input, and touchpad input.
Teensy 4.0 FX engine for 16-bit stereo delay and effects such as Corus and Shimmer.

Screenshot 2026-04-27 220044.png
 
Last edited:
For communication between the audio engine (Teensy 4.1) and the GUI interface (Teensy 4.0), I established a high-speed serial connection with 6,000,000 baud to smoothly display audio signals and waveforms. Unfortunately, the Teensy 4.0 only has one SPI port, and that's needed for the touchscreen.

Jeannie2_Block_02.png


Screenshot 2026-05-02 122547.png


Screenshot 2026-05-02 122625.png
 
I've been messing around with FlexIOSPI to reasonably good effect, though it does have its quirks. I think it may fail to implement anything other than SPI Mode 0, but having raised an issue with the author I got only the response "No idea... Sorry,". So maybe it does work and I'm dumb, or it doesn't... I'd planned to use it to mop up a bunch of I/O using MCP23S17 port expanders and an ADC, but the ADC turns out to be a bit weird and need to swap between SPI modes, so I'm having to re-jig my thinking a bit.

One thing to be aware of is that FlexIO#3 doesn't support DMA; there are a reasonable number of pins on Teensy 4.0 which are accessible from #1 and #2, so it's probably not a showstopper in most cases.
 
I use Visual Studio Code and PlatformIO for programming. I've connected the two Teensy devices to the PC via USB and combined the two Teensy projects (audio engine & GUI) into a single workspace. I use the TyTools program to program the Teensy devices. The Environment Switcher (red) allows me to address and flash the two Teensy projects separately.

Screenshot 2026-05-02 125015.png
 
I've been messing around with FlexIOSPI to reasonably good effect, though it does have its quirks. I think it may fail to implement anything other than SPI Mode 0, but having raised an issue with the author I got only the response "No idea... Sorry,". So maybe it does work and I'm dumb, or it doesn't... I'd planned to use it to mop up a bunch of I/O using MCP23S17 port expanders and an ADC, but the ADC turns out to be a bit weird and need to swap between SPI modes, so I'm having to re-jig my thinking a bit.

One thing to be aware of is that FlexIO#3 doesn't support DMA; there are a reasonable number of pins on Teensy 4.0 which are accessible from #1 and #2, so it's probably not a showstopper in most cases.

I'm using every pin on the Teensy 4.0. Perhaps it would be better to switch to a second Teensy 4.1. Then a high-speed SPI connection between the first Teensy (audio engine) and the second Teensy (GUI) would be possible and I would have a few GPIOs free for future features :unsure:
 
I might be wrong, but one advantage I see of using Serial between the Teensys is that you just do a Serial.print.... and it's all buffered and your code can go and get on with other things.
 
Probably worth doing some searching and experimenting before committing to that route. I haven't tried it myself, but seem to recall people having significant trouble getting SPI comms working between two Teensys. I think they ended up using UARTs, like you already do!

On the other hand, SPI is much faster than I²C for port expanders...
 
Seriall Data Port with 6 MHz on the Teensy 4.1 is a bit slow for transmitting real-time audio data for a sample curve on the display. However, I can reduce the 16-bit data to 8 bits and transmit it in blocks. That's sufficient for the graphic. This increases the speed.
 
I'm using every pin on the Teensy 4.0.
Even the Bottom pins? The small end pads look to have SPI2 34-37
And the D+ and D- give USB_Host. Though T_4.1 as Host could treat T_4.0 as a device and give USB Serial connect for best speed, just lose easy connect to the PC to program without cable swap or maybe FlasherX.
teensy40_card10b_rev2.png
 
Audio and Graphics Test

By distributing the audio and graphics functions across two Teensy MCUs, the graphics on the touchscreen are fast enough and run smoothly. The red LED (left) indicates data transmission from the GUI MCU to the audio MCU. In the video, I change the envelope parameters using the encoders. The encoder data is processed by the GUI MCU and then sent to the audio MCU.

 
With an mcp23017 you can add 8 encoders on 2 wires. Upto 8 chips for 64 encoders, 128 buttons or leds
 
Back
Top