Multiple, simultaneous playback of one sound

Hello again.



Long story short:



Each player in my game could potentially fire their weapon at the same time. Currently, I have one sound bound to the firing event, and I update the position of the sound to the point of fire before triggering the event.



The problem is obvious: if everyone fires their gun at just about the same time, the sound is moved to the last position to be processed. Stranger behavior happens if a new fire event happens mid-shot of another. The sound "jumps" to the new position before it finishes playing.



The solution (at least what I have thought of so far):

Make a copy of the sound that I have for each player and update their respective sound on their shot.



The questions:

  1. Is this a logical/common solution to this problem?
  2. How do I handle the binding of events using the SoundPool? I’ve attempted the above solution, and the problem seems to persist (the sound jumping around). Wouldn’t the hashing function try to bind all these different sounds to the same, common resource?
  3. How have you guys handled this scenario in your projects?



    Thanks.

In Dirt I built my own SoundManager which boils down to an array of ProgrammableSound objects all attached to a single SoundNode. Adding a new sound to the manager binds it to all array items (which is fine because it’s only binding an id, the actual sound is only stored once.) Then when a sound event occurs, a non-busy ProgrammableSound (or channel I’m calling it) is found and fired off at the necessary position. Depending on how many array positions you use (I use 8…) you can get the same sound going at the same time in many positions.

Cool. Here’s what I have so far:



import java.net.URL;

import com.jme.sound.SoundPool;
import com.jme.sound.scene.ProgrammableSound;
import com.jme.sound.scene.SoundNode;

public class SoundManager {
   
   public SoundNode soundNode;
   
   public int numberOfChannels;
   public ProgrammableSound[] channel;

   public SoundManager(int numberOfChannels) {
      this.numberOfChannels = numberOfChannels;
      
      soundNode = new SoundNode();
      
      channel = new ProgrammableSound[numberOfChannels];
      
      for(int i = 0; i < numberOfChannels; i++) {
         channel[i] = new ProgrammableSound();
         soundNode.attachChild(channel[i]);
      }
   }
   
   public void loadSound(URL soundURL, int globalEventID) {
      URL[] array = new URL[1];
      array[0] = soundURL;
      loadSound(array, globalEventID);
   }
   
   public void loadSound(URL soundURL[], int globalEventID) {
      int soundEventID = SoundPool.compile(soundURL);
      bindEvent(soundEventID, globalEventID);
   }
   
   
   public void bindEvent(int soundEventID, int globalEventID) {
      for(int i = 0; i < numberOfChannels; i++) {
         channel[i].bindEvent(soundEventID, globalEventID);
      }
   }
   
   public ProgrammableSound getChannel() {
      ProgrammableSound returnSound = null;
      for(int i = 0; i < numberOfChannels; i++) {
         if(!channel[i].isPlaying()) {
            return channel[i];
         }
      }
      return returnSound;
   }

}




So now I have another question:

Can I have multiple SoundNode's in my scene? I'd like to have one that uses your idea for "dynamic" sounds and one for constant sounds (background music, ambient noise). So in my simple update, I'd have something like this:



protected void simpleRender() {
      SoundAPIController.getRenderer().draw(resourcePool.STATIC_SOUNDNODE);
      SoundAPIController.getRenderer().draw(resourcePool.DYNAMIC_SOUNDNODE);
   }


protected void simpleUpdate(){
resourcePool.STATIC_SOUNDNODE.updateGeometricState(time, true);
      resourcePool.DYNAMIC_SOUNDNODE.updateGeometricState(time, true);
   }



Thanks for the great idea.

Glad I could be of help, that’s why we’re here.



I don’t see why you couldn’t have multiple SoundNodes in theory… I haven’t tried it myself though. Let me know how that goes as it’s also an interesting idea.

I’ve had some similar issues as described in #3 that I’m not sure are completely gone… In fact, it seems like they got worse with the newer lwjgl, but that could be my imagination…



When I set up my channels I do this to each:


    for (int i = 0; i < MAX_CHANNELS; i++) {
      m_soundBank[i] = new ProgrammableSound();
      m_soundBank[i].setAllowInterrupt(false);

      m_soundBank[i].setGain(VOLUME_MULT*DirtConfig.getInstance().getSfxVolume());
      m_soundBank[i].setLooping(false);
      m_soundBank[i].setMaxDistance(2000);
      m_soundBank[i].setRolloffFactor(.01f);

      m_sNode.attachChild(m_soundBank[i]);
    }



Also, for 1 & 2, how do you execute the sound? I do: (where getOpenChannel is almost identical to yours...)

  public void fireEvent(int eventid, com.jme.math.Vector3f position) {
    ProgrammableSound progSound = getOpenChannel();
    if (progSound == null) {
      System.err.println("Couldn't play sound!!");
      return;
    }
    progSound.setPosition(position);
    m_sNode.onEvent(eventid);
  }


  public void fireEvent(int eventid, com.jme.math.Vector3f position) {
    ProgrammableSound progSound = getOpenChannel();
    if (progSound == null) {
      System.err.println("Couldn't play sound!!");
      return;
    }
    progSound.setPosition(position);
    m_sNode.onEvent(eventid);
  }



This is almost identical to what I am doing. The only difference is that I do these calls...

m_soundBank[i].setGain(VOLUME_MULT*DirtConfig.getInstance().getSfxVolume());
      m_soundBank[i].setLooping(false);
      m_soundBank[i].setMaxDistance(2000);
      m_soundBank[i].setRolloffFactor(.01f);



