Package org.farng.mp3

Source Code of org.farng.mp3.MP3File

package org.farng.mp3;

import org.farng.mp3.filename.FilenameTag;
import org.farng.mp3.filename.FilenameTagBuilder;
import org.farng.mp3.id3.AbstractID3v2;
import org.farng.mp3.id3.AbstractID3v2Frame;
import org.farng.mp3.id3.ID3v1;
import org.farng.mp3.id3.ID3v1_1;
import org.farng.mp3.id3.ID3v2_2;
import org.farng.mp3.id3.ID3v2_3;
import org.farng.mp3.id3.ID3v2_4;
import org.farng.mp3.lyrics3.AbstractLyrics3;
import org.farng.mp3.lyrics3.Lyrics3v1;
import org.farng.mp3.lyrics3.Lyrics3v2;

import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
* <TABLE border=0> <TBODY> <TR> <TD class=h2>How is MP3 built?</TD></TR></TBODY></TABLE> <TABLE border=0> <TBODY> <TR
* vAlign=top> <TD> <P>Most people with a little knowledge in MP3 files know that the sound is divided into smaller
* parts and compressed with a psycoacoustic model. This smaller pieces of the audio is then put into something called
* 'frames', which is a little datablock with a header. I'll focus on that header in this text.</P>
* <p/>
* <P>The header is 4 bytes, 32 bits, big and begins with something called sync. This sync is, at least according to the
* MPEG standard, 12 set bits in a row. Some add-on standards made later uses 11 set bits and one cleared bit. The sync
* is directly followed by a ID bit, indicating if the file is a MPEG-1 och MPEG-2 file. 0=MPEG-2 and 1=MPEG-1</P>
* <p/>
* <P>The layer is defined with the two layers bits. They are oddly defined as</P> <CENTER> <TABLE cellSpacing=0
* cellPadding=2 border=1> <TBODY> <TR> <TD>0 0</TD> <TD>Not defined</TD></TR> <TR> <TD>0 1</TD> <TD>Layer III</TD></TR>
* <TR> <TD>1 0</TD> <TD>Layer II</TD></TR> <TR> <TD>1 1</TD> <TD>Layer I</TD></TR></TBODY></TABLE> </CENTER>
* <p/>
* <P>With this information and the information in the bitrate field we can determine the bitrate of the audio (in
* kbit/s) according to this table.</P> <CENTER> <TABLE cellSpacing=0 cellPadding=2 border=1> <TBODY> <TR>
* <TD>Bitrate<BR>value</TD> <TD>MPEG-1,<BR>layer I</TD> <TD>MPEG-1,<BR>layer II</TD> <TD>MPEG-1,<BR>layer III</TD>
* <TD>MPEG-2,<BR>layer I</TD> <TD>MPEG-2,<BR>layer II</TD> <TD>MPEG-2,<BR>layer III</TD></TR> <TR> <TD>0 0 0 0</TD>
* <TD></TD> <TD></TD> <TD></TD> <TD></TD> <TD></TD> <TD></TD></TR> <TR> <TD>0 0 0 1</TD> <TD>32</TD> <TD>32</TD>
* <TD>32</TD> <TD>32</TD> <TD>32</TD> <TD>8</TD></TR> <TR> <TD>0 0 1 0</TD> <TD>64</TD> <TD>48</TD> <TD>40</TD>
* <TD>64</TD> <TD>48</TD> <TD>16</TD></TR> <TR> <TD>0 0 1 1</TD> <TD>96</TD> <TD>56</TD> <TD>48</TD> <TD>96</TD>
* <TD>56</TD> <TD>24</TD></TR> <TR> <TD>0 1 0 0</TD> <TD>128</TD> <TD>64</TD> <TD>56</TD> <TD>128</TD> <TD>64</TD>
* <TD>32</TD></TR> <TR> <TD>0 1 0 1</TD> <TD>160</TD> <TD>80</TD> <TD>64</TD> <TD>160</TD> <TD>80</TD> <TD>64</TD></TR>
* <TR> <TD>0 1 1 0</TD> <TD>192</TD> <TD>96</TD> <TD>80</TD> <TD>192</TD> <TD>96</TD> <TD>80</TD></TR> <TR> <TD>0 1 1
* 1</TD> <TD>224</TD> <TD>112</TD> <TD>96</TD> <TD>224</TD> <TD>112</TD> <TD>56</TD></TR> <TR> <TD>1 0 0 0</TD>
* <TD>256</TD> <TD>128</TD> <TD>112</TD> <TD>256</TD> <TD>128</TD> <TD>64</TD></TR> <TR> <TD>1 0 0 1</TD> <TD>288</TD>
* <TD>160</TD> <TD>128</TD> <TD>288</TD> <TD>160</TD> <TD>128</TD></TR> <TR> <TD>1 0 1 0</TD> <TD>320</TD> <TD>192</TD>
* <TD>160</TD> <TD>320</TD> <TD>192</TD> <TD>160</TD></TR> <TR> <TD>1 0 1 1</TD> <TD>352</TD> <TD>224</TD> <TD>192</TD>
* <TD>352</TD> <TD>224</TD> <TD>112</TD></TR> <TR> <TD>1 1 0 0</TD> <TD>384</TD> <TD>256</TD> <TD>224</TD> <TD>384</TD>
* <TD>256</TD> <TD>128</TD></TR> <TR> <TD>1 1 0 1</TD> <TD>416</TD> <TD>320</TD> <TD>256</TD> <TD>416</TD> <TD>320</TD>
* <TD>256</TD></TR> <TR> <TD>1 1 1 0</TD> <TD>448</TD> <TD>384</TD> <TD>320</TD> <TD>448</TD> <TD>384</TD>
* <TD>320</TD></TR> <TR> <TD>1 1 1 1</TD> <TD></TD> <TD></TD> <TD></TD> <TD></TD> <TD></TD>
* <TD></TD></TR></TBODY></TABLE> </CENTER>
* <p/>
* <P>The sample rate is described in the frequency field. These values is dependent of which MPEG standard is used
* according to the following table.</P> <CENTER> <TABLE cellSpacing=0 cellPadding=2 border=1> <TBODY> <TR>
* <TD>Frequency<BR>value</TD> <TD>MPEG-1</TD> <TD>MPEG-2</TD></TR> <TR> <TD>0 0</TD> <TD>44100 Hz</TD> <TD>22050
* Hz</TD></TR> <TR> <TD>0 1</TD> <TD>48000 Hz</TD> <TD>24000 Hz</TD></TR> <TR> <TD>1 0</TD> <TD>32000 Hz</TD> <TD>16000
* Hz</TD></TR> <TR> <TD>1 1</TD> <TD></TD> <TD></TD></TR></TBODY></TABLE> </CENTER>
* <p/>
* <P>Three bits is not needed in the decoding process at all. These are the copyright bit, original home bit and the
* private bit. The copyright has the same meaning as the copyright bit on CDs and DAT tapes, i.e. telling that it is
* illegal to copy the contents if the bit is set. The original home bit indicates, if set, that the frame is located on
* its original media. No one seems to know what the privat bit is good for.
* <p/>
* <p/>
* <p/>
* <P>If the protection bit is NOT set then the frame header is followed by a 16 bit checksum, inserted before the audio
* data. If the padding bit is set then the frame is padded with an extra byte. Knowing this the size of the complete
* frame can be calculated with the following formula</P> <CENTER> <P>FrameSize = 144 * BitRate / SampleRate<BR>when the
* padding bit is cleared and</P>
* <p/>
* <P>FrameSize = (144 * BitRate / SampleRate) + 1<BR>when the padding bit is set.
* <p/>
* <P></CENTER>
* <p/>
* <P>The frameSize is of course an integer. If for an example BitRate=128000, SampleRate=44100 and the padding bit is
* cleared, then the FrameSize = 144 * 128000 / 44100 = 417
* <p/>
* <p/>
* <p/>
* <P>The mode field is used to tell which sort of stereo/mono encoding that has been used. The purpose of the mode
* extension field is different for different layers, but I really don't know exactly what it's for.</P> <CENTER> <TABLE
* cellSpacing=0 cellPadding=2 border=1> <TBODY> <TR> <TD>Mode value</TD> <TD>mode</TD></TR> <TR> <TD>0 0</TD>
* <TD>Stereo</TD></TR> <TR> <TD>0 1</TD> <TD>Joint stereo</TD></TR> <TR> <TD>1 0</TD> <TD>Dual channel</TD></TR> <TR>
* <TD>1 1</TD> <TD>Mono</TD></TR></TBODY></TABLE> </CENTER>
* <p/>
* <P>The last field is the emphasis field. It is used to sort of 're-equalize' the sound after a Dolby-like noise
* supression. This is not very used and will probably never be. The following noise supression model is used</P>
* <CENTER> <TABLE cellSpacing=0 cellPadding=2 border=1> <TBODY> <TR> <TD>Emphasis value</TD> <TD>Emphasis
* method</TD></TR> <TR> <TD>0 0</TD> <TD>none</TD></TR> <TR> <TD>0 1</TD> <TD>50/15ms</TD></TR> <TR> <TD>1 0</TD>
* <TD></TD></TR> <TR> <TD>1 1</TD> <TD>CCITT j.17</TD></TR></TBODY></TABLE> </CENTER></TD> </TR></TBODY></TABLE>
*
* @author Eric Farng
* @version $Revision: 1.5 $
*/
public class MP3File {

