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."
SLEEP
BEEP
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.

( This code can be found at .\tutorial\Lesson12\MyBonnie.bas )

SCREEN 13
COLOR 1
FOR i% = 1 TO 21
   LOCATE 2 + i%, 2: PRINT CHR$(178)
   LOCATE 2 + i%, 39: PRINT CHR$(178)
NEXT i%
FOR i% = 2 TO 39
   LOCATE 2, i%: PRINT CHR$(223)
   LOCATE 23, i%: PRINT CHR$(220)
NEXT i%
COLOR 9
LOCATE 3, 16: PRINT CHR$(34); "MY BONNIE"; CHR$(34)
SLEEP 3
FOR i% = 1 TO 34
   SELECT CASE i%
       CASE 1: LOCATE 5, 5
       CASE 10: LOCATE 10, 5
       CASE 18: LOCATE 15, 5
       CASE 27: LOCATE 20, 5
   END SELECT
   READ note%, duration%, word$
   SOUND note%, duration%: PRINT word$;
NEXT i%
LOCATE 23, 16: PRINT "Thank You!"
SLEEP 4
SYSTEM
DATA 392,8,"My ",659,8,"Bon-",587,8,"nie ",523,8,"lies ",587,8,"O-",523,8,"Ver ",440,8,"the "
DATA 392,8,"O-",330,32,"cean ",392,8,"My ",659,8,"Bon-",587,8,"nie ",523,8,"lies "
DATA 523,8,"O-",494,8,"ver ",523,8,"the ",587,40,"sea ",392,8,"My ",659,8,"Bon-",587,8,"nie"
DATA 523,8," lies ",587,8,"O-",523,8,"ver ",440,8,"the ",392,8,"O-",330,32,"cean ",392,8,"Oh "
DATA 440,8,"bring ",587,8,"back ",523,8,"my ",494,8,"Bon-",440,8,"nie ",494,8,"to ",523,32,"me..!"

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.

( This code can be found at .\tutorial\Lesson12\WTOPlayDemo.bas )

