Pausing an MP3 File Using JLayer

While JavaZoom’s JLayer package makes playing an MP3 from beginning to end fairly painless, it doesn’t provide any easy way to pause and resume an MP3 at an arbitrary point during playback. This tutorial attempts to address that lack.

Update: Arthur Assuncao has written an alternate version of this code for a school project. I’ve run through it, and it looks like it’s not quite as crufty as mine. Notably, his code incorporates the thread stopping in the actual player class itself, which is something I’ve been too lazy to do for months. Arthur’s version also has a Swing frontend, which isn’t for me, but some people might like.

Arthur’s version is linked in the comments section. Oh, by the way, he changed the filename from “Test.mp3” to “Temp.mp3”, so look out for that. It’s also partly in… Portuguese? I’m guessing?… but it’s not too hard to figure out, and it really gives things a nice cosmopolitan flair.

1. If you have not already done so, download and install the Java Development Kit. Details are given in a previous tutorial.

2. In any convenient location, create a new directory named “JLayerPausableTest”.

3. Download JLayer, by JavaZoom. JLayer is an open-source library that allows MP3 files to be played from Java. As of this writing, the latest version is available at “http://www.javazoom.net/javalayer/sources.html”.

4. Uncompress the contents of the downloaded JLayer archive file to any convenient location and copy the pre-compiled JAR file named “jl1.0.1.jar” to the JLayerPausableTest directory.

5. In the JLayerPausable directory, create a new text file named “JLayerPausableTest.java” containing the text shown below.

import java.io.*;

public class JLayerPausableTest
{
	public static void main(String[] args)
	{
		SoundJLayer soundToPlay = new SoundJLayer("Test.mp3");

		BufferedReader consoleReader = new BufferedReader
		(
			new InputStreamReader(System.in)
		);

		System.out.println("About to start playing sound.");
		System.out.println("Press enter to pause...");

		soundToPlay.play();

		while (1 == 1)
		{
			try
			{
				consoleReader.readLine();
			}
			catch (Exception ex)
			{
				ex.printStackTrace();
			}

			System.out.println("toggling pause");

			soundToPlay.pauseToggle();
		}
	}
}

class SoundJLayer extends JLayerPlayerPausable.PlaybackListener implements Runnable
{
	private String filePath;
	private JLayerPlayerPausable player;
	private Thread playerThread;	

	public SoundJLayer(String filePath)
	{
		this.filePath = filePath;	
	}

	public void pause()
	{
		this.player.pause();

		this.playerThread.stop();
		this.playerThread = null;
	}

	public void pauseToggle()
	{
		if (this.player.isPaused == true)
		{
			this.play();
		}
		else
		{
			this.pause();
		}
	}

	public void play()
	{
		if (this.player == null)
		{
			this.playerInitialize();
		}

		this.playerThread = new Thread(this, "AudioPlayerThread");

		this.playerThread.start();
	}

	private void playerInitialize()
	{
		try
		{
			String urlAsString = 
				"file:///" 
				+ new java.io.File(".").getCanonicalPath() 
				+ "/" 
				+ this.filePath;

			this.player = new JLayerPlayerPausable
			(
				new java.net.URL(urlAsString),
				this 
			);
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}

	// PlaybackListener members

	public void playbackStarted(JLayerPlayerPausable.PlaybackEvent playbackEvent)
	{
		System.out.println("playbackStarted()");
	}

	public void playbackFinished(JLayerPlayerPausable.PlaybackEvent playbackEvent)
	{
		System.out.println("playbackEnded()");
	}	

	// IRunnable members

	public void run()
	{
		try
		{
			this.player.resume();
		}
		catch (javazoom.jl.decoder.JavaLayerException ex)
		{
			ex.printStackTrace();
		}

	}
}

6. Still in the JLayerPausableTest directory, create a new text file named “JLayerPlayerPausable.java”, containing the following text.

/* *-----------------------------------------------------------------------
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   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 Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *----------------------------------------------------------------------
 */

import java.io.*;
import java.net.*;
import javazoom.jl.decoder.*;
import javazoom.jl.player.*;

public class JLayerPlayerPausable
{
    // This class is loosely based on javazoom.jl.player.AdvancedPlayer.

    private java.net.URL urlToStreamFrom;
    private Bitstream bitstream;
    private Decoder decoder;
    private AudioDevice audioDevice;
    private boolean isClosed = false;
    private boolean isComplete = false;
    private PlaybackListener listener;
    private int frameIndexCurrent;

    public boolean isPaused;

    public JLayerPlayerPausable
    (
        URL urlToStreamFrom,
        PlaybackListener listener
    ) 
    throws JavaLayerException
    {
        this.urlToStreamFrom = urlToStreamFrom;
        this.listener = listener;
    }

    public void pause()
    {
        this.isPaused = true;
        this.close();
    }

    public boolean play() throws JavaLayerException
    {
        return this.play(0);
    }

    public boolean play(int frameIndexStart) throws JavaLayerException 
    {
        return this.play(frameIndexStart, -1, 52);
    }

