Yet another Midi Trigger module

Hi there,

after a long time I’ve managed to finish a new module. This time it is an 9 channel midi trigger module, to translate the 5pin DIN MIDI connector to 9 different single outputs with 5V trigger pulses, for example to trigger different drum modules.
The module is Arduino nano based, and features a SSD1306 OLED display and a rotary encoder. I have implemented a little menu to configure the most parameters of the module, such as the input midi channel (default: 10), the midi note number for each trigger channel and the trigger length (default 10ms). All parameters can be stored in the internal eeprom.

To increase the OLED lifetime the display will go off after 10s without any input.

The front is made from 1.5mm thick aluminum, the frontplate is laminated paper covered with clear adhesive foil for school books.

Here are schematic, arduino source code (I use visual studio code) and some fotos.

Schematic (100% working):

Main menu:

Edit dialog for the midi channel:

Complete module:

PCB (funfact: I’ve forgot to connect 2(!) gnd areas to the ground plane :rofl:)

complete module:

Source Code:

#include <Arduino.h>
#include <MIDI.h>
#include <EEPROM.h>
#include <Encoder.h>
#include <Wire.h>
#include <U8g2lib.h>

// ########################################################################
// declarations
MIDI_CREATE_DEFAULT_INSTANCE();

// define Arduino outputs
#define TRIG1 2
#define TRIG2 3
#define TRIG3 4
#define TRIG4 5
#define TRIG5 6
#define TRIG6 7
#define TRIG7 8
#define TRIG8 9
#define TRIG9 10

// Display
// SDA A4
// SCL A5
#define OLED_ADDRESS 0x3C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
bool displayOn = LOW;
#define textHeight 12
#define boxHeight textHeight + 2
#define lcdWidth 128

// Rotary Encoder
#define ENCODER_OPTIMIZE_INTERRUPTS
#define CLK A2
#define DT A1
#define BUTTONENC A0
Encoder myEnc(DT, CLK);
float oldPosition = -999;
float newPosition = -999;
bool lastencoderPressed = LOW;
bool encoderPressed = LOW;

long timerDisplay = 0;
long displayOnTime = 20000;

// shut off display 10s after last action
long displayTime = 10000;

// current menu mode
int menuMode = 0;
bool doUpdateMenu = true;

// trigger length in ms
unsigned int trigLength; // = 10;

// current Midi Channel
unsigned int midiChannel; // = 10;

/*
MIDI Notes and sounds from the General Midi Table
27	Laser	                    60	High Bongo
28	Whip	                    61	Low Bongo
29	Scratch Push	            62	Conga Dead Stroke
30	Scratch Pull	            63	Conga
31	Stick Click	              64	Tumba
32	Metronome Click	          65	High Timbale
34	Metronome Bell	          66	Low Timbale
35	Bass Drum	                67	High Agogo
36	Kick Drum	                68	Low Agogo
37	Snare Cross Stick	        69	Cabasa
38	Snare Drum	              70	Maracas
39	Hand Clap	                71	Whistle Short
40	Electric Snare Drum	      72	Whistle Long
41	Floor Tom 2	              73	Guiro Short
42	Hi-Hat Closed	            74	Guiro Long
43	Floor Tom 1	              75	Claves
44	Hi-Hat Foot	              76	High Woodblock
45	Low Tom	                  77	Low Woodblock
46	Hi-Hat Open	              78	Cuica High
47	Low-Mid Tom	              79	Cuica Low
48	High-Mid Tom	            80	Triangle Mute
49	Crash Cymbal	            81	Triangle Open
50	High Tom	                82	Shaker
51	Ride Cymbal	              83	Sleigh Bell
52	China Cymbal	            84	Bell Tree
53	Ride Bell	                85	Castanets
54	Tambourine	              86	Surdu Dead Stroke
55	Splash cymbal	            87	Surdu
56	Cowbell	                  91	Snare Drum Rod
57	Crash Cymbal 2	          92	Ocean Drum
58	Vibraslap	                93	Snare Drum Brush
59	Ride Cymbal 2
*/

// assignments for the trigger notes
unsigned int trig1Note; // = 35;
unsigned int trig2Note; // = 36;
unsigned int trig3Note; // = 38;
unsigned int trig4Note; // = 39;
unsigned int trig5Note; //  = 45;
unsigned int trig6Note; // = 47;
unsigned int trig7Note; // = 48;
unsigned int trig8Note; // = 50;
unsigned int trig9Note; // = 42;

