Quake2 MP3 Playback

 

Copyright info

All code in this tutorial is protected by the [GPL License].
All text in this tutorial is protected by the [FDL License].

 

Tutorial by: Heffo

Mailto: heffobomber@hotmail.com

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 http://www.quakesrc.org/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 http://www.quakesrc.org/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

 

 

Tutorial Originally found at: