Showing posts with label OMM. Show all posts
Showing posts with label OMM. Show all posts

Tuesday, June 16, 2015

Exponential curve using lookup table

It has been a while since my last update. My daughter arrived on Norway's national day, the 17th of May. It's so incredibly nice and I am the proudest dad ever! It does take the focus away from other things though :-)

Just before she arrived, I bought a book from 1980, musical applications of microprocessors. It may sound outdated but in fact is a treasure - it explains lots about sound theory, filters, DAC and ADC etc.

Today I am reading about generating waveforms digitally. Since the book is from 1980, the available computers were incredibly slow, worse than many of today's microcontrollers. This is a plus, since it means it explains a lot of neat tricks to make things run faster.

For example, it shows how to generate a sine wave using a small lookup table and doing linear interpolation between the points (p. 388). If certain criteria are met, the task only requires two subtractions, an addition and one multiplication.

In the OMM, I intend to do linear to exponential conversion using a lookup table. With 16bit values this would require 128kB for a full table. This can be reduced dramatically if I accept a little inaccuracy and use the same interpolation. As long as the result is not used for controlling the frequency of a vco, it should not be much of a problem

Saving space would also allow me to implement for example both 50dB and 70dB curves etc.

Sunday, February 1, 2015

Working OMM prototype

I've spent the days since getting back to Norway making an OMM prototype running on an 8 bit PIC18F458 @ 20MHz.

Things are looking very good indeed, except for speed of course. I've implemented:

- node structs
- function pointers and pointer lookup
- matrix array calculation
- DAC output to 14 bit SPI DAC (MAX544
- input buffer reading and outputbuffer writing

In addition I've coded the following node functions:
- Sum
- Invert
- Invert each side (positive stays positive etc)
- Ramp (but without proper interval calculation)
- Delay line (allows loops in network)
- Input
- Output

Next up should be
- Timers for constant output to DAC
- SPI input for controllers (but that is not possible without a second SPI module)
- JSON parsing
- Table lookup for tuning and exponentialization.

Here is the first output from the OMM - the frequency is too low to get a good image on the scope:



A few specifics on the SPI and the DAC:


The DAC accepts a maximum SPI speed of 10MHz. In the tests I did, I ran the SPI at F_osc/4, which means 5MHz when the oscillator frequency is 20MHz. I achieved what I think is a sample rate of about 89kHz but that is without any calculations between samples. In practice then, it seems the maximum DAC rate is slightly less than 180kHz when running the SPI clock at 10MHz (from the PIC that is, it is entirely possible that you can achieve higher speeds otherwise).

Here is  the code for the DAC tryout:

void writeToDac(){
unsigned int dacout;
/**** DAC ****/
SPI1_Init();
TRISC = 0; //trisc as output
LATC.B0 = 1;
dacout=0;
// The following code runs at about 89kHz.
while(1){
LATC.B0 = 0; //must write directly to latch (didn't work with PORTC.B0!)
SPI1_write(hi(dacout));
SPI1_write(lo(dacout) & 0b11111100);
LATC.B0 = 1; //latches values in DAC.
dacout += 2048;
}
}

The first time I got the DAC working it looked like this:


The visible steps are because I chose to use only 32 steps to speed things up and to see what is happening. After removing all delays and maximizing the speed it looks like this:


Here is the SPI clock btw:


Tuesday, January 20, 2015

On voltages and other practicalities

While dreaming up the OMM I have for some reason constantly kept thinking about the output from the DACs as 5V, with a OMM value of 0 corresponding to 0V and 65536 to 5V while at the same time intending to use a 1V/octave VCOs and having the pitch CV controlled from the OMM.

This, of course, leads to a severe limitation: the synth would not be able to span more than 5 octaves, and no tuning or modulation would be possible for the highest and lowest notes.

There are a few different solutions to this problem:

1) one could use a separate output as octave selector. By keeping this at 2.5V normally, we could add or subtract 2 octaves and still have 0.5V left for tuning and modulation. This will however lead to having to do some tricks when receiving notes outsidere the normal 5 octaves.

2) one can use 10V instead of 5V for the DACs. This gives us the same 10 octaves as adding an octave selector CV. However, this requires a redesign of some modules such as the VCA.

3) one can change the input resistor on the VCOs so their response is 1/2V per octave, again giving us the same 10 octaves.

I am not sure which one to choose. I will have a closer look at how other midi-2-CV converters do this.

I also need to figure out how many octaves to support. If it should be possible to do one-octave pitch bends 10 volts only allows 8 full octaves, and even then other modulations and tuning must be kept separate.

LFOs and modulation


Our numerical LFOs will have an amplitude from 0 to 64k. But when using the LFO to modulate the pitch CV, to get a vibrato effect, we want it to increase the pitch for half the cycle and lower it for the other half. In essence, we would be better off using a signed int. In other cases, and for other operators, we are only interested in the positive parts of the waveform. And in any case the DACs would normally only output a single-polarity signal (though this can be fixed).

Again, I need to check how others are doing this.

Other things to think about when it comes to LFOs is that it should be possible to adjust their starting point and direction (up/down), as well as to choose whether or not to reset the cycle at the start of a note and to sync it to an external clock.

