Page 1 of 1

Game Programming in Linux for Windows Programmers - Part 5 Fun with sound.

#1 WolfCoder  Icon User is offline

  • Isn't a volcano just an angry hill?
  • member icon


Reputation: 790
  • View blog
  • Posts: 7,623
  • Joined: 05-May 05

Posted 14 March 2010 - 09:37 PM

Introduction

I'm back! This time we're going to learn how to program with sound in SDL. I've seen too many tutorials that either overlook this or simplify it to some library that parrots back a WAVE file. Playing back a WAVE file in SDL isn't that hard and I don't think it would make a good tutorial either- more like some tiny forum post's worth of information. I want to show you how to write your own sound mixer! We're going to get down and low level so you can write your own special sound engine. You'll understand what goes on under all those sound libraries you can rummage from the Internet.

Now, first you have to understand what goes on with the sound. You may or may not already know about 8 and 16 bit sound samples and a sampling rate. Sound exists as waves through the air... Take your hand and take a quick swing, if you're fast enough you'll hear something. Now imagine a speedboat running through the water and the shapes it makes in the water. If you take a measurement on the height of one spot in the water, you'll notice it goes up and down and eventually rests in the middle. For our particular purpose, we're going to measure a placid level in the water as 0, and make positive go up and negative go down. Hehe, I got you to swing wildly in the air and feel silly! Don't feel too bad, I did it myself while writing this tutorial.

Posted Image
Ripples just like in water

We're going to transfer these values into our speakers and get the diaphragm inside to move exactly how we want in this same fashion. SDL allows us to take these values and easily pass them all the way to the DAC (Digital to Analog Converter) without messing with whatever the end-user has installed in their machine. It's as though we have a bob in some water that we control and re-create the shapes in the water we want using a machine. Are you ready for this? You're going to have lots of fun annoying yourself with your own programs.

Opening the Sound Device

We need to tell SDL what we want out of the operating system, much like we had to do with the video surfaces in the first tutorial. Take a look at this sample code:

/*
	SDL Sound Example

	written by WolfCoder (2010)
*/

/* Includes */
#include <stdio.h>
#include <SDL.h>

/* Buffer fill-upper */
void fill_audio(void *data,Uint8 *stream,int len)
{
}

/* Open the audio device to what we want */
void open_audio()
{
	SDL_AudioSpec as;
	/* Open SDL */
	SDL_Init(SDL_INIT_AUDIO);
	/* Fill out what we want */
	as.freq = 44100;
	as.format = AUDIO_S16SYS;
	as.channels = 2;
	as.samples = 1024;
	as.callback = fill_audio;
	/* Get it */
	SDL_OpenAudio(&as,NULL);
	/* Go! */
	SDL_PauseAudio(0);
}

/* Clean up things and close device */
void close_audio()
{
	/* Close */
	SDL_CloseAudio();
	/* End */
	SDL_Quit();
}

/* Program entry */
int main()
{
	/* Open the audio */
	open_audio();
	/* Get a character */
	fgetc(stdin);
	/* Close audio */
	close_audio();
	return 0;
}



This will actually compile. Go back to part 1 if you forgot how to do the whole compiling and linking thing to SDL. If it works you might hear nothing, or you might hear faint popping sounds. So... What's going on? This is even simpler than it looks, the hard part is the properties.

/* Fill out what we want */
as.freq = 44100;
as.format = AUDIO_S16SYS;
as.channels = 2;
as.samples = 1024;
as.callback = fill_audio;



44100 is a typical sampling rate for good sound. Please note that just because the output rate is 44100, the data you use with it doesn't have to be. The rate that goes here has more of an effect on performance and quality balancing rather than the size of sounds on disk. The AUDIO_S16SYS means the data is 16 bits wide and it is in signed format. The channels are pretty much 1 = mono and 2 = stereo. The fun thing is that SDL supports surround sound for 4 and 6 channels- but we won't go into that for the sake of sanity. The samples is the length of the sound buffer which you can tinker with to get the sound to fizzle and pop less if your game gets hot on the CPU (but the sound will become less responsive to game events). The callback... Now here's something I'm going to have to explain in detail.

