Arduino MIDI to CV Convertor


/*  Another MIDI2CV Box
 *  MIDI interface for Hz/V and V/oct synths. Connect your favourite keyboard or DAW to the box via MIDI; 
 *  the box will in turn control your synth gate On/Off (straight +5V/0V or S-Trig, reversed, 
 *  0V/+5V), pitch (variable voltage via DAC output), velocity (0V to +5V, RC filtered) and a MIDI Control 
 *  Change variable at your will (0V to +5V, RC filtered).
 *  
 *  Connections:
 * (1) MIDI connector: see general online reference 
 * (2) DAC MCP4725:
 * SDA pin to A4/SDA (Arduino UNO adn nano) 2 micro 
 * SCL pin to A5/SCL (Arduino UNO adn nano) 3 micro
 * GND to GND
 * VCC to +5V
 * (3) Outputs: 
 * Arduino gate OUT pin (pin 12 by default) to synth gate/trigger IN via 1K Ohm resistor - pin 7 micro
 * Arduino velocity OUT pin (pin 10 by default) to synth VCA IN via RC filter (1K Ohm, 100uF) - pin 5 micro
 * Arduino MIDI CC OUT pin (pin 9 by default) to synth VCF IN via RC filter (1K Ohm, 100uF) - pin 6 micro
 * DAC OUT to synth VCO IN
 * 
 * MIDI messages table:
 *    Message                      Status    Data 1               Data 2
 *    Note Off                     8n        Note Number          Velocity
 *    Note On                      9n        Note Number          Velocity
 *    Polyphonic Aftertouch        An        Note Number          Pressure
 *    Control Change               Bn        Controller Number    Data
 *    Program Change               Cn        Program Number       Unused
 *    Channel Aftertouch           Dn        Pressure             Unused
 *    Pitch Wheel                  En        LSB                  MSB    
 *    
 * Key
 * n is the MIDI Channel Number (0-F)
 * LSB is the Least Significant Byte
 * MSB is the Least Significant Byte
 * There are several different types of controller messages. 
 * 
 * useful links, random order:
 *  https://en.wikipedia.org/wiki/CV/gate
 *  https://www.instructables.com/id/Send-and-Receive-MIDI-with-Arduino/
 *  http://www.songstuff.com/recording/article/midi_message_format
 *  https://espace-lab.org/activites/projets/en-arduino-processing-midi-data/
 *  https://learn.sparkfun.com/tutorials/midi-shield-hookup-guide/example-2-midi-to-control-voltage
 *  https://provideyourown.com/2011/analogwrite-convert-pwm-to-voltage/
 *  https://www.midi.org/specifications/item/table-3-control-change-messages-data-bytes-2
 *  https://arduino-info.wikispaces.com/Arduino-PWM-Frequency
 *  
 *  by Barito, 2017 - 2018
 *  
 *  UPDATE-FORK- Nov 2021
 *  ProMicro Overhaul, Screen, buttons and save functions added by Dan Suter 2021
 *  Working on midi through using out socket.
 *  https://dansfing.uk - PCB and complete kit to make this project
 */
#include <EEPROM.h>
#include <Adafruit_MCP4725.h>
#include <MIDI.h>
#include <U8x8lib.h>
#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
//U8X8_SSD1306_128X64_NONAME_4W_HW_SPI u8x8(/* cs=*/ 4, /* dc=*/ 8, /* reset=*/ 19);
U8X8_SH1106_128X64_NONAME_4W_HW_SPI u8x8(/* cs=*/ A1, /* dc=*/ A0, /* reset=*/ 14);
//set at your will ...
#define MIDI_CHANNEL 1 //the MIDI channel you want your box to listen to (1-16)
int REVERSE_GATE = 0; //V-Trig = 0, S-Trig = 1
#define CC_NUMBER 19 //MIDI CC number
bool HZV = 0; //set to "0" for V/oct

Adafruit_MCP4725 dac;

byte gatePin = 7;
byte velocityPin = 5; //pwm frequency is going to be increased for this in the setup
byte CCPin = 6; //pwm frequency is going to be increased for this in the setup

