Programming The Sound Blaster 16
Example 3


	    Mission : Be Able To Read The .VOC Format. 

                       Download the Expansion Pack!
                


Intro


I bet you think you're pretty cool about now don't ya! Well we do have a pretty good understanding of how the Sound Blaster card works, but now we start getting into the more advanced topics. We need to know how to actually output a sound to the speakers, since i suppose that's the point to all of this isn't it :) Before getting into the actual outputing we need to know how to open and use sound files. I decided to cover .VOC (Sound Blaster) files in this tutorial since if you are trying to program a Sound Blaster, you most likely use .VOC files. With a little modification, our program can accept a .WAV file and get the same output results. Unfortunately i'm only going to cover .VOC files. Lets go over the general layout of a .VOC file!


General Layout


The .VOC file contains a header and a data block. The neeto thing about the data block is that it can be composed of subblocks. This allows us to store more than 1 sound in 1 .VOC file, and it can even have its own attributes like a different sample rate! While this feature is interesting, i'll choose to ignore it since i know that i'll only be storing 1 sample for each .VOC file, sound reasonable? Here's the layout of the header and data blocks.

Header Format


Bytes
    
Description
0x00-0x13h This is the file type description. It MUST read 'Creative Voice File' followed by an EOF (0x1A)
0x14-0x15 The start of the data block. This is defined because in future versions, the size of the header might change. Now it is set to 0x1A
0x16-0x17 The File Version. The first byte is the major version and the second is the minor version number.
0x18-0x19 The identification code. This can verify that we are reading a real .VOC file. It is the compliment of the version in 16 and 17h plus 1234h.


Data Block Format


Each data block starts with the same 4 bytes.

Block Type : 1 byte
Block Length : 3 bytes

The block type can be any of the following, each interpreting the length differently.

SubBlocks


Block Number
			
Info
0End Of Data BlockThis is the only block type without data indicating its length.
1New Sample DataBlock length includes the Sample Rate and Data Packing fields . The Sample Rate is calculated as: SR=256-1,000,000/real sampling rate, so to get the real sampling rate, that would be equal to: real sampling rate = (-1000000)/(SR-256), where SR is the value read from the .VOC file in each.

The Data Packing field corresponds to the following:
0 : 8 bits normal, unpacked samples
1 : 4 bits packed
2 : 2.6 bits packed
3 : 2 bits packed
2Sample DataNot Utilized by the Source Code
3SilenceNot Utilized by the Source Code
4MarkerNot Utilized by the Source Code
5ASCII TextNot Utilized by the Source Code
6Start of a RepetitionNot Utilized by the Source Code
7End of RepetitionNot Utilized by the Source Code
8Additional InformationNot Utilized by the Source Code
9New Block TypeThis is a recent addition to the block types. It does the same thing as block type 1, but makes things a little easier.


As it stands, this might look a little confusing. Don't worry, the souce code will clear up any confusion that you may have. I'm a little confused and I wrote this! The source only covers data blocks 0,1 and 9 since we know that each .VOC file is only going to represent 1 pretty little sound! Let's create a function that reads the main header of the .VOC file and determines if it is a real .VOC or not.


int SB16::ReadVocHeader(FILE *stream)
{ struct VocHeader
  { unsigned char Description[20];
	 unsigned short DataBlockOffset;
	 unsigned short Version;
	 unsigned short IDCode;
  }header;

  fread((VocHeader*)&header,sizeof(header),1,stream);
  if(strncmp((char *)header.Description,"Creative Voice File\x1A",10)!=0)
	{  return 1; //Not a Valid VOC File	
	}
	if(header.Version!=0x010A && header.Version!=0x0114)
	{ //Supports version 110 and 120
          cout<<"Header Version : "<<header.Version<<"NOT Recognized!"<<endl;
	  return 2;//See Above
	}
  if(~header.Version + 0x1234 != header.IDCode)
	{ return 3;//See Above
	}

  cout<<"Version: "<<dec<<(header.Version>>8)<<"."<<dec<<(header.Version&0xff)<<" ";
  fseek(stream,header.DataBlockOffset,SEEK_SET);
  return 0; //All is OK!
}


This function reads the main header of the .VOC file and determines if it authentic or not. If it is, it will return a 0 meaning everything is ok. Any other number can be used as a flag that something went wrong. Here is the function that calls ReadVodheader().

void SB16::Load_Sound(char *filename,int placenumber)
{ FILE *VocFile;
  char Eof=0,Error=0,blocktype;

  VocFile= fopen(filename,"rb");
  if(VocFile==NULL)
	 {cout<<"Unable to open \""<<filename<<"\""<<endl;
	  
	 }

  cout<<filename<<": ";
  Error=ReadVocHeader(VocFile);
  if(Error)
	{cout<<"Can't open "<<filename<<"!!\n";exit(1);
	}
 while(Eof != 1)
	{blocktype=0;
	 fread(&blocktype,1,1,VocFile);//read attribute of it
	 switch(blocktype & 0xFF)
	  { case 0: Eof=1;break;
		 case 1: Error=BlockTypeOne(VocFile,placenumber);break;
		 case 9: Error=BlockTypeNine(VocFile,placenumber);break;
		 default: {cout<<"Unsupported sub-block format '"<<(blocktype&0xFF)
                           cout<<"' in "<<filename<<endl<<endl;
                           Error=1;
                           break;	
                          } 
	  }
	  if(Error)
		{ cout<<"An error has occured while trying to read : "
                  cout<<filename<<endl;
		}
	}
  
  fclose(VocFile);
}

