Sound Playback Using Auto Init DMA Mode
Example 6


    Mission : The ultimate way to achieve flawless playback! Let's make some very minor 
              changes to our SB16 class to be able to utilize this mode!
               Download the Expansion Pack!



Intro


Auto Initialization Mode is AWESOME!! It has the ability to produce flawless playback without annoying clicks whenever the DMA buffer is filled. It sounds just like the sample is supposed to! We are going to extend our class with just a couple of functions and since we knew we were going to make this transition the changes will be VERY minor and fit right in! Remember that we will still be using the DMA class we created for Single Cycle DMA Transfers. Here's a peek at it if you don't remember what it looks like!

Before we go into the source code changes in any depth we must first discuss how AutoInitialization Mode works. Remember previously that whenever the DSP generated an interrupt, we had to reprogram the DSP and DMA for another transfer which caused ticks in the sound output. Well, Auto Initialization mode will continue to transfer a block of memory until you tell it to stop. This might sound kind of odd for the newbies our there. Why would we want to transfer a block of memory more than once to the sound card?! Yippy, we get the same chunk of sound playing monotonously over the speakers until we go nutz! Well, here's where the real art comes into play! We program the DMA to transfer ALL of the DMA buffer and the DSP to transfer HALF the memory! So now, whenever the DSP generates its interrupt, we know that it has successfully transferred 50% of the whole buffer. The reason this is so important is because when that half has been played, we can fill it up with NEW data while the second half is playing. This way we will ALWAYS be filling in new data ahead of the transfer so we get flawless playback!

Since we have already gone over the steps to program a DMA transfer, let's get a quick look the header file and then see where our minute changes will be!


Header File Additions


  void SetupAutoInitDSP();
  void Auto8Mono();
  void Old8Mono();
  void Auto8Stereo();
  void Auto16Mono();
  void Auto16Stereo();

  void FillHalfBuffer();
  void MemMove(unsigned char*, unsigned char*,unsigned int);
  char SideToFill;

This is all we need to add to use Auto Initialization DMA Mode! SetupAutoInitDSP is located inside our main SetupDSP function. That function determines what mode we wish to use and calls one of the following 4 functions that follow it. Old8Mono is called from Auto8Mono if the DSP version is less than 4.x. Finally we have the function that fills only half the DMA buffer and a custom function that moves memory from one place to another. SideToFill will be used as a flag to tell us which side of the DMA buffer to fill, but all this will be explained later! Lets cover the code in the same fashion we did with Single Cycle DMA Transfers.


1. Setup and Enable the Interrupt Service Routine


Everything works great! Don't make ANY changes!!! I told you this would be easy!


2. Program the DMA


We only need to make a slight modification to our DMA routine. We must decide wether to use Auto-Initialization Mode or Single Cycle.

void SB16::SetupDMA()
{...

 if(TransferMode==1)
  {dma.SetControlByteMask(DemandMode,AddressIncrement,SingleCycle,WriteTransfer);
       //Put into 8-bit SingleCycle mode
  }
 else
  {dma.SetControlByteMask(DemandMode,AddressIncrement,AutoInit,WriteTransfer);
       //Put into 8-bit AutoInit mode
  TransferLength=HALFBUFFSIZE;
  }
...
}

Here's a portion of our SetupDMA code. Instead of calling SetControlByteMask with SingleCycle we now have to test which mode we will be using and set the mask accordingly. If we are in Autoinitialization Mode we will be programming the DSP to transfer HALF of our DMA buffer, so set TransferLength to HALFBUFFSIZE!


3. Program the DSP


void SB16::SetupDSP()
{  if(TransferMode==1)
    {SetupSingleCycleDSP();
    }
  else
   { SetupAutoInitDSP();
   }
}

Before, only the SetupSingleCycleDSP function was called. Now we test to see which mode it is in, and call the appropriate function!

void SB16::SetupAutoInitDSP()
{if(ModeBits==8)
   { if(!ModeStereoMono)
      { Auto8Mono();    //8 bit Mono
      }
     else
      { Auto8Stereo();  //8 bit Stereo
      }
   }
  else
    { if(!ModeStereoMono)
       { Auto16Mono();  //16 bit Mono
       }
      else
       { Auto16Stereo();//16 bit Stereo
       }
    }
}

This looks extremely familiar! If it doesn't, that most likely means you haven't read the tutorial on Single Cycle Transfers, shame on you!! Here we are merely testing how many bits we are using (8 or 16) and which mode (Stereo or Mono), and calling the right function to get the job done!

void SB16::Auto8Mono()
{ if(DSPVersionNum <4.0)
   {Old8Mono();
   }
 SendFrequency();
 WriteDSP(0xC6);
 WriteDSP(0x00);
 SendLength();
}

