Digitizing a ZX Spectrum Tape with Turbo Loading Programs

This page will present a successfull attempt to recover data from a ZX Spectrum tape, that is over 30 years old, with programs that used both normal loading speed and turbo loading speed.

The tape was published by a local recording studio, in my home town of Tulcea, Romania, during my teenage years, in the early 1990's. I was fascinated by it, since it contains about 20 games, modified to load with turbo speed. Games are also compressed using RLE method (run-length encoding). The message shown when each game loads is "Compressed form in RAM by Grig". Kudos to Grig, he did a nice job producing this compilation at the time. 

I was able to recover 4 games from this tape, with the purpose of understanding how they were modified and to create a PC tool that produces the same kind of compressed, turbo-loading games, for ZX Spectrums and clones, that can only load games from tape, providing a loading time that's the shortest possible.

Required hardware:

DIN5 to DIN5 cable

Used between the tape player and the HC computer

DIN5 to dual jack cable

Used between HC computer and PC or between the tape player and PC; The black splitter is usefull for laptops with one audio jack with stereo output and mono microphone input

ICE Felix tape player

It has DIN5 connector audio input/output

Required software:

Tapir with turbo blocks updated

Turbo Copy utility

Audio signal recorded in Audacity

The easy way

This is the simple way of converting data from a tape, but in only works with very good tapes. For most tapes I have, that were recorded at home 25-30 years ago, it doesn't work.


The hard way

For the turbo loading games I had on my tape, the method above didn't work. Better results are found when the HC/Spectrum computer does the digitization of the signal. The procedure will load turbo blocks at 3050 baud, but save them to PC at regular speed of 1500 baud, so that the PC tools can decode the signal. So the sequence is tape->Turbo Copy load at 1500/3050 baud -> Turbo Copy save to PC at 1500 baud -> record WAV with Audacity -> decode WAV with MakeTZX or audio2tape.

TZX files with turbo loading games restored from tape

The Turbo Custom Loader

The BASIC loader shown in Tapir

Analyzing the game loaders

  10 REM   11 INK SGN PI: PAPER SGN PI: BORDER SGN PI: CLEAR VAL "24575": RANDOMIZE USR VAL "23760": PRINT #VAL "0";" [ink 2]C[ink 3]o[ink 4]m[ink 5]p[ink 6]r[ink 7]e[ink 2]s[ink 3]s[ink 4]e[ink 5]d [ink 6]f[ink 7]o[ink 2]r[ink 3]m [ink 4]i[ink 5]n [ink 6]R[ink 7]A[ink 2]M [ink 3]b[ink 4]y [ink 5]G[ink 6]R[ink 7]I[ink 2]G": LOAD ""CODE : RANDOMIZE USR VAL "65289": RANDOMIZE USR VAL "23813"  12 RANDOMIZE USR VAL "52600"
5CD0 1138C7       LD DE,C738 ;custom font table5CD3 D5           PUSH DE5CD4 D5           PUSH DE5CD5 010003       LD BC,0300 ;font table length = 768 decimal5CD8 21003D       LD HL,3D00 ;font table in ROM5CDB EDB0         LDIR5CDD D1           POP DE5CDE 15           DEC D5CDF ED53365C     LD (5C36),DE  ;CHARS variable5CE3 E1           POP HL5CE4 01F802       LD BC,02F8 ;5CE7 7D           LD A,L5CE8 3D           DEC A5CE9 E606         AND 065CEB 57           LD D,A5CEC CB2A         SRA D5CEE 7E           LD A,(HL)5CEF F5           PUSH AF5CF0 3E00         LD A,005CF2 B2           OR D5CF3 F1           POP AF5CF4 2805         JR Z,5CFB5CF6 CB27         SLA A5CF8 15           DEC D5CF9 20FB         JR NZ,5CF65CFB 1F           RRA5CFC B6           OR (HL)5CFD 77           LD (HL),A5CFE 23           INC HL5CFF 0B           DEC BC5D00 78           LD A,B5D01 B1           OR C5D02 20E3         JR NZ,5CE75D04 C9           RET

