Programming a Simple Music Synthesizer in Java

The code given below uses the WavFile class from a previous post to build a text-to-music synthesizer.

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

public class MusicSynthesisTest
{
    public static void main(String[] args)
    {
        parseAndSynthesizeSong();
    }    

    private static void parseAndSynthesizeSong()
    {
        Music.Song songToSynthesize = new Music.Song
        (
            "Dance of the Sugar Plum Fairies",
            new Music.Movement[]
            {
                Music.Movement.parseFromStrings
                (
                    new String[]
                    {
                        "O:3;R.1;____________R.4;G.8;E.8;G.4;F#4;D#4;E.4;D.8;D.8;D.8",
                        "O:1;G.4;B.4;G.4;B.4;G.4;B.4;____G.4;B.4;G.4;B.4;G.4;____B.4",
                    }
                )
            }
        );    

        WavFile songAsWavFile = MusicToWavFileConverter.convertSongToWavFile
        (
            songToSynthesize, 
            WavFile.SamplingInfo.Instances.Default
        );

        songAsWavFile.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 MusicToWavFileConverter
{
    // methods

    public static WavFile convertSongToWavFile(Music.Song songToConvert, WavFile.SamplingInfo samplingInfo)
    {
        WavFile returnValue = new WavFile
        (
            songToConvert.name + ".wav",
            samplingInfo,
            null // samples
        );

        for (int m = 0; m < songToConvert.movements.length; m++)
        {
            Music.Movement movement = songToConvert.movements[m];

            double secondsPerBeat = 1.0 / (movement.tempo.beatsPerMinute / Music.SecondsPerMinute); 

            for (int p = 0; p < movement.parts.length; p++)
            {
                Music.Part part = movement.parts[p];
                Music.Voice voice = part.voice;

                double timeOffsetInSecondsCurrent = 0;

                int numberOfNotes = part.notes.length;

                for (int n = 0; n < numberOfNotes; n++)
                {
                    Music.Note note = part.notes[n];
                    Music.Pitch notePitch = note.pitches[0];

                    if (notePitch.noteLetter.isControlCode == false)
                    {
                        double noteDurationInSeconds = 
                            movement.timeSignature.durationForBeat
                            / note.duration                
                            * secondsPerBeat;

                        addVoiceToWavFileSamples
                        (
                            returnValue, 
                            voice, 
                            note.volume,
                            notePitch.frequencyInCyclesPerSecond(),
                            timeOffsetInSecondsCurrent,
                            noteDurationInSeconds
                        );

                        timeOffsetInSecondsCurrent += noteDurationInSeconds;
                    }
                }
            }
        }

        return returnValue;
    }

    private static void addVoiceToWavFileSamples
    (
        WavFile wavFile, 
        Music.Voice voice, 
        Music.Volume volume,
        double frequencyInCyclesPerSecond,
        double timeOffsetInSecondsStart, 
        double durationToFilterInSeconds
    )
        {
            WavFile.SamplingInfo samplingInfo = wavFile.samplingInfo;
            int numberOfChannels = samplingInfo.numberOfChannels;
            int samplesPerSecond = samplingInfo.samplesPerSecond;

            int sampleIndexStart = (int)(timeOffsetInSecondsStart * samplesPerSecond);
            int durationToFilterInSamples = (int)(durationToFilterInSeconds * samplesPerSecond);
            int sampleIndexEnd = sampleIndexStart + durationToFilterInSamples;

            int durationOfWavFileInSamples = wavFile.durationInSamples();
            if (durationOfWavFileInSamples < sampleIndexEnd)
            {
                wavFile.extendOrTrimSamples(sampleIndexEnd);
            }

            WavFile.Sample[][] samplesForChannels = wavFile.samplesForChannels;

            double secondsPerSample = 1.0 / samplingInfo.samplesPerSecond;

            WavFile.Sample samplePrototype = wavFile.samplingInfo.samplePrototype();

            for (int s = sampleIndexStart; s < sampleIndexEnd; s++)
            {
                double timeOffsetInSecondsCurrent = s * secondsPerSample;

                for (int c = 0; c < numberOfChannels; c++)
                {
                    WavFile.Sample sampleExisting = samplesForChannels[c][s];                    

                    double voiceSampleValue = voice.sample
                    (
                        frequencyInCyclesPerSecond, 
                        timeOffsetInSecondsCurrent
                    ) * volume.relativeLoudness;

                    double sampleValueNew = sampleExisting.convertToDouble() + voiceSampleValue;
                    double sampleValueNewAbsolute = Math.abs(sampleValueNew);
                    if (sampleValueNewAbsolute > 1)
                    {
                        sampleValueNew = sampleValueNew / sampleValueNewAbsolute;   
                    }

                    sampleExisting.setFromDouble(sampleValueNew);
                }
            }
        }
}

class Music
{
    // constants

    public static final double SecondsPerMinute = 60;

    // inner classes 

    public static class Movement
    {
        public String name;
        public TimeSignature timeSignature;
        public Tempo tempo;
        public Part[] parts;

        public Movement(String name, TimeSignature timeSignature, Tempo tempo, Part[] parts)
        {
            this.name = name;
            this.timeSignature = timeSignature;
            this.tempo = tempo;
            this.parts = parts;
        }

        public static Movement parseFromStrings(String[] partsAsStrings)
        {
            int numberOfParts = partsAsStrings.length;
            Part[] parts = new Part[numberOfParts];
            for (int p = 0; p < numberOfParts; p++)
            {
                String partAsString = partsAsStrings[p];
                Part part = Part.parseFromString(partAsString);
                parts[p] = part;
            }

            Movement returnMovement = new Movement
            (
                "[movement from string]",
                TimeSignature.Instances.FourFour,
                Tempo.Instances.Default,
                parts
            );

            return returnMovement;
        }
    }

    public static class Note
    {
        public Pitch[] pitches;
        public Volume volume;
        public double duration;

        public Note(Pitch pitch, Volume volume, double duration)
        {
            this(new Pitch[] { pitch }, volume, duration);
        }

        public Note(Pitch[] pitches, Volume volume, double duration)
        {
            this.pitches = pitches;
            this.volume = volume;
            this.duration = duration;
        }

        public static Note parseFromString(Volume volume, Octave octave, String noteAsString)
        {
            String noteLetterAsString = noteAsString.substring(0, 2);
            String durationAsString = noteAsString.substring(2);

            Note returnValue = new Note
            (
                new Pitch
                (
                    octave,
                    NoteLetter.parseFromString(noteLetterAsString)
                ),
                volume,
                Double.parseDouble(durationAsString)
            );

            return returnValue;
        }
    }

    public static class Octave
    {
        public int octaveIndex;
        public double frequencyOfNoteLetterC;

        public Octave(int octaveIndex, double frequencyOfNoteLetterC)
        {
            this.octaveIndex = octaveIndex;
            this.frequencyOfNoteLetterC = frequencyOfNoteLetterC;
        }

        public static class Instances
        {
            public static Octave Octave0 = new Octave(0, 55);
            public static Octave Octave1 = new Octave(1, 110);
            public static Octave Octave2 = new Octave(2, 220);
            public static Octave Octave3 = new Octave(3, 440);
            public static Octave Octave4 = new Octave(4, 880);

            public static Octave[] _All = new Octave[]
            {
                Octave0,
                Octave1,
                Octave2,
                Octave3,
                Octave4,
            };
        }
    }

    public static class NoteLetter
    {
        public String symbol;
        public double frequencyMultiplier;
        public boolean isControlCode;

        public NoteLetter(String symbol, double frequencyMultiplier)
        {
            this(symbol, frequencyMultiplier, false);
        }

        public NoteLetter(String symbol, double frequencyMultiplier, boolean isControlCode)
        {
            this.symbol = symbol;
            this.frequencyMultiplier = frequencyMultiplier;
            this.isControlCode = isControlCode;
        }

        public static class Instances
        {
            private static final double tonesPerOctave = 12;
            private static final double octavesPerTone = 1 / tonesPerOctave;

            // todo
            public static NoteLetter Octave = new NoteLetter("O:", 0, true);
            public static NoteLetter Volume = new NoteLetter("V:", 0, true);

            public static NoteLetter Rest = new NoteLetter("R.", 0);

            public static NoteLetter C_ = new NoteLetter("C.", Math.pow(2, 0 * octavesPerTone));
            public static NoteLetter Cs = new NoteLetter("C#", Math.pow(2, 1 * octavesPerTone));
            public static NoteLetter D_ = new NoteLetter("D.", Math.pow(2, 2 * octavesPerTone));
            public static NoteLetter Ds = new NoteLetter("D#", Math.pow(2, 3 * octavesPerTone));
            public static NoteLetter E_ = new NoteLetter("E.", Math.pow(2, 4 * octavesPerTone));
            public static NoteLetter F_ = new NoteLetter("F.", Math.pow(2, 5 * octavesPerTone));
            public static NoteLetter Fs = new NoteLetter("F#", Math.pow(2, 6 * octavesPerTone));
            public static NoteLetter G_ = new NoteLetter("G.", Math.pow(2, 7 * octavesPerTone));
            public static NoteLetter Gs = new NoteLetter("G#", Math.pow(2, 8 * octavesPerTone));
            public static NoteLetter A_ = new NoteLetter("A.", Math.pow(2, 9 * octavesPerTone));
            public static NoteLetter As = new NoteLetter("A#", Math.pow(2, 10 * octavesPerTone));
            public static NoteLetter B_ = new NoteLetter("B.", Math.pow(2, 11 * octavesPerTone));

            public static NoteLetter[] _All = new NoteLetter[]
            {
                Octave,
                Volume,

                Rest,

                A_, As, B_, C_, Cs, D_, Ds, E_, F_, Fs, G_, Gs,
            };
        }

        public static NoteLetter parseFromString(String noteLetterAsString)
        {
            NoteLetter returnValue = null;

            NoteLetter[] noteLettersAll = NoteLetter.Instances._All;
            int numberOfNoteLetters = noteLettersAll.length;

            for (int n = 0; n < numberOfNoteLetters; n++)
            {
                NoteLetter noteLetter = noteLettersAll[n];

                if (noteLetterAsString.equals(noteLetter.symbol))
                {
                    returnValue = noteLetter;
                    break;
                }
            }

            return returnValue;
        }
    }

    public static class Part
    {
        public String name;
        public Voice voice;
        public Volume volumeInitial;
        public Note[] notes;

        public Part(String name, Voice voice, Volume volumeInitial, Note[] notes)
        {
            this.name = name;
            this.voice = voice;
            this.volumeInitial = volumeInitial;
            this.notes = notes;
        }

        public static Part parseFromString(String partAsString)
        {
            Volume volumeCurrent = Volume.Instances.Medium;
            Octave octaveCurrent = Octave.Instances.Octave3;

            partAsString = partAsString.replaceAll(" ", "");
            partAsString = partAsString.replaceAll("-", "");
            partAsString = partAsString.replaceAll("_", "");
            partAsString = partAsString.replaceAll("\t", "");
            partAsString = partAsString.replaceAll("|", "");

            String[] notesAsStrings = partAsString.split(";");
            int numberOfNotes = notesAsStrings.length;
            Note[] notes = new Note[numberOfNotes];
            for (int n = 0; n < numberOfNotes; n++)
            {
                String noteAsString = notesAsStrings[n];
                Note note = Note.parseFromString(volumeCurrent, octaveCurrent, noteAsString);
                NoteLetter noteLetter = note.pitches[0].noteLetter;

                if (noteLetter.isControlCode == true)
                {
                    int controlCodeArgument = (int)(note.duration);

                    if (noteLetter == NoteLetter.Instances.Octave)
                    {
                        octaveCurrent = Octave.Instances._All[controlCodeArgument];
                    }
                    else if (noteLetter == NoteLetter.Instances.Volume)
                    {
                        volumeCurrent = Volume.Instances._All[controlCodeArgument];
                    }        
                }

                notes[n] = note;
            }

            Part returnValue = new Part
            (
                "[part from string]",
                Voice.Instances.Sine,
                Volume.Instances.Medium,
                notes
            );

            return returnValue;
        }
    }

    public static class Pitch
    {
        public Octave octave;
        public NoteLetter noteLetter;

        public Pitch(Octave octave, NoteLetter noteLetter)
        {
            this.octave = octave;
            this.noteLetter = noteLetter;
        }

        public double frequencyInCyclesPerSecond()
        {
            double returnValue = 
                this.octave.frequencyOfNoteLetterC 
                * this.noteLetter.frequencyMultiplier;

            return returnValue;
        }
    }

    public static class Song
    {
        public String name;
        public Movement[] movements;

        public Song(String name, Movement[] movements)
        {
            this.name = name;
            this.movements = movements;
        }
    }

    public static class Tempo
    {
        public String name;
        public double beatsPerMinute;

        public Tempo(String name, double beatsPerMinute)
        {
            this.name = name;
            this.beatsPerMinute = beatsPerMinute;
        }

        public static class Instances
        {
            public static Tempo Default = new Tempo("Default", 80);

            public static Tempo Andante = new Tempo("Andante", 60);
        }
    }

    public static class TimeSignature
    {
        public double beatsPerMeasure;
        public double durationForBeat;

        public TimeSignature(double beatsPerMeasure, double durationForBeat)
        {
            this.beatsPerMeasure = beatsPerMeasure;
            this.durationForBeat = durationForBeat;
        }

        public static class Instances
        {
            public static TimeSignature FourFour = new TimeSignature(4, 4);
        }
    }

    public static abstract class Voice
    {
        public String name;

        public Voice(String name)
        {
            this.name = name;
        }

        public abstract double sample(double timeOffsetInSeconds, double frequencyInCyclesPerSecond);

        public static class Instances
        {
            public static Voice Sine = new VoiceSine();
        }
    }

    public static class VoiceSine extends Voice
    {
        public VoiceSine()
        {
            super("Sine");
        }

        public double sample(double timeOffsetInSeconds, double frequencyInCyclesPerSecond)
        {
            double timeOffsetInCycles = timeOffsetInSeconds * frequencyInCyclesPerSecond;
            double fractionOfCycleCurrent = timeOffsetInCycles - Math.floor(timeOffsetInCycles);
            double returnValue = Math.sin(fractionOfCycleCurrent * Math.PI * 2.0); 

            return returnValue;
        }        
    }

    public static class Volume
    {
        public String name;
        public double relativeLoudness;

        public Volume(String name, double relativeLoudness)
        {
            this.name = name;
            this.relativeLoudness = relativeLoudness;
        }

        public static class Instances
        {
            public static Volume Quiet = new Volume("Loud", .1);
            public static Volume Medium = new Volume("Medium", .01);
            public static Volume Loud = new Volume("Loud", .1);

            public static Volume[] _All = new Volume[]
            {
                Quiet,
                Medium,
                Loud,
            };
        }
    }
}

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)
    {
        this(filePath, WavFile.SamplingInfo.Instances.Default, null);
    }

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

