package org.farng.mp3.id3; import java.io.IOException; import java.io.RandomAccessFile; import java.util.Iterator; import org.farng.mp3.AbstractMP3Tag; import org.farng.mp3.InvalidTagException; import org.farng.mp3.MP3File; import org.farng.mp3.TagConstant; import org.farng.mp3.TagException; import org.farng.mp3.TagNotFoundException; /** * <p class=t> The two biggest design goals were to be able to implement ID3v2 without disturbing old software too much * and that ID3v2 should be as flexible and expandable as possible. </p> * <p/> * <p class=t> The first criterion is met by the simple fact that the <a href="#MPEG">MPEG</a> decoding software uses a * syncsignal, embedded in the audiostream, to 'lock on to' the audio. Since the ID3v2 tag doesn't contain a valid * syncsignal, no software will attempt to play the tag. If, for any reason, coincidence make a syncsignal appear within * the tag it will be taken care of by the 'unsynchronisation scheme' described in <a href="#sec5">section 5</a>. </p> * <p/> * <p class=t> The second criterion has made a more noticeable impact on the design of the ID3v2 tag. It is constructed * as a container for several information blocks, called frames, whose format need not be known to the software that * encounters them. At the start of every frame there is an identifier that explains the frames' format and content, and * a size descriptor that allows software to skip unknown frames. </p> * <p/> * <p class=t> If a total revision of the ID3v2 tag should be needed, there is a version number and a size descriptor in * the ID3v2 header. </p> * <p/> * <p class=t> The ID3 tag described in this document is mainly targeted at files encoded with <a * href="#MPEG">MPEG</a>-1/2 layer I, <a href="#MPEG">MPEG</a>-1/2 layer II, <a href="#MPEG">MPEG</a>-1/2 layer III and * MPEG-2.5, but may work with other types of encoded audio. </p> * <p/> * <p class=t> The bitorder in ID3v2 is most significant bit first (MSB). The byteorder in multibyte numbers is most * significant byte first (e.g. $12345678 would be encoded $12 34 56 78). </p> * <p/> * <p class=t> It is permitted to include padding after all the final frame (at the end of the ID3 tag), making the size * of all the frames together smaller than the size given in the head of the tag. A possible purpose of this padding is * to allow for adding a few additional frames or enlarge existing frames within the tag without having to rewrite the * entire file. The value of the padding bytes must be $00. </p> <p class=t> The ID3v2 tag header, which should be the * first information in the file, is 10 bytes as follows: </p> * <p/> * <p><center> <table border=0> <tr><td nowrap>ID3v2/file identifier</td><td rowspan=4> </td><td * width="100%">"ID3"</td></tr> <tr><td>ID3v2 version</td><td>$03 00</td></tr> <tr><td>ID3v2 * flags</td><td>%abc00000</td></tr> <tr><td>ID3v2 size</td><td>4 * %0xxxxxxx</td></tr> </table> </center> * <p/> * <p class=t> The first three bytes of the tag are always "ID3" to indicate that this is an ID3v2 tag, directly * followed by the two version bytes. The first byte of ID3v2 version is it's major version, while the second byte is * its revision number. In this case this is ID3v2.3.0. All revisions are backwards compatible while major versions are * not. If software with ID3v2.2.0 and below support should encounter version three or higher it should simply ignore * the whole tag. Version and revision will never be $FF. </p> * <p/> * <p class=t> The version is followed by one the ID3v2 flags field, of which currently only three flags are used. </p> * <p/> * <p class=t> a - Unsynchronisation </p> * <p/> * <p class=ind> Bit 7 in the 'ID3v2 flags' indicates whether or not unsynchronisation is used (see <a * href="#sec5">section 5</a> for details); a set bit indicates usage.</p> * <p/> * <p class=t> b - Extended header </p> * <p/> * <p class=ind> The second bit (bit 6) indicates whether or not the header is followed by an extended header. The * extended header is described in <a href="#sec3.2">section 3.2</a>. </p> * <p/> * <p class=t> c - Experimental indicator </p> * <p/> * <p class=ind> The third bit (bit 5) should be used as an 'experimental indicator'. This flag should always be set * when the tag is in an experimental stage. </p> * <p/> * <p class=t> All the other flags should be cleared. If one of these undefined flags are set that might mean that the * tag is not readable for a parser that does not know the flags function. </p> * <p/> * <p class=t> The ID3v2 tag size is encoded with four bytes where the most significant bit (bit 7) is set to zero in * every byte, making a total of 28 bits. The zeroed bits are ignored, so a 257 bytes long tag is represented as $00 00 * 02 01. </p> * <p/> * <p class=t> The ID3v2 tag size is the size of the complete tag after unsychronisation, including padding, excluding * the header but not excluding the extended header (total tag size - 10). Only 28 bits (representing up to 256MB) are * used in the size description to avoid the introducuction of 'false syncsignals'. </p> * <p/> * <p class=t> An ID3v2 tag can be detected with the following pattern:<br> $49 44 33 yy yy xx zz zz zz zz<br> Where yy * is less than $FF, xx is the 'flags' byte and zz is less than $80. </p> * * @author Eric Farng * @version $Revision: 2374 $ */ public class ID3v2_3 extends ID3v2_2 { protected boolean crcDataFlag = false; protected boolean experimental = false; protected boolean extended = false; protected int crcData = 0; protected int paddingSize = 0; /** * Creates a new ID3v2_3 object. */ public ID3v2_3() { setMajorVersion((byte) 2); setRevision((byte) 3); } /** * Creates a new ID3v2_3 object. */ public ID3v2_3(final ID3v2_3 copyObject) { super(copyObject); this.crcDataFlag = copyObject.crcDataFlag; this.experimental = copyObject.experimental; this.extended = copyObject.extended; this.crcData = copyObject.crcData; this.paddingSize = copyObject.paddingSize; } /** * Creates a new ID3v2_3 object. */ public ID3v2_3(final AbstractMP3Tag mp3tag) { if (mp3tag != null) { final ID3v2_4 convertedTag; if ((mp3tag instanceof ID3v2_4 == false) && (mp3tag instanceof ID3v2_3 == true)) { throw new UnsupportedOperationException("Copy Constructor not called. Please type cast the argument"); } if (mp3tag instanceof ID3v2_4) { convertedTag = (ID3v2_4) mp3tag; } else { convertedTag = new ID3v2_4(mp3tag); } this.extended = convertedTag.extended; this.experimental = convertedTag.experimental; this.crcDataFlag = convertedTag.crcDataFlag; this.crcData = convertedTag.crcData; this.paddingSize = convertedTag.paddingSize; this.compression = convertedTag.compression; this.unsynchronization = convertedTag.unsynchronization; final AbstractID3v2 id3tag = convertedTag; final Iterator iterator = id3tag.getFrameIterator(); AbstractID3v2Frame frame; ID3v2_3Frame newFrame; while (iterator.hasNext()) { frame = (AbstractID3v2Frame) iterator.next(); newFrame = new ID3v2_3Frame(frame); this.setFrame(newFrame); } } } /** * Creates a new ID3v2_3 object. */ public ID3v2_3(final RandomAccessFile file) throws TagException, IOException { this.read(file); } public String getIdentifier() { return "ID3v2.30"; } public int getSize() { int size = 3 + 2 + 1 + 4; if (this.extended) { if (this.crcDataFlag) { size += (4 + 2 + 4 + 4); } else { size += (4 + 2 + 4); } } final Iterator iterator = this.getFrameIterator(); AbstractID3v2Frame frame; while (iterator.hasNext()) { frame = (AbstractID3v2Frame) iterator.next(); size += frame.getSize(); } return size; } public void append(final AbstractMP3Tag tag) { if (tag instanceof ID3v2_3) { this.experimental = ((ID3v2_3) tag).experimental; this.extended = ((ID3v2_3) tag).extended; this.crcDataFlag = ((ID3v2_3) tag).crcDataFlag; this.paddingSize = ((ID3v2_3) tag).paddingSize; this.crcData = ((ID3v2_3) tag).crcData; } super.append(tag); } public boolean equals(final Object obj) { if ((obj instanceof ID3v2_3) == false) { return false; } final ID3v2_3 id3v2_3 = (ID3v2_3) obj; if (this.crcData != id3v2_3.crcData) { return false; } if (this.crcDataFlag != id3v2_3.crcDataFlag) { return false; } if (this.experimental != id3v2_3.experimental) { return false; } if (this.extended != id3v2_3.extended) { return false; } if (this.paddingSize != id3v2_3.paddingSize) { return false; } return super.equals(obj); } public void overwrite(final AbstractMP3Tag tag) { if (tag instanceof ID3v2_3) { this.experimental = ((ID3v2_3) tag).experimental; this.extended = ((ID3v2_3) tag).extended; this.crcDataFlag = ((ID3v2_3) tag).crcDataFlag; this.paddingSize = ((ID3v2_3) tag).paddingSize; this.crcData = ((ID3v2_3) tag).crcData; } super.overwrite(tag); } public void read(final RandomAccessFile file) throws TagException, IOException { final int size; final byte[] buffer = new byte[4]; if (seek(file) == false) { throw new TagNotFoundException(getIdentifier() + " tag not found"); } // read the major and minor @version number & flags byte file.read(buffer, 0, 3); if ((buffer[0] != 3) || (buffer[1] != 0)) { throw new TagNotFoundException(getIdentifier() + " tag not found"); } setMajorVersion(buffer[0]); setRevision(buffer[1]); this.unsynchronization = (buffer[2] & TagConstant.MASK_V23_UNSYNCHRONIZATION) != 0; this.extended = (buffer[2] & TagConstant.MASK_V23_EXTENDED_HEADER) != 0; this.experimental = (buffer[2] & TagConstant.MASK_V23_EXPERIMENTAL) != 0; // read the size file.read(buffer, 0, 4); size = byteArrayToSize(buffer); final long filePointer = file.getFilePointer(); if (this.extended) { // int is 4 bytes. final int extendedHeaderSize = file.readInt(); // the extended header is only 6 or 10 bytes. if (extendedHeaderSize != 6 && extendedHeaderSize != 10) { throw new InvalidTagException("Invalid Extended Header Size."); } file.read(buffer, 0, 2); this.crcDataFlag = (buffer[0] & TagConstant.MASK_V23_CRC_DATA_PRESENT) != 0; // if it's 10 bytes, the CRC flag must be set // and if it's 6 bytes, it must not be set if (((extendedHeaderSize == 10) && (this.crcDataFlag == false)) || ((extendedHeaderSize == 6) && (this.crcDataFlag == true))) { throw new InvalidTagException("CRC Data flag not set correctly."); } this.paddingSize = file.readInt(); if ((extendedHeaderSize == 10) && this.crcDataFlag) { this.crcData = file.readInt(); } } ID3v2_3Frame next; this.clearFrameMap(); // read all the frames. this.setFileReadBytes(size); AbstractID3v2.resetPaddingCounter(); while ((file.getFilePointer() - filePointer) <= size) { try { next = new ID3v2_3Frame(file); final String id = next.getIdentifier(); if (this.hasFrame(id)) { this.appendDuplicateFrameId(id + "; "); this.incrementDuplicateBytes(this.getFrame(id).getSize()); } this.setFrame(next); } catch (InvalidTagException ex) { if (ex.getMessage().equals("Found empty frame")) { this.incrementEmptyFrameBytes(10); } else { this.incrementInvalidFrameBytes(); } } } this.setPaddingSize(getPaddingCounter()); } public boolean seek(final RandomAccessFile file) throws IOException { final byte[] buffer = new byte[3]; file.seek(0); // read the tag if it exists file.read(buffer, 0, 3); final String tag = new String(buffer, 0, 3); if (tag.equals("ID3") == false) { return false; } // read the major and minor @version number file.read(buffer, 0, 2); // read back the @version bytes so we can read and save them later file.seek(file.getFilePointer() - 2); return ((buffer[0] == 3) && (buffer[1] == 0)); } public String toString() { final Iterator iterator = this.getFrameIterator(); AbstractID3v2Frame frame; String str = getIdentifier() + " " + this.getSize() + "\n"; str += ("compression = " + this.compression + "\n"); str += ("unsynchronization = " + this.unsynchronization + "\n"); str += ("crcData = " + this.crcData + "\n"); str += ("crcDataFlag = " + this.crcDataFlag + "\n"); str += ("experimental = " + this.experimental + "\n"); str += ("extended = " + this.extended + "\n"); str += ("paddingSize = " + this.paddingSize + "\n"); while (iterator.hasNext()) { frame = (ID3v2_3Frame) iterator.next(); str += (frame.toString() + "\n"); } return str + "\n"; } public void write(final AbstractMP3Tag tag) { if (tag instanceof ID3v2_3) { this.experimental = ((ID3v2_3) tag).experimental; this.extended = ((ID3v2_3) tag).extended; this.crcDataFlag = ((ID3v2_3) tag).crcDataFlag; this.paddingSize = ((ID3v2_3) tag).paddingSize; this.crcData = ((ID3v2_3) tag).crcData; } super.write(tag); } public void write(final RandomAccessFile file) throws IOException { final String str; final Iterator iterator; final byte[] buffer = new byte[6]; final MP3File mp3 = new MP3File(); mp3.seekMP3Frame(file); final long mp3start = file.getFilePointer(); file.seek(0); ID3v2_3Frame frame; str = "ID3"; for (int i = 0; i < str.length(); i++) { buffer[i] = (byte) str.charAt(i); } buffer[3] = 3; buffer[4] = 0; if (this.unsynchronization) { buffer[5] |= TagConstant.MASK_V23_UNSYNCHRONIZATION; } if (this.extended) { buffer[5] |= TagConstant.MASK_V23_EXTENDED_HEADER; } if (this.experimental) { buffer[5] |= TagConstant.MASK_V23_EXPERIMENTAL; } file.write(buffer); // write size file.write(sizeToByteArray((int) mp3start - 10)); if (this.extended) { if (this.crcDataFlag) { file.writeInt(10); buffer[0] = 0; buffer[0] |= TagConstant.MASK_V23_CRC_DATA_PRESENT; file.write(buffer, 0, 2); file.writeInt(this.paddingSize); file.writeInt(this.crcData); } else { file.writeInt(6); file.write(buffer, 0, 2); file.writeInt(this.paddingSize); } } // write all frames iterator = this.getFrameIterator(); while (iterator.hasNext()) { frame = (ID3v2_3Frame) iterator.next(); frame.write(file); } } public String getSongTitle() { String text = ""; AbstractID3v2Frame frame = getFrame("TIT2"); if (frame != null) { FrameBodyTIT2 body = (FrameBodyTIT2) frame.getBody(); text = body.getText(); } return text.trim(); } public String getLeadArtist() { String text = ""; AbstractID3v2Frame frame = getFrame("TPE1"); if (frame != null) { FrameBodyTPE1 body = (FrameBodyTPE1) frame.getBody(); text = body.getText(); } return text.trim(); } public String getAlbumTitle() { String text = ""; AbstractID3v2Frame frame = getFrame("TALB"); if (frame != null) { FrameBodyTALB body = (FrameBodyTALB) frame.getBody(); text = body.getText(); } return text.trim(); } public String getYearReleased() { String text = ""; AbstractID3v2Frame frame = getFrame("TYER"); if (frame != null) { FrameBodyTYER body = (FrameBodyTYER) frame.getBody(); text = body.getText(); } return text.trim(); } public String getSongComment() { String text = ""; AbstractID3v2Frame frame = getFrame("COMM" + ((char) 0) + "eng" + ((char) 0) + ""); if (frame != null) { FrameBodyCOMM body = (FrameBodyCOMM) frame.getBody(); text = body.getText(); } return text.trim(); } public String getSongGenre() { String text = ""; AbstractID3v2Frame frame = getFrame("TCON"); if (frame != null) { FrameBodyTCON body = (FrameBodyTCON) frame.getBody(); text = body.getText(); } return text.trim(); } public String getTrackNumberOnAlbum() { String text = ""; AbstractID3v2Frame frame = getFrame("TRCK"); if (frame != null) { FrameBodyTRCK body = (FrameBodyTRCK) frame.getBody(); text = body.getText(); } return text.trim(); } public String getSongLyric() { String text = ""; AbstractID3v2Frame frame = getFrame("SYLT"); if (frame != null) { FrameBodySYLT body = (FrameBodySYLT) frame.getBody(); text = body.getLyric(); } if (text == "") { frame = getFrame("USLT" + ((char) 0) + "eng" + ((char) 0) + ""); if (frame != null) { FrameBodyUSLT body = (FrameBodyUSLT) frame.getBody(); text = body.getLyric(); } } return text.trim(); } public String getAuthorComposer() { String text = ""; AbstractID3v2Frame frame = getFrame("TCOM"); if (frame != null) { FrameBodyTCOM body = (FrameBodyTCOM) frame.getBody(); text = body.getText(); } return text.trim(); } public void setSongTitle(String songTitle) { AbstractID3v2Frame field = getFrame("TIT2"); if (field == null) { field = new ID3v2_3Frame(new FrameBodyTIT2((byte) 0, songTitle.trim())); setFrame(field); } else { ((FrameBodyTIT2) field.getBody()).setText(songTitle.trim()); } } public void setLeadArtist(String leadArtist) { AbstractID3v2Frame field = getFrame("TPE1"); if (field == null) { field = new ID3v2_3Frame(new FrameBodyTPE1((byte) 0, leadArtist.trim())); setFrame(field); } else { ((FrameBodyTPE1) field.getBody()).setText(leadArtist.trim()); } } public void setAlbumTitle(String albumTitle) { AbstractID3v2Frame field = getFrame("TALB"); if (field == null) { field = new ID3v2_3Frame(new FrameBodyTALB((byte) 0, albumTitle.trim())); setFrame(field); } else { ((FrameBodyTALB) field.getBody()).setText(albumTitle.trim()); } } public void setYearReleased(String yearReleased) { AbstractID3v2Frame field = getFrame("TYER"); if (field == null) { field = new ID3v2_3Frame(new FrameBodyTYER((byte) 0, yearReleased.trim())); setFrame(field); } else { ((FrameBodyTYER) field.getBody()).setText(yearReleased.trim()); } } public void setSongComment(String songComment) { AbstractID3v2Frame field = getFrame("COMM"); if (field == null) { field = new ID3v2_3Frame(new FrameBodyCOMM((byte) 0, "ENG", "", songComment.trim())); setFrame(field); } else { ((FrameBodyCOMM) field.getBody()).setText(songComment.trim()); } } public void setSongGenre(String songGenre) { AbstractID3v2Frame field = getFrame("TCON"); if (field == null) { field = new ID3v2_3Frame(new FrameBodyTCON((byte) 0, songGenre.trim())); setFrame(field); } else { ((FrameBodyTCON) field.getBody()).setText(songGenre.trim()); } } public void setTrackNumberOnAlbum(String trackNumberOnAlbum) { AbstractID3v2Frame field = getFrame("TRCK"); if (field == null) { field = new ID3v2_3Frame(new FrameBodyTRCK((byte) 0, trackNumberOnAlbum.trim())); setFrame(field); } else { ((FrameBodyTRCK) field.getBody()).setText(trackNumberOnAlbum.trim()); } } public void setSongLyric(String songLyrics) { AbstractID3v2Frame field = getFrame("SYLT"); if (field == null) { field = new ID3v2_3Frame(new FrameBodyUSLT((byte) 0, "ENG", "", songLyrics.trim())); setFrame(field); } else { ((FrameBodyUSLT) field.getBody()).setLyric(songLyrics.trim()); } } public void setAuthorComposer(String authorComposer) { AbstractID3v2Frame field = getFrame("TCOM"); if (field == null) { field = new ID3v2_3Frame(new FrameBodyTCOM((byte) 0, authorComposer.trim())); setFrame(field); } else { ((FrameBodyTCOM) field.getBody()).setText(authorComposer.trim()); } } }