aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java')
-rw-r--r--src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java338
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;
+ }
+
+}