Reading and Writing a WAV File in Java

The following tutorial reads the header and samples of a WAV audio file into a Java object, writes that Java object back out to a different WAV file, and then compares the contents of the two files to make sure they match. It provides a useful foundation for further audio processing work, but in and of itself it’s nothing much to get excited about. Hey, not every program can be a soul-elevating intellectual delight.

1. If you have not already done so, download and install the Java Development Kit. Details are given in a previous tutorial. Make a note of the directory in which the file “javac.exe” is located.

2. In any convenient location, create a new directory named WavFormatTest.

3. Copy a test WAV file to the newly created WavFormatTest directory. For this example, the file “\Windows\Media\tada.wav” will be used.

4. In the WavFormatTest directory, create a new text file named “WavFormatTest.java” containing the following text.

import java.io.*;
import java.util.*;

public class WavFormatTest
{
    public static void main(String[] args)
    {
        readAndRewriteWav(args[0]);
    }    

    private static void readAndRewriteWav(String filePathToReadFromMinusExtension)
    {
        String fileExtension = ".wav";

        String filePathToReadFrom =
            filePathToReadFromMinusExtension
            + fileExtension;

        WavFile wavFileToTest = WavFile.readFromFilePath(filePathToReadFrom);

        String filePathToWriteTo =
            filePathToReadFromMinusExtension
            + "-ReadThenWritten"
            + fileExtension;

        wavFileToTest.filePath = filePathToWriteTo;

        wavFileToTest.writeToFilePath();
    }    
}

class DataInputStreamLittleEndian
{
    private DataInputStream systemStream;

    public DataInputStreamLittleEndian(DataInputStream systemStream)
    {
        this.systemStream = systemStream;
    }

    public void close() throws IOException
    {
        this.systemStream.close();
    }

    public void read(byte[] byteBufferToReadInto) throws IOException
    {
        // no need to translate to little-endian here

        this.systemStream.read(byteBufferToReadInto);
    }

    public int readInt() throws IOException
    {
        byte[] bytesLittleEndian = new byte[4];
        this.systemStream.read(bytesLittleEndian);

        long returnValueAsLong =
        (
            (bytesLittleEndian[0] & 0xFF)
            | ((bytesLittleEndian[1] & 0xFF) << 8 )
            | ((bytesLittleEndian[2] & 0xFF) << 16)
            | ((bytesLittleEndian[3] & 0xFF) << 24)
        );

        return (int)returnValueAsLong;
    }

    public short readShort() throws IOException
    {
        byte[] bytesLittleEndian = new byte[2];
        this.systemStream.read(bytesLittleEndian);

        int returnValueAsInt =
        (
            (bytesLittleEndian[0] & 0xFF)
            | ((bytesLittleEndian[1] & 0xFF) << 8 )
        );

        return (short)returnValueAsInt;
    }
}

class DataOutputStreamLittleEndian
{
    private DataOutputStream systemStream;

    public DataOutputStreamLittleEndian(DataOutputStream systemStream)
    {
        this.systemStream = systemStream;
    }

    public void close() throws IOException
    {
        this.systemStream.close();
    }

    public void writeString(String stringToWrite) throws IOException
    {
        this.systemStream.writeBytes(stringToWrite);
    }

    public void writeBytes(byte[] bytesToWrite) throws IOException
    {        
        this.systemStream.write
        (
            bytesToWrite, 0, bytesToWrite.length
        );
    }

    public void writeInt(int intToWrite) throws IOException
    {
            byte[] intToWriteAsBytesLittleEndian = new byte[]
        {
            (byte)(intToWrite & 0xFF),
                (byte)((intToWrite >> 8 ) & 0xFF),
                (byte)((intToWrite >> 16) & 0xFF),
                (byte)((intToWrite >> 24) & 0xFF),
        };

        this.systemStream.write(intToWriteAsBytesLittleEndian, 0, 4);
    }

    public void writeShort(short shortToWrite) throws IOException
    {
            byte[] shortToWriteAsBytesLittleEndian = new byte[]
        {
            (byte)shortToWrite,
                (byte)(shortToWrite >>> 8 & 0xFF),
        };

        this.systemStream.write(shortToWriteAsBytesLittleEndian, 0, 2);
    }    
}

class WavFile
{
    public static final int BitsPerByte = 8;
    public static final int NumberOfBytesInRiffWaveAndFormatChunks = 36;

    public String filePath;
    public SamplingInfo samplingInfo;
    public Sample[][] samplesForChannels;

