VOC File Player

This article was contributed by Pierre Fournier .

Wave files (.WAV) are very easy to handle with Microsoft compilers. Unfortunately, I had to work with Creative Labs' .VOC audio files which are not directly supported.

Although some documentation say that you simply need to "throw the sound file to the wave mapper which will take care of everything!", it was not that easy. When I was doing so, I was hearing a constant 'pop' in the playback. To solve this problem, I had to extract each data block from the file and get rid of the unwanted data.

VOC files work in blocks. Each of the nine different blocks has its own format. A basic VOC file only uses blocks of type 1, 8 and 9. This is what I had to work with, and this is what my code uses.

The code uses the waveOutxxx() functions. These functions work wonderfully, as long as you provide the correct information to them. The class used to play the VOC file is declared as follow:

class CVocPlayer
{
public:
 CVocPlayer();
 virtual ~CVocPlayer();

 void lay( const CString &rcFileName, CWnd *pCallbackWnd );
 inline void Reset() const        
 { 
  waveOutReset( hWaveOut ); 
 }
 void Clear();

protected:
 char *pData;
 bool boPlaying;

 HWAVEOUT hWaveOut;
 WAVEHDR sWaveHdr;

protected:
 void Decode( const CString &rcFileName, FILEINFO *psFileInfo );
};

Obviously, the function used to play the file is Play(). Notice that a pointer to a window must be specified as a parameter. This window is the one that will be notified when the audio file is done playing, and this step is required to cleanup by calling the Clear() function. There are many ways to let us know when the file is done playing, and this is the one I prefer. Changing the method shouldn't be too hard.

The Decode() function uses a FILEINFO structure. This structure is used to gather information about the audio file required by the device. The structure is defined as follow:

typedef struct
{
   UCHAR       ucBitsPerSample;
   UCHAR       ucChannels;
   USHORT      usFileFormat;
   USHORT      usTimeConstant;
   long        lSamplesPerSeconds;
   long        lTotalLength;
} FILEINFO;

Now, the source code of the Play() function

void CVocPlayer::Play( const CString &rcFileName, CWnd *pCallbackWnd )
{
 Clear();

 // Decode the file
 FILEINFO sFileInfo;
 Decode( rcFileName, &sFileInfo );

 // Prepare a WAVEFORMATEX required for opening the device driver
 WAVEFORMATEX sWaveFormat;
 sWaveFormat.wFormatTag           = WAVE_FORMAT_PCM;
 sWaveFormat.nChannels            = sFileInfo.ucChannels;
 sWaveFormat.nSamplesPerSec       = sFileInfo.lSamplesPerSeconds;
 sWaveFormat.nAvgBytesPerSec      = sFileInfo.lSamplesPerSeconds;
 sWaveFormat.nBlockAlign          = 1;
 sWaveFormat.wBitsPerSample       = sFileInfo.ucBitsPerSample;
 sWaveFormat.cbSize               = sizeof(WAVEFORMATEX);

 // Try to open the device driver
 MMRESULT Result = waveOutOpen( &hWaveOut, WAVE_MAPPER, &sWaveFormat,
 (ULONG)pCallbackWnd->m_hWnd, 0, 
 CALLBACK_WINDOW );
 if ( Result != MMSYSERR_NOERROR )
 {
  hWaveOut = 0;
  return;
 }

 // Prepare the header
 sWaveHdr.lpData            = pData;
 sWaveHdr.dwBufferLength    = sFileInfo.lTotalLength;
 sWaveHdr.dwFlags           = 0;
 sWaveHdr.dwLoops           = 0;
 waveOutPrepareHeader( hWaveOut, &sWaveHdr, sizeof(sWaveHdr) );

 // Play the file
 boPlaying = true;
 waveOutWrite( hWaveOut, &sWaveHdr, sizeof(sWaveHdr) );
}

