I have written a new class for gathering several audio tracks into a single SourceDataLine. I am trying to write something that is light, fast and simple enough to be used in Java Games. The coding is pure Java.
I wouldn’t call it ready for prime time yet. But I thought it was far enough along to display and get feedback, especially given the considerable talent and experience here at JGO. I’ve successfully played 4 .wavs and 2 fm-generated “RayGuns” = 6 tracks total. I don’t know what its true capacity is yet, in terms of performance, especially given that optimization is not my forte.
The two key elements are the AudioMixer class, and the Abstract MixerTrack interface. The AudioMixer class contains and operates on an array of MixerTrack sub-interfaces. [EDIT: At this point (11/8/11) I have started supporting two sub-interfaces: “ContinuousMixerTrack” and “TriggeredMixerTrack”. Continuous uses start() and stop(), Triggered uses play().]
A MixerTrack interface can be used to implement a wrapper for any audio format, as long as the wrapper creates two float arrays, one for stereo left and one for stereo right. The data values created by the wrapper should range from -32767f to 32767f. I may decide to change this range to -1.0f to 1.0f in the future, if integration with the “outside world” merits.
Here is the MixerTrack interface code. [EDIT: updated 11/8/11]
import java.io.IOException;
abstract public interface MixerTrack {
boolean isRunning();
void read() throws IOException;
float[] audioValsL();
float[] audioValsR();
void setVolume(float f);
float getVolume();
}
The sub-interface for continuously playing tracks follows. I was considering a file-read data source “continuous” because one doesn’t normally retrigger such files. And if you do attempt to do so, it usually involves opening an entirely new AudioInputStream. Also, I didn’t want to encumber such files with the various triggered file methods, and vice versa.
public interface ContinuousMixerTrack extends MixerTrack {
void start();
void stop();
}
The easiest way to explain it is maybe to show an example of its use. Following is a wrapper for a .wav file. [EDIT: updated 11/11/11]
import java.io.IOException;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
public class WavTrack implements ContinuousMixerTrack
{
// use volatile to enforce execution order
private volatile boolean running;
private float[] audioValsL;
private float[] audioValsR;
private int sampleBufferSize;
private final int bytesToRead;
private byte[] buffer;
private String fileName;
private AudioInputStream ais;
private volatile float volume;
@Override
public boolean isRunning() {return running;}
@Override
public float[] audioValsL() {return audioValsL;}
@Override
public float[] audioValsR() {return audioValsR;}
@Override
public float getVolume() {return volume;}
@Override
public void setVolume(float volume)
{
this.volume = volume;
}
// CONSTRUCTOR
public WavTrack(AudioMixer audioMixer,
String fileName)
throws UnsupportedAudioFileException, IOException
{
this.fileName = fileName;
URL url = AudioMixer.class.getResource(fileName);
ais = AudioSystem.getAudioInputStream(url);
sampleBufferSize = audioMixer.getSampleBufferSize();
audioValsL = new float[sampleBufferSize];
audioValsR = new float[sampleBufferSize];
bytesToRead = sampleBufferSize * 4;
buffer = new byte[bytesToRead];
audioMixer.addTrack(this);
// default settings
running = false;
}
@Override
public void start()
{
System.out.println("start wav track: " + fileName);
running = true;
}
@Override
public void stop()
{
System.out.println("stop wav track: " + fileName);
running = false;
}
@Override
public void read() throws IOException
{
if (running)
{
int bytesRead = ais.read(buffer, 0, bytesToRead);
int j = 0;
int completeSamplesRead = bytesRead / 4;
for (int i = 0; i < completeSamplesRead; i++)
{
audioValsR[i] = (float)
(( buffer[j++] & 0xff )
| ( buffer[j++] << 8 ));
audioValsL[i] = (float)
(( buffer[j++] & 0xff )
| ( buffer[j++] << 8 ));
audioValsR[i] *= volume;
audioValsL[i] *= volume;
}
// the rest can/should be filled in with zeros
for (int i = Math.max(0, completeSamplesRead);
i < sampleBufferSize; i++)
{
audioValsR[i] = 0;
audioValsL[i] = 0;
}
}
}
}
I think the Constructor should be straight-forward for anyone who has ever coded the playback of a .wav via the javax.audio.sampled library, using a relative file location to open an AudioInputStream.
The most important function of the wrapper is the Read method. Each Read iteration converts the .wav data to the float values that the AudioMixer expects. Because the AudioMixer is continually asking for and expecting a full buffer’s worth of data, I have the WavTrack fill in the float arrays with 0’s if there is none or not enough sound data returned from the read().
TODO: put in a quick, dynamic ramp-up from 0 to the volume upon start() and from the volume down to 0 on the stop(). This will help ensure there will be no clicks when starting and stopping.
I’ve also written a “RayGun” using this interface. It is a silly, simple FM toy, that has multiple cursors reading data from a WaveTable. I mention this because a MixerTrack doesn’t have to read data from disk. It can generate the data on the fly. Or one can be written to function similarly to a Clip, storing the data in whatever form makes most sense. I’m envisioning a “ClipShooter” with a WaveTable for the internal representation of audio, and have this wrapper support multiple cursors so that multiple triggerings can play the sound concurrently. [Done: 11/3/11, see post #3]
In the next post, I’ll display the AudioMixer itself.