CLS
LOCATE 12, 28
PRINT "The William Tell Overture."
LOCATE 13, 25
PRINT "(takes a few seconds to compile)"
M$ = M$ + "mfl16t155o2mnb4p8msbbmnb4p8msbbb8g#8e8g#8b8g#8b8o3e8o2b8g#8e8g#8b8g#8b8o3e8o2mnb4p8msbbmnb4"
M$ = M$ + "p8msbbmnb4p8msbbmnb4p8msbbb8bbb8b8b8bbb8b8b8bbb8b8b8bbb8b8mlb2b2b8p8p4p4p8mso1bbb8bbb8bbo2e8f#8g#8o1bb"
M$ = M$ + "b8bbo2e8g#g#f#8d#8o1b8bbb8bbb8bbo2e8f#8g#8eg#mlb4bmsag#f#e8g#8e8o3bbb8bbb8bbo4e8f#8"
M$ = M$ + "g#8o3bbb8bbo4e8g#g#f#8d#8o3b8bbb8bbb8bbo4e8f#8g#8mleg#b4bag#f#mse8g#8e8o3g#g#g#8g#g#g#8g#g#"
M$ = M$ + "g#8o4c#8o3g#8o4c#8o3g#8o4c#8o3g#8f#8e8d#8c#8g#g#g#8g#g#g#8g#g#g#8o4c#8o3g#8o4c#8"
M$ = M$ + "o3g#8o4c#8o3b8a#8b8a#8b8g#g#g#8g#g#g#8g#g#g#8o4c#8o3g#8o4c#8o3g#8o4c#8o3g#8f#8"
M$ = M$ + "e8d#8c#8g#g#g#8g#g#g#8g#g#g#8o4c#8o3g#8o4c#8o3g#8o4c#8o3b8a#8b8o2bbb8f#f#"
M$ = M$ + "f#8f#f#f#8g#8a8f#4mna8msg#8mne4msg#8f#8f#8f#8o3f#f#f#8f#f#f#8g#8"
M$ = M$ + "a8mnf#4msa8g#8mne4msg#8f#8o2bbb8o1bbb8bbb8bbo2mne8f#8g#8o1bbb8bbo2e8g#g#f#8d#8o1b8bbb8bb"
M$ = M$ + "b8bbo2e8f#8g#8eg#mlb4mnbag#f#e8g#8e8o3bbb8bbb8bbo4e8f#8g#8o3bbb8bbo4e8g#g#f#8d#8o3b8bb"
M$ = M$ + "b8bbb8bbo4e8f#8g#8mleg#mlb4mnbag#f#mne8g#8e8o3mle56f56g56a56b56o4c56d56mne8eee8e8mlg#4g#8"
M$ = M$ + "mnf#8e8d#8e8c#8mso3bo4c#o3bo4c#o3bo4c#d#eo3abababo4c#d#o3g#ag#ag#abo4c#o3f#"
M$ = M$ + "g#f#g#f#g#f#g#f#g#f#d#o2bo3mlbo4c#d#e8d#8e8c#8o3msbo4c#o3bo4c#o3bo4c#d#eo3abababo4c#d#o3g#"
M$ = M$ + "ag#ag#abo4c#o3f#g#f#g#f#af#emne8p8mlc#4mnc#o2cmso3c#o2co3d#c#o2baag#ec#c#c#c#c#e"
M$ = M$ + "d#o1cg#g#g#g#g#g#o2c#eg#o3c#c#c#c#c#o2co3c#o2co3d#c#o2baag#ec#c#c#c#c#ed#o1cg#g#g#g#g#mng#"
M$ = M$ + "o2c#eg#o3msc#ed#c#d#o2cg#g#g#o3g#ec#d#o2cg#g#g#o3g#ec#d#o2bg#g#a#gd#d#g#gg#gg#ag#f#e"
M$ = M$ + "o1ba#bo2eo1bo2f#o1bo2g#ed#eg#eaf#bo3g#f#ed#f#ec#o2bo3c#o2bo3c#d#ef#g#o2ababo3c#d#ef#o2g#"
M$ = M$ + "ag#aco3c#d#eo2f#g#f#g#f#g#f#g#f#g#f#d#o1bco2c#d#eo1ba#bo2eo1bo2f#o1bo2g#ed#eg#eaf#b"
M$ = M$ + "o3g#f#ed#f#ec#o2bo3c#o2bo3c#d#ef#g#o2ababo3c#d#ef#o2g#ag#abo3c#d#eo2f#o3c#o2co3c#d#c#o2af#mne"
M$ = M$ + "o3mlef#g#abo4c#d#mne8mseee8e8mlg#4g#8msf#8mse8d#8e8c#8o3bo4c#o3bo4c#o3bo4c#d#eo3a"
M$ = M$ + "bababo4c#d#o3g#ag#ag#abo4c#o3f#g#f#g#f#g#f#g#f#g#f#d#o2bo3mlbo4c#d#mne8eee8e8mlg#4g#8"
M$ = M$ + "msf#8e8d#8e8c#8o3bo4c#o3bo4c#o3bo4c#d#eo3abababo4c#d#o3g#ag#ag#abo4c#o3f#"
M$ = M$ + "g#f#g#f#ag#f#e8o2b8o3e8g#g#g#8mng#g#g#8g#g#g#8o4c#8o3g#8o4c#8o3g#8o4c#8o3g#8f#8e8"
M$ = M$ + "d#8c#8g#g#g#8g#g#g#8g#g#g#8o4c#8o3g#8o4c#8o3g#8o4c#8o3b8a#8b8a#8b8g#g#g#8"
M$ = M$ + "g#g#g#8g#g#g#8o4c#8o3g#8o4c#8o3g#8o4c#8o3g#8f#8e8d#8c#8g#g#g#8g#g#g#8g#g#g#8"
M$ = M$ + "o4c#8o3g#8o4c#8o3g#8o4c#8o3b8a#8b8a#8b8o2f#f#f#8f#f#f#8g#8a8f#4a8g#8"
M$ = M$ + "e4g#8f#8o0b8o1b8o2f#f#f#8f#f#f#8g#8a8f#4a8g#8e4g#8f#8bbb8o1bbb8bbb8bbo2e8f#8g#8"
M$ = M$ + "o1bbb8bbo2e8g#g#f#8d#8o1b8bbb8bbb8bbo2e8f#8g#8eg#mlb4mnbag#f#e8o1b8o2e8o3bbb8bbb8bbo4e8"
M$ = M$ + "f#8g#8o3bbb8bbo4e8g#g#f#8d#8o3b8bbb8bbb8bbo4e8f#8g#8o3eg#mlb4mnbag#f#mlef#g#mnamlg#abo4mnc#mlo3bo4c#d#mnemld#"
M$ = M$ + "ef#mng#ao3bo4ao3bo4ao3bo4ao3bo4ao3bo4ao3bo4ao3bo4ao3bmlef#g#mnamlg#abmno4c#mlo3bo4c#d#mnemld#ef#mng#ao3bo4ao3bo4a"
M$ = M$ + "o3bo4ao3bo4ao3bo4ao3bo4ao3bo4ao3bp16mlg#o4g#o3mng#p16mld#o4d#o3mnd#p16"
M$ = M$ + "mleo4eo3mnep16mlao4ao3mnap16mlg#o4g#o3mng#p16mld#o4d#o3mnd#p16mleo4eo3mnep16"
M$ = M$ + "mlao4ao3mnao4go3go4go3go4go3go4go3go4msg8e8c8e8o4mng#o3g#o4g#o3g#o4g#o3g#o4g#o3g#o4msg#8e8o3b8o4e8mng#o3g#o4g#o3g#o4g#"
M$ = M$ + "o3g#o4g#o3g#o4msg#8f8c#8f8mna#o3a#o4a#o3a#o4a#o3a#o4a#o3a#o4msa#8g8e8g8b8p16mna#p16ap16g#p16f#p16ep16"
M$ = M$ + "d#p16c#p16o3bp16a#p16ap16g#p16f#p16ep16d#p16f#mlef#g#mnamlg#abmno4c#o3mlbo4c#d#mnemld#ef#mng#ao3bo4ao3bo4a"
M$ = M$ + "o3bo4ao3bo4ao3bo4ao3bo4ao3bo4ao3bmlef#g#mnamlg#abmno4c#o3mlb"
M$ = M$ + "o4c#d#mnemld#ef#mng#ao3bo4ao3bo4ao3bo4ao3bo4ao3bo4ao3bo4a"
M$ = M$ + "o3bo4ao3bp16mlg#o4g#o3mng#p16mld#o4d#o3mnd#p16mleo4eo3mnep16mlao4ao3mnap16"
M$ = M$ + "mlg#o4g#o3mng#p16mld#o4d#o3mnd#p16mleo4eo3mnep16mlao4ao3mnao4go3go4go3go4g"
M$ = M$ + "o3go4go3go4g8e8c8e8g#o3g#o4g#o3g#o4g#o3g#o4g#o3g#o4g#8e8o3b8o4e8g#o3g#o4g#o3g#o4g#o3g#o4g#o3g#o4msg#8mnf8c#8"
M$ = M$ + "f8a#o3a#o4a#o3a#o4a#o3a#o4a#o3a#o4a#8g8e8g8b8p16a#p16ap16g#p16f#p16ep16d#p16c#p16o3bp16a#p16"
M$ = M$ + "ap16g#p16f#p16ep16d#p16fmled#ed#mne8bbb8bbb8bbo4e8f#8g#8o3bbb8bbb8bbo4g#8a8b8p8e8f#8g#8p8o3g#8"
M$ = M$ + "a8b8p8p2o2bco3c#dd#eff#gg#aa#bco4c#d#ed#f#d#ed#f#d#ed#f#d#ed#f#d#ed#f#d#ed#f#d#ed#f#d#e"
M$ = M$ + "d#f#d#e8eo3eo4eo3eo4eo3eo4e8o3bo2bo3bo2bo3bo2bo3b8g#o2g#o3g#o2g#o3g#o2g#o3g8eo2eo3eo2eo3eo2eo3e8eee8"
M$ = M$ + "e8e8o2bbb8b8b8g#g#g#8g#8g#8eee8e8e8o1b8o2e8o1b8o2g#8e8b8g#8o3e8o2b8o3e8o2b8o3g#8e8b8g#8o4e4"
M$ = M$ + "p8eee8e8e8e8e4p8p16ee4p8p16o2ee2"
PLAY M$

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.

