同步游戏音频及音乐

简介

In any application or game, sound and music playback will have a slight delay. For games, this delay is often so small that it is negligible. Sound effects will come out a few milliseconds after any play() function is called. For music this does not matter as in most games it does not interact with the gameplay.

Still, for some games (mainly, rhythm games), it may be required to synchronize player actions with something happening in a song (usually in sync with the BPM). For this, having more precise timing information for an exact playback position is useful.

Achieving very low playback timing precision is difficult. This is because many factors are at play during audio playback:

  • 音频以块的形式混合(不是连续的),取决于使用的音频缓冲区的大小(检查项目设置中的延迟)。
  • Mixed chunks of audio are not played immediately.
  • 图形应用程序接口延迟显示两到三帧。
  • 当在电视上播放时,由于图像处理可能会增加一些延迟。

The most common way to reduce latency is to shrink the audio buffers (again, by editing the latency setting in the project settings). The problem is that when latency is too small, sound mixing will require considerably more CPU. This increases the risk of skipping (a crack in sound because a mix callback was lost).

This is a common tradeoff, so Godot ships with sensible defaults that should not need to be altered.

The problem, in the end, is not this slight delay but synchronizing graphics and audio for games that require it. Beginning with Godot 3.2, some helpers were added to obtain more precise playback timing.

使用系统时钟同步

As mentioned before, If you call AudioStreamPlayer.play(), sound will not begin immediately, but when the audio thread processes the next chunk.

这个延迟是无法避免的,但是可以通过调用来预估:参考:音频服务.获取_时间_至_下个_混音()<类_音频服务_方法_获取_时间_至_下个_混音>。

The output latency (what happens after the mix) can also be estimated by calling AudioServer.get_output_latency().

Add these two and it’s possible to guess almost exactly when sound or music will begin playing in the speakers during _process():

GDScript

  1. var time_begin
  2. var time_delay
  3. func _ready()
  4. time_begin = OS.get_ticks_usec()
  5. time_delay = AudioServer.get_time_to_next_mix() + AudioServer.get_output_latency()
  6. $Player.play()
  7. func _process(delta):
  8. # Obtain from ticks.
  9. var time = (OS.get_ticks_usec() - time_begin) / 1000000.0
  10. # Compensate for latency.
  11. time -= time_delay
  12. # May be below 0 (did not begin yet).
  13. time = max(0, time)
  14. print("Time is: ", time)

In the long run, though, as the sound hardware clock is never exactly in sync with the system clock, the timing information will slowly drift away.

在节奏游戏中,一首歌在几分钟后开始和结束,这种方法很好(也是推荐的方法)。而对于一款回放时间更长的游戏来说,游戏最终将失去同步,需要一种不同的方法。

使用声音硬件时钟同步

使用:参考:`音频流播放器.获取_回放_位置()<类_音频流播放器_方法_获取_回放_位置>`来获取歌曲的当前位置,但它并没有那么有用。这个值将以块的形式递增(每次音频回调混合一个声音块时),如此多的调用可以返回相同的值。除此之外,由于前面提到的原因,该值也将与扬声器不同步。

To compensate for the “chunked” output, there is a function that can help: AudioServer.get_time_since_last_mix().

将这个函数的返回值添加到*获取_回放_位置()*可以提高精度:

GDScript

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

To increase precision, subtract the latency information (how much it takes for the audio to be heard after it was mixed):

GDScript

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

由于多线程的工作方式,结果可能会有点轻微抖动。只需检查该值是否小于前一帧中的值(如果小于,则将其丢弃)。这个方法也不如之前的精确,但它适用于任何长度的歌曲,或者将任何东西(例如音效)与音乐同步。

下面是使用这种方法之前相同的代码:

GDScript

  1. func _ready()
  2. $Player.play()
  3. func _process(delta):
  4. var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()
  5. # Compensate for output latency.
  6. time -= AudioServer.get_output_latency()
  7. print("Time is: ", time)