AudioCue is a Java audio-playback class, modeled on javax.sound.sampled.Clip, enhanced with concurrent playback capabilities and with dynamic handling of volume, pan and frequency. Included in the library is an audio mixing tool for merging the output of multiple AudioCues into a single line out.
The project is now published and available on Maven.org. Starting from an earlier project, unit tests were added, and a more rigorous API documentation written. Over the course of making those changes, some errors and shortcomings were uncovered and corrected. The changes were extensive enough that this project was given a starting release version of 2.0.0. The previous version is no longer being maintained. The current Maven version (as of June, 2023) is 2.1.0.
The best way to make use of AudioCue is with the Maven build tool, listing AudioCue as a dependency. To use AudioCue as a Maven dependency, add the following to your project's POM file.
<dependencies>
<dependency>
<groupId>com.adonax</groupId>
<artifactId>audiocue</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>
When used in this way, source code and Javadocs documentation will automatically be linked.
Another option is to fork this project and clone it to your local development environment. From there, executing Maven's install command will put the library into your local Maven repository. Alternatively, you can directly make use of the jar file that is created in the /target subdirectory via the package command by adding this jar file to your project's classpath. Lastly, since there are only a few files, you can simply copy these files directly into your project. Just be sure, if you do, to edit the package lines of the files to appropriately reflect their new file locations.
The library jar, as well as source and documentation jars are publicly available at the Maven site for downloading if you do not wish to use Maven as the build tool.
Code fragments:
// Recommended: define as an instance variable.
AudioCue myAudioCue;
//////
// Recommended: preload one time only.
// This example assumes "myAudio.wav" is located in src/main/resources folder
// of a Maven project.
URL url = this.getClass().getResource("/myAudio.wav");
// The following allows up to 4 concurrent playbacks.
AudioCue myAudioCue = AudioCue.makeStereoCue(url, 4);
myAudioCue.open();
//////
// Recommended: the play() method should be called on a preloaded
// instance variable. Reloading or reopening prior for successive plays
// will add processing latency to no practical purpose.
myAudioCue.play();
// See API for parameters for vol, pan, speed and number of repetitions.
// NOTE: The play method returns immediately. Playback occurs on a
// daemon thread, and will not hold a program open.
//////
// To release resources when sound is no longer needed
myAudioCue.close();
The following example plays an AudioCue via a Swing Button.
public class AudioCuePlayExample {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable(){
public void run()
{
DemoFrame frame = new DemoFrame();
frame.pack;
frame.setVisible(true);
}
});
}
}
class DemoFrame extends JFrame {
private AudioCue bell;
public DemoFrame() {
JPanel panel = new JPanel();
JButton button = new JButton("Play sound");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
bell.play();
}
});
panel.add(button);
add(panel);
// Set up the AudioCue
URL url = this.getClass().getResource("/bell.wav");
try {
bell = AudioCue.makeStereoCue(url, 6);
bell.open();
} catch (UnsupportedAudioFileException | IOException | LineUnavailableException e) {
e.printStackTrace();
}
}
}
To dynamically change the playback of a sound, we need to identify
the playback instance. Each playback instance has its own identifier,
an int
ranging from 0 to the number of preallocated concurrent instances minus one.
Identifiers are obtained from a pool of available instances. An instance is reserved
and its identifier is returned when the play method is executed:
int instanceID = myAudioCue.play();
You can also reserve an instance and obtain its identifier from the pool of available instances as follows:
int instanceID = myAudioCue.obtainInstance();
An instance can be started, stopped, or released by including the instance identifier as an argument:
myAudioCue.start(instanceID);
myAudioCue.stop(instanceID);
myAudioCue.releaseInstance(instanceID);
Releasing returns the instance identifier to the pool of available instances.
An important distinction exists between instances obtained from one of the
play
methods versus the obtainInstance()
method.
The default value of the property recycleWhenDone
for an instance
obtained from play()
is true
. Thus, when playback completes, the instance
and its identifier will automatically be returned to the pool of available instances.
An instance arising from obtainInstance()
has the property recycleWhenDone
set to false
. In this case, when playback completes, the instance identifier
is not returned to the pool of available instances, and the instance remains
receptive to additional commands. In this case, after playing through to the
end, the frame position will be that of the end of the audio-data file, and,
like a Clip
that has played through, will require repositioning and restarting
for further plays.
Properties that can be altered for an instance include the following:
//*volume*:
myAudioCue.setVolume(instanceID, value); // double ranging from 0 (silent)
// to 1 (full volume)
//*panning*:
myAudioCue.setPan(instanceID, value); // double ranging from -1 (full left)
// to 1 (full right)
//*speed of playback*:
myAudioCue.setSpeed(instanceID, value); // value is a factor that is
// multiplied against the normal playback rate, e.g.,
// 2 will double playback speed, 0.5 will halve it
//*position* (cannot be changed while instance is playing):
myAudioCue.setFramePosition(instanceID, value); // position in frames
myAudioCue.setMillisecondPosition(instanceID, value); // position in milliseconds
myAudioCue.setsetFractionalPosition(instanceID, value);
// value is a decimal fraction of the cue length
// where 0 = start, 1 = end
//*other*:
myAudioCue.setLooping(handle, value); // number of additional times the cue will replay
// -1 = infinite looping
myAudioCue.setRecycleWhenDone(handle, value); // boolean that determines whether or
// not the instance will be returned to the
// pool of available instances when the cue
// plays to completion
Output configuration occurs within the AudioCue's open
methods. The
default configuration employs javax.sound.sampled.AudioSystem's default
Mixer
and a SourceDataLine
set with a 1024-frame buffer and the highest
thread priority. A high thread priority should not affect performance of
the rest of an application, as audio threads normally spend the vast majority
of their time in a blocked state. The buffer size can be set to
optimize the balance between latency and dropouts. Incidents of dropouts
are usually lessened by increasing the buffer size.
You can override the defaults by using an alternate form of the open()
method. For
example:
myAudioCue.open(mixer, bufferFrames, threadPriority);
Each AudioCue
can have its own configuration, and will output on its own SourceDataLine
line.
Most operating systems will support multiple, concurrently playing SourceDataLine
streams. But if desired, the streams can be merged into a single output line using
audiocue.AudioMixer
. This is accomplished by
providing an AudioMixer
instance as an argument to the AudioCue
open() method:
myAudioCue.open(myAudioMixer);
Internally, this method adds the AudioCue
to the AudioMixer
using the interface
AudioMixerTrack. If an AudioCue
is a track on an AudioMixer
, the AudioCue
method close() will automatically remove the track from the AudioMixer
.
myAudioCue.close();
The AudioMixer
is configured, upon instantiation, with a default
javax.sound.sampled.Mixer
, with a buffer size of 8192 frames, and
with the highest thread priority. Alternate values can be provided
at instantiation.
All AudioCues
routed through an AudioMixer
will use the
AudioMixer's
configuration properties. Requests to add or remove AudioCues
from the AudioMixer
are concurrency safe but with a latency equal to the time
that elapses until the buffer iterates.
In the following somewhat artificial example, we create and start an
AudioMixer
, add an AudioCue
track, play the cue, then shut it all down.
AudioMixer audioMixer = new AudioMixer();
audioMixer.start();
// At this point, AudioMixer will create and start a runnable and will
// actively output 'silence' (zero values) on its SourceDataLine.
URL url = this.getClass().getResource("myAudio.wav");
AudioCue myAudioCue = AudioCue.makeStereoCue(url, 1);
myAudioCue.open(mixer);
// The open method automatically adds the AudioCue to
// the AudioMixer.
myAudioCue.play();
Thread.sleep(2000); // For purposes of the demo only, to hold the program open to give the AudioCue time
// to play to completion (assumes cue is shorter than 2 seconds).
myAudioCue.close(); // will remove AudioCue from the mix
audioMixer.stop(); // AudioMixer will stop outputting and will
// close the runnable in an 'orderly' manner.
Reminder, this is an artificial example: best practice is to initialize and
open an AudioCue
only once, and to then reuse the AudioCue
for multiple playbacks.
Also, this example uses Thread.sleep()
to prevent the program from advancing and
closing before the cue finishes playing. The reason for this is that AudioMixer
, like
AudioCue
, launches a daemon thread internally for playback, so a playing instance will
not prevent a program from closing once all the program instructions are completed. Unlike this
example, an AudioCue
is more typically called by a GUI that remains open, or from a long-running
program, and is able to complete playing without the program closing. In this more common
use case, pausing the thread that has the play() method is quite unnecessary.
Additional examples and test files for AudioCue are available in the project audiocue-tutorial-examples. I have a series of tutorial videos in the works which will feature the programs in this project. As they are completed (assuming I'm able to do this) links will be posted here.
There is another accompanying project audiocue-demo. However, this code is not entirely compatible with version 2.0.0 and beyond, and in some cases will require tinkering in order to work. This project is now obsolete, and is not being maintained.
Functionality illustrated in audiocue-tutorial-examples includes the following:
- a working basic example of fire-and-forget play;
- the real time response of an
AudioCue
toJSlider
-driven changes to volume, panning and pitch; - the use of scaling functions for enhanced control of volume and panning;
- the utilization of
AudioCueListener
, with an example where an animation reacts to the start and the completion of a playback; - the mixing of multiple
Audiocues
in anAudioMixer
, creating an aleatory soundscape; - the loading of an
AudioCue
directly with PCM data and other PCM-related utilities; - the playback of a sound cue from a file rather than an
AudioCue
in theAudioMixer
by implementing theAudioMixerTrack
interface.
Please check the Issues area for entering or working on suggestions.
If AudioCue has been helpful, it would be great to hear (by email or a "buy me a coffee/beer" donation).
Programmer/Sound-Designer/Composer: Phil Freihofner
URL: http://adonax.com
Email: phil AT adonax.com
If using StackOverflow for a question, chances are highest that I will see it if you include the tag javasound.
I'm happy to list links for any applications that use AudioCue.
Suggestions for the improvement of this README or any other aspect of the AudioCue
are
appreciated.