Task 15  Task 15: Music and Sound Effects  Task 15


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 synthsize their sounds with up to 3 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 noone 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.

- Old School Sound -

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

- BEEP -

The BEEP statement is a simple as it gets when generating a sound from your programs. It does literally what its name implies, generates a beep sound. Early mainframe terminals could generate a beep by having an ASCII character 7 sent to them, or what was known as a BELL character. The BEEP command is a throw-back to these very early days of BASIC.

PRINT "I am going to beep when you press ENTER."
INPUT a$
BEEP
PRINT "That was cool."


In fact, the ASCII value of 7 still generates a beep to this day!

PRINT CHR$(7) ' BEEP heard


- SOUND -

The SOUND statement is used to create sounds of varying frequency and duration using the PC speaker. QB64 or your motherboard *should* route the sounds to your sound card so you can hear them. 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 number including 0 (zero). Duration is timed in 1/18th second intervals meaning that 18 equals 1 second, 27 equals 1.5 seconds, 36 equals 2 seconds and so on. If you are the type of programmer that needs exact figures, 18.2 is exactly one second. If you know how to read music and wish to generate tunes from sheet music there is a listing of notes/octaves and their frequency values on the SOUND QB64 Wiki page.


Here is an example of the SOUND statement used to play "My Bonnie" taken from the SOUND QB64 Wiki Page.

SCREEN 13
_FULLSCREEN
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%
SLEEP 2
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..!"

- PLAY -

The PLAY statement is used to play musical notes through the PC speaker. QB64 will reroute the sound to the computer's sound card. PLAY is actually a very powerful musical processing command for those with a background in music and know how to read sheet music. A string that has been formatted with music directives is used by PLAY to create the music.

PLAY "CDEFGAB" ' third octave scale in default quarter notes

The above command will play a scale of quarter notes in 4/4 time using the default third octave. To change the notes to eigth notes you would add the music directive L8:

PLAY "L8CDEFGAB" ' third octave scale in eigth notes

There are music directives to set tempo, volume, legato, staccato and add rests amongst a variety of other directives. A complete list of directives are contained on the PLAY QB64 Wiki page. Below is 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.

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$

- QB64 Sound Capabilities -

As the previous statements painfully point out, BASIC was never meant to handle sound beyond the simple beeps and blips offered by early video games. Luckily for us game coders, the designer of QB64 added extremely powerful sound capability to this new generation of BASIC. You can play WAV, MP3, OGG, AIFF, RIFF, VOC, MOD and MIDI files with ease and control how and where the playing is to begin in them as well. There are even commands built into QB64 that allow for you to create custom sound using waveform creation.

- _SNDOPEN() -

Before an external sound file can be used it must be loaded into RAM using the _SNDOPEN() function. _SNDOPEN can load a number of different types of external sound files and the features available depends on the type of sound file you are loading. The syntax for the _SNDOPEN function is as follows:

sound_handle& = _SNDOPEN (filename$[, "[VOL][,][SYNC][,][LEN][,][PAUSE][,][SETPOS]"])

Don't let the function's appeared complexity scare you. The information surrounded in brackets ( [] ) is optional and only needed when certain features for a given sound file type is needed. All of the example sound files used in the this tutorial have already been included in a folder called SND and the following code examples will reflect this path. First, let's open an external sound file and play it to show how easy using the sound statements in QB64 really is.


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(".\SND\WilliamTell.mp3") '        load MP3 file into RAM
_SNDPLAY WilliamTell& '                                   play MP3 file from RAM
SLEEP '                                                   wait for a key press
IF _SNDPLAYING(WilliamTell&) THEN _SNDSTOP WilliamTell& ' stop sound if playing
_SNDCLOSE WilliamTell& '                                  remove MP3 from RAM
SYSTEM '                                                  return to Windows

A long integer value is returned by _SNDOPEN which is a handle number that references the loaded sound file in RAM.