    /**
     * the ID3v2 tag that this file contains.
     */
    private AbstractID3v2 id3v2tag;
    /**
     * the Lyrics3 tag that this file contains.
     */
    private AbstractLyrics3 lyrics3tag;
    /**
     * the mp3 file that this instance represents. This value can be null. This value is also used for any methods that
     * are called without a file argument
     */
    private File mp3file;
    /**
     * the ID3v2_4 tag that represents the parsed filename.
     */
    private FilenameTag filenameTag;
    /**
     * the ID3v1 tag that this file contains.
     */
    private ID3v1 id3v1tag;
    /**
     * value read from the MP3 Frame header
     */
    private boolean copyProtected;
    /**
     * value read from the MP3 Frame header
     */
    private boolean home;
    /**
     * value read from the MP3 Frame header
     */
    private boolean padding;
    /**
     * value read from the MP3 Frame header
     */
    private boolean privacy;
    /**
     * value read from the MP3 Frame header
     */
    private boolean protection;
    /**
     * value read from the MP3 Frame header
     */
    private boolean variableBitRate;
    /**
     * value read from the MP3 Frame header
     */
    private byte emphasis;
    /**
     * value read from the MP3 Frame header
     */
    private byte layer;
    /**
     * value read from the MP3 Frame header
     */
    private byte mode;
    /**
     * value read from the MP3 Frame header
     */
    private byte modeExtension;
    /**
     * value read from the MP3 Frame header
     */
    private byte mpegVersion;
    /**
     * frequency determined from MP3 Version and frequency value read from the MP3 Frame header
     */
    private double frequency;
    /**
     * bitrate calculated from the frame MP3 Frame header
     */
    private int bitRate;

    /**
     * Creates a new empty MP3File object that is not associated with a specific file.
     */
    public MP3File() {
        super();
    }

