package org.farng.mp3.id3; import java.io.IOException; import java.io.RandomAccessFile; import java.util.Iterator; import org.farng.mp3.InvalidTagException; import org.farng.mp3.TagConstant; import org.farng.mp3.TagUtility; import org.farng.mp3.lyrics3.FieldBodyAUT; import org.farng.mp3.lyrics3.FieldBodyEAL; import org.farng.mp3.lyrics3.FieldBodyEAR; import org.farng.mp3.lyrics3.FieldBodyETT; import org.farng.mp3.lyrics3.FieldBodyINF; import org.farng.mp3.lyrics3.FieldBodyLYR; import org.farng.mp3.lyrics3.Lyrics3v2Field; import org.farng.mp3.object.ObjectLyrics3Line; /** * <p>   All ID3v2 frames consists of one frame header followed by one or more<br>    fields * containing the actual information. The header is always 10<br>    bytes and laid out as follows:</p> * <p/> * <p>     Frame ID      $xx xx xx xx  (four characters)<br> *      Size      4 * %0xxxxxxx<br>      * Flags         $xx xx</p> * <p/> * <p>   The frame ID is made out of the characters capital A-Z and 0-9.<br>    Identifiers * beginning with "X", "Y" and "Z" are for experimental<br>    frames and free * for everyone to use, without the need to set the<br>    experimental bit in the tag header. Bear in mind * that someone else<br>    might have used the same identifier as you. All other identifiers are<br> *    either used or reserved for future use.</p> * <p/> * <p>   The frame ID is followed by a size descriptor containing the size of<br>    the data in the * final frame, after encryption, compression and<br>    unsynchronisation. The size is excluding the frame * header ('total<br>    frame size' - 10 bytes) and stored as a 32 bit synchsafe integer.</p> * <p/> * <p>   In the frame header the size descriptor is followed by two flag<br>    bytes. These flags * are described in section 4.1.</p> * <p/> * <p>   There is no fixed order of the frames' appearance in the tag,<br>    although it is desired * that the frames are arranged in order of<br>    significance concerning the recognition of the file. An * example of<br>    such order: UFID, TIT2, MCDI, TRCK ...</p> * <p/> * <p>   A tag MUST contain at least one frame. A frame must be at least 1<br>    byte big, * excluding the header.</p> * <p/> * <p>   If nothing else is said, strings, including numeric strings and URLs<br>    [URL], are * represented as ISO-8859-1 [ISO-8859-1] characters in the<br>    range $20 - $FF. Such strings are * represented in frame descriptions<br>    as <text string>, or <full text string> if newlines * are allowed. If<br>    nothing else is said newline character is forbidden. In ISO-8859-1 a<br> *    newline is represented, when allowed, with $0A only.</p> * <p/> * <p>   Frames that allow different types of text encoding contains a text<br>    encoding * description byte. Possible encodings:</p> * <p/> * <p>     $00   ISO-8859-1 [ISO-8859-1]. Terminated with $00.<br> *      $01   UTF-16 [UTF-16] encoded Unicode [UNICODE] with BOM. All<br> *            strings in the same frame SHALL have the same * byteorder.<br>            Terminated with $00 00.<br> *      $02   UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM.<br> *            Terminated with $00 00.<br>      * $03   UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00.</p> * <p/> * <p>   Strings dependent on encoding are represented in frame descriptions<br>    as <text * string according to encoding>, or <full text string<br>    according to encoding> if newlines are * allowed. Any empty strings of<br>    type $01 which are NULL-terminated may have the Unicode BOM * followed<br>    by a Unicode NULL ($FF FE 00 00 or $FE FF 00 00).</p> * <p/> * <p>   The timestamp fields are based on a subset of ISO 8601. When being as<br>    precise as * possible the format of a time string is<br>    yyyy-MM-ddTHH:mm:ss (year, "-", month, * "-", day, "T", hour (out of<br>    24), ":", minutes, ":", * seconds), but the precision may be reduced by<br>    removing as many time indicators as wanted. Hence * valid timestamps<br>    are<br>    yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddTHH, yyyy-MM-ddTHH:mm * and<br>    yyyy-MM-ddTHH:mm:ss. All time stamps are UTC. For durations, use<br>    the slash * character as described in 8601, and for multiple non-<br>    contiguous dates, use multiple strings, if * allowed by the frame<br>    definition.</p> * <p/> * <p>   The three byte language field, present in several frames, is used to<br>    describe the * language of the frame's content, according to ISO-639-2<br>    [ISO-639-2]. The language should be * represented in lower case. If the<br>    language is not known the string "XXX" should be * used.</p> * <p/> * <p>   All URLs [URL] MAY be relative, e.g. "picture.png", "../doc.txt".</p> * <p/> * <p>   If a frame is longer than it should be, e.g. having more fields than<br>    specified in * this document, that indicates that additions to the<br>    frame have been made in a later version of the * ID3v2 standard. This<br>    is reflected by the revision number in the header of the tag.<br> </p> <a * name="sec4.1"></a> * <p/> * <h3>4.1.   Frame header flags</h3> * <p/> * <p>   In the frame header the size descriptor is followed by two flag<br>    bytes. All unused * flags MUST be cleared. The first byte is for<br>    'status messages' and the second byte is a format * description. If an<br>    unknown flag is set in the first byte the frame MUST NOT be changed<br> *    without that bit cleared. If an unknown flag is set in the second<br>    byte the frame is * likely to not be readable. Some flags in the second<br>    byte indicates that extra information is added * to the header. These<br>    fields of extra information is ordered as the flags that indicates<br> *    them. The flags field is defined as follows (l and o left out because<br>    ther resemblence * to one and zero):</p> * <p/> * <p>     %0abc0000 %0h00kmnp</p> * <p/> * <p>   Some frame format flags indicate that additional information fields<br>    are added to the * frame. This information is added after the frame<br>    header and before the frame data in the same order * as the flags that<br>    indicates them. I.e. the four bytes of decompressed size will precede<br> *    the encryption method byte. These additions affects the 'frame size'<br>    field, but are not * subject to encryption or compression.<br>    </p> * <p/> * <p>   The default status flags setting for a frame is, unless stated<br>    otherwise, 'preserved * if tag is altered' and 'preserved if file is<br>    altered', i.e. %00000000.<br> </p> <a * name="sec4.1.1"></a> * <p/> * <h3>4.1.1. Frame status flags</h3> * <p/> * <p>   a - Tag alter preservation</p> * <p/> * <p>     This flag tells the tag parser what to do with this frame if it is<br> *      unknown and the tag is altered in any way. This applies to all<br>      * kinds of alterations, including adding more padding and reordering<br>      the frames.</p> * <p/> * <p>     0     Frame should be preserved.<br>      * 1     Frame should be discarded.<br> </p> * <p/> * <p>   b - File alter preservation</p> * <p/> * <p>     This flag tells the tag parser what to do with this frame if it is<br> *      unknown and the file, excluding the tag, is altered. This does not<br> *      apply when the audio is completely replaced with other audio data.</p> * <p/> * <p>     0     Frame should be preserved.<br>      * 1     Frame should be discarded.<br> </p> * <p/> * <p>   c - Read only</p> * <p/> * <p>      This flag, if set, tells the software that the contents of this<br> *       frame are intended to be read only. Changing the contents might<br> *       break something, e.g. a signature. If the contents are changed,<br> *       without knowledge of why the frame was flagged read only and<br> *       without taking the proper means to compensate, e.g. recalculating<br> *       the signature, the bit MUST be cleared.<br> </p> <a name="sec4.1.2"></a> * <p/> * <h3>4.1.2. Frame format flags</h3> * <p/> * <p>   h - Grouping identity</p> * <p/> * <p>      This flag indicates whether or not this frame belongs in a group<br> *       with other frames. If set, a group identifier byte is added to the<br> *       frame. Every frame with the same group identifier belongs to the<br> *       same group.</p> * <p/> * <p>      0     Frame does not contain group information<br> *       1     Frame contains group information<br> </p> * <p/> * <p>   k - Compression</p> * <p/> * <p>      This flag indicates whether or not the frame is compressed.<br> *       A 'Data Length Indicator' byte MUST be included in the frame.</p> * <p/> * <p>      0     Frame is not compressed.<br> *       1     Frame is compressed using zlib [zlib] deflate method.<br> *             If set, this requires the 'Data Length Indicator' * bit<br>             to be set as well.<br> </p> * <p/> * <p>   m - Encryption<br>    </p> * <p/> * <p>      This flag indicates whether or not the frame is encrypted. If set,<br> *       one byte indicating with which method it was encrypted will be<br> *       added to the frame. See description of the ENCR frame for more<br> *       information about encryption method registration. Encryption<br> *       should be done after compression. Whether or not setting this flag<br> *       requires the presence of a 'Data Length Indicator' depends on the<br> *       specific algorithm used.</p> * <p/> * <p>      0     Frame is not encrypted.<br> *       1     Frame is encrypted.</p> * <p/> * <p>   n - Unsynchronisation</p> * <p/> * <p>      This flag indicates whether or not unsynchronisation was applied<br> *       to this frame. See section 6 for details on unsynchronisation.<br> *       If this flag is set all data from the end of this header to the<br> *       end of this frame has been unsynchronised. Although desirable, the<br> *       presence of a 'Data Length Indicator' is not made mandatory by<br> *       unsynchronisation.</p> * <p/> * <p>      0     Frame has not been unsynchronised.<br> *       1     Frame has been unsyrchronised.</p> * <p/> * <p>   p - Data length indicator</p> * <p/> * <p>      This flag indicates that a data length indicator has been added to<br> *       the frame. The data length indicator is the value one would write<br> *       as the 'Frame length' if all of the frame format flags were<br> *       zeroed, represented as a 32 bit synchsafe integer.</p> * <p/> * <p>      0      There is no Data Length Indicator.<br> *       1      A data length Indicator has been added to the * frame.<br> </p> * * @author Eric Farng * @version $Revision: 2374 $ */ public class ID3v2_4Frame extends ID3v2_3Frame { protected boolean dataLengthIndicator = false; protected boolean unsynchronization = false; /** * Creates a new ID3v2_4Frame object. */ public ID3v2_4Frame() { // base empty constructor } /** * Creates a new ID3v2_4Frame object. */ public ID3v2_4Frame(final ID3v2_4Frame copyObject) { super(copyObject); this.dataLengthIndicator = copyObject.dataLengthIndicator; this.unsynchronization = copyObject.unsynchronization; } /** * Creates a new ID3v2_4Frame object. */ public ID3v2_4Frame(final AbstractID3v2FrameBody body) { super(body); } /** * Creates a new ID3v2_4Frame object. */ public ID3v2_4Frame(final AbstractID3v2Frame frame) { if (frame instanceof ID3v2_4Frame) { final ID3v2_4Frame f = (ID3v2_4Frame) frame; this.unsynchronization = f.unsynchronization; this.dataLengthIndicator = f.dataLengthIndicator; } if (frame instanceof ID3v2_3Frame) { // a id3v2_4 frame is of type id3v2_3 frame also ... final ID3v2_3Frame f = (ID3v2_3Frame) frame; this.tagAlterPreservation = f.tagAlterPreservation; this.fileAlterPreservation = f.fileAlterPreservation; this.readOnly = f.readOnly; this.groupingIdentity = f.groupingIdentity; this.compression = f.compression; this.encryption = f.encryption; } if (frame instanceof ID3v2_2Frame) { // no variables yet } if (frame.getBody() == null) { // do nothing } else if (TagUtility.isID3v2_4FrameIdentifier(frame.getIdentifier())) { this.setBody((AbstractID3v2FrameBody) TagUtility.copyObject(frame.getBody())); // } else if (TagUtility.isID3v2_3FrameIdentifier(frame.getIdentifier())) { // // @TODO correctly convert tags // this.setBody((AbstractID3v2FrameBody) TagUtility.copyObject(frame.getBody())); // } else if (TagUtility.isID3v2_2FrameIdentifier(frame.getIdentifier())) { // // @TODO correctly convert tags // this.setBody((AbstractID3v2FrameBody) TagUtility.copyObject(frame.getBody())); } } /** * Creates a new ID3v2_4Frame object. */ public ID3v2_4Frame(final boolean readOnly, final boolean groupingIdentity, final boolean compression, final boolean encryption, final boolean unsynchronization, final boolean dataLengthIndicator, final AbstractID3v2FrameBody body) { super(body); this.readOnly = readOnly; this.groupingIdentity = groupingIdentity; this.compression = compression; this.encryption = encryption; this.unsynchronization = unsynchronization; this.dataLengthIndicator = dataLengthIndicator; } /** * Creates a new ID3v2_4Frame object. */ public ID3v2_4Frame(final Lyrics3v2Field field) throws InvalidTagException { final String id = field.getIdentifier(); final String value; if (id.equals("IND")) { throw new InvalidTagException("Cannot create ID3v2.40 frame from Lyrics3 indications field."); } else if (id.equals("LYR")) { final FieldBodyLYR lyric = (FieldBodyLYR) field.getBody(); ObjectLyrics3Line line; final Iterator iterator = lyric.iterator(); final FrameBodySYLT sync; final FrameBodyUSLT unsync; final boolean hasTimeStamp = lyric.hasTimeStamp(); // we'll create only one frame here. // if there is any timestamp at all, we will create a sync'ed frame. sync = new FrameBodySYLT((byte) 0, "ENG", (byte) 2, (byte) 1, ""); unsync = new FrameBodyUSLT((byte) 0, "ENG", "", ""); while (iterator.hasNext()) { line = (ObjectLyrics3Line) iterator.next(); if (hasTimeStamp) { sync.addLyric(line); } else { unsync.addLyric(line); } } if (hasTimeStamp) { this.setBody(sync); } else { this.setBody(unsync); } } else if (id.equals("INF")) { value = ((FieldBodyINF) field.getBody()).getAdditionalInformation(); this.setBody(new FrameBodyCOMM((byte) 0, "ENG", "", value)); } else if (id.equals("AUT")) { value = ((FieldBodyAUT) field.getBody()).getAuthor(); this.setBody(new FrameBodyTCOM((byte) 0, value)); } else if (id.equals("EAL")) { value = ((FieldBodyEAL) field.getBody()).getAlbum(); this.setBody(new FrameBodyTALB((byte) 0, value)); } else if (id.equals("EAR")) { value = ((FieldBodyEAR) field.getBody()).getArtist(); this.setBody(new FrameBodyTPE1((byte) 0, value)); } else if (id.equals("ETT")) { value = ((FieldBodyETT) field.getBody()).getTitle(); this.setBody(new FrameBodyTIT2((byte) 0, value)); } else if (id.equals("IMG")) { throw new InvalidTagException("Cannot create ID3v2.40 frame from Lyrics3 image field."); } else { throw new InvalidTagException("Cannot caret ID3v2.40 frame from " + id + " Lyrics3 field"); } } /** * Creates a new ID3v2_4Frame object. */ public ID3v2_4Frame(final RandomAccessFile file) throws IOException, InvalidTagException { this.read(file); } public int getSize() { return this.getBody().getSize() + 4 + 2 + 4; } public boolean equals(final Object obj) { if ((obj instanceof ID3v2_4Frame) == false) { return false; } final ID3v2_4Frame id3v2_4Frame = (ID3v2_4Frame) obj; if (this.unsynchronization != id3v2_4Frame.unsynchronization) { return false; } if (this.dataLengthIndicator != id3v2_4Frame.dataLengthIndicator) { return false; } return super.equals(obj); } public void read(final RandomAccessFile file) throws IOException, InvalidTagException { long filePointer; final byte[] buffer = new byte[4]; byte b; // lets scan for a non-zero byte; do { filePointer = file.getFilePointer(); b = file.readByte(); org.farng.mp3.id3.AbstractID3v2.incrementPaddingCounter(); } while (b == 0); file.seek(filePointer); org.farng.mp3.id3.AbstractID3v2.decrementPaddingCounter(); // read the four character identifier file.read(buffer, 0, 4); final String identifier = new String(buffer, 0, 4); // is this a valid identifier? if (isValidID3v2FrameIdentifier(identifier) == false) { file.seek(file.getFilePointer() - 3); throw new InvalidTagException(identifier + " is not a valid ID3v2.40 frame"); } filePointer = file.getFilePointer(); // skip the 4 byte size file.skipBytes(4); // read the flag bytes file.read(buffer, 0, 2); this.tagAlterPreservation = (buffer[0] & TagConstant.MASK_V24_TAG_ALTER_PRESERVATION) != 0; this.fileAlterPreservation = (buffer[0] & TagConstant.MASK_V24_FILE_ALTER_PRESERVATION) != 0; this.readOnly = (buffer[0] & TagConstant.MASK_V24_READ_ONLY) != 0; this.groupingIdentity = (buffer[1] & TagConstant.MASK_V24_GROUPING_IDENTITY) != 0; this.compression = (buffer[1] & TagConstant.MASK_V24_COMPRESSION) != 0; this.encryption = (buffer[1] & TagConstant.MASK_V24_ENCRYPTION) != 0; this.unsynchronization = (buffer[1] & TagConstant.MASK_V24_FRAME_UNSYNCHRONIZATION) != 0; this.dataLengthIndicator = (buffer[1] & TagConstant.MASK_V24_DATA_LENGTH_INDICATOR) != 0; file.seek(filePointer); this.setBody(readBody(identifier, file)); } public void write(final RandomAccessFile file) throws IOException { final byte[] buffer = new byte[4]; final long filePointer; final String str = TagUtility.truncate(getIdentifier(), 4); for (int i = 0; i < str.length(); i++) { buffer[i] = (byte) str.charAt(i); } file.write(buffer, 0, str.length()); filePointer = file.getFilePointer(); // skip the size bytes file.skipBytes(4); setAlterPreservation(); buffer[0] = 0; buffer[1] = 0; if (this.tagAlterPreservation) { buffer[0] |= TagConstant.MASK_V24_TAG_ALTER_PRESERVATION; } if (this.fileAlterPreservation) { buffer[0] |= TagConstant.MASK_V24_FILE_ALTER_PRESERVATION; } if (this.readOnly) { buffer[0] |= TagConstant.MASK_V24_READ_ONLY; } if (this.groupingIdentity) { buffer[1] |= TagConstant.MASK_V24_GROUPING_IDENTITY; } if (this.compression) { buffer[1] |= TagConstant.MASK_V24_COMPRESSION; } if (this.encryption) { buffer[1] |= TagConstant.MASK_V24_ENCRYPTION; } if (this.unsynchronization) { buffer[1] |= TagConstant.MASK_V24_FRAME_UNSYNCHRONIZATION; } if (this.dataLengthIndicator) { buffer[1] |= TagConstant.MASK_V24_DATA_LENGTH_INDICATOR; } file.write(buffer, 0, 2); file.seek(filePointer); this.getBody().write(file); } }