Hi! Hope you're enjoying this blog. I have a new home at www.goldsborough.me. Be sure to also check by there for new posts <3

Friday, April 4, 2014

Developing a digital synthesizer in C++: Part 3 - Complex waveforms

In the first part of this series, "Developing a digital synthesizer in C++", I showed you how you could create a simple sine wave in code and store it in a sample buffer, after which I also outlined how you can then store this data buffer into a WAV file and actually listen to the data. However, after a while, this sine wave will get a little boring to listen to. The fact is, that there are an infinite number of other sound waves we could have created, a few, as well as our friend, the sine wave, are shown in this picture:


Source:http://cdn.ttgtmedia.com/WhatIs/images/waveform.gif

However, these are not as straightforward to make as calling the sine function on a phase value. Add to that the fact that complex waveforms can be created using various different methods, each yielding more or less the same result. I will explain two frequently used methods that are quite opposite to each other in the approach taken and the outcome received, namely the creation of complex waveforms through addition of sinusoids (sine waves), also called Fourier synthesis, followed by the creation of complex waveforms via direct calculation. As this subject is quite big and equally important, I will split up the implementations of each method into other blog posts, providing the general introduction for each of them here.

At the end of each part of this sub-series on complex waveforms, I will include functions both for summing a variable number of sine waves to give any waveform imaginable through Fourier synthesis, as well as a number of functions for direct calculation of waveforms, without even demanding cake or ice cream in return

A quick note on a few naming conventions used in this blog post:

  • A partial, so an integer multiple of a fundamental pitch, is the same thing as a harmonic or an overtone. These are just different terms used. I use the word partial. 
  • Sawtooth waves and ramp waves are often confused with one another. For me, a sawtooth wave descends from the full amplitude down to the lowest amplitude linearly, before going back to the full amplitude from one sample to the next. Ramp waves, as I refer to them, do the opposite in ascending linearly from the lowest amplitude to the maximum before dropping back down. 

Fourier Synthesis

In the early 1800s French mathematician Joseph Fourier came up with a number of equations that are nowadays known as the Fourier Series. What Monsieur Fourier found was, basically, that any complex waveform can be created simply by summing sine and cosine waves. This is quite astonishing, if you think that even sharp-cornered square waves can be created just by summing together lots of sine waves! The mathematics behind this are naturally quite complex, however the perk of us being programmers is that we do not necessarily need to think in terms of mathematical formulas, but only in terms of applying the theory handed down to us by our fellow mathematicians for us to use pragmatically. 

Here is a picture showing the product of summing a certain number of sine waves to give a rough looking square wave, with a perfect one behind:



To go into more detail, the properties of the above sine waves are that each wave (partial, more precisely) following the fundamental pitch/frequency (first picture) is an odd partial, so 3 times the fundamental frequency, then 5 times the fund. freq., then 7 times etc. and the amplitude of each partial is 1 divided by the partial number (the reciprocal). So partial number one (in this case the fundamental frequency) has an amplitude of 1/1 times the maximum amplitude. The next partial, in this case the next odd partial, which is 3, has an amplitude of 1/3 times the maximum. The next partial of 1/5 etc. 

Brief digression about why the amplitude must decrease with an increasing partial number

The amplitude cannot be the same for each partial because adding sine waves at the same amplitude would quickly lead to overflow of the data type as they would eventually become values that could no longer be represented by the primitive types used to hold them. For example, WAV files use 16 bit signed integers to represent sample values, so ± 32767. If you have an amplitude at that maximum so, e.g. 32767 and add another one at the same amplitude, you would end up with a value that is larger than 32767. As you might know, when signed primitive types overflow, they change their sign, therefore, your sine wave would be normal until it overflows, after which you suddenly have a negative peak!

Theoretical example:

Take a fundamental pitch, so just a plain old sine wave, at a certain frequency, say 20Hz. Add to this the next odd partial so in this case the third partial, which is at 20 * 3 = 60 Hz ( then 5 * 20, 7 * 20, 9 * 20 etc.). As explained, we cannot have all partials at the full amplitude, or else the integer used to hold the sample values is likely to overflow. Therefore, we must have this partial at the reciprocal of the partial number. The amplitude is therefore 1 / 3 the maximum amplitude.

Thus, for any nth partial, given a fundamental frequency (above I used 20 Hz) and a maximum amplitude a:

  • Its frequency is: f n
  • Its amplitude is: a / n 

With each partial added, the waveform created looks more and more like a real, perfect square wave. Here is an image of what the square waves look like depending on how many sinusoids/partials you add (partial number in the legend):

Source: http://www.fourieroptics.org.uk/wp-content/uploads/2012/02/squarewave2.png

I usually use 64 partials for the square waves I want nearest to perfection. It gives extremely crisp and nicely sounding square waves without having to take the partial adding to an extreme. I would not recommend using 200 partials as shown above, as 64 is really more than enough and already more than many other implementations use, however it is finally up to you, of course. Here is what my square wave looks like when summing 64 partials:





Pretty close, right ?! Notice, however, the "horns" at the end of some of the peaks? This is the so called Gibbs phenomenon and is a result of summing a finite series of sine waves and not an infinite series, as Fourier's formula suggests. However, there is a really neat work-around called the Lanczos sigma factor with which we can counter-act it.

Follow this link to get to the more detailed explanation and implementation of creating sine waves through Fourier synthesis.

Direct calculation

Another, apparently easier, method of creating complex waveforms is through direct calculation, so through certain mathematical formulas that compute the exact (I will explain shortly why this is not always good) waveforms wanted. 

The simplest waveform to calculate directly is, of course, the square wave. I'm sure that if you give it a thought, you can already guess how this waveform can be created in code, purely through mathematics. It is nothing else than setting the samples of the first half of the period to the full amplitude and the other half to the lowest amplitude. Here is an example of creating a square wave in the Python programming language (Python looks so close to pseudo code that I'm sure you understand it with any background):

buffer = []

period = 100

midPoint = int(period/2)

for n in range(period):
    
    if n < midPoint:
        buffer.append(1)
        
    else:
        buffer.append(-1)


This code fills the buffer with the maximum amplitude until it reaches the mid point, after which we add one sample at half the amplitude and then continue with the lowest amplitude for the rest of the period.

As you can see, direct calculation is often a lot easier, though this comes at a cost. 

Follow this link to see how to create a few other waveforms directly.

VIP - Very important point (I am so funny)

Finally, I want to point out something very important concerning the creation of complex waves. The fact is, nature is not perfect. Nature is chaos. Therefore, it is essential to understand that the most naturally sounding waveforms, closest to what we would hear in reality, are never the most perfect ones. Thus, Fourier synthesis will always create more naturally sounding waveforms and should usually be consulted if you want to use the waveforms for audio, as the methods involving direct calculation, while from a mathematical standpoint being perfect, will not sound as familiar to the human ear. There are, however, uses for directly calculated waveforms, namely whenever you want to use them for modulation, like with an LFO (low frequency oscillator), as any minor imperfection will make your tone sound different which is therefore usually unwanted. 

1 comment :

  1. Finally, I want to point out something very important concerning the creation of complex waves. The fact is, nature is not perfect. Nature is chaos. Therefore, it is essential to understand that the most naturally sounding waveforms, closest to what we would hear in reality, are never the most perfect ones. Thus, Fourier synthesis will always create more naturally sounding waveforms and should usually be consulted if you want to use the waveforms for audio, as the methods involving direct calculation, while from a mathematical standpoint being perfect, will not sound as familiar to the human ear.

    what a joke.

    ReplyDelete