New polyhonic Teensy DIY Sampler

Rolfdegen

Well-known member
Hello dear DIY friends..
I'm working on a new project called Degenerator 2. It's a polyphonic sampler and synthesizer based on Teensy 4.1 with 16MB sample ram and touchscreen.
In 2011 I started developing my first monophonic sampler/synthesizer. The project was later given the name "Degenerator".
The degenerator was based on the most powerful 8-bit MCU from Atmel at the time, the ATXMEGA128A1 with a 32MHz clock speed.
It is now the year 2024 and it is time for a comeback but with a bit more power. A polyphonic (8 voices) sampler/synthesizer is planned based on a Teensy 4.1 MCU with 600MHz and 16MegaByte sampler RAM.
The "Degenerator II" project has already taken its first steps (see video). However, there will be more controls and a 3.2" touch display (as in the video).


The machine ARM-Cortex-M7 600 MHz (Teensy 4.1)
Screenshot 2024-11-19 194124.png


The 16 MegaByte sample RAM on the back

Screenshot 2024-11-19 194409.png


32bit Stereo DAC
Screenshot 2024-11-19 194600.png
 
Next Step Menu pages..
Things are moving forward. The touchscreen is already working well. Next comes access to the patch files on the DS card. The patch files contain the samples that are loaded into the 16MB PSRAM on the Teensy and the parameters for the oscillators, filters, effects, etc.

 
Current envelope design...

20250102_192547.jpg


The blue dots are the active voices. There will be 6 controls for the envelope setting. Attack, decay, sustain, release, delay and curve.
There will also be a loop function. The blue button can be used to select the envelope number. Envelope 1 + 2 are reserved for filters and VCA. Envelope 3 + 4 for modulation. The red button is used to select the envelope type. Either linear or exponential.
 
So that I don't always have to take photos to show display content, I looked for and found another solution.
A clever programmer its name is Kris has developed a screenshot function for the display driver ILI9341_t3 that allows
the display content to be printed out as a BMP image or saved to an SD card. The colors are now correct too.
This helps me when write posts and instruction manual. But it only works for the touch screen display with ILI9341 chip.

Display picture
20250105_173250.jpg


Bitmap file from SD Card
Example.jpg


github link: https://github.com/KrisKasprzak/ILI9341_t3_PrintScreen

I have modified Kris' ILI9341_t3_PrintScreen.h for the ILI9343_t3n driver (renamed ILI9341_t3 to ILI9341_t3n). The demo program draws red rectangles and text. It is then written to the SD card as a bitmap and finally read in again and drawn on the screen (see zip-file into Attach).
 

Attachments

  • PrintScreen.zip
    4.7 KB · Views: 21
Last edited:
In the degenerator 2, up to seven gridless encoders are used. They are responsible for parameter input, program selection and volume. In the degenerator(1) I implemented the encoder query using two serial 8-bit shift registers of the type 74HC164. This slowed down the encoder's scanning speed and occasionally resulted in losses when evaluating the steps or positions. In order to significantly improve the encoder query in the degenerator 2, each encoder now has its own small slave processor "tied to its nose". This is a small ATtiny402 processor in SMD design. Cost factor approx. 75-80 cents. The chip ensures fast and uninterrupted scanning at the encoder connections and transmits the data on a common I2C bus to the Teensy 4.1 CPU. This saves me up to 14 IO lines on the Teensy CPU and valuable time when querying the encoder connections. For testing purposes, I have already ordered small, ready-made I2C boards with a soldered and programmed ATtiny CPU from Duppa. In the later Degnerator2 prototype, the ATtiny CPU will be programmed with the open source firmware via a programming adapter and soldered directly onto the mainboard. I'm excited to see when the boards arrive and whether the whole thing will work as smoothly as I imagine 🤔

I2C encoder board, e.g. from DuPPa.net
with an ATtinsy 402 CPU installed

