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:
A tape player. It is known that the worst quality the player has, the better the tape recovering results are, since more modern players have more sound processing, that interferes with the encoded data. I used a tape player manufactured by ICE Felix, designed for loading data with HC computers.
A ZX Spectrum or clone. I used an ICE Felix HC-2000.
A PC with Windows/Linux and audio in and out jacks. I used a laptop with 3.5 audio jack that also has the mic input connection on the audio jack.
A cable connecting the tape player to the ZX Spectrum/HC. I used a DIN5 to DIN5 cable, since HC has DIN5 tape input/output. DIN5 is the round male connector with 5 pins.
A cable connecting the ZX Spectrum/HC in/out to the laptop audio in/out. I used a cable I made, with DIN5 connector for HC and 2 male jacks of 3.5 mm. For the laptop I also used a splitter, from the 4 pole 3.5 male jack to 2 female 3.5 jacks, so that I can use the laptop's mic pin for recording.
A cable for current drain between Spectrum/HC computer and the laptop. I used a serial cable I had, connecting the HC to the laptop. You can also use a cable between the audio jack from the laptop and the HC grounding from one of the connectors. Without this kind of cable, you can fry the laptop and/or the HC! Without it, you can notice that the HC computer will reset when connecting it to the laptop audio jack. Thanks to Bogdan M. for clarifying this.
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:
Audacity - used for recording the audio signal from the tape recorder or from the Spectrum/HC computer on the PC, producing a WAV file. https://www.audacityteam.org/download/
MakeTZX by Ramsoft - used to convert the WAV file into TZX. Audio2tape can also be used. https://tzxvault.org/tools.htm
Audio2tape from the fuse-utils package - used to convert the WAV file into TZX. Sometimes this one has better results than MakeTZX, sometimes it's the other way around. https://sourceforge.net/projects/fuse-emulator/files/fuse-utils/1.4.3/fuse-utils-1.4.3-win32.zip/download
Turbo Copy - very useful program running on Spectrum/HC, since it allows loading at variable speeds, supporting speeds from 1400 to 7500 baud, helping with loading problematic tapes or with the tape speed being too fast/slow and most important, supports turbo loaders. I think I found this one included in the Warajevo emulator tape files. When using it on HC, it must be loaded from tape, not from disk, as it doesn't support the HC IF1 features and produces bad copies (found this out the hard way). https://drive.google.com/file/d/1i33nGNjPbziG1efd7dCMTZqi8U2dqL6Q/view?usp=drive_link
Tapir - very nice program for editing and playing TZX files. https://www.alessandrogrussu.it/tapir/
A ZX Spectrum emulator like Spectaculator https://www.spectaculator.com/downloads/ or Fuse https://sourceforge.net/projects/fuse-emulator/
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.
Connect the tape player output to the PC's microphone input.
Record the audio signal from the tape player into Audacity.
Use command "audio2tape -r recording.wav recording.tzx". The flag "-r" will correct the timings to be the ideal timings for the singnal and will create standard TZX blocks instead of custom ones. Program MakeTZX can also be used: "maketzx recording.wav recording.tzx -ln". The flag "-ln" has the same effect as flag "-r" for audio2tape.
Test the resulting recording.tzx in an emulator or play it to the Spectrum/HC to be loaded.
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.
Connect the PC audio jack to the HC/Spectrum computer and transfer program "Turbo Copy".
Connect the tape player to the HC/Spectrum computer and load the program blocks from tape. I used speed 1500 for the normal loading blocks and 3050 for the turbo loading blocks. Several retries might be needed for the turbo blocks. I stopped trying after 3-4 failed attempts for each game.
Connect the HC/Spectrum output to the PC microphone input and record the program saved by Turbo Copy using Audacity on PC. Use speed 1500 for all blocks, since faster speed won't be recognized later by the PC tools.
Convert the WAV file to TZX using audio2tape (works better for me) or MakeTZX.
Open the TZX in Tapir and update the turbo blocks to use the turbo timings. Only the lenght of the 0 and 1 bits are to be changed. The other parameters are same as standard for the turbo blocks, for this tape I have. Bit 0 pulse is changed from 855 to 592. Bit 1 pulse is changed from 1710 to 1180. Bit 1 length is usually twice of the bit 0 length.
TZX files with turbo loading games restored from tape
The Turbo Custom Loader
The BASIC loader shown in Tapir
Analyzing the game loaders
The BASIC loader has a machine code block in line 10, at address 23760.
The first call in the BASIC loader is to the machine code routine at 23760, which replaces the font table with a custom one, generated at runtime based on the default font table. Then it prints the text "Compressed form in RAM by Grig" using embedded color codes, using the custom font.
The BASIC loader loads a standard speed block of 247 bytes, at address range $FF09 - $FFFF, which is the custom turbo tape loader plus code that calls that tape loader, which loads the compressed SCREEN$ and executes it for unpacking, and also loads the compressed main block.
;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
The BASIC loader then calls routine at 23813/$5D05 which does decompression using RLE method for the main game block, starting at the end of the compressed block $E1D4, going in reverse. The RLE unpacker is interesting, as it uses the control byte with value 0 to signal a literal sequence, with length of sequence in the next 2 following bytes, and if it's > 0, it's used as the repeated length, with the repeated value as the next following byte. So it's either "<0><2 bytes literal sequence length>" or "<N><1 byte value, repeated N times>".
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
The tape loader routine looks like this. Only 2 parameters matter: P_COMPARE=$DC=$80+92 instead of $CB in ROM and P_DELAY=$0D instead of $0F in ROM. Resulting designed baud rate is around 2000.
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
The SCREEN$ unpacking routine at $6EBE. Seems to do column based screen display. The logic could be simplified.
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!