/*
* 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;
}