Realtime Special Sound Fx

Mission :Let’s change a little more of our source code to create Realtime Sound FX!!

Intro

Well, it’s finally here. A tutorial on Realtime Sound FX!! So far I only have one to divulge, but more are to come some time in the future. Realtime Sound FX make a good game unbelievable! As more games come out with unbelievable graphics, it takes a considerable amount of time and energy to top the previous release. I think the next wave of game development that will astonish us will be Realtime Sound FX. A lot of games have really great sounds, but it would be nice if we could add environmental effects into our game using these sounds as a basis. We can already see some manufacturers creating specialized hardware for 3d sound, next to come will be sound accelerators that will allow us to modify a sound in real-time and mix it in a stream of output. Sound far fetched? I don’t think so :)

Phase Shift Delay

First off, I just made up the name for this effect. Call it what you will, but as you will find out, it really suits the effect it gives us. As with the 3d sound method, we will put the sound card into Stereo mode and use Mono samples. This lets us have complete control over the output going to each speaker. We will begin the sound immediately after it wants to be played in the left speaker, but the output will be delayed a fraction of a second in the right speaker. The actual time of the delay is sample dependent. MEANING that the delay is supplied in the number of samples. Let’s say we specify 200 samples. That time will be.0045351 seconds in a 44.1khz mode and .0090702 seconds in a 20.05khz mode. The maximum sample delay is defined as PSDMAX and must be a power of 2 (2,4,8,16,32,64,128,256,512,1024…).

Not only did I add this effect, but I also did a little cleaning of the code so that it made a little more sense. For instance, our pointer naming scheme was a little awkward. We notice in the last tutorial that when 3d sound was enabled that function pointers changed jobs, the GetSample pointer turned into the GrabSample pointer. Let’s take a quick look at the changes we made to the header file!

  1. void SB16::SetPtrFunctions()
  2. {
  3.     GetSamplePSDptr = &SB16::GetSamplePSD;
  4.     if(ModeBits==16)
  5.     {
  6.         MixingFunction=&SB16::FillBuffer16;
  7.         GetSample=&SB16::GetSample16;
  8.         if(Sound3d)
  9.         {
  10.             if(ModeStereoMono)
  11.             {
  12.                 GrabSample=&SB16::Get3dSampleStereo; //3d 16 Stereo
  13.             }
  14.             else
  15.             {
  16.                 GrabSample=&SB16::Get3dSampleMono;   //3d 16 Mono
  17.             }
  18.         }
  19.     }
  20.     else
  21.     {
  22.         MixingFunction=&SB16::FillBuffer8; //default 8 bit mixing scheme
  23.         GetSample=&SB16::GetSample8;
  24.         if(Sound3d)
  25.         {
  26.             if(ModeStereoMono)
  27.             {
  28.                 GrabSample=&SB16::Get3dSampleStereo; //3d 8 Stereo
  29.             }
  30.             else
  31.             {
  32.                 GrabSample=&SB16::Get3dSampleMono;   //3d 8 Mono
  33.             }
  34.         }
  35.     }
  36. }

