Any java 3d sound api instead of OpenAL

Started by Melssj5, March 04, 2008, 11:31:37 PM

Previous topic - Next topic

Melssj5

Hi, I sthere any good 3d sound api for java, I dont want to use openAL becausse is tooooooooooo complicated, even for simple things like playing a FIle, I found a sound API for java that requires only 2 lines of code to play a file (almost 30 or more with open AL) but is not 3d and its useless for he game I am developing.
Nada por ahora

paulscode

#1
I had the same issue as you - OpenAL being extremely complicated.  I purchased a book by Andrew Davison, called Pro Java 6 3D Game Development (a very good book, btw).  There is a chapter in the book dedicated to the JOAL binding of OpenAL, and it goes into creating a class to simplify loading, playing, and moving sounds in 3D.  I followed the guide in the book to create a class called SoundManager that makes using OpenAL way easier.

-- EDIT:  Previously posted code contained errors, so I removed it. --

paulscode

#2
Hey, I hadn't done a lot of testing on the SoundManager code I mentioned above (I have been only using it for 2D and not using the full functionality).  Since I got this code from a professional-looking book, I just assumed there weren't any problems.  Further tests show that sounds are not getting quieter when they move further from the listener.  It is probably related to a typo on my part - I am looking into it.  I did find a couple of minor typos in the code, but I haven't yet found the source of the problem.

-- EDIT:  Previously posted code contained errors, so I removed it. --

paulscode

Today I switched the sound manager code over to the lwjgl binding of OpenAL (since jPCT uses lwjgl this seems more logical than using JOAL).  The two bindings are actually quite similar, so I had no trouble switching over.  I am even having the exact same problem I had with the JOAL version (sounds not getting quieter when they move further away).  I am getting help on the lwjgl forums, so I will post the working code when we get all the kinks worked out.  ;D

EgonOlsen

A port to LWJGL's sound stuff would be cool, because that would enable me to use this too. I don't want to add another binding to the mix.

paulscode

#5
Ok, I've worked out all the kinks.  Only thing to remember, is you have to use "mono" .wav files for the 3D effects to work.  "Stereo" files will just play at normal volume without any fading between speakers (this is a "feature" of OpenAL).

8) Here is the Sound Manager:

package Sounds;

import java.io.InputStream;

import java.nio.IntBuffer;
import java.nio.FloatBuffer;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import org.lwjgl.util.WaveData;

public class SoundManager
{   
    // Global stores for the sounds:
    private HashMap<String, IntBuffer> buffersMap;  // (name, buffer) pairs
    private HashMap<String, IntBuffer> sourcesMap;  // (name, buffer) pairs
   
    private FloatBuffer listenerPosition;  // Listener's position
    private FloatBuffer listenerOrientation;  // Listener's orientation
    private FloatBuffer listenerVelocity;  // Listener's velocity
   
    private float listenerAngle = 0;  // listener's angle (in radians)
   
    public SoundManager()
    {
        buffersMap = new HashMap<String, IntBuffer>();
        sourcesMap = new HashMap<String, IntBuffer>();
       
        initOpenAL();
        initListener();
    }
    private void initOpenAL()
    {
        try
        {
            AL.create();
        }
        catch( LWJGLException e )
        {
            e.printStackTrace();
            return;
        }
        AL10.alGetError();
    }   
   
    // Position and orientate the listener:
    private void initListener()
    {
        // Set the listener's initial position:
        listenerPosition = BufferUtils.createFloatBuffer( 3 ).put(
            new float[] { 0.0f, 0.0f, 0.0f } );
        listenerPosition.flip();
       
        // Set the listener's initial orientation:
        // The first 3 elements are the "look at" point
        // The second 3 elements are the "up direction"
        listenerOrientation = BufferUtils.createFloatBuffer( 6 ).put (
            new float[] { 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f } );
        listenerOrientation.flip();
       
        // Set the listener's initial velocity:
        listenerVelocity = BufferUtils.createFloatBuffer( 3 ).put (
            new float[] { 0.0f, 0.0f, 0.0f } );
        listenerVelocity.flip();
       
        AL10.alListener( AL10.AL_POSITION, listenerPosition );
        AL10.alListener( AL10.AL_VELOCITY, listenerVelocity );
        AL10.alListener( AL10.AL_ORIENTATION, listenerOrientation );
    }
   
    public void cleanUp()
    {
        Set<String> keys = sourcesMap.keySet();
        Iterator<String> iter = keys.iterator();
       
        String name;
        IntBuffer buffer, source;
        while( iter.hasNext() )
        {
            name = iter.next();
           
            source = sourcesMap.get( name );
            System.out.println( "Stopping " + name );
            AL10.alSourceStop( source );
            AL10.alDeleteSources( source );
           
            buffer = buffersMap.get( name );           
            AL10.alDeleteBuffers( buffer );
        }
    }
   
