I bought a teensyrom a while ago.
UPDATE 20250712 : Display
UPDATE 20250904 : Added PitchBend and stabilized potmeters.
Tyrone and I wanted to control the settings using potmeters.
So I grabbed a Teensy 4.1 controller and some 10K potmeters, and worte some code
Code for 12 pots, pitch bend and display, don’t forget to set your USB mode to midi!
Schematic soon, also all tweaks and note sending.
Todo: Nice 3D printed Pitch Bend wheel, rest of display code and extra buttons!
Let’s use an old box to hold the pots!
Knob 1 = volume now, and left of box has centering Pitch Bend.


CODE
#include <MIDIUSB.h> #include <ResponsiveAnalogRead.h> const int pitchbend = A14; const int filterresonance = A0; const int filtercutoff = A1; const int voicemode = A2; const int modulationmode = A3; const int attack = A4; const int release = A5; const int pulsewidth = A6; const int tremolodepth = A7; const int tremolospeed = A8; const int oscwave = A9; const int oscvoice2 = A10; const int oscvoice3 = A11; const int volume = A12; ResponsiveAnalogRead analogpitchbend(pitchbend, true); ResponsiveAnalogRead analogfilterresonance(filterresonance, true); ResponsiveAnalogRead analogfiltercutoff(filtercutoff, true); ResponsiveAnalogRead analogvoicemode(voicemode, true); ResponsiveAnalogRead analogmodulationmode(modulationmode, true); ResponsiveAnalogRead analogattack(attack, true); ResponsiveAnalogRead analogrelease(release, true); ResponsiveAnalogRead analogpulsewidth(pulsewidth, true); ResponsiveAnalogRead analogtremolodepth(tremolodepth, true); ResponsiveAnalogRead analogtremolospeed(tremolospeed, true); ResponsiveAnalogRead analogoscwave(oscwave, true); ResponsiveAnalogRead analogoscvoice2(oscvoice2, true); ResponsiveAnalogRead analogoscvoice3(oscvoice3, true); ResponsiveAnalogRead analogvolume(volume, true); // A0 -> A3, A6 -> A13 const int numPots = 12; int lastfilterresonance = -1; int lastfiltercutoff = -1; int lastvoicemode = -1; int lastmodulationmode = -1; int lastattack = -1; int lastrelease = -1; int lastpulsewidth = -1; int lasttremolodepth = -1; int lasttremolospeed = -1; int lastoscwave = -1; int lastoscvoice2 = -1; int lastoscvoice3 = -1; int lastvolume = -1; // CC 7 ff niet // Custom mappings: const int potPins[numPots] = {A0, A1, A2, A3, A6, A7, A8, A9, A10, A11, A12, A13}; // Analog pins const int ccNumbers[numPots] = {0,1,2,3,4,5,6,8,9,13,14,15}; // CC numbers const int midiChannels[numPots]= {1,1,1,1,1,1,1,1,1,1,1,1}; // MIDI channels (1–16) int lastValues[numPots]; // Store last values to reduce redundant MIDI messages void setup() { Serial.begin(9600); for (int i = 0; i < numPots; i++) { pinMode(potPins[i], INPUT); lastValues[i] = -1; } pinMode(pitchbend, INPUT); } void loop() { analogfilterresonance.update(); if(analogfilterresonance.hasChanged()) { int analogValue = analogfilterresonance.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lastfilterresonance) > 0){ usbMIDI.sendControlChange(7, midiValue, 1); lastfilterresonance = midiValue; } } analogfiltercutoff.update(); if(analogfiltercutoff.hasChanged()) { int analogValue = analogfiltercutoff.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lastvoicemode) > 0){ usbMIDI.sendControlChange(1, midiValue, 1); lastvoicemode = midiValue; } } analogvoicemode.update(); if(analogvoicemode.hasChanged()) { int analogValue = analogvoicemode.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lastvoicemode) > 0){ usbMIDI.sendControlChange(2, midiValue, 1); lastvoicemode = midiValue; } } analogmodulationmode.update(); if(analogmodulationmode.hasChanged()) { int analogValue = analogmodulationmode.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lastmodulationmode) > 0){ usbMIDI.sendControlChange(3, midiValue, 1); lastmodulationmode = midiValue; } } analogattack.update(); if(analogattack.hasChanged()) { int analogValue = analogattack.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lastattack) > 0){ usbMIDI.sendControlChange(4, midiValue, 1); lastattack = midiValue; } } analogrelease.update(); if(analogrelease.hasChanged()) { int analogValue = analogrelease.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lastrelease) > 0){ usbMIDI.sendControlChange(5, midiValue, 1); lastrelease = midiValue; } } analogpulsewidth.update(); if(analogpulsewidth.hasChanged()) { int analogValue = analogpulsewidth.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lastpulsewidth) > 0){ usbMIDI.sendControlChange(6, midiValue, 1); lastpulsewidth = midiValue; } } analogtremolodepth.update(); if(analogtremolodepth.hasChanged()) { int analogValue = analogtremolodepth.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lasttremolodepth) > 0){ usbMIDI.sendControlChange(8, midiValue, 1); lasttremolodepth = midiValue; } } analogtremolospeed.update(); if(analogtremolospeed.hasChanged()) { int analogValue = analogtremolospeed.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lasttremolospeed) > 0){ usbMIDI.sendControlChange(9, midiValue, 1); lasttremolospeed = midiValue; } } analogoscwave.update(); if(analogoscwave.hasChanged()) { int analogValue = analogoscwave.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lastoscwave) > 0){ usbMIDI.sendControlChange(13, midiValue, 1); lastoscwave = midiValue; } } analogoscvoice2.update(); if(analogoscvoice2.hasChanged()) { int analogValue = analogoscvoice2.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lastoscvoice2) > 0){ usbMIDI.sendControlChange(14, midiValue, 1); lastoscvoice2 = midiValue; } } analogoscvoice3.update(); if(analogoscvoice3.hasChanged()) { int analogValue = analogoscvoice3.getValue(); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lastoscvoice3) > 0){ usbMIDI.sendControlChange(15, midiValue, 1); lastoscvoice3 = midiValue; } } // PITCHBEND! analogpitchbend.update(); if(analogpitchbend.hasChanged()) { int pitchBend = map(analogpitchbend.getValue(), 0, 1023, 0, 16383); // Map to MIDI Pitch Bend range sendPitchBend(pitchBend, 0); } /* int potValue = analogRead(A14); // Read pot (0–1023) int pitchBend = map(potValue, 0, 1023, 0, 16383); // Map to MIDI Pitch Bend range //pitchBend = pitchBend + 8192; if (abs(pitchBend - lastPitch) > 250) { // Send only on significant change sendPitchBend(pitchBend, 0); Serial.println(pitchBend); lastPitch = pitchBend; } */ delay(5); // CPU-friendly update rate } void sendPitchBend(int value, byte channel) { byte lsb = value & 0x7F; byte msb = (value >> 7) & 0x7F; midiEventPacket_t pitchBendPacket = {0x0E, 0xE0 | (channel & 0x0F), lsb, msb}; MidiUSB.sendMIDI(pitchBendPacket); // Needed? MidiUSB.flush(); }
CODE for display
#include <MIDIUSB.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels #define OLED_RESET 4 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // A0 -> A3, A6 -> A13 const int numPots = 12; // A14 - Pitch bend! int lastPitch = -1; //A4 A5 I2C display // Custom mappings: const int potPins[numPots] = {A0, A1, A2, A3, A6, A7, A8, A9, A10, A11, A12, A13}; // Analog pins const int ccNumbers[numPots] = {0,1,2,3,4,5,19,7,8,9,13,14}; // CC numbers const int midiChannels[numPots]= {1,1,1,1,1,1,1,1,1,1,1,1}; // MIDI channels (1–16) int lastValues[numPots]; // Store last values to reduce redundant MIDI messages void setup() { for (int i = 0; i < numPots; i++) { pinMode(potPins[i], INPUT); lastValues[i] = -1; } display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextSize(2); display.setTextColor(WHITE); display.clearDisplay(); display.display(); } void loop() { for (int i = 0; i < numPots; i++) { int analogValue = analogRead(potPins[i]); int midiValue = analogValue / 8; // Scale 0–1023 to 0–127 if (abs(midiValue - lastValues[i]) > 1) { usbMIDI.sendControlChange(ccNumbers[i], midiValue, midiChannels[i]); lastValues[i] = midiValue; } } int potValue = analogRead(A12); // Read pot (0–1023) int pitchBend = map(potValue, 0, 1023, 0, 16383); // Map to MIDI Pitch Bend range if (abs(pitchBend - lastPitch) > 5) { // Send only on significant change sendPitchBend(pitchBend, 0); // Channel 1 lastPitch = pitchBend; } displayInfo(); delay(5); // CPU-friendly update rate } void sendPitchBend(int value, byte channel) { byte lsb = value & 0x7F; byte msb = (value >> 7) & 0x7F; midiEventPacket_t pitchBendPacket = {0x0E, 0xE0 | (channel & 0x0F), lsb, msb}; MidiUSB.sendMIDI(pitchBendPacket); // Needed? MidiUSB.flush(); } void displayInfo(){ byte x0, y0, x1, y1; // start/end coordinates for drawing lines on OLED display.clearDisplay(); display.setCursor(0, 0); display.print("Attack / Release"); // draw attack line x0 = 0; y0 = 63; x1 = map(attackParam, 0, 127, 0, ((SCREEN_WIDTH / 4) - 1)); y1 = 20; display.drawLine(x0, y0, x1, y1, SH110X_WHITE); // draw release line x0 = x1; // start line from previous line's final x,y location y0 = y1; x1 = x0 + map(releaseParam, 0, 127, 0, ((SCREEN_WIDTH / 4) - 1)); y1 = 63; display.drawLine(x0, y0, x1, y1, SH110X_WHITE); display.display(); }