/* * Copyright 2011 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; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import com.jsyn.io.AudioOutputStream; /** * Write audio data to a WAV file. * *
 * 
 * WaveFileWriter writer = new WaveFileWriter(file);
 * writer.setFrameRate(22050);
 * writer.setBitsPerSample(24);
 * writer.write(floatArray);
 * writer.close();
 * 
 * 
* * @author Phil Burk (C) 2011 Mobileer Inc */ public class WaveFileWriter implements AudioOutputStream { private static final short WAVE_FORMAT_PCM = 1; private OutputStream outputStream; private long riffSizePosition = 0; private long dataSizePosition = 0; private int frameRate = 44100; private int samplesPerFrame = 1; private int bitsPerSample = 16; private int bytesWritten; private File outputFile; private boolean headerWritten = false; private final static int PCM24_MIN = -(1 << 23); private final static int PCM24_MAX = (1 << 23) - 1; /** * Create a writer that will write to the specified file. * * @param outputFile * @throws FileNotFoundException */ public WaveFileWriter(File outputFile) throws FileNotFoundException { this.outputFile = outputFile; FileOutputStream fileOut = new FileOutputStream(outputFile); outputStream = new BufferedOutputStream(fileOut); } /** * @param frameRate default is 44100 */ public void setFrameRate(int frameRate) { this.frameRate = frameRate; } public int getFrameRate() { return frameRate; } /** For stereo, set this to 2. Default is 1. */ public void setSamplesPerFrame(int samplesPerFrame) { this.samplesPerFrame = samplesPerFrame; } public int getSamplesPerFrame() { return samplesPerFrame; } /** Only 16 or 24 bit samples supported at the moment. Default is 16. */ public void setBitsPerSample(int bits) { if ((bits != 16) && (bits != 24)) { throw new IllegalArgumentException("Only 16 or 24 bits per sample allowed. Not " + bits); } bitsPerSample = bits; } public int getBitsPerSample() { return bitsPerSample; } @Override public void close() throws IOException { outputStream.close(); fixSizes(); } /** Write entire buffer of audio samples to the WAV file. */ @Override public void write(double[] buffer) throws IOException { write(buffer, 0, buffer.length); } /** Write audio to the WAV file. */ public void write(float[] buffer) throws IOException { write(buffer, 0, buffer.length); } /** Write single audio data value to the WAV file. */ @Override public void write(double value) throws IOException { if (!headerWritten) { writeHeader(); } if (bitsPerSample == 24) { writePCM24(value); } else { writePCM16(value); } } private void writePCM24(double value) throws IOException { // Offset before casting so that we can avoid using floor(). // Also round by adding 0.5 so that very small signals go to zero. double temp = (PCM24_MAX * value) + 0.5 - PCM24_MIN; int sample = ((int) temp) + PCM24_MIN; // clip to 24-bit range if (sample > PCM24_MAX) { sample = PCM24_MAX; } else if (sample < PCM24_MIN) { sample = PCM24_MIN; } // encode as little-endian writeByte(sample); // little end writeByte(sample >> 8); // middle writeByte(sample >> 16); // big end } private void writePCM16(double value) throws IOException { // Offset before casting so that we can avoid using floor(). // Also round by adding 0.5 so that very small signals go to zero. double temp = (Short.MAX_VALUE * value) + 0.5 - Short.MIN_VALUE; int sample = ((int) temp) + Short.MIN_VALUE; if (sample > Short.MAX_VALUE) { sample = Short.MAX_VALUE; } else if (sample < Short.MIN_VALUE) { sample = Short.MIN_VALUE; } writeByte(sample); // little end writeByte(sample >> 8); // big end } /** Write audio to the WAV file. */ @Override public void write(double[] buffer, int start, int count) throws IOException { for (int i = 0; i < count; i++) { write(buffer[start + i]); } } /** Write audio to the WAV file. */ public void write(float[] buffer, int start, int count) throws IOException { for (int i = 0; i < count; i++) { write(buffer[start + i]); } } // Write lower 8 bits. Upper bits ignored. private void writeByte(int b) throws IOException { outputStream.write(b); bytesWritten += 1; } /** * Write a 32 bit integer to the stream in Little Endian format. */ public void writeIntLittle(int n) throws IOException { writeByte(n); writeByte(n >> 8); writeByte(n >> 16); writeByte(n >> 24); } /** * Write a 16 bit integer to the stream in Little Endian format. */ public void writeShortLittle(short n) throws IOException { writeByte(n); writeByte(n >> 8); } /** * Write a simple WAV header for PCM data. */ private void writeHeader() throws IOException { writeRiffHeader(); writeFormatChunk(); writeDataChunkHeader(); outputStream.flush(); headerWritten = true; } /** * Write a 'RIFF' file header and a 'WAVE' ID to the WAV file. */ private void writeRiffHeader() throws IOException { writeByte('R'); writeByte('I'); writeByte('F'); writeByte('F'); riffSizePosition = bytesWritten; writeIntLittle(Integer.MAX_VALUE); writeByte('W'); writeByte('A'); writeByte('V'); writeByte('E'); } /** * Write an 'fmt ' chunk to the WAV file containing the given information. */ public void writeFormatChunk() throws IOException { int bytesPerSample = (bitsPerSample + 7) / 8; writeByte('f'); writeByte('m'); writeByte('t'); writeByte(' '); writeIntLittle(16); // chunk size writeShortLittle(WAVE_FORMAT_PCM); writeShortLittle((short) samplesPerFrame); writeIntLittle(frameRate); // bytes/second writeIntLittle(frameRate * samplesPerFrame * bytesPerSample); // block align writeShortLittle((short) (samplesPerFrame * bytesPerSample)); writeShortLittle((short) bitsPerSample); } /** * Write a 'data' chunk header to the WAV file. This should be followed by call to * writeShortLittle() to write the data to the chunk. */ public void writeDataChunkHeader() throws IOException { writeByte('d'); writeByte('a'); writeByte('t'); writeByte('a'); dataSizePosition = bytesWritten; writeIntLittle(Integer.MAX_VALUE); // size } /** * Fix RIFF and data chunk sizes based on final size. Assume data chunk is the last chunk. */ private void fixSizes() throws IOException { RandomAccessFile randomFile = new RandomAccessFile(outputFile, "rw"); try { // adjust RIFF size long end = bytesWritten; int riffSize = (int) (end - riffSizePosition) - 4; randomFile.seek(riffSizePosition); writeRandomIntLittle(randomFile, riffSize); // adjust data size int dataSize = (int) (end - dataSizePosition) - 4; randomFile.seek(dataSizePosition); writeRandomIntLittle(randomFile, dataSize); } finally { randomFile.close(); } } private void writeRandomIntLittle(RandomAccessFile randomFile, int n) throws IOException { byte[] buffer = new byte[4]; buffer[0] = (byte) n; buffer[1] = (byte) (n >> 8); buffer[2] = (byte) (n >> 16); buffer[3] = (byte) (n >> 24); randomFile.write(buffer); } }