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.
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
Metadataobjecta
dictthat maps difficulty names toChartobjectsa
Timingobject 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
)