    /**
     * Creates a new MP3File object.
     */
    public MP3File(final MP3File copyObject) {
        super();
        copyProtected = copyObject.copyProtected;
        home = copyObject.home;
        padding = copyObject.padding;
        privacy = copyObject.privacy;
        protection = copyObject.protection;
        variableBitRate = copyObject.variableBitRate;
        emphasis = copyObject.emphasis;
        layer = copyObject.layer;
        mode = copyObject.mode;
        modeExtension = copyObject.modeExtension;
        mpegVersion = copyObject.mpegVersion;
        frequency = copyObject.frequency;
        bitRate = copyObject.bitRate;
        mp3file = new File(copyObject.mp3file.getAbsolutePath());
        filenameTag = new FilenameTag(copyObject.filenameTag);
        id3v2tag = (AbstractID3v2) TagUtility.copyObject(copyObject.id3v2tag);
        lyrics3tag = (AbstractLyrics3) TagUtility.copyObject(copyObject.lyrics3tag);
        id3v1tag = (ID3v1) TagUtility.copyObject(copyObject.id3v1tag);
    }

    /**
     * Creates a new MP3File object and parse the tag from the given filename.
     *
     * @param filename MP3 file
     *
     * @throws IOException  on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public MP3File(final String filename) throws IOException, TagException {
        this(new File(filename));
    }

    /**
     * Creates a new MP3File object and parse the tag from the given file Object.
     *
     * @param file MP3 file
     *
     * @throws IOException  on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public MP3File(final File file) throws IOException, TagException {
        this(file, true);
    }

    /**
     * Creates a new MP3File object and parse the tag from the given file Object.
     *
     * @param file      MP3 file
     * @param writeable open in read (false) or read-write (true) mode
     *
     * @throws IOException  on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public MP3File(final File file, final boolean writeable) throws IOException, TagException {
        super();
        mp3file = file;
        final RandomAccessFile newFile = new RandomAccessFile(file, writeable ? "rw" : "r");
        try {
            id3v1tag = new ID3v1_1(newFile);
        } catch (TagNotFoundException ex) {
            // tag might be different version
        }
        try {
            if (id3v1tag == null) {
                id3v1tag = new ID3v1(newFile);
            }
        } catch (TagNotFoundException ex) {
            // ok if it's null
        }
        try {
            id3v2tag = new ID3v2_4(newFile);
        } catch (TagNotFoundException ex) {
            // maybe different version
        }
        try {
            if (id3v2tag == null) {
                id3v2tag = new ID3v2_3(newFile);
            }
        } catch (TagNotFoundException ex) {
            // maybe a different version
        }
        try {
            if (id3v2tag == null) {
                id3v2tag = new ID3v2_2(newFile);
            }
        } catch (TagNotFoundException ex) {
            // it's ok to be null
        }
        try {
            lyrics3tag = new Lyrics3v2(newFile);
        } catch (TagNotFoundException ex) {
            // maybe a different version
        }
        try {
            if (lyrics3tag == null) {
                lyrics3tag = new Lyrics3v1(newFile);
            }
        } catch (TagNotFoundException ex) {
            //it's ok to be null
        }
        newFile.close();
        try {
            filenameTag = FilenameTagBuilder.createFilenameTagFromMP3File(this);
        } catch (Exception ex) {
            throw new TagException("Unable to create FilenameTag", ex);
        }
    }

    public int getBitRate() {
        return bitRate;
    }

    public boolean isCopyProtected() {
        return copyProtected;
    }

    public byte getEmphasis() {
        return emphasis;
    }

    /**
     * Sets the filename tag for this MP3 File. Refer to <code>TagUtilities.parseFileName</code> and
     * <code>TagUtilities.createID3v2Tag</code> for more information about parsing file names into <code>ID3v2_4</code>
     * objects.
     *
     * @param filenameTag parsed <code>ID3v2_4</code> filename tag
     */
    public void setFilenameTag(final FilenameTag filenameTag) {
        this.filenameTag = filenameTag;
    }

    /**
     * Sets the filename tag for this MP3 File. Refer to <code>TagUtilities.parseFileName</code> and
     * <code>TagUtilities.createID3v2Tag</code> for more information about parsing file names into <code>ID3v2_4</code>
     * objects.
     *
     * @return parsed <code>ID3v2_4</code> filename tag
     */
    public FilenameTag getFilenameTag() {
        return filenameTag;
    }

    /**
     * Sets all four (id3v1, lyrics3, filename, id3v2) tags in this instance to the <code>frame</code> argument if the
     * tag exists. This method does not use the options inside the <code>tagOptions</code> object.
     *
     * @param frame frame to set / replace in all four tags.
     */
    //todo this method is very inefficient.
    public void setFrameAcrossTags(final AbstractID3v2Frame frame) {
        if (id3v1tag != null) {
            final ID3v2_4 id3v1 = new ID3v2_4(id3v1tag);
            id3v1.setFrame(frame);
            id3v1tag.overwrite(id3v1);
        }
        if (id3v2tag != null) {
            id3v2tag.setFrame(frame);
        }
        if (lyrics3tag != null) {
            final ID3v2_4 lyrics3 = new ID3v2_4(lyrics3tag);
            lyrics3.setFrame(frame);
            lyrics3tag = new Lyrics3v2(lyrics3);
        }
        if (filenameTag != null) {
            filenameTag.setFrame(frame);
        }
    }

