dansfing.uk
Updated November 2021
Back
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();
}