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 expandable. </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 section 5. </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's 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 to files encoded with <a href="#mpeg">MPEG-2 * layer I, MPEG-2 layer II, MPEG-2 layer III</a> 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.<br> </p> * <p/> * <p class=t> <i>Padding is good as it increases the write speed when there is already a tag present in a file. If the * new tag is one byte longer than the previous tag, than the extra byte can be taken from the padding, instead of * having to shift the entire file one byte. Padding is of course bad in that it increases the size of the file, but if * the amount of padding is wisely chosen (with clustersize in mind), the impact on filesystems will be virtually none. * As the contents is $00, it is also easy for modems and other transmission devices/protocols to compress the padding. * Having a $00 filled padding also increases the ability to recover erroneous tags.</i> </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>ID3/file identifier</td><td rowspan=3> </td><td * width="100%">"ID3"</td></tr> <tr><td>ID3 version</td><td>$02 00</td></tr> <tr><td>ID3 * flags</td><td>%xx000000</td></tr> <tr><td>ID3 size</td><td>4 * </td><td>%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 ID3 tag, directly followed * by the two version bytes. The first byte of ID3 version is it's major version, while the second byte is its revision * number. All revisions are backwards compatible while major versions are not. If software with ID3v2 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><i> In the first draft of ID3v2 the identifier was "TAG", just as in ID3v1. It was later changed to "MP3" * as I thought of the ID3v2 as the fileheader MP3 had always been missing. When it became appearant than ID3v2 was * going towards a general purpose audio header the identifier was changed to "ID3". </i></p> * <p/> * <p class=t> The first bit (bit 7) in the 'ID3 flags' is indicating whether or not <a * href="#sec5">unsynchronisation</a> is used; a set bit indicates usage. </p> * <p/> * <p class=t> The second bit (bit 6) is indicating whether or not compression is used; a set bit indicates usage. Since * no compression scheme has been decided yet, the ID3 decoder (for now) should just ignore the entire tag if the * compression bit is set. </p> * <p/> * <p class=t><i> Currently, zlib compression is being considered for the compression, in an effort to stay out of the * all-too-common marsh of patent trouble. Have a look at the additions draft for the latest developments. </i></p> * <p/> * <p class=t> The ID3 tag size is encoded with four bytes where the first 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><i> We really gave it a second thought several times before we introduced these awkward size descriptions. * The reason is that we thought it would be even worse to have a file header with no set size (as we wanted to * unsynchronise the header if there were any false synchronisations in it). An easy way of calculating the tag size is * A*2^21+B*2^14+C*2^7+D = A*2097152+B*16384+C*128+D, where A is the first byte, B the second, C the third and D the * fourth byte. </i></p> * <p/> * <p class=t> The ID3 tag size is the size of the complete tag after unsychronisation, including padding, excluding the * header (total tag size - 10). The reason to use 28 bits (representing up to 256MB) for size description is that we * don't want to run out of space here. </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_2 extends AbstractID3v2 { protected boolean compression = false; protected boolean unsynchronization = false; /** * Creates a new ID3v2_2 object. */ public ID3v2_2() { super(); setMajorVersion((byte) 2); setRevision((byte) 2); } /** * Creates a new ID3v2_2 object. */ public ID3v2_2(final ID3v2_2 copyObject) { super(copyObject); this.compression = copyObject.compression; this.unsynchronization = copyObject.unsynchronization; } /** * Creates a new ID3v2_2 object. */ public ID3v2_2(final AbstractMP3Tag mp3tag) { if (mp3tag != null) { final ID3v2_4 convertedTag; if ((mp3tag instanceof ID3v2_3 == false) && (mp3tag instanceof ID3v2_2 == true)) { throw new UnsupportedOperationException("Copy Constructor not called. Please type cast the argument"); } else if (mp3tag instanceof ID3v2_4) { convertedTag = (ID3v2_4) mp3tag; } else { convertedTag = new ID3v2_4(mp3tag); } this.compression = convertedTag.compression; this.unsynchronization = convertedTag.unsynchronization; final AbstractID3v2 id3tag = convertedTag; final Iterator iterator = id3tag.getFrameIterator(); AbstractID3v2Frame frame; ID3v2_2Frame newFrame; while (iterator.hasNext()) { frame = (AbstractID3v2Frame) iterator.next(); newFrame = new ID3v2_2Frame(frame); this.setFrame(newFrame); } } } /** * Creates a new ID3v2_2 object. */ public ID3v2_2(final RandomAccessFile file) throws TagException, IOException { this.read(file); } public String getIdentifier() { return "ID3v2_2.20"; } public int getSize() { int size = 3 + 2 + 1 + 4; final Iterator iterator = getFrameIterator(); ID3v2_2Frame frame; while (iterator.hasNext()) { frame = (ID3v2_2Frame) iterator.next(); size += frame.getSize(); } return size; } public void append(final AbstractMP3Tag tag) { if (tag instanceof ID3v2_2) { this.unsynchronization = ((ID3v2_2) tag).unsynchronization; this.compression = ((ID3v2_2) tag).compression; } super.append(tag); } public boolean equals(final Object obj) { if ((obj instanceof ID3v2_2) == false) { return false; } final ID3v2_2 id3v2_2 = (ID3v2_2) obj; if (this.compression != id3v2_2.compression) { return false; } if (this.unsynchronization != id3v2_2.unsynchronization) { return false; } return super.equals(obj); } public void overwrite(final AbstractMP3Tag tag) { if (tag instanceof ID3v2_2) { this.unsynchronization = ((ID3v2_2) tag).unsynchronization; this.compression = ((ID3v2_2) tag).compression; } super.overwrite(tag); } public void read(final RandomAccessFile file) throws TagException, IOException { final int size; ID3v2_2Frame next; final byte[] buffer = new byte[4]; if (seek(file) == false) { throw new TagNotFoundException("ID3v2.20 tag not found"); } // read the major and minor @version number & flags byte file.read(buffer, 0, 3); if ((buffer[0] != 2) || (buffer[1] != 0)) { throw new TagNotFoundException(getIdentifier() + " tag not found"); } setMajorVersion(buffer[0]); setRevision(buffer[1]); this.unsynchronization = (buffer[2] & TagConstant.MASK_V22_UNSYNCHRONIZATION) != 0; this.compression = (buffer[2] & TagConstant.MASK_V22_COMPRESSION) != 0; // read the size file.read(buffer, 0, 4); size = byteArrayToSize(buffer); this.clearFrameMap(); final long filePointer = file.getFilePointer(); // read all frames this.setFileReadBytes(size); resetPaddingCounter(); while ((file.getFilePointer() - filePointer) <= size) { try { next = new ID3v2_2Frame(file); final String id = next.getIdentifier(); if (this.hasFrame(id)) { this.appendDuplicateFrameId(id + "; "); this.incrementDuplicateBytes(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()); /** * int newSize = this.getSize(); if ((this.padding + newSize - 10) != * size) { System.out.println("WARNING: Tag sizes don't add up"); * System.out.println("ID3v2.20 tag size : " + newSize); * System.out.println("ID3v2.20 padding : " + this.padding); * System.out.println("ID3v2.20 total : " + (this.padding + newSize)); * System.out.println("ID3v2.20 file size: " + size); } */ } 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] == 2) && (buffer[1] == 0)); } public String toString() { final Iterator iterator = this.getFrameIterator(); ID3v2_2Frame frame; String str = getIdentifier() + " - " + this.getSize() + " bytes\n"; str += ("compression = " + this.compression + "\n"); str += ("unsynchronization = " + this.unsynchronization + "\n"); while (iterator.hasNext()) { frame = (ID3v2_2Frame) iterator.next(); str += (frame.toString() + "\n"); } return str + "\n"; } public void write(final AbstractMP3Tag tag) { if (tag instanceof ID3v2_2) { this.unsynchronization = ((ID3v2_2) tag).unsynchronization; this.compression = ((ID3v2_2) tag).compression; } super.write(tag); } public void write(final RandomAccessFile file) throws IOException { final String str; ID3v2_2Frame frame; 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); // write the first 10 tag bytes str = "ID3"; for (int i = 0; i < str.length(); i++) { buffer[i] = (byte) str.charAt(i); } buffer[3] = 2; buffer[4] = 0; if (this.unsynchronization) { buffer[5] |= TagConstant.MASK_V22_UNSYNCHRONIZATION; } if (this.compression) { buffer[5] |= TagConstant.MASK_V22_COMPRESSION; } file.write(buffer); //write size; file.write(sizeToByteArray((int) mp3start - 10)); // write all frames iterator = this.getFrameIterator(); while (iterator.hasNext()) { frame = (ID3v2_2Frame) 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_2Frame(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_2Frame(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_2Frame(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_2Frame(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_2Frame(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_2Frame(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_2Frame(new FrameBodyTRCK((byte) 0, trackNumberOnAlbum.trim())); setFrame(field); } else { ((FrameBodyTRCK) field.getBody()).setText(trackNumberOnAlbum.trim()); } } public void setSongLyric(String songLyrics) { AbstractID3v2Frame field = getFrame("USLT"); if (field == null) { field = new ID3v2_2Frame(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_2Frame(new FrameBodyTCOM((byte) 0, authorComposer.trim())); setFrame(field); } else { ((FrameBodyTCOM) field.getBody()).setText(authorComposer.trim()); } } }