Sound Playback Using Single Cycle DMA Mode
Example 5


	    Mission : Discover a new way to get sound output.  It's certainly a 
                    better alternative than Direct Mode, but it still isn't as 
                    high a quality as we want.
                      Download the Expansion Pack!



Intro


Up to this point we've been able to play sound only through Direct Mode. This method worked but was a real hassle to use and actually was hard to make worth its while to program. The 1st alternative, Single Cycle DMA Mode, while being better than Direct Mode isn't as good as Auto-Intialization Mode. We will cover Single Cycle DMA Mode first to get us ready to use my method of choice, AutoInit Mode!

Single Cycle simply means that we yell at the DMA and DSP to transfer x number of bytes. After they are done "doing their thing" they will yell back at us in the form of an interrupt. Which interrupt will be generated? Well the Interrupt the Sound Blaster is set for of course! First off, let's go over the basic steps of setting up a transfer in this mode.

There are two other classes that are discussed in tutorials, the DMA class for programming the DMA transfers and the ISRS class which handles the interface to the interrupt.


Steps to Programming a Single Cycle DMA Transfer


1.Setup and Enable the Interrupt Service Routine
2.Program the DMA
3.Program the DSP
4. Acknowledge the interrupt
If not done, goto 2
5.Return ISR Routine to normal

Our task involves several differen't issues, programming the Interrupt, DSP and DMA to all work together. I'm going to go from one step to the next, introduce the code and as usual explain it, and then procede to the next step! Without further boring introduction, lets look at hooking up the Sound Blaster Interrupt!


1. Setup and Enable the Interrupt Service Routine


  void SetupSBISR();
  void GetSBIRQ(unsigned char);
  _go32_dpmi_seginfo OldSB,NewSB;
  char SetSB,SBIRQ;
  unsigned short SBMaskSave;


We've added two new function to our ISRS class and a couple of variables. We created the GetSBIRQ function so that other functions can set the SBIRQ variable. I'm ignoring the fact that SBIRQ is declared public, but later on when we develop our ISRS class we might set it to protected or normal private. We created SetupSBISR to do all the hooking up for us! We declare SetSB so that when the destructor is called we return the ISR only if it was actually set. Lastely we have SBMaskSave which preserves the Programmable Interrupt Controller's mask. In all we have only 2 new functions, let's get into the .CPP file!

...
extern SB16_ISR (_go32_dpmi_registers*);

ISRS::ISRS()
 {SBIRQ=-1;
 }

void ISRS::GetSBIRQ(unsigned char IRQ)
 {SBIRQ=IRQ;
 }

void ISRS::SetupSBISR()
 { SetSB=1;
  if(SBIRQ>=0)
   {cout<<"Please set the Sound Blaster IRQ Number in the ISR class!\n";
    sleep(4);
    exit(1);
   }
  _go32_dpmi_get_protected_mode_interrupt_vector(SBIRQ+8,&OldSB);
  NewSB.pm_offset = (int)SB16_ISR;
  NewSB.pm_selector=_go32_my_cs();
  _go32_dpmi_allocate_iret_wrapper(&NewSB);
  _go32_dpmi_set_protected_mode_interrupt_vector(SBIRQ+8,&NewSB);
  SBMaskSave=inp(0x21);
  short temp=(1<<SBIRQ);
  outp(0x21,(SBMaskSave& ~temp));
 }

We first declare a function prototype for our isr and define it as extern since we won't be defining it here. Afterward we have our simple little access function to set the SBIRQ variable. Finally we have the SetupSBISR function! Here's where the sound blaster interrupt finally gets set. Before we do ANYTHING we check to see that the SBIRQ is a valid number. If it isn't, the rest of the code will not work and in a best case senario it will just not work, but in a worst case, it might crash the computer! After we know that the Sound Blaster IRQ has been set we save the old IRQ with OldSB and follow the normal steps of setting an interrupt. The last 3 lines communicate with the PIC or Programmable Interrupt Controller to make sure that everything is returned when we finish.

void ISRS::ReturnISRs()
{
  if(SetSB)
   {short temp=inp(0x21);
    temp|=(1<<SBIRQ);
    outp(0x21,temp);

    _go32_dpmi_set_protected_mode_interrupt_vector(SBIRQ+8,&OldSB);
    _go32_dpmi_free_iret_wrapper(&NewSB);
   }
}




2. Program the DMA


void SB16::SetupDMA()
{ disable();

 if(ModeBits==8)
   {dma.SetDMAChannel(BLASTER.DMAChan);
   }
 dma.DisableChannel();      
 dma.SetControlByteMask(DemandMode,AddressIncrement,SingleCycle,ReadTransfer);
 dma.SetControlByte();
 dma.ClearFlipFlop();      
 dma.SetBufferInfo();
 dma.SetTransferLength(BUFFSIZE);
 enable();                        
 dma.EnableChannel();            
}