...when I get the channel, although your method is probably what I should do given that all the sounds will probably have the same characteristics. If you are not noticing any of the artifacts that I am, this could be the source of my problems.

As for #3, let me know if you figure it out and I'll do the same for you. :)

Sure thing!

I don’t know why I think of this before…



We attach all these sounds to one sound node. We bind our event ID’s to all the sounds. But when we call onEvent(…), we are not specifying which of the sounds we are supposed to use. We can only call onEvent on the sound node that the sounds are attached to. The only thing we are doing is updating the position of 1 of X sounds, but we are still playing ALL the sounds, correct?

When you call onEvent() you are passing the id of the sound you want to play…



Hmm… You know, one difference in our sound managers is that in mine, I generate and store a new int id for each unique sound and return that id to the caller so they know what to pass in when they call fireEvent. I notice you call your id "globalEventID"… is that perhaps because you are using one single int for everything? That would not be good.

Ah, so I could bind a global int + an offset proportional to the number of channels to make a unique ID for each sound in a programmable sound object. Groovy. Thanks for the insight.

erm, I think that’s right if I understand you correctly. For example, if I had a sound “boom.ogg” assigned to my shooting action, I’d pass that url to my manager and it would return me a single id representing “boom.ogg” Whenever I wanted to hear “boom.ogg” i call the manager and pass it that unique int.



All of my programmable sounds are set up inside the manager with that same int for boom.ogg. Another sound added to the manager would result in a single brand new int generated and setup on all channels.



Maybe I should just post my code rather than jabbering on and on. hehe.

Hmm. Now I am confused as to how you are making a single sound play versus all of them.



When you pass in the ID that your manager returns to you, you are calling soundNode.onEvent(ID) correct? Since all sounds have that ID bound to it, why wouldn’t they all play that sound?



How are you making a specific channel play that sound since there is no way to directly call that channel play?



In any case, my solution did work.



I don’t have my code available right now so I’ll just have to describe it.



When I create my SoundManager, I give it the number of channels I wish to have (in this case ‘8’). I then wrote a method, loadSound(URL, globalID).



The method first compiles the sound to get the SoundManager’s ID for that sound. Then, it binds that returned ID (call this programID) to each sound in the following way:



for(int i = 0; i < numberOfChannels; i++) {
  channel[i].bindEvent(i*numberOfChannels + globalID, programID);
}



When I want to play a sound, I get the index of the open channel, and then call SoundNode.onEvent(openChannelIndex*numberOfChannels + globalID) where globalID is the ID of the sound I want to play. In this way, only the open channel plays the sound associated with globalID instead of all of the channels attached to the soundNode.

An example: I have two sounds with the following global ID's

public static final int SOUND_BLASTER = 1;
public static final int SOUND_SUPER_BLASTER = 2;

If I have 8 channels, then the event ID returned by the sound compiler for SOUND_BLASTER gets bound to:

Channel 0: 1 ( 0*8 + 1 = 1 )
Channel 1: 9 ( 1*8 + 1 = 9 )
Channel 2: 17 ( 2*8 + 1 = 17 )
...
Channel 7: 57

For SOUND_SUPER_BLASTER:

Channel 0: 2 ( 0*8 + 2 = 2 )
Channel 1: 10 ( 1*8 + 2 = 10 )
Channel 2: 18 ( 2*8 + 2 = 18 )
...
Channel 7: 58


Then I use the same function when I want to play the sound. I hope that makes sense.
"captK" wrote:
Hmm. Now I am confused as to how you are making a single sound play versus all of them.

When you pass in the ID that your manager returns to you, you are calling soundNode.onEvent(ID) correct? Since all sounds have that ID bound to it, why wouldn't they all play that sound?

How are you making a specific channel play that sound since there is no way to directly call that channel play?

Hmm, I see your point... oddly though, things seem to work OK... maybe that's because I'm not really taxing the system yet so the issue is not all that apparent? Anyhow, your solution or something similar looks like the way to go.

Hi,



Whats the status of this? Did you get it working?



Have I understood this correctly? You’ve got a SoundManager which has a loadSound(URL), which loads and stores a sound from a given URL, and returns an ID based on the URL.



Then, in your code, you call playSound(ID, POSITION) which plays the sound through an empty “channel”?



/Per

Could this be assimilated into CVS ? :slight_smile:

This is great!



I also think this class should be included in jME, since it’s really not application specific.



Thank you



/Per

I’m glad I could help guys. Everyone on this project has my greatest respect; I’m honored that you want to put this in the project.



If it is going to go in there, someone in charge needs to decide where it fits in the current structure, if the name is appropriate, etc. Also, there is a System.err.println comment that would need to be changed to a logging message. The licensing/copyright header needs to be added. I’ll make these changes, document the class, and add author and contribution tags as well.



Let me know, and thanks again.

:smiley:

Document it and then post it on code review. We’ll get it into the base line after that. :slight_smile:

Here is a link to the code review page: http://www.mojomonkeycoding.com/jmeforum/viewtopic.php?t=972



I’ve added documentation to what I think needed to be documented, changed some variable types to private, added the copyright header, and made the System.err.println into a logging message.



Right now, the package declaration places it in the sound directory, but the final decesion lays with you guys, of cource. Thanks again :smiley: .