package com.robonobo.plugin.mp3; import java.io.*; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import javazoom.jlgui.basicplayer.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.PropertyConfigurator; import com.robonobo.common.concurrent.CatchingRunnable; import com.robonobo.common.concurrent.SafetyNet; import com.robonobo.common.pageio.buffer.*; import com.robonobo.common.pageio.paginator.Paginator; import com.robonobo.common.pageio.paginator.QuickStartFilePaginator; import com.robonobo.common.util.ExceptionEvent; import com.robonobo.common.util.ExceptionListener; import com.robonobo.core.api.AudioPlayer; import com.robonobo.core.api.AudioPlayerListener; import com.robonobo.core.api.model.Stream; import com.robonobo.mina.external.buffer.PageBuffer; public class Mp3AudioPlayer implements AudioPlayer { private Log log = LogFactory.getLog(getClass()); private Stream s; private PageBuffer pb; private List<AudioPlayerListener> listeners = new ArrayList<AudioPlayerListener>(); private BasicPlayer basicPlayer; /** * The number of bytes we have progressed through our current inputstream, if any */ private int progressBytes = 0; /** * The number of ms we have seeked to, if any */ private long seekMs = 0; private MP3PlaybackListener listener = new MP3PlaybackListener(); private Status desiredStatus; private ThreadPoolExecutor executor; private boolean ignorePlayerEvents = false; public Mp3AudioPlayer(Stream s, PageBuffer pb, ThreadPoolExecutor executor) { this.s = s; this.pb = pb; this.executor = executor; } public void play() throws IOException { executor.execute(new CatchingRunnable() { public void doRun() throws Exception { try { desiredStatus = Status.Playing; if (basicPlayer == null) { basicPlayer = new BasicPlayer(); basicPlayer.addBasicPlayerListener(listener); basicPlayer.open(new PageBufferInputStream(pb)); seekMs = 0; progressBytes = 0; basicPlayer.play(); } else basicPlayer.resume(); } catch (Exception e) { for (AudioPlayerListener listener : listeners) { listener.onError(e.getMessage()); } } } }); } public void stop() { executor.execute(new CatchingRunnable() { public void doRun() throws Exception { desiredStatus = Status.Stopped; if (basicPlayer != null) { try { basicPlayer.stop(); } catch (BasicPlayerException ignore) { } basicPlayer = null; } } }); } public void pause() throws IOException { executor.execute(new CatchingRunnable() { public void doRun() throws Exception { try { // NOTE due to basicplayer's wonderfulness, if it is still opening the stream (doing its internal // buffering) it won't respect this pause call, and will start playing as soon as it has buffered // enough - see MP3PlaybackListener.stateUpdated for how we handle this desiredStatus = Status.Paused; basicPlayer.pause(); } catch (BasicPlayerException e) { throw new IOException("Caught BasicPlayerException pausing: " + e.getMessage()); } } }); } /** * @param ms * Position in the stream to seek to, as a millisecs offset from start of stream */ public void seek(final long ms) throws IOException { // Can only seek if playing or paused if (desiredStatus == Status.Stopped) { log.error("Can't seek while stopped"); return; } // Work out where we're seeking to in byte terms float wayThrough = (float) ms / s.getDuration(); final int seekBytes = (int) (wayThrough * s.getSize()); // BasicPlayer refuses to seek unless playing a file - rubbish - so we start again // with a new player, keeping track of where we seeked to so our progress callback // stays accurate log.info("Seeking: Restarting playback stream, skipping " + seekBytes + "b"); try { ignorePlayerEvents = true; basicPlayer.stop(); PageBufferInputStream pbis = new PageBufferInputStream(pb); pbis.skip(seekBytes); basicPlayer.open(pbis); progressBytes = 0; seekMs = ms; basicPlayer.play(); if (desiredStatus == Status.Paused) basicPlayer.pause(); } catch (BasicPlayerException e) { log.error("Error seeking", e); stop(); } finally { ignorePlayerEvents = false; } } public void addListener(AudioPlayerListener listener) { listeners.add(listener); } public void removeListener(AudioPlayerListener listener) { listeners.remove(listener); } class MP3PlaybackListener implements BasicPlayerListener { public void opened(java.lang.Object stream, java.util.Map properties) { } public void progress(int bytesread, long microseconds, byte[] pcmdata, java.util.Map properties) { if (desiredStatus != Status.Playing) return; progressBytes = bytesread; for (AudioPlayerListener listener : listeners) { listener.onProgress((seekMs * 1000) + microseconds); } } public void setController(BasicController controller) { } public void stateUpdated(BasicPlayerEvent e) { log.debug("BasicPlayer fired event: " + e.toString()); switch (e.getCode()) { case BasicPlayerEvent.PLAYING: // We might have been paused while we were waiting for enough data to arrive (in which case basicplayer // will ignore our pause() call) - if so, pause now if(desiredStatus == Status.Paused) { try { basicPlayer.pause(); } catch (BasicPlayerException ex) { log.error("Caught exception whilst pausing after buffering", ex); stop(); } return; } for (AudioPlayerListener listener : listeners) { listener.playbackStarted(); } break; case BasicPlayerEvent.RESUMED: for (AudioPlayerListener listener : listeners) { listener.playbackStarted(); } break; case BasicPlayerEvent.EOM: for (AudioPlayerListener listener : listeners) { listener.onCompletion(); } desiredStatus = Status.Stopped; break; } } } public static void main(String[] args) throws Exception { PropertyConfigurator.configureAndWatch("../gui/src/java/log4j.properties"); final Log log = LogFactory.getLog(Mp3AudioPlayer.class); SafetyNet.addListener(new ExceptionListener() { public void onException(ExceptionEvent ex) { log.error("SafetyNet caught exception", ex.getException()); } }); final PrintStream out = System.out; BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); if (args.length != 1) { out.println("Usage: Mp3AudioPlayer <sound file>"); System.exit(1); } // Paginate our file into a source pagebuffer File f = new File(args[0]); FileChannel fc = new FileInputStream(f).getChannel(); Mp3FormatSupportProvider fsp = new Mp3FormatSupportProvider(); Stream s = fsp.getStreamForFile(f); s.setStreamId("id:flarp"); SimplePageInfoStore srcPis = new SimplePageInfoStore(); srcPis.init(s.getStreamId()); final FilePageBuffer srcPb = new FilePageBuffer(s.getStreamId(), f, srcPis); Paginator p = new QuickStartFilePaginator(32 * 1024, f.length(), 0); p.paginate(fc, srcPb); fc.close(); // Now create a dest pb, and spawn a thread to put pages into it SimplePageInfoStore destPis = new SimplePageInfoStore(); destPis.init(s.getStreamId()); File destFile = File.createTempFile("mp3audioplayer", "dat"); final FilePageBuffer destPb = new FilePageBuffer(s.getStreamId(), destFile, destPis); Thread myThread = new Thread(new CatchingRunnable() { public void doRun() throws Exception { for (long pn = 0; pn < srcPb.getTotalPages(); pn++) { out.println("Putting page " + pn); destPb.putPage(srcPb.getPage(pn)); // 1.5 pages per sec == 384kbps... if the test mp3 is higher // than this, decrease this sleep time Thread.sleep(667L); } } }); myThread.start(); // Let's play this sucker ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(8); Mp3AudioPlayer player = new Mp3AudioPlayer(s, destPb, executor); player.addListener(new AudioPlayerListener() { public void onCompletion() { out.println("Playback completed!"); } public void onError(String error) { out.println("Got error: " + error); } public void onProgress(long microsecs) { out.println("Progress: " + microsecs / 1000 + "ms"); } @Override public void playbackStarted() { out.println("Playback started!"); } }); player.play(); while (true) { out.println("Playing: Hit enter to pause"); in.readLine(); player.pause(); out.println("Paused: Hit enter to play"); in.readLine(); player.play(); } } }