This function opens the file filename, reads its header (Error=ReadVocHeader(VocFile);) and then goes into a loop where it processes subblocks. As stated before, it only recognizes blocks of type 0,1 and 9. If the data block type isn't legal, it spits out an error message and exits. In all reality it is still possible to keeps on processing. This is possible because we know what the block type AND lengths are. This enables us to advance the file pointer to the start of the next data block. Now that we've narrowed the field down to 3 possible data block types , lets go over those as well. Block 0 simply means the end of the file so thats pretty self explanitory. Here's 1 and 9:


int SB16::BlockTypeOne(FILE *stream,int index)
{ struct header
  {char Tc;
   char Pack;
  }Header;

  unsigned long Len=0,SampleRate=0;
  char *temp,*len,*samprate;

  fread(&Len,3,1,stream); //size for REAL
  fread(&Header,sizeof(header),1,stream);
  
  SampleRate=1000000L/256L-(long)Header.Tc;
  Len-=2;
  temp = new unsigned char[Len];
  if(temp ==NULL)
   {cout<<"Error allocating memory!\n";
    return 1;
   }
  fread((unsigned char *)temp,Len,1,stream);
  switch((int)Header.Pack & 0xFF)
  { case 0:cout<<"8 bit unsigned\n";break;
	 case 1:cout<<"4 bit ADPCM\n";break;
	 case 2:cout<<"2.6 bit ADPCM\n";break;
	 case 3:cout<<"2 bit ADPCM\n";break;
	 default:break;
  }
 cout<<"Length : "<<Len<<endl;
 Sounds[index].Length=Len;
 Sounds[index].Sound=(unsigned char*)temp;
 return 0;
}

The only essential parts of this function are the fread calls. If we really wanted, we could make this function VERY small. Instead, lets read in the data, calculate the sampling rate, data pack fields, and the length of the data block. While we're at it, why not print everything out!?

int SB16::BlockTypeNine(FILE *stream,int index)
{struct block9
 {unsigned long SamplesPerSecond;
  char BitsPerSample;
  char Channel;
  unsigned short  Format;
  char Reserved[4]; 
 }Block9;
 
 unsigned char *dataptr;
	
 char MonoStereo[3][9]={"","Mono","Stereo"};
 char Bits[17][8]={"0","1","2","3","4","5","6","7","8","9","10",
						 "11","12","13","14","15","16"};
 long Length=0;
 fread(&Length,3,1,stream);
 fread(&Block9,sizeof(block9),1,stream);
 Length-=12;
 cout<<"Length: "<<dec<<Length;
 dataptr = new unsigned char[1+Length];
	
 if(dataptr == NULL)
  {cout<<"ERROR: dataptr == NULL!\nLength = "<<dec<<Length;
  }

 cout<<" "<<Bits[Block9.BitsPerSample]<<" bit "<<MonoStereo[Block9.Channel];
 switch(Block9.Format)
  {case 0x0000:cout<<" PCM ";break;
	case 0x0001:
	case 0x0002: 
	case 0x0200:
	case 0x0003:cout<<" ADPCM ";break;
	case 0x0004:cout<<" Signed ";break;
	case 0x0006:cout<<" ALAW ";break;
	case 0x0007:cout<<" MULAW ";break;
	default:cout<<"Unsupported Format!\n";break;
  }
  cout<<dec<<Block9.SamplesPerSecond<<" hz"<<endl;
  fread((unsigned char*)dataptr,Length,1,stream);
  dataptr[strlen((char*)dataptr)+1]='\0';
  Sounds[index].Length=Length;
  Sounds[index].Sound=dataptr;
  return 0;
}
As with the last function, this one really only needs the fread calls. This data block type provides us with a lot more information than block number 1. For the curious, PCM stands for pulse code modulation, and ADPCM stands for Adaptive Delta Pulse Code Modulation. Instead of having the sample values actually stand for voltages, in ADPCM format they stand for the difference in voltage giving unlimited voltages, but each signal differing less than 255. In both examples, Sounds[] is a dynamically allocated array of structures. We use index to tell us which position we are at in filling in the Sounds array. Get it? To finish everything off, lets create a function that reads the file names from a data file, and another function that frees up all the memory taken up by the sound files after runtime.

void SB16::Load_Sounds()
{ FILE *s;
  int x;
  
	 if((s=fopen("sounds.dat","r+"))== NULL)
	  { cout<<"Error reading sounds.dat\n";
	  }
	  else
	  {  char *filename = new char[15];
		 for(x=0;x<2/*NumberOfSounds*/;x++)
		  {fgets(filename,14,s);
			filename[strlen(filename)-1]='\0';
			Load_Sound(filename,x);
		  }
		delete filename;
	}
	fclose(s);
}

void SB16::Unload_Sound(unsigned char *sound_ptr)
{ if(sound_ptr != NULL)
	 {delete sound_ptr;
	 }

}

Well, there we have it, the wonderful .VOC format. If you have any questions, comments, or some tips on how i can improve this page, please send me some Feedback!