// Filename:- cl_mp3.cpp // // (The interface module between all the MP3 stuff and Trek) -Ste. // // Note that this SOF2 versions is a lot messier than EF1's version, because of having to kludge stuff around // this annoying link-list sound-memory system, so if you ever want to add it to another project, use EF1 version. // // this include must remain at the top of every CPP file #include "client.h" #include "snd_local.h" //#include "../qcommon/memory.h" #include "snd_mp3.h" // only included directly by a few snd_xxxx.cpp files plus this one #include "../mp3code/mp3struct.h" // keep this rather awful file secret from the rest of the program #include "../mp3code/copyright.h" // maybe I'm re-inventing the wheel, here, but I can't see any functions that already do this, so... // char *Filename_WithoutPath(const char *psFilename) { static char sString[MAX_QPATH]; // !! const char *p = strrchr(psFilename,'\\'); if (!p++) p=psFilename; strcpy(sString,p); return sString; } // returns (eg) "\dir\name" for "\dir\name.bmp" // char *Filename_WithoutExt(const char *psFilename) { static char sString[MAX_QPATH]; // ! strcpy(sString,psFilename); char *p = strrchr(sString,'.'); char *p2= strrchr(sString,'\\'); // special check, make sure the first suffix we found from the end wasn't just a directory suffix (eg on a path'd filename with no extension anyway) // if (p && (p2==0 || (p2 && p>p2))) *p=0; return sString; } int iFilesFound; int iFilesUpdated; int iErrors; qboolean qbForceRescan; qboolean qbForceStereo; void R_CheckMP3s( const char *psDir ) { // Com_Printf(va("Scanning Dir: %s\n",psDir)); Com_Printf("."); // stops useful info scrolling off screen char **sysFiles, **dirFiles; int numSysFiles, i, numdirs; dirFiles = FS_ListFiles( psDir, "/", &numdirs); if (numdirs > 2) { for (i=2;ifVolRange; // free sfx->data... // { // Hunk_FreeTempMemory( SFX.data ); // this will have been allocated inside S_LoadSound_Finalise() // // I want a big thankyou from the Mac guys for providing this define... :-) -ste // // #ifndef INT_MIN // #define INT_MIN (-2147483647 - 1) /* minimum (signed) int value */ // #endif // pSFX->iLastTimeUsed = INT_MIN; // force this to be oldest sound file, therefore disposable... pSFX->bInMemory = qtrue; SND_FreeOldestSound(); // ... and do the disposal // now set our temp SFX struct back to default name so nothing else accidentally uses it... // strcpy(pSFX->sSoundName, sReservedSFXEntrynameForMP3); pSFX->bDefaultSound = qfalse; } // other stuff... // Z_Free(pbUnpackBuffer); } // well, time to update the file now... // fileHandle_t f = FS_FOpenFileWrite( sFilename ); if (f) { // write the file back out, but omitting the tag if there was one... // int iWritten = FS_Write(pbData, iSize-(pTAG?sizeof(*pTAG):0), f); if (iWritten) { // make up a new tag if we didn't find one in the original file... // id3v1_1 TAG; if (!pTAG) { pTAG = &TAG; memset(&TAG,0,sizeof(TAG)); strncpy(pTAG->id,"TAG",3); } strncpy(pTAG->title, Filename_WithoutPath(Filename_WithoutExt(sFilename)), sizeof(pTAG->title)); strncpy(pTAG->artist, "Raven Software", sizeof(pTAG->artist) ); strncpy(pTAG->year, "2001", sizeof(pTAG->year) ); strncpy(pTAG->comment, va("%s %g",sKEY_MAXVOL,fMaxVol), sizeof(pTAG->comment) ); strncpy(pTAG->album, va("%s %d",sKEY_UNCOMP,iActualUnpackedSize),sizeof(pTAG->album) ); if (FS_Write( pTAG, sizeof(*pTAG), f )) // NZ = success { iFilesUpdated++; } else { Com_Printf("*********** Failed write to file!\n"); iErrors++; } } else { Com_Printf("*********** Failed write to file!\n"); iErrors++; } FS_FCloseFile( f ); } else { Com_Printf("*********** Failed to re-open for write!\n"); iErrors++; } } else { Com_Error(ERR_DROP, "******* This MP3 should be deleted: %s\n",sFilename); } } else { Com_Printf("*********** File was not a valid MP3!\n"); iErrors++; } } else { Com_Printf(" ( OK )\n"); } FS_FreeFile( pbData ); } } FS_FreeFileList( sysFiles ); FS_FreeFileList( dirFiles ); } // this console-function is for development purposes, and makes sure that sound/*.mp3 /s have tags in them // specifying stuff like their max volume (and uncompressed size) etc... // void S_MP3_CalcVols_f( void ) { char sStartDir[MAX_QPATH] = {"sound"}; const char sUsage[] = "Usage: mp3_calcvols [-rescan] [startdir (default='sound')]\n"; if (Cmd_Argc()>4) // 3 optional arguments { Com_Printf(sUsage); return; } S_StopAllSounds(); qbForceRescan = qfalse; qbForceStereo = qfalse; iFilesFound = 0; iFilesUpdated = 0; iErrors = 0; for (int i=1; iiTimeQuery_UnpackedLength = iUnpackLength; lpMP3Stream->iTimeQuery_SampleRate = iRate; lpMP3Stream->iTimeQuery_Channels = iChannels; lpMP3Stream->iTimeQuery_Width = iWidth; bRetval = qtrue; } } return bRetval; } float MP3Stream_GetPlayingTimeInSeconds( LP_MP3STREAM lpMP3Stream ) { if (lpMP3Stream->iTimeQuery_UnpackedLength) // fields initialised? return (float)((((double)lpMP3Stream->iTimeQuery_UnpackedLength / (double)lpMP3Stream->iTimeQuery_SampleRate) / (double)lpMP3Stream->iTimeQuery_Channels) / (double)lpMP3Stream->iTimeQuery_Width); return 0.0f; } float MP3Stream_GetRemainingTimeInSeconds( LP_MP3STREAM lpMP3Stream ) { if (lpMP3Stream->iTimeQuery_UnpackedLength) // fields initialised? return (float)(((((double)(lpMP3Stream->iTimeQuery_UnpackedLength - (lpMP3Stream->iBytesDecodedTotal * (lpMP3Stream->iTimeQuery_SampleRate / dma.speed)))) / (double)lpMP3Stream->iTimeQuery_SampleRate) / (double)lpMP3Stream->iTimeQuery_Channels) / (double)lpMP3Stream->iTimeQuery_Width); return 0.0f; } // ( m_lengthInSeconds = ((iUnpackedDataLength / iRate) / iChannels) / iWidth; ) // expects data already loaded, filename arg is for error printing only // qboolean MP3_FakeUpWAVInfo( const char *psLocalFilename, void *pvData, int iDataLen, int iUnpackedDataLength, int &format, int &rate, int &width, int &channels, int &samples, int &dataofs, qboolean bStereoDesired /* = qfalse */ ) { // some things can be done instantly... // format = 1; // 1 for MS format dataofs= 0; // will be 0 for me (since there's no header in the unpacked data) // some things need to be read... (though the whole stereo flag thing is crap) // char *psError = C_MP3_GetHeaderData(pvData, iDataLen, &rate, &width, &channels, bStereoDesired ); if (psError) { Com_Printf(va(S_COLOR_RED"%s\n(File: %s)\n",psError, psLocalFilename)); } // and some stuff needs calculating... // samples = iUnpackedDataLength / width; return psError ? qfalse : qtrue; } const char sKEY_MAXVOL[]="#MAXVOL"; // formerly #defines const char sKEY_UNCOMP[]="#UNCOMP"; // " " // returns qtrue for success... // qboolean MP3_ReadSpecialTagInfo(byte *pbLoadedFile, int iLoadedFileLen, // (in) id3v1_1** ppTAG, // (out), can be NULL int *piUncompressedSize, float *pfMaxVol // (out), can be NULL ) { qboolean qbError = qfalse; id3v1_1* pTAG = (id3v1_1*) ((pbLoadedFile+iLoadedFileLen)-sizeof(id3v1_1)); // sizeof = 128 if (!strncmp(pTAG->id, "TAG", 3)) { // TAG found... // // read MAXVOL key... // if (strncmp(pTAG->comment, sKEY_MAXVOL, strlen(sKEY_MAXVOL))) { qbError = qtrue; } else { if ( pfMaxVol) { *pfMaxVol = atof(pTAG->comment + strlen(sKEY_MAXVOL)); } } // // read UNCOMP key... // if (strncmp(pTAG->album, sKEY_UNCOMP, strlen(sKEY_UNCOMP))) { qbError = qtrue; } else { if ( piUncompressedSize) { *piUncompressedSize = atoi(pTAG->album + strlen(sKEY_UNCOMP)); } } } else { pTAG = NULL; } if (ppTAG) { *ppTAG = pTAG; } return (pTAG && !qbError) ? qtrue: qfalse; } #define FUZZY_AMOUNT (5*1024) // so it has to be significantly over, not just break even, because of // the xtra CPU time versus memory saving cvar_t* cv_MP3overhead = NULL; void MP3_InitCvars(void) { cv_MP3overhead = Cvar_Get("s_mp3overhead", va("%d", sizeof(MP3STREAM) + FUZZY_AMOUNT), CVAR_ARCHIVE ); } // a file has been loaded in memory, see if we want to keep it as MP3, else as normal WAV... // // return = qtrue if keeping as MP3 // // (note: the reason I pass in the unpacked size rather than working it out here is simply because I already have it) // qboolean MP3Stream_InitFromFile( sfx_t* sfx, byte *pbSrcData, int iSrcDatalen, const char *psSrcDataFilename, int iMP3UnPackedSize, qboolean bStereoDesired /* = qfalse */ ) { // first, make a decision based on size here as to whether or not it's worth it because of MP3 buffer space // making small files much bigger (and therefore best left as WAV)... // if (cv_MP3overhead && ( //iSrcDatalen + sizeof(MP3STREAM) + FUZZY_AMOUNT < iMP3UnPackedSize iSrcDatalen + cv_MP3overhead->value < iMP3UnPackedSize ) ) { // ok, let's keep it as MP3 then... // float fMaxVol = 128; // seems to be a reasonable typical default for maxvol (for lip synch). Naturally there's no #define I can use instead... MP3_ReadSpecialTagInfo(pbSrcData, iSrcDatalen, NULL, NULL, &fMaxVol ); // try and read a read maxvol from MP3 header // fill in some sfx_t fields... // // Q_strncpyz( sfx->name, psSrcDataFilename, sizeof(sfx->name) ); sfx->eSoundCompressionMethod = ct_MP3; sfx->fVolRange = fMaxVol; //sfx->width = 2; sfx->iSoundLengthInSamples = ((iMP3UnPackedSize / 2/*sfx->width*/) / (44100 / dma.speed)) / (bStereoDesired?2:1); // // alloc mem for data and store it (raw MP3 in this case)... // sfx->pSoundData = (short *) SND_malloc( iSrcDatalen, sfx ); memcpy( sfx->pSoundData, pbSrcData, iSrcDatalen ); // now init the low-level MP3 stuff... // MP3STREAM SFX_MP3Stream = {0}; // important to init to all zeroes! char *psError = C_MP3Stream_DecodeInit( &SFX_MP3Stream, /*sfx->data*/ /*sfx->soundData*/ pbSrcData, iSrcDatalen, dma.speed,//(s_khz->value == 44)?44100:(s_khz->value == 22)?22050:11025, 2/*sfx->width*/ * 8, bStereoDesired ); SFX_MP3Stream.pbSourceData = (byte *) sfx->pSoundData; if (psError) { // This should never happen, since any errors or problems with the MP3 file would have stopped us getting // to this whole function, but just in case... // Com_Printf(va(S_COLOR_YELLOW"File \"%s\": %s\n",psSrcDataFilename,psError)); // This will leave iSrcDatalen bytes on the hunk stack (since you can't dealloc that), but MP3 files are // usually small, and like I say, it should never happen. // // Strictly speaking, I should do a Z_Malloc above, then I could do a Z_Free if failed, else do a Hunk_Alloc // to copy the Z_Malloc data into, then Z_Free, but for something that shouldn't happen it seemed bad to // penalise the rest of the game with extra alloc demands. // return qfalse; } // success ( ...on a plate). // // make a copy of the filled-in stream struct and attach to the sfx_t struct... // sfx->pMP3StreamHeader = (MP3STREAM *) Z_Malloc( sizeof(MP3STREAM), TAG_SND_MP3STREAMHDR ); memcpy( sfx->pMP3StreamHeader, &SFX_MP3Stream, sizeof(MP3STREAM) ); // return qtrue; } return qfalse; } // decode one packet of MP3 data only (typical output size is 2304, or 2304*2 for stereo, so input size is less // // return is decoded byte count, else 0 for finished // int MP3Stream_Decode( LP_MP3STREAM lpMP3Stream, qboolean bDoingMusic ) { lpMP3Stream->iCopyOffset = 0; if (0)//!bDoingMusic) { // SOF2: need to make a local buffer up so we can decode the piece we want from a contiguous bitstream rather than // this linklist junk... // // since MP3 packets are generally 416 or 417 bytes in length it seems reasonable to just find which linked-chunk // the current read offset lies within then grab the next one as well (since they're 2048 bytes) and make one // buffer with just the two concat'd together. Shouldn't be much of a processor hit. // sndBuffer *pChunk = (sndBuffer *) lpMP3Stream->pbSourceData; // // may as well make this static to avoid cut down on stack-validation run-time... // static byte byRawBuffer[SND_CHUNK_SIZE_BYTE*2]; // *2 for byte->short // easily enough to decode one frame of MP3 data, most are 416 or 417 bytes // fast-forward to the correct chunk... // int iBytesToSkipPast = lpMP3Stream->iSourceReadIndex; while (iBytesToSkipPast >= SND_CHUNK_SIZE_BYTE) { pChunk = pChunk->next; if (!pChunk) { // err.... reading off the end of the data stream guys... // // pChunk = (sndBuffer *) lpMP3Stream->pbSourceData; // restart return 0; // ... 0 bytes decoded, so will just stop caller-decoder all nice and legal as EOS } iBytesToSkipPast -= SND_CHUNK_SIZE_BYTE; } // ok, pChunk is now the 2k or so chunk we're in the middle of... // int iChunk1BytesToCopy = SND_CHUNK_SIZE_BYTE - iBytesToSkipPast; memcpy(byRawBuffer,((byte *)pChunk->sndChunk) + iBytesToSkipPast, iChunk1BytesToCopy); // // concat next chunk on to this as well... // pChunk = pChunk->next; if (pChunk) { memcpy(byRawBuffer + iChunk1BytesToCopy, pChunk->sndChunk, SND_CHUNK_SIZE_BYTE); } else { memset(byRawBuffer + iChunk1BytesToCopy, 0, SND_CHUNK_SIZE_BYTE); } // now we need to backup some struct fields, fake 'em, do the lo-level call, then restore 'em... // byte *pbSourceData_Old = lpMP3Stream->pbSourceData; int iSourceReadIndex_Old= lpMP3Stream->iSourceReadIndex; lpMP3Stream->pbSourceData = &byRawBuffer[0]; lpMP3Stream->iSourceReadIndex= 0; // since this is zero, not the buffer offset within a chunk, we can play tricks further down when restoring unsigned int uiBytesDecoded = C_MP3Stream_Decode( lpMP3Stream ); lpMP3Stream->iSourceReadIndex += iSourceReadIndex_Old; // note '+=' rather than '=', to take account of movement. lpMP3Stream->pbSourceData = pbSourceData_Old; return uiBytesDecoded; } else { // SOF2 music, or EF1 anything... // return C_MP3Stream_Decode( lpMP3Stream ); } } // returns qtrue for all ok // qboolean MP3Stream_Rewind( channel_t *ch ) { ch->iMP3SlidingDecodeWritePos = 0; ch->iMP3SlidingDecodeWindowPos= 0; /* char *psError = C_MP3Stream_Rewind( &ch->MP3StreamHeader ); if (psError) { Com_Printf(S_COLOR_YELLOW"%s\n",psError); return qfalse; } return qtrue; */ // speed opt, since I know I already have the right data setup here... // memcpy(&ch->MP3StreamHeader, ch->thesfx->pMP3StreamHeader, sizeof(ch->MP3StreamHeader)); return qtrue; } // returns qtrue while still playing normally, else qfalse for either finished or request-offset-error // qboolean MP3Stream_GetSamples( channel_t *ch, int startingSampleNum, int count, short *buf, qboolean bStereo ) { qboolean qbStreamStillGoing = qtrue; const int iQuarterOfSlidingBuffer = sizeof(ch->MP3SlidingDecodeBuffer)/4; const int iThreeQuartersOfSlidingBuffer = (sizeof(ch->MP3SlidingDecodeBuffer)*3)/4; // Com_Printf("startingSampleNum %d\n",startingSampleNum); count *= 2/* <- = SOF2; ch->sfx->width*/; // count arg was for words, so double it for bytes; // convert sample number into a byte offset... (make new variable for clarity?) // startingSampleNum *= 2 /* <- = SOF2; ch->sfx->width*/ * (bStereo?2:1); if ( startingSampleNum < ch->iMP3SlidingDecodeWindowPos) { // what?!?!?! smegging time travel needed or something?, forget it memset(buf,0,count); return qfalse; } // OutputDebugString(va("\nRequest: startingSampleNum %d, count %d\n",startingSampleNum,count)); // OutputDebugString(va("WindowPos %d, WindowWritePos %d\n",ch->iMP3SlidingDecodeWindowPos,ch->iMP3SlidingDecodeWritePos)); // qboolean _bDecoded = qfalse; while (! ( (startingSampleNum >= ch->iMP3SlidingDecodeWindowPos) && (startingSampleNum + count < ch->iMP3SlidingDecodeWindowPos + ch->iMP3SlidingDecodeWritePos) ) ) { // if (!_bDecoded) // { // Com_Printf(S_COLOR_YELLOW"Decode needed!\n"); // } // _bDecoded = qtrue; // OutputDebugString("Scrolling..."); int _iBytesDecoded = MP3Stream_Decode( (LP_MP3STREAM) &ch->MP3StreamHeader, bStereo ); // stereo only for music, so this is safe // OutputDebugString(va("%d bytes decoded\n",_iBytesDecoded)); if (_iBytesDecoded == 0) { // no more source data left so clear the remainder of the buffer... // memset(ch->MP3SlidingDecodeBuffer + ch->iMP3SlidingDecodeWritePos, 0, sizeof(ch->MP3SlidingDecodeBuffer)-ch->iMP3SlidingDecodeWritePos); // OutputDebugString("Finished\n"); qbStreamStillGoing = qfalse; break; } else { memcpy(ch->MP3SlidingDecodeBuffer + ch->iMP3SlidingDecodeWritePos,ch->MP3StreamHeader.bDecodeBuffer,_iBytesDecoded); ch->iMP3SlidingDecodeWritePos += _iBytesDecoded; // if reached 3/4 of buffer pos, backscroll the decode window by one quarter... // if (ch->iMP3SlidingDecodeWritePos > iThreeQuartersOfSlidingBuffer) { memmove(ch->MP3SlidingDecodeBuffer, ((byte *)ch->MP3SlidingDecodeBuffer + iQuarterOfSlidingBuffer), iThreeQuartersOfSlidingBuffer); ch->iMP3SlidingDecodeWritePos -= iQuarterOfSlidingBuffer; ch->iMP3SlidingDecodeWindowPos+= iQuarterOfSlidingBuffer; } } // OutputDebugString(va("WindowPos %d, WindowWritePos %d\n",ch->iMP3SlidingDecodeWindowPos,ch->iMP3SlidingDecodeWritePos)); } // if (!_bDecoded) // { // Com_Printf(S_COLOR_YELLOW"No decode needed\n"); // } assert(startingSampleNum >= ch->iMP3SlidingDecodeWindowPos); memcpy( buf, ch->MP3SlidingDecodeBuffer + (startingSampleNum-ch->iMP3SlidingDecodeWindowPos), count); // OutputDebugString("OK\n\n"); return qbStreamStillGoing; } ///////////// eof /////////////