    /**
     * Gets the frames from all four (id3v1, lyrics3, filename, id3v2) mp3 tags in this instance for each tag that
     * exists. This method does not use the options inside the <code>tagOptions</code> object.
     *
     * @param identifier ID3v2.4 Tag Frame Identifier.
     *
     * @return ArrayList of all instances of the desired frame. Each instance is returned as an
     *         <code>ID3v2_4Frame</code>. The nature of the code returns the array in a specific order, but this order
     *         is not guaranteed for future versions of this library.
     */
    //todo this method is very inefficient.
    public List getFrameAcrossTags(final String identifier) {
        if (identifier != null && identifier.length() > 0) {
            final List list = new ArrayList(32);
            Iterator iterator;
            if (id3v1tag != null) {
                final ID3v2_4 id3v1 = new ID3v2_4(id3v1tag);
                if (id3v1.hasFrameOfType(identifier)) {
                    iterator = id3v1.getFrameOfType(identifier);
                    while (iterator.hasNext()) {
                        list.add(iterator.next());
                    }
                }
            }
            if (id3v2tag != null) {
                if (id3v2tag.hasFrameOfType(identifier)) {
                    iterator = id3v2tag.getFrameOfType(identifier);
                    while (iterator.hasNext()) {
                        list.add(iterator.next());
                    }
                }
            }
            if (lyrics3tag != null) {
                final ID3v2_4 lyrics3 = new ID3v2_4(lyrics3tag);
                if (lyrics3.hasFrameOfType(identifier)) {
                    iterator = lyrics3.getFrameOfType(identifier);
                    while (iterator.hasNext()) {
                        list.add(iterator.next());
                    }
                }
            }
            if (filenameTag != null) {
                if (filenameTag.hasFrameOfType(identifier)) {
                    iterator = filenameTag.getFrameOfType(identifier);
                    while (iterator.hasNext()) {
                        list.add(iterator.next());
                    }
                }
            }
            return list;
        }
        return null;
    }

    public double getFrequency() {
        return frequency;
    }

    public boolean isHome() {
        return home;
    }

    /**
     * Sets the <code>ID3v1</code> tag for this object. A new <code>ID3v1_1</code> object is created from the argument
     * and then used here.
     *
     * @param mp3tag Any MP3Tag object can be used and will be converted into a new ID3v1_1 object.
     */
    public void setID3v1Tag(final AbstractMP3Tag mp3tag) {
        id3v1tag = new ID3v1_1(mp3tag);
    }

    public void setID3v1Tag(final ID3v1 id3v1tag) {
        this.id3v1tag = id3v1tag;
    }

    /**
     * Returns the <code>ID3v1</code> tag for this object.
     *
     * @return the <code>ID3v1</code> tag for this object
     */
    public ID3v1 getID3v1Tag() {
        return id3v1tag;
    }

    /**
     * Sets the <code>ID3v2</code> tag for this object. A new <code>ID3v2_4</code> object is created from the argument
     * and then used here.
     *
     * @param mp3tag Any MP3Tag object can be used and will be converted into a new ID3v2_4 object.
     */
    public void setID3v2Tag(final AbstractMP3Tag mp3tag) {
        id3v2tag = new ID3v2_4(mp3tag);
    }

    public void setID3v2Tag(final AbstractID3v2 id3v2tag) {
        this.id3v2tag = id3v2tag;
    }

    /**
     * Returns the <code>ID3v2</code> tag for this object.
     *
     * @return the <code>ID3v2</code> tag for this object
     */
    public AbstractID3v2 getID3v2Tag() {
        return id3v2tag;
    }

    public byte getLayer() {
        return layer;
    }

    /**
     * Sets the <code>Lyrics3</code> tag for this object. A new <code>Lyrics3v2</code> object is created from the
     * argument and then used here.
     *
     * @param mp3tag Any MP3Tag object can be used and will be converted into a new Lyrics3v2 object.
     */
    public void setLyrics3Tag(final AbstractMP3Tag mp3tag) {
        lyrics3tag = new Lyrics3v2(mp3tag);
    }

    public void setLyrics3Tag(final AbstractLyrics3 lyrics3tag) {
        this.lyrics3tag = lyrics3tag;
    }

    /**
     * Returns the <code>ID3v1</code> tag for this object.
     *
     * @return the <code>ID3v1</code> tag for this object
     */
    public AbstractLyrics3 getLyrics3Tag() {
        return lyrics3tag;
    }

    public byte getMode() {
        return mode;
    }

    public byte getModeExtension() {
        return modeExtension;
    }

    /**
     * Returns the byte position of the first MP3 Frame that this object 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
     *
     * @throws IOException           on any I/O error
     * @throws FileNotFoundException if the file exists but is a directory rather than a regular file or cannot be
     *                               opened for any other reason
     */
    public long getMp3StartByte() throws IOException, FileNotFoundException {
        return getMp3StartByte(mp3file);
    }

    /**
     * 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.
     *
     * @param file MP3 file to search
     *
     * @return the byte position of the first MP3 Frame
     *
     * @throws IOException           on any I/O error
     * @throws FileNotFoundException if the file exists but is a directory rather than a regular file or cannot be
     *                               opened for any other reason
     */
    public long getMp3StartByte(final File file) throws IOException, FileNotFoundException {
        RandomAccessFile rfile = null;
        long startByte = 0L;
        try {
            rfile = new RandomAccessFile(file, "r");
            seekMP3Frame(rfile);
            startByte = rfile.getFilePointer();
        } finally {
            if (rfile != null) {
                rfile.close();
            }
        }
        return startByte;
    }

    public void setMp3file(final File mp3file) {
        this.mp3file = mp3file;
    }

    public File getMp3file() {
        return mp3file;
    }

    public byte getMpegVersion() {
        return mpegVersion;
    }

    public boolean isPadding() {
        return padding;
    }

    public boolean isPrivacy() {
        return privacy;
    }

