« Bring on the Mobile Pants | Main | Rookie phone OS mistakes- brought to you by Microsoft »

Java Audio File Processing for loops and samples

Let's talk about sound. Making interesting new sounds is challenging- they even gives Oscars for it! I focus on making samples and loops for use with software like Sonar and Acid and my Alesis QSR. Here I will focus on the techniques for creating Alesis QCards I have developed over the years using the Java language. The truth is there are tools that do what I want, but I don't own them. I prefer to hack my own way.

The general process:
get a cool sound (such as from my custom java synthesizer, mini analog synths, bent toys, or other techniques)
turn it into a smoothly looping audio file (here I use Sound Forge)
set the audio file properties (see the RIFF file specification and my code samples) to make a file that the QSR will play as a loop when played from a midi keyboard
have fun with your unique sounds and make some music

I'm not planning to make much effort to explain the code below. If you are reading this, you can figure it out, but hopefully it is not easy ;) You should take a look at the RIFF specification. Go google it.

This snippet is called from a java class with a 'main' method from the command line. I actually use a batch file and run from a directory full of audio files to be processed. Key
points are that wav files are full of bytes of audio data, but 16-bit audio is composed of two byte values (I only deal with mono), so conversion is required.

  /*
     * This takes a regular wave file and adds the extra
     * data (smpl chunk) to it and makes it 'loop forward'. Takes two inputs:
     * file name and midi pitch (of the sample).
     */
    public static void convertWavToQSRLoop (String fileName, int midiPitch) throws IOException
    {
     //store the wav data in 16-bit sample format
     short[] data = null;
     //store the wav data in byte format
     byte[] dataChunk = null;
     //must read in the datachunk from the original file
     RandomAccessFile output = new RandomAccessFile(fileName, "rw");
        //skip to where the size of the data chunk is stored
     output.seek(40);
     //read the size
     byte[] size = new byte[4];
     output.read(size);
     int dataSize = getLittleEndianInt(size);
     //read in the byte audio data
     dataChunk = new byte[dataSize];
        output.read(dataChunk);
     //convert to short samples
        data = byteToShort(dataChunk);
        System.out.println("number of samples is " + data.length);
        //changed resulting file name
     writeMonoAudioLoop (data, "qsr_" + fileName, midiPitch);
    }
   