// trigger timer 
unsigned long trig1Timer = 0;
unsigned long trig2Timer = 0;
unsigned long trig3Timer = 0;
unsigned long trig4Timer = 0;
unsigned long trig5Timer = 0;
unsigned long trig6Timer = 0;
unsigned long trig7Timer = 0;
unsigned long trig8Timer = 0;
unsigned long trig9Timer = 0;

// ################################################################################
//  void DisplayOff()
// ################################################################################
void DisplayOff(){
  u8g2.clearDisplay();
}

// ################################################################################
// Menu Functions
// ################################################################################
void showConfigScreen (char *text, unsigned int *variable, unsigned int minValue, unsigned int maxValue){
  bool updateDisplay = true;

  do 
  {    
    lastencoderPressed = encoderPressed;
    encoderPressed = digitalRead(BUTTONENC);

    newPosition = myEnc.read();
    if ((newPosition - 3) / 4 > oldPosition / 4)    
    {
      // encoder rotated clock wise
      oldPosition = newPosition;
      (*variable)++;
      if((*variable) > maxValue){
        (*variable) = maxValue;
      }         
      updateDisplay = true; 
    } 
    else if ((newPosition + 3) / 4 < oldPosition / 4)
    {
      // encoder rotated counter clock wise
      oldPosition = newPosition;
      (*variable)--;
      if((*variable) < minValue){
        (*variable) = minValue;
      }          
      updateDisplay = true;
    }

    if (updateDisplay){
      u8g2.firstPage();
      do {
        u8g2.setCursor (0,12);
        u8g2.println (text);
        u8g2.setCursor (8, 28);
        u8g2.print(">");
        u8g2.setCursor (20, 28);
        u8g2.print(*variable);
        updateDisplay = false;

      }while ( u8g2.nextPage() );
    }
  } while (!(!lastencoderPressed && encoderPressed));
}


void setMidiChannel(){
  showConfigScreen("Set Midi Ch.:", &midiChannel, 1, 16);
  MIDI.setInputChannel(midiChannel);
}
void setTrig1Note() {
  showConfigScreen("Set Trig1 Note:", &trig1Note, 27, 93);
}
void setTrig2Note() {
  showConfigScreen("Set Trig2 Note:", &trig2Note, 27, 93);
}
void setTrig3Note() {
  showConfigScreen("Set Trig3 Note:", &trig3Note, 27, 93);
}
void setTrig4Note() {
  showConfigScreen("Set Trig4 Note:", &trig4Note, 27, 93);
}
void setTrig5Note() {
  showConfigScreen("Set Trig5 Note:", &trig5Note, 27, 93);
}
void setTrig6Note() {
  showConfigScreen("Set Trig6 Note:", &trig6Note, 27, 93);
}
void setTrig7Note() {
  showConfigScreen("Set Trig7 Note:", &trig7Note, 27, 93);
}
void setTrig8Note() {
  showConfigScreen("Set Trig8 Note:", &trig8Note, 27, 93);
}
void setTrig9Note() {
  showConfigScreen("Set Trig9 Note:", &trig9Note, 27, 93);
}
void setTriggerLenght() {
  showConfigScreen("Set Trig Len:", &trigLength, 1, 100);
}

void saveEeprom() {
  int address  = 0;
  EEPROM.put(address,midiChannel);
  address += sizeof(midiChannel);
  EEPROM.put(address,trig1Note);
  address += sizeof(trig1Note);
  EEPROM.put(address,trig2Note);
  address += sizeof(trig2Note);
  EEPROM.put(address,trig3Note);
  address += sizeof(trig3Note);
  EEPROM.put(address,trig4Note);
  address += sizeof(trig4Note);
  EEPROM.put(address,trig5Note);
  address += sizeof(trig5Note);
  EEPROM.put(address,trig6Note);
  address += sizeof(trig6Note);
  EEPROM.put(address,trig7Note);
  address += sizeof(trig7Note);
  EEPROM.put(address,trig8Note);
  address += sizeof(trig8Note);
  EEPROM.put(address,trig9Note);
  address += sizeof(trig9Note);
  EEPROM.put(address,trigLength);
  EEPROM.put(100, 99);
}

