0

I'm trying to code a rhythm game in Java. I'm playing an audio file with a javax.sound.sampled.Clip and I want to know the exact position where I am.

Following my researches, a good practice for a rhythm game is to keep track of the time using the position in the music playing instead of the System time (to avoid a shift between the audio and the game).

I already coded a main loop which uses System.nanoTime() to keep a consistent frame rate. But when I try to get the time of the clip, I have the same time returned for several frames. Here is an extract from my code :

while (running) {
    // Get a consistent frame rate
    now = System.nanoTime();
    delta += (now - lastTime) / timePerTick;
    timer += now - lastTime;
    lastTime = now;
    if (delta >= 1) {
        if(!paused) {
            tick((long) Math.floor(delta * timePerTick / 1000000));
        }
        display.render();
        ticks++;
        delta--;
    }
    // Show FPS ...
}

// ...

private void tick(long frameTime) {
    // Doing some stuff ...
    System.out.println(clip.getMicrosecondPosition());
}

I was expecting a consistent output (with increments of approximately 8,333 µs per frame for 120 fps) but I get the same time for several frames.

Console return:

0
0
748
748
748
10748
10748
10748
20748
20748
20748
30748

How can I keep consistently and accurately keep track of the position where I am in the song ? Is there any alternative to the javax Clip ?

Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
Lugrim
  • 11
  • 2
  • Is this an AWT game, or a JavaFX game? JavaFX’s MediaPlayer class has a currentTime property, but mixing AWT and JavaFX may be more trouble than it’s worth. – VGR May 02 '19 at 14:18
  • @VGR I'm afraid it's an AWT game :/ JavaFX didn't seem really appropriate to make a game (maybe I'm mistaken ?) – Lugrim May 02 '19 at 14:40
  • JavaFX is actually a much better choice from a technological standpoint, but that is of little value if a developer isn’t as familiar with it as with AWT. And your choice to use Clip is perfectly reasonable. What type of audio are you playing? .wav files, or something else? – VGR May 02 '19 at 14:51
  • @VGR Yes, I'm playing a .wav. I haven't really any preference between AWT and JavaFX, and since I tried to use a MVC architecture, I think it shouldn't be too hard to change, maybe it's worth a try ? – Lugrim May 02 '19 at 17:11
  • It is indeed worth a try. Animation in particular is a lot easier in JavaFX. – VGR May 02 '19 at 19:28
  • I'm wondering if it would be possible for you to get your timing info independent of the playback. If you know a Clip is X millis long, and was started at time Y, can you use that info to test the success of an action instead of inspecting the Clip? That could end up being more reliable a way to go, depending on what you are doing. – Phil Freihofner May 03 '19 at 22:20
  • @PhilFreihofner Yes, but the whole point here is to have the timing dependent of the playback to compensate for the playback lags, which would create a difference between the audio and the game. – Lugrim May 05 '19 at 09:12
  • Clips are probably not lagging at all once they start playing (or you would hear it via dropouts). Audio playback is one of the most reliable, thanks to blocking queues used. For position in real time from inspecting Clip, issues include the granularity of the frame position (limited by the buffer size), and multi-thread switching by the cpu (which has to include GC). If you can get a reliable start time via LineListener.update(javax.sound.sampled.LineEvent.START) for the Clip, and calculate via time/frame conversions, accuracy should only be limited by start time itself. – Phil Freihofner May 05 '19 at 20:27
  • Which raises question (just checking) do you know that you should never load and open a Clip on the same command as when you start is? I'm mean, you can, but the Clip won't start until the load and open are completed and this introduces lag. Hence, preloading, preopening, and holding the Clip in memory until the moment for the start. This could be compounding your timing issues. As I said, just checking, as a LOT of people overlook this. – Phil Freihofner May 05 '19 at 20:33

2 Answers2

1

