Synchronisez le jeu avec les sons et la musique

Introduction

Dans tout application ou jeu, les fichiers son et musique sont joués avec un léger délai. Pour les jeux, ce délai est souvent si faible qu'il est négligeable. Les effets sonores sont joués quelques millisecondes après l'appel de la fonction play(). Pour la musique, cela n'a pas d'importance puisque dans la plupart des jeux elle n'interagit pas avec le gameplay.

Néanmoins, pour certains jeux (notamment, les jeux de rythme), il peut être nécessaire de synchroniser les actions du joueurs avec quelque chose qui arrive dans une chanson (habituellement synchronisé avec le BPM - battements par minutes-). Pour cet effet, avoir des informations plus précises pour rejouer à des moments-clés est utile.

Il est difficile d’atteindre une précision de synchronisation de lecture très faible. En effet, de nombreux facteurs entrent en jeu lors de la lecture audio :

  • Un son est mixé par parties (et non de manière continue), en fonction de la taille de la mémoire tampon dédiée (vérifier la latence dans les réglages du projet).
  • Les parties d'audio mixées ne sont pas jouées immédiatement.
  • Les API graphiques gèrent l'affichage avec deux ou trois images de retard.
  • Lorsque l'on joue sur une télévision, un retard supplémentaire peut être causé par le traitement de l'image.

La méthode la plus courante pour réduire la latence est de réduire la taille de la mémoire tampon audio (en changeant la latence dans les paramètres du projet, comme vu précédemment). Le problème est qu'une latence trop faible entraîne beaucoup plus d'appels au CPU pour le mixage. Cela augmente le risque de sauts ( défauts sonores causés par un appel raté à la fonction de mixage).

Il s'agit là d'un compromis courant, de sorte que Godot est livré avec des valeurs par défaut raisonnables qui ne devraient pas avoir besoin d'être modifiés.

Le problème, en fin de compte, n'est pas ce léger retard, mais la synchronisation graphique et audio pour les jeux qui l'exigent. A partir de Godot 3.2, des aides ont été ajoutées pour obtenir un timing de lecture plus précis.

Utilisation de l'horloge système pour la synchronisation

Comme mentionné précédemment, si vous appelez AudioStreamPlayer.play(), le son ne commencera pas immédiatement, mais lorsque le thread audio traitera le bloc suivant.

Ce délai ne peut être évité mais il peut être estimé en appelant AudioServer.get_time_to_next_mix().

La latence de sortie (ce qui se passe après le mixage) peut aussi être estimée en appelant AudioServer.get_output_latency().

Ajoutez ces deux là et il est possible de deviner presque exactement quand le son ou la musique commencera à jouer dans les haut-parleurs durant _process() :

var time_begin
var time_delay


func _ready()
    time_begin = OS.get_ticks_usec()
    time_delay = AudioServer.get_time_to_next_mix() + AudioServer.get_output_latency()
    $Player.play()


func _process(delta):
    # Obtain from ticks.
    var time = (OS.get_ticks_usec() - time_begin) / 1000000.0
    # Compensate for latency.
    time -= time_delay
    # May be below 0 (did not begin yet).
    time = max(0, time)
    print("Time is: ", time)

À long terme, cependant, comme l'horloge matérielle du son n'est jamais exactement synchronisée avec l'horloge du système, les informations de synchronisation vont lentement dériver.

Pour un jeu de rythme où une chanson commence et se termine après quelques minutes, cette approche est bonne (et c'est l'approche recommandée). Pour un jeu où la lecture peut durer beaucoup plus longtemps, le jeu finira par être désynchronisé et une approche différente est nécessaire.

Utilisation de l'horloge matérielle du son pour la synchronisation

Utiliser AudioStreamPlayer.get_playback_position() pour obtenir la position actuelle du morceau semble idéal, mais ce n'est pas si utile en l'état. Cette valeur s'incrémentera par morceaux (à chaque fois que le rappel audio a mélangé un bloc de son), donc plusieurs appels peuvent retourner la même valeur. De plus, la valeur ne sera pas synchronisée avec les haut-parleurs pour les raisons mentionnées précédemment.

Pour compenser la sortie "chunked", il existe une fonction qui peut aider : AudioServer.get_time_since_last_mix().

Ajouter la valeur de retour de cette fonction à get_playback_position() augmente la précision :

var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()

Pour augmenter la précision, soustrayez l'information de latence (combien il faut pour que l'audio soit entendu après avoir été mixé) :

var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix() - AudioServer.get_output_latency()

Le résultat peut être un peu instable en raison de la façon dont les threads multiples fonctionnent. Vérifiez simplement que la valeur n'est pas inférieure à celle de l'image précédente (jetez-la si c'est le cas). C'est aussi une approche moins précise que la précédente, mais elle fonctionnera pour des musiques de n'importe quelle longueur, ou en synchronisant n'importe quoi (effets sonores, par exemple) avec la musique.

Voici le même code qu'avant en utilisant cette approche :

func _ready()
    $Player.play()


func _process(delta):
    var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()
    # Compensate for output latency.
    time -= AudioServer.get_output_latency()
    print("Time is: ", time)