    public boolean isProtection() {
        return protection;
    }

    /**
     * Returns true if there are any unsynchronized tags in this object. A fragment is unsynchronized if it exists in
     * two or more tags but is not equal across all of them.
     *
     * @return true of any fragments are unsynchronized.
     */
    //todo there might be a faster way to do this, other than calling
    //getUnsynchronizedFragments()
    public boolean isUnsynchronized() {
        return getUnsynchronizedFragments().size() > 0;
    }

    /**
     * Returns a HashSet of unsynchronized fragments across all tags in this object. A fragment is unsynchronized if it
     * exists in two or more tags but is not equal across all of them.
     *
     * @return a HashSet of unsynchronized fragments
     */
    public Set getUnsynchronizedFragments() {
        final ID3v2_4 total = new ID3v2_4(id3v2tag);
        final Set set = new HashSet(32);
        total.append(id3v1tag);
        total.append(lyrics3tag);
        total.append(filenameTag);
        total.append(id3v2tag);
        final ID3v2_4 id3v1 = new ID3v2_4(id3v1tag);
        final ID3v2_4 lyrics3 = new ID3v2_4(lyrics3tag);
        final ID3v2_4 filename = new ID3v2_4(filenameTag);
        final AbstractID3v2 id3v2 = id3v2tag;
        final Iterator iterator = total.iterator();
        while (iterator.hasNext()) {
            final AbstractID3v2Frame frame = (AbstractID3v2Frame) iterator.next();
            final String identifier = frame.getIdentifier();
            if (id3v2 != null) {
                if (id3v2.hasFrame(identifier)) {
                    if (!id3v2.getFrame(identifier).isSubsetOf(frame)) {
                        set.add(identifier);
                    }
                }
            }
            if (id3v1.hasFrame(identifier)) {
                if (!id3v1.getFrame(identifier).isSubsetOf(frame)) {
                    set.add(identifier);
                }
            }
            if (lyrics3.hasFrame(identifier)) {
                if (!lyrics3.getFrame(identifier).isSubsetOf(frame)) {
                    set.add(identifier);
                }
            }
            if (filename.hasFrame(identifier)) {
                if (!filename.getFrame(identifier).isSubsetOf(frame)) {
                    set.add(identifier);
                }
            }
        }
        return set;
    }

    public void setVariableBitRate(final boolean variableBitRate) {
        this.variableBitRate = variableBitRate;
    }

    public boolean isVariableBitRate() {
        return variableBitRate;
    }

    /**
     * Adjust the lenght of the ID3v2 padding at the beginning of the MP3 file referred to in this object. The ID3v2
     * size will be calculated, then a new file will be created with enough size to fit the <code>ID3v2</code> tag in
     * this object. The old file will be deleted, and the new file renamed. All parameters will be taken from the
     * <code>tagOptions</code> object.
     *
     * @throws FileNotFoundException if the file exists but is a directory rather than a regular file or cannot be
     *                               opened for any other reason
     * @throws IOException           on any I/O error
     * @throws TagException          on any exception generated by this library.
     */
    public boolean adjustID3v2Padding() throws FileNotFoundException, IOException, TagException {
        return adjustID3v2Padding(TagOptionSingleton.getInstance().getId3v2PaddingSize(),
                                  TagOptionSingleton.getInstance().isId3v2PaddingWillShorten(),
                                  TagOptionSingleton.getInstance().isId3v2PaddingCopyTag(),
                                  mp3file);
    }

    /**
     * Adjust the length of the ID3v2 padding at the beginning of the MP3 file this object refers to. The ID3v2 size
     * will be calculated, then a new file will be created with enough size to fit the <code>ID3v2</code> tag. The old
     * file will be deleted, and the new file renamed.
     *
     * @param paddingSize  Initial padding size. This size is doubled until the ID3v2 tag will fit.
     * @param willShorten  if the newly calculated padding size is less than the padding length of the file, then news
     *                     the new shorter padding size if this is true.
     * @param copyID3v2Tag if true, write the <code>ID3v2</code> tag of this object into the file
     *
     * @throws FileNotFoundException if the file exists but is a directory rather than a regular file or cannot be
     *                               opened for any other reason
     * @throws IOException           on any I/O error
     * @throws TagException          on any exception generated by this library.
     */
    public boolean adjustID3v2Padding(final int paddingSize, final boolean willShorten, final boolean copyID3v2Tag)
            throws FileNotFoundException, IOException, TagException {
        return adjustID3v2Padding(paddingSize, willShorten, copyID3v2Tag, mp3file);
    }

