Sunday, June 03, 2018

Monophonic USB MIDI to CV Converter with 8 Octave Range

This is a USB-powered monophonic USB MIDI to CV converter with a roughly 8 octave range, from roughly -3.3V to 5V for pitch CV, 0V to 4.096V for velocity CV and a 3.3V gate for note on / note off events. The DAC is 12 bits, meaning that the range of 8.32V at a rate of 1V per octave is distributed across 4096 steps, yielding a pitch resolution of approximately 2.43 cents per data step.

The Teensy LC separates an incoming MIDI note message into values for pitch CV, velocity CV and a note gate. The pitch and velocity values are sent to an MCP4822 dual 12-bit DAC.

The first channel of the MCP4822 is amplified by two using the first op-amp of a TL072, and then offset by negative 3.3V by using the second op-amp of a TL072 resulting in a pitch CV range of approximately -3.3V to 5V. The velocity CV and note gate are fed directly to the Eurorack setup via a 1k resistors.

A Digilent Powerbrick uses 5V from the VIN on the Teensy LC and transforms this into  ±12V power supply, used to power the TL072.

This type of setup and can be refined and scaled to higher bit depth resolutions by using various DACs, different voltage ranges by changing the ratio between R2 and R1 (for scale) and feeding a different voltage into R8 (for offset).

#include <SPI.h>

int cs_pin = 0;
int gate_pin = 14;
int amp_gain = 0; // gain for amplifier - 0 = 2x, 1 = 1x
int previous_pitch;
float pitch_change_value;
float pitch_value;
float maximum_voltage = 8.32; // voltage output after op amps
float maximum_pitch_value = maximum_voltage * 12.0; 
float number_of_steps = 4096; // DAC resolution

void setup() {
  pinMode(cs_pin, OUTPUT);
  pinMode(gate_pin, OUTPUT);
  digitalWriteFast(cs_pin, HIGH);

  SPI.begin();
  usbMIDI.setHandleNoteOn(OnNoteOn);
  usbMIDI.setHandleNoteOff(OnNoteOff);
  usbMIDI.setHandlePitchChange(OnPitchChange);

}

void loop() {
  usbMIDI.read();
}

void OnNoteOn (byte channel, byte pitch, byte velocity) {
  if (channel == 1) {
    if (velocity > 0) {

      previous_pitch = pitch;
      pitch_value = map(previous_pitch + pitch_change_value, 0.0, maximum_pitch_value, 0.0, number_of_steps);
      writeDAC(cs_pin, 0, pitch_value);
      writeDAC(cs_pin, 1, velocity << 5);
      digitalWriteFast(gate_pin, HIGH);
    }

    else {
      digitalWriteFast(gate_pin, LOW);
    }
  }
}

void OnNoteOff (byte channel, byte pitch, byte velocity) {
  if (channel == 1) {
    digitalWriteFast(gate_pin, LOW);
  }
}

void OnPitchChange (byte channel, int pitch_change) {
  if (channel == 1) {
    pitch_change_value = map((float) pitch_change, 0.0, 16383.0, -12.0, 12.0);
    pitch_value = map(previous_pitch + pitch_change_value, 0.0, maximum_pitch_value, 0.0, number_of_steps);
    writeDAC(cs_pin, 0, pitch_value);
  }
}

void writeDAC (int cs, int dac, int val) {
  digitalWrite(cs, LOW);
  dac = dac & 1;
  val = val & 4095;
  SPI.transfer(dac << 7 | amp_gain << 5 | 1 << 4 | val >> 8);
  SPI.transfer(val & 255);
  digitalWrite(cs, HIGH);
}






2 comments:

Fox Burroughs said...

Love this post, Seb. I have done some modifications for my own purposes and combined it with ideas from DSP Synth's USB MIDI2CV project and stuffed it all into a 1U tile module.
The changes I chose to make was to use an ATtiny85 instead of a Teensy to help with size, cost and to remove unneeded parts. The other changes I made include a capacitor in parallel to R2 and R4 at about 680pF to function as a low pass filter for very high frequencies (above 23kHz). Another change I made was made R2 into 10k Ohms and R1 into a 15-turn 10k trimmer (turned to 5k) so very precise adjustments can be made to compensate for the 1% accuracy of R2. I also added a resistor in series to the non-inverting inputs of each op amp. The value of these resistors is the parallel value of R1//R2 and R3//R4 respectively. (So roughly 3k3 and 5k). These are to compensate for bias current voltage drops in the divider network. These two changes can stop the op amp from oscillating on its own in some cases. And lastly I have added a single npn transistor buffer to the gate output to grant it a touch more current if the receiving devices need it.
I separated DGND and ANGD too! ;)

Sebastian Tomczak said...

Great ideas all round, thanks for sharing! That is great news that the ATtiny 85 can act as a USB device, awesome! I will have to check that out. Good idea on the trim pot too.