Java tutorial
/** * BasicMp3Player. * *----------------------------------------------------------------------- * 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. *---------------------------------------------------------------------- */ package com.player; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.sound.sampled.AudioFileFormat; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Control; import javax.sound.sampled.DataLine; import javax.sound.sampled.FloatControl; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; import javax.sound.sampled.UnsupportedAudioFileException; import javazoom.jlgui.basicplayer.BasicController; import javazoom.jlgui.basicplayer.BasicPlayerEvent; import javazoom.jlgui.basicplayer.BasicPlayerEventLauncher; import javazoom.jlgui.basicplayer.BasicPlayerException; import javazoom.jlgui.basicplayer.BasicPlayerListener; import javazoom.spi.PropertiesContainer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.tritonus.share.sampled.TAudioFormat; import org.tritonus.share.sampled.file.TAudioFileFormat; /** * BasicPlayer is a threaded audio javazoom.jlgui.player.test. */ public class BasicMP3Player extends BasicPlayer implements BasicController, Runnable { private static final int EXTERNAL_BUFFER_SIZE = 4000 * 4; private static final int SKIP_INACCURACY_SIZE = 1200; private Thread m_thread = null; private Object m_dataSource; private AudioInputStream m_encodedaudioInputStream; private int encodedLength = -1; private AudioInputStream m_audioInputStream; private AudioFileFormat m_audioFileFormat; private SourceDataLine m_line; private FloatControl m_gainControl; private FloatControl m_panControl; private int lineBufferSize = -1; private long threadSleep = -1; private static Log log = LogFactory.getLog(BasicPlayer.class); /** * These variables are used to distinguish stopped, paused, playing states. We need them to * control Thread. */ public static final int UNKNOWN = -1; public static final int PLAYING = 0; public static final int PAUSED = 1; public static final int STOPPED = 2; public static final int OPENED = 3; public static final int SEEKING = 4; private int m_status = UNKNOWN; // Listeners to be notified. private Collection m_listeners = null; private Map empty_map = new HashMap(); /** * Constructs a Basic Player. */ public BasicMP3Player() { setSupportedFileTypeExtensions(); m_dataSource = null; m_listeners = new ArrayList(); reset(); } protected void reset() { m_status = UNKNOWN; if (m_audioInputStream != null) { synchronized (m_audioInputStream) { closeStream(); } } m_audioInputStream = null; m_audioFileFormat = null; m_encodedaudioInputStream = null; encodedLength = -1; if (m_line != null) { m_line.stop(); m_line.close(); m_line = null; } m_gainControl = null; m_panControl = null; } public void addBasicPlayerListener(BasicPlayerListener bpl) { m_listeners.add(bpl); } /** * Set SourceDataLine buffer size. It affects audio latency. (the delay between line.write(data) * and real sound). Minimum value should be over 10000 bytes. * * @param size * -1 means maximum buffer size available. */ public void setLineBufferSize(int size) { lineBufferSize = size; } /** * Return SourceDataLine buffer size. * * @return -1 maximum buffer size. */ public int getLineBufferSize() { return lineBufferSize; } /** * Set thread sleep time. Default is -1 (no sleep time). * * @param time * in milliseconds. */ public void setSleepTime(long time) { threadSleep = time; } /** * Return thread sleep time in milliseconds. * * @return -1 means no sleep time. */ public long getSleepTime() { return threadSleep; } /** * Returns BasicPlayer status. * * @return status */ public int getStatus() { return m_status; } /** * Open file to play. */ public void open(File file) throws BasicPlayerException { log.info("open(" + file + ")"); if (file != null) { m_dataSource = file; initAudioInputStream(); } } /** * Open URL to play. */ public void open(URL url) throws BasicPlayerException { log.info("open(" + url + ")"); if (url != null) { m_dataSource = url; initAudioInputStream(); } } /** * Open inputstream to play. */ public void open(InputStream inputStream) throws BasicPlayerException { log.info("open(" + inputStream + ")"); if (inputStream != null) { m_dataSource = inputStream; initAudioInputStream(); } } /** * Inits AudioInputStream and AudioFileFormat from the data source. * * @throws BasicPlayerException */ private void initAudioInputStream() throws BasicPlayerException { try { reset(); notifyEvent(BasicPlayerEvent.OPENING, getEncodedStreamPosition(), -1, m_dataSource); if (m_dataSource instanceof URL) { initAudioInputStream((URL) m_dataSource); } else if (m_dataSource instanceof File) { initAudioInputStream((File) m_dataSource); } else if (m_dataSource instanceof InputStream) { initAudioInputStream((InputStream) m_dataSource); } createLine(); // Notify listeners with AudioFileFormat properties. Map properties = null; if (m_audioFileFormat instanceof TAudioFileFormat) { // Tritonus SPI compliant audio file format. properties = ((TAudioFileFormat) m_audioFileFormat).properties(); // Clone the Map because it is not mutable. properties = deepCopy(properties); } else properties = new HashMap(); // Add JavaSound properties. if (m_audioFileFormat.getByteLength() > 0) properties.put("audio.length.bytes", new Integer(m_audioFileFormat.getByteLength())); if (m_audioFileFormat.getFrameLength() > 0) properties.put("audio.length.frames", new Integer(m_audioFileFormat.getFrameLength())); if (m_audioFileFormat.getType() != null) properties.put("audio.type", (m_audioFileFormat.getType().toString())); // Audio format. AudioFormat audioFormat = m_audioFileFormat.getFormat(); if (audioFormat.getFrameRate() > 0) properties.put("audio.framerate.fps", new Float(audioFormat.getFrameRate())); if (audioFormat.getFrameSize() > 0) properties.put("audio.framesize.bytes", new Integer(audioFormat.getFrameSize())); if (audioFormat.getSampleRate() > 0) properties.put("audio.samplerate.hz", new Float(audioFormat.getSampleRate())); if (audioFormat.getSampleSizeInBits() > 0) properties.put("audio.samplesize.bits", new Integer(audioFormat.getSampleSizeInBits())); if (audioFormat.getChannels() > 0) properties.put("audio.channels", new Integer(audioFormat.getChannels())); if (audioFormat instanceof TAudioFormat) { // Tritonus SPI compliant audio format. Map addproperties = ((TAudioFormat) audioFormat).properties(); properties.putAll(addproperties); } Iterator it = m_listeners.iterator(); while (it.hasNext()) { BasicPlayerListener bpl = (BasicPlayerListener) it.next(); bpl.opened(m_dataSource, properties); } m_status = OPENED; notifyEvent(BasicPlayerEvent.OPENED, getEncodedStreamPosition(), -1, null); } catch (LineUnavailableException e) { throw new BasicPlayerException(e); } catch (UnsupportedAudioFileException e) { throw new BasicPlayerException(e); } catch (IOException e) { throw new BasicPlayerException(e); } } /** * Inits Audio ressources from file. */ private void initAudioInputStream(File file) throws UnsupportedAudioFileException, IOException { m_audioInputStream = AudioSystem.getAudioInputStream(file); m_audioFileFormat = AudioSystem.getAudioFileFormat(file); } /** * Inits Audio ressources from URL. */ private void initAudioInputStream(URL url) throws UnsupportedAudioFileException, IOException { m_audioInputStream = AudioSystem.getAudioInputStream(url); m_audioFileFormat = AudioSystem.getAudioFileFormat(url); } /** * Inits Audio ressources from InputStream. */ private void initAudioInputStream(InputStream inputStream) throws UnsupportedAudioFileException, IOException { m_audioInputStream = AudioSystem.getAudioInputStream(inputStream); m_audioFileFormat = AudioSystem.getAudioFileFormat(inputStream); } /** * Inits Audio ressources from AudioSystem.<br> */ protected void initLine() throws LineUnavailableException { log.info("initLine()"); if (m_line == null) createLine(); if (!m_line.isOpen()) { openLine(); } else { AudioFormat lineAudioFormat = m_line.getFormat(); AudioFormat audioInputStreamFormat = m_audioInputStream == null ? null : m_audioInputStream.getFormat(); if (!lineAudioFormat.equals(audioInputStreamFormat)) { m_line.close(); openLine(); } } } /** * Inits a DateLine.<br> * We check if the line supports Gain and Pan controls. From the AudioInputStream, i.e. from the * sound file, we fetch information about the format of the audio data. These information include * the sampling frequency, the number of channels and the size of the samples. There information * are needed to ask JavaSound for a suitable output line for this audio file. Furthermore, we * have to give JavaSound a hint about how big the internal buffer for the line should be. Here, * we say AudioSystem.NOT_SPECIFIED, signaling that we don't care about the exact size. JavaSound * will use some default value for the buffer size. */ private void createLine() throws LineUnavailableException { log.info("Create Line"); if (m_line == null) { AudioFormat sourceFormat = m_audioInputStream.getFormat(); log.info("Create Line : Source format : " + sourceFormat.toString()); AudioFormat targetFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), 16, sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false); log.info("Create Line : Target format: " + targetFormat); // Keep a reference on encoded stream to progress notification. m_encodedaudioInputStream = m_audioInputStream; try { // Get total length in bytes of the encoded stream. encodedLength = m_encodedaudioInputStream.available(); } catch (IOException e) { log.error("Cannot get m_encodedaudioInputStream.available()", e); } // Create decoded stream. m_audioInputStream = AudioSystem.getAudioInputStream(targetFormat, m_audioInputStream); AudioFormat audioFormat = m_audioInputStream.getFormat(); DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, AudioSystem.NOT_SPECIFIED); m_line = (SourceDataLine) AudioSystem.getLine(info); /*-- Display supported controls --*/ Control[] c = m_line.getControls(); for (int p = 0; p < c.length; p++) { log.debug("Controls : " + c[p].toString()); } /*-- Is Gain Control supported ? --*/ if (m_line.isControlSupported(FloatControl.Type.MASTER_GAIN)) { m_gainControl = (FloatControl) m_line.getControl(FloatControl.Type.MASTER_GAIN); log.info("Master Gain Control : [" + m_gainControl.getMinimum() + "," + m_gainControl.getMaximum() + "] " + m_gainControl.getPrecision()); } /*-- Is Pan control supported ? --*/ if (m_line.isControlSupported(FloatControl.Type.PAN)) { m_panControl = (FloatControl) m_line.getControl(FloatControl.Type.PAN); log.info("Pan Control : [" + m_panControl.getMinimum() + "," + m_panControl.getMaximum() + "] " + m_panControl.getPrecision()); } } } /** * Opens the line. */ private void openLine() throws LineUnavailableException { if (m_line != null) { AudioFormat audioFormat = m_audioInputStream.getFormat(); int buffersize = lineBufferSize; if (buffersize <= 0) buffersize = m_line.getBufferSize(); m_line.open(audioFormat, buffersize); log.info("Open Line : BufferSize=" + buffersize); } } /** * Stops the playback.<br> * Player Status = STOPPED.<br> * Thread should free Audio ressources. */ protected void stopPlayback() { if ((m_status == PLAYING) || (m_status == PAUSED)) { if (m_line != null) { m_line.flush(); m_line.stop(); } m_status = STOPPED; notifyEvent(BasicPlayerEvent.STOPPED, getEncodedStreamPosition(), -1, null); synchronized (m_audioInputStream) { closeStream(); } log.info("stopPlayback() completed"); } } /** * Pauses the playback.<br> * Player Status = PAUSED. */ protected void pausePlayback() { if (m_line != null) { if (m_status == PLAYING) { m_line.flush(); m_line.stop(); m_status = PAUSED; log.info("pausePlayback() completed"); notifyEvent(BasicPlayerEvent.PAUSED, getEncodedStreamPosition(), -1, null); } } } /** * Resumes the playback.<br> * Player Status = PLAYING. */ protected void resumePlayback() { if (m_line != null) { if (m_status == PAUSED) { m_line.start(); m_status = PLAYING; log.info("resumePlayback() completed"); notifyEvent(BasicPlayerEvent.RESUMED, getEncodedStreamPosition(), -1, null); } } } /** * Starts playback. */ protected void startPlayback() throws BasicPlayerException { if (m_status == STOPPED) initAudioInputStream(); if (m_status == OPENED) { log.info("startPlayback called"); if (!(m_thread == null || !m_thread.isAlive())) { log.info("WARNING: old thread still running!!"); int cnt = 0; while (m_status != OPENED) { try { if (m_thread != null) { log.info("Waiting ... " + cnt); cnt++; Thread.sleep(1000); if (cnt > 2) { m_thread.interrupt(); } } } catch (InterruptedException e) { throw new BasicPlayerException("Wait error", e); } } } // Open SourceDataLine. try { initLine(); } catch (LineUnavailableException e) { throw new BasicPlayerException("Cannot init line", e); } log.info("Creating new thread"); m_thread = new Thread(this); m_thread.start(); if (m_line != null) { m_line.start(); m_status = PLAYING; notifyEvent(BasicPlayerEvent.PLAYING, getEncodedStreamPosition(), -1, null); } } } /** * Main loop. Player Status == STOPPED || SEEKING => End of Thread + Freeing Audio Ressources.<br> * Player Status == PLAYING => Audio stream data sent to Audio line.<br> * Player Status == PAUSED => Waiting for another status. */ public void run() { log.info("Thread Running"); int nBytesRead = 1; byte[] abData = new byte[EXTERNAL_BUFFER_SIZE]; // Lock stream while playing. synchronized (m_audioInputStream) { // Main play/pause loop. while ((nBytesRead != -1) && (m_status != STOPPED) && (m_status != SEEKING) && (m_status != UNKNOWN)) { if (m_status == PLAYING) { // Play. try { nBytesRead = m_audioInputStream.read(abData, 0, abData.length); if (nBytesRead >= 0) { byte[] pcm = new byte[nBytesRead]; System.arraycopy(abData, 0, pcm, 0, nBytesRead); m_line.write(abData, 0, nBytesRead); // Compute position in bytes in encoded stream. int nEncodedBytes = getEncodedStreamPosition(); // Notify listeners Iterator it = m_listeners.iterator(); while (it.hasNext()) { BasicPlayerListener bpl = (BasicPlayerListener) it.next(); if (m_audioInputStream instanceof PropertiesContainer) { // Pass audio parameters such as instant // bitrate, ... Map properties = ((PropertiesContainer) m_audioInputStream).properties(); bpl.progress(nEncodedBytes, m_line.getMicrosecondPosition(), pcm, properties); } else bpl.progress(nEncodedBytes, m_line.getMicrosecondPosition(), pcm, empty_map); } } } catch (IOException e) { log.error("Thread cannot run()", e); m_status = STOPPED; notifyEvent(BasicPlayerEvent.STOPPED, getEncodedStreamPosition(), -1, null); } // Nice CPU usage. if (threadSleep > 0) { try { Thread.sleep(threadSleep); } catch (InterruptedException e) { log.error("Thread cannot sleep(" + threadSleep + ")", e); } } } else { // Pause try { Thread.sleep(1000); } catch (InterruptedException e) { log.error("Thread cannot sleep(1000)", e); } } } // Free audio resources. if (m_line != null) { m_line.drain(); m_line.stop(); m_line.close(); m_line = null; } // Notification of "End Of Media" if (nBytesRead == -1) { notifyEvent(BasicPlayerEvent.EOM, getEncodedStreamPosition(), -1, null); } // Close stream. closeStream(); } m_status = STOPPED; notifyEvent(BasicPlayerEvent.STOPPED, getEncodedStreamPosition(), -1, null); log.info("Thread completed"); } /** * Skip bytes in the File inputstream. It will skip N frames matching to bytes, so it will never * skip given bytes length exactly. * * @param bytes * @return value>0 for File and value=0 for URL and InputStream * @throws BasicPlayerException */ protected long skipBytes(long bytes) throws BasicPlayerException { long totalSkipped = 0; if (m_dataSource instanceof File) { log.info("Bytes to skip : " + bytes); int previousStatus = m_status; m_status = SEEKING; long skipped = 0; try { synchronized (m_audioInputStream) { notifyEvent(BasicPlayerEvent.SEEKING, getEncodedStreamPosition(), -1, null); initAudioInputStream(); if (m_audioInputStream != null) { // Loop until bytes are really skipped. while (totalSkipped < (bytes - SKIP_INACCURACY_SIZE)) { skipped = m_audioInputStream.skip(bytes - totalSkipped); totalSkipped = totalSkipped + skipped; log.info("Skipped : " + totalSkipped + "/" + bytes); if (totalSkipped == -1) throw new BasicPlayerException("Skip not supported"); } } } notifyEvent(BasicPlayerEvent.SEEKED, getEncodedStreamPosition(), -1, null); m_status = OPENED; if (previousStatus == PLAYING) startPlayback(); else if (previousStatus == PAUSED) { startPlayback(); pausePlayback(); } } catch (IOException e) { throw new BasicPlayerException(e); } } return totalSkipped; } /** * Notify listeners about a BasicPlayerEvent. * * @param code * event code. * @param position * in the stream when the event occurs. */ protected void notifyEvent(int code, int position, double value, Object description) { /* * Iterator it = m_listeners.iterator(); while (it.hasNext()) { BasicPlayerListener bpl = * (BasicPlayerListener) it.next(); bpl.stateUpdated(new * BasicPlayerEvent(this,code,position,value,description)); } */ BasicPlayerEventLauncher trigger = new BasicPlayerEventLauncher(code, position, value, description, m_listeners, this); trigger.start(); } protected int getEncodedStreamPosition() { int nEncodedBytes = -1; if (m_dataSource instanceof File) { try { if (m_encodedaudioInputStream != null) { nEncodedBytes = encodedLength - m_encodedaudioInputStream.available(); } } catch (IOException e) { // log.debug("Cannot get // m_encodedaudioInputStream.available()",e); } } return nEncodedBytes; } protected void closeStream() { // Close stream. try { if (m_audioInputStream != null) { m_audioInputStream.close(); log.info("Stream closed"); } } catch (IOException e) { log.info("Cannot close stream", e); } } /** * Returns true if Gain control is supported. */ public boolean hasGainControl() { return m_gainControl != null; } /** * Returns Gain value. */ public float getGainValue() { if (hasGainControl()) { return m_gainControl.getValue(); } return 0.0F; } /** * Gets max Gain value. */ public float getMaximumGain() { if (hasGainControl()) { return m_gainControl.getMaximum(); } return 0.0F; } /** * Gets min Gain value. */ public float getMinimumGain() { if (hasGainControl()) { return m_gainControl.getMinimum(); } return 0.0F; } /** * Returns true if Pan control is supported. */ public boolean hasPanControl() { return m_panControl != null; } /** * Returns Pan precision. */ public float getPrecision() { if (hasPanControl()) { return m_panControl.getPrecision(); } return 0.0F; } /** * Returns Pan value. */ public float getPan() { if (hasPanControl()) { return m_panControl.getValue(); } return 0.0F; } /** * Deep copy of a Map. * * @param src * @return */ private Map deepCopy(Map src) { HashMap map = new HashMap(); if (src != null) { Iterator it = src.keySet().iterator(); while (it.hasNext()) { Object key = it.next(); Object value = src.get(key); map.put(key, value); } } return map; } /** * @see javazoom.jlgui.basicplayer.BasicController#seek(long) */ public long seek(long bytes) throws BasicPlayerException { return skipBytes(bytes); } /** * @see javazoom.jlgui.basicplayer.BasicController#play() */ public void play() throws BasicPlayerException { startPlayback(); } /** * @see javazoom.jlgui.basicplayer.BasicController#stop() */ public void stop() throws BasicPlayerException { stopPlayback(); } /** * @see javazoom.jlgui.basicplayer.BasicController#pause() */ public void pause() throws BasicPlayerException { pausePlayback(); } /** * @see javazoom.jlgui.basicplayer.BasicController#resume() */ public void resume() throws BasicPlayerException { resumePlayback(); } /** * Sets Pan value. Linear scale : -1.0 <--> +1.0 */ public void setPan(double fPan) throws BasicPlayerException { if (hasPanControl()) { log.debug("Pan : " + fPan); m_panControl.setValue((float) fPan); notifyEvent(BasicPlayerEvent.PAN, getEncodedStreamPosition(), fPan, null); } else throw new BasicPlayerException("Pan control not supported"); } /** * Sets Gain value. Linear scale 0.0 <--> 1.0 Threshold Coef. : 1/2 to avoid saturation. */ public void setGain(double fGain) throws BasicPlayerException { if (hasGainControl()) { double minGainDB = getMinimumGain(); double ampGainDB = ((10.0f / 20.0f) * getMaximumGain()) - getMinimumGain(); double cste = Math.log(10.0) / 20; double valueDB = minGainDB + (1 / cste) * Math.log(1 + (Math.exp(cste * ampGainDB) - 1) * fGain); log.debug("Gain : " + valueDB); m_gainControl.setValue((float) valueDB); notifyEvent(BasicPlayerEvent.GAIN, getEncodedStreamPosition(), fGain, null); } else throw new BasicPlayerException("Gain control not supported"); } public void setSupportedFileTypeExtensions() { supportedFileTypeExtensions = new String[] { ".mp3", ".ogg" }; } }