    /**
     * Adjust the length of the ID3v2 padding at the beginning of the MP3 file this object refers to. The ID3v2 size
     * will be calculated, then a new file will be created with enough size to fit the <code>ID3v2</code> tag. The old
     * file will be deleted, and the new file renamed.
     *
     * @param paddingSize  Initial padding size. This size is doubled until the ID3v2 tag will fit. A paddingSize of
     *                     zero will create a padding length exactly equal to the tag size.
     * @param willShorten  Shorten the padding size by halves if the ID3v2 tag will fit
     * @param copyID3v2Tag if true, write the <code>ID3v2</code> tag of this object into the file
     * @param file         The file to adjust the padding length of
     *
     * @throws FileNotFoundException if the file exists but is a directory rather than a regular file or cannot be
     *                               opened for any other reason
     * @throws IOException           on any I/O error
     * @throws TagException          on any exception generated by this library.
     */
    public boolean adjustID3v2Padding(final int paddingSize,
                                      final boolean willShorten,
                                      final boolean copyID3v2Tag,
                                      final File file) throws FileNotFoundException, IOException, TagException {
        int id3v2TagSize = 0;
        final long mp3start = getMp3StartByte(file);
        long newPaddingSize = paddingSize;
        FileOutputStream outStream = null;
        FileInputStream inStream = null;
        File backupFile = null;
        File paddedFile = null;
        if (newPaddingSize < 0) {
            throw new TagException("Invalid paddingSize: " + newPaddingSize);
        }
        if (hasID3v2Tag()) {
            id3v2TagSize = getID3v2Tag().getSize();
        }
        if (newPaddingSize != 0) {
            // double padding size until it's large enough
            while (newPaddingSize < id3v2TagSize) {
                newPaddingSize *= TagOptionSingleton.getInstance().getId3v2PaddingMultiplier();
            }
        }
        if (newPaddingSize < mp3start && !willShorten) {
            return false;
        }
        if (newPaddingSize == mp3start) {
            return false;
        }
        try {
            // we first copy everything to a new file, then replace the original
            paddedFile = File.createTempFile("temp", ".mp3", file.getParentFile());
            outStream = new FileOutputStream(paddedFile);
            inStream = new FileInputStream(file);
            byte[] buffer;
            if (copyID3v2Tag == true) {
                // paddingSize < mp3start && willshorten == false
                // was already checked for outside of the try block.
                if ((newPaddingSize < mp3start) && willShorten) {
                    // copy the current tag
                    buffer = new byte[(int) newPaddingSize];
                    inStream.read(buffer, 0, buffer.length);
                    outStream.write(buffer, 0, buffer.length);
                    buffer = new byte[(int) (mp3start - newPaddingSize)];

                    // skip the rest of the tag that didn't fit
                    inStream.read(buffer, 0, buffer.length);

                    // paddingSize > mp3start
                } else {
                    // copy the current tag
                    buffer = new byte[(int) mp3start];
                    inStream.read(buffer, 0, buffer.length);
                    outStream.write(buffer, 0, buffer.length);

                    // add zeros for the rest of the padding
                    if (newPaddingSize - mp3start > 0) {
                        buffer = new byte[(int) (newPaddingSize - mp3start)];
                        outStream.write(buffer, 0, buffer.length);
                    }
                }
            } else {
                buffer = new byte[(int) newPaddingSize];

                // skip the tag
                inStream.skip(mp3start);

                // write zeros for the tag
                outStream.write(buffer, 0, buffer.length);
            }
            buffer = new byte[1024];
            int b = inStream.read(buffer, 0, buffer.length);
            while (b == 1024) {
                outStream.write(buffer, 0, buffer.length);
                b = inStream.read(buffer, 0, buffer.length);
            }
            if (b != -1) {
                outStream.write(buffer, 0, b);
            }
            backupFile = new File(file.getParentFile(), TagUtility.appendBeforeExtension(file.getName(), ".original"));
            TagUtility.copyFile(file, backupFile);
            if (backupFile.exists()) {
                backupFile.setLastModified(file.lastModified());
            } else {
                return false;
            }
            TagUtility.copyFile(paddedFile, file);
            return true;
        } finally {
            if (inStream != null) {
                inStream.getFD().sync();
                inStream.close();
            }
            if (outStream != null) {
                outStream.getFD().sync();
                outStream.close();
            }
            if ((backupFile != null) &&
                (TagOptionSingleton.getInstance().isOriginalSavedAfterAdjustingID3v2Padding() == false)) {
                backupFile.delete();
            }
            if (paddedFile != null) {
                paddedFile.delete();
            }
        }
    }

    public void delete(final AbstractMP3Tag mp3tag) throws FileNotFoundException, IOException {
        mp3tag.delete(new RandomAccessFile(mp3file, "rw"));
    }

    /**
     * Returns true if this object contains an filename pseudo-tag
     *
     * @return true if this object contains an filename pseudo-tag
     */
    public boolean hasFilenameTag() {
        return (filenameTag != null);
    }

    /**
     * Returns true if this object contains an <code>Id3v1</code> tag
     *
     * @return true if this object contains an <code>Id3v1</code> tag
     */
    public boolean hasID3v1Tag() {
        return (id3v1tag != null);
    }

    /**
     * Returns true if this object contains an <code>Id3v2</code> tag
     *
     * @return true if this object contains an <code>Id3v2</code> tag
     */
    public boolean hasID3v2Tag() {
        return (id3v2tag != null);
    }

    /**
     * Returns true if this object contains an <code>Lyrics3</code> tag
     *
     * @return true if this object contains an <code>Lyrics3</code> tag
     */
    public boolean hasLyrics3Tag() {
        return (lyrics3tag != null);
    }

