Scream Tracker 3 module: Perl parsing library

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.

File extension

s3m

KS implementation details

License: CC0-1.0

References

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.

Perl source code to parse Scream Tracker 3 module

S3m.pm

# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

use strict;
use warnings;
use IO::KaitaiStruct 0.009_000;

########################################################################
package S3m;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{song_name} = IO::KaitaiStruct::Stream::bytes_terminate($self->{_io}->read_bytes(28), 0, 0);
    $self->{magic1} = $self->{_io}->read_bytes(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);
    $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);
    $self->{master_volume} = $self->{_io}->read_bits_int_be(7);
    $self->{_io}->align_to_byte();
    $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} = ();
    my $n_channels = 32;
    for (my $i = 0; $i < $n_channels; $i++) {
        $self->{channels}[$i] = S3m::Channel->new($self->{_io}, $self, $self->{_root});
    }
    $self->{orders} = $self->{_io}->read_bytes($self->num_orders());
    $self->{instruments} = ();
    my $n_instruments = $self->num_instruments();
    for (my $i = 0; $i < $n_instruments; $i++) {
        $self->{instruments}[$i] = S3m::InstrumentPtr->new($self->{_io}, $self, $self->{_root});
    }
    $self->{patterns} = ();
    my $n_patterns = $self->num_patterns();
    for (my $i = 0; $i < $n_patterns; $i++) {
        $self->{patterns}[$i] = S3m::PatternPtr->new($self->{_io}, $self, $self->{_root});
    }
    if ($self->has_custom_pan() == 252) {
        $self->{channel_pans} = ();
        my $n_channel_pans = 32;
        for (my $i = 0; $i < $n_channel_pans; $i++) {
            $self->{channel_pans}[$i] = S3m::ChannelPan->new($self->{_io}, $self, $self->{_root});
        }
    }
}

sub song_name {
    my ($self) = @_;
    return $self->{song_name};
}

sub magic1 {
    my ($self) = @_;
    return $self->{magic1};
}

sub file_type {
    my ($self) = @_;
    return $self->{file_type};
}

sub reserved1 {
    my ($self) = @_;
    return $self->{reserved1};
}

sub num_orders {
    my ($self) = @_;
    return $self->{num_orders};
}

sub num_instruments {
    my ($self) = @_;
    return $self->{num_instruments};
}

sub num_patterns {
    my ($self) = @_;
    return $self->{num_patterns};
}

sub flags {
    my ($self) = @_;
    return $self->{flags};
}

sub version {
    my ($self) = @_;
    return $self->{version};
}

sub samples_format {
    my ($self) = @_;
    return $self->{samples_format};
}

sub magic2 {
    my ($self) = @_;
    return $self->{magic2};
}

sub global_volume {
    my ($self) = @_;
    return $self->{global_volume};
}

sub initial_speed {
    my ($self) = @_;
    return $self->{initial_speed};
}

sub initial_tempo {
    my ($self) = @_;
    return $self->{initial_tempo};
}

sub is_stereo {
    my ($self) = @_;
    return $self->{is_stereo};
}

sub master_volume {
    my ($self) = @_;
    return $self->{master_volume};
}

sub ultra_click_removal {
    my ($self) = @_;
    return $self->{ultra_click_removal};
}

sub has_custom_pan {
    my ($self) = @_;
    return $self->{has_custom_pan};
}

sub reserved2 {
    my ($self) = @_;
    return $self->{reserved2};
}

sub ofs_special {
    my ($self) = @_;
    return $self->{ofs_special};
}

sub channels {
    my ($self) = @_;
    return $self->{channels};
}

sub orders {
    my ($self) = @_;
    return $self->{orders};
}

sub instruments {
    my ($self) = @_;
    return $self->{instruments};
}

sub patterns {
    my ($self) = @_;
    return $self->{patterns};
}

sub channel_pans {
    my ($self) = @_;
    return $self->{channel_pans};
}

########################################################################
package S3m::ChannelPan;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{reserved1} = $self->{_io}->read_bits_int_be(2);
    $self->{has_custom_pan} = $self->{_io}->read_bits_int_be(1);
    $self->{reserved2} = $self->{_io}->read_bits_int_be(1);
    $self->{pan} = $self->{_io}->read_bits_int_be(4);
}

