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();
}
}