Converting a WAV whose audio ends where the data ends loses the last block completely audio2tape reports success and exits 0.
curl -sSLO https://worldofspectrum.net/pub/sinclair/games/m/ManicMiner.tzx.zip
unzip -j ManicMiner.tzx.zip '*.tzx'
tape2wav "Manic Miner.tzx" mm.wav
audio2tape mm.wav mm-out.tzx
ls -l "Manic Miner.tzx" mm-out.tzx # 33,205 bytes in, 471 bytes out
The log shows five blocks decoded with every checksum passing, and then simply stops:
Block ended, found 19 bytes ... Checksum:PASS Header "ManicMiner"
Block ended, found 80 bytes ... Checksum:PASS
Block ended, found 19 bytes ... Checksum:PASS Header CODE "mmm"
Block ended, found 258 bytes ... Checksum:PASS
Block ended, found 19 bytes ... Checksum:PASS Header CODE "mm1"
found 5 ROM blocks
The 32,770-byte block "mm1" is never written. Of the five that were decoded only four reach the file, because the first one is dropped as well (second cause, below).
Same on other tapes, converted tzx → tape2wav → audio2tape at 44.1 kHz:
| Tape | data blocks in | blocks in the output | bytes in | bytes out |
|---|---|---|---|---|
| Manic Miner | 6 | 4 | 33,205 | 471 |
| Jet Set Willy | 8 | 6 | 41,069 | 8,339 |
| Batman – The Movie | 11 | 9 | 125,290 | 89,318 |
In each case the missing blocks are the first, and the last — which is the largest.
A recording that has hiss or silence afterwards, whose pulse ends the block, so the block is emitted — but one byte short:
| Manic Miner, same 32,770-byte block | result |
|---|---|
| audio ends with the data | block never emitted |
| 1 s of trailing silence appended | 32,769 bytes, Error have incomplete byte (7 bits), Checksum:FAIL |
| 1 s of level-matched noise appended | 32,769 bytes, same failure |
| with the fix below | 32,770 bytes, Checksum:PASS |
Two independent places, both in the emitting path rather than the decoder.
1. No flush at end of input. A block is only ever completed in
converter/getpulse2.cc, when a pulse arrives that is too long to be data:
} else if ( first_pulse + pulse_length > loader->DATA_TOTAL_MAX ) {
loader->end_block( first_pulse, first_tstates );
audio2tape.cc feeds the pulse list to romloader and then asks for the blocks. If the
pulses run out while a block is in progress, end_block() is never called, so the bytes
accumulated in data are dropped without a word — and get_block_count() never counts
them.
2. The first block is gated on a preceding gap. In audio2tape.cc:
tstate_end = rl.get_block_start( i );
if( tstate_start != tstate_end ) {
...write the gap...
rl.get_block( tzx, i, standard_rom_timings ); /* inside the if */
tstate_start = rl.get_block_end( i );
}
A tape whose first block starts at tstate 0 has tstate_start == tstate_end == 0, so the
block is neither written nor does tstate_start advance. Every tape that begins with a
block rather than with leader noise loses it.
--- a/audio2tape.cc
+++ b/audio2tape.cc
@@ -203,6 +203,8 @@
tstates += *i;
}
+ rl.flush( tstates );
+
// get blocks from ROMLoader, filling gaps with data from original tape
std::cout << "found " << rl.get_block_count() << " ROM blocks\n";
@@ -222,12 +224,14 @@
tstate_end );
}
}
+ }
- // and now the ROM block
- rl.get_block( tzx, i, standard_rom_timings );
+ rl.get_block( tzx, i, standard_rom_timings );
- tstate_start = rl.get_block_end( i );
- }
+ tstate_start = rl.get_block_end( i );
}
--- a/converter/romloader.cc
+++ b/converter/romloader.cc
@@ -173,6 +173,16 @@
romloader::get_bits_through_byte()
{
return num_bits;
+}
+
+void
+romloader::flush( double end_tstates )
+{
+ if( data.size() || num_bits )
+ end_block( 0, end_tstates );
}
plus the declaration in converter/romloader.h.
Result — every block recovered, and every payload byte-identical to the original tape:
| Tape | stock | patched |
|---|---|---|
| Manic Miner | 471 bytes, 4 of 6 blocks | 33,304 bytes, 6/6 blocks, all byte-exact |
| Jet Set Willy | 8,339 bytes, 6 of 8 | 41,172 bytes, 8/8, all byte-exact |
| Batman – The Movie | 89,318 bytes, 9 of 11 | 125,338 bytes, 11/11, all byte-exact |
(The output is a little larger than the input because audio2tape writes 0x11 turbo
blocks with measured timings where the source used 0x10 standard blocks; -r uses
idealised ROM timings instead. The payloads are identical either way.)
Same round trip as bug #369 (.tap → tape2wav → wav, fixed 2021, thanks to Alberto
Garcia's patch), which suggests this path is worth a regression test rather than another
decade of luck.
Two other findings in the same tool, reported separately so they can be judged on their own:
audio2tape -k writing a TZX that tzxlist cannot read back, and a divide-by-zero in
romloader::stats() that writes bit1 = 0 into blocks. Neither is needed to reproduce or
fix this one.
Thanks, addressed in [0a930b] and with a few following cleanups.
Related
Commit: [0a930b]
Last edit: Fredrick Meunier 2026-08-24