aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/com/jsyn/util/WaveFileWriter.java
diff options
context:
space:
mode:
authorRubbaBoy <[email protected]>2020-07-06 02:33:28 -0400
committerPhil Burk <[email protected]>2020-10-30 11:19:34 -0700
commit46888fae6eb7b1dd386f7af7d101ead99ae61981 (patch)
tree8969bbfd68d2fb5c0d8b86da49ec2eca230a72ab /src/main/java/com/jsyn/util/WaveFileWriter.java
parentc51e92e813dd481603de078f0778e1f75db2ab05 (diff)
Restructured project, added gradle, JUnit, logger, and more
Added Gradle (and removed ant), modernized testing via the JUnit framework, moved standalone examples from the tests directory to a separate module, removed sparsely used Java logger and replaced it with SLF4J. More work could be done, however this is a great start to greatly improving the health of the codebase.
Diffstat (limited to 'src/main/java/com/jsyn/util/WaveFileWriter.java')
-rw-r--r--src/main/java/com/jsyn/util/WaveFileWriter.java293
1 files changed, 293 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/util/WaveFileWriter.java b/src/main/java/com/jsyn/util/WaveFileWriter.java
new file mode 100644
index 0000000..32e9995
--- /dev/null
+++ b/src/main/java/com/jsyn/util/WaveFileWriter.java
@@ -0,0 +1,293 @@
+/*
+ * 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.
+ *
+ * <pre>
+ * <code>
+ * WaveFileWriter writer = new WaveFileWriter(file);
+ * writer.setFrameRate(22050);
+ * writer.setBitsPerSample(24);
+ * writer.write(floatArray);
+ * writer.close();
+ * </code>
+ * </pre>
+ *
+ * @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);
+ }
+
+}