Tutorial *30*

This tutorial will show you how to add MP3 audio playback to the Quake2 executable. It is based on the LGPL mpglib libary from the MPG123 commandline mp3 player for linux.

Since it mpglib libary is LGPL there are no legal reasons for not using it in the Quake2 source, but the MP3 format itself does have some issues regarding the pattented compression technology, basically if you aren't selling your modified Quake2 engine you won't have any problems, but if you are making money off it then you have to pay a royalty for the use of the compression algorithms. For more details on this check out www.mp3licencing.com

My next project will be to add playback of the OGG Vorbis format to the exe for those of you who would rather steer clear of the patent issues, and if you don't know what OGG Vorbis is then check out www.vorbis.com to find out all about it

After all that, on to adding MP3 playback!

First, open the zip file entitled 'mpglib.zip' you will see it contains two header files and a lib file as such..

mpg123.h - 4.9Kb
mpglib.h - 0.8Kb
mpglib.lib - 47.4Kb

these are the files you will need to actually handle the decoding of the MP3, extract them into the 'client' folder of your source release. If you want to edit the source of the lib, you can find it in the optional 'mpglibsrc.zip' file.

You will need to include the lib file in the quake2 exe's build settings, so to do this under Visual C++ 6 you go to 'Project->Settings' then click the 'quake2' mini-icon in the tree box below it. Over to the right the settings will change to reflect the exe's project settings, You will need to do this for both the 'WIN32 Release' and 'WIN32 Debug' configurations, which you can select with the 'Settings For' listbox at the top left of the window. On the options at the right of the screen, click the 'link' tab, then set the 'Category' listbox to 'General', you will then see a textbox labeled 'Object/libary modules', add to the end of the list of libs in the box 'client\mpglib.lib'. Don't forget to do that for both the Debug and Release configurations.

We need to define a new structure to hold the MP3 data, so open the header file 'snd_loc.h' and scroll down it untill you find the type declration for the 'wavinfo_t' structure, then below it add...



	// Heffo - MP3 Audio Support

	typedef struct

	{

		int                     samplerate;		// Samplerate (44100, 22050, 11025)

		byte                    channels;		// Channels (1 = Mono, 2 = Stereo)

		int                     samples;		// Number of samples



		byte                    *mp3data;		// Pointer to the compressed MP3 data from the file

		int                     mp3pos;			// Position in the compressed buffer

		int                     mp3size;		// Size of the compressed buffer



		byte                    *rawdata;		// Pointer to the decompressed MP3 data

		int                     rawpos;			// Position in the decompressed buffer

		int                     rawsize;		// Size of the decompressed buffer

	} mp3_t;

The next step is to actually start including some new code to the engine, so open up the file 'snd_mem.c' and scroll right to the top of the file and locate the lines..



	#include "client.h"

	#include "snd_loc.h"

You now add below them these lines...



	#include "mpg123.h"                     //Heffo - MP3 Audio Support

	#include "mpglib.h"                     //Heffo - MP3 Audio Support

	sfxcache_t *S_LoadMP3Sound (sfx_t *s);  //Heffo - MP3 Audio Support

What the two '#include' lines do is include the header files for the mpglib libary into the source so the compiler knows what functions and variables the lib is providing. the 3rd line is a declaration of the MP3 loader function we will be adding shortly.

You will now need to be in the function 'S_LoadSound' which you will find if you scroll down a little bit. What we need to do here is to allow it to tell if it's an MP3 we are trying to load, and load it instead of a WAV file. Since this function is rather messy, I opted to write a seperate loader function for MP3s and call it from this one if an MP3 was requested, instead of making this function load both formats itself.

Find the code section that reads...



	if (name[0] == '#')

		strcpy(namebuffer, &name[1]);

	else

		Com_sprintf (namebuffer, sizeof(namebuffer), "sound/%s", name);

and below it put this..



	//Heffo - MP3 Audio Support

	len = strlen(name);                // Find the length of the filename

	if(!strcmp(name+len-4, ".mp3"))    // compare the last 4 characters of the filename

		return S_LoadMP3Sound(s);  // An MP3, so return the MP3 loader's return result

What it does is checks to see if the filename requested is an MP3 file by comparing the last four characters of the requested filename with '.mp3' and if it is, it calls the new MP3 loading function which we declared earlier, and returns it's result. If it's not an MP3, then the function just passes into the standard WAV loading code.

