/**
* Copyright 2008 - 2015 The Loon Game Engine Authors
*
* 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.5
*/
package loon.javase;
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;
class JavaSEBigClip implements Clip, LineListener {
private SourceDataLine dataLine;
private byte[] audioData;
private ByteArrayInputStream inputStream;
private int loopCount = 1;
private int countDown = 1;
private int loopPointStart;
private int loopPointEnd;
private int framePosition;
private Thread thread;
private boolean active;
private long timelastPositionSet;
private int bufferUpdateFactor = 2;
public JavaSEBigClip() {
}
public JavaSEBigClip(Clip clip) throws LineUnavailableException {
dataLine = AudioSystem.getSourceDataLine(clip.getFormat());
}
public byte[] getAudioData() {
return audioData;
}
private long convertFramesToMilliseconds(int frames) {
return (frames / (long) dataLine.getFormat().getSampleRate()) * 1000;
}
private int convertMillisecondsToFrames(long milliseconds) {
return (int) (milliseconds / dataLine.getFormat().getSampleRate());
}
@Override
public void update(LineEvent le) {
}
@Override
public void loop(int count) {
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;
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);
}
}
@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() {
active = false;
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);
bytesRead = inputStream.read(data, 0, data.length);
while (bytesRead != -1
&& (loopCount == Clip.LOOP_CONTINUOUSLY || countDown > 0)
&& active) {
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--;
}
timelastPositionSet = System.currentTimeMillis();
byte[] newData = tempData;
dataLine.write(newData, 0, newData.length);
if (startOrMove) {
int len = bufSize / bufferUpdateFactor;
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);
inputStream.reset();
bytesRead = inputStream.read(data, 0, data.length);
}
}
active = false;
countDown = 1;
framePosition = 0;
inputStream.reset();
dataLine.stop();
}
};
thread = new Thread(r);
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();
}
}