osu! reference BPM
In this article, we’ll discuss how osu! calculates the reference BPM for any chart.
Experimentations
osu! seems to be looking for the reference bpm using a naive approach, where it assumes timing points are sorted
Consider the following
OFFSET BPM
0 200 <Start of Map>
100 300
200 150
300 600
5000 - <End of Map>
osu! will display 150 - 600 (600) where 600 is the reference BPM.
Let’s try with this instead
OFFSET BPM
0 200 <Start of Map>
100 300
300 600
200 150
5000 - <End of Map>
Here, it is 150 - 300 (150).
And another example
OFFSET BPM
0 200 <Start of Map>
300 600
200 150
100 300
5000 - <End of Map>
Here, it is 200 - 300 (300).
Hypothesis
It is obviously suffering from a naive assumption that all timing points are sorted.
I’m guessing that this is the algorithm used.
Example 1
Consider this example
OFFSET BPM
0 200 <Start of Map>
300 600
200 150
100 300
5000 - <End of Map>
osu! stores these information without sorting, in a stack.
bpms: [0ms, 200bpm]
[300ms, 600bpm]
[200ms, 150bpm]
[100ms, 300bpm]
lastOffset: 5000ms
It then calculates how long a bpm lasts, the differences
diffs = differences(bpms)
assert(diffs == [300ms, 200bpm]
[-100ms, 600bpm]
[-100ms, 150bpm]
[4900ms, 300bpm])
The largest difference will be the reference bpm
refBpm = max(diffs.offsets()).bpm()
assert(refBpm == 300bpm)
I’m not too sure how the range is determined but I’m not too concerened about it.
Example 2
OFFSET BPM
0 200 <Start of Map>
100 300
300 600
200 150
5000 - <End of Map>
bpms = [0ms, 200bpm]
[100ms, 300bpm]
[300ms, 600bpm]
[200ms, 150bpm]
lastOffset = 5000ms
diffs = differences(bpms)
assert(diffs == [100ms, 200bpm]
[200ms, 300bpm]
[-100ms, 600bpm]
[4800ms, 150bpm])
refBpm = max(diffs.offsets()).bpm()
assert(refBpm == 150bpm)
Result: 150 - 300 (150)
Example 3
OFFSET BPM
0 200 <Start of Map>
300 600
200 150
100 300
400 100
5000 - <End of Map>
bpms: [0ms, 200bpm]
[300ms, 600bpm]
[200ms, 150bpm]
[100ms, 300bpm]
[400ms, 100bpm]
lastOffset: 5000ms
diffs = differences(bpms)
assert(diffs == [300ms, 200bpm]
[-100ms, 600bpm]
[-100ms, 150bpm]
[300ms, 300bpm]
[4600ms, 100bpm])
refBpm = max(diffs.offsets()).bpm()
assert(refBpm == 600bpm)
Usages
Let’s say you have a map that you’ve planned on reference bpm 200, but after some time, it changes to 300 or 100. It’s a pain to adjust all SVs to fix it.
What you can do is manipulate osu into using a specific bpm by adding a 0 offset BPM Line.
If you already have BPMs that are negative, it shouldn’t matter, however, they will affect the range displayed.
Take for example this map
...
//Storyboard Layer 4 (Overlay)
//Storyboard Sound Samples
[TimingPoints]
-23,240,4,2,0,50,1,0
6697,240,4,2,0,50,1,0
15817,240,4,2,0,50,1,0
17737,-166.666666666667,4,2,0,50,0,0
18457,240,4,2,0,25,1,0
...
174702,240,4,2,0,50,1,0
202782,240,4,2,0,50,1,0
240222,428.571428571429,4,2,0,50,1,0
281364,480,4,2,0,50,1,0
329364,240,4,2,0,50,1,0
[HitObjects]
448,192,17737,5,0,0:0:0:0:
Its reference is 250, let’s say you want to change it to 600.
You have to add a 0 offset BPM line at the end in the .osu MANUALLY.
...
...
174702,240,4,2,0,50,1,0
202782,240,4,2,0,50,1,0
240222,428.571428571429,4,2,0,50,1,0
281364,480,4,2,0,50,1,0
329364,240,4,2,0,50,1,0
0,100,4,2,0,50,1,0 < ------- HERE
[HitObjects]
448,192,17737,5,0,0:0:0:0:
This adds a 0 Offset, 600 Bpm line at the very start of the map.
Reminder: 60,000 / 600 = 100. That’s how you get the second value in the line. 60,000 is a fixed value. 100 just represents the ms between beats.
This will change the reference bpm to 600 as expected.
Caveats
There are 2 problems:
- If you save the map, it’ll sort itself and it won’t work anymore.
- If you upload the map, it’ll save itself (back to 1.)
The solution to (1.) is to just not save or keep moving it back.
The solution to (2.) is to save the map before clicking the final upload button.