/* 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.util.BitConversion; import slash.metamusic.util.StringHelper; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.logging.Logger; /** * My instances represent a ID3v1 tail of the MP3 frames as * described in http://www.id3.org/id3v1.html. * * @author Christian Pesch * @version $Id: ID3v1Tail.java 953 2007-01-21 15:31:50Z cpesch $ */ public class ID3v1Tail implements ID3MetaData { /** * Logging output */ protected static final Logger log = Logger.getLogger(ID3v1Tail.class.getName()); public static final String ID3V1TAG = "TAG"; public static final int TAG_SIZE = 3; public static final int ID3V1_SIZE = 128; public static final int TRACK_SIZE = 30; public static final int ARTIST_SIZE = 30; public static final int ALBUM_SIZE = 30; public static final int YEAR_SIZE = 4; public static final int INDEX_SIZE = 1; public static final int GENRE_SIZE = 1; public static final int COMMENT_SIZE = 28; /** * Encoding to use when converting from bytes to Unicode (String). */ protected static final String ENCODING = "ISO8859_1"; /** * Char to fill up strings with */ protected static final char EMPTY_CHAR = ' '; /** * Create a new (empty) tail. */ public ID3v1Tail() { this("", "", "", 0, new ID3Genre(""), 0, ""); } public ID3v1Tail(String artist, String album, String track, int index, ID3Genre genre, int year, String comment) { this.artist = artist; this.album = album; this.track = track; this.index = index; this.genre = genre; this.year = year; this.comment = comment; valid = true; id3v1dot1 = true; } public ID3v2Header toID3v2Header() { return new ID3v2Header(getArtist(), getAlbum(), getTrack(), getIndex(), getGenre(), getYear(), getComment() ); } // --- read/write object ----------------------------------- /** * Reads the ID3v1 tail from the InputStream. * * @param in the InputStream to read * @return if the read tail is valid * @throws NoID3v1TailException if no tail can be found or exists * @throws IOException if an error occurs */ public boolean read(InputStream in) throws NoID3v1TailException, IOException { valid = false; readSize = 0; log.fine("Reading ID3v1 tail"); // search for a valid tail boolean tailFound = false; while (checkForID3v1Tail(in)) { tailFound = parse(in); if (tailFound) { valid = true; readSize = ID3V1_SIZE; break; } } if (!tailFound) throw new NoID3v1TailException(); return valid; } /** * Check if ID3v1 tail is present * * @param in the InputStream to read * @return true if tail is present * @throws IOException if an error occurs */ protected boolean checkForID3v1Tail(InputStream in) throws IOException { byte tag[] = ID3V1TAG.getBytes(ENCODING); while (true) { // read through stream until T is read int t = in.read(); while (t != tag[0] && t != -1) { t = in.read(); } if (t == -1) { return false; } // now next byte must be A int a = in.read(); if (a == tag[1]) { // now next byte must be G int g = in.read(); if (g == tag[2]) { // synchronized return true; } if (g == -1) return false; } if (a == -1) { return false; } else { // continue search } } } protected boolean parse(InputStream in) throws NoID3v1TailException, IOException { int sizeToRead = ID3V1_SIZE; int sizeWhenTagRead = ID3V1_SIZE - ID3V1TAG.length(); // ID3V1TAG has been read, using modulo operation if // there is falsely more than one tail if (sizeWhenTagRead == in.available() % 128) sizeToRead = sizeWhenTagRead; byte[] buffer = new byte[ID3V1_SIZE]; if (in.read(buffer, ID3V1_SIZE - sizeToRead, sizeToRead) != sizeToRead) throw new NoID3v1TailException(); // ID3V1TAG has not been read yet, so check it if (sizeToRead == ID3V1_SIZE) { String id3v1Tag = new String(buffer, 0, 3, ENCODING); if (!id3v1Tag.equals(ID3V1TAG)) return false; } track = StringHelper.trim(new String(buffer, 3, ALBUM_SIZE, ENCODING)); artist = StringHelper.trim(new String(buffer, 33, ARTIST_SIZE, ENCODING)); album = StringHelper.trim(new String(buffer, 63, TRACK_SIZE, ENCODING)); year = BitConversion.stringToInt(new String(buffer, 93, YEAR_SIZE, ENCODING)); id3v1dot1 = (BitConversion.unsignedByteToInt(buffer[125]) == 0 && BitConversion.unsignedByteToInt(buffer[126]) > 0); index = id3v1dot1 ? BitConversion.unsignedByteToInt(buffer[126]) : -1; comment = StringHelper.trim(new String(buffer, 97, id3v1dot1 ? 28 : 30, ENCODING)); genre = new ID3Genre(BitConversion.unsignedByteToInt(buffer[127])); // last bytes read - this has been the tag return in.available() == 0; } /** * Writes the ID3v1 tail to the OutputStream. * * @param out the OutputStream to write to * @throws IOException if an error occurs */ public void write(OutputStream out) throws IOException { byte[] bytes = getBytes(); log.fine("Writing ID3v1 tail (" + bytes.length + " bytes)"); out.write(bytes); } protected byte[] getBytes() throws UnsupportedEncodingException { byte[] data = new byte[(int) getWriteSize()]; byte[] tag = ID3V1TAG.getBytes(ENCODING); System.arraycopy(tag, 0, data, 0, TAG_SIZE); byte[] trackArray = trimString(track, ALBUM_SIZE).getBytes(ENCODING); System.arraycopy(trackArray, 0, data, 3, ALBUM_SIZE); byte[] artistArray = trimString(artist, ARTIST_SIZE).getBytes(ENCODING); System.arraycopy(artistArray, 0, data, 33, ARTIST_SIZE); byte[] albumArray = trimString(album, TRACK_SIZE).getBytes(ENCODING); System.arraycopy(albumArray, 0, data, 63, TRACK_SIZE); byte[] yearArray = trimInt(year, YEAR_SIZE).getBytes(ENCODING); System.arraycopy(yearArray, 0, data, 93, YEAR_SIZE); byte[] commentArray = trimString(comment, getCommentSize()).getBytes(ENCODING); System.arraycopy(commentArray, 0, data, id3v1dot1 ? 97 : 95, getCommentSize()); if (id3v1dot1) { byte[] indexArray = new byte[]{0x0, (byte) index}; System.arraycopy(indexArray, 0, data, 125, INDEX_SIZE + 1); } byte[] genreArray = new byte[]{(byte) genre.getId()}; System.arraycopy(genreArray, 0, data, 127, GENRE_SIZE); return data; } private int getCommentSize() { return COMMENT_SIZE + (id3v1dot1 ? 0 : INDEX_SIZE); } private String trimString(String str, int len) { if (str == null) str = ""; if (str.length() > len) return str.substring(0, len - 1); StringBuffer buffer = new StringBuffer(str); while (buffer.length() < len) buffer.append(EMPTY_CHAR); return buffer.toString(); } private String trimInt(int value, int len) { StringBuffer buffer = new StringBuffer(Integer.toString(value)); while (buffer.length() < len) buffer.insert(0, EMPTY_CHAR); return buffer.toString(); } // --- get object ------------------------------------------ public boolean isValid() { return valid; } public boolean isID3v1dot1() { return id3v1dot1; } public long getReadSize() { return readSize; } private int length(String string) { return string != null ? string.length() : 0; } public long getContentSize() { return length(track) + length(artist) + length(album) + length(comment); } public long getWriteSize() { return getContentSize() > 0 ? ID3V1_SIZE : 0; } // --- MetaData get ---------------------------------------- public String getTrack() { return track; } public String getArtist() { return artist; } public String getAlbum() { return album; } public int getYear() { return year > 0 ? year : -1; } public ID3Genre getGenre() { return genre; } public String getComment() { return comment; } public int getIndex() { if (!isID3v1dot1()) throw new IllegalArgumentException("No track information exists in ID3v1"); return index; } // --- set object ------------------------------------------ public void setID3v1dot1(boolean isID3v1dot1) { this.id3v1dot1 = isID3v1dot1; // reduce comment size if (isID3v1dot1) setComment(getComment()); } public void setValid(boolean valid) { this.valid = valid; if (valid) setID3v1dot1(true); } // --- MetaData set ---------------------------------------- public void setTrack(String newTrack) { if (newTrack != null && newTrack.length() > ALBUM_SIZE) newTrack = newTrack.substring(0, ALBUM_SIZE); this.track = newTrack; } public void setArtist(String newArtist) { if (newArtist != null && newArtist.length() > ARTIST_SIZE) newArtist = newArtist.substring(0, ARTIST_SIZE); this.artist = newArtist; } public void setAlbum(String newAlbum) { if (newAlbum != null && newAlbum.length() > TRACK_SIZE) newAlbum = newAlbum.substring(0, TRACK_SIZE); this.album = newAlbum; } public void setYear(int newYear) { if (newYear < 0) newYear = 0; else if (newYear > 9999) newYear = 9999; this.year = newYear; } public void setGenre(ID3Genre newGenre) { this.genre = newGenre; } public void setIndex(int newIndex) { setID3v1dot1(true); if (newIndex < 0) newIndex = 0; else if (newIndex > 9999) newIndex = 9999; this.index = newIndex; } public void setComment(String newComment) { if (newComment != null && newComment.length() > getCommentSize()) newComment = newComment.substring(0, getCommentSize()); this.comment = newComment; } // --- overwrites Object ----------------------------------- public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("ID3v1Tail[isValid=").append(isValid()); if (isValid()) { buffer.append(", isID3v1.1=").append(isID3v1dot1()). append(", track=").append(getTrack()). append(", artist=").append(getArtist()). append(", album=").append(getAlbum()). append(", year=").append(getYear()). append(", comment=").append(getComment()); if (isID3v1dot1()) buffer.append(", index=").append(getIndex()); buffer.append(", genre=").append(getGenre()); } buffer.append("]"); return buffer.toString(); } // --- member variables ------------------------------------ /** * file data */ protected boolean valid; protected long readSize; /** * ID3v1 data */ protected boolean id3v1dot1; protected String track; protected String artist; protected String album; protected String comment; protected ID3Genre genre; protected int year; protected int index; }