WilliamTell& = _SNDOPEN(".\SND\WilliamTell.mp3") '        load MP3 file into RAM

The long integer variable WilliamTell& now holds a numeric value that can be used by other sound statements and functions to manipulate this sound file. The sound file loaded in this case, WilliamTell.mp3, is an MP3 file. _SNDOPEN will return a value of 0 (zero) if the sound file failed to open for what ever reason, so it's best to check for the existance of the sound file and check that the sound file opened correctly. QB64 can manipulate sound files by varying their volume. However, MP3 files start acting flaky when code is used to vary their properties and should be avoided for such actions. OGG files, on the other hand, seem to handle having their properties manipulated just fine, so it's advisable to convert MP3 files to OGG files at all times. In fact, OGG files are my sound file of choice for practically everything. You can obtain a free program called Audacity that converts sound files between different types and it's what I use to convert MP3 and other sound formats to OGG. Let's modify the previous code example to see how to change the volume of a sound file:


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."
IF _FILEEXISTS(".\SND\WilliamTell.ogg") THEN '            does sound file exist?
    WilliamTell& = _SNDOPEN(".\SND\WilliamTell.ogg", "VOL") 'yes, load OGG into RAM
    IF WilliamTell& = 0 THEN '                            did sound file load?
        PRINT '                                           no, inform user of error
        PRINT " ERROR: WilliamTell.ogg failed to load."
        END '                                             end program
    END IF
ELSE '                                                    no, file not found
    PRINT '                                               inform user of error
    PRINT " ERROR: SND\WilliamTell.ogg file not found."
    END '                                                 end program
END IF
_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
SYSTEM '                                                  return to Windows

This time the sound file, WilliamTell.ogg, was loaded with the "VOL" directive meaning that the sound file's volume can be manipulated.

    WilliamTell& = _SNDOPEN(".\SND\WilliamTell.ogg", "VOL") ' yes, load OGG into RAM

There are five directives that can be added to _SNDOPEN to manipulate sound files and they are:

"VOL"    - can change the volume and balance of the sound using _SNDVOL and _SNDBAL
"LEN"    - can get the length of a sound using the _SNDLEN function
"PAUSE"  - can pause the sound using _SNDPAUSE and then the _SNDPAUSED function can be used to check it
"SETPOS" - can change the position the sound is playing from (or will be played from) using _SNDSETPOS
"SYNC"   - can load the sound into a unique channel to be played simultaniously with other sounds


When using directives they are separated by commas within one string like so:

WilliamTell& = _SNDOPEN(".\SND\WilliamTell.ogg", "VOL,SYNC")

Here we've loaded a sound file with the ability to have its volume changed and be synchronized with other sound files that are currently playing (more on that later).


Not all sound directives, however, work with all sound file types. The following is a listing of sound file types supported by QB64 and the sound directives available to each:

WAV - "VOL,SYNC,LEN,PAUSE"
OGG - "VOL,SYNC,LEN,PAUSE"
AIF - "VOL,SYNC,LEN,PAUSE"
RIF - "VOL,SYNC,LEN,PAUSE"
VOC - "VOL,SYNC,LEN,PAUSE"
MID - "VOL"
MOD - "VOL,PAUSE"
MP3 - "VOL,PAUSE,SETPOS"

- _SNDPLAY and _SNDPLAYCOPY -

The _SNDPLAY statement will play a sound file that is loaded and associated with a file handle set by _SNDOPEN.

_SNDPLAY WilliamTell& '                                   play OGG file from RAM

The long integer value WilliamTell& was previously associated to the sound file WilliamTell.ogg through the use of _SNDOPEN. _SNDPLAY will start playing the sound file and continue on, that is, once you start a sound it continues to play while the program continues unlike the PLAY statement.

The _SNDPLAYCOPY statement copies a sound, plays it at a given volume and then closes the copied sound. The sound to be used with _SNDPLAYCOPY must have been previously opened with the "VOL" and "SYNC" directives. This statement is handy when a sound needs to repeat over top of itself, such as when a gun is firing in rapid succession.

