Go Down

Topic: Grain table synthesis well explained (Read 1 time) previous topic - next topic

harish017

Setting the registers

Arduino language makes it really easy to work with PWM signals. However, it does not let change the frequency. In order to generate Audio signals we need to move to higher frequencies than the Arduino language allows and use directly the PWM regsiters of the ATmega.

The ATmega 328p has three timers (Timer 0, Timer 1 and Timer 2) that allow to control 6 PWM outputs. By directly changing the registers into the microcontroller we can have a bigger control on the frequency of the output signal. The Arduino system has a 16MHz clock and each timer generates the timer clock by dividing teh system clock by a prescale factor. Some timers have some properties than the others have not and adapt to more uses.

Timers can operate four different modes: Normal mode, Clear Time on Compare Match mode, Fast PWM and Phase Correct PWM mode. This last mode is used by Auduino. The function AudioOn() sets the registers in order properly configure the mode:

void audioOn() {
#if defined(__AVR_ATmega8__)
// ATmega8 has different registers
TCCR2 = _BV(WGM20) | _BV(COM21) | _BV(CS20);
TIMSK = _BV(TOIE2);
#elif defined(__AVR_ATmega1280__)
TCCR3A = _BV(COM3C1) | _BV(WGM30);
TCCR3B = _BV(CS30);
TIMSK3 = _BV(TOIE3);
#else
// Set up PWM to 31.25kHz, phase accurate
TCCR2A = _BV(COM2B1) | _BV(WGM20);
TCCR2B = _BV(CS20);
TIMSK2 = _BV(TOIE2);
#endif
}

The three statements do the same but for different versions of the ATmega. I will only focus on the third as it is the corresponding for the 328p, the ATmega version that Arduino UNO mounts.

First line sets up the TCCR2A register with high values for the bits (_BV means bit vector) COM2B1 and WGM20. It can be checked at Chapter 17 of the ATmega what it exactly does (but I needed the references below in order to more or less understand it).
The Waveform Generation Mode bits (WGM22/21/20), now at 001, determine the PWM Phase Correct Mode with a top value of 255. The Compare Match Output B Mode bits (COM2B1/0), nowt at 10, clear OC2B on Compare Match when up-counting and set OC2B on Compare Match when down-counting. The Compare Match Output A Mode bits (COM2A1/0) are both at zero what disables this output.

TCCR2B is a second Timer/Cunter Control Register. By only setting its CS20 bit to a high value no prescaler is selected. Note that the prescaler function is already carried by the previous register that is in practice dividing the 16MHz clock frequency by a factor of 512. More information about this register can be found at datasheet's chapter 17.

TIMSK2 (Timer/Counter 2 Interrupt Mask Register) only enables TOIE2 (Timer/Counter2 Overflow Interrupt Enable). Basically what it does is to create an interrupt every time the counter overflows.

OC2B is the output connected to pin 5 of the microprocessor, which corresponds to Arduino's digital pin 3. OCR2B (Output Compare Register), called PWM_VALUE in Auduino, takes the value of the synthesizer's output. What it basically does is counting at a 16MHz speed increment a counter from 0 to 255, and when it reaches 255 count down again back to 0. When OCR2B is higher than the counter, OC2B is high and viceversa.

As a result the PWM runs at 31,25 kHz, much faster than it would do with Arduino functions.

Reading values and choosing pitch

void loop() {
// Stepped pentatonic mapping: D, E, G, A, B
syncPhaseInc = mapPentatonic(analogRead(SYNC_CONTROL));

grainPhaseInc  = mapPhaseInc(analogRead(GRAIN_FREQ_CONTROL)) / 2;
grainDecay     = analogRead(GRAIN_DECAY_CONTROL) / 8;
grain2PhaseInc = mapPhaseInc(analogRead(GRAIN2_FREQ_CONTROL)) / 2;
grain2Decay    = analogRead(GRAIN2_DECAY_CONTROL) / 4;
}

Auduino uses 5 potentiometers to control five different parameters. The void loop() function that continuously executes controls the values of those potentiometers and gives them a useful value.

syncPhaseInc stores the value of the first potentiomenter and is used to control the pitch. mapPentatonic is function that converts this value to another value that will correspond to an analog frequency. It uses the pentatonic scale, meaning that looks in a table where there only are frequencies corresponding to the pentatonic scale. Any other scale could be used by adding the value corresponding to each note in its scale. Auduino doesn't have a complete set of scales but it also has a smooth frequency mapping table.

The value that corresponds to each analog frequency can be calculated with the following equation:

digital frequency / analog frequency = 2^16 / value

where:
digital frequency is 31,25 kHz.
analog frequency is the value of the frequency corresponding to the note we want to get.
2^16 is the number of possible values a 16 bit variable can get. Later will be explained why is it there and how it works.
'value' is the number we have to tell to the Auduino so we get the desired note.

As an example, let's say that we want a 440 Hz A. Substituting all parameteres in the formula we would get, after rounding, a value of 923.

grainPhaseInc and grain2PhaseInc store values used to generate triangular waves. And grainDecay and grain2Decay are used to control its weight/amplification.

The granular synthesis

Here I explain each part of the code. Comments within the code are from Peter Knight, author of the Auduino.

SIGNAL(PWM_INTERRUPT)

