Trippr audio programming: The catch (part 3)

December 10, 2018 - Jos van Tol

In part 2 we created a buffer in memory for our audio data. Every time our audio card needs more sound to play, our audio callback SDL_AudioCallback() function gets automatically called. A writtenToDevice flag gets set and we will have to fill our buffer with new data. Right now, there's not much to worry about. All we have to know is:

void *Buffer;
uint32_t SamplesToWrite;

This buffer will be filled with our samples. Each sample holds two channels of two bytes (16-bits).

At the start of the program we allocate memory for the buffer with malloc().

#define BUFFER_SIZE 1024
Buffer = malloc(BUFFER_SIZE * sizeof(int16_t) * 2);

For the binaural beating effect we need two tones, left and right. Both frequencies differ by amount. So the left channel subtracts amount/2 from the base frequency, and the right adds the same amount.

When we have the right frequency for a channel we can calculate the correct value for a sample with the sin() function. This results in a value between -1 and 1. We will have to scale this to the signed 16-bit value of  -32,768 to 32,767. So we multiply the sine-function with the Amplitude variable;

#define SAMPLING_RATE 44100
uint32_t Amplitude = 32767;
int16_t *Output = Buffer;
// For every SampleIndex:
float Sine = sin(TWO_PI * Frequency * SampleIndex / SAMPLING_RATE);
int16_t Sample = Amplitude * Sine;
*Output++ = Sample;

If we use a for-loop over the whole buffer we get a tone at the given frequency.

But there is one catch.

Every time we start calculating the buffer, the value of the very first sample will be 0. The wave will start at the same point at the start of every buffer. This will result in a loud click every 1024 samples. That is, 43 times every second. This results is a very bad noise!

Also, if the frequency was changed last frame by the user. How do we make sure the new wave form cleanly glues to the previous one?

We need to know what the value is at the end of the buffer. But also if the signal was going up or down. A way to do this is to check what the phase of the signal was. The period of the sine wave is the amount of samples per phase. So if the amount of samples per second is 44,100. Then for example, the samples per period of a 100 Hz tone is 441, or SAMPLING_RATE / Frequency. Then if we take the modulo (or the rest after dividing) of the complete buffer by this period, we can figure out what our position, between 0 and 1, is in the phase.

float Period = SAMPLING_RATE / Frequency;
float Phase = (SamplesToWrite - Period * (int32_t)(SamplesToWrite/Period1)) / Period;
// ANSI C doesn't have a modulo operator.
// You can also do Phase = (SamplesToWrite % Period) / Period; in C++

If we multiply this phase by TWO_PI and add that to the sin() function when we calculate our samples, the new buffer has a phase offset that makes it stick perfectly to the last buffer.

This is maybe hard to visualise but you can find the final sine wave algorithm here:

int16_t *Output = Buffer;
uint32_t SamplesToWrite = BUFFER_SIZE;

for (uint32_t SampleIndex = 0; SampleIndex < SamplesToWrite; SampleIndex++)
  Sample = 32767 * sin(TWO_PI * Frequency * SampleIndex / SAMPLING_RATE + Phase * TWO_PI);
  *Output++ = Sample;

float Period = SAMPLING_RATE / Frequency;
Phase = (SamplesToWrite - Period * (uint32_t)(SamplesToWrite/Period)) / Period + Phase;

In the next, last part we will visit a neat trick to create a soft noise as a layer underneath the tones. But for the technical review of how the audio programming was done in Trippr this is about everything I wanted to talk about. Thanks for reading!