Here's all we need to setup the DMA transfer. Each of these functions are defined in the DMA programming tutorial which is located here. Remember that we first have to pass the DMA channel of the Sound Blaster to the DMA class before anything will work correctly. After that has been set we can go through the normal procedures for setting up the transfer. That order would be Disabling the channel we are going to be programming, Setting the Control Byte to tell the DMA how we are going to be transfering memory, Clearing the Flip-Flop, Setting the address and page of the buffer, Sending how much we are going to be transfering and finally Enabling our DMA channel which is all ready to go! When we switch to Auto-Initialization DMA Transfers, this routine will change only slightly which is nice since the less programming we have to do the BETTER! Lets move to step 3!


3. Program the DSP


The DMA section of our SB16 class looked pretty easy because we were able to encapsilate all the appropriate functions within the DMA class. Going from the SetupDMA function to the SetupDSP function will require more explanation and the ability to see the big picture. Luckily I have an uncanny ability to overly explain :) First off we are going to create the master SetupDSP function. Remember that the Sound Blaster 16 is capable of more than just Direct or Single Cycle transfers. For this reason we are going to do some checking to see what mode we will be using. From there we will call individual functions that will communicate with the DSP and set it correctly. The nice thing about these functions is that we made it backwardly compatable so we can do Single Cycle DMA Transfers on any Sound Blaster card. The only stipulation is that if we are going to do a SC DMA Transfer on a card with DSP version less than 4.xx it MUST be Mono 8 Bit, Stereo and 16 Bit SC Transfers were introduced with the 4.xx line of DSP chips! Ok, I think I've babbled enough :)

void SB16::SetupDSP()
{  SetupSingleCycleDSP();
}

Golly this sure looks impressive huh?! Since we haven't learned how to do Auto-Init. DMA Transfers, we will just have our SetupSingleCycleDSP function placed here. Later we will check to see which mode we will be using. Let's find out what this mystery function does.

// we have to assume that our settings match the sound card so we won't be
// requesting 8-stereo or 16 anything with under a 4.0 dsp
void SB16::SetupSingleCycleDSP()
 { if(ModeBits==8)
   { if(!ModeStereoMono)
      { SingleCycle8Mono();    //8 bit Mono
      }
     else
      { SingleCycle8Stereo();  //8 bit Stereo
      }
   }
  else
    { if(!ModeStereoMono)
       { SingleCycle16Mono();  //16 bit Mono
       }
      else
       { SingleCycle16Stereo();//16 bit Stereo
       }
    }
 }

Before we go into this function into any detail, I'd like to mention that by the time this function is called, we have properly determined if the requested mode is supported under the current DSP Version. I haven't put in these checking routines so 1. You can place them were you feel appropriate and 2. Because hey let's face it 30+ tutorials in 2 months is a little mind racking, do some work for yourself you lazy tard! Eh hem..Sorry about that :) Ok, the explanation of this little function. We first check ModeBits which represents how many bits our sound data will be. This is set to 8 by default and should be set in your Kick-Butt program in a setup routine of some sort. We also check ModeStereoMono to see if we are going to be using Stereo or Mono sources. In each case we call the appropriate function. Please not that I haven't dabbled in 16 bit DMA transfers yet so the 16 Bit functions are yet to be tested. Now let's see what each function does.

...
#define NumberOfSounds 1
#define LoByte(x)(short)(x & 0x00FF)
#define HiByte(x)(short)((x&0xFF00)>>8)


void SB16::SingleCycle8Mono()
{ //make sure we use the right one, if dsp version is less that 4 then do old
  if(DSPVersionNum<4)
   { OldSingleCycle();
   }
  else
   { SendFrequency();
     WriteDSP(0xC0);       //8 bit
     WriteDSP(0x00);       //Mono
     SendLength();
   }
}

void SB16::OldSingleCycle()
{                                     //has to be 8 mono
  WriteDSP(DSP_TIME_CONSTANT);
  WriteDSP(HiByte(OldTimeConstant()));//use only high byte
  WriteDSP(0x14);                     //8 bit pcm output
  SendLength();
}

unsigned short SB16::OldTimeConstant()
{ return 65536-(256000000/((ModeStereoMono+1)*SoundRate));
}

Although any Sound Blaster card can do an 8-Bit Single Cycle Transfer, we can't communicate with the DSP in the same way for every card. For this reason we test to see which DSP Version we will be dealing with and if it's below 4.xx we call OldSingleCycle which will get the job done. Each function has been constructed according to the Creative Labs specifications. In the newer cards (DSP Version 4.xx) we send the frequency we are going to be using, Write how many Bits we will be transfering at a time, Send if we are going to be using Mono or Stereo mode, and finally Send the length of the transfer. Notice how the old method differs. We send the command to the DSP to set the Time Constant, Send the Time Constant using only the High Byte, Tell the DSP to go into Single Cycle mode (which can only be 8 Bit Mono) and follow it all up by Sending the length of our transfer. Here's the remaining functions to set the correct Single Cycle DMA Mode.

