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 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
Have Fun! |