/* * 21.04.2004 Original verion. davagin@udm.ru. *----------------------------------------------------------------------- * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. *---------------------------------------------------------------------- */ package davaguine.jmac.info; import davaguine.jmac.tools.ByteArrayReader; import davaguine.jmac.tools.File; import davaguine.jmac.tools.JMACException; import java.io.EOFException; import java.io.IOException; /** * Author: Dmitry Vaguine * Date: 04.03.2004 * Time: 14:51:31 */ public class APEHeader { public final static int MAC_FORMAT_FLAG_8_BIT = 1; // is 8-bit public final static int MAC_FORMAT_FLAG_CRC = 2; // uses the new CRC32 error detection public final static int MAC_FORMAT_FLAG_HAS_PEAK_LEVEL = 4; // unsigned __int32 Peak_Level after the header public final static int MAC_FORMAT_FLAG_24_BIT = 8; // is 24-bit public final static int MAC_FORMAT_FLAG_HAS_SEEK_ELEMENTS = 16; // has the number of seek elements after the peak level public final static int MAC_FORMAT_FLAG_CREATE_WAV_HEADER = 32; // create the wave header on decompression (not stored) public APEHeader(final File file) { m_pIO = file; } public void Analyze(APEFileInfo pInfo) throws IOException { // find the descriptor pInfo.nJunkHeaderBytes = FindDescriptor(true); if (pInfo.nJunkHeaderBytes < 0) throw new JMACException("Unsupported Format"); // read the first 8 bytes of the descriptor (ID and version) m_pIO.mark(10); final ByteArrayReader reader = new ByteArrayReader(m_pIO, 8); if (!reader.readString(4, "US-ASCII").equals("MAC ")) throw new JMACException("Unsupported Format"); int version = reader.readUnsignedShort(); m_pIO.reset(); if (version >= 3980) { // current header format AnalyzeCurrent(pInfo); } else { // legacy support AnalyzeOld(pInfo); } } protected void AnalyzeCurrent(APEFileInfo m_APEFileInfo) throws IOException { m_APEFileInfo.spAPEDescriptor = APEDescriptor.read(m_pIO); if ((m_APEFileInfo.spAPEDescriptor.nDescriptorBytes - APEDescriptor.APE_DESCRIPTOR_BYTES) > 0) m_pIO.skipBytes((int) (m_APEFileInfo.spAPEDescriptor.nDescriptorBytes - APEDescriptor.APE_DESCRIPTOR_BYTES)); final APEHeaderNew APEHeader = APEHeaderNew.read(m_pIO); if ((m_APEFileInfo.spAPEDescriptor.nHeaderBytes - APEHeaderNew.APE_HEADER_BYTES) > 0) m_pIO.skipBytes((int) (m_APEFileInfo.spAPEDescriptor.nHeaderBytes - APEHeaderNew.APE_HEADER_BYTES)); // fill the APE info structure m_APEFileInfo.nVersion = m_APEFileInfo.spAPEDescriptor.nVersion; m_APEFileInfo.nCompressionLevel = APEHeader.nCompressionLevel; m_APEFileInfo.nFormatFlags = APEHeader.nFormatFlags; m_APEFileInfo.nTotalFrames = (int) APEHeader.nTotalFrames; m_APEFileInfo.nFinalFrameBlocks = (int) APEHeader.nFinalFrameBlocks; m_APEFileInfo.nBlocksPerFrame = (int) APEHeader.nBlocksPerFrame; m_APEFileInfo.nChannels = APEHeader.nChannels; m_APEFileInfo.nSampleRate = (int) APEHeader.nSampleRate; m_APEFileInfo.nBitsPerSample = APEHeader.nBitsPerSample; m_APEFileInfo.nBytesPerSample = m_APEFileInfo.nBitsPerSample / 8; m_APEFileInfo.nBlockAlign = m_APEFileInfo.nBytesPerSample * m_APEFileInfo.nChannels; m_APEFileInfo.nTotalBlocks = (int) ((APEHeader.nTotalFrames == 0) ? 0 : ((APEHeader.nTotalFrames - 1) * m_APEFileInfo.nBlocksPerFrame) + APEHeader.nFinalFrameBlocks); m_APEFileInfo.nWAVHeaderBytes = (int) ((APEHeader.nFormatFlags & MAC_FORMAT_FLAG_CREATE_WAV_HEADER) > 0 ? WaveHeader.WAVE_HEADER_BYTES : m_APEFileInfo.spAPEDescriptor.nHeaderDataBytes); m_APEFileInfo.nWAVTerminatingBytes = (int) m_APEFileInfo.spAPEDescriptor.nTerminatingDataBytes; m_APEFileInfo.nWAVDataBytes = m_APEFileInfo.nTotalBlocks * m_APEFileInfo.nBlockAlign; m_APEFileInfo.nWAVTotalBytes = m_APEFileInfo.nWAVDataBytes + m_APEFileInfo.nWAVHeaderBytes + m_APEFileInfo.nWAVTerminatingBytes; m_APEFileInfo.nAPETotalBytes = m_pIO.isLocal() ? (int) m_pIO.length() : -1; m_APEFileInfo.nLengthMS = (int) ((m_APEFileInfo.nTotalBlocks * 1000L) / m_APEFileInfo.nSampleRate); m_APEFileInfo.nAverageBitrate = (m_APEFileInfo.nLengthMS <= 0) ? 0 : (int) ((m_APEFileInfo.nAPETotalBytes * 8L) / m_APEFileInfo.nLengthMS); m_APEFileInfo.nDecompressedBitrate = (m_APEFileInfo.nBlockAlign * m_APEFileInfo.nSampleRate * 8) / 1000; m_APEFileInfo.nSeekTableElements = (int) (m_APEFileInfo.spAPEDescriptor.nSeekTableBytes / 4); m_APEFileInfo.nPeakLevel = -1; // get the seek tables (really no reason to get the whole thing if there's extra) m_APEFileInfo.spSeekByteTable = new int[m_APEFileInfo.nSeekTableElements]; for (int i = 0; i < m_APEFileInfo.nSeekTableElements; i++) m_APEFileInfo.spSeekByteTable[i] = m_pIO.readIntBack(); // get the wave header if ((APEHeader.nFormatFlags & MAC_FORMAT_FLAG_CREATE_WAV_HEADER) <= 0) { if (m_APEFileInfo.nWAVHeaderBytes > Integer.MAX_VALUE) throw new JMACException("The HeaderBytes Parameter Is Too Big"); m_APEFileInfo.spWaveHeaderData = new byte[m_APEFileInfo.nWAVHeaderBytes]; try { m_pIO.readFully(m_APEFileInfo.spWaveHeaderData); } catch (EOFException e) { throw new JMACException("Can't Read Wave Header Data"); } } } protected void AnalyzeOld(APEFileInfo m_APEFileInfo) throws IOException { APEHeaderOld header = APEHeaderOld.read(m_pIO); // fail on 0 length APE files (catches non-finalized APE files) if (header.nTotalFrames == 0) throw new JMACException("Unsupported Format"); int nPeakLevel = -1; if ((header.nFormatFlags & MAC_FORMAT_FLAG_HAS_PEAK_LEVEL) > 0) nPeakLevel = m_pIO.readIntBack(); if ((header.nFormatFlags & MAC_FORMAT_FLAG_HAS_SEEK_ELEMENTS) > 0) m_APEFileInfo.nSeekTableElements = m_pIO.readIntBack(); else m_APEFileInfo.nSeekTableElements = (int) header.nTotalFrames; // fill the APE info structure m_APEFileInfo.nVersion = header.nVersion; m_APEFileInfo.nCompressionLevel = header.nCompressionLevel; m_APEFileInfo.nFormatFlags = header.nFormatFlags; m_APEFileInfo.nTotalFrames = (int) header.nTotalFrames; m_APEFileInfo.nFinalFrameBlocks = (int) header.nFinalFrameBlocks; m_APEFileInfo.nBlocksPerFrame = ((header.nVersion >= 3900) || ((header.nVersion >= 3800) && (header.nCompressionLevel == CompressionLevel.COMPRESSION_LEVEL_EXTRA_HIGH))) ? 73728 : 9216; if (header.nVersion >= 3950) m_APEFileInfo.nBlocksPerFrame = 73728 * 4; m_APEFileInfo.nChannels = header.nChannels; m_APEFileInfo.nSampleRate = (int) header.nSampleRate; m_APEFileInfo.nBitsPerSample = (m_APEFileInfo.nFormatFlags & MAC_FORMAT_FLAG_8_BIT) > 0 ? 8 : ((m_APEFileInfo.nFormatFlags & MAC_FORMAT_FLAG_24_BIT) > 0 ? 24 : 16); m_APEFileInfo.nBytesPerSample = m_APEFileInfo.nBitsPerSample / 8; m_APEFileInfo.nBlockAlign = m_APEFileInfo.nBytesPerSample * m_APEFileInfo.nChannels; m_APEFileInfo.nTotalBlocks = (int) ((header.nTotalFrames == 0) ? 0 : ((header.nTotalFrames - 1) * m_APEFileInfo.nBlocksPerFrame) + header.nFinalFrameBlocks); m_APEFileInfo.nWAVHeaderBytes = (int) ((header.nFormatFlags & MAC_FORMAT_FLAG_CREATE_WAV_HEADER) > 0 ? WaveHeader.WAVE_HEADER_BYTES : header.nHeaderBytes); m_APEFileInfo.nWAVTerminatingBytes = (int) header.nTerminatingBytes; m_APEFileInfo.nWAVDataBytes = m_APEFileInfo.nTotalBlocks * m_APEFileInfo.nBlockAlign; m_APEFileInfo.nWAVTotalBytes = m_APEFileInfo.nWAVDataBytes + m_APEFileInfo.nWAVHeaderBytes + m_APEFileInfo.nWAVTerminatingBytes; m_APEFileInfo.nAPETotalBytes = m_pIO.isLocal() ? (int) m_pIO.length() : -1; m_APEFileInfo.nLengthMS = (int) ((m_APEFileInfo.nTotalBlocks * 1000L) / m_APEFileInfo.nSampleRate); m_APEFileInfo.nAverageBitrate = (int) ((m_APEFileInfo.nLengthMS <= 0) ? 0 : ((m_APEFileInfo.nAPETotalBytes * 8L) / m_APEFileInfo.nLengthMS)); m_APEFileInfo.nDecompressedBitrate = (m_APEFileInfo.nBlockAlign * m_APEFileInfo.nSampleRate * 8) / 1000; m_APEFileInfo.nPeakLevel = nPeakLevel; // get the wave header if ((header.nFormatFlags & MAC_FORMAT_FLAG_CREATE_WAV_HEADER) <= 0) { if (header.nHeaderBytes > Integer.MAX_VALUE) throw new JMACException("The HeaderBytes Parameter Is Too Big"); m_APEFileInfo.spWaveHeaderData = new byte[(int) header.nHeaderBytes]; try { m_pIO.readFully(m_APEFileInfo.spWaveHeaderData); } catch (EOFException e) { throw new JMACException("Can't Read Wave Header Data"); } } // get the seek tables (really no reason to get the whole thing if there's extra) m_APEFileInfo.spSeekByteTable = new int[m_APEFileInfo.nSeekTableElements]; for (int i = 0; i < m_APEFileInfo.nSeekTableElements; i++) m_APEFileInfo.spSeekByteTable[i] = m_pIO.readIntBack(); if (header.nVersion <= 3800) { m_APEFileInfo.spSeekBitTable = new byte[m_APEFileInfo.nSeekTableElements]; try { m_pIO.readFully(m_APEFileInfo.spSeekBitTable); } catch (EOFException e) { throw new JMACException("Can't Read Seek Bit Table"); } } } protected int FindDescriptor(boolean bSeek) throws IOException { int nJunkBytes = 0; // We need to limit this method if m_pIO is represented as URL // We'll not support ID3 tags for such files if (m_pIO.isLocal()) { // figure the extra header bytes m_pIO.mark(1000); // skip an ID3v2 tag (which we really don't support anyway...) ByteArrayReader reader = new ByteArrayReader(10); reader.reset(m_pIO, 10); final String tag = reader.readString(3, "US-ASCII"); if (tag.equals("ID3")) { // why is it so hard to figure the lenght of an ID3v2 tag ?!? reader.readByte(); reader.readByte(); int byte5 = reader.readUnsignedByte(); int nSyncSafeLength; nSyncSafeLength = (reader.readUnsignedByte() & 127) << 21; nSyncSafeLength += (reader.readUnsignedByte() & 127) << 14; nSyncSafeLength += (reader.readUnsignedByte() & 127) << 7; nSyncSafeLength += (reader.readUnsignedByte() & 127); boolean bHasTagFooter = false; if ((byte5 & 16) > 0) { bHasTagFooter = true; nJunkBytes = nSyncSafeLength + 20; } else { nJunkBytes = nSyncSafeLength + 10; } // error check if ((byte5 & 64) > 0) { // this ID3v2 length calculator algorithm can't cope with extended headers // we should be ok though, because the scan for the MAC header below should // really do the trick } m_pIO.skipBytes(nJunkBytes - 10); // scan for padding (slow and stupid, but who cares here...) if (!bHasTagFooter) { while (m_pIO.read() == 0) nJunkBytes++; } } m_pIO.reset(); m_pIO.skipBytes(nJunkBytes); } m_pIO.mark(1000); // scan until we hit the APE header, the end of the file, or 1 MB later int nGoalID = ('M' << 24) | ('A' << 16) | ('C' << 8) | (' '); int nReadID = m_pIO.readInt(); // Also, lets suppose that MAC header placed in beginning of file in case of external source of file if(m_pIO.isLocal()) { int nScanBytes = 0; while (nGoalID != nReadID && nScanBytes < (1024 * 1024)) { nReadID = (nReadID << 8) | m_pIO.readByte(); nJunkBytes++; nScanBytes++; } } if (nGoalID != nReadID) nJunkBytes = -1; // seek to the proper place (depending on result and settings) if (bSeek && (nJunkBytes != -1)) { // successfully found the start of the file (seek to it and return) m_pIO.reset(); m_pIO.skipBytes(nJunkBytes); m_pIO.mark(1000); } else { // restore the original file pointer m_pIO.reset(); } return nJunkBytes; } protected File m_pIO; }