Improving EasyOgg

OggInputStream depends on LWJGL as far as I know, it is a bad idea for projects that use JOGG and JORBIS to avoid using JOAL that is buggy under Linux.

Nevertheless, there is a version of OggInputStream for JORBIS here:
http://home.halden.net/tombr/ogg/OggInputStream.java
I’m going to use this one if possible.

That was actually the OggInputStream I was referring to (I wrote it btw) :wink: Did not know there were another OggInputStream around.

Anyway, I’ve attached my new OggClip.

edit: I’ve added fallback support for loading to JavaSound, so it is possible to send in wav files and it should work.

fwiw, lwjgl uses openal-soft, which should work fine under linux ?

Fine but you still use some threads.

Please find my source code below. Nevertheless, I have not tested it, I’m going to do it:

/*This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation, version 2
  of the License.
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston,
  MA 02111-1307, USA.
*/
package sound;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
import java.util.AbstractMap.SimpleEntry;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;

/**
 * This class handles an OGG sound sample whose data are loaded prior
 * to playback to improve the performance (instead of streaming at real time).
 * The only drawback of this method is not to support samples with variable rates.
 * Moreover, the use is split in several steps to handle the resource carefully. 
 * It is well fitted for programs that require a fine control on sounds, for example
 * games using lots of noises. It is possible to prepare a sample without 
 * opening it in order to avoid busying a line uselessly. On the other hand, it is 
 * possible to close a sample for the same reason. I do not try to reopen the 
 * underlying clip because it is highly probable to fail and it is more efficient to 
 * close the previous line cleanly and then to get a fresh line by taking into 
 * account the modifications of the context that might have happened between the 
 * latest opening and the latest closure (for example, the mixer used previously might 
 * have become unavailable).
 * @author Julien Gouesse
 *
 */
public final class Sample{
    
    
    private Clip clip;
    
    private byte[] uncompressedDataArray;
    
    private AudioFormat audioFormat;

    
    Sample(InputStream inputStream)throws IllegalArgumentException{
        Map.Entry<byte[],int[]> result=loadOgg(inputStream);
        int rate=result.getValue()[0];
        int channels=result.getValue()[1];
        uncompressedDataArray=result.getKey();       
        audioFormat=new AudioFormat(rate,16,channels,true,false);
        if(uncompressedDataArray==null)
            throw new IllegalArgumentException("Sound sample not supported, data loading failed");       
    }
    
    Sample(File file) throws IllegalArgumentException,FileNotFoundException{
        this(new BufferedInputStream(new FileInputStream(file)));
    }
    
    Sample(URL url) throws IllegalArgumentException,FileNotFoundException,URISyntaxException{
        this(new BufferedInputStream(new FileInputStream(new File(url.toURI()))));
    }
    
    Sample(Sample sample){
        uncompressedDataArray=sample.uncompressedDataArray;
        audioFormat=sample.audioFormat;
        clip=null;
    }
    
    /**
     * Open a sample to prepare it to be played. 
     * Do it prior to play the sample.
     * @throws IllegalArgumentException
     */
    private void open()throws IllegalArgumentException{
        if(clip!=null&&clip.isOpen())
            throw new UnsupportedOperationException("Impossible to open an already opened sample");
        //Now we are sure that the clip is null or closed
        DataLine.Info info = new DataLine.Info(Clip.class,audioFormat,AudioSystem.NOT_SPECIFIED);       
        if(AudioSystem.isLineSupported(info))
            {Mixer mixer=getBestFittedMixer(info);
             try{clip=(Clip)mixer.getLine(info);
                 clip.open(audioFormat,uncompressedDataArray,0,uncompressedDataArray.length);
                } 
             catch(LineUnavailableException lue)
             {throw new IllegalArgumentException("Sound sample not supported",lue);}
            }
        else
            throw new IllegalArgumentException("Sound sample not supported, no line supported");
    }
    
