Sunday, November 23, 2014

An Introduction to Java Sound

This article talks about how to include sound into your project. We will assume you have a .wav file to input into your program. Sound files can be created and edited with Audacity. I googled Audacity, selected Audacity: Free Audio Editor and Recorder (Source Forge), Download, Audacity for Windows®, Audacity 2.0.3 installer. We will be adding to the project created in the previous posts. I have the following directory structure:
  • "C:\Users\Scott\workspace\myrepo" is a clone of the myrepo.git central repository.
  • "C:\Users\Scott\workspace\myrepo\my-app" is the project folder for my Maven/Java Project.
  1. Configure your input and output directories for your project.
    • In the my-app folder (project folder) add two new directories, input and output.
    • In the output folder create a .gitignore with the following two lines:
    • Note: To create the .gitignore file with Notepad, you can create a new Text Document, edit in Notepad, then File->Save As, and Enter File name: ".gitignore" (include the double quotes here) and Save as type: "All Files".
    • Note: Git doesn't track folders, just files. We want other developers to get the output folder when they clone the central repository but we don't want them to get all the files in the output folder. This setup ensures that they get only the output folder and the .gitignore file.
  2. Create input.wav using Audacity.
    • Install Audacity.
    • Launch Audacity.
    • Create a .wav file that is about ten seconds of continuous sound.
    • Name the .wav file input.wav and save the file to your input folder (ie "C:\Users\Scott\workspace\myrepo\my-app\input\input.wav").
    • Note: In Audacity, you can either open a file or record sound.
    • Note: In Audacity, you can use the Selection Tool and Trim Audio to extract a few seconds.
    • Note: In Audacity, you can File->Export and Enter Save as type: "WAV (Microsoft) signed 16 bit PCM.
  3. Create the WavFileHelper Class.
    • In Eclipse, Package Explorer, right click on src/main/java->New->Package, Enter "com.mycompany.app.util".
    • In Eclipse, Package Explorer, right click on the com.mycompany.app.util package->New->Class, Enter "WavFileHelper.
    • Add the following code:
      package com.mycompany.app.util;

      import java.io.*;
      import java.nio.ByteBuffer;
      import java.nio.ByteOrder;
      import java.util.LinkedHashMap;

      import javax.sound.sampled.*;

      /**
       * This class reads a .wav file and converts it to a bunch of byte arrays.
       * 
       * The info represented by these byte arrays is then printed out.
       * 
       * An example of playing these byte arrays with the speakers is used.
       * 
       * It also converts the byte arrays to a .wav file.
       * 
       * An extension of this concept can record from a microphone. In this case, some
       * values like sampling rate would need to be assumed.
       * 
       * See https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ for .wav file
       * spec
       * 
       * @author sizu
       */

      public class WavFileHelper {

          public static void main(String[] args) {
              final String NEWLINE = "\n";
              int recordingSampleRate = 22050;
              short recordingBitsPerSample = 16;
              short recordingNumChannels = 2;
              String inputPath = "/Users/Scott/workspace/myrepo/my-app/input/input.wav"// Place the wav file in the top level
                                                  // directory, ie S:/input.wav
              String outputPath = "/Users/Scott/workspace/myrepo/my-app/output/output.wav";
              String recordedPath = "/Users/Scott/workspace/myrepo/my-app/output/capture.wav";

              System.out.println("START");
              try {
                  if (!new File(inputPath).exists()) {
                      System.err.println("Error loading file:"
                              + new File(inputPath).getAbsolutePath());
                  }

                  WavFileHelper wavFileHelper = new WavFileHelper();

                  WavData wavRecordData = new WavData();
                  wavRecordData.put(WaveSection.SAMPLE_RATE, recordingSampleRate);
                  wavRecordData.put(WaveSection.BITS_PER_SAMPLE,
                          recordingBitsPerSample);
                  wavRecordData.put(WaveSection.NUM_CHANNELS, recordingNumChannels);

                  System.out.println(NEWLINE + "CONVERT WAV FILE TO BYTE ARRAY");
                  WavData wavInputData = wavFileHelper.read(new File(inputPath));

                  System.out.println(NEWLINE + "CONVERT BYTE ARRAY TO WAV FILE");
                  wavFileHelper.write(new File(outputPath), wavInputData);

                  System.out.println(NEWLINE
                          + "DISPLAY BYTE ARRAY INFORMATION FOR INPUT FILE");
                  wavInputData.printByteInfo();

                  System.out
                          .println(NEWLINE
                                  + "START RECORDING - You can connect the microphone to the speakers");
                  WavAudioRecorder recorder = new WavFileHelper.WavAudioRecorder(
                          wavRecordData);
                  recorder.startRecording();

                  System.out.println(NEWLINE
                          + "PLAY BYTE ARRAY (THIS WILL BE RECORDED)");
                  WavAudioPlayer player = new WavFileHelper.WavAudioPlayer(
                          wavInputData);
                  player.playAudio();

                  System.out.println(NEWLINE + "STOP RECORDING FOR RECORDING");
                  recorder.stopRecording();

                  System.out.println(NEWLINE + "DISPLAY BYTE ARRAY INFORMATION");
                  wavRecordData.printByteInfo();

                  System.out.println(NEWLINE + "SAVE RECORDING IN WAV FILE");
                  wavFileHelper.write(new File(recordedPath), wavRecordData);

              } catch (Exception ex) {
                  ex.printStackTrace();
              }
              System.out.println("FINISH");
          }

          public WavData read(File inputFile) throws Exception {
              WavData returnVal = new WavData();

              // Analyze redundant info
              int dataSize = (int) inputFile.length() - WavData.HEADER_SIZE;
              WaveSection.DATA.numBytes = dataSize; // Can't have two threads
                                                      // using this at the same
                                                      // time

              // Read from File
              DataInputStream inFile = new DataInputStream(new FileInputStream(
                      inputFile));

              for (WaveSection waveSection : WaveSection.values()) {
                  byte[] readBytes = new byte[waveSection.numBytes];
                  for (int i = 0; i < waveSection.numBytes; i++) {
                      readBytes[i] = inFile.readByte();
                  }
                  returnVal.put(waveSection, readBytes);
              }

              inFile.close();

              return returnVal;
          }

          public void write(File outputFile, WavData waveData) throws Exception {
              // Analyze redundant info
              int dataSize = waveData.get(WaveSection.DATA).length;
              waveData.put(WaveSection.CHUNK_SIZE, dataSize + 36);
              waveData.put(WaveSection.SUBCHUNK2_SIZE, dataSize);

              int byteRate = waveData.getInt(WaveSection.SAMPLE_RATE)
                      * waveData.getShort(WaveSection.BLOCK_ALIGN);
              waveData.put(WaveSection.BYTE_RATE, byteRate);

              // Write to File
              DataOutputStream dataOutputStream = new DataOutputStream(
                      new FileOutputStream(outputFile));

              for (WaveSection waveSection : WaveSection.values()) {
                  dataOutputStream.write(waveData.getBytes(waveSection));
              }

              dataOutputStream.close();
          }

          public static enum WaveSection {
              // 12 Bytes
              CHUNK_ID(4, ByteOrder.BIG_ENDIAN), CHUNK_SIZE(4,
                      ByteOrder.LITTLE_ENDIAN), FORMAT(4, ByteOrder.BIG_ENDIAN),

              // 24 Bytes
              SUBCHUNK1_ID(4, ByteOrder.BIG_ENDIAN), SUBCHUNK1_SIZE(4,
                      ByteOrder.LITTLE_ENDIAN), AUDIO_FORMAT(2,
                      ByteOrder.LITTLE_ENDIAN), NUM_CHANNELS(2,
                      ByteOrder.LITTLE_ENDIAN), SAMPLE_RATE(4,
                      ByteOrder.LITTLE_ENDIAN), BYTE_RATE(4, ByteOrder.LITTLE_ENDIAN), BLOCK_ALIGN(
                      2, ByteOrder.LITTLE_ENDIAN), BITS_PER_SAMPLE(2,
                      ByteOrder.LITTLE_ENDIAN),

              // 8 Bytes
              SUBCHUNK2_ID(4, ByteOrder.BIG_ENDIAN), SUBCHUNK2_SIZE(4,
                      ByteOrder.LITTLE_ENDIAN), DATA(0, ByteOrder.LITTLE_ENDIAN), ;

              private Integer numBytes;
              private ByteOrder endian;

              WaveSection(Integer numBytes, ByteOrder endian) {
                  this.numBytes = numBytes;
                  this.endian = endian;
              }
          }

          public static class WavData extends LinkedHashMap {
              static final long serialVersionUID = 1;
              static int HEADER_SIZE = 44; // There are 44 bits before the data
                                              // section
              static int DEFAULT_SUBCHUNK1_SIZE = 16;
              static short DEFAULT_AUDIO_FORMAT = 1;
              static short DEFAULT_BLOCK_ALIGN = 4;
              static String DEFAULT_CHUNK_ID = "RIFF";
              static String DEFAULT_FORMAT = "WAVE";
              static String DEFAULT_SUBCHUNK1_ID = "fmt ";
              static String DEFAULT_SUBCHUNK2_ID = "data";

              public WavData() {
                  this.put(WaveSection.CHUNK_ID, DEFAULT_CHUNK_ID);
                  this.put(WaveSection.FORMAT, DEFAULT_FORMAT);
                  this.put(WaveSection.SUBCHUNK1_ID, DEFAULT_SUBCHUNK1_ID);
                  this.put(WaveSection.SUBCHUNK1_SIZE, DEFAULT_SUBCHUNK1_SIZE);
                  this.put(WaveSection.AUDIO_FORMAT, DEFAULT_AUDIO_FORMAT);
                  this.put(WaveSection.BLOCK_ALIGN, DEFAULT_BLOCK_ALIGN);
                  this.put(WaveSection.SUBCHUNK2_ID, DEFAULT_SUBCHUNK2_ID);

                  this.put(WaveSection.CHUNK_SIZE, 0);
                  this.put(WaveSection.SUBCHUNK2_SIZE, 0);
                  this.put(WaveSection.BYTE_RATE, 0);
              }

              public void put(WaveSection waveSection, String value) {
                  byte[] bytes = value.getBytes();
                  this.put(waveSection, bytes);
              }

              public void put(WaveSection waveSection, int value) {
                  byte[] bytes = ByteBuffer.allocate(4)
                          .order(ByteOrder.LITTLE_ENDIAN).putInt(value).array();
                  this.put(waveSection, bytes);
              }

              public void put(WaveSection waveSection, short value) {
                  byte[] bytes = ByteBuffer.allocate(2)
                          .order(ByteOrder.LITTLE_ENDIAN).putShort(value).array();
                  this.put(waveSection, bytes);
              }

              public byte[] getBytes(WaveSection waveSection) {
                  return this.get(waveSection);
              }

              public String getString(WaveSection waveSection) {
                  byte[] bytes = this.get(waveSection);
                  return new String(bytes);
              }

              public int getInt(WaveSection waveSection) {
                  byte[] bytes = this.get(waveSection);
                  return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)
                          .getInt();
              }

              public short getShort(WaveSection waveSection) {
                  byte[] bytes = this.get(waveSection);
                  return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)
                          .getShort();
              }

              public void printByteInfo() {
                  for (WaveSection waveSection : WaveSection.values()) {
                      if (waveSection.numBytes == 4
                              && waveSection.endian == ByteOrder.BIG_ENDIAN) {
                          System.out.println("SECTION:" + waveSection + ":STRING:"
                                  + this.getString(waveSection));
                      } else if (waveSection.numBytes == 4
                              && waveSection.endian == ByteOrder.LITTLE_ENDIAN) {
                          System.out.println("SECTION:" + waveSection + ":INTEGER:"
                                  + this.getInt(waveSection));
                      } else if (waveSection.numBytes == 2
                              && waveSection.endian == ByteOrder.LITTLE_ENDIAN) {
                          System.out.println("SECTION:" + waveSection + ":SHORT:"
                                  + this.getShort(waveSection));
                      } else {
                          // Data Section
                      }
                  }
              }

              public AudioFormat createAudioFormat() {
                  boolean audioSignedSamples = true// Samples are signed
                  boolean audioBigEndian = false;
                  float sampleRate = (floatthis.getInt(WaveSection.SAMPLE_RATE);
                  int bitsPerSample = (intthis
                          .getShort(WaveSection.BITS_PER_SAMPLE);
                  int numChannels = (intthis.getShort(WaveSection.NUM_CHANNELS);
                  return new AudioFormat(sampleRate, bitsPerSample, numChannels,
                          audioSignedSamples, audioBigEndian);
              }
          }

          public static class WavAudioPlayer {
              WavData waveData = new WavData();

              public WavAudioPlayer(WavData waveData) {
                  this.waveData = waveData;
              }

              public void playAudio() throws Exception {
                  byte[] data = waveData.getBytes(WaveSection.DATA);

                  // Create an audio input stream from byte array
                  AudioFormat audioFormat = waveData.createAudioFormat();
                  InputStream byteArrayInputStream = new ByteArrayInputStream(data);
                  AudioInputStream audioInputStream = new AudioInputStream(
                          byteArrayInputStream, audioFormat, data.length
                                  / audioFormat.getFrameSize());

                  // Write audio input stream to speaker source data line
                  DataLine.Info dataLineInfo = new DataLine.Info(
                          SourceDataLine.class, audioFormat);
                  SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem
                          .getLine(dataLineInfo);
                  sourceDataLine.open(audioFormat);
                  sourceDataLine.start();

                  // Loop through input stream to write to source data line
                  byte[] tempBuffer = new byte[10000];
                  int cnt;
                  while ((cnt = audioInputStream.read(tempBuffer, 0,
                          tempBuffer.length)) != -1) {
                      sourceDataLine.write(tempBuffer, 0, cnt);
                  }

                  // Cleanup
                  sourceDataLine.drain();
                  sourceDataLine.close();
                  byteArrayInputStream.close();
              }
          }

          public static class WavAudioRecorder implements Runnable {
              WavData waveData = new WavData();
              boolean recording = true;
              Thread runningThread;
              ByteArrayOutputStream byteArrayOutputStream;

              public WavAudioRecorder(WavData waveData) {
                  this.waveData = waveData;
              }

              public void startRecording() {
                  this.recording = true;
                  this.runningThread = new Thread(this);
                  runningThread.start();
              }

              @SuppressWarnings("deprecation")
              public WavData stopRecording() throws Exception {
                  this.recording = false;
                  runningThread.stop();

                  waveData.put(WaveSection.DATA, byteArrayOutputStream.toByteArray());

                  return waveData;
              }

              public void run() {
                  try {
                      // Create an audio output stream for byte array
                      byteArrayOutputStream = new ByteArrayOutputStream();

                      // Write audio input stream to speaker source data line
                      AudioFormat audioFormat = waveData.createAudioFormat();
                      DataLine.Info info = new DataLine.Info(TargetDataLine.class,
                              audioFormat);
                      TargetDataLine targetDataLine = (TargetDataLine) AudioSystem
                              .getLine(info);
                      targetDataLine.open(audioFormat);
                      targetDataLine.start();

                      // Loop through target data line to write to output stream
                      int numBytesRead;
                      byte[] data = new byte[targetDataLine.getBufferSize() / 5];
                      while (recording) {
                          numBytesRead = targetDataLine.read(data, 0, data.length);
                          byteArrayOutputStream.write(data, 0, numBytesRead);
                      }

                      // Cleanup
                      targetDataLine.stop();
                      targetDataLine.close();
                      byteArrayOutputStream.close();
                  } catch (Exception ex) {
                      ex.printStackTrace();
                  }
              }
          }
      }
  4. Right click within WavFileHelper->Run As->Java Application.
    • You may also use the short cut: Alt+Shift+X, J.
    • Or from the Windows command prompt:
      "C:\Program Files\Java\jdk1.7.0_25\bin\javac.exe" "C:\Users\Scott\workspace\myrepo\my-app\src\main\java\com\mycompany\app\util\WavFileHelper.java"
      "C:\Program Files\Java\jdk1.7.0_25\bin\java.exe" -classpath "C:\Users\Scott\workspace\myrepo\my-app\src\main\java" com.mycompany.app.util.WavFileHelper
The recorded file may have either no sound or softer sound depending on your speakers and microphone setup. Also, if you have git setup, you should push these changes to your central repository as demonstrated in Step 7 of Adding Git Version Control to Your Project: Part I.

This post was reposted from http://scottizu.wordpress.com/2013/08/18/an-introduction-to-java-sound/, originally written on August 18th, 2013.

No comments:

Post a Comment