ACIDCAT . FILE FORMAT REFERENCE

SAP Anatomy

Atari 8-bit XL / XEPOKEY . 6502
rev 2026.08
magic SAP CR LF
header ASCII text
payload Atari executable
endian little

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.

the sound hardware

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.

the signature

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.

the text header

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.

AUTHORComposer. Real name with an optional scene handle in parentheses; multiple authors joined with &.
NAMETitle.
DATEYear, DD/MM/YYYY, or a range. 199? is a legal way to say “some year in the nineties”.
SONGSSubsong count. Omitted when there is one.
DEFSONGWhich subsong plays first, indexed from zero. Defaults to 0.
TYPEPlayer type: B, C, D, S or R. Determines which other tags are required.
INITAddress of the setup routine. Required for B, D and S; invalid for C.
MUSICAddress of the music data. Required for C; invalid for everything else.
PLAYERAddress of the routine called on the timer.
FASTPLAYScanlines between player calls. A scanline is 114 clock cycles; the default is one frame, 312 for PAL and 262 for NTSC.
STEREODual POKEY. Takes no argument.
NTSCPlay as NTSC rather than the default PAL. Takes no argument.
COVOXCOVOX expansion at the given address, which can only be D600.
TIMEDuration as M:SS.fff, optionally followed by LOOP. One line per subsong, in order.
▸the character set is narrower than ASCII0x20-0x5F . 0x61-0x7A . 0x7C

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.

where the text stops

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.

the binary half

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.

▸the end address is inclusiveend - start + 1

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.

▸FF FF is optional after the first blockso you cannot resynchronise

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.

▸0x02E0 is not special hereunlike a real Atari executable

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 player types

The TYPE tag does more than label the file: it determines the calling convention, and which other tags are legal.

BThe standard. The subsong number goes in the accumulator, INIT is called and returns, then PLAYER is called every FASTPLAY scanlines and returns each time.
CA 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.
DDigitised 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.
SA 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.
RA 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.

SAP file layout 0x00 text header ASCII, CR LF line endings "SAP" CR LF the entire signature AUTHOR / NAME / DATE SONGS / DEFSONG TYPE decides which tags below are legal INIT / MUSIC / PLAYER / FASTPLAY TIME one line per subsong (no end marker -- the boundary is the first FF FF) .. binary half standard Atari executable FF FF required on block 1, optional after start u16 little-endian last u16 little-endian, INCLUSIVE data end - start + 1 bytes ... further blocks, walked in order