Compression
Energy.Base.Compression groups several independent compression codecs as nested classes. Each codec exposes static Compress and Decompress methods that take and return a byte[].
A null input returns null. An empty input returns an empty array.
Deflate
Raw DEFLATE using the standard framework streams.
byte[] Energy.Base.Compression.Deflate.Compress(byte[] data)
byte[] Energy.Base.Compression.Deflate.Decompress(byte[] data)
GZip
GZIP using the standard framework streams. Same shape as Deflate, with a GZIP header and trailer.
byte[] Energy.Base.Compression.GZip.Compress(byte[] data)
byte[] Energy.Base.Compression.GZip.Decompress(byte[] data)
ZX0
ZX0 is an LZ77-family format with a very small decompressor, popular on 8-bit targets. This implementation uses the forward, non-classic mode.
Compress runs the optimal parser, so its output is byte-for-byte identical to the reference zx0 tool for the same input and mode. Decompress follows the same state machine as the reference dzx0 decoder, including overlapping back-references.
The optimal parser is the cost driver. Its running time grows with the input size and the back-reference window, the same way the reference tool does, so large inputs take noticeably longer than the byte-oriented codecs. The parser allocates its working nodes from a single reusable arena and holds no shared mutable state, so Compress is safe to call from multiple threads at once.
Format offset limit
The ZX0 format encodes a back-reference offset in two parts: an interlaced Elias-Gamma-coded MSB and a 7-bit LSB. The MSB value 256 is reserved as EOF, so the MSB ranges from 0 to 255. The decoder reconstructs the offset as MSB * 128 - LSB, where LSB ranges from 0 to 127. The maximum encodable offset is therefore 255 * 128 = 32640.
No MaxOffset value above 32640 can improve the compression ratio because the format cannot express offsets beyond that distance. A decompression buffer larger than 32641 bytes is wasted on ZX0. Callers that request a larger buffer should clamp to 32641 and warn the user that the excess has no effect.
byte[] Energy.Base.Compression.ZX0.Compress(byte[] data)
byte[] Energy.Base.Compression.ZX0.Compress(byte[] data, Energy.Base.Compression.ZX0.Options options)
byte[] Energy.Base.Compression.ZX0.Decompress(byte[] data)
Options
The Compress overload that takes Energy.Base.Compression.ZX0.Options lets the caller limit the maximum back-reference offset the optimizer may use. The default window is the full ZX0 maximum of 32640 bytes, so the parameterless Compress and new Options() produce byte-identical output.
A window-limited stream is still a valid ZX0 stream. Any ZX0 decompressor can decode it. The constraint is one-directional: a stream compressed with a smaller window can be decompressed by any decoder, but a stream compressed with the full window may contain offsets that a constrained decoder (such as a 1024-byte ring-buffer player) cannot resolve. The caller is responsible for choosing a window that matches the target decoder.
Decompress has no Options overload. The ZX0 bitstream is self-describing: offsets and lengths are encoded in the stream, and the decoder applies them regardless of the window that was used during compression.
| Field | Type | Default | Range | Meaning |
|---|---|---|---|---|
MaxOffset |
int |
32640 | 1–32640 | Maximum back-reference distance in bytes the optimizer may use |
Values outside the range are clamped: below 1 becomes 1, above 32640 becomes 32640. A null options reference is treated as the default.
Energy.Base.Compression.ZX0.Options options = new Energy.Base.Compression.ZX0.Options();
options.MaxOffset = 1023;
byte[] compressed = Energy.Base.Compression.ZX0.Compress(data, options);
byte[] restored = Energy.Base.Compression.ZX0.Decompress(compressed);
LZ4
Raw LZ4 block format (no frame header). Suitable for fast decompression on constrained targets. Output is compatible with the reference LZ4 block format.
byte[] Energy.Base.Compression.LZ4.Compress(byte[] data)
byte[] Energy.Base.Compression.LZ4.Compress(byte[] data, Energy.Base.Compression.LZ4.Options options)
byte[] Energy.Base.Compression.LZ4.Decompress(byte[] data)
Example usage:
byte[] input = GetBytes();
byte[] compressed = Energy.Base.Compression.LZ4.Compress(input);
byte[] output = Energy.Base.Compression.LZ4.Decompress(compressed);
Options
The Compress overload that takes Energy.Base.Compression.LZ4.Options lets the caller limit the maximum back-reference offset the compressor may use. The default window is the full LZ4 maximum of 65535 bytes, so the parameterless Compress and new Options() produce byte-identical output.
A window-limited stream is still a valid LZ4 raw block. Any LZ4 decompressor can decode it. The constraint is one-directional: a stream compressed with a smaller window can be decompressed by any decoder, but a stream compressed with the full window may contain offsets that a constrained decoder (such as a 1024-byte ring-buffer player) cannot resolve. The caller is responsible for choosing a window that matches the target decoder.
Decompress has no Options overload. The LZ4 block format is self-describing: offsets and lengths are encoded in the stream, and the decoder applies them regardless of the window that was used during compression.
| Field | Type | Default | Range | Meaning |
|---|---|---|---|---|
MaxOffset |
int |
65535 | 1–65535 | Maximum back-reference distance in bytes the compressor may use |
Values outside the range are clamped: below 1 becomes 1, above 65535 becomes 65535. A null options reference is treated as the default.
Energy.Base.Compression.LZ4.Options options = new Energy.Base.Compression.LZ4.Options();
options.MaxOffset = 1023;
byte[] compressed = Energy.Base.Compression.LZ4.Compress(data, options);
byte[] restored = Energy.Base.Compression.LZ4.Decompress(compressed);
LZSS
LZSS is an LZ77 variant optimised for small memory and fast decompression on old hardware. This implementation matches the per-stream encoding of the Atari SAP-R compressor dmsc/lzss-sap. Its primary use is compressing register change log data, but it works on any byte array.
Compress runs an optimal parse, so for a given input and parameters its output is bit-for-bit identical to the reference encoder. A stream produced by the reference tool decodes correctly with Decompress. Each call holds all working state in per-call locals, so Compress and Decompress are safe to call from multiple threads at once.
An invalid option or malformed input returns null (the same as null input); the caller does not receive an exception.
Methods
byte[] Energy.Base.Compression.LZSS.Compress(byte[] data)
byte[] Energy.Base.Compression.LZSS.Compress(byte[] data, Energy.Base.Compression.LZSS.Options options)
byte[] Energy.Base.Compression.LZSS.Decompress(byte[] data)
byte[] Energy.Base.Compression.LZSS.Decompress(byte[] data, Energy.Base.Compression.LZSS.Options options)
Options
All format parameters live in Energy.Base.Compression.LZSS.Options. A raw LZSS stream carries no parameter header, so Decompress must receive the same options that were used to Compress.
| Field | Type | Default | Range | Meaning |
|---|---|---|---|---|
OffsetBits |
int |
4 | 0–12 | Bits for a back-reference offset; window = 2^OffsetBits bytes |
LengthBits |
int |
4 | ≥ 2; sum 8–16 | Bits for a match length |
MinimumMatch |
int |
2 | 1–16 | Shortest run stored as a back-reference |
LiteralFirst |
bool |
true | — | Emit the first input byte as a raw literal before the bit stream |
ForceLastLiteral |
bool |
true | — | Ensure the stream ends in a literal so a decoder can detect end-of-stream |
PositionStartZero |
bool |
false | — | Base offsets at zero rather than the maximum offset (older format variant) |
FrameSize |
int |
0 | ≥ 0 | 0 or 1 = plain single stream; ≥ 2 = interleaved multi-channel SAP-R container |
The default constructor sets the 8-bit preset (the reference -8 flag):
Energy.Base.Compression.LZSS.Options options = new Energy.Base.Compression.LZSS.Options();
// OffsetBits=4, LengthBits=4, MinimumMatch=2
// LiteralFirst=true, ForceLastLiteral=true, PositionStartZero=false, FrameSize=0
Single-stream usage
The parameterless overloads use the default 8-bit preset: a 16-byte back-reference window and match lengths from 2 to 17 bytes.
byte[] data = File.ReadAllBytes("input.bin");
byte[] compressed = Energy.Base.Compression.LZSS.Compress(data);
byte[] restored = Energy.Base.Compression.LZSS.Decompress(compressed);
Round-trip check:
bool ok = (restored != null && restored.Length == data.Length);
Presets
Three presets match the reference lzss-sap tool’s -8, -12, and -16 flags.
8-bit preset — 16-byte window, match lengths 2–17 (this is the default Options())
Energy.Base.Compression.LZSS.Options options = new Energy.Base.Compression.LZSS.Options();
// OffsetBits=4, LengthBits=4, MinimumMatch=2
12-bit preset — 128-byte window, match lengths 2–33
Energy.Base.Compression.LZSS.Options options = new Energy.Base.Compression.LZSS.Options();
options.OffsetBits = 7;
options.LengthBits = 5;
options.MinimumMatch = 2;
16-bit preset (lz16) — 256-byte window, match lengths 1–256
Energy.Base.Compression.LZSS.Options options = new Energy.Base.Compression.LZSS.Options();
options.OffsetBits = 8;
options.LengthBits = 8;
options.MinimumMatch = 1;
Compress and decompress with an explicit preset:
Energy.Base.Compression.LZSS.Options options = new Energy.Base.Compression.LZSS.Options();
options.OffsetBits = 8;
options.LengthBits = 8;
options.MinimumMatch = 1;
byte[] compressed = Energy.Base.Compression.LZSS.Compress(data, options);
byte[] restored = Energy.Base.Compression.LZSS.Decompress(compressed, options);
SAP-R multi-channel container
The FrameSize option selects the interleaved multi-channel container layer that the Atari SAP-R format (dmsc/lzss-sap) adds around the per-stream codec. Setting FrameSize to N tells the codec that the input is a sequence of N-byte frames, where each byte position across all frames is an independent channel. The same Compress and Decompress methods handle both the single-stream and the container format; only the FrameSize field changes.
Container layout
For a frame size of N, the compressed stream is laid out as:
Channel-skip header. One flag bit per channel from N−1 down to 1, packed least-significant-bit first into whole bytes. A set bit marks a channel whose value never changes across the whole input; only its initial byte is stored and its tokens are omitted. Channel 0 is always active and has no bit. The partial header byte is padded and terminated before the next section.
Initial values. One raw byte per channel from N−1 down to 0: the value of that channel in frame 0. These are written as raw bytes after the header is flushed.
Token stream. For each frame position after frame 0, and for each active (non-skipped) channel from N−1 down to 0, one LZSS literal-or-match token is emitted into a single shared bit stream. A channel covered by an in-progress multi-frame match emits nothing for the positions that match spans; the next token for that channel appears only when the match ends.
The per-stream encoding (flag bits LSB-first, match position and length fields, literal-first byte) is identical to the single-stream path. The optimal parse is run per channel, independently.
Ring buffer model
Each channel keeps a ring buffer of 2^OffsetBits entries. Frame 0’s value is seeded at ring index maxOffset − 1, and the write cursor starts at 0 and advances by one per frame, wrapping modulo maxOffset. A match token names a start position and a copy count. With the default PositionStartZero=false, the first copied byte is at ring index (codePos + 1) & (maxOffset − 1). Each subsequent copy advances the index by one, so the match can reference the seeded initial value and can overlap its own output.
Decompress a SAP-R lz16 file
A SAP-R .lz16 file uses the 16-bit preset (8-bit offset, 8-bit length, minimum match 1) with 9 channels: AUDF1, AUDC1, AUDF2, AUDC2, AUDF3, AUDC3, AUDF4, AUDC4, and AUDCTL, played back at 50 Hz. To decompress:
Energy.Base.Compression.LZSS.Options options = new Energy.Base.Compression.LZSS.Options();
options.OffsetBits = 8;
options.LengthBits = 8;
options.MinimumMatch = 1;
options.FrameSize = 9;
byte[] lz16file = File.ReadAllBytes("song.lz16");
byte[] frames = Energy.Base.Compression.LZSS.Decompress(lz16file, options);
// frames.Length == numberOfFrames * 9
// frames[f * 9 + 0] == AUDF1 for frame f
// frames[f * 9 + 1] == AUDC1 for frame f
// ...
// frames[f * 9 + 8] == AUDCTL for frame f
Each group of 9 consecutive bytes in frames is one frame of POKEY register values in channel order 0 through 8.
Compress POKEY register data to lz16
The input must be a byte array whose length is a multiple of FrameSize. Each group of 9 bytes is one frame.
Energy.Base.Compression.LZSS.Options options = new Energy.Base.Compression.LZSS.Options();
options.OffsetBits = 8;
options.LengthBits = 8;
options.MinimumMatch = 1;
options.FrameSize = 9;
byte[] frames = File.ReadAllBytes("song.raw"); // length must be a multiple of 9
byte[] lz16file = Energy.Base.Compression.LZSS.Compress(frames, options);
File.WriteAllBytes("song.lz16", lz16file);
The compressed output is byte-for-byte identical to the file produced by the reference lzss-sap tool for the same input, including the channel-skip header, the initial bytes, and the interleaved token stream.
Constant channels (channel-skip)
Any channel from 1 upward whose value never changes across all frames is detected automatically and stored only as its initial byte. Its bit in the skip header is set to 1 and no tokens are emitted for it. This is transparent to the caller: Decompress reconstructs the constant value for every frame from the stored initial byte.
Example: if AUDCTL (channel 8) is 0x00 throughout the recording, the compressor sets bit 7 of the skip header and the decompressor fills all AUDCTL bytes with 0x00 without reading any tokens.
// After decompression, read channel 8 (AUDCTL) for all frames:
for (int f = 0; f < frames.Length / 9; f++)
{
byte audctl = frames[f * 9 + 8];
// audctl is correct regardless of whether the channel was skipped
}
Round-trip verification
Energy.Base.Compression.LZSS.Options options = new Energy.Base.Compression.LZSS.Options();
options.OffsetBits = 8;
options.LengthBits = 8;
options.MinimumMatch = 1;
options.FrameSize = 9;
byte[] original = File.ReadAllBytes("song.lz16");
byte[] decompressed = Energy.Base.Compression.LZSS.Decompress(original, options);
byte[] recompressed = Energy.Base.Compression.LZSS.Compress(decompressed, options);
// recompressed is byte-for-byte identical to original
bool roundTrip = (recompressed != null && recompressed.Length == original.Length);
for (int i = 0; i < original.Length && roundTrip; i++)
roundTrip = (recompressed[i] == original[i]);
Accessing individual channels after decompression
int channelCount = 9;
int frameCount = decompressed.Length / channelCount;
byte[] audf1 = new byte[frameCount];
byte[] audc1 = new byte[frameCount];
for (int f = 0; f < frameCount; f++)
{
audf1[f] = decompressed[f * channelCount + 0];
audc1[f] = decompressed[f * channelCount + 1];
}
Using FrameSize with non-POKEY data
FrameSize is not specific to POKEY or 9 channels. Any data organised as fixed-size frames of independent sub-streams benefits from the same container. For example, 4-channel sensor readings sampled at a fixed rate:
Energy.Base.Compression.LZSS.Options options = new Energy.Base.Compression.LZSS.Options();
options.OffsetBits = 8;
options.LengthBits = 8;
options.MinimumMatch = 1;
options.FrameSize = 4; // 4 bytes per frame = 4 channels
byte[] sensorFrames = BuildSensorFrames(); // length must be a multiple of 4
byte[] compressed = Energy.Base.Compression.LZSS.Compress(sensorFrames, options);
byte[] restored = Energy.Base.Compression.LZSS.Decompress(compressed, options);
The multi-channel container requires LiteralFirst = true (the default). FrameSize must be 0 or greater; a negative value returns null.
Note on POKEY register normalisation
The reference lzss-sap tool rewrites certain AUDC register values to a canonical form while reading a raw POKEY dump before it compresses. This codec does not perform that normalisation: it treats whatever bytes it receives as already canonical. A decode-then-re-encode round trip is always byte-identical because the decoded data is already canonical. Callers who compress a raw POKEY dump directly (rather than a file previously decoded from lz16) must apply any normalisation themselves before calling Compress.
Choosing a codec:
Use Deflate or GZip for general-purpose compression where a standard format is expected.
Use LZ4 when decompression speed matters more than ratio.
Use ZX0 for the smallest output and a tiny decompressor, accepting a slower compression step.
Use LZSS for compatibility with the SAP-R lzss-sap format, or for a small, fast, tunable LZ77 codec for log-style data.