open-manifold / Open-Manifold

A free and open-source clone of Rhythm 'n' Face for PC.
Other
10 stars 2 forks source link

Preload music file before playing (a.k.a fix the "Music Load Stutter") #3

Open SuperFromND opened 1 year ago

SuperFromND commented 1 year ago

Currently, Open Manifold runs under the assumption that a music file will always be able to playback with 100% consistency, never stuttering or pausing to load more streamed audio data at all. However, this simply isn't true in practice, and if your computer has even a slight hitch while loading in new music data mid-gameplay, the game's internal metronome (which is used to control beat checks and music progression) will NOT pause or take it into account, which results in desynced music; a deal-breaker for a rhythm game.

There is a very janky, but somewhat effective workaround to this problem: the loop() function includes a bit of code that essentially "snaps" the music playhead back to where it's expected to be at the start of every shape:

// triggers at the start of every shape (a.k.a, the start of every CPU/player phase)
float timer = song_beat_position * ((60.f/bpm * 2.f) / time_signature_bottom);
Mix_SetMusicPosition(timer);

However, this is merely a bandaid to cover a much bigger problem, and one that really should be addressed.

Preloading the entire music file before starting the level would be able to fix this issue, at the cost of eating up a bit of RAM and longer level-load times. Unfortunately, SDL2 Mixer doesn't appear to support preloading an entire music file beforehand by its very design:

SDL_mixer has two separate data structures for audio data. One it calls a "chunk," which is meant to be a file completely decoded into memory up front, and the other it calls "music" which is a file intended to be decoded on demand. ... Chunks might take more memory, but once they are loaded won't need to decode again, whereas music always needs to be decoded on the fly.

SuperFromND commented 1 year ago

For future reference (my own included): the current implementation of Open Manifold's time synchronization code is very loosely based on this old comment by (former) Redditor /u/jusksmit. It's one of the earliest (and least elegant) parts of the codebase, and definitely the one in most need of a refactor. It was also written before SDL_Mixer implemented Mix_GetMusicPosition, so much of it is likely obsolete/redundant.

SuperFromND commented 1 year ago

Okay, never mind on the obsolete part. Turns out Mix_GetMusicPosition works on seconds, not milliseconds (what we need).

To elaborate: Currently, what we do is set a value at the start of a beat in milliseconds, and then a second value which is the first value + the length of a beat, and then we just keep checking the time until it's equal or more than that second value, in which we then increment both. It's rough and it does work, but definitely not ideal.

The problem is, SDL_Mixer has no way of getting the current play time, so instead we set the first value using SDL_GetTicks() and then use the BPM value to calculate the rest from there. Obviously, not ideal.

One idea I had is to use the BPM value to calculate exact beat times instead of approximating them with our current implementation. I don't know if that would help much though.

Another idea is to use Mix_HookMusic or Mix_SetPostMix to track the playhead ourselves, but I'm not entirely sure how to do that either, and it's questionable if this would even be more accurate or precise than SDL's GetTicks function.

SalsaGal commented 1 year ago

Why not just use a chunk instead of music for this? While it's technically wrong, it'd work better since it loads everything up front.

SuperFromND commented 1 year ago

Unfortunately, chunks don't support Mix_SetMusicPosition();, which is how Open Manifold handles playback rewinding in the case of missing shapes.