;loads the SCREEN$ block header and main block, using the address and length from the headerFF09 F3           DIFF0A DD21F5FE     LD IX,FEF5FF0E 111100       LD DE,0011FF11 3E00         LD A,00FF13 37           SCFFF14 CD4FFF       CALL FF4FFF17 30F1         JR NC,FF0AFF19 DD2A02FF     LD IX,(FF02)FF1D ED5B00FF     LD DE,(FF00)FF21 3EFF         LD A,FFFF23 37           SCFFF24 CD4FFF       CALL FF4FFF27 30F0         JR NC,FF19
;Executes the SCREEN$ block, to unpack it and display it. Called code is in section 6 below.FF29 FB           EIFF2A CDBE6E       CALL 6EBE
;Loads the main game header and block and returns to BASIC.FF2D F3           DIFF2E DD21F5FE     LD IX,FEF5FF32 111100       LD DE,0011FF35 3E00         LD A,00FF37 37           SCFFF38 CD4FFF       CALL FF4FFF3B 30F1         JR NC,FF2EFF3D DD2A02FF     LD IX,(FF02)FF41 ED5B00FF     LD DE,(FF00)FF45 3EFF         LD A,FFFF47 37           SCFFF48 CD4FFF       CALL FF4FFF4B 30F0         JR NC,FF3DFF4D FB           EIFF4E C9           RET

5D05 11D4E1       LD DE,E1D4 ;end of block as loaded5D08 21FFFF       LD HL,FFFF ;final destination end address of the block5D0B 1A           LD A,(DE)5D0C FE00         CP 00 ;check if byte is 0, which means literal sequence; if > 0, this byte is the repeated length5D0E 200B         JR NZ,5D1B ;jump to RLE encoded sequence5D10 EB           EX DE,HL ;switch source and destination registers5D11 2B           DEC HL ;read literal sequence length, 2 bytes, in BC5D12 46           LD B,(HL)5D13 2B           DEC HL5D14 4E           LD C,(HL)5D15 2B           DEC HL5D16 EDB8         LDDR ;transfer literal sequence5D18 EB           EX DE,HL ;switch source and destination registers5D19 18F0         JR 5D0B ;read next sequence
5D1B 47           LD B,A ;repeated byte sequence handling; put repeated count to reg. B5D1C 1B           DEC DE5D1D 1A           LD A,(DE) ;put value to repeat in reg. A5D1E 1B           DEC DE5D1F 77           LD (HL),A5D20 2B           DEC HL5D21 10FC         DJNZ 5D1F ;loop for repeated byte sequence length
5D23 D5           PUSH DE5D24 E5           PUSH HL5D25 23           INC HL5D26 11DF60       LD DE,60DF ;this value is the start of unpacked block5D29 B7           OR A5D2A ED52         SBC HL,DE ;check if it reached the beginning of block5D2C E1           POP HL5D2D D1           POP DE5D2E 20DB         JR NZ,5D0B ;restarting loop if not finished yet.5D30 C9           RET