This is a pointer to function. The function must have the same parameters as mine even though our sampler input is not unsigned 8 bit! Grrrr, don't worry, you can just cast inside the function. They're nice enough to pass the length of the buffer in bytes to us so our function can scale to whatever we put for the buffer size (in samples) from before. The data pointer is a little more complicated, you can research threads and user data later if you want to use it.

For now, we're just going to use mathematical functions to generate waves, but realize the function you write and give to the audio device is running in a different thread! Write to memory that doesn't belong to the function at your own risk. The function is called every time the buffer needs to be filled with some more data.

Old School Sound

Alright! Let's generate us some chiptunes~ Modify the callback to the following function:

void fill_audio(void *data,Uint8 *stream,int len)
{
	short *buff;
	int i;
	/* Cast */
	buff = (short*)stream;
	len /= 2; /* Because we're now using shorts */
	/* Noise */
	for(i = 0;i < len;i++)
		buff[i] = rand();
}



Recompile the program and run, and it should play this trippy noise, as in pure noise. If it doesn't, check your audio and check the return values of the calls to SDL sound. Notice how it messes with your idea of stereo in your head? Each channel has a different noise value at any time, so it sounds stranger. So, how do you control the stereo channels? Just modify this part of the callback:

for(i = 0;i < len;i += 2)
	buff[i] = rand();



This will cause noise to come out of only the left channel (if it isn't your speakers are backwards). And this will make noise come out of only the right channel.

for(i = 1;i < len;i += 2)
	buff[i] = rand();



You see where I'm going with this? Now all we need is a tone generator.

A really easy wave to use would be a square wave. A square wave simply alternates between the maximum value and the minimum values you can put into the buffer so that half of a period is low and the other half is high.

    hi
   |---
   |
   |
---|
lo
<----->
T/Period



Nothing too complicated. Let's write a function that generates middle C using a square wave. The frequency of middle C is 261.626Hz. We're going to do some math here so bear with me.

/* Middle C Square Generator */
#define MIDDLE_C 261.626
#define MIDDLE_C_SAMPLES (int)(44100/MIDDLE_C)
int middle_c_dist = 0;
short middle_c_gen()
{
	middle_c_dist++; /* Take one sample */
	if(middle_c_dist >= MIDDLE_C_SAMPLES)
		middle_c_dist = 0;
	/* Low? */
	if(middle_c_dist < MIDDLE_C_SAMPLES/2)
		return -32768;
	/* High */
	return 32767;
}



So I took the frequency we are using and divided it by middle C's frequency. This gives is how many Middle C's we can fit in in our mixing frequency which is the number of samples to use. The function here just generates the next sample and increments a spinning counter you see here. Modify the callback to:

/* Buffer fill-upper */
void fill_audio(void *data,Uint8 *stream,int len)
{
	short *buff;
	int i;
	/* Cast */
	buff = (short*)stream;
	len /= 2; /* Because we're now using shorts */
	/* Square */
	for(i = 0;i < len;i += 2)
	{
		buff[i] = middle_c_gen(); /* Left */
		buff[i+1] = buff[i]; /* Right, same as left */
	}
}



Give this a run and you should hear middle C coming out of your speakers. It's a good chance it's popping like crazy. This is because we've set our buffers a little too low for such a high mixing frequency. Just change it to

/* Fill out what we want */
as.freq = 44100;
as.format = AUDIO_S16SYS;
as.channels = 2;
as.samples = 4096;
as.callback = fill_audio;



And it should sound nice and smooth. That's all there is to it! We could feed the buffer data from a wave file in a similar fashion or anything we want. We can apply special effects to the buffer in software (although they might be slow) and anything else we want. This is a pretty standard way to access the sound and it's very similar to writing a sound engine for the GBA if you're using actual sound samples. If you're writing a classic old-school style game and want chiptunes to match, you can just use these functions to save tons of space.

There's much more I can do, but I really want you to mess with different wave forms and experiment with things yourself. Here's a list of extra exercises to do in order to learn more.

- Read the SDL functions that load and convert wave files so you can play them back. There are functions that convert wave files to your mixing format for you.

- Research what a "wave duty" cycle in a square wave is. Try changing the square function to use a different wave duty.

- Play back some sound file or series of tones, and then apply some special effect like echo to it.

- Research what "binaural beats" are, create a sine wave generator and attempt to generate binaural beats.

Until next time, have some fun!

Is This A Good Question/Topic? 0
  • +

Page 1 of 1