void restoreDefaults() {
  midiChannel = 10;
  trig1Note = 35;
  trig2Note = 36;
  trig3Note = 38;
  trig4Note = 39;
  trig5Note = 45;
  trig6Note = 47;
  trig7Note = 48;
  trig8Note = 50;
  trig9Note = 42;
  trigLength = 10;
  saveEeprom();

  u8g2.firstPage();
  do {
    u8g2.setCursor (0,12);
    u8g2.println ("Defaults");
    u8g2.setCursor (8, 28);
    u8g2.println ("restored!");
  }while ( u8g2.nextPage() );

  delay (2000);
}

void saveSettings() {
  saveEeprom();

  u8g2.firstPage();
  do {
    u8g2.setCursor (0,12);
    u8g2.println ("Settings Saved!");
  } while ( u8g2.nextPage() );
  delay (2000);
}

void loadSettings() {
  // If found value 99 in address 100 there is valid data in eeprom
  // if not load default values and save them to eeprom

  int address  = 0;
  int tempValue;
  EEPROM.get(100,tempValue);

  if (tempValue != 99) {
    restoreDefaults();
  } else {
    EEPROM.get(address,midiChannel);
    address += sizeof(midiChannel);
    EEPROM.get(address,trig1Note);
    address += sizeof(trig1Note);
    EEPROM.get(address,trig2Note);
    address += sizeof(trig2Note);
    EEPROM.get(address,trig3Note);
    address += sizeof(trig3Note);
    EEPROM.get(address,trig4Note);
    address += sizeof(trig4Note);
    EEPROM.get(address,trig5Note);
    address += sizeof(trig5Note);
    EEPROM.get(address,trig6Note);
    address += sizeof(trig6Note);
    EEPROM.get(address,trig7Note);
    address += sizeof(trig7Note);
    EEPROM.get(address,trig8Note);
    address += sizeof(trig8Note);
    EEPROM.get(address,trig9Note);
    address += sizeof(trig9Note);
    EEPROM.get(address,trigLength);
  }
}

void exitMenu() {
  menuMode = 0;
  DisplayOff();
  doUpdateMenu = false;
}

// menu items
const char *menuItems[] = 
{
  "MIDI Channel",
  "TRIG1 Note",
  "TRIG2 Note",
  "TRIG3 Note",
  "TRIG4 Note",
  "TRIG5 Note",
  "TRIG6 Note",
  "TRIG7 Note",
  "TRIG8 Note",
  "TRIG9 Note",
  "Trigger Length",
  "Save Settings",
  "Restore Defaults",
  "<Exit Menu>",
};

// function pointers for menu items
void (*function_pointer[]) () =
{
  setMidiChannel,
  setTrig1Note,
  setTrig2Note,
  setTrig3Note,
  setTrig4Note,
  setTrig5Note,
  setTrig6Note,
  setTrig7Note,
  setTrig8Note,
  setTrig9Note,
  setTriggerLenght,
  saveSettings,
  restoreDefaults,
  exitMenu,
};

int menuLength = sizeof(menuItems) / 2;
int menuInLCD = 4;
int menuPosition = 0;
int menuPositionOnLCD = 0;


// ################################################################################
//  void ShowWelcome()
// ################################################################################
void ShowWelcome(){
  u8g2.firstPage();
  do {
    u8g2.setCursor (10,24);
    u8g2.println ("MidiTrigger");
    u8g2.setCursor (10,36);
    u8g2.println ("V1.0");
  }while ( u8g2.nextPage() );
}


// ################################################################################
// void setup
// ################################################################################
void setup() {
  // configure pins
  pinMode(TRIG1, OUTPUT);
  pinMode(TRIG2, OUTPUT);
  pinMode(TRIG3, OUTPUT);
  pinMode(TRIG4, OUTPUT);
  pinMode(TRIG5, OUTPUT);
  pinMode(TRIG6, OUTPUT);
  pinMode(TRIG7, OUTPUT);
  pinMode(TRIG8, OUTPUT);
  pinMode(TRIG9, OUTPUT);

  pinMode(BUTTONENC, INPUT);

  // load data from EEPROM
  loadSettings();

  // initialize MIDI
  MIDI.begin(midiChannel);           

  // initialize display
  u8g2.begin();  
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_pxplusibmvga8_tr);

  // SPI Settings
  // https://docs.arduino.cc/learn/communication/spi?_gl=1*1y7brsy*_ga*MTM3OTI0NDcyMS4xNjcyMTQ2MTI1*_ga_NEXN8H46L5*MTY3MzM3OTg5OC41LjEuMTY3MzM3OTkwOS4wLjAuMA..
  //SPI.begin();
  //SPI.setBitOrder(MSBFIRST);            // bit order
  //SPI.setClockDivider(SPI_CLOCK_DIV4);  // (16MHz/4)
  //SPI.setDataMode(SPI_MODE0);       

  // show welcome message on startup
  ShowWelcome();
  delay(2000);
  DisplayOff();
}


