It’s been just shy of 4 years since I published my post on how to load custom samples into the Cheetah SpecDrum software. At some point in that time the software I used to create my TZX files has disappeared from the internet, so I figured it’s time I learned how TZX files work (or at least well enough to get samples into my SpecDrum).
Headers Up
All Spectrum TZX files begin with a 10 byte header block that contains a file signature (“ZXTape!” in ASCII), a stop byte and the major and minor file specification revision numbers. The current specification is 1.20.
Offset | Value | Type | Description |
---|---|---|---|
0x00 | "ZXTape!" | ASCII | TZX File Signature |
0x07 | 0x1A | Byte | End of text marker |
0x08 | 0x01 | Byte | TZX Major Revision |
0x09 | 0x14 | Byte | TZX Minor Revision |
This header can be represented by a packed hex string, as represented below
b'\x5A\x58\x54\x61\x70\x65\x21\x1A\x01\x14'
Data Block – Names
The SpecDrum software uses two standard speed data blocks to hold the names of the drums and the audio data. Standard speed data blocks begin with 0x10.
The first block contains the names of the drums, and apart from the TZX block information it is composed entirely of ASCII text.
Bytes 0x01 and 0x02 contain the length in milliseconds that will play after the block is finished playing. In the example below it is 1000 ms (0x03E8). Note that all TZX values that are more than one byte long are stored least significant byte (LSB) first. This doesn’t apply to application data.
Bytes 0x03 and 0x04 contain the length of the data portion of the block in bytes. This length parameter ignores the ID byte, the pause length bytes and length bytes, but it does include the checksum byte.
Byte 0x05 is not part of the TZX specification, but many applications set it to 0x00 to indicate a pseudo-header block.
Byte 0x06 is the beginning of the name data. SpecDrum expects the first byte of the names string to be 0x63, or “c” in ASCII.
Bytes 0x07 to 0x3E store the names of the drums. The SpecDrum software can use characters A-Z (capitals) numbers 0-9 and spaces, but does no support special characters. Each drum name is comprised of exactly 7 ASCII characters.
The final byte is a checksum byte that is calculated by bitwise XORing each byte together, starting after the length bytes (0x05) and continuing until the end of the block. The resulting byte is appended to the end of the block as the last byte.
Offset | Value | Type | Description |
---|---|---|---|
0x00 | 0x10 | Byte | TZX Block ID |
0x01 | '\xE8\x03' | 2 Bytes | Pause after block (in milliseconds) |
0x03 | '\x3B\x00' | 2 Bytes | Length of data block (starting at 0x05) |
0x05 | 0x00 | Byte | Data block flag |
0x06 | "c" | ASCII (1 byte) | Beginning of name data |
0x07 | "DRUM 1 " | ASCII (7 bytes) | 7 character drum name |
0x0E | "DRUM 2 " | ASCII (7 bytes) | 7 character drum name |
0x15 | "DRUM 3 " | ASCII (7 bytes) | 7 character drum name |
0x1C | "DRUM 4 " | ASCII (7 bytes) | 7 character drum name |
0x23 | "DRUM 5 " | ASCII (7 bytes) | 7 character drum name |
0x2A | "DRUM 6 " | ASCII (7 bytes) | 7 character drum name |
0x31 | "DRUM 7 " | ASCII (7 bytes) | 7 character drum name |
0x38 | "DRUM 8 " | ASCII (7 bytes) | 7 character drum name |
0x3F | 0x6B | Byte | XOR checksum |
Data Block – Audio
The audio block is broadly the same as the names block. The first difference is that the flag bit is set to 0xFF instead of 0x00, which unofficially indicates that this is a standard data block.
Offset | Value | Type | Description |
---|---|---|---|
0x00 | 0x10 | Byte | TZX Block ID |
0x01 | '\xE8\x03' | 2 Bytes | Pause after block (in milliseconds) |
0x03 | '\x02\x54' | 2 Bytes | Length of data block (starting at 0x05) |
0x05 | 0xFF | Byte | Data block flag |
0x06 | '\xFF\x02\x04...' | 21504 Bytes | Audio block (signed 8 bit samples) |
0x5406 | 0x3B | Byte | XOR checksum |
Packing Bits
With the theory out of the way, here is how to put it into practice using a short Python program.
import struct
drum_names = ["DRUM 1 ", "DRUM 2 ","DRUM 3 ","DRUM 4 ","DRUM 5 ","DRUM 6 ","DRUM 7 ","DRUM 8 "]
tzxheader = b'\x5A\x58\x54\x61\x70\x65\x21\x1A\x01\x0D'
drum_names_data = bytearray()
audio_block_data = bytearray()
pause_after_block = 1000 #milliseconds pause between blocks
def calculate_checksum(block):
checksum = 0
for i in block:
checksum ^= i
return checksum
#Create Names Block
# Add flag byte of 0x00
drum_names_data.insert(0, 0x00)
# Add name block start byte (0x63)
drum_names_data.extend('c'.encode('iso-8859-1'))
# Add each drum name to the data block
for i in drum_names:
drum_names_data.extend(i.encode('iso-8859-1'))
# Calculate and add checksum
drum_names_data.append(calculate_checksum(drum_names_data))
# Construct the header block
drum_names_header = struct.pack('<Bhh', 0x10, pause_after_block, len(drum_names_data))
# Create Audio block
# Add flag byte of 0xFF
audio_block_data.insert(0, 0xFF)
# Open audio file and append it to the bytearray
with open("audio_block.bin","rb") as file:
audio_block_data.extend(file.read())
# Calculate and add checksum
audio_block_data.append(calculate_checksum(audio_block_data))
# Construct the header block
audio_block_header = struct.pack('<Bhh', 0x10, pause_after_block, len(audio_block_data))
with open("kit.tzx", "wb") as output_file:
output_file.write(tzxheader)
output_file.write(drum_names_header)
output_file.write(drum_names_data)
output_file.write(audio_block_header)
output_file.write(audio_block_data)
The program can be broken down into 3 sections. From line 1 to line 13 is where the program is set up. The struct module is imported and variables and functions are declared. Lines 15 to 38 are where the Names and Audio blocks are constructed. Finally, lines 40 to 45 are where the TZX file is created and the blocks are written to it.
If you place a valid audio block in the same directory as this script and run it with Python then a valid SpecDrum TZX file will be created. It does no validation on the names or audio block, but if you want those niceties and others then check out my Github repo for a more fleshed out version of the above program. I have also packaged it to an exe and uploaded it to the releases page.
Leave a Reply