Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Music getPlayingOffset() more accurate?  (Read 7001 times)

0 Members and 1 Guest are viewing this topic.

Lauthai

  • Newbie
  • *
  • Posts: 6
    • View Profile
Music getPlayingOffset() more accurate?
« on: February 20, 2020, 11:40:05 pm »
I am working on a rhythm style game and am using sf::Music to load and play my song files.  However, when I try to use the getPlayingOffset().asMilliseconds() function, it only is producing updates to the number every 20 milliseconds. For example:

// Create and open a song file
sf::Music song;
song.openFromFile(gameState.getSongPlaying().getAudioFilePath());

song.play();

// Test by outputting the offset.
while(1) {
    cout << song.getPlayingOffset().asMilliseconds() << endl;
}
 

I would expect that it would output something continouous like 1, 2, 3, 4, ... but instead get 0, 0, ..., 0, 20, 20 and repeat by 20s.

Is there a way to make it so that the offset is more accurately outputting to the millisecond instead of 20? Would that involve modifying a compiling a custom copy of the audio.dll?  Or would you have a better suggestion to get the current millisecond since the start of the song (i.e. would output 1000 if the song has been going for 1 second)?

EDIT: Would it be better to load the song into a sound buffer and then play using sf::Sound instead? Would the reduce the update delay?
« Last Edit: February 20, 2020, 11:43:29 pm by Lauthai »

eXpl0it3r

  • SFML Team
  • Hero Member
  • *****
  • Posts: 11030
    • View Profile
    • development blog
    • Email
Re: Music getPlayingOffset() more accurate?
« Reply #1 on: February 24, 2020, 08:52:02 am »
Probably better to keep control of a buffer, that way you know exactly where in the buffer you are.
Official FAQ: https://www.sfml-dev.org/faq.php
Official Discord Server: https://discord.gg/nr4X7Fh
——————————————————————
Dev Blog: https://duerrenberger.dev/blog/

Stepland

  • Newbie
  • *
  • Posts: 6
    • View Profile
    • Email
Re: Music getPlayingOffset() more accurate?
« Reply #2 on: March 22, 2020, 05:49:31 pm »
I was looking for something like this for my own project so I searched how other rhythm games were handling this, turns out it's a really common problem yet I did not find much regarding SFML on that matter.

My goals for a solution were :
  • Having a linear output, avoiding both the step function shape and jitter
  • Being at most a few ms off regardless of playback time
  • Not hogging the CPU

Here's a lengthy guide through a few methods I came up with.

The first basic (and wrong) idea all the following methods are build upon is starting an sf::Clock when you call sf::Music::play() and just reading elasped time from said sf::Clock, this has two problems :
  • sf::Music::play() is not blocking, since it just instructs another thread that playback has been requested, for this reason and maybe others having to do with the underlying sound lib, there will be an unpredictable delay between the time this function returns and the time playback actually starts
  • I'm not sure why (audio hardware using its own clock ?) but sf::Clock will pretty quickly drift away from the time reported by sf::Music::getPlayingOffset() (as much as a few seconds per minute), and the drift factor itself is pretty unpredictable

Method #1 : The Watchdog Thread
  • Have a "watchdog" thread monitor changes in the value returned by sf::Music::getPlayingOffset()
  • Each time a new value is detected, reset an sf::Clock
  • Report offset as : last detected value + time elapsed on the sf::Clock

This works better because even if we are still using an sf::Clock() to keep time elapsed since the last buffer update, sf::Clock does not drift too much from the audio clock on a timespan as short as a buffer.

Here's my code as a subclass of sf::Music :
(click to show/hide)
(click to show/hide)

Here's my test code :
(click to show/hide)

And the resulting chart :
(click to show/hide)

You can clearly see how the code waits for the first step to correct the raw offset

The downside of this method is the watchdog thread, my implementation uses a 1ms loop, it's still not unreasonably fast but the impact is noticeable when you look at CPU usage. A minimal executable simply playing back an audio file using sf::Music takes about 1.5% of the CPU on my machine, bare playback with Method #1 takes 5%.

Method #2 : overriding onGetData()

Instead of restarting the clock using a busy loop in the watchdog thread, override the buffer callback, onGetData(). Since it should be called on every buffer request from the underlying sound lib, we should get similar results while reducing CPU load right ?

Here's my code :
(click to show/hide)
(click to show/hide)

And here are the results :
(click to show/hide)

You can clearly see it's way too early, my understanding is that audio data is queued/buffered way earlier than when it is actually played. We are wayyy off (still only about 10ms early) and we have weird glitches at the beggining, but at least CPU usage is back to normal.

Method #3 : Taking Lag Into Account