    /**
     * Load an OGG file
     * @param inputStream
     * @return data, rate and channels
     */
    private static final Map.Entry<byte[],int[]> loadOgg(InputStream inputStream){
        ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
        byteOutputStream.reset();
        byte[] tmpBuffer=new byte[10240*4];
        OggInputStream oggInputStream=new OggInputStream(inputStream);
        boolean done = false;
        int bytesRead;
        while(!done)
            {try{bytesRead=oggInputStream.read(tmpBuffer,0,tmpBuffer.length);} 
             catch(IOException ioe)
             {ioe.printStackTrace();
              bytesRead=0;
             }
             byteOutputStream.write(tmpBuffer,0,bytesRead);
             done=(bytesRead!=tmpBuffer.length||bytesRead<0);
            }
        byte[] uncompressedData=byteOutputStream.toByteArray();
        return(new SimpleEntry<byte[],int[]>(uncompressedData,new int[]{oggInputStream.getRate(),oggInputStream.getFormat()}));
    }
    
    /**
     * Reopen a closed sample
     * Notice that a new line might be used
     * @return true if the sample is already usable or if the operation has been successful
     *         false if the operation has failed
     */
    public final boolean reopen(){
        boolean success=true;
        if(clip!=null&&!clip.isOpen())
            try{open();}
            catch(IllegalArgumentException iae)
            {success=false;}
        return(success);
    }
    
    
    
    /**
     * Release the line and the native resources
     * NB: On Mac, the JVM 1.5 freezes after this call 
     */
    public final void close(){
        if((clip!=null&&!clip.isOpen())||clip==null)
            throw new UnsupportedOperationException("Impossible to close a closed sample or a never-opened sample");
        try{clip.close();}
        catch(SecurityException se)
        {se.printStackTrace();}
    }
    
    /**
     * Play the sample once from the beginning
     */
    public final void play(){
        loop(1);
    }
    
    /**
     * Pause the sample
     */
    public final void pause(){
        if((clip!=null&&!clip.isOpen())||clip==null)
            throw new UnsupportedOperationException("Impossible to pause a closed sample");
        if(clip.isRunning())
            clip.stop();                              
    }
    
    /**
     * Resume a paused sample from the paused frame and play it count times
     * Do nothing if the sample was not paused
     * @param count
     */
    public void resume(int count){
        if((clip!=null&&!clip.isOpen())||clip==null)
            throw new UnsupportedOperationException("Impossible to resume a closed sample");
        if(!clip.isRunning())
            clip.loop(count);           
    }
    
    /**
     * Play it indefinitely from the beginning
     */
    public final void loop(){
        loop(Clip.LOOP_CONTINUOUSLY);
    }
    
    /**
     * Play it count times from the beginning
     * @param count
     */
    public final void loop(int count){
        if((clip!=null&&!clip.isOpen())||clip==null)
            throw new UnsupportedOperationException("Impossible to play a closed sample");
        if(clip.isRunning())
            stop();     
        clip.loop(count);
    }
    
    /**
     * Stop the sample and rewind it
     */
    public final void stop(){
        if((clip!=null&&!clip.isOpen())||clip==null)
            throw new UnsupportedOperationException("Impossible to stop a closed sample");
        clip.stop();
        clip.setFramePosition(0);
    }
    
    /**
     * Try to find a mixer that both supports this line and supports
     * as much line as possible
     */
    private static final Mixer getBestFittedMixer(DataLine.Info info){
        Mixer currentMixer=null;
        Mixer.Info[] mi=AudioSystem.getMixerInfo();
        Mixer bestMixer=null;
        for(int i=0;i<mi.length;i++)
            {currentMixer=AudioSystem.getMixer(mi[i]);
             if(currentMixer.isLineSupported(info))
                 if(bestMixer==null||bestMixer.getMaxLines(info)<currentMixer.getMaxLines(info))
                     bestMixer=currentMixer;                         
            }
        //The best mixer cannot be null as AudioSystem.isLineSupported returned true
        return(bestMixer);
    }
}

I will add the gain and the balance control later, it doesn’t require a lot of time.

