singe/docs/fr/lessons/27-midi.adoc

526 lines
23 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_27_music_and_midi]
== Leçon 27 : Musique et MIDI
image::learn/27-midi.png[La leçon terminée, 480]
Cette leçon, ce sont deux leçons qui se trouvent vivre ensemble.
La première moitié, c'est la musique : comment un jeu joue correctement un
morceau de musique, pourquoi ce n'est pas la même chose que jouer un effet
sonore, et que faire quand la musique doit rester au pas de la vidéo.
La seconde moitié, c'est le MIDI, que la plupart des gens n'ont jamais croisé
et qui donne à un jeu quelque chose que rien d'autre ne donne. À la fin, un
clavier musical branché sur votre ordinateur jouera à votre jeu.
[#_a_sound_effect_and_a_piece_of_music_are_different_things]
=== Un effet sonore et un morceau de musique sont deux choses différentes
Vous savez déjà jouer un son. La leçon dix a chargé un `.wav` avec `soundLoad`
et l'a déclenché avec `soundPlay`, et si vous donnez à ces deux appels une
chanson de quatre minutes, ils la joueront.
Ne le faites pas. Singe sépare les effets sonores et la musique, et cette
séparation vaut la peine pour quatre raisons.
* *Ils ont chacun leur volume.* `soundSetVolume` ne touche jamais à la musique
et `musicSetVolume` ne touche jamais aux coups de feu. Tous les jeux jamais
sortis en ont eu besoin, parce que les joueurs baissent la musique et montent
les effets.
* *La musique n'a pas de limite de canaux.* Les effets sonores se partagent
seize canaux, et quand les seize sont occupés, le dix-septième ne joue pas.
Un morceau de musique n'est pas sur un canal, alors rien ne peut l'évincer.
* *Un handle, c'est un morceau*, et il se met en pause, reprend, s'arrête et
s'estompe tout seul sans rien déranger d'autre.
* *La musique se met en pause avec le jeu.* Appuyez sur la touche de pause et
elle reste là où elle en est. Revenez et elle repart. Vous n'écrivez rien
pour cela.
Une chose qui n'est *pas* différente mérite d'être connue, parce qu'on suppose
souvent le contraire : Singe décode un fichier de musique en mémoire dès le
départ, exactement comme un effet sonore. Un long morceau coûte de la mémoire.
Il n'est pas lu sur le disque petit à petit. Cinq minutes de musique, ce sont
quelques dizaines de mégaoctets de mémoire tant qu'elles sont chargées, alors
chargez le morceau du niveau où vous êtes et déchargez-le avec `musicUnload`
quand vous en sortez.
[#_playing_a_track]
=== Jouer un morceau
Chaque lecteur de ce livre a déjà un morceau de musique, parce que le moteur en
a extrait un dans votre dossier de travail la première fois que vous avez
lancé quoi que ce soit. Commencez un fichier avec ces trois lignes :
[source,lua]
----
local theme = musicLoad("Singe/menuIntro.flac")
musicSetVolume(64)
musicPlay(theme, -1)
function onOverlayUpdate()
overlayClear()
overlayPrint(2, 2, "Music: " .. (musicIsPlaying(theme) and "playing" or "stopped"))
return OVERLAY_UPDATED
end
----
Lancez-le avec `Singe -R music`. Dix secondes de musique, puis elle
recommence, et elle n'arrête pas de recommencer.
C'est ce que fait le `-1`. `musicPlay` prend un nombre de *boucles* : omettez-le
ou passez `0` et le morceau joue une fois, passez `3` et il joue quatre fois en
tout, et passez `-1` et il se répète jusqu'à ce que quelque chose l'arrête. Un
menu, un écran titre ou le fond sonore d'un niveau, c'est presque toujours
`-1`.
Deux détails de ce petit script méritent qu'on s'y arrête. `musicLoad` prend un
chemin, et pas un chemin en `DIR ..`, parce que `Singe/menuIntro.flac` est l'un
des fichiers du moteur lui-même et se trouve dans le dossier que le moteur a
extrait dans votre dossier de travail. `musicIsPlaying` répond `true` ou
`false`, et le `and ... or ...` au milieu de cet `overlayPrint` est le raccourci
Lua de la leçon trois pour choisir entre deux valeurs.
[#_the_volume_scale_is_not_the_one_you_know]
=== L'échelle de volume n'est pas celle que vous connaissez
`musicSetVolume(64)` le règle à peu près à la moitié. Les effets sonores vont
de `0` à `63` ; la musique va de `0` à `128`, soit deux fois plus loin.
Ce n'est pas une erreur, et ce n'est pas une blague. Singe sait faire tourner
des jeux écrits pour Hypseus, un moteur dont le volume de la musique a toujours
été sur une échelle de `0` à `128`, et changer les nombres rendrait, sans
prévenir, la musique de ces jeux deux fois plus forte que ne le voulaient leurs
auteurs. Alors Singe garde les deux échelles, chacune attachée à la famille
d'appels à laquelle elle appartient. Effets : de `0` à `63`. Musique : de `0` à
`128`. Donnez à `musicSetVolume` un nombre hors de sa plage et le script
s'arrête, ce que vous verrez par vous-même à la fin de cette leçon.
[#_what_singe_will_play]
=== Ce que Singe sait jouer
Presque tout. WAV, Ogg Vorbis, Opus, MP3, FLAC, WavPack, AAC, et une longue
liste au-delà, tous par le même `musicLoad`.
Deux familles sont plus intéressantes que les formats ordinaires, et toutes
deux existent parce qu'elles sont minuscules.
Les *modules de tracker* -- `.mod`, `.s3m`, `.xm`, `.it`, et une cinquantaine
de cousins -- contiennent une poignée de courts instruments enregistrés et la
liste des notes auxquelles les jouer. Un air de quatre minutes peut tenir en
moins de cent kilo-octets.
Les *chiptunes* -- `.nsf`, `.spc`, `.vgm`, `.gbs`, et leur famille -- vont plus
loin : elles contiennent les *écritures de registres* destinées à la vraie puce
sonore d'une Nintendo, d'une Mega Drive ou d'un Spectrum, et Singe émule la
puce pour les jouer. Si vous voulez qu'un jeu sonne comme 1989 plutôt que comme
un enregistrement de 1989, c'est comme ça.
Les uns comme les autres se chargent avec `musicLoad`, comme tout le reste. La
section *Formats vidéo, audio et de conteneur* de la Référence donne la liste
complète, y compris quel conteneur va avec quel codec.
[#_stopping_pausing_and_fading]
=== Arrêter, mettre en pause, estomper
[source,lua]
----
musicPause(theme)
musicResume(theme)
musicStop(theme)
musicStop(theme, 2000)
----
`musicPause` immobilise le morceau là où il en est et `musicResume` repart de
cet endroit. `musicStop` le termine, et le morceau revient à son début la
prochaine fois que vous le jouez. Avec un second nombre, `musicStop` fait
disparaître le morceau en fondu pendant ce nombre de millisecondes au lieu de
le couper net -- deux secondes ici. Ce fondu est la chose la plus facile que
vous puissiez faire pour qu'un jeu ait l'air fini. Une musique qui s'arrête au
milieu d'une mesure quand le joueur franchit une porte ressemble à un bug, même
pour quelqu'un qui ne saurait pas dire pourquoi.
Appelez l'un des quatre sans aucun handle et il agit sur toute la musique à la
fois, ce qui est exactement ce qu'il faut à un écran de pause.
Maintenant, la limite, en toute honnêteté, parce que vous allez la chercher et
qu'elle n'est pas là : *Singe ne sait pas faire de fondu enchaîné entre deux
morceaux.* `musicSetVolume` est un seul gain pour toute la musique, pas un
volume par morceau, alors il n'y a aucun moyen de faire monter un morceau
pendant qu'un autre descend. Ce que vous pouvez faire, c'est estomper l'ancien
et démarrer le nouveau pendant qu'il s'éteint, ce qui suffit pour un changement
de niveau et ne donne pas du tout le même résultat, à l'oreille, pour un thème
de combat qui se glisse sous un thème d'exploration. S'il vous faut aujourd'hui
un vrai fondu enchaîné, le moyen de l'obtenir est de faire des deux morceaux un
seul fichier et de sauter à l'intérieur, ou d'accepter le chevauchement.
[#_music_video_and_staying_in_step]
=== La musique, la vidéo et la synchronisation
Si votre jeu lit de la vidéo -- le sujet de la troisième partie -- il y a une
chose à savoir sur le son, et ce n'est pas du tout un appel de musique.
Singe affiche une image de la vidéo quand on entend le son qui va avec, pas
d'après un minuteur. Le son est l'horloge, et l'image le suit. C'est le bon
sens, parce qu'une oreille remarque un décalage d'un cinquième de seconde dans
une voix alors qu'un œil ne remarque pas le même décalage sur une bouche.
Le moteur mesure son propre chemin audio au démarrage. Ce qu'il ne peut pas
mesurer, c'est tout ce qui vient après que le système d'exploitation a laissé
partir le son : un amplificateur, un téléviseur, un récepteur HDMI, un casque
Bluetooth. Chacun ajoute son propre retard, il est différent sur chaque
machine, et il se manifeste par un son qui arrive un peu après la chose qui l'a
produit.
Le remède tient en un nombre :
[source,lua]
----
singeSetAudioDelay(45)
----
Quarante-cinq millisecondes de « le son est en retard ; retiens l'image
d'autant ». Vous pouvez aussi le passer en ligne de commande avec
`--audiodelay=45`, ou le mettre dans l'entrée du jeu dans `games.dat`.
`singeGetAudioDelay` relit la valeur en vigueur, ce qu'un menu de service
affiche à côté de son curseur.
Vous n'avez pas à deviner ce nombre. Le menu de Singe lui-même a un écran de
calibrage : il joue un clic une fois par seconde et fait clignoter l'écran en
blanc, vous réglez jusqu'à ce que l'éclair et le clic arrivent ensemble, et il
enregistre le résultat pour tous les jeux de cette machine. La section
*Synchronisation audio* de la Référence le décrit et explique pourquoi ça
marche. Si vous construisez une borne, lancez-le une fois après avoir câblé les
haut-parleurs, et n'y pensez plus jamais.
[#_the_other_thing_called_midi]
=== L'autre chose qui s'appelle MIDI
Le MIDI, ce sont deux choses sans rapport sous un seul nom, et les distinguer,
c'est déjà presque tout comprendre.
Voici la première, en un paragraphe. Un enregistrement -- un `.wav`, un
`.mp3` -- est une mesure d'un son, prise quarante-quatre mille fois par
seconde. Le MIDI, ce n'est pas ça. Un fichier MIDI contient *ce qui a été
joué* : à ce moment, sur ce canal, enfoncez la touche soixante avec cette
force ; un peu plus tard, relâchez-la. C'est plus proche d'une partition, ou du
rouleau perforé d'un piano mécanique, que d'un enregistrement. C'est pour cela
qu'un fichier MIDI fait quelques kilo-octets quand la même musique en audio
fait quelques mégaoctets, et c'est aussi pour cela qu'un fichier MIDI ne
contient aucun son. Il faut que quelque chose joue les notes.
Un fichier MIDI est de la musique, alors il se joue comme de la musique :
[source,lua]
----
local tune = musicLoad("music/overworld.mid")
musicPlay(tune, -1)
----
Mais ce qui joue les notes doit bien venir de quelque part, et cette chose,
c'est une *SoundFont* : un fichier plein d'instruments enregistrés -- un piano,
une trompette, une caisse claire -- avec les notes et les points de boucle
marqués, pour que le moteur puisse en tirer n'importe quelle note de n'importe
quel instrument.
Singe n'en fournit pas. Une bonne SoundFont pèse des dizaines de mégaoctets, et
ce seraient des dizaines de mégaoctets transportés par chaque copie du moteur
pour un format que la plupart des jeux n'utilisent jamais. Alors vous en
désignez une avec `--soundfont`, ou vous en mettez une en
`Singe/soundfont.sf2` pour qu'un jeu empaqueté porte la sienne, ou vous laissez
Singe trouver celle que votre distribution Linux a installée. Sans SoundFont
nulle part, le chargement d'un `.mid` échoue et le dit.
Voilà pourquoi le même fichier MIDI sonne différemment sur deux machines, et
pourquoi il sonnait si mal sur les ordinateurs des années quatre-vingt-dix. Le
fichier est une liste de notes. Ce que vous entendez, c'est la SoundFont qui
s'est trouvée les jouer : une bonne, c'est une salle pleine d'instruments, une
mauvaise, une salle pleine de jouets, et les notes sont identiques dans les
deux. Si un fichier MIDI va dans votre jeu, livrez la SoundFont avec. Sinon,
vous n'avez pas choisi le son de votre musique ; vous l'avez laissé à ce qui
est installé sur la machine du joueur.
[#_midi_as_a_wire]
=== Le MIDI comme un câble
Maintenant, la deuxième chose, qui est la plus intéressante.
Le MIDI, c'est aussi un câble, et des messages qui circulent dessus. Branchez
un clavier musical sur votre ordinateur et chaque touche que vous enfoncez
envoie un message : *note on, canal 1, touche 60, vélocité 90*. Tournez un
bouton sur une surface de contrôle et elle envoie *le contrôleur 7 du canal 1
vaut maintenant 40*. Ces messages existent, que quelque chose joue de la
musique ou non. C'est un périphérique d'entrée de plus, comme un joystick à
quatre-vingt-huit boutons qui savent avec quelle force vous les frappez.
Et ça marche dans l'autre sens. Votre jeu peut envoyer ces messages, vers un
synthétiseur, un module de sons, un contrôleur d'éclairage de scène, ou
n'importe quoi d'autre sur le câble.
Les ports sont numérotés à partir de zéro, et Singe n'ouvre rien tant que vous
ne le demandez pas -- un jeu qui ne parle jamais de MIDI ne paie jamais pour le
MIDI. Un port d'entrée et un port de sortie peuvent être ouverts à la fois.
Faites l'inventaire de la machine comme ceci :
[source,lua]
----
local inPort = "none"
local outPort = "none"
if midiInputCount() > 0 and midiOpenInput(0) then
inPort = midiInputName(0)
end
if midiOutputCount() > 0 and midiOpenOutput(0) then
outPort = midiOutputName(0)
end
----
Sur une machine sans aucun MIDI, les nombres valent zéro, les appels
d'ouverture répondent `false`, et rien n'échoue. C'est important : cela veut
dire que vous pouvez écrire ce code dans un vrai jeu et le publier, et les
quatre-vingt-dix-neuf joueurs sur cent qui n'ont pas de clavier MIDI ne sauront
jamais qu'il est là. Les noms servent à un écran de réglages qui laisse le
joueur choisir le port, parce que le port zéro est rarement celui qu'il veut.
`midiRescan` cherche à nouveau ce qui a été branché pendant que le jeu tourne.
[#_listening]
=== Écouter
Chaque message qui arrive va à un callback, de la même façon que chaque appui
sur une touche va à `onInputPressed` :
[source,lua]
----
local MIDI_NOTE_ON = 0x90
function onMidiMessage(status, data1, data2, bytes)
if (status & 0xF0) ~= MIDI_NOTE_ON or data2 == 0 then
return
end
heard = noteName(data1)
end
----
La plupart des messages MIDI font trois octets, et les trois nombres qu'on vous
donne sont ces octets, exactement tels que l'appareil les a envoyés. Un message
plus court vous donne un `0` à la place de chaque octet qu'il n'a pas.
`status` dit de quelle sorte de message il s'agit et de quel canal il vient,
les deux mélangés : la sorte est dans les quatre bits du haut et le canal dans
les quatre du bas. `0x90`, c'est note on, et une note sur le canal 1 arrive
comme `0x90` alors que la même note sur le canal 5 arrive comme `0x94`.
`status & 0xF0` garde la moitié du haut et jette le canal, ce que vous voulez,
sauf si le canal vous importe. (`&` est le « et » bit à bit de Lua, et `0x90`
est un nombre écrit en hexadécimal -- en base seize -- parce que c'est ainsi
que tous les documents MIDI du monde les écrivent.)
`data1` et `data2` sont les deux autres octets, et leur sens dépend de la
sorte. Pour un note on, ce sont la touche et la vélocité : la touche `60` est
le do central, et la vélocité, c'est la force avec laquelle elle a été frappée,
de `1` à `127`.
Le test de `data2 == 0` n'est pas de la maniaquerie. Un note on avec une
vélocité de zéro veut dire *note off* : c'est ainsi que presque tous les
claviers jamais fabriqués disent « j'ai lâché », et si vous ne le filtrez pas,
votre jeu réagira deux fois pour chaque touche.
`bytes` est le message entier sous forme de chaîne ; c'est ainsi que vous lisez
un message de plus de trois octets.
[#_playing]
=== Jouer
Envoyer est plus facile que recevoir.
[source,lua]
----
midiNoteOn(1, 60, 100)
midiNoteOff(1, 60)
----
Le do central sur le canal 1, frappé à cent, puis relâché. Ici, les canaux vont
de `1` à `16`, comme les compte une personne, et non de `0` à `15` comme le
câble ; c'est Singe qui est gentil avec vous.
Chaque `midiNoteOn` a besoin d'un `midiNoteOff`. Une note jamais relâchée sonne
jusqu'à ce que quelque chose l'arrête, et le premier bug MIDI classique, c'est
un jeu qui plante ou qui quitte avec trois notes enfoncées et laisse un accord
suspendu dans la pièce. Alors associez-les par paires, et faites-le avec un
minuteur plutôt qu'à la main :
[source,lua]
----
function playTarget()
if not midiIsOutputOpen() then
return
end
local key = target
midiNoteOn(1, key, 100)
timerAfter(HOLD_MS, function()
midiNoteOff(1, key)
end)
end
----
`local key = target` fait un vrai travail. `timerAfter` exécute sa fonction
quatre dixièmes de seconde plus tard, et d'ici là `target` a pu changer pour
une autre note ; la copier dans une variable locale garantit que le minuteur
relâche la note qui a vraiment été enfoncée. Copiez tout ce dont un minuteur
aura besoin plus tard.
Les autres appels d'envoi font le reste de ce qu'est le MIDI :
`midiProgramChange` choisit l'instrument que joue un canal, `midiControlChange`
bouge un contrôleur (le volume, c'est 7, le panoramique 10, la pédale de
sustain 64), `midiPitchBend` infléchit la hauteur d'un canal, et `midiSend`
envoie n'importe quel message sous forme d'octets bruts, pour ce que les appels
nommés ne couvrent pas.
[#_a_reason_to_care]
=== Une raison de s'y intéresser
Tout cela, c'est de la mécanique. Voici à quoi ça sert.
Réunissez les deux moitiés et le jeu demande une note, le joueur la joue sur
un vrai instrument, et le jeu sait s'il a juste :
[source,lua]
----
function onMidiMessage(status, data1, data2, bytes)
if (status & 0xF0) ~= MIDI_NOTE_ON or data2 == 0 then
return
end
heard = noteName(data1)
if data1 == target then
score = score + 1
pickTarget()
playTarget()
end
end
----
Onze lignes, et vous avez le squelette d'un jeu pour apprendre la musique.
Ajoutez une horloge et vous avez un jeu de rythme qui note un vrai jeu
d'instrument plutôt que quatre boutons en plastique. Ajoutez `data2` au test
et il tient compte de la force avec laquelle vous frappez la note. Comparez le
moment où la note est arrivée au moment où elle était attendue et vous notez la
précision rythmique à la milliseconde près.
Et comme le MIDI sort autant qu'il entre, une borne peut faire des choses que
rien d'autre ne peut. Un vrai instrument câblé à la machine. Un module de sons
externe qui joue la partition, pour que l'air ait une profondeur qu'aucune
banque d'échantillons ne lui donne. Des lumières de scène sur la borne qui
clignotent en rythme parce que le jeu leur a envoyé le rythme. Si vous êtes du
genre à construire des bornes d'arcade, c'est l'appel qui permet à la borne
d'être une vraie chose plutôt qu'une boîte autour d'un ordinateur.
Le script terminé est `learn/27-midi.singe` : les commandes de la musique, la
liste des ports et le jeu de devinette, en une centaine de lignes. La barre
d'espace envoie la note, Alt gauche met la musique en pause, Maj gauche la fait
s'estomper, et les flèches haut et bas règlent le volume. Lancez-le sans
matériel MIDI : la moitié musique marche et la moitié MIDI dit deux fois
`none`, et c'est ainsi qu'il doit se comporter sur la machine d'un joueur.
[#_cleaning_up]
=== Faire le ménage
[source,lua]
----
function onShutdown()
midiCloseInput()
midiCloseOutput()
musicUnload(theme)
end
----
`onShutdown` est appelé une fois quand le jeu se termine, et c'est là qu'un
script rend ce qu'il a emprunté. Fermer un port qui n'est pas ouvert ne fait
rien, alors il n'y a rien à vérifier avant.
[#_try_it_27]
=== Essayez
. *Faites-la boucler quatre fois au lieu d'indéfiniment.* Changez
`musicPlay(theme, -1)` en `musicPlay(theme, 3)` et comptez. Le nombre dit
combien de fois elle se *répète*, pas combien de fois elle joue.
. *Estompez plus longtemps.* Changez le fondu du bouton qui arrête la musique
pour `musicStop(theme, 4000)` et écoutez la différence que font quatre secondes.
Puis essayez `musicStop(theme, 100)`.
. *Affichez le canal.* Dans `onMidiMessage`, calculez le canal avec
`(status & 0x0F) + 1` et affichez-le. Jouez sur votre clavier et voyez sur
quel canal il est -- puis trouvez ses réglages et changez ce canal, pour vous
prouver que le nombre est réel.
. *Notez la vélocité.* Donnez un point pour la bonne note, et trois points si
`data2` dépasse cent, pour que le jeu récompense ceux qui jouent avec
conviction.
. *Jouez la réponse.* Quand le joueur trouve la bonne note, faites jouer au jeu
un petit accord en retour, avec trois appels à `midiNoteOn` espacés de
quelques notes et trois minuteurs pour les relâcher. Il vous faudra pour cela
un vrai port de sortie ou un synthétiseur logiciel, et sous Linux, c'est
n'importe quel programme qui s'enregistre auprès d'ALSA.
[#_break_it_on_purpose_26]
=== Cassez-le exprès
Les deux échelles de volume sont faciles à confondre, et Singe préfère
s'arrêter plutôt que de jouer votre musique à un niveau que vous ne vouliez
pas. Ajoutez une ligne sous `musicPlay(theme, -1)`, avec le nombre qui serait
le plein volume d'un effet sonore si vous le doubliez de tête en vous
trompant :
[source,lua]
----
musicSetVolume(200)
----
Singe s'arrête avant que le jeu démarre, en donnant la ligne de votre
fichier où se trouve cet appel :
----
107:musicSetVolume: Invalid music volume value: 200
----
La ligne, l'appel qui a protesté, et la plainte, qui vous renvoie ici le nombre
pour qu'il n'y ait pas de discussion sur ce qui est arrivé. La musique, c'est
de `0` à `128`. Les effets sonores, de `0` à `63`. Quand un appel de volume
arrête votre jeu, la question à se poser, c'est à laquelle des deux familles
vous parlez.
[#_what_you_learned_27]
=== Ce que vous avez appris
* La musique et les effets sonores sont séparés dans Singe : chacun son volume,
pas de limite de canaux pour la musique, et une pause qui suit celle du jeu.
* Un fichier de musique est décodé en mémoire dès le départ, si bien qu'un long
morceau coûte de la mémoire et doit être déchargé quand vous quittez le
niveau.
* `musicPlay(track, -1)` boucle indéfiniment ; un nombre positif dit combien de
fois il se répète.
* Le volume de la musique va de `0` à `128` et celui des effets de `0` à `63`,
et cette différence vient de la compatibilité avec Hypseus ; ce n'est pas une
erreur.
* `musicStop(track, milliseconds)` fait un fondu de sortie, et un fondu est la
finition la moins chère qui soit. Il n'y a pas de fondu enchaîné, parce qu'un
seul gain couvre toute la musique.
* Singe présente la vidéo d'après l'horloge audio, et `singeSetAudioDelay`
corrige le retard de ce par quoi sort le son.
* Un fichier MIDI contient des notes, pas du son, et il lui faut une SoundFont
pour qu'on l'entende. Singe n'en fournit aucune, alors livrez la vôtre, sinon
votre musique sera ce que la machine du joueur se trouve avoir.
* Les ports MIDI sont tout autre chose : des messages en direct, qui entrent et
sortent, sur un câble, sans rapport avec les fichiers MIDI.
* `onMidiMessage` vous donne les octets bruts. Les quatre bits du haut de
`status` sont la sorte, les quatre du bas sont le canal, et un note on de
vélocité zéro veut dire note off.
* Chaque `midiNoteOn` a besoin d'un `midiNoteOff`, et c'est un minuteur qui les
associe.
* Sur une machine sans MIDI, chaque appel répond `false` ou zéro et rien
n'échoue, alors la prise en charge du MIDI peut être publiée sans risque.
[#_next_time_27]
=== La prochaine fois
Votre jeu a de la musique, et un score qu'un clavier MIDI peut faire grimper. La
leçon vingt-huit emmène ce score hors de la machine : les comptes, des tableaux
de meilleurs scores que d'autres peuvent voir, et le catalogue qui présente
votre jeu au monde entier.