/* * JorbisFormatConversionProvider.java * * This file is part of Tritonus: http://www.tritonus.org/ */ /* * Copyright (c) 1999 - 2003 by Matthias Pfisterer * * 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. */ /* |<--- this code is formatted to fit into 80 columns --->| */ package org.tritonus.sampled.convert.jorbis; import java.io.EOFException; import java.io.InputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import org.tritonus.share.TDebug; import org.tritonus.share.sampled.AudioFormats; import org.tritonus.share.sampled.convert.TAsynchronousFilteredAudioInputStream; import org.tritonus.share.sampled.convert.TEncodingFormatConversionProvider; import com.jcraft.jogg.SyncState; import com.jcraft.jogg.StreamState; import com.jcraft.jogg.Page; import com.jcraft.jogg.Packet; import com.jcraft.jorbis.Info; import com.jcraft.jorbis.Comment; import com.jcraft.jorbis.DspState; import com.jcraft.jorbis.Block; /** Pure-java decoder for ogg vorbis streams. The FormatConversionProvider uses the pure-java ogg vorbis decoder from www.jcraft.com/jorbis/. <p> See vorbis spec for more info: http://xiph.org/vorbis/doc/Vorbis_I_spec.html @author Matthias Pfisterer */ public class JorbisFormatConversionProvider extends TEncodingFormatConversionProvider { // only used as abbreviation private static final AudioFormat.Encoding VORBIS = new AudioFormat.Encoding("VORBIS"); private static final AudioFormat.Encoding PCM_SIGNED = new AudioFormat.Encoding("PCM_SIGNED"); private static final AudioFormat[] INPUT_FORMATS = { // mono // TODO: mechanism to make the double specification with // different endianess obsolete. new AudioFormat(VORBIS, -1.0F, -1, 1, -1, -1.0F, false), new AudioFormat(VORBIS, -1.0F, -1, 1, -1, -1.0F, true), // stereo new AudioFormat(VORBIS, -1.0F, -1, 2, -1, -1.0F, false), new AudioFormat(VORBIS, -1.0F, -1, 2, -1, -1.0F, true), // TODO: other channel configurations }; private static final AudioFormat[] OUTPUT_FORMATS = { // mono, 16 bit signed new AudioFormat(PCM_SIGNED, -1.0F, 16, 1, 2, -1.0F, false), new AudioFormat(PCM_SIGNED, -1.0F, 16, 1, 2, -1.0F, true), // stereo, 16 bit signed new AudioFormat(PCM_SIGNED, -1.0F, 16, 2, 4, -1.0F, false), new AudioFormat(PCM_SIGNED, -1.0F, 16, 2, 4, -1.0F, true), // TODO: other channel configurations }; /** Constructor. */ // TODO: check interaction with base class public JorbisFormatConversionProvider() { super(Arrays.asList(INPUT_FORMATS), Arrays.asList(OUTPUT_FORMATS)/*, true, // new behaviour false*/); // bidirectional .. constants UNIDIR../BIDIR..? } public AudioInputStream getAudioInputStream(AudioFormat targetFormat, AudioInputStream audioInputStream) { /** The AudioInputStream to return. */ AudioInputStream convertedAudioInputStream = null; if (TDebug.TraceAudioConverter) { TDebug.out(">JorbisFormatConversionProvider.getAudioInputStream(): begin"); TDebug.out("checking if conversion supported"); TDebug.out("from: " + audioInputStream.getFormat()); TDebug.out("to: " + targetFormat); } // what is this ??? targetFormat=getDefaultTargetFormat(targetFormat, audioInputStream.getFormat()); if (isConversionSupported(targetFormat, audioInputStream.getFormat())) { if (TDebug.TraceAudioConverter) { TDebug.out("conversion supported; trying to create DecodedJorbisAudioInputStream"); } convertedAudioInputStream = new DecodedJorbisAudioInputStream( targetFormat, audioInputStream); } else { if (TDebug.TraceAudioConverter) { TDebug.out("conversion not supported; throwing IllegalArgumentException"); TDebug.out("<"); } throw new IllegalArgumentException("conversion not supported"); } if (TDebug.TraceAudioConverter) { TDebug.out("<JorbisFormatConversionProvider.getAudioInputStream(): end"); } return convertedAudioInputStream; } // TODO: recheck !! protected AudioFormat getDefaultTargetFormat(AudioFormat targetFormat, AudioFormat sourceFormat) { if (TDebug.TraceAudioConverter) { TDebug.out("JorbisFormatConversionProvider.getDefaultTargetFormat(): target format: " + targetFormat); } if (TDebug.TraceAudioConverter) { TDebug.out("JorbisFormatConversionProvider.getDefaultTargetFormat(): source format: " + sourceFormat); } AudioFormat newTargetFormat = null; // return first of the matching formats // pre-condition: the predefined target formats (FORMATS2) must be well-defined ! Iterator iterator=getCollectionTargetFormats().iterator(); while (iterator.hasNext()) { AudioFormat format = (AudioFormat) iterator.next(); if (AudioFormats.matches(targetFormat, format)) { newTargetFormat = format; } } if (newTargetFormat == null) { throw new IllegalArgumentException("conversion not supported"); } if (TDebug.TraceAudioConverter) { TDebug.out("JorbisFormatConversionProvider.getDefaultTargetFormat(): new target format: " + newTargetFormat); } // hacked together... // ... only works for PCM target encoding ... newTargetFormat = new AudioFormat(targetFormat.getEncoding(), sourceFormat.getSampleRate(), newTargetFormat.getSampleSizeInBits(), newTargetFormat.getChannels(), newTargetFormat.getFrameSize(), sourceFormat.getSampleRate(), newTargetFormat.isBigEndian()); if (TDebug.TraceAudioConverter) { TDebug.out("JorbisFormatConversionProvider.getDefaultTargetFormat(): really new target format: " + newTargetFormat); } return newTargetFormat; } /** AudioInputStream returned on decoding of ogg vorbis. An instance of this class is returned if you call AudioSystem.getAudioInputStream(AudioFormat, AudioInputStream) to decode an ogg/vorbis stream. This class contains the logic of maintaining buffers and calling the decoder. */ /* Class should be private, but is public due to a bug (?) in the aspectj compiler. */ /*private*/public static class DecodedJorbisAudioInputStream extends TAsynchronousFilteredAudioInputStream { private static final int BUFFER_MULTIPLE = 4; private static final int BUFFER_SIZE = BUFFER_MULTIPLE * 256 * 2; private static final int CONVSIZE = BUFFER_SIZE * 2; private InputStream m_oggBitStream = null; // Ogg structures private SyncState m_oggSyncState = null; private StreamState m_oggStreamState = null; private Page m_oggPage = null; private Packet m_oggPacket = null; // Vorbis structures private Info m_vorbisInfo = null; private Comment m_vorbisComment = null; private DspState m_vorbisDspState = null; // actually is an ogg structure private Block m_vorbisBlock = null; private List<String> m_songComments = new ArrayList<String>(); // is altered later in a dubious way private int convsize = -1; // BUFFER_SIZE * 2; // TODO: further checking private byte[] convbuffer = new byte[CONVSIZE]; private float[][][] _pcmf = null; private int[] _index = null; // TODO: introduce state variable private boolean m_bHeadersExpected; /** * Constructor. */ public DecodedJorbisAudioInputStream(AudioFormat outputFormat, AudioInputStream bitStream) { super(outputFormat, AudioSystem.NOT_SPECIFIED); if (TDebug.TraceAudioConverter) { TDebug.out("DecodedJorbisAudioInputStream.<init>(): begin"); } m_oggBitStream = bitStream; m_bHeadersExpected = true; init_jorbis(); if (TDebug.TraceAudioConverter) { TDebug.out("DecodedJorbisAudioInputStream.<init>(): end"); } } /** * Initializes all the jOrbis and jOgg vars that are used for song playback. */ private void init_jorbis() { m_oggSyncState = new SyncState(); m_oggStreamState = new StreamState(); m_oggPage = new Page(); m_oggPacket = new Packet(); m_vorbisInfo = new Info(); m_vorbisComment = new Comment(); m_vorbisDspState = new DspState(); m_vorbisBlock = new Block(m_vorbisDspState); m_oggSyncState.init(); } /** Callback from circular buffer. */ public void execute() { if (TDebug.TraceAudioConverter) TDebug.out(">DecodedJorbisAudioInputStream.execute(): begin"); if (m_bHeadersExpected) { if (TDebug.TraceAudioConverter) TDebug.out("reading headers..."); // Headers (+ Comments). try { readHeaders(); } catch (IOException e) { if (TDebug.TraceAllExceptions) { TDebug.out(e); } closePhysicalStream(); if (TDebug.TraceAudioConverter) TDebug.out("<DecodedJorbisAudioInputStream.execute(): end"); return; } m_bHeadersExpected = false; setupVorbisStructures(); } if (TDebug.TraceAudioConverter) TDebug.out("decoding..."); // Decoding ! while (writeMore()) { try { readOggPacket(); } catch (IOException e) { if (TDebug.TraceAllExceptions) { TDebug.out(e); } closePhysicalStream(); if (TDebug.TraceAudioConverter) TDebug.out("<DecodedJorbisAudioInputStream.execute(): end"); return; } decodeDataPacket(); } if (m_oggPacket.e_o_s != 0) { if (TDebug.TraceAudioConverter) TDebug.out("end of vorbis stream reached"); shutDownLogicalStream(); } if (TDebug.TraceAudioConverter) TDebug.out("<DecodedJorbisAudioInputStream.execute(): end"); } /* The end of the vorbis stream is reached. So we shut down the logical bitstream and vorbis structures. */ private void shutDownLogicalStream() { m_oggStreamState.clear(); m_vorbisBlock.clear(); m_vorbisDspState.clear(); m_vorbisInfo.clear(); m_bHeadersExpected = true; } private void closePhysicalStream() { if (TDebug.TraceAudioConverter) TDebug.out("DecodedJorbisAudioInputStream.closePhysicalStream(): begin"); m_oggSyncState.clear(); try { if (m_oggBitStream != null) { m_oggBitStream.close(); } getCircularBuffer().close(); } catch (Exception e) { if (TDebug.TraceAllExceptions) { TDebug.out(e); } } if (TDebug.TraceAudioConverter) TDebug.out("DecodedJorbisAudioInputStream.closePhysicalStream(): end"); } /** Read and process all three vorbis headers. */ private void readHeaders() throws IOException { readIdentificationHeader(); readCommentAndCodebookHeaders(); processComments(); } /** Read the vorbis identification header. @throw IOException */ private void readIdentificationHeader() throws IOException { readOggPage(); m_oggStreamState.init(m_oggPage.serialno()); m_vorbisInfo.init(); m_vorbisComment.init(); if (m_oggStreamState.pagein(m_oggPage) < 0) { throw new IOException("can't read first page of Ogg bitstream data, perhaps stream version mismatch"); } if (m_oggStreamState.packetout(m_oggPacket) != 1) { throw new IOException("can't read initial header packet"); } if (m_vorbisInfo.synthesis_headerin(m_vorbisComment, m_oggPacket) < 0) { throw new IOException("packet is not a vorbis header"); } } /** Read the comment header and the codebook header pages. */ private void readCommentAndCodebookHeaders() throws IOException { for (int i = 0; i < 2; i++) { readOggPacket(); if (m_vorbisInfo.synthesis_headerin(m_vorbisComment, m_oggPacket) < 0) { throw new IOException("packet is not a vorbis header"); } } } /** */ private void processComments() { byte[][] ptr = m_vorbisComment.user_comments; String currComment = ""; m_songComments.clear(); for (int j = 0; j < ptr.length; j++) { if (ptr[j] == null) { break; } currComment = (new String(ptr[j], 0, ptr[j].length - 1)).trim(); m_songComments.add(currComment); /* if (currComment.toUpperCase().startsWith("ARTIST")) { String artistLabelValue = currComment.substring(7); } else if (currComment.toUpperCase().startsWith("TITLE")) { String titleLabelValue = currComment.substring(6); String miniDragLabel = currComment.substring(6); } */ if (TDebug.TraceAudioConverter) TDebug.out("Comment: " + currComment); } currComment = "Bitstream: " + m_vorbisInfo.channels + " channel," + m_vorbisInfo.rate + "Hz"; m_songComments.add(currComment); if (TDebug.TraceAudioConverter) TDebug.out(currComment); if (TDebug.TraceAudioConverter) currComment = "Encoded by: " + new String(m_vorbisComment.vendor, 0, m_vorbisComment.vendor.length - 1); m_songComments.add(currComment); if (TDebug.TraceAudioConverter) TDebug.out(currComment); } /** Setup structures needed for vorbis decoding. Precondition: m_vorbisInfo has to be initialized completely (i.e. all three headers are read). */ private void setupVorbisStructures() { convsize = BUFFER_SIZE / m_vorbisInfo.channels; m_vorbisDspState.synthesis_init(m_vorbisInfo); m_vorbisBlock.init(m_vorbisDspState); _pcmf = new float[1][][]; _index = new int[m_vorbisInfo.channels]; } /** Decode a packet of vorbis data. This method assumes that a packet is available in {@link #m_oggPacket m_oggPacket}. The content of this packet is run through the decoder. The resulting PCM data are written to the circular buffer. */ private void decodeDataPacket() { int samples; if (m_vorbisBlock.synthesis(m_oggPacket) == 0) { // test for success! m_vorbisDspState.synthesis_blockin(m_vorbisBlock); } while ((samples = m_vorbisDspState.synthesis_pcmout(_pcmf, _index)) > 0) { float[][] pcmf = _pcmf[0]; int bout = (samples < convsize ? samples : convsize); // convert floats to signed ints and // interleave for (int nChannel = 0; nChannel < m_vorbisInfo.channels; nChannel++) { int pointer = nChannel * getSampleSizeInBytes(); int mono = _index[nChannel]; for (int j = 0; j < bout; j++) { float fVal = pcmf[nChannel][mono + j]; clipAndWriteSample(fVal, pointer); pointer += getFrameSize(); } } m_vorbisDspState.synthesis_read(bout); getCircularBuffer().write(convbuffer, 0, getFrameSize() * bout); } } /** Scale and clip the sample and write it to convbuffer. */ private void clipAndWriteSample(float fSample, int nPointer) { int nSample; // TODO: check if clipping is necessary if (fSample > 1.0F) { fSample = 1.0F; } if (fSample < -1.0F) { fSample = -1.0F; } switch (getFormat().getSampleSizeInBits()) { case 16: nSample = (int) (fSample * 32767.0F); if (isBigEndian()) { convbuffer[nPointer++] = (byte) (nSample >> 8); convbuffer[nPointer] = (byte) (nSample & 0xFF); } else { convbuffer[nPointer++] = (byte) (nSample & 0xFF); convbuffer[nPointer] = (byte) (nSample >> 8); } break; case 24: nSample = (int) (fSample * 8388607.0F); if (isBigEndian()) { convbuffer[nPointer++] = (byte) (nSample >> 16); convbuffer[nPointer++] = (byte) ((nSample >>> 8) & 0xFF); convbuffer[nPointer] = (byte) (nSample & 0xFF); } else { convbuffer[nPointer++] = (byte) (nSample & 0xFF); convbuffer[nPointer++] = (byte) ((nSample >>> 8) & 0xFF); convbuffer[nPointer] = (byte) (nSample >> 16); } break; case 32: nSample = (int) (fSample * 2147483647.0F); if (isBigEndian()) { convbuffer[nPointer++] = (byte) (nSample >> 24); convbuffer[nPointer++] = (byte) ((nSample >>> 16) & 0xFF); convbuffer[nPointer++] = (byte) ((nSample >>> 8) & 0xFF); convbuffer[nPointer] = (byte) (nSample & 0xFF); } else { convbuffer[nPointer++] = (byte) (nSample & 0xFF); convbuffer[nPointer++] = (byte) ((nSample >>> 8) & 0xFF); convbuffer[nPointer++] = (byte) ((nSample >>> 16) & 0xFF); convbuffer[nPointer] = (byte) (nSample >> 24); } break; } } /** Read an ogg packet. This method does everything necessary to read an ogg packet. If needed, it calls {@link #readOggPage readOggPage()}, which, in turn, may read more data from the stream. The resulting packet is placed in {@link #m_oggPacket m_oggPacket} (for which the reference is not altered; is has to be initialized before). */ private void readOggPacket() throws IOException { while (true) { int result = m_oggStreamState.packetout(m_oggPacket); if (result == 1) { return; } if (result == -1) { throw new IOException("can't read packet"); } readOggPage(); if (m_oggStreamState.pagein(m_oggPage) < 0) { throw new IOException("can't read page of Ogg bitstream data"); } } } /** Read an ogg page. This method does everything necessary to read an ogg page. If needed, it reads more data from the stream. The resulting page is placed in {@link #m_oggPage m_oggPage} (for which the reference is not altered; is has to be initialized before). Note: this method doesn't deliver the page read to a StreamState object (which assembles pages to packets). This has to be done by the caller. */ private void readOggPage() throws IOException { while (true) { int result = m_oggSyncState.pageout(m_oggPage); if (result == 1) { return; } // we need more data from the stream int nIndex = m_oggSyncState.buffer(BUFFER_SIZE); // TODO: call stream.read() directly int nBytes = readFromStream(m_oggSyncState.data, nIndex, BUFFER_SIZE); // TODO: This clause should become obsolete; readFromStream() should // propagate exceptions directly. if (nBytes == -1) { throw new EOFException(); } m_oggSyncState.wrote(nBytes); } } /** Read raw data from to ogg bitstream. Reads from {@ #m_oggBitStream m_oggBitStream} a specified number of bytes into a buffer, starting at a specified buffer index. @param buffer the where the read data should be put into. Its length has to be at least nStart + nLength. @param nStart @param nLength the number of bytes to read @return the number of bytes read (maybe 0) or -1 if there is no more data in the stream. */ private int readFromStream(byte[] buffer, int nStart, int nLength) throws IOException { return m_oggBitStream.read(buffer, nStart, nLength); } /** */ private int getSampleSizeInBytes() { return getFormat().getFrameSize() / getFormat().getChannels(); } /** . @return . */ private int getFrameSize() { return getFormat().getFrameSize(); } /** Returns if this stream (the decoded one) is big endian. @return true if this stream is big endian. */ private boolean isBigEndian() { return getFormat().isBigEndian(); } /** * */ public void close() throws IOException { super.close(); m_oggBitStream.close(); } } } /*** JorbisFormatConversionProvider.java ***/