Playing a Sine Wave 1
The Audio Callback
We want to play a sine wave of the form
\[y = \sin(2 \pi f t)\]Usually f would be cycles per second, and t would be seconds, but it is easier for us to express these as cycles per sample and samples. Here are our includes and some global variables for keeping track of things we'll need
#include "SDL.h"
#include <stdio.h>
#include <math.h>
#define FREQ 200 /* the frequency we want */
unsigned int audio_pos; /* which sample we are up to */
int audio_len; /* how many samples left to play, stops when <= 0 */
float audio_frequency; /* audio frequency in cycles per sample */
float audio_volume; /* audio volume, 0 - ~32000 */
audio_pos acts as our t, keeping track of the sample we are up to. We store the position outside the callback so we can keep the sine wave produced at the end of one callback and the start of another continuous. Now for the actual callback:
void MyAudioCallback(void* userdata, Uint8* stream, int len) {
len /= 2; /* 16 bit */
int i;
Sint16* buf = (Sint16*)stream;
for(i = 0; i < len; i++) {
buf[i] = audio_volume * sin(2 * M_PI * audio_position * audio_frequency);
audio_position++;
}
audio_len -= len;
return;
}
We've explicitly used a mono signed 16 bit format in our callback, so the number of samples is the length in bytes / 2. We go through the buffer given to us by SDL (stream
) and fill it with our sine wave values, then subtract the number of samples written from our remaining length.
FYI: The buffer that we fill is allocated by SDL and is probably adjacent to SDL's own audio data structures. If a buffer overflow doesn't cause a segmentation fault, it will probably mess up SDL e.g. it can cause a double free / memory corruption abort from libc upon calling SDL_CloseAudioDevice
. So if you see weird errors when closing the device, you probably messed up the code for writing to stream
.
Setup and Playing the Audio
For the setup, we just need to initialize the SDL audio subsystem, and open a device with our specification. We want our format to be AUDIO_S16
, and the number of channels to be 1. The sampling rate doesn't really matter, although you need the rate to be atleast twice the frequency of the sine wave you want to play. A specification like this one, telling SDL to only allow frequency change, should be fine
want.freq = 44100;
want.format = AUDIO_S16;
want.channels = 1;
want.samples = 4096;
want.callback = MyAudioCallback;
SDL_AudioDeviceID dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
Now all that's left to do is set up the global variables, and play the audio. When you open a device, the audio is automatically paused, so playing is just a matter of unpausing it. We wait 500 ms at a time until we've played audio_len
samples, then we can just close the device and exit normally.
audio_len = have.freq * 5; /* 5 seconds */
audio_pos = 0;
audio_frequency = 1.0 * FREQ / have.freq; /* 1.0 to make it a float */
audio_volume = 6000; /* ~1/5 max volume */
SDL_PauseAudioDevice(dev, 0); /* play! */
while(audio_len > 0) {
SDL_Delay(500);
}
Exercises
- Can the audio callback be used as a timer? Test it out by measuring the variability of the interval at which it's called with SDL_GetTicks. If it appears stable, see how stable by using SDL_GetPerformanceCounter for higher precision.
- With a sample rate of 44.1 kHz, how long will it take pos (
unsigned int
) to overflow? Try changing pos to anunsigned short
to hear what happens when the overflow occurs. How can this be avoided for the sine wave? When might this be unavoidable?
References
- https://wiki.libsdl.org/CategoryAudio
- Uses javascript SyntaxHighlighter for syntax highlighting.
- Uses javascript MathJax for LateX rendering.
To the extent possible under law,
Craig Hughes
has waived all copyright and related or neighboring rights to
the text and source code in this example/tutorial.