    /**
     * Saves the tags in this object to the file referred to by this object. It will be saved as
     * TagConstants.MP3_FILE_SAVE_WRITE
     *
     * @throws IOException  on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public void save() throws IOException, TagException {
        save(mp3file, TagOptionSingleton.getInstance().getDefaultSaveMode());
    }

    /**
     * Saves the tags in this object to the file referred to by this object. It will be saved as
     * TagConstants.MP3_FILE_SAVE_WRITE
     *
     * @param saveMode write, overwrite, or append. Defined as <code>TagConstants.MP3_FILE_SAVE_WRITE
     *                 TagConstants.MP3_FILE_SAVE_OVERWRITE TagConstants.MP3_FILE_SAVE_APPEND </code>
     *
     * @throws IOException  on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public void save(final int saveMode) throws IOException, TagException {
        save(mp3file, saveMode);
    }

    /**
     * Saves the tags in this object to the file argument. It will be saved as TagConstants.MP3_FILE_SAVE_WRITE
     *
     * @param filename file to save the this object's tags to
     *
     * @throws IOException  on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public void save(final String filename) throws IOException, TagException {
        save(new File(filename), TagOptionSingleton.getInstance().getDefaultSaveMode());
    }

    /**
     * Saves the tags in this object to the file argument. It will be saved as TagConstants.MP3_FILE_SAVE_WRITE
     *
     * @param file file to save the this object's tags to
     *
     * @throws IOException  on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public void save(final File file) throws IOException, TagException {
        save(file, TagOptionSingleton.getInstance().getDefaultSaveMode());
    }

    /**
     * Saves the tags in this object to the file argument. It will be saved as TagConstants.MP3_FILE_SAVE_WRITE
     *
     * @param filename file to save the this object's tags to
     * @param saveMode write, overwrite, or append. Defined as <code>TagConstants.MP3_FILE_SAVE_WRITE
     *                 TagConstants.MP3_FILE_SAVE_OVERWRITE TagConstants.MP3_FILE_SAVE_APPEND </code>
     *
     * @throws IOException  on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public void save(final String filename, final int saveMode) throws IOException, TagException {
        save(new File(filename), saveMode);
    }

    /**
     * Saves the tags in this object to the file argument. It will be saved as TagConstants.MP3_FILE_SAVE_WRITE
     *
     * @param file     file to save the this object's tags to
     * @param saveMode write, overwrite, or append. Defined as <code>TagConstants.MP3_FILE_SAVE_WRITE
     *                 TagConstants.MP3_FILE_SAVE_OVERWRITE TagConstants.MP3_FILE_SAVE_APPEND </code>
     *
     * @throws IOException  on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public void save(final File file, final int saveMode) throws IOException, TagException {
        if ((saveMode < TagConstant.MP3_FILE_SAVE_FIRST) || (saveMode > TagConstant.MP3_FILE_SAVE_LAST)) {
            throw new TagException("Invalid Save Mode");
        }
        RandomAccessFile rfile = null;
        try {
            if (id3v2tag != null) {
                adjustID3v2Padding(TagOptionSingleton.getInstance().getId3v2PaddingSize(),
                                   TagOptionSingleton.getInstance().isId3v2PaddingWillShorten(),
                                   TagOptionSingleton.getInstance().isId3v2PaddingCopyTag(),
                                   file);
            }

            // we can't put these two if's together because
            // adjustid3v2padding needs all handles on the file closed;
            rfile = new RandomAccessFile(file, "rw");
            if (TagOptionSingleton.getInstance().isId3v2Save()) {
                if (id3v2tag == null) {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        (new ID3v2_4()).delete(rfile);
                    }
                } else {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_WRITE) {
                        id3v2tag.write(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_APPEND) {
                        id3v2tag.append(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        id3v2tag.overwrite(rfile);
                    }
                }
            }
            if (TagOptionSingleton.getInstance().isLyrics3Save()) {
                if (lyrics3tag == null) {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        (new Lyrics3v2()).delete(rfile);
                    }
                } else {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_WRITE) {
                        lyrics3tag.write(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_APPEND) {
                        lyrics3tag.append(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        lyrics3tag.overwrite(rfile);
                    }
                }
            }
            if (TagOptionSingleton.getInstance().isId3v1Save()) {
                if (id3v1tag == null) {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        (new ID3v1()).delete(rfile);
                    }
                } else {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_WRITE) {
                        id3v1tag.write(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_APPEND) {
                        id3v1tag.append(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        id3v1tag.overwrite(rfile);
                    }
                }
            }
            if (TagOptionSingleton.getInstance().isFilenameTagSave()) {
                if (filenameTag != null) {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_WRITE) {
                        filenameTag.write(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_APPEND) {
                        filenameTag.append(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        filenameTag.overwrite(rfile);
                    }
                }
            }
        } finally {
            if (rfile != null) {
                rfile.close();
            }
        }
    }

    /**
     * Returns true if the first MP3 frame can be found for the MP3 file that this object refers to. This is the first
     * byte of music data and not the ID3 Tag Frame.
     *
     * @return true if the first MP3 frame can be found
     *
     * @throws IOException on any I/O error
     */
    public boolean seekMP3Frame() throws IOException {
        RandomAccessFile rfile = null;
        boolean found = false;
        try {
            rfile = new RandomAccessFile(mp3file, "r");
            found = seekMP3Frame(rfile);
        } finally {
            if (rfile != null) {
                rfile.close();
            }
        }
        return found;
    }

    /**
     * Returns true if the first MP3 frame can be found for the MP3 file argument. It tries to sync as many frame as
     * defined in <code>TagOptions.getNumberMP3SyncFrame</code> This is the first byte of music data and not the ID3 Tag
     * Frame.
     *
     * @param seekFile MP3 file to seek
     *
     * @return true if the first MP3 frame can be found
     *
     * @throws IOException on any I/O error
     */
    public boolean seekMP3Frame(final RandomAccessFile seekFile) throws IOException {
        boolean syncFound = false;
        byte first;
        byte second;
        long filePointer = 1;
        variableBitRate = false;
        try {
            seekFile.seek(0);
            do {
                first = seekFile.readByte();
                if (first == (byte) 0xFF) {
                    filePointer = seekFile.getFilePointer();
                    second = (byte) (seekFile.readByte() & (byte) 0xE0);
                    if (second == (byte) 0xE0) {
                        seekFile.seek(filePointer - 1);

                        // seek the next frames, recursively
                        syncFound = seekNextMP3Frame(seekFile,
                                                     TagOptionSingleton.getInstance().getNumberMP3SyncFrame());
                    }
                    seekFile.seek(filePointer);
                }
            } while (syncFound == false);
            seekFile.seek(filePointer - 1);
        } catch (EOFException ex) {
            syncFound = false;
        } catch (IOException ex) {
            throw ex;
        }
        return syncFound;
    }

