Introduction

Motivation

Yesterday, I was at BeMyApp contest, where we had to develop something about music. After a stupid lyrics generators using markov chains and an uninteresting game in html5, I decided to code a synthesizer (A dream I had for many years, since I'm really interested in sound synthesis).

Theory

Reminders

Okay, let's start with some sound theory.

Sound is mechanical wave propagating in the air. If those waves are periodic, it's a note. Taking a fundamental frequency (mostly 440 Hz, we can define our notes with the following formula:

\[f(n) = (\sqrt[12]{2})^{n-49} 440 Hz\]

As Fourier tells us, every periodic wave can be described in terms of a sum of sinus functions.

\[x_{signal}(t) = \sum_{k=0}^{N}A_{i}\cos(t * F_i)\]

The wave with the lowest F_i is named "fundamental", and multiples of this frequency are called "harmonics".

Let's see our 5 primitive waveforms:

Sinus

The simplest wave is then a sinus: pure signal, without any harmonics. The formula is:

\[x_{sinus}(t) = \sin(2\pi ft)\]

Square

A square waveform, in sense of Fourier is an infinite serie summing all odd frequencies multiple of the harmonic.

\[x_{square}(t) = \frac{4}{\pi}\sum_{k=1}^{\infty}     \frac{\sin(2\pi(2k-1)ft)}{2k-1}\]

Hopefully, we won't need to code such a formula. Electrically, we do this by commutating a DC generator, and we will do almost the same programatically.

Sawtooth

A sawtooth is defined as a sum of all even harmonics

\[x_{saw}(t) = \frac{2}{\pi} \sum_{k=1}^{\infty}(-1)^{k+1}\frac{\sin(2\pi kft)}{k}\]

Electrically, I think that this is achieved with some RLC circuits (please, if you know, confirm it). Programatically, we don't need to code such a formula.

Triangle

A triangle is the sum of odd harmonics, multiplying every (4n-1)th harmonic by -1 and rolling off the harmonics by the inverse square of their relative frequency to the fundamental (thx Wikipedia :D)

\[x_{triangle} = \frac{8}{\pi^{2}}   \sum_{k=0}^{\infty}(-1)^{k}\frac{\sin((2k+1)\omega t)}{(2k+1)^2}\]

Whitenoise

It's just random numbers guys.

x_{whitenoise}(t) = rand()

Recap

recap

Synthesis systems

Additive synthesis

This system is directly inspired from Fourier's series: take a lot of sinus, and add them to make a complex sound.

Substractive synthesis

Take complex sounds (like the 5 ones defined above), and then apply some filters to treat them.

FM synthesis

This kind of synthesis has almost an unpredictable result, and works like radio does: the timbre of a simple waveform is changed by frequency modulating it with a modulating frequency that is also in the audio range, resulting in a more complex waveform and a different-sounding tone.

Wavetable synthesis

This one may be though like a combinations of others: take some sounds, and play them each for few milliseconds. The result can really be crazy. This is the one we will implement, because it allows killing acid sounds without efforts.

We are in a digital world

A word about your sound card

Since we're using a computer, we're dealing with discrete values. We will send our sound to the sound card in the form of many int16, so we have to be able to give the amplitude of our signal considering sampling rate and the frequency. In most systems, the default sampling rate is 44100 Hz, it means that you have to feed your sound card with 44100 "values" per second (for each channel, that's why, assuming stereo, I'll duplicate each value).

Generating waveforms

Enter the world of algorithms. Since I wrote my synthesizer both in Haskell and C++, I'll show, for each part, the language which is the easier to understand. If you never read haskell, list are constructed with :, don't bother about infinite lists and recursions (allowed by laziness), and everything should be okay.

I need to introduce two magic numbers widely used in the code: 44100 is the default sampling rate we target, (-32767, 32768) are short_min and short_max

-- takes a frequency and returns the number of data needed to fill a period
getPeriod :: Float -> Int
getPeriod freq = freq * 2 * pi / 44100

-- takes a number of milliseconds and returns the corresponding number of data
msToDatas :: Int -> Int
msToDatas ms = truncate $ 44100.0 / (1000.0 / fromIntegral ms)

Sine

It's really straigthforward, generate the ith value using the period, and recurse on the (i+1)th value.

sinW :: Float -> Int -> [Int16]
sinW freq i = sinW' i (getPeriod freq)

sinW' :: Int -> Float -> [Int16]
sinW' i period =
        let new = (round $ (* 32767.0) $ sin (fromIntegral i * period)) in
              new:new:sinW' (i + 1) period

Square

The square waveform is A.\mathrm{sign}(\sin(i * \mathit{period})).

squareW :: Float -> Int -> [Int16]
squareW freq i = squareW' i (getPeriod freq)

squareW' :: Int -> Float -> [Int16]
squareW' i period = let new = (round . (* 32767.0) . fromIntegral . sign
                          $ sin (fromIntegral i * period)) in
            new:new:squareW' (i + 1) period