( This code can be found at .\tutorial\Lesson12\WTOBetter.bas )

DIM WilliamTell& ' handle to hold sound file

CLS '                                                            inform user
LOCATE 12, 28
PRINT "The William Tell Overture."
LOCATE 13, 30
PRINT "(press any key to end)"
WilliamTell& = _SNDOPEN(".\tutorial\Lesson12\WilliamTell.ogg") ' load OGG file into RAM
_SNDPLAY WilliamTell& '                                          play OGG file from RAM
SLEEP '                                                          wait for a key press
IF _SNDPLAYING(WilliamTell&) THEN _SNDSTOP WilliamTell& '        stop sound if playing
_SNDCLOSE WilliamTell& '                                         remove OGG from RAM
SYSTEM '                                                         return to Windows

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:

_SNDPLAY 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.

( This code can be found at .\tutorial\Lesson12\SNDPLAYCOPYDemo.bas )

DIM Phaser&
DIM Count%

Phaser& = _SNDOPEN(".\tutorial\lesson12\Phaser.ogg")

PRINT
PRINT
" Rapid phaser using _SNDPLAY."
_DELAY 1
FOR Count% = 1 TO 20
   _SNDPLAY Phaser&
   _DELAY .125
NEXT Count%
PRINT
PRINT
" Rapid phaser using _SNDPLAYCOPY."
_DELAY 1
FOR Count% = 1 TO 20
   _SNDPLAYCOPY Phaser&, .75
   _DELAY .125
