// 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;
}
}
}