Menu

#520 audio2tape discards the final block of a recording

v1.9.2
closed-fixed
nobody
None
5
5 days ago
2026-08-23
AJ B
No

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.

It also costs a byte on ordinary recordings

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

Cause

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.

Fix

--- 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 (.taptape2wavwav, 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.

Related

Bugs: #523

Discussion

  • Fredrick Meunier

    • status: open --> closed-fixed
    • Group: future --> NextRelease
     
  • Fredrick Meunier

    Thanks, addressed in [0a930b] and with a few following cleanups.

     

    Related

    Commit: [0a930b]


    Last edit: Fredrick Meunier 2026-08-24

Log in to post a comment.