将游戏与音频和音乐同步

介绍

在任何应用程序或游戏中,声音和音乐播放都会有轻微的延迟。对于游戏来说,这种延迟通常很小,可以忽略不计。在调用任何play()函数几毫秒后,声音效果就会出现。对于音乐来说,这并不重要,因为在大多数游戏中,它不会与游戏进行交互。

不过,对于某些游戏(主要是节奏游戏),可能需要将玩家的动作与歌曲中发生的事情同步(通常与bpm同步)。为此,有更精确的时间信息来精确播放位置是有用的。

很难达到非常低的播放定时精度。这是因为在音频播放期间有许多因素在播放:

  • 根据所用音频缓冲区的大小(检查项目设置中的延迟),音频以块(而不是连续)的形式混合。

  • 混合的音频块不会立即播放。

  • 图形API延迟显示两到三帧。

  • 在电视上播放时,可能会由于图像处理而增加一些延迟。

减少延迟的最常见方法是缩小音频缓冲区(同样,在项目设置中编辑延迟设置)。问题是,当延迟太小时,混音需要相当多的CPU。这会增加跳过的风险(声音中的裂缝,因为混音回调丢失)。

这是一个常见的权衡,所以Godot提供了合理的违约,不需要修改。

最后,问题不在于这种轻微的延迟,而在于需要它的游戏的图形和音频的同步。从Godot3.2开始,添加了一些助手以获得更精确的播放时间。

使用系统时钟进行同步

如前所述,如果您致电 AudioStreamPlayer.play() ,声音不会立即开始,但当音频线程处理下一个块时。

不能避免这种延迟,但可以通过调用 AudioServer.get_time_to_next_mix() .

输出延迟(混合后发生的情况)也可以通过调用 AudioServer.get_output_latency() .

加上这两个,几乎可以准确地猜测扬声器中的声音或音乐何时开始播放:

var actual_play_time = AudioServer.get_time_to_next_mix() + AudioServer.get_output_latency()
$Song.play()

这样,在 _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 being yet).
    time = max(0, time)
    print("Time is: ", time)

不过,从长远来看,由于声音硬件时钟从来没有与系统时钟完全同步,因此时间信息将缓慢地漂移。

对于一个节奏游戏,一首歌在几分钟后开始和结束,这种方法是好的(这是推荐的方法)。对于播放时间可能更长的游戏,游戏最终会失去同步,需要另一种方法。

使用声音硬件时钟进行同步

使用 AudioStreamPlayer.get_playback_position() 要获得歌曲的当前位置听起来很理想,但并不像现在这样有用。这个值将以块的形式递增(每次音频回调混合一个声音块时),因此许多调用可以返回相同的值。此外,由于前面提到的原因,该值也将与扬声器不同步。

为了补偿“分块”输出,有一个函数可以帮助: AudioServer.get_time_since_last_mix() .

将此函数的返回值添加到 get_playback_position() 提高精度:

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

要提高精度,请减去延迟信息(音频混合后需要多少时间才能听到):

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

由于多个线程的工作方式,结果可能有点不稳定。只需检查该值是否不小于前一帧中的值(如果不小于,则丢弃它)。这也是一种比以前更不精确的方法,但它适用于任何长度的歌曲,或将任何内容(如音效)与音乐同步。

以下代码与使用此方法之前的代码相同:

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)