    public WavFile
    (
        String filePath,
        SamplingInfo samplingInfo,
        Sample[][] samplesForChannels
    )
    {
        this.filePath = filePath;
        this.samplingInfo = samplingInfo;
        this.samplesForChannels = samplesForChannels;
    }

    public static WavFile readFromFilePath(String filePathToReadFrom)
    {        
        WavFile returnValue = new WavFile(filePathToReadFrom, null, null);

        try
        {            
            DataInputStream dataInputStream = new DataInputStream
            (
                new BufferedInputStream
                (
                    new FileInputStream(filePathToReadFrom)
                )
            );

            DataInputStreamLittleEndian reader;
            reader = new DataInputStreamLittleEndian
            (
                dataInputStream
            );

        returnValue.readFromFilePath_ReadChunks(reader);

            reader.close();
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }            

        return returnValue;
    }

    private void readFromFilePath_ReadChunks(DataInputStreamLittleEndian reader) 
        throws IOException
    {
        byte[] riff = new byte[4];
        reader.read(riff);          

        int numberOfBytesInFile = reader.readInt();

        byte[] wave = new byte[4];
        reader.read(wave);

        this.readFromFilePath_ReadChunks_Format(reader);
        this.readFromFilePath_ReadChunks_Data(reader);
    }

    private void readFromFilePath_ReadChunks_Format(DataInputStreamLittleEndian reader)
        throws IOException
    {
        byte[] fmt = new byte[4];
        reader.read(fmt);
        int chunkSizeInBytes = reader.readInt();
        Short formatCode = reader.readShort();

        Short numberOfChannels = reader.readShort();
        int samplesPerSecond = reader.readInt();
        int bytesPerSecond = reader.readInt();
        Short bytesPerSampleMaybe = reader.readShort();
        Short bitsPerSample = reader.readShort();

        SamplingInfo samplingInfo = new SamplingInfo
        (
            "[from file]",
            chunkSizeInBytes,
            formatCode,
            numberOfChannels,
            samplesPerSecond,
            bitsPerSample    
        );

        this.samplingInfo = samplingInfo;
    }

    private void readFromFilePath_ReadChunks_Data(DataInputStreamLittleEndian reader)
        throws IOException
    {
        byte[] data = new byte[4];
        reader.read(data);
        int subchunk2Size = reader.readInt();

        byte[] samplesForChannelsMixedAsBytes = new byte[subchunk2Size];
        reader.read(samplesForChannelsMixedAsBytes );

        Sample[][] samplesForChannels = Sample.buildManyFromBytes
        (
            samplingInfo,
            samplesForChannelsMixedAsBytes
        );

    this.samplesForChannels = samplesForChannels;    
    }

