home *** CD-ROM | disk | FTP | other *** search
Java Source | 2000-09-28 | 19.9 KB | 688 lines | [TEXT/CWIE] |
- /*
- File: MP3File.java
-
- Copyright: © Copyright 1999-2000 Apple Computer, Inc. All rights reserved.
-
- Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Computer, Inc.
- ("Apple") in consideration of your agreement to the following terms, and your
- use, installation, modification or redistribution of this Apple software
- constitutes acceptance of these terms. If you do not agree with these terms,
- please do not use, install, modify or redistribute this Apple software.
-
- In consideration of your agreement to abide by the following terms, and subject
- to these terms, Apple grants you a personal, non-exclusive license, under Apple’s
- copyrights in this original Apple software (the "Apple Software"), to use,
- reproduce, modify and redistribute the Apple Software, with or without
- modifications, in source and/or binary forms; provided that if you redistribute
- the Apple Software in its entirety and without modifications, you must retain
- this notice and the following text and disclaimers in all such redistributions of
- the Apple Software. Neither the name, trademarks, service marks or logos of
- Apple Computer, Inc. may be used to endorse or promote products derived from the
- Apple Software without specific prior written permission from Apple. Except as
- expressly stated in this notice, no other rights or licenses, express or implied,
- are granted by Apple herein, including but not limited to any patent rights that
- may be infringed by your derivative works or by other works in which the Apple
- Software may be incorporated.
-
- The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO
- WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
- WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
- COMBINATION WITH YOUR PRODUCTS.
-
- IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
- GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
- OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT
- (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
- ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
- Change History (most recent first):
-
- */
-
-
- package com.apple.jens.mp3;
-
- import java.io.*;
- import java.util.Hashtable;
-
- import com.apple.mrj.MRJFileUtils;
- import com.apple.mrj.MRJOSType;
-
-
- /** Provides read-only access to MP3 metadata,
- including MPEG data such as bitrate and length,
- and ID3 tags such as track title and artist.
- Supports MPEG level 1 and 2, layers 1-3;
- and ID3, ID3v2.2 and ID3v2.3.
-
- */
-
- public class MP3File extends File {
-
- public MP3File( String path ) {
- super(path);
- }
-
-
- public MP3File( File file ) {
- super(file.getPath());
- }
-
-
- public MP3File( File dir, String name ) {
- super(dir,name);
- }
-
-
- // FILE TYPE CHECKING:
-
-
- /** Checks the filename suffix (and filetype, if on Mac OS)
- to determine whether the given file appears to be an MP3 file.
- It does <i>not</i> look at the contents of the file. */
- public static boolean isMP3FileType( File file ) {
- if( file.isDirectory() || !file.exists() )
- return false;
- if( file.getName().toLowerCase().endsWith(".mp3") )
- return true;
- try{
- MRJOSType type = MRJFileUtils.getFileType(file);
- return type == kTypeMPG3 || type == kTypeMPEG;
- }catch( IOException x ) {
- x.printStackTrace(System.err);
- return false;
- }
- }
-
- /** Checks the filename suffix (and filetype, if on Mac OS)
- to determine whether this file appears to be an MP3 file.
- It does <i>not</i> look at the contents of the file. */
- public boolean isMP3FileType( ) {
- return isMP3FileType(this);
- }
-
- private static final MRJOSType kTypeMPG3 = new MRJOSType("MPG3");
- private static final MRJOSType kTypeMPEG = new MRJOSType("MPEG");
-
-
- // MPEG ACCESSORS:
-
-
- /** Returns the <i>approximate</i> length in seconds of the audio.
- This is computed based on the bitrate and the length of the entire file,
- so it can be significantly off if there is a lot of data in the file
- other than actual MPEG frames (e.g. lengthy ID3 tags.)
- It is also blissfully ignorant of variable-bit-rate files! */
- public long getLength( ) throws IOException, MP3Exception {
- readData();
- return fLength;
- }
-
-
- /** Returns the MPEG level -- MP3 is level 1. */
- public int getMPEGLevel( ) throws IOException, MP3Exception {
- readData();
- return fMPEGLevel;
- }
-
-
- /** Returns the MPEG layer -- MP3 is layer 3. */
- public int getMPEGLayer( ) throws IOException, MP3Exception {
- readData();
- return fMPEGLayer;
- }
-
-
- /** Returns the bitrate (bits per second) of the audio data.
- This will of course not be valid for variable-bit-rate files. */
- public int getBitRate( ) throws IOException, MP3Exception {
- readData();
- return fBitRate;
- }
-
-
- /** Returns the sample rate (samples per second) of the data. */
- public int getSampleRate( ) throws IOException, MP3Exception {
- readData();
- return fSampleRate;
- }
-
-
- // ID3 ACCESSORS:
-
-
- /** Returns the version of the ID3 tag, as one of the constants
- kNoID3, kID3v1, kID3v22, kID3v23 */
- public int getID3Version( ) throws IOException, MP3Exception {
- readData();
- return fID3Version;
- }
-
-
- private Tag findTag( String name ) throws IOException, MP3Exception {
- readData();
- return (Tag) fTags.get(name);
- }
-
-
- /** Returns true if the file's ID3 data contains the given named tag.
- Tag names are four-character strings defined in the ID3 specification. */
- private boolean hasTag( String name ) throws IOException, MP3Exception {
- readData();
- return fTags.containsKey(name);
- }
-
-
- /** Returns the raw contents of a named ID3 tag as a byte array.
- Tag names are four-character strings defined in the ID3 specification. */
- public byte[] getRawTag( String name ) throws IOException, MP3Exception {
- Tag tag = findTag(name);
- if( tag != null )
- return tag.raw;
- else
- return null;
- }
-
-
- /** Returns the contents of a textual ID3 tag as a Java String.
- Tag names are four-character strings defined in the ID3 specification. */
- public synchronized String getTag( String name ) throws IOException, MP3Exception {
- Tag tag = findTag(name);
- if( tag == null )
- return null;
- if( tag.text == null ) {
- // Convert tag data to text:
- String encoding;
- int start=0, length=tag.raw.length;
- if( fID3Version==kID3v1 ) {
- encoding = "Cp437";
- } else {
- // In ID3v2, 1st byte indicates encoding to use
- encoding = (tag.raw[0]==0) ?"ISO-8859-1" :"Unicode";
- start++;
- length--;
- }
- try{
- tag.text = new String(tag.raw,start,length, encoding);
- }catch( UnsupportedEncodingException x ) {
- if(DEBUG)System.err.println("MP3File: Unsupported encoding "+encoding);
- tag.text = new String(tag.raw);
- }
- }
- return tag.text;
- }
-
-
- /** Returns the flags associated with a named ID3 tag.
- Note that only ID3v2.3 defines flags; if the file has an older-format ID3 tag,
- zero will be returned.
- Tag names are four-character strings defined in the ID3 specification. */
- public int getTagFlags( String name ) throws IOException, MP3Exception {
- Tag tag = findTag(name);
- if( tag != null )
- return tag.flags;
- else
- return 0;
- }
-
-
- /** Returns the title of the audio track, as given in the ID3 tag. */
- public String getTitle( ) throws IOException, MP3Exception {
- return getTag(kTagTitle);
- }
-
- /** Returns the name of the artist who recorded the audio track, as given in the ID3 tag. */
- public String getArtist( ) throws IOException, MP3Exception {
- return getTag(kTagArtist);
- }
-
- /** Returns the name of the album on which the audio track appears, as given in the ID3 tag. */
- public String getAlbum( ) throws IOException, MP3Exception {
- return getTag(kTagAlbum);
- }
-
- /** Returns the year the audio track was recorded, as given in the ID3 tag. */
- public String getYear( ) throws IOException, MP3Exception {
- return getTag(kTagYear);
- }
-
- /** Returns the comment field from the ID3 tag.
- In ID3v1, this field is often used to give a URL,
- but later versions have specific fields for various types of URLs. */
- public String getComment( ) throws IOException, MP3Exception {
- return getTag(kTagComment);
- }
-
-
- // PUBLIC CONSTANTS:
-
-
- /** ID3 format version codes */
- public static final int
- kNoID3 = 0,
- kID3v1 = 1,
- kID3v22 = 2,
- kID3v23 = 3;
-
-
- /** IDs of some of the more common frames.
- Use these constants with <code>getTag</code>, etc. */
- public static final String
- kTagTitle = "TIT2",
- kTagArtist = "TPE1",
- kTagAlbum = "TALB",
- kTagYear = "TYER",
- kTagComment = "COMM";
-
-
- //.MPEG INTERNALS:
-
-
- private void readMP3Info( RandomAccessFile io ) throws IOException, MP3Exception {
- // First 'synchronize' to the first MPEG frame:
- io.seek(0);
- int header;
- try{
- while(true) {
- header = io.readByte() & 0xFF;
- if( header == 0xFF ) {
- header = io.readByte() & 0xFF;
- if( header >= 0xE0 )
- break;
- }
- }
- }catch( EOFException x ) {
- throw new MP3Exception("No MPEG frames found -- is this an MP3 file?");
- }
-
- // Read the next 2 bytes of the header:
- header = (header<<16) | (io.readShort() & 0xFFFF);
-
- if(DEBUG)System.out.println("MP3File: MPEG frame header = "+
- Integer.toHexString(header));
-
- // Now parse the 3-byte frame header:
- fMPEGLevel = bit(header,19) ?1 :2;
- fMPEGLayer = readMPEGLayer(header);
- //fProtection = bit(header,16) == 0;
- fBitRate = readBitRate(header);
- fSampleRate = readSampleRate(header);
- fLength = computeLength(io);
-
- if(DEBUG)System.out.println("MP3File: level="+fMPEGLevel
- +", layer="+fMPEGLayer
- +", bitrate="+fBitRate
- +", sampleRate="+fSampleRate
- +", length="+fLength);
- }
-
-
- private static boolean bit( int i, int bit ) {
- return ((i>>bit) & 1) != 0;
- }
-
- private static int readMPEGLayer( int header ) {
- header = (header >> 17) & 0x03;
- if( header==0 )
- return 0;
- else
- return 4-header;
- }
-
- private int readBitRate( int header ) {
- int value = (header >> 12) & 0x0F;
- if( value == 15 )
- return 0; // illegal
- else
- return kBitRateTable[ 15*(3*(fMPEGLevel-1) + (fMPEGLayer-1))
- + value ] * 1000;
- // (Yes, "k" means "1000" here, not "1024" ... if I use 1024, the MP3 players
- // that connect to the server start getting left behind.
- }
-
- private static final int[] kBitRateTable =
- { 0, 32, 64, 96,128,160,192,224,256,288,320,352,384,416,448, // MPEG1 layer1
- 0, 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384, // MPEG1 layer2
- 0, 32, 40, 48, 56, 64, 80, 96,112,128,160,192,224,256,320, // MPEG1 layer3 [MP3]
- 0, 32, 64, 96,128,160,192,224,256,288,320,352,384,416,448, // MPEG2 layer1
- 0, 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384, // MPEG2 layer2
- 0, 8, 16, 24, 32, 64, 80, 56, 64,128,160,112,128,256,320 }; // MPEG2 layer3
-
- private int readSampleRate( int header ) {
- int rate;
- switch( (header>>10) & 0x03 ) {
- case 0: rate=44100; break;
- case 1: rate=48000; break;
- case 2: rate=32000; break;
- default: return 0; // illegal
- }
- if( fMPEGLevel > 1 )
- rate >>= 1;
- return rate;
- }
-
- private long computeLength( RandomAccessFile io ) throws IOException {
- long dataSize = io.length() - (io.getFilePointer()-3);
- return dataSize * 8 / fBitRate;
- }
-
-
- // ID3 INTERNALS:
-
-
- private synchronized void readData( ) throws IOException, MP3Exception {
- if( fRead )
- return;
-
- RandomAccessFile io = new RandomAccessFile(this,"r");
- try{
- fTags = new Hashtable();
-
- if( ! readID3v2(io) &&
- ! readID3v1(io) ) {
- if(DEBUG)System.err.println("MP3File: No ID3 tag found");
- fID3Version = kNoID3;
- }
-
- readMP3Info(io);
-
- fRead = true;
-
- }finally{
- try{
- io.close();
- }catch( IOException x ) {
- }
- }
- }
-
-
- /** Attempts to read ID3v1 data; returns false if there isn't any */
- private boolean readID3v1( RandomAccessFile io ) throws IOException, ID3Exception {
- // Read the last 128 bytes, where the ID3v1 tag lives:
- long start = io.length() - 128;
- if( start <= 0 )
- return false;
- byte[] buf = new byte[128];
- io.seek(start);
- io.readFully(buf);
-
- // Check for the ID3v1 header:
- if( buf[0]!='T' || buf[1]!='A' || buf[2]!='G' ) {
- if(DEBUG)System.out.println("MP3File: No ID3v1 tag found: "
- +(char)buf[0]+(char)buf[1]+(char)buf[2]);
- return false;
- }
-
- if(DEBUG)System.out.println("MP3File: Found ID3v1 tag...");
-
- // Now tweeze out each tag value:
- int pos = 3;
- for( int i=0; i<kID3v1TagCount; i++ ) {
- int size = kID3v1TagSize[i];
- int len;
- for( len=size; len>0; len-- ) {
- char c = (char)buf[pos+len-1];
- if( c!=' ' && c!='\0' )
- break;
- }
- if( len > 0 ) {
- byte[] raw = new byte[len];
- System.arraycopy(buf,pos, raw,0, len);
- Tag tag = new Tag();
- tag.id = kID3v1TagName[i];
- tag.raw = raw;
- tag.flags = 0;
- fTags.put(tag.id,tag);
- }
- pos += size;
- }
-
- fID3Version = kID3v1;
- return true;
- }
-
-
- /** Attempts to read ID3v2 data; returns false if there isn't any */
- private boolean readID3v2( RandomAccessFile io ) throws IOException, ID3Exception {
- io.seek(0);
-
- // Check for the ID3v2 header:
- byte[] buf = new byte[3];
- io.readFully(buf);
- if( buf[0]!='I' || buf[1]!='D' || buf[2]!='3' )
- return false;
-
- // Check the ID3v2 version:
- int version = io.readShort() & 0xFFFF;
- if( version <= 0x02FF )
- fID3Version = kID3v22;
- else if( version <= 0x03FF )
- fID3Version = kID3v23;
- else
- throw new ID3Exception("ID3v2 version "+(version/256.0)+" is not supported");
-
- if(DEBUG)System.err.println("MP3File: ID3v2 version is "+Integer.toHexString(version));//TEMP
-
- // Read the flags:
- int flags;
- boolean unsync, extended, experimental;
- flags = io.readByte() << 24;
- if( fID3Version==kID3v22 ) {
- unsync = (flags & 0x80000000) != 0;
- if( (flags & 0x40000000) != 0 )
- throw new ID3Exception("Cannot read compressed ID3v2.2 tag");
- extended = false;
- } else {
- unsync = (flags & 0x80000000) != 0;
- extended = (flags & 0x40000000) != 0;
- //experimental = (flags & 0x20000000) != 0;
-
- if( (flags & 0x1FFFFFFF) != 0 ) {
- // Spec says that tag might not be readable if unknown flags are set
- throw new ID3Exception("ID3v2 unknown flags in use: 0x"+Integer.toHexString(flags));
- }
- }
-
- // Read the total header size:
- int totalSize = read7BitInt(io.readInt());
-
- if(DEBUG)System.err.println("MP3File: totalSize="+totalSize);
-
- // Apply unsynchronization if necessary:
- DataInput in;
- if( unsync )
- in = new DataInputStream( new UnsyncInputStream(io) );
- else
- in = io;
-
- // Read the extended header, if any:
- if( extended ) {
- int size = in.readInt();
- if( size < 0 )
- throw new ID3Exception("Illegal extended header size "+size);
- totalSize -= 4 + size;
- // Just ignore extended header contents
- in.skipBytes(size);
- }
-
- // Read frames one at a time:
- byte[] idBuf = new byte[ fID3Version==kID3v22 ?3 :4 ];
- while( totalSize > 0 ) {
- // Read the frame name:
- in.readFully(idBuf);
- if( idBuf[0]==0 ) {
- // We've reached the end of tags and gone into blank padding space.
- totalSize = 0; // skip totalSize test after end of loop
- break;
- }
- Tag tag = new Tag();
- tag.id = new String(idBuf,"ISO-8859-1");
- if(DEBUG)System.err.println("MP3File: read tag '"+tag.id+"'");
- if( fID3Version==kID3v22 )
- tag.id = mapID3v22Name(tag.id);
-
- // Read the data size:
- int size;
- if( fID3Version==kID3v22 ) {
- // ID3v2.2 field size is 3(!) bytes:
- size = ((in.readShort() & 0xFF)<<8) | (in.readByte() & 0xFF);
- totalSize -= 6;
- } else {
- size = in.readInt();
- tag.flags = in.readShort() & 0xFFFF;
- totalSize -= 10;
- }
- if( size < 1 || size > totalSize )
- throw new ID3Exception("Illegal frame size "+size+" for frame '"+tag.id+"'");
- totalSize -= size;
-
- // Read the raw data:
- tag.raw = new byte[size];
- in.readFully(tag.raw);
-
- fTags.put(tag.id,tag);
- }
-
- if( totalSize != 0 )
- throw new ID3Exception("ID3 size mismatch");
-
- return true;
- }
-
-
- /** Converts an integer from the 7-bit-byte encoding used in ID3v2,
- in which the high bit of each byte is unused. */
- private int read7BitInt( int i ) {
- return ((i & 0x0000007F))
- | ((i & 0x00007F00) >> 1)
- | ((i & 0x007F0000) >> 2)
- | ((i & 0x7F000000) >> 3);
- }
-
-
- /** Maps an ID3v2.2 frame name to the equivalent ID3v2.3 name. */
- private String mapID3v22Name( String name ) {
- for( int i=0; i<kID3v22Converter.length; i+=2 )
- if( kID3v22Converter[i].equals(name) )
- return kID3v22Converter[i+1];
- return name; // by default return it unchanged
- }
-
-
- // PRIVATE DATA:
-
-
- /** True if the data has been read yet. */
- private boolean fRead;
-
- private int fMPEGLevel;
- private int fMPEGLayer;
- private int fBitRate;
- private int fSampleRate;
-
- /** Audio length in seconds. */
- private long fLength;
- /** Version of the ID3 tag. */
- private int fID3Version;
-
- /** Hashtable mapping ID3v2.3 tag names to Tag objects */
- private Hashtable fTags;
-
-
- // PRIVATE CONSTANTS:
-
-
- private static final int kID3v1TagCount = 7;
- private static final int[] kID3v1TagSize = {30, 30, 30, 4, 29, 1, 1};
- private static final String[] kID3v1TagName = { "TIT2", "TPE1", "TALB", "TYER",
- "COMM", "TRCK", "TCON"};
-
- /** Maps ID3v2.2 tag names to their ID3v2.3 equivalents */
- private static final String[] kID3v22Converter = { "TAL","TALB", "TBP","TBPM",
- "TCM","TCOM", "TCR","TCOP",
- "TDA","TDAT", "TDY","TDLY",
- "TEN","TENC", "TXT","TEXT",
- "TFT","TFLT", "TIM","TIME",
- "TT1","TIT1", "TT2","TIT2",
- "TT3","TIT3", "TKE","TKEY",
- "TLA","TLAN", "TLE","TLEN",
- "TMT","TMED", "TOT","TOAL",
- "TOF","TOFN", "TOL","TOLY",
- "TOA","TOPE", "TOR","TORY",
- "TP1","TPE1", "TP2","TPE2",
- "TP3","TPE3", "TP4","TPE4",
- "TPA","TPOS", "TPB","TPUB",
- "TRD","TRDA", "TSI","TSIZ",
- "TRC","TSRC", "TYE","TYER" };
-
-
- // INNER CLASSES:
-
-
- /** A simple passive struct to hold tag data. */
- private class Tag {
- public String id;
- public int flags;
- public byte[] raw;
- public String text;
- }
-
-
- /** An InputStream that performs the "unsyncing" process that decodes ID3v2 tag data */
- private class UnsyncInputStream extends InputStream {
-
- public UnsyncInputStream( RandomAccessFile in ) {
- fIn = in;
- }
-
- public int read( ) throws IOException {
- int b = fIn.read();
- if( fHadFF ) {
- fHadFF = false;
- if( b == 0x00 )
- b = fIn.read(); // Skip 00 byte after FF [ID3 2.3 spec sect. 5]
- } else if( b==0xFF )
- fHadFF = true;
- return b;
- }
-
- private final RandomAccessFile fIn;
- private boolean fHadFF = false;
-
- }
-
-
- // DEBUGGING STUFF:
-
-
- /** For testing -- can be run directly from command line or JBindery. */
- public static void main( String[] args ) throws IOException, MP3Exception {
- File dir = new File(args[0]); // 1st arg is directory of MP3 files
- String[] list = dir.list();
- for( int i=0; i<list.length; i++ ) {
- MP3File f = new MP3File(dir,list[i]);
- System.out.println("***** Testing file "+f);
- if( f.isMP3FileType() ) {
- System.out.println("\tlevel = "+f.getMPEGLevel());
- System.out.println("\tlayer = "+f.getMPEGLayer());
- System.out.println("\tbitrate= "+f.getBitRate());
- System.out.println("\tsmprate= "+f.getSampleRate());
- System.out.println("\tlength = "+f.getLength()+" secs");
- System.out.println("\tID3 = "+f.getID3Version());
- System.out.println("\ttitle = '"+f.getTitle()+"'");
- System.out.println("\tartist = '"+f.getArtist()+"'");
- System.out.println("\talbum = '"+f.getAlbum()+"'");
- System.out.println("\tyear = '"+f.getYear()+"'");
- System.out.println("\tcomment= '"+f.getComment()+"'");
- }
- }
- }
-
- private static final boolean DEBUG = false;
-
- }
-