/* * 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.apps.viewer.controls.multichannelPlayback; import java.awt.Point; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Vector; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.SourceDataLine; import javax.sound.sampled.UnsupportedAudioFileException; /** * Represents two nodes / one output line * * @author Ewald Peiszer * @version $Id: PlaybackThread.java 3590 2010-05-21 10:43:45Z mayer $ */ public class PlaybackThread extends Thread { String id; Point[] aPos = new Point[2]; /** Files to play */ File[][] aFiles = null; /** Flag if channel is empty: in this case, there will be silence on the respective channel */ boolean[] aEmpty = new boolean[2]; /** * Flag: if <code>true</code>, then the respective channel is waiting for a <code>DecoderThread</code> to finish. */ boolean[] aWaitForDecoder = { false, 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 bRepeat_Shuffle = true; /** Used as a pointer if <code>bRepeat_Shuffle</code> is false */ /** Flag: if false, no global statistic variable will be updated by this Thread */ boolean bUpdStats = true; /** Probability to decode a mp3 file to wav */ float p_decode = Commons.p_decode; /** * (if bRepeat == false): if the first channel's musicfile stops, it is set to true. * <p> * If the second channel's musicfile stops, this thread is stopped. */ boolean bOtherChannelAlreadyFinished = false; private boolean threadSuspended = false; protected File file1, file2; protected SourceDataLine line = null; protected AudioInputStream[] ain = new AudioInputStream[2]; // We read audio data from here protected boolean ready = false; protected boolean bQuitLoop = false; // 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. // int framesize = datalineformat.getFrameSize( ); // sollte 4 sein protected int datalineframesize = Commons.datalineformat.getFrameSize(); // sollte 4 sein protected int monoframesize = Commons.monoformat.getFrameSize(); // sollte 2 sein protected byte[][] buffer = { new byte[1 * 1024 * datalineframesize], new byte[1 * 1024 * datalineframesize] }; // the // buffer /* * protected byte[] buffer1 = new byte[1 * 1024 * monoframesize]; // the buffer protected byte[] buffer2 = new byte[1 * 1024 * monoframesize]; // * the buffer */ protected byte[] datalinebuffer = new byte[1 * 1024 * datalineframesize]; // the data line buffer // int numbytes1 = 0, numbytes2 = 0; // how many bytes /* * public PlaybackThread(String id, String[][] asFiles, SourceDataLine line, boolean bUpdStats) { this(id, asFiles, line, true, * MultichannelMp3_Shared.p_decode, bUpdStats); } */ public PlaybackThread(String id, TPlaybackThreadDataRecord record, SourceDataLine line, boolean bRepeat, float p_decode, boolean bUpdStats) { this(id, record, line, bRepeat, p_decode); this.bUpdStats = bUpdStats; } public PlaybackThread(String id, TPlaybackThreadDataRecord record, SourceDataLine line, boolean bRepeat, float p_decode) { this(id, record, line); this.bRepeat_Shuffle = bRepeat; this.p_decode = p_decode; } /** * Convenience constructur that takes a <code>TPlaybackThreadDataRecord</code> and pulls all data from it to create * a new thread * <p> * Note the constructor call that looks quite crazy with all its necessary casts. */ /* * public PlaybackThread(String id, TPlaybackThreadDataRecord record, SourceDataLine line) { // We assume that if a channel is not used, * avMusic[i] is null // otherwise also the pPos[i] must be _not null_. String[][] aas; if (record.avMusic[0] != null && record.avMusic[1] != * null) { // both are used aas = new String[][] { (String[]) record.avMusic[0].toArray(new String[0]), (String[]) record.avMusic[1].toArray(new * String[0])}; this(id, record.pPos[0].x, record.pPos[0].y, record.pPos[1].x, record.pPos[1].y, new String[][] { (String[]) * record.avMusic[0].toArray(new String[0]), (String[]) record.avMusic[1].toArray(new String[0])} , line); } else if (record.avMusic[0] == null && * record.avMusic[1] != null) { // only right is used aas = new String[][] { {}, (String[]) record.avMusic[1].toArray(new String[0])}; } else if * (record.avMusic[0] != null && record.avMusic[1] == null) { // only left is used aas = new String[][] { (String[]) record.avMusic[0].toArray(new * String[0]), {} }; } this(id, record.pPos[0].x, record.pPos[0].y, record.pPos[1].x, record.pPos[1].y, new String[][] { (String[]) * record.avMusic[0].toArray(new String[0]), (String[]) record.avMusic[1].toArray(new String[0])} , line); } */ public PlaybackThread(String id, TPlaybackThreadDataRecord record, SourceDataLine line) { this.setName(getClass().getSimpleName() + " (" + id + ")"); this.id = id; if (record.pPos[0] != null) { this.aPos[0] = record.pPos[0]; } if (record.pPos[1] != null) { this.aPos[1] = record.pPos[1]; } this.line = line; // Initialize so that we are ready to start playing if "start()" is invoked Vector<File> vTmp = new Vector<File>(); File f = null; try { // Create array of files aFiles = new File[2][]; String sAktFile; // Try to create an array of files out of the strings and make sure all files // exist for (int j = 0; j < record.avMusic.length; j++) { if (record.avMusic[j] != null) { aEmpty[j] = false; for (int i = 0; i < record.avMusic[j].size(); i++) { sAktFile = (String) record.avMusic[j].get(i); f = new File(sAktFile); if (f.exists()) { vTmp.add(f); } else { log_warning("File not found '" + sAktFile + "'. Ignoring"); } } } // Make sure there is at least one file if (vTmp.size() >= 1) { log_fine("Channel " + j + ": ok."); aFiles[j] = vTmp.toArray(new File[0]); vTmp.clear(); } else { log_warning("Channel " + j + ": No song."); muteChannel(j); // throw new Exception(); } } /* * this.file1 = new File(sfile1); this.file2 = new File(sfile2); */ /* * DataLine.Info info = new DataLine.Info(SourceDataLine.class, MultichannelMp3_Shared.datalineformat); // Open the line through which * we'll play the streaming audio. line = (SourceDataLine) AudioSystem.getLine(info); */ line.open(Commons.datalineformat); // everything is ok... ready = true; } catch (Exception ex) { log_warning("Could not create NodeThread '" + id + "'. " + ex.getMessage()); ex.printStackTrace(); } finally { // Always relinquish the resources we use if (line != null) { line.close(); } if (ain[0] != null) { try { ain[0].close(); } catch (IOException ex2) { } } if (ain[1] != null) { try { ain[1].close(); } catch (IOException ex1) { } } } } /* * public PlaybackThread(String id, int x1, int y1, int x2, int y2, String[][] asFiles, SourceDataLine line) { this.id = id; this.x1 = x1; this.x2 = * x2; this.y1 = y1; this.y2 = y2; this.line = line; // Initialize so that we are ready to start playing if "start()" is invoked Vector vTmp = new * Vector(); File f = null; try { // Create array of files aFiles = 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 < asFiles.length; j++) { if (asFiles[j] != null) { aEmpty[j] = false; for (int i = 0; i < * asFiles[j].length; i++) { f = new File(asFiles[j][i]); if (f.exists()) { vTmp.add(f); } else { log_warning("File not found '" + asFiles[j][i] + * "'. Ignoring"); } } // Make sure there is at least one file if (vTmp.size() >= 1) { log_fine("Channel " + j + " ok."); aFiles[j] = (File[]) * vTmp.toArray(new File[0]); vTmp.clear(); } else { log_warning("No song."); muteChannel(j); //throw new Exception(); } } else { aEmpty[j] = * true; } } line.open(MultichannelMp3_Shared.datalineformat); // everything is ok... ready = true; } catch (Exception ex) { log_warning("Could * not create NodeThread '" + id + "'. " + ex.getMessage()); ex.printStackTrace(); } finally { // Always relinquish the resources we use if (line != * null) line.close(); if (ain[0] != null) { try { ain[0].close(); } catch (IOException ex2) { } } if (ain[1] != null) { try { ain[1].close(); } * catch (IOException ex1) { } } } } */ public void muteChannel(int channel) { Arrays.fill(buffer[channel], (byte) 0); // bytesread1 = buffer[channel].length; if (!aEmpty[channel]) { aEmpty[channel] = true; if (bUpdStats) { // MultichannelMp3_Shared.iCurrentlyMuted++; if (Commons.cf != null && aPos[channel] != null) { Commons.cf.mod.setMutedSpeaker(aPos[channel].x, aPos[channel].y, true); Commons.cf.updateStats(); } } } } public void unmuteChannel(int channel) { if (aEmpty[channel]) { aEmpty[channel] = false; if (bUpdStats) { // MultichannelMp3_Shared.iCurrentlyMuted--; if (Commons.cf != null && aPos[channel] != null) { Commons.cf.mod.setMutedSpeaker(aPos[channel].x, aPos[channel].y, false); } Commons.cf.updateStats(); } } } public AudioInputStream getNextSong(int channel, boolean stats) throws IOException, UnsupportedAudioFileException { File tempfile = null; if (bRepeat_Shuffle) { // normal mode, get random song from list tempfile = aFiles[channel][Commons.rand.nextInt(aFiles[channel].length)]; } else { // "playsound-mode" tempfile = aFiles[channel][0]; // One more file to come? if (aFiles[channel].length > 1) { // yes, remove File with index 0 Vector<File> tmp = new Vector<File>(Arrays.asList(aFiles[channel])); tmp.remove(0); aFiles[channel] = tmp.toArray(new File[0]); } else { // no, set Array to null as a signal to mute this channel aFiles[channel] = null; } } // Is this already a wav? if (tempfile.getName().endsWith(".wav")) { // play it log_fine("playing wav..."); } else { if (Commons.lhmDecoded.containsKey(tempfile)) { // This file has already been decoded, so play the decoded version instead tempfile = Commons.lhmDecoded.get(tempfile); log_fine("found in HashMap, playing decoded version.."); } else { // Is perhaps a decoded version already there? File fTryDecoded = new File(DecoderThread.getDecodedFileName(tempfile)); if (fTryDecoded.exists()) { // use already decoded version (was not in Hashmap) log_fine("Accidently found: " + fTryDecoded + ". I'll use it."); Commons.lhmDecoded.put(tempfile, fTryDecoded); tempfile = fTryDecoded; } else { // Should we decode this file or just play the mp3-version? if (Commons.rand.nextFloat() < p_decode) { // Decode log_info("Decoding: " + tempfile); // Change table model if (aPos[channel] != null) { Commons.cf.mod.setDecodingAt(aPos[channel].x, aPos[channel].y, tempfile.getName()); } aWaitForDecoder[channel] = true; // While the file is being decoded, mute this channel muteChannel(channel); DecoderThread dt = new DecoderThread(this, tempfile, channel, stats); dt.start(); } else { // Play the mp3 file log_fine("playing mp3..."); } } } } if (!aEmpty[channel]) { return prepareAin(tempfile, channel, stats); } else { return null; } } public AudioInputStream prepareAin(File file, int channel, boolean stats) throws IOException, UnsupportedAudioFileException { AudioInputStream ain; log_info("Next song for channel " + channel + ": " + file); // Change table model if (aPos[channel] != null) { Commons.cf.mod.setSongAt(aPos[channel].x, aPos[channel].y, file.getName()); } ain = AudioSystem.getAudioInputStream(file); ain = AudioSystem.getAudioInputStream(Commons.datalineformat, ain); // ain = AudioSystem.getAudioInputStream(MultichannelMp3_Shared.monoformat, ain); // ain = AudioSystem.getAudioInputStream(MultichannelMp3_Shared.datalineformat, ain); if (stats && bUpdStats) { Commons.iSongscount++; if (Commons.cf != null) { Commons.cf.updateStats(); } } return ain; } public void decodingFinished(File file, int channel, boolean stats, DecoderThread dt) { try { unmuteChannel(channel); ain[channel] = prepareAin(file, channel, stats); // this repaints grid Commons.lhmDecoded.put(dt.getEncodedFile(), file); Commons.iDecodedcount++; if (Commons.cf != null) { Commons.cf.updateStats(); } aWaitForDecoder[channel] = false; } catch (Exception ex) { ex.printStackTrace(); decodingFailed(channel, stats); } } public void decodingFailed(int channel, boolean stats) { // Try again try { log_warning("Decoding failed. Trying next song..."); AudioInputStream aintemp = getNextSong(channel, stats); if (aintemp != null) { ain[channel] = aintemp; unmuteChannel(channel); aWaitForDecoder[channel] = false; } } catch (Exception ex) { log_warning("Getting next song failed... Trying next song"); ex.printStackTrace(); decodingFailed(channel, stats); } } @Override public void run() { if (ready) { log_info(" starts"); // int numbytes1 = 0, numbytes2 = 0; // TODO: remaining bytes?! int[] bytesread = { 0, 0 }; try { if (!aEmpty[0]) { ain[0] = getNextSong(0, false); } else { // left channel empty Arrays.fill(buffer[0], (byte) 0); bytesread[0] = buffer[0].length; log_fine("Left channel empty, filling with 0"); } if (!aEmpty[1]) { ain[1] = getNextSong(1, false); } else { // right channel empty Arrays.fill(buffer[1], (byte) 0); bytesread[1] = buffer[1].length; log_fine("Right channel empty, filling with 0"); } line.open(Commons.datalineformat); line.start(); while (!bQuitLoop) { // Thread suspended? try { if (threadSuspended) { line.stop(); log_fine("waiting..."); synchronized (this) { while (threadSuspended) { wait(); } } } } catch (InterruptedException e) { } // First, read some bytes from the input streams. if (!aEmpty[0] && ain[0] != null) { bytesread[0] = ain[0].read(buffer[0], 0, buffer[0].length); } if (!aEmpty[1] && ain[1] != null) { bytesread[1] = ain[1].read(buffer[1], 0, buffer[1].length); } // For each channel for (int iChannel = 0; iChannel < 2; iChannel++) { // 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[iChannel] == -1 && !aWaitForDecoder[iChannel]) { if (bRepeat_Shuffle) { ain[iChannel] = getNextSong(iChannel, true); } else { // "playsound-mode" // Still another song to come for this channel? if (aFiles[iChannel] != null) { // get next song ain[iChannel] = getNextSong(iChannel, true); } else { // other channel already finished? if (bOtherChannelAlreadyFinished) { // yes, so we break out of the loop and end // the thread bQuitLoop = true; } else { // no, in the meantime: silence, "empty" bOtherChannelAlreadyFinished = true; muteChannel(iChannel); bytesread[iChannel] = buffer[iChannel].length; } } } } } // Construct dataline-Buffer (from two stereo buffers; naive, bytewise) /* * for (int j = 0; j < buffer1.length - 4; j = j + 4) { datalinebuffer[j] = (byte) (buffer1[j] / 2 + buffer1[j+2] / 2); * datalinebuffer[j+1] = (byte) (buffer1[j+1] / 2 + buffer1[j+3] / 2); datalinebuffer[j+2] = (byte) (buffer2[j] / 2 + buffer2[j+2] / * 2); datalinebuffer[j+3] = (byte) (buffer2[j+1] / 2 + buffer2[j+3] / 2); } */ // Construct dataline-Buffer (from two stereo buffers; better, samplewise) /* * int iSample1, iSample2, iMeanSample; boolean bBigEndian = true; // temp for (int j = 0; j < buffer1.length - 4; j = j + 4) { * iSample1 = TConversionTool.bytesToInt16(buffer1, j, bBigEndian); iSample2 = TConversionTool.bytesToInt16(buffer1, j+2, * bBigEndian); iMeanSample = iSample1 / 2 + iSample2 / 2; TConversionTool.intToBytes16(iMeanSample, datalinebuffer, j, * bBigEndian); iSample1 = TConversionTool.bytesToInt16(buffer2, j, bBigEndian); iSample2 = TConversionTool.bytesToInt16(buffer2, * j+2, bBigEndian); iMeanSample = iSample1 / 2 + iSample2 / 2; TConversionTool.intToBytes16(iMeanSample, datalinebuffer, j+2, * bBigEndian); } */ // Construct dataline-Buffer (from two stereo buffers; optimized!?) int iMeanSample; for (int j = 0; j < buffer[0].length - 4; j = j + 4) { iMeanSample = ((buffer[0][j + 1] << 8 | buffer[0][j] & 0xFF) >> 1) + ((buffer[0][j + 3] << 8 | buffer[0][j + 2] & 0xFF) >> 1); datalinebuffer[j] = (byte) (iMeanSample & 0xFF); datalinebuffer[j + 1] = (byte) (iMeanSample >> 8); iMeanSample = ((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) (iMeanSample & 0xFF); datalinebuffer[j + 3] = (byte) (iMeanSample >> 8); } // Construct dataline-Buffer (from two mono buffers) /* * for (int j = 0; j < buffer1.length - 4; j = j + 2) { datalinebuffer[j * 2] = buffer1[j]; datalinebuffer[(j * 2) + 1] = * buffer1[j+1]; datalinebuffer[(j * 2) + 2] = buffer2[j]; datalinebuffer[(j * 2) + 3] = buffer2[j+1]; } */ // log_info("writing"); line.write(datalinebuffer, 0, datalinebuffer.length); } // the following commands are commented out because they // are never reached... line.drain(); line.close(); } catch (Exception e) { e.printStackTrace(); // System.exit(1); } finally { // Always relinquish the resources we use if (line != null) { line.close(); } if (ain[0] != null) { try { ain[0].close(); ain[0] = null; } catch (IOException ex2) { } } if (ain[1] != null) { try { ain[1].close(); ain[1] = null; } catch (IOException ex1) { } } } } } public void stop_playback() { bQuitLoop = true; if (threadSuspended) { resume_playback(); } } public void pause_playback() { threadSuspended = true; // line.stop(); } public void resume_playback() { threadSuspended = false; synchronized (this) { log_fine("notifiy"); this.notify(); } line.start(); } protected void log_fine(String msg) { Commons.log.fine(id + ": " + msg); } protected void log_info(String msg) { Commons.log.info(id + ": " + msg); } protected void log_warning(String msg) { Commons.log.warning(id + ": " + msg); } }