/*
* Entagged Audio Tag library
* Copyright (c) 2003-2005 Rapha?l Slinckx <raphael@slinckx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package entagged.audioformats.mp3.util;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Hashtable;
import entagged.audioformats.mp3.Id3v2Tag;
import entagged.audioformats.mp3.util.id3frames.ApicId3Frame;
import entagged.audioformats.mp3.util.id3frames.CommId3Frame;
import entagged.audioformats.mp3.util.id3frames.GenericId3Frame;
import entagged.audioformats.mp3.util.id3frames.Id3Frame;
import entagged.audioformats.mp3.util.id3frames.TextId3Frame;
import entagged.audioformats.mp3.util.id3frames.TimeId3Frame;
import entagged.audioformats.mp3.util.id3frames.UfidId3Frame;
/**
* This class parses an ID3V2 tag from a given {@link java.nio.ByteBuffer}.<br>
* It handles the versions 2,3 and 4.
*
* @author Rapha?l Slinckx , Christian Laireiter
*/
public class Id3v24TagReader {
/**
* This field maps the field names of the version 2 frames to the one of
* version 3.<br>
*/
private Hashtable conversion22to23;
/**
* Creates an instance.
*
*/
public Id3v24TagReader() {
initConversionTable();
}
private String convertFromId3v22(String field) {
String s = (String) this.conversion22to23.get(field);
if (s == null)
return "";
return s;
}
private Id3Frame createId3Frame(String field, byte[] data, byte version)
throws UnsupportedEncodingException {
if (version == Id3v2Tag.ID3V22)
field = convertFromId3v22(field);
// Text frames
if (field.startsWith("T") && !field.startsWith("TX")) {
if (field.equalsIgnoreCase("TDRC")) {
return new TimeId3Frame(field, data, version);
}
return new TextId3Frame(field, data, version);
}
// Comment
else if (field.startsWith("COMM"))
return new CommId3Frame(data, version);
// Universal file id
else if (field.startsWith("UFID"))
return new UfidId3Frame(data, version);
else if (field.startsWith("APIC"))
return new ApicId3Frame(data, version);
// Any other frame
else
return new GenericId3Frame(field, data, version);
}
/**
* This Method fills {@link #conversion}.
*
*/
private void initConversionTable() {
// TODO: APIC frame must update the mime-type to be converted ??
// TODO: LINK frame (2.3) has a frame ID of 3-bytes making it
// incompatible with 2.3 frame ID of 4bytes, WTF???
this.conversion22to23 = new Hashtable(100);
String[] v22 = { "BUF", "CNT", "COM", "CRA", "CRM", "ETC", "EQU",
"GEO", "IPL", "LNK", "MCI", "MLL", "PIC", "POP", "REV", "RVA",
"SLT", "STC", "TAL", "TBP", "TCM", "TCO", "TCR", "TDA", "TDY",
"TEN", "TFT", "TIM", "TKE", "TLA", "TLE", "TMT", "TOA", "TOF",
"TOL", "TOR", "TOT", "TP1", "TP2", "TP3", "TP4", "TPA", "TPB",
"TRC", "TRD", "TRK", "TSI", "TSS", "TT1", "TT2", "TT3", "TXT",
"TXX", "TYE", "UFI", "ULT", "WAF", "WAR", "WAS", "WCM", "WCP",
"WPB", "WXX" };
String[] v23 = { "RBUF", "PCNT", "COMM", "AENC", "", "ETCO", "EQUA",
"GEOB", "IPLS", "LINK", "MCDI", "MLLT", "APIC", "POPM", "RVRB",
"RVAD", "SYLT", "SYTC", "TALB", "TBPM", "TCOM", "TCON", "TCOP",
"TDAT", "TDLY", "TENC", "TFLT", "TIME", "TKEY", "TLAN", "TLEN",
"TMED", "TOPE", "TOFN", "TOLY", "TORY", "TOAL", "TPE1", "TPE2",
"TPE3", "TPE4", "TPOS", "TPUB", "TSRC", "TRDA", "TRCK", "TSIZ",
"TSSE", "TIT1", "TIT2", "TIT3", "TEXT", "TXXX", "TYER", "UFID",
"USLT", "WOAF", "WOAR", "WOAS", "WCOM", "WCOP", "WPUB", "WXXX" };
for (int i = 0; i < v22.length; i++) {
this.conversion22to23.put(v22[i], v23[i]);
}
}
/**
* This method is used to skip the extended header in the reading process.
*
* @param data
* the buffer containing the extended header. (at current
* location)
* @param version
* the ID3V2 version {@link Id3v2Tag#ID3V22}.<br>
* @return the size of the extended Header. (skipping already performed)
*/
private int processExtendedHeader(ByteBuffer data, byte version) {
// TODO Verify that we have an syncsfe int
int extsize = 0;
byte[] exthead = new byte[4];
data.get(exthead);
if (version == Id3v2Tag.ID3V23) {
extsize = readSize(data, Id3v2Tag.ID3V23);
// The extended header size excludes those first four bytes.
data.position(data.position() + (extsize));
} else {
extsize = readSyncsafeInteger(data);
data.position(data.position() + (extsize));
}
return extsize;
}
/**
* This method reads an ID3V2 tag from the given {@link ByteBuffer} at its
* curren pointer location.<br>
*
* @param data
* ID3V2 tag.
* @param ID3Flags
* The flags of the tag header.
* @param version
* Version Flag. (used to handle some version specific
* implementations).
* @return An ID3V2 tag representation.
* @throws UnsupportedEncodingException
* Thrown on charset conversions, if system does not support
* them.
*/
public Id3v2Tag read(ByteBuffer data, boolean[] ID3Flags, byte version)
throws UnsupportedEncodingException {
// get the tagsize from the buffers size.
int tagSize = data.limit();
byte[] b;
// Create a result object
Id3v2Tag tag = new Id3v2Tag();
// ---------------------------------------------------------------------
// If the flags indicate an extended header to be present, read its
// size and skip it. (It does not contain any useful information, maybe
// CRC)
if ((version == Id3v2Tag.ID3V23 || version == Id3v2Tag.ID3V24)
&& ID3Flags[1]) {
processExtendedHeader(data, version);
}
// ---------------------------------------------------------------------
/*
* Now start the extraction of the text frames.
*/
// The frame names differ in lengths between version 2 to 3
int specSize = (version == Id3v2Tag.ID3V22) ? 3 : 4;
// As long as we have unread bytes...
for (int a = 0; a < tagSize; a++) {
// Create buffer taking the name of the frame.
b = new byte[specSize];
// Do we still have enough bytes for reading the name?
if (data.remaining() <= specSize)
break;
// Read the Name
data.get(b);
// Convert the bytes (of the name) into a String.
String field = new String(b);
// If byte[0] is zero, we have invalid data
if (b[0] == 0)
break;
// Now we read the length of the current frame
int frameSize = readSize(data, version);
// If the framesize is greater than the bytes we've left to read,
// or the frame length is zero, abort. Invalid data
if ((frameSize > data.remaining()) || frameSize <= 0) {
// ignore empty frames
System.err.println(field
+ " Frame size error, skiping the rest of the tag:"
+ frameSize);
break;
}
b = new byte[frameSize
+ ((version == Id3v2Tag.ID3V23 || version == Id3v2Tag.ID3V24) ? 2
: 0)];
// Read the complete frame into the byte array.
data.get(b);
// Check the frame name once more
if (!"".equals(field)) {
Id3Frame f = null;
/*
* Now catch possible errors occuring in the data
* interpretation. Even if a frame is not valid regarding the
* spec, the rest of the tag could be read.
*/
try {
// Create the Frame upon the byte array data.
f = createId3Frame(field, b, version);
} catch (UnsupportedEncodingException uee) {
throw uee;
} catch (Exception e) {
e.printStackTrace();
}
// If the frame was successfully parsed, add it to the tag.
if (f != null)
tag.add(f);
}
}
return tag;
}
/**
* This mehtod reads the data of an integer out of the given buffer.<br>
* Since different version of ID3V2 tags have a different size definiton,
* the version is needed.
*
* @param bb
* The buffer containing the integer.
* @param version
* The ID3V2 version. {@link Id3v2Tag#ID3V22}.
* @return The integer value
*/
private int readSize(ByteBuffer bb, int version) {
int value = 0;
if (version == Id3v2Tag.ID3V24) {
value = readSyncsafeInteger(bb);
} else {
if (version == Id3v2Tag.ID3V23)
value += (bb.get() & 0xFF) << 24;
value += (bb.get() & 0xFF) << 16;
value += (bb.get() & 0xFF) << 8;
value += (bb.get() & 0xFF);
}
return value;
}
/**
* This method reads the next 4 byte of the buffer and interprets them as a
* sync safe integer.<br>
*
* @param buffer
* Buffer to read from
* @return represented integer value.
*/
private int readSyncsafeInteger(ByteBuffer buffer) {
int value = 0;
value += (buffer.get() & 0xFF) << 21;
value += (buffer.get() & 0xFF) << 14;
value += (buffer.get() & 0xFF) << 7;
value += buffer.get() & 0xFF;
return value;
}
}