View Javadoc

1   /*
2    * Entagged Audio Tag library
3    * Copyright (c) 2003-2005 Rapha?l Slinckx <raphael@slinckx.net>
4    * 
5    * This library is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU Lesser General Public
7    * License as published by the Free Software Foundation; either
8    * version 2.1 of the License, or (at your option) any later version.
9    *  
10   * This library is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13   * Lesser General Public License for more details.
14   * 
15   * You should have received a copy of the GNU Lesser General Public
16   * License along with this library; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18   */
19  package entagged.audioformats.mp3.util;
20  
21  import java.io.UnsupportedEncodingException;
22  import java.nio.ByteBuffer;
23  import java.util.Hashtable;
24  
25  import entagged.audioformats.mp3.Id3v2Tag;
26  import entagged.audioformats.mp3.util.id3frames.ApicId3Frame;
27  import entagged.audioformats.mp3.util.id3frames.CommId3Frame;
28  import entagged.audioformats.mp3.util.id3frames.GenericId3Frame;
29  import entagged.audioformats.mp3.util.id3frames.Id3Frame;
30  import entagged.audioformats.mp3.util.id3frames.TextId3Frame;
31  import entagged.audioformats.mp3.util.id3frames.TimeId3Frame;
32  import entagged.audioformats.mp3.util.id3frames.UfidId3Frame;
33  
34  /***
35   * This class parses an ID3V2 tag from a given {@link java.nio.ByteBuffer}.<br>
36   * It handles the versions 2,3 and 4.
37   * 
38   * @author Rapha?l Slinckx , Christian Laireiter
39   */
40  public class Id3v24TagReader {
41  
42  	/***
43  	 * This field maps the field names of the version 2 frames to the one of
44  	 * version 3.<br>
45  	 */
46  	private Hashtable conversion22to23;
47  
48  	/***
49  	 * Creates an instance.
50  	 * 
51  	 */
52  	public Id3v24TagReader() {
53  		initConversionTable();
54  	}
55  
56  	private String convertFromId3v22(String field) {
57  		String s = (String) this.conversion22to23.get(field);
58  
59  		if (s == null)
60  			return "";
61  
62  		return s;
63  	}
64  
65  	private Id3Frame createId3Frame(String field, byte[] data, byte version)
66  			throws UnsupportedEncodingException {
67  		if (version == Id3v2Tag.ID3V22)
68  			field = convertFromId3v22(field);
69  
70  		// Text frames
71  		if (field.startsWith("T") && !field.startsWith("TX")) {
72  			if (field.equalsIgnoreCase("TDRC")) {
73  				return new TimeId3Frame(field, data, version);
74  			}
75  			return new TextId3Frame(field, data, version);
76  		}
77  		// Comment
78  		else if (field.startsWith("COMM"))
79  			return new CommId3Frame(data, version);
80  		// Universal file id
81  		else if (field.startsWith("UFID"))
82  			return new UfidId3Frame(data, version);
83  		else if (field.startsWith("APIC"))
84  			return new ApicId3Frame(data, version);
85  		// Any other frame
86  		else
87  			return new GenericId3Frame(field, data, version);
88  	}
89  
90  	/***
91  	 * This Method fills {@link #conversion}.
92  	 * 
93  	 */
94  	private void initConversionTable() {
95  
96  		// TODO: APIC frame must update the mime-type to be converted ??
97  		// TODO: LINK frame (2.3) has a frame ID of 3-bytes making it
98  		// incompatible with 2.3 frame ID of 4bytes, WTF???
99  
100 		this.conversion22to23 = new Hashtable(100);
101 		String[] v22 = { "BUF", "CNT", "COM", "CRA", "CRM", "ETC", "EQU",
102 				"GEO", "IPL", "LNK", "MCI", "MLL", "PIC", "POP", "REV", "RVA",
103 				"SLT", "STC", "TAL", "TBP", "TCM", "TCO", "TCR", "TDA", "TDY",
104 				"TEN", "TFT", "TIM", "TKE", "TLA", "TLE", "TMT", "TOA", "TOF",
105 				"TOL", "TOR", "TOT", "TP1", "TP2", "TP3", "TP4", "TPA", "TPB",
106 				"TRC", "TRD", "TRK", "TSI", "TSS", "TT1", "TT2", "TT3", "TXT",
107 				"TXX", "TYE", "UFI", "ULT", "WAF", "WAR", "WAS", "WCM", "WCP",
108 				"WPB", "WXX" };
109 		String[] v23 = { "RBUF", "PCNT", "COMM", "AENC", "", "ETCO", "EQUA",
110 				"GEOB", "IPLS", "LINK", "MCDI", "MLLT", "APIC", "POPM", "RVRB",
111 				"RVAD", "SYLT", "SYTC", "TALB", "TBPM", "TCOM", "TCON", "TCOP",
112 				"TDAT", "TDLY", "TENC", "TFLT", "TIME", "TKEY", "TLAN", "TLEN",
113 				"TMED", "TOPE", "TOFN", "TOLY", "TORY", "TOAL", "TPE1", "TPE2",
114 				"TPE3", "TPE4", "TPOS", "TPUB", "TSRC", "TRDA", "TRCK", "TSIZ",
115 				"TSSE", "TIT1", "TIT2", "TIT3", "TEXT", "TXXX", "TYER", "UFID",
116 				"USLT", "WOAF", "WOAR", "WOAS", "WCOM", "WCOP", "WPUB", "WXXX" };
117 
118 		for (int i = 0; i < v22.length; i++) {
119 			this.conversion22to23.put(v22[i], v23[i]);
120 		}
121 	}
122 
123 	/***
124 	 * This method is used to skip the extended header in the reading process.
125 	 * 
126 	 * @param data
127 	 *            the buffer containing the extended header. (at current
128 	 *            location)
129 	 * @param version
130 	 *            the ID3V2 version {@link Id3v2Tag#ID3V22}.<br>
131 	 * @return the size of the extended Header. (skipping already performed)
132 	 */
133 	private int processExtendedHeader(ByteBuffer data, byte version) {
134 		// TODO Verify that we have an syncsfe int
135 		int extsize = 0;
136 		byte[] exthead = new byte[4];
137 		data.get(exthead);
138 		if (version == Id3v2Tag.ID3V23) {
139 			extsize = readSize(data, Id3v2Tag.ID3V23);
140 			// The extended header size excludes those first four bytes.
141 			data.position(data.position() + (extsize));
142 		} else {
143 			extsize = readSyncsafeInteger(data);
144 			data.position(data.position() + (extsize));
145 		}
146 		return extsize;
147 	}
148 
149 	/***
150 	 * This method reads an ID3V2 tag from the given {@link ByteBuffer} at its
151 	 * curren pointer location.<br>
152 	 * 
153 	 * @param data
154 	 *            ID3V2 tag.
155 	 * @param ID3Flags
156 	 *            The flags of the tag header.
157 	 * @param version
158 	 *            Version Flag. (used to handle some version specific
159 	 *            implementations).
160 	 * @return An ID3V2 tag representation.
161 	 * @throws UnsupportedEncodingException
162 	 *             Thrown on charset conversions, if system does not support
163 	 *             them.
164 	 */
165 	public Id3v2Tag read(ByteBuffer data, boolean[] ID3Flags, byte version)
166 			throws UnsupportedEncodingException {
167 		// get the tagsize from the buffers size.
168 		int tagSize = data.limit();
169 		byte[] b;
170 		// Create a result object
171 		Id3v2Tag tag = new Id3v2Tag();
172 		// ---------------------------------------------------------------------
173 		// If the flags indicate an extended header to be present, read its
174 		// size and skip it. (It does not contain any useful information, maybe
175 		// CRC)
176 		if ((version == Id3v2Tag.ID3V23 || version == Id3v2Tag.ID3V24)
177 				&& ID3Flags[1]) {
178 			processExtendedHeader(data, version);
179 		}
180 		// ---------------------------------------------------------------------
181 		/*
182 		 * Now start the extraction of the text frames.
183 		 */
184 		// The frame names differ in lengths between version 2 to 3
185 		int specSize = (version == Id3v2Tag.ID3V22) ? 3 : 4;
186 		// As long as we have unread bytes...
187 		for (int a = 0; a < tagSize; a++) {
188 			// Create buffer taking the name of the frame.
189 			b = new byte[specSize];
190 
191 			// Do we still have enough bytes for reading the name?
192 			if (data.remaining() <= specSize)
193 				break;
194 
195 			// Read the Name
196 			data.get(b);
197 
198 			// Convert the bytes (of the name) into a String.
199 			String field = new String(b);
200 			// If byte[0] is zero, we have invalid data
201 			if (b[0] == 0)
202 				break;
203 
204 			// Now we read the length of the current frame
205 			int frameSize = readSize(data, version);
206 
207 			// If the framesize is greater than the bytes we've left to read,
208 			// or the frame length is zero, abort. Invalid data
209 			if ((frameSize > data.remaining()) || frameSize <= 0) {
210 				// ignore empty frames
211 				System.err.println(field
212 						+ " Frame size error, skiping the rest of the tag:"
213 						+ frameSize);
214 				break;
215 			}
216 
217 			b = new byte[frameSize
218 					+ ((version == Id3v2Tag.ID3V23 || version == Id3v2Tag.ID3V24) ? 2
219 							: 0)];
220 			// Read the complete frame into the byte array.
221 			data.get(b);
222 
223 			// Check the frame name once more
224 			if (!"".equals(field)) {
225 				Id3Frame f = null;
226 				/*
227 				 * Now catch possible errors occuring in the data
228 				 * interpretation. Even if a frame is not valid regarding the
229 				 * spec, the rest of the tag could be read.
230 				 */
231 				try {
232 					// Create the Frame upon the byte array data.
233 					f = createId3Frame(field, b, version);
234 				} catch (UnsupportedEncodingException uee) {
235 					throw uee;
236 				} catch (Exception e) {
237 					e.printStackTrace();
238 				}
239 				// If the frame was successfully parsed, add it to the tag.
240 				if (f != null)
241 					tag.add(f);
242 			}
243 		}
244 
245 		return tag;
246 	}
247 
248 	/***
249 	 * This mehtod reads the data of an integer out of the given buffer.<br>
250 	 * Since different version of ID3V2 tags have a different size definiton,
251 	 * the version is needed.
252 	 * 
253 	 * @param bb
254 	 *            The buffer containing the integer.
255 	 * @param version
256 	 *            The ID3V2 version. {@link Id3v2Tag#ID3V22}.
257 	 * @return The integer value
258 	 */
259 	private int readSize(ByteBuffer bb, int version) {
260 		int value = 0;
261 		if (version == Id3v2Tag.ID3V24) {
262 			value = readSyncsafeInteger(bb);
263 		} else {
264 			if (version == Id3v2Tag.ID3V23)
265 				value += (bb.get() & 0xFF) << 24;
266 			value += (bb.get() & 0xFF) << 16;
267 			value += (bb.get() & 0xFF) << 8;
268 			value += (bb.get() & 0xFF);
269 		}
270 		return value;
271 	}
272 
273 	/***
274 	 * This method reads the next 4 byte of the buffer and interprets them as a
275 	 * sync safe integer.<br>
276 	 * 
277 	 * @param buffer
278 	 *            Buffer to read from
279 	 * @return represented integer value.
280 	 */
281 	private int readSyncsafeInteger(ByteBuffer buffer) {
282 		int value = 0;
283 		value += (buffer.get() & 0xFF) << 21;
284 		value += (buffer.get() & 0xFF) << 14;
285 		value += (buffer.get() & 0xFF) << 7;
286 		value += buffer.get() & 0xFF;
287 		return value;
288 	}
289 }