Now to add the bulk of the new MP3 playback code, I won't explain how it works here, because I have heavily commented the source so you can see how I was thinking when I wrote it. Immediately under the S_LoadSound function add all this...



	/*

	===============================================================================



	MP3 Audio Support

	By Robert 'Heffo' Heffernan



	===============================================================================

	*/

	#define TEMP_BUFFER_SIZE 0x100000 // 1Mb Temporary buffer for storing the decompressed MP3 data



	// MP3_New - Allocate & initialise a new mp3_t structure

	mp3_t *MP3_New (char *filename)

	{

		mp3_t *mp3;



		// Allocate a piece of ram the same size as the mp3_t structure

		mp3 = malloc(sizeof(mp3_t));

		if(!mp3)

			return NULL; // Couldn't allocate it, so return nothing



		// Wipe the freshly allocated mp3_t structure

		memset(mp3, 0, sizeof(mp3_t));



		// Load the MP3 file via the filesystem routines to ensure PAK file compatability

		// set the mp3->mp3data pointer to the memory location of the newly loaded file

		// and the mp3->mp3size value to the size of the file

		mp3->mp3size = FS_LoadFile(filename, (void **)&mp3->mp3data);

		if(!mp3->mp3data)

		{

			// The File couldn't be loaded so free the memory used by the mp3_t structure

			free(mp3);



			// Then return nothing;

			return NULL;

		}



		// The mp3_t structure was allocated, and the file loaded, so return the structure

		return mp3;

	}



	// MP3_Free - Release an mp3_t structure, and free all memory used by it

	void MP3_Free (mp3_t *mp3)

	{

		// Was an mp3_t to be released passed in?

		if(!mp3)

			return; // Nope, so just return



		// Does the mp3_t still have the file loaded?

		if(mp3->mp3data)

			FS_FreeFile(mp3->mp3data); // Yes, so free the file



		// Does the mp3_t have any raw audio data allocated?

		if(mp3->rawdata)

			free(mp3->rawdata); // Yes, so free the raw data



		// All other memory has been freed, so free the mp3_t itself

		free(mp3);

	}



	// ExtendRawBuffer - Allocates & extends a buffer for the raw decompressed audio data

	qboolean ExtendRawBuffer (mp3_t *mp3, int size)

	{

		byte *newbuf;



		// Was a valid mp3_t provided?

		if(!mp3)

			return false; // Nope, so return a failure



		// Are we missing a buffer for the raw data?

		if(!mp3->rawdata)

		{

			// Yes, so make one the size requested

			mp3->rawdata = malloc(size);

			if(!mp3->rawdata)

				return false; // We couldn't allocate the memory, so return a failure



			// Set the size of the buffer and return success

			mp3->rawsize = size;

			return true;

		}



		// Make a new buffer the size of the old one, plus the extra size requested

		newbuf = malloc(mp3->rawsize + size);

		if(!newbuf)

			return false; // We couldn't allocate the memory, so return a failure



		// Copy the contents of the old buffer into the new one

		memcpy(newbuf, mp3->rawdata, mp3->rawsize);



		// Increase the buffer size by the amount requested

		mp3->rawsize += size;



		// Release the old buffer

		free(mp3->rawdata);



		// Point the mp3_t's rawbuffer to the address of the new buffer

		mp3->rawdata = newbuf;



		// Everything went okay, so return success

		return true;

	}



	qboolean MP3_Process (mp3_t *mp3)

	{

		struct mpstr	mpeg;

		byte			sbuff[8192];

		byte			*tbuff, *tb;

		int				size, ret, tbuffused;



		// Was a valid mp3_t provided?

		if(!mp3)

			return false; // Nope, so return a failure



		// Do we have any MP3 data to decompress?

		if(!mp3->mp3data)

			return false; // Nope, so return a failure



		// Allocate room for a large tempoary decode buffer

		tbuff = malloc(TEMP_BUFFER_SIZE);

		if(!tbuff)

			return false; // Couldn't allocate the room, so return a failure



		// Initialise the MP3 decoder

		InitMP3(&mpeg);



		// Decode the 1st frame of MP3 data, and store it in the tempoary buffer

		ret = decodeMP3(&mpeg, mp3->mp3data, mp3->mp3size, tbuff, TEMP_BUFFER_SIZE, &size);



		// Copy the MP3s format details like samplerate, and number of channels

		mp3->samplerate = freqs[mpeg.fr.sampling_frequency];

		mp3->channels = mpeg.fr.stereo;



		// Set a pointer to the start of the tempoary buffer and then offset it by the number of

		// bytes decoded. Also set the amout of the temp buffer that has been used

		tb = tbuff + size;

		tbuffused = size;



		// Start a loop that ends when the MP3 decoder fails to return successfully

		while(ret == MP3_OK)

		{

			// Decode the next frame of MP3 data into a smaller tempoary buffer

			ret = decodeMP3(&mpeg, NULL, 0, sbuff, 8192, &size);



			// Check to see if we have enough room in the large tempoary buffer to store the new

			// sample data

			if(tbuffused+size >= TEMP_BUFFER_SIZE)

			{

				// Nope, so extend the mp3_t structures raw sample buffer by the amount of the

				// large temp buffer that has been used

				if(!ExtendRawBuffer(mp3, tbuffused))

				{

					// Failed to extend the sample buffer, so free the large temp buffer

					free(tbuff);



					// Shutdown the MP3 decoder

					ExitMP3(&mpeg);



					// Return a failure

					return false;

				}



				// Copy the large tempoary buffer into the freshly extended sample buffer

				memcpy(mp3->rawdata + mp3->rawpos, tbuff, tbuffused);



				// Reset the large tempoary buffer, and extend the reported size of the sample

				// buffer

				tbuffused = 0; tb = tbuff;

				mp3->rawpos = mp3->rawsize;

			}



			// Copy the small tempoary buffer into the large tempoary buffer, and adjust the

			// amount used

			memcpy(tb, sbuff, size);

			tb+=size; tbuffused+=size;

		}



		// All decoding has been done so flush the large tempoary buffer into the sample buffer

		// Extend the mp3_t structures raw sample buffer by the amount of the large temp buffer

		// that has been used

		if(!ExtendRawBuffer(mp3, tbuffused))

		{

			// Failed to extend the sample buffer, so free the large temp buffer

			free(tbuff);



			// Shutdown the MP3 decoder

			ExitMP3(&mpeg);



			// Return a failure

			return false;

		}



		// Copy the large tempoary buffer into the freshly extended sample buffer

		memcpy(mp3->rawdata + mp3->rawpos, tbuff, tbuffused);



		// Calculate the number of samples based on the size of the decompressed MP3 data

		// divided by two for 16bit per sample, then divided by the number of channels in the MP3

		mp3->samples = (mp3->rawsize / 2) / mp3->channels;



		// Free the large tempoary buffer

		free(tbuff);



		// Shutdown the MP3 decoder

		ExitMP3(&mpeg);



		// Everything worked, so return success

		return true;

	}



	// S_LoadMP3Sound - A modified version of S_LoadSound that supports MP3 files

	sfxcache_t *S_LoadMP3Sound (sfx_t *s)

	{

		char	namebuffer[MAX_QPATH];

		mp3_t	*mp3;

		float	stepscale;

		sfxcache_t	*sc;

		char	*name;



		// Workout the filename being opened

		if (s->truename)

			name = s->truename;

		else

			name = s->name;



		if (name[0] == '#')

			strcpy(namebuffer, &name[1]);

		else

			Com_sprintf (namebuffer, sizeof(namebuffer), "sound/%s", name);





		// Allocate an mp3_t structure for the new MP3 file

		mp3 = MP3_New(namebuffer);

		if(!mp3)

		{

			// Couldn't allocate it, so show an error and return nothing

			Com_DPrintf ("Couldn't load %s\n", namebuffer);

			return NULL;

		}



		// Process the mp3_t and check to see if something went wrong

		if(!MP3_Process(mp3))

		{

			// Something did, so free the mp3_t, show an error and return nothing

			MP3_Free(mp3);

			Com_DPrintf ("Couldn't load %s\n", namebuffer);

			return NULL;

		}



		// Make sure the MP3 is a mono file, reject it if it isn't

		if(mp3->channels == 2)

		{

			// It's a stereo file, so free it, show an error and return nothing

			MP3_Free(mp3);

			Com_Printf ("%s is a stereo sample\n",s->name);

			return NULL;

		}



		// Work out a scale to bring the samplerate of the MP3 and Quake2's playback rate inline

		stepscale = (float)mp3->samplerate / dma.speed;



		// Allocate a piece of zone memory to store the decompressed & scaled MP3 aswell as it's

		// attatched sfxcache_t structure

		sc = s->cache = Z_Malloc ((mp3->rawsize / stepscale) + sizeof(sfxcache_t));

		if (!sc)

		{

			// Couldn't allocate the zone, so free the MP3, show an error and return nothing

			MP3_Free(mp3);

			Com_DPrintf ("Couldn't load %s\n", namebuffer);

			return NULL;

		}



		// Copy a few details of the MP3 into the sfxcache_t structure

		sc->length = mp3->samples;

		sc->loopstart = -1;

		sc->speed = mp3->samplerate;

		sc->width = 2;

		sc->stereo = mp3->channels;



		// Resample the decompressed MP3 to match Quake2's samplerate

		ResampleSfx (s, sc->speed, sc->width, mp3->rawdata);



		// Free the mp3_t structure as it's nolonger needed

		MP3_Free(mp3);



		// Return the sfxcache_t structure for the decoded & processed MP3

		return sc;

	}

Okay, now that you have added all that, you hopefully should be able to put an MP3 in the baseq2/sound folder, compile your exe, and when you run it, in the console type 'play .mp3' it should load and start to play the mp3. If that happens then it worked perfectly. Keep in mind that Quake2 only likes files in the 22Khz, 16bit, Mono format, and with the MP3 compression it doesn't really matter what compression bitrate you use.

Have Fun!
Robert 'Heffo' Heffernan
heffobomber@hotmail.com



 
Not logged in
Sign up
Login:
Passwd: