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