Description
The MIDI Adapter is designed for use with the Prototyping System for Teensy 4.1 baseboard. It provides the physical and electrical interface between the Teensy 4.1 and external MIDI devices that use the standard 5-pin connectors.
If working with MIDI over USB, the baseboard already includes a USB Host connector which can be used with that interface.
This MIDI functionality is built into the Project System for Teensy 4.1 baseboard, so this adapter is not useful for that application.
Package Includes:
- MIDI Adapter
- Qty 2 11mm M3 nylon standoffs with screws
KEY FEATURES OF MIDI ADAPTER:
- Standard 5-pin MIDI IN/OUT connectors
- Opto-isolated input
- Buffered output
- MIDI activity LEDs
- Supports Serial1 or Serial6 operation
- Nylon standoffs for supporting the board
Theory of Operation
The MIDI Adapter uses the standard 5-pin DIN MIDI connectors for connecting to devices that use those connections.
The input connector uses an H11L1 optoisolator to electrically protect the Teensy 4.1 from anything that may be misconnected or shorted on the input.
The output connector uses an SN74LVC1G125 buffer chip to electrically isolate the output connection from the Teensy 4.1 as well. It also provides additional drive current capability, though that isn’t required.
MIDI uses current for signaling rather than voltage, so resistors are used to set the signaling current to approximately 5mA when working with 3.3V logic. Some of the resistors are fairly large in order to handle possible short-circuit conditions on the output connector without damage.
Jumpers allow either Serial1 (pins 0/1) or Serial6 (pins 24/25) to be used for the MIDI ports.
LEDs by the input and output connectors show any activity on those ports.
Setting Up the MIDI Adapter
The MIDI Adapter is designed to fit on the right side of the Proto Adapter area of the Prototyping System for Teensy 4.1 baseboard as shown below. The nylon standoffs support the left edge of the board since it hangs out in space. It is important to put the screws into the nylon standoffs on both the top of the adapter and bottom of the baseboard to firmly attached the module in place since MIDI connectors can take a fair amount of force to insert.
The Teensy 4.x (Rev D) Audio Adapter fits into the adapter slot on the left side of the Proto Adapter area.
As shipped, the MIDI port is set for Serial6 (pins 24/25) as shown below. This avoids any possible conflict with the ESP32-S serial link if an ESP32-S is installed on the baseboard. Most example programs are setup to work with Serial1 (pins 0/1), so a slight adjustment to the programs will need to be made to work with Serial6.
In the example program below, these lines define which serial port to use. In this case Serial6 is selected.
MIDI_CREATE_INSTANCE(HardwareSerial, Serial6, MIDI); //MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI);
If desired, the jumpers on the adapter can both be moved down to Serial1. In that case, if an ESP32-S is installed, the jumpers shown below can be removed from the baseboard to isolate Serial1 from the ESP32-S.
If neither of those serial ports appeal to you, the Serial Port Select jumpers can be removed altogether from the adapter and a different serial port can be connected by jumping from the center pin to the desired serial pins on the Teensy 4.1.
Using the MIDI Adapter
Now that we have MIDI connectivity, we need to connect something on the input that outputs MIDI commands.
Here we are using an Arturia MiniLab 3 keyboard for input because it has a 5-pin connector output. Any MIDI keyboard with a 5-pin output connector should work. Be aware that most inexpensive MIDI keyboards use MIDI over USB and do not have the 5-pin output.
The program below is a slightly modified version of the Teensy-synth tutorial from notesandvolts.com. It changes the serial port to Serial6 and initializes the LCD and puts something on it so its not just showing an annoying white screen. The program is light on comments, but more info can be found on the notesandvolts.com blog linked to above. It will open in a new window.
The main loop() simply reads the MIDI port and then calls LFOupdate(false, LFOmodeSelect, FILfactor, LFOdepth); to handle any messages that come in.
The MIDI library is included with the Teensyduino, so you do not need to worry about installing it separately. Copy and paste the program below into an empty sketch in the Arduino IDE and download it to the Teensy 4.1. With the software downloaded and running on the Teensy 4.1 and speakers or headset connected to the Audio Adapter and a MIDI keyboard plugged into the MIDI IN connector, you should have a basic working synthesizer setup.
Note that the default functionality for the MIDI library is to echo any input commands back out the output connector as a loop-thru, so you will notice that the activity LEDs for both the input and output connectors will blink at the same time.
Basic Synth Program
// Teensy-Synth Part 9 // 5-PIN MIDI INPUT // By Notes and Volts // www.notesandvolts.com // With some minor tweaks by ProtoSupplies.com #include <Audio.h> #include <Wire.h> #include <SPI.h> #include <SD.h> #include <SerialFlash.h> #include <MIDI.h> #include <ILI9341_t3.h> #include <font_ArialBold.h> // GUItool: begin automatically generated code AudioSynthWaveform waveform2; //xy=382,453 AudioSynthWaveform waveform1; //xy=386,394 AudioSynthNoisePink pink1; //xy=394,504 AudioMixer4 mixer1; //xy=577,454 AudioFilterStateVariable filter1; //xy=725,457 AudioEffectEnvelope envelope1; //xy=888,458 AudioOutputI2S i2s1; //xy=1049,465 AudioConnection patchCord1(waveform2, 0, mixer1, 1); AudioConnection patchCord2(waveform1, 0, mixer1, 0); AudioConnection patchCord3(pink1, 0, mixer1, 2); AudioConnection patchCord4(mixer1, 0, filter1, 0); AudioConnection patchCord5(filter1, 0, envelope1, 0); AudioConnection patchCord6(envelope1, 0, i2s1, 0); AudioConnection patchCord7(envelope1, 0, i2s1, 1); AudioControlSGTL5000 sgtl5000_1; //xy=565,556 // GUItool: end automatically generated code // LCD control pins defined by the baseboard #define TFT_CS 40 #define TFT_DC 9 // Use main SPI bus MOSI=11, MISO=12, SCK=13 with different control pins ILI9341_t3 tft = ILI9341_t3(TFT_CS, TFT_DC); // GLOBAL VARIABLES const byte BUFFER = 8; //Size of keyboard buffer const float noteFreqs[128] = {8.176, 8.662, 9.177, 9.723, 10.301, 10.913, 11.562, 12.25, 12.978, 13.75, 14.568, 15.434, 16.352, 17.324, 18.354, 19.445, 20.602, 21.827, 23.125, 24.5, 25.957, 27.5, 29.135, 30.868, 32.703, 34.648, 36.708, 38.891, 41.203, 43.654, 46.249, 48.999, 51.913, 55, 58.27, 61.735, 65.406, 69.296, 73.416, 77.782, 82.407, 87.307, 92.499, 97.999, 103.826, 110, 116.541, 123.471, 130.813, 138.591, 146.832, 155.563, 164.814, 174.614, 184.997, 195.998, 207.652, 220, 233.082, 246.942, 261.626, 277.183, 293.665, 311.127, 329.628, 349.228, 369.994, 391.995, 415.305, 440, 466.164, 493.883, 523.251, 554.365, 587.33, 622.254, 659.255, 698.456, 739.989, 783.991, 830.609, 880, 932.328, 987.767, 1046.502, 1108.731, 1174.659, 1244.508, 1318.51, 1396.913, 1479.978, 1567.982, 1661.219, 1760, 1864.655, 1975.533, 2093.005, 2217.461, 2349.318, 2489.016, 2637.02, 2793.826, 2959.955, 3135.963, 3322.438, 3520, 3729.31, 3951.066, 4186.009, 4434.922, 4698.636, 4978.032, 5274.041, 5587.652, 5919.911, 6271.927, 6644.875, 7040, 7458.62, 7902.133, 8372.018, 8869.844, 9397.273, 9956.063, 10548.08, 11175.3, 11839.82, 12543.85}; byte globalNote = 0; byte globalVelocity = 0; int octave = 0; const float DIV127 = (1.0 / 127.0); float detuneFactor = 1; float bendFactor = 1; int bendRange = 12; unsigned int LFOspeed = 2000; float LFOpitch = 1; float LFOdepth = 0; byte LFOmodeSelect = 0; int FILfreq = 10000; float FILfactor = 1; // Select which serial port to use either Serial1 (0,1) or Serial6(24/25) // If using Serial1, disconnect ESP32-S 0/1 jumpers to prevent conflict MIDI_CREATE_INSTANCE(HardwareSerial, Serial6, MIDI); //MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI); void setup() { AudioMemory(30); // usbMIDI.setHandleControlChange(myControlChange); // usbMIDI.setHandleNoteOff(myNoteOff); // usbMIDI.setHandleNoteOn(myNoteOn); // usbMIDI.setHandlePitchChange(myPitchBend); MIDI.begin(); MIDI.setHandleNoteOn(myNoteOn); MIDI.setHandleNoteOff(myNoteOff); MIDI.setHandlePitchBend(myPitchBend); MIDI.setHandleControlChange(myControlChange); sgtl5000_1.enable(); sgtl5000_1.volume(0.32); waveform1.begin(WAVEFORM_SAWTOOTH); waveform1.amplitude(0.75); waveform1.frequency(82.41); waveform1.pulseWidth(0.15); waveform2.begin(WAVEFORM_SAWTOOTH); waveform2.amplitude(0.75); waveform2.frequency(123); waveform2.pulseWidth(0.15); pink1.amplitude(1.0); mixer1.gain(0, 0.5); mixer1.gain(1, 0.5); mixer1.gain(2, 0.0); envelope1.attack(10); envelope1.decay(0); envelope1.sustain(1); envelope1.release(500); // Put something on LCD screen tft.begin(); tft.setRotation(3); // Rotates screen to match the baseboard orientation tft.fillScreen(ILI9341_BLUE); tft.setCursor(16, 110); // Set cursor position tft.setFont(Arial_32_Bold); // Set font style and size tft.printf("Fun with MIDI"); } void loop() { // usbMIDI.read(); MIDI.read(); LFOupdate(false, LFOmodeSelect, FILfactor, LFOdepth); } void myNoteOn(byte channel, byte note, byte velocity) { if ( note > 23 && note < 108 ) { globalNote = note; globalVelocity = velocity; keyBuff(note, true); LFOupdate(true, LFOmodeSelect, FILfactor, LFOdepth); } } void myNoteOff(byte channel, byte note, byte velocity) { if ( note > 23 && note < 108 ) { keyBuff(note, false); } } void myPitchBend(byte channel, int bend) { float bendF = bend; bendF = bendF / 8192; bendF = bendF * bendRange; bendF = bendF / 12; bendFactor = pow(2, bendF); oscSet(); } void keyBuff(byte note, bool playNote) { static byte buff[BUFFER]; static byte buffSize = 0; // Add Note if (playNote == true && (buffSize < BUFFER) ) { oscPlay(note); buff[buffSize] = note; buffSize++; return; } // Remove Note else if (playNote == false && buffSize != 0) { for (byte found = 0; found < buffSize; found++) { if (buff[found] == note) { for (byte gap = found; gap < (buffSize - 1); gap++) { buff[gap] = buff[gap + 1]; } buffSize--; buff[buffSize] = 255; if (buffSize != 0) { oscPlay(buff[buffSize - 1]); return; } else { oscStop(); return; } } } } } void oscPlay(byte note) { waveform1.frequency(noteFreqs[note] * bendFactor * LFOpitch); waveform2.frequency(noteFreqs[note + octave] * detuneFactor * bendFactor * LFOpitch); float velo = (globalVelocity * DIV127); waveform1.amplitude(velo); waveform2.amplitude(velo); pink1.amplitude(velo); envelope1.noteOn(); } void oscStop() { envelope1.noteOff(); } void oscSet() { waveform1.frequency(noteFreqs[globalNote] * bendFactor * LFOpitch); waveform2.frequency(noteFreqs[globalNote + octave] * detuneFactor * bendFactor * LFOpitch); } void myControlChange(byte channel, byte control, byte value) { switch (control) { case 100: mixer1.gain(0, (value * DIV127)); break; case 101: mixer1.gain(1, (value * DIV127)); break; case 102: mixer1.gain(2, (value * DIV127)); break; case 103: switch (value) { case 0: octave = 24; break; case 1: octave = 12; break; case 2: octave = 0; break; case 3: octave = -12; break; case 4: octave = -24; break; } oscSet(); break; case 104: envelope1.attack(3000 * (value * DIV127)); break; case 105: envelope1.decay(3000 * (value * DIV127)); break; case 106: envelope1.sustain(value * DIV127); break; case 107: envelope1.release(3000 * (value * DIV127)); break; case 108: switch (value) { case 0: waveform1.begin(WAVEFORM_SINE); break; case 1: waveform1.begin(WAVEFORM_TRIANGLE); break; case 2: waveform1.begin(WAVEFORM_SAWTOOTH); break; case 3: waveform1.begin(WAVEFORM_PULSE); break; } break; case 109: switch (value) { case 0: waveform2.begin(WAVEFORM_SINE); break; case 1: waveform2.begin(WAVEFORM_TRIANGLE); break; case 2: waveform2.begin(WAVEFORM_SAWTOOTH); break; case 3: waveform2.begin(WAVEFORM_PULSE); break; } case 110: detuneFactor = 1 - (0.05 * (value * DIV127)); oscSet(); break; case 111: FILfactor = value * DIV127; FILfreq = 10000 * (value * DIV127); if (LFOmodeSelect < 1 || LFOmodeSelect > 5)filter1.frequency(FILfreq); break; case 112: filter1.resonance((4.3 * (value * DIV127)) + 0.7); break; case 113: if (value <= 12 && value > 0) bendRange = value; break; case 114: { float xSpeed = value * DIV127; xSpeed = pow(100, (xSpeed - 1)); LFOspeed = (70000 * xSpeed); break; } case 115: LFOdepth = value * DIV127; break; case 116: LFOmodeSelect = value; break; } } void LFOupdate(bool retrig, byte mode, float FILtop, float FILbottom) { static float LFO = 0; static unsigned long LFOtime = 0; static bool LFOdirection = false; unsigned long currentMicros = micros(); static bool LFOstop = false; static float LFOrange = 0; static byte oldMode = 0; static bool retriggered = false; if (retrig == true) retriggered = true; if (currentMicros - LFOtime >= LFOspeed) { LFOtime = currentMicros; if (mode != oldMode) { if (mode == 0 || mode == 8) { LFOpitch = 1; oscSet(); filter1.frequency(FILfreq); } else if (mode >= 1 || mode <= 7) { LFOpitch = 1; oscSet(); } else if (mode >= 9 || mode <= 13) { filter1.frequency(FILfreq); } oldMode = mode; } LFOrange = FILtop - FILbottom; if (LFOrange < 0) LFOrange = 0; // LFO Modes switch (mode) { case 0: //Filter OFF return; break; case 1: //Filter FREE filter1.frequency(10000 * ((LFOrange * LFO) + LFOdepth)); break; case 2: //Filter DOWN if (retriggered == true) { LFOdirection = true; LFO = 1.0; } filter1.frequency(10000 * ((LFOrange * LFO) + LFOdepth)); break; case 3: //Filter UP if (retriggered == true) { LFOdirection = false; LFO = 0; } filter1.frequency(10000 * ((LFOrange * LFO) + LFOdepth)); break; case 4: //Filter 1-DN if (retriggered == true) { LFOstop = false; LFOdirection = true; LFO = 1.0; } if (LFOstop == false) filter1.frequency(10000 * ((LFOrange * LFO) + LFOdepth)); break; case 5: //Filter 1-UP if (retriggered == true) { LFOstop = false; LFOdirection = false; LFO = 0; } if (LFOstop == false) filter1.frequency(10000 * ((LFOrange * LFO) + LFOdepth)); break; case 8: //Pitch OFF return; break; case 9: //Pitch FREE LFOpitch = (LFO * LFOdepth) + 1; oscSet(); break; case 10: //Pitch DOWN if (retriggered == true) { LFOdirection = true; LFO = 1.0; } LFOpitch = (LFO * LFOdepth) + 1; oscSet(); break; case 11: //Pitch UP if (retriggered == true) { LFOdirection = false; LFO = 0; } LFOpitch = (LFO * LFOdepth) + 1; oscSet(); break; case 12: //Pitch 1-DN if (retriggered == true) { LFOstop = false; LFOdirection = true; LFO = 1.0; } if (LFOstop == false) { LFOpitch = (LFO * LFOdepth) + 1; oscSet(); } break; case 13: //Pitch 1-UP if (retriggered == true) { LFOstop = false; LFOdirection = false; LFO = 0; } if (LFOstop == false) { LFOpitch = (LFO * LFOdepth) + 1; oscSet(); } break; } retriggered = false; // Update LFO if (LFOdirection == false) { //UP LFO = (LFO + 0.01); if (LFO >= 1) { LFOdirection = true; LFOstop = true; } } if (LFOdirection == true) { //Down LFO = (LFO - 0.01); if (LFO <= 0) { LFOdirection = false; LFOstop = true; } } } }
Further Reading:
PJRC Audio Forum – Excellent source of technical information for using audio with Teensy.
notesandvolts.com – Additional information on the example synth program and creating synthesizers in general.
Notes:
- None
Technical Specifications
Operating Voltage | 3.3V |
Connectors | 5-pin DIN |
Board Dimensions (PCB) | 63.5 x 63.5mm (2.5 x 2.5″) |
Country of Origin | Designed in USA, Manufactured in China |