        // hack
        if (this.samplingInfo == null)
        {
            this.samplingInfo = WavFile.SamplingInfo.Instances.Default;
        }

        if (this.samplesForChannels == null)
        {
            int numberOfChannels = this.samplingInfo.numberOfChannels; 

            this.samplesForChannels = new Sample[numberOfChannels][];
            for (int c = 0; c < numberOfChannels; c++)
            {
                this.samplesForChannels[c] = new Sample[0];
            }
        }
    }

    // static methods

    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;
    }

    // instance methods

    public int durationInSamples()
    {
        int returnValue = 0;
        if (this.samplesForChannels != null)
        {
            if (this.samplesForChannels.length > 0)
            {
                returnValue = this.samplesForChannels[0].length;
            }
        }

        return returnValue;        
    }

    public void extendOrTrimSamples(int numberOfSamplesToExtendOrTrimTo)
    {
        int numberOfChannels = this.samplingInfo.numberOfChannels;
        Sample[][] samplesForChannelsNew = new Sample[numberOfChannels][];

        for (int c = 0; c < numberOfChannels; c++)
        {
            Sample[] samplesForChannelOld = this.samplesForChannels[c];
            Sample[] samplesForChannelNew = new Sample[numberOfSamplesToExtendOrTrimTo];

            for (int s = 0; s < samplesForChannelOld.length && s < numberOfSamplesToExtendOrTrimTo; s++)
            {
                samplesForChannelNew[s] = samplesForChannelOld[s];                
            }

            Sample samplePrototype = this.samplingInfo.samplePrototype();

            for (int s = samplesForChannelOld.length; s < numberOfSamplesToExtendOrTrimTo; s++)
            {
                samplesForChannelNew[s] = samplePrototype.build();
            }

            samplesForChannelsNew[c] = samplesForChannelNew;
        }

        this.samplesForChannels = samplesForChannelsNew;
    }

    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 build();
        public abstract Sample setFromBytes(byte[] valueAsBytes);
        public abstract Sample setFromDouble(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.build().setFromBytes
                    (
                        sampleValueAsBytes
                    );
                }
            }

            return returnSamples;
        }

        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 class Sample16 extends Sample
    {
        public static double MaxValueAsDouble = (double)(Short.MAX_VALUE);

        public short value;

        public Sample16(short value)
        {
            this.value = value;
        }

        // Sample members
        public Sample build()
        {
            return new Sample16((short)0);
        }

        public Sample setFromBytes(byte[] valueAsBytes)
        {
            this.value = (short)
            (
                ((valueAsBytes[0] & 0xFF))
                | (short)((valueAsBytes[1] & 0xFF) << 8 )
            );

            return this;
        }

        public Sample setFromDouble(double valueAsDouble)
        {
            this.value =
            (
                (short)
                (
                    valueAsDouble
                    * Short.MAX_VALUE
                )
            );

            return this;
        }

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

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

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

        public int value;

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

        // Sample members

        public Sample build()
        {
            return new Sample24(0);
        }

        public Sample setFromBytes(byte[] valueAsBytes)
        {
            this.value =
            (
                ((valueAsBytes[0] & 0xFF))
                | ((valueAsBytes[1] & 0xFF) << 8 )
                | ((valueAsBytes[2] & 0xFF) << 16)
            );

            return this;
        }

        public Sample setFromDouble(double valueAsDouble)
        {
            this.value = 
            (
                (int)
                (
                    valueAsDouble
                    * Integer.MAX_VALUE
                )
            );

            return this;
        }

        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 static double MaxValueAsDouble = (double)(Integer.MAX_VALUE);

        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 build()
        {
            return new Sample32(0);
        }

        public Sample setFromBytes(byte[] valueAsBytes)
        {
            this.value = 
            (
                ((valueAsBytes[0] & 0xFF))
                | ((valueAsBytes[1] & 0xFF) << 8 )
                | ((valueAsBytes[2] & 0xFF) << 16)
                | ((valueAsBytes[3] & 0xFF) << 24)
            );

            return this;
        }

        public Sample setFromDouble(double valueAsDouble)
        {
            this.value = 
            (
                (int)
                (
                    valueAsDouble
                    * Integer.MAX_VALUE
                )
            );

            return this;
        }

        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 / Sample32.MaxValueAsDouble;
        }    
    }

    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;
        }        
    }
}
This entry was posted in Uncategorized and tagged , , , , . Bookmark the permalink.

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