Prior when we were using 3d sound, the GetSample pointer changed functions, it was set to the Get3dSample groups. NOW we’ve changed it so that no matter what, we will always know that GetSample will always retrieve data directly from the sample with no effects or changes. Likewise the 3d mixing routines use GrabSample now, but in reality the functionality stays the same, just some name changes. Again, just a minute change to another function.

  1. long SB16::MixSamples()
  2. {
  3.     //go through linked list and average the outgoing signal
  4.     long Total=0,SoundCount;
  5.     CurrentSample = SampleHead;
  6.  
  7.     if(CurrentSample == SampleTail)
  8.     {
  9.         return 0; //silence…..
  10.     }
  11.     CurrentSample = CurrentSample->Next;
  12.    
  13.     // there is at least 1 sound needed to play
  14.     while(CurrentSample != NULL)
  15.     {
  16.         if(CurrentSample->Position == CurrentSample->Length )
  17.         {
  18.             FrontLink = CurrentSample->Previous;
  19.             EndLink = CurrentSample->Next;
  20.    
  21.             if(EndLink == NULL)
  22.             {
  23.                 //this is the last item in the list
  24.                 FrontLink->Next = NULL;
  25.                 delete CurrentSample;
  26.                 CurrentSample = NULL; //trigger exit
  27.                 SampleTail = FrontLink; //only true if last item.
  28.             }
  29.             else
  30.             {
  31.                 FrontLink->Next = EndLink;
  32.                 EndLink->Previous = FrontLink;
  33.                 delete CurrentSample;
  34.                 CurrentSample = EndLink;
  35.             }
  36.         }
  37.         else
  38.         {
  39.             if(SoundCount == MaxNumberToMix)
  40.             {
  41.                 return Total;
  42.             }
  43.            
  44.             Total+=(this->*CurrentSample->MixingRoutine)(CurrentSample);
  45.             CurrentSample=CurrentSample->Next;
  46.             SoundCount++;
  47.         }
  48.     }
  49.     return Total;
  50. }

The only change we made here was what function we call to retrieve a sample from CurrentSample. Notice that we are now using the function contained in the SampleHeader structure, MixingRoutine. This should have been set accurately when we called the Play function. Let’s cover another really simple function before diving into the elusive new mixing function!

  1. void SB16::Play(int sound_num,long PSD)
  2. {
  3.     SampleTail->Next = new SampleHeader();
  4.     SampleTail->Next->Previous=SampleTail;
  5.     SampleTail->Next->Next = NULL;
  6.     SampleTail->Next->Position=0;
  7.     SampleTail->Next->Length = Sounds[sound_num].Length+PSD;
  8.     SampleTail->Next->SoundNumber = sound_num;
  9.     SampleTail->Next->PSD=PSD;
  10.     SampleTail->Next->MixingRoutine=(this->*&SB16::GetSamplePSDptr);
  11.     SampleTail = SampleTail->Next;
  12. }

Here we define a new Play function that accepts a long for our PSD number. Since we have PSDMax set to 16k we really don’t need a long, but this way if you change the PSDMax number to larger than 65k (heaven forbid!) you won’t need to change the function! We assign all the elements the usual way, but notice the parts in white. We assign the new variable PSD with the PSD passed to our function. Notice that we are setting Length to the length PLUS our PSD, more on this later! We also set MixingRoutine to a function pointer that was set to the GetSamplePSD function when we called SetPtrFunctions. You should be asking that since we KNOW that we are using the Phase Shift Delay effect, why not just point directly to the GetSamplePSD function?! Well, it seems GCC 2.8.1 doesn’t want to let us!! I tried that and it told me
“sorry, not implemented, address of bound pointer to member expression”.

In order to work around this, I had to declare our function pointer that pointed to GetSamplePSD and then set our member pointer (MixingRoutine) to that! Oddball, yes, but hey it works! We know that if we use this Play function, that our PSD value will be set, AND that this sound will use the GetSamplePSD function in the MixSamples function! This way if we wanted another sound to play simultaneously and NOT use a PSD, we could! Finally, the function everyone is talking about!

  1. long SB16::GetSamplePSD(Sample_ptr S)
  2. {
  3.     long sample=0;
  4.  
  5.     if(LeftChannel)
  6.     {
  7.       // length is acutally length-psd, so should be attempt at new sample?
  8.         if(S->PSDCounter < ((S->Length>>1)-S->PSD))
  9.         {
  10.             sample=(this->*GetSample)(S);
  11.         }
  12.         else
  13.         {
  14.             S->SampleList[S->PSDCounter&(PSDMAX-1)]=0; //return 0 on other side!
  15.             return 0;
  16.         }
  17.    
  18.         //MARKER
  19.         S->SampleList[S->PSDCounter&(PSDMAX-1)]=sample<b><<16;</b>
  20.         return sample<b><<16;</b>
  21.     }
  22.     else
  23.     {
  24.         //don’t need to see if position-psd is positive because a negative number
  25.         //raps over to the end of our buffer since we are and’ing PSDMAX-1.  since we 
  26.         //initialized all the buffer to 0, we could care less!!
  27.         S->PSDCounter++;
  28.         return S->SampleList[((S->PSDCounter-1)-S->PSD)&(PSDMAX-1)];
  29.     }
  30.  
  31. }

