Intro to Audio Programming Part 4: Algorithms for Different Sound Waves in C#

In the last article, we saw how to synthesize a sine wave at 440Hz and save it to a working WAV file. Next, we’ll expand on that application and learn how to implement some other common waveforms.

image

First of all, I copied WaveFun from the last article as a foundation to work from and called it WaveFun2. The UI provides a few more configuration options, as shown here to the right.

>> DOWNLOAD THE DELICIOUS CODE HERE (21 KB) <<

You can now specify where you want the file to go, as well as the frequency and volume of the waveform. The code has been lightly refactored to support feeding these values from the UI. I’m not going to go into great detail on these changes in this post, but I will show you how to generate the waves.

You can pick from five total waveforms: Sine, Square, Sawtooth, Triangle and White Noise. These are very common waveforms used in synthesizing music.

Let’s learn more about these different waveforms.

Also, for the sine and square waves, we are using a variable t that represents the angular frequency (basically, the “tone” frequency in radians and adjusted for sample rate).

Sine

We have already done some work around the sine wave. It is the smoothest sounding tone signal we can produce.

The Shape

A sine wave looks like this:

image

The Equation

Sample = Amplitude * sin(t*i)

where t is the angular frequency and i is a unit of time.

The Algorithm

Generating just one 16-bit channel of a sine wave (mono) is very easy.

 for (int i = 0; i < numSamples; i++)
{
    data.shortArray[i] = Convert.ToInt16(amplitude * Math.Sin(t * i));
}

 

If you wanted to generate two identical, properly aligned channels of sine data, you have to write the same value twice in a row because channel data is interleaved. Further examples will show this in multichannel format, like below.

 for (int i = 0; i < numSamples - 1; i ++)
{
    for (int channel = 0; channel < format.wChannels; channel ++)
    {
        data.shortArray[i + channel] = Convert.ToInt16(amplitude * Math.Sin(t * i));
    }
}

Square

The square wave is closely related to the sine wave, although it is not in sine form. The square wave produces a very harsh tone due to the abrupt rises and falloffs in the waveform:

image

The Equation

There are a number of ways to generate square waves, and many of them generate imperfect square waves (especially electronics). Since we are using a very precise program with nice things like for loops, we can generate a square wave that is absolutely perfect.

The equation we will be using is:

Sample = Amplitude * sgn(sin(t * i))

In this case the sgn function just tells us whether the value of the sine function is positive, negative, or zero.

The Algorithm

To generate two channels of a square wave:

 for (int i = 0; i < numSamples - 1; i++)
{                        
    for (int channel = 0; channel < format.wChannels; channel++)
    {
        data.shortArray[i] = Convert.ToInt16(amplitude * Math.Sign(Math.Sin(t * i)));
    }
}

Sawtooth

The sawtooth wave has tonal qualities somewhere between a sine and a square wave, almost like a saxophone. It ramps up in a linear fashion and then falls off.

image

The Equation

A “proper” sawtooth wave is created with additive synthesis, which we’ll get into later. Multiple sine waves are added together to create harmonics, until eventually the wave takes the shape of the sawtooth (check this nice animation from wikipedia).

Synthesizing a sawtooth in this manner is best done with an algorithm known as a fast Fourier transform, which we won’t get into just yet because it’s kinda complicated, although all kinds of filters and effects are calculated using FFT algorithms. Instead, we’ll be calculating it as if we were plotting a graph, which means we get “infinite” harmonics. This isn’t really a good thing; it will sound less smooth than a sawtooth calculated with a FFT, but whatever.

The equation for a sawtooth wave is sometimes expressed in this way:

y(t) = x – floor(x);

However, we’ll be generating the wave procedurally.

The Algorithm

The algorithm we will be using is a lot like the one we’d use just to plot this wave on a chart.

We have to determine the “step” of the y-coordinate such that we get a nice diagonal line that goes from minimum amplitude to maximum amplitude over one wavelength, and this is based on the frequency of the wave. The “step” is the difference in amplitude between adjacent samples.

image

    1: // Determine the number of samples per wavelength
    2: int samplesPerWavelength = Convert.ToInt32(format.dwSamplesPerSec / (frequency / format.wChannels));
    3:  
    4: // Determine the amplitude step for consecutive samples
    5: short ampStep = Convert.ToInt16((amplitude * 2) / samplesPerWavelength);
    6:  
    7: // Temporary sample value, added to as we go through the loop
    8: short tempSample = (short)-amplitude;
    9:  
   10: // Total number of samples written so we know when to stop
   11: int totalSamplesWritten = 0;
   12:  
   13: while (totalSamplesWritten < numSamples)
   14: {
   15:     tempSample = (short)-amplitude;
   16:  
   17:     for (uint i = 0; i < samplesPerWavelength && totalSamplesWritten < numSamples; i++)
   18:     {
   19:         for (int channel = 0; channel < format.wChannels; channel++)
   20:         {
   21:             tempSample += ampStep;
   22:             data.shortArray[totalSamplesWritten] = tempSample;
   23:  
   24:             totalSamplesWritten++;
   25:         }
   26:     }                        
   27: }

On line 2, we calculate the number of samples in 1 “tooth.”

On line 5, we calculate the amplitude step by taking the total amplitude range (amplitude * 2) and dividing it by the number of samples per saw tooth.

On line 8, we declare a temporary sample to use. This sample has the amplitude step added to it until we get to maximum amplitude.

We put this in a while loop, because our sample data might end in the middle of a sawtooth wave and we don’t want to go out of bounds.

On line 15, we reset the sample to the minimum amplitude (this happens before each saw tooth is calculated).

We use the same structure – two for loops – for this algorithm. We also add checks to make sure we’re not at the end of the sample data yet.

On line 21 we increment the temporary sample value by the amplitude step and assign it to the data array.

Triangle

The triangle wave is like the sawtooth wave, but instead of having a sharp falloff between wavelengths, the amplitude rises and falls in a smooth linear fashion.

The triangle wave produces a slightly more coarse tone than the sine wave.

image

The Formula

… again, uses a fast Fourier transform to synthesize from a sine wave using odd harmonics. So let’s just create one the easy way!

The Algorithm

The algorithm we use is similar to that of the sawtooth wave shown above. All we are doing is changing the sign of the ampStep variable whenever the current sample (absolute value) is greater than the specified amplitude. So when the wave reaches the max and min, the step changes its sign so the wave goes the other way.

 for (int i = 0; i < numSamples - 1; i++)
{
    for (int channel = 0; channel < format.wChannels; channel++)
    {
        // Negate ampstep whenever it hits the amplitude boundary
        if (Math.Abs(tempSample) > amplitude)
            ampStep = (short)-ampStep;

        tempSample += ampStep;
        data.shortArray[i + channel] = tempSample;
    }
}                    

White Noise

Generating white noise is probably the easiest of them all. White noise consists of totally random samples. Interestingly, white noise is sometimes used to generate random numbers.

image

The Equation

… There isn’t one.

The Algorithm

All you need to do is randomize every sample from –amplitude to amplitude. We don’t care about the number of channels in most cases so we just fill every sample with a new random number.

 Random rnd = new Random();
short randomValue = 0;

for (int i = 0; i < numSamples; i++)
{
    randomValue = Convert.ToInt16(rnd.Next(-amplitude, amplitude));
    data.shortArray[i] = randomValue;
}

Conclusion

These are just some examples of waves that you can use to create interesting sounds. Next, we are going to learn to combine them to create more complex waveforms.

Currently Playing: Black Label Society – 1919 Eternal – Demise of Sanity