There are a number of difficulties to deal with. First off, Java doesn't provide real-time guarantees, so the moment when a portion of the sound data is being processed doesn't necessarily correspond to when it is heard. Secondly, it is easy to get messed up by the OS system clock's lack of granularity, which is what I'm guessing is contributing to your seeing timings appear multiple times. We had a lot of back-and-forth about that at java-gaming.org several years ago. [EDIT: now I'm thinking your check is looping faster than the granularity of the buffer size being used by the Clip.]

The only way I know to deal with getting really precise timing is to abandon the use of Clip altogether, and instead, count frames while outputting on a SourceDataLine. I've had good success with this. It is the very last point prior to output to the native sound rendering code, and thus has the best timing.

That said, it might be possible to implement some sort of pulse (for example, using a util.Timer) at a regular interval (corresponding to quarter notes or sixteenth notes) and have the TimerTask initiate the playback of Clip objects. I haven't tried that myself but it could be sufficient for the rhythm game. For this you would use Clip objects and not SourceDataLine. But make sure that each Clip is fully loaded and ready to go when played. Don't make the common mistake of loading them anew for each playback.

But when I last tried to do something like this, using a common game loop, the result wasn't that hot.

JavaFX is great for game writing! I find the graphics much easier to handle than AWT. I wrote a tutorial for getting started with JavaFX for game programming. It is out of date, as far as setting up with Eclipse, if you are using JavaFX 11. But if you are using Java 8 I think it still covers the basics.

At JGO, people are in a couple different camps. Many like to use Libgdx, a library, and some of us continue to use JavaFX or AWT/Swing. There are another couple libraries also championed by various members.

JavaFX has its own sound output, but I haven't played with it, and don't know if it is any better than a Clip for rhythmic precision. The cues all play great, but it is always going to be hard getting usable timing info from the cues given Java's system of continually switching between threads.

Was just reviewing contents on JGO. You might find this thread interesting, where the OP was trying to make a metronome. http://www.java-gaming.org/topics/java-audio-metronome-timing-and-speed-problems/33591/view.html

[EDIT: P.S., I've written a reliable metronome, and have an event playback system that is reliable enough to string together musical event in seamless rhythm, and Listeners that can follow things like the volume envelopes of the notes being played in real time. It should be doable to do what you want, ultimately.]

Phil Freihofner
  • 7,645
  • 1
  • 20
  • 41
  • Idea on using SourceDataLine and frame-counting: 1) have a variable to hold the current beat that can be consulted by your game, 2) calculate the number of frames that comprise the amount of time for a beat, 3) in the loop which sends data to the SDL, count frames and update the variable. You can make the variable as granular as you like. Since you are counting in a loop that writes to the SDL and SDL employs a blocking stream, it will be throttled most closely to the rate of the heard playback. Consult the variable to determine the current beat of the music. – Phil Freihofner May 04 '19 at 23:10
0

The issue with clip.getMicrosecondPosition() is that it can only tell you the position of audio that has been sent to the native audio system or corresponds to a buffer that is being rendered.

What does that mean?

When playing audio, you typically have a bunch of samples in memory (like in a Clip) that you send to the native audio system to play. Now, sending each sample individually would be super inefficient, which is why you always send samples in bulk. In javax.sound.sampled terms, you are writing them to a SourceDataLine using its write(buf) method. Now when you write the data, it is not sent directly to the speaker, but instead written to a buffer. The native system then takes samples from that buffer and sends them to the speaker. The system may choose again to take samples from that buffer in bulk.

So when you ask the clip for its microsecond position, it may not really give you the position of what has actually been rendered (that is played on the speaker), but what has been written from the in-memory clip to the line's buffer or what has been taken by the system from the line's internal buffer.

So the resolution of clip.getMicrosecondPosition() may depend on buffer sizes.

To influence the used buffer sizes, you might want to try something like this when creating your clip:

int bufferSize = 1024;
AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
    AudioSystem.NOT_SPECIFIED, 16, 2, 4, AudioSystem.NOT_SPECIFIED, true);
DataLine.Info info = new DataLine.Info(Clip.class, format, bufferSize);
Clip clip = (Clip) AudioSystem.getLine(info)

This asks the audio system to use a buffer of just 1024 bytes when playing the clip. Using a format with 16 bits per sample, 2 channels per frame, and a sample rate of 44100 Hz, this would correspond to 256 frames (4 bytes/frame), i.e., 256/44100=0.0058 seconds, a very short buffer.

For a high resolution (low latency audio), a short buffer is preferable. However, when making the buffer too short, you may experience buffer underflows, which means your audio will "hang" or "stutter". This is very annoying to the user. Keep in mind that Java uses garbage collection. So you may simply not be able to continuously write audio samples to the system, when Java is busy throwing out the garbage (lack of real time capabilities).

Hope this helps. Good luck!

Hendrik
  • 5,085
  • 24
  • 56
  • Yes, playing with the buffer size has definite downsides. I also ran into problems with dropouts when using smaller buffers. The newer Java versions are coming up with some garbage collection choices that may alleviate this (Shenandoah? ZGC?) But I don't know, haven't tested it. – Phil Freihofner May 03 '19 at 22:17
  • What are you talking about? https://docs.oracle.com/javase/7/docs/api/javax/sound/sampled/Clip.html#getMicrosecondLength() It just returns the duration of the audio file. – Jin Lim Feb 08 '21 at 02:19
  • We are talking about *getMicrosecondPosition()*, not *getMicrosecondLength()*. – Hendrik Feb 08 '21 at 20:18