/*
* Copyright 2004-2010 Information & Software Engineering Group (188/1)
* Institute of Software Technology and Interactive Systems
* Vienna University of Technology, Austria
*
* 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.ifs.tuwien.ac.at/dm/somtoolbox/license.html
*
* 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.
*/
package at.tuwien.ifs.somtoolbox.audio;
import java.awt.Point;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Random;
import java.util.Vector;
import java.util.logging.Logger;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
/**
* Thread to play music files.
*
* @author Ewald Peiszer
* @version $Id: PlaybackThread.java 3590 2010-05-21 10:43:45Z mayer $
*/
public class PlaybackThread extends Thread {
public static int decodedCount = 0;
public static LinkedHashMap<File, File> decodedFiles = new LinkedHashMap<File, File>(50); // <File, File>
/** Probability to decode a mp3 file to wav */
public static final float DEFAULT_PROBABILITY_TO_DECODE = 0.5f;
public static Random rand = new Random();
// stats vars
public static int songCount = 0;
protected AudioInputStream[] audioInputStream = new AudioInputStream[2]; // We read audio data from here
// Allocate a buffer for reading from the input stream and writing to the line.
// Make it large enough to hold 4k audio frames. Note that the SourceDataLine also has its own internal buffer.
protected byte[][] buffer = { new byte[1 * 1024 * Constants.DATALINE_FORMAT.getFrameSize()],
new byte[1 * 1024 * Constants.DATALINE_FORMAT.getFrameSize()] }; // the buffer
protected byte[] datalineBuffer = new byte[1 * 1024 * Constants.DATALINE_FORMAT.getFrameSize()]; // the data line
// buffer
private String decodedOutputDir;
/** Flag if channel is empty: in this case, there will be silence on the respective channel */
boolean[] empty = new boolean[2];
protected File file1;
protected File file2;
/** Files to play */
File[][] files = null;
String id;
protected SourceDataLine line = null;
protected int monoFramesize = Constants.MONO_FORMAT.getFrameSize(); // sollte 2 sein
/**
* (if bRepeat == false): if the first channel's musicfile stops, it is set to true.<br>
* If the second channel's musicfile stops, this thread is stopped.
*/
boolean otherChannelAlreadyFinished = false;
private Vector<PlaybackListener> playbackListeners = new Vector<PlaybackListener>();
Point[] positions = new Point[2];
float probalityToDecode = DEFAULT_PROBABILITY_TO_DECODE;
protected boolean quitLoop = false;
protected boolean ready = false;
/**
* Flag: if true, then music is played endlessly, alwas repeating. Songs are picked in random order.
* <p>
* If false, then on each channel there will be played each song in the list, from the first to the last, then exit.
*/
boolean repeatShuffle = true;
private boolean threadSuspended = false;
/** Flag: if false, no global statistic variable will be updated by this Thread */
boolean updateStats = true;
/**
* Flag: if <code>true</code>, then the respective channel is waiting for a {@link DecoderThread} to finish.
*/
boolean[] waitForDecoder = { false, false };
public PlaybackThread(String id, PlaybackThreadDataRecord record, SourceDataLine line, boolean repeat,
float probalityToDecode, boolean updateStats, String decodedOutputDir) {
this(id, record, line, repeat, probalityToDecode, decodedOutputDir);
this.updateStats = updateStats;
}
public PlaybackThread(String id, PlaybackThreadDataRecord record, SourceDataLine line, boolean repeat,
float probalityToDecode, String decodedOutputDir) {
this(id, record, line, decodedOutputDir);
this.repeatShuffle = repeat;
this.probalityToDecode = probalityToDecode;
}
/**
* Convenience constructur that takes a {@link PlaybackThreadDataRecord} and pulls all data from it to create a new
* thread.<br>
* Note the constructor call that looks quite crazy with all its necessary casts.
*/
public PlaybackThread(String id, PlaybackThreadDataRecord record, SourceDataLine line, String decodedOutputDir) {
setName(this.getClass().getSimpleName() + " (" + id + ")");
this.id = id;
this.decodedOutputDir = decodedOutputDir;
if (record.position[0] != null) {
this.positions[0] = record.position[0];
}
if (record.position[1] != null) {
this.positions[1] = record.position[1];
}
this.line = line;
// Initialize so that we are ready to start playing if "start()" is invoked
Vector<File> tempVector = new Vector<File>();
File file = null;
try {
// Create array of files
files = new File[2][];
// Try to create an array of files out of the strings and make sure all files exist
for (int j = 0; j < record.listOfSongs.length; j++) {
if (record.listOfSongs[j] != null) {
empty[j] = false;
for (int i = 0; i < record.listOfSongs[j].size(); i++) {
String currentSong = record.listOfSongs[j].get(i);
file = new File(currentSong);
if (file.exists()) {
tempVector.add(file);
} else {
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").warning(
id + ": " + "File not found '" + currentSong + "'. Ignoring");
}
}
}
// Make sure there is at least one file
if (tempVector.size() >= 1) {
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").fine(id + ": " + "Channel " + j + ": ok.");
files[j] = tempVector.toArray(new File[0]);
tempVector.clear();
} else {
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").warning(
id + ": " + "Channel " + j + ": No song.");
muteChannel(j);
// throw new Exception();
}
}
line.open(Constants.DATALINE_FORMAT);
// everything is ok...
ready = true;
} catch (Exception ex) {
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").warning(
id + ": " + "Could not create NodeThread '" + id + "'. " + ex.getMessage());
ex.printStackTrace();
} finally { // Always relinquish the resources we use
if (line != null) {
line.close();
}
if (audioInputStream[0] != null) {
try {
audioInputStream[0].close();
} catch (IOException ex2) {
}
}
if (audioInputStream[1] != null) {
try {
audioInputStream[1].close();
} catch (IOException ex1) {
}
}
}
}
public boolean addPlaybackListener(PlaybackListener listener) {
return playbackListeners.add(listener);
}
public void decodingFailed(int channel, boolean stats) {
// Try again
try {
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").warning(
id + ": " + "Decoding failed. Trying next song...");
AudioInputStream aintemp = getNextSong(channel, stats);
if (aintemp != null) {
audioInputStream[channel] = aintemp;
unMuteChannel(channel);
waitForDecoder[channel] = false;
}
} catch (Exception ex) {
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").warning(
id + ": " + "Getting next song failed... Trying next song");
ex.printStackTrace();
decodingFailed(channel, stats);
}
}
public void decodingFinished(File file, int channel, boolean stats, DecoderThread dt) {
try {
unMuteChannel(channel);
audioInputStream[channel] = prepareAudioInputStream(file, channel, stats); // this repaints grid
PlaybackThread.decodedFiles.put(dt.getEncodedFile(), file);
PlaybackThread.decodedCount++;
for (int i = 0; i < playbackListeners.size(); i++) {
PlaybackListener playbackListener = playbackListeners.get(i);
playbackListener.updateStats(PlaybackThread.songCount);
}
waitForDecoder[channel] = false;
} catch (Exception ex) {
ex.printStackTrace();
decodingFailed(channel, stats);
}
}
public AudioInputStream getNextSong(int channel, boolean stats) throws IOException, UnsupportedAudioFileException {
File tempFile = null;
if (repeatShuffle) {
// normal mode, get random song from list
tempFile = files[channel][PlaybackThread.rand.nextInt(files[channel].length)];
} else {
// "playsound-mode"
tempFile = files[channel][0];
// One more file to come?
if (files[channel].length > 1) {
// yes, remove File with index 0
Vector<File> tmp = new Vector<File>(Arrays.asList(files[channel]));
tmp.remove(0);
files[channel] = tmp.toArray(new File[0]);
} else {
// no, set Array to null as a signal to mute this channel
files[channel] = null;
}
}
// Is this already a wav?
if (tempFile.getName().endsWith(".wav")) {
// play it
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").fine(id + ": " + "playing wav...");
} else {
if (PlaybackThread.decodedFiles.containsKey(tempFile)) {
// This file has already been decoded, so play the decoded version instead
tempFile = PlaybackThread.decodedFiles.get(tempFile);
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").fine(
id + ": " + "found in HashMap, playing decoded version..");
} else {
// Is perhaps a decoded version already there?
File tryDecodedFile = new File(DecoderThread.getDecodedFileName(tempFile, decodedOutputDir,
Constants.DECODED_SUFFIX));
if (tryDecodedFile.exists()) {
// use already decoded version (was not in Hashmap)
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").fine(
id + ": " + "Accidently found: " + tryDecodedFile + ". I'll use it.");
PlaybackThread.decodedFiles.put(tempFile, tryDecodedFile);
tempFile = tryDecodedFile;
} else {
// Should we decode this file or just play the mp3-version?
if (PlaybackThread.rand.nextFloat() < probalityToDecode) {
// Decode
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").info(
id + ": " + "Decoding: " + tempFile);
// Change table model
if (positions[channel] != null) {
for (int i = 0; i < playbackListeners.size(); i++) {
PlaybackListener playbackListener = playbackListeners.get(i);
playbackListener.setDecodingAt(positions[channel].x, positions[channel].y,
tempFile.getName());
}
}
waitForDecoder[channel] = true;
// While the file is being decoded, mute this channel
muteChannel(channel);
DecoderThread decoderThread = new DecoderThread(this, tempFile, channel, stats,
decodedOutputDir, Constants.DECODED_SUFFIX);
decoderThread.start();
} else {
// Play the mp3 file
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").fine(id + ": " + "playing mp3...");
}
}
}
}
if (!empty[channel]) {
return prepareAudioInputStream(tempFile, channel, stats);
} else {
return null;
}
}
public boolean isReady() {
return ready;
}
public void muteChannel(int channel) {
Arrays.fill(buffer[channel], (byte) 0);
if (!empty[channel]) {
empty[channel] = true;
if (updateStats && positions[channel] != null) {
for (int i = 0; i < playbackListeners.size(); i++) {
PlaybackListener playbackListener = playbackListeners.get(i);
playbackListener.setMutedSpeaker(positions[channel].x, positions[channel].y, true);
playbackListener.updateStats(PlaybackThread.songCount);
}
}
}
}
public void pausePlayback() {
threadSuspended = true;
}
public AudioInputStream prepareAudioInputStream(File file, int channel, boolean stats) throws IOException,
UnsupportedAudioFileException {
AudioInputStream audioInputStream;
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").info(
id + ": " + "Next song for channel " + channel + ": " + file);
// update listeners
if (positions[channel] != null) {
for (int i = 0; i < playbackListeners.size(); i++) {
PlaybackListener playbackListener = playbackListeners.get(i);
playbackListener.setSongAt(positions[channel].x, positions[channel].y, file.getName());
}
}
audioInputStream = AudioSystem.getAudioInputStream(file);
audioInputStream = AudioSystem.getAudioInputStream(Constants.DATALINE_FORMAT, audioInputStream);
if (stats && updateStats) {
PlaybackThread.songCount++;
for (int i = 0; i < playbackListeners.size(); i++) {
PlaybackListener playbackListener = playbackListeners.get(i);
playbackListener.updateStats(PlaybackThread.songCount);
}
}
return audioInputStream;
}
public boolean removePlaybackListener(PlaybackListener listener) {
return playbackListeners.remove(listener);
}
public void resumePlayback() {
threadSuspended = false;
synchronized (this) {
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").fine(id + ": " + "notifiy");
this.notify();
}
line.start();
}
@Override
public void run() {
if (ready) {
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").info(id + ": " + " starts");
int[] bytesRead = { 0, 0 };
try {
if (!empty[0]) {
audioInputStream[0] = getNextSong(0, false);
} else {
// left channel empty
Arrays.fill(buffer[0], (byte) 0);
bytesRead[0] = buffer[0].length;
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").fine(
id + ": " + "Left channel empty, filling with 0");
}
if (!empty[1]) {
audioInputStream[1] = getNextSong(1, false);
} else {
// right channel empty
Arrays.fill(buffer[1], (byte) 0);
bytesRead[1] = buffer[1].length;
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").fine(
id + ": " + "Right channel empty, filling with 0");
}
line.open(Constants.DATALINE_FORMAT);
line.start();
while (!quitLoop) {
// Thread suspended?
try {
if (threadSuspended) {
line.stop();
Logger.getLogger("at.tuwien.ifs.somtoolbox.multichannel").fine(id + ": " + "waiting...");
synchronized (this) {
while (threadSuspended) {
wait();
}
}
}
} catch (InterruptedException e) {
}
// First, read some bytes from the input streams.
if (!empty[0] && audioInputStream[0] != null) {
bytesRead[0] = audioInputStream[0].read(buffer[0], 0, buffer[0].length);
}
if (!empty[1] && audioInputStream[1] != null) {
bytesRead[1] = audioInputStream[1].read(buffer[1], 0, buffer[1].length);
}
// For each channel
for (int channel = 0; channel < 2; channel++) {
// If any stream is at the end, we get the next song good?
// TODO: Only if we are not waiting for a DecoderThread
if (bytesRead[channel] == -1 && !waitForDecoder[channel]) {
if (repeatShuffle) {
audioInputStream[channel] = getNextSong(channel, true);
} else {
// "playsound-mode"
// Still another song to come for this channel?
if (files[channel] != null) {
// get next song
audioInputStream[channel] = getNextSong(channel, true);
} else {
// other channel already finished?
if (otherChannelAlreadyFinished) {
// yes, so we break out of the loop and end
// the thread
quitLoop = true;
} else {
// no, in the meantime: silence, "empty"
otherChannelAlreadyFinished = true;
muteChannel(channel);
bytesRead[channel] = buffer[channel].length;
}
}
}
}
}
// Construct dataline-Buffer (from two stereo buffers; optimized!?)
int meanSample;
for (int j = 0; j < buffer[0].length - 4; j = j + 4) {
meanSample = ((buffer[0][j + 1] << 8 | buffer[0][j] & 0xFF) >> 1)
+ ((buffer[0][j + 3] << 8 | buffer[0][j + 2] & 0xFF) >> 1);
datalineBuffer[j] = (byte) (meanSample & 0xFF);
datalineBuffer[j + 1] = (byte) (meanSample >> 8);
meanSample = ((buffer[1][j + 1] << 8 | buffer[1][j] & 0xFF) >> 1)
+ ((buffer[1][j + 3] << 8 | buffer[1][j + 2] & 0xFF) >> 1);
datalineBuffer[j + 2] = (byte) (meanSample & 0xFF);
datalineBuffer[j + 3] = (byte) (meanSample >> 8);
}
line.write(datalineBuffer, 0, datalineBuffer.length);
}
line.drain();
line.close();
} catch (Exception e) {
e.printStackTrace();
} finally { // Always relinquish the resources we use
if (line != null) {
line.close();
}
if (audioInputStream[0] != null) {
try {
audioInputStream[0].close();
audioInputStream[0] = null;
} catch (IOException ex2) {
}
}
if (audioInputStream[1] != null) {
try {
audioInputStream[1].close();
audioInputStream[1] = null;
} catch (IOException ex1) {
}
}
}
}
}
public void stopPlayback() {
quitLoop = true;
if (threadSuspended) {
resumePlayback();
}
}
public void unMuteChannel(int channel) {
if (empty[channel]) {
empty[channel] = false;
if (updateStats && positions[channel] != null) {
for (int i = 0; i < playbackListeners.size(); i++) {
PlaybackListener playbackListener = playbackListeners.get(i);
playbackListener.setMutedSpeaker(positions[channel].x, positions[channel].y, true);
playbackListener.updateStats(PlaybackThread.songCount);
}
}
}
}
}