[#_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.