diff --git a/skoolkit/__init__.py b/skoolkit/__init__.py
index 7336326f..f72b9e82 100644
--- a/skoolkit/__init__.py
+++ b/skoolkit/__init__.py
@@ -199,6 +199,9 @@ def get_word(data, index):
def get_word3(data, index):
return get_word(data, index) + 65536 * data[index + 2]
+def as_dword(num):
+ return (num % 256, (num >> 8) % 256, (num >> 16) % 256, (num >> 24) % 256)
+
def get_dword(data, index):
return get_word3(data, index) + 16777216 * data[index + 3]
diff --git a/skoolkit/bin2tap.py b/skoolkit/bin2tap.py
index d56f261a..1745f8a3 100644
--- a/skoolkit/bin2tap.py
+++ b/skoolkit/bin2tap.py
@@ -20,7 +20,7 @@
from skoolkit import SkoolKitError, integer, parse_int, read_bin_file, VERSION
from skoolkit.components import get_snapshot_reader
-from skoolkit.tape import write_tap
+from skoolkit.tape import write_pzx, write_tap
def _get_str(chars):
return [ord(c) for c in chars]
@@ -142,9 +142,9 @@ def _get_bank_loader(title, address, start_addr, banks, out7ffd):
data.append(0x80 | out7ffd) # End marker
return (_get_header(title, len(data), address), _make_block(data))
-def run(ram, clear, org, start, stack, tapfile, scr, banks, out7ffd, loader_addr):
- title = os.path.basename(tapfile)
- if title.lower().endswith('.tap'):
+def run(ram, clear, org, start, stack, tape_file, scr, banks, out7ffd, loader_addr):
+ title = os.path.basename(tape_file)
+ if title.lower().endswith(('.tap', '.pzx')):
title = title[:-4]
if banks is None:
blocks = _get_basic_loader(title, clear, start, scr)
@@ -178,22 +178,26 @@ def run(ram, clear, org, start, stack, tapfile, scr, banks, out7ffd, loader_addr
for b in sorted(banks):
blocks.append(_make_block(banks[b]))
- write_tap(tapfile, blocks)
+ if tape_file.lower().endswith('.pzx'):
+ write_pzx(tape_file, blocks)
+ else:
+ write_tap(tape_file, blocks)
def main(args):
parser = argparse.ArgumentParser(
- usage='bin2tap.py [options] FILE [file.tap]',
- description="Convert a binary (raw memory) file or a SNA, SZX or Z80 snapshot into a TAP file. "
- "FILE may be a regular file, or '-' to read a binary file from standard input.",
+ usage='bin2tap.py [options] FILE [OUTFILE]',
+ description="Convert a binary (raw memory) file or a SNA, SZX or Z80 snapshot into a PZX or TAP file. "
+ "FILE may be a regular file, or '-' to read a binary file from standard input. "
+ "If OUTFILE is not given, a TAP file is created.",
add_help=False
)
parser.add_argument('infile', help=argparse.SUPPRESS, nargs='?')
parser.add_argument('outfile', help=argparse.SUPPRESS, nargs='?')
group = parser.add_argument_group('Options')
group.add_argument('--7ffd', metavar='N', dest='out7ffd', type=integer,
- help="Add 128K RAM banks to the TAP file and write N to port 0x7ffd after they've loaded.")
+ help="Add 128K RAM banks to the tape file and write N to port 0x7ffd after they've loaded.")
group.add_argument('--banks', metavar='N[,N...]',
- help="Add only these 128K RAM banks to the TAP file (default: 0,1,3,4,6,7).")
+ help="Add only these 128K RAM banks to the tape file (default: 0,1,3,4,6,7).")
group.add_argument('-b', '--begin', dest='begin', metavar='BEGIN', type=integer,
help="Begin conversion at this address (default: ORG for a binary file, 16384 for a snapshot).")
group.add_argument('-c', '--clear', dest='clear', metavar='N', type=integer,
@@ -209,7 +213,7 @@ def main(args):
group.add_argument('-s', '--start', dest='start', metavar='START', type=integer,
help="Set the start address to JP to (default: BEGIN).")
group.add_argument('-S', '--screen', dest='screen', metavar='FILE',
- help="Add a loading screen to the TAP file. FILE may be a snapshot or a 6912-byte SCR file.")
+ help="Add a loading screen to the tape file. FILE may be a snapshot or a 6912-byte SCR file.")
group.add_argument('-V', '--version', action='version', version='SkoolKit {}'.format(VERSION),
help='Show SkoolKit version number and exit.')
@@ -258,19 +262,19 @@ def main(args):
del banks[b]
start = namespace.start or begin
stack = namespace.stack or begin
- tapfile = namespace.outfile
- if tapfile is None:
+ tape_file = namespace.outfile
+ if tape_file is None:
if infile.lower().endswith(('.bin', '.sna', '.szx', '.z80')):
prefix = os.path.basename(infile)[:-4]
elif infile == '-':
prefix = 'program'
else:
prefix = os.path.basename(infile)
- tapfile = prefix + ".tap"
+ tape_file = prefix + ".tap"
scr = namespace.screen
if scr is not None:
if snapshot_reader.can_read(scr):
scr = snapshot_reader.get_snapshot(scr)[16384:23296]
else:
scr = read_bin_file(scr, 6912)
- run(ram, clear, begin, start, stack, tapfile, scr, banks, out7ffd, loader_addr)
+ run(ram, clear, begin, start, stack, tape_file, scr, banks, out7ffd, loader_addr)
diff --git a/skoolkit/rzxplay.py b/skoolkit/rzxplay.py
index 8cfdff68..c08e31c1 100644
--- a/skoolkit/rzxplay.py
+++ b/skoolkit/rzxplay.py
@@ -28,8 +28,8 @@
except ImportError: # pragma: no cover
pygame = None
-from skoolkit import (VERSION, SkoolKitError, CSimulator, get_dword, get_word,
- parse_int, read_bin_file, warn, write)
+from skoolkit import (VERSION, SkoolKitError, CSimulator, as_dword, get_dword,
+ get_word, parse_int, read_bin_file, warn, write)
from skoolkit.pagingtracer import PagingTracer
from skoolkit.simulator import Simulator
from skoolkit.simutils import from_snapshot, get_state
@@ -130,9 +130,6 @@ def __init__(self, screen=None, p_rectangles=None, c_rectangles=None, clock=None
self.frame_count = 0
self.stop = False
-def as_dword(num):
- return (num % 256, (num >> 8) % 256, (num >> 16) % 256, (num >> 24) % 256)
-
def write_rzx(fname, context, rzx_blocks):
creator_b = (83, 107, 111, 111, 108, 75, 105, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
major, minor = re.match('([0-9]+).([0-9]+)', VERSION).groups()
diff --git a/skoolkit/tape.py b/skoolkit/tape.py
index 325e60e5..9db6f73b 100644
--- a/skoolkit/tape.py
+++ b/skoolkit/tape.py
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License along with
# SkoolKit. If not, see .
-from skoolkit import SkoolKitError, get_word, get_word3, get_dword, read_bin_file
+from skoolkit import SkoolKitError, as_dword, get_word, get_word3, get_dword, read_bin_file
from skoolkit.basic import get_char
ARCHIVE_INFO = {
@@ -757,3 +757,27 @@ def write_tap(fname, blocks):
length = len(data)
f.write(bytes((length % 256, length // 256)))
f.write(bytes(data))
+
+def write_pzx(fname, blocks):
+ with open(fname, 'wb') as f:
+ f.write(b'PZXT\x02\x00\x00\x00\x01\x00')
+ for i, data in enumerate(blocks):
+ if i:
+ f.write(b'PAUS\x04\x00\x00\x00\xe0\x67\x35\x00')
+ if data[0]:
+ f.write(b'PULS\x08\x00\x00\x00\x97\x8c\x78\x08\x9b\x02\xdf\x02')
+ else:
+ f.write(b'PULS\x08\x00\x00\x00\x7f\x9f\x78\x08\x9b\x02\xdf\x02')
+ length = len(data)
+ bits = 0x80000000 + length * 8
+ f.write(bytes((
+ 68, 65, 84, 65, # DATA
+ *as_dword(length + 16), # Block length
+ *as_dword(bits), # Polarity and bit count
+ 177, 3, # Tail pulse (945)
+ 2, # p0
+ 2, # p1
+ 87, 3, 87, 3, # s0 (855, 855)
+ 174, 6, 174, 6, # s1 (1710, 1710)
+ *data # Data
+ )))
diff --git a/sphinx/source/changelog.rst b/sphinx/source/changelog.rst
index 23f23180..7d6d2035 100644
--- a/sphinx/source/changelog.rst
+++ b/sphinx/source/changelog.rst
@@ -3,7 +3,9 @@ Changelog
9.3b1
-----
-* Added support to :ref:`tapinfo.py` and :ref:`tap2sna.py` for PZX files
+* Added support to :ref:`tapinfo.py` and :ref:`tap2sna.py` for reading PZX
+ files
+* Added support to :ref:`bin2tap.py` for writing PZX files
* Added support to :ref:`tap2sna.py ` for the ``m`` (memory)
replacement field in the ``TraceLine`` configuration parameter
* Added support to :ref:`trace.py ` for the ``m`` (memory)
diff --git a/sphinx/source/commands.rst b/sphinx/source/commands.rst
index ccd49cea..ac349aba 100644
--- a/sphinx/source/commands.rst
+++ b/sphinx/source/commands.rst
@@ -87,7 +87,7 @@ Run `bin2sna.py` with no arguments to see the list of available options::
bin2tap.py
----------
`bin2tap.py` converts a binary (raw memory) file or a SNA, SZX or Z80 snapshot
-into a TAP file. For example::
+into a PZX or TAP file. For example::
$ bin2tap.py game.bin
@@ -97,16 +97,16 @@ of code to run) and the stack pointer are set to 65536 minus the length of
`game.bin`. These values can be changed by passing options to `bin2tap.py`. Run
it with no arguments to see the list of available options::
- usage: bin2tap.py [options] FILE [file.tap]
+ usage: bin2tap.py [options] FILE [OUTFILE]
- Convert a binary (raw memory) file or a SNA, SZX or Z80 snapshot into a TAP
- file. FILE may be a regular file, or '-' to read a binary file from standard
- input.
+ Convert a binary (raw memory) file or a SNA, SZX or Z80 snapshot into a PZX or
+ TAP file. FILE may be a regular file, or '-' to read a binary file from
+ standard input. If OUTFILE is not given, a TAP file is created.
Options:
- --7ffd N Add 128K RAM banks to the TAP file and write N to port
- 0x7ffd after they've loaded.
- --banks N[,N...] Add only these 128K RAM banks to the TAP file
+ --7ffd N Add 128K RAM banks to the tape file and write N to
+ port 0x7ffd after they've loaded.
+ --banks N[,N...] Add only these 128K RAM banks to the tape file
(default: 0,1,3,4,6,7).
-b BEGIN, --begin BEGIN
Begin conversion at this address (default: ORG for a
@@ -123,7 +123,7 @@ it with no arguments to see the list of available options::
-s START, --start START
Set the start address to JP to (default: BEGIN).
-S FILE, --screen FILE
- Add a loading screen to the TAP file. FILE may be a
+ Add a loading screen to the tape file. FILE may be a
snapshot or a 6912-byte SCR file.
-V, --version Show SkoolKit version number and exit.
@@ -145,7 +145,7 @@ crashing. The lowest usable address with the ``--clear`` option on a bare 48K
Spectrum is 23972 (5DA4) if a loading screen is used, or 23952 (0x5D90)
otherwise.
-To create a TAP file that loads a 128K game, use the ``--7ffd``, ``--begin``
+To create a tape file that loads a 128K game, use the ``--7ffd``, ``--begin``
and ``--clear`` options along with a 128K snapshot or a 128K binary file as
input, where:
@@ -161,7 +161,7 @@ the number of RAM banks to load) is placed one above the CLEAR address. Use the
address with the ``--clear`` option on a bare 128K Spectrum is 23977 (0x5DA9)
if a loading screen is used, or 23957 (0x5D95) otherwise.
-By default, 128K RAM banks 0, 1, 3, 4, 6 and 7 are added to the TAP file. If
+By default, 128K RAM banks 0, 1, 3, 4, 6 and 7 are added to the tape file. If
one or more of these RAM banks are not required, use the ``--banks`` option to
specify a smaller set of RAM banks to add. If none of these RAM banks are
required, use ``,`` (a single comma) as the argument to the ``--banks`` option.
@@ -172,6 +172,8 @@ block on the tape.
+---------+-------------------------------------------------------------------+
| Version | Changes |
+=========+===================================================================+
+| 9.3 | Added support for writing PZX files |
++---------+-------------------------------------------------------------------+
| 9.1 | Added the ``--7ffd``, ``--banks`` and ``--loader`` options and |
| | support for writing 128K TAP files |
+---------+-------------------------------------------------------------------+
diff --git a/sphinx/source/conf.py b/sphinx/source/conf.py
index 63c6f177..29855172 100644
--- a/sphinx/source/conf.py
+++ b/sphinx/source/conf.py
@@ -224,7 +224,7 @@
('man/bin2sna.py', 'bin2sna.py',
'convert a binary file into an SZX or Z80 snapshot', _authors, 1),
('man/bin2tap.py', 'bin2tap.py',
- 'convert a binary file or snapshot into a TAP file', _authors, 1),
+ 'convert a binary file or snapshot into a PZX or TAP file', _authors, 1),
('man/rzxinfo.py', 'rzxinfo.py',
'show the blocks in or extract the snapshots from an RZX file', _authors, 1),
('man/rzxplay.py', 'rzxplay.py',
diff --git a/sphinx/source/man/bin2tap.py.rst b/sphinx/source/man/bin2tap.py.rst
index f841ab83..cb9904ab 100644
--- a/sphinx/source/man/bin2tap.py.rst
+++ b/sphinx/source/man/bin2tap.py.rst
@@ -6,22 +6,23 @@ bin2tap.py
SYNOPSIS
========
-``bin2tap.py`` [options] FILE [file.tap]
+``bin2tap.py`` [options] FILE [OUTFILE]
DESCRIPTION
===========
``bin2tap.py`` converts a binary (raw memory) file or a SNA, SZX or Z80
-snapshot into a TAP file. FILE may be a regular file, or '-' to read a binary
-file from standard input.
+snapshot into a PZX or TAP file. FILE may be a regular file, or '-' to read a
+binary file from standard input. If OUTFILE is not given, a TAP file is
+created.
OPTIONS
=======
--7ffd `N`
- Add 128K RAM banks to the TAP file and write `N` to port 0x7ffd after they've
- loaded.
+ Add 128K RAM banks to the tape file and write `N` to port 0x7ffd after
+ they've loaded.
--banks `N[,N...]`
- Add only these 128K RAM banks to the TAP file (default: 0,1,3,4,6,7).
+ Add only these 128K RAM banks to the tape file (default: 0,1,3,4,6,7).
-b, --begin `BEGIN`
Begin conversion at this address. The default begin address is the origin
@@ -55,8 +56,8 @@ OPTIONS
must be a decimal number, or a hexadecimal number prefixed by '0x'.
-S, --screen `FILE`
- Add a loading screen to the TAP file. `FILE` may be a snapshot or a 6912-byte
- SCR file.
+ Add a loading screen to the tape file. `FILE` may be a snapshot or a
+ 6912-byte SCR file.
-V, --version
Show the SkoolKit version number and exit.
@@ -82,7 +83,7 @@ Spectrum is 23952 (0x5D90).
128K TAPES
==========
-To create a TAP file that loads a 128K game, use the ``--7ffd``, ``--begin``
+To create a tape file that loads a 128K game, use the ``--7ffd``, ``--begin``
and ``--clear`` options along with a 128K snapshot or a 128K binary file as
input, where:
@@ -97,7 +98,7 @@ the number of RAM banks to load) is placed one above the CLEAR address. Use the
``--loader`` option to place it at an alternative address. The lowest usable
address with the ``--clear`` option on a bare 128K Spectrum is 23977 (0x5DA9).
-By default, 128K RAM banks 0, 1, 3, 4, 6 and 7 are added to the TAP file. If
+By default, 128K RAM banks 0, 1, 3, 4, 6 and 7 are added to the tape file. If
one or more of these RAM banks are not required, use the ``--banks`` option to
specify a smaller set of RAM banks to add. If none of these RAM banks are
required, use ``,`` (a single comma) as the argument to the ``--banks`` option.
@@ -112,8 +113,8 @@ EXAMPLES
|
| ``bin2tap.py game.bin``
-2. Convert ``game.bin`` into a TAP file that starts execution at 32768 when
+2. Convert ``game.bin`` into a PZX file that starts execution at 32768 when
loaded:
|
- | ``bin2tap.py -s 32768 game.bin``
+ | ``bin2tap.py -s 32768 game.bin game.pzx``
diff --git a/sphinx/source/whatis.rst b/sphinx/source/whatis.rst
index 9290f441..22be8bb7 100644
--- a/sphinx/source/whatis.rst
+++ b/sphinx/source/whatis.rst
@@ -52,8 +52,8 @@ With SkoolKit you can:
list the BASIC program it contains
* use :ref:`rzxinfo.py` to analyse the blocks in an RZX file, and extract
snapshots from it
-* use :ref:`bin2tap.py` to convert a snapshot or raw memory file into a TAP
- file
+* use :ref:`bin2tap.py` to convert a snapshot or raw memory file into a PZX or
+ TAP file
* use :ref:`bin2sna.py` to convert a raw memory file into a Z80 or SZX snapshot
* use :ref:`snapmod.py` to modify the register values or memory contents in a
Z80 or SZX snapshot
diff --git a/tests/test_bin2tap.py b/tests/test_bin2tap.py
index 91c4e8f3..f535f0e6 100644
--- a/tests/test_bin2tap.py
+++ b/tests/test_bin2tap.py
@@ -8,20 +8,20 @@ def mock_run(*args):
global run_args
run_args = args
-def mock_write_tap(fname, blocks):
+def mock_write_tape(fname, blocks):
global tape_fname, tape_blocks
tape_fname = fname
tape_blocks = blocks
class Bin2TapTest(SkoolKitTestCase):
- @patch.object(bin2tap, 'write_tap', mock_write_tap)
- def _run(self, args, tapfile=None):
- if tapfile is None:
- tapfile = args.split()[-1][:-4] + '.tap'
+ @patch.object(bin2tap, 'write_tap', mock_write_tape)
+ def _run(self, args, tape_file=None):
+ if tape_file is None:
+ tape_file = args.split()[-1][:-4] + '.tap'
output, error = self.run_bin2tap(args)
self.assertEqual(output, '')
self.assertEqual(error, '')
- self.assertEqual(tape_fname, tapfile)
+ self.assertEqual(tape_fname, tape_file)
return tape_blocks
def _get_word(self, num):
@@ -385,6 +385,14 @@ def test_no_options(self):
blocks = self._run(binfile)
self._check_tape(blocks, bin_data, binfile)
+ @patch.object(bin2tap, 'write_pzx', mock_write_tape)
+ def test_write_pzx(self):
+ bin_data = [1, 2, 3, 4, 5]
+ binfile = self.write_bin_file(bin_data, suffix='.bin')
+ pzxfile = 'out.pzx'
+ blocks = self._run(f'{binfile} {pzxfile}', pzxfile)
+ self._check_tape(blocks, bin_data, binfile, name=pzxfile[:-4])
+
def test_nonstandard_bin_name(self):
bin_data = [0]
binfile = self.write_bin_file(bin_data, suffix='.ram')
@@ -392,7 +400,7 @@ def test_nonstandard_bin_name(self):
blocks = self._run(binfile, tapfile)
self._check_tape(blocks, bin_data, binfile)
- @patch.object(bin2tap, 'write_tap', mock_write_tap)
+ @patch.object(bin2tap, 'write_tap', mock_write_tape)
def test_bin_in_subdirectory(self):
tapfile = self.write_bin_file(suffix='.tap')
bin_data = [1]
@@ -402,7 +410,7 @@ def test_bin_in_subdirectory(self):
self.assertEqual(error, '')
self._check_tape(tape_blocks, bin_data, binfile)
- @patch.object(bin2tap, 'write_tap', mock_write_tap)
+ @patch.object(bin2tap, 'write_tap', mock_write_tape)
def test_nonstandard_bin_name_in_subdirectory(self):
tapfile = self.write_bin_file(suffix='.ram.tap')
bin_data = [1]
diff --git a/tests/test_tape.py b/tests/test_tape.py
index adf4dfbb..71f8f844 100644
--- a/tests/test_tape.py
+++ b/tests/test_tape.py
@@ -1,5 +1,19 @@
-from skoolkittest import SkoolKitTestCase
-from skoolkit.tape import write_tap
+from skoolkittest import SkoolKitTestCase, as_dword
+from skoolkit.tape import write_pzx, write_tap
+
+def get_pzx_blocks(fname):
+ blocks = []
+ with open(fname, 'rb') as f:
+ pzx = f.read()
+ i = 0
+ while i < len(pzx):
+ block_id = ''.join(chr(b) for b in pzx[i:i + 4])
+ i += 4
+ block_len = pzx[i] + 256 * pzx[i + 1] + 65536 * pzx[i + 2] + 16777216 * pzx[i + 3]
+ i += 4
+ blocks.append((block_id, tuple(pzx[i:i + block_len])))
+ i += block_len
+ return blocks
def get_tap_blocks(fname):
blocks = []
@@ -12,7 +26,42 @@ def get_tap_blocks(fname):
i += block_len + 2
return blocks
+def to_pzx_data(data):
+ bits = 0x80000000 + len(data) * 8
+ return (
+ *as_dword(bits), # Polarity and bit count
+ 177, 3, # Tail pulse (945)
+ 2, 2, # p0, p1
+ 87, 3, 87, 3, # s0 (855, 855)
+ 174, 6, 174, 6, # s1 (1710, 1710)
+ *data # Data
+ )
+
class TapeTest(SkoolKitTestCase):
+ def test_write_pzx(self):
+ blocks = ([0, 1, 2], [255, 4, 5, 6, 7], [255, 8, 9, 10])
+ pzxfile = 'test_write_pzx.pzx'
+ write_pzx(pzxfile, blocks)
+ pzx_blocks = get_pzx_blocks(pzxfile)
+ puls_long = (127, 159, 120, 8, 155, 2, 223, 2)
+ puls_short = (151, 140, 120, 8, 155, 2, 223, 2)
+ paus = (224, 103, 53, 0) # 3500000 T-states
+ exp_blocks = (
+ ('PZXT', (1, 0)),
+ ('PULS', puls_long),
+ ('DATA', to_pzx_data(blocks[0])),
+ ('PAUS', paus),
+ ('PULS', puls_short),
+ ('DATA', to_pzx_data(blocks[1])),
+ ('PAUS', paus),
+ ('PULS', puls_short),
+ ('DATA', to_pzx_data(blocks[2]))
+ )
+ self.assertEqual(len(pzx_blocks), len(exp_blocks))
+ for (exp_id, exp_data), (block_id, data) in zip(exp_blocks, pzx_blocks):
+ self.assertEqual(block_id, exp_id)
+ self.assertEqual(exp_data, data)
+
def test_write_tap(self):
blocks = ([0, 1, 2], [3, 4, 5, 6, 7])
tapfile = 'test_write_tap.tap'