Lesson 12: Music and Sound Effects
Can you imagine playing computer games today that have no sound? Well, the earliest computer games, mainly in the 70's, were just that, completely silent! It wasn't until the advent of the first home video game consoles that sound and video games went hand in hand. These early console sounds were very crude by today's standards, with their beeps and noise generators, but it was fantastic nonetheless. The early home computer market quickly caught up allowing home programmers the ability to create noisy software on demand. Early IBM PC and compatible programmers had nothing more than a simple 4 ohm speaker to play with, affectionately named the "PC Squeaker" by programmers of the day. Other early home computers, such as the Ataris, Apples, Commodores and TRS-80 Color Computers, could synthesize their sounds with up to 3 or more voices typically through a television speaker. Before long companies such as Creative Labs and AdLib were introducing dedicated sound boards with FM synthesis and MIDI playback capability that was truly mind-blowing at the time. With each new advance in computer sound technology came new computer games that took advantage of their capabilities. Today however sound is ubiquitous with computing with no one giving it a second thought. Sound cards are routinely included on motherboards and built into all portable devices as a standard feature. Only the precision audiophile or extreme gamer even bothers to go out and purchase a high end sound card today. What this means to you, budding game programmer, is that you have very powerful sound processors at your fingertips for your games to come to life with.
Let's start off by introducing the sound statements built into QB64 that are left-overs from the early days of BASIC. These statements typically used the PC Squeaker to create frequency and duration controlled sound waveforms to induce beeps and blips.
Note: Some newer computers today do not have a built in PC speaker on the motherboard and therefore these basic sounds may not be heard on your system. Some modern sound card drivers will reroute these signals to the sound card for you, but if not you're not missing much. Other motherboard manufacturers have taken this into account and route PC speaker output through the built-in sound card. Windows sometimes gets confused by these early statements as well and will interpret the statement as a Windows warning sound or similar. These statements are being shown so you know they exist and how to use them, however, with QB64's built-in vastly improved sound capability you may never use them.
The BEEP Statement
The BEEP statement is as simple as it gets when generating a sound for your programs. It does literally what the name implies and generates a beep tone. Early mainframe terminals could generate a beep by having an ASCII character 7 (BEL) sent to them. The BEEP command is a throw-back from those early days of computing.
PRINT "I am going to beep when you press ENTER."
PRINT "That was cool!"
Note: While the beep is being produced your program will pause for the duration of the sound. Using BEEP in a loop for game sounds is therefore not recommended.
The SOUND Statement
The SOUND statement is used to create a sound determined by a given frequency and duration. The SOUND statement does not use your sound card but instead uses the audio oscillator built into your motherboard for generating tones out of the PC speaker. Your computer may reroute these tones to your computer's sound card if it doesn't have a PC speaker. The SOUND statement is used by supplying a frequency and duration like so:
SOUND frequency, duration
Frequency can be any value from 37 to 32767 and duration can be any positive number including zero. Duration is timed in 1/18th second intervals meaning that roughly a duration of 18.2 equals 1 second. If you know how to read music and wish to generate tunes from sheet music using the SOUND statement there is a listing of notes/octaves and their frequency values on the QB64 Wiki SOUND page.
Here is an example of the SOUND statement being used to play "My Bonnie" taken from the QB64 Wiki SOUND page. It has been included in the .\tutorial\Lesson12 directory as MyBonnie.BAS.
The PLAY Statement
The PLAY statement was designed to play a predefined string of notes using built in musical language directives. By default PLAY plays notes from the third octave in quarter notes in 4/4 time:
PLAY "CDEFGAB" ' third octave scale in quarter notes
Here a directive was added to use eighth notes instead:
PLAY "L8CDEFGAB" ' third octave scale in eighth notes
There are music directives to set the tempo, volume, legato, staccato, and add rests amongst a variety of other directives. A complete list of them are contained in the QB64 Wiki PLAY page. Below is the code to play the William Tell Overture done completely with the PLAY statement! Keep in mind that someone had to hand code this entire thing in at some point using sheet music. The original programmer is unknown as the source was found on the Internet without any credit information. If you know who programmed this please let me know so the source can be cited. It has been included in the .\tutorial\Lesson12 directory as WTOPlayDemo.BAS.
As the SOUND and PLAY statements painfully point out BASIC was never meant to handle sound beyond simple beeps and tones generated by the PC speaker sound oscillator. Now that sound cards are standard fare let's get into the new sound statements offered by QB64.
The _SNDOPEN Statement
QB64 makes use of external sound files that are loaded into memory. The _SNDOPEN statement is used to load a sound file into memory and return a long integer handle value for use later on.
SoundHandle& = _SNDOPEN(FileName$)
The _SNDOPEN statement can load WAV, OGG, and MP3 sound file types. If _SNDOPEN returns a value of 0 it means there was an error opening the sound file. You should always check that the long integer variable you assign as a handle contains a value greater than 0 before attempting to play the sound. Using the PLAY statement earlier we played the William Tell Overture. Let's try that again using an external sound file. Save the following example as WTOBetter.BAS.
In line 8 the file WilliamTell.OGG was loaded into memory and given the handle of WilliamTell& to be referenced later on. The _SNDPLAY statement in line 9 (_SNDPLAY discussed later) used that handle as a reference to start playing the file in memory.
Even though QB64 supports WAV and MP3 sound files I've personally found that OGG files work best, especially when the sound file's attributes such as volume or balance need to be altered. You can obtain a free program called Audacity that among other things converts between different sound types easily and quickly.
The _SNDPLAY and _SNDPLAYCOPY Statements
The _SNDPLAY statement will play a sound file by using the handle value previously set with the _SNDOPEN statement. Once _SNDPLAY starts playing a file it will continue to play in the background until the sound file has completed or was told to stop using code.
In the example code included with the _SNDOPEN section above line 9 shows _SNDPLAY starting a previously loaded sound file:
_SDNPLAY WilliamTell& ' play OGG file from RAM
The _SNDPLAYCOPY statement plays a copy of a sound file loaded into RAM. _SNDPLAYCOPY is useful for situations where multiple copies of the sound need to be playing at once such as enemy explosions in a game. The example code below illustrates the difference between _SNDPLAY and _SNDPLAYCOPY. Save the code as SNDPLAYCOPYDemo.BAS.
There is a definite difference in sound quality and depth between the two statements. _SNDPLAY when asked to play a sound again will stop the same previous sound if it is still playing. _SNDPLAYCOPY however will play an entirely new copy of the same sound file allowing any previous sounds to complete. This allows the same sound file to overlap adding depth.
_SNDPLAYCOPY also has an optional volume control parameter that can control the volume of copied sounds. Line 17 of the example:
_SDNPLAYCOPY Phaser&, .75
is playing the copied sound file at 75% volume. The optional volume value can be from 0 (no sound) to 1 (full sound). Controlling the volume of multiple copies can lead to some interesting effects such as this echo demonstrated in the next example. Save the code as EchoDemo.BAS when finished.
With the use of timing or frame counting within a game this sort of echo effect could easily be incorporated.
The _SNDLOOP Statement
The _SNDLOOP statement is used to play a sound file in a continuous loop. This is handy for sound such as ambient "mood" music in the background of games, or the "blip", "blip", blip" of a radar screen. Here is an example of keeping a drum beat going forever from a 3 second OGG sound clip. Save the code as SNDLOOPDemo.BAS when finished.
The _SNDPLAYFILE Statement
The _SNDPLAYFILE statement is used to play a sound without having to first load it into memory and generate a handle value for it. This would be useful for "cut scenes" in a game where perhaps a narrator explains what has happened up to this point in the game. For sounds that are used over and over again, such as the rapid firing of a gun, it's best to use _SNDOPEN and _SNDPLAY or _SNDPLAYCOPY.
_SNDPLAYFILE will open the sound file, play it, and close it without any further interaction needed from the code. The statement will not return any errors as well. If the sound file is not found, or there was an error loading it, _SNDPLAYFILE will simply do nothing.
_SNDPLAYFILE ".\tutorial\Lesson12\WilliamTell.OGG", , .5
_SNDPLAYFILE also has an optional volume parameter than can be added to control the volume in the same way _SNDPLAYCOPY does. Notice that you'll need to use two commas however.
The _SNDPLAYING Statement
The _SNDPLAYING statement is used to determine is a sound is currently playing, returning a value of 0 (false) if it is not and and a value of -1 (true) if it is. In the example code provided in the _SNDOPEN statement line 11 is used to check if the William Tell Overture sound file is still playing or not:
IF _SNDPLAYING(WilliamTell&) THEN _SNDSTOP WilliamTell& ' stop sound if playing
This statement can also be useful to move onto to new sections of your game based on a narrator finishing a talk for example.
_LIMIT 10 ' don't hog the CPU
LOOP UNTIL _KEYHIT OR NOT _SNDPLAYING(Narrator1&)
Here the code waits for either a key being pressed or the sound file to finish.
The _SNDSTOP Statement
The _SNDSTOP statement is used to stop a sound from playing. It's usually best to check if the sound is actually playing first before issuing the _SNDSTOP statement.
IF _SNDPLAYING(MySound&) THEN _SNDSTOP MySound& ' stop sound if playing
Note: _SNDSTOP will not work on sounds playing that were started with _SNDPLAYFILE.
The _SNDVOL Statement
The _SNDVOL statement is used to control the volume level of a sound even if it's currently playing. The following example code will allow you to change the volume of the sound playing by using the UP and DOWN arrow keys on the keyboard. Save the example as VolumeControl.BAS when finished.
The value supplied to _SNDVOL must be from 0 (no sound) to 1 (full sound) therefore a value of .25 would be equal to 25% volume. If you attempt to supply a value beyond this range a run-time error will occur in your program.
The _SNDPAUSE and _SNDPAUSED Statements
The _SNDPAUSE statement is used to pause a sound that is currently playing. _SNDPLAY can be used to start the sound once again from where it was paused.
IF _SNDPLAYING(MySound&) THEN _SNDPAUSE MySound& ' pause the sound if playing
SLEEP ' wait for a key stroke
IF _SNDPAUSED(MySound&) THEN _SNDPLAY MySound& ' start sound where pause left off
The _SNDPAUSED statement is used to test if a sound has been paused and returns a 0 (false) if the sound is not paused and -1 (true) if a sound is currently paused.
The _SNDLIMIT Statement
The _SNDLIMIT statement is used to limit the number of seconds a sound is allowed to play. _SNDLIMIT will not work on sounds that were started using _SNDLOOP. The example below shows a sound being limited to 5 seconds of play time. Save the code as SNDLIMITDemo.BAS when finished.
To remove the limit you can either set the limit to 0, pause, or stop the sound.
The _SNDLEN Statement
The _SNDLEN statement returns the number of seconds contained in a sound file by supplying the file handle created with an _SNDOPEN statement.
Seconds! = _SNDLEN(WilliamTell&)
The _SNDSETPOS and _SNDGETPOS Statements
The _SNDSETPOS statement sets the starting point in seconds where to start playing a sound file from. _SNDGETPOS returns the current position in seconds within a sound file being played. The example below shows these two statements in action. Save the code as SNDPOSDemo.BAS when finished.
_SNDSETPOS requires a single precision value and the _SNDGETPOS statement returns a single precision value. If you exceed the number of seconds contained in a sound file using _SNDSETPOS then playback will be interrupted.
The _SNDCLOSE Statement
The _SNDCLOSE statement is used to remove a sound file from RAM. It's good programming practice to clean up after yourself and remove all assets such as sound files and graphics files your program loaded before it terminates.
_SNDCLOSE WilliamTell& ' remove sound file from memory
A Sound Programming Example
The following code example shows how external sound files can be used to achieve impressive results. A piano has 88 keys and therefore 88 distinct tones. Each of these piano tones is contained in a small OGG file and loaded into RAM. By pressing a corresponding key on the keyboard a piano note is heard corresponding to the piano keyboard. In just a few lines of code a simulated piano is born. The keyboard keys to use the piano are as follows:
ESC - exit the program
RIGHT ARROW - increase piano octave
LEFT ARROW - decrease piano octave
(piano keys) - R T U I O (black piano keys)
D F G H J K L (white piano keys)
The code has been included in the .\tutorial\Lesson12 directory as Piano.BAS. You'll need to copy Piano.BAS to your qb64 folder before executing it.
Figure 1: The QB64 piano example