    /**
     * Returns the MP3 frame size for the file this object refers to. It assumes that <code>seekNextMP3Frame</code> has
     * already been called.
     *
     * @return MP3 Frame size in bytes.
     */
    private int getFrameSize() {
        if (frequency == 0) {
            return 0;
        }
        final int size;
        final int paddingByte = padding ? 1 : 0;
        if (layer == 3) { // Layer I
            size = (int) ((((12 * bitRate) / frequency) + paddingByte) * 4);
        } else {
            size = (int) (((144 * bitRate) / frequency) + paddingByte);
        }
        return size;
    }

    /**
     * Reads the mp3 frame header from the current posiiton in the file and sets this object's private variables to what
     * is found. It assumes the <code>RandomAccessFile</code> is already pointing to a valid MP3 Frame.
     *
     * @param file File to read frame header
     *
     * @throws IOException          on any I/O error
     * @throws TagNotFoundException if MP3 Frame sync bites were not immediately found
     * @throws InvalidTagException  if any of the header values are invlaid
     */
    private void readFrameHeader(final RandomAccessFile file)
            throws IOException, TagNotFoundException, InvalidTagException {
        final byte[] buffer = new byte[4];
        file.read(buffer);

        // sync
        if ((buffer[0] != (byte) 0xFF) || ((buffer[1] & (byte) 0xE0) != (byte) 0xE0)) {
            throw new TagNotFoundException("MP3 Frame sync bits not found");
        }
        mpegVersion = (byte) ((buffer[1] & TagConstant.MASK_MP3_VERSION) >> 3);
        layer = (byte) ((buffer[1] & TagConstant.MASK_MP3_LAYER) >> 1);
        protection = (buffer[1] & TagConstant.MASK_MP3_PROTECTION) != 1;
        final int bitRateValue = (buffer[2] & TagConstant.MASK_MP3_BITRATE) |
                                 (buffer[1] & TagConstant.MASK_MP3_ID) |
                                 (buffer[1] & TagConstant.MASK_MP3_LAYER);
        final Long object = (Long) TagConstant.bitrate.get(new Long(bitRateValue));
        if (object != null) {
            if (object.longValue() != bitRate) {
                variableBitRate = true;
            }
            bitRate = object.intValue();
        } else {
            throw new InvalidTagException("Invalid bit rate");
        }
        final int frequencyValue = (buffer[2] & TagConstant.MASK_MP3_FREQUENCY) >>> 2;
        if (mpegVersion == 3) { // Version 1.0
            switch (frequencyValue) {
                case 0:
                    frequency = 44.1;
                    break;
                case 1:
                    frequency = 48.0;
                    break;
                case 2:
                    frequency = 32.0;
                    break;
            }
        } else if (mpegVersion == 2) { // Version 2.0
            switch (frequencyValue) {
                case 0:
                    frequency = 22.05;
                    break;
                case 1:
                    frequency = 24.00;
                    break;
                case 2:
                    frequency = 16.00;
                    break;
            }
        } else if (mpegVersion == 00) { // Version 2.5
            switch (frequencyValue) {
                case 0:
                    frequency = 11.025;
                    break;
                case 1:
                    frequency = 12.00;
                    break;
                case 2:
                    frequency = 8.00;
                    break;
            }
        } else {
            throw new InvalidTagException("Invalid MPEG version");
        }
        padding = (buffer[2] & TagConstant.MASK_MP3_PADDING) != 0;
        privacy = (buffer[2] & TagConstant.MASK_MP3_PRIVACY) != 0;
        mode = (byte) ((buffer[3] & TagConstant.MASK_MP3_MODE) >> 6);
        modeExtension = (byte) ((buffer[3] & TagConstant.MASK_MP3_MODE_EXTENSION) >> 4);
        copyProtected = (buffer[3] & TagConstant.MASK_MP3_COPY) != 0;
        home = (buffer[3] & TagConstant.MASK_MP3_HOME) != 0;
        emphasis = (byte) ((buffer[3] & TagConstant.MASK_MP3_EMPHASIS));
    }

    /**
     * Returns true if the first MP3 frame can be found for the MP3 file argument. It is recursive and called by
     * seekMP3Frame. This is the first byte of music data and not the ID3 Tag Frame.
     *
     * @param file       MP3 file to seek
     * @param iterations recursive counter
     *
     * @return true if the first MP3 frame can be found
     *
     * @throws IOException on any I/O error
     */
    private boolean seekNextMP3Frame(final RandomAccessFile file, final int iterations) throws IOException {
        final boolean syncFound;
        final byte[] buffer;
        final byte first;
        final byte second;
        final long filePointer;
        if (iterations == 0) {
            syncFound = true;
        } else {
            try {
                readFrameHeader(file);
            } catch (TagException ex) {
                return false;
            }
            final int size = getFrameSize();
            if ((size <= 0) || (size > file.length())) {
                return false;
            }
            buffer = new byte[size - 4];
            file.read(buffer);
            filePointer = file.getFilePointer();
            first = file.readByte();
            if (first == (byte) 0xFF) {
                second = (byte) (file.readByte() & (byte) 0xE0);
                if (second == (byte) 0xE0) {
                    file.seek(filePointer);

                    // recursively find the next frames
                    syncFound = seekNextMP3Frame(file, iterations - 1);
                } else {
                    syncFound = false;
                }
            } else {
                syncFound = false;
            }
        }
        return syncFound;
    }
}
TOP

Related Classes of org.farng.mp3.MP3File

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.