Multiplication vs attenuation/scaling - virtual VCAs


I've realized that we need an additional function, one that treats the inputs like they are between 0 and 1 when multiplying them. This is analogous to how a VCA works, when the CV is at max, the output is the same as the input. When the CV is half of its Max, the output is attentatet by 50% etc. In other words, the output is scaled depending on the CV, between 0 and 1 times the input.

Monday, January 19, 2015

Ramp generation

If the running time of the matrix calculations is constant and triggered by a timer, generating a ramp is simply the task of adding the same number to an accumulator variable every cycle.

But since the number added is never exactly the number necessary for a correct ramp progression (we are dealing with integers after all), we will accumulate an error. This may be ok, but we should keep control of how big the error may become and take actions to prevent it from becoming too big.

If we work with 16 bit numbers, we will get an error of maximum 1/65536 per cycle. But assuming we are running at 44.1kHz, this will accumulate to an error of 44100/65535 = 67% in one second! Not very accurate I'd say.

Lets do the cslculation the opposite way. If we decide that yhe maximum acceptable error is 1% per second, what resolution do we need for our interval?

Well, in one second, we should be at most 0.01 * 65536 = 655 away from the correct answer.  At 44.1kHz, this means that the maximum error per cycle must be less than 655/44100 = 0.015, or 1/67th of the error we get from using an integer.

To remedy this, we may store the accumulator and the value to add as a number with more bits (in doing this, we will multiply the interval, or rather, calculate it differently) and divide the accumulator down to 16 bits when the output is needed.

To get the necessary precision we need to be able to store 1/67ths. By adding 6 bits to the number we can store 1/64 fractions, which is not quite 1/64 but may be good enough, or we can add an additional two bits and get a 1/256 precision, making the accumulator and interval variables 24bit (it may be practical to make them 32bit while at it, not sure).

Converting the accumulator back to 16bit is simply a matter of shifting it 8bits to the right.

Looping or multi stage ramps (envelopes)


When the ramp reaches the desired height, we want to stop adding to it, reset it or even start subtracting from it. This means that every time we have added something to the accumulator, we should check its value and possibly perform an action. We may even have to keep track of state, as what stage of an envelope we are in. The state, as well as the accumulator and value to add should be stored within the ramp node's struct if we want a truly node-type agnostic algorithm that lets us have any number of any type of node (unlike the Matrix 12 which has a fixed number of ramps, LFOs etc).

Sunday, January 18, 2015

The OMMNoM

I mentioned in the previous post that I want the synth to be like the Oberheim Matrix line with its software modulation matrix. Here is my take on it: The OMM (open modulation matrix) and its counterpart, the OMMNoM (OMM Node Manager).

The OMM is a piece of firmware running inside the mcu of each voice. It takes a set of inputs, constants, ramps/envelopes and function generators, and through a series of transformations calculates the CVs that control the voice. It is open in the sense that it is possible and easy to add input sources and transformations to the code and it has an open protocol so anything able to generate a simple string stream may be used to create the OMM model.

The OMMNoM is a program used to create the matrix. I envision this as a graphical tool running on a tablet. Through dragging and dropping nodes representing sources and transformations onto a canvas, and then connecting their inputs and outputs to each other and to cv outputs, one creates modulation matrices as advanced as the ones in the Oberheim Matrix line through a much more intuitive user interface.

In the matrix, any output may be connected to any input. Some nodes, such as summing and multiplication accept multiple inputs, while others, for example linear-to-exponential converters and cv outputs only have a single input. All nodes have one output, except the cv outputs which have none (or rather, they have a physical output that connects to the analog part of the voice). An output may of course be connected to any number of inputs.

So far I've thought of the following nodes:

External inputs
- midi controller messages
- keyboard pitch
- keyboard trigger/gate
- pitch bend wheel
- modulation wheel
- physical input switches
- physical input potmeters
- virtual input switches
- virtual input potmeters
- noise input connected to physical noise module of the analog card (use with sample and hold for example).

Controllable functions/function generators
- ramp generators (inc looping?)
- multi stage envelopes (inc looping?)
- LFOs
- Sample and Hold
- Trigger (sends trigger pulse if input is high)
- Comparator - sets output high when one input is higher than the other
- switch - turns on or off a signal when input is high or low)

Mathematical expressions
- sum
- multiply
- divide
- maximum
- minimum
- average
- exponentiation (possibly with dB control)

Logical operators, all with adjustable thresholds (with defaults)
- and
- or
- not
- xor

Outputs
- CV
- trigger pulse (for analog envelope)
- gate (for analog envelope)
- other binary pins (controlling switches etc)

Other
- numerical constants - these may be entered directly on a node input instead of being a separate node.

The outputs will most likely be named after what they connect to on the analog voice card, but this is not a necessity for the OMM model which just maps the output to a pin or DAC address.

Usage example:

To create an echo with diminishing volume,
an LFO may be connected to the trigger and gate input of an envelope, which in turn is multiplied with a down-sloping ramp. The output from the multiplier should be run through an exponentiation and then connected to the output CV going to the voice's output VCA.