A .sap is two files concatenated: a header of plain ASCII tag lines, then
a standard Atari executable holding the 6502 player and its music data. The specification
points out you can build one with cat. Like NSF and PSID it ships the program rather
than the music, and the tags say which addresses to call and how often.
POKEY is the chip every SAP exists to drive, and it is not a synthesiser so much as a bank of dividers. Four channels, each an 8-bit frequency divider off the main clock with its own 4-bit volume. Pairs can be joined into 16-bit channels, trading two voices for the pitch resolution that bass lines need -- an 8-bit divider is coarse enough that low notes land audibly out of tune.
Timbre comes from polynomial counters rather than from waveform shapes. A 17-bit, a 5-bit and a 4-bit shift register can be switched into a channel's output to break up the square wave, giving everything from pure tone through buzzing pitched distortion to white noise. The same mechanism produces both the noise and the character, which is why POKEY music sounds the way it does.
Two further techniques matter for what the binary half will be doing. Setting a
channel to volume-only mode turns its volume register into a 4-bit DAC, which is how
digitised samples are played -- and why the TYPE D files below need cycle-exact timing.
And a second POKEY may be fitted for stereo, declared by the STEREO tag.
Five bytes, and that is the entire magic: the three characters SAP
followed immediately by CR LF. It is a weak signature -- three common letters and a line ending --
so identification should require a valid second line before accepting a file.
One tag per line, CR LF terminated. A line is an uppercase tag name, then optionally a single space and an argument. Arguments are a quoted string, a decimal integer, a hexadecimal address, or a single letter.
| AUTHOR | Composer. Real name with an optional scene handle in parentheses; multiple authors joined with &. |
| NAME | Title. |
| DATE | Year, DD/MM/YYYY, or a range. 199? is a legal way to say “some year in the nineties”. |
| SONGS | Subsong count. Omitted when there is one. |
| DEFSONG | Which subsong plays first, indexed from zero. Defaults to 0. |
| TYPE | Player type: B, C, D, S or R. Determines which other tags are required. |
| INIT | Address of the setup routine. Required for B, D and S; invalid for C. |
| MUSIC | Address of the music data. Required for C; invalid for everything else. |
| PLAYER | Address of the routine called on the timer. |
| FASTPLAY | Scanlines between player calls. A scanline is 114 clock cycles; the default is one frame, 312 for PAL and 262 for NTSC. |
| STEREO | Dual POKEY. Takes no argument. |
| NTSC | Play as NTSC rather than the default PAL. Takes no argument. |
| COVOX | COVOX expansion at the given address, which can only be D600. |
| TIME | Duration as M:SS.fff, optionally followed by LOOP. One line per subsong, in order. |
Arguments may use only characters that mean the same thing in ASCII and in ATASCII, the Atari's own encoding: space through underscore, all lowercase letters, and the pipe. There is no backquote, tilde or curly brace in ATASCII, so those four cannot appear.
There is no escape mechanism, and doublequotes inside a quoted argument are best avoided because not every player copes. Arguments should stay within 120 characters plus the enclosing quotes.
The narrow set has a second consequence, used below: 0xFF cannot occur in a valid header.
The specification defines no end-of-header marker. There is no terminating
tag, no blank line, no length field. The boundary has to be inferred, and the inference is sound
rather than quoted: the header cannot contain 0xFF, and the binary half is required
to begin with two of them.
So the first FF FF is the boundary. Checking that the byte before it is
a line ending is what separates a real boundary from an FF FF that happens to fall
inside a damaged file.
A standard Atari executable: one or more blocks, each naming where it loads. This is
the same layout a .xex uses, and it is the one part of SAP with a second independent
description.
A block holds end - start + 1 bytes. Reading it as
end - start loses the last byte of every block in the file, and the file still
parses, so nothing complains and the damage is silent.
A block whose end address is below its start is malformed.
The two 0xFF bytes are required only on the first block.
Later blocks may omit them, which means a reader cannot recover from a bad length by scanning
forward for the next FF FF -- there may not be one, and a run of two FF
bytes inside block data is indistinguishable from a header.
Blocks must be walked in order from the first. One wrong length desynchronises everything after it.
In an ordinary Atari executable, 0x02E0-0x02E1 holds the run
address and 0x02E2-0x02E3 the initialisation address, and the loader jumps to
them. In SAP those are ordinary RAM with no special treatment: the INIT and
PLAYER tags supply the entry points instead.
A block loading data to 0x02E0 is therefore just a block, not an
entry vector.
The TYPE tag does more than label the file: it determines the calling
convention, and which other tags are legal.
| B | The standard. The subsong number goes in the accumulator, INIT is called and returns, then PLAYER is called every FASTPLAY scanlines and returns each time. |
| C | A convenience type for Chaos Music Composer. MUSIC is required and INIT is invalid; setup is a fixed call sequence into PLAYER+3, after which PLAYER+6 is called on the timer. |
| D | Digitised audio. Like B except the INIT routine never returns -- it drives the audio itself using WSYNC, VCOUNT and POKEY timer interrupts. Registers are saved before each PLAYER call and restored back into the still-running routine. |
| S | A convenience type for SoftSynth. PLAYER is not used; instead location 0x45 is decremented on the timer and 0xB07B is incremented when it reaches zero. The default FASTPLAY for this type is 78, not 312. |
| R | A raw dump of POKEY registers 0xD200-0xD208 at the FASTPLAY rate, instead of an executable. The binary half is therefore not block-structured, and the boundary rule above does not apply. |
The specification is internally inconsistent about R: the description of
the TYPE tag lists only B, C, D and S, while the player-types section documents R in
full. Reading it as defined-but-unimplemented resolves the contradiction.