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

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

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 [email protected].

Latest Posts

Related Stories