NEXT Count%
_SNDCLOSE Phaser&

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:

_SNDPLAYCOPY 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.

( This code can be found at .\tutorial\Lesson12\EchoDemo.bas )

DIM Phaser&

Phaser& = _SNDOPEN(".\tutorial\Lesson12\Phaser.ogg")

PRINT
PRINT
" Press ENTER to fire phaser or ESC to quit."
DO
   IF _KEYDOWN(13) THEN
       _SNDPLAYCOPY Phaser&
       _DELAY .25
       _SNDPLAYCOPY Phaser&, .25
       _DELAY .25
       _SNDPLAYCOPY Phaser&, .1
   END IF
LOOP UNTIL _KEYDOWN(27)
_SNDCLOSE Phaser&

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.

( This code can be found at .\tutorial\Lesson12\SNDLOOPDemo.bas )

DIM Drums&

Drums& = _SNDOPEN(".\tutorial\Lesson12\BasicRock.OGG")
_SNDLOOP Drums&
SLEEP
_SNDSTOP Drums&
_SNDCLOSE Drums&

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.

_SNDPLAY Narrator&
DO
   _LIMIT 10 ' don't hog the CPU
LOOP UNTIL _KEYHIT OR NOT SNDplaying(Narrator&)

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.

( This code can be found at .\tutorial\Lesson12\VolumeControl.bas )

DIM WilliamTell& ' handle to hold sound file
DIM Volume! '      current sound volume

Volume! = 1 '                                                    set initial volume (100%)
CLS '                                                            print instructions to user
LOCATE 12, 28
PRINT "The William Tell Overture."
LOCATE 13, 30
PRINT "(press ESC key to end)"
LOCATE 15, 23
PRINT "UP/DOWN arrow keys to change volume."
WilliamTell& = _SNDOPEN(".\tutorial\Lesson12\WilliamTell.ogg") ' load OGG into RAM
_SNDPLAY WilliamTell& '                                          play OGG file from RAM
_SNDVOL WilliamTell&, Volume! '                                  set OGG volume
DO '                                                             MAIN LOOP begin
   _LIMIT 30 '                                                   30 loops per second
   LOCATE 18, 31 '                                               display volume and balance
   PRINT "Current volume :"; RTRIM$(STR$(INT(Volume! * 100))); "%  " 'show volume
   IF _KEYDOWN(18432) THEN '                                     up arrow pressed?
        Volume! = Volume! + .01 '                                yes, increase volume
       IF Volume! > 1 THEN Volume! = 1 '                         keep volume within limits
       _SNDVOL WilliamTell&, Volume! '                           set OGG volume
   END IF
   IF _KEYDOWN(20480) THEN '                                     down arrow pressed?
        Volume! = Volume! - .01 '                                yes, decrease volume
       IF Volume! < 0 THEN Volume! = 0 '                         keep volume within limits
       _SNDVOL WilliamTell&, Volume! '                           set OGG volume
   END IF
