MIDI to CV code
Arduino Code:
/******************************************************************************
MIDI-CV.ino
MIDI to Control Voltage converter for synthesizers.
Byron Jacquot, SparkFun Electronics
October 2, 2015
https://github.com/sparkfun/MIDI_Shield/tree/V_1.5/Firmware/MIDI-CV
This functions as a MIDI-to-control voltage interface for analog synthesizers.
It was developed for the Moog Werkstatt WS-01, but should work with any volt-per-octave
synthesizer.
Resources:
This code is dependent on the FortySevenEffects MIDI library for Arduino.
https://github.com/FortySevenEffects/arduino_midi_library
This was done using version 4.2, hash fb693e724508cb8a473fa0bf1915101134206c34
This library is now under the MIT license, as well.
You'll need to install that library into the Arduino IDE before compiling.
It is also dependent on the notemap class, stored in the same repository (notemap.cpp/h).
Development environment specifics:
It was developed for the Arduino Uno compatible SparkFun RedBoard, with a SparkFun
MIDI Shield and a pair of Microchip MCP4725 DACs to generate the control voltages.
Written, compiled and loaded with Arduino 1.6.5
This code is released under the [MIT License](http://opensource.org/licenses/MIT).
Please review the LICENSE.md file included with this example. If you have any questions
or concerns with licensing, please contact techsupport@sparkfun.com.
Distributed as-is; no warranty is given.
******************************************************************************/
#include <SoftwareSerial.h>
#include <MsTimer2.h>
#include <MIDI.h>
#include <Wire.h>
#include "notemap.h"
// VERBOSE mode switches MIDI to use soft serial on pins 8 & 9,
// and enables a bunch of debug print messages.
#define VERBOSE 0
// Instantiate the MIDI interface using the macro
// - HardwareSerial is the type of serial port to be used underneath
// the MIDI routines.
// - Serial1 is the name of that portto be used. On a pro-micro, "Serial"
// is the USB serial port, and "Serial1" is the hardware UART.
// - "MIDI" parameter is the resulting object name.
#if VERBOSE
SoftwareSerial SoftSerial(8, 9);
MIDI_CREATE_INSTANCE(SoftwareSerial, SoftSerial, MIDI);
#else
MIDI_CREATE_INSTANCE(HardwareSerial, Serial, MIDI);
#endif
// Functional assignments to Arduino pin numbers.
// Digital outputs
static const int GATEPIN = 8;
static const int REDLEDPIN = 7;
static const int GREENLEDPIN = 6;
// Analog input
static const int PIN_TEMPO_POT = 1;
// Digital inputs
static const int UPBTNPIN = 2;
static const int DNBTNPIN = 3;
static const int SHORTBTNPIN= 4;
static const int BTN_DEBOUNCE = 50;
// global variables
//
// notetracker knows which note ons & offs we have seen.
// We refer to it when it's time to generate CV and gate signals,
static notetracker themap;
// Variables for arpeggiator clock.
static uint32_t tempo_delay = 10;
static bool send_tick = false;
// The last bend records the most recently seen bend message.
// We need to keep track so we can update note CV when we get new notes,
// or new bend messages - we need the other half in order to put them together.
// bend is signed, 14-bit
static int16_t last_bend = 0;
// constants to describe the MIDI input.
// NUM_KEYS is the number of keys we're interpreting
// BASE_KEY is the offset of the lowest key number
static const int8_t NUM_KEYS = 49;
static const int8_t BASE_KEY = 36;
// The tuning constant - representing the DAC conts per semitone
//
// Arrived at using: (((2^<DAC resolution>)/5)/12) * 100
//
// 2^12 = 4096 total DAC counts.
// 4096/5 = 819.2 DAC counts per volt on a 5V supply
// 819.2/12 = dac counts per semitone = 68.26
// times 100 for some extra calculation precision = 6826
static const uint32_t DAC_CAL = 6826;
static const int notes[] = { 60, 62, 64, 65, 67, 69, 71, 72, 74, 76, 77, 79, 81, 83, 84 };
int random_notes[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
bool is_random = false;
int random_count = 0;
static const int notes_length = sizeof(notes);
static const int random_notes_length = sizeof(random_notes);
/*
* void updateCV(uint8_t key)
*
*Converts key number to DAC count value,
*and sends the value tio the DAC
*/
void updateCV(uint8_t key)
{
#if 0
Serial.print("KEY: ");
Serial.print(key);
#endif
uint32_t val = 400ul + ((key * DAC_CAL)/100ul);
#if 0
val = last_key * 6826ul;
Serial.print(" VALa: ");
Serial.print(val, HEX);
val /= 100;
Serial.print(" VALb: ");
Serial.print(val, HEX);
val += 400 ;
Serial.print(" VALc: ");
Serial.print(val, HEX);
#endif
val += last_bend;
// Serial.print(" VAL2: ");
// Serial.println(val, HEX);
Wire.beginTransmission(0x60);
Wire.write(byte((val & 0x0f00) >> 8));
Wire.write(byte(val & 0xff));
Wire.endTransmission();
}
/*
* void updateOutputs()
*
* Update otputs sets the outputs to the current conditions.
* Called from note on, note off, arp tick.
*/
void updateOutputs()
{
uint8_t key;
key = themap.whichKey();
#if VERBOSE
Serial.print("key: ");
Serial.println(key, HEX);
#endif
// key is in terms of MIDI note number.
// Constraining the key number to 4 octave range
// Soc we can do 4 Volts +/- ~0.5V bend range.
if (key < BASE_KEY)
{
key = 0;
}
else if ( key > BASE_KEY + NUM_KEYS)
{
key = NUM_KEYS;
}
else
{
key -= BASE_KEY;
}
updateCV(key);
digitalWrite(GATEPIN, themap.getGate());
}
/////////////////////////////////////////////////////////////////////////
// Callbacks for the MIDI parser
/////////////////////////////////////////////////////////////////////////
/* void handleNoteOn(byte channel, byte pitch, byte velocity)
*
* Called by MIDI parser when note on messages arrive.
*/
void handleNoteOn(byte channel, byte pitch, byte velocity)
{
// Do whatever you want when a note is pressed.
// Try to keep your callbacks short (no delays ect)
// otherwise it would slow down the loop() and have a bad impact
// on real-time performance.
Serial.print("on: ");
Serial.println(pitch , HEX);
themap.noteOn(pitch);
updateOutputs();
}
/* void handleNoteOff(byte channel, byte pitch, byte velocity)
*
* Called by MIDI parser when note off messages arrive.
*/
void handleNoteOff(byte channel, byte pitch, byte velocity)
{
// Do something when the note is released.
// Note that NoteOn messages with 0 velocity are interpreted as NoteOffs.
Serial.print("off: ");
Serial.println(pitch , HEX);
themap.noteOff(pitch);
updateOutputs();
}
/*void handlePitchBend(byte channel, int bend)
*
* Called by parser when bend messages arrive.
*/
void handlePitchBend(byte channel, int bend)
{
#if VERBOSE
Serial.print("bend: ");
Serial.println(bend , HEX);
#endif
// Bend data from the parser is 14 bits, signed, centered
// on 0.
// unsigned conversion & dual-7-bit thwacking
// already handled by midi parser
last_bend = bend >> 5;
#if VERBOSE
Serial.print("newbend: ");
Serial.println(last_bend, HEX);
#endif
updateOutputs();
}
/* void handleCC(byte channel, byte number, byte value)
*
* Called by parser when continuous controller message arrive
*/
void handleCC(byte channel, byte number, byte value)
{
#if VERBOSE
Serial.print("cc: ");
Serial.print(number);
Serial.print(" chan: ");
Serial.print(channel, HEX);
Serial.print("val: ");
Serial.println(value, HEX);
#endif
switch (number)
{
case 1:
{ // Mod wheel
Wire.beginTransmission(0x61);
// Turn 7 bits into 12
Wire.write(byte((value & 0x70) >> 3));
Wire.write(byte((value & 0x0f) << 4));
Wire.endTransmission();
};
break;
case 64:
{ // sustain pedal
themap.setSustain( (value != 0) );
}
// Other CC's would line up here...
default:
break;
}
}
/////////////////////////////////////////////////////////////////////////
// millisecond timer related
//
// See documentation for MStimer2 (http://playground.arduino.cc/Main/MsTimer2).
////////////////////////////////////////////////////////////////////////
void timer_callback()
{
// Tell the mainline loop that time has elapsed
send_tick = true;
}
/* void tick_func()
*
* Called by mainline loop when send_tick is true.
* Keeps track of rising/falling edges, and notifies notetracker
* of clock status.
*/
void tick_func()
{
static uint8_t counter = 0;
counter++;
if(counter & 0x01)
{
digitalWrite(REDLEDPIN, HIGH);
themap.tickArp(false);
updateOutputs();
}
else
{
digitalWrite(REDLEDPIN, LOW);
themap.tickArp(true);
updateOutputs();
}
}
/////////////////////////////////////////////////////////////////////////
// Panel interface control routines
/////////////////////////////////////////////////////////////////////////
/*void check_pots()
*
* Read the analog input (tempo control)
*/
void check_pots()
{
uint32_t pot_val;
uint32_t calc;
pot_val = analogRead(PIN_TEMPO_POT);
// Result is 10 bits
//calc = (((0x3ff - pot_val) * 75)/1023) + 8;
calc = (((0x3ff - pot_val) * 1800)/1023) + 25;
tempo_delay = calc ;
}
/* void up_btn_func()
*
* Called when button reader detects arpeggio up button has been pressed.
*
* If not in up mode, turn on up mode.
* If in up mode, stops arpeggiator.
*/
void up_btn_func()
{
#if VERBOSE
Serial.println("Up!");
#endif
if(themap.getMode() == notetracker::ARP_UP)
{
digitalWrite(GREENLEDPIN, HIGH);
themap.setModeNORMAL;
}
else
{
digitalWrite(GREENLEDPIN, LOW);
themap.setModeARP_UP;
}
}
/* void dn_btn_func()
*
* Called when button reader detects arpeggio up button has been pressed.
*
* If not in dn mode, turn on up mode.
* If in dn mode, stops arpeggiator.
*/
void dn_btn_func()
{
#if VERBOSE
Serial.println("Dn!");
#endif
if(themap.getMode() == notetracker::ARP_DN)
{
digitalWrite(GREENLEDPIN, HIGH);
themap.setModeNORMAL;
}
else
{
digitalWrite(GREENLEDPIN, LOW);
themap.setModeARP_DN;
}
}
/* void short_btn_func()
*
* Toggles staccato articulations
* Works independently of arpeggiator enablement.
*/
void short_btn_func()
{
#if VERBOSE
Serial.print("Short!");
#endif
if(themap.getShort())
{
themap.setShort(false);
}
else
{
themap.setShort(true);
}
}
/* void random_func()
*
* Generate a random sequence
*/
void random_func()
{
if(is_random)
{
digitalWrite(GREENLEDPIN, HIGH);
is_random = false;
}
else {
digitalWrite(GREENLEDPIN, LOW);
is_random = true;
}
}
/* Button behavior function array for ease of referencing using index
*
*/
void(*func_array[3])(void) =
{
up_btn_func,
dn_btn_func,
//short_btn_func
random_func
};
/* void check_buttons()
*
* Implements debounced button polling for the 3 buttons on then MIDI shield.
*
* Buttons are active low, pulled up.
* On a poll cycle, reads each button.
* If button is pressed (LOW), it counts up.
* If button is held for enough cycles, it calls the indirect routine for the button.
* When button is released, debounce counter clears.
*/
void check_buttons()
{
static uint8_t deb_array[3];
uint8_t val;
for(uint8_t i = 0; i < 3; i++)
{
// active low, pulled high
val = digitalRead(i + UPBTNPIN);
if(val == LOW)
{
if(deb_array[i] < BTN_DEBOUNCE+1)
{
deb_array[i]++;
if(deb_array[i] == BTN_DEBOUNCE)
{
(*func_array[i])();
}
}
}
else
{
deb_array[i] = 0;
}
}
}
/////////////////////////////////////////////////////////////////////////
// Arduino boilerplate - setup() & loop()
/////////////////////////////////////////////////////////////////////////
void setup()
{
// Output pins
pinMode(GATEPIN, OUTPUT);
pinMode(REDLEDPIN, OUTPUT);
pinMode(GREENLEDPIN, OUTPUT);
digitalWrite(REDLEDPIN, HIGH);
digitalWrite(GREENLEDPIN, HIGH);
// Button pins
pinMode(UPBTNPIN, INPUT_PULLUP);
pinMode(DNBTNPIN, INPUT_PULLUP);
pinMode(SHORTBTNPIN, INPUT_PULLUP);
randomSeed(analogRead(2));
for(uint8_t i = 0; i < random_notes_length; i++) {
random_notes[i] = random(0, notes_length - 1);
}
Serial.begin(115200); //This pipes to the serial monitor
Wire.begin();
// Initiate MIDI communications, listen to all channels
//MIDI.begin(MIDI_CHANNEL_OMNI);
MIDI.begin(1);
// .begin sets the thru mode to on, so we'll have to turn it off if we don't want echo
MIDI.turnThruOff();
// so it is called upon reception of a NoteOn.
MIDI.setHandleNoteOn(handleNoteOn); // Put only the name of the function
// Do the same for NoteOffs
MIDI.setHandleNoteOff(handleNoteOff);
//MIDI.setHandleControlChange(handleCC);
MIDI.setHandlePitchBend(handlePitchBend);
MsTimer2::set(tempo_delay, timer_callback);
MsTimer2::start();
Serial.println("setup complete");
}
void loop()
{
// Pump the MIDI parser as quickly as we can.
// This will invoke the callbacks when messages are parsed.
MIDI.read();
// check the tempo pot.
check_pots();
// check panel buttons
check_buttons();
if (is_random) {
int index = random_count % random_notes_length;
// Add a single new random note on each cycle
if (index == 0) {
int random_note_index = random(0, notes_length - 1);
int update_index = (random_count / random_notes_length) % random_notes_length;
random_notes[update_index] = random_note_index;
}
int note = random_notes[index];
updateCV(note);
digitalWrite(GATEPIN, HIGH);
digitalWrite(REDLEDPIN, LOW);
//delay(tempo_delay);
delay(tempo_delay * (note >= 70 ? 1 : 3));
digitalWrite(GATEPIN, LOW);
digitalWrite(REDLEDPIN, HIGH);
delay(50);
random_count = (random_count + 1) % (random_notes_length * random_notes_length);
return;
}
if(send_tick)
{
send_tick = false;
tick_func();
MsTimer2::stop();
MsTimer2::set(tempo_delay, timer_callback);
MsTimer2::start();
}
}