/* For Copyright and License see LICENSE.txt and COPYING.txt in the root directory */ /** * */ package com.nerdscentral.audio.io; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; public class SFWavSubsystem { private enum IOState { READING, WRITING, CLOSED } private final static int BUFFER_SIZE = 4096; private final static int FMT_CHUNK_ID = 0x20746D66; private final static int DATA_CHUNK_ID = 0x61746164; private final static int RIFF_CHUNK_ID = 0x46464952; private final static int RIFF_TYPE_ID = 0x45564157; private File file; // File that will be // read from or written // to private IOState ioState; // Specifies the IO // State of the Wav // File (used for // snaity checking) private int bytesPerSample; // Number of bytes // required to store a // single sample private long numFrames; // Number of frames // within the data // section private FileOutputStream oStream; // Output stream used // for writting data private FileInputStream iStream; // Input stream used // for reading data private double doubleScale; // Scaling factor used // for int <-> double // conversion private double doubleOffset; // Offset factor used // for int <-> double // conversion private boolean wordAlignAdjust; // Specify if an extra // byte at the end of // the data chunk is // required for word // alignment // Wav Header private int numChannels; // 2 bytes unsigned, // 0x0001 (1) to 0xFFFF // (65,535) private long sampleRate; // 4 bytes unsigned, // 0x00000001 (1) to // 0xFFFFFFFF // (4,294,967,295) // Although a java int // is 4 bytes, it is // signed, so need to // use a long private int blockAlign; // 2 bytes unsigned, // 0x0001 (1) to 0xFFFF // (65,535) private int validBits; // 2 bytes unsigned, // 0x0002 (2) to 0xFFFF // (65,535) // Buffering private final byte[] buffer; // Local buffer used // for IO private int bufferPointer; // Points to the // current position in // local buffer private int bytesRead; // Bytes read after // last read into local // buffer private long frameCounter; // Current number of // frames read or // written // Cannot instantiate WavFile directly, must either use newWavFile() or // openWavFile() private SFWavSubsystem() { this.buffer = new byte[BUFFER_SIZE]; } public int getNumChannels() { return this.numChannels; } public long getNumFrames() { return this.numFrames; } public long getFramesRemaining() { return this.numFrames - this.frameCounter; } public long getSampleRate() { return this.sampleRate; } public int getValidBits() { return this.validBits; } public static SFWavSubsystem newWavFile(File file, int numChannels, long numFrames, int validBits, long sampleRate) throws IOException, SFWavFileException { // Instantiate new Wavfile and initialise SFWavSubsystem wavFile = new SFWavSubsystem(); wavFile.file = file; wavFile.numChannels = numChannels; wavFile.numFrames = numFrames; wavFile.sampleRate = sampleRate; wavFile.bytesPerSample = (validBits + 7) / 8; wavFile.blockAlign = wavFile.bytesPerSample * numChannels; wavFile.validBits = validBits; // Sanity check arguments if (numChannels < 1 || numChannels > 65535) throw new SFWavFileException( "Illegal number of channels, valid range 1 to 65536"); //$NON-NLS-1$ if (numFrames < 0) throw new SFWavFileException("Number of frames must be positive"); //$NON-NLS-1$ if (validBits < 2 || validBits > 65535) throw new SFWavFileException( "Illegal number of valid bits, valid range 2 to 65536"); //$NON-NLS-1$ if (sampleRate < 0) throw new SFWavFileException("Sample rate must be positive"); //$NON-NLS-1$ // Create output stream for writing data wavFile.oStream = new FileOutputStream(file); // Calculate the chunk sizes long dataChunkSize = wavFile.blockAlign * numFrames; long mainChunkSize = 4 + // Riff Type 8 + // Format ID and size 16 + // Format data 8 + // Data ID and size dataChunkSize; // Chunks must be word aligned, so if odd number of audio data bytes // adjust the main chunk size if (dataChunkSize % 2 == 1) { mainChunkSize += 1; wavFile.wordAlignAdjust = true; } else { wavFile.wordAlignAdjust = false; } // Set the main chunk size putLE(RIFF_CHUNK_ID, wavFile.buffer, 0, 4); putLE(mainChunkSize, wavFile.buffer, 4, 4); putLE(RIFF_TYPE_ID, wavFile.buffer, 8, 4); // Write out the header wavFile.oStream.write(wavFile.buffer, 0, 12); // Put format data in buffer long averageBytesPerSecond = sampleRate * wavFile.blockAlign; putLE(FMT_CHUNK_ID, wavFile.buffer, 0, 4); // Chunk ID putLE(16, wavFile.buffer, 4, 4); // Chunk Data Size putLE(1, wavFile.buffer, 8, 2); // Compression Code (Uncompressed) putLE(numChannels, wavFile.buffer, 10, 2); // Number of channels putLE(sampleRate, wavFile.buffer, 12, 4); // Sample Rate putLE(averageBytesPerSecond, wavFile.buffer, 16, 4); // Average Bytes // Per Second putLE(wavFile.blockAlign, wavFile.buffer, 20, 2); // Block Align putLE(validBits, wavFile.buffer, 22, 2); // Valid Bits // Write Format Chunk wavFile.oStream.write(wavFile.buffer, 0, 24); // Start Data Chunk putLE(DATA_CHUNK_ID, wavFile.buffer, 0, 4); // Chunk ID putLE(dataChunkSize, wavFile.buffer, 4, 4); // Chunk Data Size // Write Format Chunk wavFile.oStream.write(wavFile.buffer, 0, 8); // Calculate the scaling factor for converting to a normalised double if (wavFile.validBits > 8) { // If more than 8 validBits, data is signed // Conversion required multiplying by magnitude of max positive // value wavFile.doubleOffset = 0; wavFile.doubleScale = Long.MAX_VALUE >> (64 - wavFile.validBits); } else { // Else if 8 or less validBits, data is unsigned // Conversion required dividing by max positive value wavFile.doubleOffset = 1; wavFile.doubleScale = 0.5 * ((1 << wavFile.validBits) - 1); } // Finally, set the IO State wavFile.bufferPointer = 0; wavFile.bytesRead = 0; wavFile.frameCounter = 0; wavFile.ioState = IOState.WRITING; return wavFile; } public static SFWavSubsystem openWavFile(File file) throws IOException, SFWavFileException { // Instantiate new Wavfile and store the file reference SFWavSubsystem wavFile = new SFWavSubsystem(); wavFile.file = file; // Create a new file input stream for reading file data wavFile.iStream = new FileInputStream(file); // Read the first 12 bytes of the file int bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 12); if (bytesRead != 12) throw new SFWavFileException("Not enough wav file bytes for header"); //$NON-NLS-1$ // Extract parts from the header long riffChunkID = getLE(wavFile.buffer, 0, 4); long chunkSize = getLE(wavFile.buffer, 4, 4); long riffTypeID = getLE(wavFile.buffer, 8, 4); // Check the header bytes contains the correct signature if (riffChunkID != RIFF_CHUNK_ID) throw new SFWavFileException("Invalid Wav Header data, incorrect riff chunk ID"); //$NON-NLS-1$ if (riffTypeID != RIFF_TYPE_ID) throw new SFWavFileException("Invalid Wav Header data, incorrect riff type ID"); //$NON-NLS-1$ // Check that the file size matches the number of bytes listed in header if (file.length() != chunkSize + 8) { throw new SFWavFileException("Header chunk size (" + chunkSize //$NON-NLS-1$ + ") does not match file size (" + file.length() + ")");} //$NON-NLS-1$ //$NON-NLS-2$ boolean foundFormat = false; boolean foundData = false; // Search for the Format and Data Chunks while (true) { // Read the first 8 bytes of the chunk (ID and chunk size) bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 8); if (bytesRead == -1) throw new SFWavFileException("Reached end of file without finding format chunk"); //$NON-NLS-1$ if (bytesRead != 8) throw new SFWavFileException("Could not read chunk header"); //$NON-NLS-1$ // Extract the chunk ID and Size long chunkID = getLE(wavFile.buffer, 0, 4); chunkSize = getLE(wavFile.buffer, 4, 4); // Word align the chunk size // chunkSize specifies the number of bytes holding data. However, // the data should be word aligned (2 bytes) so we need to calculate // the actual number of bytes in the chunk long numChunkBytes = (chunkSize % 2 == 1) ? chunkSize + 1 : chunkSize; if (chunkID == FMT_CHUNK_ID) { // Flag that the format chunk has been found foundFormat = true; // Read in the header info bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 16); // Check this is uncompressed data int compressionCode = (int) getLE(wavFile.buffer, 0, 2); if (compressionCode != 1) throw new SFWavFileException("Compression Code " + compressionCode + " not supported"); //$NON-NLS-1$ //$NON-NLS-2$ // Extract the format information wavFile.numChannels = (int) getLE(wavFile.buffer, 2, 2); wavFile.sampleRate = getLE(wavFile.buffer, 4, 4); wavFile.blockAlign = (int) getLE(wavFile.buffer, 12, 2); wavFile.validBits = (int) getLE(wavFile.buffer, 14, 2); if (wavFile.numChannels == 0) throw new SFWavFileException( "Number of channels specified in header is equal to zero"); //$NON-NLS-1$ if (wavFile.blockAlign == 0) throw new SFWavFileException("Block Align specified in header is equal to zero"); //$NON-NLS-1$ if (wavFile.validBits < 2) throw new SFWavFileException("Valid Bits specified in header is less than 2"); //$NON-NLS-1$ if (wavFile.validBits > 64) throw new SFWavFileException( "Valid Bits specified in header is greater than 64, this is greater than a long can hold"); //$NON-NLS-1$ // Calculate the number of bytes required to hold 1 sample wavFile.bytesPerSample = (wavFile.validBits + 7) / 8; if (wavFile.bytesPerSample * wavFile.numChannels != wavFile.blockAlign) throw new SFWavFileException( "Block Align does not agree with bytes required for validBits and number of channels"); //$NON-NLS-1$ // Account for number of format bytes and then skip over // any extra format bytes numChunkBytes -= 16; if (numChunkBytes > 0) wavFile.iStream.skip(numChunkBytes); } else if (chunkID == DATA_CHUNK_ID) { // Check if we've found the format chunk, // If not, throw an exception as we need the format information // before we can read the data chunk if (foundFormat == false) throw new SFWavFileException("Data chunk found before Format chunk"); //$NON-NLS-1$ // Check that the chunkSize (wav data length) is a multiple of // the // block align (bytes per frame) if (chunkSize % wavFile.blockAlign != 0) throw new SFWavFileException( "Data Chunk size is not multiple of Block Align"); //$NON-NLS-1$ // Calculate the number of frames wavFile.numFrames = chunkSize / wavFile.blockAlign; // Flag that we've found the wave data chunk foundData = true; break; } else { // If an unknown chunk ID is found, just skip over the chunk // data wavFile.iStream.skip(numChunkBytes); } } // Throw an exception if no data chunk has been found if (foundData == false) throw new SFWavFileException("Did not find a data chunk"); //$NON-NLS-1$ // Calculate the scaling factor for converting to a normalised double if (wavFile.validBits > 8) { // If more than 8 validBits, data is signed // Conversion required dividing by magnitude of max negative value wavFile.doubleOffset = 0; wavFile.doubleScale = 1 << (wavFile.validBits - 1); } else { // Else if 8 or less validBits, data is unsigned // Conversion required dividing by max positive value wavFile.doubleOffset = -1; wavFile.doubleScale = 0.5 * ((1 << wavFile.validBits) - 1); } wavFile.bufferPointer = 0; wavFile.bytesRead = 0; wavFile.frameCounter = 0; wavFile.ioState = IOState.READING; return wavFile; } // Get and Put little endian data from local buffer // ------------------------------------------------ private static long getLE(byte[] buffer, final int posIn, final int numBytesIn) { int numBytes = numBytesIn - 1; int pos = posIn + numBytes; long val = buffer[pos] & 0xFF; for (int b = 0; b < numBytes; b++) val = (val << 8) + (buffer[--pos] & 0xFF); return val; } private static void putLE(final long valIn, byte[] buffer, final int posIn, final int numBytes) { long val = valIn; int pos = posIn; for (int b = 0; b < numBytes; b++) { buffer[pos] = (byte) (val & 0xFF); val >>= 8; pos++; } } // Sample Writing and Reading // -------------------------- private void writeSample(final long valIn) throws IOException { long val = valIn; for (int b = 0; b < this.bytesPerSample; b++) { if (this.bufferPointer == BUFFER_SIZE) { this.oStream.write(this.buffer, 0, BUFFER_SIZE); this.bufferPointer = 0; } this.buffer[this.bufferPointer] = (byte) (val & 0xFF); val >>= 8; this.bufferPointer++; } } private long readSample() throws IOException, SFWavFileException { long val = 0; for (int b = 0; b < this.bytesPerSample; b++) { if (this.bufferPointer == this.bytesRead) { int read = this.iStream.read(this.buffer, 0, BUFFER_SIZE); if (read == -1) throw new SFWavFileException("Not enough data available"); //$NON-NLS-1$ this.bytesRead = read; this.bufferPointer = 0; } int v = this.buffer[this.bufferPointer]; if (b < this.bytesPerSample - 1 || this.bytesPerSample == 1) v &= 0xFF; val += v << (b * 8); this.bufferPointer++; } return val; } // Integer // ------- public int readFrames(int[] sampleBuffer, int numFramesToRead) throws IOException, SFWavFileException { return readFrames(sampleBuffer, 0, numFramesToRead); } public int readFrames(final int[] sampleBuffer, final int offsetIn, final int numFramesToRead) throws IOException, SFWavFileException { if (this.ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance"); //$NON-NLS-1$ int offSet = offsetIn; for (int f = 0; f < numFramesToRead; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) { sampleBuffer[offSet] = (int) readSample(); offSet++; } this.frameCounter++; } return numFramesToRead; } public int readFrames(int[][] sampleBuffer, int numFramesToRead) throws IOException, SFWavFileException { return readFrames(sampleBuffer, 0, numFramesToRead); } public int readFrames(int[][] sampleBuffer, final int offSetIn, final int numFramesToRead) throws IOException, SFWavFileException { if (this.ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance"); //$NON-NLS-1$ int offSet = offSetIn; for (int f = 0; f < numFramesToRead; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) sampleBuffer[c][offSet] = (int) readSample(); offSet++; this.frameCounter++; } return numFramesToRead; } public int writeFrames(int[] sampleBuffer, int numFramesToWrite) throws IOException { return writeFrames(sampleBuffer, 0, numFramesToWrite); } public int writeFrames(int[] sampleBuffer, final int offSetIn, int numFramesToWrite) throws IOException { if (this.ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance"); //$NON-NLS-1$ int offSet = offSetIn; for (int f = 0; f < numFramesToWrite; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) { writeSample(sampleBuffer[offSet]); offSet++; } this.frameCounter++; } return numFramesToWrite; } public int writeFrames(int[][] sampleBuffer, int numFramesToWrite) throws IOException { return writeFrames(sampleBuffer, 0, numFramesToWrite); } public int writeFrames(int[][] sampleBuffer, final int offSetIn, final int numFramesToWrite) throws IOException { if (this.ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance"); //$NON-NLS-1$ int offSet = offSetIn; for (int f = 0; f < numFramesToWrite; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) writeSample(sampleBuffer[c][offSet]); offSet++; this.frameCounter++; } return numFramesToWrite; } // Long // ---- public int readFrames(long[] sampleBuffer, int numFramesToRead) throws IOException, SFWavFileException { return readFrames(sampleBuffer, 0, numFramesToRead); } public int readFrames(long[] sampleBuffer, final int offSetIn, final int numFramesToRead) throws IOException, SFWavFileException { if (this.ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance"); //$NON-NLS-1$ int offSet = offSetIn; for (int f = 0; f < numFramesToRead; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) { sampleBuffer[offSet] = readSample(); offSet++; } this.frameCounter++; } return numFramesToRead; } public int readFrames(long[][] sampleBuffer, int numFramesToRead) throws IOException, SFWavFileException { return readFrames(sampleBuffer, 0, numFramesToRead); } public int readFrames(long[][] sampleBuffer, final int offSetIn, final int numFramesToRead) throws IOException, SFWavFileException { if (this.ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance"); //$NON-NLS-1$ int offSet = offSetIn; for (int f = 0; f < numFramesToRead; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) sampleBuffer[c][offSet] = readSample(); offSet++; this.frameCounter++; } return numFramesToRead; } public int writeFrames(long[] sampleBuffer, int numFramesToWrite) throws IOException { return writeFrames(sampleBuffer, 0, numFramesToWrite); } public int writeFrames(long[] sampleBuffer, final int offSetIn, final int numFramesToWrite) throws IOException { if (this.ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance"); //$NON-NLS-1$ int offSet = offSetIn; for (int f = 0; f < numFramesToWrite; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) { writeSample(sampleBuffer[offSet]); offSet++; } this.frameCounter++; } return numFramesToWrite; } public int writeFrames(long[][] sampleBuffer, int numFramesToWrite) throws IOException { return writeFrames(sampleBuffer, 0, numFramesToWrite); } public int writeFrames(long[][] sampleBuffer, final int offSetIn, final int numFramesToWrite) throws IOException { if (this.ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance"); //$NON-NLS-1$ int offSet = offSetIn; for (int f = 0; f < numFramesToWrite; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) writeSample(sampleBuffer[c][offSet]); offSet++; this.frameCounter++; } return numFramesToWrite; } // Double // ------ public int readFrames(double[] sampleBuffer, int numFramesToRead) throws IOException, SFWavFileException { return readFrames(sampleBuffer, 0, numFramesToRead); } public int readFrames(double[] sampleBuffer, final int offSetIn, final int numFramesToRead) throws IOException, SFWavFileException { if (this.ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance"); //$NON-NLS-1$ int offSet = offSetIn; for (int f = 0; f < numFramesToRead; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) { sampleBuffer[offSet] = this.doubleOffset + readSample() / this.doubleScale; offSet++; } this.frameCounter++; } return numFramesToRead; } public int readFrames(double[][] sampleBuffer, int numFramesToRead) throws IOException, SFWavFileException { return readFrames(sampleBuffer, 0, numFramesToRead); } public int readFrames(double[][] sampleBuffer, final int offSetIn, int numFramesToRead) throws IOException, SFWavFileException { if (this.ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance"); //$NON-NLS-1$ int offSet = offSetIn; for (int f = 0; f < numFramesToRead; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) sampleBuffer[c][offSet] = this.doubleOffset + readSample() / this.doubleScale; offSet++; this.frameCounter++; } return numFramesToRead; } public int writeFrames(double[] sampleBuffer, int numFramesToWrite) throws IOException { return writeFrames(sampleBuffer, 0, numFramesToWrite); } public int writeFrames(double[] sampleBuffer, final int offSetIn, final int numFramesToWrite) throws IOException { if (this.ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance"); //$NON-NLS-1$ int offSet = offSetIn; for (int f = 0; f < numFramesToWrite; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) { writeSample((long) (this.doubleScale * (this.doubleOffset + sampleBuffer[offSet]))); offSet++; } this.frameCounter++; } return numFramesToWrite; } public int writeFrames(double[][] sampleBuffer, int numFramesToWrite) throws IOException { return writeFrames(sampleBuffer, 0, numFramesToWrite); } public int writeFrames(double[][] sampleBuffer, final int offSetIn, final int numFramesToWrite) throws IOException { if (this.ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance"); //$NON-NLS-1$ int offSet = offSetIn; for (int f = 0; f < numFramesToWrite; f++) { if (this.frameCounter == this.numFrames) return f; for (int c = 0; c < this.numChannels; c++) writeSample((long) (this.doubleScale * (this.doubleOffset + sampleBuffer[c][offSet]))); offSet++; this.frameCounter++; } return numFramesToWrite; } public void close() throws IOException { // Close the input stream and set to null if (this.iStream != null) { this.iStream.close(); this.iStream = null; } if (this.oStream != null) { // Write out anything still in the local buffer if (this.bufferPointer > 0) this.oStream.write(this.buffer, 0, this.bufferPointer); // If an extra byte is required for word alignment, add it to the // end if (this.wordAlignAdjust) this.oStream.write(0); // Close the stream and set to null this.oStream.close(); this.oStream = null; } // Flag that the stream is closed this.ioState = IOState.CLOSED; } public void display() { display(System.out); } public void display(PrintStream out) { out.printf("File: %s\n", this.file); //$NON-NLS-1$ out.printf("Channels: %d, Frames: %d\n", this.numChannels, this.numFrames); //$NON-NLS-1$ out.printf("IO State: %s\n", this.ioState); //$NON-NLS-1$ out.printf("Sample Rate: %d, Block Align: %d\n", this.sampleRate, this.blockAlign); //$NON-NLS-1$ out.printf("Valid Bits: %d, Bytes per sample: %d\n", this.validBits, this.bytesPerSample); //$NON-NLS-1$ } }