Music-Maker is a ZX81/TS1000 melody composition and playback program that lets the user define a tune measure by measure, specifying pitch and duration values for each note. Playback is driven by machine code accessed via USR 16514, with timing and pitch values written directly to memory locations 16516, 16518, 16520, and 16528 using POKE statements. The tempo is stored as a single-element array Z(1), calculated as 960 divided by the user-supplied tempo value, and passed to the machine code routine. Notes are stored in parallel arrays N() for pitch and D() for duration, with pitch value 100 used as a special rest code that triggers POKE 16528,255 instead of 254. After entry, the melody can be replayed, have its tempo adjusted, or be saved to tape under the user-supplied melody name.
Program Analysis
Program Structure
The program is divided into several functional sections linked by GOTO jumps rather than subroutines:
- Lines 20, 180–210: Title screen and initialisation — displays the “MUSIC-MAKER” and “MOUNTAINEER SOFTWARE 1984” banners, then clears the screen.
- Lines 220–350: Session setup — prompts for melody name (
X$), number of measures (M), and tempo (T); computesZ(1)=960/T. - Lines 360–670: Note entry — iterates over each measure (
J) and each note within it (K), collecting pitch intoN(I)and duration intoD(I). - Lines 30–170: Playback loop — executes the machine code routine for each note, then presents a post-play menu (replay, tempo change, or tape save).
- Lines 680–850: Tape save — counts down and issues
SAVE X$to record the program under the melody name. - Lines 860–940: Tempo change — re-prompts for
Tand recomputesZ(1)before returning to the menu. - Lines 950–960: Program self-save at line 950, followed by
RUN.
Machine Code Sound Routine
All audio output is delegated to a machine code routine whose entry point is address 16514. Four memory locations are configured via POKE before each USR call:
| Address | Variable | Purpose |
|---|---|---|
| 16516 | Z(1) | Tempo/timing constant (960÷T), set once per play |
| 16520 | N(I) | Pitch value for current note |
| 16518 | D(I)*1000/N(I) | Duration scaled by pitch |
| 16528 | 254 or 255 | Rest flag: 255 = rest (N=100), 254 = normal note |
The machine code is not defined within this BASIC listing; it is presumed to reside in memory already (likely POKEd in elsewhere or present in a companion loader). The result of USR 16514 is assigned to A at line 100 but the value is never used — this is simply the idiomatic way to call a machine code routine that returns a value.
Data Storage
Three arrays hold the session data:
Z(1)— single-element array holding the tempo constant passed to machine code.N(X)— pitch values; pitch 100 is the special rest code.D(X)— duration values; duration for noteIis passed asD(I)*1000/N(I), scaling duration relative to pitch.A(M)— note counts per measure; summed during entry to compute total note countX.
The total note count X is accumulated at line 410 inside the measure-count loop, and the N() and D() arrays are dimensioned only after all measure sizes are known (line 460–470), which is a correct two-pass approach.
Key BASIC Idioms
- Busy-wait loops:
FOR I=1 TO 50: NEXT I(and similar) are used as crude delays throughout, a common ZX81 technique in the absence of aPAUSEcommand in FAST mode. - FAST/SLOW switching: Playback runs inside
FAST(line 30) to maximise the timing accuracy available to the machine code, withSLOW(line 120) restored for display output. - Centred text:
AT 2,16-(INT (LEN X$/2));X$at lines 380 and 510 centres the melody name by computing its half-length offset — a standard ZX81 centring idiom. - INKEY$ polling: Lines 140 and 700 use the
IF INKEY$="" THEN GOTOspin-wait pattern to wait for any keypress.
Notable Techniques
The tempo formula Z(1)=960/T converts a musical tempo number into a machine-code-friendly period constant. The duration scaling D(I)*1000/N(I) means that for a given user duration value, higher-pitched (higher-frequency) notes have proportionally shorter machine code duration counts, which compensates for the relationship between pitch period and perceived note length.
The post-play menu (lines 130–170) is implemented without a PAUSE — it waits for a non-empty INKEY$ at line 140, then tests the same INKEY$ again at lines 150 and 160. Because INKEY$ is re-evaluated on each reference, there is a race condition: by the time lines 150 or 160 are reached, the key may have been released, causing the test to fail and falling through to line 170 (replay) even if S or T was pressed. A held key press is needed to reliably reach those branches.
Bugs and Anomalies
- INKEY$ race (lines 140–160): As noted, each reference to
INKEY$is a fresh read; a brief keypress detected at line 140 may not still be held when tested at lines 150 and 160. - Missing line 630: The note-entry loop ends with
NEXT Jat line 620, but line numbers jump from 620 to 640 — line 630 is absent. This is harmless as it causes no execution gap. - Unused variable A:
LET A=USR 16514at line 100 stores the machine code return value in scalarA, which conflicts with the arrayA(M)dimensioned at line 360. On the ZX81, assigning to scalarAafterDIM A(M)would erase the array. However, by the time playback reaches line 100, note entry is complete andA()is no longer needed, so no data is lost in practice. - Tape save countdown: The inner loop
FOR J=1 TO 10: NEXT Jin the countdown (lines 730–740, 770–780) provides very short delays between countdown digit updates — the countdown will appear nearly instantaneous rather than one second per digit.
Content
Source Code
10 REM NEXT A\##I-: INKEY$PEEK COPY ( UNPLOT INKEY$<= RETURN ( UNPLOT H4 LET 94 GOTO TAN
15 REM "MUSIC-MAKER" 1984 BILL FERREBEE 749 HILL STREET NO. 6 PARKERSBURG, WV 26104 TO SAVE GOTO 950
20 GOTO 180
30 FAST
40 POKE 16516,Z(1)
50 FOR I=1 TO X
60 POKE 16520,N(I)
70 POKE 16518,D(I)*1000/N(I)
80 IF N(I)=100 THEN POKE 16528,255
90 IF N(I)<>100 THEN POKE 16528,254
100 LET A=USR 16514
110 NEXT I
120 SLOW
130 PRINT AT 7,4;"PRESS % %S%P%A%C%E% TO STOP";AT 9,3;"PRESS % %T% TO CHANGE TEMPO";AT 11,0;"PRESS % %S% TO SAVE MELODY ON TAPE";AT 13,1;"PRESS ANY OTHER KEY TO REPLAY"
140 IF INKEY$="" THEN GOTO 140
150 IF INKEY$="S" THEN GOTO 680
160 IF INKEY$="T" THEN GOTO 860
170 GOTO 30
180 PRINT AT 10,9;"% %M%U%S%I%C%-%M%A%K%E%R% ";AT 21,2;"% %M%O%U%N%T%A%I%N%E%E%R% %S%O%F%T%W%A%R%E% % %1%9%8%4% "
190 FOR I=1 TO 100
200 NEXT I
210 CLS
220 DIM Z(1)
230 LET X=0
240 PRINT TAB 6;"% %M%U%S%I%C%-%M%A%K%E%R% %L%O%A%D%E%R% ";AT 3,0;"NAME OF MELODY ?: ";
250 INPUT X$
260 PRINT X$;AT 5,0;"HOW MANY MEASURES ?:";
270 INPUT M
280 PRINT M
290 PRINT AT 7,0;"TEMPO ?:";
300 INPUT T
310 PRINT T
320 LET Z(1)=960/T
330 FOR I=1 TO 50
340 NEXT I
350 CLS
360 DIM A(M)
370 FOR I=1 TO M
380 PRINT AT 0,6;"% %M%U%S%I%C%-%M%A%K%E%R% %L%O%A%D%E%R% ";AT 2,16-(INT (LEN X$/2));X$;AT 4,0;"HOW MANY NOTES IN MEASURE ";I;" ?:";AT 4,30;" "
390 INPUT A(I)
400 PRINT AT 4,30;A(I)
410 LET X=X+A(I)
420 FOR J=1 TO 50
430 NEXT J
440 NEXT I
450 CLS
460 DIM N(X)
470 DIM D(X)
480 LET I=1
490 FOR J=1 TO M
500 FOR K=1 TO A(J)
510 PRINT AT 0,16-(INT (LEN X$/2));X$
520 PRINT AT 2,9;" ";TAB 21;" ";AT 2,0;"MEASURE :";J;TAB 15;"NOTE :";K
530 PRINT AT 4,9;" ";AT 6,12;" "
540 PRINT AT 4,0;"% %P%I%T%C%H% :";
550 INPUT N(I)
560 PRINT N(I)
570 PRINT AT 6,0;"% %D%U%R%A%T%I%O%N% :";
580 INPUT D(I)
590 PRINT D(I)
600 LET I=I+1
610 NEXT K
620 NEXT J
640 FOR I=1 TO 50
650 NEXT I
660 CLS
670 GOTO 130
680 CLS
690 PRINT AT 9,0;"PREPARE TAPE RECORDER FOR SAVING";AT 11,3;"(PRESS ANY KEY WHEN READY)"
700 IF INKEY$="" THEN GOTO 700
710 PRINT AT 9,0;" START TAPE RECORDER NOW... ";AT 11,3;" COUNTDOWN : 5 "
720 FOR I=4 TO 0 STEP -1
730 FOR J=1 TO 10
740 NEXT J
750 PRINT AT 11,21;I
760 NEXT I
770 FOR J=1 TO 10
780 NEXT J
790 CLS
800 SAVE X$
810 PRINT AT 10,5;"NAME OF SAVED PROGRAM:";AT 11,16-(INT (LEN X$/2));X$
820 FOR I=1 TO 100
830 NEXT I
840 CLS
850 GOTO 130
860 CLS
870 PRINT AT 1,0;"TEMPO ?:";
880 INPUT T
890 PRINT T
900 LET Z(1)=960/T
910 FOR I=1 TO 50
920 NEXT I
930 CLS
940 GOTO 130
950 SAVE "1023%9"
960 RUN
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
