Sincronize a jogabilidade com áudio e música

Introdução

Em qualquer aplicativo ou jogo, a reprodução de som e música terá um pequeno atraso. Para jogos, esse atraso geralmente é tão pequeno que é insignificante. Os efeitos sonoros aparecerão alguns milissegundos depois que qualquer função play() for chamada. Para a música, isso não importa, pois na maioria dos jogos ela não interage com a jogabilidade.

Ainda assim, para alguns jogos (principalmente jogos de ritmo), pode ser necessário sincronizar as ações do jogador com algo que está acontecendo em uma música (geralmente em sincronia com o BPM). Para isso, é útil ter informações de tempo mais precisas para uma posição de reprodução exata.

Alcançar uma precisão de tempo de reprodução muito baixa é difícil. Isso ocorre porque muitos fatores estão em jogo durante a reprodução de áudio:

  • O áudio é mixado em pedaços (não continuamente), dependendo do tamanho dos buffers de áudio usados (verifique a latência nas configurações do projeto).

  • Pedaços mistos de áudio não são reproduzidos imediatamente.

  • As APIs gráficas exibem dois ou três quadros atrasados.

  • Ao jogar em TVs, algum atraso pode ser adicionado devido ao processamento de imagem.

A maneira mais comum de reduzir a latência é diminuir os buffers de áudio (novamente, editando a configuração de latência nas configurações do projeto). O problema é que quando a latência é muito pequena, a mixagem de som exigirá consideravelmente mais CPU. Isso aumenta o risco de pular (uma rachadura no som porque uma chamada de retorno -callback- de mixagem foi perdida).

Esta é uma compensação comum, então Godot vem com padrões sensatos que não precisam ser alterados.

O problema, no final das contas, não é esse pequeno atraso, mas sincronizar gráficos e áudio para jogos que exigem isso. A partir do Godot 3.2, alguns auxiliares foram adicionados para obter um tempo de reprodução mais preciso.

Usando o relógio do sistema para sincronizar

Como mencionado anteriormente, se você chamar AudioStreamPlayer.play(), o som não começará imediatamente, mas quando o thread de áudio processar o próximo trecho.

Este atraso não pode ser evitado, mas pode ser estimado chamando AudioServer.get_time_to_next_mix().

A latência de saída (o que acontece após a mixagem) também pode ser estimada chamando AudioServer.get_output_latency().

Adicione esses dois e é possível adivinhar quase exatamente quando o som ou a música começará a tocar nos alto-falantes durante _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)

A longo prazo, porém, como o relógio do hardware de som nunca está exatamente sincronizado com o relógio do sistema, as informações de temporização lentamente se afastarão.

Para um jogo de ritmo em que uma música começa e termina após alguns minutos, essa abordagem é adequada (e é a abordagem recomendada). Para um jogo em que a reprodução pode durar muito mais tempo, o jogo eventualmente ficará fora de sincronia e uma abordagem diferente será necessária.

Usando o relógio de hardware de som para sincronizar

Usar AudioStreamPlayer.get_playback_position() para obter a posição atual da música parece ideal, mas não é tão útil assim. Este valor será incrementado em blocos (toda vez que a chamada de retorno -callback- de áudio mixar um bloco de som), então muitas chamadas podem retornar o mesmo valor. Somado a isso, o valor também ficará dessincronizado com os alto-falantes pelos motivos citados anteriormente.

Para compensar a saída "fragmentada", existe uma função que pode ajudar: AudioServer.get_time_since_last_mix().

Adicionar o valor de retorno desta função a get_playback_position() aumenta a precisão:

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

Para aumentar a precisão, subtraia a informação de latência (quanto leva para o áudio ser ouvido depois de mixado):

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

O resultado pode ser um pouco instável devido ao funcionamento de vários threads. Apenas verifique se o valor não é menor do que no quadro anterior (descarte-o se for o caso). Essa também é uma abordagem menos precisa do que a anterior, mas funcionará para músicas de qualquer duração ou para sincronizar qualquer coisa (efeitos sonoros, por exemplo) com a música.

Aqui está o mesmo código de antes usando essa abordagem:

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)