RealTime Sound FX Mixing Structures
Example 7


	    Mission :Let's change a little of how we fill in our DMA buffer and achieve seamless
                   Realtime Sound FX Mixing! 
                    Download the Expansion Pack!



Intro


In the RealTime SoundFX Mixing tutorial we learned how we can mix two or more sound together. That's all fine and dandy, but we need to develop some code that will store a list of sounds with vital mixing information and also some routines that will use this information to correctly fill the DMA buffer.

The Idea


To start our routines we have to develop a way to save the request of more than 1 sound when it wants to be played. I say "Wants to be played" in the sence that certain program events will trigger the playing of these sounds. At that time, the sound "wants to be played :)"To conquer this we will create a double linked list of sound structures. When a sounds wants to be played we will add a sound structure to the end of this list with the appropriate data filled in. Once the sound is done playing then we will erase it from the list. As a configuration parameter we can limit the size of the list to speed up game speed. I've found that mixing 45 sounds together becomes a little CPU intensive :). Let's start getting into the code that makes it happen!

The Code


typedef struct SampleHeader
 {unsigned long Position;    
  unsigned long Length;      
  unsigned short SoundNumber; 
  SampleHeader *Next;
  SampleHeader *Previous;
  SampleHeader()
  {Position=Length=SoundNumber=0;
   Next=Previous=NULL;
  }
 }Sampler,*Sample_ptr;

Here's the structure that holds exactly 1 sound request. Position will be an index into the sound that represents how much of the sound has already been played. Length is the sound's overall length. SoundNumber is the index into our list of sounds, so we play the right one. Finally we have Next and Previous which give us the ability to use a double linked list. If we want to add a sound request we allocate a new Sampler instance using the Next pointer at the end of our list, so it kind of acts like a que, first in first out, length depending.
Now that we know what our sound structure is going to look like, lets go on to the function that adds a sound request to the double linked list.

void SB16::Play
(unsigned int sound_num) 
{SampleTail->Next = new SampleHeader();
 SampleTail->Next->Previous=SampleTail; 
 SampleTail->Next->Length= Sounds[sound_num].Length; 
 SampleTail->Next->SoundNumber = sound_num; 
 SampleTail = SampleTail->Next; 
}

If you are familiar with double linked lists then this function requires little explanation. But for the newbies here it is. We have a member of the SB16 class called SampleTail. This is a pointer to a dynamically allocated instance of the SampleHeader structure which represents the last element in our double linked list. We first allocate a new instance of the SampleHeader structure using the Next of SampleTail to point to it. We use Next->Previous (since we just allocated it we can do this) to assign the previous pointer. While we are at it let's save the sound length and sound number. Notice that all of them are using the Next-> pointer. Lastly set the SampleTail pointer to the newly allocated instance! Now that we have access to a variable list of sound requests, let's create a new function that will use this list instead of just 1 sound, to fill in the DMA buffer!

void SB16::FillBuffer()
{unsigned short i;
 unsigned char *start=(unsigned char*)dma.MK_FP(dma.SegInfo.rm_segment,0);

 SideToFill^=1;
 if(SideToFill)
   { start+=HALFBUFFSIZE;
   }
 for(i=0;i<HALFBUFFSIZE;i++,start++)
  {*start=MixSamples()+128;
  }
}

This routine is called by the SB16 interrupt instead of FillHalfBuffer which was used in the last tutorial. First, we declare start to point to the beginning of our DMA buffer. Secondly, we change SideToFill to the other half of the DMA buffer. I use the variable SideToFill as a flag to tell us which half of the DMA buffer we will be filling. The for loop goes through half of the DMA buffer setting each element equal to whatever MixSamples returns.

char SB16::MixSamples()
{long Total=0,SoundCount;
 CurrentSample = SampleHead;

 if(CurrentSample == SampleTail)
 {return 0; //silence.....
 } 
 CurrentSample = CurrentSample->Next; //go 1 past head nod
 
// there is at least 1 sound needed to play
 while(CurrentSample != NULL) 
  {
   if (CurrentSample->Position == CurrentSample->Length )
    {FrontLink = CurrentSample->Previous; 
      EndLink = CurrentSample->Next;
      if(EndLink == NULL) 
        {//this is the last item in the list 
         FrontLink->Next = NULL; 
         delete CurrentSample; 
         CurrentSample = NULL; //trigger exit 
         SampleTail = FrontLink; //only true if last item. 
        } 
      else 
        {FrontLink->Next = EndLink;
         EndLink->Previous = FrontLink; 
         delete CurrentSample; 
         CurrentSample = EndLink; 
        } 
     } 
    else
     {if(SoundCount == MaxNumberToMix)
      { return ClipChar(Total);
      }
      Total+=GetSample(CurrentSample);
      CurrentSample=CurrentSample->Next;
      SoundCount++;
     }
   } 
 } 
 return ClipChar(Total);
}

