First Steps in Programming
RISC OS Computers
Martyn Fox

Chapter 12 : Adding Sound

The game that we produced in section 11 may have had some good graphics but it definitely lacked something which any decent game has. It was remarkably silent. To remedy this we need to look, not surprisingly, at the SOUND command.

Your RISC OS machine is capable of producing eight sounds at the same time, but only one of them is usually working. There is a very good reason for this. Acorn machines pride themselves on being extremely fast computers, thanks to their ARM or StrongARM processors. A sound channel takes a lot of the machine's processing power and slows it down a little bit. Eight sound channels would slow it down a lot, even if they weren't actually making sounds, so seven of them are left turned off unless they're needed. One is always active so that it can produce the 'beep'.

Raising Your VOICE

Basic refers to a sound channel as a VOICE. The first thing to understand is that the rest of RISC OS, including the part that processes star commands, uses the word 'voice' to refer to the waveform which produces a particular sound, and refers to a sound channel as a channel!

Try typing;

    *Voices

and you should see something like this:

            Voice      Name
   1          1   WaveSynth-Beep
              2   StringLib-Soft
              3   StringLib-Pluck
              4   StringLib-Steel
              5   StringLib-Hard
              6   Percussion-Soft
              7   Percussion-Medium
              8   Percussion-Snare
              9   Percussion-Noise
   ^^^^^^^^ Channel allocation map

The names on the right refer to the sound waveforms that the machine knows about. They are built into the machine's ROM and are always there. If you've been playing a game with its own sounds, it's possible that some of the waveforms are still loaded and will show up in the list. The game's directory may well contain some module files for producing these sounds and you can add them to the list by loading them, either by double-clicking on them or using the *RMLoad command.

Waveform Names and Numbers

To the left of the waveform names are the numbers by which they can be called. The extra number 1 to the left of 'WaveSynth-Beep' shows that channel 1 has this waveform assigned to it. You can change this by typing, say:

    *ChannelVoice 1 5

or alternatively:

    *ChannelVoice 1 StringLib-Hard

If you now type *Voices again, you will see that the '1' has moved down so that it's opposite waveform 5, 'StringLib-Hard'.

Now press Ctrl-G to produce a beep and you'll hear a great difference! Channel 1 is the beep channel and anything you do to this channel affects the beep. Any program you run now will produce a beep with the new sound, unless it deliberately reprograms channel 1 itself.

Don't worry, you haven't broken your machine. You can restore the beep to its nice old soft self (unless of course you prefer the new one!) by typing:

    *ChannelVoice 1 1

to put things back as they were.

Basic has two very similar keywords, VOICES and VOICE, which should not be confused. VOICES sets the number of active sound channels, which must be 1, 2, 4 or 8.

VOICE is the Basic equivalent of the star command *ChannelVoice, but can only be followed by the name of the waveform in quotes, not its number, for example:

    VOICE 1,"StringLib-Hard"

The SOUND Command

Now that we've investigated voices and channels, we're ready to make a sound. We'll stick with sound channel 1 for now and you can assign whichever waveform you like to it for the purpose of this experiment.

There are four numbers which follow a SOUND command - the channel number, amplitude, pitch and duration. If you don't want the sound to be made instantly, you can add a fifth one to represent a delay.

Try typing:

    SOUND 1,-15,53,20

You should get a note lasting one second which is, in fact, middle C.

The first number is the channel number and indicates that the command is for channel 1, which we have to use because it's the only one active at the moment. The number for the amplitude, or volume, is -15. Amplitude can be represented by a negative number, from zero for silence to -15 for maximum loudness. It is also possible to use a positive number from 256 to 383. In this case the number is logarithmic, with each increase of 16 doubling the sound amplitude. We'll stick with negative numbers in this guide.

The pitch number 53 is the number for middle C. Pitch is represented by a number from 1 to 255, as follows:

              Octave number

   Note   1    2     3     4     5     6

   A          41    89   137   185   233
   A#         45    93   141   189   237
   B      1   49    97   145   193   241
   C      5   53   101   149   197   245
   C#     9   57   105   153   201   249
   D     13   61   109   157   205   253
   D#    17   65   113   161   209
   E     21   69   117   165   213
   F     25   73   121   169   217
   F#    29   77   125   173   221
   G     33   81   129   177   225
   G#    37   85   133   181   229

Octave 2 is the one containing middle C.

It is also possible to represent pitch by a number from &100 (256) to 32767 (&7FFF), in which case middle C is &4000. You may have to use pitch numbers in this range if you want to experiment with sound from module files.