    public boolean play
    (
        int frameIndexStart, 
        int frameIndexFinal, 
        int correctionFactorInFrames
    ) 
    throws JavaLayerException
    {
        try
        {
            this.bitstream = new Bitstream
            (
                this.urlToStreamFrom.openStream()
            );
        }
        catch (java.io.IOException ex)
        {}

        this.audioDevice = FactoryRegistry.systemRegistry().createAudioDevice();
        this.decoder = new Decoder();
        this.audioDevice.open(decoder);

        boolean shouldContinueReadingFrames = true;

        this.isPaused = false;
        this.frameIndexCurrent = 0;

        while 
        (
            shouldContinueReadingFrames == true
            &&
            this.frameIndexCurrent < frameIndexStart - correctionFactorInFrames 
        ) 
        {
            shouldContinueReadingFrames = this.skipFrame();
            this.frameIndexCurrent++;
        }

        if (this.listener != null) 
        {
            this.listener.playbackStarted
            (
                new PlaybackEvent
                (
                    this,
                    PlaybackEvent.EventType.Instances.Started,
                    this.audioDevice.getPosition()
                )
            );
        }

        if (frameIndexFinal < 0)
        {
            frameIndexFinal = Integer.MAX_VALUE;
        }

        while 
        (
            shouldContinueReadingFrames == true 
            && 
            this.frameIndexCurrent < frameIndexFinal
        )
        {
            if (this.isPaused == true)
            {
                shouldContinueReadingFrames = false;    
                try { Thread.sleep(1); } catch (Exception ex) {}
            }
            else
            {
                shouldContinueReadingFrames = this.decodeFrame();
                this.frameIndexCurrent++;    
            }
        }

        // last frame, ensure all data flushed to the audio device.
        if (this.audioDevice != null)
        {
            this.audioDevice.flush();

            synchronized (this)
            {
                this.isComplete = (this.isClosed == false);
                this.close();
            }

            // report to listener
            if (this.listener != null) 
            {
                this.listener.playbackFinished
                (
                    new PlaybackEvent
                    (
                        this,
                        PlaybackEvent.EventType.Instances.Stopped,
                        this.audioDevice.getPosition()
                    )
                );
            }
        }

        return shouldContinueReadingFrames;
    }

    public boolean resume() throws JavaLayerException
    {
        return this.play(this.frameIndexCurrent);
    }

    public synchronized void close()
    {
        if (this.audioDevice != null)
        {
            this.isClosed = true;

            this.audioDevice.close();

            this.audioDevice = null;

            try
            {
                this.bitstream.close();
            }
            catch (Exception ex)
            {}
        }
    }

    protected boolean decodeFrame() throws JavaLayerException
    {
        boolean returnValue = false;

        try
        {
            if (this.audioDevice != null)
            {                
                Header header = this.bitstream.readFrame();
                if (header != null)
                {
                    // sample buffer set when decoder constructed
                    SampleBuffer output = (SampleBuffer) decoder.decodeFrame
                    (
                        header, bitstream
                    );

                    synchronized (this)
                    {
                        if (this.audioDevice != null)
                        {
                            this.audioDevice.write
                            (
                                output.getBuffer(), 
                                0, 
                                output.getBufferLength()
                            );
                        }
                    }

                    this.bitstream.closeFrame();
                }
            }
        }
        catch (RuntimeException ex)
        {
            throw new JavaLayerException("Exception decoding audio frame", ex);
        }
        return true;
    }

    protected boolean skipFrame() throws JavaLayerException
    {
        boolean returnValue = false;

        Header header = bitstream.readFrame();

        if (header != null) 
        {
            bitstream.closeFrame();
            returnValue = true;
        }

        return returnValue;
    }

    public void stop()
    {
        this.listener.playbackFinished
        (
            new PlaybackEvent
            (
                this,
                PlaybackEvent.EventType.Instances.Stopped,
                this.audioDevice.getPosition()
            )
        );

        this.close();
    }

    // inner classes

    public static class PlaybackEvent
    {    
        public JLayerPlayerPausable source;
        public EventType eventType;
        public int frameIndex;

        public PlaybackEvent
        (
            JLayerPlayerPausable source, 
	    EventType eventType, 
            int frameIndex
        )        
        {
            this.source = source;
            this.eventType = eventType;
            this.frameIndex = frameIndex;
        }

        public static class EventType
        {
            public String name;

            public EventType(String name)
            {
                this.name = name;
            }

            public static class Instances
            {
                public static EventType Started = new EventType("Started");
                public static EventType Stopped = new EventType("Stopped");
            }
        }
    }

    public static abstract class PlaybackListener
    {
        public void playbackStarted(PlaybackEvent event){}
        public void playbackFinished(PlaybackEvent event){}
    }
}

7. Still in the JLayerPausableTest directory, create a new text file named “ProgramBuildAndRun-WithJLayerJar.bat”, containing the following text, and substitute the path of the directory containing javac.exe in the appropriate place.

set javaPath=[the path of the directory containing javac.exe]
for %%* in (.) do (set programName=%%~n*)

%javaPath%\javac.exe -classpath .;jl1.0.1.jar *.java
%javaPath%\java.exe -classpath .;jl1.0.1.jar %programName%

pause

8. Copy any convenient MP3 file to the JLayerPausable directory and rename it “Test.mp3”. The file should be long enough to make pausing and unpausing practical.

9. Double-click the icon of the newly created “ProgramBuildAndRun-WithJLayerJar.bat” script to run it. A console window will appear, and the file named Test.mp3 will start playing. Press the enter key to pause and unpause the sound.

Notes

