Reading & writing simfiles

Opening simfiles

The top-level simfile module offers 3 convenience functions for loading simfiles from the filesystem, depending on what kind of filename you have:

>>> import simfile
>>> springtime1 = simfile.open('testdata/Springtime/Springtime.ssc')
>>> springtime2, filename = simfile.opendir('testdata/Springtime')
>>> for sim, filename in simfile.openpack('testdata'):
...     if sim.title == 'Springtime':
...         springtime3 = sim
...
>>> print springtime1 == springtime2 and springtime2 == springtime3
True

Plus two more that don’t take filenames:

Note

If you’re about to write this:

with open('path/to/simfile.sm', 'r') as infile:
    sim = simfile.load(infile)

Consider writing sim = simfile.open('path/to/simfile.sm') instead. This lets the library determine the correct encoding, rather than defaulting to your system’s preferred encoding. It’s also shorter and easier to remember.

The type returned by functions like open() and load() is declared as Simfile. This is a union of the two concrete simfile types, SMSimfile and SSCSimfile:

>>> import simfile
>>> springtime = simfile.open('testdata/Springtime/Springtime.ssc')
>>> type(springtime)
<class 'simfile.ssc.SSCSimfile'>
>>> nekonabe = simfile.open('testdata/nekonabe/nekonabe.sm')
>>> type(nekonabe)
<class 'simfile.sm.SMSimfile'>

The “magic” that determines which type to use is documented under simfile.load(). If you’d rather use the underlying types directly, instantiate them with either a file or string argument:

>>> from simfile.ssc import SSCSimfile
>>> with open('testdata/Springtime/Springtime.ssc', 'r') as infile:
...     springtime = SSCSimfile(file=infile)

Note

These Simfile types don’t know about the filesystem; you can’t pass them a filename directly, nor do they offer a .save() method (see Writing simfiles to disk for alternatives). Decoupling this knowledge from the simfile itself enables them to live in-memory, without a corresponding file and without introducing state-specific functionality to the core simfile classes.

Accessing simfile properties

Earlier we used the title attribute to get a simfile’s title. Many other properties are exposed as attributes as well:

>>> import simfile
>>> springtime = simfile.open('testdata/Springtime/Springtime.ssc')
>>> springtime.music
'Kommisar - Springtime.mp3'
>>> springtime.samplestart
'105.760'
>>> springtime.labels
'0=Song Start'

Refer to Known properties for the full list of attributes for each simfile format. Many properties are shared between the SM and SSC formats, so you can use them without checking what kind of Simfile or Chart you have.

All properties return a string value, or None if the property is missing. The possibility of None can be annoying in type-checked code, so you may want to write expressions like sf.title or "" to guarantee a string.

Attributes are great, but they can’t cover every property found in every simfile in existence. When you need to deal with unknown properties, you can use any simfile or chart as a dictionary of uppercase property names (they all extend OrderedDict under the hood):

>>> import simfile
>>> springtime = simfile.open('testdata/Springtime/Springtime.ssc')
>>> springtime['ARTIST']
'Kommisar'
>>> springtime['ARTIST'] is springtime.artist
True
>>> for property, value in springtime.items():
...     if property == 'TITLETRANSLIT': break
...     print(property, '=', repr(value))
...
VERSION = '0.83'
TITLE = 'Springtime'
SUBTITLE = ''
ARTIST = 'Kommisar'

Note

One consequence of the backing OrderedDict is that duplicate properties are not preserved. This is a rare occurrence among existing simfiles, usually indicative of manual editing, and it doesn’t appear to have any practical use case. However, if the loss of this information is a concern, consider using msdparser to stream the key-value pairs directly.

Accessing charts

Charts are different from regular properties, because a simfile can have zero to many charts. The charts are stored in a list under the charts attribute:

>>> import simfile
>>> springtime = simfile.open('testdata/Springtime/Springtime.ssc')
>>> len(springtime.charts)
9
>>> springtime.charts[0]
<SSCChart: dance-single Challenge 12>

To find a particular chart, use a for-loop or Python’s built-in filter function:

>>> import simfile
>>> springtime = simfile.open('testdata/Springtime/Springtime.ssc')
>>> list(filter(
...     lambda chart: chart.stepstype == 'pump-single' and int(chart.meter) > 20,
...     springtime.charts,
... ))
...
[<SSCChart: pump-single Challenge 21>]

