// MP3Properties // $Id: MP3Properties.java,v 1.4 2008/05/19 06:08:18 dmitriy Exp $ // // de.vdheide.mp3: Access MP3 properties, ID3 and ID3v2 tags // Copyright (C) 1999 Jens Vonderheide <jens@vdheide.de> // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Library General Public // License as published by the Free Software Foundation; either // version 2 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Library General Public License for more details. // // You should have received a copy of the GNU Library General Public // License along with this library; if not, write to the // Free Software Foundation, Inc., 59 Temple Place - Suite 330, // Boston, MA 02111-1307, USA. /** * This class reads properties like bit rate etc. from * an MP3 file. Of course these properties are read only... * It supports ID3v2, i.e. it reliably skips * even files which tags do not use the unsynchronization scheme * * Illegal entries are marked with special return values, not * with exceptions. This enables the class to continue reading other * properties. */ package de.vdheide.mp3; import java.io.File; import java.io.InputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.Serializable; public class MP3Properties implements Serializable { /********** Constructors **********/ /** * Create a new instance connected to <code>file</code>. * Properties are read immediately. * * @param file File to connect to * @exception IOException If an I/O error occurs * @exception NoMP3FrameException If file does not contain at least one mp3 frame */ public MP3Properties(File file) throws IOException, NoMP3FrameException { readProperties(file); } /********** Public variables **********/ /** * Constants for mode */ public final static int MODE_STEREO = 0; public final static int MODE_JOINT_STEREO = 1; public final static int MODE_DUAL_CHANNEL = 2; public final static int MODE_MONO = 3; /** * Constants for emphasis */ public final static int EMPHASIS_ILLEGAL = 0; public final static int EMPHASIS_NONE = 1; public final static int EMPHASIS_5015MS = 2; public final static int EMPHASIS_CCITT = 3; /********** Public methods **********/ /** * @return MPEG level */ public int getMPEGLevel() { return level; } /** * @return Layer, 0 for illegal entries */ public int getLayer() { return layer; } /** * @return bitrate, 0 for illegal entries */ public int getBitrate() { return bitrate; } /** * @return samplerate, 0 for illegal entries */ public int getSamplerate() { return samplerate; } /** * Returns mode (mono, stereo) used in MP3 file. * Please use the constants MODE_XXX. * * @return Mode */ public int getMode() { return mode; } /** * Returns emphasis used in MP3 file. There are constants... * * @return emphasis */ public int getEmphasis() { return emphasis; } /** * @return Protection set? */ public boolean getProtection() { return protection; } /** * @return Padding set? */ public boolean getPadding() { return padding; } /** * @return Private bit set? */ public boolean getPrivate() { return privat; } /** * @return Copyright bit set? */ public boolean getCopyright() { return copy; } /** * @return Original? */ public boolean getOriginal() { return original; } /** * @return Length in seconds */ public long getLength() { return length; } public boolean isVBR() { return vbr; } /********** Private fields **********/ protected File file; protected int level; protected int layer; protected int bitrate; protected int samplerate; protected int mode; protected int emphasis; protected boolean protection; protected boolean padding; protected boolean privat; protected boolean original; protected boolean copy; protected long length; protected boolean vbr; protected int frameSize; /********** Private methods **********/ /** * Read properties from MP3 file * * @param file File to read from * @exception IOException If an I/O error occurs * @exception NoMP3FrameException If file does not contain at least one mp3 frame */ protected void readProperties(File file) throws IOException, NoMP3FrameException { this.file = file; FileInputStream in = new FileInputStream(file); // if no ID3v2 tag is present, we must undo the 10 bytes skip // done by checking in.mark(15); // skip over ID3v2 tag (if present) and get size int id3v2_tagsize = skipID3v2(in); if (id3v2_tagsize == 0) { // no tag, restart stream in.close(); in = new FileInputStream(file); } // synchronize to next MP3 frame // usually, this should not be necessary int second = synchronize(in); int third = in.read(); int fourth = in.read(); // second, third and fourth now contain the second, third and fourth byte of // MP3 frame header, respectively // read properties int vbrOffset = 0; level = convertMPEGLevel(getBit(second, 3)); layer = convertLayer(getBit(second, 2), getBit(second, 1)); // protection bit in invers (1: no crc) protection = ((getBit(second, 0)) == 0); bitrate = convertBitrate(getBit(third, 7), getBit(third, 6), getBit(third, 5), getBit(third, 4)); samplerate = convertSamplerate(getBit(third, 3), getBit(third, 2)); padding = (getBit(third, 1) == 1); privat = (getBit(third, 0) == 1); mode = convertMode(getBit(fourth, 7), getBit(fourth, 6)); copy = (getBit(fourth, 3) == 1); original = (getBit(fourth, 2) == 1); emphasis = convertEmphasis(getBit(fourth, 1), getBit(fourth, 0)); boolean bLevel = ((second >> 3) & 1) == 1; boolean bMode = ((fourth >> 6) & 3) != 3; if ( bLevel ) { /* mpeg1 */ if( bMode ) vbrOffset =32; else vbrOffset = 17; } else { /* mpeg2 */ if( bMode ) vbrOffset = 17; else vbrOffset = 9; } in.skip(vbrOffset); if ('X' == in.read()) if ('i' == in.read()) if ('n' == in.read()) if ('g' == in.read()) { vbr = true; byte[] b = new byte[4]; in.read(b); int flags = (int)Bytes.byteArrayToLong(b, 0, 4); if ((flags & 0x0001) == 0x0001) {// frame count in.read (b); frameSize = (int)Bytes.byteArrayToLong (b, 0, 4); } if ((flags & 0x0002) == 0x0002) { // byte count in.read (b); int fileSize = (int)Bytes.byteArrayToLong (b, 0, 4); } } length = calculateLength(id3v2_tagsize); in.close(); } /** * If ID3v2 tag present, skips it and returns length. * Input stream must be set to the first byte. * * @param in Stream to read from * @return Size of ID3v2 tag or 0 if not present * @exception IOException If an I/O error occurs */ protected int skipID3v2(FileInputStream in) throws IOException { ID3v2Header header = null; try { header = new ID3v2Header(new IOAdapter(in)); } catch (Exception e) { // no header return 0; } // if we reach this point, file has an ID3v2 header, // get size and skip input stream to first byte after // tag. File position is now at first byte after // header. in.skip(header.getTagSize()); return header.getTagSize(); } /** * Sets input stream to third byte of MP3 frame * header (first byte is 0xff, second is consumed in synchronizing) * and returns the byte already consumed. * * @param in Stream to read from * @return Second byte of MP3 frame header * @exception IOException If an I/O error occurs * @exception NoMP3FrameException If file does not contain at least one mp3 frame */ protected int synchronize(FileInputStream in) throws IOException, NoMP3FrameException { // skip until start of header (at least 11 bits in a row set to 1) boolean finished = false; int store = 0; while (!finished) { // read through stream until 0xff is read int skip = in.read(); while (skip != 255 && skip != -1 ) { skip = in.read(); } if (skip == -1) { // End of stream reached without finding a frame throw new NoMP3FrameException(); } // now next byte must to >= 224 store = in.read(); if (store >= 224) { // synchronized finished = true; } else if (store == -1) { // End of stream reached without finding a frame throw new NoMP3FrameException(); } else { // continue search } } // if we reach this point, an MP3 frame has been found. If // file does not contain one, method has already thrown an // NoMP3FrameException return store; } // Note: All conversion methods use an int to represent a bit /** * Converts bit to MPEG level */ protected int convertMPEGLevel(int in) { if (in == 1) { // 1 = MPEG-1 return 1; } else { // 0 = MPEG-2 return 2; } } /** * Convert 2 bits to layer */ protected int convertLayer(int in1, int in2) { if (in1==0 && in2==0) { // Illegal combination return 0; } else { // Layer is 4-in value return (4 - ((in1 << 1) + in2)); } } /** * Convert 4 bits to bitrate */ protected int convertBitrate(int in1, int in2, int in3, int in4) { // array used for conversion. // First index is the input (combined to one byte) // Second index is MPEG level and layer // (MPEG-1, layer 1; MPEG-1, layer 2; MPEG-1, layer3; // MPEG-2, layer 1; MPEG-2, layer 2; MPEG-2, layer3) int [][]convert = { { 0, 0, 0, 0, 0, 0 }, { 32, 32, 32, 32, 32, 8 }, { 64, 48, 40, 64, 48, 16 }, { 96, 56, 48, 96, 56, 24 }, { 128, 64, 56, 128, 64, 32 }, { 160, 80, 64, 160, 80, 64 }, { 192, 96, 80, 192, 96, 80 }, { 224, 112, 96, 224, 112, 56 }, { 256, 128, 112, 256, 128, 64 }, { 288, 160, 128, 288, 160, 128 }, { 320, 192, 160, 320, 192, 160 }, { 352, 224, 192, 352, 224, 112 }, { 384, 256, 224, 384, 256, 128 }, { 416, 320, 256, 416, 320, 256 }, { 448, 384, 320, 448, 384, 320 }, { 0, 0, 0, 0, 0, 0 } }; // calculate indices int index1 = (in1 << 3) | (in2 << 2) | (in3 << 1) | in4; // MPEG level and layer must already be read int index2 = (level - 1) * 3 + layer - 1; return convert[index1][index2]; } /** * Convert 2 bits to samplerate */ protected int convertSamplerate(int in1, int in2) { int sample = 0; switch ((in1 << 1) | in2) { case 0: sample = 44100; break; case 1: sample = 48000; break; case 2: sample = 32000; break; case 3: // Illegal sample = 0; break; } if (level == 1) { return sample; } else { return sample / 2; } } /** * Convert 2 bits to mode */ protected int convertMode(int in1, int in2) { int []convert = { MODE_STEREO, MODE_JOINT_STEREO, MODE_DUAL_CHANNEL, MODE_MONO }; return convert[(in1 << 1) | in2]; } /** * Convert 2 bits to emphasis */ protected int convertEmphasis(int in1, int in2) { int []convert = {EMPHASIS_NONE, EMPHASIS_5015MS, EMPHASIS_ILLEGAL, EMPHASIS_CCITT }; return convert[(in1 << 1) | in2]; } static final int [] bs = { -1, 384, 1152, 1152 }; /** * Calculate length (in seconds) of file. * This is pretty accurate, so it *may* differ from results by many other programs * (like Nightmare's ID3 Tagger ;-)) */ protected long calculateLength(int id3v2_tagsize) { /* long framesize = (long)Math.ceil( 144 * bitrate / samplerate ); // header size is 4 bytes // 1 byte is added if padding is set, // 4 bytes checksum is added if protection is NOT set int headersize = 4 + (padding ? 1 : 0) + (protection ? 0 : 4); long filesize = file.length() - id3v2_tagsize; // #frames = ceil(filesize / (framesize + headersize)) // sizeofallframes = #frames * framesize // length = sizeofallframes / bitrate * 8 / 1000 return (long)(bitrate / (Math.ceil(filesize / (framesize + headersize)) * framesize) * 0.008); */ // This does not work, at least not for small bitrates // I have to think about it TODO // Instead, go for the easy solution // if (vbr) // System.err.println("length = "+( ((long) frameSize * 1000 * bs[layer]) / samplerate)); if (vbr) return ((long) frameSize /* 1000 */* bs[layer]) / samplerate; else if (bitrate > 0) return (long)Math.floor((file.length() - id3v2_tagsize) / bitrate * 0.008); else return 0; } /** * Check if selected bit is set in <code>input</code>. * This does not really belong here, but it is needed... * * @param input Value to check * @param bit Bit number to check (0..7 with 7 MSB) * @return 1 if bit is set, 0 otherwise */ private int getBit(int input, int bit) { if ((input & (1 << bit)) > 0) { return 1; } else { return 0; } } }