Scream Tracker 3 module is a tracker music file format that, as all tracker music, bundles both sound samples and instructions on which notes to play. It originates from a Scream Tracker 3 music editor (1994) by Future Crew, derived from original Scream Tracker 2 (.stm) module format.
Instrument descriptions in S3M format allow to use either digital samples or setup and control AdLib (OPL2) synth.
Music is organized in so called patterns
. "Pattern" is a generally
a 64-row long table, which instructs which notes to play on which
time measure. "Patterns" are played one-by-one in a sequence
determined by orders
, which is essentially an array of pattern IDs
This page hosts a formal specification of Scream Tracker 3 module using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.
All parsing code for Python generated by Kaitai Struct depends on the Python runtime library. You have to install it before you can parse data.
The Python runtime library can be installed from PyPI:
python3 -m pip install kaitaistruct
Parse a local file and get structure in memory:
data = S3m.from_file("path/to/local/file.s3m")
Or parse structure from a bytes:
from kaitaistruct import KaitaiStream, BytesIO
raw = b"\x00\x01\x02..."
data = S3m(KaitaiStream(BytesIO(raw)))
After that, one can get various attributes from the structure by invoking getter methods like:
data.num_orders # => Number of orders in a song
# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild
# type: ignore
import kaitaistruct
from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO
from enum import IntEnum
if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 11):
raise Exception("Incompatible Kaitai Struct Python API: 0.11 or later is required, but you have %s" % (kaitaistruct.__version__))
class S3m(KaitaiStruct):
"""Scream Tracker 3 module is a tracker music file format that, as all
tracker music, bundles both sound samples and instructions on which
notes to play. It originates from a Scream Tracker 3 music editor
(1994) by Future Crew, derived from original Scream Tracker 2 (.stm)
module format.
Instrument descriptions in S3M format allow to use either digital
samples or setup and control AdLib (OPL2) synth.
Music is organized in so called `patterns`. "Pattern" is a generally
a 64-row long table, which instructs which notes to play on which
time measure. "Patterns" are played one-by-one in a sequence
determined by `orders`, which is essentially an array of pattern IDs
- this way it's possible to reuse certain patterns more than once
for repetitive musical phrases.
.. seealso::
Source - http://hackipedia.org/browse.cgi/File%20formats/Music%20tracker/S3M%2c%20ScreamTracker%203/Scream%20Tracker%203.20%20by%20Future%20Crew.txt
"""
def __init__(self, _io, _parent=None, _root=None):
super(S3m, self).__init__(_io)
self._parent = _parent
self._root = _root or self
self._read()
def _read(self):
self.song_name = KaitaiStream.bytes_terminate(self._io.read_bytes(28), 0, False)
self.magic1 = self._io.read_bytes(1)
if not self.magic1 == b"\x1A":
raise kaitaistruct.ValidationNotEqualError(b"\x1A", self.magic1, self._io, u"/seq/1")
self.file_type = self._io.read_u1()
self.reserved1 = self._io.read_bytes(2)
self.num_orders = self._io.read_u2le()
self.num_instruments = self._io.read_u2le()
self.num_patterns = self._io.read_u2le()
self.flags = self._io.read_u2le()
self.version = self._io.read_u2le()
self.samples_format = self._io.read_u2le()
self.magic2 = self._io.read_bytes(4)
if not self.magic2 == b"\x53\x43\x52\x4D":
raise kaitaistruct.ValidationNotEqualError(b"\x53\x43\x52\x4D", self.magic2, self._io, u"/seq/10")
self.global_volume = self._io.read_u1()
self.initial_speed = self._io.read_u1()
self.initial_tempo = self._io.read_u1()
self.is_stereo = self._io.read_bits_int_be(1) != 0
self.master_volume = self._io.read_bits_int_be(7)
self.ultra_click_removal = self._io.read_u1()
self.has_custom_pan = self._io.read_u1()
self.reserved2 = self._io.read_bytes(8)
self.ofs_special = self._io.read_u2le()
self.channels = []
for i in range(32):
self.channels.append(S3m.Channel(self._io, self, self._root))
self.orders = self._io.read_bytes(self.num_orders)
self.instruments = []
for i in range(self.num_instruments):
self.instruments.append(S3m.InstrumentPtr(self._io, self, self._root))
self.patterns = []
for i in range(self.num_patterns):
self.patterns.append(S3m.PatternPtr(self._io, self, self._root))
if self.has_custom_pan == 252:
pass
self.channel_pans = []
for i in range(32):
self.channel_pans.append(S3m.ChannelPan(self._io, self, self._root))
def _fetch_instances(self):
pass
for i in range(len(self.channels)):
pass
self.channels[i]._fetch_instances()
for i in range(len(self.instruments)):
pass
self.instruments[i]._fetch_instances()
for i in range(len(self.patterns)):
pass
self.patterns[i]._fetch_instances()
if self.has_custom_pan == 252:
pass
for i in range(len(self.channel_pans)):
pass
self.channel_pans[i]._fetch_instances()
class Channel(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
super(S3m.Channel, self).__init__(_io)
self._parent = _parent
self._root = _root
self._read()
def _read(self):
self.is_disabled = self._io.read_bits_int_be(1) != 0
self.ch_type = self._io.read_bits_int_be(7)
def _fetch_instances(self):
pass
class ChannelPan(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
super(S3m.ChannelPan, self).__init__(_io)
self._parent = _parent
self._root = _root
self._read()
def _read(self):
self.reserved1 = self._io.read_bits_int_be(2)
self.has_custom_pan = self._io.read_bits_int_be(1) != 0
self.reserved2 = self._io.read_bits_int_be(1) != 0
self.pan = self._io.read_bits_int_be(4)
def _fetch_instances(self):
pass
class Instrument(KaitaiStruct):
class InstTypes(IntEnum):
sample = 1
melodic = 2
bass_drum = 3
snare_drum = 4
tom = 5
cymbal = 6
hihat = 7
def __init__(self, _io, _parent=None, _root=None):
super(S3m.Instrument, self).__init__(_io)
self._parent = _parent
self._root = _root
self._read()
def _read(self):
self.type = KaitaiStream.resolve_enum(S3m.Instrument.InstTypes, self._io.read_u1())
self.filename = KaitaiStream.bytes_terminate(self._io.read_bytes(12), 0, False)
_on = self.type
if _on == S3m.Instrument.InstTypes.sample:
pass
self.body = S3m.Instrument.Sampled(self._io, self, self._root)
else:
pass
self.body = S3m.Instrument.Adlib(self._io, self, self._root)
self.tuning_hz = self._io.read_u4le()
self.reserved2 = self._io.read_bytes(12)
self.sample_name = KaitaiStream.bytes_terminate(self._io.read_bytes(28), 0, False)
self.magic = self._io.read_bytes(4)
if not self.magic == b"\x53\x43\x52\x53":
raise kaitaistruct.ValidationNotEqualError(b"\x53\x43\x52\x53", self.magic, self._io, u"/types/instrument/seq/6")
def _fetch_instances(self):
pass
_on = self.type
if _on == S3m.Instrument.InstTypes.sample:
pass
self.body._fetch_instances()
else:
pass
self.body._fetch_instances()
class Adlib(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
super(S3m.Instrument.Adlib, self).__init__(_io)
self._parent = _parent
self._root = _root
self._read()
def _read(self):
self.reserved1 = self._io.read_bytes(3)
if not self.reserved1 == b"\x00\x00\x00":
raise kaitaistruct.ValidationNotEqualError(b"\x00\x00\x00", self.reserved1, self._io, u"/types/instrument/types/adlib/seq/0")
self._unnamed1 = self._io.read_bytes(16)
def _fetch_instances(self):
pass
class Sampled(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
super(S3m.Instrument.Sampled, self).__init__(_io)
self._parent = _parent
self._root = _root
self._read()
def _read(self):
self.paraptr_sample = S3m.SwappedU3(self._io, self, self._root)
self.len_sample = self._io.read_u4le()
self.loop_begin = self._io.read_u4le()
self.loop_end = self._io.read_u4le()
self.default_volume = self._io.read_u1()
self.reserved1 = self._io.read_u1()
self.is_packed = self._io.read_u1()
self.flags = self._io.read_u1()
def _fetch_instances(self):
pass
self.paraptr_sample._fetch_instances()
_ = self.sample
if hasattr(self, '_m_sample'):
pass
@property
def sample(self):
if hasattr(self, '_m_sample'):
return self._m_sample
_pos = self._io.pos()
self._io.seek(self.paraptr_sample.value * 16)
self._m_sample = self._io.read_bytes(self.len_sample)
self._io.seek(_pos)
return getattr(self, '_m_sample', None)
class InstrumentPtr(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
super(S3m.InstrumentPtr, self).__init__(_io)
self._parent = _parent
self._root = _root
self._read()
def _read(self):
self.paraptr = self._io.read_u2le()
def _fetch_instances(self):
pass
_ = self.body
if hasattr(self, '_m_body'):
pass
self._m_body._fetch_instances()
@property
def body(self):
if hasattr(self, '_m_body'):
return self._m_body
_pos = self._io.pos()
self._io.seek(self.paraptr * 16)
self._m_body = S3m.Instrument(self._io, self, self._root)
self._io.seek(_pos)
return getattr(self, '_m_body', None)
class Pattern(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
super(S3m.Pattern, self).__init__(_io)
self._parent = _parent
self._root = _root
self._read()
def _read(self):
self.size = self._io.read_u2le()
self._raw_body = self._io.read_bytes(self.size - 2)
_io__raw_body = KaitaiStream(BytesIO(self._raw_body))
self.body = S3m.PatternCells(_io__raw_body, self, self._root)
def _fetch_instances(self):
pass
self.body._fetch_instances()
class PatternCell(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
super(S3m.PatternCell, self).__init__(_io)
self._parent = _parent
self._root = _root
self._read()
def _read(self):
self.has_fx = self._io.read_bits_int_be(1) != 0
self.has_volume = self._io.read_bits_int_be(1) != 0
self.has_note_and_instrument = self._io.read_bits_int_be(1) != 0
self.channel_num = self._io.read_bits_int_be(5)
if self.has_note_and_instrument:
pass
self.note = self._io.read_u1()
if self.has_note_and_instrument:
pass
self.instrument = self._io.read_u1()
if self.has_volume:
pass
self.volume = self._io.read_u1()
if self.has_fx:
pass
self.fx_type = self._io.read_u1()
if self.has_fx:
pass
self.fx_value = self._io.read_u1()
def _fetch_instances(self):
pass
if self.has_note_and_instrument:
pass
if self.has_note_and_instrument:
pass
if self.has_volume:
pass
if self.has_fx:
pass
if self.has_fx:
pass
class PatternCells(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
super(S3m.PatternCells, self).__init__(_io)
self._parent = _parent
self._root = _root
self._read()
def _read(self):
self.cells = []
i = 0
while not self._io.is_eof():
self.cells.append(S3m.PatternCell(self._io, self, self._root))
i += 1
def _fetch_instances(self):
pass
for i in range(len(self.cells)):
pass
self.cells[i]._fetch_instances()
class PatternPtr(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
super(S3m.PatternPtr, self).__init__(_io)
self._parent = _parent
self._root = _root
self._read()
def _read(self):
self.paraptr = self._io.read_u2le()
def _fetch_instances(self):
pass
_ = self.body
if hasattr(self, '_m_body'):
pass
self._m_body._fetch_instances()
@property
def body(self):
if hasattr(self, '_m_body'):
return self._m_body
_pos = self._io.pos()
self._io.seek(self.paraptr * 16)
self._m_body = S3m.Pattern(self._io, self, self._root)
self._io.seek(_pos)
return getattr(self, '_m_body', None)
class SwappedU3(KaitaiStruct):
"""Custom 3-byte integer, stored in mixed endian manner."""
def __init__(self, _io, _parent=None, _root=None):
super(S3m.SwappedU3, self).__init__(_io)
self._parent = _parent
self._root = _root
self._read()
def _read(self):
self.hi = self._io.read_u1()
self.lo = self._io.read_u2le()
def _fetch_instances(self):
pass
@property
def value(self):
if hasattr(self, '_m_value'):
return self._m_value
self._m_value = self.lo | self.hi << 16
return getattr(self, '_m_value', None)