🎶

Godot Engineのオーディオの基本と同期

2022/12/12に公開

はじめに

Godot Engine Advent Calendar 202212日目の記事になります。
4日目の記事もよろしくお願いします。

https://zenn.dev/saitos/articles/godot-pokemon-zukan

ゲームにとって音は重要な要素の一つ

昔、家庭用ゲームの開発チームのディレクターが漏らしていた言葉を思い出します。
開発の後期にサウンドが納品されて、ゲームにようやく組み込まれたときに「あぁ、ゲームになったなぁ」という言葉です。

オーディオはゲームにとって絵やプログラムと同じくらい重要な要素です。
ユーザーの体験としても、ゲーム性の底上げとしても、ただ音を流していれば良いわけではなく、最適なタイミングで最適な音をより良い形で流すこともゲームデザインのひとつだと考えています。

前提

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 ホールのような空間のエフェクトを適用する。
DelayReverbフィルタを適用する。
Master
Master 最終的なミックス。Compressorを掛けて最終的な音量が0dBを超えないように調整します。 なし

上記の例では、最初の「音声データ」は生の声で問題ありません。
VoiceBusラジオのような音に変換します。

次に、AreaBusでは、空間のエフェクトを掛けます。
これによって、別の空間にいる場合は、AreaBusを通さないルーティングで、簡単に音を変化させることができます。

AudioServerでオーディオバスの処理をする

先程の例の中で、AreaBusを通したくない場合があります。
AudioServerクラスで、特定のバスをバイパスすることができます。

実際に先程の例と同じオーディオバスのレイアウトでプロジェクトを作ってみました。

https://youtu.be/X7U3ZKtKQvw

# ボタンで再生やバイパス切り替えができるようにしておく
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 CountBar 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ファイルからのみ取得できるので、ご注意ください。

ゲームとオーディオの同期

いよいよ本題ですが、上記までの内容の詳細や同期については公式のドキュメントにも記載されています。

https://docs.godotengine.org/ja/stable/tutorials/audio/index.html

ただ、同期に関して、少し情報が足らなかったので、補足説明も含めてご紹介します。

オーディオの時間の取得

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小節分の長さになります。
つまり、引数のdivision4816を入れることが多いです。
それぞれ4ビート、8ビート、16ビートと同じ意味になります。

Division分割という意味ですが、まさに音楽的な刻みを意味しています。

1拍あたりの遷移が0~1で返ってきてるので…

lerp(0,100,get_measure_time())

などとして、0~100の線形の値を取ることもできます。

これを利用すると、割と簡単にリズムゲームを作ることができます。

課題

実は、まだ1拍あたりの時間の増分を取ることができていません。
全体的に音の再生の経過時間を基準としているので、前フレームとの差分を取るのがいいとは思っていますが、実行していません。

もし良い方法があればお知らせいただけると幸いです。

最後に

冒頭にも書きましたが、音は重要なのでゲームを演出する絵やエフェクト、シェーダープログラムなどと同等に作り込みたい部分です。

今後もゲーム開発中に気づいたことや、今回の記事のさらなる深掘りができたら記事にしていきたいと思います。

私自身もまだまだ知識不足ですので、ぜひGodot Japanのディスコードサーバーにご参加頂いて、一緒に知識を共有できればと思っています。

https://discord.gg/DyFvSJZ

Discussion