float outVoltmV;
int velocityOut;
int CCOut=19;
uint16_t dacValue;
//CHANGE IF BY PRESSING "C" ON YOUR KEYBOARD YOU HAVE ANOTHER NOTE OUTPUTTED BY THE SYNTH (HZ/V). 
int noteHZVshift = -1;//With external USB hub I had to set this to "-1". With PC USB to "+2".
//CHANGE IF YOU HAVE A DETUNING BETWEEN ADIACENT NOTES/OCTAVES (V/oct).
float VoctLinCoeff = 0.0833;//If your +5V are straight +5.000V this is 1/12 = 0.0833.
//CHANGE TO SHIFT BY OCTAVES (V/oct).
float VoctShift = -2.0;
byte lastNote;
MIDI_CREATE_DEFAULT_INSTANCE();
int CH_BUT = 4;
int INV_BUT = 8;
int CC_BUT = 9 ; 
int midichin = 1;
int ccout = 19;
int current_cc;

// Handle the MIDI Channel Button
long ch_button_timer=0;
long ch_button_long_timer=250;
boolean ch_buttonActive = false;
boolean ch_longPressActive = false;
// Handle the Invert Gate Button
long inv_button_timer=0;
long inv_button_long_timer=250;
boolean inv_buttonActive = false;
boolean inv_longPressActive = false;

// Handle the CC Button
long cc_button_timer=0;
long cc_button_long_timer=250;
boolean cc_buttonActive = false;
boolean cc_longPressActive = false;
unsigned long OLEDinterval=30000; // SAVE Burnout turn OLED off after x Milliseconds
unsigned long OLEDpreviousMillis=0; // millis() returns an unsigned long.



void setup() {  
  midichin = readIntFromEEPROM(45);
 ccout = readIntFromEEPROM(47);
//For Arduino Uno, Nano, and any other board using ATmega 8, 168 or 328
//TCCR0B = TCCR0B & B11111000 | B00000001;    // D5, D6: set timer 0 divisor to 1 for PWM frequency of 62500.00 Hz
TCCR1B = TCCR1B & B11111000 | B00000001;    // D9, D10: set timer 1 divisor to 1 for PWM frequency of 31372.55 Hz
//TCCR2B = TCCR2B & B11111000 | B00000001;    // D3, D11: set timer 2 divisor to 1 for PWM frequency of 31372.55 Hz
pinMode(gatePin, OUTPUT);
pinMode(velocityPin, OUTPUT);
pinMode(CCPin, OUTPUT);

//Buttons
  pinMode(CH_BUT, INPUT_PULLUP);
  pinMode(INV_BUT, INPUT_PULLUP);
  pinMode(CC_BUT, INPUT_PULLUP);
if(REVERSE_GATE==1){digitalWrite(gatePin, HIGH);}
else {digitalWrite(gatePin, LOW);}
MIDI.setHandleNoteOn(handleNoteOn);
MIDI.setHandleNoteOff(handleNoteOff);
MIDI.setHandleControlChange(handleControlChange);
MIDI.begin(MIDI_CHANNEL_OMNI);// start MIDI and listen to channel "MIDI_CHANNEL"
MIDI.turnThruOn();
// For Adafruit MCP4725A1 the address is 0x62 (default) or 0x63 (ADDR pin tied to VCC)
// For MCP4725A0 the address is 0x60 or 0x61
// For MCP4725A2 the address is 0x64 or 0x65
dac.begin(0x60);
 u8x8.begin();
 pre();
 u8x8.setFont(u8x8_font_chroma48medium8_r);
 
  u8x8.setCursor(0,2);
  u8x8.print("    v.1 2021");
   u8x8.setCursor(0,4);
  u8x8.print(" MIDI TO CV BOX"); 
  delay(5000);
  u8x8.clearDisplay();

  displayCHupdate(midichin);
}

