Header Readers is a dual-mode tape header analysis utility that reads and decodes the metadata blocks written at the start of tape-saved files. The program offers two operating modes: a streaming list mode (lines 10–270) that continuously reads and displays headers one after another, and a detailed single-header mode (lines 500–950) that decodes each field individually including program area size, variable area size, auto-start line, array variable names, and starting addresses. Both modes rely on short machine code routines POKEd into RAM at address 30000 (or 29957) to interface directly with the tape hardware. The list mode encodes its machine code as a space-delimited decimal string in n$ and uses VAL slicing to extract and POKE each byte, while the 2068 mode READs its routine from DATA statements. A menu at line 1000 lets the user choose between the two modes at startup.
Program Structure
The listing is divided into three functional blocks, selected from a menu at line 1000:
- Lines 10–270 — Streaming list mode: continuously reads tape headers and prints a compact table showing filename, length, and type remarks.
- Lines 500–950 — Single-header detail mode: reads one header at a time and decodes every field with descriptive labels, with an option to COPY the output or loop for another header.
- Lines 960–990 — A raw hex/decimal dump of the 17 bytes stored from address 30001, reachable from the detail mode flow (after line 900) but never explicitly branched to from the menu.
The entry point is line 1000, which presents a two-option menu. RUN on line 1040 restarts the whole program if neither option is chosen. Line 9900 contains a SAVE command to preserve the program.
Machine Code Injection — List Mode
Lines 40–60 install a machine code routine at address 30000. The bytes are encoded as a space-delimited decimal string in n$. The loop at line 60 iterates 16 times, each iteration calling VAL n$( TO PI) — using PI (≈3.14159) as the upper bound of a string slice, which VAL truncates to 3, effectively reading the first three characters of the remaining string — and then advances n$ forward by 4 characters with n$=a$(4 TO ). Note the reference to a$ here when the variable was named n$, which is a bug: the slice should read n$(4 TO ), so the loop as written will either error or produce incorrect POKEs depending on the value of a$ at runtime.
The DEF FN p(x) at line 40 also establishes p=30016 as a base pointer into the header buffer, used throughout lines 130–240 to PEEK decoded header fields.
Machine Code Injection — Detail Mode
Lines 510–580 use READ/DATA to POKE 44 bytes into addresses 29957–30000. The routine is invoked via RANDOMIZE USR 29957 at line 630. After the call, the decoded header byte at address 30001 holds the file type, and addresses 30002–30017 hold the filename and parameter fields.
Line 540 contains a DATA value of c (an unquoted variable reference) and NOT PI (which evaluates to 0) mixed with numeric literals. The c reference is suspicious — it would be read as the current value of the variable c during the FOR/READ loop, which is the loop control variable itself. This is almost certainly a typo for a numeric literal (likely 0 or another byte value).
Line 610 does POKE 30001,4 to pre-initialize the type byte to an invalid value (4 = “not a header”), then calls CLEAR with no argument — on most implementations this resets variables, which would undo the carefully POKEd routine. This line appears anomalous.
Header Field Decoding
Both modes decode the standard 17-byte tape header format. The field layout assumed by the program is:
| Offset | Field | Mode |
|---|---|---|
| +0 | File type (0=BASIC, 1=Num Array, 2=Char Array, 3=Code) | Both |
| +1 to +10 | Filename (10 chars) | Both |
| +11/+12 | Data length (16-bit LE) | List mode |
| +12/+13 | Data length (detail: 30012/30013) | Detail mode |
| +13/+14 | Parameter 1 (start address or auto-start line) | List mode |
| +14/+15 | Parameter 1 (detail: 30014/30015) | Detail mode |
| +15/+16 | Parameter 2 (program area length for BASIC) | List mode |
| +16/+17 | Parameter 2 (detail: 30016/30017) | Detail mode |
Array Variable Name Decoding
In the list mode (lines 220–230), array variable names are recovered by reading the descriptor byte at p+14, dividing by 32 to get a “tier” value t, and computing the character code as s - 32*t + 96. If t=2 or t=6, a $ suffix is appended for string arrays.
In the detail mode (lines 850–860), the approach is different: for numeric arrays (type 1), two candidate names are computed as CHR$(PEEK 30015 - 64) and CHR$(PEEK 30015 - 32), joined with ” OR “; for character arrays (type 2), CHR$(PEEK 30015 - 128)+"$" and CHR$(PEEK 30015 - 96)+"$".
Notable BASIC Idioms
NOT PIevaluates to 0 (since PI is non-zero, NOT returns 0), used pervasively as a readable zero literal.SGN PIevaluates to 1 (PI > 0), used as the FOR loop start value in line 60.VAL "number"inGO TOandCLEARstatements is a standard memory-saving idiom.TAB PI - LEN STR$ iat line 980 uses PI (≈3) to right-align single and double digit numbers in a column of width 3.PAUSE NOT PI(line 90) pauses for 0 frames, which combined with the subsequent keypress check acts as a wait-for-key idiom.- Line 750 uses
OVER 1to overlay the filenamea$on an underline of underscores, producing an underlined appearance.
Bugs and Anomalies
- Line 60 — wrong variable name: The loop advances the string with
n$=a$(4 TO )but the string being built isn$. This should ben$=n$(4 TO ). As written, this will read froma$(likely empty or undefined), corrupting the POKEd machine code bytes after the first iteration. - Line 540 — bare
cin DATA: The DATA statement contains the tokencas a value. During the READ loop,cis the loop variable, so the byte POKEd will equal the current loop index rather than a fixed opcode. This is almost certainly a missing numeric literal. - Line 610 — bare
CLEAR:CLEARwithout an argument resets all variables. This executes after POKEing machine code into RAM, but the machine code occupies fixed numeric addresses so survivesCLEAR; however, it also resets the RAMTOP, which may reclaim the POKEd area depending on implementation. - Line 900 — typo “Lenght”: The label prints as “Lenght:” instead of “Length:”.
- Line 960 — unreachable from menu: The raw byte dump block at lines 960–990 is never branched to from the main menu or from the detail mode’s normal flow at line 910 (which goes to the INPUT prompt). It appears to be dead code or a debug remnant.
Content
Source Code
5 RUN VAL "999"
10 REM tape analyser
20 BORDER NOT PI:PAPER NOT PI:INK 7
30 CLEAR VAL "29999"
40 DEF FN p(x)= PEEK x+256* PEEK (x+1):LET p=30016
50 LET n$="221 33 64117 17 17 0175 55205 86 5216195 48117"
60 FOR i= SGN PI TO VAL "16":POKE 29999+i, VAL n$( TO PI):LET n$=a$(4 TO ):NEXT i
70 PRINT INVERSE 1;" *Tape Analyser*",
80 PRINT " NAME"; TAB VAL "10";"Length Remarks"'':PLOT NOT PI, VAL "156":DRAW VAL "255", NOT PI
90 PRINT #1;" Start tape, then press any key":PAUSE NOT PI
100 PRINT #1; AT 1, NOT PI,,
110 RANDOMIZE USR 30000
120 LET n$=""
130 LET type= PEEK p:LET l= FN p(p+11):FOR i=p+1 TO p+10:LET n$=n$+ CHR$ PEEK i:NEXT i
140 LET l$= STR$ l:LET l$=" "(1 TO 6- LEN l$)+l$
150 PRINT n$;l$;" ";
160 IF type>3 THEN PRINT "Read Error":GO TO 110
170 IF type<3 THEN GO TO 210
180 LET s= FN p(p+13)
190 IF s=16384 AND l=6192 THEN PRINT "SCREEN$ ":GO TO 210
200 PRINT "CODE at: ";s:GO TO 110
210 IF type=0 THEN GO TO 240
220 LET s= PEEK (p+14):LET t= INT (s/32):LET l$= CHR$ (s-32*t+96):IF t=2 OR t=6 THEN LET l$=l$+"$"
230 PRINT "Array: ";l$:GO TO 110
240 LET l= FN p(p+13):LET s= FN p(p+15)
250 PRINT "Basic";
260 IF l<10000 THEN PRINT " LINE ";l;
270 PRINT :GO TO 110
500 REM "2068hdrrdr"-2068 Header Reader.
510 BORDER NOT PI:PAPER NOT PI:INK 7:CLEAR VAL "29956":RESTORE VAL "500"
520 FOR a=29957 TO 30000
530 READ c:POKE a,c:NEXT a
540 DATA 175,55,221,33,49,117,17,c, NOT PI,243
550 DATA 245,219,255,203,255,211,255,219,244,50
560 DATA 48,117,62,1,211,244,241,205,252,0
570 DATA 58,48,117,211,244,219,255,203,191,211
580 DATA 255,251,201,0
610 POKE 30001,4:CLEAR
620 PRINT AT 10,8; "Start the tape-"
630 RANDOMIZE USR 29957
640 CLS
650 LET type= PEEK 30001
660 IF type>3 THEN PRINT AT 10,9; FLASH 1;"Not a Header":GO TO 930
670 LET a$=""
680 FOR i=0 TO 9
690 LET a$=a$+ CHR$ PEEK (30002+i)
700 NEXT i
710 LET len= VAL "PEEK 30012+256* PEEK 30013"
720 LET start= VAL "PEEK 30014+256* PEEK 30015"
730 LET pa= VAL "PEEK 30016+256* PEEK 30017"
740 LET var=len-pa
750 PRINT "Name:__________; "; OVER 1; AT 0,6;a$''
760 IF type THEN GO TO 830
770 PRINT "Program type:BASIC."''
780 PRINT "Program area:";pa;"bytes."''
790 PRINT "Variables area:";var;"bytes."''
800 IF start>9999 THEN PRINT "no auto-start.":GO TO 910
810 PRINT "auto-start at line";start;"."
820 GO TO 910
830 LET t$="Code Block."
840 IF start=16384 AND len=6912 THEN LET t$="Screen."
850 IF type=1 THEN LET t$="Numeric Array.":LET n$= CHR$ (PEEK 30015-64)+" OR "+ CHR$ (PEEK 30015-32)
860 IF type=2 THEN LET t$="Character Array.":LET n$= CHR$ (PEEK 30015-128)+"$ OR "+ CHR$ (PEEK 30015-96)+"$"
870 PRINT "type:";t$''
880 IF type=1 OR type=2 THEN PRINT "Array Variable Name:";n$''
890 IF type=3 THEN PRINT "Starting Address:";start''
900 PRINT "Lenght:";len;"bytes."
910 INPUT "Copy(y/n)?";r$
920 IF r$="Y" OR r$="y" THEN COPY
930 INPUT "Read another header(y/n)?";r$
940 IF r$="Y" OR r$="y" THEN GO TO 620
950 STOP
960 CLS :PRINT "Bytes Contents"; OVER 1; AT NOT PI, NOT PI;"____ ________"''
970 FOR i=1 TO 17
980 PRINT TAB PI- LEN STR$ i;i; TAB 12- LEN STR$ PEEK (30000+i); PEEK (30000+i)
990 NEXT i:GO TO 910
1000 PAPER VAL "6":BORDER VAL "6":INK VAL "9":CLS
1010 PRINT '''' TAB VAL "10";"Header Readers"'''" 1 LIST Headers"''" 2 1 AT a time"
1020 INPUT "Select 1, 2, ";i:IF i= VAL "1" THEN GO TO VAL "10"
1030 IF i= VAL "2" THEN GO TO VAL "500"
1040 RUN
9900 SAVE "HdrReads 2" LINE 1
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