Durations and Timings

The final number represents the duration of the note and is in twentieths of a second. As this number is 20, our note lasted for one second. Naturally, if you change the number to 10, the note will last for half a second.

You can also change the pitch to, say, D above middle C by changing the pitch number to 61. You may like to try playing the first two notes of a tune by putting two sound commands one after the other, like this:

    SOUND 1,-15,53,10:SOUND 1,-15,61,10

Unfortunately you will find that the second sound is made immediately and cancels out the first so you only get one note. What we need is a way of delaying the second sound.

BEAT, BEATS and TEMPO

The sound system takes its timings from the beat counter. This counter starts from zero and counts up to a number determined by the Basic keyword BEATS, at which point it's reset to zero and starts again.

If you type:

    PRINT BEATS

you will almost certainly get the answer zero, suggesting that the beat counter doesn't count at all. This is because BEATS is set to zero when the machine is reset or Esc pressed.

Before we set a value for BEATS, we need to know how fast the beat counter counts. This is determined by another Basic variable, TEMPO. If you type:

    PRINT TEMPO

you will probably get 4096, which in hexadecimal form is &1000. TEMPO is measured in beats per centi-second but the three lowest hexadecimal digits are a fractional part of the number. In other words, if TEMPO is &1000, the beat counter increases at one beat per centi-second (or 100 beats per second). If you double TEMPO to &2000 (or 8192), the beat counter counts at twice the rate and if you halve it to &800 (or 2048), it counts at half a beat per centi-second, 50 beats per second. For our experiments, we may as well leave it as it is.

Now that we know how fast the counter runs, we can decide how often we want it to be reset. Let's reset it once every second when it's reached 100, which we do by typing:

    BEATS 100

We can now delay the second note by adding a fifth number to its command, the after parameter. This tells the sound system not to produce the note until the beat counter has reached a certain value. If we sound the first note for half a second, we want to delay the second one by this amount, that is 50 beats.

After setting BEATS to 100, you could try typing the following:

    SOUND 1,-15,53,10:SOUND 1,-15,61,10,50

Notice the extra number on the end of the second command, delaying the sound by 50 beats.

Unfortunately, this won't work properly. If you try it you will probably either get a shortened first note or no first note at all. The problem is that the beat counter is counting all the time. If it's somewhere between zero and 50 when the commands are given, the second note will be sounded as soon as it gets to 50 which will shorten the first note. If it has already passed 50, the second note will be sounded immediately, so there will be no first note at all.

The way out of this problem is to wait until the counter is reset to zero and then give the commands. We can read the counter with the Basic keyword BEAT (not to be confused with BEATS), and wait for it to return to zero using a REPEAT ... UNTIL loop like this:

    REPEAT UNTIL BEAT=0:SOUND 1,-15,53,10:SOUND 1,-15,61,10,50

There may be a slight pause before the first note, while the loop waits for the counter to come back round to zero but you should now have both notes sounding correctly.

An Arpeggio

By using four half-second notes and with the beat counter repeating every two seconds, we can play an arpeggio:

    10 REM Arpeggio
    20 REM plays an arpeggio
    30 BEATS 200
    40 REPEAT UNTIL BEAT=0
    50 SOUND 1,-15,53,10
    60 SOUND 1,-15,69,10,50
    70 SOUND 1,-15,81,10,100
    80 SOUND 1,-15,101,10,150

