singe/docs/fr/lessons/10-sound.adoc

623 lines
24 KiB
Text
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[#_lesson_10_sound]
== Leçon 10 : Le son
image::learn/10-sound.png[La leçon terminée, 480]
Un jeu sans son a l'air cassé, d'une façon difficile à cerner. Les tirs partent
et rien ne se passe. Les rochers éclatent et rien ne se passe. Mettez un bruit
d'une demi-seconde sur chacun de ces moments et, tout à coup, le même jeu semble
fait de quelque chose.
Cette leçon donne à votre jeu un tir, une explosion et de la musique en dessous.
En chemin, elle vous présente votre cinquième callback : celui dont le moteur se
sert pour vous dire qu'un son a fini de jouer.
[#_one_noise]
=== Un bruit
Le kit d'illustrations que vous avez copié à la leçon neuf contient deux sons,
`shoot.wav` et `boom.wav`, et l'explosion est le plus facile des deux à
entendre. Créez un nouveau fichier à côté de votre dossier `art` et tapez ceci.
[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
----
Lancez-le et tapez rapidement trois ou quatre fois sur la barre d'espace. Les
détonations s'empilent les unes sur les autres au lieu de se couper la parole.
Garder la barre d'espace enfoncée vous donne exactement une détonation, pas une
rafale. C'est le même comportement que celui que vous avez rencontré à la leçon
trois : une touche tenue, c'est un seul appui, signalé une fois, et le moteur
jette les répétitions que votre clavier envoie. C'est pour ça que diriger le
vaisseau demande `onInputReleased` et une variable, plutôt que de compter les
appuis.
[#_what_just_happened_6]
=== Ce qui vient de se passer
La forme est la même qu'à la leçon neuf : chargez une fois en haut, servez-vous
du handle, rendez-le à la fin.
[source,lua]
----
local boom = soundLoad(DIR .. "art/boom.wav")
----
`soundLoad` lit un fichier audio, le garde en mémoire et vous renvoie un
handle -- la même idée qu'un handle de sprite, un nombre qui veut dire « ce
son-là » pour le moteur. `DIR` est là pour la même raison qu'à la leçon neuf :
le son se trouve dans le dossier de votre jeu, et le jeu doit pouvoir le
trouver, d'où que Singe ait été lancé.
Tout ce que la leçon neuf a dit du chargement s'applique ici mot pour mot.
Chargez en haut du fichier, une fois. Un `soundLoad` dans `onOverlayUpdate`
charge le son soixante fois par seconde sans jamais en rendre un seul, et le
jeu en meurt quelques minutes plus tard.
[source,lua]
----
soundPlay(boom)
----
Jouez-le. C'est tout ce qu'il faut pour jouer un effet sonore, et le moteur
s'occupe du reste : le son démarre, il se termine, et il ne gêne jamais votre
programme.
Mais `soundPlay` renvoie quelque chose, lui aussi, et ce n'est *pas* le nombre
que vous lui avez donné. Changez la ligne et regardez :
[source,lua]
----
local channel = soundPlay(boom)
debugPrint("clip " .. boom .. " on channel " .. channel)
----
Tapez rapidement une demi-douzaine de fois sur la barre d'espace et observez
les deux nombres. Le numéro du son ne change jamais. Le numéro de canal
grimpe -- 0, puis 1, puis 2 -- et une fois que les premières détonations sont
finies, il redescend et se remet à réutiliser les petits numéros.
C'est la chose qui piège tout le monde avec le son, alors elle mérite d'être
dite lentement. *Deux sortes de nombres différentes sont en jeu.*
Le *handle du son*, celui que donne `soundLoad`, c'est l'enregistrement. Il n'y
en a qu'un. Il ne change pas, il n'est ni en train de jouer ni arrêté, c'est le
son posé en mémoire, qui attend qu'on s'en serve.
Le *canal* que donne `soundPlay`, c'est une lecture de cet enregistrement.
Singe en a seize, numérotés de `0` à `15`, et chaque son qu'on entend en ce
moment en utilise un. Voilà pourquoi les détonations se chevauchent : le
deuxième appui n'a pas interrompu le premier, il a lancé une deuxième lecture
sur le canal libre suivant.
Tout ce qui agit sur un *son que vous entendez* prend un canal : `soundStop`,
`soundIsPlaying`, `soundPause`, `soundResume`. Tout ce qui agit sur
l'*enregistrement* prend le handle : `soundPlay` et `soundUnload`. Donnez l'un
là où l'autre est attendu et vous n'aurez pas toujours une erreur, parce que `3`
est un numéro de canal valable et aussi un handle de son tout à fait
correct -- vous aurez du silence, ou le mauvais son qui s'arrête, ce qui est
bien plus difficile à trouver.
Seize, c'est beaucoup, mais ce n'est pas infini. Quand tous les canaux sont
occupés, rien ne joue et `soundPlay` renvoie `-1` au lieu d'un numéro de canal.
Un jeu qui lance énormément de sons à la fois doit s'y attendre, et vous allez
en écrire un dans une minute.
[#_volume]
=== Le volume
Il y a trois volumes, et ils se multiplient entre eux.
Le volume *général* des effets couvre tout. `soundGetVolume` le lit et
`soundSetVolume` le règle, sur une échelle de `0` à `63`.
[source,lua]
----
soundSetVolume(40)
----
Résistez. Ce nombre appartient au joueur, pas à vous : il part de ce que le
joueur a demandé en lançant le jeu, et un jeu qui l'écrase est un jeu trop fort
dans la pièce tranquille de quelqu'un. Ne changez le volume général que si le
joueur vous le demande, depuis un écran d'options. `soundGetVolume` est là pour
qu'un écran d'options puisse afficher sa valeur actuelle au lieu de la deviner.
Ce dont vous devriez vous servir, c'est le volume du *canal*, qui se place sous
le volume général. Donnez un troisième argument à `soundPlay` et cette
lecture-là est plus douce :
[source,lua]
----
soundPlay(shootClip, 0, 35)
----
Le `0` du milieu est le nombre de répétitions, que vous devez donner parce que
le volume vient après. `0` veut dire jouer une fois. `-1` veut dire boucler
jusqu'à ce que quelque chose l'arrête, et c'est comme ça que vous feriez le
ronronnement d'un moteur ou le vent. L'entrée de la Référence pour `soundPlay`
donne les trois arguments.
Le troisième volume, c'est la distance, pour les sons placés dans un monde en
3D. Ce sera pour la quatrième partie, quand le jeu passera en trois dimensions.
La musique a son propre volume, et il est sur une autre échelle. On y revient
dans un instant.
[#_stopping_and_not_stopping]
=== Arrêter, et ne pas arrêter
`soundStop` prend un canal et le coupe là où il en est :
[source,lua]
----
soundStop(engineChannel)
engineChannel = -1
----
Remarquez la deuxième ligne. Dès qu'un canal s'arrête, le moteur est libre de
donner ce même numéro au `soundPlay` suivant, si bien qu'un numéro de canal que
vous gardez encore peut maintenant appartenir à un tout autre son. Jetez-le dès
que vous l'arrêtez. `-1` est la bonne chose à mettre à sa place, parce que
c'est la même valeur que celle que vous donne `soundPlay` quand il n'a rien pu
jouer du tout, et un seul test couvre les deux cas : `if channel >= 0 then`.
`soundStop`, c'est pour un son qui n'a plus lieu d'être -- le bruit du moteur
quand le moteur meurt, l'alarme quand quelqu'un a répondu à l'alarme. Ce n'est
pas le bon outil pour un effet court. Une explosion d'une demi-seconde coupée au
bout d'un dixième de seconde sonne comme une erreur, parce que c'en est une.
Alors, la plupart du temps, ne faites rien du tout. Laissez-le finir. Un effet
sonore que vous lancez et auquel vous ne pensez plus se termine tout seul et
libère son canal tout seul, et c'est exactement ce que vous voulez.
C'est vrai même à la fin. `soundUnload` ne coupe pas un son qui est encore en
train de jouer : le moteur garde l'audio en vie jusqu'à ce que le dernier canal
qui s'en sert se soit arrêté. Vous pouvez décharger dans `onShutdown` sans
craindre d'avoir fait taire quelque chose.
`soundIsPlaying(channel)` répond à la question : ce canal fait-il du bruit en
ce moment ? Son usage principal n'est pas la curiosité mais la retenue : une
alarme qui doit sonner une fois, et ne pas redémarrer chaque fois que le jeu
remarque que le danger est toujours là.
[source,lua]
----
function raiseAlarm()
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
alarmChannel = soundPlay(alarmClip)
end
end
----
Lisez la condition à voix haute : jouez l'alarme s'il n'y a pas de canal
d'alarme, ou si celui que vous avez s'est arrêté. Sinon, laissez-la tranquille.
[#_the_sound_that_stutters]
=== Le son qui bégaie
Voici l'erreur. Tout le monde l'écrit, en général dans la semaine qui suit sa
découverte du son.
Supposons que vous vouliez que le vaisseau continue de tirer tant que le bouton
est enfoncé, plutôt qu'une fois par appui. Vous le feriez comme pour diriger :
une variable `firing` mise à `true` dans `onInputPressed` et remise à `false`
dans `onInputReleased`, et le tir créé dans `onOverlayUpdate` tant qu'elle est
vraie. C'est une envie parfaitement sensée, et le bruit a tout l'air d'avoir sa
place juste là, avec le tir :
[source,lua]
----
function onOverlayUpdate()
if firing then
shots[#shots + 1] = { x = shipX, y = shipY }
soundPlay(shootClip)
end
...
----
Ce qui en sort n'est pas un tir. C'est un crépitement qui bourdonne, ou une
note dure et plate, ou le bruit d'une petite machine en train de casser.
`onOverlayUpdate` s'exécute environ soixante fois par seconde. Tenez le bouton
une seconde et vous avez demandé soixante lectures d'un son qui dure environ un
cinquième de seconde, chacune démarrant par-dessus la précédente. Environ onze
d'entre elles jouent à chaque instant, chacune une image derrière la précédente,
et ce que vous entendez, ce sont soixante copies de la même attaque empilées les
unes sur les autres.
La solution n'est pas un son plus malin. C'est de jouer le son là où
l'*événement* se produit plutôt que là où l'*état* est vrai :
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
soundPlay(shootClip, 0, 35)
end
end
----
`onInputPressed` se produit une fois, quand le bouton s'enfonce. Un appui, un
tir, un bruit.
Si vous voulez vraiment un tir continu, la réponse est la même sous une autre
forme : faites du tir lui-même un événement. Comptez les images, exactement
comme le cycle de marche les comptait à la leçon neuf, et ne créez un tir
qu'une image sur huit. Le bruit va avec le tir, donc il joue sept ou huit fois
par seconde au lieu de soixante, et ça, c'est une mitrailleuse plutôt qu'un
crépitement.
Voilà la règle, et elle mérite de sortir de cette leçon avec vous : *les sons
vont avec les événements, pas avec les conditions*. Un appui, un coup, une
mort, un objet ramassé -- chacun arrive une fois et mérite un son. « Le bouton
est enfoncé » et « le vaisseau bouge » ne sont pas des événements ; ils sont
vrais pendant des centaines d'images d'affilée, et tout ce que vous jouez à
partir d'eux joue des centaines de fois.
Quand un son appartient vraiment à un état qui dure -- un moteur, le vent, une
sirène -- lancez-le une fois avec `-1` comme nombre de répétitions, gardez le
canal, et arrêtez-le quand l'état prend fin.
[#_the_fifth_callback]
=== Le cinquième callback
Parfois, vous avez vraiment besoin de savoir quand un son est fini. Écrivez
cette fonction et le moteur l'appellera :
[source,lua]
----
function onSoundCompleted(channel)
end
----
C'est votre cinquième callback, après `onOverlayUpdate` à la leçon un, les deux
callbacks des commandes à la leçon trois et `onShutdown` à la leçon neuf, et il
marche comme tous les autres : vous l'écrivez, vous ne l'appelez jamais, c'est le moteur qui vous
appelle. Le `channel` qu'il vous passe est le canal qui vient de se taire -- le
numéro que `soundPlay` vous a donné quand vous l'avez lancé.
Il est appelé quand un son arrive au bout, quand la dernière répétition d'une
boucle se termine, *et* quand vous arrêtez un canal vous-même avec `soundStop`.
Ce dernier cas surprend ceux qui se servent du callback pour enchaîner un son
sur le suivant : arrêter un canal exprès le signale quand même comme terminé,
et si vous n'y prenez pas garde, le son suivant de la chaîne démarre malgré
tout.
Le jeu va s'en servir pour quelque chose qui en vaut la peine. Quand la
dernière vie s'en va, l'explosion du vaisseau joue, et le jeu reste immobile --
pas de GAME OVER, pas de musique qui s'éteint -- jusqu'à ce que cette explosion
soit vraiment finie. Ensuite, tout s'arrête d'un coup. Ça tient en une poignée
de lignes, et c'est la différence entre une fin et une coupure.
[#_music_is_not_a_sound_effect]
=== La musique n'est pas un effet sonore
Vous pourriez charger un air de trois minutes avec `soundLoad` et le jouer avec
`soundPlay`. Ne le faites pas. La musique a sa propre famille d'appels, pour des
raisons qui comptent :
* La musique a son propre volume, séparé de celui des effets, si bien qu'un
joueur peut baisser la musique et entendre encore les tirs.
* Il n'y a pas de limite de seize canaux, ni de numéro de canal. Un handle,
c'est un morceau de musique, et c'est par ce handle que vous l'arrêtez, le
mettez en pause et le reprenez.
* `musicStop` peut faire disparaître un morceau en fondu sur une seconde ou deux
au lieu de le couper.
Les appels sont le reflet de ceux que vous connaissez déjà :
[source,lua]
----
local theme = musicLoad("Singe/menuIntro.flac")
musicSetVolume(50)
musicPlay(theme, -1)
----
`musicLoad` donne un handle. `musicPlay` le lance, avec le même nombre de
répétitions que `soundPlay` : `-1` pour toujours. `musicStop(theme)` l'arrête,
et `musicStop(theme, 1500)` prend une seconde et demie pour le faire disparaître
en fondu. `musicUnload` le rend.
Maintenant, le piège. *Le volume de la musique va de 0 à 128, pas de 0 à 63.*
Les effets sonores utilisent une échelle et la musique une autre, deux fois plus
longue, pour des raisons historiques qui ne consolent de rien quand votre
musique sort à la moitié du volume que vous vouliez. Quand vous travaillez avec
la musique, doublez le nombre que vous auriez choisi pour un effet.
Le morceau chargé ici est la musique de menu du moteur lui-même, tirée du
dossier `Singe` que le moteur a déballé dans votre dossier de travail lors de
votre premier lancement. Il dure dix secondes et il a été écrit pour s'éteindre
en fondu à la fin, donc en boucle il sonne exactement comme une boucle de dix
secondes qui n'arrête pas de s'éteindre. C'est très bien pour apprendre les
appels et ça ne vaut rien du tout pour un jeu fini, qui embarque un morceau plus
long bien à lui. N'importe lequel des formats habituels fera l'affaire : la
section Musique de la Référence les donne, et Ogg Vorbis ou MP3 sont les choix
raisonnables, parce que quelques minutes de musique en WAV font un fichier
énorme.
[#_the_game_gets_a_soundtrack]
=== Le jeu reçoit une bande-son
Ouvrez le jeu de la leçon neuf. Tout ce qui suit s'y ajoute ; rien n'en est
retiré.
Quatre choses à charger, en haut, avec les 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")
----
Les deux premières portent `DIR` et les deux dernières non, et c'est voulu.
`DIR` veut dire le dossier de *votre* jeu. Un nom qui commence par `Singe/` veut
dire à la place le dossier du moteur lui-même, où qu'il soit -- c'est pourquoi
`dofile("Singe/Framework.singe")` marche depuis la leçon sept sans `DIR` lui
non plus. Vos propres fichiers ont besoin de `DIR` ; ceux du moteur, jamais.
Deux nouvelles variables, à côté des autres :
[source,lua]
----
local dying = false
local deathChannel = -1
----
`dying`, c'est le moment entre la perte de la dernière vie et la fin de la
partie : l'explosion résonne encore, et rien ne bouge à l'écran.
Le tir reçoit son bruit dans `onInputPressed`, à l'endroit même où le tir est
créé, et le redémarrage reçoit un clic, pour qu'appuyer sur `1` donne
l'impression d'avoir fait quelque chose :
[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
----
Un rocher abattu fait boum, et un rocher qui vous percute aussi. Dans
`updateShots`, à côté du score :
[source,lua]
----
newRock(rock)
score = score + HIT_SCORE
gone = true
soundPlay(boomClip, 0, 45)
----
Et dans `updateRocks`, là où la vie est perdue, la partie intéressante :
[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
----
Quand la dernière vie s'en va, l'explosion joue et son canal est mémorisé. Le
jeu est maintenant `dying`, ce qui arrête tout mouvement mais n'affiche pas
encore GAME OVER.
Le `if channel < 0`, c'est la limite des seize canaux, prise en compte. Si tous
les canaux se trouvaient occupés à cet instant précis, il n'y a ni explosion ni
canal à attendre, et sans ces deux lignes le jeu resterait dans `dying` pour
toujours, à attendre un son qui n'a jamais joué. Attendre quelque chose qui ne
peut pas arriver est l'une des façons les plus simples de bloquer un programme,
et le garde-fou coûte toujours moins cher que le bogue.
Maintenant, le nouveau callback, et la petite fonction qu'il partage avec ce
garde-fou :
[source,lua]
----
function endGame()
dying = false
deathChannel = -1
over = true
musicStop(theme, 1500)
end
function onSoundCompleted(channel)
if channel == deathChannel then
endGame()
end
end
----
Le moteur appelle `onSoundCompleted` pour *chaque* canal qui s'arrête, alors la
première chose que fait la fonction, c'est vérifier si celui-ci est le canal qui
l'intéresse. Tout le reste -- chaque tir, chaque rocher -- passe et est ignoré.
`onOverlayUpdate` doit rester immobile pendant l'explosion, et ça tient en un
mot :
[source,lua]
----
if not over and not dying then
updateShip()
updateShots()
updateRocks()
end
----
`startGame` remet à zéro les deux nouvelles variables et relance la musique
depuis le début :
[source,lua]
----
dying = false
deathChannel = -1
musicSetVolume(50)
musicPlay(theme, -1)
----
Et `onShutdown` rend ce qui a été chargé :
[source,lua]
----
function onShutdown()
spriteUnload(shipSprite)
spriteUnload(rockSprite)
spriteUnload(shotSprite)
spriteUnload(starSprite)
soundUnload(shootClip)
soundUnload(boomClip)
soundUnload(clickClip)
musicUnload(theme)
end
----
Lancez-le. Tirez sur les rochers, laissez-vous toucher trois fois, et écoutez
la fin : la dernière détonation joue jusqu'au bout, et c'est seulement quand
elle s'est tue que la musique commence à s'éteindre et que les mots apparaissent.
Le script terminé se trouve dans le dossier `learn` sous le nom
`10-sound.singe`, si vous voulez comparer.
[#_try_it_10]
=== Essayez
. *Rendez le tir encore plus discret.* Changez le `35` du `soundPlay` du tir en
`10`, puis en `63`. Trouvez le nombre où il passe sous les explosions au lieu
de passer par-dessus.
. *Baissez la musique sans toucher aux effets.* Changez `musicSetVolume(50)` en
`musicSetVolume(15)`. Vérifiez que les détonations sont exactement aussi
fortes qu'avant, ce qui est toute la raison pour laquelle la musique est un
système à part.
. *Coupez la fin.* Dans `endGame`, changez `musicStop(theme, 1500)` en
`musicStop(theme)`. Écoutez les deux fins deux fois. La différence tient à un
nombre dans un argument, et ce n'est pas une petite différence.
. *Donnez un moteur au vaisseau.* Lancez `soundPlay(clickClip, -1, 12)` dans
`startGame`, gardez le canal qu'il vous donne dans une variable à lui, et
arrêtez-le dans `endGame`. Le clic dure un cinquantième de seconde, donc en
boucle c'est un bourdonnement, ce qui est ce que ce kit a de plus proche d'un
moteur. Puis retirez de nouveau le `soundStop`, jouez trois parties
d'affilée, et écoutez ce que vous avez construit.
. *Comptez les canaux.* Affichez le numéro de canal chaque fois qu'un rocher
explose. Puis augmentez `ROCK_COUNT` jusqu'à ce que plusieurs explosent en
même temps, et continuez de l'augmenter jusqu'à voir passer un `-1`.
[#_break_it_on_purpose_9]
=== Cassez-le exprès
Vous avez maintenant deux échelles de volume en tête, et tôt ou tard la mauvaise
sortira de vos doigts. Ajoutez cette ligne toute seule, juste sous les quatre
lignes `soundLoad` et `musicLoad` :
[source,lua]
----
soundSetVolume(100)
----
Le jeu refuse de démarrer :
----
20:soundSetVolume: Invalid sound volume value: 100
----
Le numéro de ligne que vous obtenez est celui de la ligne où vous l'avez
vraiment tapée, donc le vôtre n'est peut-être pas 20. Ensuite vient la fonction
qui a protesté, puis la valeur contre laquelle elle a protesté. Le message ne
vous dit pas quelle est la plage permise -- c'est à ça que sert l'entrée de la
Référence pour `soundSetVolume` -- mais il vous dit bien le nombre qu'elle a
refusé, et une fois que vous savez que les effets s'arrêtent à 63, la
correction tient en un nombre.
`100` est un nombre suspect à trouver dans un volume, et ça vaut la peine de
savoir pourquoi vous l'avez tapé. Presque à coup sûr, vous avez pensé au volume
comme à un pourcentage, parce que tous les curseurs de volume dont vous vous
êtes jamais servi vont jusqu'à 100. Celui de Singe, non. Les effets vont de `0`
à `63` et la musique de `0` à `128`, et ni l'un ni l'autre n'est un pourcentage
de quoi que ce soit.
Voilà la forme générale d'une erreur du moteur, par opposition aux erreurs Lua
de la leçon un : un numéro de ligne, le nom de la fonction du moteur que vous
avez appelée, et une plainte au sujet des valeurs que vous lui avez passées.
Quand vous en voyez une, l'entrée de la Référence pour cette fonction est la
prochaine chose à lire.
[#_what_you_learned_10]
=== Ce que vous avez appris
* `soundLoad` lit un son et renvoie un handle. Chargez une fois, en haut du
fichier.
* `soundPlay` lance un son et renvoie un *canal*, qui est une autre sorte de
nombre que le handle.
* Singe mélange seize canaux. Quand ils sont tous occupés, `soundPlay` renvoie
`-1` et rien ne joue.
* Les numéros de canal sont réutilisés. Oubliez-en un dès que vous l'avez
arrêté.
* Laissez les effets courts finir tranquillement ; `soundStop` est pour les sons
qui n'ont plus lieu d'être.
* `soundUnload` ne coupe pas un son qui est encore en train de jouer.
* Jouez un son là où l'événement se produit, pas là où une condition est vraie,
sinon il joue soixante fois par seconde.
* `onSoundCompleted(channel)` est un callback que le moteur appelle chaque fois
qu'un canal se tait, quelle qu'en soit la raison.
* La musique est un système à part, avec ses propres handles, son propre volume,
et pas de limite de canaux.
* Le volume des effets va de `0` à `63`. Celui de la musique va de `0` à `128`.
Ce ne sont pas des pourcentages.
[#_next_time_10]
=== La prochaine fois
Vos rochers sont touchés quand leurs rectangles se chevauchent, et c'est
pourquoi un tir qui passe à travers un coin de ciel compte quand même. À la
leçon onze, vous aurez les tests de collision du moteur lui-même, vous
apprendrez ce qu'est une boîte de collision, et vous découvrirez pourquoi le
rectangle autour d'un rocher rond n'a pas la bonne forme.