001/*
002 *  $Source: /cvsroot2/open/projects/jOggPlayer/ca/bc/webarts/widgets/VorbisInfo.java,v $
003 *  $Name:  $
004 *  $Revision: 491 $
005 *  $Date: 2008-10-13 18:01:39 -0700 (Mon, 13 Oct 2008) $
006 *  $Locker:  $
007 */
008/**
009 *  A utility for reading information and comments from the header packets of an
010 *  Ogg Vorbis stream. <tt> <p>
011 *
012 *  Copyright (C) 2001 Matthew Elder </p> <p>
013 *
014 *  This library is free software; you can redistribute it and/or modify it
015 *  under the terms of the GNU Library General Public License as published by
016 *  the Free Software Foundation; either version 2 of the License, or (at your
017 *  option) any later version. </p> <p>
018 *
019 *  This library is distributed in the hope that it will be useful, but WITHOUT
020 *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
021 *  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
022 *  for more details. </p> <p>
023 *
024 *  You should have received a copy of the GNU Library General Public License
025 *  along with this library; if not, write to the Free Software Foundation,
026 *  Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. </p> </tt>
027 *
028 * @author     Matthew M. Elder
029 * @version    0.1
030 */
031
032package ca.bc.webarts.widgets;
033
034import java.io.*;
035import java.util.*;
036
037
038/**
039 *  This is a class to encapsulate the Vorbis comments associated in a Ogg Vorbis file.
040 *
041 * @author    Matthew Elder
042 */
043public class VorbisInfo
044{
045
046  // stuff from comment header
047  /**  Description of the Field */
048  String vendor;
049  /**  The number of comments. */
050  int comments = 0;
051  /**  The comments name value pairs in a Hashtable. */
052  Hashtable comment;
053
054  // stuff from info header
055  /**  Info Header channels. */
056  private int channels;
057  /**  Info Header  bit rate. */
058  private long rate = 0;
059  /**  Description of the Field */
060  private long bitrate_upper;
061  /**  Description of the Field */
062  private long bitrate_nominal;
063  /**  Description of the Field */
064  private long bitrate_lower;
065
066  // for doing checksum
067  /**  for doing checksum. */
068  private long[] crc_lookup = new long[256];
069  /**  Description of the Field */
070  private int crc_ready = 0;
071
072  // for reading data
073  /**  Description of the Field */
074  private byte[] header;
075  /**  Description of the Field */
076  private byte[] packet;
077
078
079  /**
080   *  Initializes Ogg Vorbis information based on the header from a stream.
081   *
082   * @param  s                an Ogg Vorbis data stream
083   * @exception  IOException  Description of the Exception
084   */
085  public VorbisInfo( InputStream s )
086    throws IOException
087  {
088
089    /*
090     *  initialize the crc_lookup table
091     */
092    for ( int i = 0; i < 256; i++ )
093    {
094      crc_lookup[i] = _ogg_crc_entry( i );
095    }
096
097    fetch_header_and_packet( s );
098    interpret_header_packet();
099
100    fetch_header_and_packet( s );
101    interpret_header_packet();
102  }
103
104  //tester main
105  /**
106   *  The main program for the VorbisInfo class
107   *
108   * @param  args           The command line arguments
109   * @exception  Exception  Description of the Exception
110   */
111  public static void main( String[] args )
112    throws Exception
113  {
114    if ( args.length != 1 )
115    {
116      System.err.println( "usage:\tjava VorbisInfo <ogg vorbis file>" );
117      System.exit( 1 );
118    }
119    BufferedInputStream b =
120        new BufferedInputStream( new FileInputStream( args[0] ) );
121    VorbisInfo vi = new VorbisInfo( b );
122    System.out.println( "\n" + vi );
123
124    b.close();
125  }
126
127
128  /**
129   *  Returns the number of channels in the bitstream.
130   *
131   * @return    the number of channels in the bitstream.
132   */
133  public int getChannels()
134  {
135    return channels;
136  }
137
138
139  /**
140   *  Returns the rate of the stream in Hz.
141   *
142   * @return    the rate of the stream in Hz.
143   */
144  public long getRate()
145  {
146    return rate;
147  }
148
149
150  /**
151   *  Returns the <em>average</em> bitrate of the stream in kbps.
152   *
153   * @return    the <em>average</em> bitrate of the stream in kbps.
154   */
155  public long getBitrate()
156  {
157    return bitrate_nominal;
158  }
159
160
161  /**
162   *  Returns a <code>Vector</code> containing values from the comment header.
163   *  Vorbis comments take the form: <blockquote><tt>FIELD=SOME STRING VALUE.
164   *  </tt></blockquote> Since there is no requirement for FIELD to be unique
165   *  there may be multiple values for one field. <code>getComments</code>
166   *  returns a <code>Vector</code> of <code>String</code> for all of the
167   *  strings associated with a field. <br>
168   *  Note: <code>field</code> is case insensitive.<br>
169   *
170   *
171   * @param  field  a case insensitive string for a field name in an Ogg Vorbis
172   *      comment header
173   * @return        a Vector of strings containing field values from an Ogg
174   *      Vorbis comment header
175   */
176  public Vector getComments( String field )
177  {
178    Vector retVal = null;
179    if (field != null && comment !=null)
180    {
181      retVal = (Vector)comment.get( field.toLowerCase() );
182    }
183    return retVal;
184  }
185
186
187  /**
188   *  Returns a <code>Set</code> of comment field names in no particular order.
189   *
190   * @return    a <code>Set</code> of comment field names in no particular
191   *      order.
192   */
193  public Set getFields()
194  {
195    return comment.keySet();
196  }
197
198
199  /**
200   *  Prints out the information from an Ogg Vorbis stream in a nice,
201   *  humanly-readable format.
202   *
203   * @return    A string representation of the object.
204   */
205  public String toString()
206  {
207
208    String str = "";
209
210    str += channels + " channels at " + rate + "Hz\n";
211    str += bitrate_nominal / 1000 + "kbps (average bitrate)\n";
212
213    Iterator fields = comment.keySet().iterator();
214    while ( fields.hasNext() )
215    {
216      String name = (String)fields.next();
217      Vector values = (Vector)comment.get( name );
218      Iterator vi = values.iterator();
219      str += name + "=";
220      boolean dumb = false;
221      while ( vi.hasNext() )
222      {
223        if ( dumb )
224        {
225          str += ", ";
226        }
227        str += vi.next();
228        dumb = true;
229      }
230      str += "\n";
231    }
232
233    return str;
234  }
235
236
237  /**
238   *  Description of the Method
239   *
240   * @param  s                Description of the Parameter
241   * @exception  IOException  Description of the Exception
242   */
243  private void fetch_header_and_packet( InputStream s )
244    throws IOException
245  {
246
247    // read in the minimal packet header
248    byte[] head = new byte[27];
249    int bytes = s.read( head );
250    //System.err.println("\nDEBUG: bytes = "+bytes);
251    if ( bytes < 27 )
252    {
253      throw new IOException( "Not enough bytes in header" );
254    }
255
256    if ( !"OggS".equals( new String( head, 0, 4 ) ) )
257    {
258      throw new IOException( "Not a valid Ogg Vorbis file" );
259    }
260
261    int headerbytes = ( touint( head[26] ) ) + 27;
262    //System.err.println("DEBUG: headerbytes = "+headerbytes);
263
264    // get the rest of the header
265    byte[] head_rest = new byte[touint( head[26] )];// :-) that's a pun
266    bytes += s.read( head_rest );
267    //System.err.println("DEBUG: bytes = "+bytes);
268    header = new byte[headerbytes];
269    Arrays.fill( header, (byte)0 );
270
271    // copy the whole header into header
272    System.arraycopy( head, 0, header, 0, 27 );
273    System.arraycopy( head_rest, 0, header, 27, headerbytes - 27 );
274
275    if ( bytes < headerbytes )
276    {
277      String error =
278          "Error reading vorbis file: " +
279          "Not enough bytes for header + seg table";
280      throw new IOException( error );
281    }
282
283    int bodybytes = 0;
284    for ( int i = 0; i < header[26]; i++ )
285    {
286      bodybytes += touint( header[27 + i] );
287    }
288    //System.err.println("DEBUG: bodybytes = "+bodybytes);
289
290    packet = new byte[bodybytes];
291    Arrays.fill( packet, (byte)0 );
292    bytes += s.read( packet );
293    //System.err.println("DEBUG: bytes = "+bytes);
294
295    if ( bytes < headerbytes + bodybytes )
296    {
297      String error =
298          "Error reading vorbis file: " +
299          "Not enough bytes for header + body";
300      throw new IOException( error );
301    }
302
303    byte[] oldsum = new byte[4];
304    System.arraycopy( header, 22, oldsum, 0, 4 );// read existing checksum
305    Arrays.fill( header, 22, 22 + 4, (byte)0 );// clear for calculation of checksum
306
307    byte[] newsum = checksum();
308    if ( !( new String( oldsum ) ).equals( new String( newsum ) ) )
309    {
310      System.err.println( "checksum failed" );
311      System.err.println( "old checksum: " +
312          oldsum[0] + "|" + oldsum[1] + "|" + oldsum[2] + "|" + oldsum[3] );
313      System.err.println( "new checksum: " +
314          newsum[0] + "|" + newsum[1] + "|" + newsum[2] + "|" + newsum[3] );
315    }
316
317  }
318
319
320  /**
321   *  Description of the Method
322   *
323   * @exception  IOException  Description of the Exception
324   */
325  private void interpret_header_packet()
326    throws IOException
327  {
328    byte packet_type = packet[0];
329    switch ( packet_type )
330    {
331        case 1:
332          //System.err.println("DEBUG: got header packet");
333          if ( rate != 0 )
334          {
335            throw new IOException( "Invalid vorbis file: info already fetched" );
336          }
337          fetch_info_info();
338          break;
339        case 3:
340          //System.err.println("DEBUG: got comment packet");
341          if ( rate == 0 )
342          {
343            throw new IOException( "Invalid vorbis file: header not complete" );
344          }
345          fetch_comment_info();
346          break;
347        case 5:
348          throw new IOException( "Invalid vorbis file: header not complete" );
349        default:
350          throw new IOException( "Invalid vorbis file: bad packet header" );
351    }
352  }
353
354
355  /**
356   *  pull the fields from the info header
357   *
358   * @exception  IOException  Description of the Exception
359   */
360  private void fetch_info_info()
361    throws IOException
362  {
363
364    // keep track of location in packet
365    int dataptr = 1;// should have already read packet[0] for packet type
366
367    String str = new String( packet, dataptr, 6 );
368    dataptr += 6;
369    if ( !"vorbis".equals( str ) )
370    {
371      throw new IOException( "Not a vorbis header" );
372    }
373    dataptr += 4;// skip version (4 bytes)
374
375    channels = packet[dataptr++];// 1 byte
376
377    rate = toulong( read32( packet, dataptr ) );
378    dataptr += 4;// just read 4 bytes
379
380    bitrate_upper = toulong( read32( packet, dataptr ) );
381    dataptr += 4;// just read 4 bytes
382
383    bitrate_nominal = toulong( read32( packet, dataptr ) );
384    dataptr += 4;// just read 4 bytes
385
386    bitrate_lower = toulong( read32( packet, dataptr ) );
387    dataptr += 4;// just read 4 bytes
388
389    dataptr++;// skip block sizes (4 bits each for a total of 1 byte)
390
391    byte eop = packet[dataptr++];
392    if ( eop != 1 )
393    {
394      throw new IOException( "End of packet expected but not found" );
395    }
396  }
397
398
399  /**
400   *  Description of the Method
401   *
402   * @exception  IOException  Description of the Exception
403   */
404  private void fetch_comment_info()
405    throws IOException
406  {
407    int dataptr = 1;
408
409    String str = new String( packet, dataptr, 6 );
410    dataptr += 6;
411    if ( !"vorbis".equals( str ) )
412    {
413      throw new IOException( "Not a vorbis header" );
414    }
415
416    comment = new Hashtable();
417
418    long len = toulong( read32( packet, dataptr ) );
419    int lenInt = Long.valueOf(len).intValue();
420    //System.err.println("DEBUG: vendor string length = "+len);
421    dataptr += 4;
422
423    /*
424     *  FIXME: Casting len to int here means possible loss of data.
425     *  I don't know who would have a comment header big
426     *  enough to cause this, but the spec says this is
427     *  an unsigned 32 bit int.
428     *  Damn java for not having unsigned ints (or it should
429     *  at least allow the use of 64 bit longs more frequently)
430     *  If I get a chance i'll write a wrapper for this(maybe).
431     */
432    vendor = new String( packet, dataptr, lenInt );
433    dataptr += lenInt;
434
435    // FIXME: similar problem to the vendor string length above
436    comments = Long.valueOf(toulong( read32( packet, dataptr ) )).intValue();
437    //System.out.println( "DEBUG: number of comments: "+comments);
438    dataptr += 4;
439    int validComment = -1;
440
441    for ( int i = 0; i < comments; i++ )
442    {
443
444      // read comment
445      len = toulong( read32( packet, dataptr ) );
446      //System.out.println( "DEBUG: length of current comment: "+len);
447      if (len>0)
448      {
449        lenInt = Long.valueOf(len).intValue();
450        dataptr += 4;
451        // FIXME: same problem as vendor string
452        String cmnt = new String( packet, dataptr, lenInt );
453        dataptr += lenInt;
454
455        // parse and store
456
457        validComment = cmnt.indexOf( '=' );
458        if (validComment >0)
459        {
460          String name = cmnt.substring( 0, validComment );
461          String value = cmnt.substring( validComment + 1 );
462          if ( comment.containsKey( name ) )
463          {
464            Vector tmp = (Vector)comment.get( name.toLowerCase() );
465            tmp.add( value );
466          }
467          else
468          {
469            Vector tmp = new Vector();
470            tmp.add( value );
471            comment.put( name.toLowerCase(), tmp );
472          }
473        }
474      }
475    }
476  }
477
478
479  /**
480   *  Description of the Method
481   *
482   * @param  data  Description of the Parameter
483   * @param  ptr   Description of the Parameter
484   * @return       Description of the Return Value
485   */
486  private int read32( byte[] data, int ptr )
487  {
488    int val = 0;
489    val = ( touint( data[ptr] ) & 0x000000ff );
490    val |= ( ( touint( data[ptr + 1] ) << 8 ) & 0x0000ff00 );
491    val |= ( ( touint( data[ptr + 2] ) << 16 ) & 0x00ff0000 );
492    val |= ( ( touint( data[ptr + 3] ) << 24 ) & 0xff000000 );
493    return val;
494  }
495
496
497  /**
498   *  Description of the Method
499   *
500   * @return    Description of the Return Value
501   */
502  private byte[] checksum()
503  {
504    long crc_reg = 0;
505
506    for ( int i = 0; i < header.length; i++ )
507    {
508      int tmp = (int)( ( ( crc_reg >>> 24 ) & 0xff ) ^ touint( header[i] ) );
509      crc_reg = ( crc_reg << 8 ) ^ crc_lookup[tmp];
510      crc_reg &= 0xffffffff;
511    }
512    for ( int i = 0; i < packet.length; i++ )
513    {
514      int tmp = (int)( ( ( crc_reg >>> 24 ) & 0xff ) ^ touint( packet[i] ) );
515      crc_reg = ( crc_reg << 8 ) ^ crc_lookup[tmp];
516      crc_reg &= 0xffffffff;
517    }
518
519    byte[] sum = new byte[4];
520    sum[0] = (byte)( crc_reg & 0xffL );
521    sum[1] = (byte)( ( crc_reg >>> 8 ) & 0xffL );
522    sum[2] = (byte)( ( crc_reg >>> 16 ) & 0xffL );
523    sum[3] = (byte)( ( crc_reg >>> 24 ) & 0xffL );
524
525    return sum;
526  }
527
528
529  /**
530   *  Description of the Method
531   *
532   * @param  index  Description of the Parameter
533   * @return        Description of the Return Value
534   */
535  private long _ogg_crc_entry( long index )
536  {
537    long r;
538
539    r = index << 24;
540    for ( int i = 0; i < 8; i++ )
541    {
542      if ( ( r & 0x80000000L ) != 0 )
543      {
544        r = ( r << 1 ) ^ 0x04c11db7L;
545      }
546      else
547      {
548        r <<= 1;
549      }
550    }
551    return ( r & 0xffffffff );
552  }
553
554
555  /**
556   *  Description of the Method
557   *
558   * @param  n  Description of the Parameter
559   * @return    Description of the Return Value
560   */
561  private long toulong( int n )
562  {
563    return ( n & 0xffffffffL );
564  }
565
566
567  /**
568   *  Description of the Method
569   *
570   * @param  n  Description of the Parameter
571   * @return    Description of the Return Value
572   */
573  private int touint( byte n )
574  {
575    return ( n & 0xff );
576  }
577
578}
579