    public void writeToFilePath()
    {
        try
        {
            DataOutputStream dataOutputStream = new DataOutputStream
            (
                new BufferedOutputStream
                (
                    new FileOutputStream
                    (
                        this.filePath
                    )
                )
            );

            DataOutputStreamLittleEndian writer;
            writer = new DataOutputStreamLittleEndian
            (
                dataOutputStream
            );            

            this.writeToFilePath_WriteChunks(writer);

            writer.close();
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
    
    private void writeToFilePath_WriteChunks(DataOutputStreamLittleEndian writer) 
        throws IOException
    {
        int numberOfBytesInSamples = (int)
        (
            this.samplesForChannels[0].length
            * this.samplingInfo.numberOfChannels
            * this.samplingInfo.bitsPerSample
            / WavFile.BitsPerByte
        );

        writer.writeString("RIFF");
        writer.writeInt
        (
            (int)
            (
                numberOfBytesInSamples 
                + WavFile.NumberOfBytesInRiffWaveAndFormatChunks
            )
        );
        writer.writeString("WAVE");

        this.writeToFilePath_WriteChunks_Format(writer);
        this.writeToFilePath_WriteChunks_Data(writer);
    }    

    private void writeToFilePath_WriteChunks_Format(DataOutputStreamLittleEndian writer) 
        throws IOException
    {
        writer.writeString("fmt ");
        writer.writeInt(this.samplingInfo.chunkSizeInBytes);
        writer.writeShort(this.samplingInfo.formatCode);
        writer.writeShort((short)this.samplingInfo.numberOfChannels);
        writer.writeInt((int)this.samplingInfo.samplesPerSecond);
        writer.writeInt((int)this.samplingInfo.bytesPerSecond());
        writer.writeShort
        (
            (short)
            (
                this.samplingInfo.numberOfChannels
                * this.samplingInfo.bitsPerSample
                / WavFile.BitsPerByte
            )
        );
        writer.writeShort((short)this.samplingInfo.bitsPerSample);    
    }    

    private void writeToFilePath_WriteChunks_Data(DataOutputStreamLittleEndian writer) 
        throws IOException
    {
        writer.writeString("data");

        int numberOfBytesInSamples = (int)
        (
            this.samplesForChannels[0].length
            * this.samplingInfo.numberOfChannels
            * this.samplingInfo.bitsPerSample
            / WavFile.BitsPerByte
        );

        writer.writeInt(numberOfBytesInSamples);

        byte[] samplesAsBytes = Sample.convertManyToBytes
        (
            this.samplesForChannels,
            this.samplingInfo
        );    

        writer.writeBytes(samplesAsBytes);
    }

    // inner classes

    public static abstract class Sample
    {
        public abstract Sample buildFromBytes(byte[] valueAsBytes);
        public abstract Sample buildFromDouble(double valueAsDouble);
        public abstract byte[] convertToBytes();
        public abstract double convertToDouble();

        public static Sample[][] buildManyFromBytes
        (
                SamplingInfo samplingInfo,
                byte[] bytesToConvert
        )
        {
            int numberOfBytes = bytesToConvert.length;

            int numberOfChannels = samplingInfo.numberOfChannels;

            Sample[][] returnSamples = new Sample[numberOfChannels][];

            int bytesPerSample = samplingInfo.bitsPerSample / WavFile.BitsPerByte;

            int samplesPerChannel =
                numberOfBytes
                / bytesPerSample
                / numberOfChannels;

            for (int c = 0; c < numberOfChannels; c++)
            {
                returnSamples[c] = new Sample[samplesPerChannel];
            }

            int b = 0;

            double halfMaxValueForEachSample = Math.pow
            (
                2, WavFile.BitsPerByte * bytesPerSample - 1
            );

            Sample samplePrototype = samplingInfo.samplePrototype();

            byte[] sampleValueAsBytes = new byte[bytesPerSample];

            for (int s = 0; s < samplesPerChannel; s++)
            {                
                for (int c = 0; c < numberOfChannels; c++)
                {
                    for (int i = 0; i < bytesPerSample; i++)
                    {
                        sampleValueAsBytes[i] = bytesToConvert[b];
                        b++;
                    }

                    returnSamples[c][s] = samplePrototype.buildFromBytes
                    (
                        sampleValueAsBytes
                    );
                }
            }
    
            return returnSamples;
        }

        public static Sample[] concatenateSets(Sample[][] setsToConcatenate)
        {
            int numberOfSamplesSoFar = 0;
    
            for (int i = 0; i < setsToConcatenate.length; i++)
            {
                Sample[] setToConcatenate = setsToConcatenate[i];
                numberOfSamplesSoFar += setToConcatenate.length;
            }

            Sample[] returnValues = new Sample[numberOfSamplesSoFar];

            int s = 0;

            for (int i = 0; i < setsToConcatenate.length; i++)
            {
                Sample[] setToConcatenate = setsToConcatenate[i];

                for (int j = 0; j < setToConcatenate.length; j++)
                {
                    returnValues[s] = setToConcatenate[j];
                    s++;
                }
            }

            return returnValues;
        }

        public static byte[] convertManyToBytes
        (
            Sample[][] samplesToConvert,
            SamplingInfo samplingInfo
        )
        {
            byte[] returnBytes = null;
    
            int numberOfChannels = samplingInfo.numberOfChannels;

            int samplesPerChannel = samplesToConvert[0].length;
    
            int bitsPerSample = samplingInfo.bitsPerSample;

            int bytesPerSample = bitsPerSample / WavFile.BitsPerByte;

            int numberOfBytes =
                numberOfChannels
                * samplesPerChannel
                * bytesPerSample;

            returnBytes = new byte[numberOfBytes];
    
            double halfMaxValueForEachSample = Math.pow
            (
                2, WavFile.BitsPerByte * bytesPerSample - 1
            );

            int b = 0;

            for (int s = 0; s < samplesPerChannel; s++)
            {
                for (int c = 0; c < numberOfChannels; c++)
                {
                    Sample sample = samplesToConvert[c][s];    

                    byte[] sampleAsBytes = sample.convertToBytes();

                    for (int i = 0; i < bytesPerSample; i++)
                    {
                        returnBytes[b] = sampleAsBytes[i];
                        b++;
                    }
                }                        
            }
    
            return returnBytes;
        }    

        public static Sample[] superimposeSets(Sample[][] setsToSuperimpose)
        {
            int maxSamplesSoFar = 0;

            for (int i = 0; i < setsToSuperimpose.length; i++)
            {
                Sample[] setToSuperimpose = setsToSuperimpose[i];

                if (setToSuperimpose.length > maxSamplesSoFar)
                {
                    maxSamplesSoFar = setToSuperimpose.length;
                }
            }

            Sample[] returnValues = new Sample[maxSamplesSoFar];

            for (int i = 0; i < setsToSuperimpose.length; i++)
            {
                Sample[] setToSuperimpose = setsToSuperimpose[i];
    
                for (int j = 0; j < setToSuperimpose.length; j++)
                {
                    Sample sampleToSuperimpose = setToSuperimpose[j];

                    double sampleValueAsDouble =
                         sampleToSuperimpose.convertToDouble();

                    if (i > 0)
                    {
                        sampleValueAsDouble +=
                            returnValues[i].convertToDouble();
                    }

                    returnValues[i] = sampleToSuperimpose.buildFromDouble
                    (
                        sampleValueAsDouble
                    );
                }
            }

            return returnValues;
        }                    
    }

    public static class Sample16 extends Sample
    {
        public short value;
    
        public Sample16(short value)
        {
            this.value = value;
        }

        // Sample members

        public Sample buildFromBytes(byte[] valueAsBytes)
        {
            short valueAsShort = (short)
            (
                ((valueAsBytes[0] & 0xFF))
                | (short)((valueAsBytes[1] & 0xFF) << 8 )
            );

            return new Sample16(valueAsShort);
        }

        public Sample buildFromDouble(double valueAsDouble)
        {
            return new Sample16
            (
                (short)
                (
                    valueAsDouble
                    * Short.MAX_VALUE
                )
            );
        }

        public byte[] convertToBytes()
        {
            return new byte[]
            {
                (byte)((this.value) & 0xFF),
                (byte)((this.value >>> 8 ) & 0xFF),
            };
        }        
    
        public double convertToDouble()
        {
            return this.value / Short.MAX_VALUE;
        }
    }

    public static class Sample24 extends Sample
    {
        public static int MAX_VALUE = (int)Math.pow(2, 23);

        public int value;

        public Sample24(int value)
        {
            this.value = value;
        }

        // Sample members

        public Sample buildFromBytes(byte[] valueAsBytes)
        {
            short valueAsShort = (short)
            (
                ((valueAsBytes[0] & 0xFF))
                | ((valueAsBytes[1] & 0xFF) << 8 )
                | ((valueAsBytes[2] & 0xFF) << 16)
            );

            return new Sample24(valueAsShort);
        }

        public Sample buildFromDouble(double valueAsDouble)
        {
            return new Sample24
            (
                (int)
                (
                    valueAsDouble
                    * Integer.MAX_VALUE
                )
            );
        }

        public byte[] convertToBytes()
        {
            return new byte[]
            {
                (byte)((this.value) & 0xFF),
                (byte)((this.value >>> 8 ) & 0xFF),
                (byte)((this.value >>> 16) & 0xFF),
            };
        }        

        public double convertToDouble()
        {
            return this.value / MAX_VALUE;
        }
    }

    public static class Sample32 extends Sample
    {
        public int value;

        public Sample32(int value)
        {
            this.value = value;
        }

        // Sample members

        public Sample addInPlace(Sample other)
        {
            this.value += ((Sample16)other).value;
            return this;
        }

        public Sample buildFromBytes(byte[] valueAsBytes)
        {
            short valueAsShort = (short)
            (
                ((valueAsBytes[0] & 0xFF))
                | ((valueAsBytes[1] & 0xFF) << 8 )
                | ((valueAsBytes[2] & 0xFF) << 16)
                | ((valueAsBytes[3] & 0xFF) << 24)
            );
    
            return new Sample32(valueAsShort);
        }

        public Sample buildFromDouble(double valueAsDouble)
        {
            return new Sample32
            (
                (int)
                (
                    valueAsDouble
                    * Integer.MAX_VALUE
                )
            );
        }

        public byte[] convertToBytes()
        {
            return new byte[]
            {
                (byte)((this.value) & 0xFF),
                (byte)((this.value >>> 8 ) & 0xFF),
                (byte)((this.value >>> 16) & 0xFF),
                (byte)((this.value >>> 24) & 0xFF),
            };
        }    

        public double convertToDouble()
        {
            return this.value / Integer.MAX_VALUE;
        }    
    }
    
    public static class SamplingInfo
    {
        public String name;        
        public int chunkSizeInBytes;
        public short formatCode;
        public short numberOfChannels;        
        public int samplesPerSecond;
        public short bitsPerSample;

        public SamplingInfo
        (
            String name,
            int chunkSizeInBytes,
            short formatCode,
            short numberOfChannels,
            int samplesPerSecond,
            short bitsPerSample
        )
        {
            this.name = name;
            this.chunkSizeInBytes = chunkSizeInBytes;
            this.formatCode = formatCode;
            this.numberOfChannels = numberOfChannels;
            this.samplesPerSecond = samplesPerSecond;
            this.bitsPerSample = bitsPerSample;
        }

        public static class Instances
        {
            public static SamplingInfo Default = new SamplingInfo
            (
                "Default",
                16, // chunkSizeInBytes
                (short)1, // formatCode
                (short)1, // numberOfChannels
                44100,     // samplesPerSecond
                (short)16 // bitsPerSample
            );
        }

        public int bytesPerSecond()
        {    
            return this.samplesPerSecond
                * this.numberOfChannels
                * this.bitsPerSample / WavFile.BitsPerByte;
        }

        public Sample samplePrototype()
        {
            Sample returnValue = null;
    
            if (this.bitsPerSample == 16)
            {
                returnValue = new WavFile.Sample16((short)0);
            }
            else if (this.bitsPerSample == 24)
            {
                returnValue = new WavFile.Sample24(0);
            }
            else if (this.bitsPerSample == 32)
            {
                returnValue = new WavFile.Sample32(0);
            }
    
            return returnValue;
        }

        public String toString()
        {
            String returnValue =
                "<SamplingInfo "
                + "chunkSizeInBytes='" + this.chunkSizeInBytes + "' "
                + "formatCode='" + this.formatCode + "' "
                + "numberOfChannels='" + this.numberOfChannels + "' "
                + "samplesPerSecond='" + this.samplesPerSecond + "' "
                + "bitsPerSample='" + this.bitsPerSample + "' "
                + "/>";
    
            return returnValue;
        }        
    }
}

5. Still in the WavFormatTest directory, create a new text file name
“ProgramBuildRunAndTest.bat”, containing the text shown below. In the indicated places, substitute the path of the directory containing javac.exe and the name of the WAV file to be tested. DO NOT include the “.wav” extension!

set javaPath="[path of directory containing javac.exe]"
set wavFileName="[name (minus extension!) of WAV file to be tested]"

@echo on
%javaPath%\javac.exe *.java
%javaPath%\java.exe WavFormatTest %wavFileName%

@echo off
echo Comparison of input and output files:
echo =============================================
fc.exe %wavFileName%.wav %wavFileName%-ReadThenWritten.wav
echo =============================================
pause

6. Double-click the icon for ProgramBuildRunAndTest.bat to run it. The program will be compiled and run, a new WAV file will be written to the directory, and the new file will be compared to the original using the “fc.exe” file-comparison program. If everything works as expected, the two files should be identical.

Notes

  • As previously noted, this isn’t a very exciting program by itself, but once all of the samples for a WAV file are represented by Java objects, it makes it easy to process them in more exciting ways. Hopefully some of these possibilities may be explored in a future tutorial.
  • Note that 16-bit and 32-bit values in the WAV file format are stored in “little-endian” format, which means that the least significant byte appears first in the input stream, while the most significant byte comes last. Also, Java lacks unsigned types. So when you’re trying to translate a WAV file into Java objects, these two facts combine to create one seriously unhallowed mess. so you may notice a lot of crufty code that was written to overcome these difficulties. Just be cool.
  • This program has really only been tested on tada.wav. This means that, despite the efforts made to make it work with generalized WAV files, it’s actually pretty likely to crash if it’s run against a file with a different sample rate, sample size, or number of channels. This is to say nothing of the difficulties that might be introduced due to encoded data or discrepancies between floating-point and integer-based samples. In summary, you may have to use your gumption. I apologize for the inconvenience.
Advertisements
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

2 Responses to Reading and Writing a WAV File in Java

  1. Julian says:

    Great! Really usefull! Just a little enhancement. When you read the WAV file, not always the 2nd chunk is the data chunk, so you may have to ask if the name is “data”. I had a problem reading a file which have a “list” chink before de “data” chunk!

  2. dhavalp says:

    thanks a bunch helped a lot in android 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s