October 31, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Writing a Seamless Looper in Java, Part 1: Playing Audio

  • July 31, 2001
  • By Greg Travis
  • Send Email »
  • More Articles »

Introduction

Audio programming can be tricky, but Java can make it a lot easier. In this article, we'll be discussing the construction of a Sound Looper -- a program that loops a snippet of sound over and over again. We'll also be discussing how to transition smoothly from one sound loop to another.

Part 1 of this two-part series concerns itself with the javax.sound.sampled package. We'll learn how it's used to access your machine's sound hardware for audio playback.

The High Concept

The software we're going to create does one thing: it plays a sound snippet over and over, without pausing between plays. If you choose your snippet properly, the repeating loop can be a basis for a piece of music. We'll call this software Looper.

Another thing that our software is going to do is to allow you to switch between different loops. The tricky part is making it work seamlessly -- making the rhythms of the different snippets line up properly. Getting this working means we have to learn about the all-important concept of latency.

Basic Audio Streaming

Before we get into any subtle details, we're going to run through the steps required to read a sound off of disk and play it through your sound hardware. These are standard operations -- they are not specific to the Looper class.

Reading the Data

One of the most important features of the javax.sound.sampled package is its ability to hide data format details from the programmer. We're going to assume that you have some audio data sitting on your hard drive, but we're not going to make any assumptions about what format it is in, because that should be taken care of automatically by the system. More precisely, the audio system is going to do its best to automatically convert your audio data to the correct format for playback on your sound hardware.

The first step is to get an InputStream from the audio system. This is done as follows:

      // Grab an audio stream connected to the file
      AudioInputStream ain = AudioSystem.getAudioInputStream( file );

An AudioInputStream has a lot of audio-related methods associated with it, but we're not going to need them for this application. For our purposes, all that matters is that an AudioStream is a standard input stream based on java.io.InputStream.

The next step is to read the entire file into memory. We can take this drastic measure only because we're dealing with small pieces of audio. (So do not try to load the entire Ring Cycle using this technique.)

      // How long, in *samples*, is the file?
      long len = ain.getFrameLength();

      // 16-bit audio means 2 bytes per sample, so we need
      // a byte array twice as long as the number of samples
      byte bs[] = new byte[(int)len*2];

      // Read everything, and make sure we got it all
      int r = ain.read( bs );
      if (r != len*2)
        throw new RuntimeException( "Read only "+r+" of "+file+" out of "+
          (len*2)+" sound bytes" );

At this point, we have the whole thing loaded into a byte array. This is a good way to hold it, since we're going to also want it as a byte array when we play the snippet back.

Playing the Data

Now that we have our sound snippet in memory, we want to play it through the sound hardware. The first thing we have to do for this is create an object that describes the kind of output audio stream we want.

To do this, we need to first create an AudioFormat object. This encodes such settings as sample rate, bits-per-sample, encoding format, and so on. In a real application, this should probably be configurable by the user, but we can just make a reasonable assumption and hard-code it:

  static public AudioFormat stdFormat =
    new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, 44100.0f, 16, 1, 2,
      44100.0f, false );

The above format is roughly the same format that CD audio is stored in. This is about as standard as you can get, so it should work in most cases.

Now that we've decided on our format, we have to create a DataLine.Info object. This object includes not only the settings found in stdFormat, but also specifies that we want a SourceDataLine. Awkwardly, SourceDataLine is the name used to describe a stream that you write audio to, such as when you're playing audio. A TargetDataLine is used to describe a stream that you read audio from, such as when you're recording audio.

      DataLine.Info info =
        new DataLine.Info( SourceDataLine.class, stdFormat );

Now, we can create our stream, and get it ready to play:

      SourceDataLine sdl = (SourceDataLine)AudioSystem.getLine( info );
      sdl.open();
      sdl.start();

You can use the SourceDataLine just like an output stream, since it is one. Playing audio data is as simple as writing to the stream:

        int returnValue = sdl.write( rawBytes, 0, numBytes );

Looping Audio

In its simplest form, looping a snippet of audio is as simple as writing it over and over:
      while (true) {
        int returnValue = sdl.write( snippet, 0, snippet.length );
      }

However, in Looper, we are only going to write a small portion of the audio at a time. This allows us to abort the playing of the current sound at any time and switch quickly to another one. (This will be explained more fully in Part Two.)

    // Write at most this many bytes per inner loop execution
    static private final int innerLoopWriteSize = 512;

    // The position within the currently playing sound
    // (i.e. the next sample to write)
    int cursor = 0;

    while (true) {
      // How many bytes are the left to write from this snippet?
      int bytesLeft = snippet.length-cursor;

      // If we've reached the end, start from the top
      // of the sound
      if (bytesLeft<=0) {
        // restart sound
        cursor = 0;
        bytesLeft = currentRaw.length;
      }

      // Don't write more than this in one loop
      int towrite = innerLoopWriteSize;
      if (towrite > bytesLeft)
        towrite = bytesLeft;

      // Write a chunk
      int r = sdl.write( snippet, cursor, towrite );

      // Remember how much we wrote, by advancing the cursor
      // to the next chunk of sound
      cursor += r;
    }

Note that we use cursor to keep track of where we are in the sample. Each time we play some audio, we play it starting from cursor, and then we increase cursor by the amount written. When cursor reaches the end of the sample, we wrap it around to the beginning.

Summary

We've looked at the basics of using the javax.sound.sampled package to play a looped sound. But we've only addressed the basics. In Part Two, we'll look at some of the more subtle issues surrounding real-time sound programming and audio latency.

About the Author

Greg Travis is a freelance programmer living in New York City. His interest in computers can probably be traced back to the episode of "The Bionic Woman" where Jamie runs around trying to escape a building whose lights and doors are controlled by an evil artificial intelligence which mocks her through loudspeakers. He's a devout believer in the religious idea that when a computer program works it's a complete coincidence. He can be reached at mito@panix.com.






Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel