Friday, October 27, 2017

On Generating a Pitch Lookup Table for a DAC

A given DAC (digital to analog converter) will contain imperfections which can be compensated for by analog and digital means. These imperfections vary depending on the load, the relative data point position, the gain error, the quality of the power supply etc.

I am interested in powering a DAC - in this case used for pitch control of a VCO (voltage controlled oscillator) - just from a USB bus. The aim is to control a modular synthesiser. The power supply is a single 5V rail from USB. The output of the DAC can be measured and a linear transfer function implied, however this transfer function will fail at the extremes, as the data either reaches 0 or full code. A more "real world" conversion between data and output can be generated using a brute force lookup table, and comparing data input to pitch output. 

In this example, an MCP4822 DAC is used to control an STO module, and a pitch lookup table is generated. 

The MCP4822 DAC has two 12-bit channels, is powered from a 3.3v to 5V supply, and has an internal voltage reference of 2.048V with an output gain of either one or two. In theory, this means a linear transfer of 4096 values across a voltage range of exactly 0 to 2.048V or 4.096V depending on the gain setting. In practice, the DAC has imperfections as the output approaches 0V and 4.096V. 

The STO module is a VCO by Make Noise with a 1V per octave input, which expects a range of -5V to 5V for a ten octave range of pitch. 

The aim is to control the STO module using the MCP4822 DAC when powered from USB (without a bipolar supply or op-amp), which translates to just over 4 octaves of pitch range. 

The method is to measure each data value in the range 0 - 4095 of the MCP4822 DAC in terms of pitch output, and then generate a lookup table. The lookup table will take, as an input, a pitch in absolute cents. 6000 cents would equate to middle C, if the midway DAC data point is tuned to middle C - but of course the STO can be tuned arbitrarily. 

A Max patch is used to step through each data value, and then measure the output frequency, and then store the values as text. The data value (from 0 - 4095) is sent to a Teensy microcontroller as a 14-bit pitch bend value. 

The Teensy writes the value to the MCP4822, which then converts the value into a voltage between 0 and 4.096V. The output of the MCP4822 is connected to the STO module. The audio output of the STO module is connected to a sound card input, which is represented in the Max patch as the adc~ object. Download the Max patch here. The patch uses the fiddle~ object. 

The Arduino sketch is shown below, and will compile for a range of Teensy boards if USB MIDI mode is selected in the tools menu. The code uses the SPI library, included with Teensyduino

On receiving a pitchbend message via USB MIDI, the Teensy will write the pitchbend value to both DAC channels of the MCP4822. Data is transferred to the MCP4822 as two bytes. The first byte contains the DAC channel, the amplifier gain, enabling the DAC output, and bits 8 - 11 of the DAC value. The second byte contains bits 0 - 7 of the DAC value. Download the Arduino sketch here

The Teensy is connected to the MCP4822 via the SPI bus and one chip select pin. 

  • Teensy digital pin 0 is the chip select pin, and is connected to MCP4822 pin 2 (!CS). 
  • Teensy digital pin 13 is the clock pin, and is connected to MCP4822 pin 3 (SCK). 
  • Teensy digital pin 11 is the data output pin, and is connected to MCP4822 pin 4 (SDI). 
  • Teensy VIN (5V) is connected to MCP4822 pin 1 (VDD). 
  • Teensy ground is connected to MCP4822 pin 7 (VSS) and pin 5 (LDAC). 
  • MCP4822 is connected to the 1V per octave input of the STO module via a 100 ohm resistor

Once each DAC data value has been correlated to a pitch output of the STO module, an excel table can be created that then does the opposite - takes a pitch input in cents, and outputs the data value of the DAC required to generate that pitch. 

The spreadsheet has a total of five data columns: A, B, C and E, F. The first three is the data collected from measuring the pitch output based on the DAC data input (a DAC data value range of 0 - 4095). The second set of two columns is a lookup table that matches a given pitch input to the DAC data value output required to generate that pitch (a pitch value range of 3545 - 8468 cents). 

The aim is to have a list of continuous values in cents, whereby an input pitch value in cents will correspond to the DAC value used to generate that pitch (or the closest possible pitch)

The data of the DAC vs output pitch (i.e. first three columns) is almost linear, however there are enough discrepancies to warrant the use of a lookup table, as the memory size cost is not huge (depending on application, of course). 

Column A is the data value that was sent to the DAC. Column B is the resultant pitch as measured from the STO module. Note that the STO module was tuned so that a data value of 2048 is middle C (MIDI pitch 60 or 6000 cents in this table). Column C is the pitch from column B converted to absolute cents. Columns A to C have a total of 4096 rows, each row corresponding to a data point of the MCP4822. 

Column E is the entire possible pitch range available (via this setup of the MCP4822) from 3545 cents to 8468 cents. Column F is the closest matching data point from column A, whereby column C matches the input of column E. The formula for column F is: "=MATCH(E3,C:C)", where E3 is the current row of E relative to F. 

To use this table in an Arduino sketch, a constant array can be created which can be referenced against an input cents value. 

For example: 

First, to create the pitch array: 

const int pitch_data_values[] = { // all of the values from column F, separated by commas 

And then to read from the pitch array: 

writeDAC(cs_pin, 0, pitch_data_values[constrain(((pitch * 100) + pitchbend_value) - 3545  , 0, 4923)]);

Note that in this case, the variable pitch is a MIDI note from 0 - 127, and pitchbend_value is a variable from -1200 to 1200 cents. This function will result in a value being written to the MCP4822 between 0 - 4096, corresponding to the MIDI note and pitch bend value. See code below for full context. 

Download an Arduino sketch with a monophonic implementation of this lookup table here


Unknown said...

What is that case? Doesnt look like a Make Noise Skiff to me...

Very cool blog! Love it!

Sebastian Tomczak said...

Thanks! It's a Modulaire Maratime case - I'm really digging it.