// ********************************************************************************************************************************** // * PROJECT: mini MIDI to CV * // * AUTHOR: PHOBoS * // * DATE: Oct 14, 2019 * // * VERSION: 1.1 * // * * // * ============================================================================================================================== * // * DESCRIPTION: * // * ------------ * // * This is a very basic MIDI to CV converter originally intended to be added to a MIDI keyboard[*]. It has a 1V/Oct CV output * // * with a range of 5 octaves (in semitones) and it will ignore any MIDI notes that are out of this range. The MIDI octave range * // * it responds to can be adjusted in the code (low_octave) or with 3 switches and can be set between -1 (octaves -1 to 3) and * // * 4 (octaves 4 to 8). The value for low_octave can be set higher but then the total range will of course be less than 5 full * // * octaves. * // * * // * It uses last note priority but doesn't memorize any notes. This means that if you press a key and hold it, then press another * // * key the CV will correspond with the last pressed key. But if you release this key it will not return to the CV for the first * // * held down key. (the Gate output does however stay on). * // * * // * It also has a Gate output that stays high when at least one note is on and a Trigger output that sends out a short pulse * // * whenever a key is pressed (or a new MIDI NoteOn message is received). The length of this trigger pulse can be adjusted in * // * the code (trigger_time) but it is not 100% accurate. * // * * // * The first time the arduino is booted it uses preset values to control the DAC. These might work straight away but because * // * the DAC uses the supply voltage as a reference and isn't entirely linear it will probably need some calibration. * // * There are 2 calibration procedures; * // * - Quick calibration calculates all the values for the DAC based on 1 setting but is not very accurate. * // * - Note calibration makes it possible to precisely adjust and store the output voltage for each note individually at any * // * given time. (this could also be used for creating non-western tuning scales) * // * * // * * // * [*]requires some extra circuitry for an external MIDI connection. * // * * // * ============================================================================================================================== * // * CALIBRATION: * // * ------------ * // * 1. Quick calibration * // * - press the encoder button on power-up untill the LED starts blinking. (it will stay on when you release the button) * // * - use the rotary encoder to adjust the output voltage to 3.000V. (or connect a VCO and tune it to the desired frequency) * // * - press the encoder button again to calculate and store all the other DAC values. (the LED will turn off) * // * * // * 2. Single Note calibration * // * - press the key (or send a NoteOn message) for the note you want to calibrate. (you don't have to hold it) * // * - use the rotary encoder to adjust the output voltage to the desired value. (or frequency) * // * - press the encoder button to store the new DAC value. (the LED will blink 1x) * // * * // * The DAC values corresponding with the CV for each note are stored in EEPROM. On startup a list of these values is send out * // * over the serial connection, so if you want to know what they are you can view them using the Arduino IDE serial monitor. * // * * // * ============================================================================================================================== * // * OCTAVE RANGE SWITCHES: * // * ---------------------- * // * * // * swC swB swA | range * // * --------------+------- * // * 0 0 0 | -1..3 * // * 0 0 1 | 0..4 * // * 0 1 0 | 1..5 * // * 0 1 1 | 2..6 * // * 1 0 0 | 3..7 * // * 1 0 1 | 4..8 * // * 1 1 0 | default range * // * 1 1 1 | default range * // * * // * The switches are optional and when left out the default range (set with low_octave) is used. * // * The octave range is set at startup so the switches have to be in the correct positions before the arduino is powered up. * // * * // * ============================================================================================================================== * // * PARTS LIST: * // * ----------- * // * 1x Arduino nano V3 * // * 1x MCP4725 12-bit DAC (this code has only been tested with a cheapo red PCB version and the I2C address may have to be * // * changed if you use a different version) * // * 1x Incremental rotary encoder with push button (for calibration) * // * NOTE: pullup resistors and debounce are configured in software so the rotary encoder doesn't require any extra components * // * 2x 1K resistor to protect the Gate and Trigger outputs * // * 1x LED (+ resistor) * // * 3x (DIP)switch to set the octave range (optional) * // * * // * ============================================================================================================================== * // * ARDUINO NANO CONNECTIONS: * // * ------------------------- * // * * // * MIDI * // * ---- * // * input: pin 17 [Rx] ( !!! this input should NOT be connected when programming the Arduino !!! ) * // * * // * * // * Rotary encoder * // * -------------- * // * output A: pin 20 [D2] * // * output B: pin 21 [D3] * // * button: pin 22 [D4] * // * (the other pins for the encoder and button need to be connected to GND) * // * * // * * // * MCP4725P I2C DAC * // * ---------------- * // * SDA: I2C Data, pin 8 [A4/D18] * // * SCL: I2C Clock, pin 9 [A5/D19] * // * (VCC = +5V, you can use the +5V output on the arduino but if you power it through the USB connection it will be lower * // * than 5V which will affect the range) * // * * // * * // * Octave switches (optional) * // * -------------------------- * // * octave switch A: pin 4 [A0/D14] * // * octave switch B: pin 5 [A1/D15] * // * octave switch C: pin 6 [A2/D16] * // * (the other side of the switches need to be connected to GND) * // * * // * * // * Outputs * // * ------- * // * LED: pin 23 [D5] (or [D13] to use the onboard LED) * // * Trigger out: pin 24 [D6] (use a 1K series resistor) * // * Gate out: pin 25 [D7] (use a 1K series resistor) * // * * // * ============================================================================================================================== * // * LIBRARIES: * // * ---------- * // * MIDI: https://github.com/FortySevenEffects/arduino_midi_library/releases [V4.31] * // * MCP4725P: https://github.com/adafruit/Adafruit_MCP4725 * // * * // * ============================================================================================================================== * // * CHANGES IN V1.1: * // * ---------------- * // * - replaced digitalWrite for Trigger and Gate outputs with direct PORT manipulation (faster). * // * - moved the encoder debounce from the adjust_dac_value() function to the read_enc() function. * // * - added inputs for (DIP)switches to set the octave range. * // * * // ********************************************************************************************************************************** // ================================================================================================================================== // * use the EEPROM library * // ---------------------------------------------------------------------------------------------------------------------------------- #include // ================================================================================================================================== // * use the MCP4725 DAC library * // ---------------------------------------------------------------------------------------------------------------------------------- #include Adafruit_MCP4725 dac; // ================================================================================================================================== // * use the MIDI library * // ---------------------------------------------------------------------------------------------------------------------------------- #include MIDI_CREATE_DEFAULT_INSTANCE(); // ================================================================================================================================== // * custom configuration * // ---------------------------------------------------------------------------------------------------------------------------------- #define trigger_time 10 // length of the trigger output signal in milliseconds #define low_octave 2 // lowest MIDI octave corresponding with a 0~1V CV output // ================================================================================================================================== // * define arduino pins * // ---------------------------------------------------------------------------------------------------------------------------------- #define enc_A 2 // digital input pin used for rotary encoder output A #define enc_B 3 // digital input pin used for rotary encoder output B #define button 4 // digital input pin used for the rotary encoder button #define octave_sw_A 10 // digital input pin used for octave switch A #define octave_sw_B 11 // digital input pin used for octave switch B #define octave_sw_C 12 // digital input pin used for octave switch C #define LED 5 // digital output pin used for the LED (change this value to 13 to use the onboard LED) #define trigger_out 6 // digital output pin used for the Trigger output (PORTD.6) in the function trigger() #define gate_out 7 // digital output pin used for the Gate output (PORTD.7) in the function midi_to_cv() // ================================================================================================================================== // * define global variables * // ---------------------------------------------------------------------------------------------------------------------------------- unsigned int dac_data[61]; // array with values for DAC (all notes) unsigned int dac_value; // 12-bit value used by DAC byte low_note; // lowest accepted MIDI note value based on octave setting byte octave_sw; // total value of the octave switches (0..7) byte note = 0; // number of current/last note (this is not a MIDI note value) byte notes_on = 0; // amount of notes that are on (used to toggle the gate output) byte note_max = 60; // maximum number of notes (60 = 5 octaves, note 0..59) bool old_enc_A = 0; // stored encoder value bool new_enc_A = 0; // read encoder value unsigned long trigger_end = 0; // used for the trigger timer bool trigger_stat = 0; // trigger status (on/off) // ********************************************************************************************************************************** // _________________________________________________________________________________________________________________________________ // | | // | [ SETUP ] | // |_________________________________________________________________________________________________________________________________| void setup() { // ================================================================================================================================== // * configure the arduino pins * // ---------------------------------------------------------------------------------------------------------------------------------- // inputs pinMode(enc_A, INPUT_PULLUP); pinMode(enc_B, INPUT_PULLUP); pinMode(button, INPUT_PULLUP); pinMode(octave_sw_A, INPUT_PULLUP); pinMode(octave_sw_B, INPUT_PULLUP); pinMode(octave_sw_C, INPUT_PULLUP); // outputs pinMode(LED, OUTPUT); pinMode(gate_out, OUTPUT); pinMode(trigger_out, OUTPUT); // set outputs low digitalWrite(LED, LOW); digitalWrite(gate_out, LOW); digitalWrite(trigger_out, LOW); // ================================================================================================================================== // * set the I2C Address of the MCP4725 * // ---------------------------------------------------------------------------------------------------------------------------------- dac.begin(0x60); // 0x60 for cheapo red PCB version // ================================================================================================================================== // * initialize MIDI * // ---------------------------------------------------------------------------------------------------------------------------------- MIDI.begin(MIDI_CHANNEL_OMNI); // receive MIDI data from all 16 channels MIDI.turnThruOff(); // turn MIDI thru mode off MIDI.setHandleNoteOn(MyHandleNoteOn); // setHandleNoteOn (byte channel, byte note, byte velocity) MIDI.setHandleNoteOff(MyHandleNoteOff); // setHandleNoteOff (byte channel, byte note, byte velocity) MIDI.setHandleControlChange(MyHandleCC); // setHandleControlChange (byte channel, byte CC, byte value) MIDI.setHandlePitchBend(MyHandlePitchBend); // setHandlePitchBend (byte channel, int value) // ================================================================================================================================== // * BOOT sequence * // ---------------------------------------------------------------------------------------------------------------------------------- // read the value from address 200 to check if the preset data has been written to the EEPROM. // If this value is not 42 the preset values will be written to the EEPROM and the value for address 200 is set to 42. // This is done so that the preset values will only be written to the EEPROM the first time it boots after programming. // 42 is an arbitrary number and any number <256 can be used but must also be changed in the function write_preset_data_to_eeprom() if (EEPROM.read(200) != 42) {write_preset_data_to_eeprom();} // ---------------------------------------------------------------------------------------------------------------------------------- // copy data from the EEPROM to the dac_data[] array and print a list of all the values Serial.begin(9600); // set the baudrate to 9600kbps for the serial monitor Serial.println("values for the DAC stored in EEPROM:"); for (byte n=0; n<=60; n++) { read_dac_data_from_eeprom(n); // copy the EEPROM value corresponding with note n to the dac_data[] array Serial.println(dac_data[n]); // print value } Serial.flush(); // wait untill all the serial data has been send Serial.begin(31250); // set the baudrate back to 31250kbps for MIDI // ---------------------------------------------------------------------------------------------------------------------------------- // read octave switch settings and set low octave value read_octave_switches(); // ---------------------------------------------------------------------------------------------------------------------------------- // run Quick calibration if the encoder button is pressed on startup if (digitalRead(button) == 0) {quick_calib();} // ---------------------------------------------------------------------------------------------------------------------------------- // set the CV output to the voltage for note 12 (~1V) send_to_dac(dac_data[12]); } // ********************************************************************************************************************************** // _________________________________________________________________________________________________________________________________ // | | // | [ MAIN PROGRAM LOOP ] | // |_________________________________________________________________________________________________________________________________| // check the trigger timer and read the encoder while there is no MIDI data received. // run Note calibration if the encoder button is pressed void loop() { MIDI.read(); while (Serial.available() <= 0) { trigger(0); read_enc(); if (digitalRead(button) == 0) { note_calib(); } } } // ********************************************************************************************************************************** // ================================================================================================================================== // * convert MIDI notes to CV value + set Gate & Trigger outputs * // ---------------------------------------------------------------------------------------------------------------------------------- void midi_to_cv(byte midi_note, byte velocity, bool active) { // ignore MIDI note values that are out of range if ((midi_note >= low_note) && (midi_note < (note_max+low_note))) { // on NoteOn message: if (active == 1) { note = midi_note - low_note; // convert the midi_note value to a note value (note value is 0..60) dac_value = dac_data[note]; // lookup the corresponding DAC value for note send_to_dac(dac_value); // send this value to the DAC trigger(1); // turn on the trigger ouput and start the timer notes_on++; // increase the note counter if (notes_on == 1) {bitSet(PORTD, 7);} // turn the Gate on when 1 key is pressed // on NoteOff message: } else { notes_on--; // decrease the note counter if (notes_on == 0) {bitClear(PORTD, 7);} // turn the Gate off when no keys are pressed } } } // ================================================================================================================================== // * trigger timer * // ---------------------------------------------------------------------------------------------------------------------------------- // turn the trigger output on and calulate when it has to be turned off based on the value of millis(). void trigger(bool trigger_on) { if (trigger_on == 1) { // trigger_on = 1 when a new key is pressed bitSet(PORTD, 6); // turn the trigger output on trigger_end = millis() + trigger_time; // calculate the value that millis() has to reach untill the trigger output is turned off trigger_stat = 1; // set a flag to indicate that the trigger timer is activated } else { if (trigger_stat == 1); { if (millis() >= trigger_end) { // check if millis() has reached the trigger_end value bitClear(PORTD, 6); // turn the trigger output off trigger_stat = 0; // reset the flag to indicate that the trigger timer is deactivated } } } } // ================================================================================================================================== // * check if encoder input A has changed from high to low * // ---------------------------------------------------------------------------------------------------------------------------------- void read_enc() { static unsigned long old_time = 0; new_enc_A = digitalRead(enc_A); // read the value of encoder output A if (old_enc_A != new_enc_A) { // check if this value has changed old_enc_A = new_enc_A; // update the stored value unsigned long new_time = millis(); if (new_time - old_time > 10) { // debounce the encoder by ignoring changes happening within 10ms if (new_enc_A == 0) { adjust_dac_value(); // adjust the DAC value if encoder output A is low } } old_time = new_time; // update the debounce timer } } // ================================================================================================================================== // * use the encoder to adjust the DAC value * // ---------------------------------------------------------------------------------------------------------------------------------- void adjust_dac_value() { if (digitalRead(enc_B) == LOW) { // decrease the DAC value if the encoder is rotated CCW if (dac_value > 0) { // limit the minimum value to 0 dac_value--; } } else { // increase the DAC value if the encoder is rotated CW if (dac_value < 4095) { // limit the maximum value to 4095 dac_value++; } } send_to_dac(dac_value); // send the new value to the DAC } // ================================================================================================================================== // * send a 12-bit value to the DAC * // ---------------------------------------------------------------------------------------------------------------------------------- int send_to_dac(int dac_value) { dac_value = max(dac_value, 0); // limit the minimum DAC value to 0 dac_value = min(dac_value, 4095); // limit the maximum DAC value to 4095 dac.setVoltage(dac_value, false); // send the value to the DAC to change the CV } // ================================================================================================================================== // * read the values of the octave switches and adjust the low_note setting * // ---------------------------------------------------------------------------------------------------------------------------------- void read_octave_switches() { // calculate the the octave switch value byte octave_sw = (digitalRead(octave_sw_A)) + (digitalRead(octave_sw_B) << 1) + (digitalRead(octave_sw_C) << 2); // use the octave switch value if it is < 6 (octave_sw = 7 if no switches are connected) if (octave_sw < 6) { low_note = octave_sw*12; // otherwise use the default value } else { low_note = (1+low_octave)*12; } } // ================================================================================================================================== // * Quick calibration * // ---------------------------------------------------------------------------------------------------------------------------------- // Quick calibration sends out the CV for note 36 (~3V) // after the CV is adjusted to the correct voltage all the other DAC values are recalculated and stored. void quick_calib() { // blink the LED while the encoder button is pressed while (digitalRead(button) == 0) { digitalWrite(LED, !digitalRead(LED)); delay(150); } digitalWrite(LED, HIGH); // turn the LED on dac_value = dac_data[36]; // lookup the DAC value for note 36 (~3V) send_to_dac(dac_value); // send this value to the DAC to change the CV output // check if the encoder has been rotated and adjust the DAC value untill the encoder button is pressed while (digitalRead(button) == 1) { read_enc(); } float dac_read_value = dac_value; // use a temporary float variable for accuracy and float dac_note_value = dac_read_value / 36; // calculate the DAC value difference between 2 notes for (byte n=0; n<=60; n++) { dac_data[n] = round(n*dac_note_value); // calculate the new DAC value for every note (the values will be evenly spread across the range) store_dac_data_in_eeprom(n); // store the new DAC value in EEPROM if (dac_data[n]>4095) {note_max = n;} // adjust the total range (number of notes) when a value is out of range } digitalWrite(LED, LOW); // turn the LED off // wait untill the encoder button is released while (digitalRead(button) == 0) { delay(10); } } // ================================================================================================================================== // * Note calibration * // ---------------------------------------------------------------------------------------------------------------------------------- // for manually adjusting and storing the DAC value of a single note void note_calib() { dac_data[note] = dac_value; // update the DAC value for the last played note in the dac_data[] array store_dac_data_in_eeprom(note); // store this value in EEPROM // blink the LED 1x to indicate the value has been updated digitalWrite(LED, HIGH); delay(150); digitalWrite(LED, LOW); // wait untill the encoder button is released while (digitalRead(button) == 0) { delay(10); } } // _________________________________________________________________________________________________________________________________ // | | // | [ MIDI ] | // |_________________________________________________________________________________________________________________________________| // ================================================================================================================================== // * MIDI Note ON * // ---------------------------------------------------------------------------------------------------------------------------------- // function(s) to execute when a NoteOn message is received void MyHandleNoteOn(byte channel, byte midi_note, byte velocity) { midi_to_cv(midi_note, velocity, 1); } // ================================================================================================================================== // * MIDI Note Off * // ---------------------------------------------------------------------------------------------------------------------------------- // function(s) to execute when a NoteOff message is received void MyHandleNoteOff(byte channel, byte midi_note, byte velocity) { midi_to_cv(midi_note, velocity, 0); } // ================================================================================================================================== // * MIDI Pitch Bend (not used) * // ---------------------------------------------------------------------------------------------------------------------------------- // 14 bit value (0~16383) // 0 ~ 8191 is downward bend B000000 00000000 ~ B011111 11111111 // 8192 is no bend B100000 00000000 // 8193 ~ 16383 is upward bend B100000 00000001 ~ B111111 11111111 // // function(s) to execute when a Pitch Bend message is received void MyHandlePitchBend(byte channel, int value) { } // ================================================================================================================================== // * MIDI Control Change (not used) * // ---------------------------------------------------------------------------------------------------------------------------------- // function(s) to execute when a CC message is received void MyHandleCC(byte channel, byte number, byte value) { // CC21~CC28 switch (number) { // ---------------------------------------------------------------------------------------------------------------------------------- // CC21: case 21: break; // ---------------------------------------------------------------------------------------------------------------------------------- // CC22: case 22: break; // ---------------------------------------------------------------------------------------------------------------------------------- // CC23: case 23: break; // ---------------------------------------------------------------------------------------------------------------------------------- // CC24: case 24: break; // ---------------------------------------------------------------------------------------------------------------------------------- // CC25: case 25: break; // ---------------------------------------------------------------------------------------------------------------------------------- // CC26: case 26: break; // ---------------------------------------------------------------------------------------------------------------------------------- // CC27: case 27: break; // ---------------------------------------------------------------------------------------------------------------------------------- // CC28: case 28: break; default: break; } } // ********************************************************************************************************************************** // _________________________________________________________________________________________________________________________________ // | | // | [ EEPROM ] | // |_________________________________________________________________________________________________________________________________| // ================================================================================================================================== // * Read DAC data from EEPROM * // ---------------------------------------------------------------------------------------------------------------------------------- // read low and high byte values from EEPROM and combine them to create an integer value // copy this value to the dac_data[] array void read_dac_data_from_eeprom(byte n) { byte low = EEPROM.read((n*2)); byte high = EEPROM.read((n*2)+1); dac_data[n] = word(high,low); } // ================================================================================================================================== // * Store DAC data in EEPROM * // ---------------------------------------------------------------------------------------------------------------------------------- // split integer values from the dac_data[] array into a low and high byte // store these byte values in EEPROM // values note0: address 0 & 1 // values note1: address 2 & 3 // values note2: address 4 & 5 // etc void store_dac_data_in_eeprom(byte n) { EEPROM.update((n*2) , lowByte (dac_data[n])); EEPROM.update((n*2)+1, highByte(dac_data[n])); } // ================================================================================================================================== // * Store preset data in EEPROM * // ---------------------------------------------------------------------------------------------------------------------------------- void write_preset_data_to_eeprom() { // DAC note CV // ------------------------ const unsigned int dac_preset[] = { 0, // 0 0,0000 V 62, // 1 0,0833 V 140, // 2 0,1667 V 202, // 3 0,2500 V 276, // 4 0,3333 V 340, // 5 0,4167 V 411, // 6 0,5000 V 478, // 7 0,5833 V 546, // 8 0,6667 V 616, // 9 0,7500 V 682, // 10 0,8333 V 754, // 11 0,9167 V 816, // 12 1,0000 V 891, // 13 1,0833 V 952, // 14 1,1667 V 1027, // 15 1,2500 V 1086, // 16 1,3333 V 1162, // 17 1,4167 V 1222, // 18 1,5000 V 1297, // 19 1,5833 V 1361, // 20 1,6667 V 1434, // 21 1,7500 V 1501, // 22 1,8333 V 1569, // 23 1,9167 V 1639, // 24 2,0000 V 1705, // 25 2,0833 V 1777, // 26 2,1667 V 1840, // 27 2,2500 V 1916, // 28 2,3333 V 1977, // 29 2,4167 V 2053, // 30 2,5000 V 2112, // 31 2,5833 V 2189, // 32 2,6667 V 2252, // 33 2,7500 V 2325, // 34 2,8333 V 2390, // 35 2,9167 V 2460, // 36 3,0000 V 2527, // 37 3,0833 V 2594, // 38 3,1667 V 2665, // 39 3,2500 V 2730, // 40 3,3333 V 2803, // 41 3,4167 V 2865, // 42 3,5000 V 2941, // 43 3,5833 V 3001, // 44 3,6667 V 3076, // 45 3,7500 V 3137, // 46 3,8333 V 3213, // 47 3,9167 V 3275, // 48 4,0000 V 3349, // 49 4,0833 V 3415, // 50 4,1667 V 3485, // 51 4,2500 V 3554, // 52 4,3333 V 3621, // 53 4,4167 V 3693, // 54 4,5000 V 3758, // 55 4,5833 V 3834, // 56 4,6667 V 3895, // 57 4,7500 V 3972, // 58 4,8333 V 4031, // 59 4,9167 V 4095 // 60 5,0000 V }; // ---------------------------------------------------------------------------------------------------------------------------------- // split integer values from the dac_preset[] array into a low and high byte // store these byte values in EEPROM // values note0: address 0 & 1 // values note1: address 2 & 3 // values note2: address 4 & 5 // etc for (byte n = 0; n<=60; n++) { EEPROM.update((n*2) , lowByte (dac_preset[n])); EEPROM.update((n*2)+1, highByte(dac_preset[n])); } // ---------------------------------------------------------------------------------------------------------------------------------- // write a value to address 200 for confirming the preset data has been stored in EEPROM EEPROM.write(200,42); // ---------------------------------------------------------------------------------------------------------------------------------- // blink the LED 3 times to indicate the preset data has been stored for (byte i=0; i<6; i++) { digitalWrite(LED, !digitalRead(LED)); delay(250); } } // ********************************************************************************************************************************** // * HAVE A FRABJOUS DAY! * // **********************************************************************************************************************************