1
1
mirror of https://github.com/wader/fq.git synced 2024-09-11 20:07:11 +03:00

Merge pull request #975 from mrcook/tzx

tzx: Add suport for ZX Spectrum TZX and TAP files
This commit is contained in:
Mattias Wadman 2024-08-08 10:22:06 +02:00 committed by GitHub
commit 7fca1970dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1051 additions and 1 deletions

View File

@ -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,

View File

@ -113,12 +113,14 @@
|[`rtmp`](#rtmp) |Real-Time&nbsp;Messaging&nbsp;Protocol |<sub>`amf0` `mpeg_asc`</sub>|
|`sll2_packet` |Linux&nbsp;cooked&nbsp;capture&nbsp;encapsulation&nbsp;v2 |<sub>`inet_packet`</sub>|
|`sll_packet` |Linux&nbsp;cooked&nbsp;capture&nbsp;encapsulation |<sub>`inet_packet`</sub>|
|[`tap`](#tap) |TAP&nbsp;tape&nbsp;format&nbsp;for&nbsp;ZX&nbsp;Spectrum&nbsp;computers |<sub></sub>|
|`tar` |Tar&nbsp;archive |<sub>`probe`</sub>|
|`tcp_segment` |Transmission&nbsp;control&nbsp;protocol&nbsp;segment |<sub></sub>|
|`tiff` |Tag&nbsp;Image&nbsp;File&nbsp;Format |<sub>`icc_profile`</sub>|
|[`tls`](#tls) |Transport&nbsp;layer&nbsp;security |<sub>`asn1_ber`</sub>|
|`toml` |Tom's&nbsp;Obvious,&nbsp;Minimal&nbsp;Language |<sub></sub>|
|[`tzif`](#tzif) |Time&nbsp;Zone&nbsp;Information&nbsp;Format |<sub></sub>|
|[`tzx`](#tzx) |TZX&nbsp;tape&nbsp;format&nbsp;for&nbsp;ZX&nbsp;Spectrum&nbsp;computers |<sub>`tap`</sub>|
|`udp_datagram` |User&nbsp;datagram&nbsp;protocol |<sub>`udp_payload`</sub>|
|`vorbis_comment` |Vorbis&nbsp;comment |<sub>`flac_picture`</sub>|
|`vorbis_packet` |Vorbis&nbsp;packet |<sub>`vorbis_comment`</sub>|
@ -137,7 +139,7 @@
|`ip_packet` |Group |<sub>`icmp` `icmpv6` `tcp_segment` `udp_datagram`</sub>|
|`link_frame` |Group |<sub>`bsd_loopback_frame` `ether8023_frame` `ipv4_packet` `ipv6_packet` `sll2_packet` `sll_packet`</sub>|
|`mp3_frame_tags` |Group |<sub>`mp3_frame_vbri` `mp3_frame_xing`</sub>|
|`probe` |Group |<sub>`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`</sub>|
|`probe` |Group |<sub>`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`</sub>|
|`tcp_stream` |Group |<sub>`dns_tcp` `rtmp` `tls`</sub>|
|`udp_payload` |Group |<sub>`dns`</sub>|
@ -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.

View File

@ -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

View File

@ -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"

View File

@ -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"}

158
format/tap/tap.go Normal file
View File

@ -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)
}

16
format/tap/tap.md Normal file
View File

@ -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

22
format/tap/testdata/README.md vendored Normal file
View File

@ -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

21
format/tap/testdata/basic_prog1.fqtest vendored Normal file
View File

@ -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)

BIN
format/tap/testdata/basic_prog1.tap vendored Normal file

Binary file not shown.

28
format/tzx/testdata/README.md vendored Normal file
View File

@ -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.

81
format/tzx/testdata/basic_prog1.fqtest vendored Normal file
View File

@ -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)

BIN
format/tzx/testdata/basic_prog1.tzx vendored Normal file

Binary file not shown.

655
format/tzx/tzx.go Normal file
View File

@ -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.",
}

17
format/tzx/tzx.md Normal file
View File

@ -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