I was trying it out, but I can’t seem to stop the music in my game. The same clip works fine when using your test case.
What I’m doing is stopping the music then closing the window and creating another window, but seems the music just keeps on playing.
I even tried turning fade off and adding a few second wait. I can’t see what is wrong… Also makes a clicking noise for a few seconds.
Also be great if there was a setdefaultvolume method.
Keep up the good work.

Edit: I think it might be to do with that I’m using another thread to stop it. Any solutions to solve this?

Use a shutdown hook like TUER (cf. connection.GameServiceProvider.java).

Seems isStopped doesn’t return the correct value after something has finished playing.
Also line 280, variable “OggInputStream oggIn = new OggInputStream(in)” in OggClip class is never closed.

This should be fixed in the attached version.

Sorry, but I can’t reproduce, and I have no idee what is wrong. In the attached file it tests stopping the clip from a separate thread. Seems to work here.

What should it do? The default volume is 1 which will play the clip without changing the gain. A volume of 0 will mute. Volume larger than 1 is allowed and will increase the sound.

Yes, and I agree that starting a new tread every time a sound is played is not a good practice. Also agree that streaming and decoding the ogg in real time is not needed when playing clips.

However if you need to play background music then it’s another mather. And for this EasyOgg should do the job as you probably don’t end up playing the music twice at the same time.

Performance is not important in the project I’m using this for. I just a need an easy way of playing clips. And it seems to be working great so I’m happy.

Thanks, though you still don’t close the file, when your done.

