/* * ----------------------------------------------------------------------- * File: $Source: /home/keith/cvsroot/projects/LanguageAids/uk/co/dabsol/stribley/sound/SimplePlayer.java,v $ * Version: $Revision: 1387 $ * Last Modified: $Date: 2009-01-30 22:15:16 +0700 (Fri, 30 Jan 2009) $ * ----------------------------------------------------------------------- * Copyright (C) 2004 Keith Stribley <tech@thanlwinsoft.org> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA * ----------------------------------------------------------------------- */ package org.thanlwinsoft.languagetest.sound; import java.io.*; import java.util.Vector; import javax.sound.sampled.*; import java.util.Iterator; /** Class to play audio files with ability to pause and move the play position. * The play can optionally be restricted to a time window within the file. * @author keith */ public class SimplePlayer implements AudioPlayer, Runnable, LineListener { private static int BUFFER_SIZE = 8820; //44100 kbit/s //private static int SKIP_BUFFER_SIZE = 1048576; //44100 kbit/s private static int SOURCE_LINE_BUFFER = 44100; private static int MARK_INVALID_SIZE = 16777216; private float framesPerSec = 44100; // set properly later private int bytesPerFrame = 4;// set properly later private int bytesPerSec = 176400; private File currentFile = null; private SourceDataLine outputLine = null; private AudioInputStream input = null; final static int UNINITIALISED_MODE = 0; final static int PLAYING_MODE = 1; final static int PAUSED_MODE = 2; final static int STOPED_MODE = 3; final static int CLOSE_MODE = 4; private int mode = UNINITIALISED_MODE; private long seekBytes = 0; private long startByteOffset = 0; private long stopByteOffset = -1; private long offset = 0; private long markOffset = 0; private Thread playThread = null; private Vector<AudioPlayListener> listeners = null; private long totalLength = -1; private boolean initialised = false; private boolean boundsChanged = false; /** Creates a new instance of SimplePlayer */ public SimplePlayer() { listeners = new Vector<AudioPlayListener>(); } /** Stops any file currently playing and opens the new audio file ready for * play back. * @param audioFile File to open * @return Returns true if file openned successfully for play back. */ public boolean open(File file, long msLengthHint, boolean forceReopen) { if (currentFile != null) { // clip already loaded if (!forceReopen && file.compareTo(currentFile)==0) { // stop play ready for a setBounds command setMode(STOPED_MODE); return true; } else { if (getMode() == CLOSE_MODE) { // previous mode has still not stopped return false; // ignore request } else if (!stopThread()) return false; // unload previous clip } } try { openInputStream(file); bytesPerFrame = input.getFormat().getFrameSize(); framesPerSec = input.getFormat().getFrameRate(); bytesPerSec = bytesPerFrame * (int)framesPerSec; System.out.println("Frame size: " + bytesPerFrame + " Frame Rate: " + framesPerSec); // reset offsets offset = 0; startByteOffset = 0; if (input.getFrameLength() > 0) { stopByteOffset = input.getFrameLength() * bytesPerFrame; totalLength = stopByteOffset; } else { stopByteOffset = -1; totalLength = -1; } System.out.println("Input file length = " + bytesToMs(totalLength)); //input obtained, now mark start so we can go back to it markOffset = 0; if (input.markSupported()) { input.mark(MARK_INVALID_SIZE); } } catch (UnsupportedAudioFileException uafe) { System.out.println(uafe); return false; } catch (IOException ioe) { System.out.println(ioe); return false; } currentFile = file; //System.out.println("FrameLength = " + input.getFrameLength()); setInitialised(0); setMode(UNINITIALISED_MODE); // spawn thread to do rest of initialisation which may block playThread = new Thread(this); playThread.start(); return true; } protected void openInputStream(File audioFile) throws IOException, UnsupportedAudioFileException { input = AudioSystem.getAudioInputStream(audioFile); AudioFormat audioFormat = input.getFormat(); System.out.println( "Play input audio format=" + audioFormat); System.out.println("Frame length=" + input.getFrameLength()); // Convert compressed audio data to uncompressed PCM format. if ( audioFormat.getEncoding() != AudioFormat.Encoding.PCM_SIGNED ) { AudioFormat newFormat = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, audioFormat.getSampleRate(), 16, audioFormat.getChannels(), audioFormat.getChannels() * 2, audioFormat.getSampleRate(), false ); System.out.println( "Converting audio format to " + newFormat ); AudioInputStream newStream = AudioSystem.getAudioInputStream( newFormat, input ); audioFormat = newFormat; input = newStream; } } /** Called when play button pressed on GUI. If the player is stoped it will cause * play to start. */ public void play() { if (getMode() != CLOSE_MODE) { setMode(PLAYING_MODE); } } /** Method to close the audio file and playback lines. * @return Returns true if file and lines closed successfully. */ public boolean close() { setMode(CLOSE_MODE); // check whether there is a thread to close if (playThread != null) { // wait for play thread to close int waitCount = 0; while (playThread.isAlive()) { sleep(); waitCount++; // prevent infinite loop if (waitCount > 100) { System.out.println("Player thread failed to stop!"); return false; } } playThread = null; } if (outputLine != null) outputLine.close(); System.out.println("Output Line closed"); outputLine = null; return true; } private boolean stopThread() { setMode(CLOSE_MODE); // check whether there is a thread to close if (playThread == null) return true; // wait for play thread to close int waitCount = 0; while (playThread.isAlive()) { sleep(); waitCount++; // prevent infinite loop if (waitCount > 100) { System.out.println("Player thread failed to stop!"); return false; } } playThread = null; return true; } /** Called when fast forward button is pressed on GUI. * When play is paused it will advance the play position in the file. * @param amount Amount to adance in ms. * @return Current play position in ms. */ public void fastForward(long amount) { if (getMode() == PAUSED_MODE) { addSeekBytes(msToBytes(amount)); } } public void pause() { switch (getMode()) { case PAUSED_MODE: setMode(PLAYING_MODE); break; case PLAYING_MODE: setMode(PAUSED_MODE); break; } } /** Called when rewind button is pressed on GUI. * When play is paused it will decurement the play position in the file. * @param amount Amount to rewind in ms. * @return New play position in ms. */ public void rewind(long amount) { if (getMode() == PAUSED_MODE) { addSeekBytes(- msToBytes(amount)); } } public long getStartMs() { return bytesToMs(getStartByteOffset()); } public long getDurationMs() { return bytesToMs(getStopByteOffset() - getStartByteOffset()); } public File getCurrentFile() { return currentFile; } /** * Method to restrict play of the sound file to the specified time window * within the file. The method will adjust the offsets appropriately if they * are out of range. * The actual offsets used can be retrieved using the @see getStartMs() * and @see getDurationMs() methods. * @param start Start time in ms from the beginning of the file * @param duration Length of play window in ms */ public void setBounds(long start, long duration) { long newStart; long newStop; if (start > 0) { newStart = roundOffsetToFrame(msToBytes(start)); } else { newStart = 0; } if (duration > 0) { newStop = roundOffsetToFrame(newStart + msToBytes(duration)); } else { if (totalLength > 0) { newStop = totalLength; } else { newStop = -1; } } // protect the section where we change the valuses used by the // play thread. synchronized (this) { if (newStart != startByteOffset || newStop != stopByteOffset) { startByteOffset = newStart; stopByteOffset = newStop; if (getMode() == PAUSED_MODE) { // bounds have changed so reset to stop mode setMode(STOPED_MODE); } // record change so that current play state doesn't get lost // between threads boundsChanged = true; } } } public void stop() { if (getMode() != CLOSE_MODE) { setMode(STOPED_MODE); } } /** * This is the thread that actually plays the data. * To avoid threading problems and avoid hanging the GUI all manipulation * of the audio output line is controlled in this thread. No calls to outputLine * should be made in any methods not confined to this thread. * The variables startOffsetByte, stopOffsetByte, offset and mode are all * modified in both this thread and the GUI thread. Hence synchronzed * getters and setters are used to access them. */ public void run() { System.out.println("Player thread spawned"); DataLine.Info info = new DataLine.Info(SourceDataLine.class, input.getFormat()); // format is an AudioFormat object int oldMode = UNINITIALISED_MODE; // for debugging if (!AudioSystem.isLineSupported(info)) { // Handle the error. setMode(CLOSE_MODE); System.out.println("Audio Line not supported"); } else { // Obtain and open the line. try { // these methods may block hence they are in player thread not in // open method called by gui // trigger start of counter if (outputLine == null || outputLine.isOpen() ) { setInitialised(0); System.out.println("Openning Line..."); outputLine = (SourceDataLine) AudioSystem.getLine(info); outputLine.open(input.getFormat(), SOURCE_LINE_BUFFER); showPlayPosition(); System.out.println("Audio connection openned"); } // while line was being openned start and stop may have been set setInitialised(1); seekToStart(); setInitialised(100); } catch (LineUnavailableException ex) { // Handle the error. //... System.out.println(ex); setMode(CLOSE_MODE); } } byte buffer[] = new byte[BUFFER_SIZE]; do { if (getMode() != oldMode) { oldMode = getMode(); System.out.println("Mode change to " + oldMode); } // This is the state machine of the player switch (getMode()) { case PLAYING_MODE: try { // check that current position hasn't been left before // the start if (isBeforeStart()) { // skip to start before sending any more data setInitialised(1); seekToStart(); setInitialised(100); } // We don't want to block on the write // since if we then stop the line // the thread will block indefinitely // Therefore see how empty the buffer is // and read data in accordingly int bufferSpace = outputLine.available(); if (bufferSpace > BUFFER_SIZE) { bufferSpace = BUFFER_SIZE; } // round to nearest frame bufferSpace = roundOffsetToFrame(bufferSpace); // if buffer isn't already topped up if (bufferSpace > 0) { //System.out.println("buffer space ="+ bufferSpace); int bytesRead = input.read(buffer,0,bufferSpace); if (bytesRead > 0) { bytesRead = roundOffsetToFrame(bytesRead); int wrote = outputLine.write(buffer, 0, bytesRead); if (wrote != bytesRead) { System.out.println("Wrote " + wrote + "/" + bytesRead); } incrementOffset(wrote); } else { // no more data System.out.println("Byte length = " + offset); // end of file if (bytesRead == -1) { synchronized (this) { stopByteOffset = offset; } System.out.println("Asked for:" + bufferSpace + " Got " + bytesRead); } } // now that there is at least some data in the // buffer start the line // check if end has been reached if (hasReachedEnd()) { /* // for a very short clip buffer might not have // been filled so in that case start play now if (!outputLine.isRunning()) outputLine.start(); // wait for line to play out data in buffer // before resetting do { sleep(); showPlayPosition(); } while (outputLine.isActive()); */ // if bounds have changed don't set stop mode // since the next play may already have been // called synchronized (this) { if (!boundsChanged) { setMode(STOPED_MODE); } } seekToStart(); setInitialised(100); } } // otherwise buffer is full else { // wait for some data to be used up sleep(); } // if play not started start it now if (!outputLine.isRunning()) { // in this case play hasn't started so now there is // a buffer's worth of data start play outputLine.start(); // allow to take effect sleep(); } showPlayPosition(); } catch (IOException ie) { System.out.println(ie); setMode(CLOSE_MODE); } //sleep(); break; case PAUSED_MODE: if (outputLine.isRunning()) outputLine.stop(); try { long currentSeek = getSeekBytes(); if (currentSeek > 0) { if (currentSeek > input.available() || (getStopByteOffset() > 0 && currentSeek > getStopByteOffset())) { setMode(STOPED_MODE); seekToStart(); } else { // skip will take initialisation state below 100% skip(currentSeek); } // make sure that initialised is set back to true setInitialised(100); // position moved so flush data in buffer outputLine.stop(); outputLine.flush(); } else if (currentSeek < 0) // to go back, reset to start and skip forward { long targetOffset = getOffset() - getSeekBytes(); seekToStart(); if (getOffset() > getStartByteOffset()) { skip(targetOffset); } // make sure that initialised is set back to true setInitialised(100); // position moved so flush data in buffer outputLine.stop(); outputLine.flush(); } showPlayPosition(); } catch (IOException ie) { System.out.println(ie); setMode(CLOSE_MODE); } sleep(); break; case STOPED_MODE: // Stopped mode should only be entered if stop is pressed. if (outputLine.isRunning()) { System.out.println("Stopping"); outputLine.stop(); outputLine.flush(); seekToStart(); // make sure that initialised is set back to true setInitialised(100); } else if (getOffset() != getStartByteOffset()) { outputLine.stop(); outputLine.flush(); seekToStart(); // make sure that initialised is set back to true setInitialised(100); } else { sleep(); } break; case CLOSE_MODE: break; default: sleep(); } } while (mode != CLOSE_MODE); // tidy up setInitialised(0); if (outputLine != null) { if (outputLine.isActive()) { outputLine.drain(); outputLine.stop(); } else { outputLine.stop(); outputLine.flush(); } } try { input.close(); input = null; currentFile = null; } catch (IOException e) { System.out.println(e); } setMode(UNINITIALISED_MODE); System.out.println("Player thread finished"); } /** Method calls the playPosition method on each registered * AudioPlayListener. * The method takes into account that the current play position will be * behind the current offset value by the amount the amount of unused data * in the outputLine buffer. */ protected void showPlayPosition() { Iterator<AudioPlayListener> l = listeners.iterator(); while (l.hasNext()) { ((AudioPlayListener)l.next()) .playPosition(bytesToMs(getOffset() - SOURCE_LINE_BUFFER + outputLine.available()), bytesToMs(totalLength)); } } /** * Method to seek to start of play window within the current file. * This should only be called from player thread. */ private void seekToStart() { try { synchronized (this) { boundsChanged = false; } setInitialised(1); // only just started so inform listeners do { // stop start changing during execution final long targetStart = getStartByteOffset(); if (getOffset() > targetStart) // need to rewind in someway { if (!input.markSupported() || markOffset > targetStart || getOffset() >= markOffset + MARK_INVALID_SIZE) { // need to reopen file input.close(); input = null; openInputStream(currentFile); markOffset = 0; System.out.println("Reopenning audiofile"); } else { input.reset(); System.out.println("Resetting audiofile"); } setOffset(markOffset); } seekBytes = 0; skip(targetStart); synchronized (this) { // if start byte offset hasn't been changed set it to current // seek position in case there is a discripancy due to frame // sizes if (targetStart == startByteOffset) { // set to this because may not be same as seek position startByteOffset = getOffset(); // exit loop break; } } // check on when to exit loop is inside synchronised block } while (getMode() != CLOSE_MODE); // now reset mark to start if (input.markSupported()) { input.mark(MARK_INVALID_SIZE); markOffset = getOffset(); } showPlayPosition(); } catch (UnsupportedAudioFileException uafe) { // unlikely unless file has changed on disk System.out.println(uafe); setMode(CLOSE_MODE); } catch (IOException ie) { System.out.println(ie); setMode(CLOSE_MODE); } } private void skip(long targetOffset) throws IOException { //byte buffer[] = new byte[SKIP_BUFFER_SIZE]; int zeroSkips = 0; double skipDistance = (double)(targetOffset - getOffset()); long skipProgress = 0; System.out.println("Skipping " + skipDistance + " bytes..."); // get within a frame of desired start position while (getOffset() <= targetOffset - bytesPerFrame && getMode() != CLOSE_MODE) { long skipped = 0; long targetSkip = targetOffset - getOffset(); // limit skip to a 1 seconds worth to show progress if (targetSkip > bytesPerSec) targetSkip = bytesPerSec; skipped = input.skip(roundOffsetToFrame(targetSkip)); // if skipped isn't making any progress then break to prevent // infinite loop if (skipped <= 0) { if (++zeroSkips > 10) { System.out.println("Only skipped " + skipped + " not " + targetSkip); return; } sleep(); // wait a bit } else { incrementOffset(skipped); skipProgress += skipped; zeroSkips = 0; setInitialised((int)Math.floor((double)skipProgress * 100.0 / skipDistance)); } } System.out.println("Skipped to " + targetOffset); } private void sleep() { try { Thread.sleep(10); } catch (InterruptedException e) { // just return immediately } } synchronized protected void setMode(int newMode) { // if the mode has been set to close don't allow any transition // except to uninitialised if (mode == CLOSE_MODE && newMode != UNINITIALISED_MODE) return; mode = newMode; } synchronized protected void addSeekBytes(long bytes) { seekBytes += bytes; } synchronized protected long getSeekBytes() { long currentSeek = seekBytes; seekBytes = 0; return currentSeek; } synchronized protected int getMode() { return mode; } /** * Returns the current position in the input file of data that has not yet * been read into the output buffer. * At one point offset was modified in both GUI and player threads, but * this is no longer the case, so method does not need to be * synchronzed anymore. * @return offset from start of audio file in bytes */ protected long getOffset() { return offset; } /** * Sets the current position in the input file of data that has not yet * been read into the output buffer. * At one point offset was modified in both GUI and player threads, but * this is no longer the case, so method does not need to be * synchronzed anymore. * @param newOffset current offset from start of audio file in bytes */ protected void setOffset(long newOffset) { offset = newOffset; //System.out.println("Play offset: " + newOffset); } /** * Sets the current position in the input file of data that has not yet * been read into the output buffer. * At one point offset was modified in both GUI and player threads, but * this is no longer the case, so method does not need to be * synchronzed anymore. * @param deltaOffset additional bytes to increment offset by */ protected void incrementOffset(long deltaOffset) { offset += deltaOffset; //System.out.println("Play offset: " + offset); } /** * Gets the offset of the start of the play window within the file. The value * of this variable is set in the GUI thread by setBounds(). This method is * synchronized to allow the player thread to access the variable without * problems. * @return First byte to be read in play window */ synchronized protected long getStartByteOffset() { return startByteOffset; } protected int roundOffsetToFrame(int newOffset) { return (int)(Math.floor((float)newOffset / (float)bytesPerFrame)) * bytesPerFrame; } protected long roundOffsetToFrame(long newOffset) { return (long)(Math.floor((float)newOffset / (float)bytesPerFrame)) * bytesPerFrame; } protected synchronized boolean hasReachedEnd() { boolean atEnd = false; if (getStopByteOffset() > -1) { if (offset >= getStopByteOffset()) atEnd = true; } return atEnd; } protected synchronized boolean isBeforeStart() { boolean beforeStart = false; if (offset < getStartByteOffset() - bytesPerFrame) beforeStart = true; return beforeStart; } /** * Gets the offset of the end of the play window within the file. The value * of this variable is set in the GUI thread by setBounds(). This method is * synchronized to allow the player thread to access the variable without * problems. * @return First byte to be read in play window */ synchronized protected long getStopByteOffset() { return stopByteOffset; } protected long bytesToMs(long value) { // treat -1 specially if (value == -1) return value; float byteOffset = (float)value; float secondsOffset = byteOffset / (float)(bytesPerFrame * framesPerSec); return (long)(secondsOffset * 1000); } protected long msToBytes(long value) { // treat -1 specially if (value == -1) return value; float secOffset = ((float)value) / 1000; float byteOffset = secOffset * bytesPerFrame * framesPerSec; return (long)(byteOffset); } public void addPlayListener(AudioPlayListener listener) { listeners.add(listener); } public void update(javax.sound.sampled.LineEvent lineEvent) { if (lineEvent.getType() == LineEvent.Type.STOP) { // if in play mode must have stopped because of lack of data // otherwise stop will be due to pause if (getMode() == PLAYING_MODE) { setMode(STOPED_MODE); } System.out.println("Play has stopped"); showPlayPosition(); } } public synchronized boolean isInitialised() { return initialised; } protected synchronized void setInitialised(int percent) { if ((percent == 100) && (mode == UNINITIALISED_MODE)) { mode = STOPED_MODE; initialised = true; } else { initialised = false; } Iterator<AudioPlayListener> l = listeners.iterator(); while (l.hasNext()) { ((AudioPlayListener)l.next()).initialisationProgress(percent); } } public boolean open(File file) { return open(file, AudioSystem.NOT_SPECIFIED, false); } public boolean isActive() { return outputLine.isActive(); } }