Much like simfiles, charts have their own “known properties” like meter and stepstype which can be fetched via attributes, as well as a backing OrderedDict which maps uppercase keys like 'METER' and 'STEPSTYPE' to the same string values.

Warning

Even the meter property is a string! Some simfiles in the wild have a non-numeric meter due to manual editing; it’s up to client code to determine how to deal with this.

If you need to compare meters numerically, you can use int(chart.meter), or int(chart.meter or '1') to sate type-checkers like mypy.

Editing simfile data

Simfile and chart objects are mutable: you can add, change, and delete properties and charts through the usual Python mechanisms.

Changes to known properties are kept in sync between the attribute and key lookups; the attributes are Python properties that use the key lookup behind the scenes.

>>> import simfile
>>> springtime = simfile.open('testdata/Springtime/Springtime.ssc')
>>> springtime.subtitle = '(edited)'
>>> springtime
<SSCSimfile: Springtime (edited)>
>>> springtime.charts.append(SMChart())
>>> len(springtime.charts)
10
>>> del springtime.displaybpm
>>> 'DISPLAYBPM' in springtime
False

If you want to change more complicated data structures like timing and note data, refer to Timing & note data for an overview of the available classes & functions, rather than operating on the string values directly.

>>> import simfile
>>> from simfile.notes import NoteData
>>> springtime = simfile.open('testdata/Springtime/Springtime.ssc')
>>> first_chart = springtime.charts[0]
>>> notedata = NoteData(first_chart)
>>> # (...modify the note data...)
>>> first_chart.notes = str(notedata)

Note

The keys of an SMChart are static; they can’t be added or removed, but their values can be replaced.

Writing simfiles to disk

There are a few options for saving simfiles to the filesystem. If you want to read simfiles from the disk, modify them, and then save them, you can use the simfile.mutate() context manager:

>>> import simfile
>>> input_filename = 'testdata/Springtime/Springtime.ssc'
>>> with simfile.mutate(
...     input_filename,
...     backup_filename=f'{input_filename}.old',
... ) as springtime:
...     if springtime.subtitle.endswith('(edited)'):
...         raise simfile.CancelMutation
...     springtime.subtitle += '(edited)'

In this example, we specify the optional backup_filename parameter to preserve the simfile’s original contents. Alternatively, we could have specified an output_filename to write the modified simfile somewhere other than the input filename.

simfile.mutate() writes the simfile back to the disk only if it exits without an exception. Any exception that reaches the context manager will propagate up, except for CancelMutation, which cancels the operation without re-throwing.

If this workflow doesn’t suit your use case, you can serialize to a file object using the simfile’s serialize() method:

>>> import simfile
>>> springtime = simfile.open('testdata/Springtime/Springtime.ssc')
>>> springtime.subtitle = '(edited)'
>>> with open('testdata/Springtime (edit).ssc', 'w', encoding='utf-8') as outfile:
...     springtime.serialize(outfile)

Finally, if your destination isn’t a file object, you can serialize the simfile to a string using str(simfile) and proceed from there.

Robust parsing of arbitrary simfiles

The real world is messy, and many simfiles on the Internet are technically malformed despite appearing to function correctly in StepMania. This library aims to be strict by default, both for input and output, but allow more permissive input handling on an opt-in basis.

The functions exposed by the top-level simfile module accept a strict parameter that can be set to False to suppress MSD parser errors:

>>> import simfile
>>> springtime = simfile.open('testdata/Springtime/Springtime.ssc', strict=False)

Warning

Due to the simplicity of the MSD format, there’s only one error condition at the data layer - stray text between parameters - which setting strict to False suppresses. Almost any text file will successfully parse as a “simfile” with this check disabled, so exercise caution when applying this feature to arbitrary files.

While most modern simfiles are encoded in UTF-8, many older simfiles use dated encodings (perhaps resembling Latin-1 or Shift-JIS). This was a pain to handle correctly in older versions, but in version 2.0, all simfile functions that interact with the filesystem detect an appropriate encoding automatically, so there’s typically no need to specify an encoding or handle UnicodeDecodeError exceptions. Read through the documentation of open_with_detected_encoding() for more details.

When grouping notes using the group_notes() function, orphaned head or tail notes will raise an exception by default. Refer to Handling holds, rolls, and jumps for more information on handling orphaned notes gracefully. (This is more common than you might imagine - “Springtime”, which comes bunded with StepMania, has orphaned tail notes in its first chart!)