Sinclair Spectrum casette reading using PC

Casette format

The following casette format information can be found from book "An Expert Guide to the Spectrum" written by Mike James and published by Granada. I have found the information be quite accurate.

Spectrum casette file consist of two parts: header part and the actual file part. Both parts have same basic structure: first there is loader tone, then a sync pulse and after it the actual data.

Loader tone signal signal is 614.9 microsecond low and 614.9 microseconds high. The sunc pulse is 190.6 microseconds low and 210 microseconds high. In data part one bit is 488,6 microseconds low and 488.6 microsecons high. Zero bit is 244.3 microseconds low and 244.3 microsecond.

The data is stored so that most significant bit of byte is saved first and least signicant bit last. The first byte of the data part of file tells the type of file and the actual data follows after it. The last byte in data part is cheksum which is obtained by xoring all data bytes together.

Decoding program

And here is a simple Turbo Pascal program which I have used to load files from spectrum tapes. It works nicely when Spectrum is directly connected to SoundBlaster and I save file in spectrum and start that loading program in PC. With casette deck test have been less succesful. The program loads only the header part, but can be easily extended.

Program Spectrum_to_PC;
 
{ Sinclair Spectrum casette loader for IBM PC compatibles
  with Sound Blaster. Sound Blaster must be at default I/O address 220h.
 
 
  Copyright 1992 Tomi Engdahl
 
 
  Sinclair Spectrum is a registered trade mark of Sinclair Reserch Ltd.
  Sound Blaster is a register trade mark of Creative Labs Inc.
 
}
 
Uses crt;
 
Var
   timer_count:word;
   tmp:byte;
   old_bit:byte;
 
Const
   Timer0=$40;
   TimerCtlr=$43;
   TimerClk=1193180;
 
   Audio0=128;
   hysteresis=20;
 
   mid_value=2000;  {2000}
 
   ltone_HI=4000;
 
 
Procedure CLI;
Begin
   Inline($FA);                  {cli, disable interrupts}
End;
 
Procedure STI;
Begin
   Inline($FB);                  {cli, enable interrupts}
End;
 
Function Timer0Read:word;
Var
   value:word;
Begin
   Port[TimerCtlr]:=0;        {latch timer 0}
   value:=Port[Timer0];
   value:=value+(Port[Timer0] shl 8);
   Timer0Read:=value;
End;
 
Function time_difference:word;
Var value:word;
Begin
{R-}
   Port[TimerCtlr]:=0;        {latch timer 0}
 value:=Port[Timer0];
   value:=value+(Port[Timer0] shl 8);
   time_difference:=timer_count-value;
   timer_count:=value;
{R+}
End;
 
Function Read_SBADC:byte;
Begin
      Inline(
      $BA/$2C/$02/      {MOV     DX,022C    }
      $EC/              {IN      AL,DX      }
      $A8/$80/          {TEST    AL,80      }
      $75/$FB/          {JNZ     0107       }
      $B0/$20/          {MOV     AL,20      }
      $EE/              {OUT     DX,AL      }
      $BA/$2E/$02/      {MOV     DX,022E    }
      $EC/              {IN    AL,DX      }
      $A8/$80/          {TEST    AL,80      }
      $74/$FB/          {JZ      0112       }
      $BA/$2A/$02/      {MOV     DX,022A    }
      $EC/              {IN      AL,DX      }
      $A2/tmp);      {MOV     a,AL       }
 
      Read_SBADC:=tmp;
End;
 
{Function schmitt_trigger(value:byte):byte;
Begin
   If old_bit=0 Then If value>(audio0+hysteresis) Then old_bit:=1;
   If old_bit=1 Then If value<(audio0-hysteresis) Then old_bit:=0;
   schmitt_trigger:=old_bit;
End;}
 
Function schmitt_trigger(value:byte):byte;
Begin
   If old_bit=0 Then If value>(audio0+hysteresis) Then old_bit:=1;
   If old_bit=1 Then If value<(audio0-hysteresis) Then old_bit:=0;
   schmitt_trigger:=old_bit;
End;
 
Function bit_from_tape:byte;
Var
   adcvalue:byte;
   count:integer;
Begin
   Repeat Until schmitt_trigger(Read_SBADC)=0;
   Rei_difference>mid_value Then bit_m_tape:=1
   Else bit_from_tape:=0;
End;
 
Procedure Wait_loader_tone;
Var
   diff:word;
   count:integer;
Begin
   diff:=0;
   Repeat
       Repeat Until schmitt_trigger(Read_SBADC)=0;
       Repeat Until schmitt_trigger(Read_SBADC)=1;
       diff:=time_difference;
       If (diff>mid_value) and (diff<Ltone_hi) Then Inc(count)
       Else count:=0;
   Until count>=10;
End;
 
 
 
{******* High level packet routines ************* }
 
Var tavut:array[0..50000] of byte;
 
Procedure load_bytes(maara:word);
Var
   tavu,p,a,n:byte;
   laskuri:word;
Begin
   ClrScr;
   Repeat Until bit_from_tape=1;          {wait for loader tone}
   Writeln('loader tone detected');
   Repeat Until bit_from_tape=0;          {synclse wait}
   {Writeln('sync');}
   For n:=0 to 7 Do p:=bit_from_tape;     {over the start byte}
   laskuri:=0;
   Repeat
      tavu:=0;
      For n:=0 to 7 Do tavu:=(tavu shl 1)+bit_from_tape;
      tavut[laskuri]:=tavu;
      Writeln(tavu);
      Inc(laskuri);
    Until laskuri>=maara;
End;
 
Procedure print_header;
Var a:integer;
Begin
   Writeln;
   Write('Program: ');
   For a:=1 to 10 Do
   Begin
      Writhr(tavut[a]));
   End;
   Writeln;
   Writeln('Start:   ',tavut[13]+256*tavut[14]);
   Writeln('Length:  ',tavut[11]+256*tavut[12]);
End;
 
Procedure header_load;
Begin
   load_bytes(17);
   print_header;
End;
 
 
{ ************* Main program ************** }

 dummy:word;
Begin
   CLI;
   old_bit:=0;
   dummy:=time_difference;
   Wait_loader_tone;
   header_load;
   STI;
   Repeat Until KeyPressed;
End.

Tomi Engdahl <[email protected]>