Ok, this function is going to be called in the MixSamples so whatever we return will go directly in our DMA buffer, mixed with the other sounds that need to be played at the time of course! The whole routine is inside an if that test wether we are programming the Left channel or the right. If LeftChannel is non-zero, then we ARE shooting for the left channel, 0 for the right. As in our 3d routines we will be programming both channels the first pass (LeftChannel !=0), and storing the results for the right speaker. Instead of just storing one sample we need to store all the samples we are delaying(ie PSD= bufferlength). Remember that in our structure we created our temp buffer to be exactly PSDMAX in size and initialized it to all 0′s. Even if we use, say 10 for our PSD, the buffer is still going to be PSDMAX all the time! Ok, before I really confuse you, let’s move on in the code. Inside the if we test to see if we need to fetch a sample from our sample in memory. Notice that we are comparing our counter PSDCounter to Length-PSD. Remember in our Play function that we set Length to Length + PSD. The reason for this is that our master mixing function (MixSamples) determines when a sound can be deleted from the list when the Position is equal to its Length. Since we are delaying a side, if we didn’t extend the length, we would never hear all of the delayed output, it would be clipped. If we don’t need to get another sample, this means that the original sound has completely played out the left speaker, and now we are just waiting for the right to finish. We need to fill in our SampleList with a 0 so that we don’t hear repeated sound in the right speaker when it is finished. Outside the if (where MARKER is ) we store the sample for the right side. If we reach MARKER, we know that first the left side is still playing and that we need to keep track of the samples for the right speaker. The else after that simply returns the sample that belongs in the right speaker.

You should be asking yourself where does the delay actually come into play?! Well, remember that PSDCounter is being incremented every time the right channel is programmed. Also notice that the right channel is indexing PSDCounter-PSD!!! This should be somewhat alarming because when we first start off, PSDCounter will be LESS than PSD and will give us a Negative index!!? Well, that would certainly be the case if we didn’t program our routine with that in mind. We do a logical AND to that expression effectively rapping the value to the end of the buffer. And index of -1 will result in an actual index of 16383. Another possible problem is if the right side actually starts playing the back side of our buffer, won’t it be garble?! Well, remember that we initialized it to all 0′s so it will play nothing but silence until it starts into the real data! Ha! So in all actuality our buffer is acting somewhat like a que. When the left channel is being programmed, it is PSD samples ahead and remember that value is being AND’d to rap around as well. This is why we have a maximum of 16383 for a PSD or PSDMAX-1, otherwise the buffer would overwrite itself. In all actuality this isn’t possible because of genius scheme of ANDing. Whew, that was a lot to say! One more point before closing!

Notice that when we are returning our samples we are returning the value shifted 16 bits to the left (sample<<16). This is actually a hard-coded routine to have 3dSound enabled. Remember from our prior tutorial that the MixSamples routine uses a 16:16 fixed point mixing buffer. The bit shifts align them to 16:16. This raises an interesting problem. Now in order to really increase the versatility of our routines, we need to develop a solid system of functions that will convert the samples into one format so that we don’t have to waste time with if’s that slow down our routines and even worse, make us hardcode and to make assumptions. I hope you get my drift.

That’s all there is to it! I told you this effect was really easy!! Get the Expansion pack and mess around with the final executable and you’ll see that this effect, while being very simple, can make some really cool effects!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>