DIM Phaser&
DIM Count%

Phaser& = _SNDOPEN(".\SND\Phaser.ogg", "VOL,SYNC")

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&

The program listing above illustrates the difference between _SNDPLAY and _SNDPLAYCOPY. When _SNDPLAY is asked to play a sound that is currently playing it stops the sound and restarts it as the first FOR...NEXT loop demonstrates. However, in the second FOR...NEXT loop _SNDPLAYCOPY is used instead, which results in the same sound overlapping. Furthermore, the volume of each copied sound can be optionally controlled as seen in this line:

    _SNDPLAYCOPY Phaser&, .75

The volume can range from 0 (zero) or 0% to 1 or 100%. If no volume value is given, the default value of 1 or 100% volume is used. Controlling the volume of multiple copies can lead to some interesting results, such as an echo effect:

DIM Phaser&
DIM Count%

Phaser& = _SNDOPEN(".\SND\Phaser.ogg", "VOL,SYNC")

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&

The use of _DELAY here would lead to undesirable results in a game, creating a half second pause each time a weapon is fired. A better method would be to either count the frames or time that has passed since the last sound statement.

- _SNDLOOP -

The _SNDLOOP statement is used to start a sound playing in a never ending loop. You'll need to use _SNDSTOP (see below) to stop the sound from playing. Warning, the following code sample is annoying!

DIM Phaser&

Phaser& = _SNDOPEN(".\SND\Phaser.ogg")
_SNDLOOP Phaser&
SLEEP
_SNDSTOP Phaser&
_SNDCLOSE Phaser&

- _SNDPLAYFILE -

The _SNDPLAYFILE statement is used to open a sound file, play the sound, and then close the file all with one command. This command may be useful if a sound file only needs to be played once, but for sounds that are to be used numerous times _SNDOPEN and _SNDPLAY or _SNDPLAYCOPY are the better choices.

Volume! = 1
Sync% = 1
_SNDPLAYFILE ".\SND\Phaser.ogg", Sync%, Volume!

Two optional parameters, sync and volume, can be added to allow the sound to be played more than once and to change the default 100% volume. The volume value can be from 0 (zero) meaning 0% to 1 meaning 100%. When using sync or volume the sound file being opened must support both of these directives. Furthermore, _SNDPLAYFILE does not return any errors, so if an error occurs the sound will simply not play.


- _SNDPLAYING() -

The _SNDPLAYING() function is used to determine if a sound is currently playing in the background.

LOOP UNTIL _KEYDOWN(27) OR NOT _SNDPLAYING(WilliamTell&) 'MAIN LOOP back
IF _SNDPLAYING(WilliamTell&) THEN _SNDSTOP WilliamTell& ' stop sound if playing


In the first line of code above the loop is exited if the user presses the ESC key or the sound file has finished playing. _SNDPLAYING will return a value of 0 (zero) or false if the sound is not playing (or has been paused) and a value of -1 or true if the sound is currently playing. The second line of code above stops the sound from playing but only after checking to see if the sound really is still playing.

- _SNDSTOP -

To stop a sound from playing you can issue the statement _SNDSTOP to do so.

IF _SNDPLAYING(WilliamTell&) THEN _SNDSTOP WilliamTell& ' stop sound if playing

This line of code stops the sound from playing after determining that the sound is in fact still playing.

- _SNDVOL -

Once you have opened a sound file with the capability of having its volume changed through the use of the "VOL" directive you can use the _SNDVOL statement to change the sound file's volume. In the previous program listing you saw this happen in a few places:

_SNDVOL WilliamTell&, Volume! '                           set OGG volume

and

    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


_SNDVOL can accept a single value range from 0 (zero) to 1 (one) meaning that .5 equals 50% volume. The single variable Volume! is used to store the current volume level of the sound file and is manipulated using the up and down arrow keys accordingly. It's important to keep _SNDVOL within the range of 0 to 1 or a run-time error will occur.