// ################################################################################
//  void UpdateMenu()
// ################################################################################
void UpdateMenu(){

  // show menu
  u8g2.firstPage();
  do {
    for(int m = 0; m <= menuLength; m++)
    {
      if(menuPosition < menuInLCD)    
      {
        if(menuPosition == m) {
          u8g2.drawRFrame (0, m * textHeight, lcdWidth, boxHeight, 2);
        }
        u8g2.setCursor(2, m * textHeight);
        u8g2.println( menuItems[m-1] );
      }
      else
      {
        if(menuPosition == m) {
          u8g2.drawRFrame(0, (m - (1 + menuPosition - menuInLCD)) * textHeight, lcdWidth, boxHeight, 2);
        }
        u8g2.setCursor(2, (m - (1 + menuPosition - menuInLCD)) * textHeight);
        u8g2.println( menuItems[m-1]);
      }
    }
    
  } while ( u8g2.nextPage() );
}




// ################################################################################
// void loop
// ################################################################################
void loop() {
  // get encoder button
  lastencoderPressed = encoderPressed;
  encoderPressed = digitalRead(BUTTONENC);
  
  if (!lastencoderPressed && encoderPressed)
  {

    timerDisplay = millis();
    displayOn = HIGH;

    switch (menuMode)
    {
      case 0:
        menuMode = 1;
        break;
      case 1:
        doUpdateMenu = true;
        function_pointer[menuPosition]();        
        if (doUpdateMenu){
          UpdateMenu();
        }
        doUpdateMenu = true;
        break;
    }
  }

  if (menuMode == 1){
    // the encoder sends 4 clicks per turn
    newPosition = myEnc.read();
    if ((newPosition - 3) / 4 > oldPosition / 4)    
    {
      // encoder was turned clock wise
      timerDisplay = millis();
      oldPosition = newPosition;
      menuPosition++;
      if(menuPosition >= (menuLength - 1)){
        menuPosition = menuLength - 1;
      }    
      UpdateMenu();
    } 
    else if ((newPosition + 3) / 4 < oldPosition / 4)
    {
      // encoder was turned counter clock wise
      timerDisplay = millis();
      oldPosition = newPosition;
      menuPosition--;
      if(menuPosition < 0){
        menuPosition = 0;
      }
      UpdateMenu();
    }

    if (millis() - timerDisplay > displayOnTime){
      menuMode = 0;
      DisplayOff();
    }
  }
 
  // check all trigger timer values and set them LOW
  long curMillis = (long)millis();
  if (curMillis - trig1Timer > trigLength){
    digitalWrite(TRIG1, LOW);
  }
  if (curMillis - trig2Timer > trigLength){
    digitalWrite(TRIG2, LOW);
  }
  if (curMillis - trig3Timer > trigLength){
    digitalWrite(TRIG3, LOW);
  }
  if (curMillis - trig4Timer > trigLength){
    digitalWrite(TRIG4, LOW);
  }
  if (curMillis - trig5Timer > trigLength){
    digitalWrite(TRIG5, LOW);
  }
  if (curMillis - trig6Timer > trigLength){
    digitalWrite(TRIG6, LOW);
  }
  if (curMillis - trig7Timer > trigLength){
    digitalWrite(TRIG7, LOW);
  }
  if (curMillis - trig8Timer > trigLength){
    digitalWrite(TRIG8, LOW);
  }
  if (curMillis - trig9Timer > trigLength){
    digitalWrite(TRIG9, LOW);
  }

  // handle midi events
  if (MIDI.read()){

    switch (MIDI.getType()) {

      case (midi::NoteOn):
      {
        // get current tone number from incoming midi data
        int currentNote = MIDI.getData1();

        // test all configured tone numbers and set the trigger to HIGH
        if (currentNote == (int)trig1Note){
          trig1Timer = millis();
          digitalWrite(TRIG1, HIGH);
        }
        if (currentNote == (int)trig2Note){
          trig2Timer = millis();
          digitalWrite(TRIG2, HIGH);
        } 
        if (currentNote == (int)trig3Note){
          trig3Timer = millis();
          digitalWrite(TRIG3, HIGH);
        } 
        if (currentNote == (int)trig4Note){
          trig4Timer = millis();
          digitalWrite(TRIG4, HIGH);
        } 
        if (currentNote == (int)trig5Note){
          trig5Timer = millis();
          digitalWrite(TRIG5, HIGH);
        } 
        if (currentNote == (int)trig6Note){
          trig6Timer = millis();
          digitalWrite(TRIG6, HIGH);
        } 
        if (currentNote == (int)trig7Note){
          trig7Timer = millis();
          digitalWrite(TRIG7, HIGH);
        } 
        if (currentNote == (int)trig8Note){
          trig8Timer = millis();
          digitalWrite(TRIG8, HIGH);
        } 
        if (currentNote == (int)trig9Note){
          trig9Timer = millis();
          digitalWrite(TRIG9, HIGH);
        }
        break;
      }

      
      // unused midi events:
      case midi::NoteOff:
      case midi::PitchBend:
      case midi::Clock:
      case midi::SystemExclusive:
      case midi::AfterTouchPoly:      
      case midi::ControlChange:
      case midi::ProgramChange:   
      case midi::AfterTouchChannel: 
      case midi::TimeCodeQuarterFrame:
      case midi::SongPosition:
      case midi::SongSelect:       
      case midi::TuneRequest:
      case midi::SystemExclusiveEnd:
      case midi::Tick:
      case midi::Start:
      case midi::Continue:
      case midi::Stop:
      case midi::ActiveSensing:
      case midi::SystemReset:
      case midi::InvalidType:
      case midi::Undefined_F4:
      case midi::Undefined_F5:
      case midi::Undefined_FD:
        break;

    }
  }
}
4 Likes