Screenshot 2025-01-10 194111.png
Screenshot 2025-01-30 153730.png
 
Last edited:
Hello there..
I have now successfully installed the six I2C encoders in the Degenerator 2. Underneath each encoder is a small ATtiny402/412 chip that communicates with the Teensy4.1 board via an I2C bus. To avoid "crank trauma" with the large number of encoders, I have programmed a dynamic acceleration for each encoder.

Encoder check

 
Hello... and have a nice Sunday
I was able to solve the problem with MIDI. It was due to the query and configuration of the 16-bit frame buffer for the display.
The frame buffer is part of the DMA memory in the Teensy4.1 CPU that controls the display.

 
Ok. I still have a small problem. If I move my finger over the slider area and come close to other touch zones, e.g. "Program" or "<Page", then the function is aborted and another touch function is executed (see Video).



My code..
C:
// Loop ---------------------------------------------------
void loop()
{
    readMidi();
    
    if (tft.asyncUpdateActive() == false)
    {
        draw_Peak();
        readEncoder();
        readTouchscreen();
        check_Symbols();
        checkScreensaver();
    }

    if (updateScreen_Timer >= 51) // 51ms
    {
        tft.updateScreenAsync(false);
        updateScreen_Timer = 0;
    }
}

// Touchscreen query -------------------------------------------
void readTouchscreen()
{
    if (touchReadTimer >= TouchRead_interval)
    {
        touchReadTimer = 0;
        read_touch_buttons();
    }
}

// read touch buttons ------------------------------------------
void read_touch_buttons()
{
    // touchbutton_ID -1 = TouchScreen was not pressed
    // touchbutton_ID  0 = non touchbutton array is pressed
    // touchbutton_ID >0 = touchbutton array is pressed
    // touchbutton_ID -2 = touchbutton array hold and move

    int touchbutton_ID = -1;
    static int old_touchbutton_ID = -1;

    if (ts.touched())
    {
        TS_Point p = ts.getPoint();
        p.x = map(p.x, 0, 240, 240, 0);
        p.y = map(p.y, 0, 320, 320, 0);
        int y = tft.height() - p.x;
        int x = p.y;

        if (old_touchbutton_ID == -1)
        {
            touchbutton_ID = get_touchbutton_ID(y, x, pageNo); // 0 non button pressed
        }

        if (touchbutton_ID > 0)
        {
            if (touchbutton_ID != old_touchbutton_ID)
            {
                old_touchbutton_ID = touchbutton_ID;
                touchFunctionHandler(touchbutton_ID, x, y, pageNo);
            }
        }
        else if (touchbutton_ID != old_touchbutton_ID)
        {
            // clear button state
            old_touchbutton_ID = touchbutton_ID;
            clear_touchbuttons_state(pageNo);
        }
        Reset_screensaver();
    }
    else
    {
        touchbutton_ID = -1;
        old_touchbutton_ID = -1;
    }
}