Here's the mother of all functions! Lets go through it section by section. All this monster is responsible for, is grabbing one sample from each of the sounds that want to be played, mixing them, and sending them off into the DMA buffer. We first declare Total and SoundCount. We will be using Total to add up all our samples and SoundCount will keep track of how many sounds we will be mixing at a time. This way if we want to limit the number of sounds mixed at once, like we are now with MaxNumberToMix, we can! To traverse through our double linked list, we must start at the beginning, so lets initialize CurrentSample to SampleHead. Before we go mixing everything, let's make sure that we even have any sounds that want to be mixed! If CurrentSample equals SampleTail, then we know that we are out of luck, no mixing is needed right now, so lets just return silence which is 0 for char. Remember that even though we are using unsigned char samples, our mixing routine must convert them to signed so the mixing works correctly. If we make it past the first if, we know that there is AT LEAST 1 sound needed to be played, so we advance to the next structure in the list by doing CurrentSample=CurrentSample->Next, which will be the 1st sound.

We now enter the while loop that will traverse through the list until it hits the last sample, where the Next pointer will equal NULL. We must now make another check before mixing. What if the sound just got done returning its last sample? In that case it has already been completely played, so we should remove it from the list. The if statement checks for this by seeing if its position is equal to its length. To remove it, we must see if it has any samples after it that still need to be played, or if its the last sample in the list. If its endlink equals NULL, we know that it is the last one. Here's where FrontLink and EndLink come into use. Their only purpose is to save the pointers to the left and right of the sample that needs to be removed. If it IS the last one, then the EndLink will be NULL and we set FrontLink accordingly. The else of the if, is pretty much the same idea, and is true when the sample is in the middle of the list.

Under the if that is in yellow, the else is used if the sample still hasn't been completely played. We first check to see if we've reached our maximum number of sounds to be mixed at a time, and if so, return our mixed sample. Please keep in mind that this tutorial covers Mixing Method #2 under RealTime Sound FX Mixing. If we get past that, then we know for CERTAIN that we have to add the sample into the mix, so add the sample to the Total, proceed to the next sample, and add 1 to our SoundCount. If we don't end up mixing our maximum number, we will eventually end up at the end of our linked list, where we will finally return the mixed sample by returning ClipChar(Total)!!
Now THAT was a lot of explaining!! Notice that we have 2 functions that haven't been covered yet, but have NO fear, they will seem pretty pathetic compared to this one!

char SB16::GetSample(Sample_ptr S)
{ S->Position++;
  return Sounds[S->SoundNumber].Sound[S->Position-1]-128;
}

char SB16::ClipChar(long num)
{
 if(num > 127)
  {num = 127;
  }
 if(num < -128)
  {num = -128;
  }
 return (char)num;
}

GetSample simply returns the element of the sound at the position where we are at. Notice that we are returning the sample-128. Here is where we convert it to SIGNED char so our mixing will work. Finally ClipChar turns our long into a usable char that we can shove into our DMA buffer!!

NOTE: This method is very fast and is extremely easy to use. Like any method it DOES have some faults. By simply putting a cap on the data type as we are in the ClipChar function, if we mix certain sounds together at inopportune times, we will end up maxing out or data type for an extended number of cycles. This will result in Distortions, although usually small ones. Two ways around this are to soften your sounds to take up less of the data type or to limit the number of sounds you will be mixing at once.

This code is very flexible, and very easy to understand and use. The big cycle eating comes into play when we start mixing the sounds and doing the compares. I know some that use lookup tables to do the mixing faster, but for me, this code works just fine! It sure wouldn't hurt to convert it into inline assembly though! Thanks for reading! If you have any comments, questions, rude remarks, need to pass gas whatever...give me some feedback!!