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; import org.farng.mp3.filename.FilenameTag; import org.farng.mp3.lyrics3.AbstractLyrics3; import org.farng.mp3.lyrics3.Lyrics3v2; import org.farng.mp3.lyrics3.Lyrics3v2Field; /** * <p>   ID3v2 is a general tagging format for audio, which makes it possible<br>    to store meta * data about the audio inside the audio file itself. The<br>    ID3 tag described in this document is mainly * targeted at files<br>    encoded with MPEG-1/2 layer I, MPEG-1/2 layer II, MPEG-1/2 layer III<br> *    and MPEG-2.5, but may work with other types of encoded audio or as a<br>    stand alone format * for audio meta data.</p> * <p/> * <p>   ID3v2 is designed to be as flexible and expandable as possible to<br>    meet new meta * information needs that might arise. To achieve that<br>    ID3v2 is constructed as a container for several * information blocks,<br>    called frames, whose format need not be known to the software that<br> *    encounters them. At the start of every frame is an unique and<br>    predefined identifier, a * size descriptor that allows software to skip<br>    unknown frames and a flags field. The flags describes * encoding<br>    details and if the frame should remain in the tag, should it be<br>    unknown to * the software, if the file is altered.</p> * <p/> * <p>   The bitorder in ID3v2 is most significant bit first (MSB). The<br>    byteorder in * multibyte numbers is most significant byte first (e.g.<br>    $12345678 would be encoded $12 34 56 78), * also known as big endian<br>    and network byte order.</p> * <p/> * <p>   Overall tag structure:</p> <table border="1"> <tr> <td width="100%" align="center"> <p * align="center">Header (10 bytes)</p> </td> </tr> <tr> <td width="100%" align="center">Extended Header (variable * length, OPTIONAL)</td> </tr> <tr> <td width="100%" align="center">Frames (variable length)</td> </tr> <tr> <td * width="100%" align="center">Padding (variable length, OPTIONAL)</td> </tr> <tr> <td width="100%" * align="center">Footer (10 bytes, OPTIONAL)</td> </tr> </table> <p>   In general, padding and footer are * mutually exclusive. See details in<br>    sections 3.3, 3.4 and 5.<br> </p> <a name="sec3.1"></a> * <p/> * <h3>3.1.   ID3v2 header</h3> * <p/> * <p>   The first part of the ID3v2 tag is the 10 byte tag header, laid out<br>    as follows:</p> * <p/> * <p>     ID3v2/file identifier      "ID3"<br> *      ID3v2 version              * $04 00<br>      ID3v2 flags                * %abcd0000<br>      ID3v2 size             * 4 * %0xxxxxxx</p> * <p/> * <p>   The first three bytes of the tag are always "ID3", to indicate that<br>    this * is an ID3v2 tag, directly followed by the two version bytes. The<br>    first byte of ID3v2 version is its * major version, while the second<br>    byte is its revision number. In this case this is ID3v2.4.0. All<br> *    revisions are backwards compatible while major versions are not. If<br>    software with * ID3v2.4.0 and below support should encounter version<br>    five or higher it should simply ignore the * whole tag. Version or<br>    revision will never be $FF.</p> * <p/> * <p>   The version is followed by the ID3v2 flags field, of which currently<br>    four flags are * used.<br> </p> * <p/> * <p>   a - Unsynchronisation</p> * <p/> * <p>     Bit 7 in the 'ID3v2 flags' indicates whether or not<br>      * unsynchronisation is applied on all frames (see section 6.1 for<br>      details); a set bit * indicates usage.<br> </p> * <p/> * <p>   b - Extended header</p> * <p/> * <p>     The second bit (bit 6) indicates whether or not the header is<br> *      followed by an extended header. The extended header is described in<br> *      section 3.2. A set bit indicates the presence of an extended<br>      * header.<br> </p> * <p/> * <p>   c - Experimental indicator</p> * <p/> * <p>     The third bit (bit 5) is used as an 'experimental indicator'. This<br> *      flag SHALL always be set when the tag is in an experimental stage.<br> </p> * <p/> * <p>   d - Footer present</p> * <p/> * <p>     Bit 4 indicates that a footer (section 3.4) is present at the very<br> *      end of the tag. A set bit indicates the presence of a footer.<br> </p> * <p/> * <p>   All the other flags MUST be cleared. If one of these undefined flags<br>    are set, the * tag might not be readable for a parser that does not<br>    know the flags function.</p> * <p/> * <p>   The ID3v2 tag size is stored as a 32 bit synchsafe integer (section<br>    6.2), making a * total of 28 effective bits (representing up to 256MB).</p> * <p/> * <p>   The ID3v2 tag size is the sum of the byte length of the extended<br>    header, the padding * and the frames after unsynchronisation. If a<br>    footer is present this equals to ('total size' - 20) * bytes, otherwise<br>    ('total size' - 10) bytes.</p> * <p/> * <p>   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<br> *    $80.<br> </p> <a name="sec3.2"></a> * <p/> * <h3>3.2. Extended header</h3> * <p/> * <p>   The extended header contains information that can provide further<br>    insight in the * structure of the tag, but is not vital to the correct<br>    parsing of the tag information; hence the * extended header is<br>    optional.</p> * <p/> * <p>     Extended header size   4 * %0xxxxxxx<br>      Number of * flag bytes       $01<br>      Extended * Flags             $xx</p> * <p/> * <p>   Where the 'Extended header size' is the size of the whole extended<br>    header, stored as * a 32 bit synchsafe integer. An extended header can<br>    thus never have a size of fewer than six * bytes.</p> * <p/> * <p>   The extended flags field, with its size described by 'number of flag<br>    bytes', is * defined as:</p> * <p/> * <p>     %0bcd0000</p> * <p/> * <p>   Each flag that is set in the extended header has data attached, which<br>    comes in the * order in which the flags are encountered (i.e. the data<br>    for flag 'b' comes before the data for flag * 'c'). Unset flags cannot<br>    have any attached data. All unknown flags MUST be unset and their<br> *    corresponding data removed when a tag is modified.</p> * <p/> * <p>   Every set flag's data starts with a length byte, which contains a<br>    value between 0 * and 127 ($00 - $7f), followed by data that has the<br>    field length indicated by the length byte. If a * flag has no attached<br>    data, the value $00 is used as length byte.<br> </p> * <p/> * <p>   b - Tag is an update</p> * <p/> * <p>     If this flag is set, the present tag is an update of a tag found<br> *      earlier in the present file or stream. If frames defined as unique<br> *      are found in the present tag, they are to override any<br>      * corresponding ones found in the earlier tag. This flag has no<br>      corresponding data.</p> * <p/> * <p>         Flag data length      $00</p> * <p/> * <p>   c - CRC data present</p> * <p/> * <p>     If this flag is set, a CRC-32 [ISO-3309] data is included in the<br> *      extended header. The CRC is calculated on all the data between the<br> *      header and footer as indicated by the header's tag length field,<br> *      minus the extended header. Note that this includes the padding (if<br> *      there is any), but excludes the footer. The CRC-32 is stored as an<br> *      35 bit synchsafe integer, leaving the upper four bits always<br>      * zeroed.</p> * <p/> * <p>        Flag data length       $05<br> *         Total frame CRC    5 * %0xxxxxxx</p> * <p/> * <p>   d - Tag restrictions</p> * <p/> * <p>     For some applications it might be desired to restrict a tag in more<br> *      ways than imposed by the ID3v2 specification. Note that the<br>      * presence of these restrictions does not affect how the tag is<br>      decoded, merely how it was * restricted before encoding. If this flag<br>      is set the tag is restricted as follows:</p> * <p/> * <p>        Flag data length       $01<br> *         Restrictions           * %ppqrrstt</p> * <p/> * <p>     p - Tag size restrictions</p> * <p/> * <p>       00   No more than 128 frames and 1 MB total tag size.<br> *        01   No more than 64 frames and 128 KB total tag size.<br> *        10   No more than 32 frames and 40 KB total tag size.<br> *        11   No more than 32 frames and 4 KB total tag size.</p> * <p/> * <p>     q - Text encoding restrictions</p> * <p/> * <p>       0    No restrictions<br>        * 1    Strings are only encoded with ISO-8859-1 [ISO-8859-1] or<br> *             UTF-8 [UTF-8].</p> * <p/> * <p>     r - Text fields size restrictions</p> * <p/> * <p>       00   No restrictions<br>        * 01   No string is longer than 1024 characters.<br>        10   No * string is longer than 128 characters.<br>        11   No string is longer * than 30 characters.</p> * <p/> * <p>       Note that nothing is said about how many bytes is used to<br> *        represent those characters, since it is encoding dependent. If a<br> *        text frame consists of more than one string, the sum of the<br> *        strungs is restricted as stated.</p> * <p/> * <p>     s - Image encoding restrictions</p> * <p/> * <p>       0   No restrictions<br>        * 1   Images are encoded only with PNG [PNG] or JPEG [JFIF].</p> * <p/> * <p>     t - Image size restrictions</p> * <p/> * <p>       00  No restrictions<br>        01  * All images are 256x256 pixels or smaller.<br>        10  All images are 64x64 * pixels or smaller.<br>        11  All images are exactly 64x64 pixels, unless * required<br>            otherwise.<br> </p> <a name="sec3.3"></a> * <p/> * <h3>3.3.   Padding</h3> * <p/> * <p>   It is OPTIONAL to include padding after the final frame (at the end<br>    of the ID3 tag), * making the size of all the frames together smaller<br>    than the size given in the tag header. A possible * purpose of this<br>    padding is to allow for adding a few additional frames or enlarge<br>    * existing frames within the tag without having to rewrite the entire<br>    file. The value of the padding * bytes must be $00. A tag MUST NOT have<br>    any padding between the frames or between the tag header and * the<br>    frames. Furthermore it MUST NOT have any padding when a tag footer is<br>    added to * the tag.<br> </p> <a name="sec3.4"></a> * <p/> * <h3>3.4.   ID3v2 footer</h3> * <p/> * <p>   To speed up the process of locating an ID3v2 tag when searching from<br>    the end of a * file, a footer can be added to the tag. It is REQUIRED<br>    to add a footer to an appended tag, i.e. a * tag located after all<br>    audio data. The footer is a copy of the header, but with a different<br> *    identifier.</p> * <p/> * <p>     ID3v2 identifier           * "3DI"<br>      ID3v2 version              * $04 00<br>      ID3v2 flags                * %abcd0000<br>      ID3v2 size             * 4 * %0xxxxxxx<br> </p> * <p/> * <p>   The default location of an ID3v2 tag is prepended to the audio so<br>    that players can * benefit from the information when the data is<br>    streamed. It is however possible to append the tag, or * make a<br>    prepend/append combination. When deciding upon where an unembedded<br>    tag * should be located, the following order of preference SHOULD be<br>    considered.<br>    </p> * <p/> * <p>     1. Prepend the tag.</p> * <p/> * <p>     2. Prepend a tag with all vital information and add a second tag at <br> *         the end of the file, before tags from other tagging systems. The<br> *         first tag is required to have a SEEK frame.<br> *       </p> * <p/> * <p>     3. Add a tag at the end of the file, before tags from other tagging<br> *         systems.<br>       </p> * <p/> * <p>   In case 2 and 3 the tag can simply be appended if no other known tags<br>    are present. * The suggested method to find ID3v2 tags are:<br>    </p> * <p/> * <p>     1. Look for a prepended tag using the pattern found in section 3.1.</p> * <p/> * <p>     2. If a SEEK frame was found, use its values to guide further<br> *         searching.</p> * <p/> * <p>     3. Look for a tag footer, scanning from the back of the file.</p> * <p/> * <p>   For every new tag that is found, the old tag should be discarded<br>    unless the update * flag in the extended header (section 3.2) is set.<br>    <br> </p> <a name="sec6"></a> * <p/> * <h3>6.   Unsynchronisation</h3> * <p/> * <p>   The only purpose of unsynchronisation is to make the ID3v2 tag as<br>    compatible as * possible with existing software and hardware. There is<br>    no use in 'unsynchronising' tags if the file * is only to be processed<br>    only by ID3v2 aware software and hardware. Unsynchronisation is only<br> *    useful with tags in MPEG 1/2 layer I, II and III, MPEG 2.5 and AAC<br>    files.<br> </p> * * @author Eric Farng * @version $Revision: 2374 $ */ public class ID3v2_4 extends ID3v2_3 { protected boolean footer = false; protected boolean tagRestriction = false; protected boolean updateTag = false; protected byte imageEncodingRestriction = 0; protected byte imageSizeRestriction = 0; protected byte tagSizeRestriction = 0; protected byte textEncodingRestriction = 0; protected byte textFieldSizeRestriction = 0; /** * Creates a new ID3v2_4 object. */ public ID3v2_4() { setMajorVersion((byte) 2); setRevision((byte) 4); } /** * Creates a new ID3v2_4 object. */ public ID3v2_4(final ID3v2_4 copyObject) { super(copyObject); this.footer = copyObject.footer; this.tagRestriction = copyObject.tagRestriction; this.updateTag = copyObject.updateTag; this.imageEncodingRestriction = copyObject.imageEncodingRestriction; this.imageSizeRestriction = copyObject.imageSizeRestriction; this.tagSizeRestriction = copyObject.tagSizeRestriction; this.textEncodingRestriction = copyObject.textEncodingRestriction; this.textFieldSizeRestriction = copyObject.textFieldSizeRestriction; } /** * Creates a new ID3v2_4 object. */ public ID3v2_4(final AbstractMP3Tag mp3tag) { if (mp3tag != null) { // if we get a tag, we want to convert to id3v2_4 // both id3v1 and lyrics3 convert to this type // id3v1 needs to convert to id3v2_4 before converting to lyrics3 if (mp3tag instanceof AbstractID3v2) { copyFromID3v2Tag((AbstractID3v2) mp3tag); } else if (mp3tag instanceof ID3v1) { // convert id3v1 tags. final ID3v1 id3tag = (ID3v1) mp3tag; ID3v2_4Frame newFrame; AbstractID3v2FrameBody newBody; if (id3tag.title.length() > 0) { newBody = new FrameBodyTIT2((byte) 0, id3tag.title); newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody); this.setFrame(newFrame); } if (id3tag.artist.length() > 0) { newBody = new FrameBodyTPE1((byte) 0, id3tag.artist); newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody); this.setFrame(newFrame); } if (id3tag.album.length() > 0) { newBody = new FrameBodyTALB((byte) 0, id3tag.album); newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody); this.setFrame(newFrame); } if (id3tag.year.length() > 0) { newBody = new FrameBodyTDRC((byte) 0, id3tag.year); newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody); this.setFrame(newFrame); } if (id3tag.comment.length() > 0) { newBody = new FrameBodyCOMM((byte) 0, "ENG", "", id3tag.comment); newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody); this.setFrame(newFrame); } if (id3tag.genre >= 0) { final String genre = "(" + Byte.toString(id3tag.genre) + ") " + TagConstant.genreIdToString.get(new Long(id3tag.genre)); newBody = new FrameBodyTCON((byte) 0, genre); newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody); this.setFrame(newFrame); } if (mp3tag instanceof ID3v1_1) { final ID3v1_1 id3tag2 = (ID3v1_1) mp3tag; if (id3tag2.track > 0) { newBody = new FrameBodyTRCK((byte) 0, Byte.toString(id3tag2.track)); newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody); this.setFrame(newFrame); } } } else if (mp3tag instanceof AbstractLyrics3) { // put the conversion stuff in the individual frame code. final Lyrics3v2 lyric; if (mp3tag instanceof Lyrics3v2) { lyric = new Lyrics3v2((Lyrics3v2) mp3tag); } else { lyric = new Lyrics3v2(mp3tag); } final Iterator iterator = lyric.iterator(); Lyrics3v2Field field; ID3v2_4Frame newFrame; while (iterator.hasNext()) { try { field = (Lyrics3v2Field) iterator.next(); newFrame = new ID3v2_4Frame(field); this.setFrame(newFrame); } catch (InvalidTagException ex) { } } } else if (mp3tag instanceof FilenameTag) { copyFromID3v2Tag(((FilenameTag) mp3tag).getId3tag()); } } } /** * Creates a new ID3v2_4 object. */ public ID3v2_4(final RandomAccessFile file) throws TagException, IOException { this.read(file); } public String getIdentifier() { return "ID3v2.40"; } public int getSize() { int size = 3 + 2 + 1 + 4; if (this.extended) { size += (4 + 1 + 1); if (this.updateTag) { size++; } if (this.crcDataFlag) { size += 5; } if (this.tagRestriction) { size += 2; } } 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_4) { this.updateTag = ((ID3v2_4) tag).updateTag; this.footer = ((ID3v2_4) tag).footer; this.tagRestriction = ((ID3v2_4) tag).tagRestriction; this.tagSizeRestriction = ((ID3v2_4) tag).tagSizeRestriction; this.textEncodingRestriction = ((ID3v2_4) tag).textEncodingRestriction; this.textFieldSizeRestriction = ((ID3v2_4) tag).textFieldSizeRestriction; this.imageEncodingRestriction = ((ID3v2_4) tag).imageEncodingRestriction; this.imageSizeRestriction = ((ID3v2_4) tag).imageSizeRestriction; } super.append(tag); } public boolean equals(final Object obj) { if ((obj instanceof ID3v2_4) == false) { return false; } final ID3v2_4 id3v2_4 = (ID3v2_4) obj; if (this.footer != id3v2_4.footer) { return false; } if (this.imageEncodingRestriction != id3v2_4.imageEncodingRestriction) { return false; } if (this.imageSizeRestriction != id3v2_4.imageSizeRestriction) { return false; } if (this.tagRestriction != id3v2_4.tagRestriction) { return false; } if (this.tagSizeRestriction != id3v2_4.tagSizeRestriction) { return false; } if (this.textEncodingRestriction != id3v2_4.textEncodingRestriction) { return false; } if (this.textFieldSizeRestriction != id3v2_4.textFieldSizeRestriction) { return false; } if (this.updateTag != id3v2_4.updateTag) { return false; } return super.equals(obj); } public void overwrite(final AbstractMP3Tag tag) { if (tag instanceof ID3v2_4) { this.updateTag = ((ID3v2_4) tag).updateTag; this.footer = ((ID3v2_4) tag).footer; this.tagRestriction = ((ID3v2_4) tag).tagRestriction; this.tagSizeRestriction = ((ID3v2_4) tag).tagSizeRestriction; this.textEncodingRestriction = ((ID3v2_4) tag).textEncodingRestriction; this.textFieldSizeRestriction = ((ID3v2_4) tag).textFieldSizeRestriction; this.imageEncodingRestriction = ((ID3v2_4) tag).imageEncodingRestriction; this.imageSizeRestriction = ((ID3v2_4) tag).imageSizeRestriction; } super.overwrite(tag); } public void read(final RandomAccessFile file) throws TagException, IOException { final int size; byte[] buffer = new byte[4]; file.seek(0); if (seek(file) == false) { throw new TagNotFoundException(getIdentifier() + " tag not found"); } // read the major and minor @version bytes & flag bytes file.read(buffer, 0, 3); if ((buffer[0] != 4) || (buffer[1] != 0)) { throw new TagNotFoundException(getIdentifier() + " tag not found"); } setMajorVersion(buffer[0]); setRevision(buffer[1]); this.unsynchronization = (buffer[2] & TagConstant.MASK_V24_UNSYNCHRONIZATION) != 0; this.extended = (buffer[2] & TagConstant.MASK_V24_EXTENDED_HEADER) != 0; this.experimental = (buffer[2] & TagConstant.MASK_V24_EXPERIMENTAL) != 0; this.footer = (buffer[2] & TagConstant.MASK_V24_FOOTER_PRESENT) != 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 must be atleast 6 bytes if (extendedHeaderSize <= 6) { throw new InvalidTagException("Invalid Extended Header Size."); } final byte numberOfFlagBytes = file.readByte(); // read the flag bytes file.read(buffer, 0, numberOfFlagBytes); this.updateTag = (buffer[0] & TagConstant.MASK_V24_TAG_UPDATE) != 0; this.crcDataFlag = (buffer[0] & TagConstant.MASK_V24_CRC_DATA_PRESENT) != 0; this.tagRestriction = (buffer[0] & TagConstant.MASK_V24_TAG_RESTRICTIONS) != 0; // read the length byte if the flag is set // this tag should always be zero but just in case // read this information. if (this.updateTag) { final int len = file.readByte(); buffer = new byte[len]; file.read(buffer, 0, len); } if (this.crcDataFlag) { // the CRC has a variable length final int len = file.readByte(); buffer = new byte[len]; file.read(buffer, 0, len); this.crcData = 0; for (int i = 0; i < len; i++) { this.crcData <<= 8; this.crcData += buffer[i]; } } if (this.tagRestriction) { final int len = file.readByte(); buffer = new byte[len]; file.read(buffer, 0, len); this.tagSizeRestriction = (byte) ((buffer[0] & TagConstant.MASK_V24_TAG_SIZE_RESTRICTIONS) >> 6); this.textEncodingRestriction = (byte) ((buffer[0] & TagConstant.MASK_V24_TEXT_ENCODING_RESTRICTIONS) >> 5); this.textFieldSizeRestriction = (byte) ((buffer[0] & TagConstant .MASK_V24_TEXT_FIELD_SIZE_RESTRICTIONS) >> 3); this.imageEncodingRestriction = (byte) ((buffer[0] & TagConstant.MASK_V24_IMAGE_ENCODING) >> 2); this.imageSizeRestriction = (byte) (buffer[0] & TagConstant.MASK_V24_IMAGE_SIZE_RESTRICTIONS); } } ID3v2_4Frame next; this.clearFrameMap(); // read the frames this.setFileReadBytes(size); resetPaddingCounter(); while ((file.getFilePointer() - filePointer) <= size) { try { next = new ID3v2_4Frame(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()); /** * int newSize = this.getSize(); if ((this.padding + newSize - 10) != * size) { System.out.println("WARNING: Tag sizes don't add up"); * System.out.println("ID3v2.40 tag size : " + newSize); * System.out.println("ID3v2.40 padding : " + this.padding); * System.out.println("ID3v2.40 total : " + (this.padding + newSize)); * System.out.println("ID3v2.40 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] == 4) && (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"); str += ("footer = " + this.footer + "\n"); str += ("imageEncodingRestriction = " + this.imageEncodingRestriction + "\n"); str += ("imageSizeRestriction = " + this.imageSizeRestriction + "\n"); str += ("tagRestriction = " + this.tagRestriction + "\n"); str += ("tagSizeRestriction = " + this.tagSizeRestriction + "\n"); str += ("textEncodingRestriction = " + this.textEncodingRestriction + "\n"); str += ("textFieldSizeRestriction = " + this.textFieldSizeRestriction + "\n"); str += ("updateTag = " + this.updateTag + "\n"); while (iterator.hasNext()) { frame = (ID3v2_4Frame) iterator.next(); str += (frame.toString() + "\n"); } return str + "\n"; } public void write(final AbstractMP3Tag tag) { if (tag instanceof ID3v2_4) { this.updateTag = ((ID3v2_4) tag).updateTag; this.footer = ((ID3v2_4) tag).footer; this.tagRestriction = ((ID3v2_4) tag).tagRestriction; this.tagSizeRestriction = ((ID3v2_4) tag).tagSizeRestriction; this.textEncodingRestriction = ((ID3v2_4) tag).textEncodingRestriction; this.textFieldSizeRestriction = ((ID3v2_4) tag).textFieldSizeRestriction; this.imageEncodingRestriction = ((ID3v2_4) tag).imageEncodingRestriction; this.imageSizeRestriction = ((ID3v2_4) tag).imageSizeRestriction; } super.write(tag); } public void write(final RandomAccessFile file) throws IOException { int size; final String str; final Iterator iterator; ID3v2_4Frame frame; final byte[] buffer = new byte[6]; final MP3File mp3 = new MP3File(); mp3.seekMP3Frame(file); final long mp3start = file.getFilePointer(); file.seek(0); str = "ID3"; for (int i = 0; i < str.length(); i++) { buffer[i] = (byte) str.charAt(i); } buffer[3] = 4; buffer[4] = 0; if (this.unsynchronization) { buffer[5] |= TagConstant.MASK_V24_UNSYNCHRONIZATION; } if (this.extended) { buffer[5] |= TagConstant.MASK_V24_EXTENDED_HEADER; } if (this.experimental) { buffer[5] |= TagConstant.MASK_V24_EXPERIMENTAL; } if (this.footer) { buffer[5] |= TagConstant.MASK_V24_FOOTER_PRESENT; } file.write(buffer); // write size file.write(sizeToByteArray((int) mp3start - 10)); if (this.extended) { size = 6; if (this.updateTag) { size++; } if (this.crcDataFlag) { size += 5; } if (this.tagRestriction) { size += 2; } file.writeInt(size); file.writeByte(1); // always 1 byte of flags in this tag buffer[0] = 0; if (this.updateTag) { buffer[0] |= TagConstant.MASK_V24_TAG_UPDATE; } if (this.crcDataFlag) { buffer[0] |= TagConstant.MASK_V24_CRC_DATA_PRESENT; } if (this.tagRestriction) { buffer[0] |= TagConstant.MASK_V24_TAG_RESTRICTIONS; } file.writeByte(buffer[0]); if (this.updateTag) { file.writeByte(0); } // this can be variable length, but this is easier if (this.crcDataFlag) { file.writeByte(4); file.writeInt(this.crcData); } if (this.tagRestriction) { // todo we need to finish this file.writeByte(1); buffer[0] = (byte) 0; if (this.tagRestriction) { buffer[0] |= TagConstant.MASK_V24_TAG_SIZE_RESTRICTIONS; } file.writeByte(this.tagSizeRestriction); file.writeByte(this.textEncodingRestriction); file.writeByte(this.textFieldSizeRestriction); file.writeByte(this.imageEncodingRestriction); file.writeByte(this.imageSizeRestriction); file.writeByte(buffer[0]); } } // write all frames iterator = this.getFrameIterator(); while (iterator.hasNext()) { frame = (ID3v2_4Frame) iterator.next(); frame.write(file); } } private void copyFromID3v2Tag(final AbstractID3v2 mp3tag) { // if the tag is id3v2_4 if (mp3tag instanceof ID3v2_4) { final ID3v2_4 tag = (ID3v2_4) mp3tag; this.footer = tag.footer; this.tagRestriction = tag.tagRestriction; this.updateTag = tag.updateTag; this.imageEncodingRestriction = tag.imageEncodingRestriction; this.imageSizeRestriction = tag.imageSizeRestriction; this.tagSizeRestriction = tag.tagSizeRestriction; this.textEncodingRestriction = tag.textEncodingRestriction; this.textFieldSizeRestriction = tag.textFieldSizeRestriction; } if (mp3tag instanceof ID3v2_3) { // and id3v2_4 tag is an instance of id3v2_3 also ... final ID3v2_3 id3tag = (ID3v2_3) mp3tag; this.extended = id3tag.extended; this.experimental = id3tag.experimental; this.crcDataFlag = id3tag.crcDataFlag; this.crcData = id3tag.crcData; this.paddingSize = id3tag.paddingSize; } if (mp3tag instanceof ID3v2_2) { final ID3v2_2 id3tag = (ID3v2_2) mp3tag; this.compression = id3tag.compression; this.unsynchronization = id3tag.unsynchronization; } final AbstractID3v2 id3tag = mp3tag; final Iterator iterator = id3tag.getFrameIterator(); AbstractID3v2Frame frame; ID3v2_4Frame newFrame; while (iterator.hasNext()) { frame = (AbstractID3v2Frame) iterator.next(); newFrame = new ID3v2_4Frame(frame); this.setFrame(newFrame); } } public String getYearReleased() { String text = ""; AbstractID3v2Frame frame = getFrame("TDRC"); if (frame != null) { FrameBodyTDRC body = (FrameBodyTDRC) frame.getBody(); text = body.getText(); } return text.trim(); } public void setYearReleased(String yearReleased) { AbstractID3v2Frame field = getFrame("TDRC"); if (field == null) { field = new ID3v2_3Frame(new FrameBodyTDRC((byte) 0, yearReleased)); setFrame(field); } else { ((FrameBodyTDRC) field.getBody()).setText(yearReleased); } } }