{

 uint8_t value;

 uint16_t output;

First line will make an interruption happen every time PWM_INTERRUPT (OCR2B) overflows, that is at a 31,25 kHz frequency. The following variables are used to keep temporal values.

 syncPhaseAcc += syncPhaseInc;

 if (syncPhaseAcc < syncPhaseInc) {

   // Time to start the next grain

   grainPhaseAcc = 0;

   grainAmp = 0x7fff;

   grain2PhaseAcc = 0;

   grain2Amp = 0x7fff;

   LED_PORT ^= 1 << LED_BIT; // Faster than using digitalWrite

 }

The syncPhaseInc value is accumulated, most of the times the condition is not met, only when syncPhaseAcc overflows. When this happens all other values are reseted. syncPhaseAcc will be overflowed at a speed that depends on the syncPhaseInc.

In other words, every time syncPhaseAcc overflows we'll start a new grain (a certain small waveform). The syncPhaseInc value controls the speed at which syncPhaseAcc overflows, so it controls the length of the grain. It'd be the same saying that it controls the number of grains per second and consequently the pitch.

Retaking the 440 A example, we've got syncPhaseInc = 923. As 2^16 / 923 = 71, we know that the counter will overflow 71 times before the new grain starts so it means we'll have 71 samples per grain when making a 440 A.

 // Increment the phase of the grain oscillators

 grainPhaseAcc += grainPhaseInc;

 grain2PhaseAcc += grain2PhaseInc;

 // Convert phase into a triangle wave

 value = (grainPhaseAcc >> 7) & 0xff;

 if (grainPhaseAcc & 0x8000) value = ~value;

 // Multiply by current grain amplitude to get sample

 output = value * (grainAmp >> 8);

 // Repeat for second grain

 value = (grain2PhaseAcc >> 7) & 0xff;

 if (grain2PhaseAcc & 0x8000) value = ~value;

 output += value * (grain2Amp >> 8);

The grain is made by the addition of two different triangular waves so in we've got a grain that is generated in a simple way but will have a quite rich and controllable shape regarding the very simple processing.

The grainPhaseInc controls the duration of the triangular wave. The higher the value the faster it'll be. How the triangular wave is generated might be a little difficult to see because it's at bit level. Basically value can only store 8 bits while the control parameters are 16, that's why MSB are moved right and multiplied by 0xff. The if condition divides the values in two if they are in the lower half they are kept, if they are in the higher half the condition is accomplished, the value is negated. As a result and after a few times we'll have a triangular shape. Last line accumulates the result in the output by adding the two different triangular shapes and amplifies the corresponding triangular wave.

 // Make the grain amplitudes decay by a factor every sample (exponential decay)

 grainAmp -= (grainAmp >> 8) * grainDecay;

 grain2Amp -= (grain2Amp >> 8) * grain2Decay;

It's pretty well self-explained, that will give to the grain a richer shape.

 // Scale output to the available range, clipping if necessary

 output >>= 9;

 if (output > 255) output = 255;

 // Output to PWM (this is faster than using analogWrite)

 PWM_VALUE = output;

}

OCR2B, PWM_VALUE here, is only 8 bit so only the 8 MSB are asigned. So in the end we get a PWM signal where the grain info is carried in the duty cycle and can be directly plugged to the loudspeakers.

AWOL

. . . and that's why we use code tags when posting code ;)

harish017


AWOL


Andy2No

This seems interesting, so I've decided to fix the readability of the code, where it was infested with smilies - by adding the code tags.

Retaking the 440 A example, we've got syncPhaseInc = 923. As 2^16 / 923 = 71, we know that the counter will overflow 71 times before the new grain starts so it means we'll have 71 samples per grain when making a 440 A.

 
Code: [Select]
// Increment the phase of the grain oscillators

 grainPhaseAcc += grainPhaseInc;

 grain2PhaseAcc += grain2PhaseInc;

 // Convert phase into a triangle wave

 value = (grainPhaseAcc >> 7) & 0xff;

 if (grainPhaseAcc & 0x8000) value = ~value;

 // Multiply by current grain amplitude to get sample

 output = value * (grainAmp >> 8);

 // Repeat for second grain

 value = (grain2PhaseAcc >> 7) & 0xff;

 if (grain2PhaseAcc & 0x8000) value = ~value;

 output += value * (grain2Amp >> 8);

The grain is made by the addition of two different triangular waves so in we've got a grain that is generated in a simple way but will have a quite rich and controllable shape regarding the very simple processing.

The grainPhaseInc controls the duration of the triangular wave. The higher the value the faster it'll be. How the triangular wave is generated might be a little difficult to see because it's at bit level. Basically value can only store 8 bits while the control parameters are 16, that's why MSB are moved right and multiplied by 0xff. The if condition divides the values in two if they are in the lower half they are kept, if they are in the higher half the condition is accomplished, the value is negated. As a result and after a few times we'll have a triangular shape. Last line accumulates the result in the output by adding the two different triangular shapes and amplifies the corresponding triangular wave.

 // Make the grain amplitudes decay by a factor every sample (exponential decay)

 grainAmp -= (grainAmp >> 8) * grainDecay;

 grain2Amp -= (grain2Amp >> 8) * grain2Decay;

It's pretty well self-explained, that will give to the grain a richer shape.

 // Scale output to the available range, clipping if necessary

 output >>= 9;

 if (output > 255) output = 255;

 // Output to PWM (this is faster than using analogWrite)

 PWM_VALUE = output;

}

OCR2B, PWM_VALUE here, is only 8 bit so only the 8 MSB are asigned. So in the end we get a PWM signal where the grain info is carried in the duty cycle and can be directly plugged to the loudspeakers.

Go Up