sub reserved1 {
    my ($self) = @_;
    return $self->{reserved1};
}

sub has_custom_pan {
    my ($self) = @_;
    return $self->{has_custom_pan};
}

sub reserved2 {
    my ($self) = @_;
    return $self->{reserved2};
}

sub pan {
    my ($self) = @_;
    return $self->{pan};
}

########################################################################
package S3m::PatternCell;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{has_fx} = $self->{_io}->read_bits_int_be(1);
    $self->{has_volume} = $self->{_io}->read_bits_int_be(1);
    $self->{has_note_and_instrument} = $self->{_io}->read_bits_int_be(1);
    $self->{channel_num} = $self->{_io}->read_bits_int_be(5);
    $self->{_io}->align_to_byte();
    if ($self->has_note_and_instrument()) {
        $self->{note} = $self->{_io}->read_u1();
    }
    if ($self->has_note_and_instrument()) {
        $self->{instrument} = $self->{_io}->read_u1();
    }
    if ($self->has_volume()) {
        $self->{volume} = $self->{_io}->read_u1();
    }
    if ($self->has_fx()) {
        $self->{fx_type} = $self->{_io}->read_u1();
    }
    if ($self->has_fx()) {
        $self->{fx_value} = $self->{_io}->read_u1();
    }
}

sub has_fx {
    my ($self) = @_;
    return $self->{has_fx};
}

sub has_volume {
    my ($self) = @_;
    return $self->{has_volume};
}

sub has_note_and_instrument {
    my ($self) = @_;
    return $self->{has_note_and_instrument};
}

sub channel_num {
    my ($self) = @_;
    return $self->{channel_num};
}

sub note {
    my ($self) = @_;
    return $self->{note};
}

sub instrument {
    my ($self) = @_;
    return $self->{instrument};
}

sub volume {
    my ($self) = @_;
    return $self->{volume};
}

sub fx_type {
    my ($self) = @_;
    return $self->{fx_type};
}

sub fx_value {
    my ($self) = @_;
    return $self->{fx_value};
}

########################################################################
package S3m::PatternCells;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{cells} = ();
    while (!$self->{_io}->is_eof()) {
        push @{$self->{cells}}, S3m::PatternCell->new($self->{_io}, $self, $self->{_root});
    }
}

sub cells {
    my ($self) = @_;
    return $self->{cells};
}

########################################################################
package S3m::Channel;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{is_disabled} = $self->{_io}->read_bits_int_be(1);
    $self->{ch_type} = $self->{_io}->read_bits_int_be(7);
}

sub is_disabled {
    my ($self) = @_;
    return $self->{is_disabled};
}

sub ch_type {
    my ($self) = @_;
    return $self->{ch_type};
}

########################################################################
package S3m::SwappedU3;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{hi} = $self->{_io}->read_u1();
    $self->{lo} = $self->{_io}->read_u2le();
}

sub value {
    my ($self) = @_;
    return $self->{value} if ($self->{value});
    $self->{value} = ($self->lo() | ($self->hi() << 16));
    return $self->{value};
}

sub hi {
    my ($self) = @_;
    return $self->{hi};
}

sub lo {
    my ($self) = @_;
    return $self->{lo};
}

########################################################################
package S3m::Pattern;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{size} = $self->{_io}->read_u2le();
    $self->{_raw_body} = $self->{_io}->read_bytes(($self->size() - 2));
    my $io__raw_body = IO::KaitaiStruct::Stream->new($self->{_raw_body});
    $self->{body} = S3m::PatternCells->new($io__raw_body, $self, $self->{_root});
}

sub size {
    my ($self) = @_;
    return $self->{size};
}

sub body {
    my ($self) = @_;
    return $self->{body};
}

sub _raw_body {
    my ($self) = @_;
    return $self->{_raw_body};
}

########################################################################
package S3m::PatternPtr;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{paraptr} = $self->{_io}->read_u2le();
}

sub body {
    my ($self) = @_;
    return $self->{body} if ($self->{body});
    my $_pos = $self->{_io}->pos();
    $self->{_io}->seek(($self->paraptr() * 16));
    $self->{body} = S3m::Pattern->new($self->{_io}, $self, $self->{_root});
    $self->{_io}->seek($_pos);
    return $self->{body};
}

