/* * DecodedVorbisAudioInputStream * * JavaZOOM : vorbisspi@javazoom.net * http://www.javazoom.net * * ---------------------------------------------------------------------------- * 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 javazoom.spi.vorbis.sampled.convert; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javazoom.spi.PropertiesContainer; import com.jcraft.jogg.Packet; import com.jcraft.jogg.Page; import com.jcraft.jogg.StreamState; import com.jcraft.jogg.SyncState; import com.jcraft.jorbis.Block; import com.jcraft.jorbis.Comment; import com.jcraft.jorbis.DspState; import com.jcraft.jorbis.Info; import org.tritonus.share.TDebug; import org.tritonus.share.sampled.convert.TAsynchronousFilteredAudioInputStream; /** * This class implements the Vorbis decoding. */ public class DecodedVorbisAudioInputStream extends TAsynchronousFilteredAudioInputStream implements PropertiesContainer { private InputStream oggBitStream_ = null; private SyncState oggSyncState_ = null; private StreamState oggStreamState_ = null; private Page oggPage_ = null; private Packet oggPacket_ = null; private Info vorbisInfo = null; private Comment vorbisComment = null; private DspState vorbisDspState = null; private Block vorbisBlock = null; static final int playState_NeedHeaders = 0; static final int playState_ReadData = 1; static final int playState_WriteData = 2; static final int playState_Done = 3; static final int playState_BufferFull = 4; static final int playState_Corrupt = -1; private int playState; private int bufferMultiple_ = 4; private int bufferSize_ = bufferMultiple_ * 256 * 2; private int convsize = bufferSize_ * 2; private byte[] convbuffer = new byte[convsize]; private byte[] buffer = null; private int bytes = 0; private float[][][] _pcmf = null; private int[] _index = null; private int index = 0; private int i = 0; // bout is now a global so that we can continue from when we have a buffer full. int bout = 0; private HashMap properties = null; private long byteslength = -1; private long currentBytes = 0; /** * Constructor. */ public DecodedVorbisAudioInputStream(AudioFormat outputFormat, AudioInputStream bitStream) { super(outputFormat, -1); this.oggBitStream_ = bitStream; try { // Try to find out inputstream length to allow skip. byteslength = oggBitStream_.available(); } catch (IOException ex) { TDebug.out("DecodedVorbisAudioInputStream : Cannot run inputStream.available() : " + ex.getMessage()); byteslength = -1; } init_jorbis(); index = 0; playState = playState_NeedHeaders; properties = new HashMap(); } /** * Initializes all the jOrbis and jOgg vars that are used for song playback. */ private void init_jorbis() { oggSyncState_ = new SyncState(); oggStreamState_ = new StreamState(); oggPage_ = new Page(); oggPacket_ = new Packet(); vorbisInfo = new Info(); vorbisComment = new Comment(); vorbisDspState = new DspState(); vorbisBlock = new Block(vorbisDspState); buffer = null; bytes = 0; currentBytes = 0L; oggSyncState_.init(); } /** * Return dynamic properties. * * <ul> * <li><b>ogg.position.byte</b> [Long], current position in bytes in the stream. *</ul> */ @Override public Map properties() { properties.put("ogg.position.byte", new Long(currentBytes)); return properties; } /** * Main loop. */ public void execute() { if (TDebug.TraceAudioConverter) { switch (playState) { case playState_NeedHeaders: TDebug.out("playState = playState_NeedHeaders"); break; case playState_ReadData: TDebug.out("playState = playState_ReadData"); break; case playState_WriteData: TDebug.out("playState = playState_WriteData"); break; case playState_Done: TDebug.out("playState = playState_Done"); break; case playState_BufferFull: TDebug.out("playState = playState_BufferFull"); break; case playState_Corrupt: TDebug.out("playState = playState_Corrupt"); break; } } // This code was developed by the jCraft group, as JOrbisPlayer.java, slightly // modified by jOggPlayer developer and adapted by JavaZOOM to suit the JavaSound // SPI. Then further modified by Tom Kimpton to correctly play ogg files that // would hang the player. switch (playState) { case playState_NeedHeaders: try { // Headers (+ Comments). readHeaders(); } catch (IOException ioe) { playState = playState_Corrupt; return; } playState = playState_ReadData; break; case playState_ReadData: int result; index = oggSyncState_.buffer(bufferSize_); buffer = oggSyncState_.data; bytes = readFromStream(buffer, index, bufferSize_); if (TDebug.TraceAudioConverter) { TDebug.out("More data : " + bytes); } if (bytes == -1) { playState = playState_Done; if (TDebug.TraceAudioConverter) { TDebug.out("Ogg Stream empty. Settings playState to playState_Done."); } break; } else { oggSyncState_.wrote(bytes); if (bytes == 0) { if ((oggPage_.eos() != 0) || (oggStreamState_.e_o_s != 0) || (oggPacket_.e_o_s != 0)) { if (TDebug.TraceAudioConverter) { TDebug.out("oggSyncState wrote 0 bytes: settings playState to playState_Done."); } playState = playState_Done; } if (TDebug.TraceAudioConverter) { TDebug.out("oggSyncState wrote 0 bytes: but stream not yet empty."); } break; } } result = oggSyncState_.pageout(oggPage_); if (result == 0) { if (TDebug.TraceAudioConverter) { TDebug.out("Setting playState to playState_ReadData."); } playState = playState_ReadData; break; } // need more data if (result == -1) { // missing or corrupt data at this page position if (TDebug.TraceAudioConverter) { TDebug.out("Corrupt or missing data in bitstream; setting playState to playState_ReadData"); } playState = playState_ReadData; break; } oggStreamState_.pagein(oggPage_); if (TDebug.TraceAudioConverter) { TDebug.out("Setting playState to playState_WriteData."); } playState = playState_WriteData; break; case playState_WriteData: // Decoding ! if (TDebug.TraceAudioConverter) { TDebug.out("Decoding"); } while (true) { result = oggStreamState_.packetout(oggPacket_); if (result == 0) { if (TDebug.TraceAudioConverter) { TDebug.out("Packetout returned 0, going to read state."); } playState = playState_ReadData; break; } // need more data else if (result == -1) { // missing or corrupt data at this page position // no reason to complain; already complained above if (TDebug.TraceAudioConverter) { TDebug.out("Corrupt or missing data in packetout bitstream; going to read state..."); // playState = playState_ReadData; // break; } continue; } else { // we have a packet. Decode it if (vorbisBlock.synthesis(oggPacket_) == 0) { // test for success! vorbisDspState.synthesis_blockin(vorbisBlock); } else { //if(TDebug.TraceAudioConverter) TDebug.out("vorbisBlock.synthesis() returned !0, going to read state"); if (TDebug.TraceAudioConverter) { TDebug.out("VorbisBlock.synthesis() returned !0, continuing."); } continue; } outputSamples(); if (playState == playState_BufferFull) { return; } } // else result != -1 } // while(true) if (oggPage_.eos() != 0) { if (TDebug.TraceAudioConverter) { TDebug.out("Settings playState to playState_Done."); } playState = playState_Done; } break; case playState_BufferFull: continueFromBufferFull(); break; case playState_Corrupt: if (TDebug.TraceAudioConverter) { TDebug.out("Corrupt Song."); // drop through to playState_Done... } case playState_Done: oggStreamState_.clear(); vorbisBlock.clear(); vorbisDspState.clear(); vorbisInfo.clear(); oggSyncState_.clear(); if (TDebug.TraceAudioConverter) { TDebug.out("Done Song."); } try { if (oggBitStream_ != null) { oggBitStream_.close(); } getCircularBuffer().close(); } catch (Exception e) { if (TDebug.TraceAudioConverter) { TDebug.out(e.getMessage()); } } break; } // switch } /** * This routine was extracted so that when the output buffer fills up, * we can break out of the loop, let the music channel drain, then * continue from where we were. */ private void outputSamples() { int samples; while ((samples = vorbisDspState.synthesis_pcmout(_pcmf, _index)) > 0) { float[][] pcmf = _pcmf[0]; bout = (samples < convsize ? samples : convsize); double fVal = 0.0; // convert doubles to 16 bit signed ints (host order) and // interleave for (i = 0; i < vorbisInfo.channels; i++) { int pointer = i * 2; //int ptr=i; int mono = _index[i]; for (int j = 0; j < bout; j++) { fVal = pcmf[i][mono + j] * 32767.; int val = (int) (fVal); if (val > 32767) { val = 32767; } if (val < -32768) { val = -32768; } if (val < 0) { val = val | 0x8000; } convbuffer[pointer] = (byte) (val); convbuffer[pointer + 1] = (byte) (val >>> 8); pointer += 2 * (vorbisInfo.channels); } } if (TDebug.TraceAudioConverter) { TDebug.out("about to write: " + 2 * vorbisInfo.channels * bout); } if (getCircularBuffer().availableWrite() < 2 * vorbisInfo.channels * bout) { if (TDebug.TraceAudioConverter) { TDebug.out("Too much data in this data packet, better return, let the channel drain, and try again..."); } playState = playState_BufferFull; return; } getCircularBuffer().write(convbuffer, 0, 2 * vorbisInfo.channels * bout); if (bytes < bufferSize_) { if (TDebug.TraceAudioConverter) { TDebug.out("Finished with final buffer of music?"); } } if (vorbisDspState.synthesis_read(bout) != 0) { if (TDebug.TraceAudioConverter) { TDebug.out("VorbisDspState.synthesis_read returned -1."); } } } // while(samples...) playState = playState_ReadData; } private void continueFromBufferFull() { if (getCircularBuffer().availableWrite() < 2 * vorbisInfo.channels * bout) { if (TDebug.TraceAudioConverter) { TDebug.out("Too much data in this data packet, better return, let the channel drain, and try again..."); // Don't change play state. } return; } getCircularBuffer().write(convbuffer, 0, 2 * vorbisInfo.channels * bout); // Don't change play state. Let outputSamples change play state, if necessary. outputSamples(); } /** * Reads headers and comments. */ private void readHeaders() throws IOException { if (TDebug.TraceAudioConverter) { TDebug.out("readHeaders("); } index = oggSyncState_.buffer(bufferSize_); buffer = oggSyncState_.data; bytes = readFromStream(buffer, index, bufferSize_); if (bytes == -1) { if (TDebug.TraceAudioConverter) { TDebug.out("Cannot get any data from selected Ogg bitstream."); } throw new IOException("Cannot get any data from selected Ogg bitstream."); } oggSyncState_.wrote(bytes); if (oggSyncState_.pageout(oggPage_) != 1) { if (bytes < bufferSize_) { throw new IOException("EOF"); } if (TDebug.TraceAudioConverter) { TDebug.out("Input does not appear to be an Ogg bitstream."); } throw new IOException("Input does not appear to be an Ogg bitstream."); } oggStreamState_.init(oggPage_.serialno()); vorbisInfo.init(); vorbisComment.init(); if (oggStreamState_.pagein(oggPage_) < 0) { // error; stream version mismatch perhaps if (TDebug.TraceAudioConverter) { TDebug.out("Error reading first page of Ogg bitstream data."); } throw new IOException("Error reading first page of Ogg bitstream data."); } if (oggStreamState_.packetout(oggPacket_) != 1) { // no page? must not be vorbis if (TDebug.TraceAudioConverter) { TDebug.out("Error reading initial header packet."); } throw new IOException("Error reading initial header packet."); } if (vorbisInfo.synthesis_headerin(vorbisComment, oggPacket_) < 0) { // error case; not a vorbis header if (TDebug.TraceAudioConverter) { TDebug.out("This Ogg bitstream does not contain Vorbis audio data."); } throw new IOException("This Ogg bitstream does not contain Vorbis audio data."); } //int i = 0; i = 0; while (i < 2) { while (i < 2) { int result = oggSyncState_.pageout(oggPage_); if (result == 0) { break; } // Need more data if (result == 1) { oggStreamState_.pagein(oggPage_); while (i < 2) { result = oggStreamState_.packetout(oggPacket_); if (result == 0) { break; } if (result == -1) { if (TDebug.TraceAudioConverter) { TDebug.out("Corrupt secondary header. Exiting."); } throw new IOException("Corrupt secondary header. Exiting."); } vorbisInfo.synthesis_headerin(vorbisComment, oggPacket_); i++; } } } index = oggSyncState_.buffer(bufferSize_); buffer = oggSyncState_.data; bytes = readFromStream(buffer, index, bufferSize_); if (bytes == -1) { break; } if (bytes == 0 && i < 2) { if (TDebug.TraceAudioConverter) { TDebug.out("End of file before finding all Vorbis headers!"); } throw new IOException("End of file before finding all Vorbis headers!"); } oggSyncState_.wrote(bytes); } byte[][] ptr = vorbisComment.user_comments; String currComment = ""; for (int j = 0; j < ptr.length; j++) { if (ptr[j] == null) { break; } currComment = (new String(ptr[j], 0, ptr[j].length - 1)).trim(); if (TDebug.TraceAudioConverter) { TDebug.out("Comment: " + currComment); } } convsize = bufferSize_ / vorbisInfo.channels; vorbisDspState.synthesis_init(vorbisInfo); vorbisBlock.init(vorbisDspState); _pcmf = new float[1][][]; _index = new int[vorbisInfo.channels]; } /** * Reads from the oggBitStream_ a specified number of Bytes(bufferSize_) worth * starting at index and puts them in the specified buffer[]. * * @param buffer * @param index * @param bufferSize_ * @return the number of bytes read or -1 if error. */ private int readFromStream(byte[] buffer, int index, int bufferSize_) { int readBytes = 0; try { readBytes = oggBitStream_.read(buffer, index, bufferSize_); } catch (Exception e) { if (TDebug.TraceAudioConverter) { TDebug.out("Cannot Read Selected Song"); } readBytes = -1; } currentBytes = currentBytes + readBytes; return readBytes; } /** * Skip bytes. */ @Override public long skip(long bytesToSkip) { if (byteslength > 0) { if (playState == playState_NeedHeaders) { try { // Headers (+ Comments). readHeaders(); } catch (IOException ioe) { playState = playState_Corrupt; return -1; } } long bytesReads = 0; if (playState != playState_Corrupt) { while (bytesToSkip > 0) { index = oggSyncState_.buffer(bufferSize_); buffer = oggSyncState_.data; bytes = readFromStream(buffer, index, bufferSize_); if (TDebug.TraceAudioConverter) { TDebug.out("More data : " + bytes); } if (bytes == -1) { playState = playState_Done; if (TDebug.TraceAudioConverter) { TDebug.out("Ogg Stream empty. Settings playState to playState_Done."); } bytesReads = 0; break; } else { bytesToSkip -= bytes; bytesReads += bytes; oggSyncState_.wrote(bytes); if (bytes == 0) { if ((oggPage_.eos() != 0) || (oggStreamState_.e_o_s != 0) || (oggPacket_.e_o_s != 0)) { if (TDebug.TraceAudioConverter) { TDebug.out("oggSyncState wrote 0 bytes: settings playState to playState_Done."); } playState = playState_Done; } if (TDebug.TraceAudioConverter) { TDebug.out("oggSyncState wrote 0 bytes: but stream not yet empty."); } break; } } oggSyncState_.pageout(oggPage_); } playState = playState_ReadData; } return bytesReads; } else { return -1; } } /** * Close the stream. */ @Override public void close() throws IOException { super.close(); oggBitStream_.close(); } }