You example does work for me too, so It isn’t just a thread problem.
Heres a small test case.

        ogg.loop();
        JFrame frame = new JFrame();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
        frame.addKeyListener(new KeyAdapter(){
			@Override
			public void keyPressed(final KeyEvent evt) {
	                try {
	                    Thread.sleep(3000);
	                    System.out.println("stop()");
	                    System.out.println("is clip stopped1 " + ogg.isStopped());
	                    ogg.stop();
	                    System.out.println("is clip stopped2 " + ogg.isStopped());
	                    while (!ogg.isStopped()) {
	                        System.out.println("clip not stopped");
	                    }
	                } catch (Exception e) {
	                    e.printStackTrace();
	                }
		});

Sorry didn’t know 1 was default.

Thanks for the test case. Managed to reproduce fhe bug. It is now fixed. Se attachment.

What do you mean? java.io.File can not be closed!

I do close the stream if that is what you mean.

[quote]What do you mean? java.io.File can not be closed!

I do close the stream if that is what you mean.
[/quote]
Oh ok, just that FindBugs was complaining about it.

So far everything seems to be going well. I do seem to get some clicking sounds on my music when using fade while playing another sound.

I don’t know if someone might be interested in what I have written in order to play ogg sounds… I’ve tested it and it works fine, it is very accurate, I only have some problems when I resume a loop. I’m going to commit my modifications on my SVN repository.

My source code is below, the method void resume(int count) doesn’t work, I tried to use setLoopPoints, flush, start, etc… without success. If you have an idea, please let me know. You need the class OggInputStream (I gave the pointer to download it in a previous post above) to compile and use my class. Don’t forget to open a sample before using it.


/*This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation, version 2
  of the License.
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston,
  MA 02111-1307, USA.
*/
package sound;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
import java.util.AbstractMap.SimpleEntry;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;

/**
 * This class handles an OGG sound sample whose data are loaded prior
 * to playback to improve the performance (instead of streaming at real time).
 * The only drawback of this method is not to support samples with variable rates.
 * Moreover, the use is split in several steps to handle the resource carefully. 
 * It is well fitted for programs that require a fine control on sounds, for example
 * games using lots of noises. It is possible to prepare a sample without 
 * opening it in order to avoid busying a line uselessly. On the other hand, it is 
 * possible to close a sample for the same reason. I do not try to reopen the 
 * underlying clip because it is highly probable to fail and it is more efficient to 
 * close the previous line cleanly and then to get a fresh line by taking into 
 * account the modifications of the context that might have happened between the 
 * latest opening and the latest closure (for example, the mixer used previously might 
 * have become unavailable).
 * @author Julien Gouesse
 *
 */
public final class Sample{
    
    
    private Clip clip;
    
    private byte[] uncompressedDataArray;
    
    private AudioFormat audioFormat;

    
    /**
     * 
     * @param inputStream
     * @throws IllegalArgumentException
     */
    public Sample(InputStream inputStream)throws IllegalArgumentException{
        //pausedFramePosition=0;
        Map.Entry<byte[],int[]> result=loadOgg(inputStream);
        int rate=result.getValue()[0];
        int channels=result.getValue()[1];
        uncompressedDataArray=result.getKey();       
        audioFormat=new AudioFormat(rate,16,channels,true,false);
        if(uncompressedDataArray==null)
            throw new IllegalArgumentException("Sound sample not supported, data loading failed");       
    }
    
    public Sample(File file) throws IllegalArgumentException,FileNotFoundException{
        this(new BufferedInputStream(new FileInputStream(file)));
    }
    
    public Sample(URL url) throws IllegalArgumentException,FileNotFoundException,URISyntaxException{
        this(new BufferedInputStream(new FileInputStream(new File(url.toURI()))));
    }
    
    public Sample(Sample sample){
        uncompressedDataArray=sample.uncompressedDataArray;
        audioFormat=sample.audioFormat;
        clip=null;
    }
    
    /**
     * Open a sample to prepare it to be played. 
     * Do it prior to play the sample.
     * @throws IllegalArgumentException
     */
    public final void open()throws IllegalArgumentException{
        if(clip!=null&&clip.isOpen())
            throw new UnsupportedOperationException("Impossible to open an already opened sample");
        //Now we are sure that the clip is null or closed
        DataLine.Info info = new DataLine.Info(Clip.class,audioFormat,AudioSystem.NOT_SPECIFIED);       
        if(AudioSystem.isLineSupported(info))
            {Mixer mixer=getBestFittedMixer(info);
             try{clip=(Clip)mixer.getLine(info);
                 clip.open(audioFormat,uncompressedDataArray,0,uncompressedDataArray.length);
                } 
             catch(LineUnavailableException lue)
             {throw new IllegalArgumentException("Sound sample not supported",lue);}
            }
        else
            throw new IllegalArgumentException("Sound sample not supported, no line supported");
    }
    
    /**
     * Load an OGG file
     * @param inputStream
     * @return data, rate and channels
     */
    private static final Map.Entry<byte[],int[]> loadOgg(InputStream inputStream){
        ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
        byteOutputStream.reset();
        byte[] tmpBuffer=new byte[10240*4];
        OggInputStream oggInputStream=new OggInputStream(inputStream);
        boolean done = false;
        int bytesRead;
        while(!done)
            {try{bytesRead=oggInputStream.read(tmpBuffer,0,tmpBuffer.length);} 
             catch(IOException ioe)
             {ioe.printStackTrace();
              bytesRead=0;
             }
             byteOutputStream.write(tmpBuffer,0,bytesRead);
             done=(bytesRead!=tmpBuffer.length||bytesRead<0);
            }
        byte[] uncompressedData=byteOutputStream.toByteArray();
        return(new SimpleEntry<byte[],int[]>(uncompressedData,new int[]{oggInputStream.getRate(),oggInputStream.getFormat()}));
    }
    
    /**
     * Reopen a closed sample
     * Notice that a new line might be used
     * @return true if the sample is already usable or if the operation has been successful
     *         false if the operation has failed
     */
    public final boolean reopen(){
        boolean success=true;
        if(clip!=null&&!clip.isOpen())
            try{open();}
            catch(IllegalArgumentException iae)
            {success=false;}
        return(success);
    }
    
    
    
    /**
     * Release the line and the native resources
     * NB: On Mac, the JVM 1.5 freezes after this call 
     */
    public final void close(){
        if((clip!=null&&!clip.isOpen())||clip==null)
            throw new UnsupportedOperationException("Impossible to close a closed sample or a never-opened sample");
        try{clip.close();}
        catch(SecurityException se)
        {se.printStackTrace();}
    }
    
    /**
     * Play the sample once from the beginning
     */
    public final void play(){
        loop(1);
    }
    
    /**
     * Pause the sample
     */
    public final void pause(){
        if((clip!=null&&!clip.isOpen())||clip==null)
            throw new UnsupportedOperationException("Impossible to pause a closed sample");
        if(clip.isRunning())
            clip.stop();
    }
    
    /**
     * Resume a paused sample from the paused frame and play it count times
     * Do nothing if the sample was not paused
     * @param count
     */
    public void resume(int count){
        if((clip!=null&&!clip.isOpen())||clip==null)
            throw new UnsupportedOperationException("Impossible to resume a closed sample");        
        if(!clip.isRunning())
            {//FIXME: the line below should not be used, that's the only way I've found
             //to repair this class when resuming a loop
             //clip.setFramePosition(0);
             clip.loop(count);
            }       
    }
    
    /**
     * Play it indefinitely from the beginning
     */
    public final void loop(){
        loop(Clip.LOOP_CONTINUOUSLY);
    }
    
    /**
     * Play it count times from the beginning
     * @param count
     */
    public final void loop(int count){
        if((clip!=null&&!clip.isOpen())||clip==null)
            throw new UnsupportedOperationException("Impossible to play a closed sample");
        if(clip.isRunning())
            stop();     
        clip.loop(count);
    }
    
    /**
     * Stop the sample and rewind it
     */
    public final void stop(){
        if((clip!=null&&!clip.isOpen())||clip==null)
            throw new UnsupportedOperationException("Impossible to stop a closed sample");
        clip.stop();
        clip.setFramePosition(0);
    }
    
    public final boolean isRunning(){
        return(clip!=null&&clip.isOpen()&&clip.isRunning());
    }
    
    /**
     * Try to find a mixer that both supports this line and supports
     * as much line as possible
     */
    private static final Mixer getBestFittedMixer(DataLine.Info info){
        Mixer currentMixer=null;
        Mixer.Info[] mi=AudioSystem.getMixerInfo();
        Mixer bestMixer=null;
        for(int i=0;i<mi.length;i++)
            {currentMixer=AudioSystem.getMixer(mi[i]);
             if(currentMixer.isLineSupported(info))
                 if(bestMixer==null||bestMixer.getMaxLines(info)<currentMixer.getMaxLines(info))
                     bestMixer=currentMixer;                         
            }
        //The best mixer cannot be null as AudioSystem.isLineSupported returned true
        return(bestMixer);
    }
}

Tom you still working on your OggClip?

Seems my users are complaining that the walking sound its making my game unplayable.
I have noticed that it does use a lot of cpu to play short repeated sounds.
Like:

			
if(ogg.isStopped()){
	ogg.play();
}

Its great for music just not repeating short sounds.

I using loop and then pausing/resuming seems a little better but looping has to play the sound a few times. It will keep playing for a few times after I sent the pause.

Are you able to look into this issue?

gouessej I would try your code but you said in your comments that it freezes on mac Java 1.5.

Haven’t changed it since last time I posted. I have to admit that playing a sound is a fairly heavy operation. Since it creates a new thread. You could modify the code to add a playOnceThenPause method. I don’t have the time to add it. I also think it is too specialized for it to be a valuable addition to the class. However I will help you if you want to do it yourself. You have to set a flag that will pause the playback after playing it once. At line 423 you have to check for this flag and set the paused flag to true.

I’ve discovered another problem with OggClip when I used it in my application. Streaming more than one sound with JavaSound will cause stuttering. It’s because the JavaSound implementation is crap/broken.

How about doing the obvious thing and using a cachedThreadExecutor instead of spawning a thread whenever you want?

That would be to easy :slight_smile:

Thanks for the help, I did what you suggested, but saw no decrease in cpu myself.