ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

QTJ Audio

by Chris Adamson
12/17/2003

QuickTime Java can be the heart and soul of cross-platform video players and editors. As you will see in this article, QTJ is also well-suited to be the engine of audio-only applications, such as MP3 players. This article will develop an audio player, QTBebop, that displays song metadata, band levels, and current time, all of which help introduce the useful audio-related tools provided by QuickTime to the Java developer. We'll also look at QuickTime's "callbacks," which are critical to all kinds of QT apps.

Too Good, Too Bad

We tend to think of audio and video applications as separate realms — iTunes and WinAmp are one kind of application, while iMovie and RealPlayer are another — but this separation exists at the application layer, not the media framework layer. QuickTime treats sound the same way it treats video or any other kind of dynamic media. In fact, there's nothing special to opening and playing a sound file in QuickTime: you create an OpenMovieFile to reference a file in a supported format (MP3, AAC, WAV, etc.), hand that to Movie.fromFile(), and call Movie.start(). The qtj61-player code from the last article in this series will play audio files with no code changes. As far as QuickTime is concerned, imported audio files are just movies with a single track of audio media.

Given that, it's quite easy to write a bare-bones, GUI-less audio player. In fact, it seems like it should consist of simply opening a file, making a Movie from that, and starting the Movie. We could express this as the following code with only a single caveat ... it doesn't work:

try {
    //this does not work
    QTSession.open();
    QTFile qtf = new QTFile (args[0]);
    OpenMovieFile omf = OpenMovieFile.asRead(qtf);
    Movie movie = Movie.fromFile (omf);
    movie.start();
} catch (QTException qte) {
    qte.printStackTrace();
}

With Java 1.4 on Mac OS X, this returns immediately, without playing any music. On Windows 2000 and XP, it seems to play a few seconds and hang. Either way, chances are you're not happy with the result. What has happened?

The problem has to do with tasking, the arcane art of giving a QuickTime movie enough cycles to actually play itself. Typically, when we build a QTJ application with a GUI, we pick up the tasking calls automatically, and thus don't have to worry about, or even know about, the need to periodically call Movie.task(). In this case, we haven't picked up any automatic tasking calls, nor set up any of our own.

On Windows, the side effect is that after getting time to play a little bit of its buffer, the Movie is never given another chance to decode and play the audio. The Mac OS X case is a little stranger -- I believe what we're seeing is that our MP3 is handed off to a native library, not actually played by Java, so the main() method returns and the JVM, seeing no non-daemon threads running, decides to shut down.

In any case, we need to provide regular callbacks to the task() method to give our movie a chance to decode and play the data. Fortunately, QTJ provides a class called TaskAllMovies, which runs a thread that provides tasking callbacks to all active movies. So we can solve our problems on Mac and Windows by adding the two highlighted lines below after the Movie object is created:

try {
    //this version works
    QTSession.open();
    QTFile qtf = new QTFile (args[0]);
    OpenMovieFile omf = OpenMovieFile.asRead(qtf);
    Movie movie = Movie.fromFile (omf);
    TaskAllMovies.addMovieAndStart();
    movie.setActive (true);  
    movie.start();
} catch (QTException qte) {
    qte.printStackTrace();
}

Call Me, Call Me

At this point, when the selected audio is finished, the application will just sit around forever. We'd like it to do something a little more sensible, like terminating the app at the end of the song. A kludgy approach would be to spawn a thread to periodically poll the movie and see if the current time has reached the end.

A better approach is to register to be notified when the movie is finished playing, using one of the callbacks that QuickTime provides. We can provide a small piece of code and tell QTJ to call this code at the end of the movie.

In the included sample CloseOnCallbackAudio.java, we simply extend the simple player to register a callback that will be called when the movie (the audio) finishes playing. This registration is done with the callMeWhen() method:

callback = new ShutdownCallBack (movie);
callback.callMeWhen();

The ShutdownCallBack is an inner class that extends QuickTime's ExtremesCallBack. In its constructor, we indicate what Movie we're interested in (specifically, the TimeBase of the movie), and provide flags to indicate on which events we want to be called:

public ShutdownCallBack (Movie m)
    throws QTException {
    super (m.getTimeBase(),
           StdQTConstants.triggerAtStop);
}

The callMeWhen() call does the actual registration of the callback. This may seem a lot like registering a listener in various Java APIs, but there's a big difference: callMeWhen() only registers code for one callback, as opposed to listeners that get called over and over until they're specifically removed. To get that kind of behavior in QTJ, we'd need to issue a new callMeWhen each time the callback is executed.

When the callback is called, its execute() method is called. Here's our simple implementation:

public void execute() {
    System.out.println ("ShutdownCallBack.execute()");
    cancelAndCleanup();
    System.exit(0);
}

Note: The cancelAndCleanup() call is a required call to disassociate our callback from QuickTime when we're done using it. As the name suggests, there are two parts: a "cancel" that cancels any pending callbacks from occurring, and a "cleanup" that cleans up system resources. A separate cancel() method exists to just cancel pending callbacks. This would be useful if we wanted to reschedule or change the conditions under which the code is called back — we would then reschedule with a new call to callMeWhen().

As you might have expected from the fact that we subclassed ExtremesCallBack, there are different classes to extend in order to achieve different behaviors. All are subclasses of QTCallBack, but provide different constructors, since some take more detailed parameters. Each takes a TimeBase, typically fetched from a Movie, and some take a flags argument whose possible values are defined as trigger... constants in the StdQTConstants class.

Class Description
ExtremesCallBack Called when the given TimeBase reaches its start or stop point. You specify the behavior with the flags triggerAtStart or triggerAtStop.
RateCallBack Called when the TimeBase's rate changes. Using the flag triggerRateChange provides a callback on any rate change. Otherwise, you can use constants such as triggerRateLT or triggerRateGT to get called when the rate becomes less than, or greater than (respectively), a supplied value. The full set of possible flags is listed in the documentation for the native CallMeWhen() function.
TimeCallBack Called when a specific time value is reached. The flags determine whether the callback occurs only when the time is moving forward (triggerTimeFwd), backward (triggerTimeBwd), or either (triggerTimeEither).
TimeJumpCallBack This callback occurs when the TimeBase's time value changes by an amount other than would be expected from continuing to play at its current rate. An obvious example would be when the user clicks on the scrubber to "jump" to a different part of the movie. Setting up this callback takes no behavior flags or parameters.

While this is primarily an article about audio, it should be clear that the callbacks have a wide range of uses in many QuickTime applications. For example, a movie-playing GUI may want to enable or disable some of its buttons and menu items, based on whether a movie is currently playing.

Pages: 1, 2

Next Pagearrow