ゲームプレイにオーディオと音楽を同期させる¶
はじめに¶
どのアプリケーションやゲームでも、サウンドと音楽の再生にはわずかな遅延があります。ゲームの場合、この遅延は多くの場合非常に小さいため無視できます。サウンドエフェクトは、play()関数が呼び出されてから数ミリ秒後に出力されます。音楽の場合、ほとんどのゲームではゲームプレイと相互作用しないため、これは重要ではありません。
それでも、一部のゲーム(主にリズムゲーム)では、プレイヤーのアクションを曲内で発生する何かと同期する必要があります(通常はBPMと同期します)。そのためには、正確な再生位置のより正確なタイミング情報を取得すると便利です。
非常に細かい再生タイミング精度を達成することは困難です。これは、オーディオの再生中に多くの要因が作用するためです。
オーディオは、使用するオーディオ バッファのサイズに応じて、チャンク単位 (連続ではなく) で混在しています (プロジェクト設定で待機時間を確認します)。
オーディオの混合チャンクはすぐには再生されません。
グラフィックAPIは2〜3フレーム遅れて表示します。
テレビで再生する場合、その画像処理のために遅延が追加される場合があります。
レイテンシーを削減する最も一般的な方法は、オーディオバッファを縮小することです(再び、プロジェクト設定でレイテンシー設定を編集します)。問題は、レイテンシーが小さすぎると、サウンドミキシングにかなり多くのCPUが必要になることです。これにより、スキップする(ミックスコールバックが失われたためサウンドの亀裂が生じる)リスクが高まります。
これは一般的なトレードオフであるため、Godotには、変更する必要のない適切なデフォルトが含まれています。
問題は、最終的に、このわずかな遅延ではなく、それを必要とするゲームのグラフィックとオーディオの同期です。 Godot 3.2以降では、より正確な再生タイミングを取得するためにいくつかのヘルパーが追加されました。
システムクロックを使用した同期¶
前述のように、AudioStreamPlayer.play() を呼び出しても、サウンドはすぐには開始されませんが、オーディオスレッドが次のチャンクを処理するときには開始されます。
この遅延は回避できませんが、AudioServer.get_time_to_next_mix() を呼び出すことで推測できます。
出力レイテンシ(ミックス後に発生するもの)は、AudioServer.get_output_latency() を呼び出すことによっても推測できます。
これら2つを追加すると、_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 begin yet).
time = max(0, time)
print("Time is: ", time)
private double _timeBegin;
private double _timeDelay;
public override void _Ready()
{
_timeBegin = OS.GetTicksUsec();
_timeDelay = AudioServer.GetTimeToNextMix() + AudioServer.GetOutputLatency();
GetNode<AudioStreamPlayer>("Player").Play();
}
public override void _Process(float _delta)
{
double time = (OS.GetTicksUsec() - _timeBegin) / 1000000.0d;
time = Math.Max(0.0d, time - _timeDelay);
GD.Print(string.Format("Time is: {0}", 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()
double time = GetNode<AudioStreamPlayer>("Player").GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix();
さらに精度を上げるには、レイテンシー情報(ミキシング後にオーディオが聞こえるまでにかかる時間)を引きます:
var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix() - AudioServer.get_output_latency()
double time = GetNode<AudioStreamPlayer>("Player").GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix() - AudioServer.GetOutputLatency();
これは以前よりも精度の低いアプローチですが、任意の長さの曲、または音楽に何か(たとえば、サウンドエフェクト)を同期させる場合にも機能します。結果は複数のスレッドの動作が原因で少し不安定になる可能性があります。値が前のフレームの値より小さくないことを確認してください(もしそうなら破棄してください)。
このアプローチを使用する前と同じコードを次に示します:
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)
public override void _Ready()
{
GetNode<AudioStreamPlayer>("Player").Play();
}
public override void _Process(float _delta)
{
double time = GetNode<AudioStreamPlayer>("Player").GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix();
// Compensate for output latency.
time -= AudioServer.GetOutputLatency();
GD.Print(string.Format("Time is: {0}", time));
}