Tri-Voice (Beep)

This file is part of Byte Power Spring 1987 . Download the collection to get this file.
Developer(s): Kristian Boisvert
Date: 1987
Type: Program
Platform(s): TS 2068

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:

  1. 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.
  2. 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 with BEEP, then jumps back to line 30.
  3. 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.
  4. Loader / saver (lines 9000–9999): Loads the binary music block “TRI-MUSIC” to address 50000 (hex 5E4 = decimal 1508, but here used as 5E4 meaning 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:

VariableStart addressOffset from 50000
V1500000
V250114114 bytes
V350372372 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 checks B again instead of C. This means the all-three-voices-resting guard never fires correctly when only C alone is the third resting voice; the intended condition was C>=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 to B, 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.
  • 5E4 as a load address: Sinclair BASIC accepts scientific notation in numeric literals, so 5E4 evaluates 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.
  • PEEK used directly in arithmetic expressions (LET A=PEEK V1) rather than requiring an intermediate variable.
  • Pointer arithmetic by incrementing V1, V2, V3 by 2 after each read, acting as a simple sequential data iterator entirely within BASIC.
  • The SAVE … LINE 9000 idiom sets the auto-run line so the loader executes immediately on LOAD.

Content

Appears On

Tape-based magazine.

Related Products

Related Articles

Last month we thought we were finished with the SOUND EFFECTS but we found a method of creating a simulated...

Related Content

Image Gallery

Tri-Voice (Beep)

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.

Scroll to Top