LOOP UNTIL _KEYDOWN(27) OR NOT _SNDPLAYING(WilliamTell&) '       MAIN LOOP back
IF _SNDPLAYING(WilliamTell&) THEN _SNDSTOP WilliamTell& '        stop sound if playing
_SNDCLOSE WilliamTell& '                                         remove OGG from RAM

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& '   continue playing where sound was paused

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.

( This code can be found at .\tutorial\Lesson12\SNDLIMITDemo.bas )

DIM WilliamTell&
DIM Count%

WilliamTell& = _SNDOPEN(".\tutorial\Lesson12\WilliamTell.ogg")
PRINT
PRINT
" The first five seconds of the William Tell Overture"
PRINT
_SNDPLAY WilliamTell&
_SNDLIMIT WilliamTell&, 5
FOR Count% = 1 TO 6
   _DELAY 1
   PRINT Count%; "..";
NEXT Count%
_SNDCLOSE WilliamTell&

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.

( This code can be found at .\tutorial\Lesson12\SNDPOSDemo.bas )

DIM WilliamTell&
DIM Seconds!

WilliamTell& = _SNDOPEN(".\tutorial\Lesson12\WilliamTell.mp3")
PRINT
PRINT
" The file WilliamTell.mp3 is 216 seconds long. Enter the number of seconds"
INPUT " to start the playing at > "; Seconds!
IF Seconds! < 0 OR Seconds! > 216 THEN END
_SNDSETPOS WilliamTell&, Seconds!
_SNDPLAY WilliamTell&
DO
   _LIMIT 60
   LOCATE 5, 2
   PRINT "Current position> "; _SNDGETPOS(WilliamTell&)
LOOP UNTIL _KEYDOWN(27) OR NOT _SNDPLAYING(WilliamTell&)
IF _SNDPLAYING(WilliamTell&) THEN _SNDSTOP WilliamTell&
_SNDCLOSE WilliamTell&

_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)

You'll need to copy Piano.BAS to your qb64 folder before executing it.

( This code can be found at .\tutorial\Lesson12\Piano.bas )

'*
'* QB64 Simple Piano
'*
'* Demonstrates the use of external sound files to create a realistic piano.
'*
'* ESC         - exit program
'* RIGHT ARROW - increase octave
'* LEFT ARROW  - decrease octave
'* Piano Keys  -  R T  U I O   (black keys)
'*             - D F GH J K L  (white keys)
'*
'* By Terry Ritchie
'* Updated 08/16/22 to reflect new tutorial web site
'* Open source code - freely modify
'* Original author's name must remain intact

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

TYPE IVORY '          key information
    u AS INTEGER '    upper case value
    l AS INTEGER '    lower case value
    Down AS INTEGER ' key position
    x AS INTEGER '    key indicator x coordinate
    y AS INTEGER '    key indicator y coordinate
END TYPE

DIM K(12) AS IVORY '  key information array
DIM Tone&(88) '       piano key sounds array
DIM imgPiano& '       piano keyboard image
DIM imgAoctave& '     active octave image
DIM imgIoctave& '     inactive octave image
DIM Octave% '         current octave
DIM Khit& '           keyboard status
DIM Keys% '           key cycle counter

'----------------------------
'- Main Program Begins Here -
'----------------------------

LOADPIANO '                                                         load piano assets
SCREEN _NEWIMAGE(512, 263, 32) '                                    create default screen
_TITLE "PIANO" '                                                    set window title
_SCREENMOVE _MIDDLE '                                               center window on desktop
_PUTIMAGE (0, 0), imgPiano& '                                       show piano image
SHOWOCTAVE '                                                        update octave indicator
DO '                                                                MAIN LOOP begins
    Khit& = _KEYHIT '                                               get keyboard status
   IF Khit& THEN '                                                  was a key hit?
       IF Khit& = 19200 OR Khit& = 19712 THEN '                     yes, left or right key?
           IF Khit& = 19200 THEN '                                  yes, left key?
                Octave% = Octave% - 1 '                             yes, decrease octave
               IF Octave% = -1 THEN Octave% = 0 '                   keep octave in limits
           ELSE '                                                   no, must be right key
                Octave% = Octave% + 1 '                             increase octave
               IF Octave% = 5 THEN Octave% = 4 '                    keep octave in limits
           END IF
            SHOWOCTAVE '                                            update octave indicator
       ELSEIF Khit& = 27 THEN '                                     no, escape key?
            QUIT '                                                  yes, quit program
       END IF
   END IF
   FOR Keys% = 1 TO 12 '                                            cycle through keys
       IF _KEYDOWN(K(Keys%).u) OR _KEYDOWN(K(Keys%).l) THEN '       key pressed?
            PRESS Keys% '                                           yes, play note
       ELSE '                                                       no
            RELEASE Keys% '                                         remove key indicator
       END IF
   NEXT Keys%
   _DISPLAY '                                                       update screen changes
