TRI-VOICE is a three-channel music player that simulates polyphonic sound using the single-voice BEEP command by rapidly interleaving notes from three independent voices. Music data for each voice is stored as raw bytes in memory starting at addresses 50000, 50114, and 50372 respectively, with each note encoded as a pair of bytes: pitch value and duration counter. The program uses 254 and 255 as sentinel values — 254 signals a rest/pause for a given voice (substituting another voice’s current note to keep BEEP busy), and 255 marks end-of-stream. The music data block (“TRI-MUSIC”) is 495 bytes loaded at address 50000 (5E4 hex) and was likely produced by a companion tool called MUSIcomp V1.1, which stores note values offset by 36 semitones. A notable bug exists at line 45, where the condition checks `B>=254` twice instead of checking `C>=254`.
Program Structure
The program is divided into a main loop and three symmetric subroutines, one per voice:
- Initialisation (lines 6–20): Variables and voice data pointers are set, and the first note for each voice is loaded via
GO SUB 100,GO SUB 200,GO SUB 300. - Main loop (lines 30–70): Each iteration decrements the three duration counters (
A1,B1,C1), fetches new notes when a counter reaches zero, applies pause-substitution logic, plays the three notes withBEEP, then jumps back to line 30. - Note-fetch subroutines (lines 100–320): Three identical routines read a pitch byte and a duration byte from the relevant pointer (
V1,V2,V3) and advance the pointer by 2. - Loader / saver (lines 9000–9999): Loads the binary music block “TRI-MUSIC” to address 50000 (hex
5E4= decimal 1508, but here used as5E4meaning 5×10⁴ = 50000 in Sinclair floating-point scientific notation), then saves both the BASIC and the code block.
Voice Data Format
Music data for each voice is stored as consecutive byte pairs in a flat binary block loaded at address 50000. The three voices begin at fixed offsets:
| Variable | Start address | Offset from 50000 |
|---|---|---|
V1 | 50000 | 0 |
V2 | 50114 | 114 bytes |
V3 | 50372 | 372 bytes |
Each note entry is two bytes: the first is the pitch (encoded with a +36 offset from MUSIcomp V1.1 format, hence A-36 etc. in the BEEP calls), and the second is a loop-iteration counter controlling how many main-loop cycles that note is held. Sentinel values 254 (rest) and 255 (end of voice) are reserved.
Tri-Voice Simulation Technique
True simultaneous polyphony is impossible with a single BEEP channel, so the program approximates it by playing each voice’s note in rapid succession within every loop iteration (line 60). The durations .014 and .019 seconds are short enough that the ear tends to fuse the three pitches into a chord. Voice 1 and Voice 3 each get 14 ms; Voice 2 gets 19 ms per cycle.
Pause/Rest Substitution Logic
When a voice is resting (pitch byte ≥ 254), the program substitutes another voice’s current pitch so that BEEP is never called with an undefined sentinel value. The substitution chain at lines 46–57 is:
- Lines 46–48: Handle cases where two voices are simultaneously resting — the resting voices borrow the active voice’s pitch.
- Line 50: If only Voice 1 is resting, it borrows Voice 2’s pitch.
- Line 55: If only Voice 2 is resting, it borrows Voice 3’s pitch.
- Line 57: If only Voice 3 is resting, it borrows Voice 1’s pitch (post-substitution value of
A).
The duration counters (A1, B1, C1) are deliberately not altered during substitution, so each voice’s timing advances independently of the borrowed pitch.
Notable Bugs and Anomalies
- Line 45 — duplicate condition: The line reads
IF A>=254 AND B>=254 AND B>=254. The third operand checksBagain instead ofC. This means the all-three-voices-resting guard never fires correctly when onlyCalone is the third resting voice; the intended condition wasC>=254. - Line 48 — self-assignment:
LET C=B: LET B=B— the second assignment is a no-op. The intent was presumably to assign some other value toB, or it may simply be redundant. - Line 42 vs line 100/200/300 termination: The end-of-music check (
IF A=255 THEN STOP) only monitors Voice 1. Voices 2 and 3 will simply freeze at their last counter position if they finish before Voice 1, which is an acceptable design constraint for this format. 5E4as a load address: Sinclair BASIC accepts scientific notation in numeric literals, so5E4evaluates to 50000 — a compact way to express the load address without typing five digits.
Key BASIC Idioms
- Multiple statements on one line with
:to reduce line overhead and speed up execution. PEEKused directly in arithmetic expressions (LET A=PEEK V1) rather than requiring an intermediate variable.- Pointer arithmetic by incrementing
V1,V2,V3by 2 after each read, acting as a simple sequential data iterator entirely within BASIC. - The
SAVE … LINE 9000idiom sets the auto-run line so the loader executes immediately onLOAD.
Content
Source Code
0
1 REM TRI-VOICE (BEEP)
2 REM RESET 1987 BYTE POWER
3 REM WRITTEN BY K. BOISVERT
4
5 REM INITIALISE VARIABLES
6 LET A=0: LET A1=0: LET B=0: LET B1=0: LET C=0: LET C1=0
10 LET V1=50000: LET V2=50114: LET V3=50372
19 REM GET FIRST NOTES
20 GO SUB 100: GO SUB 200: GO SUB 300
29 REM COUNTER FOR VOICE 1 IF 0 THEN GET NEXT NOTE
30 LET A1=A1-1: IF A1=0 THEN GO SUB 100
34 REM COUNTER FOR VOICE 2 IF 0 THEN GET NEXT NOTE
35 LET B1=B1-1: IF B1=0 THEN GO SUB 200
39 REM COUNTER FOR VOICE 3 IF 0 THEN GET NEXT NOTE
40 LET C1=C1-1: IF C1=0 THEN GO SUB 300
41 REM CHECK IF END OF MUSIC YOU MAY NEED TO CHANGE THIS FOR YOUR OWN WAY OF KNOWING WHEN MUSIC IS FINISHED
42 IF A=255 THEN STOP
44 REM SPECIAL CHECK SINCE 3 VOICE MUSIC 2 VOICES CAN BE OFF AT THE SAME TIME!
45 IF A>=254 AND B>=254 AND B>=254 THEN PAUSE 1: GO TO 30
46 IF B>=254 AND C>=254 THEN LET C=A: LET B=A
47 IF A>=254 AND B>=254 THEN LET A=C: LET B=C
48 IF C>=254 AND A>=254 THEN LET C=B: LET B=B
49 REM IF ONLY VOICE 1 IS A PAUSE THEN PLAY THE SAME NOTE AS VOICE 2 BUT STILL USING COUNTER FOR VOICE 1
50 IF A>=254 THEN LET A=B
54 REM IF ONLY VOICE 2 IS A PAUSE THEN PLAY THE SAME NOTE AS VOICE 3 BUT STILL USING COUNTER FOR VOICE 2
55 IF B>=254 THEN LET B=C
56 REM IF ONLY VOICE 3 IS A PAUSE THEN PLAY THE SAME NOTE AS VOICE 1 BUT STILL USING COUNTER FOR VOICE 3
57 IF C>=254 THEN LET C=A
59 REM PLAY THE NOTES ONE RIGHT AFTER THE OTHER A-36 AND B-36 MEANS MUSIcomp DATA -36 (TO GET THE RIGHT NOTE, SEE SEE MUSIcomp V1.1) IF YOU PUT YOUR OWN DATA YOU WILL NOT NEED TO ALTER THE VALUES OF A OR B. IF YOU WANT AN OCTAVE HIGHER, JUST ADD [-/+] 12 TIMES THE # OF OCTAVES TO BE ADDED OR SUBSTRACTED. IE:IF YOU WANT 3 OCTAVES LOWER, SUBSTRACT 36.
60 BEEP .014,A-36: BEEP .019,B-36: BEEP .014,C-36
69 REM COMPLETE LOOP
70 GO TO 30
99 REM IF VOICE FINISHED THEN DO NOT GET ANY MORE NOTES
100 IF A=255 THEN RETURN
109 REM GET NEXT NOTE OF VOICE1
110 LET A=PEEK V1: LET A1=PEEK (V1+1): LET V1=V1+2
120 RETURN
199 REM IF VOICE FINISHED THEN DO NOT GET ANY MORE NOTES
200 IF B=255 THEN RETURN
209 REM GET NEXT NOTE OF VOICE2
210 LET B=PEEK V2: LET B1=PEEK (V2+1): LET V2=V2+2
220 RETURN
299 REM IF VOICE FINISHED THEN DO NOT GET ANY MORE NOTES
300 IF C=255 THEN RETURN
309 REM GET NEXT NOTE OF VOICE3
310 LET C=PEEK V3: LET C1=PEEK (V3+1): LET V3=V3+2
320 RETURN
8999 REM LOADER
9000 LOAD "TRI-MUSIC"CODE 5E4,495: RUN
9998 REM SAVE PROGRAM
9999 SAVE "TRI-VOICE" LINE 9000: SAVE "TRI-MUSIC"CODE 5E4,495
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