void SB16::SingleCycle8Stereo()
{ SendFrequency();
  WriteDSP(0xC0); //8 bit
  WriteDSP(0x20); //Stereo
  SendLength();
}

void SB16::SingleCycle16Mono()
{ SendFrequency();
  WriteDSP(0xB0); //16 bit
  WriteDSP(0x00); //Mono
  SendLength();
}

void SB16::SingleCycle16Stereo()
{ SendFrequency();
  WriteDSP(0xB0); //16 bit
  WriteDSP(0x20); //Stereo
  SendLength();
}

These functions are simply communicating with the DSP in the way described by Creative Labs documentation, so they don't require a lot of explaining. I created the following two functions to help us out.

void SB16::SendLength()
{ WriteDSP(LoByte(TransferLength-1));
  WriteDSP(HiByte(TransferLength-1));
}
void SB16::SendFrequency()
 { WriteDSP(0x41);
   WriteDSP(HiByte(SoundRate));
   WriteDSP(LoByte(SoundRate));
 }

All we need to know is that whenever we send the length of our transfer to the DSP, we send the Lo Byte first, followed by the High Byte. The SendFrequency function speaks for itself.. Now on to our final topic!


4. Acknowledge the Interrupt


void SB16::ServiceISR()
 {disable();
  if(TransferMode==1) //SS
   { ServiceSC();
   }
  else        //AI
   { ServiceAI();
   }
  enable();
  outp(0x20,0x20); //eoi command to PIC
 }

This is the actual function that gets called when the Sound Blaster interrupt occurs. We have to accomplish 2 things for everything to move smoothly. We 1. Have to acknowledge the Sound Blaster Card and tell it that we heard its cry for an interupt, and 2. Tell the Program Interrupt Controller that we are done with our interrupt function. Doing this will make everyone happy, but somewhere in the middle we have to fill our DMA Buffer with useful information! I've split this function into sections with an if statement. We will run ServiceSC for Single Cycle Mode and Service AI for Auto-Initialization Mode. Before we attempt to do anything, we disable all interrupts because we are selfish :) After the appropriate function has been run, we enable all interrupts and send the Program Interrupt Controller a 0x20 which is called the End Of Interrupt command, this tells it everything is a-ok! Let's see what the other 2 functions do!

void SB16::ServiceSC()
 { if(Done==0)
   {FillWholeBuffer();
    SetupDMA();
    SetupDSP();
    inp(DSPStatus);
   }

 }

void SB16::ServiceAI()
 { 
 }

Here's where we are supposed to tell the Sound Blaster to shut up and that we heard its cries for an interrupt, and also to fill the DMA Buffer accordingly. We don't define anthing in the ServiceAI yet, we will fill that in in the next tutorial :) In the ServiceSC function, we first check to see if we have transfered enough, if we have, do nothing because the program is about to exit. This method works fine for programs that are going to play a sound and exit, like our little demo. The rule of thumb is to always acknowledge the Sound Blaster's interrupt request even if we aren't going to fill the DMA Buffer with anything useful. As you can see we do nothing if we are done. If we aren't done, we fill the entire DMA Buffer with new data, program the DMA for the transfer, program the DSP for the transfer, and then lastely acknowledge the interrupt the Sound Blaster has requested. All we need to do is do an inport on the DSP Status Register! Well, we are just about finished with our routines. Here's a couple of misc. functions that we are going to use.

void SB16::SetModeBits(unsigned char Bits)
{ ModeBits=Bits;
}

void SB16::SetTransferMode(unsigned char mode)
{ TransferMode=mode;
}


void SB16::FillWholeBuffer()
{ unsigned char *d=(unsigned char*)dma.MK_FP(dma.phys>>4,0);
  unsigned char *s = Sounds[0].Sound;
  unsigned long amount;
  amount=BUFFSIZE;

  if(SoundSize < BUFFSIZE)
   {amount=SoundSize;
    Done=1;
    TransferLength=SoundSize;
   }
  s+=place;
 memmove((unsigned char *)d,(unsigned char *)s,amount);

  SoundSize-=BUFFSIZE;
  place+=BUFFSIZE;
}

Almost done! We define two access functions to properly set the ModeBits and TransferMode variables. Then we have our master FillWholeBuffer function. It determines how many time it can fill the DMA Buffer entirely with sound data, then when the amount remaining is less than the whole DMA Buffer(BUFFSIZE), it will program the DSP to only transfer the remaining portion! All of this code is used for DJGPP, but VERY little is needed to port it to 16 bit compilers. I hope this opened your eyes somewhat. For a real example, please check the example program with source code! If you have any questions, comments, or some tips on how i can improve this page, please send me some Feedback!