/* You may freely copy, distribute, modify and use this class as long as the original author attribution remains intact. See message below. Copyright (C) 2001 Christian Pesch. All Rights Reserved. */ package slash.metamusic.mp3; import slash.metamusic.mp3.util.BitConversion; import slash.metamusic.mp3.util.CRC16; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.logging.Logger; /** * My instances represent the properties of a MP3 File, * which is parsed. Then, information about the MP3 meta data * may be queried. * * @author Christian Pesch * @version $Id: MP3Properties.java 958 2007-02-28 14:44:37Z cpesch $ */ public class MP3Properties extends AbstractAudioProperties { /** * Logging output */ protected static final Logger log = Logger.getLogger(ID3v2Frame.class.getName()); // (MPEG-1, layer 1; MPEG-1, layer 2; MPEG-1, layer3; // MPEG-2, layer 1; MPEG-2, layer 2; MPEG-2, layer3; // MPEG-2.5, layer 1; MPEG-2.5, layer 2; MPEG-2.5, layer3) public static final int[][] BIT_RATES = { {0, 0, 0, 0, 0, 0, 0, 0, 0}, {32, 32, 32, 32, 32, 8, 32, 32, 8}, {64, 48, 40, 64, 48, 16, 40, 48, 16}, {96, 56, 48, 96, 56, 24, 48, 56, 24}, {128, 64, 56, 128, 64, 32, 56, 64, 32}, {160, 80, 64, 160, 80, 64, 64, 80, 40}, {192, 96, 80, 192, 96, 80, 80, 96, 48}, {224, 112, 96, 224, 112, 56, 96, 112, 56}, {256, 128, 112, 256, 128, 64, 112, 128, 64}, {288, 160, 128, 288, 160, 128, 128, 144, 80}, {320, 192, 160, 320, 192, 160, 160, 160, 96}, {352, 224, 192, 352, 224, 112, 192, 176, 112}, {384, 256, 224, 384, 256, 128, 224, 192, 128}, {416, 320, 256, 416, 320, 256, 256, 224, 144}, {448, 384, 320, 448, 384, 320, 320, 256, 160}, {0, 0, 0, 0, 0, 0, 0, 0, 0} }; // MPEG-1; MPEG-2; MPEG-2.5 public static final int[][] SAMPLE_FREQUENCIES = { {44100, 22050, 11025}, {48000, 24000, 12000}, {32000, 16000, 8000} }; public static final String[] VERSION_STRINGS = { "MPEG-1", "MPEG-2", "MPEG-2.5" }; public static final String[] LAYER_STRINGS = { "I", "II", "III" }; /** * Constants for emphasis */ public final static int EMPHASIS_NONE = 0; public final static int EMPHASIS_5015MS = 1; public final static int EMPHASIS_RESERVED = 2; public final static int EMPHASIS_CCITT_J17 = 3; public static final int[] EMPHASISES = { EMPHASIS_NONE, EMPHASIS_RESERVED, EMPHASIS_5015MS, EMPHASIS_CCITT_J17 }; public static final String[] EMPHASIS_STRINGS = { "none", "reserved", "50/15ms", "CCIT J.17" }; public static final String VBR_FLAG = "Xing"; public static final int FRAMES_FLAG = 0x0001; public static final int BYTES_FLAG = 0x0002; public static final int TOC_FLAG = 0x0004; public static final int VBR_SCALE_FLAG = 0x0008; public static final int CHECKSUM_SIZE = 2; private static final int PADDING_SEARCH_SIZE = 256 + 16; public static final int MILLISECONDS_PER_FRAME = 26; /** * Construct new MP3 properties. */ public MP3Properties() { } // --- read/write object ----------------------------------- public boolean read(InputStream in) throws NoMP3FrameException, IOException { valid = false; readSize = 0; // synchronize to next MP3 frame // usually, this should not be necessary int second = synchronize(in); int third = in.read(); int fourth = in.read(); if (readSize > 0) { log.fine("First FFFB after " + readSize + " bytes"); } // second, third and fourth now contain the second, third and fourth byte of // MP3 frame header, respectively valid = parse(second, third, fourth); // header checksum if (valid && protection) { byte[] buffer = new byte[CHECKSUM_SIZE]; if (in.read(buffer, 0, CHECKSUM_SIZE) != CHECKSUM_SIZE) throw new IOException("Read invalid checksum"); checksum = (short) BitConversion.extract2Endian(buffer); CRC16 crc = new CRC16(); crc.update(0xFF, 8); crc.update(second, 8); crc.update(third, 8); crc.update(fourth, 8); short compare = crc.getValue(); if (checksum != compare) log.severe("Invalid header checksum found: " + checksum + ", calculated: " + compare); } vbr = searchVBR(in); return valid; } protected boolean parse(int second, int third, int fourth) throws UnsupportedEncodingException { valid = false; version = convertMPEGVersion(BitConversion.getBit(second, 4), BitConversion.getBit(second, 3)); layer = convertLayer(BitConversion.getBit(second, 2), BitConversion.getBit(second, 1)); // protection bit in invers (1: no crc) protection = ((BitConversion.getBit(second, 0)) == 0); bitrate = convertBitrate(BitConversion.getBit(third, 7), BitConversion.getBit(third, 6), BitConversion.getBit(third, 5), BitConversion.getBit(third, 4)) * 1000; sampleFrequency = convertSampleFrequency(BitConversion.getBit(third, 3), BitConversion.getBit(third, 2)); padding = (BitConversion.getBit(third, 1) == 1); privated = (BitConversion.getBit(third, 0) == 1); mode = convertMode(BitConversion.getBit(fourth, 7), BitConversion.getBit(fourth, 6)); modeExtension = convertModeExtension(BitConversion.getBit(fourth, 5), BitConversion.getBit(fourth, 4)); copyrighted = (BitConversion.getBit(fourth, 3) == 1); original = (BitConversion.getBit(fourth, 2) == 1); emphasis = convertEmphasis(BitConversion.getBit(fourth, 1), BitConversion.getBit(fourth, 0)); valid = true; return valid; } /** * Converts bit to MPEG version. * <p/> * Bit Version * 1 1 MPEG-1 * 1 0 MPEG-2 * 0 0 MPEG-2.5 * <p/> * Note: Uses an int to represent a bit */ protected int convertMPEGVersion(int in1, int in2) { if (in1 == 1) { if (in2 == 1) { // 1 1 = MPEG-1 return 1; } else { // 1 0 = MPEG-2 return 2; } } else { if (in2 == 0) { // 0 0 = MPEG-2.5 return 3; } else { // Illegal combination return 0; } } } /** * Convert 2 bits to layer: * <p/> * Bit Layer * 0 0 Not defined * 0 1 Layer III * 1 0 Layer II * 1 1 Layer I * <p/> * Note: Uses an int to represent a bit */ 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 * * @param in1 eighth bit of the third byte of the MPEG header represented by an int * @param in2 seventh bit of the third byte of the MPEG header represented by an int * @param in3 sixth bit of the third byte of the MPEG header represented by an int * @param in4 fifth bit of the third byte of the MPEG header represented by an int * @return the bitrate in Kbit/s */ protected int convertBitrate(int in1, int in2, int in3, int in4) { // first index is the input (combined to one byte) int index1 = (in1 << 3) | (in2 << 2) | (in3 << 1) | in4; // second index is MPEG version and layer int index2 = (version - 1) * 3 + layer - 1; if (index1 < 0 || index1 > BIT_RATES.length) { log.severe("Unknown bitrate index: " + index1); return 0; } if (index2 < 0 || index2 > BIT_RATES[index1].length) { log.severe("Unknown MPEG version and layer index: " + index2); return 0; } return BIT_RATES[index1][index2]; } /** * Convert 2 bits to sample frequency * * @param in1 fourth bit of the third byte of the MPEG header represented by an int * @param in2 third bit of the third byte of the MPEG header represented by an int * @return the sample frequency in Hz */ protected int convertSampleFrequency(int in1, int in2) { // first index is input (combined to one byte) int index1 = (in1 << 1) | in2; // second index is MPEG version int index2 = version - 1; if (index1 < 0 || index1 >= SAMPLE_FREQUENCIES.length) { log.severe("Unknown sample frequency index: " + index1); return 0; } if (index2 < 0 || index2 >= SAMPLE_FREQUENCIES[index1].length) { log.severe("Unknown MPEG version: " + index2); return 0; } return SAMPLE_FREQUENCIES[index1][index2]; } /** * Convert 2 bits to mode * <p/> * Note: Uses an int to represent a bit */ protected int convertMode(int in1, int in2) { int index = (in1 << 1) | in2; // illegal values if (index < 0 || index > modes.length) return 0; return modes[index]; } /** * Convert 2 bits to mode extension * <p/> * Note: Uses an int to represent a bit */ protected int convertModeExtension(int in1, int in2) { //noinspection UnnecessaryLocalVariable int index = (in1 << 1) | in2; // The purpose of the mode extension field is different for // different layers, but I really don't know exactly what it's // for. return index; } /** * Convert 2 bits to emphasis * <p/> * Note: Uses an int to represent a bit */ protected int convertEmphasis(int in1, int in2) { int index = (in1 << 1) | in2; // illegal values if (index < 0 || index > EMPHASISES.length) return 0; return EMPHASISES[index]; } /** * 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 * @throws IOException If an I/O error occurs * @throws NoMP3FrameException If file does not contain at least one mp3 frame */ protected int synchronize(InputStream 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 = readByte(in); while (skip != 0xFF && skip != -1) { skip = readByte(in); } if (skip == -1) { // End of stream reached without finding a frame throw new NoMP3FrameException(); } // now next byte must to >= 0xE0 store = readByte(in); if (store >= 0xE0) { // synchronized finished = true; } else if (store == -1) { // End of stream reached without finding a frame throw new NoMP3FrameException(); } else { // continue search } } // reduce the read size by the 0xFFFB header already read readSize -= 2; // 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; } /** * Searches for a VBR header. * <p/> * <p>Xing VBR Headers explained: * <p/> * Each frame represents exactly .026 seconds of playback time. * But frames in a VBR mp3 vary in size, according to the bit rate * and encoding methods used, on a frame by frame basis. * <p/> * This makes random seeks to percentage points within a VBR file * rather difficult without reading/decoding the file to count * frames up to the seek point -- which also requires knowing the * file size and frame count in advance. * <p/> * The Xing VBR header eases this problem, using a header which * (optionally) provides a framecount (Xframes), byte count (Xbytes), * and a 100-position TOC for seeking to percentages with the file. * The seeks are not exactly dead on, so the software using them * must then scan for the next Sync pattern to find the next frame * after seeking to the byte offset indicated. * <p/> * The seek table itself does not store byte offsets, but rather * scale factors from 0 to 255 representing the seek points as the * number of 1/256's of the filesize. * <p/> * For example, to do a seek to the P% point one would do something * like this to calculate the corresponding byte offset: * <p/> * if (P < 0) P = 0; * if (P > 99) P = 99; * byteoffset = Xbytes * Xtoc[P] / 256; * <p/> * If the percentage is a fractional (floating point) value, * then we must interpolate between two table entries for * more accurate positioning: * <p/> * int Pi; float Tp, Tr; * if (P < 0.0) P = 0.0; * if (P > 100.0) P = 100.0; * Pi = (int)P; * if (Pi >= 99) { * Tp = Xtoc[Pi = 99]; * Tr = 256.0; * } else { * Tp = Xtoc[Pi]; * Tr = Xtoc[Pi+1]; * } * P = ( (P - Pi) * (Tr - Tp) + Tp ) * (1.0 / 256.0); * byteoffset = (int)(P * Xbytes); * <p/> * The Xing header also includes an "Xscale" value, the use for which * appears to be a complete mystery. If anyone can explain to me what * this field is, and how to use it, then I will add code here to validate * and repair it as well. For now, we mostly just leave it alone. </p> * * @param in Stream to read from * @return whether the MP3 file is a variable bitrate file * @throws IOException If an I/O error occurs */ protected boolean searchVBR(InputStream in) throws IOException { boolean vbr = false; int vbrSkip; // MPEG-1 if (version == 1) { vbrSkip = (mode != 3 ? 32 : 17); // MPEG-2 & 2.5 } else { vbrSkip = (mode != 3 ? 17 : 9); } // search for vbr if (in.skip(vbrSkip) != vbrSkip) throw new IOException("Cannot read " + vbrSkip + "bytes"); if (BitConversion.findString(in, VBR_FLAG, ENCODING)) { byte[] buffer = new byte[4]; if (in.read(buffer, 0, buffer.length) != buffer.length) throw new IOException("Cannot read " + buffer.length + "bytes"); int headFlags = BitConversion.extract4BigEndian(buffer); if (in.read(buffer, 0, buffer.length) != buffer.length) throw new IOException("Cannot read " + buffer.length + "bytes"); frames = 0; if ((headFlags & FRAMES_FLAG) != 0) { frames = BitConversion.extract4BigEndian(buffer); if (frames <= 0) log.severe("No VBR frame count found"); if (in.read(buffer, 0, buffer.length) != buffer.length) throw new IOException("Cannot read " + buffer.length + "bytes"); } if ((headFlags & BYTES_FLAG) != 0) { setFileSize(BitConversion.extract4BigEndian(buffer)); if (in.read(buffer, 0, buffer.length) != buffer.length) throw new IOException("Cannot read " + buffer.length + "bytes"); } if ((headFlags & TOC_FLAG) != 0) { byte[] toc = new byte[100]; if (in.read(toc, 0, toc.length) != toc.length) throw new IOException("Cannot read " + toc.length + "bytes"); // for(int i=0; i <= 100; i += 5) // log.fine(i+"% at byte: "+seekPoint(toc, fileSizeWithoutHeader, i)); } if (in.read(buffer, 0, buffer.length) != buffer.length) throw new IOException("Cannot read " + buffer.length + "bytes"); if ((headFlags & VBR_SCALE_FLAG) != 0) { /*int vbrScale =*/ BitConversion.extract4BigEndian(buffer); encoder = new String(buffer, ENCODING); } byte[] large = new byte[PADDING_SEARCH_SIZE]; if (in.read(large, 0, large.length) != large.length) throw new IOException("Cannot read " + large.length + "bytes"); int count = 0; // TODO 070105: removed space condition as iTunes writes things like "iTunes v7.0.2" while (count < large.length && large[count] != 0 && /*large[count] != 32 &&*/ large[count] < 127) count++; encoder = encoder + new String(large, 0, count, ENCODING); // TODO EXPERIMENTAL: Search for beginning of next frame int ff = count; while (ff < large.length && large[ff] != -1) ff++; if (ff != 257) log.warning("Next FF frame after " + ff + " bytes"); vbr = true; } return vbr; } /** * Interpolate in TOC to get file seek point in bytes */ protected int seekPoint(byte[] toc, long fileSize, double percent) { if (percent < 0.0) percent = 0.0; if (percent > 100.0) percent = 100.0; int a = (int) percent; if (a > 99) a = 99; double fa = BitConversion.unsignedByteToInt(toc[a]); double fb = 0.0; if (a < 99) { fb = BitConversion.unsignedByteToInt(toc[a + 1]); } else { fb = 256.0; } double fx = fa + (fb - fa) * (percent - a); return (int) ((1.0 / 256.0) * fx * fileSize); } // --- get object ------------------------------------------ public boolean isMP3() { return isValid(); } public boolean isWAV() { return false; } public boolean isOgg() { return false; } public int getFrames() { if (vbr) return frames; // for cbr, every frame has the same size return (int) (getFrameSize() > 0 ? (getDataSize() / getFrameSize()) : 0); } public int getFrameSize() { if (!vbr) { // avoid division by zero error if (getSampleFrequency() == 0) return 0; // MPEG-2.5 has other constant if (version == 3) return (int) (12 * getBitRate() / (getSampleFrequency() + (getPadding() ? 1 : 0))); else return (int) (144 * getBitRate() / (getSampleFrequency() + (getPadding() ? 1 : 0))); } // for vbr just average the frame size about the known frame count return (int) (getDataSize() / frames); } public long getBitRate() { if (bitrate == 0) { log.severe("Undefined bit rate in MP3 properties"); return 0; } // for vbr just average the bitrate about the known frame count if (vbr) return getDataSize() * 8 / (frames * MILLISECONDS_PER_FRAME) * 1000; else return bitrate; } public boolean isVBR() { return vbr; } public int getSeconds() { long bitrate = getBitRate(); if (bitrate == 0) return 0; return (int) Math.ceil(getDataSize() * 8.0 / bitrate); } public int getMPEGVersion() { return version; } public String getMPEGVersionString() { if (version < 1 || version > VERSION_STRINGS.length) { log.severe("Unknown MPEG version: " + version); return Integer.toString(version); } return VERSION_STRINGS[version - 1]; } public int getMPEGLayer() { return layer; } public String getMPEGLayerString() { if (layer < 1 || layer > LAYER_STRINGS.length) { log.severe("Unknown MPEG layer: " + layer); return Integer.toString(layer); } return LAYER_STRINGS[layer - 1]; } public boolean getPadding() { return padding; } public int getCRC() { if (!protection) return 0; return checksum; } public int getMode() { return mode; } public int getModeExtension() { return modeExtension; } public boolean isProtected() { return protection; } public boolean isPrivate() { return privated; } public boolean isCopyrighted() { return copyrighted; } public boolean isOriginal() { return original; } public int getEmphasis() { return emphasis; } public String getEmphasisString() { return EMPHASIS_STRINGS[emphasis]; } public String getEncoder() { return encoder; } // --- member variables ------------------------------------ /** * MPEG properties */ protected int version = 0; protected int layer = 0; protected int bitrate = 0; protected int frames = 0; protected boolean protection = false; protected short checksum = 0; protected boolean padding = false; protected int mode = 0; protected int modeExtension = 0; protected boolean privated = false; protected boolean copyrighted = false; protected boolean original = false; protected int emphasis = 0; protected String encoder = ""; }