diff options
Diffstat (limited to 'src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java')
-rw-r--r-- | src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java b/src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java new file mode 100644 index 0000000..89b443c --- /dev/null +++ b/src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java @@ -0,0 +1,232 @@ +/* + * Copyright 2009 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.util.soundfile; + +import java.io.EOFException; +import java.io.IOException; + +import com.jsyn.data.FloatSample; +import com.jsyn.data.SampleMarker; +import com.jsyn.util.SampleLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AIFFFileParser extends AudioFileParser { + + private static final Logger LOGGER = LoggerFactory.getLogger(AIFFFileParser.class); + + private static final String SUPPORTED_FORMATS = "Only 16 and 24 bit PCM or 32-bit float AIF files supported."; + static final int AIFF_ID = ('A' << 24) | ('I' << 16) | ('F' << 8) | 'F'; + static final int AIFC_ID = ('A' << 24) | ('I' << 16) | ('F' << 8) | 'C'; + static final int COMM_ID = ('C' << 24) | ('O' << 16) | ('M' << 8) | 'M'; + static final int SSND_ID = ('S' << 24) | ('S' << 16) | ('N' << 8) | 'D'; + static final int MARK_ID = ('M' << 24) | ('A' << 16) | ('R' << 8) | 'K'; + static final int INST_ID = ('I' << 24) | ('N' << 16) | ('S' << 8) | 'T'; + static final int NONE_ID = ('N' << 24) | ('O' << 16) | ('N' << 8) | 'E'; + static final int FL32_ID = ('F' << 24) | ('L' << 16) | ('3' << 8) | '2'; + static final int FL32_ID_LC = ('f' << 24) | ('l' << 16) | ('3' << 8) | '2'; + + int sustainBeginID = -1; + int sustainEndID = -1; + int releaseBeginID = -1; + int releaseEndID = -1; + boolean typeFloat = false; + + @Override + FloatSample finish() throws IOException { + setLoops(); + + if ((byteData == null)) { + throw new IOException("No data found in audio sample."); + } + float[] floatData = new float[numFrames * samplesPerFrame]; + if (bitsPerSample == 16) { + SampleLoader.decodeBigI16ToF32(byteData, 0, byteData.length, floatData, 0); + } else if (bitsPerSample == 24) { + SampleLoader.decodeBigI24ToF32(byteData, 0, byteData.length, floatData, 0); + } else if (bitsPerSample == 32) { + if (typeFloat) { + SampleLoader.decodeBigF32ToF32(byteData, 0, byteData.length, floatData, 0); + } else { + SampleLoader.decodeBigI32ToF32(byteData, 0, byteData.length, floatData, 0); + } + } else { + throw new IOException(SUPPORTED_FORMATS + " size = " + bitsPerSample); + } + + return makeSample(floatData); + } + + double read80BitFloat() throws IOException { + /* + * This is not a full decoding of the 80 bit number but it should suffice for the range we + * expect. + */ + byte[] bytes = new byte[10]; + parser.read(bytes); + int exp = ((bytes[0] & 0x3F) << 8) | (bytes[1] & 0xFF); + int mant = ((bytes[2] & 0xFF) << 16) | ((bytes[3] & 0xFF) << 8) | (bytes[4] & 0xFF); + // LOGGER.debug( "exp = " + exp + ", mant = " + mant ); + return mant / (double) (1 << (22 - exp)); + } + + void parseCOMMChunk(IFFParser parser, int ckSize) throws IOException { + samplesPerFrame = parser.readShortBig(); + numFrames = parser.readIntBig(); + bitsPerSample = parser.readShortBig(); + frameRate = read80BitFloat(); + if (ckSize > 18) { + int format = parser.readIntBig(); + // Validate data format. + if ((format == FL32_ID) || (format == FL32_ID_LC)) { + typeFloat = true; + } else if (format == NONE_ID) { + typeFloat = false; + } else { + throw new IOException(SUPPORTED_FORMATS + " format " + IFFParser.IDToString(format)); + } + } + + bytesPerSample = (bitsPerSample + 7) / 8; + bytesPerFrame = bytesPerSample * samplesPerFrame; + } + + /* parse tuning and multi-sample info */ + @SuppressWarnings("unused") + void parseINSTChunk(IFFParser parser, int ckSize) throws IOException { + int baseNote = parser.readByte(); + int detune = parser.readByte(); + originalPitch = baseNote + (0.01 * detune); + + int lowNote = parser.readByte(); + int highNote = parser.readByte(); + + parser.skip(2); /* lo,hi velocity */ + int gain = parser.readShortBig(); + + int playMode = parser.readShortBig(); /* sustain */ + sustainBeginID = parser.readShortBig(); + sustainEndID = parser.readShortBig(); + + playMode = parser.readShortBig(); /* release */ + releaseBeginID = parser.readShortBig(); + releaseEndID = parser.readShortBig(); + } + + private void setLoops() { + SampleMarker cuePoint = cueMap.get(sustainBeginID); + if (cuePoint != null) { + sustainBegin = cuePoint.position; + } + cuePoint = cueMap.get(sustainEndID); + if (cuePoint != null) { + sustainEnd = cuePoint.position; + } + } + + void parseSSNDChunk(IFFParser parser, int ckSize) throws IOException { + long numRead; + // LOGGER.debug("parseSSNDChunk()"); + int offset = parser.readIntBig(); + parser.readIntBig(); /* blocksize */ + parser.skip(offset); + dataPosition = parser.getOffset(); + int numBytes = ckSize - 8 - offset; + if (ifLoadData) { + byteData = new byte[numBytes]; + numRead = parser.read(byteData); + } else { + numRead = parser.skip(numBytes); + } + if (numRead != numBytes) + throw new EOFException("AIFF data chunk too short!"); + } + + void parseMARKChunk(IFFParser parser, int ckSize) throws IOException { + long startOffset = parser.getOffset(); + int numCuePoints = parser.readShortBig(); + // LOGGER.debug( "parseCueChunk: numCuePoints = " + numCuePoints + // ); + for (int i = 0; i < numCuePoints; i++) { + // Some AIF files have a bogus numCuePoints so check to see if we + // are at end. + long numInMark = parser.getOffset() - startOffset; + if (numInMark >= ckSize) { + LOGGER.debug("Reached end of MARK chunk with bogus numCuePoints = " + + numCuePoints); + break; + } + + int uniqueID = parser.readShortBig(); + int position = parser.readIntBig(); + int len = parser.read(); + String markerName = parseString(parser, len); + if ((len & 1) == 0) { + parser.skip(1); /* skip pad byte */ + } + + SampleMarker cuePoint = findOrCreateCuePoint(uniqueID); + cuePoint.position = position; + cuePoint.name = markerName; + + if (IFFParser.debug) { + LOGGER.debug("AIFF Marker at " + position + ", " + markerName); + } + } + } + + /** + * Called by parse() method to handle FORM chunks in an AIFF specific manner. + * + * @param ckID four byte chunk ID such as 'data' + * @param ckSize size of chunk in bytes + * @exception IOException If parsing fails, or IO error occurs. + */ + @Override + public void handleForm(IFFParser parser, int ckID, int ckSize, int type) throws IOException { + if ((ckID == IFFParser.FORM_ID) && (type != AIFF_ID) && (type != AIFC_ID)) + throw new IOException("Bad AIFF form type = " + IFFParser.IDToString(type)); + } + + /** + * Called by parse() method to handle chunks in an AIFF specific manner. + * + * @param ckID four byte chunk ID such as 'data' + * @param ckSize size of chunk in bytes + * @exception IOException If parsing fails, or IO error occurs. + */ + @Override + public void handleChunk(IFFParser parser, int ckID, int ckSize) throws IOException { + switch (ckID) { + case COMM_ID: + parseCOMMChunk(parser, ckSize); + break; + case SSND_ID: + parseSSNDChunk(parser, ckSize); + break; + case MARK_ID: + parseMARKChunk(parser, ckSize); + break; + case INST_ID: + parseINSTChunk(parser, ckSize); + break; + default: + break; + } + } + +} |