  • If you don’t require pause functionality, a simpler usage of the JLayer package is available in another tutorial.
  • The JLayerPlayerPausable class achieves pausing by completely disposing of almost all of its support objects when pause() is called, only to rebuild them all when resume() is called. I don’t vouch for the code’s efficiency or elegance, but it seems to do the job on the machine this tutorial was tested with.
  • The original intention of this post was just to provide a straightforward list of instructions for recompiling the JLayer jar with a few added lines of code in AdvancedPlayer, as described on another website, but that implementation was deemed unsatisfactory because there was an approximate one-second delay after setting the isPaused flag before the sound actually stopped, which renders it ineffective for certain applications.
  • The correctionFactorInFrames parameter on the JLayerPlayerPausable.play() method is there because otherwise some fraction of a second of the sound gets “lost” after every pause. That is, the playback resumes about half a second later in the sound than when it was stopped. The default value (52 frames) was arrived at by trial and error. I’m not sure how much this value will need to be adjusted from situation to situation.
Advertisements
This entry was posted in Uncategorized and tagged , , , , . Bookmark the permalink.

16 Responses to Pausing an MP3 File Using JLayer

  1. roger says:

    man it feels so dirty to use Thread#stop … and having to add this functionality…yikes…

    • roger says:

      also NB that there is a #stop method available, for followers.

      • I agree that instance of Thread.stop makes the angels cry… come to think of it, it’s in the demo program itself, not in the “JLayerPlayerPausable” class, so I don’t remember now if it’s even necessary. Maybe I’ll look through this again with an eye to decruftification.

        By the way, I’m pretty sure there aren’t any “followers”, because yours is the very first comment I’ve gotten that wasn’t spam. Congratulations!

      • roger says:

        LOL glad to hear it!

  2. Sushil Kumar says:

    The example of JLayerPausableTest that you gave is working perfectly but the song never ends means playbackFinished() functio is never called. Please help.

    • Sorry to hear you’re having trouble, Sushil. You might try the version of JLayerPlayerPausable that your fellow commenter Arthur Assuncao created, and see if that solves your problem. Clicking the “Stop” button on his version seems to be firing the playbackFinished() method on my machine.

  3. Arthur Assuncao says:

    I modified and improved your code and am using a college work, see:
    JLayerPlayerPausable: http://pastebin.com/yZnCa6Nx
    JLayerPausableTest: http://pastebin.com/2K5Bbw4g

    Thanks.

  4. Maximilian Berger says:

    I costumized Arthurs code for a university project and we would like to release it as open source soon. Since we’re not sure how to go about the licencing we’d like to get the GPL off of this class if you agree.

    Modified version:
    http://pastebin.com/Qskrvrqt

    I based a MP3Player class with a lot of transparency ontop of the JLayerPausable let me know if you’re interested to see that as well.

    • I’m not a licensing expert… I basically just left the GPL notice that was already there at the top of that file when I adapted it from JavaZoom’s code, and it looks like Arthur just added a couple of lines at the end of it to indicate that he got it from here and then modified it. You might check out the “How to Use the GNU Licenses for Your Own Software” page from the GNU website at http://www.gnu.org/licenses/gpl-howto.html.

  5. Pingback: MP3 Player Em Java Com JLayer | BSJUG – Baixada Santista Java Users Group

  6. Marcos says:

    como deixar um arquivo mp3 dentro do aplicativo, dentro do classpath do projeto, dentro do .jar com esses metodos tocar, parar, e pausar?
    how to make an mp3 file within the application, within the project classpath, inside. these methods jar with play, stop, and pause?

    • Aron says:

      Use getResources.
      String yourPackage = String.format(“/%s/”, YourClass.class.getPackage().getName();
      yourPackage.replace(“.”, “/”));
      String filePath = YourClass.class.getResource(package+file.mp3).getPath();
      Try.

      • J. Marcos B. says:

        String yourPackage = String.format(“/%s/”, JLayerPausableTest.class.getPackage().getName());
        yourPackage.replace(“.”, “/”);
        String filePath = JLayerPausableTest.class.getResource(“music/” + “demo.mp3”).getPath();

        No work!
        ERROR: The method format(String, Object[]) in the type String is not applicable for the arguments (String, String)

  7. J. Marcos B. says:

    I´m trying to..

    // get mp3 path
    	String mp3 = "music/demo.mp3";
    
    	InputStream in = JLayerPausableTest.class.getClassLoader()
    			.getResourceAsStream(mp3);

    ERROR Here: urlToStreamFrom.openStream(); The method openStream() is undefined for the type InputStream

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s