Skip to content

Commit

Permalink
Add support to tap2sna.py for TZX block type 0x15 (direct recording)
Browse files Browse the repository at this point in the history
  • Loading branch information
skoolkid committed Feb 7, 2024
1 parent b177458 commit 184769f
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 33 deletions.
20 changes: 13 additions & 7 deletions skoolkit/loadtracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ def get_edges(blocks, first_edge, polarity, analyse=False):
tstates += timings.pilot
edges.append(tstates)

# Sync pulses
for s in timings.sync:
# Sync pulses / Pulse Sequence / Direct Recording
for s in timings.sync or timings.pulses:
if analyse:
ear = (len(edges) - 1) % 2
print(f'{tstates:>10} {ear:>3} Pulse ({s} T-states)')
Expand Down Expand Up @@ -102,9 +102,10 @@ def get_edges(blocks, first_edge, polarity, analyse=False):
b *= 2
indexes.append((start, len(edges) - 1))
data_blocks.append(data)
elif i == len(blocks) - 1: # pragma: no cover
# If the last block on the tape contains pulses but no data, add a
# dummy (empty) data block to ensure that the pulses are read
elif i == len(blocks) - 1 or timings.pulses: # pragma: no cover
# If this block contains a Direct Recording, or is the last block
# on the tape and contains a Pulse Sequence, add a dummy (empty)
# data block to ensure that the pulses are read
indexes.append((len(edges) - 1, len(edges) - 1))
data_blocks.append(())