FF4F F3           DIFF50 14           INC DFF51 08           EX AF,AF'FF52 15           DEC DFF53 F3           DIFF54 3E0F         LD A,0F ;border whiteFF56 D3FE         OUT (FE),AFF58 213F05       LD HL,053FFF5B E5           PUSH HL
FF5C DBFE         IN A,(FE)FF5E 1F           RRAFF5F E620         AND 20FF61 F602         OR 02FF63 4F           LD C,AFF64 BF           CP AFF65 C0           RET NZ ;LD-BREAK
FF66 CDE1FF       CALL FFE1 ;LD-EDGE-1FF69 30FA         JR NC,FF65
;LD_WAITFF6B 210A00       LD HL,000A ;short waitFF6E 10FE         DJNZ FF6E
FF70 2B           DEC HLFF71 7C           LD A,HFF72 B5           OR LFF73 20F9         JR NZ,FF6EFF75 CDDDFF       CALL FFDDFF78 30EB         JR NC,FF65 ;LD-BREAK
;LD-LEADERFF7A 069C         LD B,9C ;leader constantFF7C CDDDFF       CALL FFDD ;LD-EDGE-2FF7F 30E4         JR NC,FF65FF81 3EC6         LD A,C6 ;FF83 B8           CP BFF84 30E0         JR NC,FF66 FF86 24           INC HFF87 20F1         JR NZ,FF7A ;LD-LEADERFF89 06C9         LD B,C9FF8B CDE1FF       CALL FFE1FF8E 30D5         JR NC,FF65FF90 78           LD A,BFF91 FED4         CP D4FF93 30F4         JR NC,FF89FF95 CDE1FF       CALL FFE1FF98 D0           RET NC
FF99 79           LD A,CFF9A EE03         XOR 03 ;BLUE & YELLOFF9C 4F           LD C,AFF9D 2600         LD H,00FF9F 06D0         LD B,D0 ;B0 in ROMFFA1 181F         JR FFC2
FFA3 08           EX AF,AF'FFA4 2007         JR NZ,FFADFFA6 300F         JR NC,FFB7FFA8 DD7500       LD (IX+00),LFFAB 180F         JR FFBC ;LD-NEXT
FFAD CB11         RL CFFAF AD           XOR LFFB0 C0           RET NZFFB1 79           LD A,CFFB2 1F           RRAFFB3 4F           LD C,AFFB4 13           INC DEFFB5 1807         JR FFBEFFB7 DD7E00       LD A,(IX+00)FFBA AD           XOR LFFBB C0           RET NZ
FFBC DD23         INC IXFFBE 1B           DEC DEFFBF 08           EX AF,AF'FFC0 06D2         LD B,D2 ;B2 in ROMFFC2 2E01         LD L,01FFC4 CDDDFF       CALL FFDD ;LD-EDGE-2FFC7 D0           RET NC
FFC8 3EDC         LD A,DC ;P_COMPARE=$DC;ROM=$CBFFCA B8           CP BFFCB CB15         RL LFFCD 06D0         LD B,D0FFCF D2C4FF       JP NC,FFC4FFD2 7C           LD A,HFFD3 AD           XOR LFFD4 67           LD H,AFFD5 7A           LD A,DFFD6 B3           OR EFFD7 20CA         JR NZ,FFA3FFD9 7C           LD A,HFFDA FE01         CP 01FFDC C9           RET
FFDD CDE1FF       CALL FFE1 ;LD-EDGE-1FFE0 D0           RET NC
;LD-EDGE-1FFE1 3E0D         LD A,0D ;P_DELAY=$0D=13;ROM=$0FFFE3 3D           DEC AFFE4 20FD         JR NZ,FFE3 ;sampling loop FFE6 A7           AND AFFE7 04           INC BFFE8 C8           RET Z
FFE9 3E7F         LD A,7FFFEB DBFE         IN A,(FE)FFED 1F           RRA;Ignore BREAK key, unlike ROMFFEE A9           XOR CFFEF E620         AND 20FFF1 20F4         JR NZ,FFE7FFF3 79           LD A,CFFF4 2F           CPLFFF5 4F           LD C,AFFF6 E67F         AND 7FFFF8 D3FE         OUT (FE),A
;Extra blue border settingFFFA 3E01         LD A,01FFFC D3FE         OUT (FE),A
FFFE 37           SCFFFFF C9           RET
6EBE 1804         JR 6EC46EC0 01FFFF       LD BC,FFFF ;these are parameters, not code, 4 bytes: 01,FF,FF,08; param 4 is bit mask6EC3 08           EX AF,AF'6EC4 CD7C00       CALL 007C6EC7 3B           DEC SP6EC8 3B           DEC SP6EC9 E3           EX (SP),HL6ECA 33           INC SP6ECB 33           INC SP6ECC A7           AND A6ECD 110700       LD DE,00076ED0 ED52         SBC HL,DE6ED2 E5           PUSH HL6ED3 FDE1         POP IY6ED5 112A01       LD DE,012A6ED8 19           ADD HL,DE6ED9 FD7E00       LD A,(IY+00) ;checks first byte param = 1 and jumps to $6EE56EDC 3D           DEC A6EDD 2806         JR Z,6EE5
6EDF 5E           LD E,(HL) ;if param > 1, loads 2 byte counter in DE and points HL to skip value of counter bytes6EE0 23           INC HL6EE1 56           LD D,(HL)6EE2 19           ADD HL,DE6EE3 18F7         JR 6EDC
6EE5 110040       LD DE,4000 ;points to start of SCREEN$ = $40006EE8 23           INC HL6EE9 23           INC HL6EEA FD7E01       LD A,(IY+01) ;A = FF6EED 3C           INC A6EEE 2004         JR NZ,6EF4 ;doesn't take jump, since A == 0
6EF0 7E           LD A,(HL)6EF1 FD7701       LD (IY+01),A ;puts value from (HL) == 0 in param 2(was $FF)6EF4 23           INC HL6EF5 FD7E02       LD A,(IY+02) ;takes 3rd param == $FF6EF8 3C           INC A6EF9 2004         JR NZ,6EFF ;doesn't take jump6EFB 7E           LD A,(HL)6EFC FD7702       LD (IY+02),A ;puts 3 in param 36EFF 23           INC HL6F00 7E           LD A,(HL)6F01 FD7703       LD (IY+03),A ;puts 0 in param 4
;checks the param 1 - value 3, param 2 - value 4, param 1 - val 4, param 2 - val 0 and sets them to values 0, 1, 1, 1 if params are currently smaller than those values6F04 FD7E01       LD A,(IY+01)6F07 FE03         CP 03 ;check p2 against 36F09 3804         JR C,6F0F6F0B FD360100     LD (IY+01),006F0F FD7E02       LD A,(IY+02)6F12 FE04         CP 04 ;check p3 against 46F14 3804         JR C,6F1A6F16 FD360201     LD (IY+02),016F1A FD8601       ADD A,(IY+01)6F1D FE04         CP 04 ;check p2 against 46F1F 3804         JR C,6F256F21 FD360201     LD (IY+02),016F25 FD7E02       LD A,(IY+02)6F28 A7           AND A ;check p3 against 06F29 2004         JR NZ,6F2F6F2B FD360201     LD (IY+02),01
6F2F 23           INC HL6F30 1E00         LD E,006F32 FD7E01       LD A,(IY+01)6F35 87           ADD A,A6F36 87           ADD A,A6F37 87           ADD A,A6F38 C640         ADD A,406F3A 57           LD D,A6F3B 4F           LD C,A6F3C 0601         LD B,016F3E FDCB0346     BIT 0,(IY+03)6F42 2002         JR NZ,6F466F44 0620         LD B,20
6F46 C5           PUSH BC6F47 FD4602       LD B,(IY+02)6F4A C5           PUSH BC6F4B 0608         LD B,086F4D C5           PUSH BC6F4E 0608         LD B,086F50 C5           PUSH BC6F51 0601         LD B,016F53 FDCB0346     BIT 0,(IY+03)6F57 2802         JR Z,6F5B6F59 0620         LD B,20 ;screen bytes on line == 32 == $206F5B D5           PUSH DE6F5C C5           PUSH BC6F5D 1843         JR 6FA2
6F5F C1           POP BC6F60 13           INC DE6F61 10F9         DJNZ 6F5C 6F63 D1           POP DE6F64 C1           POP BC6F65 14           INC D6F66 10E8         DJNZ 6F50 ;loop ends here for 1/3 of screen?
6F68 7A           LD A,D6F69 D608         SUB 086F6B 57           LD D,A6F6C 7B           LD A,E6F6D C620         ADD A,20 ;go to the next char line on screen6F6F 5F           LD E,A6F70 C1           POP BC6F71 10DA         DJNZ 6F4D
6F73 7A           LD A,D6F74 C608         ADD A,086F76 57           LD D,A6F77 C1           POP BC6F78 10D0         DJNZ 6F4A
6F7A 51           LD D,C6F7B 1C           INC E6F7C C1           POP BC6F7D 10C7         DJNZ 6F46
6F7F 1E00         LD E,006F81 3E58         LD A,58 ;attribute start address == $58006F83 FD8601       ADD A,(IY+01)6F86 57           LD D,A6F87 FD4602       LD B,(IY+02)6F8A 4B           LD C,E6F8B FDCB03DE     SET 3,(IY+03)6F8F C5           PUSH BC6F90 1810         JR 6FA2
6F92 C1           POP BC6F93 0B           DEC BC6F94 13           INC DE6F95 78           LD A,B6F96 B1           OR C6F97 20F6         JR NZ,6F8F
6F99 FD3601FF     LD (IY+01),FF ;set param 2 and 3 to default value $FF6F9D FD3602FF     LD (IY+02),FF6FA1 C9           RET
6FA2 FDCB0356     BIT 2,(IY+03)6FA6 280D         JR Z,6FB56FA8 D9           EXX6FA9 05           DEC B6FAA 79           LD A,C6FAB D9           EXX6FAC 2803         JR Z,6FB16FAE 12           LD (DE),A6FAF 1818         JR 6FC9
6FB1 FDCB0396     RES 2,(IY+03)6FB5 7E           LD A,(HL)6FB6 23           INC HL6FB7 A7           AND A6FB8 200E         JR NZ,6FC86FBA 7E           LD A,(HL)6FBB D9           EXX6FBC 47           LD B,A6FBD D9           EXX6FBE 23           INC HL6FBF 7E           LD A,(HL)6FC0 D9           EXX6FC1 4F           LD C,A6FC2 D9           EXX6FC3 23           INC HL6FC4 FDCB03D6     SET 2,(IY+03)6FC8 12           LD (DE),A6FC9 FDCB035E     BIT 3,(IY+03)6FCD 20C3         JR NZ,6F926FCF 188E         JR 6F5F
;Screen binary follows here.

What's next?

I included in HCDisk a new feature that generates TZX files with turbo blocks. Inspired from  Z802TZX tool from https://worldofspectrum.net/utilities/,  I added conversion for some of the compressed games from here https://github.com/0sAND1s/SpectrumGameCompressor to load from turbo TZX files, making the loading time less than 1 minute for most games!