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());
}
}
}