Godot Engineのオーディオの基本と同期
はじめに
Godot Engine Advent Calendar 202212日目の記事になります。
4日目の記事もよろしくお願いします。
ゲームにとって音は重要な要素の一つ
昔、家庭用ゲームの開発チームのディレクターが漏らしていた言葉を思い出します。
開発の後期にサウンドが納品されて、ゲームにようやく組み込まれたときに「あぁ、ゲームになったなぁ」という言葉です。
オーディオはゲームにとって絵やプログラムと同じくらい重要な要素です。
ユーザーの体験としても、ゲーム性の底上げとしても、ただ音を流していれば良いわけではなく、最適なタイミングで最適な音をより良い形で流すこともゲームデザインのひとつだと考えています。
前提
Godot Engine 4.0
に沿った内容となりますので、3.x
とは若干異なりますのでご了承ください。
Godot Engineのオーディオ
画像はCakewalkの画面。お気に入りのソフト。
Godot Engineのオーディオシステムは、2D・3Dの位置情報を持ったオーディオも存在します。
エフェクトをかけることも可能です。
Godot Engine 4.0
からはOGGファイル
のメタデータを使用してプログラムからメタ情報にアクセスしやすくなりました。
今回の記事では、こういった内容を少しずつ取り上げたいと思います。
大まかに「こんな事ができる」を紹介
- 特定のエリアでBGMが水中にいるような音に変わる
- 特定のエリアでホールなどの音の響く空間を作ることができる
- 音が重なり大音量になるのを防ぎ、音量を圧縮することができる
- 個別に様々なフィルターをかけることができる
- 入力された音を解析することができる
- マイクから音を録音することができる
大まかには以上ですが、自由度が高くいろいろなことができるので、試してみると楽しいです。
オーディオバスについて
オーディオバスとは、オーディオチャンネルとも呼ばれ、デシベルスケールで表現されたシステムになります。
これは、一般的なDAWソフトなどと同様の単位になるため、オーディオデザイナーとのコミュニケーションもスムーズになります。
オーディオバスは複数のバスを設定することができ、最終的にMaster
バスでミックスされた音がスピーカーやヘッドホンなどのデバイスから流れます。
別のバスからルーティングして、Master
バスでミックスすることができます。
オーディオバスのルーティング
オーディオバスに入力された音がミックスされ、指定したバスにルーティングすることができます。
オーディオバスのルーティング先は、必ずそのバスよりも左側にあるバスしか指定できません。
もし指定したルーティング先がない場合は、自動的にMaster
バスにルーティングされます。
ルーティングのサンプル
例えば、以下のようなケースで使うことができます。
オーディオバス名 | 処理 | ルーティング先 |
---|---|---|
VoiceBus | 音声データがそのまま入力され、HiPassFilter を適用する。ラジオ音源のようになる。 |
AreaBus |
AreaBus | ホールのような空間のエフェクトを適用する。Delay やReverb フィルタを適用する。 |
Master |
Master | 最終的なミックス。Compressor を掛けて最終的な音量が0dB を超えないように調整します。 |
なし |
上記の例では、最初の「音声データ」は生の声で問題ありません。
VoiceBus
でラジオのような音に変換します。
次に、AreaBus
では、空間のエフェクトを掛けます。
これによって、別の空間にいる場合は、AreaBus
を通さないルーティングで、簡単に音を変化させることができます。
AudioServerでオーディオバスの処理をする
先程の例の中で、AreaBus
を通したくない場合があります。
AudioServer
クラスで、特定のバスをバイパスすることができます。
実際に先程の例と同じオーディオバスのレイアウトでプロジェクトを作ってみました。
# ボタンで再生やバイパス切り替えができるようにしておく
func _ready():
play_button.pressed.connect(_on_pressed_play_button)
area_bypass_button.toggled.connect(_on_toggled_area_bypass_button)
# 再生ボタンが押されたらオーディオを再生
func _on_pressed_play_button():
$AudioStreamPlayer.play()
# トグルボタンでバイパスのON・OFFを切り替える
func _on_toggled_area_bypass_button(_value:bool):
var area_bus_id:int = AudioServer.get_bus_index("AreaBus")
AudioServer.set_bus_bypass_effects(area_bus_id, _value)
これによって、必要なタイミングでエフェクトを掛けておき、不要になったらバイパスする処理が可能なことがわかります。
AudioStreamPlayerとAudioStream
AudioStreamPlayer
はノードで、AudioStream
はリソースです。
AudioStreamPlayer
は位置情報のないオーディオ再生機能ですが、AudioStreamPlayer2D
およびAudioStreamPlayer3D
は位置による聞こえ方を調整できます。
ドップラー効果などがわかりやすい例ですが、先述した特定のエリアでのみ反応するようなものも実装することができます。
音量の設定
Godot Japanのディスコードサーバーでも少し話題になったので触れておきます。
先述の通り、Godot Engineのオーディオはデシベルスケールで表現されています。
6dBごとに、2倍または半分になるので、12dBで4倍、18dBで8倍のように複雑です。
liner_to__db()
メソッドで0.0~1.0
を指定することで、デシベルスケールの値が返ってきますので、それを音量として設定するのが良いと思います。
OGGファイルにメタ情報を埋め込む
アセットはすべてインポートパネルから諸々設定できるのですが、oggファイルの場合は以下のような画面になります。
設定名 | 説明 |
---|---|
ループ | このサウンドがループするかどうか |
ループのオフセット | ループした際のオフセット位置(秒数) |
BPM | このサウンドのBPM |
Beat Count | 拍の数 |
Bar Beats | 1小節あたりの拍の数 |
Beat Count
とBar Beats
の説明が難しいのですが、4/4拍子
の分母と分子の部分だと認識してます。
(違ったらご指摘ください)
これらはすべてAudioStreamOggVorbis
クラスのプロパティで取得することができます。
Godot Engine 4.0
ではOGGファイルそのものがAudioStreamOggVorbis
というリソースファイルになりますので、インポートの設定のみで、値を取得することができます。
@onready var bgm:AudioStreamOggVorbis = preload("res://TestDrums-88.00bpm.ogg")
func _ready():
print(bgm.bpm) # 88
例えば、すでにAudioStreamPlayer
のノードが存在していて、このノードから音を取得したい場合は、以下のようにすると良いです。
func _ready():
var _stream:AudioStream = $AudioStreamPlayer.get_stream()
ちなみにBPMなどのプロパティはOGGファイルからのみ取得できるので、ご注意ください。
ゲームとオーディオの同期
いよいよ本題ですが、上記までの内容の詳細や同期については公式のドキュメントにも記載されています。
ただ、同期に関して、少し情報が足らなかったので、補足説明も含めてご紹介します。
オーディオの時間の取得
func _ready():
$Player.play() # AudioStreamPlayerを再生
func _process(delta):
# オーディオ再生時間とミックスにかかった時間を計算します。
var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()
# 出力の遅延時間を取得して補正します
time -= AudioServer.get_output_latency()
print("Time is: ", time)
公式から引用ですが、これでオーディオの時間を毎フレーム取得することができました。
ただ、これだけでは若干使いづらいので、先述したOGGファイルのプロパティを組み合わせて、リズムを取ってみます。
1小節あたりの時間をノーマライズして返す
曲のBPMや何拍子の音楽なのかが取得できていますので、曲の時間から計算します。
# ノーマライズした1小節あたりの時間を返す
func get_measure_time() -> float:
var bars = $Player.stream.bar_beats
var bpm = $Player.stream.
return wrapf(get_audio_time() * bpm / 60.0, 0, bars) / bars
func get_audio_time() -> float:
var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()
time -= AudioServer.get_output_latency()
return time
1拍あたりの時間をノーマライズして返す
1小節を分割すればよいだけなので、先程のget_mesure_time()
をカスタマイズします。
func get_measure_time(division:int) -> float:
var bars = $Player.stream.bar_beats
var bpm = $Player.stream.bpm
return wrapf(get_audio_time() * bpm * max(1, division) / 60.0, 0, bars) / bars
例えば120BPM
の場合、1小節は240BPM
が2小節分の長さになります。
つまり、引数のdivision
は4
か8
か16
を入れることが多いです。
それぞれ4ビート、8ビート、16ビートと同じ意味になります。
Division
は分割という意味ですが、まさに音楽的な刻みを意味しています。
1拍あたりの遷移が0~1で返ってきてるので…
lerp(0,100,get_measure_time())
などとして、0~100の線形の値を取ることもできます。
これを利用すると、割と簡単にリズムゲームを作ることができます。
課題
実は、まだ1拍あたりの時間の増分を取ることができていません。
全体的に音の再生の経過時間を基準としているので、前フレームとの差分を取るのがいいとは思っていますが、実行していません。
もし良い方法があればお知らせいただけると幸いです。
最後に
冒頭にも書きましたが、音は重要なのでゲームを演出する絵やエフェクト、シェーダープログラムなどと同等に作り込みたい部分です。
今後もゲーム開発中に気づいたことや、今回の記事のさらなる深掘りができたら記事にしていきたいと思います。
私自身もまだまだ知識不足ですので、ぜひGodot Japanのディスコードサーバーにご参加頂いて、一緒に知識を共有できればと思っています。
Discussion