sign x | x < 0 = -1
       | otherwise = 1

Sawtooth

Algorithmically, this is simple : samplesPerWavelength is the number of data to play during one period, ampStep is the "amount of amplitude" to add between each step. Start at int16_min, add ampStep until we reach int16_max, then restart.

sawW :: Int -> Float -> [Int16]
sawW i period = sawW' i period (-32767)

sawW' :: Int -> Float -> Int -> [Int16]
sawW' i period tempSample
        | i < samplesPerWavelength =
                let new = fromIntegral tempSample in
                new:new:sawW' (i + 1) period (tempSample + ampStep period)
        | otherwise = (-32767):(-32767):sawW' 0 period (-32767 + ampStep period)
        where
            samplesPerWavelength :: Int
            samplesPerWavelength = truncate $ 44100.0 / period
            ampStep :: Int
            ampStep = (44100 * 2) `div` samplesPerWavelength period

Triangle

This looks like the sawtooth: samplesPerWavelength is the number of data to play during one period, ampStep is the "amount of amplitude" to add or substract between each step. Start at int16_min, add ampStep until we reach int16_max, then substract ampStep until we reach int16_min etc...

triW :: Float -> Int-> [Int16]
triW period i = triW' period (-32766)
                   $ (44100 * 3)
                       `div` (samplesPerWavelength period)
       where
           samplesPerWavelength :: Float -> Int
           samplesPerWavelength freq = truncate $ 44100.0 / freq

triW' :: Float -> Int -> Int -> [Int16]
triW' period tempSample ampStep
       | abs tempSample > 32767 =
               let new = fromIntegral $ tempSample + ampStep in
                   new:new:triW' period (tempSample - ampStep) (-ampStep)
       | otherwise =
           fromIntegral tempSample
           :fromIntegral tempSample:triW' period (tempSample + ampStep) ampStep

Playing notes

Okay, now you can play frequencies. You should create a simple array which allows a note number to be converted to a frequency, and you'll be able to play notes.

Wavetable Synthesis

As I promised, here, we do some wavetable synthesis: just play some waveforms a few milliseconds.

int main()
{
    double freqs[] = {
#define X(A) A,
# include "freqs.def"
#undef X
    };

    srand(time(nullptr));

    // One theme I composed for Subliminal AEon
    int note = 0;
    int song[] = {
        48, 55, 56, 48, 51, 55, 50, 53,
        48, 55, 56, 48, 51, 55, 50, 53,
        47, 50, 51, 55, 51, 50, 51, 48,
        47, 50, 51, 55, 51, 50, 51, 48
    };

    while (true)
    {
        for (size_t i = 0; i < 5; ++i)
        {
            // Second parameter is the number or milliseconds to play
            Sinus(freqs[song[note]], 6);
            Square(freqs[song[note]], 5);
            Sinus(freqs[song[note]], 7);
            Saw(freqs[song[note]], 3);
            Triangle(freqs[song[note]], 3);
            Sinus(freqs[song[note]], 7);
            White(2);
            Sinus(freqs[song[note]], 7);
        }
        note = (note + 1) % (sizeof (song) / sizeof (song[0]));
    }
    return (0);
}

And just play that using

./a.out | aplay -c 2 -f U16_LE -r 44100

You may have to adjust parameters of aplay depending on your architectures

Going further

New waves

As an example, you can now merge waveforms by adding or multiplying them to create additionnal waveforms. Feel free to create whatever you want when merging.

triPlusSquare :: Float -> [Int16]
triPlusSquareW freq = zipWith addW (triW freq) (squareW (freq * 2))

addW :: Int16 -> Int16 -> Int16
addW x y = (x `div` 2 + y `div` 2)

Filters

It sounds really acid. We will give us the possibility (in the next article :D) to use Fourier's transform to use {low,high}-pass filters.

Conclusion

I was really proud to create my own synthesizer, you should do it too :D !