/**
* Copyright 2008 - 2012
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* @project loon
* @author cping
* @email:javachenpeng@yahoo.com
* @version 0.3.3
*/
package loon.media;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.Control;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
/**
* Reformatted and adapted version of BigClip as posted to:
* http://stackoverflow.
* com/questions/9470148/how-do-you-play-a-long-audio-clip-in-java
*
* An implementation of the javax.sound.sampled.Clip that is designed to handle
* Clips of arbitrary size, limited only by the amount of memory available to
* the app. It uses the post 1.4 thread behaviour (daemon thread) that will stop
* the sound running after the main has exited.
* <ul>
* <li>2012-12-18 - Fixed bug with LOOP_CONTINUOUSLY and some bugs with drain()
* and buffer sizes.
* <li>2012-02-29 - Reworked play/loop to fix several bugs.
* <li>2009-09-01 - Fixed bug that had clip ..clipped at the end, by calling
* drain() (before calling stop()) on the dataline after the play loop was
* complete. Improvement to frame and microsecond position determination.
* <li>2009-08-17 - added convenience constructor that accepts a Clip. Changed
* the private convertFrameToM..seconds methods from 'micro' to 'milli' to
* reflect that they were dealing with units of 1000/th of a second.
* <li>2009-08-14 - got rid of flush() after the sound loop, as it was cutting
* off tracks just before the end, and was found to be not needed for the
* fast-forward/rewind functionality it was introduced to support.
* <li>2009-08-11 - First binary release.
* </ul>
* N.B. Remove @Override notation and logging to use in 1.3+
*
* @since 1.5
* @version 2012-12-18
* @author Andrew Thompson
* @author Alejandro Garcia
* @author Michael Thomas
*/
class BigClip implements Clip, LineListener {
/** The DataLine used by this Clip. */
private SourceDataLine dataLine;
/** The raw bytes of the audio data. */
private byte[] audioData;
/** The stream wrapper for the audioData. */
private ByteArrayInputStream inputStream;
/** Loop count set by the calling code. */
private int loopCount = 1;
/** Internal count of how many loops to go. */
private int countDown = 1;
/** The start of a loop point. Defaults to 0. */
private int loopPointStart;
/** The end of a loop point. Defaults to the end of the Clip. */
private int loopPointEnd;
/** Stores the current frame position of the clip. */
private int framePosition;
/** Thread used to run() sound. */
private Thread thread;
/** Whether the sound is currently playing or active. */
private boolean active;
/** Stores the last time bytes were dumped to the audio stream. */
private long timelastPositionSet;
private int bufferUpdateFactor = 2;
/**
* Default constructor for a BigClip. Does nothing. Information from the
* AudioInputStream passed in open() will be used to get an appropriate
* SourceDataLine.
*/
public BigClip() {
}
/**
* There are a number of AudioSystem methods that will return a configured
* Clip. This convenience constructor allows us to obtain a SourceDataLine
* for the BigClip that uses the same AudioFormat as the original Clip.
*
* @param clip
* Clip The Clip used to configure the BigClip.
*/
public BigClip(Clip clip) throws LineUnavailableException {
dataLine = AudioSystem.getSourceDataLine(clip.getFormat());
}
/**
* Provides the entire audio buffer of this clip.
*
* @return audioData byte[] The bytes of the audio data that is loaded in
* this Clip.
*/
public byte[] getAudioData() {
return audioData;
}
/** Converts a frame count to a duration in milliseconds. */
private long convertFramesToMilliseconds(int frames) {
return (frames / (long) dataLine.getFormat().getSampleRate()) * 1000;
}
/** Converts a duration in milliseconds to a frame count. */
private int convertMillisecondsToFrames(long milliseconds) {
return (int) (milliseconds / dataLine.getFormat().getSampleRate());
}
@Override
public void update(LineEvent le) {
// PlayN.log().debug("update: " + le);
}
@Override
public void loop(int count) {
// PlayN.log().debug("loop(" + count + ") - framePosition: " +
// framePosition);
loopCount = count;
countDown = count;
active = true;
inputStream.reset();
start();
}
@Override
public void setLoopPoints(int start, int end) {
if (start < 0 || start > audioData.length - 1 || end < 0
|| end > audioData.length) {
throw new IllegalArgumentException("Loop points '" + start
+ "' and '" + end + "' cannot be set for buffer of size "
+ audioData.length);
}
if (start > end) {
throw new IllegalArgumentException("End position " + end
+ " preceeds start position " + start);
}
loopPointStart = start;
framePosition = loopPointStart;
loopPointEnd = end;
}
@Override
public void setMicrosecondPosition(long milliseconds) {
framePosition = convertMillisecondsToFrames(milliseconds);
}
@Override
public long getMicrosecondPosition() {
return convertFramesToMilliseconds(getFramePosition());
}
@Override
public long getMicrosecondLength() {
return convertFramesToMilliseconds(getFrameLength());
}
@Override
public void setFramePosition(int frames) {
framePosition = frames;
int offset = framePosition * format.getFrameSize();
try {
inputStream.reset();
inputStream.read(new byte[offset]);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public int getFramePosition() {
long timeSinceLastPositionSet = System.currentTimeMillis()
- timelastPositionSet;
int size = dataLine.getBufferSize() * (format.getChannels() / 2)
/ bufferUpdateFactor;
// Step down to the next whole frame.
size /= dataLine.getFormat().getFrameSize();
size *= dataLine.getFormat().getFrameSize();
int framesSinceLast = (int) ((timeSinceLastPositionSet / 1000f) * dataLine
.getFormat().getFrameRate());
int framesRemainingTillTime = size - framesSinceLast;
return framePosition - framesRemainingTillTime;
}
@Override
public int getFrameLength() {
return audioData.length / format.getFrameSize();
}
AudioFormat format;
@Override
public void open(AudioInputStream stream) throws IOException,
LineUnavailableException {
AudioInputStream is1;
format = stream.getFormat();
if (format.getEncoding() != AudioFormat.Encoding.PCM_SIGNED) {
is1 = AudioSystem.getAudioInputStream(
AudioFormat.Encoding.PCM_SIGNED, stream);
} else {
is1 = stream;
}
format = is1.getFormat();
InputStream is2 = is1;
byte[] buf = new byte[1 << 16];
int numRead = 0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
numRead = is2.read(buf);
while (numRead > -1) {
baos.write(buf, 0, numRead);
numRead = is2.read(buf, 0, buf.length);
}
is2.close();
audioData = baos.toByteArray();
AudioFormat afTemp;
if (format.getChannels() < 2) {
int frameSize = format.getSampleSizeInBits() * 2 / 8;
afTemp = new AudioFormat(format.getEncoding(),
format.getSampleRate(), format.getSampleSizeInBits(), 2,
frameSize, format.getFrameRate(), format.isBigEndian());
} else {
afTemp = format;
}
setLoopPoints(0, audioData.length);
dataLine = AudioSystem.getSourceDataLine(afTemp);
dataLine.open();
inputStream = new ByteArrayInputStream(audioData);
}
@Override
public void open(AudioFormat format, byte[] data, int offset, int bufferSize)
throws LineUnavailableException {
byte[] input = new byte[bufferSize];
for (int ii = 0; ii < input.length; ii++) {
input[ii] = data[offset + ii];
}
ByteArrayInputStream inputStream = new ByteArrayInputStream(input);
try {
AudioInputStream ais1 = AudioSystem
.getAudioInputStream(inputStream);
AudioInputStream ais2 = AudioSystem.getAudioInputStream(format,
ais1);
open(ais2);
} catch (UnsupportedAudioFileException uafe) {
throw new IllegalArgumentException(uafe);
} catch (IOException ioe) {
throw new IllegalArgumentException(ioe);
}
// TODO - throw IAE for invalid frame size, format.
}
@Override
public float getLevel() {
return dataLine.getLevel();
}
@Override
public long getLongFramePosition() {
return dataLine.getLongFramePosition() * 2 / format.getChannels();
}
@Override
public int available() {
return dataLine.available();
}
@Override
public int getBufferSize() {
return dataLine.getBufferSize();
}
@Override
public AudioFormat getFormat() {
return format;
}
@Override
public boolean isActive() {
return dataLine.isActive();
}
@Override
public boolean isRunning() {
return dataLine.isRunning();
}
@Override
public boolean isOpen() {
return dataLine.isOpen();
}
@Override
public void stop() {
// PlayN.log().debug("BigClip.stop()");
active = false;
// why did I have this commented out?
dataLine.stop();
if (thread != null) {
try {
active = false;
thread.join();
} catch (InterruptedException wakeAndContinue) {
}
}
}
public byte[] convertMonoToStereo(byte[] data, int bytesRead) {
byte[] tempData = new byte[bytesRead * 2];
if (format.getSampleSizeInBits() == 8) {
for (int ii = 0; ii < bytesRead; ii++) {
byte b = data[ii];
tempData[ii * 2] = b;
tempData[ii * 2 + 1] = b;
}
} else {
for (int ii = 0; ii < bytesRead - 1; ii += 2) {
byte b1 = data[ii];
byte b2 = data[ii + 1];
tempData[ii * 2] = b1;
tempData[ii * 2 + 1] = b2;
tempData[ii * 2 + 2] = b1;
tempData[ii * 2 + 3] = b2;
}
}
return tempData;
}
@Override
public void start() {
Runnable r = new Runnable() {
public void run() {
dataLine.start();
active = true;
int bytesRead = 0;
int frameSize = dataLine.getFormat().getFrameSize();
int bufSize = dataLine.getBufferSize();
boolean startOrMove = true;
byte[] data = new byte[bufSize];
int offset = framePosition * frameSize;
bytesRead = inputStream.read(new byte[offset], 0, offset);
// PlayN.log().debug("bytesRead " + bytesRead);
bytesRead = inputStream.read(data, 0, data.length);
// PlayN.log().debug("loopCount " + loopCount);
// PlayN.log().debug("countDown " + countDown);
// PlayN.log().debug("bytesRead " + bytesRead);
while (bytesRead != -1
&& (loopCount == Clip.LOOP_CONTINUOUSLY || countDown > 0)
&& active) {
// PlayN.log().debug("BigClip.start() loop " +
// framePosition);
int framesRead;
byte[] tempData;
if (format.getChannels() < 2) {
tempData = convertMonoToStereo(data, bytesRead);
framesRead = bytesRead / format.getFrameSize();
bytesRead *= 2;
} else {
framesRead = bytesRead
/ dataLine.getFormat().getFrameSize();
tempData = Arrays.copyOfRange(data, 0, bytesRead);
}
framePosition += framesRead;
if (framePosition >= loopPointEnd) {
framePosition = loopPointStart;
inputStream.reset();
countDown--;
// PlayN.log().debug("Loop Count: " + countDown);
}
timelastPositionSet = System.currentTimeMillis();
byte[] newData = tempData;
dataLine.write(newData, 0, newData.length);
if (startOrMove) {
int len = bufSize / bufferUpdateFactor;
// Step down to the next whole frame.
len /= frameSize;
len *= frameSize;
data = new byte[len];
startOrMove = false;
}
bytesRead = inputStream.read(data, 0, data.length);
if (bytesRead < 0
&& (--countDown > 0 || loopCount == Clip.LOOP_CONTINUOUSLY)) {
inputStream.read(new byte[offset], 0, offset);
// PlayN.log().debug("loopCount " + loopCount);
// PlayN.log().debug("countDown " + countDown);
inputStream.reset();
bytesRead = inputStream.read(data, 0, data.length);
}
}
// PlayN.log().debug("BigClip.start() loop ENDED" +
// framePosition);
active = false;
countDown = 1;
framePosition = 0;
inputStream.reset();
dataLine.stop();
}
};
thread = new Thread(r);
// makes thread behaviour compatible with JavaSound post 1.4
thread.setDaemon(true);
thread.start();
}
@Override
public void flush() {
dataLine.flush();
}
@Override
public void drain() {
dataLine.drain();
}
@Override
public void removeLineListener(LineListener listener) {
dataLine.removeLineListener(listener);
}
@Override
public void addLineListener(LineListener listener) {
dataLine.addLineListener(listener);
}
@Override
public Control getControl(Control.Type control) {
return dataLine.getControl(control);
}
@Override
public Control[] getControls() {
if (dataLine == null) {
return new Control[0];
} else {
return dataLine.getControls();
}
}
@Override
public boolean isControlSupported(Control.Type control) {
return dataLine.isControlSupported(control);
}
@Override
public void close() {
dataLine.close();
}
@Override
public void open() throws LineUnavailableException {
throw new IllegalArgumentException(
"illegal call to open() in interface Clip");
}
@Override
public Line.Info getLineInfo() {
return dataLine.getLineInfo();
}
}