Skip to content

08 BPM and Beat

In this chapter, we will refactor Note to use BPM and beat data instead of time.

BPM and Beat

Note currently uses time in seconds, but rhythm game charts are commonly written in beats. Each note has a beat number, and its time is calculated from that beat and the BPM (beats per minute) of the background music, which can change over time.

An engine could store BPM changes and perform this conversion itself, but Sonolus provides standard BPM change entities and conversion functions. Let's integrate our engine with them.

BPM Changes

First, we need to tell Sonolus about every BPM change.

Sonolus.py provides the standard BpmChange entity for level data. In guide/level.py, replace the existing sonolus.script.level import so it includes BpmChange:

python
from sonolus.script.level import BpmChange, Level, LevelData

Then insert a BPM change before the Note entity in the entity list:

python
level = Level(
    # ...
    data=LevelData(
        bgm_offset=0,
        entities=[
            # ...
            BpmChange(beat=0, bpm=120),
            # ...
        ],
    ),
)

This entity establishes a BPM of 120 starting at beat 0.

BpmChange comes from sonolus.script.level, so we do not need to implement it ourselves. Standard timing functions can use the BPM changes in the level even when the play mode does not include the archetype.

Refactoring

With the BPM change in place, open guide/play/note.py. Replace the archetype import so it uses StandardImport instead of imported:

python
from sonolus.script.archetype import PlayArchetype, StandardImport

Replace the imported time field with an imported beat field:

python
class Note(PlayArchetype):
    beat: StandardImport.BEAT

    # ...

StandardImport.BEAT declares the field as imported and gives it the standard Sonolus data name for a beat. No separate = imported() is needed. Using the standard name is not required for a custom Note archetype, but it keeps chart data consistent with other timing entities.

In guide/level.py, replace Note(time=2) with:

python
Note(beat=2)

Finally, delete the temporary update_parallel callback and its debug_log import from guide/play/note.py. The time field no longer exists. Reopen the development level and confirm that it still starts without a compilation error.