package org.farng.mp3; 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; 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; /** * <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: 2374 $ */ 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; } }