Programming The Sound Blaster 16
Example 2


	    Mission : Learn how to communicate with the Mixer
		      Be able to set any level, either channel to a certain intensity

		       Download the Expansion Pack!
                

Intro


Now that we have somewhat of an understanding of how to communicate with the Sound Blaster's DSP, lets extend what we've learned to include interfacing with the mixer chip. The only tricky aspect of talking to the mixer is to make sure that 1) We are communicating through the proper port, and 2) that we are setting the appropriate bits before sending the level. That's all there is to it. For this example we are assuming that the Sound Blaster 16 is present, so we are going to be programming the CT1745 mixer. The CT1745 has a set of ports that make it backwardly compatible with its earlier parents, we will attend to that later. For now we are just using the newer ones.

Something to mention: I refer to the following as "Ports" when actually they are referred as "Registers". I decided to refer to them as ports because I think they are more like programming ports through the mixer chip than registers. This kind of ideology helps new people understand the interfacing better as well. In all actuality, the mixer address and data ports are the only true "ports". Here's the listing:

Mixer Chips


CT1335	Sound Blaster 2.0 CD Interface Card
CT1345	Sound Blaster Pro
CT1745	Sound Blaster 16


Old Ports


VOCVolume
	
0x4
MICVolume 0xA
MasterVolume 0x22
FMVolume 0x26
CDVolume 0x28
LineVolume 0x2E

New Ports


MasterLeft 0x30
    
LineLeft0x38
    
InputGainRight0x40
MasterRight0x31LineRight 0x39OutputGainLeft0x41
VocLeft 0x32MicVolume 0x3AOutputGainRight0x42
VocRight 0x33PCSpeaker 0x3BAGC0x43
MidiLeft 0x34OutPutSwitches0x3CTrebleLeft0x44
MidiRight 0x35InputL 0x3DTrebleRight0x45
CDLeft 0x36InputR 0x3EBassLeft0x46
CDRight 0x37InputGainLeft 0x3FBassRight0x47



As you can see, we have a listing of the old ports that were included to be backwardly compatable along with the new ones. The old ones are actually mapped into the new ones. The only ports that need explaining are the 0x3C-0x3E. OuputSwitches is used with a 5 bit number to tell the mixer which channels should be on or off. InputL and InputR are used with a 7 bit number to tell the mixer what channels to read for input.

Now that we know what registers/ports are used for what, we need to know how to communicate with the mixer chip. Remember that the before we set the mixer ports in SetIO() as:
MixerAddr= (BLASTER.IOAddr + 0x0004);
MixerData= (BLASTER.IOAddr + 0x0005);
This means that we still need to do the initializing the same way we did before in order to communicate with the mixer. The nice thing about talking to the mixer is that we no longer have to wait for anything. Here is a short example that sets the master volume's left channel to half of its maximum volume:
BYTE masterleft=16;
outp(MixerAddr,MasterLeft);
outp(MixerData,masterleft<<3);

There we go! We send the mixer's address port the port number that we want to set, then send the data to the mixer's data port! One thing that you should be wondering about is the bit sifts to the masterleft variable. Each port has its bits aligned to a certain position, and have certain bits reserved for other stuff. For this reason we have to be sure to check what port we are trying to set, and then make the appropriate shifts if necessary. Before going into that in depth, lets create a structure that can hold all of the possible mixer levels settings. This way if we want to set multiple ports at once, we can fill this puppy in and send it off to a function that traverses through it and sets all of them in 1 shot!
typedef struct Blaster_Mixer_Struct
{BYTE masterleft,masterright;
 BYTE vocleft,vocright;
 BYTE midileft,midiright;
 BYTE cdleft,cdright;
 BYTE lineleft,lineright;
 BYTE micvolume;
 BYTE pcspeaker;
 BYTE outputswitches;
 BYTE inputswitchesleft;
 BYTE inputswitchesright;
 BYTE inputgainleft,inputgainright;
 BYTE outputgainleft,outputgainright;
 BYTE agc;
 BYTE trebleleft,trebleright;
 BYTE bassleft,bassright;
}MixerStruct;
Here's our pretty structure for holding information about the mixer chip's levels. In the class we will be developing (a continuation from Lesson 1), we are going to use 2 instances of this structure. One will be the new settings, and the other will hold the settings that were saved at program start-up. Before we start screwing around with the mixer's settings, lets devise a way to save the existing levels, so we can return them after our program has finished. Lets layer it so that all we have to do is figure out the setting for 1 level, and then build on that to be able to save all levels. Here's the function that will return the setting for 1 level.

BYTE SB16::GetMixerSetting(BYTE Source)
{BYTE temp;
 outp(MixerAddr,Source);
 temp=inp(MixerData);

 // The bit shifts are to align the bits to the lower end of the BYTE

 if(Source >=0x30 && Source <=0x3A)
  {return temp>>3;
  }
 else
  if(Source >=0x3F && Source <=0x42)
   {return temp>>6;
   }
 else
  if(Source >=0x44 && Source <=0x47)
  {return temp>>4;
  }
}

