View Javadoc

1   package org.xnap.plugin.viewer.vorbisviewer;
2   
3   import java.io.BufferedInputStream;
4   import java.io.FileInputStream;
5   import java.io.IOException;
6   import java.io.InputStream;
7   import java.util.Arrays;
8   import java.util.Hashtable;
9   import java.util.Iterator;
10  import java.util.Set;
11  import java.util.Vector;
12  
13  /***
14   *
15   * A utility for reading information and comments from the header
16   * packets of an Ogg Vorbis stream.
17   *
18   * <tt>
19   * <p>
20   * Copyright (C) 2001  Matthew Elder
21   * </p>
22   * <p>
23   * This library is free software; you can redistribute it and/or
24   * modify it under the terms of the GNU Library General Public
25   * License as published by the Free Software Foundation; either
26   * version 2 of the License, or (at your option) any later version.
27   * </p>
28   * <p>
29   * This library is distributed in the hope that it will be useful,
30   * but WITHOUT ANY WARRANTY; without even the implied warranty of
31   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
32   * GNU Library General Public License for more details.
33   * </p>
34   * <p>
35   * You should have received a copy of the GNU Library General Public
36   * License along with this library; if not, write to the
37   * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
38   * Boston, MA  02111-1307, USA.
39   * </p>
40   * </tt>
41   *
42   * @author Matthew M. Elder
43   * @version 0.1
44   *
45   */
46  
47  public class VorbisInfo {
48  
49    // stuff from info header
50    private int channels;
51    private long rate=0;
52    private long bitrate_upper;
53    private long bitrate_nominal;
54    private long bitrate_lower;
55  
56    // stuff from comment header
57    String vendor;
58    int comments=0;
59    Hashtable comment;
60  
61    // for doing checksum
62    private long[] crc_lookup = new long[256];
63    private int crc_ready=0;
64  
65    // for reading data
66    private byte[] header;
67    private byte[] packet;
68  
69    /***
70     * Initializes Ogg Vorbis information based on the header from a stream.
71     * @param s an Ogg Vorbis data stream
72     */
73    public VorbisInfo(InputStream s) throws IOException {
74  
75      /* initialize the crc_lookup table */
76      for (int i=0;i<256;i++)
77        crc_lookup[i]=_ogg_crc_entry(i);
78  
79      fetch_header_and_packet(s);
80      interpret_header_packet();
81  
82      fetch_header_and_packet(s);
83      interpret_header_packet();
84    }
85  
86    /***
87     * Returns the number of channels in the bitstream.
88     * @return the number of channels in the bitstream.
89     */
90    public int getChannels() {
91      return channels;
92    }
93  
94    /***
95     * Returns the rate of the stream in Hz.
96     * @return the rate of the stream in Hz.
97     */
98    public long getRate() {
99      return rate;
100   }
101 
102   /***
103    * Returns the <em>average</em> bitrate of the stream in kbps.
104    * @return the <em>average</em> bitrate of the stream in kbps.
105    */
106   public long getBitrate() {
107     return bitrate_nominal;
108   }
109 
110   /***
111    * Returns a <code>Vector</code> containing values from the comment header.
112    * Vorbis comments take the form:
113    *   <blockquote><tt>FIELD=SOME STRING VALUE.</tt></blockquote>
114    * Since there is no requirement for FIELD to be unique there may
115    * be multiple values for one field.
116    * <code>getComments</code> returns a <code>Vector</code> of
117    * <code>String</code> for all of the strings associated with
118    * a field.
119    * <br>Note: <code>field</code> is case insensitive.<br>
120    *
121    * @param field a case insensitive string for a field name in an Ogg Vorbis
122    *              comment header
123    *
124    * @return a Vector of strings containing field values from an Ogg Vorbis
125               comment header
126    */
127   public Vector getComments(String field) {
128     return (Vector)comment.get(field.toLowerCase());
129   }
130 
131   /***
132    * Returns a <code>Set</code> of comment field names in no particular order.
133    * @return a <code>Set</code> of comment field names in no particular order.
134    */
135   public Set getFields() {
136     return comment.keySet();
137   }
138 
139   private void fetch_header_and_packet(InputStream s) throws IOException {
140 
141     // read in the minimal packet header
142     byte[] head = new byte[27];
143     int bytes = s.read(head);
144     //System.err.println("\nDEBUG: bytes = "+bytes);
145     if(bytes < 27)
146       throw new IOException("Not enough bytes in header");
147 
148     if(!"OggS".equals(new String(head, 0, 4)))
149       throw new IOException("Not a valid Ogg Vorbis file");
150 
151     int headerbytes = (touint(head[26])) + 27;
152     //System.err.println("DEBUG: headerbytes = "+headerbytes);
153 
154     // get the rest of the header
155     byte[] head_rest = new byte[touint(head[26])];   // :-) that's a pun
156     bytes += s.read(head_rest);
157     //System.err.println("DEBUG: bytes = "+bytes);
158     header = new byte[headerbytes];
159     Arrays.fill(header,(byte)0);
160 
161     // copy the whole header into header
162     System.arraycopy(head,0,header,0,27);
163     System.arraycopy(head_rest,0,header,27,headerbytes-27);
164 
165     if(bytes<headerbytes) {
166       String error = 
167         "Error reading vorbis file: " +
168         "Not enough bytes for header + seg table";
169       throw new IOException(error);
170     }
171 
172     int bodybytes = 0;
173     for(int i=0; i<header[26]; i++)
174       bodybytes += touint(header[27+i]);
175     //System.err.println("DEBUG: bodybytes = "+bodybytes);
176 
177     packet = new byte[bodybytes];
178     Arrays.fill(packet,(byte)0);
179     bytes += s.read(packet);
180     //System.err.println("DEBUG: bytes = "+bytes);
181 
182     if(bytes<headerbytes+bodybytes) {
183       String error = 
184         "Error reading vorbis file: " +
185         "Not enough bytes for header + body";
186       throw new IOException(error);
187     }
188 
189     byte[] oldsum = new byte[4];
190     System.arraycopy(header,22,oldsum,0,4); // read existing checksum
191     Arrays.fill(header, 22, 22+4, (byte)0); // clear for calculation of checksum
192 
193     byte[] newsum = checksum();
194     if(!(new String(oldsum)).equals(new String(newsum)) ) { 
195       System.err.println("checksum failed");
196       System.err.println("old checksum: " +
197                          oldsum[0]+"|"+oldsum[1]+"|"+oldsum[2]+"|"+oldsum[3]);
198       System.err.println("new checksum: " +
199                          newsum[0]+"|"+newsum[1]+"|"+newsum[2]+"|"+newsum[3]);
200     }
201 
202   }
203 
204   private void interpret_header_packet() throws IOException {
205     byte packet_type = packet[0];
206     switch(packet_type) {
207       case 1:
208         //System.err.println("DEBUG: got header packet");
209         if(rate != 0)
210           throw new IOException("Invalid vorbis file: info already fetched");
211         fetch_info_info();
212         break;
213       case 3:
214         //System.err.println("DEBUG: got comment packet");
215         if(rate == 0)
216           throw new IOException("Invalid vorbis file: header not complete");
217         fetch_comment_info();
218         break;
219       case 5:
220         throw new IOException("Invalid vorbis file: header not complete");
221       default:
222         throw new IOException("Invalid vorbis file: bad packet header");
223     }
224   }
225 
226   /***
227    * pull the fields from the info header
228    */
229   private void fetch_info_info() throws IOException {
230 
231     // keep track of location in packet
232     int dataptr=1;  // should have already read packet[0] for packet type
233 
234     String str = new String(packet,dataptr,6);
235     dataptr += 6;
236     if(!"vorbis".equals(str))
237       throw new IOException("Not a vorbis header");
238     dataptr += 4; // skip version (4 bytes)
239 
240     channels = packet[dataptr++];  // 1 byte
241 
242     rate = toulong(read32(packet, dataptr));
243     dataptr += 4; // just read 4 bytes
244 
245     bitrate_upper = toulong(read32(packet, dataptr));
246     dataptr += 4; // just read 4 bytes
247 
248     bitrate_nominal = toulong(read32(packet, dataptr));
249     dataptr += 4; // just read 4 bytes
250 
251     bitrate_lower = toulong(read32(packet, dataptr));
252     dataptr += 4; // just read 4 bytes
253 
254     dataptr++;    // skip block sizes (4 bits each for a total of 1 byte)
255 
256     byte eop = packet[dataptr++];
257     if(eop!=1) throw new IOException("End of packet expected but not found");
258   }
259 
260   private void fetch_comment_info() throws IOException {
261     int dataptr=1;
262 
263     String str = new String(packet,dataptr,6);
264     dataptr += 6;
265     if(!"vorbis".equals(str))
266       throw new IOException("Not a vorbis header");
267 
268     comment = new Hashtable();
269 
270     long len = toulong(read32(packet,dataptr));
271     //System.err.println("DEBUG: vendor string length = "+len);
272     dataptr += 4;
273 
274     /*
275      * FIXME: Casting len to int here means possible loss of data.
276      *        I don't know who would have a comment header big
277      *        enough to cause this, but the spec says this is
278      *        an unsigned 32 bit int.
279      *        Damn java for not having unsigned ints (or it should
280      *        at least allow the use of 64 bit longs more frequently)
281      *        If I get a chance i'll write a wrapper for this(maybe).
282      */
283     vendor = new String(packet,dataptr,(int)len);
284     dataptr += len;
285 
286     // FIXME: similar problem to the vendor string length above
287     comments = (int)toulong(read32(packet,dataptr));
288     dataptr += 4;
289 
290     for(int i=0; i<comments; i++) {
291 
292       // read comment
293       len = toulong(read32(packet,dataptr));
294       dataptr += 4;
295       // FIXME: same problem as vendor string
296       String cmnt = new String(packet,dataptr,(int)len);
297       dataptr += len;
298 
299       // parse and store
300       String name = cmnt.substring(0,cmnt.indexOf('='));
301       String value = cmnt.substring(cmnt.indexOf('=')+1);
302       if(comment.containsKey(name)) {
303         Vector tmp = (Vector)comment.get(name.toLowerCase());
304         tmp.add(value);
305       } else {
306         Vector tmp = new Vector();
307         tmp.add(value);
308         comment.put(name.toLowerCase(),tmp);
309       }
310     }
311   }
312 
313   private int read32 (byte[] data, int ptr) {
314     int val = 0;
315     val  = ( touint(data[ptr])          & 0x000000ff);
316     val |= ((touint(data[ptr+1]) << 8)  & 0x0000ff00);
317     val |= ((touint(data[ptr+2]) << 16) & 0x00ff0000);
318     val |= ((touint(data[ptr+3]) << 24) & 0xff000000);
319     return val;
320   }
321 
322   private byte[] checksum() {
323     long crc_reg=0;
324 
325     for(int i=0;i<header.length;i++) {
326       int tmp = (int)(((crc_reg >>> 24)&0xff) ^ touint(header[i]));
327       crc_reg=(crc_reg<<8)^crc_lookup[tmp];
328       crc_reg &= 0xffffffff;
329     }
330     for(int i=0;i<packet.length;i++) {
331       int tmp = (int)(((crc_reg >>> 24)&0xff) ^ touint(packet[i]));
332       crc_reg=(crc_reg<<8)^crc_lookup[tmp];
333       crc_reg &= 0xffffffff;
334     }
335 
336     byte[] sum = new byte[4]; 
337     sum[0]=(byte)(crc_reg & 0xffL);
338     sum[1]=(byte)((crc_reg>>>8) & 0xffL);
339     sum[2]=(byte)((crc_reg>>>16) & 0xffL);
340     sum[3]=(byte)((crc_reg>>>24) & 0xffL);
341 
342     return sum;
343   }
344 
345   private long _ogg_crc_entry(long index){
346     long r;
347 
348     r = index << 24;
349     for (int i=0; i<8; i++) {
350       if ((r & 0x80000000L) != 0) {
351         r = (r << 1) ^ 0x04c11db7L;
352       } else {
353         r<<=1;
354       }
355     }
356     return (r & 0xffffffff);
357   }
358 
359   private long toulong(int n) {
360     return (n & 0xffffffffL);
361   }
362 
363   private int touint(byte n) {
364     return (n & 0xff);
365   }
366 
367   /***
368    * Prints out the information from an Ogg Vorbis stream in a
369    * nice, humanly-readable format.
370    */
371   public String toString() {
372 
373     String str = "";
374 
375     str += channels+" channels at "+rate+"Hz\n";
376     str += bitrate_nominal/1000+"kbps (average bitrate)\n";
377 
378     Iterator fields = comment.keySet().iterator();
379     while(fields.hasNext()) {
380       String name = (String)fields.next();
381       Vector values = (Vector)comment.get(name);
382       Iterator vi = values.iterator();
383       str += name+"=";
384       boolean dumb=false;
385       while(vi.hasNext()) {
386         if(dumb) str += ", ";
387         str += vi.next();
388         dumb=true;
389       }
390       str+="\n";
391     }
392 
393     return str;
394   }
395 
396   //tester main
397   static void main(String[] args) throws Exception {
398     if(args.length!=1) {
399       System.err.println("usage:\tjava VorbisInfo <ogg vorbis file>");
400       System.exit(1);
401     }
402     BufferedInputStream b =
403       new BufferedInputStream(new FileInputStream(args[0]));
404     VorbisInfo vi = new VorbisInfo(b);
405     System.out.println("\n"+vi);
406 
407     b.close();
408   }
409 
410 }