Timing data
Timing data is the properties of a simfile that determine when notes must be hit. For simple songs, this typically just means two things: an initial BPM and an offset to synchronize the audio file to the steps. However, simfile authors have many more options to control the exact timing, including:
BPM changes
Stops or delays (pausing the scrolling notes)
Fakes (segments where all notes are considered fake & thus unhittable)
Warps (segments that are instantly skipped over, rendering any warped-over notes fake)
The SM format only supports BPM changes, stops, and delays - and only at the simfile level. The newer SSC format introduces fakes and warps; it also supports split timing, where a chart has its own timing data that overrides the simfile timing data.
Working with timing data
Import TimingData
and pass it a Simfile or an AttachedChart
to parse its timing data:
>>> import simfile
>>> from simfile.timing import TimingData
>>>
>>> sim = simfile.open('testdata/Springtime/Springtime.ssc')
>>> timing_data = TimingData(sim)
Note
You can always pass an AttachedChart,
even if it’s an SMChart!
If the chart has no timing data of its own,
TimingData will use the simfile’s timing properties instead.
Read the parsed timing data using attributes with the same names as in the simfile:
>>> timing_data.offset
Decimal('-0.090')
>>> timing_data.bpms
BeatValues([BeatValue(beat=Beat(0), value=Decimal('181.685'))])
>>> timing_data.stops
BeatValues([])
Decimal is the built-in Python Decimal class.
BeatValues is a thin subclass of Python’s list
that contains BeatValue objects.
TimingData is a mutable data structure,
much like Simfile & Chart objects.
However, changes made to TimingData do not automatically propagate
back to the Simfile or Chart they came from.
Use TimingData.write_to() to update the source object:
>>> from decimal import Decimal
>>> timing_data.offset += Decimal('0.009')
>>> timing_data.write_to(sim)
>>> sim.offset
'-0.081'
Converting between beats and time
Import TimingEngine and pass it a TimingData object
to convert between beats and time:
>>> import simfile
>>> from simfile.timing import TimingData
>>> from simfile.timing.engine import TimingEngine
>>>
>>> sim = simfile.open('testdata/Springtime/Springtime.ssc')
>>> timing_data = TimingData(sim)
>>> timing_engine = TimingEngine(timing_data)
>>> timing_engine.time_at(Beat(32))
10.658
>>> timing_engine.beat_at(10.658)
Beat(32)
Because TimingData stores fake regions and warps,
you can also use it to determine whether a regular note on a given beat would be hittable:
>>> timing_engine.hittable(Beat(32))
True
However, this doesn’t account for the fake note type (F in note data).
So, you may prefer to import the time_chart() function
and pass it an AttachedChart.
This function yields TimedNote objects
that have time and hittable fields,
the latter of which is set to False for both region-based and single-note fakes:
>>> import simfile
>>> from simfile.notes.timed import time_chart
>>>
>>> sim = simfile.open('testdata/spin_cycle/spin_cycle.ssc')
>>> for timed_note in time_chart(sim.charts[0]):
... if not timed_note.hittable:
... print(f"First fake note on beat {timed_note.note.beat} / time {timed_note.time}")
... break
...
First fake note on beat 327.000 / time 130.791
>>>
Display BPM
Simfiles have an optional displaybpm attribute
that determines how to display the song’s BPM on the music selection screen.
It can be a single BPM, a :-separated BPM range,
or an asterisk * to hide the BPM from the player.
If there’s no displaybpm set,
StepMania uses the actual bpms to determine what to display instead.
Import the displaybpm() function and
pass it a Simfile or an AttachedChart
to get the displayed BPM as an object:
>>> import simfile
>>> from simfile.timing.displaybpm import displaybpm
>>> springtime = simfile.open('testdata/Springtime/Springtime.ssc')
>>> disp = displaybpm(springtime)
>>> if disp.value:
... print(f"Static value: {disp.value}")
... elif disp.range:
... print(f"Range of values: {disp.range[0]}-{disp.range[1]}")
... else:
... print(f"* (obfuscated BPM)")
...
Static value: 182
The return value will be one of
StaticDisplayBPM, RangeDisplayBPM, or RandomDisplayBPM.
All of these classes implement the same four properties,
so you typically don’t need to check which class you got.
Here are some example inputs & outputs:
Actual BPM |
DISPLAYBPM value |
Class |
min |
max |
value |
range |
|---|---|---|---|---|---|---|
300 |
|
300 |
300 |
300 |
None |
|
12-300 |
|
|
300 |
300 |
300 |
None |
12-300 |
|
12 |
300 |
None |
(12, 300) |
|
12-300 |
|
|
150 |
300 |
None |
(150, 300) |
12-300 |
|
|
None |
None |
None |
None |
If you want to ignore the DISPLAYBPM and get the actual BPM value / range,
pass ignore_specified=True to the displaybpm() function:
disp = displaybpm(sf)
if not disp.max: # This means we got a RandomDisplayBPM
disp = displaybpm(sf, ignore_specified=True)
Warning
If you’re familiar with how M-Mods work,
it may seem intuitive that you could calculate the scroll rate
based on the max displayed BPM.
However, be warned that StepMania ignores BPMs above some threshold,
and that threshold is set by the active StepMania theme
(and so is effectively arbitrary).
A maximum BPM above 500 or so
should be considered untrustworthy for M-Mod calculations
due to this quirk of StepMania.