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 C# generated by Kaitai Struct depends on the C# runtime library. You have to install it before you can parse data.
The C# runtime library is available in the NuGet Gallery. Installation instructions can also be found there.
Parse a local file and get structure in memory:
var data = S3m.FromFile("path/to/local/file.s3m");
Or parse structure from a byte array:
byte[] someArray = new byte[] { ... };
var data = new S3m(new KaitaiStream(someArray));
After that, one can get various attributes from the structure by accessing properties like:
data.NumOrders // => Number of orders in a song
// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild
using System.Collections.Generic;
namespace Kaitai
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Reference: <a href="http://hackipedia.org/browse.cgi/File%20formats/Music%20tracker/S3M%2c%20ScreamTracker%203/Scream%20Tracker%203.20%20by%20Future%20Crew.txt">Source</a>
/// </remarks>
public partial class S3m : KaitaiStruct
{
public static S3m FromFile(string fileName)
{
return new S3m(new KaitaiStream(fileName));
}
public S3m(KaitaiStream p__io, KaitaiStruct p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root ?? this;
_read();
}
private void _read()
{
_songName = KaitaiStream.BytesTerminate(m_io.ReadBytes(28), 0, false);
_magic1 = m_io.ReadBytes(1);
if (!((KaitaiStream.ByteArrayCompare(Magic1, new byte[] { 26 }) == 0)))
{
throw new ValidationNotEqualError(new byte[] { 26 }, Magic1, M_Io, "/seq/1");
}
_fileType = m_io.ReadU1();
_reserved1 = m_io.ReadBytes(2);
_numOrders = m_io.ReadU2le();
_numInstruments = m_io.ReadU2le();
_numPatterns = m_io.ReadU2le();
_flags = m_io.ReadU2le();
_version = m_io.ReadU2le();
_samplesFormat = m_io.ReadU2le();
_magic2 = m_io.ReadBytes(4);
if (!((KaitaiStream.ByteArrayCompare(Magic2, new byte[] { 83, 67, 82, 77 }) == 0)))
{
throw new ValidationNotEqualError(new byte[] { 83, 67, 82, 77 }, Magic2, M_Io, "/seq/10");
}
_globalVolume = m_io.ReadU1();
_initialSpeed = m_io.ReadU1();
_initialTempo = m_io.ReadU1();
_isStereo = m_io.ReadBitsIntBe(1) != 0;
_masterVolume = m_io.ReadBitsIntBe(7);
m_io.AlignToByte();
_ultraClickRemoval = m_io.ReadU1();
_hasCustomPan = m_io.ReadU1();
_reserved2 = m_io.ReadBytes(8);
_ofsSpecial = m_io.ReadU2le();
_channels = new List<Channel>();
for (var i = 0; i < 32; i++)
{
_channels.Add(new Channel(m_io, this, m_root));
}
_orders = m_io.ReadBytes(NumOrders);
_instruments = new List<InstrumentPtr>();
for (var i = 0; i < NumInstruments; i++)
{
_instruments.Add(new InstrumentPtr(m_io, this, m_root));
}
_patterns = new List<PatternPtr>();
for (var i = 0; i < NumPatterns; i++)
{
_patterns.Add(new PatternPtr(m_io, this, m_root));
}
if (HasCustomPan == 252) {
_channelPans = new List<ChannelPan>();
for (var i = 0; i < 32; i++)
{
_channelPans.Add(new ChannelPan(m_io, this, m_root));
}
}
}
public partial class ChannelPan : KaitaiStruct
{
public static ChannelPan FromFile(string fileName)
{
return new ChannelPan(new KaitaiStream(fileName));
}
public ChannelPan(KaitaiStream p__io, S3m p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root;
_read();
}
private void _read()
{
_reserved1 = m_io.ReadBitsIntBe(2);
_hasCustomPan = m_io.ReadBitsIntBe(1) != 0;
_reserved2 = m_io.ReadBitsIntBe(1) != 0;
_pan = m_io.ReadBitsIntBe(4);
}
private ulong _reserved1;
private bool _hasCustomPan;
private bool _reserved2;
private ulong _pan;
private S3m m_root;
private S3m m_parent;
public ulong Reserved1 { get { return _reserved1; } }
/// <summary>
/// If true, then use a custom pan setting provided in the `pan`
/// field. If false, the channel would use the default setting
/// (0x7 for mono, 0x3 or 0xc for stereo).
/// </summary>
public bool HasCustomPan { get { return _hasCustomPan; } }
public bool Reserved2 { get { return _reserved2; } }
public ulong Pan { get { return _pan; } }
public S3m M_Root { get { return m_root; } }
public S3m M_Parent { get { return m_parent; } }
}
public partial class PatternCell : KaitaiStruct
{
public static PatternCell FromFile(string fileName)
{
return new PatternCell(new KaitaiStream(fileName));
}
public PatternCell(KaitaiStream p__io, S3m.PatternCells p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root;
_read();
}
private void _read()
{
_hasFx = m_io.ReadBitsIntBe(1) != 0;
_hasVolume = m_io.ReadBitsIntBe(1) != 0;
_hasNoteAndInstrument = m_io.ReadBitsIntBe(1) != 0;
_channelNum = m_io.ReadBitsIntBe(5);
m_io.AlignToByte();
if (HasNoteAndInstrument) {
_note = m_io.ReadU1();
}
if (HasNoteAndInstrument) {
_instrument = m_io.ReadU1();
}
if (HasVolume) {
_volume = m_io.ReadU1();
}
if (HasFx) {
_fxType = m_io.ReadU1();
}
if (HasFx) {
_fxValue = m_io.ReadU1();
}
}
private bool _hasFx;
private bool _hasVolume;
private bool _hasNoteAndInstrument;
private ulong _channelNum;
private byte? _note;
private byte? _instrument;
private byte? _volume;
private byte? _fxType;
private byte? _fxValue;
private S3m m_root;
private S3m.PatternCells m_parent;
public bool HasFx { get { return _hasFx; } }
public bool HasVolume { get { return _hasVolume; } }
public bool HasNoteAndInstrument { get { return _hasNoteAndInstrument; } }
public ulong ChannelNum { get { return _channelNum; } }
public byte? Note { get { return _note; } }
public byte? Instrument { get { return _instrument; } }
public byte? Volume { get { return _volume; } }
public byte? FxType { get { return _fxType; } }
public byte? FxValue { get { return _fxValue; } }
public S3m M_Root { get { return m_root; } }
public S3m.PatternCells M_Parent { get { return m_parent; } }
}
public partial class PatternCells : KaitaiStruct
{
public static PatternCells FromFile(string fileName)
{
return new PatternCells(new KaitaiStream(fileName));
}
public PatternCells(KaitaiStream p__io, S3m.Pattern p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root;
_read();
}
private void _read()
{
_cells = new List<PatternCell>();
{
var i = 0;
while (!m_io.IsEof) {
_cells.Add(new PatternCell(m_io, this, m_root));
i++;
}
}
}
private List<PatternCell> _cells;
private S3m m_root;
private S3m.Pattern m_parent;
public List<PatternCell> Cells { get { return _cells; } }
public S3m M_Root { get { return m_root; } }
public S3m.Pattern M_Parent { get { return m_parent; } }
}
public partial class Channel : KaitaiStruct
{
public static Channel FromFile(string fileName)
{
return new Channel(new KaitaiStream(fileName));
}
public Channel(KaitaiStream p__io, S3m p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root;
_read();
}
private void _read()
{
_isDisabled = m_io.ReadBitsIntBe(1) != 0;
_chType = m_io.ReadBitsIntBe(7);
}
private bool _isDisabled;
private ulong _chType;
private S3m m_root;
private S3m m_parent;
public bool IsDisabled { get { return _isDisabled; } }
/// <summary>
/// Channel type (0..7 = left sample channels, 8..15 = right sample channels, 16..31 = AdLib synth channels)
/// </summary>
public ulong ChType { get { return _chType; } }
public S3m M_Root { get { return m_root; } }
public S3m M_Parent { get { return m_parent; } }
}
/// <summary>
/// Custom 3-byte integer, stored in mixed endian manner.
/// </summary>
public partial class SwappedU3 : KaitaiStruct
{
public static SwappedU3 FromFile(string fileName)
{
return new SwappedU3(new KaitaiStream(fileName));
}
public SwappedU3(KaitaiStream p__io, S3m.Instrument.Sampled p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root;
f_value = false;
_read();
}
private void _read()
{
_hi = m_io.ReadU1();
_lo = m_io.ReadU2le();
}
private bool f_value;
private int _value;
public int Value
{
get
{
if (f_value)
return _value;
_value = (int) ((Lo | (Hi << 16)));
f_value = true;
return _value;
}
}
private byte _hi;
private ushort _lo;
private S3m m_root;
private S3m.Instrument.Sampled m_parent;
public byte Hi { get { return _hi; } }
public ushort Lo { get { return _lo; } }
public S3m M_Root { get { return m_root; } }
public S3m.Instrument.Sampled M_Parent { get { return m_parent; } }
}
public partial class Pattern : KaitaiStruct
{
public static Pattern FromFile(string fileName)
{
return new Pattern(new KaitaiStream(fileName));
}
public Pattern(KaitaiStream p__io, S3m.PatternPtr p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root;
_read();
}
private void _read()
{
_size = m_io.ReadU2le();
__raw_body = m_io.ReadBytes((Size - 2));
var io___raw_body = new KaitaiStream(__raw_body);
_body = new PatternCells(io___raw_body, this, m_root);
}
private ushort _size;
private PatternCells _body;
private S3m m_root;
private S3m.PatternPtr m_parent;
private byte[] __raw_body;
public ushort Size { get { return _size; } }
public PatternCells Body { get { return _body; } }
public S3m M_Root { get { return m_root; } }
public S3m.PatternPtr M_Parent { get { return m_parent; } }
public byte[] M_RawBody { get { return __raw_body; } }
}
public partial class PatternPtr : KaitaiStruct
{
public static PatternPtr FromFile(string fileName)
{
return new PatternPtr(new KaitaiStream(fileName));
}
public PatternPtr(KaitaiStream p__io, S3m p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root;
f_body = false;
_read();
}
private void _read()
{
_paraptr = m_io.ReadU2le();
}
private bool f_body;
private Pattern _body;
public Pattern Body
{
get
{
if (f_body)
return _body;
long _pos = m_io.Pos;
m_io.Seek((Paraptr * 16));
_body = new Pattern(m_io, this, m_root);
m_io.Seek(_pos);
f_body = true;
return _body;
}
}
private ushort _paraptr;
private S3m m_root;
private S3m m_parent;
public ushort Paraptr { get { return _paraptr; } }
public S3m M_Root { get { return m_root; } }
public S3m M_Parent { get { return m_parent; } }
}
public partial class InstrumentPtr : KaitaiStruct
{
public static InstrumentPtr FromFile(string fileName)
{
return new InstrumentPtr(new KaitaiStream(fileName));
}
public InstrumentPtr(KaitaiStream p__io, S3m p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root;
f_body = false;
_read();
}
private void _read()
{
_paraptr = m_io.ReadU2le();
}
private bool f_body;
private Instrument _body;
public Instrument Body
{
get
{
if (f_body)
return _body;
long _pos = m_io.Pos;
m_io.Seek((Paraptr * 16));
_body = new Instrument(m_io, this, m_root);
m_io.Seek(_pos);
f_body = true;
return _body;
}
}
private ushort _paraptr;
private S3m m_root;
private S3m m_parent;
public ushort Paraptr { get { return _paraptr; } }
public S3m M_Root { get { return m_root; } }
public S3m M_Parent { get { return m_parent; } }
}
public partial class Instrument : KaitaiStruct
{
public static Instrument FromFile(string fileName)
{
return new Instrument(new KaitaiStream(fileName));
}
public enum InstTypes
{
Sample = 1,
Melodic = 2,
BassDrum = 3,
SnareDrum = 4,
Tom = 5,
Cymbal = 6,
Hihat = 7,
}
public Instrument(KaitaiStream p__io, S3m.InstrumentPtr p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root;
_read();
}
private void _read()
{
_type = ((InstTypes) m_io.ReadU1());
_filename = KaitaiStream.BytesTerminate(m_io.ReadBytes(12), 0, false);
switch (Type) {
case InstTypes.Sample: {
_body = new Sampled(m_io, this, m_root);
break;
}
default: {
_body = new Adlib(m_io, this, m_root);
break;
}
}
_tuningHz = m_io.ReadU4le();
_reserved2 = m_io.ReadBytes(12);
_sampleName = KaitaiStream.BytesTerminate(m_io.ReadBytes(28), 0, false);
_magic = m_io.ReadBytes(4);
if (!((KaitaiStream.ByteArrayCompare(Magic, new byte[] { 83, 67, 82, 83 }) == 0)))
{
throw new ValidationNotEqualError(new byte[] { 83, 67, 82, 83 }, Magic, M_Io, "/types/instrument/seq/6");
}
}
public partial class Sampled : KaitaiStruct
{
public static Sampled FromFile(string fileName)
{
return new Sampled(new KaitaiStream(fileName));
}
public Sampled(KaitaiStream p__io, S3m.Instrument p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root;
f_sample = false;
_read();
}
private void _read()
{
_paraptrSample = new SwappedU3(m_io, this, m_root);
_lenSample = m_io.ReadU4le();
_loopBegin = m_io.ReadU4le();
_loopEnd = m_io.ReadU4le();
_defaultVolume = m_io.ReadU1();
_reserved1 = m_io.ReadU1();
_isPacked = m_io.ReadU1();
_flags = m_io.ReadU1();
}
private bool f_sample;
private byte[] _sample;
public byte[] Sample
{
get
{
if (f_sample)
return _sample;
long _pos = m_io.Pos;
m_io.Seek((ParaptrSample.Value * 16));
_sample = m_io.ReadBytes(LenSample);
m_io.Seek(_pos);
f_sample = true;
return _sample;
}
}
private SwappedU3 _paraptrSample;
private uint _lenSample;
private uint _loopBegin;
private uint _loopEnd;
private byte _defaultVolume;
private byte _reserved1;
private byte _isPacked;
private byte _flags;
private S3m m_root;
private S3m.Instrument m_parent;
public SwappedU3 ParaptrSample { get { return _paraptrSample; } }
public uint LenSample { get { return _lenSample; } }
public uint LoopBegin { get { return _loopBegin; } }
public uint LoopEnd { get { return _loopEnd; } }
/// <summary>
/// Default volume
/// </summary>
public byte DefaultVolume { get { return _defaultVolume; } }
public byte Reserved1 { get { return _reserved1; } }
/// <summary>
/// 0 = unpacked, 1 = DP30ADPCM packing
/// </summary>
public byte IsPacked { get { return _isPacked; } }
public byte Flags { get { return _flags; } }
public S3m M_Root { get { return m_root; } }
public S3m.Instrument M_Parent { get { return m_parent; } }
}
public partial class Adlib : KaitaiStruct
{
public static Adlib FromFile(string fileName)
{
return new Adlib(new KaitaiStream(fileName));
}
public Adlib(KaitaiStream p__io, S3m.Instrument p__parent = null, S3m p__root = null) : base(p__io)
{
m_parent = p__parent;
m_root = p__root;
_read();
}
private void _read()
{
_reserved1 = m_io.ReadBytes(3);
if (!((KaitaiStream.ByteArrayCompare(Reserved1, new byte[] { 0, 0, 0 }) == 0)))
{
throw new ValidationNotEqualError(new byte[] { 0, 0, 0 }, Reserved1, M_Io, "/types/instrument/types/adlib/seq/0");
}
__unnamed1 = m_io.ReadBytes(16);
}
private byte[] _reserved1;
private byte[] __unnamed1;
private S3m m_root;
private S3m.Instrument m_parent;
public byte[] Reserved1 { get { return _reserved1; } }
public byte[] Unnamed_1 { get { return __unnamed1; } }
public S3m M_Root { get { return m_root; } }
public S3m.Instrument M_Parent { get { return m_parent; } }
}
private InstTypes _type;
private byte[] _filename;
private KaitaiStruct _body;
private uint _tuningHz;
private byte[] _reserved2;
private byte[] _sampleName;
private byte[] _magic;
private S3m m_root;
private S3m.InstrumentPtr m_parent;
public InstTypes Type { get { return _type; } }
public byte[] Filename { get { return _filename; } }
public KaitaiStruct Body { get { return _body; } }
public uint TuningHz { get { return _tuningHz; } }
public byte[] Reserved2 { get { return _reserved2; } }
public byte[] SampleName { get { return _sampleName; } }
public byte[] Magic { get { return _magic; } }
public S3m M_Root { get { return m_root; } }
public S3m.InstrumentPtr M_Parent { get { return m_parent; } }
}
private byte[] _songName;
private byte[] _magic1;
private byte _fileType;
private byte[] _reserved1;
private ushort _numOrders;
private ushort _numInstruments;
private ushort _numPatterns;
private ushort _flags;
private ushort _version;
private ushort _samplesFormat;
private byte[] _magic2;
private byte _globalVolume;
private byte _initialSpeed;
private byte _initialTempo;
private bool _isStereo;
private ulong _masterVolume;
private byte _ultraClickRemoval;
private byte _hasCustomPan;
private byte[] _reserved2;
private ushort _ofsSpecial;
private List<Channel> _channels;
private byte[] _orders;
private List<InstrumentPtr> _instruments;
private List<PatternPtr> _patterns;
private List<ChannelPan> _channelPans;
private S3m m_root;
private KaitaiStruct m_parent;
public byte[] SongName { get { return _songName; } }
public byte[] Magic1 { get { return _magic1; } }
public byte FileType { get { return _fileType; } }
public byte[] Reserved1 { get { return _reserved1; } }
/// <summary>
/// Number of orders in a song
/// </summary>
public ushort NumOrders { get { return _numOrders; } }
/// <summary>
/// Number of instruments in a song
/// </summary>
public ushort NumInstruments { get { return _numInstruments; } }
/// <summary>
/// Number of patterns in a song
/// </summary>
public ushort NumPatterns { get { return _numPatterns; } }
public ushort Flags { get { return _flags; } }
/// <summary>
/// Scream Tracker version that was used to save this file
/// </summary>
public ushort Version { get { return _version; } }
/// <summary>
/// 1 = signed samples, 2 = unsigned samples
/// </summary>
public ushort SamplesFormat { get { return _samplesFormat; } }
public byte[] Magic2 { get { return _magic2; } }
public byte GlobalVolume { get { return _globalVolume; } }
public byte InitialSpeed { get { return _initialSpeed; } }
public byte InitialTempo { get { return _initialTempo; } }
public bool IsStereo { get { return _isStereo; } }
public ulong MasterVolume { get { return _masterVolume; } }
public byte UltraClickRemoval { get { return _ultraClickRemoval; } }
public byte HasCustomPan { get { return _hasCustomPan; } }
public byte[] Reserved2 { get { return _reserved2; } }
/// <summary>
/// Offset of special data, not used by Scream Tracker directly.
/// </summary>
public ushort OfsSpecial { get { return _ofsSpecial; } }
public List<Channel> Channels { get { return _channels; } }
public byte[] Orders { get { return _orders; } }
public List<InstrumentPtr> Instruments { get { return _instruments; } }
public List<PatternPtr> Patterns { get { return _patterns; } }
public List<ChannelPan> ChannelPans { get { return _channelPans; } }
public S3m M_Root { get { return m_root; } }
public KaitaiStruct M_Parent { get { return m_parent; } }
}
}