1 | package org.farng.mp3.id3; |
2 | |
3 | import org.farng.mp3.AbstractMP3Tag; |
4 | import org.farng.mp3.InvalidTagException; |
5 | import org.farng.mp3.MP3File; |
6 | import org.farng.mp3.TagConstant; |
7 | import org.farng.mp3.TagException; |
8 | import org.farng.mp3.TagNotFoundException; |
9 | |
10 | import java.io.IOException; |
11 | import java.io.RandomAccessFile; |
12 | import java.util.Iterator; |
13 | |
14 | /** |
15 | * <p class=t> The two biggest design goals were to be able to implement ID3v2 without disturbing old software too much |
16 | * and that ID3v2 should be expandable. </p> |
17 | * <p/> |
18 | * <p class=t> The first criterion is met by the simple fact that the <a href="#mpeg">MPEG</a> decoding software uses a |
19 | * syncsignal, embedded in the audiostream, to 'lock on to' the audio. Since the ID3v2 tag doesn't contain a valid |
20 | * syncsignal, no software will attempt to play the tag. If, for any reason, coincidence make a syncsignal appear within |
21 | * the tag it will be taken care of by the 'unsynchronisation scheme' described in section 5. </p> |
22 | * <p/> |
23 | * <p class=t> The second criterion has made a more noticeable impact on the design of the ID3v2 tag. It is constructed |
24 | * as a container for several information blocks, called frames, whose format need not be known to the software that |
25 | * encounters them. At the start of every frame there is an identifier that explains the frames's format and content, |
26 | * and a size descriptor that allows software to skip unknown frames. </p> |
27 | * <p/> |
28 | * <p class=t> If a total revision of the ID3v2 tag should be needed, there is a version number and a size descriptor in |
29 | * the ID3v2 header. </p> |
30 | * <p/> |
31 | * <p class=t> The ID3 tag described in this document is mainly targeted to files encoded with <a href="#mpeg">MPEG-2 |
32 | * layer I, MPEG-2 layer II, MPEG-2 layer III</a> and MPEG-2.5, but may work with other types of encoded audio. </p> |
33 | * <p/> |
34 | * <p class=t> The bitorder in ID3v2 is most significant bit first (MSB). The byteorder in multibyte numbers is most |
35 | * significant byte first (e.g. $12345678 would be encoded $12 34 56 78). </p> |
36 | * <p/> |
37 | * <p class=t> It is permitted to include padding after all the final frame (at the end of the ID3 tag), making the size |
38 | * of all the frames together smaller than the size given in the head of the tag. A possible purpose of this padding is |
39 | * to allow for adding a few additional frames or enlarge existing frames within the tag without having to rewrite the |
40 | * entire file. The value of the padding bytes must be $00.<br> </p> |
41 | * <p/> |
42 | * <p class=t> <i>Padding is good as it increases the write speed when there is already a tag present in a file. If the |
43 | * new tag is one byte longer than the previous tag, than the extra byte can be taken from the padding, instead of |
44 | * having to shift the entire file one byte. Padding is of course bad in that it increases the size of the file, but if |
45 | * the amount of padding is wisely chosen (with clustersize in mind), the impact on filesystems will be virtually none. |
46 | * As the contents is $00, it is also easy for modems and other transmission devices/protocols to compress the padding. |
47 | * Having a $00 filled padding also increases the ability to recover erroneous tags.</i> </p> <p class=t> The ID3v2 tag |
48 | * header, which should be the first information in the file, is 10 bytes as follows: </p> |
49 | * <p/> |
50 | * <p><center> <table border=0> <tr><td nowrap>ID3/file identifier</td><td rowspan=3> </td><td |
51 | * width="100%">"ID3"</td></tr> <tr><td>ID3 version</td><td>$02 00</td></tr> <tr><td>ID3 |
52 | * flags</td><td>%xx000000</td></tr> <tr><td>ID3 size</td><td>4 * </td><td>%0xxxxxxx</td></tr> </table> </center> |
53 | * <p/> |
54 | * <p class=t> The first three bytes of the tag are always "ID3" to indicate that this is an ID3 tag, directly followed |
55 | * by the two version bytes. The first byte of ID3 version is it's major version, while the second byte is its revision |
56 | * number. All revisions are backwards compatible while major versions are not. If software with ID3v2 and below support |
57 | * should encounter version three or higher it should simply ignore the whole tag. Version and revision will never be |
58 | * $FF. </p> |
59 | * <p/> |
60 | * <p class=t><i> In the first draft of ID3v2 the identifier was "TAG", just as in ID3v1. It was later changed to "MP3" |
61 | * as I thought of the ID3v2 as the fileheader MP3 had always been missing. When it became appearant than ID3v2 was |
62 | * going towards a general purpose audio header the identifier was changed to "ID3". </i></p> |
63 | * <p/> |
64 | * <p class=t> The first bit (bit 7) in the 'ID3 flags' is indicating whether or not <a |
65 | * href="#sec5">unsynchronisation</a> is used; a set bit indicates usage. </p> |
66 | * <p/> |
67 | * <p class=t> The second bit (bit 6) is indicating whether or not compression is used; a set bit indicates usage. Since |
68 | * no compression scheme has been decided yet, the ID3 decoder (for now) should just ignore the entire tag if the |
69 | * compression bit is set. </p> |
70 | * <p/> |
71 | * <p class=t><i> Currently, zlib compression is being considered for the compression, in an effort to stay out of the |
72 | * all-too-common marsh of patent trouble. Have a look at the additions draft for the latest developments. </i></p> |
73 | * <p/> |
74 | * <p class=t> The ID3 tag size is encoded with four bytes where the first bit (bit 7) is set to zero in every byte, |
75 | * 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> |
76 | * <p/> |
77 | * <p class=t><i> We really gave it a second thought several times before we introduced these awkward size descriptions. |
78 | * The reason is that we thought it would be even worse to have a file header with no set size (as we wanted to |
79 | * unsynchronise the header if there were any false synchronisations in it). An easy way of calculating the tag size is |
80 | * A*2^21+B*2^14+C*2^7+D = A*2097152+B*16384+C*128+D, where A is the first byte, B the second, C the third and D the |
81 | * fourth byte. </i></p> |
82 | * <p/> |
83 | * <p class=t> The ID3 tag size is the size of the complete tag after unsychronisation, including padding, excluding the |
84 | * header (total tag size - 10). The reason to use 28 bits (representing up to 256MB) for size description is that we |
85 | * don't want to run out of space here. </p> |
86 | * <p/> |
87 | * <p class=t> An ID3v2 tag can be detected with the following pattern:<br> $49 44 33 yy yy xx |
88 | * zz zz zz zz <br> Where yy is less than $FF, xx is the 'flags' byte and zz is less than $80. </p> |
89 | * |
90 | * @author Eric Farng |
91 | * @version $Revision: 1.5 $ |
92 | */ |
93 | public class ID3v2_2 extends AbstractID3v2 { |
94 | |
95 | protected boolean compression = false; |
96 | protected boolean unsynchronization = false; |
97 | |
98 | /** |
99 | * Creates a new ID3v2_2 object. |
100 | */ |
101 | public ID3v2_2() { |
102 | super(); |
103 | setMajorVersion((byte) 2); |
104 | setRevision((byte) 2); |
105 | } |
106 | |
107 | /** |
108 | * Creates a new ID3v2_2 object. |
109 | */ |
110 | public ID3v2_2(final ID3v2_2 copyObject) { |
111 | super(copyObject); |
112 | this.compression = copyObject.compression; |
113 | this.unsynchronization = copyObject.unsynchronization; |
114 | } |
115 | |
116 | /** |
117 | * Creates a new ID3v2_2 object. |
118 | */ |
119 | public ID3v2_2(final AbstractMP3Tag mp3tag) { |
120 | if (mp3tag != null) { |
121 | final ID3v2_4 convertedTag; |
122 | if ((mp3tag instanceof ID3v2_3 == false) && (mp3tag instanceof ID3v2_2 == true)) { |
123 | throw new UnsupportedOperationException("Copy Constructor not called. Please type cast the argument"); |
124 | } else if (mp3tag instanceof ID3v2_4) { |
125 | convertedTag = (ID3v2_4) mp3tag; |
126 | } else { |
127 | convertedTag = new ID3v2_4(mp3tag); |
128 | } |
129 | this.compression = convertedTag.compression; |
130 | this.unsynchronization = convertedTag.unsynchronization; |
131 | final AbstractID3v2 id3tag = convertedTag; |
132 | final Iterator iterator = id3tag.getFrameIterator(); |
133 | AbstractID3v2Frame frame; |
134 | ID3v2_2Frame newFrame; |
135 | while (iterator.hasNext()) { |
136 | frame = (AbstractID3v2Frame) iterator.next(); |
137 | newFrame = new ID3v2_2Frame(frame); |
138 | this.setFrame(newFrame); |
139 | } |
140 | } |
141 | } |
142 | |
143 | /** |
144 | * Creates a new ID3v2_2 object. |
145 | */ |
146 | public ID3v2_2(final RandomAccessFile file) throws TagException, IOException { |
147 | this.read(file); |
148 | } |
149 | |
150 | public String getIdentifier() { |
151 | return "ID3v2_2.20"; |
152 | } |
153 | |
154 | public int getSize() { |
155 | int size = 3 + 2 + 1 + 4; |
156 | final Iterator iterator = getFrameIterator(); |
157 | ID3v2_2Frame frame; |
158 | while (iterator.hasNext()) { |
159 | frame = (ID3v2_2Frame) iterator.next(); |
160 | size += frame.getSize(); |
161 | } |
162 | return size; |
163 | } |
164 | |
165 | public void append(final AbstractMP3Tag tag) { |
166 | if (tag instanceof ID3v2_2) { |
167 | this.unsynchronization = ((ID3v2_2) tag).unsynchronization; |
168 | this.compression = ((ID3v2_2) tag).compression; |
169 | } |
170 | super.append(tag); |
171 | } |
172 | |
173 | public boolean equals(final Object obj) { |
174 | if ((obj instanceof ID3v2_2) == false) { |
175 | return false; |
176 | } |
177 | final ID3v2_2 id3v2_2 = (ID3v2_2) obj; |
178 | if (this.compression != id3v2_2.compression) { |
179 | return false; |
180 | } |
181 | if (this.unsynchronization != id3v2_2.unsynchronization) { |
182 | return false; |
183 | } |
184 | return super.equals(obj); |
185 | } |
186 | |
187 | public void overwrite(final AbstractMP3Tag tag) { |
188 | if (tag instanceof ID3v2_2) { |
189 | this.unsynchronization = ((ID3v2_2) tag).unsynchronization; |
190 | this.compression = ((ID3v2_2) tag).compression; |
191 | } |
192 | super.overwrite(tag); |
193 | } |
194 | |
195 | public void read(final RandomAccessFile file) throws TagException, IOException { |
196 | final int size; |
197 | ID3v2_2Frame next; |
198 | final byte[] buffer = new byte[4]; |
199 | if (seek(file) == false) { |
200 | throw new TagNotFoundException("ID3v2.20 tag not found"); |
201 | } |
202 | |
203 | // read the major and minor @version number & flags byte |
204 | file.read(buffer, 0, 3); |
205 | if ((buffer[0] != 2) || (buffer[1] != 0)) { |
206 | throw new TagNotFoundException(getIdentifier() + " tag not found"); |
207 | } |
208 | setMajorVersion(buffer[0]); |
209 | setRevision(buffer[1]); |
210 | this.unsynchronization = (buffer[2] & TagConstant.MASK_V22_UNSYNCHRONIZATION) != 0; |
211 | this.compression = (buffer[2] & TagConstant.MASK_V22_COMPRESSION) != 0; |
212 | |
213 | // read the size |
214 | file.read(buffer, 0, 4); |
215 | size = byteArrayToSize(buffer); |
216 | this.clearFrameMap(); |
217 | final long filePointer = file.getFilePointer(); |
218 | |
219 | // read all frames |
220 | this.setFileReadBytes(size); |
221 | resetPaddingCounter(); |
222 | while ((file.getFilePointer() - filePointer) <= size) { |
223 | try { |
224 | next = new ID3v2_2Frame(file); |
225 | final String id = next.getIdentifier(); |
226 | if (this.hasFrame(id)) { |
227 | this.appendDuplicateFrameId(id + "; "); |
228 | this.incrementDuplicateBytes(getFrame(id).getSize()); |
229 | } |
230 | this.setFrame(next); |
231 | } catch (InvalidTagException ex) { |
232 | if (ex.getMessage().equals("Found empty frame")) { |
233 | this.incrementEmptyFrameBytes(10); |
234 | } else { |
235 | this.incrementInvalidFrameBytes(); |
236 | } |
237 | } |
238 | } |
239 | this.setPaddingSize(getPaddingCounter()); |
240 | |
241 | /** |
242 | * int newSize = this.getSize(); if ((this.padding + newSize - 10) != |
243 | * size) { System.out.println("WARNING: Tag sizes don't add up"); |
244 | * System.out.println("ID3v2.20 tag size : " + newSize); |
245 | * System.out.println("ID3v2.20 padding : " + this.padding); |
246 | * System.out.println("ID3v2.20 total : " + (this.padding + newSize)); |
247 | * System.out.println("ID3v2.20 file size: " + size); } |
248 | */ |
249 | } |
250 | |
251 | public boolean seek(final RandomAccessFile file) throws IOException { |
252 | final byte[] buffer = new byte[3]; |
253 | file.seek(0); |
254 | |
255 | // read the tag if it exists |
256 | file.read(buffer, 0, 3); |
257 | final String tag = new String(buffer, 0, 3); |
258 | if (tag.equals("ID3") == false) { |
259 | return false; |
260 | } |
261 | |
262 | // read the major and minor @version number |
263 | file.read(buffer, 0, 2); |
264 | |
265 | // read back the @version bytes so we can read and save them later |
266 | file.seek(file.getFilePointer() - 2); |
267 | return ((buffer[0] == 2) && (buffer[1] == 0)); |
268 | } |
269 | |
270 | public String toString() { |
271 | final Iterator iterator = this.getFrameIterator(); |
272 | ID3v2_2Frame frame; |
273 | String str = getIdentifier() + " - " + this.getSize() + " bytes\n"; |
274 | str += ("compression = " + this.compression + "\n"); |
275 | str += ("unsynchronization = " + this.unsynchronization + "\n"); |
276 | while (iterator.hasNext()) { |
277 | frame = (ID3v2_2Frame) iterator.next(); |
278 | str += (frame.toString() + "\n"); |
279 | } |
280 | return str + "\n"; |
281 | } |
282 | |
283 | public void write(final AbstractMP3Tag tag) { |
284 | if (tag instanceof ID3v2_2) { |
285 | this.unsynchronization = ((ID3v2_2) tag).unsynchronization; |
286 | this.compression = ((ID3v2_2) tag).compression; |
287 | } |
288 | super.write(tag); |
289 | } |
290 | |
291 | public void write(final RandomAccessFile file) throws IOException { |
292 | final String str; |
293 | ID3v2_2Frame frame; |
294 | final Iterator iterator; |
295 | final byte[] buffer = new byte[6]; |
296 | final MP3File mp3 = new MP3File(); |
297 | mp3.seekMP3Frame(file); |
298 | final long mp3start = file.getFilePointer(); |
299 | file.seek(0); |
300 | |
301 | // write the first 10 tag bytes |
302 | str = "ID3"; |
303 | for (int i = 0; i < str.length(); i++) { |
304 | buffer[i] = (byte) str.charAt(i); |
305 | } |
306 | buffer[3] = 2; |
307 | buffer[4] = 0; |
308 | if (this.unsynchronization) { |
309 | buffer[5] |= TagConstant.MASK_V22_UNSYNCHRONIZATION; |
310 | } |
311 | if (this.compression) { |
312 | buffer[5] |= TagConstant.MASK_V22_COMPRESSION; |
313 | } |
314 | file.write(buffer); |
315 | |
316 | //write size; |
317 | file.write(sizeToByteArray((int) mp3start - 10)); |
318 | |
319 | // write all frames |
320 | iterator = this.getFrameIterator(); |
321 | while (iterator.hasNext()) { |
322 | frame = (ID3v2_2Frame) iterator.next(); |
323 | frame.write(file); |
324 | } |
325 | } |
326 | |
327 | public String getSongTitle() { |
328 | String text = ""; |
329 | AbstractID3v2Frame frame = getFrame("TIT2"); |
330 | if (frame != null) { |
331 | FrameBodyTIT2 body = (FrameBodyTIT2) frame.getBody(); |
332 | text = body.getText(); |
333 | } |
334 | return text.trim(); |
335 | } |
336 | |
337 | public String getLeadArtist() { |
338 | String text = ""; |
339 | AbstractID3v2Frame frame = getFrame("TPE1"); |
340 | if (frame != null) { |
341 | FrameBodyTPE1 body = (FrameBodyTPE1) frame.getBody(); |
342 | text = body.getText(); |
343 | } |
344 | return text.trim(); |
345 | } |
346 | |
347 | public String getAlbumTitle() { |
348 | String text = ""; |
349 | AbstractID3v2Frame frame = getFrame("TALB"); |
350 | if (frame != null) { |
351 | FrameBodyTALB body = (FrameBodyTALB) frame.getBody(); |
352 | text = body.getText(); |
353 | } |
354 | return text.trim(); |
355 | } |
356 | |
357 | public String getYearReleased() { |
358 | String text = ""; |
359 | AbstractID3v2Frame frame = getFrame("TYER"); |
360 | if (frame != null) { |
361 | FrameBodyTYER body = (FrameBodyTYER) frame.getBody(); |
362 | text = body.getText(); |
363 | } |
364 | return text.trim(); |
365 | } |
366 | |
367 | public String getSongComment() { |
368 | String text = ""; |
369 | AbstractID3v2Frame frame = getFrame("COMM" + ((char) 0) + "eng" + ((char) 0) + ""); |
370 | if (frame != null) { |
371 | FrameBodyCOMM body = (FrameBodyCOMM) frame.getBody(); |
372 | text = body.getText(); |
373 | } |
374 | return text.trim(); |
375 | } |
376 | |
377 | public String getSongGenre() { |
378 | String text = ""; |
379 | AbstractID3v2Frame frame = getFrame("TCON"); |
380 | if (frame != null) { |
381 | FrameBodyTCON body = (FrameBodyTCON) frame.getBody(); |
382 | text = body.getText(); |
383 | } |
384 | return text.trim(); |
385 | } |
386 | |
387 | public String getTrackNumberOnAlbum() { |
388 | String text = ""; |
389 | AbstractID3v2Frame frame = getFrame("TRCK"); |
390 | if (frame != null) { |
391 | FrameBodyTRCK body = (FrameBodyTRCK) frame.getBody(); |
392 | text = body.getText(); |
393 | } |
394 | return text.trim(); |
395 | } |
396 | |
397 | public String getSongLyric() { |
398 | String text = ""; |
399 | AbstractID3v2Frame frame = getFrame("SYLT"); |
400 | if (frame != null) { |
401 | FrameBodySYLT body = (FrameBodySYLT) frame.getBody(); |
402 | text = body.getLyric(); |
403 | } |
404 | if (text == "") { |
405 | frame = getFrame("USLT" + ((char) 0) + "eng" + ((char) 0) + ""); |
406 | if (frame != null) { |
407 | FrameBodyUSLT body = (FrameBodyUSLT) frame.getBody(); |
408 | text = body.getLyric(); |
409 | } |
410 | } |
411 | return text.trim(); |
412 | } |
413 | |
414 | public String getAuthorComposer() { |
415 | String text = ""; |
416 | AbstractID3v2Frame frame = getFrame("TCOM"); |
417 | if (frame != null) { |
418 | FrameBodyTCOM body = (FrameBodyTCOM) frame.getBody(); |
419 | text = body.getText(); |
420 | } |
421 | return text.trim(); |
422 | } |
423 | |
424 | public void setSongTitle(String songTitle) { |
425 | AbstractID3v2Frame field = getFrame("TIT2"); |
426 | if (field == null) { |
427 | field = new ID3v2_2Frame(new FrameBodyTIT2((byte) 0, songTitle.trim())); |
428 | setFrame(field); |
429 | } else { |
430 | ((FrameBodyTIT2) field.getBody()).setText(songTitle.trim()); |
431 | } |
432 | } |
433 | |
434 | public void setLeadArtist(String leadArtist) { |
435 | AbstractID3v2Frame field = getFrame("TPE1"); |
436 | if (field == null) { |
437 | field = new ID3v2_2Frame(new FrameBodyTPE1((byte) 0, leadArtist.trim())); |
438 | setFrame(field); |
439 | } else { |
440 | ((FrameBodyTPE1) field.getBody()).setText(leadArtist.trim()); |
441 | } |
442 | } |
443 | |
444 | public void setAlbumTitle(String albumTitle) { |
445 | AbstractID3v2Frame field = getFrame("TALB"); |
446 | if (field == null) { |
447 | field = new ID3v2_2Frame(new FrameBodyTALB((byte) 0, albumTitle.trim())); |
448 | setFrame(field); |
449 | } else { |
450 | ((FrameBodyTALB) field.getBody()).setText(albumTitle.trim()); |
451 | } |
452 | } |
453 | |
454 | public void setYearReleased(String yearReleased) { |
455 | AbstractID3v2Frame field = getFrame("TYER"); |
456 | if (field == null) { |
457 | field = new ID3v2_2Frame(new FrameBodyTYER((byte) 0, yearReleased.trim())); |
458 | setFrame(field); |
459 | } else { |
460 | ((FrameBodyTYER) field.getBody()).setText(yearReleased.trim()); |
461 | } |
462 | } |
463 | |
464 | public void setSongComment(String songComment) { |
465 | AbstractID3v2Frame field = getFrame("COMM"); |
466 | if (field == null) { |
467 | field = new ID3v2_2Frame(new FrameBodyCOMM((byte) 0, "eng", "", songComment.trim())); |
468 | setFrame(field); |
469 | } else { |
470 | ((FrameBodyCOMM) field.getBody()).setText(songComment.trim()); |
471 | } |
472 | } |
473 | |
474 | public void setSongGenre(String songGenre) { |
475 | AbstractID3v2Frame field = getFrame("TCON"); |
476 | if (field == null) { |
477 | field = new ID3v2_2Frame(new FrameBodyTCON((byte) 0, songGenre.trim())); |
478 | setFrame(field); |
479 | } else { |
480 | ((FrameBodyTCON) field.getBody()).setText(songGenre.trim()); |
481 | } |
482 | } |
483 | |
484 | public void setTrackNumberOnAlbum(String trackNumberOnAlbum) { |
485 | AbstractID3v2Frame field = getFrame("TRCK"); |
486 | if (field == null) { |
487 | field = new ID3v2_2Frame(new FrameBodyTRCK((byte) 0, trackNumberOnAlbum.trim())); |
488 | setFrame(field); |
489 | } else { |
490 | ((FrameBodyTRCK) field.getBody()).setText(trackNumberOnAlbum.trim()); |
491 | } |
492 | } |
493 | |
494 | public void setSongLyric(String songLyrics) { |
495 | AbstractID3v2Frame field = getFrame("USLT"); |
496 | if (field == null) { |
497 | field = new ID3v2_2Frame(new FrameBodyUSLT((byte) 0, "ENG", "", songLyrics.trim())); |
498 | setFrame(field); |
499 | } else { |
500 | ((FrameBodyUSLT) field.getBody()).setLyric(songLyrics.trim()); |
501 | } |
502 | } |
503 | |
504 | public void setAuthorComposer(String authorComposer) { |
505 | AbstractID3v2Frame field = getFrame("TCOM"); |
506 | if (field == null) { |
507 | field = new ID3v2_2Frame(new FrameBodyTCOM((byte) 0, authorComposer.trim())); |
508 | setFrame(field); |
509 | } else { |
510 | ((FrameBodyTCOM) field.getBody()).setText(authorComposer.trim()); |
511 | } |
512 | } |
513 | } |