void SB16::Old8Mono()
{ WriteDSP(DSP_TIME_CONSTANT);
  WriteDSP(HiByte(OldTimeConstant()));//use only high byte
  WriteDSP(0x48);
  SendLength();
  WriteDSP(0x1C);
}

void SB16::Auto8Stereo()
{ SendFrequency();
  WriteDSP(0xC6);
  WriteDSP(0x20);
  SendLength();
}

void SB16::Auto16Mono()
{ SendFrequency();
  WriteDSP(0xB6);
  WriteDSP(0x00);
  SendLength();
}

void SB16::Auto16Stereo()
{ SendFrequency();
  WriteDSP(0xB6);
  WriteDSP(0x20);
  SendLength();
}

Just as in Single Cycle Transfers, we are sending the frequency, followed by the command for 8 or 16 bit, the command for Mono or Stereo and finally the length of the transfer. Almost done! Immediately after this function is called, we are going to be IN Auto Initialization Mode, so the transfer will immediately start!! The Old8Mono function simply sends the Time Constant according, sends the DSP the command 0x48 to tell it we are about to send the buffer length, and finally we write 0x1c to get into Auto-Initialization Mode the Old Fashioned way!

4. Acknowledge the Interrupt


If you forgot what our ISR looked like, here it is!

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

We disable interrupts while ours is running, call the appropriate ISR, enable interrupts again, and tell the PIC we are all done! Let's take a look at ServiceAI!

void SB16::ServiceAI()
 { if(ModeBits == 8)
    { inp(DSPStatus);
    }
   else
    { outp(MixerAddr,0x82);
      short temp=0;
      temp=inp(MixerData);
      if(temp & 2)
       { inp(DSPIntAck);
       }
    }
   FillHalfBuffer();
 }

Here we acknowledge the interrupt by either 1. doing an inport on the DSPStatus port if it's an 8 bit transfer or reading a WORD from the MixerData port, test if bit 2 is set, and then do an inport on the DSP Interrupt Acknowledge Port which we define in tutorial #1. Finally we run FillHalfBuffer which fills the first or second half of our DMA buffer!

void SB16::FillHalfBuffer()
{ unsigned char *d=(unsigned char*)dma.MK_FP(dma.phys>>4,0);
  unsigned char *s = Sounds[0].Sound;
  SideToFill^=1;
 
  if(SideToFill)
   {d+=HALFBUFFSIZE;
   }

  if(!Done)
  {s+=place;

    if(SoundSize < HALFBUFFSIZE)
     {MemMove(d,s,SoundSize);
      memset((unsigned char *)d+amount,128,HALFBUFFSIZE-SoundSize);
      WriteDSP(DSP_HALT_8_AUTO_INIT);
     }
   else
     {MemMove(d,s,HALFBUFFSIZE);
      SoundSize-=HALFBUFFSIZE;
      place+=HALFBUFFSIZE;
    }
  }
}

This is the tricky little function that fills only one half of the dma buffer. If we go through it section by section, it'll be really easy to read. We first define d and s as the destination (our DMA buffer) and the source (the sample). We then set SideToFill to the opposite of what it was. It will turn to 0 if it was a 1 and turn to 1 if it was a 0! We then determine which half of the buffer we will be filling and if it's going to be the second half, we increase out pointer by HALFBUFFSIZE so it will be pointing to the middle of the buffer, right in place! We then increment place by HALFBUFFSIZE. This variable represents how far into the sound we are. We then determine if the remainder of the sound is larger or smaller than the half of the DMA buffer we are working with. This is important because if we only need to play 5 more bytes and we try to fill in HALFBUFFSIZE, then we will be filling in garbage! We want to play only the sample, then fill in the rest with silence. The else of that if statement moves the amount normally since we've got more than HALFBUFFSIZE to play. The main part of the if moves SoundSize to the DMA buffer. We then set the remaining portion to 0 voltage or 128 in our data type of unsigned char. We then tell the DSP to STOP processing. The DSP will then do 1 more interrupt where the last part of the sample is finally played!

void SB16::MemMove(unsigned char* dest,unsigned char*source,unsigned int length)
{length/=4;// or length>>=2
asm("rep
     movsl"
       : : "D" (dest), "S" (source), "c" (length) : "%esi","%edi","%ecx");
}

Finally here is a new function in inline assembly that will move memory from one place to another in a small attempt to save some cycles :)

There you have it! Auto-Initialization DMA Mode is ideal for flawless playback. In my game, I put the DSP into Auto-Init Mode right away and it remains in that mode for the entire game until it exits. While running it sees if any sounds need to be played wether because of keystrokes or certain events, and then it mixes them together and sends them off to the DMA buffer for seamless sound! Read this tutorial more than once and then take a look at the whole source code that you can download so you REALLY understand it. After that move onto RealTime Sound FX Mixing to learn how the PROS do it!! If you have any questions, comments, or some tips on how i can improve this page, please send me some Feedback!