Tandy BASIC MML
Tandy BASIC MML is a Music Macro Language created by Microsoft initially for the IBM PCjr, but was later adopted by the Tandy Corporation for their Tandy 1000 computer line. The MML consists of single-character commands, usually followed by a number, to represent notes as well as various other musical functions. It is based on the earlier Microsoft BASIC MML but, instead of sending audio through the PC Speaker, it uses the Tandy 3 Voice (or PCjr 3 Voice). This MML was first used in a version of Microsoft Advanced BASIC (BASICA) that had been modified to work with the IBM PCjr, and was ported forward into a special version of GW-BASIC for Tandy models. Although this MML was initially designed for the PCjr, the name Tandy BASIC MML is used because the Tandy was vastly more popular, and most people experienced the MML through it.
Tandy BASIC MML has four statements that can produce sound: BEEP, NOISE, PLAY, and SOUND. PLAY is the most complex of these statements and exclusively uses the Tandy BASIC MML. Games programmed in a Microsoft BASIC language on the PCjr or Tandy 1000 most likely used these statements for their sound.
Contents
Output
The PCjr and Tandy 1000 computers had different methods of outputting audio. The PCjr had an internal PC Speaker and could output the PCjr 3 Voice to a television speaker. Early models of the Tandy 1000 had a large internal speaker that could play the Tandy 3 Voice without the need for an external speaker as well as an RCA jack for sending audio to an external speaker. Later versions of the Tandy 1000 eliminated the RCA jack, but included a headphone jack.
By default in Tandy BASIC, audio is sent to both speakers, but each can be individually turned on or off to eliminate an unwanted echo. This is accomplished by toggling BEEP (for the internal speaker) and SOUND (for the external speaker). The choice of names for the commands may seem odd, but they were originally designed with the PCjr in mind, and the PCjr only had a PC speaker internally, which was only useful for basic sound like BEEP.
The web site Nerdly Pleasures goes into more detail on how the speaker configuration worked on the various models that supported Tandy BASIC: nerdlypleasures.blogspot.com/2013/06/ibm-pcjr-and-tandy-1000-sound.html
Modes
Microsoft's BASIC for the Tandy and PCjr supports two audio modes, foreground and background. When audio is set to foreground mode, each PLAY statement is processed completely before the next BASIC statement is processed. When audio is set to background mode, PLAY statements are put into the PLAY buffer and played in the background while additional BASIC statements are processed. You can see this at work in the following example:
10 INPUT "Press ENTER to see Foreground Mode:", A$ 20 PLAY "MF C D E F G A B", "O2 C P4 E P4 G P4 B-" 30 PRINT "Done" 40 INPUT "Press ENTER to see Background Mode:", A$ 50 PLAY "MB C D E F G A B", "O2 C P4 E P4 G P4 B-" 60 PRINT "Done"
However, the PLAY buffer only holds 32 commands. If a PLAY statement exceeds this buffer, BASIC halts further execution until only 32 commands are left and those are then put in the PLAY buffer and processed in the background. You can see this result in the following example:
10 PLAY "MB L8 O0 C D E F G A B O1 C D E F G A B" 20 PLAY "O2 C D E F G A B O3 C D E F G A B" 30 PLAY "O4 C D E F G A B O5 C D E F G A B" 40 PLAY "O6 C D E F G A B" 40 PRINT "Buffer no longer filled"
Since Microsoft BASIC remains halted as long as the PLAY buffer is filled, to have background music you must either use very short tunes, or slowly stream commands into the buffer as your program runs. Streaming into the buffer is made possible with the PLAY(n) or ON PLAY statements.
Statements
BEEP
The BEEP statement plays an 800 Hz sound for one-quarter of a second, usually to give audio feedback to the user.
BEEP [switch]
- switch is an optional argument to toggle the internal speaker and must be either ON or OFF.
If switch is absent, the sound of the BEEP will played on the active speakers. Setting the switch to ON will turn on the internal speaker, and all future BEEP, NOISE, PLAY and SOUND statements will be played on the internal speaker. Setting the switch of OFF will turn off the internal speaker and no audio statements will be heard on the internal speaker. BEEP (and the other audio statements) can also be sent to the external speaker by toggling the SOUND statement.
This example program showcases how BEEP is played and how it can be sent to different output:
10 PRINT "Sending a beep to the internal speaker:" 20 BEEP ON: SOUND OFF 30 BEEP 40 PRINT "Sending a beep to the external speaker:" 50 BEEP OFF: SOUND ON 60 BEEP 70 PRINT "Sending a beep to both speakers:" 80 BEEP ON: SOUND ON 90 BEEP 100 PRINT "Muting both speakers:" 110 BEEP OFF: SOUND OFF 120 BEEP
Notes:
- The sound BEEP makes can be duplicated with the statement PRINT CHR$(7).
- BEEP is not affected by background audio mode.
NOISE
NOISE generates noise through the Tandy 3 Voice (or PCjr 3 Voice) to the external speaker as specified by the arguments.
NOISE source, volume, duration
- source must be an integer from 0 to 7. The first four values generate periodic noise (buzz sound), while the remaining four generate white noise (static). Each is generated at different frequencies. If source is set to 3 or 7, the frequency used will be the last frequency set by SOUND for channel 2. See SOUND for more details.
Source | Noise Type | Frequency Estimate | Frequency Precise |
---|---|---|---|
0 | Periodic | 6,999.1 | 3,579,545 / 512 |
1 | Periodic | 3,495.6 | 3,579,545 / 1,024 |
2 | Periodic | 1,747.8 | 3,579,545 / 2,048 |
3 | Periodic | Custom | See above |
4 | White | 6,999.1 | 3,579,545 / 512 |
5 | White | 3,495.6 | 3,579,545 / 1,024 |
6 | White | 1,747.8 | 3,579,545 / 2,048 |
7 | White | Custom | See above |
- volume must be an integer from 0 to 15. 0 is silent, 1 is the quietest, and 15 is the loudest; the default is 8.
- duration must be a number from 0 to 65535. It represents the length of the noise in clock cycles, of which there are 18.2 cycles to a second. Despite accepting such a 16-bit integer, the NOISE function automatically stops after 5 seconds (or 91 clock cycles).
NOISE is not affected by foreground mode. All NOISE commands are sent into the background PLAY buffer and processed in turn. However, whenever a SOUND OFF statement or a NOISE statement with a duration of 0 is add to the buffer, the background buffer is immediately cleared of all audio statements.
This example will let you test NOISE with its various volumes, frequencies, and types.
10 CLS: SOUND ON 20 V = 8: F = 2000: T$ = "PERIODIC" 30 PRINT " -= NOISE TEST =-" 40 PRINT 50 PRINT "+ , - : Adjust volume" 60 PRINT "< , > : Adjust frequency by 1" 70 PRINT ", , . : Adjust frequency by 10" 80 PRINT "SPACE : Toggle noise type" 90 PRINT " ESC : Exit." 100 IF T$ = "PERIODIC" THEN SOURCE = 3 ELSE SOURCE = 7 110 NOISE 0, 0, 0 120 SOUND F, 0.01, 0, 2 130 NOISE SOURCE, V, 91 140 LOCATE 9, 1 : PRINT "Volume: " + STR$(V) + " " 150 LOCATE 10, 1 : PRINT "Frequency: " + STR$(F) + " " 160 LOCATE 11, 1 : PRINT "Type: " + T$ + " " 170 U = 0 180 K$ = INKEY$ 190 IF K$ = "-" OR K$ = "_" THEN U = 1 : V = V - 1 : IF V < 0 THEN V = 0 200 IF K$ = "=" OR K$ = "+" THEN U = 1 : V = V + 1 : IF V > 15 THEN V = 15 210 IF K$ = "," THEN U = 1 : F = F - 10 : IF F < 1 THEN F = 1 220 IF K$ = "." THEN U = 1 : F = F + 10 : IF F > 32767 THEN F = 32767 230 IF K$ = "<" THEN U = 1 : F = F - 1 : IF F < 1 THEN F = 1 240 IF K$ = ">" THEN U = 1 : F = F + 1 : IF F > 32767 THEN F = 32767 250 IF K$ = " " THEN U = 1 : IF T$ = "PERIODIC" THEN T$ = "WHITE" ELSE T$ = "PERIODIC" 260 IF K$ = CHR$(27) THEN NOISE 0, 0, 0 : END 270 IF U = 1 THEN GOTO 100 280 GOTO 170
The SOUND statement can be used concurrently with the NOISE statement. For example:
10 SOUND ON 20 NOISE 0, 10, 30 30 SOUND 400, 10, 15, 0 40 SOUND 500, 10, 15, 1 50 SOUND 600, 10, 15, 2 60 NOISE 4, 10, 30 70 SOUND 400, 10, 15, 0 80 SOUND 500, 10, 15, 1 90 SOUND 600, 10, 15, 2
Notes:
- NOISE will cause an error unless SOUND is set to ON.
ON PLAY(n)
ON PLAY creates an event trap which sends the execution pointer to the specified subroutine when the number of commands in background buffer goes one below n. This is similar to PLAY(n), however PLAY(n) is preferred in most implementations because it is easier to use and debug and runs faster in most instances and supports the Tandy 3 Voice (or PCjr 3 Voice) channels individually. ON PLAY, being event driven, is less likely to encounter interruptions in music when a program is running particularly slow, but such slowdowns shouldn't exist in properly optimized code. ON PLAY only works in background mode. Here is the syntax:
ON PLAY(n) GOSUB linenumber PLAY action
- n can be a number between 1 and 32.
- linenumber must be a line number in the program or 0.
- action must be either ON, OFF, or STOP.
Once an ON PLAY statement is processed, you can activate the event with PLAY action. Setting the action to ON will cause BASIC check the number of background PLAY commands in any of the three channels after every BASIC statement is processed, if the number in any of the channels is one less than the number specified, process the GOSUB. When the action is set OFF, BASIC will stop checking. When the action is set to STOP, BASIC will continue to check the PLAY buffer after every statement, but it will not process the GOSUB when the number of PLAY commands goes one below n. However, BASIC will remember that it has gone below, and the next time you set PLAY to ON, it will process the GOSUB immediately. This example will play Ode to Joy in the background while still allowing user input:
10 DIM MUSIC$(4) 20 MUSIC$(1) = "E8 E8 F8 G8 G8 F8 E8 D8 C8 C8 E8 E8 E8 D12 D4" 30 MUSIC$(2) = "E8 E8 F8 C8 G8 F8 E8 D8 C8 C8 D8 E8 D8 C12 C4" 40 MUSIC$(3) = "D8 D8 E8 C8 D8 E12 F12 E8 C8 D8 E12 F12 E8 D8 C8 D8 P8" 50 MUSIC$(4) = "E8 E8 F8 G8 G8 F8 E8 D8 C8 C8 D8 E8 D8 C12 C4" 60 MUSICPART = 0 70 PLAY "MB O2 T120" 80 ON PLAY(1) GOSUB 1000 90 PLAY ON 100 GOSUB 1000 500 K$ = INKEY$ 510 IF LEN(K$) > 0 THEN PRINT K$ 520 IF K$ = CHR$(27) THEN END 530 GOTO 500 1000 MUSICPART = MUSICPART + 1 1010 IF MUSICPART > 4 THEN MUSICPART = 1 1020 PLAY MUSIC$(MUSICPART) 1030 RETURN 500
Notes:
- When ON PLAY processes its GOSUB, it also sets the PLAY to STOP to prevent recursive traps. When the next RETURN statement is processed, PLAY is automatically set back to ON unless you explicitly set it to OFF in the subroutine.
- If an ON ERROR event is processed, all other events, including ON PLAY, are set to OFF.
- Setting the GOSUB linenumber to 0 is the same as a PLAY OFF statement.
- Since ON PLAY is usually set before the main loop, it is common to end the subroutine with RETURN n, where n is a line number inside the main loop so as to not reset the program.
PLAY
The MML of Tandy BASIC is primarily implemented with the PLAY statement. It accepts up to three arguments, each must be a character string containing the commands you wish to play. Here is the syntax:
PLAY string [, string] [, string]
- string must be a character string consisting of commands using Tandy BASIC MML which are described below. Each argument is played through its respective channel of the Tandy 3 Voice (or PCjr 3 Voice). For example:
10 PLAY "T200", "T200", "T200" 20 PLAY "O4 G8 G8 P8 G8 P8 E8 G8 P8 B8", "O3 A#8 A8 P8 A8 P8 A8 A8 P8 O4 D8", "O1 D8 D8 P8 D8 P8 D8 D8 P8 O2 B8"
You can include a blank string if you want to skip one or both of the first two channels. This can be useful if you're using one channel for sound effects and the other two for music. For example, this audio will only play on the 2nd and 3rd channels, leaving the 1st free:
10 PLAY "", "E F G", "D E F"
Here is a complete list of the MML commands accepted by the PLAY statement.
Command | Mnemonic For | Description |
---|---|---|
A-G[n] | A, B, C, D, E, F, and G | Plays the corresponding note. The octave is set by O, default 2. The length of the note is set by L, default 1 (a whole note). The tempo is set by T, default 120. Each note may be followed by n, a number from 1-64, indicating the duration of the note; 1 is a whole note, 2 is a half, 4 is a quarter, 8 is an 8th, and so on. Irregular note lengths like 27 are allowed. When n is present, the length set by L is ignored. |
Ln | Length | Sets the length of each note equal to n for those notes without an optional duration. 1 is a whole note, 2 is a half, 4 is a quarter, etc., up to 64. The default length is 4. This is useful for simplifying a PLAY command. For example the two following lines will produce identical sounds, but the first is less code:
10 PLAY "L16 C D E F G A B" 20 PLAY "C16 D16 E16 F16 G16 A16 B16" |
MF | Music Foreground | Sets audio into foreground mode. PLAY and SOUND statements must be processed completely before BASIC will process the next statement. This is the default mode. PLAY(n) will always return 0 in this mode, and ON PLAY will not fire events in this mode. |
MB | Music Background | Sets audio into background mode. PLAY and SOUND commands will send their commands to the PLAY buffer which can hold up to 32 commands. PLAY(n) will return the number of commands in the buffer, and ON PLAY will fire events in this mode. |
MN | Music Normal | Plays music in normal style. Each note plays for seven-eights of a time set using L. This is the default. |
ML | Music Legato | Plays music in legato style. Each note plays for the full period set by L. |
MS | Music Staccato | Plays music in staccato style. Each note plays for three-quarters of the time set using L. To hear the difference between the three run this example: 10 PLAY "L8" 20 PRINT "Normal " : PLAY "MN C D E F G A B" 30 PRINT "Legato " : PLAY "ML C D E F G A B" 40 PRINT "Staccato" : PLAY "MS C D E F G A B" |
Vn | Volume | Sets the volume to n for the voice in which it's processed. n must range from 0 to 15 where 0 is silence, 1 is the quietest, and 15 is the loudest; the default volume is 8. To hear the volume range, run this example:
10 SOUND ON 20 FOR I = 0 TO 15 30 PLAY "V" + STR$(I) + " C D E" 40 NEXT I |
Nn | Note | Plays note n, Play note n where n is a number from 0 to 84. In the 7 possible octaves, there are 84 notes. If you use 0, it indicates a rest. Just using N, you can play all of the notes you could play using A-G and O, however, most musically inclined readers prefer to read the notes. |
On | Octave | Changes the octave to n where n is a number 0 to 6, to indicate the 7 octaves. The default is 4. Middle C is at the beginning of octave 3. This example will play C across each octave:
10 PLAY "O0 C O1 C O2 C O3 C O4 C O5 C O6 C" |
Pn | Pause | Pauses for the length of n, where 1 is a whole note, 2 is a half note, 4 is a quarter note, etc., up to 64. Unlike notes, the n is mandatory. |
Tn | Tempo | Sets the tempo to n beats per minute. n must be 32-255 and represents the number of quarter notes in a minute. The default is 120. |
-, #, + | Flat, Sharp | - (minus) plays the preceding note flat, # (pound) or + (plus) plays the preceding note sharp. These can only be added to notes that would be a black key on a piano. For example:
10 PLAY "A#8 D-" |
. | Dot | Placing a . (period) after a note increases its play time by 3/2 times the period set by L (length) times T (tempo). You can have multiple dots. A single dot will play the note at one and a half times its normal time, two dots will play at 9/4 times the note's usual time, three dots plays at 27/8 times, and so forth. For example:
10 PLAY "E. F.. G... E8. F8. G8." You can also append a period to P to increase the length of a rest. |
>, < | Octave Change | > (greater-than) raises to the next higher octave, < (less than) decreases to the next lower octave. Octaves are prevented from being lowered below 0 or raised above 7. For example:
10 PLAY "O4 C D > C D > C D < C D < C D < C D" |
Xstring; | Execute | Executes a sub-string within the current string. The string is a variable assigned to a string of additional PLAY commands. For example:
10 MORE$ = "C16 D16 E16 F16" 20 PLAY "A8 B16 XMORE$; G4 A8 XMORE$; B4 A8 XMORE$;" |
Notes:
- Because of the slow clock interrupt rate in early PCs, some notes do not play at higher tempos; for example PLAY "T255 A64" may not be heard. These note/tempo combinations must be determined through experimentation.
- Since PLAY accepts any string, you can set a variable to a list of commands and send the variable as the argument instead of a constant character string. For example:
10 A$ = "L32 C D E F G A B" 20 B$ = "L32 B A G F E D C" 30 PLAY A$ 40 PLAY B$ 50 PLAY A$ + B$
PLAY(n)
The PLAY(n) function will return the number of commands in the PLAY background music buffer for the specified voice when music is set to background mode. The syntax is:
result = PLAY(n)
- result is the number of commands left in the PLAY buffer, it can be from 0 to 32.
- n is the voice channel and must be 0-2.
This function is useful because it allows you to move to a new set of PLAY commands. For example, this program will play Ode to Joy on a loop in the background, while allowing the user to keep interacting with the program.
10 DIM MUSIC$(4) 20 MUSIC$(1) = "E8 E8 F8 G8 G8 F8 E8 D8 C8 C8 E8 E8 E8 D12 D4" 30 MUSIC$(2) = "E8 E8 F8 C8 G8 F8 E8 D8 C8 C8 D8 E8 D8 C12 C4" 40 MUSIC$(3) = "D8 D8 E8 C8 D8 E12 F12 E8 C8 D8 E12 F12 E8 D8 C8 D8 P8" 50 MUSIC$(4) = "E8 E8 F8 G8 G8 F8 E8 D8 C8 C8 D8 E8 D8 C12 C4" 60 PLAY "MB O2 T120" 70 MUSICPART = 0 500 K$ = INKEY$ 510 IF LEN(K$) > 0 THEN PRINT K$ 520 IF PLAY(0) < 4 THEN GOSUB 1000 530 IF K$ = CHR$(27) THEN END 540 GOTO 500 1000 MUSICPART = MUSICPART + 1 1010 IF MUSICPART > 4 THEN MUSICPART = 1 1020 PLAY MUSIC$(MUSICPART) 1030 RETURN
SOUND
The SOUND statement can be used either to play a tone by specifying a frequency and duration, or to turn on or off the external speaker by specifying a switch. The syntax is as follows:
SOUND frequency, duration [, volume] [, voice] SOUND switch
- frequency must be an integer from 0 to 32767. Values below around 110 are treated as 110. Values above 20,000 can't be heard.
- duration must be a number from 0 to 65535 and is the duration you want to play the sound in clock ticks (which occur 18.2 times per second). Despite accepting such a 16-bit integer, the SOUND function automatically stops after 5 seconds (or 91 clock cycles).
- volume must be an integer from 0 to 15 and is the volume. 0 is silence, 1 is quietest, 15 is the loudest, 8 is default. This argument is only available when SOUND is set ON.
- voice must be an integer from 0 to 2. Specifies the voice you wish to play the sound through.
- switch must be ON or OFF and sets the status of the external speaker. If SOUND is set OFF, the background PLAY buffer is cleared without being played.
The following code will test the various frequencies, volume, and channels:
10 CLS: SOUND ON 20 V = 8: F = 150: C = 0 30 PRINT " -= SOUND TEST =-" 40 PRINT 50 PRINT "+ , - : Adjust volume." 60 PRINT ", , . : Adjust frequency by 1." 70 PRINT "< , > : Adjust frequency by 10." 80 PRINT "[ , ] : Adjust speaker." 90 PRINT " ESC : Exit." 100 SOUND 0, 0 110 SOUND F, 92.5, V, C 120 LOCATE 9, 1 : PRINT "Volume: " + STR$(V) + " " 130 LOCATE 10, 1 : PRINT "Frequency: " + STR$(F) + " " 140 LOCATE 11, 1 : PRINT "Channel: " + STR$(C) + " " 150 U = 0 160 K$ = INKEY$ 170 IF K$ = "-" OR K$ = "_" THEN U = 1 : V = V - 1 : IF V < 0 THEN V = 0 180 IF K$ = "=" OR K$ = "+" THEN U = 1 : V = V + 1 : IF V > 15 THEN V = 15 190 IF K$ = "," THEN U = 1 : F = F - 1 : IF F < 110 THEN F = 110 200 IF K$ = "." THEN U = 1 : F = F + 1 : IF F > 32767 THEN F = 32767 210 IF K$ = "<" THEN U = 1 : F = F - 10 : IF F < 110 THEN F = 110 220 IF K$ = ">" THEN U = 1 : F = F + 10 : IF F > 32767 THEN F = 32767 230 IF K$ = "[" OR K$ = "]" THEN U = 1 : C = C - 1 : IF C < 0 THEN C = 2 240 IF K$ = "{" OR K$ = "}" THEN U = 1 : C = C + 1 : IF C > 2 THEN C = 0 250 IF K$ = CHR$(27) THEN SOUND 0, 0 : END 260 IF U = 1 THEN GOTO 100 270 GOTO 150
Notes:
- Setting the duration to 0 will silence any previous SOUND statement.
Games That Use Tandy BASIC MML
Links
- ftp.oldskool.org/pub/tvdog/tandy1000/documents/1kguide.zip - Tandy 1000 Guide (Includes BASICA manual) PDF.