From fec445c3156c3237192e5e87a0c965e9fd3ca1cb Mon Sep 17 00:00:00 2001 From: Richard Dymond Date: Fri, 28 Jun 2024 17:53:34 -0300 Subject: [PATCH] Add support to bin2tap.py for writing PZX files --- skoolkit/__init__.py | 3 ++ skoolkit/bin2tap.py | 34 +++++++++++--------- skoolkit/rzxplay.py | 7 ++--- skoolkit/tape.py | 26 +++++++++++++++- sphinx/source/changelog.rst | 4 ++- sphinx/source/commands.rst | 24 ++++++++------- sphinx/source/conf.py | 2 +- sphinx/source/man/bin2tap.py.rst | 25 +++++++-------- sphinx/source/whatis.rst | 4 +-- tests/test_bin2tap.py | 24 ++++++++++----- tests/test_tape.py | 53 ++++++++++++++++++++++++++++++-- 11 files changed, 148 insertions(+), 58 deletions(-) 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'