Line 40 waits until the beat counter has been reset to zero, then the remaining lines give their sound commands. The first note is sounded without a delay, the second with a delay of 50 beats (50 centi-seconds as we haven't altered the value of TEMPO), the third with a delay of 100 beats and the fourth a delay of 150 beats.

We can think of the time for 200 counts as one bar of music. If we wanted to add a second bar, we could wait for BEAT to come round to zero again and add some more notes.

A Simple Tune

We are now ready to think in terms of playing a tune. Frère Jaques is a good one as it's very simple, and to make it even easier, we'll play it in C major (no sharps or flats!).

The notes are all read in from DATA statements. You could replace the numbers with your own to make the program play any tune you like. Each note has two numbers referring to the pitch (which you can get from the table near the beginning of this section) and the duration, in twentieths of a second.

To simplify the DATA statements, one line is used for each bar. The eight lines in fact consist of four pairs of identical lines, simply because every bar in this tune repeats itself.

The list of data is finished off with a -1. When the program reads this, it starts reading the data from the beginning again so that the program repeats itself ad infinitum (or ad nauseam!).

Line 40 sets the limit of the beat counter to 200. This allows us to play four half-second notes in one bar.

The REPEAT ... UNTIL loop between lines 50 and 90 calls PROCbar each time the beat counter is reset. Line 60 waits for the reset, line 70 calls PROCbar and line 80 ensures that the beat counter has moved on from zero before returning to the beginning of the loop. If we didn't do this the program may try to play several bars at the same time.

Setting the Timing for Each Note

PROCbar uses variable beat% to work out how much delay should be given to each note, by adding up the durations of the preceding notes in the bar. For each note, line 140 reads the pitch data. If it reads -1, it's reached the end of the data. Line 160 then restores the DATA pointer to the beginning of the DATA statements and calls PROCbar again (this is called recursion) to restart the piece.

Assuming that the pitch data wasn't -1, line 180 then reads the next number, which is the duration of the note. Armed with these two pieces of information, line 190 is able to construct the SOUND command for the note. Each note is given a delay, taken from the value of variable beat%.

After the command, beat% is increased to take account of the length of the note. The variable dur%, which sets the duration of the note in the SOUND command, is in twentieths of a second so it has to be multiplied by 5 before being added onto the value of beat%, which is in centi-seconds.

A convention which we've adopted here is to use a pitch number of zero to indicate a rest. If pitch% is zero, line 190 doesn't produce a SOUND command at all but beat% is still increased to take account of the length of the rest.

When beat% reaches (or exceeds, to be on the safe side) 200, the bar is full and PROCbar ends, returning control to the main program to wait for the next reset of the beat counter.

If you think the tune is played too slowly, you can always increase TEMPO. To double the speed for example, type:

    TEMPO &2000

before running the program. Although this shortens the delay between the notes it doesn't make the notes themselves any shorter. Each one is, in fact, shortened by being cut off when the following one is produced, but the rests disappear.

Adding Music to the Game

Now we can consider how to add a procedure to our Munchie game to make it play a tune.

This next listing, Munchie5, only shows the parts of the program which have been changed.

We've added some lines to PROCinit. Line 190 sets the number of active channels to two. This is so that we can play the tune on channel 2 while still producing beeps on channel 1 whenever our character swallows up a circle.

Line 200 sets the waveform for the music, while line 210 redefines the one for the beep. It has to compete with the music now so it needs to be louder. Line 220 sets up the beat counter and line 230 creates a variable which we will use to tell us whether or not PROCbar has been called.

In the main program, the error handler on line 30 and the two extra lines 80 and 90 reset the sound channels to their original state, so that a beep reverts to its usual self again when the program has finished, whether by coming to an end or through an error.

Keeping the Loop Going

PROCgame contains the loop which the game runs round while it's being played. We can use this loop to keep calling PROCbar but we mustn't hold it up or the game would freeze. For this reason we can't use our REPEAT ... UNTIL loops to wait for the beat counter to come round to zero so we have to think of something else. This is where variable bar_sent%, which we created in PROCinit, comes in.

If line 400 detects that the beat counter is in the first half of its count, the following line checks to see if PROCbar has already been called that time round the counting cycle. If it hasn't, line 410 calls it, then sets bar_sent% to TRUE, indicating that it has been dealt with. On the next few times round the loop, bar_sent% will be seen to be TRUE, preventing PROCbar from being called again.

Once the beat counter has passed the halfway stage in its counting, bar_sent% can be reset to FALSE again by line 430. This procedure allows plenty of flexibility in the exact time when BEAT is read. It isn't necessary to detect exactly when BEAT is zero, which is important because it could happen while the program is busy redrawing a sprite.

PROCbar is identical to the procedure in our Frère Jaques program except that the SOUND command now calls channel 2.

When a game is completed, the tune stops because the loop in PROCgame is no longer being run. When you start a new game, PROCgame is called again and line 380 resets the DATA pointer to the beginning of the data, so the tune starts from the beginning again.

Stereo Sounds

Some models of RISC OS computer have only one internal speaker but others have two and can reproduce stereo sounds. All machines, though, have a stereo audio socket which can feed a Hi-Fi amplifier or stereo headphones so any RISC OS computer can produce a stereo output.

You can position any sound channel wherever you like in the stereo image by using the STEREO command, for example:

    STEREO 1,-127

The STEREO command is followed by two numbers. The first is the channel number and the second is the stereo position, -127 for extreme left, 0 for centre and 127 for extreme right. The example positions channel 1 on the left-hand side of the stereo image.

All channels are automatically set to the centre unless you change them.

previousmain indexnext

 
© Martyn & Christine Fox 2003