To read a setting, we specify the port as normal through the MixerAddr port, then all we have to do is do an inp on the mixer's data port (MixerData). The only problem is that the mixer returns the value of the port with the bits still aligned to its weird scheme. To correct this we see what port was being looked at. Was it between 0x30 and 0x3A? If so shift it right 3 bits. Was it between 0x3F and 0x42? If so then shift it 6 bits to the right. Was it between 0x44 and 0x47? Again, if so shift it 4 bits to the right. This will give us a real value that we can use that is properly aligned.

Now that we can read the value of one port, lets create a function that goes through all of the mixer's ports, and also traverses through the elements in our structure to save the settings before we mess them all up!

void SB16::ReserveOldMixerSettings()
{BYTE *pointer=&OldMixerSettings.masterleft;

 for(BYTE index=0x30;index<=0x47;index++)
  {*pointer=GetMixerSetting(index);
   pointer++;
  }
 NewMixerSettings=OldMixerSettings; //Initialize new levels to old ones
}
Initially, this was a huge function that did a GetMixerSetting for every item, with everything written out. About 30 minutes later i realized that i could just use a pointer and an index to traverse through both, effectively cutting my function size down 70%, and making me feel reeeeealy stupid :) All this function is doing is creating a pointer to the 1st element in the OldMixerSettings structure, and creating an index (index) that will move through each port. Through each cycle, it goes to the next port number (index++) and to the next element in the structure (pointer++). This wouldn't work too well if the data types were different, but we lucked out and they are all BYTE's!!!

The only thing left on our agenda is to create a function that sets the channels we want to the levels desired. These functions assume that NewMixerSettings and OldMixerSettings are both instances of the MixerStruct structure. I decided to do it this way instead of creating a function that is passed the address of an instance simply because we won't be messing with volume levels too often. Hey, it works for me!
void SB16::SetMixerSettings()
{
  BYTE *pointer=&NewMixerSettings.masterleft;

 for(BYTE index=MasterLeft;index<=MicVolume;index++)
  {outp(MixerAddr,index);
   outp(MixerData,(*pointer<<3));
   pointer++;
  }

  outp(MixerAddr,PCSpeaker);
  outp(MixerData,NewMixerSettings.pcspeaker<<6);

 pointer = &NewMixerSettings.outputswitches;
 for(BYTE index=OutPutSwitches;index<=InputR;index++)
  {outp(MixerAddr,index);
   outp(MixerData,*pointer);
   pointer++;
  }


 pointer = &NewMixerSettings.inputgainleft;
 for(BYTE index=InputGainLeft;index<=OutputGainRight;index++)
  {outp(MixerAddr,index);
   outp(MixerData,(*pointer)<<6);
   pointer++;
  }
  outp(MixerAddr,AGC);
  outp(MixerData,NewMixerSettings.agc);

 pointer = &NewMixerSettings.trebleleft;
 for(BYTE index=TrebleLeft;index<=BassRight;index++)
  {outp(MixerAddr,index);
   outp(MixerData,(*pointer)<<4);
   pointer++;
  }

}
This function looks the weirdest by far, but it is saving us a LOT of hassle. All the programmer needs to know, are the maximum levels for each channel that they are trying to set. Knowing this we can simply set a level without knowing its bit shifting scheme. This means that the NewMixerSettings and the OldMixerSettings structures contain UNALIGNED data. When the settings are read in, they are shifted before being returned. When the settings are finally set, they are shifted back into there correct positions creating harmony once again :) The only thing left to look at is the table with all of the bit positions so we know the maximum levels we can set stuff at! Here it is:

Registers/Port Listing


Bit Number
Index 76543210
0x00Reset Mixer
0x04Voice Volume (L)Voice Volume (R)
0x0A.Mic Volume
0x22Master Volume (L)Master Volume (R)
0x26MIDI Volume (L)MIDI Volume (R)
0x28CD Volume (L) CD Volume (R)
0x2ELine Volume (L)Line Volume (R)
0x30Master Volume (L).
0x31Master Volume (R).
0x32Voice Volume (L).
0x33Voice Volume (R).
0x34MIDI Volume (L).
0x35MIDI Volume (R).
0x36CD Volume (L).
0x37CD Volume (R).
0x38Line Volume (L).
0x39Line Volume (R).
0x3AMic Volume.
0x3BPC Speaker Volume.
0x3COutput Mixer Switches
..Line.LLine.RCD.LCD.RMic
0x3DInput Mixer (L) Switches
..MIDI.LMIDI.RLine.LLine.RCD.LCD.RMic
0x3EInput Mixer (R) Switches
..MIDI.LMIDI.RLine.LLine.RCD.LCD.RMic
0x3FInput Gain (L) .
0x40Input Gain (R) .
0x41Output Gain (L).
0x42Output Gain (R).
0x43.AGC
0x44Treble (L).
0x45Treble (R).
0x46Bass (L).
0x47Bass (R).


Well there we go! Those couple of functions give us complete control over the Sound Blaster's Mixing Chip! If you have any comments, feedback, pointers, suggestions or anything else, please give me some Feedback