Arduino R2R Digital Audio – Part 3

So this is where we start to get closer to some proper direct digital synthesis using the hardware developed in part 1 and moving on from the timer-driven code developed in part 2.

The essence of this project is to update the code so that rather than calculating the next value to output “on the fly” it will read it from a stored table of values already in memory.  This also means that by changing the values in memory we can change the waveform too.

These are the key Arduino tutorials for the main concepts used in this project:

If you are new to Arduino, see the Getting Started pages.

Parts list

  • Arduino Uno
  • R2R circuit from Part 1.
  • 2x 10k potentiometers as mentioned in Part 2.

The Circuit

The pitch control is connected to the Arduino A0 analog input and the waveform control is connected to A1.

Note that by default all output signals are generated with a positive bias around 2.5V, resulting in waveforms going from 0 to 5V.  The best solution is to add a 10uF aluminium bipolar (non-polarized – NP) electrolytic capacitor between the output and the jack as a DC blocking capacitor.  This will result in a waveform that swings +-2.5V at the output stage.

Warning: Like all these projects – be mindful of what you plug this into – don’t plug it into anything of value. An old or second hand amplifier is great!

The Code

There isn’t much code, but there are a lot of concepts so there are a lot of comments to try to aid understanding!  The basic principle is as follows.

  • We output samples from a 256-value wave table at a basic sample rate of 16,384 Hz.
  • We change the frequency by skipping values in the table and jumping ahead – this results in the wave being sent out more quickly, thus increasing the resultant audio frequency.  It can also “slow down” if required.
  • The increment used for the skipping is calculated from a frequency determined by the first potentiometer reading.
  • There are four different waves that can be stored in the wavetable – a sine wave, sawtooth, triangle and square wave.  The selection is controlled by the second potentiometer.
  • The sawtooth, triangle and square waves are all calculated in the code and written out to the wavetable when required.
  • The sine wave is pre-calculated and stored in a 128 set of values in the Arduino’s PROGMEM memory and these values are used to initialize the wave table when required.  The values are used “as is” for the first half of the wave and then reversed for the second half.

The first potentiometer uses the map() function to obtain a “true frequency” reading between the minimum and maximum supported frequencies – I chose 55Hz (A1) to 1760Hz (A6).

The second potentiometer is truncated down to reading just one of four values using a bitshift as follows:

pot2 = analogRead(A1) >> 8;

The four choices of wave are passed into the setWavetable() function to choose one of the following:

  • 0 = sine wave
  • 1 = sawtooth wave
  • 2 = triangle wave
  • 3 = square wave

Once again, all the playback of the samples is performed using the timer interrupt as described in part 2 but this time, instead of adding delays to the outputting of the samples to make them slower, we skip ahead to speed up the reading of the samples from the wavetable to make them quicker.

Here is a quick summary of the way the calculations are performed for the dds part of the code.

  • There is a running index (the “accumulator”) into the wavetable which is incremented every “tick” of the code.
  • The size of the increment is calculated as follows:
    • The frequency that we have to output samples is given by the required audio frequency times the wave table size as we have to complete a full sweep of the wave table to get one cycle of the required audio frequency.
    • The time between outputting individual samples (the period) is 1 / sample output frequency.
    • The amount we have to increment our index into the wave table is therefore the time between every “tick” / required time to output our samples.
  • Simplifying the maths (as described in the code) this yields the following formula for the increment:
    • increment = required audio frequency * wave table size / sample rate

This means that the basic logic for writing out a sample is as follows:

Use the current accumulator as an index into the wavetable.
Output the sample from that point in the wavetable.
Calculate the increment according to the above formula.
Add the increment to the accumulator.
Wait for the next TICK.

The formula will almost certainly end up with increments as decimal values so there are a few tricks in the code to attempt to retain accuracy without having to deal with fractions.

The accumulator value is defined as an unsigned 16-bit value, so 0 to 65535. However, I treat it as a “Fixed Point Maths” value, in that I treat the top 8-bits as the integer part of the value and the bottom 8-bits as the decimal part – this is thus an 8.8 value.  This means that whenever it is used it is 256 times larger than the index I’ll need to use when accessing the wave table.  This means that the final “dividing down” isn’t done until the last minute preventing the accumulation of rounding errors.

Another trick relates to the calculation for the increment to the accumulator.  Most of the values in that calculation are actually constant, so rather then calculate them in the code, I “pre-calculate” them by hand. I can benefit even more if I use a sample rate that plays nicely with the fact that I have a 256 wave table and that I am calculating the increment using 8.8 fixed point maths, making it 256 times bigger. I thus use a sample rate of 16,384Hz:

  • Increment = frequency * wave table size / sample rate
  • Increment = frequency * 256 / 16384
  • FP Maths Increment = 256 * frequency * 256 / 16384
  • FP Maths Increment = frequency * 65536 / 16384
  • FP Maths Increment = frequency * 4

Find it on GitHub here.

Closing Thoughts

Now that the basics of wavetable synthesis have been implemented it would be fun to start experimenting with alternative waveforms.

Kevin

Leave a comment