- _SNDPAUSE and _SNDPAUSED() -

You can pause a sound by using the _SNDPAUSE statement, but only if the sound has been opened with the "PAUSE" directive.

IF _SNDPLAYING(MySound&) THEN _SNDPAUSE MySound& ' pause the sound if playing
SLEEP '                                            wait for a keystroke
IF _SNDPAUSED(MySound&) THEN _SNDPLAY MySound& '   start the sound where pause left off


The _SNDPAUSED() function can be used to test if a sound has been paused. _SNDPAUSED will return a value of 0 (zero) or false if the sound is not paused and a value of 1 or true if the sound file is in a paused state.

Once a sound has been paused you must use the _SNDPLAY statement to start it playing again from where it was paused.

- _SNDLIMIT -

The _SNDLIMIT statement is used to limit a sound to play for a set number of seconds. _SNDLIMIT will not work on sounds that were started using _SNDLOOP.

DIM WilliamTell&
DIM Count%

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

To remove the limit on a sound file set the limit to 0 (zero):

_SNDLIMIT WilliamTell&, 0


- _SNDLEN() -

Use the _SNDLEN() function to get the number of seconds of sound contained in a sound file. The file being checked for length must have been previously opened using the "LEN" directive.

DIM WilliamTell&
DIM Count%
DIM SoundLen!

WilliamTell& = _SNDOPEN(".\SND\WilliamTell.ogg", "VOL,SYNC,LEN")
PRINT
PRINT " The first five seconds of the William Tell Overture"
PRINT
SoundLen! = _SNDLEN(WilliamTell&)
_SNDLIMIT WilliamTell&, 5
_SNDPLAY WilliamTell&
FOR Count% = 1 TO 5
    _DELAY 1
    PRINT Count%; "..";
NEXT Count%
PRINT
PRINT
PRINT " The total number of seconds in this file is"; SoundLen!
_SNDCLOSE WilliamTell&

Notice that the sound directive "LEN" was added to the _SNDOPEN function above to enable the _SNDLEN function to get the number of seconds in the sound. Also, _SNDLEN returns a single value to be stored in a single variable type as was done with SoundLen! above.

- _SNDSETPOS and _SNDGETPOS() -

Both of these commands will only work with sound files opened with the "SETPOS" directive, and therefore only MP3 sound file types. As stated before, MP3 files are quirky to work with and should be avoided as much as possible, but if you must work with MP3 files these commands can be used to manipulate them.

_SNDSETPOS allows a position in seconds to be set within an MP3 file where playing is to start at. Curiously, MP3 files do not support the "LEN" directive, so you must know beforehand how long an MP3 file is as to not exceed the number of seconds it contains using _SNDSETPOS. The following code example may, or may not, work for you. Again, MP3 files are difficult to work with.


DIM WilliamTell&
DIM Seconds!

WilliamTell& = _SNDOPEN(".\SND\WilliamTell.mp3", "VOL,SETPOS")
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&

The _SNDGETPOS() function is used to get the current play position within an MP3 file, as shown by this line of code:

    PRINT "Current position> "; _SNDGETPOS(WilliamTell&)

Again, if the code example above does not work for you it's because of the quirky nature of MP3 files ... try to avoid them if you can.

- _SNDCLOSE -

_SNDCLOSE is used to remove a sound file from RAM. This should be done if a sound is no longer needed or before ending a program to perform a cleanup of RAM. Make sure the sound has been stopped using _SNDSTOP before removing the sound file from memory.

_SNDCLOSE WilliamTell&

An Example Use of External Sound Files

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 GH J K L   (white piano keys)


'*
'* 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)
'*

'--------------------------------
'- 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$ = ".\SND\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$, "VOL,SYNC,LEN,PAUSE") ' 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
Path$ = ".\GFX\PIANO\"
IF _DIREXISTS(Path$) THEN '                                        path exist?
    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
