1
1
mirror of https://github.com/wader/fq.git synced 2024-12-01 19:12:34 +03:00
fq/format/tzx/tzx.go
2024-08-18 19:45:02 +02:00

660 lines
22 KiB
Go

package tzx
// https://worldofspectrum.net/TZXformat.html
import (
"embed"
"golang.org/x/text/encoding/charmap"
"github.com/wader/fq/format"
"github.com/wader/fq/pkg/decode"
"github.com/wader/fq/pkg/interp"
"github.com/wader/fq/pkg/scalar"
)
//go:embed tzx.md
var tzxFS embed.FS
var tapFormat decode.Group
func init() {
interp.RegisterFormat(
format.TZX,
&decode.Format{
Description: "TZX tape format for ZX Spectrum computers",
Groups: []*decode.Group{format.Probe},
DecodeFn: tzxDecode,
Dependencies: []decode.Dependency{
{Groups: []*decode.Group{format.TAP}, Out: &tapFormat},
},
})
interp.RegisterFS(tzxFS)
}
func tzxDecode(d *decode.D) any {
d.Endian = decode.LittleEndian
d.FieldRawLen("signature", 8*8, d.AssertBitBuf([]byte("ZXTape!\x1A")))
d.FieldU8("major_version")
d.FieldU8("minor_version")
decodeBlocks(d)
return nil
}
func decodeBlocks(d *decode.D) {
d.FieldArray("blocks", func(d *decode.D) {
for !d.End() {
d.FieldStruct("block", func(d *decode.D) {
decodeBlock(d)
})
}
})
}
func decodeBlock(d *decode.D) {
blocks := map[uint64]func(d *decode.D){
// ID: 10h (16d) | Standard Speed Data
// This block is replayed with the standard Spectrum ROM timing values
// (the values in curly brackets in block ID 11). The pilot tone
// consists of 8063 pulses if the first data byte (the flag byte)
// is < 128, 3223 otherwise.
0x10: func(d *decode.D) {
// Pause after this block (ms.) {1000}
d.FieldU16("pause")
// A single TAP Data Block
peekBytes := d.PeekBytes(2) // get the TAP data block length
length := uint16(peekBytes[1])<<8 | uint16(peekBytes[0]) // bytes are stored in LittleEndian
length += 2 // include the two bytes for this value
d.FieldFormatLen("tap", int64(length)*8, &tapFormat, nil)
},
// ID: 11h (17d) | Turbo Speed Data
// This block is very similar to the normal TAP block but with some
// additional info on the timings and other important differences. The
// same tape encoding is used as for the standard speed data block. If
// a block should use some non-standard sync or pilot tones (i.e. all
// sorts of protection schemes) then the next three blocks describe it.
0x11: func(d *decode.D) {
d.FieldU16("pilot_pulse") // Length of PILOT pulse {2168}
d.FieldU16("sync_pulse_1") // Length of SYNC first pulse {667}
d.FieldU16("sync_pulse_2") // Length of SYNC second pulse {735}
d.FieldU16("bit0_pulse") // Length of ZERO bit pulse {855}
d.FieldU16("bit1_pulse") // Length of ONE bit pulse {1710}
// Length of PILOT tone (number of pulses)
// {8063 header (flag<128), 3223 data (flag>=128)}
d.FieldU16("pilot_tone")
// Used bits in the last byte (other bits should be 0) {8}
// e.g. if this is 6, then the bits used (x) in the last byte are: xxxxxx00,
// where MSb is the leftmost bit, LSb is the rightmost bit
d.FieldU8("used_bits")
d.FieldU16("pause") // Pause after this block (ms.) {1000}
length := d.FieldU24("length") // Length of data that follows
// Data as in .TAP files
d.FieldRawLen("data", int64(length)*8)
},
// ID: 12h (18d) | Pure Tone
// This will produce a tone which is basically the same as the pilot
// tone in 10h and 11h blocks.
0x12: func(d *decode.D) {
d.FieldU16("pulse_length") // Length of one pulse in T-states
d.FieldU16("pulse_count") // Number of pulses
},
// ID: 13h (19d) | Sequence of Pulses
// This will produce N pulses, each having its own timing. Up to 255
// pulses can be stored in this block.
0x13: func(d *decode.D) {
count := d.FieldU8("pulse_count")
d.FieldArray("pulses", func(d *decode.D) {
for i := uint64(0); i < count; i++ {
d.FieldU16("pulse")
}
})
},
// ID: 14h (20d) | Pure Data
// This is the same as in the turbo loading data block, except that it
// has no pilot or sync pulses.
0x14: func(d *decode.D) {
d.FieldU16("bit0_pulse") // Length of ZERO bit pulse
d.FieldU16("bit1_pulse") // Length of ONE bit pulse
d.FieldU8("used_bits") // Used bits in last byte
d.FieldU16("pause") // Pause after this block (ms.)
length := d.FieldU24("length") // Length of data that follows
// Data as in .TAP files
d.FieldRawLen("data", int64(length)*8)
},
// ID: 15h (21d) | Direct Recording
// This block is used for tapes which have some parts in a format such
// that the turbo loader block cannot be used. This is not like a VOC
// file since the information is much more compact. Each sample value
// is represented by one bit only (0 for low, 1 for high) which means
// that the block will be at most 1/8 the size of the equivalent VOC.
// The preferred sampling frequencies are 22050 or 44100 Hz
// (158 or 79 T-states/sample).
0x15: func(d *decode.D) {
d.FieldU16("t_states") // Number of T-states per sample (bit of data)
d.FieldU16("pause") // Pause after this block in milliseconds (ms.)
d.FieldU8("used_bits") // Used bits (samples) in last byte of data (1-8)
length := d.FieldU24("length") // Length of data that follows
d.FieldRawLen("data", int64(length)*8) // Samples data. Each bit represents a state on the EAR port
},
// ID: 18h (24d) | CSW Recording
// This block contains a sequence of raw pulses encoded in CSW format
// v2 (Compressed Square Wave).
0x18: func(d *decode.D) {
length := d.FieldU32("length") // Block length (without these four bytes)
// NOTE: remove these next 4 fields from the length so
// the data size is calculated correctly
length -= 2 + 3 + 1 + 4
// Pause after this block (in ms)
d.FieldU16("pause")
// Sampling rate
d.FieldU24("sample_rate")
// Compression type
d.FieldU8("compression_type", scalar.UintMapSymStr{0x00: "unknown", 0x01: "rle", 0x02: "zrle"})
// Number of stored pulses (after decompression)
d.FieldU32("stored_pulse_count")
// CSW data, encoded according to the CSW specification
d.FieldRawLen("data", int64(length)*8)
},
// ID: 19h (25d) | Generalized Data
// This block was developed to represent an extremely wide range of data
// encoding techniques. Each loading component (pilot tone, sync pulses,
// data) is associated to a specific sequence of pulses, where each
// sequence (wave) can contain a different number of pulses from the
// others. In this way it is possible to have a situation where bit 0 is
// represented with 4 pulses and bit 1 with 8 pulses.
0x19: func(d *decode.D) {
length := d.FieldU32("length") // Block length (without these four bytes)
// TBD:
// Pause uint16 // Pause after this block (ms)
// TOTP uint32 // Total number of symbols in pilot/sync block (can be 0)
// NPP uint8 // Maximum number of pulses per pilot/sync symbol
// ASP uint8 // Number of pilot/sync symbols in the alphabet table (0=256)
// TOTD uint32 // Total number of symbols in data stream (can be 0)
// NPD uint8 // Maximum number of pulses per data symbol
// ASD uint8 // Number of data symbols in the alphabet table (0=256)
// PilotSymbols []Symbol // 0x12 SYMDEF[ASP] Pilot and sync symbols definition table
// PilotStreams []PilotRLE // 0x12+ (2*NPP+1)*ASP - PRLE[TOTP] Pilot and sync data stream
// DataSymbols []Symbol // 0x12+ (TOTP>0)*((2*NPP+1)*ASP)+TOTP*3 - SYMDEF[ASD] Data symbols definition table
// DataStreams []uint8 // 0x12+ (TOTP>0)*((2*NPP+1)*ASP)+ TOTP*3+(2*NPD+1)*ASD - BYTE[DS] Data stream
d.FieldRawLen("data", int64(length)*8)
},
// ID: 20h (32d) | Pause Tape Command
// This will make a silence (low amplitude level (0)) for a given time
// in milliseconds. If the value is 0 then the emulator or utility should
// (in effect) STOP THE TAPE, until the user or emulator requests it.
0x20: func(d *decode.D) {
d.FieldU16("pause") // Pause duration in ms.
},
// ID: 21h (33d) | Group Start
// This block marks the start of a group of blocks which are to be
// treated as one single (composite) block. For each group start block
// there must be a group end block. Nesting of groups is not allowed.
0x21: func(d *decode.D) {
length := d.FieldU8("length")
d.FieldStr("group_name", int(length), charmap.ISO8859_1)
},
// ID: 22h (34d) | Group End
// This indicates the end of a group. This block has no body.
0x22: func(d *decode.D) {},
// JumpTo
// ID: 23h (35d)
// This block will allow for jumping from one block to another within
// the file. All blocks are included in the block count!
0x23: func(d *decode.D) {
d.FieldS16("value", scalar.SintMapSymStr{
0: "loop_forever",
1: "next_block",
2: "skip_block",
-1: "prev_block",
})
},
// ID: 24h (36d) | Loop Start
// Indicates a sequence of identical blocks, or of identical groups of
// blocks. This block is the same as the FOR statement in BASIC.
0x24: func(d *decode.D) {
d.FieldU16("repetitions") // Number of repetitions (greater than 1)
},
// ID: 25h (37d) | Loop End
// This is the same as BASIC's NEXT statement. It means that the utility
// should jump back to the start of the loop if it hasn't been run for
// the specified number of times. This block has no body.
0x25: func(d *decode.D) {},
// ID: 26h (38d) | Call Sequence
// This block is an analogue of the CALL Subroutine statement. It
// basically executes a sequence of blocks that are somewhere else and
// then goes back to the next block. Because more than one call can be
// normally used you can include a list of sequences to be called. CALL
// blocks can be used in the LOOP sequences and vice versa. The value
// is relative so that you can add some blocks in the beginning of the
// file without disturbing the call values.
// Look at 'Jump To Block' for reference on the values.
0x26: func(d *decode.D) {
count := d.FieldU16("count")
d.FieldArray("call_blocks", func(d *decode.D) {
for i := uint64(0); i < count; i++ {
d.FieldS16("offset")
}
})
},
// ID: 27h (39d) | Return From Sequence
// This block indicates the end of the Called Sequence. The next block
// played will be the block after the last CALL block (or the next Call,
// if the Call block had multiple calls). This block has no body.
0x27: func(d *decode.D) {},
// ID: 28h (40d) | Select
// This block is useful when the tape consists of two or more separately
// loadable parts. With this block it is possible to select one of the
// parts and the utility/emulator will start loading from that block.
// All offsets are relative signed words.
0x28: func(d *decode.D) {
// Length of the whole block (without these two bytes)
d.FieldU16("length")
count := d.FieldU8("count")
d.FieldArray("selections", func(d *decode.D) {
for i := 0; i < int(count); i++ {
d.FieldStruct("selection", func(d *decode.D) {
d.FieldS16("offset") // Relative Offset as `signed` value
length := d.FieldU8("length") // Length of description text (max 30 chars)
d.FieldStr("description", int(length), charmap.ISO8859_1)
})
}
})
},
// ID: 2Ah (42d) | Stop Tape When 48k Mode
// When this block is encountered, the tape will stop ONLY if the machine
// is an 48K Spectrum. This block is to be used for multi-loading games
// that load one level at a time in 48K mode, but load the entire tape at
// once if in 128K mode.
// This block has no body of its own, but follows the extension rule.
0x2A: func(d *decode.D) {
d.FieldU32("length") // Length of the block without these four bytes (0)
},
// ID: 2Bh (43d) | Set Signal Level
// This block sets the current signal level to the specified value
// (high or low). It should be used whenever it is necessary to avoid
// any ambiguities, e.g. with custom loaders which are level-sensitive.
0x2B: func(d *decode.D) {
d.FieldU32("length") // Block length (without these four bytes)
d.FieldU8("signal_level", scalar.UintMapSymStr{0: "low", 1: "high"})
},
// ID: 30h (48d) | Text Description
// This is meant to identify parts of the tape, such as where level 1
// starts, where to rewind to when the game ends, etc. This description
// is not guaranteed to be shown while the tape is playing, but can be
// read while browsing the tape or changing the tape pointer.
// The description can be up to 255 characters long.
0x30: func(d *decode.D) {
length := d.FieldU8("length")
d.FieldStr("description", int(length), charmap.ISO8859_1)
},
// ID: 31h (49d) | Message
// This will enable the emulators to display a message for a given time.
// This should not stop the tape and it should not make silence. If the
// time is 0 then the emulator should wait for the user to press a key.
0x31: func(d *decode.D) {
// Time (in seconds) for which the message should be displayed
d.FieldU8("display_time")
// Length of the text message
length := d.FieldU8("length")
// Message that should be displayed in ASCII format
d.FieldStr("message", int(length), charmap.ISO8859_1)
},
// ID: 32h (50d) | Archive Info
// This optional block is used at the beginning of the tape containing
// various metadata about the tape.
0x32: func(d *decode.D) {
d.FieldU16("length") // Length of the whole block without these two bytes
count := d.FieldU8("count") // Number of entries in the archive info
// the archive strings
d.FieldArray("entries", func(d *decode.D) {
for i := uint64(0); i < count; i++ {
d.FieldStruct("entry", func(d *decode.D) {
d.FieldU8("id", scalar.UintMapSymStr{
0x00: "title",
0x01: "publisher",
0x02: "author",
0x03: "year",
0x04: "language",
0x05: "category",
0x06: "price",
0x07: "loader",
0x08: "origin",
0xFF: "comment",
})
length := d.FieldU8("length")
d.FieldStr("value", int(length), charmap.ISO8859_1)
})
}
})
},
// ID: 33h (51d) | Hardware Type
// This blocks contains information about the hardware that the programs
// on this tape use.
0x33: func(d *decode.D) {
// Number of machines and hardware types for which info is supplied
count := d.FieldU8("count")
d.FieldArray("hardware_info", func(d *decode.D) {
for i := uint64(0); i < count; i++ {
d.FieldStruct("info", func(d *decode.D) {
// Hardware Type ID (computers, printers, mice, etc.)
typeId := d.FieldU8("type_id", hwInfoTypes)
// Hardware Device ID (ZX81, Kempston Joystick, etc.)
d.FieldU8("device_id", hwInfoDevices[typeId])
// Hardware compatibility information
d.FieldU8("info_id", hwInfoCompatibilityInfo)
})
}
})
},
// ID: 35h (53d) | Custom Info
// This block contains various custom data. For example, it might contain
// some information written by a utility, extra settings required by a
// particular emulator, etc.
0x35: func(d *decode.D) {
d.FieldStr("identification", 16, charmap.ISO8859_1)
length := d.FieldU32("length")
d.FieldRawLen("info", int64(length)*8)
},
// ID: 5Ah (90d) | Glue Block
// This block is generated when two ZX Tape files are merged together.
// It is here so that you can easily copy the files together and use
// them. Of course, this means that resulting file would be 10 bytes
// longer than if this block was not used. All you have to do if you
// encounter this block ID is to skip next 9 bytes. If you can avoid
// using this block for this purpose, then do so; it is preferable to
// use a utility to join the two files and ensure that they are both
// of the higher version number.
0x5A: func(d *decode.D) {
// Value: { "XTape!",0x1A,MajR,MinR }
// Just skip these 9 bytes and you will end up on the next ID.
d.FieldRawLen("value", 9*8)
},
}
blockType := d.PeekUintBits(8)
// Deprecated block types: C64RomType, C64TurboData, EmulationInfo, Snapshot
if blockType == 0x16 || blockType == 0x17 || blockType == 0x34 || blockType == 0x40 {
d.Fatalf("deprecated block type encountered: %02x", blockType)
}
blockLabel := blockTypeMapper[blockType]
d.FieldStruct(blockLabel, func(d *decode.D) {
d.FieldU8("type", blockTypeMapper)
if fn, ok := blocks[blockType]; ok {
fn(d)
} else {
d.Fatalf("block type not valid, got: %02x", blockType)
}
})
}
var blockTypeMapper = scalar.UintMapSymStr{
0x10: "standard_speed_data",
0x11: "turbo_speed_data",
0x12: "pure_tone",
0x13: "sequence_of_pulses",
0x14: "pure_data",
0x15: "direct_recording", // deprecated
0x16: "c64_rom_type", // deprecated
0x17: "c64_turbo_data",
0x18: "csw_recording",
0x19: "generalized_data",
0x20: "pause_tape_command",
0x21: "group_start",
0x22: "group_end",
0x23: "jump_to",
0x24: "loop_start",
0x25: "loop_end",
0x26: "call_sequence",
0x27: "return_from_sequence",
0x28: "select",
0x2A: "stop_tape_when_48k_mode",
0x2B: "set_signal_level",
0x30: "text_description",
0x31: "message",
0x32: "archive_info",
0x33: "hardware_type",
0x34: "emulation_info", // deprecated
0x35: "custom_info",
0x40: "snapshot", // deprecated
0x5A: "glue_block",
}
var hwInfoTypes = scalar.UintMapDescription{
0x00: "Computers",
0x01: "External storage",
0x02: "ROM/RAM type add-ons",
0x03: "Sound devices",
0x04: "Joysticks",
0x05: "Mice",
0x06: "Other controllers",
0x07: "Serial ports",
0x08: "Parallel ports",
0x09: "Printers",
0x0a: "Modems",
0x0b: "Digitizers",
0x0c: "Network adapters",
0x0d: "Keyboards & keypads",
0x0e: "AD/DA converters",
0x0f: "EPROM programmers",
0x10: "Graphics",
}
var hwInfoDevices = map[uint64]scalar.UintMapDescription{
0x00: { // Computers
0x00: "ZX Spectrum 16k",
0x01: "ZX Spectrum 48k, Plus",
0x02: "ZX Spectrum 48k ISSUE 1",
0x03: "ZX Spectrum 128k +(Sinclair)",
0x04: "ZX Spectrum 128k +2 (grey case)",
0x05: "ZX Spectrum 128k +2A, +3",
0x06: "Timex Sinclair TC-2048",
0x07: "Timex Sinclair TS-2068",
0x08: "Pentagon 128",
0x09: "Sam Coupe",
0x0a: "Didaktik M",
0x0b: "Didaktik Gama",
0x0c: "ZX-80",
0x0d: "ZX-81",
0x0e: "ZX Spectrum 128k, Spanish version",
0x0f: "ZX Spectrum, Arabic version",
0x10: "Microdigital TK 90-X",
0x11: "Microdigital TK 95",
0x12: "Byte",
0x13: "Elwro 800-3 ",
0x14: "ZS Scorpion 256",
0x15: "Amstrad CPC 464",
0x16: "Amstrad CPC 664",
0x17: "Amstrad CPC 6128",
0x18: "Amstrad CPC 464+",
0x19: "Amstrad CPC 6128+",
0x1a: "Jupiter ACE",
0x1b: "Enterprise",
0x1c: "Commodore 64",
0x1d: "Commodore 128",
0x1e: "Inves Spectrum+",
0x1f: "Profi",
0x20: "GrandRomMax",
0x21: "Kay 1024",
0x22: "Ice Felix HC 91",
0x23: "Ice Felix HC 2000",
0x24: "Amaterske RADIO Mistrum",
0x25: "Quorum 128",
0x26: "MicroART ATM",
0x27: "MicroART ATM Turbo 2",
0x28: "Chrome",
0x29: "ZX Badaloc",
0x2a: "TS-1500",
0x2b: "Lambda",
0x2c: "TK-65",
0x2d: "ZX-97",
},
0x01: { // External storage
0x00: "ZX Microdrive",
0x01: "Opus Discovery",
0x02: "MGT Disciple",
0x03: "MGT Plus-D",
0x04: "Rotronics Wafadrive",
0x05: "TR-DOS (BetaDisk)",
0x06: "Byte Drive",
0x07: "Watsford",
0x08: "FIZ",
0x09: "Radofin",
0x0a: "Didaktik disk drives",
0x0b: "BS-DOS (MB-02)",
0x0c: "ZX Spectrum +3 disk drive",
0x0d: "JLO (Oliger) disk interface",
0x0e: "Timex FDD3000",
0x0f: "Zebra disk drive",
0x10: "Ramex Millennia",
0x11: "Larken",
0x12: "Kempston disk interface",
0x13: "Sandy",
0x14: "ZX Spectrum +3e hard disk",
0x15: "ZXATASP",
0x16: "DivIDE",
0x17: "ZXCF",
},
0x02: { // ROM/RAM type add_ons
0x00: "Sam Ram",
0x01: "Multiface ONE",
0x02: "Multiface 128k",
0x03: "Multiface +3",
0x04: "MultiPrint",
0x05: "MB-02 ROM/RAM expansion",
0x06: "SoftROM",
0x07: "1k",
0x08: "16k",
0x09: "48k",
0x0a: "Memory in 8-16k used",
},
0x03: { // Sound devices
0x00: "Classic AY hardware (compatible with 128k ZXs)",
0x01: "Fuller Box AY sound hardware",
0x02: "Currah microSpeech",
0x03: "SpecDrum",
0x04: "AY ACB stereo (A+C=left, B+C=right); Melodik",
0x05: "AY ABC stereo (A+B=left, B+C=right)",
0x06: "RAM Music Machine",
0x07: "Covox",
0x08: "General Sound",
0x09: "Intec Electronics Digital Interface B8001",
0x0a: "Zon-X AY",
0x0b: "QuickSilva AY",
0x0c: "Jupiter ACE",
},
0x04: { // Joysticks
0x00: "Kempston",
0x01: "Cursor, Protek, AGF",
0x02: "Sinclair 2 Left (12345)",
0x03: "Sinclair 1 Right (67890)",
0x04: "Fuller",
},
0x05: { // Mice
0x00: "AMX mouse",
0x01: "Kempston mouse",
},
0x06: { // Other controllers
0x00: "Trickstick",
0x01: "ZX Light Gun",
0x02: "Zebra Graphics Tablet",
0x03: "Defender Light Gun",
},
0x07: { // Serial ports
0x00: "ZX Interface 1",
0x01: "ZX Spectrum 128k",
},
0x08: { // Parallel ports
0x00: "Kempston S",
0x01: "Kempston E",
0x02: "ZX Spectrum +3",
0x03: "Tasman",
0x04: "DK'Tronics",
0x05: "Hilderbay",
0x06: "INES Printerface",
0x07: "ZX LPrint Interface 3",
0x08: "MultiPrint",
0x09: "Opus Discovery",
0x0a: "Standard 8255 chip with ports 31,63,95",
},
0x09: { // Printers
0x00: "ZX Printer, Alphacom 32 & compatibles",
0x01: "Generic printer",
0x02: "EPSON compatible",
},
0x0a: { // Modems
0x00: "Prism VTX 5000",
0x01: "T/S 2050 or Westridge 2050",
},
0x0b: { // Digitizers
0x00: "RD Digital Tracer",
0x01: "DK'Tronics Light Pen",
0x02: "British MicroGraph Pad",
0x03: "Romantic Robot Videoface",
},
0x0c: { // Network adapters
0x00: "ZX Interface 1",
},
0x0d: { // Keyboards & keypads
0x00: "Keypad for ZX Spectrum 128k",
},
0x0e: { // AD/DA converters
0x00: "Harley Systems ADC 8.2",
0x01: "Blackboard Electronics",
},
0x0f: { // EPROM programmers
0x00: "Orme Electronics",
},
0x10: { // Graphics
0x00: "WRX Hi-Res",
0x01: "G007",
0x02: "Memotech",
0x03: "Lambda Colour",
},
}
var hwInfoCompatibilityInfo = scalar.UintMapDescription{
00: "RUNS on this machine or with this hardware, but may or may not use the hardware or special features of the machine.",
01: "USES the hardware or special features of the machine, such as extra memory or a sound chip.",
02: "RUNS but it DOESN'T use the hardware or special features of the machine.",
03: "DOESN'T RUN on this machine or with this hardware.",
}