Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

A Java-Based Music Player for MP3, Ogg, & WAV


Feb03: A Java-Based Music Player For MP3, Ogg, & WAV

Sing is an author and consultant, and his most recent book is Early Adopter JXTA: P2P Java (Wrox Press, 2001). He can be reached at [email protected].


The speed of today's desktop PCs coupled with breakthroughs in Java Virtual Machine (VM) optimization technology opens the door to Java-based, real-time signal processing, like that required by digital music players. In this article, I'll examine the design and construction of a digital music player written in Java that supports real-time decoding/playback of music in MP3, WAV, and Ogg Vorbis formats. In the process, I'll also explore audio programming using the JavaSound API.

JavaSound is a lightweight, low-level programming API that supports recording/playback of digital audio and MIDI data. Beginning with Java 2 Version 1.3.0, JavaSound is part of the base JDK distribution. Any JavaSound-based audio application (such as my music player) should run without additional software downloads or installation on any Java 1.3.x implementation across operating system and hardware platforms. Before JDK 1.3.0, JavaSound was a standard Java extension API. However, the JavaSound API is currently available for JDK 1.1 (and later) through the Java Media Framework (JMF) distribution (http://java.sun.com/products/java-media/jmf/). There is also an open-source JavaSound implementation (http://www.tritonus.org/), which was written to provide Linux support.

JavaSound Architecture

Figure 1 illustrates the general architecture of the JavaSound API. The thick dotted lines indicate the API calls specified by Sun Microsystem's JavaSound standard. The one at the top is the API you write to. The JavaSound APIs are contained in the javax.sound.sampled and javax.sound.midi packages. The area in the middle of Figure 1 is the actual implementation of the JavaSound API.

Like the API on top, the Service Provider Interface (SPI) is standardized across all implementations of JavaSound. The SPI is created to support customized extensions in the form of plug-ins. By coding plug-ins extensions compatible with this SPI, you can extend the features of the JavaSound implementation without modifying the implementation itself. The SPI is contained in the javax.sound.sampled.spi and javax.sound.midi.spi packages.

One interesting outcome of this architecture is the ability to extend JavaSound features without affecting existing applications. In fact, you can add features to applications without changing the code. For example, consider a music player application that supports only WAV-file playback. By adding a plug-in module supporting the decoding of MP3 files, you can add MP3 playback as a feature to the player—without changing a single line of code in the player itself.

Also, well-designed SPI extensions (plug-ins) are independent of both the JavaSound implementation and the platform. Take, for example, a plug-in that supports the decoding of MP3 encoded music files written to the JavaSound SPI. When coded in 100-percent Java, this plug-in is compatible with all JavaSound implementations across the hardware/software platform. Figure 2 shows how the same plug-in code can run across three completely different platforms and operating systems.

JavaSound Basics

Figure 3 illustrates a JavaSound-based audio mixer. For audio input applications, the mixer can be used to control input mixing from a set of audio input ports (one or more microphones, line-in, CD player, tape player, or whatever) before it reaches the TargetDataLine—a line from which you can obtain an incoming digitized stream of audio programmatically.

For audio output applications, the mixer is used to control the mixing of a set of SourceDataLines before it is played through a set of output ports (speakers, headphones, line-out, and so on). SourceDataLine is a line from which you can send a digitized stream of audio data. For example, you can read a WAV file and write to a SourceDataLine for playback over a pair of speakers. Alternatively, the input to the mixer can also be sourced from a clip—a special type of data line that you can load a complete stream of audio data into. In practice, a clip is a completely in-memory buffered stream of audio data. Clips are useful for applications requiring repeated playing of different audio segments (background music for games, for instance).

Figure 4 shows the relationships among some of the frequently used interfaces and classes in the JavaSound API. The Line interface unifies all of the classes in Figure 4. This base interface provides access to controls (objects that can be used to vary the audio quality; for instance, gain control for volume manipulations), lets lines be opened/ closed, and handles event-listening registration. The AudioSystem class serves as a class factory in JavaSound, containing a set of static methods (methods that can be called without creating an instance of an AudioSystem) that you can use to create or obtain JavaSound resources from a default configuration. Incidentally, the default configuration for the current implementation of JavaSound (as of JDK 1.4) is to input sound from the local soundcard's microphone, and output sound to speakers attached to the local soundcard. The support for ports and mixer in this implementation of JavaSound is incomplete. However, this default configuration suffices for many audio applications, including this music player.

Audio Data and Storage Formats

Sampled digital audio data—those coming in via TargetDataLine or going out on SourceDataLine—must be in standard audio formats. The AudioFormat class encapsulates the parameters that specify an audio format. These parameters include: encoding used, Pulse Code Modulation (PCM), or MPEG-1 Audio Layer 3 (MP3) or other encoding; the number of channels, sample rate, bits per sample, frame rate and size; and byte ordering.

When audio data is stored on disk, there are several file formats you can use. The JavaSound reference implementation includes support for the WAV (Windows), AIFF (Apple Macintosh), and AU (UNIX) file formats. These formats can be specified using the AudioFileFormat class. Not all audio data formats can be stored in (or played back from) all audio file formats—the specifics are highly platform and OS dependent. In this player, I only consider WAV files that contain PCM mono or stereo data. This is a popular audio data/file-format combination, and often used for CD-quality music data. Compressed audio encoding (such as MP3 and Ogg Vorbis) typically have their own file storage format (.MP3 and .OGG, respectively), and usually are not stored in WAV/AIFF/AU format.

Designing the Music Player

The music player I present here (Figure 5) consists of several interoperating classes; see Table 1. The GUI consists of a Swing JFrame with menu, JList called filenamesList, and set of JButtons. PlayerFrame has a private PlayerBase member; this PlayBase instance is instantiated at the end of the GUIInit() method:

pBase = new PlayerBase();

Each button in the GUI is created in the code similar to Listing One. The addButtonIconText() method is a private method that places the icon graphics above the text label of the button. The icon graphics are standard Java graphics from the Java Look-and-Feel Graphics Repository (http://developer.java.sun.com/developer/ techDocs/hi/repository/). When these buttons are pressed, an xxxxClicked() event-handling function is called. The playClicked() handler gets the selected file from the JList, and calls a helper playFile() method in the PlayerBase instance.

Listing Two shows the operating-system-independent way of concatenating the music directory and the filename to form the path of the file to be played. The stopClicked() and pauseClicked() methods call the corresponding stop() and pause() method in PlayerBase; see Listing Three. The prevClicked() and nextClicked() methods perform multiple steps in their task. First, they stop the player via the stop() method in PlayerBase, then adjust the currently selected item in the JList by advancing or moving backwards the selection. Finally, it calls playClicked() to play the newly selected entry; see Listing Four.

Coding the Player Logic

The PlayerBase class manages the logic of the music player. The code in the play() method (Listing Five), called when the Play button is clicked, shows how it is carried out.

First, the code in play() checks to make sure that the player is stopped, not paused. Then it determines if it is the first time that play() has been called. If it is, it creates a thread, playerThread, which is passed a reference to the PlayerBase instance itself. This is possible because PlayerBase implements the Runnable interface. You need to use a separate thread to handle playing to keep the GUI responsive while the thread reads from the file, decodes, and writes audio data to the speaker.

After starting the thread, the play() method seizes the monitor on the static synch object, sets the stopped flag to False, and notifies the waiting thread (playerThread will be waiting for a notify on the static "synch" monitor before starting to play).

If you now check the run() method in PlayerBase, Listing Six is the newly created thread executing. This thread loops until the threadExit flag becomes True: it starts up, waits to be signaled (which happens when someone clicks the Play button), then plays the music. Table 2 lists the flags that maintain the player's state information.

Playing Music from a WAV File

The playMusic() method is where you use JavaSound to play the selected file. To do this, first use the AudioSystem class to obtain an AudioInputStream (variable ais) based on the file:

ais= AudioSystem.getAudioInputStream(new File(fileToPlay));

Then, get the audio data format of the file using the getFormat() method of the AudioInputStream. Using this format, you try to get a SourceDataLine that supports the format using the getLine() method in Listing Seven. If you are dealing with WAV files, the audio data is now in uncompressed PCM format and you can begin using the line that is obtained to play the audio data read from the AudioInputStream (ais).

Playing Music from A Compressed Audio File

If you are working with compressed audio (MP3 and/or Ogg Vorbis), however, Listing Seven won't work because you need to perform a transformation first—decoding MP3/Ogg Vorbis to PCM. You do this with code like Listing Eight (see the play() method) does this:

1. Creates a custom AudioFormat that is the outcome of the decompression—PCM encoded, but maintains the same sample rate and channel information as the compressed stream.

2. Creates an AudioInputStream that converts the original AudioInputStream into the new AudioFormat.

3. Gets a SourceDataLine that handles the decoded format.

Obtaining a Compatible SourceDataline

Listing Seven includes the getLine() helper method, which gets a SourceDataLine compatible with the AudioFormat that is passed in. Null is returned if no compatible SourceDataLine can be obtained. To get a compatible line, you must fill in a DataLine.info structure and pass it to the AudioSystem class factory via a call to AudioSystem.getLine() method. Listing Nine is my getLine() method. Note how a DataLine.info instance is created.

The rest of the playMusic() method contains a loop that reads from the AudioInputStream and writes to the SourceDataLine.

Extending JavaSound with SPI

To compile the source, you can use the compile.bat file (easily modifiable for UNIX use). From the code directory of the distribution, you can also use the command line javac com/ddj/ddjplayer/*.java. After compiling, run the player using java com.ddj.ddjplayer.DdjPlayer. The player works, but only plays the sample WAV file because the JDK JavaSound implementation only supports WAV, AIFF, and AU. You can use the JavaSound SPI to add MP3 and Ogg Vorbis support to the player, but it needs the JAR files plug-in from open-source projects to do this.

To get these files, download the 100-percent Java Vorbis decoder from JavaCraft (http://www.jcraft.com/jorbis/). The most recent available version at the time of writing is 0.0.12. Next, there is an SPI wrapper for the JOrbis decoder; this is the glue that makes the decoder work with JavaSound transparently. You can find binaries for this project at JavaZoom (http://www.javazoom.net/vorbisspi/vorbisspi.html). The most recent available version of VorbisSPI at the time of writing is 0.6. Finally, you also need to provide MP3 support. JavaZoom also has a JavaSound-compatible, 100-percent Java decoder called JavaLayer (http://www.javazoom.net/javalayer/javalayer.html). The latest available version of JavaLayer at the time of writing is 0.2.0. Be sure that you download the classic version of JavaLayer (not the J2ME version).

After you've extracted all the binaries from the downloaded OSS projects, place all the JAR files in the code directory of the source distribution for this article. Start the player using the command line: java -classpath .;.\jogg-0.0.5.jar;.\jorbis-0.0.12.jar;.\jl020.jar;.\mp3sp.jar;.\vorbisspi0.6.jar com.ddj.ddjplayer.DdjPlayer. You may need to change the version numbers if you are downloading later versions of the libraries and plug-ins. By including the SPI extensions in the player's classpath, the JavaSound run time automatically locates and uses them. The source code for the player (available electronically) includes three audio files for test: a WAV file, MP3 file, and OGG file (under the music directory).

Conclusion

The JavaSound API makes it easy to incorporate audio features into any application. Likewise, the Service Provider Interface (SPI) lets you extend and enhance JavaSound's audio format-handling capability without requiring application code changes.

DDJ

Listing One

JButton pauseButton;
 ...
addButtonIconText(pauseButton, "Pause", "Pause24.gif");
     playButton.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(ActionEvent e) {
        playClicked(e);
      }
    });

Back to Article

Listing Two

void playClicked(ActionEvent e) {
   String fileToPlay = (String) filenamesList.getSelectedValue();
   if (fileToPlay != null) {
        pBase.playFile(searchDir + 
            System.getProperty("file.separator") + fileToPlay);
   }
  }

Back to Article

Listing Three

void stopClicked(ActionEvent e) {
   pBase.stop();
  }
void pauseClicked(ActionEvent e) {
   pBase.pause();
  }

Back to Article

Listing Four

  void prevClicked(ActionEvent e) {
    pBase.stop();
    filenamesList.setSelectedIndex(   filenamesList.getSelectedIndex() - 1);
    playClicked(e);
  }
  void nextClicked(ActionEvent e) {
    pBase.stop();
    filenamesList.setSelectedIndex((filenamesList.getSelectedIndex()+1) 
                                                     % curPlayListLength);
   playClicked(e);
  }

Back to Article

Listing Five

public void play() {
   if ((!stopped) || (paused)) return;
   if (playerThread == null)  {
     playerThread = new Thread(this);
     playerThread.start();
     try { Thread.sleep(500); 
               } catch (Exception ex) {}
     }
    synchronized(synch) {
              stopped = false;
              synch.notifyAll();
      }
  }

Back to Article

Listing Six

public void run() {
    while (! threadExit)  {
       waitforSignal();
       if (! stopped)
           playMusic();
       }
  }

Back to Article

Listing Seven

if (ais != null)  {
  baseFormat = ais.getFormat();
line = getLine(baseFormat);
  ...
  }

Back to Article

Listing Eight

AudioFormat  decodedFormat = new AudioFormat(
    AudioFormat.Encoding.PCM_SIGNED,
    baseFormat.getSampleRate(),
    16,
    baseFormat.getChannels(),
    baseFormat.getChannels() * 2,
    baseFormat.getSampleRate(),
    false);
ais = AudioSystem.getAudioInputStream(decodedFormat, ais);
line = getLine(decodedFormat);

Back to Article

Listing Nine

private SourceDataLine getLine(AudioFormat audioFormat)  {
        SourceDataLine res = null;
            DataLine.Info info = new DataLine.Info(SourceDataLine.class,
                                                           audioFormat);
    try  {
       res = (SourceDataLine) AudioSystem.getLine(info);
       res.open(audioFormat);
      }
    catch (Exception e) {
     }
      return res;
  }

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.