sub paraptr {
    my ($self) = @_;
    return $self->{paraptr};
}

########################################################################
package S3m::InstrumentPtr;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{paraptr} = $self->{_io}->read_u2le();
}

sub body {
    my ($self) = @_;
    return $self->{body} if ($self->{body});
    my $_pos = $self->{_io}->pos();
    $self->{_io}->seek(($self->paraptr() * 16));
    $self->{body} = S3m::Instrument->new($self->{_io}, $self, $self->{_root});
    $self->{_io}->seek($_pos);
    return $self->{body};
}

sub paraptr {
    my ($self) = @_;
    return $self->{paraptr};
}

########################################################################
package S3m::Instrument;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

our $INST_TYPES_SAMPLE = 1;
our $INST_TYPES_MELODIC = 2;
our $INST_TYPES_BASS_DRUM = 3;
our $INST_TYPES_SNARE_DRUM = 4;
our $INST_TYPES_TOM = 5;
our $INST_TYPES_CYMBAL = 6;
our $INST_TYPES_HIHAT = 7;

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{type} = $self->{_io}->read_u1();
    $self->{filename} = IO::KaitaiStruct::Stream::bytes_terminate($self->{_io}->read_bytes(12), 0, 0);
    my $_on = $self->type();
    if ($_on == $S3m::Instrument::INST_TYPES_SAMPLE) {
        $self->{body} = S3m::Instrument::Sampled->new($self->{_io}, $self, $self->{_root});
    }
    else {
        $self->{body} = S3m::Instrument::Adlib->new($self->{_io}, $self, $self->{_root});
    }
    $self->{tuning_hz} = $self->{_io}->read_u4le();
    $self->{reserved2} = $self->{_io}->read_bytes(12);
    $self->{sample_name} = IO::KaitaiStruct::Stream::bytes_terminate($self->{_io}->read_bytes(28), 0, 0);
    $self->{magic} = $self->{_io}->read_bytes(4);
}

sub type {
    my ($self) = @_;
    return $self->{type};
}

sub filename {
    my ($self) = @_;
    return $self->{filename};
}

sub body {
    my ($self) = @_;
    return $self->{body};
}

sub tuning_hz {
    my ($self) = @_;
    return $self->{tuning_hz};
}

sub reserved2 {
    my ($self) = @_;
    return $self->{reserved2};
}

sub sample_name {
    my ($self) = @_;
    return $self->{sample_name};
}

sub magic {
    my ($self) = @_;
    return $self->{magic};
}

########################################################################
package S3m::Instrument::Sampled;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{paraptr_sample} = S3m::SwappedU3->new($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();
}

sub sample {
    my ($self) = @_;
    return $self->{sample} if ($self->{sample});
    my $_pos = $self->{_io}->pos();
    $self->{_io}->seek(($self->paraptr_sample()->value() * 16));
    $self->{sample} = $self->{_io}->read_bytes($self->len_sample());
    $self->{_io}->seek($_pos);
    return $self->{sample};
}

sub paraptr_sample {
    my ($self) = @_;
    return $self->{paraptr_sample};
}

sub len_sample {
    my ($self) = @_;
    return $self->{len_sample};
}

sub loop_begin {
    my ($self) = @_;
    return $self->{loop_begin};
}

sub loop_end {
    my ($self) = @_;
    return $self->{loop_end};
}

sub default_volume {
    my ($self) = @_;
    return $self->{default_volume};
}

sub reserved1 {
    my ($self) = @_;
    return $self->{reserved1};
}

sub is_packed {
    my ($self) = @_;
    return $self->{is_packed};
}

sub flags {
    my ($self) = @_;
    return $self->{flags};
}

########################################################################
package S3m::Instrument::Adlib;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{reserved1} = $self->{_io}->read_bytes(3);
    $self->{_unnamed1} = $self->{_io}->read_bytes(16);
}

sub reserved1 {
    my ($self) = @_;
    return $self->{reserved1};
}

sub _unnamed1 {
    my ($self) = @_;
    return $self->{_unnamed1};
}

1;