    public boolean load( String name, boolean toLoop )
    {
        if( sourcesMap.get( name ) != null )
        {
            System.out.println( name + " already loaded" );
            return true;
        }
       
        IntBuffer buffer = initBuffer( name );
        if( buffer == null )
            return false;
       
        IntBuffer source = initSource( name, buffer, toLoop );
       
        if( source == null )
        {
            // no need for the buffer anymore
            AL10.alDeleteBuffers( buffer );
            return false;
        }
       
        if( toLoop )
            System.out.println( "Looping source created for " + name );
        else
            System.out.println( "Source created for " + name );
       
        buffersMap.put( name, buffer );
        sourcesMap.put( name, source );
       
        return true;
    }
       
    public boolean load( String name, float x, float y, float z, boolean toLoop )
    {
        if( load( name, toLoop ) )
            return setPos( name, x, y, z );
        else
            return false;
    }
   
    // return a handle to the given resource
    private InputStream getResource( String resourceName )
    {
        return getClass().getClassLoader().getResourceAsStream( resourceName );
    }
   
    // Load a file, and create a buffer for it:
    private IntBuffer initBuffer( String filename )
    {
        WaveData waveFile = WaveData.create( getResource( "Sounds/" + filename ) );
       
        IntBuffer buffer = BufferUtils.createIntBuffer( 1 );
        AL10.alGenBuffers( buffer );
        if( AL10.alGetError() != AL10.AL_NO_ERROR )
        {
            System.out.println( "Error loading file: " + filename );
            return null;
        }
        AL10.alBufferData( buffer.get( 0 ), waveFile.format, waveFile.data, waveFile.samplerate );
        return buffer;
    }
   
    // Create a source (a point in space that makes sound):
    private IntBuffer initSource( String name, IntBuffer buffer, boolean toLoop )
    {
        IntBuffer source = BufferUtils.createIntBuffer( 1 );
        AL10.alGenSources( source );
       
        // Check for errors:
        if( AL10.alGetError() != AL10.AL_NO_ERROR )
        {
            System.out.println( "Error creating a source for: " + name );
            return null;
        }       
       
        // Position the source at the origin:
        FloatBuffer sourcePosition = BufferUtils.createFloatBuffer( 3 ).put(
            new float[] { 0.0f, 0.0f, 0.0f } );
        sourcePosition.flip();
       
        // The source has no initial velocity:
        FloatBuffer sourceVelocity = BufferUtils.createFloatBuffer( 3 ).put(
            new float[] { 0.0f, 0.0f, 0.0f } );
        sourceVelocity.flip();

        AL10.alSourcei( source.get( 0 ), AL10.AL_BUFFER, buffer.get(0) );
        AL10.alSourcef( source.get( 0 ), AL10.AL_PITCH, 1.0f );
        AL10.alSourcef( source.get( 0 ), AL10.AL_GAIN, 1.0f );
        AL10.alSource( source.get( 0 ), AL10.AL_POSITION, sourcePosition );
        AL10.alSource( source.get( 0 ), AL10.AL_VELOCITY, sourceVelocity );
       
        if( toLoop )
            AL10.alSourcei( source.get( 0 ), AL10.AL_LOOPING, AL10.AL_TRUE );  // looping
        else
            AL10.alSourcei( source.get( 0 ), AL10.AL_LOOPING, AL10.AL_FALSE );  // looping
       
        return source;
    }
   
    // Move the named sound to (x, y, z):
    public boolean setPos( String name, float x, float y, float z )
    {
        IntBuffer source = sourcesMap.get( name );
        if( source == null )
        {
            System.out.println( "No source found for " + name );
            return false;
        }
       
        // Create a FloatBuffer with the given coordinates:
        FloatBuffer sourcePosition = BufferUtils.createFloatBuffer( 3 ).put(
            new float[] { x, y, z } );
        sourcePosition.flip();
       
        AL10.alSource( source.get( 0 ), AL10.AL_POSITION, sourcePosition );
        return true;
    }
   
    public boolean play( String name )
    {
        IntBuffer source = sourcesMap.get( name );
        if( source == null )
        {
            System.out.println( "No source found for " + name );
            return false;
        }
       
        AL10.alSourcePlay( source.get( 0 ) );
        return true;
    }
   
    // move the listener by (x, z) step
    public void moveListener( float xStep, float zStep )
    {
        float x = listenerPosition.get( 0 ) + xStep;
        float z = listenerPosition.get( 2 ) + zStep;
        setListenerPos( x, z );
    }
   
