When writting DSP, we need to wrap our head around this simple thing: There is no such thing as none-discrete signals when we use computers. We need to provide a framework in wich to works to allow a discretization of signal. We work in discrete domain, wich is easier to handle for our single example to sample a single sinewave. We will use a 440 hertz frequency and we will sample it 44100 times every seconds. For this article, I will use C++, but note that GNU Octave have a very handy way to handle in a much more higher level those type of computation.

Discrete signal

A broad overview would be to think of a continuous signal as a sinewave going smoothly from -1 to 1… Easy to imagine, but somehow a bit harder to deal with when we deal with speakers, as we need to send them data to be “discrete”.

In this example, we can see how a signal goes from the continuous approach to a “per sample” approach. We simply sample along side the sinewave on a constant interval to create a digital-signal to send to the speaker. We are not there yet, but you get the idea: for one seconds of audio, we sample(or create) 44100 times a signal, this signal is then send to your speakers to play that stuff.

I won’t go over all the details of sampling an incoming signal, as it’s out of this ballgame, this article is oriented towards generating a single sinewave from an offline C++ program and to play the audio file to generate a simple tone(we will dive into chord creation later….)!

So at this point: we need a C++ program that will “punch in” 44100 samples per seconds(it’s a lot), and we will arange those samples to generate a single sinewave at 400.

While playing a single frequency is quite boring, let’s image playing 2 of them at the same time: How exciting?

We combine those 2 using c in green: a*b wich will output those two frequency played at the sametime. Therefore, we will sample the c result.

C++ program

Making a simple sine

When working with raw audio data, in this example we use .wav, we want to sample on a discrete domain, therefore time, in .wav we use 44100 sample per seconds. I won’t go into all the setup to output those data into a file, as it has been a lot covered online. So let’s define a simple sinewave class, this way we can start messing around with signals.

class SineOscillator
{
	float frequency, amplitude, angle = 0.0f, offset = 0.0f;
public: 
	SineOscillator(float freq, float amp) : frequency(freq), amplitude(amp) 
	{
		offset = 2 * PI * frequency / sampleRate;
	};
	float process()
	{
		auto sample = amplitude * std::sin(angle);
		angle += offset;
		return sample;
	}
};

In the “audio-loop” we can call a simple process to render a simple 440hz(A4), this is how we do it.

        SineOscillator sineOscillator2(440, .5);

        float duration = 2.0f;
        int samplerate = 44100;
	// Audiorender Loop
	for (int i = 0; i < sampleRate * duration; i++)
	{

		float sample = sineOscillator1.process();
		int intSample = static_cast<int>((sample) * maxAmplitude);
		writeToFile(audioFile, intSample, 2);
		
	}

We generate with this loop a 2 seconds output with a simple tone of 440 hertz. If you are interested in reading more on processing tone to musical <—> note, I wrote an article on that too. But in short, every note on your piano has a root frequencies associated to it. In the case of a A on the fourth octave, it is 440 hz.

If you are curious, here are the formulas to map around those fuckers. Let’s note that we use the 440 standard tuning.

Or in more simpler format:

A is 440, let’s render the data and listen that(very ugly) sound.

Let’s analyse the data

On a audio spectrum analyzer

We clearly see that the frequency is 440hz. I used Ableton Spectrum, for this snapshot.

Let’s take a the file sampling itself.

As mentionned, for 1 second of audio, we generate 44100 samples in code, one sinewave is very boring but let’s take a look at it. For a more decent understanding of the sinewave, let’s use a visual approach of the function sin(x)

sin(0) = 0 and sin(2pi) = 0, wich defines the core idea of sinewave meaning sine function is periodic. Complete cycle is 2pi. We do not use x in time but in concrete domain of time with 44100 sampling.

In our code, we defined a sinewave being sampled 44100 time per seconds, wich leads to this amount of sampling data.

So if we scale by two the sine in code we will have a twice more.

With out two oscillator in code:

	SineOscillator topImage(880, .5);
	SineOscillator bottomImage(440, .5);

We see clearly that idea of a 440 * 2, or playing the upper note on your piano. Somehow like hitting those two note at the same time in blue it’s 440, and in red it 440*2.

How to combine those two.

We are a bit closer to a chord, but we are not there yet. Let’s add 2 sinewave for now. We already have two sinewaves, one playing a 440hz and another one playing a 880hz, but how can we combine them?

Let’s start with a bit of code:

	// Define oscillator shit
	SineOscillator sinewave1(880, .5);
	SineOscillator sinewave2(440, .5);
	// Audiorender Loop
	for (int i = 0; i < sampleRate * duration; i++)
	{

		float sample = sinewave1.process();
		int intSample = static_cast<int>((sample) * maxAmplitude);
		writeToFile(audioFile, intSample, 2);
		
	}

One naive way to do it would be to check for sample every two iteration, this way we could play them side by side with only one sampling different every (1/44100) seconds.

