Best way to get multiple instances of same sound effect?

I have about 400 guys running around on my little planet. I want to fire a water splashing sound effect for every guy who is running into the sea. So I need multiple instances of the splashing sound effect.

If I am not mistaking, playInstance is not helpful here because the effects need to be spatial.
I tried cloning the AudioNode and playing whatever clone is free at the moment.

And I tried loading multiple AudioNodes with the same WAV.

Both work, but in both cases there is a crackling when a number of them are playing. The crackling gets worse the higher the number of playing nodes get.

What is the best way to do this? I also need to play a lot of footsteps, thumping noises and roars…

2 Likes

Edit: tl;dr: sound design is hard.

I don’t think this is true. playInstance() just kicks off a sound in a way that you can no longer control it… it’s a separate one-off instance. It will inherit whatever properties the AudioNode has… whatever that may be.

But you are doing to run into other problems.

First, there is a limit to how many sounds can play at once. It might be as low as 16… I don’t remember. It’s low enough that I’ve hit it before and had to adjust.

Second, the audio quality of playing lots of the same sound at the same volume (not spatial, right?), same pitch, etc… is going to sound awful. There is no magic here… whatever audio data is playing is mixed together and goes out two channels (left+right). Artifacts are going to be multiplied quickly and they are going to be even more obvious when the sounds are exactly the same. This is the physical limitation of the way audio is processed.

That crackling is likely caused by a thousand artifacts lined up nicely to actually hear them. If you are playing 16 different sounds at once, you may get some artifacts from the math but they are going to be rare and irregular enough that they won’t produce an audible effect. When you have the same sound, closely overlapping, every overlapping peak, etc. will clip and line up nicely to produce audible ‘errors’.

You could achieve the same thing in a high-end digital audio workstation by cutting and pasting the same audio source 16 times and varying the start time just slightly.

This is why no game on the planet does sound effects this way. Usually, you will get 8-10 effects that are closest to the ‘ears’ of the player… and often some amount of randomization to be sure that if the same two sounds are playing, they are at slightly different pitch or have been timed far enough apart not to matter.

Presumably, in your case you are trying to hear the whole planet at once regardless of distance… so you will need to think of the “whole planet” as a sound source. So it’s not 100 little feet making splashes… it’s a planet with one set of feet making splashes… then a planet with 1-10 feet making splashes… then a planet with 10-100 feet making splashes… and create three separate sound effects for those cases. (or whatever demarcation seems appropriate).

Even if your sounds really are spatial, that’s still the best bet for potentially dozens (hundreds?) of the same sound playing. Create a sound for 1 character in the water, create a sound for 10 characters in the water, create a sound for 100 characters in the water… then based on how many splashing characters are within ear shot, pick one of those to play.

10 Likes

Thanks for this interesting theoretical explanation. The suggestion you end with is very interesting, I already have a system in place that mixes the music with either ambient sea, forest or mountain sound. So I will build on that and see how that will turn out.

1 Like

Just to share the solution I came up with:

public class SoundCube {
    
    AudioNode[][][] scNodes;
    AudioNode[]     prioList;
    
    int dimCount1,dimCount2,dimCount3;
    int totalCount;
    int maxConcurrent;
    
    float weightMatrix[][];      
    int countMatrix[][];
    
    public SoundCube(Main _app, String[] _dim1 , String[] _dim2, int _maxConcurrent){
        // How many guys are making the same noise?
        String[] _dim3= {"one","few","many"};
        
        dimCount1=_dim1.length;
        dimCount2=_dim2.length;
        dimCount3=_dim3.length;
        totalCount=dimCount1*dimCount2*dimCount3;

        maxConcurrent=_maxConcurrent;
        
        scNodes=new AudioNode[dimCount1][dimCount2][dimCount3];
        weightMatrix=new float[dimCount1][dimCount2];
        countMatrix =new int[dimCount1][dimCount2];
        prioList=new AudioNode[totalCount];

        for (int i=0;i<dimCount1;i++){
            for (int j=0;j<dimCount2;j++){
                weightMatrix[i][j]=0;
                countMatrix[i][j] =0;
                for (int k=0;k<dimCount3;k++){
                    AudioNode sf = new AudioNode(_app.getAssetManager(), 
                            "Sounds/"+_dim1[i]+"_"+_dim2[j]+"_"+_dim3[k]+".ogg", 
                            AudioData.DataType.Buffer);
                    sf.setPositional(false);
                    sf.setLooping(true);
                    sf.setVolume(.9f);
                    _app.getRootNode().attachChild(sf);            
                    scNodes[i][j][k]=sf;                      
                }            
            }            
        }
    }
    