Another idea I had was to make a hybrid of the first two methods :
  • Measure the lag by measuring the time between :
    • the first onGetData() call
    • the first step in the raw offset given by getPlayingOffset()
  • Report offset as previously but substract lag

The first step in the raw offset is monitored by a fast busy loop in a watchdog thread as previously but since we only need to check for the first step to measure lag, the thread only runs for about one second at most.

Here's my code :
(click to show/hide)
(click to show/hide)

And here's the resulting chart :
(click to show/hide)

We are clearly overshooting on our lag correction.

Method #3.5 : Take less lag into account

Interestingly, only substracting half the measured lag gives this curve :
(click to show/hide)

I then tried adjusting the lag correction to get the same results as Method #1 but I couldn't find a non-magic value based solution, while substracting half the lag seemed like a reasonable thing, I'm not going to try and adjust it even more to get the curves looking nice since that would probably just make my code overfitted to my test file , there has to be some hardware-dependant things that will break any result that's been found by twiddling the amount of lag correction.

I'm unsure about the ideal solution, I like Method 3.5 but I think the correction we are looking for is closer to Method #1
« Last Edit: March 22, 2020, 07:32:41 pm by Stepland »

Laurent

  • Administrator
  • Hero Member
  • *****
  • Posts: 32498
    • View Profile
    • SFML's website
    • Email
Re: Music getPlayingOffset() more accurate?
« Reply #3 on: March 22, 2020, 07:21:03 pm »
If I understand correctly, we're tied to the internal precision of OpenAL Soft, which depends on the sample rate and internal buffer size. The sample rate is a property of the loaded sound, so it can be changed easily; the internal buffer size can be changed with a configuration file (see https://github.com/kcat/openal-soft/blob/master/alsoftrc.sample#L74).

I don't like solutions using a separate clock, ie. not relying completely on the played bytes, but I guess there's no other reasonable choice as long as OpenAL Soft doesn't give us more precision.
Laurent Gomila - SFML developer

Stepland

  • Newbie
  • *
  • Posts: 6
    • View Profile
    • Email
Re: Music getPlayingOffset() more accurate?
« Reply #4 on: March 22, 2020, 07:49:36 pm »
If one were able to measure time using the audio hardware clock you could have a simplified Method #1 where you just check for the first step and then measure time from there since there should be no drift ? Or maybe other things I don't know about would come into play, I'm unsure.

Looks like it was in the works at some point for OpenAL ? http://openal.org/pipermail/openal/2017-December/000670.html

Stepland

  • Newbie
  • *
  • Posts: 6
    • View Profile
    • Email
Re: Music getPlayingOffset() more accurate?
« Reply #5 on: February 18, 2022, 01:29:40 am »
Just found out about AL_SEC_OFFSET_LATENCY_SOFT (It's described here) and figured it might help.

There might be a way to use it to make a better Method #2

I tried it but I couldn't quite make sense of the values it was returning. I'm very new to OpenAL so I'm still struggling a bit ...

Stepland

  • Newbie
  • *
  • Posts: 6
    • View Profile
    • Email
Re: Music getPlayingOffset() more accurate?
« Reply #6 on: February 19, 2022, 01:26:44 am »
I am getting very close to a solution :



Subtracting lag from the offset reported by OpenAL gives a very smooth result, only two thing to fix :

  • It moves back to zero every time sf::SoundStream clears unsed buffers
  • it's a bit late compared to the reported offset

I already have ideas to overcome both of these. I'm getting there !

Stepland

  • Newbie
  • *
  • Posts: 6
    • View Profile
    • Email
Re: Music getPlayingOffset() more accurate?
« Reply #7 on: February 19, 2022, 02:36:11 am »
Well, I completely overlooked that I could just do

getPlayingOffset() - latency

This is almost perfect :

(click to show/hide)
(click to show/hide)

Stepland

  • Newbie
  • *
  • Posts: 6
    • View Profile
    • Email
Re: Music getPlayingOffset() more accurate?
« Reply #8 on: February 28, 2022, 10:45:37 pm »
Alright here's what I came up with so far !

(click to show/hide)
(click to show/hide)

I just measure the initial lag once per play call and shift by that amount.

Apparently the reinterpret_cast of a void pointer to a function pointer is something very non-standard that could very possibly break at any given time, but it worked for me ! (gcc (Ubuntu 9.3.0-17ubuntu1~20.04))

If a more knowledgeable person could step in and help us find a better way to handle this that would be awesome.

The resulting value occasionally jumps a little, but overall it's pretty clean :

(click to show/hide)
(click to show/hide)
(click to show/hide)

It even seems to work when changing the playback position or the pitch while the song plays, so I don't think I'll bother fixing this any further. It seems to suit my needs for now, and I hope it'll be enough for you as well
« Last Edit: February 28, 2022, 10:47:47 pm by Stepland »