package dolda.xiphutil; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.HashMap; import java.util.Map; import com.jcraft.jogg.Packet; import com.jcraft.jorbis.Block; import com.jcraft.jorbis.Comment; import com.jcraft.jorbis.DspState; import com.jcraft.jorbis.Info; /** * The <code>VorbisStream</code> class provides a convenient means of * decoding Vorbis data. It can be constructed either from a * <code>PacketStream</code> object which can come from any source, or * from a normal Java IO <code>InputStream</code> object. In the * latter case, it will treat the data as Ogg data and construct a * <code>PacketStream</code> accordingly. * * <p>Decoded data can be fetched either as arrays of floats, * representing individual samples, using the {@link #decode()} * function, or a Java IO <code>InputStream</code> can be fetched, * which will emit a 16-bit little endian PCM stream. * * <p>After a <code>VorbisStream</code> object has been constructed, * it will immediately provide the metadata from the Vorbis stream in * public fields. See {@link #uc}, {@link #vnd}, {@link #chn} and * {@link #rate} for details. * * @author Fredrik Tolf <code><fredrik@dolda2000.com></code> */ public class VorbisStream { private PacketStream in; private Info info = new Info(); private Comment cmt = new Comment(); private DspState dsp = new DspState(); private Block blk = new Block(dsp); private float[][][] pcmp; private int[] idxp; /** * A <code>java.util.Map</code> instance, providing the Vorbis * comments as key-value pairs decoded as normal * <code>java.lang.String</code>s. The keys are guaranteed to be * lower case and interned, and the values are exactly as provided * from the Vorbis comment. Please see the * <a href="http://www.xiph.org/vorbis/doc/v-comment.html">Vorbis * documentation</a> for more information on standardized keys. */ public final Map<String, String> uc; /** * A string identifying the program and/or library which encoded * the Vorbis data. */ public final String vnd; /** * The number of channels in this Vorbis data. One channel means * monaural sound, and two channels means stereo sound, but do * note that other values may occur as well. */ public final int chn; /** * The sampling frequency of the Vorbis data in Hertz. */ public final int rate; /** * Constructs a <code>VorbisStream</code> from a * <code>PacketStream</code>. The constructor will have ensured * that the fields containing the Vorbis metadata have been * initialized when it returns. * * @exception java.io.IOException if the * <code>PacketStream</code> itself throws an * <code>IOException</code>. * @exception FormatException if a format * error is found in the input. */ public VorbisStream(PacketStream in) throws IOException { this.in = in; info.init(); cmt.init(); for(int i = 0; i < 3; i++) { Packet pkt = in.packet(); if(pkt == null) throw(new VorbisException()); if(info.synthesis_headerin(cmt, pkt) < 0) throw(new VorbisException()); } vnd = new String(cmt.vendor, 0, cmt.vendor.length - 1, "UTF-8"); HashMap<String, String> uc = new HashMap<String, String>(); for(int i = 0; i < cmt.user_comments.length - 1; i++) { byte[] cb = cmt.user_comments[i]; String cs = new String(cb, 0, cb.length - 1, "UTF-8"); int ep; if((ep = cs.indexOf('=')) < 1) throw(new VorbisException()); uc.put(cs.substring(0, ep).toLowerCase().intern(), cs.substring(ep + 1)); } this.uc = Collections.unmodifiableMap(uc); chn = info.channels; rate = info.rate; dsp.synthesis_init(info); blk.init(dsp); pcmp = new float[1][][]; idxp = new int[chn]; } /** * Constructs a <code>VorbisStream</code> from a Java IO * <code>InputStream</code>. The data from the stream will be * decoded as Ogg data containing Vorbis. The constructor will * have ensured that the fields containing the Vorbis metadata * have been initialized when it returns. * * @exception java.io.IOException if the <code>InputStream</code> * itself throws an <code>IOException</code>. * @exception FormatException if a format error is found in * the input. */ public VorbisStream(InputStream in) throws IOException { this(new PacketStream(new PageStream(in))); } /** * Perform a decode cycle. The return value is an array of float * arrays. It contains one float array for each channel in the * Vorbis stream, each of which contains one float for each * sample. Each sample ranges from -1.0 to 1.0. * * <p>The array is guaranteed to contain as many sample arrays as * the {@link #chn} field indicates. The sample arrays are also * guaranteed to be of pairwise equal length, and to contain at * least one sample. Therefore, if <code>buf</code> is a returned * value from this function, the expression * <code>buf[0].length</code> is guaranteed to work for fetching * the number of samples from this cycle. * * @return The decoded data, or <code>null</code> when the stream * ends. * * @exception java.io.IOException if the backing input stream * itself throws an <code>IOException</code>. * @exception FormatException if a format error is found in * the input. */ public float[][] decode() throws IOException { while(true) { int len = dsp.synthesis_pcmout(pcmp, idxp); if(len > 0) { float[][] ret = new float[chn][]; for(int i = 0; i < chn; i++) { ret[i] = new float[len]; System.arraycopy(pcmp[0][i], idxp[i], ret[i], 0, len); } dsp.synthesis_read(len); return(ret); } Packet pkt = in.packet(); if(pkt == null) return(null); if((blk.synthesis(pkt) != 0) || (dsp.synthesis_blockin(blk) != 0)) throw(new VorbisException()); } } /** * Constructs and returns a <code>java.io.InputStream</code> which * uses the {@link #decode()} function to decode data, and encodes * it as a 16-bit little endian byte stream of PCM data. The * <code>close</code> function of the <code>InputStream</code> * object will chain to the {@link #close()} function of this * <code>VorbisStream</code> object. * * @return A <code>java.io.InputStream</code> object from which * the PCM stream can be read. */ public InputStream pcmstream() { return(new InputStream() { private byte[] buf; private int bufp; private boolean convert() throws IOException { float[][] inb = decode(); if(inb == null) { buf = new byte[0]; return(false); } buf = new byte[2 * chn * inb[0].length]; int p = 0; for(int i = 0; i < inb[0].length; i++) { for(int c = 0; c < chn; c++) { int s = (int)(inb[c][i] * 32767); buf[p++] = (byte)s; buf[p++] = (byte)(s >> 8); } } bufp = 0; return(true); } public int read() throws IOException { byte[] rb = new byte[1]; int ret; do { ret = read(rb); if(ret < 0) return(-1); } while(ret == 0); return(rb[0]); } public int read(byte[] dst, int off, int len) throws IOException { if((buf == null) && !convert()) return(-1); if(buf.length - bufp < len) len = buf.length - bufp; System.arraycopy(buf, bufp, dst, off, len); if((bufp += len) == buf.length) buf = null; return(len); } public void close() throws IOException { VorbisStream.this.close(); } }); } /** * Returns a string description of this object. */ public String toString() { return(String.format("Vorbis Stream (encoded by `%s', %d comments, %d channels, sampled at %d Hz)", vnd, uc.size(), chn, rate)); } /** * This function implements a main function that can be used for * either testing the functionality of this * <code>VorbisStream</code> implementation or individual * Ogg/Vorbis files. It takes one command-line argument to be * interpreted as the path to an Ogg/Vorbis file to be decoded * into PCM data. The PCM data is output to standard output. * * You can use the <code>sox</code> program to play Ogg/Vorbis * files with this class as well: * <pre> * java dolda.xiphutil.VorbisStream test.ogg | sox -t .raw -r 44100 -sw -c 2 - -t ossdsp /dev/dsp * </pre> */ public static void main(String[] args) throws Exception { VorbisStream vs = new VorbisStream(new FileInputStream(args[0])); InputStream pcm = vs.pcmstream(); byte[] buf = new byte[4096]; int ret; while((ret = pcm.read(buf)) >= 0) System.out.write(buf); } /** * Closes the stream backing this object. * * @exception java.io.IOException if the backing input stream * itself throws an <code>IOException</code>. */ public void close() throws IOException { in.close(); } }