Creating TZX files in Python

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.

OffsetValueTypeDescription
0x00"ZXTape!"ASCIITZX File Signature
0x070x1AByteEnd of text marker
0x080x01ByteTZX Major Revision
0x090x14ByteTZX Minor Revision
A typical ZX Spectrum TZX header block

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.

OffsetValueTypeDescription
0x000x10ByteTZX Block ID
0x01'\xE8\x03'2 BytesPause after block (in milliseconds)
0x03'\x3B\x00'2 BytesLength of data block (starting at 0x05)
0x050x00ByteData 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
0x3F0x6BByteXOR checksum
An example of a SpecDrum Names block

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.

OffsetValueTypeDescription
0x000x10ByteTZX Block ID
0x01'\xE8\x03'2 BytesPause after block (in milliseconds)
0x03'\x02\x54'2 BytesLength of data block (starting at 0x05)
0x050xFFByteData block flag
0x06'\xFF\x02\x04...'21504 BytesAudio block (signed 8 bit samples)
0x54060x3BByteXOR checksum
An example of a SpecDrum Audio block

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.

One response to “Creating TZX files in Python”

  1. […] a program to create SpecDrum TZX files myself. I have documented how TZX files can be constructed here, and developed that idea into a command line application that makes the process […]

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.