int displayCHupdate(int chin){
  //u8x8.clearLine(6);
  if(chin<=9){
u8x8.setCursor(0,2);

  u8x8.print("MIDI");
  u8x8.setCursor(5,2);

  u8x8.print(chin);
  u8x8.print(' ');
  }
  if(chin>=10){
u8x8.setCursor(0,2);
  
  u8x8.print("MIDI");
  u8x8.setCursor(5,2);
 
  u8x8.print(chin);
 
  }
}

int displayCCupdate(int chin){
  
  if(chin<=9){ 
    u8x8.clearLine(7);
   u8x8.setCursor(8,7);
   u8x8.inverse();
  u8x8.print("CC ");
  u8x8.setCursor(12,7);
 
  u8x8.noInverse();

  u8x8.setCursor(12,7);
  u8x8.print(chin);
  
  }
  if((chin>=10)&&(chin<=99)){
  u8x8.setCursor(8,7);
   u8x8.inverse();
  u8x8.print("CC ");
  u8x8.setCursor(12,7);
  u8x8.noInverse();
  u8x8.print(chin);
   u8x8.print(' ');
  }
    if(chin>=100){
  u8x8.setCursor(8,7);
   u8x8.inverse();
  u8x8.print("CC ");
  u8x8.setCursor(12,7);
  u8x8.noInverse();
  u8x8.print(chin);
 
  }
}

int readIntFromEEPROM(int address)
{
  return (EEPROM.read(address) << 8) + EEPROM.read(address + 1);
}

void writeIntIntoEEPROM(int address, int number)
{ 
  EEPROM.write(address, number >> 8);
  EEPROM.write(address + 1, number & 0xFF);
}

void loop() {
//You cant use delays in the main loop.
unsigned long OLEDcurrentMillis = millis(); // grab current time
if ((unsigned long)(OLEDcurrentMillis - OLEDpreviousMillis) >= OLEDinterval) {
    u8x8.clearDisplay();
   // save the "current" time
   OLEDpreviousMillis = millis();
 }
//Start Midi Read on our selected channel  
MIDI.read(midichin);
//Listen for button presses change values and update screen
if (digitalRead(CH_BUT) == 0) {

if (ch_buttonActive == false) {
      int chin;
      ch_buttonActive = true;
      ch_button_timer = millis();
      if (midichin<=15){midichin++;} else {midichin=1;}
      displayCHupdate(midichin);
    }

    if ((millis() - ch_button_timer > ch_button_long_timer) && (ch_longPressActive == false)) {
      ch_longPressActive = true;
      midichin=midichin-1;
      ccout=current_cc;
      displayCHupdate(midichin);
       displayCCupdate(ccout);
    }

  } else {

    if (ch_buttonActive == true) {

      if (ch_longPressActive == true) {
        ch_longPressActive = false;
      } else {
      }
     ch_buttonActive = false;
    }
  }

   if ((digitalRead(INV_BUT) == 0)) {


if (inv_buttonActive == false) {
      int inval;
      inv_buttonActive = true;
      inv_button_timer = millis();
      if (REVERSE_GATE==1){REVERSE_GATE=0;} else {REVERSE_GATE=1; inval=1;}
     
    u8x8.setCursor(8,4);
//   u8x8.inverse();
  u8x8.print("INV");
  u8x8.setCursor(12,4);
 // u8x8.noInverse();
  u8x8.print(REVERSE_GATE);
    }

    if ((millis() - inv_button_timer > inv_button_long_timer) && (inv_longPressActive == false)) {
      inv_longPressActive = true;

     writeIntIntoEEPROM(45, midichin);
    writeIntIntoEEPROM(47, ccout);
      u8x8.setCursor(0,7);
       u8x8.print("SAVED ");
    }

  } else {

    if (inv_buttonActive == true) {

      if (inv_longPressActive == true) {

        inv_longPressActive = false;
      } else {
      }
     inv_buttonActive = false;
    }
  }

   if ((digitalRead(CC_BUT) == 0)) {
  //CC_NUMBER change
  if (cc_buttonActive == false) {
     
      cc_buttonActive = true;
      cc_button_timer = millis();
      if (ccout<=126){ccout++;} else {ccout=1;}
    
      displayCCupdate(ccout);
     // CC_NUMBER=ccin;
    }

    if ((millis() - cc_button_timer > cc_button_long_timer) && (cc_longPressActive == false)) {
      cc_longPressActive = true;
      int back2=2;
       if (ccout<=126){ccout=ccout-back2;} else {ccout=1;}
        displayCCupdate(ccout);
    }

  } else {

    if (cc_buttonActive == true) {

      if (cc_longPressActive == true) {

        cc_longPressActive = false;
      } else {
      }
     cc_buttonActive = false;
    }
  }
                                           
} /// END OF main loop

