diff options
Diffstat (limited to 'src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java')
-rw-r--r-- | src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java | 338 |
1 files changed, 338 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java b/src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java new file mode 100644 index 0000000..a083961 --- /dev/null +++ b/src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java @@ -0,0 +1,338 @@ +/* + * 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; + +class WAVEFileParser extends AudioFileParser implements ChunkHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(WAVEFileParser.class); + + static final short WAVE_FORMAT_PCM = 1; + static final short WAVE_FORMAT_IEEE_FLOAT = 3; + static final short WAVE_FORMAT_EXTENSIBLE = (short) 0xFFFE; + + static final byte[] KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = { + 3, 0, 0, 0, 0, 0, 16, 0, -128, 0, 0, -86, 0, 56, -101, 113 + }; + static final byte[] KSDATAFORMAT_SUBTYPE_PCM = { + 1, 0, 0, 0, 0, 0, 16, 0, -128, 0, 0, -86, 0, 56, -101, 113 + }; + + static final int WAVE_ID = ('W' << 24) | ('A' << 16) | ('V' << 8) | 'E'; + static final int FMT_ID = ('f' << 24) | ('m' << 16) | ('t' << 8) | ' '; + static final int DATA_ID = ('d' << 24) | ('a' << 16) | ('t' << 8) | 'a'; + static final int CUE_ID = ('c' << 24) | ('u' << 16) | ('e' << 8) | ' '; + static final int FACT_ID = ('f' << 24) | ('a' << 16) | ('c' << 8) | 't'; + static final int SMPL_ID = ('s' << 24) | ('m' << 16) | ('p' << 8) | 'l'; + static final int LTXT_ID = ('l' << 24) | ('t' << 16) | ('x' << 8) | 't'; + static final int LABL_ID = ('l' << 24) | ('a' << 16) | ('b' << 8) | 'l'; + + int samplesPerBlock = 0; + int blockAlign = 0; + private int numFactSamples = 0; + private short format; + + WAVEFileParser() { + } + + @Override + FloatSample finish() throws IOException { + if ((byteData == null)) { + throw new IOException("No data found in audio sample."); + } + float[] floatData = new float[numFrames * samplesPerFrame]; + if (bitsPerSample == 16) { + SampleLoader.decodeLittleI16ToF32(byteData, 0, byteData.length, floatData, 0); + } else if (bitsPerSample == 24) { + SampleLoader.decodeLittleI24ToF32(byteData, 0, byteData.length, floatData, 0); + } else if (bitsPerSample == 32) { + if (format == WAVE_FORMAT_IEEE_FLOAT) { + SampleLoader.decodeLittleF32ToF32(byteData, 0, byteData.length, floatData, 0); + } else if (format == WAVE_FORMAT_PCM) { + SampleLoader.decodeLittleI32ToF32(byteData, 0, byteData.length, floatData, 0); + } else { + throw new IOException("WAV: Unsupported format = " + format); + } + } else { + throw new IOException("WAV: Unsupported bitsPerSample = " + bitsPerSample); + } + + return makeSample(floatData); + } + + // typedef struct { + // long dwIdentifier; + // long dwPosition; + // ID fccChunk; + // long dwChunkStart; + // long dwBlockStart; + // long dwSampleOffset; + // } CuePoint; + + /* Parse various chunks encountered in WAV file. */ + void parseCueChunk(IFFParser parser, int ckSize) throws IOException { + int numCuePoints = parser.readIntLittle(); + if (IFFParser.debug) { + LOGGER.debug("WAV: numCuePoints = " + numCuePoints); + } + if ((ckSize - 4) != (6 * 4 * numCuePoints)) + throw new EOFException("Cue chunk too short!"); + for (int i = 0; i < numCuePoints; i++) { + int dwName = parser.readIntLittle(); /* dwName */ + int position = parser.readIntLittle(); // dwPosition + parser.skip(3 * 4); // fccChunk, dwChunkStart, dwBlockStart + int sampleOffset = parser.readIntLittle(); // dwPosition + + if (IFFParser.debug) { + LOGGER.debug("WAV: parseCueChunk: #" + i + ", dwPosition = " + position + + ", dwName = " + dwName + ", dwSampleOffset = " + sampleOffset); + } + SampleMarker cuePoint = findOrCreateCuePoint(dwName); + cuePoint.position = position; + } + } + + void parseLablChunk(IFFParser parser, int ckSize) throws IOException { + int dwName = parser.readIntLittle(); + int textLength = (ckSize - 4) - 1; // don't read NUL terminator + String text = parseString(parser, textLength); + if (IFFParser.debug) { + LOGGER.debug("WAV: label id = " + dwName + ", text = " + text); + } + SampleMarker cuePoint = findOrCreateCuePoint(dwName); + cuePoint.name = text; + } + + void parseLtxtChunk(IFFParser parser, int ckSize) throws IOException { + int dwName = parser.readIntLittle(); + int dwSampleLength = parser.readIntLittle(); + parser.skip(4 + (4 * 2)); // purpose through codepage + int textLength = (ckSize - ((4 * 4) + (4 * 2))) - 1; // don't read NUL + // terminator + if (textLength > 0) { + String text = parseString(parser, textLength); + if (IFFParser.debug) { + LOGGER.debug("WAV: ltxt id = " + dwName + ", dwSampleLength = " + + dwSampleLength + ", text = " + text); + } + SampleMarker cuePoint = findOrCreateCuePoint(dwName); + cuePoint.comment = text; + } + } + + void parseFmtChunk(IFFParser parser, int ckSize) throws IOException { + format = parser.readShortLittle(); + samplesPerFrame = parser.readShortLittle(); + frameRate = parser.readIntLittle(); + parser.readIntLittle(); /* skip dwAvgBytesPerSec */ + blockAlign = parser.readShortLittle(); + bitsPerSample = parser.readShortLittle(); + + if (IFFParser.debug) { + LOGGER.debug("WAV: format = 0x" + Integer.toHexString(format)); + LOGGER.debug("WAV: bitsPerSample = " + bitsPerSample); + LOGGER.debug("WAV: samplesPerFrame = " + samplesPerFrame); + } + bytesPerFrame = blockAlign; + bytesPerSample = bytesPerFrame / samplesPerFrame; + samplesPerBlock = (8 * blockAlign) / bitsPerSample; + + if (format == WAVE_FORMAT_EXTENSIBLE) { + int extraSize = parser.readShortLittle(); + short validBitsPerSample = parser.readShortLittle(); + int channelMask = parser.readIntLittle(); + byte[] guid = new byte[16]; + parser.read(guid); + if (IFFParser.debug) { + LOGGER.debug("WAV: extraSize = " + extraSize); + LOGGER.debug("WAV: validBitsPerSample = " + validBitsPerSample); + LOGGER.debug("WAV: channelMask = " + channelMask); + System.out.print("guid = {"); + for (int i = 0; i < guid.length; i++) { + System.out.print(guid[i] + ", "); + } + LOGGER.debug("}"); + } + if (matchBytes(guid, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) { + format = WAVE_FORMAT_IEEE_FLOAT; + } else if (matchBytes(guid, KSDATAFORMAT_SUBTYPE_PCM)) { + format = WAVE_FORMAT_PCM; + } + } + if ((format != WAVE_FORMAT_PCM) && (format != WAVE_FORMAT_IEEE_FLOAT)) { + throw new IOException( + "Only WAVE_FORMAT_PCM and WAVE_FORMAT_IEEE_FLOAT supported. format = " + format); + } + if ((bitsPerSample != 16) && (bitsPerSample != 24) && (bitsPerSample != 32)) { + throw new IOException( + "Only 16 and 24 bit PCM or 32-bit float WAV files supported. width = " + + bitsPerSample); + } + } + + private boolean matchBytes(byte[] bar1, byte[] bar2) { + if (bar1.length != bar2.length) + return false; + for (int i = 0; i < bar1.length; i++) { + if (bar1[i] != bar2[i]) + return false; + } + return true; + } + + private int convertByteToFrame(int byteOffset) throws IOException { + if (blockAlign == 0) { + throw new IOException("WAV file has bytesPerBlock = zero"); + } + if (samplesPerFrame == 0) { + throw new IOException("WAV file has samplesPerFrame = zero"); + } + return (samplesPerBlock * byteOffset) / (samplesPerFrame * blockAlign); + } + + private int calculateNumFrames(int numBytes) throws IOException { + int nFrames; + if (numFactSamples > 0) { + // nFrames = numFactSamples / samplesPerFrame; + nFrames = numFactSamples; // FIXME which is right + } else { + nFrames = convertByteToFrame(numBytes); + } + return nFrames; + } + + // Read fraction in range of 0 to 0xFFFFFFFF and + // convert to 0.0 to 1.0 range. + private double readFraction(IFFParser parser) throws IOException { + // Put L at end or we get -1. + long maxFraction = 0x0FFFFFFFFL; + // Get unsigned fraction. Have to fit in long. + long fraction = (parser.readIntLittle()) & maxFraction; + return (double) fraction / (double) maxFraction; + } + + void parseSmplChunk(IFFParser parser, int ckSize) throws IOException { + parser.readIntLittle(); // Manufacturer + parser.readIntLittle(); // Product + parser.readIntLittle(); // Sample Period + int unityNote = parser.readIntLittle(); + double pitchFraction = readFraction(parser); + originalPitch = unityNote + pitchFraction; + + parser.readIntLittle(); // SMPTE Format + parser.readIntLittle(); // SMPTE Offset + int numLoops = parser.readIntLittle(); + parser.readIntLittle(); // Sampler Data + + int lastCueID = Integer.MAX_VALUE; + for (int i = 0; i < numLoops; i++) { + int cueID = parser.readIntLittle(); + parser.readIntLittle(); // type + int loopStartPosition = parser.readIntLittle(); + // Point to sample one after. + int loopEndPosition = parser.readIntLittle() + 1; + // TODO handle fractional loop sizes? + double endFraction = readFraction(parser); + parser.readIntLittle(); // playCount + + // Use lowest numbered cue. + if (cueID < lastCueID) { + sustainBegin = loopStartPosition; + sustainEnd = loopEndPosition; + } + } + } + + void parseFactChunk(IFFParser parser, int ckSize) throws IOException { + numFactSamples = parser.readIntLittle(); + } + + void parseDataChunk(IFFParser parser, int ckSize) throws IOException { + long numRead; + dataPosition = parser.getOffset(); + if (ifLoadData) { + byteData = new byte[ckSize]; + numRead = parser.read(byteData); + } else { + numRead = parser.skip(ckSize); + } + if (numRead != ckSize) { + throw new EOFException("WAV data chunk too short! Read " + numRead + " instead of " + + ckSize); + } + numFrames = calculateNumFrames(ckSize); + } + + @Override + public void handleForm(IFFParser parser, int ckID, int ckSize, int type) throws IOException { + if ((ckID == IFFParser.RIFF_ID) && (type != WAVE_ID)) + throw new IOException("Bad WAV form type = " + IFFParser.IDToString(type)); + } + + /** + * Called by parse() method to handle chunks in a WAV specific manner. + * + * @param ckID four byte chunk ID such as 'data' + * @param ckSize size of chunk in bytes + * @return number of bytes left in chunk + */ + @Override + public void handleChunk(IFFParser parser, int ckID, int ckSize) throws IOException { + switch (ckID) { + case FMT_ID: + parseFmtChunk(parser, ckSize); + break; + case DATA_ID: + parseDataChunk(parser, ckSize); + break; + case CUE_ID: + parseCueChunk(parser, ckSize); + break; + case FACT_ID: + parseFactChunk(parser, ckSize); + break; + case SMPL_ID: + parseSmplChunk(parser, ckSize); + break; + case LABL_ID: + parseLablChunk(parser, ckSize); + break; + case LTXT_ID: + parseLtxtChunk(parser, ckSize); + break; + default: + break; + } + } + + /* + * (non-Javadoc) + * @see com.softsynth.javasonics.util.AudioSampleLoader#isLittleEndian() + */ + boolean isLittleEndian() { + return true; + } + +} |