/*
* @(#)AiffParser.java 1.14 02/08/21
*
* Copyright (c) 1996-2002 Sun Microsystems, Inc. All rights reserved.
*/
package com.sun.media.parser.audio;
import java.io.IOException;
import javax.media.Time;
import javax.media.Duration;
import javax.media.Track;
import javax.media.BadHeaderException;
import javax.media.protocol.PullSourceStream;
import javax.media.protocol.Positionable;
import javax.media.Format;
import javax.media.format.AudioFormat;
import javax.media.protocol.ContentDescriptor;
import com.sun.media.parser.BasicPullParser;
import com.sun.media.parser.BasicTrack;
import com.sun.media.util.SettableTime;
/**
* TODO: find out about the wave chunk in aiff files with "32 bit integer compression"
* Why does the header say AIFF instead of AIFC? Why does the header say 32 bits/pixel
* even though when I created in in QT3.0, it said 16 bits/pixel.
*/
public class AiffParser extends BasicPullParser {
private Time duration = Duration.DURATION_UNKNOWN;
private Format format = null;
private Track[] tracks = new Track[1]; // Only 1 track is there for aiff
private int numBuffers = 4; // TODO: check
private int bufferSize = -1;
private int dataSize;
private SettableTime mediaTime = new SettableTime(0L);
private PullSourceStream stream = null;
private int maxFrame;
private int blockSize = 0;
private double sampleRate = -1.0;
private long minLocation;
private long maxLocation;
private String encodingString = null;
private int samplesPerBlock = 1;
private double timePerBlockNano = -1.0;
private double locationToMediaTime = -1;
public final static String FormID = "FORM"; // ID for Form Chunk
public final static String FormatVersionID = "FVER"; // ID for Format Version Chunk
public final static String CommonID = "COMM"; // ID for Common Chunk
public final static String SoundDataID = "SSND"; // ID for Sound Data Chunk
private static ContentDescriptor[] supportedFormat = new ContentDescriptor[] {new ContentDescriptor("audio.x_aiff")};
public final static int CommonIDSize = 18; // ID for Common Chunk for AIFF
private boolean isAIFC = false;
private boolean commonChunkSeen = false; // COMM chunk mandatory
private boolean soundDataChunkSeen = false; // COMM chunk mandatory
private boolean formatVersionChunkSeen = false; // mandatory for aifc
public ContentDescriptor [] getSupportedInputContentDescriptors() {
return supportedFormat;
}
public Track[] getTracks() throws IOException, BadHeaderException {
if (tracks[0] != null)
return tracks;
stream = (PullSourceStream) streams[0];
if (cacheStream != null) {
// Disable jitter buffer during parsing of the header
cacheStream.setEnabledBuffering(false);
}
readHeader();
if (cacheStream != null) {
cacheStream.setEnabledBuffering(true);
}
tracks[0] = new AiffTrack((AudioFormat) format,
/*enabled=*/ true,
new Time(0),
numBuffers,
bufferSize,
minLocation,
maxLocation
);
return tracks;
}
/**
* Creats format and computes bufferSize
*/
private void /* for now void */ readHeader()
throws IOException, BadHeaderException {
boolean signed = true;
String magic = readString(stream);
if (!(magic.equals(FormID))) {
throw new BadHeaderException("AIFF Parser: expected string " +
FormID + ", got "
+ magic);
}
int fileLength = readInt(stream) + 8;
String formType = readString(stream);
if (formType.equals("AIFC")) {
isAIFC = true;
} else {
encodingString = AudioFormat.LINEAR; // and signed is true
}
int remainingLength = fileLength - 12;
String compressionType = null;
int offset = 0;
int channels = -1;
int sampleSizeInBits = -1;
while (remainingLength >= 8) {
String type = readString(stream);
int size = readInt(stream);
remainingLength -= 8;
/**
* The Format Version Chunk contains a timestamp field that indicates
* when the format version of this AIFF-C file was defined. This in
* turn indicates what format rules this file conforms to and allows
* you to ensure that your application can handle a particular AIFF-C
* file. Every AIFF-C file must contain one and only one Format Version
* Chunk
*/
if (type.equals(FormatVersionID)) {
if (!isAIFC) {
// System.err.println("Warning: AIFF file shouldn't have Format version chunk");
}
int timestamp = readInt(stream);
if (size != 4) {
throw new BadHeaderException("Illegal FormatVersionID: chunk size is not 4 but "
+ size);
}
formatVersionChunkSeen = true;
} else if (type.equals(CommonID)) {
if (size < CommonIDSize) {
throw new BadHeaderException("Size of COMM chunk should be atleast " +
CommonIDSize);
}
channels = readShort(stream);
if ( channels < 1 )
throw new BadHeaderException("Number of channels is " + channels);
maxFrame = readInt(stream);
sampleSizeInBits = readShort(stream);
if (sampleSizeInBits <= 0) {
throw new BadHeaderException("Illegal sampleSize " + sampleSizeInBits);
}
sampleRate = readIeeeExtended(stream);
if ( sampleRate < 0 )
throw new BadHeaderException("Negative Sample Rate " + sampleRate);
int remainingCommSize = size - CommonIDSize;
if (isAIFC) {
/**
* AIFC files have compressionType and compressionName
* as extra fields
*/
if (remainingCommSize < 4) {
throw new BadHeaderException("COMM chunk in AIFC doesn't have compressionType info");
}
compressionType = readString(stream);
if (compressionType == null) {
throw new BadHeaderException("Compression type for AIFC is null");
}
skip(stream, remainingCommSize-4); // skip compressionName
}
commonChunkSeen = true;
} else if (type.equals(SoundDataID)) {
if (soundDataChunkSeen) {
throw new BadHeaderException("Cannot have more than 1 Sound Data Chunk");
}
offset = readInt(stream);
blockSize = readInt(stream);
minLocation = getLocation(stream);
dataSize = size - 8;
maxLocation = minLocation + dataSize; // TODO: Verify
soundDataChunkSeen = true;
if (commonChunkSeen) {
// parsing of mandatory chunks done
remainingLength -= 8;
break;
}
skip(stream, size-8);
} else {
// System.err.println("Chunk " + type + " not handled");
skip(stream, size);
}
remainingLength -= size;
}
/**
* Commented out the following even though it is valid, because
* in order to significantly speedup up parsing time (especially
* in the http case) the optional chunks following the Sound Data
* Chunk (SSND) are not processed unless the mandatory chunk
* COMM comes after SSND. Skipping past the typically large SSND
* chunk to parse optional chunks will cause the parse time to be high
* when using a slow http connection. Though the FVER chunk in the case
* of AIFC (compressed format) is mandatory for AIFC, it is not
* currently used; therefore unless it comes before the mandatory
* COMM or SSND chunks, it is not processed. Since the FVER chunk may
* come after the SSND chunk and hence not processed, the following code
* to check for it is commented out.
*/
// if (isAIFC && !formatVersionChunkSeen) {
// throw new BadHeaderException("Mandatory chunk FVER not present in AIFC file");
// }
if ( !commonChunkSeen ) {
throw new BadHeaderException("Mandatory chunk COMM missing");
}
if ( !soundDataChunkSeen ) {
throw new BadHeaderException("Mandatory chunk SSND missing");
}
double durationSeconds = -1;
if (isAIFC) {
String c = compressionType;
if (c.equalsIgnoreCase("NONE"))
encodingString = AudioFormat.LINEAR;
else if (c.equalsIgnoreCase("twos"))
encodingString = AudioFormat.LINEAR;
else if (c.equalsIgnoreCase("raw")) {
encodingString = AudioFormat.LINEAR;
signed = false;
} else if ( c.equalsIgnoreCase("ULAW")) {
encodingString = AudioFormat.ULAW;
sampleSizeInBits = 8;
signed = false;
} else if ( c.equalsIgnoreCase("ALAW")) {
encodingString = AudioFormat.ALAW;
sampleSizeInBits = 8;
signed = false;
} else if ( c.equalsIgnoreCase("G723")) {
/**
* For some reason, aiff files
* specify block size incorrectly as 0
* for mac3, mac6, ima4
* So, compute blockSize
*/
/**
* TODO: get/compute samplesPerBlock
* If block size is incorrectly specified
* as 0, compute it if possible
*/
encodingString = AudioFormat.G723;
// samplesPerBlock = ???? TODO
// blockSize = ???? TODO
// timePerBlockNano = ???? TODO
} else if ( c.equalsIgnoreCase("MAC3")) {
// 'MAC3' Samples have been compressed using MACE 3:1.
encodingString = AudioFormat.MAC3;
// 2 bytes represent 6 samples
blockSize = 2;
samplesPerBlock = 6;
timePerBlockNano = (samplesPerBlock * 1.0E9) / sampleRate;
} else if ( c.equalsIgnoreCase("MAC6")) {
// 'MAC6' Samples have been compressed using MACE 6:1.
encodingString = AudioFormat.MAC6;
// 1 byte represent 6 samples
blockSize = 1;
samplesPerBlock = 6;
timePerBlockNano = (samplesPerBlock * 1.0E9) / sampleRate;
} else if ( c.equalsIgnoreCase("IMA4")) {
encodingString = AudioFormat.IMA4;
/**
* Each packet contains 64 samples. Each sample is 4 bits/channel.
* So 64 samples is 32 bytes/channel.
* The 2 in the equation refers two bytes that the Apple's
* IMA compressor puts at the front of each packet, which
* are referred to as predictor bytes
*/
blockSize = (32 + 2) * channels;
samplesPerBlock = 64;
timePerBlockNano = (samplesPerBlock * 1.0E9) / sampleRate;
}
else
throw new BadHeaderException("Unsupported encoding" + c);
}
if (blockSize == 0)
blockSize = channels * sampleSizeInBits / 8;
/**
* There are few aiff files that have the maxFrame value in the
* aiff header as 0. maxFrame is needed to compute media duration.
* maxFrame can also be computed as (dataSize / blockSize).
* But files that report maxFrame as 0 also report a very high and
* incorrect number for dataSize.
* This number, typically 2130706432, is much higher that the
* fileSize. The maxFrame that is computed is obviously wrong.
* So, maxFrame is not computed if header says that it is 0.
* Examples of such files are bark16.aiff, nick16.aiff, pcm.aiff.
* Apple's MoviePlayer is unable to play these files.
* Since aiff is an Apple format, we can assume that these files
* are illegal or malformed aiff files.
* JMF will, however, play these files. Duration will be unknown
* until end of Media. After end of media, dataSize can be
* computed as (maxLocation - minLocation) and duration can
* be computed.
* Ironically, for files that report a corect maxFrame in the
* header, the computed maxFrame (ie dataSize/blockSize) agrees
* with that number.
*/
// if (maxFrame == 0) {
// System.out.println("maxFrame is 0, computing it");
// maxFrame = dataSize / blockSize;
//}
bufferSize = blockSize * (int) (sampleRate / samplesPerBlock);
durationSeconds = (maxFrame * samplesPerBlock) / sampleRate;
// System.out.println("durationSeconds is " + durationSeconds);
if (durationSeconds > 0)
duration = new Time(durationSeconds);
locationToMediaTime = samplesPerBlock / (sampleRate * blockSize);
format = new AudioFormat(encodingString,
sampleRate,
sampleSizeInBits,
channels,
AudioFormat.BIG_ENDIAN,
signed ? AudioFormat.SIGNED : AudioFormat.UNSIGNED,
/*frameSizeInBits=*/ blockSize*8,
Format.NOT_SPECIFIED, // No FRAME_RATE specified
Format.byteArray);
// System.out.println("format is " + format);
}
public Time setPosition(Time where, int rounding) {
if (! seekable ) {
return getMediaTime();
}
long time = where.getNanoseconds();
long newPos;
if (time < 0)
time = 0;
// timePerBlockNano is only computed for compressed formats
// where mutiple samples are packed into a packet or block
if (timePerBlockNano == -1) {
// LINEAR, ULAW, ALAW
// TODO: Precalculate constant expressions
int bytesPerSecond = (int) sampleRate * blockSize;
double newPosd = time * sampleRate * blockSize / 1000000000.0;
double remainder = (newPosd % blockSize);
newPos = (long) (newPosd - remainder);
if (remainder > 0) {
switch (rounding) {
case Positionable.RoundUp:
newPos += blockSize;
break;
case Positionable.RoundNearest:
if (remainder > (blockSize / 2.0))
newPos += blockSize;
break;
}
}
} else {
// IMA4, MAC3, MAC6
double blockNum = time / timePerBlockNano;
int blockNumInt = (int) blockNum;
double remainder = blockNum - blockNumInt;
if (remainder > 0) {
switch (rounding) {
case Positionable.RoundUp:
blockNumInt++;
break;
case Positionable.RoundNearest:
if (remainder > 0.5)
blockNumInt++;
break;
}
}
newPos = blockNumInt * blockSize;
}
// if ( newPos > maxLocation )
// newPos = maxLocation;
newPos += minLocation;
((BasicTrack) tracks[0]).setSeekLocation(newPos);
if (cacheStream != null) {
synchronized(this) {
// cacheStream.setPosition(where.getNanoseconds());
cacheStream.abortRead();
}
}
return where;
}
public Time getMediaTime() {
long location;
long seekLocation = ((BasicTrack) tracks[0]).getSeekLocation();
if (seekLocation != -1)
location = seekLocation - minLocation;
else
location = getLocation(stream) - minLocation;
synchronized(mediaTime) {
mediaTime.set( location * locationToMediaTime);
}
return mediaTime;
}
public Time getDuration() {
if (maxFrame <= 0) {
if ( tracks[0] != null ) {
long mediaSizeAtEOM = ((BasicTrack) tracks[0]).getMediaSizeAtEOM();
if (mediaSizeAtEOM > 0) {
maxFrame = (int) (mediaSizeAtEOM / blockSize);
double durationSeconds = (maxFrame * samplesPerBlock) / sampleRate;
if (durationSeconds > 0)
duration = new Time(durationSeconds);
}
}
}
return duration;
}
/**
* Returns a descriptive name for the plug-in.
* This is a user readable string.
*/
public String getName() {
return "Parser for AIFF file format";
}
class AiffTrack extends BasicTrack {
private double sampleRate;
private float timePerFrame; // TODO calculate this
private SettableTime frameToTime = new SettableTime();
AiffTrack(AudioFormat format, boolean enabled, Time startTime,
int numBuffers, int bufferSize,
long minLocation, long maxLocation) {
super(AiffParser.this,
format, enabled, AiffParser.this.duration,
startTime, numBuffers, bufferSize,
AiffParser.this.stream, minLocation, maxLocation);
double sampleRate = format.getSampleRate();
int channels = format.getChannels();
int sampleSizeInBits = format.getSampleSizeInBits();
float bytesPerSecond;
float samplesPerFrame;
// timePerBlockNano is only computed for compressed formats
// where mutiple samples are packed into a packet or block
if (timePerBlockNano == -1) {
// LINEAR, ULAW, ALAW
bytesPerSecond = (float) (sampleRate * blockSize);
timePerFrame = bufferSize / bytesPerSecond;
} else {
// IMA4, MAC3, MAC6
float blocksPerFrame = bufferSize / (float) blockSize;
samplesPerFrame = blocksPerFrame * samplesPerBlock;
timePerFrame = (float) (samplesPerFrame / sampleRate);
}
}
AiffTrack(AudioFormat format, boolean enabled, Time startTime,
int numBuffers, int bufferSize) {
this(format, enabled,
startTime, numBuffers, bufferSize,
0L, Long.MAX_VALUE);
}
}
/**
* read_ieee_extended
* Extended precision IEEE floating-point conversion routine.
* @argument DataInputStream
* @return double
* @exception IOException
*/
private double readIeeeExtended(PullSourceStream stream)
throws IOException {
double f = 0;
int expon = 0;
long hiMant = 0, loMant = 0;
long t1, t2;
double huge = 3.40282346638528860e+38;
int s;
expon = readShort(stream);
hiMant = readInt(stream);
if (hiMant < 0) // 2's complement
hiMant += 4294967296L;
loMant = readInt(stream);
if (loMant < 0) // 2's complement
loMant += 4294967296L;
if (expon == 0 && hiMant == 0 && loMant == 0) {
f = 0;
} else {
if (expon == 0x7FFF)
f = huge;
else {
expon -= 16383;
expon -= 31;
f = (hiMant * Math.pow(2, expon));
expon -= 32;
f += (loMant * Math.pow(2, expon));
}
}
return f;
}
}
|