1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cts.verifier.audio.audiolib; 18 19 import java.io.BufferedOutputStream; 20 import java.io.File; 21 import java.io.FileNotFoundException; 22 import java.io.FileOutputStream; 23 import java.io.IOException; 24 import java.io.OutputStream; 25 import java.io.RandomAccessFile; 26 27 28 /** 29 * Write audio data to a WAV file. 30 * 31 * <pre> 32 * <code> 33 * WaveFileWriter writer = new WaveFileWriter(file); 34 * writer.setFrameRate(22050); 35 * writer.setBitsPerSample(24); 36 * writer.write(floatArray); 37 * writer.close(); 38 * </code> 39 * </pre> 40 * 41 * This was borrowed with Phil Burk's permission from JSyn at: 42 * https://github.com/philburk/jsyn/blob/master/src/main/java/com/jsyn/util/WaveFileWriter.java 43 */ 44 public class WaveFileWriter { 45 46 private static final short WAVE_FORMAT_PCM = 1; 47 private static final int PCM24_MIN = -(1 << 23); 48 private static final int PCM24_MAX = (1 << 23) - 1; 49 50 private final OutputStream mOutputStream; 51 private long mRiffSizePosition = 0; 52 private long mDataSizePosition = 0; 53 private int mFrameRate = 44100; 54 private int mSamplesPerFrame = 1; 55 private int mBitsPerSample = 16; 56 private int mBytesWritten; 57 private final File mOutputFile; 58 private boolean mHeaderWritten = false; 59 60 /** 61 * Create a writer that will write to the specified file. 62 */ WaveFileWriter(File outputFile)63 public WaveFileWriter(File outputFile) throws FileNotFoundException { 64 mOutputFile = outputFile; 65 FileOutputStream fileOut = new FileOutputStream(outputFile); 66 mOutputStream = new BufferedOutputStream(fileOut); 67 } 68 69 /** 70 * @param frameRate default is 44100 71 */ setFrameRate(int frameRate)72 public void setFrameRate(int frameRate) { 73 mFrameRate = frameRate; 74 } 75 getFrameRate()76 public int getFrameRate() { 77 return mFrameRate; 78 } 79 80 /** 81 * For stereo, set this to 2. Default is 1. 82 */ setSamplesPerFrame(int samplesPerFrame)83 public void setSamplesPerFrame(int samplesPerFrame) { 84 mSamplesPerFrame = samplesPerFrame; 85 } 86 getSamplesPerFrame()87 public int getSamplesPerFrame() { 88 return mSamplesPerFrame; 89 } 90 91 /** 92 * Only 16 or 24 bit samples supported at the moment. Default is 16. 93 */ setBitsPerSample(int bits)94 public void setBitsPerSample(int bits) { 95 if ((bits != 16) && (bits != 24)) { 96 throw new IllegalArgumentException( 97 "Only 16 or 24 bits per sample allowed. Not " + bits); 98 } 99 mBitsPerSample = bits; 100 } 101 getBitsPerSample()102 public int getBitsPerSample() { 103 return mBitsPerSample; 104 } 105 106 /** 107 * Close the stream and fix the chunk sizes. 108 * @throws IOException if the close fails 109 */ close()110 public void close() throws IOException { 111 mOutputStream.close(); 112 fixSizes(); 113 } 114 115 /** 116 * Write entire buffer of audio samples to the WAV file. 117 */ write(float[] buffer)118 public void write(float[] buffer) throws IOException { 119 write(buffer, 0, buffer.length); 120 } 121 122 /** 123 * Write single audio data value to the WAV file. 124 */ write(float value)125 public void write(float value) throws IOException { 126 if (!mHeaderWritten) { 127 writeHeader(); 128 } 129 130 if (mBitsPerSample == 24) { 131 writePCM24(value); 132 } else { 133 writePCM16(value); 134 } 135 } 136 writePCM24(float value)137 private void writePCM24(float value) throws IOException { 138 // Offset before casting so that we can avoid using floor(). 139 // Also round by adding 0.5 so that very small signals go to zero. 140 float temp = (PCM24_MAX * value) + 0.5f - PCM24_MIN; 141 int sample = ((int) temp) + PCM24_MIN; 142 // clip to 24-bit range 143 if (sample > PCM24_MAX) { 144 sample = PCM24_MAX; 145 } else if (sample < PCM24_MIN) { 146 sample = PCM24_MIN; 147 } 148 // encode as little-endian 149 writeByte(sample); // little end 150 writeByte(sample >> 8); // middle 151 writeByte(sample >> 16); // big end 152 } 153 writePCM16(float value)154 private void writePCM16(float value) throws IOException { 155 // Offset before casting so that we can avoid using floor(). 156 // Also round by adding 0.5 so that very small signals go to zero. 157 float temp = (Short.MAX_VALUE * value) + 0.5f - Short.MIN_VALUE; 158 int sample = ((int) temp) + Short.MIN_VALUE; 159 if (sample > Short.MAX_VALUE) { 160 sample = Short.MAX_VALUE; 161 } else if (sample < Short.MIN_VALUE) { 162 sample = Short.MIN_VALUE; 163 } 164 writeByte(sample); // little end 165 writeByte(sample >> 8); // big end 166 } 167 168 /** 169 * Write audio to the WAV file. 170 */ write(float[] buffer, int start, int count)171 public void write(float[] buffer, int start, int count) throws IOException { 172 for (int i = 0; i < count; i++) { 173 write(buffer[start + i]); 174 } 175 } 176 177 // Write lower 8 bits. Upper bits ignored. writeByte(int b)178 private void writeByte(int b) throws IOException { 179 mOutputStream.write(b); 180 mBytesWritten += 1; 181 } 182 183 /** 184 * Write a 32 bit integer to the stream in Little Endian format. 185 */ writeIntLittle(int n)186 public void writeIntLittle(int n) throws IOException { 187 writeByte(n); 188 writeByte(n >> 8); 189 writeByte(n >> 16); 190 writeByte(n >> 24); 191 } 192 193 /** 194 * Write a 16 bit integer to the stream in Little Endian format. 195 */ writeShortLittle(short n)196 public void writeShortLittle(short n) throws IOException { 197 writeByte(n); 198 writeByte(n >> 8); 199 } 200 201 /** 202 * Write a simple WAV header for PCM data. 203 */ writeHeader()204 private void writeHeader() throws IOException { 205 writeRiffHeader(); 206 writeFormatChunk(); 207 writeDataChunkHeader(); 208 mOutputStream.flush(); 209 mHeaderWritten = true; 210 } 211 212 /** 213 * Write a 'RIFF' file header and a 'WAVE' ID to the WAV file. 214 */ writeRiffHeader()215 private void writeRiffHeader() throws IOException { 216 writeByte('R'); 217 writeByte('I'); 218 writeByte('F'); 219 writeByte('F'); 220 mRiffSizePosition = mBytesWritten; 221 // This will be overwritten by fixSizes() when the writer is closed. 222 writeIntLittle(Integer.MAX_VALUE); 223 writeByte('W'); 224 writeByte('A'); 225 writeByte('V'); 226 writeByte('E'); 227 } 228 229 /** 230 * Write an 'fmt ' chunk to the WAV file containing the given information. 231 */ writeFormatChunk()232 public void writeFormatChunk() throws IOException { 233 int bytesPerSample = (mBitsPerSample + 7) / 8; 234 235 writeByte('f'); 236 writeByte('m'); 237 writeByte('t'); 238 writeByte(' '); 239 writeIntLittle(16); // chunk size 240 writeShortLittle(WAVE_FORMAT_PCM); 241 writeShortLittle((short) mSamplesPerFrame); 242 writeIntLittle(mFrameRate); 243 // bytes/second 244 writeIntLittle(mFrameRate * mSamplesPerFrame * bytesPerSample); 245 // block align 246 writeShortLittle((short) (mSamplesPerFrame * bytesPerSample)); 247 writeShortLittle((short) mBitsPerSample); 248 } 249 250 /** 251 * Write a 'data' chunk header to the WAV file. This should be followed by call to 252 * writeShortLittle() to write the data to the chunk. 253 */ writeDataChunkHeader()254 public void writeDataChunkHeader() throws IOException { 255 writeByte('d'); 256 writeByte('a'); 257 writeByte('t'); 258 writeByte('a'); 259 mDataSizePosition = mBytesWritten; 260 // This will be overwritten by fixSizes() when the writer is closed. 261 writeIntLittle(Integer.MAX_VALUE); // size 262 } 263 264 /** 265 * Fix RIFF and data chunk sizes based on final size. Assume data chunk is the last chunk. 266 */ fixSizes()267 private void fixSizes() throws IOException { 268 RandomAccessFile randomFile = new RandomAccessFile(mOutputFile, "rw"); 269 try { 270 // adjust RIFF size 271 long end = mBytesWritten; 272 int riffSize = (int) (end - mRiffSizePosition) - 4; 273 randomFile.seek(mRiffSizePosition); 274 writeRandomIntLittle(randomFile, riffSize); 275 // adjust data size 276 int dataSize = (int) (end - mDataSizePosition) - 4; 277 randomFile.seek(mDataSizePosition); 278 writeRandomIntLittle(randomFile, dataSize); 279 } finally { 280 randomFile.close(); 281 } 282 } 283 writeRandomIntLittle(RandomAccessFile randomFile, int n)284 private void writeRandomIntLittle(RandomAccessFile randomFile, int n) throws IOException { 285 byte[] buffer = new byte[4]; 286 buffer[0] = (byte) n; 287 buffer[1] = (byte) (n >> 8); 288 buffer[2] = (byte) (n >> 16); 289 buffer[3] = (byte) (n >> 24); 290 randomFile.write(buffer); 291 } 292 293 } 294