This is required to read certain 4 byte values from a data file (unless I'm totally stupid). I tried regular java techniques and they could not read the values properly. It is not simple enough to read and write integer values, since java use a platform independent technique but I use an Intel-based computer which uses little endian data formats.

    public static int getLittleEndianInt(byte[] bytes) throws IOException
    {
        //reverse
        byte b1 = bytes[0];
        byte b2 = bytes[1];
        byte b3 = bytes[2];
        byte b4 = bytes[3];
       
        bytes[0] = b4;
        bytes[1] = b3;
        bytes[2] = b2;
        bytes[3] = b1;
       
        //reverse and re-read
        DataInputStream bais = new DataInputStream(new ByteArrayInputStream(bytes));
        return bais.readInt();
    }
   
This converts an array of bytes from a file that represents audio data into an array of 16-bit audio samples.

    public static short[] byteToShort(byte[] data)
    {
        byte lsb;
        byte msb;
       
        short[] output = new short[data.length / 2];
       
        int index = 0;
        for (int i = 0; i < data.length; i+=2)
        {
         //read two bytes at a time( lsb and msb) and convert to a short. lsb is first (littleendian)?
            lsb = (data[i]);
            msb = (data[i + 1]);
           
            output[index++] = (short)((msb * 255) + lsb);
        }
       
        return output;
    }

Once you get the samples, it is time to write the file. Java Sound is used, somewhat. One trick is getting the pitch. This is non-trivial because many sounds/samples don't necessarily have a pitch. You must listen and compare OR use a guitar tuner and see what you get. Even so, you will likely have to fine tune the samples (+/- tens of cents) in your synthesizer patch editor to get them to be in tune.

    /**
     * For writing mono PCM wav files (CD quality) Key point to this method is that it
    * operates on an array of 16-bit audio samples (mono or stereo). Adds data for making loops.
     */
    public static boolean writeMonoAudioLoop (short[] data, String fileName, int midiPitch)
    {   
        return writeAudioLoop(data, fileName, MONO_WAV_CD, midiPitch);
    }   


The key to audio file modification is to use RandomAccessFile. Some file parameters change once you add the extra 'smpl' chunk data so you must go back and re-write them.

 /**
    * For use when other format is desired. Key point to this method is that it
    * operates on an array of 16-bit audio samples (mono or stereo).
    *
    */
    public static boolean writeAudioLoop(short[] data, String fileName, AudioFormat format, int midiPitch)
    {
        //convert short[] to byte[]
        byte[] byteSamples = shortToByte(data);
        //System.out.println("number of samples written to file= " + data.length);
                      
        AudioInputStream stream = null;
               
        // For encodings like PCM, a frame consists of the set of samples for all
        //channels at a given point in time, and so the size of a frame (in bytes)
        //is always equal to the size of a sample (in bytes) times the number of channels.
        RandomAccessFile output = null;

        try
        {
           
    /**
     * How to add the extra bytes to the audio file. First- open file with intent to append to
     * end of it. Second- create the data to append, based on know byte values to insert PLUS
     * knowledge about the size of the sample data chunk (need to know sample start/end points).
     * Write the new data to end of existing file and make other needed changes.
     *
     * Need to write a bunch of 4 byte little endian int values to the file. Only special value is the     * sample end value. Rest of values are zero- no shit.
     */

         File sampleFile = new File(fileName);
            stream = new AudioInputStream(new ByteArrayInputStream(byteSamples), format, data.length);
           
            AudioSystem.write(stream, AudioFileFormat.Type.WAVE, sampleFile);
            System.out.println("generated file " + fileName);
            stream.close();
            output = new RandomAccessFile(sampleFile, "rw");
            //add the new code to put in the "Loop forward" values in the sample chunk
            //data.length = number of samples (minus 1 is sample end point, I believe- zero based)
            //byes of data = data.length * 2 (data is short[])
           
            System.out.println("wrote samples=" + data.length);
            System.out.println("writing sample chunk info");
           
            int originalSize = (int)output.length();//OK to narrow
            output.seek(originalSize);//go to end of file?
            System.out.println("end of file=" + originalSize);
            output.writeBytes("smpl");//lowercase per the spec
           
            //must write little endian format- what's up? thought this would be the norm, but it is not.
           
            int sampleDataSize = 36 + 24 + (data.length * 2);
            //write chunks of 4 byte arrays
            output.write(getLittleEndianInt(60));//just the chunk data- not sample data size!!
            output.writeInt(0);//manufacturer- n/a
            output.writeInt(0);//product- n/a
            output.write(getLittleEndianInt(22675));//sample period- 1/44100 in nanoseconds       
            output.write(getLittleEndianInt(midiPitch));//midi unity note (sample root note)
            output.writeInt(0);//midi pitch fraction- 0 means no sample fine tuning
            output.writeInt(0);//smpte format- not used
            output.writeInt(0);//smpte offset- no offset
            output.write(getLittleEndianInt(1));//num sample loops- one
            output.writeInt(0);//sample data- no extra data needed so use zero
           
            //list of sample loops
            output.writeInt(0);//cue point ID
            output.writeInt(0);//type - the all-important "loop forward"
            output.writeInt(0);//start
            output.write(getLittleEndianInt(data.length - 1));//end (in samples)
            output.writeInt(0);//fraction of sample to loop at (not used)
            output.writeInt(0);//playcount; 0 = infinite sustain

            //change overall file size in original wav header!! -original plus 68.
            output.seek(4);
            output.write(getLittleEndianInt(originalSize + 68));
           
        }
        catch (IOException e)
        {
            e.printStackTrace();
            return false;
        }
        finally
        {
         try
         {
          output.close();
         }
         catch (IOException e)
         {
          e.printStackTrace();
          return false;
         }
        }
       
        //if all went OK
        return true;
    }
 

Still with me? Files to download:

http://www.erichizdepski.com/code/AudioUtils.java

http://www.erichizdepski.com/code/QSRLooper.java

http://www.erichizdepski.com/code/WavReader.java

 

 

 

TrackBack

TrackBack URL for this entry:
http://www.erichizdepski.com/blog-mt1/mt-tb.fcgi/19


Hosting by Yahoo!

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)