// get touch_buuton ID -----------------------------------------
int get_touchbutton_ID(int y, int x, uint8_t pageNo)
{
    int touchbutton_ID = 0; // non touchbutton

    // menu page 1
    if (pageNo == Program_Menu)
    {
        for (uint8_t index = 0; index < 8; index++)
        {
            uint16_t x1 = TouchKey_page1[index][0];
            uint16_t x2 = TouchKey_page1[index][1];
            uint16_t key_width = TouchKey_page1[index][2];
            uint16_t key_height = TouchKey_page1[index][3];

            if ((x > x1) && (x < (x1 + key_width)))
            {
                if ((y > x2) && (y < (x2 + key_height)))
                {
                    if (TK_state_P1[index] == false)
                    {
                        TK_state_P1[index] = true;
                        touchbutton_ID = index + 1;
                    }
                }
            }
        }
    }
    // menu page 2
    else if (pageNo == Osc_Menu)
    {
        for (uint8_t index = 0; index < 8; index++)
        {
            uint16_t x1 = TouchKey_page2[index][0];
            uint16_t x2 = TouchKey_page2[index][1];
            uint16_t key_width = TouchKey_page2[index][2];
            uint16_t key_height = TouchKey_page2[index][3];

            if ((x > x1) && (x < (x1 + key_width)))
            {
                if ((y > x2) && (y < (x2 + key_height)))
                {
                    if (TK_state_P2[index] == false)
                    {
                        TK_state_P2[index] = true;
                        touchbutton_ID = index + 1;
                    }
                }
            }
        }
    }
    // menu page 3  // Mixer menu
    else if (pageNo == Mixer_menu)
    {
        for (uint8_t index = 0; index < 9; index++) // index = 9 touchbutton
        {
            uint16_t x1 = TouchKey_page3[index][0];
            uint16_t x2 = TouchKey_page3[index][1];
            uint16_t key_width = TouchKey_page3[index][2];
            uint16_t key_height = TouchKey_page3[index][3];

            if ((x > x1) && (x < (x1 + key_width)))
            {
                if ((y > x2) && (y < (x2 + key_height)))
                {
                    if (TK_state_P3[index] == false)
                    {
                        TK_state_P3[index] = true;
                        touchbutton_ID = index + 1;
                    }
                }
            }
        }
    }
    // menu page 4
    else if (pageNo == Envelope_menu)
    {
        for (uint8_t index = 0; index < 8; index++)
        {
            uint16_t x1 = TouchKey_page4[index][0];
            uint16_t x2 = TouchKey_page4[index][1];
            uint16_t key_width = TouchKey_page4[index][2];
            uint16_t key_height = TouchKey_page4[index][3];

            if ((x > x1) && (x < (x1 + key_width)))
            {
                if ((y > x2) && (y < (x2 + key_height)))
                {
                    if (TK_state_P4[index] == false)
                    {
                        TK_state_P4[index] = true;
                        touchbutton_ID = index + 1;
                    }
                }
            }
        }
    }

    return touchbutton_ID;
}

// touch button function handler -------------------------------
void touchFunctionHandler(int touchbutton_ID, uint16_t xpos, uint16_t ypos, int pageNo)
{

    switch (pageNo)
    {
    case Program_Menu:
        handle_touchscreen_menu_1(touchbutton_ID);
        break;
    case Osc_Menu:
        handle_touchscreen_menu_2(touchbutton_ID);
        break;
    case Mixer_menu:
        handle_touchscreen_menu_3(touchbutton_ID, xpos, ypos);
        break;
    case Envelope_menu:
        handle_touchscreen_menu_4(touchbutton_ID);
        break;
    default:
        break;
    }
}

