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)