    // position the listener at (xNew, zNew)
    public void setListenerPos( float xNew, float zNew )
    {
        float xOffset = xNew - listenerPosition.get( 0 );
        float zOffset = zNew - listenerPosition.get( 2 );
       
        // We are not changing the y-coord:
        // ( Listener only moves over XZ plane )
        listenerPosition.put( new float[] {xNew, 0.0f, zNew} );
        listenerPosition.flip();
       
        AL10.alListener( AL10.AL_POSITION, listenerPosition );  // update the listener's position
       
        // Keep the listener facing the same direction by
        // moving the "look at" point by the offset values:
        listenerOrientation.put( 0, listenerOrientation.get( 0 ) + xOffset );
        listenerOrientation.put( 2, listenerOrientation.get( 2 ) + zOffset );
       
        AL10.alListener( AL10.AL_ORIENTATION, listenerOrientation );  // update the listener's orientation
    }
   
    // turn the listener counterclockwise by "angle" radians
    public void turnListener( float angle )
    {
        setListenerOrientation( listenerAngle + angle );
    }
   
    // set the listener's orientation to be "angle" radians
    // in the counterclockwise direction around the y-axis
    public void setListenerOrientation( float angle )
    {
        listenerAngle = angle;
       
        float xOffset = -1.0f * (float) Math.sin( angle );
        float zOffset = -1.0f * (float) Math.cos( angle );
       
        // face in the (xLen, zLen) direction by adding the
        // offset values to the listener's "look at" point:
        listenerOrientation.put( 0, listenerOrientation.get( 0 ) + xOffset );
        listenerOrientation.put( 2, listenerOrientation.get( 2 ) + zOffset );
       
        AL10.alListener( AL10.AL_ORIENTATION, listenerOrientation );  // update the listener's orientation
    }
}


All you need to know to use this class:

SoundManager soundManager = new SoundManager();  // Create the Sound Manager

soundManager.load( soundName, true );  // load a sound (true = looping, false = play once)

soundManager.play( soundName );  // play the sound

/*** You can move the sound source ***/
soundManager.setPos( soundName, x, y, z );

/*** You can move the listener ***/
moveListener( xStep, zStep );  // move relative to current position
setListenerPos( x, z );  // move to the specified location

/*** And you can rotate the listener ***/
turnListener( angle );  // turn relative to current orientation
setListenerOrientation( float angle );  // look in the specified direction

soundManager.cleanUp();  // stop playing sounds and delete all buffers


Now that it actually works, I'll be happy to help you tweak it to your preference.  Some things that I could potentially change are:

1)  The fading distance.  Right now it is pretty short, but that is easy to change (It'll just require me to stick in a scalar value and some simple multiplication).  Just let me know what is a good distance for sounds to fade away.

2)  A method for playing a sound from more than one source location.  This would also be fairly straight-forward, if it's a feature you'll need for your program.  It would just require a call to another method to specify a unique name for each source.

3)  The ability to change the listener's Y coordinate.  This is totally doable, it would just require a little bit fancier math on my part.

4)  The listener's ability to "look" up or down (rotate around the X axis).  This is doable, but it might be difficult to come up with a user-friendly interface (You'd have to supply a "look-at point" and an "up direction".  Actually, it might not really be too bad, come to think of it).

5)  Different ways to load sound files (right now the sounds must be part of the jar).  Also easy to do, if this is something you require for your program.

Let me know if you need any of these capabilities in your program, or anything else I didn't think of, and I will make the necessary changes.  I will probably end up needing to do some of this stuff eventually anyway, when I am ready to start putting sound in the project I'm working on.

-- EDIT:  Oh, and one more thing:  currently, all sounds need to be compiled into the JAR, and located in the package "Sounds/".  If you want to stick them somewhere else, just change the Sound Manager's "initBuffer" method.

Melssj5

Thanks, I will try it tomorrow and will post any results. I were also trying to use any other sound api and just adding a method to calc he source distance from the camera and adjusting the volume from that distance but JMF is not working and JLayer has no volume adjusment. I guess this code posted is the only way to go by now. I also got some 3d sound working but the fading efect wasnt good enough with lwjgl open al. I will try this joal tomorrow.
Nada por ahora

paulscode

 :o Oh!  The SoundManager uses lwjgl now instead of JOAL.

Realistically, though, both lwjgl and JOAL are merely thin layers of Java over the underlying OpenAL, so I seriously doubt there is any performance difference between the two libraries.

