Skip to content
LGB Z edited this page Mar 18, 2015 · 2 revisions

S3MAL - S3M (AdLib) Lite

S3M is a nice format for store AdLib songs (digital sampled version - which is the most common as with any module format - is not interesting for us now). However it has some complexity which is not always needed if we want only AdLib instruments, easy usage on a C64, and so on.

S3MAL is my own creation to have a simplified format. Strictly speaking, it's not even a format, the converter which produces S3MAL from S3M generates assembly files which must be compiled with the CA65 assembler. You can - in theory - assemble that file (into a separate "binary"), but it's useless, as information encoded as labels. This is natural however, since the purpose of S3MAL is to build a song into your program (to link with the player code which uses the provided labels): it's not a loadable format.

For sure, for shorter songs using DRO or IMF format is better but it wastes memory a lot: every AdLib register event is described, using a structured format with separated instrument information (like S3M in general, or the "simplified" format S3MAL) the size can be smaller - however you need more CPU power to decode/play though.

Details

The generated assembly file (which is CA65 assembler syntax!) contains the following important labels you must use:

  • s3mal_patterns_lo
  • s3mal_patterns_hi

Low&high bytes of the addresses of the patterns. Note: there is no "order table"! The pattern table is used as order table too: your player should simply play patterns specified here, until the high byte is zero: it specifies the end of the song (low byte is undefined in that case, you should not assume there is valid information there then!)

  • s3mal_inst_lo
  • s3mal_inst_hi

Low&high bytes of the addresses of the instruments.

Instruments

Patterns

First, an important note: S3M specifies that patterns should be always 64 rows in size. In S3MAL there is no an exact size. The pattern can be smaller and longer too. It's really not important to know, only if you have some special command (jump) in the patterns. Then, converter will encode the patterns in a way that it will work. But you must not treat to have standard S3M patterns! It's also possible that the pattern table contain a single entry: a pattern with hundreds of rows, and that contains the whole song! It helps to decrease the complexity of the file, and also playing code use more CPU of course, if you need to "seek" to a new pattern. It's the converter task to optimize and "decide" how to "slice" the song into patterns: there is no relation with the original S3M patterns at all (only with some special commands).

Unlike S3M's pattern, you always have 8 channels, no more or less (of course it's ok not to use all: as the pattern rows specifies which channel is used to represent a note/volume/command). These are always mapped directly to the first 8 AdLib channels, no channel mapping. Yes, no mistake here: 8 channels, not 9! The reason about this: one channel is reserved (the last) to play sound effects which is unrelated to the song (it's nice anyway: you can include "instruments" you want to use as effects - like in a game - and while music is played back, those instruments can be used on the free channel to generate the sound of the desired effect).

The pattern "compression" algorithm is somewhat similar to the S3M's but with some modifications. The algorithm is about this:

  • seek to the needed pattern (given by pattern table)
  • now read a single byte
  • if bit 7 is set, it's a special byte, the following points does not apply, read later at bit 7 related lines again
  • otherwise, the lower 3 bits contain the AdLib channel (remember, you have 8 channels only, not 9!)
  • the high 5 bites encodes the need of reading more byte(s)
  • if bit 3 is 1, read one byte, it's the tone information. The instrument will be used to play: the previous one used in this channel unless if bit 4 is 1, see below. Note=$FE is special, key-off AdLib event (see in S3M specification).
  • if bit 4 is 1, read one byte, the instrument number. It also affects the "last used instrument in this channel" info: the next row for same channel will use this instrument as well if no instrument specification is done for it!
  • if bit 5 is 1, read one byte: it adjust the volume of the channel. If bit 5 is zero, there is no change in the volume.
  • if bit 6 is 1, read one byte: that is the command code. If bit 6 is zero, no command. If there is a command, the command can have some command info bytes you should read. You can't tell how much bytes though: you must examine the exact command (see later) to know that information!
  • if bit 7 is set, it's a special byte, the the meaning of the bits etc does not apply! So "negative numbers" (can be tested with BPL/BMI in assembly) are the special ones.
  • play (or not play - if there is nothing to do) and/or process pattern command (if any), keep the timing in your mind, then continue with the "now read a single byte" step (unless the pattern is over, the song is over, or a command specifies some special jump instruction)

The special bytes (if bit 7 is set only, so if the number is "negative" is you interpret the byte as 8 bit signed otherwise):

  • $FF: end of the whole pattern
  • $FE: end of the row
  • $Ex: skip "x" number for rows, they are treated as empty ones, it also means the end of the current row!

Other values are not used.

The default ("empty") values for tone/instrument/volume/command is the same as with S3M.

It's possible (like with S3M) to encode more "columns" for the same AdLib channel in a row: however that situation is quite undefined, and the converter will not produce such a file!

Pattern decoding in play-time

Implementation notes: you may want a zero page location contains (two bytes) the current byte location by the pattern decoder, let's call it ZP. It's initialized by the "pattern seek" from the pattern table. PATZP is the actual pattern number, again a zero page location. It must be initialized zero at the very beginning, and usually you want to INC it after the end of the played pattern (unless there is something special, like "jump" command). The code can be something like this to "seek pattern" (also the "end of the song" situation is handled here):

LDX PATZP
LDA s3mal_patterns_hi,X
BEQ end_of_the_whole_song
STA ZP+1
LDA s3mal_patterns_lo,X
STA ZP

Then, to decode patterns, you may want to read bytes like this (after initialization Y to zero):

LDA (ZP),Y

After loading the "first byte" you can use BMI opcode: if bit 7 is set, it's a special byte (not encoded as channel + bit mask of things to read).

Then just increment Y then do that loading for situations you need to read one byte. If row is over however, you must add Y to the zero page location and you should use Y=0 again for the next row, as +255 as offset (specified by Y register) is not enough in most cases to decode a whole pattern (especially because pattern length can be quite long in S3MAL). Example for this addition:

TYA
CLC
ADC ZP
STA ZP
BCC no_need_to_modify_high_byte
INC ZP+1
...

Also, it's natural to play sounds while decoding: after you read a "column", you can play the note (or anything it's about ...) on the specified channel. No need to decode pattern first (it's possible there is not even enough memory for that!) to play it in an undecoded form, it is just waste of your CPU, as decoding is needed anyway, so it's faster to also play then.

Commands

The most complex part of S3M (and module formats in general - I mean implementing all of the commands) are the commands (at least this is my opinion). As S3MAL wants to be a light version we don't support every commands S3M knows about.

Important note: as stated in the previous section (patterns) a command can have zero, one or more bytes as parameter. This is unlike S3M where command always have one byte as a parameter! The reason for more bytes: some specific commands (like jump) uses memory addresses instead for easier/faster decoding.