22k seems a little high for current limiting LEDs, I would have thought closer to 220r

thats right, but I used low current leds and this was the perfect intensity. But it clearly depends on the leds. I dont want the pulses to be too bright.

The super bright green LEDs I use are still plenty bright with 22k.

Recommended resistor at 5v on a super bright red led is 150r

Green needs about 4x red to get the same brightness, but 150r would be blindingly bright. Way more than I want in my synth case. This varies by LED and personal preference, of course.

yes, to get the maximum brigthness this is ok, but I dont want to illuminate my living room with this, just a little glimpse when the trigger is active :relaxed:
I’ve played a lot with the resistor values and these are the best.

No one mentioned green, we are talking red LEDs used In this design. If you are having to put a 22k led to keep the brightness down on a super bright then dont use a super bright, you are using them incorrectly a super bright is supposed to be super bright, the clue is in the name. Use a normal led as it’s supposed to be used.

And yes a green LED can take your retinas out at 30 paces on full brightness.

Ok, from this view you are right.
I used the Leds which I have a lot in my parts bin and dont want to buy extra parts.
And you dont have to use leds at full power.
By using a red led and a 220 ohms resistor at 5V you are driving about 16mA trough the led, and this is nearly the allowed maximum and more than the nano can handle if all channels would be triggered simultaneously. I always select the leds to run at the minimum possible current.

2 Likes

I’ve just built something very similar to this and I have 10v triggers and standard LEDs with 220r in series, not bright at all really, but it depends on the LEDs as different manufacturers have different brightnesses even for the same LED type.

Really only 220R? This would be 34mA with an average forward voltage of 2.5V… this is way more than the maximum current of 20mA of a standard led.

Why? I use them a lot at super low current for not blindingly bright indicators. They work fine and draw much less current, therefore don’t load down the power rails so much. Just because they can be super bright doesn’t mean they have to be.

2 Likes

Well it’s not for the LEDs it was simply a current limit on the gate drivers and I attached LEDs as a test. The LEDs are driven from a separate 3.3v powered 74hc595 and use a lower value resistor. On the 10v circuit they were not actually that bright, but these are just some random red LEDs I picked up online, no idea of the specs. They seem fine on the 3.3v circuit.