By doing that :

	// Audiorender Loop
	for (int i = 0; i < sampleRate * duration; i++)
	{
		if (i % 2 == 0)
		{
			float sample = sinewave1.process();
			int intSample = static_cast<int>((sample)*maxAmplitude);
			writeToFile(audioFile, intSample, 2);
		}
		else
		{
			float sample = sinewave2.process();
			int intSample = static_cast<int>((sample)*maxAmplitude);
			writeToFile(audioFile, intSample, 2);
		
		}
	}

In our render loop, we have a function that prints 44000 sampling per seconds. We can therefore generate a simple audio signal with a frequency of choice, for now being a sinewave. Let’s add two of them playing at the same time.

Adding three sinewaves


    int duration = 12;
    SineOscillator sineOscillator(440, 0.5);
    SineOscillator sineOscillator2(880, 0.5);
    SineOscillator sineOscillator3(880 * 2, 0.5);

    /* Audio render loop */
    for (int i = 0; i < sampleRate * duration; i++) {
        auto sample = sineOscillator.process();
        auto sample2 = sineOscillator2.process();
        auto sample3 = sineOscillator3.process();
        int intSample = static_cast<int> (
            (sample * maxAmplitude)  + 
            (sample2 * maxAmplitude) + 
            (sample3 * maxAmplitude)) 
            * .05;

        writeToFile(audioFile, intSample, 2);
    }

The code is pretty straight to the point, and it generates this audio file of 12 seconds.

The one part we need to take a look at is the intSample static casting.

Here are all of the three frequencies being displayed on Demos. Note the *2 and *4, those means that for the frequency 1 we simply sample x, and for the freq two, we sample *2, and for frequency 3 we sample *4. This meaning that we start from whatever core frequency of a note, and play it +1 octave by multiplying the frequency by 2, and for 2 octaves higher we multiply by 4.

So for A4 on your piano, you would later get A5, and A6.

We want the red signal to be used in our sampling amplitude.

This would make the 3 sine oscillators to be “playing” at the same time.

        int intSample = static_cast<int> (
            (sample * maxAmplitude)  + 
            (sample2 * maxAmplitude) + 
            (sample3 * maxAmplitude)) 
            * .05;

In our super friends the Spectrum, we have something like that:

Full code looks something like this:

https://gist.github.com/antoinefortin/e05ec2f1302209be0e1bd85ea0531eaa

#include <iostream>
#include <cmath>
#include <fstream>
#define M_PI 3.1415926
using namespace std;

const int sampleRate = 44100;
const int bitDepth = 16;

class SineOscillator {
    float frequency, amplitude, angle = 0.0f, offset = 0.0f;
public:
    SineOscillator(float freq, float amp) : frequency(freq), amplitude(amp) {
        offset = 2 * M_PI * frequency / sampleRate;
    }
    float process() {
        auto sample = amplitude * sin(angle);
        angle += offset;
        return sample;
        // Asin(2pif/sr)
    }
};

void writeToFile(ofstream& file, int value, int size) {
    file.write(reinterpret_cast<const char*> (&value), size);
}

int main() {
  
    ofstream audioFile;
    audioFile.open("waveform.wav", ios::binary);

    //Header chunk
    audioFile << "RIFF";
    audioFile << "----";
    audioFile << "WAVE";

    // Format chunk
    audioFile << "fmt ";
    writeToFile(audioFile, 16, 4); // Size
    writeToFile(audioFile, 1, 2); // Compression code
    writeToFile(audioFile, 1, 2); // Number of channels
    writeToFile(audioFile, sampleRate, 4); // Sample rate
    writeToFile(audioFile, sampleRate * bitDepth / 8, 4); // Byte rate
    writeToFile(audioFile, bitDepth / 8, 2); // Block align
    writeToFile(audioFile, bitDepth, 2); // Bit depth

    //Data chunk
    audioFile << "data";
    audioFile << "----";

    int preAudioPosition = audioFile.tellp();

    auto maxAmplitude = pow(2, bitDepth - 1) - 1;

    int duration = 12;
    SineOscillator sineOscillator(440, 0.5);
    SineOscillator sineOscillator2(880, 0.5);
    SineOscillator sineOscillator3(880 * 2, 0.5);

    /* Audio render loop */
    for (int i = 0; i < sampleRate * duration; i++) {
        auto sample = sineOscillator.process();
        auto sample2 = sineOscillator2.process();
        auto sample3 = sineOscillator3.process();
        int intSample = static_cast<int> (
            (sample * maxAmplitude)  + 
            (sample2 * maxAmplitude) + 
            (sample3 * maxAmplitude)) 
            * .05;

        writeToFile(audioFile, intSample, 2);
    }
    int postAudioPosition = audioFile.tellp();

    audioFile.seekp(preAudioPosition - 4);
    writeToFile(audioFile, postAudioPosition - preAudioPosition, 4);

    audioFile.seekp(4, ios::beg);
    writeToFile(audioFile, postAudioPosition - 8, 4);

    audioFile.close();
    return 0;
}

Leave a Reply

Your email address will not be published. Required fields are marked *