// handle touchscreen Menu page 3 Mixer ------------------------------
FLASHMEM void handle_touchscreen_menu_3(int touchbutton_ID, uint16_t xpos, uint16_t ypos)
{
    // menu forward button
    if (touchbutton_ID == 4)
    {
        pageNo++;
        if (pageNo >= 3)
        {
            pageNo = 3;
        }
        if (old_pageNo != pageNo)
        {
            old_pageNo = pageNo;
            draw_menu_page(pageNo);
        }
    }
    // menu back button
    else if (touchbutton_ID == 1)
    {
        pageNo--;
        if (pageNo <= 0)
        {
            pageNo = 0;
        }
        if (old_pageNo != pageNo)
        {
            old_pageNo = pageNo;
            draw_menu_page(pageNo);
        }
    }

    // Top menu 1 Programs
    else if (touchbutton_ID == 5)
    {
        pageNo = 0;
        draw_menu_page(pageNo);
    }
    // Top menu 2 osc1-3
    else if (touchbutton_ID == 6)
    {
        pageNo = 1;
        draw_menu_page(pageNo);
    }
    // Top menu 3 Filter
    else if (touchbutton_ID == 7)
    {
        pageNo = 2;
        draw_menu_page(pageNo);
    }
    // Top menu 4 Envelope
    else if (touchbutton_ID == 8)
    {
        pageNo = 3;
        draw_menu_page(pageNo);
    }

    // Osc1 Mixer
    else if (touchbutton_ID == 9)
    {
        //Serial.print("Ypos:   "); Serial.println(ypos);
        //Serial.print("oscMix: "); Serial.println(oscMix[0]);

        if (ypos >= 195)
        {
            ypos = 195;
        }
        else if (ypos <= 41)
        {
            ypos = 41;
        }
        
        static uint8_t old_val = 0;

        oscMix[0] = 127 - ((ypos - 41) * 0.82);
        if (oscMix[0] != old_val)
        {
            old_val = oscMix[0];
            float Oscgain = 0.00787 * oscMix[0];
            osc1GainMix(Oscgain);

            uint8_t level = 0.93 * oscMix[0];
            if (level <= 1)
            {
                level = 1;
            }
            tft.fillRect(20, 70, 20, 120, ILI9341_DARKERGREEN);
            tft.fillRect(42, 70, 20, 120, ILI9341_DARKERGREEN);
            tft.fillRect(20, 70 + (120 - level), 20, level, ILI9341_DARKGREEN);
            tft.fillRect(42, 70 + (120 - level), 20, level, ILI9341_DARKGREEN);
            tft.drawFastHLine(20, 70 + (120 - level), 20, ILI9341_YELLOW);
            tft.drawFastHLine(20, 70 + (119 - level), 20, ILI9341_YELLOW);
            tft.drawFastHLine(42, 70 + (120 - level), 20, ILI9341_YELLOW);
            tft.drawFastHLine(42, 70 + (119 - level), 20, ILI9341_YELLOW);
        }
    }
}

// clear touchhandler state ------------------------------------
void clear_touchbuttons_state(int pageNo)
{
    switch (pageNo)
    {
    case 0:
        for (uint8_t index = 0; index < 8; index++)
        {
            if (TK_state_P1[index] == true)
            {
                TK_state_P1[index] = false;
            }
        }
        break;
    case 1:
        for (uint8_t index = 0; index < 8; index++)
        {
            if (TK_state_P2[index] == true)
            {
                TK_state_P2[index] = false;
            }
        }
        break;
    case 2:
        for (uint8_t index = 0; index < 13; index++)
        {
            if (TK_state_P3[index] == true)
            {
                TK_state_P3[index] = false;
            }
        }
        break;
    case 3:
        for (uint8_t index = 0; index < 8; index++)
        {
            if (TK_state_P4[index] == true)
            {
                TK_state_P4[index] = false;
            }
        }
        break;
    default:
        break;
    }
}
 
I was able to solve the problem. I added a "holdState" flag to the touch button query. If I press and hold a touch button, other buttons are locked. If I release the touch button, the other touch buttons are unlocked (see my Video).


C:
// read touch buttons ------------------------------------------
void read_touch_buttons()
{
    // touchbutton_ID -1 = TouchScreen was not pressed
    // touchbutton_ID  0 = non touchbutton array is pressed
    // touchbutton_ID >0 = touchbutton array is pressed
    // holdState       0 = touchButton is pressed for the first time
    // holdState      -1 = touch button remains pressed

    int holdState = 0;

    if (ts.touched())
    {
        TS_Point p = ts.getPoint();
        p.x = map(p.x, 0, 240, 240, 0);
        p.y = map(p.y, 0, 320, 320, 0);
        int y = tft.height() - p.x;
        int x = p.y;

        // pressed for the first time
        if (old_touchbutton_ID == -1)
        {
            touchbutton_ID = get_touchbutton_ID(y, x, pageNo);
            holdState = -1;
        }

        // touchbutton array is pressed
        if (touchbutton_ID > 0)
        {
            old_touchbutton_ID = touchbutton_ID;
            touchFunctionHandler(touchbutton_ID, holdState, x, y, pageNo);
        }
        Reset_screensaver();
    }
    else
    {   // clear touch button status if nothing is pressed
        if (old_touchbutton_ID != -1)
        {
            touchbutton_ID = -1;
            old_touchbutton_ID = -1;
            clear_touchbuttons_state(pageNo);
        }
    }
}
 
