osu!taiko barlines are generated automatically from red timing point times, beat lengths, and time signatures. osu!stable and osu!lazer use the same measure interval, but differ in their initial starting positions, timing point boundaries, floating-point error correction, and integer conversion. Consequently, the same beatmap may show a 1 ms barline offset or a doubled barline in only one client.
barLength = beatLength × meter is added repeatedly to the current time.double is truncated with conv.i4.1e-7 ms of an integer is corrected to that integer and retained as a double.Because the osu!stable source code is not public, the taiko barline-generation path was traced through the disassembled IL of the osu!.exe build under investigation. The core routine is method 060020D7 in that build. The investigation covered the timing point float64 values, repeated measure-length addition, the position of conv.i4, and the condition used to switch to the next red timing point.
Method IDs and obfuscated names are specific to the build under investigation.
The following links are pinned to the commits examined for this investigation.
1e-7 tolerance| Term | Meaning |
|---|---|
redTime | Time of a red line (an uninherited timing point) |
beatLength | Length of one beat stored in the red timing point, in milliseconds |
meter | Numerator of the time signature; normally 4 for 4/4 |
barLength | Length of one measure, calculated as beatLength × meter |
t | Current candidate barline time; internally a double in both clients |
OmitFirstBarLine | Flag that omits the first barline at a red timing point; effects bit 3 (value 8) in an .osu file |
barLength = beatLength × meter
stable retrieves only uninherited timing points and sorts them by time. The first candidate t is found by normalizing the first red timing point time to a positive remainder of the first measure length.
barLength = firstRed.beatLength × firstRed.meter
t = firstRed.time
- trunc(firstRed.time / barLength) × barLength
if (t < 0):
t += barLength
trunc truncates toward zero. For an ordinary positive measure length, the result is 0 <= t < barLength.
redLines = uninherited timing points
limit = end time of the last HitObject + 1
i = 0
t = initial starting position
while (t <= limit):
currentRed = redLines[i]
if not (t <= currentRed.time
and currentRed.OmitFirstBarLine):
drawTime = conv.i4(t)
add a barline at drawTime
barLength =
currentRed.beatLength × currentRed.meter
if (barLength < 0.001):
switch to the next red line
t = time of the next red line
else:
t += barLength
if (a next red line exists
and t >= time of the next red line):
i += 1
t = redLines[i].time
t += barLength.conv.i4 converts the value to an integer immediately before the barline object is created.conv.i4 truncates toward zero. At ordinary positive times, this is equivalent to floor.N - ε, conv.i4 truncates it to N - 1.generationStartTime = min(0, firstHitTime)
barLength =
timingPoint.BeatLength
× timingPoint.TimeSignature.Numerator
if (timingPoint.Time > generationStartTime):
startTime = timingPoint.Time
else:
barCount = ceil(
(generationStartTime - timingPoint.Time)
/ barLength
)
startTime =
timingPoint.Time + barCount × barLength
if (timingPoint.OmitFirstBarLine):
startTime += barLength
OmitFirstBarLine is enabled, the starting position is advanced by one measure.endTime =
a next red line exists
? time of the next red line
: end of the last HitObject + 1 + barLength
for (double t = startTime;
Precision.AlmostBigger(endTime, t);
t += barLength):
roundedTime =
Math.Round(t, MidpointRounding.AwayFromZero)
if (abs(t - roundedTime) <= 1e-7):
t = roundedTime
add a barline at the double value t
1e-7 ms, the value is corrected to that integer.t, accumulated error is reset at that point as well.StartTime remains a double; it is not truncated as it is in stable.Precision.AlmostBigger(endTime, t) means endTime > t - 1e-7. A value where t == endTime is also generated as a barline belonging to the preceding section.121037.99999999991, is corrected to 121038. On the other hand, because the section endpoint is included, both adjacent sections may generate a barline at the same time.| Item | osu!stable | osu!lazer |
|---|---|---|
| Measure length | beatLength × meter | BeatLength × TimeSignature.Numerator |
| Advancement | Repeatedly add the measure length | Repeatedly add the measure length |
| Initial starting position | Normalize the first red timing point to a positive remainder | First aligned position at or after min(0, firstHitTime) |
| Near-integer correction | None | Correct when within 1e-7 ms of an integer |
| Final time value | Truncate toward zero with conv.i4 | Retain as a double |
| Candidate matching the next red line | Switch to the new section before drawing | May generate at both the preceding section’s endpoint and the new section’s start |
| Final generation range | End of the last HitObject + 1 ms | End of the last HitObject + 1 ms + one measure |
| Omit first barline | Suppress with a drawing condition at the red-line start | Advance the starting time by one measure |
Test beatmap: USAO - Glitch in My System (polytone Edit) [Charlotte's Inner Oni]
redTime = 1038
beatLength = 297.029702970297
meter = 4
barLength = 1188.118811881188...
Mathematically, 101 measures after the red timing point at 1038 is 121038 ms, but stable’s repeated-addition result falls just below the integer.
stable barline:
t = 1038
add barLength to t 101 times
rawBarlineTime = 121037.99999999991
conv.i4(rawBarlineTime) = 121037
rendered barline time = 02:01.037
stable’s resnap command uses a separate process and calculates the time directly by multiplying the selected snap index. For this 1/1 snap, the result is:
stable resnap:
snapTime = redTime + 404 × beatLength
snapTime = 121038
conv.i4(snapTime) = 121038
resnap time = 02:01.038
Not in this example. lazer also reaches 121037.99999999991 during calculation, but its distance from the integer is approximately 8.73e-11 ms, which is within the correction tolerance.
lazer barline:
rawBarlineTime = 121037.99999999991
roundedTime = 121038
abs(rawBarlineTime - roundedTime) <= 1e-7
correct t to 121038
StartTime = 121038
N - ε → N - 1 mechanism as stable.Test beatmap: CS4W - Complementary Contrast (miyagishima) [Inner Oni]
preceding red line:
992,400,4,1,0,70,1,8
omit first barline = true
target red line:
144992,400,4,1,0,100,1,1
omit first barline = false
barLength = 400 × 4 = 1600
144992 - 992 = 144000 = 90 measures
Because the preceding red timing point omits its first barline, its section generates at 2592, 4192, ... 143392, 144992.
143392 + 1600 = 144992
144992 >= next red line at 144992
→ switch to the next red line before drawing it
as part of the preceding section
→ generate one barline at 144992 from the new red line
preceding section:
endTime = 144992
t = 144992
Precision.AlmostBigger(144992, 144992) = true
→ one barline from the section beginning at 992
new section:
startTime = 144992
omit first barline = false
→ one barline from the section beginning at 144992
first red line:
-221,419.58041958042,4,1,0,40,1,0
next red line:
1457,419.58041958042,4,1,0,80,1,0
first HitObject:
408 ms
barLength = 419.58041958042 × 4
= 1678.32167832168 ms
When the first red timing point is at a negative time, both clients find the first non-negative time aligned with that timing point’s measure cycle. This page calls that time the initial candidate. In this example, the initial candidate at 1457.32167832168 ms is approximately 0.322 ms after the next red timing point at 1457 ms.
| Client | How the initial candidate is handled | Result in this example |
|---|---|---|
| osu!stable | Draw before checking the boundary, then convert with conv.i4 | Generate 1457.321... → 1457 ms |
| osu!lazer | Use the next red timing point as the section endpoint and compare as double before drawing | Exclude because 1457.321... > 1457 |
initial candidate:
-221 + 1678.32167832168
= 1457.32167832168
1. Draw the initial candidate first
conv.i4(1457.32167832168) = 1457
→ one barline from the red line at -221 ms
2. Add the measure length
t = 3135.64335664336
t >= next red line at 1457
→ switch to the next red line and move t back to 1457
3. Generate one barline at 1457 ms
from the next red line
After drawing the initial candidate, stable adds the measure length and checks for a timing-point switch using the next candidate. When the next red timing point is before the initial candidate, stable first generates the initial candidate and then moves its internal time backward to the next red timing point. This produces the unusual time decrease in generation order.
If OmitFirstBarLine is disabled on the next red timing point, its starting barline is also drawn. In this example, conv.i4 truncates the initial candidate from 1457.321... → 1457 ms, so it overlaps the 1457 ms barline from the next red timing point exactly.
conv.i4(initial candidate) = next red-line time.Moving only the first red timing point from -221 ms to -218 ms does not change the fact that the initial candidate lies after the next red timing point. It does, however, change the exact time of the barline from the preceding section.
first red line:
-218,419.58041958042,4,1,0,40,1,0
initial candidate:
-218 + 1678.32167832168
= 1460.32167832168
from preceding section:
conv.i4(1460.32167832168) = 1460
from next red line:
conv.i4(1457) = 1457
stable’s internal generation order becomes 1460 → 1457 ms, decreasing by 3 ms. The lines appear close together on the playfield, but mathematically they are not two barlines at the same time.
generationStartTime = min(0, 408) = 0
first red-line section:
barCount = ceil((0 - (-221)) / 1678.32167832168)
= 1
startTime = -221 + 1 × 1678.32167832168
= 1457.32167832168
endTime = next red line = 1457
Precision.AlmostBigger(1457, 1457.32167832168)
= false
→ no barline is generated from the preceding section
next red-line section:
startTime = 1457
→ generate one barline at 1457 ms
conv.i4, so a barline can appear 1 ms before the resnap time.double, preventing a 1 ms-early barline caused by the same mechanism.