589 lines
21 KiB
Text
589 lines
21 KiB
Text
== Lesson 10: Sound
|
|
|
|
image::learn/10-sound.png[The finished lesson, 480]
|
|
|
|
A game with no sound feels broken in a way that is hard to put a finger on.
|
|
The shots go out and nothing happens. The rocks break and nothing happens. Put
|
|
a half second noise on each of those and the same game suddenly feels like it
|
|
is made of something.
|
|
|
|
This lesson gives your game a shot, an explosion, and music underneath. On the
|
|
way it introduces your third callback: the one the engine uses to tell you
|
|
that a sound has finished playing.
|
|
|
|
=== One Noise
|
|
|
|
The art kit you copied in lesson nine has two sounds in it, `shoot.wav` and
|
|
`boom.wav`, and the explosion is the easier of the two to hear. Make a new
|
|
file beside your `art` folder and type this in.
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
|
|
local boom = soundLoad(DIR .. "art/boom.wav")
|
|
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
soundPlay(boom)
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Press space.")
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
function onShutdown()
|
|
soundUnload(boom)
|
|
end
|
|
----
|
|
|
|
Run it and tap the space bar three or four times quickly. The bangs pile up on
|
|
top of each other instead of cutting each other off.
|
|
|
|
Holding the space bar down gives you exactly one bang, not a stream of them.
|
|
That is the same behaviour you met in lesson three: a held key is one press,
|
|
reported once, and the engine throws away the repeats your keyboard sends. It
|
|
is why steering needs `onInputReleased` and a variable rather than counting
|
|
presses.
|
|
|
|
=== What Just Happened
|
|
|
|
The shape is the same as lesson nine's: load once at the top, use the handle,
|
|
give it back at the end.
|
|
|
|
[source,lua]
|
|
----
|
|
local boom = soundLoad(DIR .. "art/boom.wav")
|
|
----
|
|
|
|
`soundLoad` reads an audio file and keeps it in memory, and hands back a
|
|
handle -- the same idea as a sprite handle, a number that means "that clip" to
|
|
the engine. `DIR` is there for the same reason it was there in lesson nine:
|
|
the sound lives in your game's folder, and the game has to be able to find it
|
|
from wherever Singe was started.
|
|
|
|
Everything lesson nine said about loading applies here word for word. Load at
|
|
the top of the file, once. A `soundLoad` inside `onOverlayUpdate` loads the
|
|
clip sixty times a second and never gives one back, and the game dies of it
|
|
some minutes later.
|
|
|
|
[source,lua]
|
|
----
|
|
soundPlay(boom)
|
|
----
|
|
|
|
Play it. That is the whole of playing a sound effect, and the engine takes
|
|
care of the rest: the clip starts, it finishes, and it never gets in the way
|
|
of your program.
|
|
|
|
But `soundPlay` hands something back too, and it is *not* the same number you
|
|
gave it. Change the line and look:
|
|
|
|
[source,lua]
|
|
----
|
|
local channel = soundPlay(boom)
|
|
debugPrint("clip " .. boom .. " on channel " .. channel)
|
|
----
|
|
|
|
Tap the space bar quickly half a dozen times and watch the two numbers. The
|
|
clip number never changes. The channel number climbs -- 0, then 1, then 2 --
|
|
and once the earlier bangs have finished it drops back and starts reusing the
|
|
low numbers again.
|
|
|
|
This is the single thing about sound that catches everybody, so it is worth
|
|
saying slowly. *Two different kinds of number are in play.*
|
|
|
|
The *clip handle* from `soundLoad` is the recording. There is one of it. It
|
|
does not change, it is not playing or not playing, it is the sound sitting
|
|
in memory waiting to be used.
|
|
|
|
The *channel* from `soundPlay` is one performance of that recording. Singe has
|
|
sixteen of them, numbered `0` to `15`, and every sound that is currently
|
|
audible is using one. That is why the bangs overlap: the second press did not
|
|
interrupt the first, it started a second performance on the next free channel.
|
|
|
|
Everything that acts on a *sound you can hear* takes a channel: `soundStop`,
|
|
`soundIsPlaying`, `soundPause`, `soundResume`. Everything that acts on the
|
|
*recording* takes the handle: `soundPlay` and `soundUnload`. Hand one where
|
|
the other belongs and you will not always get an error, because `3` is a valid
|
|
channel number and also a perfectly good clip handle -- you will get silence,
|
|
or the wrong sound stopping, which is far harder to find.
|
|
|
|
Sixteen is a lot, but it is not endless. When every channel is busy, nothing
|
|
plays and `soundPlay` hands back `-1` instead of a channel number. A game that
|
|
fires a great many sounds at once should expect that, and you will write one
|
|
in a minute.
|
|
|
|
=== Volume
|
|
|
|
There are three volumes, and they multiply together.
|
|
|
|
The *master* effects volume covers everything. `soundGetVolume` reads it and
|
|
`soundSetVolume` sets it, on a scale of `0` to `63`.
|
|
|
|
[source,lua]
|
|
----
|
|
soundSetVolume(40)
|
|
----
|
|
|
|
Resist. That number is the player's, not yours: it starts at whatever they
|
|
asked for when they launched the game, and a game that overwrites it is a game
|
|
that is too loud in somebody's quiet room. Change the master volume only when
|
|
the player asks you to, from an options screen. `soundGetVolume` is there so
|
|
that an options screen can show what it currently is instead of guessing.
|
|
|
|
What you should reach for is the *channel* volume, which sits under the
|
|
master. Give `soundPlay` a third argument and this one performance is quieter:
|
|
|
|
[source,lua]
|
|
----
|
|
soundPlay(shootClip, 0, 35)
|
|
----
|
|
|
|
The `0` in the middle is how many times to repeat, which you have to give
|
|
because the volume comes after it. `0` means play it once. `-1` means loop
|
|
until something stops it, which is how you would do an engine drone or wind.
|
|
The manual's entry for `soundPlay` lists all three arguments.
|
|
|
|
The third volume is distance, for sounds placed in a 3D world. That is for
|
|
part four, when the game moves into three dimensions.
|
|
|
|
Music has a volume of its own, and it is on a different scale. More on that in
|
|
a moment.
|
|
|
|
=== Stopping, and Not Stopping
|
|
|
|
`soundStop` takes a channel and cuts it off where it is:
|
|
|
|
[source,lua]
|
|
----
|
|
soundStop(engineChannel)
|
|
engineChannel = -1
|
|
----
|
|
|
|
Note the second line. As soon as a channel stops, the engine is free to hand
|
|
that same number to the next `soundPlay`, so a channel number you are still
|
|
holding may now belong to a completely different sound. Throw it away the
|
|
moment you stop it. `-1` is the useful thing to put there, because it is the
|
|
same value `soundPlay` gives you when it could not play at all, so one test
|
|
covers both: `if channel >= 0 then`.
|
|
|
|
`soundStop` is for a sound that is wrong now -- the engine noise when the
|
|
engine dies, the alarm when the alarm is answered. It is the wrong tool for a
|
|
short effect. A half second explosion chopped off after a tenth of a second
|
|
sounds like a mistake, because it is one.
|
|
|
|
So most of the time, do nothing at all. Let it finish. A sound effect that you
|
|
start and never think about again ends by itself and frees its channel by
|
|
itself, which is exactly what you want.
|
|
|
|
This holds even at the end. `soundUnload` does not cut off a clip that is
|
|
still playing: the engine keeps the audio alive until the last channel using
|
|
it has stopped. You can unload in `onShutdown` without worrying that you have
|
|
silenced something.
|
|
|
|
`soundIsPlaying(channel)` answers whether a channel is making noise right now.
|
|
Its main use is not curiosity but restraint: an alarm that should sound once,
|
|
and not restart every time the game notices the danger is still there.
|
|
|
|
[source,lua]
|
|
----
|
|
function raiseAlarm()
|
|
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
|
|
alarmChannel = soundPlay(alarmClip)
|
|
end
|
|
end
|
|
----
|
|
|
|
Read the condition aloud: play the alarm if there is no alarm channel, or if
|
|
the one you have has stopped. Otherwise leave it alone.
|
|
|
|
=== The Sound That Stutters
|
|
|
|
Here is the mistake. Everybody writes it, usually within a week of discovering
|
|
sound.
|
|
|
|
Suppose you wanted the ship to keep firing while the button is held down,
|
|
rather than once per press. You would do it the way you steer: a `firing`
|
|
variable set to `true` in `onInputPressed` and back to `false` in
|
|
`onInputReleased`, and the shot created in `onOverlayUpdate` while it is true.
|
|
That is a perfectly sensible thing to want, and the noise looks like it
|
|
belongs right there with the shot:
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
if firing then
|
|
shots[#shots + 1] = { x = shipX, y = shipY }
|
|
soundPlay(shootClip)
|
|
end
|
|
...
|
|
----
|
|
|
|
What comes out is not a shot. It is a buzzing rattle, or a hard flat tone, or
|
|
a sound like a small machine breaking.
|
|
|
|
`onOverlayUpdate` runs about sixty times a second. Hold the button for one
|
|
second and you have asked for sixty performances of a clip that lasts about
|
|
a fifth of a second, each one starting on top of the last. Sixteen of them are
|
|
playing at once within a quarter of a second, the other forty-four come back
|
|
`-1` and are silently dropped, and what you hear is sixty copies of the same
|
|
attack piled on each other.
|
|
|
|
The fix is not a cleverer sound. It is playing the sound where the *event*
|
|
happens rather than where the *state* is true:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
soundPlay(shootClip, 0, 35)
|
|
end
|
|
end
|
|
----
|
|
|
|
`onInputPressed` happens once, when the button goes down. One press, one shot,
|
|
one noise.
|
|
|
|
If you really do want held fire, the answer is the same one in a different
|
|
shape: make the firing itself an event. Count frames, exactly as the walk
|
|
cycle counted them in lesson nine, and create a shot only every eighth frame.
|
|
The noise goes with the shot, so it plays seven or eight times a second
|
|
instead of sixty, and that is a machine gun rather than a rattle.
|
|
|
|
That is the rule, and it is worth carrying out of this lesson: *sounds belong
|
|
with events, not with conditions*. A press, a hit, a death, a pickup -- each
|
|
of those happens once and deserves one sound. "The button is down" and "the
|
|
ship is moving" are not events; they are true for hundreds of frames in a row,
|
|
and anything you play from them plays hundreds of times.
|
|
|
|
When a sound really does belong to a state that lasts -- an engine, wind, a
|
|
siren -- start it once with `-1` for the loop count, keep the channel, and
|
|
stop it when the state ends.
|
|
|
|
=== The Third Callback
|
|
|
|
Sometimes you do need to know when a sound has finished. Write this function
|
|
and the engine will call it:
|
|
|
|
[source,lua]
|
|
----
|
|
function onSoundCompleted(channel)
|
|
end
|
|
----
|
|
|
|
That is your third callback, after `onOverlayUpdate` in lesson one and the two
|
|
input callbacks in lesson three, and it works the same way as all of them: you
|
|
write it, you never call it, the engine calls you. The `channel` it hands you
|
|
is the channel that has just gone quiet -- the number `soundPlay` gave you
|
|
when you started it.
|
|
|
|
It fires when a clip runs out, when the last repeat of a loop finishes, *and*
|
|
when you stop a channel yourself with `soundStop`. That last one surprises
|
|
people who use the callback to chain one sound into the next: stopping a
|
|
channel on purpose still reports it as completed, and if you are not careful
|
|
the next sound in the chain starts anyway.
|
|
|
|
The game is about to use it for something worth doing. When the last life
|
|
goes, the ship's explosion plays, and the game holds still -- no GAME OVER,
|
|
no music fading -- until that explosion has actually finished. Then everything
|
|
stops at once. It takes a handful of lines and it is the difference between an
|
|
ending and a cut.
|
|
|
|
=== Music Is Not a Sound Effect
|
|
|
|
You could load a three minute tune with `soundLoad` and play it with
|
|
`soundPlay`. Do not. Music gets its own family of calls, for reasons that
|
|
matter:
|
|
|
|
* Music has its own volume, separate from the effects, so a player can turn
|
|
the music down and still hear the shots.
|
|
* There is no sixteen channel limit and no channel number. One handle is one
|
|
piece of music, and you stop it, pause it, and resume it by that handle.
|
|
* `musicStop` can fade a piece out over a second or two instead of cutting it.
|
|
|
|
The calls mirror the ones you already know:
|
|
|
|
[source,lua]
|
|
----
|
|
local theme = musicLoad("Singe/menuIntro.flac")
|
|
|
|
musicSetVolume(50)
|
|
musicPlay(theme, -1)
|
|
----
|
|
|
|
`musicLoad` gives a handle. `musicPlay` starts it, with the same loop count as
|
|
`soundPlay`: `-1` for forever. `musicStop(theme)` ends it, and
|
|
`musicStop(theme, 1500)` takes a second and a half to fade it away.
|
|
`musicUnload` gives it back.
|
|
|
|
Now the trap. *Music volume runs from 0 to 128, not 0 to 63.* Sound effects
|
|
use one scale and music uses another, twice as long, for historical reasons
|
|
that are no comfort at all when your music comes out at half the volume you
|
|
meant. When you are working with music, double the number you had in mind for
|
|
an effect.
|
|
|
|
The piece being loaded there is the engine's own menu music, out of the
|
|
`Singe` folder the engine unpacked into your work folder on your first run.
|
|
It is ten seconds long and it was written to fade out at the end, so looping
|
|
it sounds exactly like a ten second loop that keeps fading out. That is fine
|
|
for learning the calls and no good at all for a finished
|
|
game, which ships a longer piece of its own. Any of the usual formats will do:
|
|
the manual's Music section lists them, and Ogg Vorbis or MP3 are the sensible
|
|
choices, because a few minutes of music as a WAV is an enormous file.
|
|
|
|
=== The Game Gets a Soundtrack
|
|
|
|
Open the game from lesson nine. Everything here is an addition to it; nothing
|
|
comes out.
|
|
|
|
Four things to load, at the top, with the sprites:
|
|
|
|
[source,lua]
|
|
----
|
|
local shootClip = soundLoad(DIR .. "art/shoot.wav")
|
|
local boomClip = soundLoad(DIR .. "art/boom.wav")
|
|
local clickClip = soundLoad("Singe/click.wav")
|
|
local theme = musicLoad("Singe/menuIntro.flac")
|
|
----
|
|
|
|
The first two have `DIR` on them and the last two do not, and that is on
|
|
purpose. `DIR` means *your* game's folder. A name that begins with `Singe/`
|
|
means the engine's own folder instead, wherever that is -- which is why
|
|
`dofile("Singe/Framework.singe")` has worked since lesson seven without a
|
|
`DIR` either. Your own files need `DIR`; the engine's never do.
|
|
|
|
Two new variables, beside the others:
|
|
|
|
[source,lua]
|
|
----
|
|
local dying = false
|
|
local deathChannel = -1
|
|
----
|
|
|
|
`dying` is the moment between the last life going and the game being over:
|
|
the explosion is still sounding, and nothing on screen is moving.
|
|
|
|
The shot gets its noise in `onInputPressed`, right where the shot itself is
|
|
created, and the restart gets a click so that pressing `1` feels like it did
|
|
something:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if over then
|
|
if what == SWITCH_START1 then
|
|
soundPlay(clickClip)
|
|
startGame()
|
|
end
|
|
return
|
|
end
|
|
if what == SWITCH_LEFT then
|
|
goLeft = true
|
|
elseif what == SWITCH_RIGHT then
|
|
goRight = true
|
|
elseif what == SWITCH_BUTTON1 then
|
|
shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY }
|
|
soundPlay(shootClip, 0, 35)
|
|
end
|
|
end
|
|
----
|
|
|
|
A rock that is shot bangs, and so does a rock that hits you. In `updateShots`,
|
|
beside the score:
|
|
|
|
[source,lua]
|
|
----
|
|
newRock(rock)
|
|
score = score + HIT_SCORE
|
|
gone = true
|
|
soundPlay(boomClip, 0, 45)
|
|
----
|
|
|
|
And in `updateRocks`, where the life is lost, the interesting part:
|
|
|
|
[source,lua]
|
|
----
|
|
if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
local channel = soundPlay(boomClip)
|
|
|
|
newRock(rock)
|
|
lives = lives - 1
|
|
if lives <= 0 then
|
|
dying = true
|
|
deathChannel = channel
|
|
if channel < 0 then
|
|
endGame()
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
When the last life goes, the explosion plays and its channel is remembered.
|
|
The game is now `dying`, which stops everything moving but does not yet say
|
|
GAME OVER.
|
|
|
|
The `if channel < 0` is the sixteen channel limit, handled. If every channel
|
|
happened to be busy at that exact moment, there is no explosion and no channel
|
|
to wait for, and without those two lines the game would sit in `dying`
|
|
forever, waiting for a sound that never played. Waiting for something that
|
|
cannot arrive is one of the easiest ways to hang a program, and the guard is
|
|
always cheaper than the bug.
|
|
|
|
Now the new callback, and the little function it shares with that guard:
|
|
|
|
[source,lua]
|
|
----
|
|
function endGame()
|
|
dying = false
|
|
deathChannel = -1
|
|
over = true
|
|
musicStop(theme, 1500)
|
|
end
|
|
|
|
|
|
function onSoundCompleted(channel)
|
|
if channel == deathChannel then
|
|
endGame()
|
|
end
|
|
end
|
|
----
|
|
|
|
The engine calls `onSoundCompleted` for *every* channel that stops, so the
|
|
first thing it does is check whether this one is the channel it cares about.
|
|
Everything else -- every shot, every rock -- passes through and is ignored.
|
|
|
|
`onOverlayUpdate` needs to hold still while the explosion runs, which is one
|
|
word:
|
|
|
|
[source,lua]
|
|
----
|
|
if not over and not dying then
|
|
updateShip()
|
|
updateShots()
|
|
updateRocks()
|
|
end
|
|
----
|
|
|
|
`startGame` clears the two new variables and starts the music over:
|
|
|
|
[source,lua]
|
|
----
|
|
dying = false
|
|
deathChannel = -1
|
|
musicSetVolume(50)
|
|
musicPlay(theme, -1)
|
|
----
|
|
|
|
And `onShutdown` gives back what was loaded:
|
|
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
spriteUnload(shipSprite)
|
|
spriteUnload(rockSprite)
|
|
spriteUnload(shotSprite)
|
|
spriteUnload(starSprite)
|
|
soundUnload(shootClip)
|
|
soundUnload(boomClip)
|
|
soundUnload(clickClip)
|
|
musicUnload(theme)
|
|
end
|
|
----
|
|
|
|
Run it. Fire at the rocks, let one hit you three times, and listen to the end:
|
|
the last bang plays out in full, and only when it is gone does the music start
|
|
to fade and the words come up.
|
|
|
|
The finished script is in the `learn` folder as `10-sound.singe` if you want
|
|
to compare.
|
|
|
|
=== Try It
|
|
|
|
. *Make the shot quieter still.* Change the `35` in the shot's `soundPlay` to
|
|
`10`, then to `63`. Find the number where it sits under the explosions
|
|
instead of on top of them.
|
|
. *Turn the music down without touching the effects.* Change
|
|
`musicSetVolume(50)` to `musicSetVolume(15)`. Confirm that the bangs are
|
|
exactly as loud as they were, which is the whole reason music is its own
|
|
system.
|
|
. *Cut the ending off.* In `endGame`, change `musicStop(theme, 1500)` to
|
|
`musicStop(theme)`. Listen to both endings twice. The difference is a number
|
|
in one argument and it is not a small difference.
|
|
. *Give the ship an engine.* Start `soundPlay(clickClip, -1, 12)` in
|
|
`startGame`, keep the channel it gives you in a variable of its own, and
|
|
stop it in `endGame`. The click lasts a fiftieth of a second, so looping it
|
|
is a buzz, which is as close to an engine as this kit gets. Then take the
|
|
`soundStop` out again, play three games in a row, and listen to what you
|
|
have built.
|
|
. *Count the channels.* Print the channel number every time a rock explodes.
|
|
Then raise `ROCK_COUNT` until several explode at once, and keep raising it
|
|
until you see a `-1` go by.
|
|
|
|
=== Break It on Purpose
|
|
|
|
You have two volume scales in your head now, and sooner or later the wrong one
|
|
comes out of your fingers. Add this line on its own, just below the four
|
|
`soundLoad` and `musicLoad` lines:
|
|
|
|
[source,lua]
|
|
----
|
|
soundSetVolume(100)
|
|
----
|
|
|
|
The game refuses to start:
|
|
|
|
----
|
|
20:soundSetVolume: Invalid sound volume value: 100
|
|
----
|
|
|
|
The line number you get is the line you actually typed it on, so yours may not
|
|
be 20. Then the function that objected, and the value it objected to. The
|
|
message does not tell you what the range is -- that is what the manual's entry
|
|
for `soundSetVolume` is for -- but it does tell you the number it would not
|
|
take, and once you know that effects stop at 63 the fix is one digit.
|
|
|
|
`100` is a suspicious number to find in a volume, and it is worth knowing why
|
|
you typed it. Almost certainly you thought of volume as a percentage, because
|
|
every volume slider you have ever used goes to 100. Singe's does not. Effects
|
|
are `0` to `63` and music is `0` to `128`, and neither is a percentage of
|
|
anything.
|
|
|
|
This is the general shape of an engine error, as opposed to the Lua errors in
|
|
lesson one: a line number, the name of the engine function you called, and a
|
|
complaint about the values you handed it. When you see one, the manual's entry
|
|
for that function is the next thing to read.
|
|
|
|
=== What You Learned
|
|
|
|
* `soundLoad` reads a clip and returns a handle. Load once, at the top of the
|
|
file.
|
|
* `soundPlay` starts a clip and returns a *channel*, which is a different kind
|
|
of number from the handle.
|
|
* Singe mixes sixteen channels. When they are all busy, `soundPlay` returns
|
|
`-1` and nothing plays.
|
|
* Channel numbers are reused. Forget one as soon as you have stopped it.
|
|
* Short effects should be left alone to finish; `soundStop` is for sounds that
|
|
have become wrong.
|
|
* `soundUnload` does not cut off a clip that is still playing.
|
|
* Play a sound where the event happens, not where a condition is true, or it
|
|
plays sixty times a second.
|
|
* `onSoundCompleted(channel)` is a callback the engine calls each time a
|
|
channel goes quiet, for any reason.
|
|
* Music is a separate system with its own handles, its own volume, and no
|
|
channel limit.
|
|
* Effects volume is `0` to `63`. Music volume is `0` to `128`. They are not
|
|
percentages.
|
|
|
|
=== Next Time
|
|
|
|
Your rocks are hit when their rectangles overlap, which is why a shot that
|
|
passes through a corner of the sky still counts. In lesson eleven you get the
|
|
engine's own collision tests, learn what a hitbox is, and find out why the
|
|
rectangle around a round rock is the wrong shape.
|