What kind of problem were you having with the fading effect?  Like I was talking about in my last post, OpenAL fades the sound away really fast (it becomes silent at something small like "distance = 10").  I can make that distance larger if you need it to be.  Just tell me what is a good distance for the volume to fade away.

Also, if your sound does not seem to be fading at all, you are probably using a "stereo" sound.  Fading only works for "mono" sounds.

I could write you a distance fader if you need one for using stereo sounds.  I actually already wrote one (before I figured out that I was supposed to be using mono sounds instead of stereo ones).  The only reason I took that code out of the SoundManager, is that it affects the overall volume of the sound coming out of BOTH speakers -- it isn't true 3D sound, because it doesn't fade between the left and right speakers depending on where the sound is in relation to the listener.  The OpenAL functionality that I am now using in the SoundManager class does a much better job, and it is true 3D sound.  I'll do my best to get it working for you, so you'll have really good sound for your project.

fireside

click here->Fireside 7 Games<-

paulscode

#9
Quote from: fireside on March 09, 2008, 08:35:54 AM
Does this work for ogg?
??? Never used ogg before.  That's a codec / sound format, right?  The above class is written for lwjgl's binding of OpenAL, and it currently only opens .wav sound files.  I could look into what other sound formats can be used with lwjgl, if that would be helpful.

-- EDIT:  I did a little more research on the subject, and I found a tutorial on streaming ogg in Open AL.  The original tutorial is written in c++, but I also found a translation of the tutorial written for JOAL.  I will see if I can re-write it in lwjgl.  I'll keep you posted.

--UPDATE:  I have finished porting the .ogg streamer over to lwjgl, and I can now play .ogg files normally, or "stream" them (that's where you use multiple buffers to break really long files like background music up into smaller pieces, so you can begin playing them immediately).  I will now implement all this in the Sound Manager and repost the code when I am finished.  I am really glad I found this, because now I know how to do multi-buffer streaming of sounds, so thanks for the question, fireside! ;D

fireside

QuoteI am really glad I found this, because now I know how to do multi-buffer streaming of sounds, so thanks for the question, fireside!

I look forward to checking out the code.  Ogg is great, very small file size.
click here->Fireside 7 Games<-

JavaMan

Maybe you should add your code as a project in the projects area. That way visitors to the site can see there are some sound classes specifically for jpct.  ::)

Melssj5

Thanks a lot. It worked well.  :o

I have made some minor changes on it but everything seems to be good.

How can I change the fading distance. I want each sound to have a different fading distance. With parameter should I change? Were is the distance applied? I need it to be changed on the play or on the load method.
Nada por ahora

paulscode

#13
Quote from: Melssj5 on March 10, 2008, 08:13:21 PM
How can I change the fading distance. I want each sound to have a different fading distance. With parameter should I change? Were is the distance applied? I need it to be changed on the play or on the load method.

The parameter that deals with distance fading (a.k.a "Attenuation"), is "AL_ROLLOFF_FACTOR" (set when creating a source).  I am adding this functionality into the SoundManager now, but if you are interested in doing it yourself (so you won't loose the other changes you have made), the method you'll need to alter is "initSource".  "initSource" is called from "load", so you should change "load" so it takes another parameter (a fade distance), then pass this on to initSource.
 
Now here is the tricky part.  Rolloff factor is a float that ranges from 0.0 to virtual infinity (I believe 1.0 is default, so you will be using rolloffs that are less than that).  The closer to 0.0 you set the rolloff factor, the greater the fading distance will be.  Higher rolloffs fade faster.

So in other words you have to do some math to convert your fade distaince into a rolloff factor.  Unfortunately, there is no way to say "I want this source to fade and become quiet at distance X" (unless you write a formula to manually set the sound's gain - I am working on this).  The problem is, if you have your speakers turned up louder, or if you are playing a louder sound file, fading will happen at a larger distance (and quieter sounds or lower volume on your speakers will fade away faster).

For now, what I did to overcome this problem, is approximation, trial and error.  I picked an avarage sound with speakers set at an average volume, and messed around with distances and rolloff factors.  I put together a data set and calculated the following Logrithmic Regression:

-- EDIT: :o Wow, I just realized I was using "moveListener" instead of "setListenerPos" when in calculated this!
DONT USE THE FORMULA I HAD HERE. IT IS WAY OFF!

paulscode

Quote from: JavaMan on March 10, 2008, 05:33:26 PM
Maybe you should add your code as a project in the projects area. That way visitors to the site can see there are some sound classes specifically for jpct.  ::)

That is a good idea.  From now on, progress on SoundManager will be located in the projects area, under the post titled "3D Sound using lwjgl binding of Open AL" (http://www.jpct.net/forum2/index.php/topic,1057.0.html).