// ID3v2.java
//
// $Id: ID3v2.java,v 1.9 2008/01/03 04:35:51 dmitriy Exp $
//
// de.vdheide.mp3: Access MP3 properties, ID3 and ID3v2 tags
// Copyright (C) 1999 Jens Vonderheide <jens@vdheide.de>
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Library General Public
// License as published by the Free Software Foundation; either
// version 2 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
// Library General Public License for more details.
//
// You should have received a copy of the GNU Library General Public
// License along with this library; if not, write to the
// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
// Boston, MA 02111-1307, USA.
/**
* Instances of this class contains an ID3v2 tag
* <p>
* Notes:
* <p>
* 1) There are two ways of detecting the size of padding used:
* a) The "Size of padding" field in the extended header
* b) Detecting all frames and substracting the tag's actual
* length from its' length in the header.
* Method a) is used in preference, so if a wrong padding
* size is stated in the extended header, all bad things
* may happen.
* <p>
* 2) Although the ID3v2 informal standard does not state it,
* this class will only detect an ID3v2 tag if is starts at
* the first byte of a file.
* <p>
* 3) There is no direct access to the header and extended header.
* Both are read, created and written internally.
*/
package de.vdheide.mp3;
import java.util.Vector;
import java.util.Enumeration;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.FileOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.RandomAccessFile;
import java.io.Serializable;
public class ID3v2 implements Serializable {
public static final int TITLE = 0;
public static final int PICTURE = 1;
public static final int ALBUM = 2;
public static final int GENRE = 3;
public static final int BPM = 4;
public static final int YEAR = 5;
public static final int COMPOSER = 6;
public static final int ARTIST = 7;
public static final int BAND = 8;
public static final int CONDUCTOR = 9;
public static final int REMIXER = 10;
public static final int PARTOFSET = 11;
public static final int TRACK = 12;
public static final int PUBLISHER = 13;
public static final int COMPILATION = 14;
public static final int PLAYCOUNTER = 15;
public static final int COMMENT = 16;
public static final String[][] FRAME_IDS = { {"TT2", "TIT2"}, // TITLE 0
{"PIC", "APIC"}, // PICTURE 1
{"TAL", "TALB"}, // ALBUM 2
{"TCO", "TCON"}, // GENRE 3
{"TBP", "TBPM"}, // BPM 4
{"TYE", "TYER"}, // YEAR 5
{"TCM", "TCOM"}, // COMPOSER 6
{"TP1", "TPE1"}, // ARTIST 7
{"TP2", "TPE2"}, // BAND 8
{"TP3", "TPE3"}, // CONDUCTOR 9
{"TP4", "TPE4"}, // REMIXER 10
{"TPA", "TPOS"}, // PARTOFSET 11
{"TRK", "TRCK"}, // TRACK 12
{"TPB", "TPUB"}, // PUBLISHER 13
{"CMP", "TCMP"}, // COMPILATION 14
{"", "PCNT"}, // PLAYCOUNTER 15
{"", "COMM"}, // COMMENT
};
static final int COPY_BUFFER_SIZE = 1024*64;
/********** Constructors **********/
/**
* Provides access to ID3v2 tag. When used with an InputStream, no writes are possible
* (<code>update</code> will fail with an <code>IOException</code>, so make sure you
* just read.
*
* @param in Input stream to read from. Stream position must be set to beginning of file
* (i.e. position of ID3v2 tag).
* @exception IOException If I/O errors occur
* @exception ID3v2IllegalVersionException If file contains an IDv2 tag of higher version than
* <code>VERSION</code>.<code>REVISION</code>
* @exception ID3v2WrongCRCException If file contains CRC and this differs from CRC calculated
* from the frames
* @exception ID3v2DecompressionException If a decompression error occurred while decompressing
* a compressed frame
*/
public ID3v2(IOAdapter in, String encoding) throws IOException, ID3v2IllegalVersionException, ID3v2WrongCRCException,
ID3v2DecompressionException
{
this.file = null;
if (encoding != null)
this.encoding = encoding;
// open file and read tag (if present)
try
{
readHeader(in);
}
catch (NoID3v2HeaderException e)
{
// no tag
// in.close();
//e.printStackTrace();
header = null;
extended_header = null;
frames = null;
return;
}
// tag present
if (header.hasExtendedHeader())
{
readExtendedHeader(in);
}
else
{
extended_header = null;
}
readFrames(in);
//in.close();
is_changed = false;
}
/**
* Provides access to <code>file</code>'s ID3v2 tag
*
* @param file File to access
* @exception IOException If I/O errors occur
* @exception ID3v2IllegalVersionException If file contains an IDv2 tag of higher version than
* <code>VERSION</code>.<code>REVISION</code>
* @exception ID3v2WrongCRCException If file contains CRC and this differs from CRC calculated
* from the frames
* @exception ID3v2DecompressionException If a decompression error occured while decompressing
* a compressed frame
*/
public ID3v2(File file, String encoding) throws IOException, ID3v2IllegalVersionException, ID3v2WrongCRCException,
ID3v2DecompressionException
{
this(new IOAdapter(file), encoding);
this.file = file;
}
public ID3v2(File file) throws IOException, ID3v2IllegalVersionException, ID3v2WrongCRCException,
ID3v2DecompressionException {
this(file, null);
}
/********** Public variables **********/
/**
* ID3v2 version
*/
public final static byte VERSION = 3;
/**
* ID3v2 compatible version
*/
public final static byte MAX_COMPATIBLE_VERSION = 4;
/**
* ID3v2 to store
*/
public final static byte STORING_COMPATIBLE_VERSION = 3;
/**
* ID3v2 revision
*/
public final static byte REVISION = 0;
public final static byte MAX_COMPATIBLE_REVISION = 0;
/********** Public methods **********/
public String getEncoding() {
return encoding;
}
/**
* This method undoes the effect of the unsynchronization scheme
* by replacing $FF $00 by $FF
*
* @param in Array of bytes to be "synchronized"
* @return Changed array or null if no "synchronization" was necessary
*/
public static byte []synchronize(byte []in)
{
boolean did_synch = false;
byte out[] = new byte[in.length];
int outpos = 0; // next position to write to
for (int i=0; i<in.length; i++)
{
// Check every byte if it is $FF
if (in[i] == (byte)255)
{
// synchronize if next byte is $00
if (in[i+1] == 0)
{
did_synch = true;
out[outpos++]=(byte)255;
i++;
}
else
{
out[outpos++]=(byte)255;
}
}
else
{
out[outpos++]=in[i];
}
}
// make out smaller if necessary
if (outpos!=in.length)
{
// removed one or more bytes
byte []tmp = new byte[outpos];
System.arraycopy(out, 0, tmp, 0, outpos);
out = tmp;
}
if (did_synch == true)
{
return out;
}
else
{
return null;
}
}
/**
* Unsynchronizes an array of bytes by replacing $FF 00 with
* $FF 00 00 and %11111111 111xxxxx with
* %11111111 00000000 111xxxxx.
*
* @param in Array of bytes to be "unsynchronized"
* @return Changed array or null if no change was necessary
*/
public static byte []unsynchronize(byte []in)
{
byte []out = new byte[in.length];
int outpos = 0; // next position to write to
boolean did_unsync = false;
for (int i=0; i<in.length; i++)
{
// Check every byte in in if it is $FF
if (true && in[i]==-1)
{
// yes, perhaps we must unsynchronize
if ((in[i+1]&0xff)>=0xe0 || in[i+1]==0)
{
// next byte is %111xxxxx or %00000000,
// we must unsynchronize
// first, enlarge out by one element
byte []tmp = new byte[out.length + 1];
System.arraycopy(out, 0, tmp, 0, outpos);
out = tmp;
tmp = null;
out[outpos++]=-1;
out[outpos++]=0;
out[outpos++]=in[i+1];
// skip next byte, we have already written it
i++;
did_unsync = true;
}
else
{
// no unsynchronization necessary
out[outpos++]=in[i];
}
}
else
{
// no unsynchronization necessary
out[outpos++]=in[i];
}
}
if (did_unsync == true)
{
// we did some unsynchronization
//System.err.println("unsynched: 0x"+rogatkin.BaseController.bytesToHex(out)+"\n for 0x"+
//rogatkin.BaseController.bytesToHex(in));
return out;
}
else
{
return null;
}
}
/**
* Enables or disables use of padding (enabled by default)
*
* @param use_padding True if padding should be used
*/
public void setUsePadding(boolean use_padding)
{
if (this.use_padding != use_padding)
{
is_changed = true;
this.use_padding = use_padding;
}
}
/**
* @return True if padding is used
*/
public boolean getUsePadding()
{
return use_padding;
}
/**
* Enables / disables use of CRC
*
* @param use_crc True if CRC should be used
*/
public void setUseCRC(boolean use_crc)
{
if (this.use_crc != use_crc)
{
is_changed = true;
this.use_crc = use_crc;
}
}
/**
* @return True if CRC is used
*/
public boolean getUseCRC()
{
return use_crc;
}
/**
* Enables / disables use of unsynchronization
*
* @param use_crc True if unsynchronization should be used
*/
public void setUseUnsynchronization(boolean use_unsynch)
{
if (this.use_unsynchronization != use_unsynch)
{
is_changed = true;
this.use_unsynchronization = use_unsynch;
}
}
/**
* @return True if unsynchronization should be used
*/
public boolean getUseUnsynchronization()
{
return use_unsynchronization;
}
/**
* Test if file already has an ID3v2 tag
*
* @return true if file has IDv2 tag
*/
public boolean hasTag()
{
if (header == null)
{
return false;
}
else
{
return true;
}
}
/**
* Get all frames
*
* @return <code>Vector</code> of all frames
* @exception NoID3v2TagException If file does not contain ID3v2 tag
*/
public Vector getFrames() throws NoID3v2TagException
{
if (frames == null)
{
throw new NoID3v2TagException();
}
return frames;
}
public TagContent getPicture() throws FrameDamagedException
{
byte []v2cont = Frame.read(this, getFrameCode(PICTURE));
if (v2cont == null)
return new TagContent();
else {
TagContent ret = new TagContent();
Parser parse = new Parser(v2cont, true, encoding);
try {
if (header.version <= 2)
try {
ret.setType(new String(parse.parseBinary(3), ID3.ISO_8859_1));
} catch(java.io.UnsupportedEncodingException ue) {
}
else
ret.setType(parse.parseText(TextFrame.ISO));
ret.setSubtype(parse.parseBinary(1));
ret.setDescription(parse.parseText());
ret.setContent(parse.parseBinary());
//System.err.println("Image is: 0x"+rogatkin.BaseController.bytesToHex(ba, 0, 100));
return ret;
} catch (ParseException e) {
throw new FrameDamagedException();
}
}
}
/**
* Return all frame with ID <code>id</code>
*
* @param id Frame ID
* @return Requested frames
* @exception NoID3v2TagException If file does not contain ID3v2Tag
* @exception ID3v2NoSuchFrameException If file does not contain requested ID3v2 frame
*/
public Vector getFrame(String id) throws NoID3v2TagException, ID3v2NoSuchFrameException
{
if (frames == null)
{
throw new NoID3v2TagException();
}
Vector res = new Vector();
ID3v2Frame tmp;
for (Enumeration e = frames.elements() ; e.hasMoreElements() ;)
{
tmp = (ID3v2Frame)e.nextElement();
// System.err.println("ID "+tmp.getID());
if (tmp.getID().equals(id))
{
res.addElement(tmp);
}
}
if (res.size()==0)
{
// no frame found
throw new ID3v2NoSuchFrameException();
}
else
{
return res;
}
}
/**
* Add a frame
*
* @param frame Frame to add
*/
public void addFrame(ID3v2Frame frame)
{
if (frames == null)
{
frames = new Vector();
}
frames.addElement(frame);
is_changed = true;
}
/**
* Remove a frame.
*
* @param frame Frame to remove
* @exception NoID3v2TagException If file does not contain ID3v2Tag
* @exception ID3v2NoSuchFrameException If file does not contain requested ID3v2 frame
*/
public void removeFrame(ID3v2Frame frame) throws NoID3v2TagException, ID3v2NoSuchFrameException
{
if (frames == null)
{
throw new NoID3v2TagException();
}
if (frames.removeElement(frame) == false)
{
throw new ID3v2NoSuchFrameException();
}
is_changed = true;
}
/**
* Remove all frames with a given id.
*
* @param id ID of frames to remove
* @exception NoID3v2TagException If file does not contain ID3v2Tag
* @exception ID3v2NoSuchFrameException If file does not contain requested ID3v2 frame
*/
public void removeFrame(String id) throws NoID3v2TagException, ID3v2NoSuchFrameException
{
if (frames == null)
{
throw new NoID3v2TagException();
}
ID3v2Frame tmp;
boolean found = false; // will be true if at least one frame was found
for (Enumeration e = frames.elements() ; e.hasMoreElements() ;)
{
tmp = (ID3v2Frame)e.nextElement();
if (tmp.getID().equals(id))
{
frames.removeElement(tmp);
found = true;
}
}
if (found == false)
{
throw new ID3v2NoSuchFrameException();
}
is_changed = true;
}
/**
* Remove a spefic frames with a given id. A number is given to identify the frame
* if more than one frame exists
*
* @param id ID of frames to remove
* @param number Number of frame to remove (the first frame gets number 0)
* @exception NoID3v2TagException If file does not contain ID3v2Tag
* @exception ID3v2NoSuchFrameException If file does not contain requested ID3v2 frame
*/
public void removeFrame(String id, int number) throws NoID3v2TagException, ID3v2NoSuchFrameException
{
if (frames == null)
{
throw new NoID3v2TagException();
}
ID3v2Frame tmp;
int count = 0; // Number of frames with id found so far
boolean removed = false; // will be true if at least frame was removed
for (Enumeration e = frames.elements() ; e.hasMoreElements() ;)
{
tmp = (ID3v2Frame)e.nextElement();
if (tmp.getID().equals(id))
{
if (count == number)
{
frames.removeElement(tmp);
removed = true;
}
else
{
count++;
}
}
}
if (removed == false)
{
throw new ID3v2NoSuchFrameException();
}
is_changed = true;
}
/**
* Remove all frames
*/
public void removeFrames()
{
if (frames != null)
{
frames = new Vector();
}
}
/**
* Write changes to file
*
* @exception IOException If an I/O error occurs
*/
public void update() throws IOException {
// don't write changes if not necessary
if (is_changed == true) {
// check if unsynchronization scheme is used
boolean uses_unsynchronization = false;
boolean extHeader = false;
// create array of bytes from frames
byte []bframes = convertFramesToArrayOfBytes();
// create new extended header (padding_size is set later if necessary)
int crc = 0;
if (extHeader && use_crc == true) {
java.util.zip.CRC32 crc_calculator = new java.util.zip.CRC32();
crc_calculator.update(bframes);
crc = (int)crc_calculator.getValue();
}
byte []bext_header = extHeader?(extended_header =new ID3v2ExtendedHeader(use_crc, crc, 0)).getBytes():new byte[0];
// unsynchronize extended header and frames if necessary
if (use_unsynchronization == true) {
byte []uns_ext_header = unsynchronize(bext_header);
if (uns_ext_header != null) {
// did unsynchronization
uses_unsynchronization = true;
bext_header = uns_ext_header;
}
byte []uns_frames = unsynchronize(bframes);
if (uns_frames != null) {
uses_unsynchronization = true;
bframes = uns_frames;
}
}
// create new header
// calculate new length
int new_length = bext_header.length + bframes.length;
ID3v2Header new_header = new ID3v2Header(VERSION, REVISION, uses_unsynchronization,
extHeader, false, new_length);
// create arrays of byte from header
byte []bheader = new_header.getBytes();
// check if length is sufficient
int length_file;
if (header == null) {
// no id3v2 tag
length_file = 0;
}
else {
length_file = header.getTagSize();// + 10;
}
// if more space is needed than provided or no padding should be used and
// lengths do not mach exactly, create a temporary file
File write_to = file;
if (header == null || (header != null && new_length > length_file ||
(use_padding==false && new_length!=length_file))) {
// create temp file
write_to = File.createTempFile("ID3", ".TMP", file.getParentFile());
// write to specific file
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(write_to));
// write header
out.write(bheader);
if (extHeader) // write extended header
out.write(bext_header);
// write frames
out.write(bframes);
if (use_padding == true) {
// if we're writing to new file, use enough padding
// to make resulting file size a multiple of 2048 bytes
// calculate resulting file size
long old_file_without_id3v2 = file.length() - length_file;
long res_file_size = old_file_without_id3v2 + new_length;
// calculate size of padding
long padding = (long)(Math.ceil(res_file_size / 2048) * 2048) - res_file_size;
for (int i=0; i<padding; i++) {
out.write(0);
}
}
// write rest of file if we are using a temporary file
BufferedInputStream copy_out = new BufferedInputStream(new FileInputStream(file));
// go to first byte after ID3v2 tag
if (header != null) {
copy_out.skip(length_file - 1);
}
byte[] buffer = new byte[COPY_BUFFER_SIZE];
int len;
while((len = copy_out.read(buffer)) > 0)
out.write(buffer, 0, len);
copy_out.close();
out.close();
// temp file: rename file to original filename
if (!write_to.renameTo(file)) {
// hell, we must copy
BufferedInputStream is = new BufferedInputStream(new FileInputStream(write_to), COPY_BUFFER_SIZE);
BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file), COPY_BUFFER_SIZE);
while((len = is.read(buffer)) > 0)
os.write(buffer, 0, len);
is.close();
os.close();
write_to.delete();
}
} else { // only update required
RandomAccessFile out = new RandomAccessFile(write_to, "rw");
out.write(bheader);
out.write(bext_header);
out.write(bframes);
if (use_padding == true) {
int padding = length_file - new_length;
//System.err.println("Writing padding of length "+padding);
for (int i=0; i<padding; i++) {
out.write(0);
}
}
out.close();
}
header = new_header;
is_changed = false;
}
}
public String getFrameCode(int index) { // getFrameSignature
return FRAME_IDS[index][header != null && header.version<=2?0:1];
}
/********** Private variables **********/
protected File file;
ID3v2Header header;
protected ID3v2ExtendedHeader extended_header;
protected Vector frames;
protected boolean is_changed = false;
protected boolean use_padding = true;
protected boolean use_crc = true;
protected boolean use_unsynchronization = true;
protected String encoding;
/********** Private methods **********/
/**
* Read ID3v2 header from file <code>in</code>
*/
protected void readHeader(IOAdapter in) throws NoID3v2HeaderException,
ID3v2IllegalVersionException,
IOException
{
header = new ID3v2Header(in);
}
/**
* Read extended ID3v2 header from input stream <tt>in</tt>
*
* @param in Input stream to read from
*/
private void readExtendedHeader(IOAdapter in) throws IOException
{
// in file pointer must be at correct position (header
// has just been read)
extended_header = new ID3v2ExtendedHeader(in);
}
/**
* Read ID3v2 frames from stream <tt>in</tt>
* Stream position must be set to beginning of frames
*
* @param in Stream to read from
*
*/
private void readFrames(IOAdapter in) throws IOException, ID3v2WrongCRCException,
ID3v2DecompressionException
{
// steps to read frames:
// 1) Read all frames as bytes (don't include padding if size of padding is
// known, i.e. ext. header exists)
// 2) If CRC is present, make CRC check on frames
// 3) Convert bytes to ID3v2Frames
//// read all frames as bytes
// calculate number of bytes to be read
int bytes_to_read;
if (extended_header != null)
{
// ext. header exists
bytes_to_read = header.getTagSize() - (extended_header.getSize() + 4) -
extended_header.getPaddingSize();
}
else
{
// no ext. header, include padding
bytes_to_read = header.getTagSize();
}
// read bytes
byte []unsynch_frames_as_byte = new byte[bytes_to_read];
in.read(unsynch_frames_as_byte);
byte []frames_as_byte;
if (header.getUnsynchronization()) {
// undo effects of unsynchronization
frames_as_byte = synchronize(unsynch_frames_as_byte);
if (frames_as_byte == null) {
frames_as_byte = unsynch_frames_as_byte;
}
} else {
frames_as_byte = unsynch_frames_as_byte;
}
//// CRC check
if (extended_header != null && extended_header.hasCRC() == true)
{
// make CRC check
// calculate crc of read frames (because extended header exists,
// they contain no padding)
java.util.zip.CRC32 crc_calculator = new java.util.zip.CRC32();
crc_calculator.update(frames_as_byte);
int crc = (int)crc_calculator.getValue();
if ((int)crc != (int)extended_header.getCRC())
{
// crc mismatch
//throw new ID3v2WrongCRCException();
}
}
//// Convert bytes to ID3v2Frames
frames = new Vector();
ByteArrayInputStream bis = new ByteArrayInputStream(frames_as_byte);
// read frames as long as there are bytes and we are not reading from padding
// (indicated by invalid frame id)
ID3v2Frame frame = null;
boolean cont = true;
while ((bis.available() > 0) && (cont == true))
{
frame = new ID3v2Frame(bis, header.version<=2);
if (frame.getID() == ID3v2Frame.ID_INVALID)
{
// reached end of frames
cont = false;
}
else
{
frames.addElement(frame);
}
}
}
/**
* Convert all frames to an array of bytes
*/
private byte[] convertFramesToArrayOfBytes()
{
ID3v2Frame tmp = null;
ByteArrayOutputStream out = new ByteArrayOutputStream(500);
for (Enumeration e = frames.elements() ; e.hasMoreElements() ;)
{
tmp = (ID3v2Frame)e.nextElement();
byte frame_in_bytes[] = tmp.getBytes();
out.write(frame_in_bytes, 0, frame_in_bytes.length);
}
return out.toByteArray();
}
}