diff --git a/README.md b/README.md index 4bb4639b..2e7899a7 100644 --- a/README.md +++ b/README.md @@ -141,12 +141,14 @@ pssh_playready, [rtmp](doc/formats.md#rtmp), sll2_packet, sll_packet, +[tap](doc/formats.md#tap), tar, tcp_segment, tiff, [tls](doc/formats.md#tls), toml, [tzif](doc/formats.md#tzif), +[tzx](doc/formats.md#tzx), udp_datagram, vorbis_comment, vorbis_packet, diff --git a/doc/formats.md b/doc/formats.md index 448dab81..80b7aea4 100644 --- a/doc/formats.md +++ b/doc/formats.md @@ -113,12 +113,14 @@ |[`rtmp`](#rtmp) |Real-Time Messaging Protocol |`amf0` `mpeg_asc`| |`sll2_packet` |Linux cooked capture encapsulation v2 |`inet_packet`| |`sll_packet` |Linux cooked capture encapsulation |`inet_packet`| +|[`tap`](#tap) |TAP tape format for ZX Spectrum computers || |`tar` |Tar archive |`probe`| |`tcp_segment` |Transmission control protocol segment || |`tiff` |Tag Image File Format |`icc_profile`| |[`tls`](#tls) |Transport layer security |`asn1_ber`| |`toml` |Tom's Obvious, Minimal Language || |[`tzif`](#tzif) |Time Zone Information Format || +|[`tzx`](#tzx) |TZX tape format for ZX Spectrum computers |`tap`| |`udp_datagram` |User datagram protocol |`udp_payload`| |`vorbis_comment` |Vorbis comment |`flac_picture`| |`vorbis_packet` |Vorbis packet |`vorbis_comment`| @@ -137,7 +139,7 @@ |`ip_packet` |Group |`icmp` `icmpv6` `tcp_segment` `udp_datagram`| |`link_frame` |Group |`bsd_loopback_frame` `ether8023_frame` `ipv4_packet` `ipv6_packet` `sll2_packet` `sll_packet`| |`mp3_frame_tags` |Group |`mp3_frame_vbri` `mp3_frame_xing`| -|`probe` |Group |`adts` `aiff` `apple_bookmark` `ar` `avi` `avro_ocf` `bitcoin_blkdat` `bplist` `bzip2` `caff` `elf` `fit` `flac` `gif` `gzip` `html` `jp2c` `jpeg` `json` `jsonl` `leveldb_table` `luajit` `macho` `macho_fat` `matroska` `moc3` `mp3` `mp4` `mpeg_ts` `nes` `ogg` `opentimestamps` `pcap` `pcapng` `png` `tar` `tiff` `toml` `tzif` `wasm` `wav` `webp` `xml` `yaml` `zip`| +|`probe` |Group |`adts` `aiff` `apple_bookmark` `ar` `avi` `avro_ocf` `bitcoin_blkdat` `bplist` `bzip2` `caff` `elf` `fit` `flac` `gif` `gzip` `html` `jp2c` `jpeg` `json` `jsonl` `leveldb_table` `luajit` `macho` `macho_fat` `matroska` `moc3` `mp3` `mp4` `mpeg_ts` `nes` `ogg` `opentimestamps` `pcap` `pcapng` `png` `tar` `tiff` `toml` `tzif` `tzx` `wasm` `wav` `webp` `xml` `yaml` `zip`| |`tcp_stream` |Group |`dns_tcp` `rtmp` `tls`| |`udp_payload` |Group |`dns`| @@ -1209,6 +1211,26 @@ fq '.tcp_connections[] | select(.server.port=="rtmp") | d' file.cap - https://rtmp.veriskope.com/docs/spec/ - https://rtmp.veriskope.com/pdf/video_file_format_spec_v10.pdf +## tap +TAP tape format for ZX Spectrum computers. + +The TAP- (and BLK-) format is nearly a direct copy of the data that is stored +in real tapes, as it is written by the ROM save routine of the ZX-Spectrum. +A TAP file is simply one data block or a group of 2 or more data blocks, one +followed after the other. The TAP file may be empty. + +You will often find this format embedded inside the TZX tape format. + +The default file extension is `.tap`. + +### Authors + +- Michael R. Cook work.mrc@pm.me, original author + +### References + +- https://worldofspectrum.net/zx-modules/fileformats/tapformat.html + ## tls Transport layer security. @@ -1378,6 +1400,27 @@ fq '.v2plusdatablock.leap_second_records | length' tziffile ### References - https://datatracker.ietf.org/doc/html/rfc8536 +## tzx +TZX tape format for ZX Spectrum computers. + +`TZX` is a file format designed to preserve cassette tapes compatible with the +ZX Spectrum computers, although some specialized versions of the format have +been defined for other machines such as the Amstrad CPC and C64. + +The format was originally created by Tomaz Kac, who was maintainer until +`revision 1.13`, before passing it to Martijn v.d. Heide. For a brief period +the company Ramsoft became the maintainers, and created revision `v1.20`. + +The default file extension is `.tzx`. + +### Authors + +- Michael R. Cook work.mrc@pm.me, original author + +### References + +- https://worldofspectrum.net/TZXformat.html + ## wasm WebAssembly Binary Format. diff --git a/format/all/all.fqtest b/format/all/all.fqtest index 4c3b2f86..7dea22d8 100644 --- a/format/all/all.fqtest +++ b/format/all/all.fqtest @@ -32,6 +32,7 @@ $ fq -n _registry.groups.probe "tar", "tiff", "tzif", + "tzx", "wasm", "webp", "zip", @@ -156,12 +157,14 @@ pssh_playready PlayReady PSSH rtmp Real-Time Messaging Protocol sll2_packet Linux cooked capture encapsulation v2 sll_packet Linux cooked capture encapsulation +tap TAP tape format for ZX Spectrum computers tar Tar archive tcp_segment Transmission control protocol segment tiff Tag Image File Format tls Transport layer security toml Tom's Obvious, Minimal Language tzif Time Zone Information Format +tzx TZX tape format for ZX Spectrum computers udp_datagram User datagram protocol vorbis_comment Vorbis comment vorbis_packet Vorbis packet diff --git a/format/all/all.go b/format/all/all.go index 882b46a6..865c6212 100644 --- a/format/all/all.go +++ b/format/all/all.go @@ -52,12 +52,14 @@ import ( _ "github.com/wader/fq/format/protobuf" _ "github.com/wader/fq/format/riff" _ "github.com/wader/fq/format/rtmp" + _ "github.com/wader/fq/format/tap" _ "github.com/wader/fq/format/tar" _ "github.com/wader/fq/format/text" _ "github.com/wader/fq/format/tiff" _ "github.com/wader/fq/format/tls" _ "github.com/wader/fq/format/toml" _ "github.com/wader/fq/format/tzif" + _ "github.com/wader/fq/format/tzx" _ "github.com/wader/fq/format/vorbis" _ "github.com/wader/fq/format/vpx" _ "github.com/wader/fq/format/wasm" diff --git a/format/format.go b/format/format.go index 41adaa68..a87ff0e0 100644 --- a/format/format.go +++ b/format/format.go @@ -165,12 +165,14 @@ var ( RTMP = &decode.Group{Name: "rtmp"} SLL_Packet = &decode.Group{Name: "sll_packet"} SLL2_Packet = &decode.Group{Name: "sll2_packet"} + TAP = &decode.Group{Name: "tap"} TAR = &decode.Group{Name: "tar"} TCP_Segment = &decode.Group{Name: "tcp_segment"} TIFF = &decode.Group{Name: "tiff"} TLS = &decode.Group{Name: "tls"} TOML = &decode.Group{Name: "toml"} Tzif = &decode.Group{Name: "tzif"} + TZX = &decode.Group{Name: "tzx"} UDP_Datagram = &decode.Group{Name: "udp_datagram"} Vorbis_Comment = &decode.Group{Name: "vorbis_comment"} Vorbis_Packet = &decode.Group{Name: "vorbis_packet"} diff --git a/format/tap/tap.go b/format/tap/tap.go new file mode 100644 index 00000000..b1e0c18f --- /dev/null +++ b/format/tap/tap.go @@ -0,0 +1,158 @@ +package tzx + +// https://worldofspectrum.net/zx-modules/fileformats/tapformat.html + +import ( + "bufio" + "bytes" + "embed" + + "golang.org/x/text/encoding/charmap" + + "github.com/wader/fq/format" + "github.com/wader/fq/pkg/bitio" + "github.com/wader/fq/pkg/decode" + "github.com/wader/fq/pkg/interp" + "github.com/wader/fq/pkg/scalar" +) + +//go:embed tap.md +var tapFS embed.FS + +func init() { + interp.RegisterFormat( + format.TAP, + &decode.Format{ + Description: "TAP tape format for ZX Spectrum computers", + DecodeFn: tapDecoder, + }) + + interp.RegisterFS(tapFS) +} + +// The TAP- (and BLK-) format is nearly a direct copy of the data that is stored +// in real tapes, as it is written by the ROM save routine of the ZX-Spectrum. +// A TAP file is simply one data block or a group of 2 or more data blocks, one +// followed after the other. The TAP file may be empty. +func tapDecoder(d *decode.D) any { + d.Endian = decode.LittleEndian + + d.FieldArray("blocks", func(d *decode.D) { + for !d.End() { + d.FieldStruct("block", func(d *decode.D) { + decodeTapBlock(d) + }) + } + }) + return nil +} + +func decodeTapBlock(d *decode.D) { + // Length of the following data. + length := d.FieldU16("length") + + // read header, fragment, or data block + switch length { + case 0: + // fragment with no data + case 1: + d.FieldRawLen("data", 8) + case 19: + d.FieldStruct("header", func(d *decode.D) { + decodeHeader(d) + }) + default: + d.FieldStruct("data", func(d *decode.D) { + decodeDataBlock(d, length) + }) + } +} + +// decodes the different types of 19-byte header blocks. +func decodeHeader(d *decode.D) { + blockStartPosition := d.Pos() + + // Always 0: byte indicating a standard ROM loading header + d.FieldU8("flag", scalar.UintMapSymStr{0: "standard_speed_data"}) + // Header type + dataType := d.FieldU8("data_type", scalar.UintMapSymStr{ + 0x00: "program", + 0x01: "numeric", + 0x02: "alphanumeric", + 0x03: "data", + }) + // Loading name of the program. Filled with spaces (0x20) to 10 characters. + d.FieldStr("program_name", 10, charmap.ISO8859_1) + + switch dataType { + case 0: + // Length of data following the header = length of BASIC program + variables. + d.FieldU16("data_length") + // LINE parameter of SAVE command. Value 32768 means "no auto-loading". + // 0..9999 are valid line numbers. + d.FieldU16("auto_start_line") + // Length of BASIC program; + // remaining bytes ([data length] - [program length]) = offset of variables. + d.FieldU16("program_length") + case 1: + // Length of data following the header = length of number array * 5 + 3. + d.FieldU16("data_length") + // Unused byte. + d.FieldU8("unused0") + // (1..26 meaning A..Z) + 128. + d.FieldU8("variable_name", scalar.UintHex) + // UnusedWord: 32768. + d.FieldU16("unused1") + case 2: + // Length of data following the header = length of string array + 3. + d.FieldU16("data_length") + // Unused byte. + d.FieldU8("unused0") + // (1..26 meaning A$..Z$) + 192. + d.FieldU8("variable_name", scalar.UintHex) + // UnusedWord: 32768. + d.FieldU16("unused1") + case 3: + // Length of data following the header, in case of a SCREEN$ header = 6912. + d.FieldU16("data_length") + // In case of a SCREEN$ header = 16384. + d.FieldU16("start_address", scalar.UintHex) + // UnusedWord: 32768. + d.FieldU16("unused") + default: + d.Fatalf("invalid TAP header type, got: %d", dataType) + } + + // Simply all bytes XORed (including flag byte). + d.FieldU8("checksum", d.UintValidate(calculateChecksum(d, blockStartPosition, d.Pos()-blockStartPosition)), scalar.UintHex) +} + +func decodeDataBlock(d *decode.D, length uint64) { + blockStartPosition := d.Pos() + + // flag indicating the type of data block, usually 255 (standard speed data) + d.FieldU8("flag", scalar.UintFn(func(s scalar.Uint) (scalar.Uint, error) { + if s.Actual == 0xFF { + s.Sym = "standard_speed_data" + } else { + s.Sym = "custom_data_block" + } + return s, nil + })) + // The essential data: length minus the flag/checksum bytes (may be empty) + d.FieldRawLen("data", int64(length-2)*8) + // Simply all bytes (including flag byte) XORed + d.FieldU8("checksum", d.UintValidate(calculateChecksum(d, blockStartPosition, d.Pos()-blockStartPosition)), scalar.UintHex) +} + +func calculateChecksum(d *decode.D, blockStartPos, blockEndPos int64) uint64 { + var blockSlice bytes.Buffer + writer := bufio.NewWriter(&blockSlice) + d.Copy(writer, bitio.NewIOReader(d.BitBufRange(blockStartPos, blockEndPos))) + + var checksum uint8 + for _, v := range blockSlice.Bytes() { + checksum ^= v + } + return uint64(checksum) +} diff --git a/format/tap/tap.md b/format/tap/tap.md new file mode 100644 index 00000000..c246fc4c --- /dev/null +++ b/format/tap/tap.md @@ -0,0 +1,16 @@ +The TAP- (and BLK-) format is nearly a direct copy of the data that is stored +in real tapes, as it is written by the ROM save routine of the ZX-Spectrum. +A TAP file is simply one data block or a group of 2 or more data blocks, one +followed after the other. The TAP file may be empty. + +You will often find this format embedded inside the TZX tape format. + +The default file extension is `.tap`. + +### Authors + +- Michael R. Cook work.mrc@pm.me, original author + +### References + +- https://worldofspectrum.net/zx-modules/fileformats/tapformat.html diff --git a/format/tap/testdata/README.md b/format/tap/testdata/README.md new file mode 100644 index 00000000..4b8b3d76 --- /dev/null +++ b/format/tap/testdata/README.md @@ -0,0 +1,22 @@ +### basic_prog1.tap + +The `basic_prog1.tap` test file was created directory from the FUSE emulator. + +Inside the emulated ZX Spectrum a BASIC program was created: + +``` +10 PRINT "fq is the best!" +20 GOTO 10 +``` + +and saved to tape: + +``` +SAVE "fqTestProg", LINE 10 +``` + +Then from FUSE select the menu item `Media > Tape > Save As..`. + +Any BASIC, machine code, screen image, or other data, can be saved directly +using the `SAVE` command. Further instructions can be found here: +https://worldofspectrum.org/ZXBasicManual/zxmanchap20.html diff --git a/format/tap/testdata/basic_prog1.fqtest b/format/tap/testdata/basic_prog1.fqtest new file mode 100644 index 00000000..19b51e22 --- /dev/null +++ b/format/tap/testdata/basic_prog1.fqtest @@ -0,0 +1,21 @@ +$ fq -d tap dv basic_prog1.tap + |00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f|0123456789abcdef|.{}: basic_prog1.tap (tap) 0x0-0x3f (63) + | | | blocks[0:2]: 0x0-0x3f (63) + | | | [0]{}: block 0x0-0x15 (21) +0x00|13 00 |.. | length: 19 0x0-0x2 (2) + | | | header{}: 0x2-0x15 (19) +0x00| 00 | . | flag: "standard_speed_data" (0) 0x2-0x3 (1) +0x00| 00 | . | data_type: "program" (0) 0x3-0x4 (1) +0x00| 66 71 54 65 73 74 50 72 6f 67 | fqTestProg | program_name: "fqTestProg" 0x4-0xe (10) +0x00| 26 00| &.| data_length: 38 0xe-0x10 (2) +0x10|0a 00 |.. | auto_start_line: 10 0x10-0x12 (2) +0x10| 26 00 | &. | program_length: 38 0x12-0x14 (2) +0x10| 01 | . | checksum: 0x1 (valid) 0x14-0x15 (1) + | | | [1]{}: block 0x15-0x3f (42) +0x10| 28 00 | (. | length: 40 0x15-0x17 (2) + | | | data{}: 0x17-0x3f (40) +0x10| ff | . | flag: "standard_speed_data" (255) 0x17-0x18 (1) +0x10| 00 0a 14 00 20 f5 22 66| .... ."f| data: raw bits 0x18-0x3e (38) +0x20|71 20 69 73 20 74 68 65 20 62 65 73 74 21 22 0d|q is the best!".| +0x30|00 14 0a 00 ec 31 30 0e 00 00 0a 00 00 0d |.....10....... | +0x30| b6| | .|| checksum: 0xb6 (valid) 0x3e-0x3f (1) diff --git a/format/tap/testdata/basic_prog1.tap b/format/tap/testdata/basic_prog1.tap new file mode 100644 index 00000000..585d7801 Binary files /dev/null and b/format/tap/testdata/basic_prog1.tap differ diff --git a/format/tzx/testdata/README.md b/format/tzx/testdata/README.md new file mode 100644 index 00000000..a2826ace --- /dev/null +++ b/format/tzx/testdata/README.md @@ -0,0 +1,28 @@ +### basic_prog1.tzx + +The `basic_prog1.tzx` test file was created directory from the FUSE emulator. + +Inside the emulated ZX Spectrum a BASIC program was created: + +``` +10 PRINT "fq is the best!" +20 GOTO 10 +``` + +and saved to tape: + +``` +SAVE "fqTestProg", LINE 10 +``` + +Then from FUSE select the menu item `Media > Tape > Save As..`. + +Any BASIC, machine code, screen image, or other data, can be saved directly +using the `SAVE` command. Further instructions can be found here: +https://worldofspectrum.org/ZXBasicManual/zxmanchap20.html + + +#### Archive Info + +The FUSE emulator is not able to add the tape metadata. As this tape block is +very simple, it was added manually using a Hex editor. diff --git a/format/tzx/testdata/basic_prog1.fqtest b/format/tzx/testdata/basic_prog1.fqtest new file mode 100644 index 00000000..d9b950d3 --- /dev/null +++ b/format/tzx/testdata/basic_prog1.fqtest @@ -0,0 +1,81 @@ +$ fq -d tzx dv basic_prog1.tzx + |00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f|0123456789abcdef|.{}: basic_prog1.tzx (tzx) 0x0-0xcd (205) +0x00|5a 58 54 61 70 65 21 1a |ZXTape!. | signature: raw bits (valid) 0x0-0x8 (8) +0x00| 01 | . | major_version: 1 0x8-0x9 (1) +0x00| 14 | . | minor_version: 20 0x9-0xa (1) + | | | blocks[0:3]: 0xa-0xcd (195) + | | | [0]{}: block 0xa-0x88 (126) +0x00| 32 | 2 | type: "archive_info" (50) 0xa-0xb (1) +0x00| 7b 00 | {. | length: 123 0xb-0xd (2) +0x00| 09 | . | count: 9 0xd-0xe (1) + | | | archive_info[0:9]: 0xe-0x88 (122) + | | | [0]{}: entry 0xe-0x1a (12) +0x00| 00 | . | id: "title" (0) 0xe-0xf (1) +0x00| 0a| .| length: 10 0xf-0x10 (1) +0x10|66 71 74 65 73 74 70 72 6f 67 |fqtestprog | value: "fqtestprog" 0x10-0x1a (10) + | | | [1]{}: entry 0x1a-0x21 (7) +0x10| 01 | . | id: "publisher" (1) 0x1a-0x1b (1) +0x10| 05 | . | length: 5 0x1b-0x1c (1) +0x10| 77 61 64 65| wade| value: "wader" 0x1c-0x21 (5) +0x20|72 |r | + | | | [2]{}: entry 0x21-0x32 (17) +0x20| 02 | . | id: "author" (2) 0x21-0x22 (1) +0x20| 0f | . | length: 15 0x22-0x23 (1) +0x20| 4d 69 63 68 61 65 6c 20 52 2e 20 43 6f| Michael R. Co| value: "Michael R. Cook" 0x23-0x32 (15) +0x30|6f 6b |ok | + | | | [3]{}: entry 0x32-0x38 (6) +0x30| 03 | . | id: "year" (3) 0x32-0x33 (1) +0x30| 04 | . | length: 4 0x33-0x34 (1) +0x30| 32 30 32 34 | 2024 | value: "2024" 0x34-0x38 (4) + | | | [4]{}: entry 0x38-0x41 (9) +0x30| 04 | . | id: "language" (4) 0x38-0x39 (1) +0x30| 07 | . | length: 7 0x39-0x3a (1) +0x30| 45 6e 67 6c 69 73| Englis| value: "English" 0x3a-0x41 (7) +0x40|68 |h | + | | | [5]{}: entry 0x41-0x4f (14) +0x40| 05 | . | id: "category" (5) 0x41-0x42 (1) +0x40| 0c | . | length: 12 0x42-0x43 (1) +0x40| 54 65 73 74 20 50 72 6f 67 72 61 6d | Test Program | value: "Test Program" 0x43-0x4f (12) + | | | [6]{}: entry 0x4f-0x5c (13) +0x40| 07| .| id: "loader" (7) 0x4f-0x50 (1) +0x50|0b |. | length: 11 0x50-0x51 (1) +0x50| 52 4f 4d 20 74 69 6d 69 6e 67 73 | ROM timings | value: "ROM timings" 0x51-0x5c (11) + | | | [7]{}: entry 0x5c-0x6e (18) +0x50| 08 | . | id: "origin" (8) 0x5c-0x5d (1) +0x50| 10 | . | length: 16 0x5d-0x5e (1) +0x50| 4f 72| Or| value: "Original release" 0x5e-0x6e (16) +0x60|69 67 69 6e 61 6c 20 72 65 6c 65 61 73 65 |iginal release | + | | | [8]{}: entry 0x6e-0x88 (26) +0x60| ff | . | id: "comment" (255) 0x6e-0x6f (1) +0x60| 18| .| length: 24 0x6f-0x70 (1) +0x70|54 5a 58 65 64 20 62 79 20 4d 69 63 68 61 65 6c|TZXed by Michael| value: "TZXed by Michael R. Cook" 0x70-0x88 (24) +0x80|20 52 2e 20 43 6f 6f 6b | R. Cook | + | | | [1]{}: block 0x88-0xa0 (24) +0x80| 10 | . | type: "standard_speed_data" (16) 0x88-0x89 (1) +0x80| e8 03 | .. | pause: 1000 0x89-0x8b (2) + |00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f|0123456789abcdef| tap{}: (tap) 0x8b-0xa0 (21) + | | | blocks[0:1]: 0x8b-0xa0 (21) + | | | [0]{}: block 0x8b-0xa0 (21) +0x80| 13 00 | .. | length: 19 0x8b-0x8d (2) + | | | header{}: 0x8d-0xa0 (19) +0x80| 00 | . | flag: "standard_speed_data" (0) 0x8d-0x8e (1) +0x80| 00 | . | data_type: "program" (0) 0x8e-0x8f (1) +0x80| 66| f| program_name: "fqTestProg" 0x8f-0x99 (10) +0x90|71 54 65 73 74 50 72 6f 67 |qTestProg | +0x90| 26 00 | &. | data_length: 38 0x99-0x9b (2) +0x90| 0a 00 | .. | auto_start_line: 10 0x9b-0x9d (2) +0x90| 26 00 | &. | program_length: 38 0x9d-0x9f (2) +0x90| 01| .| checksum: 0x1 (valid) 0x9f-0xa0 (1) + | | | [2]{}: block 0xa0-0xcd (45) +0xa0|10 |. | type: "standard_speed_data" (16) 0xa0-0xa1 (1) +0xa0| e8 03 | .. | pause: 1000 0xa1-0xa3 (2) + |00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f|0123456789abcdef| tap{}: (tap) 0xa3-0xcd (42) + | | | blocks[0:1]: 0xa3-0xcd (42) + | | | [0]{}: block 0xa3-0xcd (42) +0xa0| 28 00 | (. | length: 40 0xa3-0xa5 (2) + | | | data{}: 0xa5-0xcd (40) +0xa0| ff | . | flag: "standard_speed_data" (255) 0xa5-0xa6 (1) +0xa0| 00 0a 14 00 20 f5 22 66 71 20| .... ."fq | data: raw bits 0xa6-0xcc (38) +0xb0|69 73 20 74 68 65 20 62 65 73 74 21 22 0d 00 14|is the best!"...| +0xc0|0a 00 ec 31 30 0e 00 00 0a 00 00 0d |...10....... | +0xc0| b6| | .| | checksum: 0xb6 (valid) 0xcc-0xcd (1) diff --git a/format/tzx/testdata/basic_prog1.tzx b/format/tzx/testdata/basic_prog1.tzx new file mode 100644 index 00000000..83968942 Binary files /dev/null and b/format/tzx/testdata/basic_prog1.tzx differ diff --git a/format/tzx/tzx.go b/format/tzx/tzx.go new file mode 100644 index 00000000..98adb5a0 --- /dev/null +++ b/format/tzx/tzx.go @@ -0,0 +1,655 @@ +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{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("archive_info", 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", hwInfoTypeMapper) + // Hardware ID (ZX81, Kempston Joystick, etc.) + d.FieldU8("id", hwInfoTypeIdMapper[typeId]) + // Hardware compatibility information + d.FieldU8("info_id", hwInfoIdMapper) + }) + } + }) + }, + + // 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", 10, 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", int64(9*8)) + }, + } + + blockType := d.FieldU8("type", blockTypeMapper) + + // Deprecated block types: C64RomType, C64TurboData, EmulationInfo, Snapshot + if blockType == 0x16 || blockType == 0x17 || blockType == 0x34 || blockType == 0x40 { + d.Fatalf("deprecated block type encountered: %02x", blockType) + } + + 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 hwInfoTypeMapper = 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 hwInfoTypeIdMapper = 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 hwInfoIdMapper = 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.", +} diff --git a/format/tzx/tzx.md b/format/tzx/tzx.md new file mode 100644 index 00000000..d713dde3 --- /dev/null +++ b/format/tzx/tzx.md @@ -0,0 +1,17 @@ +`TZX` is a file format designed to preserve cassette tapes compatible with the +ZX Spectrum computers, although some specialized versions of the format have +been defined for other machines such as the Amstrad CPC and C64. + +The format was originally created by Tomaz Kac, who was maintainer until +`revision 1.13`, before passing it to Martijn v.d. Heide. For a brief period +the company Ramsoft became the maintainers, and created revision `v1.20`. + +The default file extension is `.tzx`. + +### Authors + +- Michael R. Cook work.mrc@pm.me, original author + +### References + +- https://worldofspectrum.net/TZXformat.html