/* You may freely copy, distribute, modify and use this class as long as the original author attribution remains intact. See message below. Copyright (C) 2001-2003 Christian Pesch. All Rights Reserved. */ package slash.metamusic.mp3; import slash.metamusic.mp3.sections.PictureType; import slash.metamusic.mp3.util.BitConversion; import slash.metamusic.mp3.util.ISO8601; import slash.metamusic.util.ImageResizer; import slash.metamusic.util.MimeTypeGuesser; import javax.activation.MimeType; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.logging.Logger; /** * My instances represent a ID3v2 header of the MP3 frames as * described in http://www.id3.org/id3v2.3.0.html. * * @author Christian Pesch * @version $Id: ID3v2Header.java 952 2007-01-17 20:14:15Z cpesch $ */ public class ID3v2Header implements ID3MetaData { /** * Logging output */ protected static final Logger log = Logger.getLogger(ID3v2Header.class.getName()); public static final String ID3V2TAG = "ID3"; public static final int ID3V2_HEADER_SIZE = 10; public static final int ID3_SIZE = 3; public static final int SIZE_SIZE = 4; public static final int FLAG_SIZE = 1; public final static int UNSYNCHRONIZATION_FLAG = 0x0080; public final static int EXTENDED_HEADER_FLAG = 0x0040; public final static int EXPERIMENTAL_FLAG = 0x0020; public final static int FOOTER_FLAG = 0x0010; /** * Encoding to use when converting from bytes to Unicode (String). */ public static final String ISO_8859_1_ENCODING = "ISO8859_1"; private static final String TRACK_TAG_NAME = "TIT2"; private static final String ARTIST_TAG_NAME = "TPE1"; private static final String ALBUM_ARTIST_TAG_NAME = "TPE2"; private static final String COMPILATION_TAG_NAME = "TCMP"; private static final String ALBUM_TAG_NAME = "TALB"; private static final String COMMENT_TAG_NAME = "COMM"; private static final String INDEX_TAG_NAME = "TRCK"; private static final String PART_OF_SET_INDEX_TAG_NAME = "TPOS"; private static final String GENRE_TAG_NAME = "TCON"; private static final String YEAR_TAG_NAME = "TYER"; private static final String SECONDS_TAG_NAME = "TLEN"; private static final String MUSICBRAINZ_ID_TAG_NAME = "UFID"; private static final String PUBLISHER_TAG_NAME = "TPUB"; private static final String ATTACHED_PICTURE_TAG_NAME = "APIC"; private static final String LYRICS_TAG_NAME = "USLT"; private static final String RATING_TAG_NAME = "RATG"; private static final String PLAY_COUNTER_TAG_NAME = "PCNT"; private static final String PLAY_TIME_TAG_NAME = "TDPL"; private static final String TAGGING_TIME_TAG_NAME = "TDTG"; private static final String ENCODER_TAG_NAME = "TENC"; private static final String COVER_FORMAT = "jpg"; private static final int COVER_SIZE_LIMIT = 600; private static final String LYRICS_DESCRIPTION = "Lyrics from http://www.lyrc.com.ar"; private static final String LYRICS_LANGUAGE = "English"; /** * Create a new (empty) header */ public ID3v2Header() { this.version = new ID3v2Version(); this.unsynchronized = false; this.extended = false; this.experimental = false; this.footer = false; this.frames = new ArrayList<ID3v2Frame>(1); valid = true; } public ID3v2Header(String artist, String album, String track, int index, ID3Genre genre, int year, String comment) { this(); setArtist(artist); setAlbum(album); setTrack(track); setIndex(index); setGenre(genre); setYear(year); setComment(comment); } public ID3v1Tail toID3v1Tail() { return new ID3v1Tail(getArtist(), getAlbum(), getTrack(), getIndex(), getGenre(), getYear(), getComment() ); } public void migrateToVersion(ID3v2Version current) { if (!getVersion().equals(current)) { for (ID3v2Frame f : getFrames()) { f.migrateToVersion(current); } setVersion(current); } } // --- read object ----------------------------------------- /** * Reads the ID3v2 header from the input stream. * * @param in the InputStream to read from * @return if the read header is valid * @throws NoID3v2HeaderException if no header can be found or exists * @throws IOException if an error occurs */ public boolean read(InputStream in) throws NoID3v2HeaderException, IOException { valid = false; readSize = 0; log.fine("Reading ID3v2 header"); if (!checkForID3v2Header(in)) { throw new NoID3v2HeaderException(); } else { valid = readHeader(header); if (valid && extended) valid = valid & readExtendedHeader(in); if (valid) valid = valid & readFrames(in); } return valid; } /** * Check if ID3v2 header is present * * @return true if tag present * @throws IOException if an error occurs */ protected boolean checkForID3v2Header(InputStream in) throws IOException { header = new byte[ID3V2_HEADER_SIZE]; if (in.read(header, 0, ID3V2_HEADER_SIZE) != ID3V2_HEADER_SIZE) return false; String id3v2Tag = new String(header, 0, ID3_SIZE, ISO_8859_1_ENCODING); if (!id3v2Tag.equals(ID3V2TAG)) return false; // next two bytes must be smaller than OxFF if (header[3] == (byte) 0xFF || header[4] == (byte) 0xFF) return false; // for safety's sake (who knows what future versions will bring), // the flags are not checked // last 4 bytes must be smaller than 0x80 (first bit set to 0) //noinspection RedundantIfStatement if ((header[6] & 0xFF) >= 0x80 || (header[7] & 0xFF) >= 0x80 || (header[8] & 0xFF) >= 0x80 || (header[9] & 0xFF) >= 0x80) return false; return true; } protected boolean readHeader(byte[] data) throws IOException { String id3v2Tag = new String(data, 0, ID3_SIZE, ISO_8859_1_ENCODING); if (id3v2Tag.equals(ID3V2TAG)) { int major = data[3] & 0xFF; int minor = data[4] & 0xFF; version = new ID3v2Version(major, minor); // read & read flags int flags = data[5] & 0xFF; unsynchronized = (flags & UNSYNCHRONIZATION_FLAG) != 0; extended = (flags & EXTENDED_HEADER_FLAG) != 0; experimental = (flags & EXPERIMENTAL_FLAG) != 0; footer = (flags & FOOTER_FLAG) != 0; // TODO read footer // size of the complete header is stored in 4 bytes, // which all have their highest bit set to 0 (unsynchronization) readSize = (data[9] & 0xFF) + ((data[8] & 0xFF) << 7) + ((data[7] & 0xFF) << 14) + ((data[6] & 0xFF) << 21); return true; } return false; } protected boolean readExtendedHeader(InputStream in) throws IOException { extendedHeader = new ID3v2ExtendedHeader(); return extendedHeader.read(in); } protected boolean readFrames(InputStream in) throws IOException { byte[] buffer = new byte[(int) readSize]; if (in.read(buffer, 0, buffer.length) != buffer.length) throw new IOException("Read invalid header"); // collect frames int frameBytes = 0; while (frameBytes < readSize) { ID3v2Frame frame = new ID3v2Frame(version); int parsedBytes = frame.parse(buffer, frameBytes); // found valid frame? if (parsedBytes > 0 && frame.isValid()) { add(frame); frameBytes += parsedBytes; } else frameBytes++; } return true; } // --- write object ---------------------------------------- /** * Writes the ID3v2 header to the OutputStream. * * @throws IOException if an error occurs */ public void write(OutputStream out) throws IOException { byte[] bytes = getBytes(); log.fine("Writing ID3v2 header (" + bytes.length + " bytes)"); out.write(bytes); if (extended) extendedHeader.write(out); } private long pad(long size) { long paddedSize = size + (64 - size % 64) + 64; assert paddedSize % 64 == 0; return paddedSize; } protected byte[] getBytes() throws UnsupportedEncodingException { int writeSize = (int) getWriteSize(); byte[] data = new byte[writeSize]; byte[] headerID3 = ID3V2TAG.getBytes(); System.arraycopy(headerID3, 0, data, 0, ID3_SIZE); byte[] headerVersion = version.getBytes(); System.arraycopy(headerVersion, 0, data, ID3_SIZE, ID3v2Version.VERSION_SIZE); byte flags = 0; if (unsynchronized) flags += UNSYNCHRONIZATION_FLAG; if (extended) flags += EXTENDED_HEADER_FLAG; if (experimental) flags += EXPERIMENTAL_FLAG; if (footer) flags += FOOTER_FLAG; byte[] headerFlags = new byte[]{flags}; System.arraycopy(headerFlags, 0, data, ID3_SIZE + ID3v2Version.VERSION_SIZE, FLAG_SIZE); // write frame size here (excluding header bytes) as LAME 3.93 does, too long frameSize = getFrameSize(); byte[] frameSizeData = new byte[SIZE_SIZE]; for (int i = 0; i < frameSizeData.length; i++) { frameSizeData[i] = (byte) ((frameSize >> ((3 - i) * 7)) & 0x7f); } // was: frameSizeData = BitConversion.create4BigEndian(contentSize); System.arraycopy(frameSizeData, 0, data, ID3_SIZE + ID3v2Version.VERSION_SIZE + FLAG_SIZE, SIZE_SIZE); int count = ID3_SIZE + ID3v2Version.VERSION_SIZE + SIZE_SIZE + FLAG_SIZE; for (ID3v2Frame f : frames) { byte[] frameData = f.getBytes(); // TODO don't want to write empty tags, think of a way to determine whether a tag is empty System.arraycopy(frameData, 0, data, count, frameData.length); count += frameData.length; } return data; } // --- get uobject ------------------------------------------ public boolean isValid() { return valid; } public ID3v2Version getVersion() { return version; } public long getReadSize() { return readSize > 0 ? readSize + ID3V2_HEADER_SIZE : 0; } /** * Return the padded size of this header including its frames. * * @return the padded size of this header including its frames */ public long getWriteSize() { long headerSize = getHeaderSize(); return headerSize > 0 ? pad(getHeaderSize()) : 0; } /** * Return the size of this header including its frames. * * @return the size of this header including its frames */ public long getHeaderSize() { long frameSize = getFrameSize(); return frameSize > 0 ? frameSize + ID3V2_HEADER_SIZE : 0; } /** * Return the summed size of the content + metadata of the * frames this header consists of. * * @return the summed size of the content of the frames * this header consists of */ public long getFrameSize() { long size = 0; for (ID3v2Frame f : frames) { size += f.getFrameSize(); } return size; } /** * Return the summed size of the content of the frames * this header consists of. * * @return the summed size of the content of the frames * this header consists of */ public long getContentSize() { long size = 0; for (ID3v2Frame f : frames) { size += f.getContentSize(); } return size; } /** * Returns true if the unsynchronization bit is set in this header. * * @return true if the unsynchronization bit is set in this header. */ public boolean isUnsynchronized() { return unsynchronized; } /** * Returns true if this tag has an extended header. * * @return true if this tag has an extended header */ public boolean isExtendedHeader() { return extended; } /** * Returns true if the experimental bit of this header is set. * * @return true if the experimental bit of this header is set */ public boolean isExperimental() { return experimental; } /** * Returns true if this tag has a footer. * * @return true if this tag has a footer */ public boolean isFooter() { return footer; } public List<ID3v2Frame> getFrames() { return frames; } public ID3v2Frame getFrame(String tagName) { for (ID3v2Frame f : frames) { if (f.isTagWithName(tagName)) { return f; } } return null; } public List<ID3v2Frame> getFrames(String tagName) { List<ID3v2Frame> result = new ArrayList<ID3v2Frame>(); for (ID3v2Frame f : frames) { if (f.isTagWithName(tagName)) result.add(f); } return result; } public ID3v2Frame getFrame(String tagName, String description, String language) { for (ID3v2Frame f : frames) { if (f.isTagWithName(tagName) && description.equals(f.getDescription()) && language.equals(f.getLanguage())) { return f; } } return null; } public String getStringContent(String tagName) { ID3v2Frame frame = getFrame(tagName); return frame != null ? frame.getStringContent() : null; } protected String getTextContent(String tagName, String description, String language) { ID3v2Frame frame = getFrame(tagName, description, language); return frame != null ? frame.getTextContent() : null; } protected String getTextContent(String tagName) { ID3v2Frame frame = getFrame(tagName); return frame != null ? frame.getTextContent() : null; } protected int getIntContent(String tagName) { String string = getStringContent(tagName); return parseInt(string, tagName); } protected Calendar getDateContent(String tagName) { String string = getStringContent(tagName); return string != null ? ISO8601.parse(string) : null; } private int parseInt(String string, String tagName) { try { if (string != null) return Integer.valueOf(string.trim()); } catch (NumberFormatException e) { log.severe("Invalid int value in tag " + tagName + ": '" + string + "' (" + string.length() + " chars)"); } return -1; } private int parseIndex(String tagName) { String indexString = getStringContent(tagName); if (indexString == null) return -1; // iTunes encodes trackindex and part of set index as index/count if (indexString.indexOf('/') != -1) indexString = indexString.substring(0, indexString.indexOf('/')); return parseInt(indexString, tagName); } private int parseCount(String tagName) { String indexString = getStringContent(tagName); if (indexString == null) return -1; if (indexString.indexOf('/') != -1) { indexString = indexString.substring(indexString.indexOf('/') + 1, indexString.length()); return parseInt(indexString, tagName); } // no slash found, no count found return -1; } protected byte[] getByteContent(String tagName) { ID3v2Frame frame = getFrame(tagName); return frame != null ? frame.getByteContent() : null; } private ID3v2Frame addID3v2Frame(String tagName, String description, String language) { tagName = getTagNameForAdd(tagName); ID3v2Frame frame = getFrame(tagName, description, language); if (frame == null) { frame = new ID3v2Frame(tagName, version); frame.setDescription(description); frame.setLanguage(language); add(frame); } return frame; } private String getTagNameForAdd(String tagName) { if (version.isObsolete()) { String obsoleteTagName = ID3v2Tag.findObsoleteTagName(tagName); if (obsoleteTagName != null) tagName = obsoleteTagName; } return tagName; } public ID3v2Frame addID3v2Frame(String tagName) { tagName = getTagNameForAdd(tagName); ID3v2Frame frame = getFrame(tagName); if (frame == null) { frame = new ID3v2Frame(tagName, version); add(frame); } return frame; } public void removeID3v2Frame(String tagName) { List<ID3v2Frame> frames = getFrames(tagName); for (ID3v2Frame f : frames) { remove(f); } } protected void setContent(String tagName, String value) { ID3v2Frame frame = addID3v2Frame(tagName); frame.setText(value); } protected void setContent(String tagName, int value) { setContent(tagName, Integer.toString(value)); } protected void setContent(String tagName, Calendar value) { setContent(tagName, ISO8601.format(value)); } protected void setContent(String tagName, byte[] value) { ID3v2Frame frame = addID3v2Frame(tagName); frame.setBytes(value); } // --- MetaData get ---------------------------------------- public String getTrack() { return getStringContent(TRACK_TAG_NAME); } public String getArtist() { return getStringContent(ARTIST_TAG_NAME); } public String getAlbumArtist() { return getStringContent(ALBUM_ARTIST_TAG_NAME); } public boolean isCompilation() { String compilation = getStringContent(COMPILATION_TAG_NAME); return compilation != null && compilation.equals("1"); } public String getAlbum() { return getStringContent(ALBUM_TAG_NAME); } public int getYear() { return getIntContent(YEAR_TAG_NAME); } public ID3Genre getGenre() { String genreStr = getStringContent(GENRE_TAG_NAME); return ID3Genre.findWellknown(genreStr); } public int getIndex() { return parseIndex(INDEX_TAG_NAME); } public int getCount() { return parseCount(INDEX_TAG_NAME); } public int getPartOfSetIndex() { return parseIndex(PART_OF_SET_INDEX_TAG_NAME); } public int getPartOfSetCount() { return parseCount(PART_OF_SET_INDEX_TAG_NAME); } public String getComment() { return getComment("", ""); } public String getComment(String description, String language) { return getTextContent(COMMENT_TAG_NAME, description, language); } public int getSeconds() { int seconds = getIntContent(SECONDS_TAG_NAME); return seconds > 0 ? seconds / 1000 : seconds; } public String getMusicBrainzId() { byte[] musicBrainzId = getByteContent(MUSICBRAINZ_ID_TAG_NAME); if (musicBrainzId == null || musicBrainzId.length == 0) return null; try { return new String(musicBrainzId, ISO_8859_1_ENCODING); } catch (UnsupportedEncodingException e) { log.severe("Cannot create string for music brainz id: " + e.getMessage() + ": " + e.getMessage()); return new String(musicBrainzId); } } public String getPublisher() { return getTextContent(PUBLISHER_TAG_NAME); } public byte[] getAttachedPicture() { return getByteContent(ATTACHED_PICTURE_TAG_NAME); } public String getLyrics(String description, String language) { return getTextContent(LYRICS_TAG_NAME, description, language); } public String getLyrics() { return getLyrics(LYRICS_DESCRIPTION, LYRICS_LANGUAGE); } public int getRating() { byte[] bytes = getByteContent(RATING_TAG_NAME); return bytes != null ? BitConversion.extract4BigEndian(bytes) : -1; } public int getPlayCount() { byte[] bytes = getByteContent(PLAY_COUNTER_TAG_NAME); return bytes != null ? BitConversion.extract4BigEndian(bytes) : -1; } public Calendar getPlayTime() { return getDateContent(ID3v2Header.PLAY_TIME_TAG_NAME); } public Calendar getTaggingTime() { return getDateContent(ID3v2Header.TAGGING_TIME_TAG_NAME); } public String getEncoder() { return getTextContent(ENCODER_TAG_NAME); } // --- set object ------------------------------------------ public void setValid(boolean valid) { this.valid = valid; } // --- MetaData set ---------------------------------------- public void setArtist(String newArtist) { setContent(ARTIST_TAG_NAME, newArtist); } public void setAlbumArtist(String newAlbumArtist) { setContent(ALBUM_ARTIST_TAG_NAME, newAlbumArtist); } public void setCompilation(boolean isCompilation) { setContent(COMPILATION_TAG_NAME, isCompilation ? "1" : "0"); } public void setAlbum(String newAlbum) { setContent(ALBUM_TAG_NAME, newAlbum); } public void setTrack(String newTrack) { setContent(TRACK_TAG_NAME, newTrack); } public void setYear(int newYear) { setContent(YEAR_TAG_NAME, newYear); } public void setGenre(ID3Genre newGenre) { String genreName = (newGenre != null) ? newGenre.getFormattedName() : ID3Genre.UNKNOWN; setContent(GENRE_TAG_NAME, genreName); } private void setIndexCount(String tagName, int newIndex, int newCount) { String indexString = ""; if (newIndex != -1) { indexString = Integer.toString(newIndex); if (newCount != -1) indexString += "/" + Integer.toString(newCount); } setContent(tagName, indexString); } public void setIndex(int newIndex) { setIndexCount(INDEX_TAG_NAME, newIndex, getCount()); } public void setCount(int newCount) { if (newCount < getIndex()) throw new IllegalArgumentException("Count " + newCount + " is smaller than index " + getIndex()); setIndexCount(INDEX_TAG_NAME, getIndex(), newCount); } public void setPartOfSetIndex(int newIndex) { setIndexCount(PART_OF_SET_INDEX_TAG_NAME, newIndex, getPartOfSetCount()); } public void setPartOfSetCount(int newCount) { if (newCount < getPartOfSetIndex()) throw new IllegalArgumentException("Count " + newCount + " is smaller than index " + getPartOfSetIndex()); setIndexCount(PART_OF_SET_INDEX_TAG_NAME, getPartOfSetIndex(), newCount); } public void setComment(String newComment) { setComment(newComment, "", ""); } public void setComment(String newComment, String description, String language) { ID3v2Frame frame = addID3v2Frame(COMMENT_TAG_NAME, description, language); frame.setText(newComment); } public void setSeconds(int newSeconds) { setContent(SECONDS_TAG_NAME, newSeconds * 1000); } public void setMusicBrainzId(String newMusicBrainzId) { ID3v2Frame frame = addID3v2Frame(MUSICBRAINZ_ID_TAG_NAME); frame.setDescription("http://musicbrainz.org"); try { frame.setBytes(newMusicBrainzId.getBytes(ISO_8859_1_ENCODING)); } catch (UnsupportedEncodingException e) { log.severe("Cannot get bytes from music brainz id: " + e.getMessage() + ": " + e.getMessage()); frame.setBytes(newMusicBrainzId.getBytes()); } } public void setPublisher(String newPublisher) { setContent(PUBLISHER_TAG_NAME, newPublisher); } public void setAttachedPicture(byte[] newAttachedPicture, MimeType mimeType, String description) { ID3v2Frame frame = addID3v2Frame(ATTACHED_PICTURE_TAG_NAME); frame.setDescription(description); frame.setPictureType(PictureType.getPictureType(0x03)); frame.setMimeType(mimeType); frame.setBytes(newAttachedPicture); } public void setCover(byte[] newCover) { byte[] resizedCover = new ImageResizer().resize(newCover, COVER_FORMAT, COVER_SIZE_LIMIT, COVER_SIZE_LIMIT); MimeType mimeType = new MimeTypeGuesser().guess(resizedCover); setAttachedPicture(resizedCover, mimeType, "cover"); } public void setLyrics(String newLyrics, String description, String language) { ID3v2Frame frame = addID3v2Frame(LYRICS_TAG_NAME, description, language); frame.setText(newLyrics); } public void setLyrics(String newLyrics) { setLyrics(newLyrics, LYRICS_DESCRIPTION, LYRICS_LANGUAGE); } public void setRating(int newRating) { ID3v2Frame frame = addID3v2Frame(RATING_TAG_NAME); frame.setBytes(BitConversion.create4BigEndian(newRating)); } public void setPlayCount(int newPlayCount) { ID3v2Frame frame = addID3v2Frame(PLAY_COUNTER_TAG_NAME); frame.setBytes(BitConversion.create4BigEndian(newPlayCount)); } public void setPlayTime(Calendar newPlayTime) { ID3v2Frame frame = addID3v2Frame(PLAY_TIME_TAG_NAME); frame.setText(ISO8601.format(newPlayTime)); } public void setTaggingTime(Calendar newTaggingTime) { ID3v2Frame frame = addID3v2Frame(TAGGING_TIME_TAG_NAME); frame.setText(ISO8601.format(newTaggingTime)); } // --- set object ------------------------------------------ public void setVersion(ID3v2Version version) { this.version = version; } /** * Set the unsynchronization flag for this header. * * @param unsynchronized the new value of the unsynchronization flag */ public void setUnsynchronized(boolean unsynchronized) { this.unsynchronized = unsynchronized; } /** * Set the value of the extended header bit of this header. * * @param extended the new value of the extended header bit */ public void setExtendedHeader(boolean extended) { this.extended = extended; } /** * Set the value of the experimental bit of this header. * * @param experimental the new value of the experimental bit */ public void setExperimental(boolean experimental) { this.experimental = experimental; } /** * Sets the value of the footer bit for this header. * * @param footer the new value of the footer bit for this header */ public void setFooter(boolean footer) { this.footer = footer; } /** * Add a ID3v2 frame * * @param frame the ID3v2 Frame to add */ public void add(ID3v2Frame frame) { frames.add(frame); } /** * Remove the given ID3v2Frame * * @param frame the ID3v2Frame to remove */ public void remove(ID3v2Frame frame) { List<ID3v2Frame> toRemove = new ArrayList<ID3v2Frame>(); for(ID3v2Frame f : frames) { if(f.getTagName().equals(frame.getTagName())) toRemove.add(f); } frames.removeAll(toRemove); } // --- overwrites Object ----------------------------------- public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("ID3v2Header[isValid=").append(isValid()); if (isValid()) { buffer.append(", "). append("release=").append(getVersion().getVersionString()).append(", "). append("unsynchronized=").append(isUnsynchronized()).append(", "). append("extended=").append(isExtendedHeader()).append(", "); if (isExtendedHeader()) buffer.append("extendHeader=").append(extendedHeader).append(", "); buffer.append("experimental=").append(isExperimental()).append(", "). append("footer=").append(isFooter()).append(", "). append("readSize=").append(getReadSize()).append(", "). append("contentSize=").append(getContentSize()).append(", "). append("track=").append(getTrack()).append(", "). append("artist=").append(getArtist()).append(", "). append("album=").append(getAlbum()).append(", "). append("year=").append(getYear()).append(", "). append("comment=").append(getComment()).append(", "). append("index=").append(getIndex()).append(", "). append("count=").append(getCount()).append(", "). append("genre=").append(getGenre()).append(", "). append("\nframes=["); for (ID3v2Frame f : getFrames()) { buffer.append(f).append(", "); } buffer.append("]"); } buffer.append("]"); return buffer.toString(); } // --- member variables ------------------------------------ /** * valid */ protected boolean valid; /** * header if read */ protected byte[] header; /** * ID3v2 data */ protected ID3v2Version version; protected ID3v2ExtendedHeader extendedHeader; protected long readSize; /** * flags */ protected boolean unsynchronized; protected boolean extended; protected boolean experimental; protected boolean footer; /** * frames */ protected List<ID3v2Frame> frames; }