Song objects#

The Song object is the main data model of jubeatools, it holds all the data jubeatools can make sense of in a chart file : metadata, timing information, and a set of charts.

Reading properties#

Loaders all return a Song object, but how is the information stored inside of it ?

Metadata#

The song metadata is accessible via the Song.metadata attribute

>>> sigsig = load_memo2(Path("sigsig.txt"))
>>> sigsig.metadata.title
'SigSig'
>>> sigsig.metadata.artist
'kors k'

See jubeatools.song.Metadata for a complete list of the existing fields

Charts#

Charts are stored in the Song.charts attribute.

It’s a dict that maps difficulty names (like "ADV" or "EXT") to Chart objects

>>> sigsig.charts
{
    'EXT': Chart(
        level=Decimal('9.1'),
        timing=Timing(
            events=(
                BPMEvent(time=Fraction(0), BPM=Decimal('179'))
            ),
            beat_zero_offset=Decimal('2.32')
        ),
        hakus=None,
        notes=[...]
    ),
}

Timing#

Timing information is split between a common timing object and per-chart timing objects. The common timing object acts as a fallback in case a chart doesn’t have its own timing object.

The common timing object is stored in the common_timing attribute of Song objects, while the chart timing is stored in the timing attribute of Chart objects.

>>> sigsig.chart.timing
Timing(
    events=BPMEvent(time=Fraction(0), BPM=Decimal('179')),
    beat_zero_offset=Decimal('2.32')
)

Constructing Song objects#

If you want to programatically create Song objects directly in python code you have to construct a lot of different sub-objects.

Let’s start with the most basic ones and work our way up the a full Song object.

All of the classes in the following sections are defined in the jubeatools.song module. You should import them from this module.

Beats#

All musical time points and durations in jubeatools are stored as a fractional amount of beats in a BeatsTime object.

BeatsTime is just a renamed copy of the Fraction class from python’s standard library.

beat_zero = BeatsTime(0)
half_a_beat = BeatsTime(1, 2)
beat_three_and_a_quarter = BeatsTime(3) + BeatsTime(1, 4)

jubeatools counts beats from zero, not one. You can think of the beat number like the duration in beats from the start.

Buttons#

jubeatools identifies the controller buttons with 0-based x and y coordinates with this orientation :

    x →
    0 1 2 3
y 0 □ □ □ □
↓ 1 □ □ □ □
  2 □ □ □ □
  3 □ □ □ □

x goes right and y goes down, both counting from 0 to 3.

A button is stored as a NotePosition object

If we label the buttons this way :

 1  2  3  4
 5  6  7  8
 9 10 11 12
13 14 15 16

Button 5 would be stored this way :

button_5 = NotePosition(x=0, y=1)

And button 12 would be stored this way :

button_12 = NotePosition(x=3, y=2)

Regular Notes#

Regular notes are stored as TapNote objects, these are just the combination of a time in beats, stored in a BeatsTime object, and a button, stored in a NotePosition

For instance, the following TapNote object

note = TapNote(
    time=BeatsTime(5, 4),
    position=NotePosition(x=1, y=2)
)

would be represented this way in a #memo2 file

□□□□ |----|
□□□□ |-①--|
□①□□ |----|
□□□□ |----|

NotePosition(x=1, y=2) means the note appears on button 10

BeatsTime(5, 4) means the note happens at beat \(\frac{5}{4}\).

Remember that jubeatools counts beats starting at zero, not one. Since \(\frac{5}{4} = 1 + \frac{1}{4}\), this means that ① happens on the second quarter note of the second beat.

Long notes#

Long notes are stored as LongNote objects.

In addition to storing their starting time and position (just like regular notes), long notes store their duration expressed in beats, as well as the starting position of their tail, represented as a NotePosition object.

For instance, the following long note

long_note = LongNote(
    time=BeatsTime(1, 2),
    position=NotePosition(x=0, y=1),
    duration=BeatsTime(1),
    tail_tip=NotePosition(x=3, y=1)
)

would be written this way in a #memo2 file

□□□□ |--①-|
①――< |--②-|
□□□□ |----|
□□□□ |----|

□□□□
2□□□
□□□□
□□□□

(assuming the file uses #circlefree=1)

BPM Changes#

A BPM Change is represented as a BPMEvent object. It defines the BPM at a given time in beats.

For instance this BPMEvent object

bpm_event = BPMEvent(time=BeatsTime(0), BPM=Decimal(120))

Defines that the BPM at beat 0 is 120

Warning

Use a string, not a float, when storing a non-interger BPM in a Decimal object

>>> Decimal("120.1")
Decimal('120.1')
>>> Decimal(120.1)
Decimal('120.099999999999994315658113919198513031005859375')

Timing#

Timing objects store all the info necessary to convert between beats and seconds. That information boils down to two things :

  • a list of BPM changes

  • an initial offset

The initial offset is the “beat zero” offset, it’s the time in seconds at which beat 0 occurs in the audio file.

Attention

If you are used to the Stepmania notion of an “offset”, this is the opposite value

The beat zero offset is stored in a SecondsTime, which is just a renamed copy of the Decimal class from the standard library

Here’s a simple example of a Timing object

timing = Timing(
    events=[BPMEvent(time=BeatsTime(0), BPM=Decimal("180.5"))],
    beat_zero_offset=SecondsTime("0.25")
)

This object means that the song’s initial beat (beat zero) happens at time 00:00.25 in the audio file, and that the song has a constant BPM of 180.5 throughout

Warning

Be sure to set the first BPM at beat 0, some parts of jubeatools won’t be able to handle a Timing object nicely if its first BPM change isn’t at beat 0

Charts#

Chart objects store a Decimal level along with a list of mixed TapNote and LongNote objects .

Here’s a small example :

basic = Chart(
    level=Decimal("1.0"),
    notes=[
        TapNote(time=BeatsTime(0), position=NotePosition(x=0, y=0)),
        LongNote(
            time=BeatsTime(0),
            position=NotePosition(x=0, y=1),
            duration=BeatsTime(1),
            tail_tip=NotePosition(x=3, y=1)
        ),
    ]
)

Metadata#

The Metadata object stores all the song information that’s not specific to any single chart.

Currently this includes :

  • Song title

  • Artist

  • Path to the audio file

  • Path to the jacket file

  • Song preview segment

  • Path to a separate audio preview file (akin to BMS preview files)

Here’s an example :

metadata = Metadata(
    title="My great song",
    artist="Myself",
    audio=Path("my_great_song.ogg"),
    cover=Path("my_great_song.png"),
    preview=Preview(start=SecondsTime("10.5"), length=("5"))
    preview_file=Path("preview.ogg")
)

Song#

Finally, the Song object combines all the previous elements.

It holds :

  • a Metadata object

  • a dict that maps difficulty names to Chart objects

  • a Timing object that applies to all charts

Here’s an example :

song = Song(
    metadata=Metadata(
        title="My great song",
        artist="Myself",
        audio=Path("my_great_song.ogg"),
        cover=Path("my_great_song.png"),
    ),
    charts={
        "BSC": basic_chart,
        "ADV": advanced_chart,
        "EXT": extreme_chart
    },
    common_timing=timing_for_all_charts
)