/* 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. */ /* * Copyright (c) 2000, Ronald Lenk * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice unmodified, this list of conditions, and the following * disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ package slash.metamusic.discid; import slash.metamusic.mp3.util.TimeConversion; import slash.metamusic.util.LibraryLoader; import slash.metamusic.util.OperationSystem; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.util.Arrays; import java.util.logging.Logger; /** * A class to represent the information contained in a Compact Disc * table of contents. * * @author Christian Pesch based on work from Ronald Lenk * @version $Id: DiscId.java 959 2007-03-11 08:21:11Z cpesch $ */ public class DiscId implements Serializable { /** * Logging output */ protected static final Logger log = Logger.getLogger(DiscId.class.getName()); private static boolean libraryLoaded = false; static { try { LibraryLoader.loadLibrary(DiscId.class.getClassLoader(), "discid"); libraryLoaded = true; } catch (IOException e) { log.severe("Cannot load native library 'discid': " + e.getMessage()); } } /** * Read the track offset array from the specified device. The * method returns the number of tracks in the TOC, not including * the leadout track, or -1 if an error occurs. */ private static native int deviceRead(String deviceName, int trackOffset[]) throws IOException; /** * Number of frames one second on a CD consists of. */ public static final int FRAMES_PER_SECOND = 75; /** * The disc id of the disc. */ private int discId; /** * The number of tracks in the disc's table of contents. If the * table of contents has not been read from the device, or the * last read resulted in an error, the value is -1. */ private int trackCount; /** * The array containing the frame offset for each track on the * disc: trackOffsets[0] through trackOffsets[trackCount - 1] * contain the offsets for the tracks on the disc, * trackOffsets[trackCount] contains the frame offset of the * leadout area on the disc. This array is created by the * constructor, and always contains 100 elements, regardless of * the number of tracks actually on the disc. */ private int trackOffsets[]; /** * The length of the disc in milliseconds. */ private int discLength; /** * Is true if the DiscId data has been read successfully. */ private boolean valid; /** * Return whether the DiscId calculation is supported on this plattform. * * @return true, if the DiscId calculation is supported on this plattform */ public static boolean isSupported() { return libraryLoaded; } /** * Construct a <code>DiscId</code> object with the given data. */ public DiscId(int discId, int trackCount, int[] trackOffsets, int discLength, boolean valid) { this.discId = discId; this.trackCount = trackCount; this.trackOffsets = trackOffsets; this.discLength = discLength; this.valid = valid; } /** * Construct a <code>DiscId</code> object with the given data. */ public DiscId(String discId, int trackCount, int[] trackOffsets, int discLength, boolean valid) { this(decodeDiscId(discId), trackCount, trackOffsets, discLength, valid); } /** * Construct an (invalid) <code>DiscId</code> object. * Call read() to have a valid DiscId object. */ public DiscId() { this(-1, -1, new int[100], -1, false); } /** * Calculate a <code>DiscId</code> object from the given data. */ public DiscId(int trackCount, int[] trackOffsets) { calculateData(trackCount, trackOffsets); } /** * Construct a <code>DiscId</code> object, and attempt to read * the table of contents from the device specified in * <code>device</code>. * * @param device A <code>File</code> for the CD device to read from. * On Solaris, this is the physical device, such as "/dev/rdsk/c0t2d0s0". * On Windows, this should always be specified as the "cdaudio" pseudo device. * On Linux, this is the physical device, such as "/dev/sdb" or "/dev/hdb". * @throws IOException If an error occurred while * reading from the specified device. */ public DiscId(File device) throws IOException { this(); read(device); } private void calculateData(int trackCount, int[] readOffsets) { this.trackOffsets = new int[100]; System.arraycopy(readOffsets, 0, trackOffsets, 0, readOffsets.length); if (trackCount > 99) { log.warning("TOC claims to have too many tracks (" + trackCount + "), limiting to 99"); trackCount = 99; } // not checking the last track int positiveFrameSizeTrackCount = 0; for (int i = 0, c = trackCount; i < c; i++) { int trackLength = trackOffsets[i + 1] - trackOffsets[i]; if (trackLength > 0) positiveFrameSizeTrackCount++; } boolean filledUpEntries = trackCount == 99 && trackOffsets[positiveFrameSizeTrackCount] == 150 && trackOffsets[trackCount] == 375; boolean negativeTrackSize = trackOffsets[trackCount - 2] > trackOffsets[trackCount - 1] && trackOffsets[trackCount] == 375; if (filledUpEntries) { log.warning("TOC is copy protected by filling up entries"); trackCount = positiveFrameSizeTrackCount; trackOffsets[0] = 150; trackOffsets[trackCount] = trackOffsets[trackCount - 1] + 300; } else if (negativeTrackSize) { log.warning("TOC is copy protected with negative track size"); trackCount--; trackOffsets[trackCount] = trackOffsets[trackCount - 1] + 600; } else if (positiveFrameSizeTrackCount != trackCount) { log.warning("TOC says " + trackCount + " tracks, but only " + positiveFrameSizeTrackCount + " tracks with positive size"); trackCount = positiveFrameSizeTrackCount; } this.trackCount = trackCount; this.discLength = trackOffsets[trackCount] / FRAMES_PER_SECOND - trackOffsets[0] / FRAMES_PER_SECOND; int counter = 0; for (int i = 0, c = trackCount; i < c; i++) { int trackOffset = trackOffsets[i] / FRAMES_PER_SECOND; while (trackOffset > 0) { counter += trackOffset % 10; trackOffset /= 10; } } this.discId = (counter & 0xFF) << 24 | discLength << 8 | trackCount; // CDEx and Feurio do not remove the first track offset from the disc length // if(negativeTrackSize) // discLength += trackOffsets[0] / FRAMES_PER_SECOND; this.valid = discId != -1 && trackCount > 0 && trackOffsets.length > 0 && discLength > 0; } /** * Read the table of contents from the device. * * @param device A <code>File</code> for the CD device to read from. * On Solaris, this is the physical device, such as "/dev/rdsk/c0t2d0s0". * On Windows, this should always be specified as the "cdaudio" pseudo device. * On Linux, this is the physical device, such as "/dev/sdb" or "/dev/hdb". * @throws IOException If an error occurs while * reading from the device. */ public void read(File device) throws IOException { if (!libraryLoaded) throw new UnsupportedOperationException("Native 'discid' library not loaded"); int[] trackOffsets = new int[100]; // do not absolutize path here since that make Windows "cdaudio" device useless int trackCount = deviceRead(device.getPath(), trackOffsets); calculateData(trackCount, trackOffsets); } /** * Returns if the DiscId has been read successfully. * * @return true if the DiscId has been read successfully */ public boolean isValid() { return valid; } /** * Return the disc id. */ public int getDiscId() { return discId; } /** * Get the number of tracks on the disc, not including the leadout * track. If the table of contents has not been read, or the last * read resulted in an error, the result is undefined. * <p/> * Note: this value is used to query the FreeDB * * @return The number of tracks on the disc, not including the * leadout track. */ public int getTrackCount() { return trackCount; } /** * Compute the FreeDB disc identifier using the algorithm specified in * the server documentation and return it as hexadecimal encoded string * ready to feed into FreeDB requests. */ public String getEncodedDiscId() { return encodeDiscId(getDiscId()); } /** * Convert the discId from an int to a format required for FreeDB queries. * <p/> * Freedb access requires that the software computes a "disc ID" which is * an identifier that is used to access thefreedb. The disc ID is a * 8-digit hexadecimal (base-16) number, computed using data from a CD's * Table-of-Contents (TOC) in MSF (Minute Second Frame) form. * The algorithm is listed below in Appendix A. * * @param discId the discId as an int * @return the discId encoded as above * @see #decodeDiscId(String) */ public static String encodeDiscId(int discId) { String encoded = Integer.toHexString(discId); while (encoded.length() < 8) encoded = "0" + encoded; return encoded; } /** * Convert the discId from the format required for FreeDB * queries to an int. If the discId is not valid, -1 is returned * * @param discId the discId as a String * @return the discId as an int * @see #encodeDiscId(int) */ public static int decodeDiscId(String discId) { try { return Long.valueOf(discId, 16).intValue(); } catch (NumberFormatException e) { return -1; } } /** * Get the length of the disc in terms of the number of 2352 byte * (1/75 sec) frames. If the table of contents has not been read, * or the last read resulted in an error, the result is undefined. * * @return The length of the disc in terms of 2352 byte (1/75 sec) * frames. */ public int getDiscLengthFrames() { return getTrackStartFrame(getTrackCount()) - getTrackStartFrame(0); } /** * Get the length of the disc including the leadin in milliseconds. * If the table of contents has not been read, or the last read * resulted in an error, the result is undefined. * * @return The length of the disc in milliseconds. */ public int getDiscLengthMillis() { return getDiscLengthSeconds() * 1000; } /** * Get the length of the disc including the leadin in seconds. * If the table of contents has not been read, or the last read * resulted in an error, the result is undefined. * * @return the length of the disc in seconds. */ public int getDiscLengthSeconds() { return discLength; } /** * Return the query string which may be used for a FreeDB query. * * @return the query string which may be used to for a FreeDB query */ public String getFreeDBQueryString() { StringBuffer buffer = new StringBuffer(); buffer.append(getEncodedDiscId()).append(" ").append(getTrackCount()).append(" "); for (int i = 0, c = getTrackCount(); i < c; i++) { buffer.append(getTrackStartFrame(i)).append(" "); } buffer.append(getDiscLengthSeconds()); return buffer.toString(); } /** * Get the starting frame of the specified track. If the table of * contents has not been read, or the last read resulted in an * error, the result is undefined. * <p/> * Note: this value is used to access the FreeDB * * @param trackNumber An integer containing the track number, * @return The starting frame of the specified track. */ public int getTrackStartFrame(int trackNumber) { return trackOffsets[trackNumber]; } /** * Get the length of the specified track in 2352 byte (1/75 sec) * frames. If the table of contents has not been read, or the last * read resulted in an error, the result is undefined. * * @param trackNumber An integer containing the track number. * @return The length of the track in frames. */ public int getTrackLengthFrames(int trackNumber) { int endFrame = trackNumber + 1 < trackOffsets.length ? getTrackStartFrame(trackNumber + 1) : getDiscLengthSeconds() * FRAMES_PER_SECOND; int startFrame = getTrackStartFrame(trackNumber); return endFrame - startFrame; } /** * Get the length of the specified track in milliseconds. * If the table of contents has not been read, or the last * read resulted in an error, result is undefined. * * @param trackNumber An integer containing the track number. * @return The length of the track in milliseconds. */ public int getTrackLengthMillis(int trackNumber) { return (int) Math.ceil(getTrackLengthFrames(trackNumber) * 1000.0 / FRAMES_PER_SECOND); } /** * Get the length of the specified track in seconds. * If the table of contents has not been read, or the last * read resulted in an error, result is undefined. * * @param trackNumber An integer containing the track number. * @return The length of the track in seconds. */ public int getTrackLengthSeconds(int trackNumber) { return (int) Math.ceil(getTrackLengthFrames(trackNumber) / FRAMES_PER_SECOND); } public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof DiscId)) return false; final DiscId discId1 = (DiscId) o; if (discId != discId1.discId) return false; if (discLength != discId1.discLength) return false; if (trackCount != discId1.trackCount) return false; if (valid != discId1.valid) return false; if (!Arrays.equals(trackOffsets, discId1.trackOffsets)) return false; return true; } public int hashCode() { int result; result = discId; result = 29 * result + trackCount; result = 29 * result + discLength; result = 29 * result + (valid ? 1 : 0); for (int trackOffset : trackOffsets) result = 29 * result + trackOffset; return result; } public String toString() { return super.toString() + "[discId=" + getEncodedDiscId() + ", trackCount=" + getTrackCount() + ", discLengthSeconds=" + getDiscLengthSeconds() + "]"; } public static void main(String[] args) throws Exception { String device = args.length == 0 ? OperationSystem.getDefaultDeviceName() : args[0]; DiscId discId = new DiscId(new File(device)); System.out.print("FreeDB query string: " + discId.getFreeDBQueryString()); System.out.println(); System.out.println("disc id: " + discId.getEncodedDiscId()); System.out.println("track count: " + discId.getTrackCount()); System.out.println("disc length: " + discId.getDiscLengthSeconds() + " seconds " + "(" + TimeConversion.getTimeFromSeconds(discId.getDiscLengthMillis() / 1000) + ") " + discId.getDiscLengthFrames() + " frames"); for (int i = 0, c = discId.getTrackCount(); i < c; i++) { System.out.println(i + ". track: " + discId.getTrackLengthMillis(i) / 1000 + " seconds (" + TimeConversion.getTimeFromMilliSeconds(discId.getTrackLengthMillis(i)) + ") " + discId.getTrackLengthFrames(i) + " frames"); } System.exit(0); } }