LOOP '                                                              MAIN LOOP back

'-----------------------------------
'- Function and Subroutine section -
'-----------------------------------


'--------------------------------------------------------------------------------------------

SUB QUIT ()

    '*
    '* Cleans RAM by removing all image and sound assets and then exits to Windows.
    '*

   SHARED Tone&() '     need access to piano key sounds array
   SHARED imgPiano& '   need access to piano keyboard image
   SHARED imgAoctave& ' need access to active octave image
   SHARED imgIoctave& ' need access to inactive octave image

   DIM Count% '         generic counter

   FOR Count% = 1 TO 88 '        cycle through all 88 sound files
       _SNDCLOSE Tone&(Count%) ' remove sound file from RAM
   NEXT Count%
   _FREEIMAGE imgPiano& '        remove piano image from RAM
   _FREEIMAGE imgAoctave& '      remove active octave image from RAM
   _FREEIMAGE imgIoctave& '      remove inactive octave image from RAM
   SYSTEM '                      return to Windows

END SUB

'--------------------------------------------------------------------------------------------

SUB RELEASE (k%)

    '*
    '* Removes key press display and sets key as being released
    '*

   SHARED K() AS IVORY ' need access to key information array

   IF K(k%).Down THEN '                                                  is key pressed?
        K(k%).Down = 0 '                                                 yes, set it as released
       SELECT CASE k% '                                                  which key is it?
           CASE 1, 3, 5, 6, 8, 10, 12 '                                  white key
               LINE (K(k%).x, K(k%).y)-(K(k%).x + 27, K(k%).y + 27), _RGB32(255, 255, 255), BF
           CASE ELSE '                                                   black key
               LINE (K(k%).x, K(k%).y)-(K(k%).x + 27, K(k%).y + 27), _RGB32(32, 32, 32), BF
       END SELECT
   END IF

END SUB

'--------------------------------------------------------------------------------------------

SUB PRESS (k%)

    '*
    '* Applies key press display and sets key as being pressed
    '*

   SHARED K() AS IVORY ' need access to key information array
   SHARED Tone&() '      need access to piano key sounds array
   SHARED Octave% '      need access to current octave

   IF NOT K(k%).Down THEN '                                               is key released?
        K(k%).Down = -1 '                                                 yes, set it as pressed
       _SNDPLAY Tone&(Octave% * 12 + k%) '                                play tone for key
       SELECT CASE k% '                                                   which key is it?
           CASE 1, 3, 5, 6, 8, 10, 12 '                                   white key
               LINE (K(k%).x, K(k%).y)-(K(k%).x + 27, K(k%).y + 27), _RGB32(0, 0, 0), BF
           CASE ELSE '                                                    black key
               LINE (K(k%).x, K(k%).y)-(K(k%).x + 27, K(k%).y + 27), _RGB32(255, 255, 255), BF
       END SELECT
   END IF

END SUB

'--------------------------------------------------------------------------------------------

SUB SHOWOCTAVE

    '*
    '* Updates the small top piano keyboard to show current active octave
    '*

   SHARED Octave% '     need access to current octave
   SHARED imgAoctave& ' need access to active octave image
   SHARED imgIoctave& ' need access to inactive octave image

   DIM Count% '         generic counter

   FOR Count% = 0 TO 4 '                                    cycle through octaves
       IF Count% = Octave% THEN '                           current octave?
           _PUTIMAGE (96 + (Count% * 64), 0), imgAoctave& ' yes, place active octave image
       ELSE '                                               no
           _PUTIMAGE (96 + (Count% * 64), 0), imgIoctave& ' place inactive octave image
       END IF
   NEXT Count%

END SUB

'--------------------------------------------------------------------------------------------