Last edited:
Hello there..

The ordered prototype PCB board with the buttons arrived today. The board has a size of 320 x 200 mm and has round solder pads on both sides. So there is enough space for a lot of control elements such as encoders, buttons and LEDs.
To the left of the display is the encoder for selecting sound programs, samples, etc. Below the display are the encoders for entering parameters.

20250219_163337.jpg


I'm not quite sure about the buttons yet. With the black buttons you can call up a menu directly, e.g. Osc, Filter, Effects, Modmatrix etc. On the left and right there are coloured buttons for different things, e.g. LFO LEDs and sequencer

Push buttons with coloured LEDs
Screenshot 2025-02-19 165111.png

Screenshot 2025-02-19 165133.png
 
In the degenerator 2, up to seven gridless encoders are used. They are responsible for parameter input, program selection and volume. In the degenerator(1) I implemented the encoder query using two serial 8-bit shift registers of the type 74HC164. This slowed down the encoder's scanning speed and occasionally resulted in losses when evaluating the steps or positions. In order to significantly improve the encoder query in the degenerator 2, each encoder now has its own small slave processor "tied to its nose". This is a small ATtiny402 processor in SMD design. Cost factor approx. 75-80 cents. The chip ensures fast and uninterrupted scanning at the encoder connections and transmits the data on a common I2C bus to the Teensy 4.1 CPU. This saves me up to 14 IO lines on the Teensy CPU and valuable time when querying the encoder connections. For testing purposes, I have already ordered small, ready-made I2C boards with a soldered and programmed ATtiny CPU from Duppa. In the later Degnerator2 prototype, the ATtiny CPU will be programmed with the open source firmware via a programming adapter and soldered directly onto the mainboard. I'm excited to see when the boards arrive and whether the whole thing will work as smoothly as I imagine 🤔

I2C encoder board, e.g. from DuPPa.net
with an ATtinsy 402 CPU installed

View attachment 36786View attachment 36787
I have been using Duppa products in my projects for a long time. They work very well and allow you to connect many encoders with only 2 pins of the Teensy. Duppa also sells very easy to use i2c led rings.
 
For the keys, I chose nice, soft Cherry keys with transparent and black keycaps.
Since the keys can be fitted with LEDs, I will also install blue LEDs. There will also be a nice, large red modulation encoder made of aluminum to control the cutoff or other targets using this button, for example.

Screenshot 2025-03-08 095924.png


The Cherry keys feel very good to the touch and have a good pressure feel. They are inexpensive and can be easily combined in terms of color and equipped with 3mm LEDs. I chose black, black/transparent with red LEDs and blue/transparent with blue LEDs for the keycaps (see picture). Today I'm going to solder and drill. Since I can't solder the small SMD chips (ATtiny CPUs) for the encoders directly onto the mainboard circuit board, I soldered them onto small adapter boards. In order to address the individual CPUs directly via the I2C bus, each CPU has four solder bridges for addressing. The CPUs are not only responsible for quickly scanning the encoders, they also control LEDs on the mainboard.

Font
20250307_152422.jpg

Back
20250307_155457.jpg
 
Another update...
All knobs and buttons are installed and positioned. I've reordered some black and transparent keycaps. There's also an LED ring for the modulation knob. It's sure to look great. We'll see...

20250311_230529.jpg


20250311_233925.jpg


20250311_230527.jpg


LED-Ring
Screenshot 2025-03-11 231133.png
 
Back
Top