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