Work in progress

The content of this page was not yet updated for Godot 4.2 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

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 = Time.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 = (Time.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)