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.

同步游戏音频及音乐

前言

在任何应用程序或游戏中, 声音和音乐播放都会有轻微的延迟. 对于游戏, 这种延迟往往小到可以忽略不计. 在调用任意play()函数后, 声音效果将在几毫秒后出现. 对于音乐来说这并不重要, 因为在大多数游戏中它不会产生互动.

不过, 对于一些游戏(主要是节奏游戏), 可能会需要让玩家的操作与歌曲中发生的事情同步(通常与BPM同步). 因此, 得到一个具体播放位置的更精确的定时信息就很有用了.

极其精准地定位播放时间是非常困难的. 因为在音频回放过程中有很多因素在起作用:

  • 音频以块(不连续)的形式混合在一起, 具体取决于所使用的音频缓冲区的大小(在项目设置中检查延迟).

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

  • 图形应用程序接口延迟显示两到三帧.

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

最常见的减少延迟的方法是缩小音频缓冲区(同样是通过编辑项目设置中的延迟设置). 问题是, 当延迟很小时, 声音混合将占用大量的CPU. 这就增加了跳音的风险(由于混合回调丢失, 导致声音出现裂缝).

这是一种常见的折衷方案, 因此Godot附带了合理的默认值. 一般这些默认值不需要更改.

归根结底, 问题并不在于这一点点的延迟, 而是同步游戏的画面和声音. 从Godot 3.2开始, 加入了一些辅助工具, 帮助获取更精确的播放时间.

使用系统时钟同步

如前所述, 如果你调用 AudioStreamPlayer.play() , 声音不会立即开始播放, 而是在音频线程处理下一个块时开始.

这个延迟是无法避免的, 但是可以通过调用 AudioServer.get_time_to_next_mix() 来估算.

输出延迟(混音后的情况)可以通过调用 AudioServer.get_output_latency() 来估算.

把这两样加起来, 就可以几乎准确地猜到 _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)

然而, 从长远来看, 由于声音硬件时钟从未与系统时钟完全同步, 计时信息会逐渐偏移.

在节奏游戏中, 一首歌持续时长仅有几分钟, 这种方法就很适合(也是推荐的方法). 而对于一款播放时间更长的游戏来说, 游戏最终将失去同步, 因此需要一种不同的方法.

使用声音硬件时钟同步

虽然可以使用 AudioStreamPlayer.get_playback_position() 来获取歌曲的当前位置, 但实际并没有那么实用. 这个值(每逢音频回调混合一块声音时)将以块为单位递增, 导致多次调用可能返回相同的值. 除此之外, 由于前面提到的原因, 该值也将与扬声器失去同步.

为了补偿 "chunked"(分块)输出, 有个函数能有所帮助: 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)