void handleNoteOn(byte channel, byte note, byte velocity){
  pre();
  lastNote = note;
  //Hz/V; x 1000 because map truncates decimals
  if (HZV){
    outVoltmV = 125.0*exp(0.0578*(note+noteHZVshift));}//0.125*1000
  //V/oct; x 1000 because map truncates decimals
  else{
    outVoltmV = 1000*((note*VoctLinCoeff)+ VoctShift);}
  dacValue = constrain(map(outVoltmV, 0, 5000, 0, 4095), 0, 4095);
  dac.setVoltage(dacValue, false);
  if(REVERSE_GATE == 1) {digitalWrite(gatePin, LOW);}
  else {digitalWrite(gatePin, HIGH);}
  velocityOut = map(velocity, 0, 127, 0, 255);
  analogWrite(velocityPin, velocityOut);
  u8x8.inverse();
  u8x8.drawString(0,0,"NOTE");
  u8x8.noInverse();

   u8x8.setCursor(5,0);

  u8x8.print(note);
    u8x8.setCursor(8,2);
    u8x8.inverse();
  u8x8.print("VEL");
      u8x8.setCursor(12,2);
      u8x8.noInverse();
  u8x8.print(velocityOut);

   u8x8.setCursor(8,4);
  // u8x8.inverse();
  u8x8.print("INV");
  u8x8.setCursor(12,4);
//  u8x8.noInverse();
 u8x8.print(REVERSE_GATE);

 u8x8.setCursor(0,2);
  u8x8.inverse();
  u8x8.print("MIDI");
u8x8.noInverse();
  u8x8.setCursor(5,2); 
   u8x8.print("  ");
   u8x8.setCursor(5,2); 
  u8x8.print(midichin);
   displayCCupdate(ccout);
}

void handleNoteOff(byte channel, byte note, byte velocity){
  if(note == lastNote){
    //dac.setVoltage(0, false);
    if(REVERSE_GATE == 1) {digitalWrite(gatePin, HIGH);}
    else {digitalWrite(gatePin, LOW);}
    analogWrite(velocityPin, 0);
  
    u8x8.drawString(0,0,"NOTE");
  }
}

void handleControlChange(byte channel, byte number, byte value){
   if(number == ccout){
      CCOut = map(value, 0, 127, 0, 255);
      analogWrite(6, CCOut);
      u8x8.setCursor(0,7);
    //  u8x8.inverse();
      u8x8.print("VAL");
    //  u8x8.noInverse();
          u8x8.setCursor(4,7);
      u8x8.print("   ");
      u8x8.setCursor(4,7);
      u8x8.print(value);
  }

   //Add another cc controller add new pins
      if(number > 0){
   CCOut = map(value, 0, 127, 0, 255);
 
  u8x8.setCursor(0,4);
  //u8x8.inverse();
  u8x8.print("C");u8x8.print(number);
    u8x8.setCursor(4,4);
  u8x8.print("   ");
      u8x8.setCursor(4,4);
  u8x8.print(value);
  current_cc=number;
  }
    /*
  //Add another cc controller add new pins
      if(number == 17){
   CCOut = map(value, 0, 127, 0, 255);
   analogWrite(10, CCOut);
  u8x8.setCursor(0,6);
  u8x8.inverse();
  u8x8.print("C17");
  u8x8.noInverse();
    u8x8.setCursor(4,6);
  u8x8.print(CCOut);
  } */
} 

void pre(void)
{
     
  u8x8.clear();

 // u8x8.inverse();
 
}