SUB LOADPIANO ()

    '*
    '* Loads the piano sounds and images and initializes variables
    '*

   SHARED K() AS IVORY ' need access to key information array
   SHARED Tone&() '      need access to piano key sounds array
   SHARED imgPiano& '    need access to piano keyboard image
   SHARED imgAoctave& '  need access to active octave image
   SHARED imgIoctave& '  need access to inactive octave image
   SHARED Octave% '      need access to current octave

   DIM Note% '           counter used to open sounds
   DIM Count% '          counter used to close sounds if error
   DIM Path$ '           path to sound and graphics files
   DIM File$ '           sound file names

    Path$ = ".\tutorial\Lesson12\PIANO\"
   IF _DIREXISTS(Path$) THEN '                                        path exist?
       FOR Note% = 1 TO 88 '                                          cycle through notes
            File$ = Path$ + LTRIM$(STR$(Note%)) + ".ogg" '            construct file name
           IF _FILEEXISTS(File$) THEN '                               sound file exist?
                Tone&(Note%) = _SNDOPEN(File$) '                      yes, load sound file
           ELSE '                                                     no, sound file missing
               PRINT '                                                report error to user
               PRINT " ERROR: Sound file "; File$; " is missing."
               IF Note% > 1 THEN '                                    did any sounds load?
                   FOR Count% = Note% TO 1 STEP -1 '                  yes, cycle notes backwards
                       _SNDCLOSE Tone&(Count%) '                      remove sound from RAM
                   NEXT Count%
                   END '                                              end program
               END IF
           END IF
       NEXT Note%
   ELSE '                                                             no, path missing
       PRINT '                                                        report error to user
       PRINT " ERROR: The SND\PIANO\ folder could not be found."
       END '                                                          end program
   END IF
   IF _FILEEXISTS(Path$ + "piano.png") THEN '                     image file exist?
        imgPiano& = _LOADIMAGE(Path$ + "piano.png", 32) '         yes, load image file
   ELSE '                                                         no, image file missing
       PRINT '                                                    report error to user
       PRINT " ERROR: piano.png missing."
       END '                                                      end program
   END IF
   IF _FILEEXISTS(Path$ + "active.png") THEN '                    image file exist?
        imgAoctave& = _LOADIMAGE(Path$ + "active.png", 32) '      yes, load image file
   ELSE '                                                         no, image file missing
       PRINT '                                                    report error to user
       PRINT " ERROR: active.png missing."
       _FREEIMAGE imgPiano& '                                     remove image from RAM
       END '                                                      end program
   END IF
   IF _FILEEXISTS(Path$ + "inactive.png") THEN '                  image file exist?
        imgIoctave& = _LOADIMAGE(Path$ + "inactive.png", 32) '    yes, load image file
   ELSE '                                                         no, image file missing
       PRINT '                                                    report error to user
       PRINT " ERROR: inactive.png missing."
       _FREEIMAGE imgPiano& '                                     remove image from RAM
       _FREEIMAGE imgAoctave& '                                   remove image from RAM
       END '                                                      end program
   END IF
    K(1).x = 22: K(1).y = 212: K(2).x = 60: K(2).y = 132 '             set indicator coordinates
    K(3).x = 95: K(3).y = 212: K(4).x = 134: K(4).y = 132
    K(5).x = 168: K(5).y = 212: K(6).x = 241: K(6).y = 212
    K(7).x = 278: K(7).y = 132: K(8).x = 314: K(8).y = 212
    K(9).x = 353: K(9).y = 132: K(10).x = 387: K(10).y = 212
    K(11).x = 428: K(11).y = 132: K(12).x = 460: K(12).y = 212
    K(1).l = 100: K(1).u = 68: K(2).l = 114: K(2).u = 82 '             set key case values
    K(3).l = 102: K(3).u = 70: K(4).l = 116: K(4).u = 84
    K(5).l = 103: K(5).u = 71: K(6).l = 104: K(6).u = 72
    K(7).l = 117: K(7).u = 85: K(8).l = 106: K(8).u = 74
    K(9).l = 105: K(9).u = 73: K(10).l = 107: K(10).u = 75
    K(11).l = 111: K(11).u = 79: K(12).l = 108: K(12).u = 76
    Octave% = 2 '                                                      set initial octave

END SUB

'--------------------------------------------------------------------------------------------

Figure 1: The QB64 piano example

Your Turn

Todo. Any ideas?

Commands and Concepts Learned