ELSE '                                                             no, path missing
    PRINT '                                                        report error to user
    PRINT " ERROR: The GFX\PIANO\ folder could not be found."
    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

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

Piano Man
Figure 1 - The QB64 Piano Example



-- Your Turn --










-- COMMAND REFERENCE --


Commands learned in previous tasks:

PRINT
INPUT
DIM
REM or '
CONST
TIME$
VAL()
IF...THEN
AND
END
GOTO
SELECT CASE...END SELECT
CASE
TO
IS
CASE ELSE
colon ( : )
FOR...NEXT
STEP
CLS
SLEEP
_DELAY
SYSTEM
DO...LOOP UNTIL (and variations)
WHILE...WEND
SCREEN
LINE
CIRCLE
PAINT
PSET
SUB
END SUB
FUNCTION
END FUNCTION
SHARED
SQR()
^ (exponent)
INPUT$
LINE INPUT
INKEY$
CHR$()
_KEYDOWN
_KEYHIT
_MOUSEX
_MOUSEY
_MOUSEINPUT
_MOUSEHIDE
_MOUSESHOW
_MOUSEMOVE
_MOUSEBUTTON
DIM
TYPE
END TYPE
AS
UCASE$()
LCASE$()
LTRIM$()
RTRIM$()
INSTR()
STR$()
DATE$
LEFT$()
RIGHT$()
MID$()
ASC()
STRING$()
SPACE$()
SWAP
AND
OR
XOR
NOT
\ (integer division)
MOD
INT()
CINT()
CLNG()
CSNG()
CDBL()
_ROUND()
FIX()
ABS()
SGN()
SIN()
COS()
TAN()
ATN()
_DIREXISTS
_FILEEXISTS
CHDIR
MKDIR
RMDIR
KILL
NAME ... AS
OPEN
INPUT (file mode)
OUTPUT (file mode)
APPEND (file mode)
BINARY (file mode)
RANDOM (file mode)
PRINT (file statement)
CLOSE
EOF()
INPUT (file statement)
REDIM
_PRESERVE
UBOUND()
WRITE (file statement)
LINE INPUT (file statement)
FREEFILE
LOF()
SHELL
FILES
_HIDE

New commands introduced in this task:

BEEP
SOUND
PLAY
_SNDOPEN()
_SNDPLAY
_SNDPLAYCOPY
_SNDLOOP
_SNDPLAYFILE
_SNDPLAYING()
_SNDSTOP
_SNDVOL
_SNDPAUSE
_SNDPAUSED()
_SNDLIMIT
_SNDLEN()
_SNDSETPOS
_SNDGETPOS()
_SNDCLOSE

Concepts learned in previous tasks:

execute
statement
expression
literal string
syntax error
variables
type declarations
literal strings
concatenation
integers
long integers
single precision
double precision
strings
null strings
reserved words
operators ( +, -, *, / )
declare
constant
relational operators
functions
nesting
order of operations
Boolean
conditions
indenting
loops
labels
frame rate
controlled loops
conditional loops
Monochrome
Grayscale
pixels
bits per pixel
radian
counter-clockwise
aspect ratio
algorithm
subroutine
function
local variables
RAM
global variables
Pythagorean Theorem
BIOS
ASCII
buffer
CMOS
ROM
ASCII chart
array
one dimensional array
element
index
two dimensional array
three dimensional array
parse
geometry
algebra
trigonometry
calculus
angular momentum
physics engine
bitmap
polygon
vector math
numbering system
transistor
binary
binary numbering system
memory address
George Boole
Boolean logic
logic gate
bitwise math
flags
Banker's Rounding
sine
radian
cosine
tangent
arctangent
sequential file
dynamic array
Comma Separated Value (CSV) file
records
database

New concepts introduced in this task:

FM synthesis
MIDI
audiophile
WAV
OGG
AIF
RIF
VOC
MID
MOD
MP3