After decoding the file and storing its information in the FILEINFO structure, we fill the WAVEFORMATEX structure with the appropriate information and give it to the waveOutOpen() function. If the device can handle the wanted format, we prepare the header of the audio output and send our data to the waveOutWrite() function.

To decode the VOC file, we go through each of its blocks and store those we want in a memory buffer (our pData member). At the same time, we fill the FILEINFO structure with the information required by the WAVEFORMATEX structure.

void CVocPlayer::Decode( const CString &rcFileName, FILEINFO *psFileInfo )
{
 // Open the file and allocate the memory
 FILE *pFile = fopen( rcFileName, "rb" );
 long lFileLength = _filelength( _fileno(pFile) );
 pData = new char[ lFileLength ];
 char *pDataPos = pData;

 // Place the file pointer at the beginning of the data
 fseek( pFile, 0x1A, SEEK_SET );

 BYTE bType;
 signed long int lLen;
 do
 {
  // Read the block type
  fread( &bType, sizeof(bType), 1, pFile );

  lLen = 0;
  switch( bType )
  {
   case 1:
   {
    fread( &lLen, 3, 1, pFile );
    lLen -= 2;     // Remove Time Constant and File Format bytes
    fread( &psFileInfo->usTimeConstant, 1, 1, pFile );
    fread( &psFileInfo->usFileFormat, 1, 1, pFile );

    // For the moment, it's a plain 8-bit mono file
    psFileInfo->ucBitsPerSample    = 8;
    psFileInfo->ucChannels         = 1;
    psFileInfo->lSamplesPerSeconds = 1000000 /
    (256-(psFileInfo->usTimeConstant % 256));

    // Store this sample in memory
    fread( pDataPos, lLen, 1, pFile );
    pDataPos += lLen;
    break;
   }

   case 8:
   {
    fseek( pFile, 3, SEEK_CUR );     // Skip the length
    fread( &psFileInfo->usTimeConstant, 2, 1, pFile );
    fread( &psFileInfo->usFileFormat, 1, 1, pFile );
    fread( &psFileInfo->ucChannels, 1, 1, pFile );

    // Block of type 8 is always followed by a block of type 1
    fread( &bType, sizeof(bType), 1, pFile );
    fread( &lLen, 3, 1, pFile );
    lLen -= 2;     // Remove Time Constant and File Format bytes
    fseek( pFile, 2, SEEK_CUR );     // Skip T.C. and F.F.

    psFileInfo->ucBitsPerSample    = 8;
    psFileInfo->ucChannels++;
    psFileInfo->usTimeConstant >>= 8;
    psFileInfo->lSamplesPerSeconds = 1000000 /
    (256-(psFileInfo->usTimeConstant % 256));

    // Store this sample in memory
    fread( pDataPos, lLen, 1, pFile );
    pDataPos += lLen;
    break;
   }

   case 9:
   {
    fread( &lLen, 3, 1, pFile );
    lLen -= 12;
    fread( &psFileInfo->lSamplesPerSeconds, 4, 1, pFile );
    fread( &psFileInfo->ucBitsPerSample, 1, 1, pFile );
    fread( &psFileInfo->ucChannels, 1, 1, pFile );
    fread( &psFileInfo->usFileFormat, 2, 1, pFile );

    // Store this sample in memory
    fread( pDataPos, lLen, 1, pFile );
    pDataPos += lLen;
    break;
   }
  };
 } while ( bType != 0 );

 psFileInfo->lTotalLength = pDataPos-pData;

 fclose( pFile );
}

After the file is played, we need to clean our stuff. The Clear() function takes care of that

void CVocPlayer::Clear()
{
 if ( !boPlaying )
  return;

 waveOutUnprepareHeader( hWaveOut, &sWaveHdr, sizeof(sWaveHdr) );
 delete [] pData;
 pData = NULL;
 waveOutClose( hWaveOut );

 boPlaying = false;
}

...and that's it! All you need to do to play the file is call the Play() function!

Downloads

Download source - 25 KB
Download demo project - 98KB