/**
* @author : Paul Taylor
*
* Version @version:$Id: MP3AudioHeader.java,v 1.34 2008/10/30 15:14:53 paultaylor Exp $
*
* MusicTag Copyright (C)2003,2004
*
* This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser
* General Public License as published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with this library; if not,
* you can get a copy from http://www.opensource.org/licenses/lgpl-license.php or write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.jaudiotagger.audio.mp3;
import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException;
import org.jaudiotagger.audio.generic.GenericAudioHeader;
import org.jaudiotagger.logging.Hex;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.logging.Logger;
/**
* Represents the audio header of an MP3 File
* <p/>
* <p>The audio header consists of a number of
* audio frames. Because we are not trying to play the audio but only extract some information
* regarding the audio we only need to read the first audio frames to ensure that we have correctly
* identified them as audio frames and extracted the metadata we reuire.
* <p/>
* <p>Start of Audio id 0xFF (11111111) and then second byte anded with 0xE0(11100000).
* For example 2nd byte doesnt have to be 0xE0 is just has to have the top 3 signicant
* bits set. For example 0xFB (11111011) is a common occurence of the second match. The 2nd byte
* defines flags to indicate various mp3 values.
* <p/>
* <p>Having found these two values we then read the header which comprises these two bytes plus a further
* two to ensure this really is a MP3Header, sometimes the first frame is actually a dummy frame with summary information
* held within about the whole file, typically using a Xing Header or LAme Header. This is most useful when the file
* is variable bit rate, if the file is variable bit rate but does not use a summary header it will not be correctly
* identified as a VBR frame and the track length will be incorrectly calculated. Strictly speaking MP3 means
* Layer III file but MP2 Layer II), MP1 Layer I) and MPEG-2 files are sometimes used and named with
* the .mp3 suffix so this library attempts to supports all these formats.
*/
public class MP3AudioHeader extends GenericAudioHeader {
protected MPEGFrameHeader mp3FrameHeader;
protected XingFrame mp3XingFrame;
protected VbriFrame mp3VbriFrame;
private long fileSize;
private long startByte;
private double timePerFrame;
private double trackLength;
private long numberOfFrames;
private long numberOfFramesEstimate;
private long bitrate;
private String encoder = "";
private static final SimpleDateFormat timeInFormat = new SimpleDateFormat("ss", Locale.UK);
private static final SimpleDateFormat timeOutFormat = new SimpleDateFormat("mm:ss", Locale.UK);
private static final SimpleDateFormat timeOutOverAnHourFormat = new SimpleDateFormat("kk:mm:ss", Locale.UK);
private static final char isVbrIdentifier = '~';
private static final int CONVERT_TO_KILOBITS = 1000;
private static final String TYPE_MP3 = "mp3";
private static final int CONVERTS_BYTE_TO_BITS = 8;
//Logger
public static Logger logger = Logger.getLogger("org.jaudiotagger.audio.mp3");
/**
* After testing the average location of the first MP3Header bit was at 5000 bytes so this is
* why chosen as a default.
*/
private final static int FILE_BUFFER_SIZE = 5000;
private final static int MIN_BUFFER_REMAINING_REQUIRED = MPEGFrameHeader.HEADER_SIZE + XingFrame.MAX_BUFFER_SIZE_NEEDED_TO_READ_XING;
private static final int NO_SECONDS_IN_HOUR = 3600;
public MP3AudioHeader() {
}
/**
* Search for the first MP3Header in the file
* <p/>
* The search starts from the start of the file, it is usually safer to use the alternative constructor that
* allows you to provide the length of the tag header as a parameter so the tag can be skipped over.
*
* @param seekFile
* @throws IOException
* @throws InvalidAudioFrameException
*/
public MP3AudioHeader(final File seekFile) throws IOException, InvalidAudioFrameException {
if (!seek(seekFile, 0)) {
throw new InvalidAudioFrameException("No audio header found within" + seekFile.getName());
}
}
/**
* Search for the first MP3Header in the file
* <p/>
* Starts searching from location startByte, this is because there is likely to be an ID3TagHeader
* before the start of the audio. If this tagHeader contains unsynchronized information there is a
* possibility that it might be inaccurately identified as the start of the Audio data. Various checks
* are done in this code to prevent this happening but it cannot be guaranteed.
* <p/>
* Of course if the startByte provided overstates the length of the tag header, this could mean the
* start of the MP3AudioHeader is missed, further checks are done within the MP3 class to recognize
* if this has occurred and take appropriate action.
*
* @param seekFile
* @param startByte
* @throws IOException
* @throws InvalidAudioFrameException
*/
public MP3AudioHeader(final File seekFile, long startByte) throws IOException, InvalidAudioFrameException {
if (!seek(seekFile, startByte)) {
throw new InvalidAudioFrameException("No audio header found within" + seekFile.getName());
}
}
/**
* Returns true if the first MP3 frame can be found for the MP3 file
* <p/>
* This is the first byte of music data and not the ID3 Tag Frame. *
*
* @param seekFile MP3 file to seek
* @param startByte if there is an ID3v2tag we dont want to start reading from the start of the tag
* @return true if the first MP3 frame can be found
* @throws IOException on any I/O error
* @noinspection NestedTryStatement
*/
public boolean seek(final File seekFile, long startByte) throws IOException {
//This is substantially faster than updating the filechannels position
long filePointerCount;
final FileInputStream fis = new FileInputStream(seekFile);
final FileChannel fc = fis.getChannel();
//Read into Byte Buffer in Chunks
ByteBuffer bb = ByteBuffer.allocateDirect(FILE_BUFFER_SIZE);
//Move FileChannel to the starting position (skipping over tag if any)
fc.position(startByte);
//Update filePointerCount
filePointerCount = startByte;
//Read from here into the byte buffer , doesnt move location of filepointer
fc.read(bb, startByte);
bb.flip();
boolean syncFound = false;
try {
do {
if (bb.remaining() <= MIN_BUFFER_REMAINING_REQUIRED) {
bb.clear();
fc.position(filePointerCount);
fc.read(bb, fc.position());
bb.flip();
if (bb.limit() <= MIN_BUFFER_REMAINING_REQUIRED) {
//No mp3 exists
return false;
}
}
//MP3File.//logger.finest("fc:"+fc.position() + "bb"+bb.position());
if (MPEGFrameHeader.isMPEGFrame(bb)) {
try {
// if (MP3AudioHeader.//logger.isLoggable(Level.FINEST))
// {
// MP3AudioHeader.//logger.finest("Found Possible header at:" + filePointerCount);
// }
mp3FrameHeader = MPEGFrameHeader.parseMPEGHeader(bb);
syncFound = true;
//if(2==1) use this line when you want to test getting the next frame without using xing
if (XingFrame.isXingFrame(bb, mp3FrameHeader)) {
// if (MP3AudioHeader.//logger.isLoggable(Level.FINEST))
// {
// MP3AudioHeader.//logger.finest("Found Possible XingHeader");
// }
try {
//Parses Xing frame without modifying position of main buffer
mp3XingFrame = XingFrame.parseXingFrame();
} catch (InvalidAudioFrameException ex) {
// We Ignore because even if Xing Header is corrupted
//doesn't mean file is corrupted
}
break;
} else if (VbriFrame.isVbriFrame(bb, mp3FrameHeader)) {
// if (MP3AudioHeader.//logger.isLoggable(Level.FINEST))
// {
// MP3AudioHeader.//logger.finest("Found Possible VbriHeader");
// }
try {
//Parses Vbri frame without modifying position of main buffer
mp3VbriFrame = VbriFrame.parseVBRIFrame();
} catch (InvalidAudioFrameException ex) {
// We Ignore because even if Vbri Header is corrupted
//doesn't mean file is corrupted
}
break;
}
// There is a small but real chance that an unsynchronised ID3 Frame could fool the MPEG
// Parser into thinking it was an MPEG Header. If this happens the chances of the next bytes
// forming a Xing frame header are very remote. On the basis that most files these days have
// Xing headers we do an additional check for when an apparent frame header has been found
// but is not followed by a Xing Header:We check the next header this wont impose a large
// overhead because wont apply to most Mpegs anyway ( Most likely to occur if audio
// has an APIC frame which should have been unsynchronised but has not been) , or if the frame
// has been encoded with as Unicode LE because these have a BOM of 0xFF 0xFE
else {
syncFound = isNextFrameValid(seekFile, filePointerCount, bb, fc);
if (syncFound) {
break;
}
}
} catch (InvalidAudioFrameException ex) {
// We Ignore because likely to be incorrect sync bits ,
// will just continue in loop
}
}
bb.position(bb.position() + 1);
filePointerCount++;
}
while (!syncFound);
} catch (EOFException ex) {
// MP3AudioHeader.//logger.log(Level.WARNING, "Reached end of file without finding sync match", ex);
syncFound = false;
} catch (IOException iox) {
// MP3AudioHeader.//logger.log(Level.SEVERE, "IOException occurred whilst trying to find sync", iox);
syncFound = false;
throw iox;
} finally {
if (fc != null) {
fc.close();
}
if (fis != null) {
fis.close();
}
}
//Return to start of audio header
// if (MP3AudioHeader.//logger.isLoggable(Level.FINEST))
// {
// MP3AudioHeader.//logger.finer("Return found matching mp3 header starting at" + filePointerCount);
// }
setFileSize(seekFile.length());
setMp3StartByte(filePointerCount);
setTimePerFrame();
setNumberOfFrames();
setTrackLength();
setBitRate();
setEncoder();
return syncFound;
}
/**
* Called in some circumstances to check the next frame to ensure we have the correct audio header
*
* @return true if frame is valid
*/
private boolean isNextFrameValid(File seekFile, long filePointerCount, ByteBuffer bb, FileChannel fc) throws IOException {
// if (MP3AudioHeader.//logger.isLoggable(Level.FINEST))
// {
// MP3AudioHeader.//logger.finer("Checking next frame" + seekFile.getName() + ":fpc:" + filePointerCount + "skipping to:" + (filePointerCount + mp3FrameHeader.getFrameLength()));
// }
boolean result = false;
int currentPosition = bb.position();
//Our buffer is not large enough to fit in the whole of this frame, something must
//have gone wrong because frames are not this large, so just return false
//bad frame header
if (mp3FrameHeader.getFrameLength() > (FILE_BUFFER_SIZE - MIN_BUFFER_REMAINING_REQUIRED)) {
// MP3AudioHeader.//logger.finer("Frame size is too large to be a frame:" + mp3FrameHeader.getFrameLength());
return false;
}
//Check for end of buffer if not enough room get some more
if (bb.remaining() <= MIN_BUFFER_REMAINING_REQUIRED + mp3FrameHeader.getFrameLength()) {
// MP3AudioHeader.//logger.finer("Buffer too small, need to reload, buffer size:" + bb.remaining());
bb.clear();
fc.position(filePointerCount);
fc.read(bb, fc.position());
bb.flip();
//So now original buffer has been replaced, so set current position to start of buffer
currentPosition = 0;
//Not enough left
if (bb.limit() <= MIN_BUFFER_REMAINING_REQUIRED) {
//No mp3 exists
// MP3AudioHeader.//logger.finer("Nearly at end of file, no header found:");
return false;
}
//Still Not enough left for next alleged frame size so giving up
if (bb.limit() <= MIN_BUFFER_REMAINING_REQUIRED + mp3FrameHeader.getFrameLength()) {
//No mp3 exists
// MP3AudioHeader.//logger.finer("Nearly at end of file, no room for next frame, no header found:");
return false;
}
}
//Position bb to the start of the alleged next frame
bb.position(bb.position() + mp3FrameHeader.getFrameLength());
if (MPEGFrameHeader.isMPEGFrame(bb)) {
try {
MPEGFrameHeader.parseMPEGHeader(bb);
// MP3AudioHeader.//logger.finer("Check next frame confirms is an audio header ");
result = true;
} catch (InvalidAudioFrameException ex) {
// MP3AudioHeader.//logger.finer("Check next frame has identified this is not an audio header");
result = false;
}
} else {
// MP3AudioHeader.//logger.finer("isMPEGFrame has identified this is not an audio header");
}
//Set back to the start of the previous frame
bb.position(currentPosition);
return result;
}
/**
* Set the location of where the Audio file begins in the file
*
* @param startByte
*/
protected void setMp3StartByte(final long startByte) {
this.startByte = startByte;
}
/**
* Returns the byte position of the first MP3 Frame that the
* <code>file</code> arguement refers to. This is the first byte of music
* data and not the ID3 Tag Frame.
*
* @return the byte position of the first MP3 Frame
*/
public long getMp3StartByte() {
return startByte;
}
/**
* Set number of frames in this file, use Xing if exists otherwise ((File Size - Non Audio Part)/Frame Size)
*/
protected void setNumberOfFrames() {
numberOfFramesEstimate = (fileSize - startByte) / mp3FrameHeader.getFrameLength();
if (mp3XingFrame != null && mp3XingFrame.isFrameCountEnabled()) {
numberOfFrames = mp3XingFrame.getFrameCount();
} else if (mp3VbriFrame != null) {
numberOfFrames = mp3VbriFrame.getFrameCount();
} else {
numberOfFrames = numberOfFramesEstimate;
}
}
/**
* @return The number of frames within the Audio File, calculated as accurrately as possible
*/
public long getNumberOfFrames() {
return numberOfFrames;
}
/**
* @return The number of frames within the Audio File, calculated by dividing the filesize by
* the number of frames, this may not be the most accurate method available.
*/
public long getNumberOfFramesEstimate() {
return numberOfFramesEstimate;
}
/**
* Set the time each frame contributes to the audio in fractions of seconds, the higher
* the sampling rate the shorter the audio segment provided by the frame,
* the number of samples is fixed by the MPEG Version and Layer
*/
protected void setTimePerFrame() {
timePerFrame = mp3FrameHeader.getNoOfSamples() / mp3FrameHeader.getSamplingRate().doubleValue();
//Because when calculating framelength we may have altered the calculation slightly for MPEGVersion2
//to account for mono/stero we seem to have to make a corresponding modification to get the correct time
if ((mp3FrameHeader.getVersion() == MPEGFrameHeader.VERSION_2) || (mp3FrameHeader.getVersion() == MPEGFrameHeader.VERSION_2_5)) {
if ((mp3FrameHeader.getLayer() == MPEGFrameHeader.LAYER_II) || (mp3FrameHeader.getLayer() == MPEGFrameHeader.LAYER_III)) {
if (mp3FrameHeader.getNumberOfChannels() == 1) {
timePerFrame = timePerFrame / 2;
}
}
}
}
/**
* @return the the time each frame contributes to the audio in fractions of seconds
*/
public double getTimePerFrame() {
return timePerFrame;
}
/**
* Estimate the length of the audio track in seconds
* Calculation is Number of frames multiplied by the Time Per Frame using the first frame as a prototype
* Time Per Frame is the number of samples in the frame (which is defined by the MPEGVersion/Layer combination)
* divided by the sampling rate, i.e the higher the sampling rate the shorter the audio represented by the frame is going
* to be.
*/
protected void setTrackLength() {
trackLength = numberOfFrames * getTimePerFrame();
}
/**
* @return Track Length in seconds
*/
public double getPreciseLength() {
return trackLength;
}
@Override
public Long getTotalSamples() {
long totalSamples = numberOfFrames * mp3FrameHeader.getNoOfSamples();
//Because when calculating framelength we may have altered the calculation slightly for MPEGVersion2
//to account for mono/stero we seem to have to make a corresponding modification to get the correct time
if ((mp3FrameHeader.getVersion() == MPEGFrameHeader.VERSION_2) || (mp3FrameHeader.getVersion() == MPEGFrameHeader.VERSION_2_5)) {
if ((mp3FrameHeader.getLayer() == MPEGFrameHeader.LAYER_II) || (mp3FrameHeader.getLayer() == MPEGFrameHeader.LAYER_III)) {
if (mp3FrameHeader.getNumberOfChannels() == 1) {
totalSamples /= 2;
}
}
}
return totalSamples;
}
public int getTrackLength() {
return (int) getPreciseLength();
}
/**
* Return the length in user friendly format
*/
public String getTrackLengthAsString() {
final Date timeIn;
try {
final long lengthInSecs = getTrackLength();
synchronized (timeInFormat) {
timeIn = timeInFormat.parse(String.valueOf(lengthInSecs));
}
if (lengthInSecs < NO_SECONDS_IN_HOUR) {
synchronized (timeOutFormat) {
return timeOutFormat.format(timeIn);
}
} else {
synchronized (timeOutOverAnHourFormat) {
return timeOutOverAnHourFormat.format(timeIn);
}
}
} catch (ParseException pe) {
//logger.warning("Unable to parse:" + getPreciseLength() + " failed with ParseException:" + pe.getMessage());
return "";
}
}
/**
* @return the audio file type
*/
public String getEncodingType() {
return TYPE_MP3;
}
/**
* Set bitrate in kbps, if Vbr use Xingheader if possible
*/
protected void setBitRate() {
if (mp3XingFrame != null && mp3XingFrame.isVbr()) {
if (mp3XingFrame.isAudioSizeEnabled() && mp3XingFrame.getAudioSize() > 0) {
bitrate = (long) ((mp3XingFrame.getAudioSize() * CONVERTS_BYTE_TO_BITS) / (timePerFrame * getNumberOfFrames() * CONVERT_TO_KILOBITS));
} else {
bitrate = (long) (((fileSize - startByte) * CONVERTS_BYTE_TO_BITS) / (timePerFrame * getNumberOfFrames() * CONVERT_TO_KILOBITS));
}
} else if (mp3VbriFrame != null) {
if (mp3VbriFrame.getAudioSize() > 0) {
bitrate = (long) ((mp3VbriFrame.getAudioSize() * CONVERTS_BYTE_TO_BITS) / (timePerFrame * getNumberOfFrames() * CONVERT_TO_KILOBITS));
} else {
bitrate = (long) (((fileSize - startByte) * CONVERTS_BYTE_TO_BITS) / (timePerFrame * getNumberOfFrames() * CONVERT_TO_KILOBITS));
}
} else {
bitrate = mp3FrameHeader.getBitRate();
}
}
protected void setEncoder() {
if (mp3XingFrame != null) {
if (mp3XingFrame.getLameFrame() != null) {
encoder = mp3XingFrame.getLameFrame().getEncoder();
return;
}
} else if (mp3VbriFrame != null) {
encoder = mp3VbriFrame.getEncoder();
}
}
/**
* @return bitrate in kbps, no indicator is provided as to whether or not it is vbr
*/
public long getBitRateAsNumber() {
return bitrate;
}
/**
* @return the BitRate of the Audio, to distinguish cbr from vbr we add a '~'
* for vbr.
*/
public String getBitRate() {
if (mp3XingFrame != null && mp3XingFrame.isVbr()) {
return isVbrIdentifier + String.valueOf(bitrate);
} else if (mp3VbriFrame != null) {
return isVbrIdentifier + String.valueOf(bitrate);
} else {
return String.valueOf(bitrate);
}
}
/**
* @return the sampling rate in Hz
*/
public int getSampleRateAsNumber() {
return mp3FrameHeader.getSamplingRate();
}
/**
* @return the sampling rate as string
*/
public String getSampleRate() {
return String.valueOf(mp3FrameHeader.getSamplingRate());
}
/**
* @return MPEG Version (1-3)
*/
public String getMpegVersion() {
return mp3FrameHeader.getVersionAsString();
}
/**
* @return MPEG Layer (1-3)
*/
public String getMpegLayer() {
return mp3FrameHeader.getLayerAsString();
}
/**
* @return the format of the audio (i.e. MPEG-1 Layer3)
*/
public String getFormat() {
return mp3FrameHeader.getVersionAsString() + " " + mp3FrameHeader.getLayerAsString();
}
/**
* @return the Channel Mode such as Stero or Mono
*/
public String getChannels() {
return mp3FrameHeader.getChannelModeAsString();
}
@Override
public int getChannelNumber() {
return mp3FrameHeader.getChannelMode() == MPEGFrameHeader.MODE_MONO ? 1 : 2;
}
/**
* @return Emphasis
*/
public String getEmphasis() {
return mp3FrameHeader.getEmphasisAsString();
}
/**
* @return if the bitrate is variable, Xing header takes precedence if we have one
*/
public boolean isVariableBitRate() {
if (mp3XingFrame != null) {
return mp3XingFrame.isVbr();
} else if (mp3VbriFrame != null) {
return mp3VbriFrame.isVbr();
} else {
return mp3FrameHeader.isVariableBitRate();
}
}
public boolean isProtected() {
return mp3FrameHeader.isProtected();
}
public boolean isPrivate() {
return mp3FrameHeader.isPrivate();
}
public boolean isCopyrighted() {
return mp3FrameHeader.isCopyrighted();
}
public boolean isOriginal() {
return mp3FrameHeader.isOriginal();
}
public boolean isPadding() {
return mp3FrameHeader.isPadding();
}
/**
* @return encoder
*/
public String getEncoder() {
return encoder;
}
public LameFrame getLameFrame() {
if (mp3XingFrame == null) {
return null;
}
return mp3XingFrame.getLameFrame();
}
public XingFrame getXingFrame() {
return mp3XingFrame;
}
public MPEGFrameHeader getMp3FrameHeader() {
return mp3FrameHeader;
}
/**
* Set the size of the file, required in some calculations
*
* @param fileSize
*/
protected void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
/**
* @return a string represntation
*/
public String toString() {
String s = "fileSize:" + fileSize + " encoder:" + encoder + " startByte:" + Hex.asHex(startByte) + " numberOfFrames:" + numberOfFrames + " numberOfFramesEst:" + numberOfFramesEstimate + " timePerFrame:" + timePerFrame + " bitrate:" + bitrate + " trackLength:" + getTrackLengthAsString();
if (this.mp3FrameHeader != null) {
s += mp3FrameHeader.toString();
}
if (this.mp3XingFrame != null) {
s += mp3XingFrame.toString();
}
return s;
}
}