    public void update(float _masterVolume){
        float max=0;
        int count=0;

        // Select the sounds to play and calc their volume 
        for (int i=0;i<dimCount1;i++){
            for (int j=0;j<dimCount2;j++){
                int score=countMatrix[i][j];
               
                if (score>0){
                    if (score<3){
                        prioList[count]=scNodes[i][j][0];

                        scNodes[i][j][1].stop();
                        scNodes[i][j][2].stop();
                    }
                    else{
                        if (score>7){
                            prioList[count]=scNodes[i][j][2];

                            scNodes[i][j][0].stop();
                            scNodes[i][j][1].stop();
                        }
                        else{   
                            prioList[count]=scNodes[i][j][1];
   
                            scNodes[i][j][0].stop();
                            scNodes[i][j][2].stop();                        
                        }
                    }

                    prioList[count].setVolume((weightMatrix[i][j]/countMatrix[i][j])*_masterVolume);
                    if (prioList[count].getStatus() != AudioSource.Status.Playing)
                        prioList[count].play();

                    count++;
                }
                else {
                            scNodes[i][j][0].stop();
                            scNodes[i][j][1].stop(); 
                            scNodes[i][j][2].stop(); 
                }
    
                weightMatrix[i][j]=0;
                countMatrix[i][j]=0;
                
                
            }
        }   
        
        // Sort the sounds based on volume
        Arrays.sort(prioList, new Comparator<AudioNode>() {
            @Override
            public int compare(AudioNode first, AudioNode second)
            {
                if (first==null) return -1;
                if (second==null) return 1;
                if (first.getVolume() != second.getVolume()) {
                    return (int)(first.getVolume() - second.getVolume());
                }
                return 0; 
            }
        });

        // Play the highest ranking sounds and stop the rest
        for (int i=0;i<count;i++){
            if (prioList[i]==null) continue; 
        
            if (i<maxConcurrent){
                // Let's not restart when playing
                if (prioList[i].getStatus() != AudioSource.Status.Stopped)
                    prioList[i].play();
            }
            else {
                prioList[i].stop();
            }            
            prioList[i]=null;
        }
    }
    
    // This method is called by every little guy that is walking. It increases a counter
    // based on where he is (land or water) and his speed (walking or running).
    public void incCounter( int x, int y, float weight ){
        countMatrix[x][y]++;
        weightMatrix[x][y]+=weight;
    }
}

I have created 12 sound files for every possibility.

scWalking = new SoundCube(app, new String[] {"walk","run"}, new String[] {"land","water"}, 3);

There is one thing that is left to be desired: I am currently just using the distance to the camera to determine if a guy needs to make noise and to calculate the weight (volume) he adds. I need a way to find out if he is in front of the camera or behind it.

Not sure yet if I want to mute if they are behind the camera, or just lower their weight.

1 Like
Vector3f relativePosition = guy.getWorldTranslation().subtract(cam.getLocation());
float front = cam.getDirection().dot(relativePosition);
if( front > 0 ) {
    guy is in front of the camera
}
2 Likes

Translated to my game it does not change when a guy gets behind the camera

I think that might be because the cam is always higher than the little dudes. Maybe I shoud focus on finding out if they are in frame or not.

Then it wasn’t implemented correctly or the guy is not behind the camera.

The code I supplied will tell you how far a ‘guy’ is in front of or behind the camera. It’s simple math. It can’t really miss.

Edit: to prove it, log the guy’s position and the camera position + direction.

1 Like

That’ s what I was trying to tell, due to how my camera is positioned and aimed, it is not likely that the little guys will get behind it.

You could try to change this to a smaller value, like front > 0.5 and this way it will do a check for the front 90 degree cone, rather than the whole 180 degrees in front of the camera.

This should give a more accurate representation of what is actually on-screen, rather than what is only in-front of the camera.

1 Like

‘front’ will be the distance from the camera plane… probably OP actually meant “not in view” which is a different problem.

…view can be checked by checking collision against the camera frustum. You could get an approximation by doing a dot of relativePos.normalize() with cam.getLeft() and cam.getUp() and using a threshold.