// ID3v2Frame.java
// $Id: ID3v2Frame.java,v 1.6 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.
/**
*
* This class contains one ID3v2 frame.
*
* Note: ID3v2 frame does not now anything about unsynchronization. That is up to
* higher level objects (i.e. ID3v2)
*/
package de.vdheide.mp3;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.io.Serializable;
public class ID3v2Frame implements Serializable
{
/********** Constructors **********/
/**
* Creates a new ID3v2 frame
*
* @param id Frame id
* @param content Frame content. Must not be unsynchronized!
* @param tag_alter_preservation True if frame should be discarded if frame id
* is unknown to software and tag is altered
* @param file_alter_preservation Same as <code>tag_alter_preservation</code>, but applies if
* file (excluding tag) is altered
* @param read_only True if frame should not be changed
* @param compression_type Use contant from this class:
* <code>ID3v2Frame.NO_COMPRESSION</code>: <code>content</code> is not compressed and should not
* be compressed.
* <code>ID3v2Frame.IS_COMPRESSED</code>: <code>content</code> is already compressed
* <code>ID3v2Frame.DO_COMPRESS</code>: <code>content</code> is not compressed, but should be
* Compression can also be switched on/off with <code>setCompression</code>
* @param encryption Encryption method or 0 if not encrypted (not completely supported,
* encryption must be done externally)
* @param group Group of frames this frame belongs to or 0 if frame does not belong to any group
* @exception ID3v2DecompressionException If content is compressed and decompresson fails
*/
public ID3v2Frame(String id, byte []content, boolean tag_alter_preservation,
boolean file_alter_preservation, boolean read_only, byte compression_type,
byte encryption_id, byte group) throws ID3v2DecompressionException
{
this.id = id;
this.content = content;
this.tag_alter_preservation = tag_alter_preservation;
this.file_alter_preservation = file_alter_preservation;
this.read_only = read_only;
this.compression = (compression_type == DO_COMPRESS || compression_type == IS_COMPRESSED);
this.encryption_id = encryption_id;
this.group = group;
if (compression_type == DO_COMPRESS)
{
// compress content
decompressed_length = this.content.length;
compressContent();
}
else if (compression_type == IS_COMPRESSED)
{
// decompress content
compressed_content = content;
decompressContent();
decompressed_length = this.content.length;
}
else
{
// no compression
decompressed_length = this.content.length;
compressed_content = this.content;
}
}
/**
* Creates a new ID3v2 frame from a stream.
* Stream position must be set to first byte of frame.
* Note: Encryption/Deencryption is not supported, so content of
* encrypted frames will be returned encrypted. It is up to
* the higher level routines to decompress it.
* Note^2: Compression/decompression supports only GZIP.
*
* @param in Stream to read from
* @param version 2
* @exception ID3v2DecompressionException If input is compressed and decompression fails
* @exception IOException If I/O error occurs
*/
public ID3v2Frame(InputStream in, boolean ver2) throws IOException, ID3v2DecompressionException {
//// read header
byte []head = new byte[ver2?6:10];
in.read(head);
// check if id is valid (no real check for errors, as you will see)
if (head[0] == 0) {
// id may not start with a 0
// you may call it "error", I call it "padding"
// so do not raise an exception, just inform user of this
// instance by setting id = ID_INVALID
this.id = ID_INVALID;
return;
}
// decode id
this.id = new String(head,0,ver2?3:4,ID3.ISO_8859_1);
// decode size (needed to read content)
int length = (int)(new Bytes(head, ver2?3:4, ver2?3:4)).getValue();
// System.err.prinltn("Ver "+ver2+" id "+id+" size "+length);
if (length > 1*1024*1024)
throw new ID3v2DecompressionException("Size of frame exceeds "+(1*1024*1024));
if (ver2 == false) {
// deocde flags
if (((head[8] & 0xff) & FLAG_TAG_ALTER_PRESERVATION) > 0) {
tag_alter_preservation = true;
}
if (((head[8] & 0xff) & FLAG_FILE_ALTER_PRESERVATION) > 0) {
file_alter_preservation = true;
}
if (((head[8] & 0xff) & FLAG_READ_ONLY) > 0) {
read_only = true;
}
if (((head[9] & 0xff) & FLAG_COMPRESSION) > 0) {
compression = true;
}
boolean encryption = false;
if (((head[9] & 0xff) & FLAG_ENCRYPTION) > 0) {
encryption = true;
}
boolean grouping = false;
if (((head[9] & 0xff) & FLAG_GROUPING) > 0) {
grouping = true;
}
// additional bytes if present
if (compression == true) {
// read decompressed size
byte []decomp_byte = new byte[4];
in.read(decomp_byte);
decompressed_length = (int)(new Bytes(decomp_byte)).getValue();
// substract 4 bytes from length to get actual content length
length -= 4;
}
if (encryption == true) {
// read encryption type
encryption_id = (byte)in.read();
length--;
}
if (grouping == true) {
// read group id
group = (byte)in.read();
// substract 1 byte from length to get actual content length
length--;
}
else {
group = 0;
}
}
//// read content
content = new byte[length];
in.read(content);
// decompress if necessary
if (compression == true) {
compressed_content = new byte[content.length];
System.arraycopy(content, 0, compressed_content, 0, content.length);
// compressed_content = content;
decompressContent();
}
}
/********** Public contants **********/
// compression type
public final static byte NO_COMPRESSION = 0;
public final static byte IS_COMPRESSED = 1;
public final static byte DO_COMPRESS = 2;
// IDs
public final static String ID_INVALID = null;
/********** Public methods **********/
public String getID()
{
return id;
}
public void setID(String id)
{
this.id = id;
}
public boolean getTagAlterPreservation()
{
return tag_alter_preservation;
}
public void setTagAlterPreservation(boolean tag_alter_preservation)
{
this.tag_alter_preservation = tag_alter_preservation;
}
public boolean getFileAlterPreservation()
{
return file_alter_preservation;
}
public void setFileAlterPreservation(boolean file_alter_preservation)
{
this.file_alter_preservation = file_alter_preservation;
}
public boolean getReadOnly()
{
return read_only;
}
public void setReadOnly(boolean read_only)
{
this.read_only = read_only;
}
public boolean getCompression()
{
return compression;
}
public void setCompression(boolean compression)
{
this.compression = compression;
}
/**
* @returns Encrytion ID or 0 if not encrypted
*/
public byte getEncryptionID()
{
return encryption_id;
}
public void setEncryption(byte encryption_id)
{
this.encryption_id = encryption_id;
}
public byte getGroup()
{
return group;
}
public void setGroup(byte group)
{
this.group = group;
}
/**
* Calculates the number of bytes necessary to store a byte representation
* of this frame
*/
public int getLength()
{
// header: frame id (4 bytes), size (4 bytes), flags (2 bytes)
// + content length
int length = 10;
// if compression is set, add 4 bytes for decompressed size
if (compression == true)
{
length += 4;
}
// if encryption is set, add one byte for encryption id
if (encryption_id >= 0)
{
length++;
}
// if group is set, add one byte for group identifier
if (group != 0)
{
length++;
}
// content
if (compression == true)
{
length += compressed_content.length;
}
else
{
length += content.length;
}
return length;
}
/**
* Returns content (decompressed)
*/
public byte[] getContent()
{
return content;
}
/**
* Returns an array of bytes representing this frame
*/
public byte[] getBytes()
{
// get length, this is used more than once, so store it
int length = getLength();
byte[] ret = new byte[length];
//// write header
// write id
int idLen = id.length();
for (int i=0; i<length; i++)
{
if (i>idLen-1)
{
// this should not happen, all ids are 4 chars long...
ret[i]=0;
}
else
{
ret[i] = (byte)id.charAt(i);
}
}
// write size
byte []size_byte = (new Bytes(length-10, 4)).getBytes();
System.arraycopy(size_byte, 0, ret, 4, 4);
// write flags
byte flag1 = 0;
if (tag_alter_preservation == true)
{
flag1 = (byte)(flag1 | FLAG_TAG_ALTER_PRESERVATION);
}
if (file_alter_preservation == true)
{
flag1 += (byte)(flag1 | FLAG_FILE_ALTER_PRESERVATION);
}
if (read_only == true)
{
flag1 += (byte)(flag1 | FLAG_READ_ONLY);
}
ret[8] = flag1;
byte flag2 = 0;
if (compression == true)
{
flag2 += (byte)(flag2 | FLAG_COMPRESSION);
}
if (encryption_id != 0)
{
flag2 += (byte)(flag2 | FLAG_ENCRYPTION);
}
if (group > 0)
{
flag2 += (byte)(flag2 | FLAG_GROUPING);
}
ret[9] = flag2;
short content_offset = 10; // first byte used for content
// decompressed size, if compressed
if (compression == true)
{
byte []decomp_byte = (new Bytes(length, 4)).getBytes();
System.arraycopy(decomp_byte, 0, ret, content_offset, 4);
content_offset += 4;
}
// encryption id if set
if (encryption_id != 0)
{
ret[content_offset] = encryption_id;
content_offset++;
}
// group id if set
if (group > 0)
{
ret[content_offset] = group;
content_offset++;
}
// content
if (compression == true)
{
compressContent();
System.arraycopy(compressed_content, 0, ret, content_offset, compressed_content.length);
}
else
{
System.arraycopy(content, 0, ret, content_offset, content.length);
}
//System.err.println("frame to wr: 0x"+rogatkin.BaseController.bytesToHex(ret));
return ret;
}
/********** Private variables **********/
private String id;
private boolean tag_alter_preservation = false;
private boolean file_alter_preservation = false;
private boolean read_only = false;
private byte encryption_id = 0;
private int decompressed_length = 0;
private boolean compression = false;
private byte group = 0;
private boolean uses_unsynch = false;
private byte []content; // decompressed
private byte []compressed_content; // compressed
private final static byte FLAG_TAG_ALTER_PRESERVATION = (byte)(1 << 7);
private final static byte FLAG_FILE_ALTER_PRESERVATION = (byte)(1 << 6);
private final static byte FLAG_READ_ONLY = (byte)(1 << 5);
private final static byte FLAG_COMPRESSION = (byte)(1 << 7);
private final static byte FLAG_ENCRYPTION = (byte)(1 << 6);
private final static byte FLAG_GROUPING = (byte)(1 << 5);
/********** Private methods **********/
/**
* Compresses content
*/
private void compressContent()
{
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try
{
GZIPOutputStream gout = new GZIPOutputStream(bout);
// write (compress)
gout.write(content, 0, content.length);
gout.close();
// write into compressed_content
compressed_content = bout.toByteArray();
// did compression really reduce size?
if (content.length <= compressed_content.length)
{
compression = false;
}
}
catch (IOException e)
{
e.printStackTrace();
// how should this happen? We are writing to memory...
}
}
/**
* Decompresses content
*/
private void decompressContent() throws ID3v2DecompressionException
{
ByteArrayInputStream bin = new ByteArrayInputStream(compressed_content);
try
{
GZIPInputStream gin = new GZIPInputStream(bin);
// GZIPInputStream does not tell the array size needed to store the
// decompressed array, so we write it byte by byte into a ByteArrayOutputStream
ByteArrayOutputStream bout = new ByteArrayOutputStream();
int res = 0;
while ((res = gin.read()) != -1)
{
bout.write(res);
}
content = bout.toByteArray();
}
catch (IOException e)
{
//throw new ID3v2DecompressionException(e.getMessage());
throw new ID3v2DecompressionException();
}
}
}