Expand Down Expand Up @@ -241,8 +242,7 @@ def run(self, stop, fast_load, timeout, tracefile, trace_line, prefix, byte_fmt,
write_line(f'Simulation stopped (PC at start address): PC={pc}')
break

if pc == 0x0556 and self.out7ffd & 0x10 and fast_load:
self.fast_load(simulator)
if pc == 0x0556 and self.out7ffd & 0x10 and fast_load and self.fast_load(simulator):
self.index = self.block_max_index
if self.index == max_index:
# Final block, so stop the tape
Expand Down Expand Up @@ -524,6 +524,11 @@ def fast_load(self, simulator):
block = self.blocks[self.block_index]
else:
raise SkoolKitError("Failed to fast load block: unexpected end of tape")
if not block:
# This block has no separately defined data (e.g. Direct Recording
# block), so it can't be fast-loaded
return False # pragma: no cover

memory = simulator.memory
ix = registers[IXl] + 256 * registers[IXh] # Start address
de = registers[E] + 256 * registers[D] # Block length
Expand Down Expand Up @@ -593,3 +598,4 @@ def fast_load(self, simulator):

registers[PC] = 0x05E2
self.announce_data = False
return True
31 changes: 29 additions & 2 deletions skoolkit/tap2sna.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,14 +239,15 @@ class TapeError(Exception):
pass

class TapeBlockTimings:
def __init__(self, pilot_len=0, pilot=0, sync=(), zero=0, one=0, pause=0, used_bits=8, error=None):
def __init__(self, pilot_len=0, pilot=0, sync=(), zero=0, one=0, pause=0, used_bits=8, pulses=(), error=None):
self.pilot_len = pilot_len
self.pilot = pilot
self.sync = sync
self.zero = zero
self.one = one
self.pause = pause
self.used_bits = used_bits
self.pulses = pulses
self.error = error

def get_tape_block_timings(first_byte, pause=3500000):
Expand Down Expand Up @@ -620,7 +621,33 @@ def _get_tzx_block(data, i, sim):
elif block_id == 21:
# Direct recording block
if sim:
timings = TapeBlockTimings(error="TZX Direct Recording (0x15) not supported")
tape_data = []
tps = get_word(data, i)
pause = get_word(data, i + 2)
used_bits = data[i + 4]
num_bytes = get_word3(data, i + 5)
j = 0
pulses = []
prev_bit = data[i + 8] & 0x80
if prev_bit:
pulses.append(0)
bit_count = 0
for j, b in enumerate(data[i + 8:i + 8 + num_bytes], 1):
if j < num_bytes:
num_bits = 8
else:
num_bits = used_bits
for k in range(num_bits):
bit = b & 0x80
if bit == prev_bit:
bit_count += 1
else:
pulses.append(bit_count * tps)
prev_bit = bit
bit_count = 1
b *= 2
pulses.append(bit_count * tps)
timings = TapeBlockTimings(pulses=pulses)
i += get_word3(data, i + 5) + 8
elif block_id == 24:
# CSW recording block
Expand Down
1 change: 1 addition & 0 deletions sphinx/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Changelog

9.2b1
-----
* Added support to :ref:`tap2sna.py` for TZX block type 0x15 (direct recording)

9.1 (2024-02-03)
----------------
Expand Down
6 changes: 4 additions & 2 deletions sphinx/source/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1373,8 +1373,8 @@ To list the options supported by `tap2sna.py`, run it with no arguments::
Set the User-Agent header.
-V, --version Show SkoolKit version number and exit.

Note that `tap2sna.py` cannot read data from TZX block types 0x15 (direct
recording), 0x18 (CSW recording) or 0x19 (generalized data block).
Note that `tap2sna.py` cannot read data from TZX block types 0x18 (CSW
recording) or 0x19 (generalized data block).

By default, `tap2sna.py` attempts to load a tape exactly as a 48K Spectrum
would (see :ref:`tap2sna-sim-load`). If that doesn't work, the ``--ram`` option
Expand Down Expand Up @@ -1676,6 +1676,8 @@ Configuration parameters may also be set on the command line by using the
+---------+-------------------------------------------------------------------+
| Version | Changes |
+=========+===================================================================+
| 9.2 | Added support for TZX block type 0x15 (direct recording) |
+---------+-------------------------------------------------------------------+
| 9.1 | The ``--ram move`` and ``--ram poke`` options can modify specific |
| | RAM banks; added the ``cmio`` simulated LOAD configuration |
| | parameter |
Expand Down
4 changes: 2 additions & 2 deletions sphinx/source/man/tap2sna.py.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ OPTIONS

TZX SUPPORT
===========
``tap2sna.py`` cannot read data from TZX block types 0x15 (direct recording),
0x18 (CSW recording) or 0x19 (generalized data block).
``tap2sna.py`` cannot read data from TZX block types 0x18 (CSW recording) or
0x19 (generalized data block).

SIMULATED LOAD
==============
Expand Down
66 changes: 46 additions & 20 deletions tests/test_tap2sna.py
Original file line number Diff line number Diff line change
Expand Up @@ -2017,42 +2017,68 @@ def test_sim_load_with_unexpected_end_of_tape(self):
self.assertEqual(cm.exception.args[0], f'Error while converting {tapfile}: Failed to fast load block: unexpected end of tape')
self.assertEqual(self.err.getvalue(), '')

@patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
@patch.object(tap2sna, '_write_snapshot', mock_write_snapshot)
def test_sim_load_with_tzx_block_type_0x15(self):
block = [
def test_sim_load_with_tzx_block_type_0x15_first_bit_0(self):
direct_recording_block = (
21, # Block ID
79, 0, # T-states per sample
50, 0, # T-states per sample
0, 0, # Pause
8, # Used bits in last byte
3, 0, 0, # Data length
1, 2, 3, # Data
]
tzxfile = self._write_tzx([block])
z80file = '{}/out.z80'.format(self.make_directory())
with self.assertRaises(SkoolKitError) as cm:
self.run_tap2sna(f'{tzxfile} {z80file}')
self.assertEqual(cm.exception.args[0], f'Error while converting {tzxfile}: TZX Direct Recording (0x15) not supported')
self.assertEqual(self.out.getvalue(), '')
self.assertEqual(self.err.getvalue(), '')
0b00000001, # Data...
0b00000010,
0b00000011
)
tzxfile = self._write_tzx([direct_recording_block])
output, error = self.run_tap2sna(tzxfile)
self.assertEqual(error, '')
self.assertEqual(len(load_tracer.blocks), 1)
timings, data = load_tracer.blocks[0]
self.assertEqual([], data)
self.assertEqual([350, 50, 300, 50, 350, 100], timings.pulses)

@patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
@patch.object(tap2sna, '_write_snapshot', mock_write_snapshot)
def test_sim_load_can_ignore_tzx_block_type_0x15(self):
def test_sim_load_with_tzx_block_type_0x15_first_bit_1(self):
direct_recording_block = (
21, # Block ID
79, 0, # T-states per sample
10, 0, # T-states per sample
0, 0, # Pause
8, # Used bits in last byte
3, 0, 0, # Data length
1, 2, 3, # Data
0b10000000, # Data...
0b00000010,
0b00000011
)
tzxfile = self._write_tzx((
direct_recording_block,
create_tzx_header_block()
))
output, error = self.run_tap2sna(f'--tape-start 2 {tzxfile}')
tzxfile = self._write_tzx([direct_recording_block])
output, error = self.run_tap2sna(tzxfile)
self.assertEqual(error, '')
self.assertEqual(len(load_tracer.blocks), 1)
timings, data = load_tracer.blocks[0]
self.assertEqual([], data)
self.assertEqual([0, 10, 130, 10, 70, 20], timings.pulses)

@patch.object(tap2sna, 'LoadTracer', MockLoadTracer)
@patch.object(tap2sna, '_write_snapshot', mock_write_snapshot)
def test_sim_load_with_tzx_block_type_0x15_unused_bits_in_last_byte(self):
direct_recording_block = (
21, # Block ID
100, 0, # T-states per sample
0, 0, # Pause
4, # Used bits in last byte
3, 0, 0, # Data length
0b00000001, # Data...
0b00000010,
0b11110000
)
tzxfile = self._write_tzx([direct_recording_block])
output, error = self.run_tap2sna(tzxfile)
self.assertEqual(error, '')
self.assertEqual(len(load_tracer.blocks), 1)
timings, data = load_tracer.blocks[0]
self.assertEqual([], data)
self.assertEqual([700, 100, 600, 100, 100, 400], timings.pulses)

@patch.object(tap2sna, '_write_snapshot', mock_write_snapshot)
def test_sim_load_with_tzx_block_type_0x18(self):
Expand Down

0 comments on commit 184769f

Please sign in to comment.