Hot off of debugging! The following code is used to control the variable speed playback of audio files as accomplished by this applet:
http://www.hexara.com/VSL/VSL2.htm
If you don’t have a .wav to use as a test, you can use this pad from Hexara: http://www.hexara.com/Audio/BFPad.wav
But you will need to download it first. Also, an overlap of 20000 works best. To use the applet, load a wav file (drop down menu, top left). Then, hold down the mouse and drag: horizontal = speed of playback, vertical = volume.
In the code below, the “SpeedEvent” object is just a package for a long (System.nanoTime()) and a double (the control value). I am packaging mouse events prior to storing and popping them in a LinkedBlockingQueue. The “RateController” is placed within the core loop of the SourceDataLine read/write, so that it is consulted once per frame. If there are items in the queue, it pops them and calculates the needed “smoothing” changes based upon the timing, as well as the time until the next pop. I’m assuming a frame rate of 44100 fps when I “reconstitute” the mouse event sequence.
[EDIT: There is an improved version on post #7.]
import java.util.concurrent.LinkedBlockingQueue;
public class RateController {
private static final double FRAMES_PER_NANO = 0.0000441;
private final LinkedBlockingQueue<SpeedEvent> speedEvents;
private SpeedEvent curSpEv;
private long elapsedFrames;
private long frameSpEv;
private double speedIncrement;
private double speed;
private long nanoStartTime;
// C O N S T R U C T O R
RateController()
{
speedEvents = new LinkedBlockingQueue<SpeedEvent>();
}
// Methods
public void init()
{
elapsedFrames = 0;
frameSpEv = 0;
nanoStartTime = System.nanoTime();
// System.out.println("rc init, nanoStart:" + nanoStartTime);
}
private long convertNanoToAbsFrame(long nanoSpEv)
{
return (long)((nanoSpEv - nanoStartTime) * FRAMES_PER_NANO);
}
public void addSpeedEvent(SpeedEvent spe)
{
speedEvents.add(spe);
// System.out.println("se added:" + spe.toString());
}
public double tick()
{
/*
* Key service.
*/
elapsedFrames++;
if (elapsedFrames >= frameSpEv)
{
curSpEv = speedEvents.poll();
if (curSpEv == null)
{
// System.out.println("pollnull");
frameSpEv += 256; // arbitrary
speedIncrement = 0; // no changes
}
else
{
// calculate new increment
frameSpEv = convertNanoToAbsFrame(curSpEv.getNanoTime());
// System.out.println("popped: " + curSpEv.toString());
// System.out.println("current framecount:" + elapsedFrames);
// System.out.println("frame of SpEv:" + frameSpEv);
if (frameSpEv < elapsedFrames)
{
speed = curSpEv.getDesiredSpeed();
elapsedFrames = frameSpEv; // readjust starting point
}
else
{
speedIncrement = (curSpEv.getDesiredSpeed() - speed)
/(frameSpEv - elapsedFrames);
// System.out.println("current Speed: " + speed);
// System.out.println("speed Incr:" + speedIncrement);
}
}
}
speed += speedIncrement;
// System.out.println("elapsed fr:" + elapsedFrames);
// System.out.println("sp:" + speed);
return speed;
}
public static void main(String[] args)
{
RateController testRC = new RateController();
testRC.init();
testRC.addSpeedEvent(new SpeedEvent(System.nanoTime(), 1.1));
for (int i = 0; i < 100; i++)
{
System.out.println(testRC.tick());
}
}
}
So, I’ve been figuring out a few things about sound controls. I’ve been posting questions here and there, asking about issues related to the GC and to how the JVM switches threads, and I think I’ve touched bottom, so to speak.
Below is a diagram to show how events occur. The first line illustrates a series of MouseControl events, but could very well be readings from a JSlider. I’m just numbering them as if they occur steadily in time (a gradual slide of the slider).
The second line shows how the JVM switches on and off the processing of the SourceDataLine. Here, I’m wanting to focus on the core loop, where one reads from a data source (I have a TargetDataLine in my example program) and writes to the SDL. Note that this loop runs ahead of playback time.
The third line attempts to show how, in the playback, the Mouse Control values or Slider values will lag.
http://www.hexara.com/VSL/AudioControlTiming.jpg
In this example, in the second SDL processing block of time, only the value of “4” is present and is used while SDL Processing handles a chunk of future sound. When the SDL processing block resumes, the current Mouse Control value is 9 (skipping 5-8).
What strategies are there to make the transitions from one control value to another smoother?
-
One can impose a maximum amount that a control value can be allowed to change. In this scenario, the Mouse Control is a “desired value” or target value, and the actual control value is incrementally moved in that direction. Best case scenario (for smoothness) is if you can do this update once per audio frame. This takes a fair bit of tuning, and is subject to “staircase” effects or slowness of response, depending upon whether you make your maximum delta too large or too small.
-
Spread the desired change out over the course of the current buffer being processed. You should know the current value and the desired value, and the number of frames in the buffer being processed. But this only works if you know that a single buffer’s worth of data is going to be run by the JVM. I tried this approach but kept having problems. When I tested my app, the core SDL read/write loop (one buffer’s worth of data) was usually run twice by the JVM. So the control value would transition in one read/write loop and just sit there in the second, creating the aforementioned “staircase” effect. When I made the buffer size 10 times smaller, instead of consulting the other threads more often, the JVM ran the core read/write loop approximately 20 times, making the staircasing even worse! (My JVM seemed to want to run the loop about 2 to 7 msec, then go away and come back 125 msec later. OS = WinXP. Probably other OS/JVM combos will do this differently.)
-
Spread the desired change out via storing and popping Mouse Control values in a FIFO array of some sort. The programming is not trivial, but at least if you get it right, the control moves smoothly at a known minimum amount of lag.
For controlling volume, I recommend using approach (1), and am doing this in the example program. The biggest concern with volume changes is that they shouldn’t be so large that they cause clicks. However, “staircase” effects are not easy to hear with volume. So, one can use a relatively simple solution.
For controlling the variable playback speed, the staircasing is very audible, so I am using approach (3).
[Edit: Now using approach 3 for both rate and volume. See post #7.]