第16章 - オーディオ
これまではグラフィックスを扱ってきましたが、すべてのゲームのもう 1 つの重要な側面はオーディオです。この章では、サウンドのサポートを追加します。
OpenAL
この章では、OpenAL (Open Audio Library) を使用してオーディオ機能について説明します。OpenAL はオーディオの OpenGL 版であり、抽象化レイヤーを介してサウンドを再生できます。そのレイヤーは、オーディオ サブシステムの根底にある複雑さから私たちを切り離します。それに加えて、3D シーンでサウンドを「レンダリング」することができます。サウンドを特定の場所に設定し、距離に応じて減衰させ、速度に応じて変更することができます (ドップラー効果のシミュレーション) 。
コーディングを開始する前に、OpenAL を扱う際に関係する主な要素を提示する必要があります。
バッファ。
ソース。
リスナー。
バッファには、音楽や効果音などのオーディオ データが格納されます。これらは、OpenGL ドメインのテクスチャに似ています。OpenAL は、オーディオ データが PCM (Pulse Coded Modulation) 形式 (モノラルまたはステレオ) であることを想定しているため、最初に PCM に変換せずに MP3 または OGG ファイルをダンプすることはできません。次の要素はソースで、音を発する 3D 空間内の位置 (ポイント) を表します。ソースはバッファーに関連付けられ (一度に 1 つだけ)、次の属性で定義できます。
・位置、ソースの場所 ($$x$$、YとZ座標)。ちなみに、OpenAL は OpenGL と同様に右手デカルト座標系を使用するため、(単純化するために) ワールド座標はサウンド空間座標系の座標と同等であると想定できます。
・ソースの移動速度を指定する速度。これは、ドップラー効果をシミュレートするために使用されます。
・サウンドの強さを変更するために使用されるゲイン (アンプ係数のようなもの)。ソースには、後でソース コードを説明するときに表示される追加の属性があります。
最後になりましたが、生成されたサウンドが聞こえるはずのリスナーです。Listener は、マイクが 3D オーディオ シーンでサウンドを受信するように設定されていることを表します。リスナーは 1 人だけです。そのため、オーディオのレンダリングはリスナーの視点から行われるとよく言われます。リスナーはいくつかの属性を共有しますが、向きなどの追加の属性があります。方向は、リスナーが向いている方向を表します。
つまり、オーディオ 3D シーンは、音を発する一連の音源と、それらを受信するリスナーによって構成されます。最終的に知覚される音は、リスナーからさまざまなソースまでの距離、それらの相対速度、および選択した伝播モデルによって異なります。ソースはバッファを共有し、同じデータを再生できます。次の図は、さまざまな要素タイプが含まれるサンプル 3D シーンを示しています。
実装
OpenAL を使用するには、最初に maven の依存関係をプロジェクト pom.xml に追加します。コンパイル時と実行時の依存関係を追加する必要があります。
...
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-openal</artifactId>
<version>${lwjgl.version}</version>
</dependency>
...
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-openal</artifactId>
<version>${lwjgl.version}</version>
<classifier>${native.target}</classifier>
<scope>runtime</scope>
</dependency>
...
それでは、コーディングを始めましょう。org.lwjglb.engine.soundオーディオの処理を担当するすべてのクラスをホストする名前で新しいパッケージを作成します。まずSoundBuffer、OpenAL バッファを表すという名前のクラスから始めます。そのクラスの定義の一部を以下に示します。
package org.lwjglb.engine.sound;
import org.lwjgl.stb.STBVorbisInfo;
import org.lwjgl.system.*;
import java.nio.*;
import static org.lwjgl.openal.AL10.*;
import static org.lwjgl.stb.STBVorbis.*;
import static org.lwjgl.system.MemoryUtil.NULL;
public class SoundBuffer {
private final int bufferId;
private ShortBuffer pcm;
public SoundBuffer(String filePath) {
this.bufferId = alGenBuffers();
try (STBVorbisInfo info = STBVorbisInfo.malloc()) {
pcm = readVorbis(filePath, info);
// Copy to buffer
alBufferData(bufferId, info.channels() == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, pcm, info.sample_rate());
}
}
public void cleanup() {
alDeleteBuffers(this.bufferId);
if (pcm != null) {
MemoryUtil.memFree(pcm);
}
}
public int getBufferId() {
return this.bufferId;
}
private ShortBuffer readVorbis(String filePath, STBVorbisInfo info) {
try (MemoryStack stack = MemoryStack.stackPush()) {
IntBuffer error = stack.mallocInt(1);
long decoder = stb_vorbis_open_filename(filePath, error, null);
if (decoder == NULL) {
throw new RuntimeException("Failed to open Ogg Vorbis file. Error: " + error.get(0));
}
stb_vorbis_get_info(decoder, info);
int channels = info.channels();
int lengthSamples = stb_vorbis_stream_length_in_samples(decoder);
ShortBuffer result = MemoryUtil.memAllocShort(lengthSamples * channels);
result.limit(stb_vorbis_get_samples_short_interleaved(decoder, channels, result) * channels);
stb_vorbis_close(decoder);
return result;
}
}
}
クラスのコンストラクターはサウンド ファイル パスを想定し、そこから新しいバッファーを作成します。最初に行うことは、 への呼び出しで OpenAL バッファを作成することalGenBuffersです。最後に、サウンド バッファは、それが保持するデータへのポインタのような整数によって識別されます。バッファーが作成されたら、その中にオーディオ データをダンプします。コンストラクターは OGG 形式のファイルを想定しているため、PCM 形式に変換する必要があります。これはreadVorbis メソッドで行われます。
以前のバージョンの LWJGL にはWaveData、WAV 形式のオーディオ ファイルをロードするために使用される という名前のヘルパー クラスがありました。このクラスは LWJGL 3 にはもう存在しません。それでも、そのクラスからソース コードを取得して、ゲームで使用することができます (おそらく変更は必要ありません)。
このSoundBufferクラスはcleanup、リソースを使い終わったときにリソースを解放するメソッドも提供します。
という名前のクラスによって実装される OpenAL のモデル化を続けましょうSoundSource。クラスは以下に定義されています。
package org.lwjglb.engine.sound;
import org.joml.Vector3f;
import static org.lwjgl.openal.AL10.*;
public class SoundSource {
private final int sourceId;
public SoundSource(boolean loop, boolean relative) {
this.sourceId = alGenSources();
alSourcei(sourceId, AL_LOOPING, loop ? AL_TRUE : AL_FALSE);
alSourcei(sourceId, AL_SOURCE_RELATIVE, relative ? AL_TRUE : AL_FALSE);
}
public void cleanup() {
stop();
alDeleteSources(sourceId);
}
public boolean isPlaying() {
return alGetSourcei(sourceId, AL_SOURCE_STATE) == AL_PLAYING;
}
public void pause() {
alSourcePause(sourceId);
}
public void play() {
alSourcePlay(sourceId);
}
public void setBuffer(int bufferId) {
stop();
alSourcei(sourceId, AL_BUFFER, bufferId);
}
public void setGain(float gain) {
alSourcef(sourceId, AL_GAIN, gain);
}
public void setPosition(Vector3f position) {
alSource3f(sourceId, AL_POSITION, position.x, position.y, position.z);
}
public void stop() {
alSourceStop(sourceId);
}
}
音源クラスは、その位置、ゲイン、および再生、停止、一時停止を制御するメソッドを設定するいくつかのメソッドを提供します。複数のソースが同じバッファを共有できることに注意してください。SoundBufferクラスと同様に、aSoundSourceは各操作で使用される識別子によって識別されます。このクラスはcleanup、予約されたリソースを解放するメソッドも提供します。しかし、コンストラクターを調べてみましょう。alGenSources最初に行うことは、呼び出しでソースを作成することです。次に、コンストラクターのパラメーターを使用していくつかの興味深いプロパティを設定します。
最初のパラメータ はloop、再生するサウンドをループ モードにするかどうかを示します。デフォルトでは、再生アクションがソースに対して呼び出されると、オーディオ データが消費されると再生が停止します。一部のサウンドではこれで問題ありませんが、バックグラウンド ミュージックなど、何度も再生する必要があるサウンドもあります。オーディオがいつ停止して再生プロセスを再開するかを手動で制御する代わりに、単純に looping プロパティを true に設定します: “ alSourcei(sourceId, AL_LOOPING, AL_TRUE);”.
もう 1 つのパラメータrelativeは、ソースの位置がリスナーに対して相対的かどうかを制御します。この場合、ソースの位置を設定するときは、基本的に、OpenAL 3D シーンの位置ではなく、世界の位置ではなく、リスナーまでの距離 (ベクトルを使用) を定義しています。これは、「alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE);”呼び出しによってアクティブ化されます。しかし、これを何に使用できますか?このプロパティは、たとえば、リスナーまでの距離によって影響を受けてはならない (減衰されてはならない) バックグラウンド サウンドの場合に役立ちます。たとえば、プレーヤーのコントロールに関連するバックグラウンド ミュージックやサウンド エフェクトを考えてみましょう。これらのソースを相対として設定し、それらの位置を(0,0,0)それらは減衰されません。
今度はリスナーの番です。これは、驚いたことに、 という名前のクラスによってモデル化されていSoundListenerます。これがそのクラスの定義です。
package org.lwjglb.engine.sound;
import org.joml.Vector3f;
import static org.lwjgl.openal.AL10.*;
public class SoundListener {
public SoundListener(Vector3f position) {
alListener3f(AL_POSITION, position.x, position.y, position.z);
alListener3f(AL_VELOCITY, 0, 0, 0);
}
public void setOrientation(Vector3f at, Vector3f up) {
float[] data = new float[6];
data[0] = at.x;
data[1] = at.y;
data[2] = at.z;
data[3] = up.x;
data[4] = up.y;
data[5] = up.z;
alListenerfv(AL_ORIENTATION, data);
}
public void setPosition(Vector3f position) {
alListener3f(AL_POSITION, position.x, position.y, position.z);
}
public void setSpeed(Vector3f speed) {
alListener3f(AL_VELOCITY, speed.x, speed.y, speed.z);
}
}
前のクラスとの違いは、リスナーを作成する必要がないことです。リスナーは常に 1 つ存在するため、作成する必要はありません。既に用意されています。したがって、コンストラクターでは、単に初期位置を設定するだけです。同じ理由で、cleanupメソッドは必要ありません。クラスには、クラスのようにリスナーの位置と速度を設定するためのメソッドもありますSoundSourceが、リスナーの向きを変更するための追加のメソッドがあります。オリエンテーションとは何かを確認しましょう。リスナーの向きは、次の図に示すように、「at」ベクトルと「up」ベクトルの 2 つのベクトルによって定義されます。
「at」ベクトルは基本的にリスナーが向いている方向を指し、デフォルトではその座標は(0,0,-1). 「上」ベクトルは、リスナーにとって上向きの方向を決定し、デフォルトでは(0,1,0)したがって、これら 2 つのベクトルのそれぞれの 3 つのコンポーネントが、alListenerfvメソッド呼び出しで設定されます。このメソッドは、float のセット (可変数の float) をプロパティ (この場合は向き) に転送するために使用されます。
続行する前に、ソースとリスナーの速度に関するいくつかの概念を強調する必要があります。ソースとリスナー間の相対速度により、OpenAL はドップラー効果をシミュレートします。ご存じないかもしれませんが、ドップラー効果は、あなたに近づいている移動物体が、遠ざかるときよりも高い周波数で放射しているように見える原因です. 問題は、単にソースまたはリスナーの速度を設定するだけでは、OpenAL はそれらの位置を更新しないということです。相対速度を使用してドップラー効果を計算しますが、位置は変更されません。そのため、移動するソースまたはリスナーをシミュレートする場合は、ゲーム ループ内でそれらの位置を更新する必要があります。
主要な要素をモデル化したので、それらを機能するように設定できます。OpenAL ライブラリを初期化する必要があるため、これを処理するという名前の新しいクラスを作成し、次のSoundManagerように開始します。
package org.lwjglb.engine.sound;
import org.joml.*;
import org.lwjgl.openal.*;
import org.lwjglb.engine.scene.Camera;
import java.nio.*;
import java.util.*;
import static org.lwjgl.openal.AL10.alDistanceModel;
import static org.lwjgl.openal.ALC10.*;
import static org.lwjgl.system.MemoryUtil.NULL;
public class SoundManager {
private final List<SoundBuffer> soundBufferList;
private final Map<String, SoundSource> soundSourceMap;
private long context;
private long device;
private SoundListener listener;
public SoundManager() {
soundBufferList = new ArrayList<>();
soundSourceMap = new HashMap<>();
device = alcOpenDevice((ByteBuffer) null);
if (device == NULL) {
throw new IllegalStateException("Failed to open the default OpenAL device.");
}
ALCCapabilities deviceCaps = ALC.createCapabilities(device);
this.context = alcCreateContext(device, (IntBuffer) null);
if (context == NULL) {
throw new IllegalStateException("Failed to create OpenAL context.");
}
alcMakeContextCurrent(context);
AL.createCapabilities(deviceCaps);
}
...
}
このクラスは、SoundBufferおよびSoundSourceインスタンスへの参照を保持して、それらを追跡し、後で適切にクリーンアップします。SoundBuffers は List に格納されますが、SoundSources は に格納されるMapため、名前で取得できます。コンストラクターは OpenAL サブシステムを初期化します。
・デフォルトのデバイスを開きます。
・そのデバイスの機能を作成します。
・OpenGL のようなサウンド コンテキストを作成し、それを現在のコンテキストとして設定します。
このSoundManagerクラスは、音源とバッファを追加するメソッドと、cleanupすべてのリソースを解放するメソッドを定義します。
public class SoundManager {
...
public void addSoundBuffer(SoundBuffer soundBuffer) {
this.soundBufferList.add(soundBuffer);
}
public void addSoundSource(String name, SoundSource soundSource) {
this.soundSourceMap.put(name, soundSource);
}
public void cleanup() {
soundSourceMap.values().forEach(SoundSource::cleanup);
soundSourceMap.clear();
soundBufferList.forEach(SoundBuffer::cleanup);
soundBufferList.clear();
if (context != NULL) {
alcDestroyContext(context);
}
if (device != NULL) {
alcCloseDevice(device);
}
}
...
}
また、リスナーとソースを管理するメソッドと、playSoundSourceその名前を使用してサウンドを有効にするメソッドも提供します。
public class SoundManager {
...
public SoundListener getListener() {
return this.listener;
}
public SoundSource getSoundSource(String name) {
return this.soundSourceMap.get(name);
}
public void playSoundSource(String name) {
SoundSource soundSource = this.soundSourceMap.get(name);
if (soundSource != null && !soundSource.isPlaying()) {
soundSource.play();
}
}
public void removeSoundSource(String name) {
this.soundSourceMap.remove(name);
}
public void setAttenuationModel(int model) {
alDistanceModel(model);
}
public void setListener(SoundListener listener) {
this.listener = listener;
}
...
}
このSoundManagerクラスには、カメラ位置を指定してリスナーの向きを更新するメソッドもあります。私たちの場合、カメラがあるときはいつでもリスナーが配置されます。では、カメラの位置と回転の情報が与えられた場合、「at」ベクトルと「up」ベクトルをどのように計算するのでしょうか? 答えは、カメラに関連付けられたビュー マトリックスを使用することです。「で」を変換する必要があります(0,0,-1)
そして「アップ」(0,1,0)
カメラの回転を考慮したベクトル。カメラに関連付けcameraMatrixられたビュー マトリックスを とします。それを達成するためのコードは次のとおりです。
public class SoundManager {
...
public void updateListenerPosition(Camera camera) {
Matrix4f viewMatrix = camera.getViewMatrix();
listener.setPosition(camera.getPosition());
Vector3f at = new Vector3f();
viewMatrix.positiveZ(at).negate();
Vector3f up = new Vector3f();
viewMatrix.positiveY(up);
listener.setOrientation(at, up);
}
...
}
上記のコードは、以前に説明した説明と同等であり、より効率的なアプローチです。完全な逆行列を計算する必要がないだけで同じ結果が得られる、 JOMLライブラリで利用可能な高速な方法を使用します。このメソッドはLWJGL フォーラムでJOML の作成者によって提供されたものなので、そこで詳細を確認できます。ソース コードを確認すると、SoundManagerクラスがビュー マトリックスの独自のコピーを計算することがわかります。
それだけです。サウンドを再生するために必要なインフラストラクチャはすべて揃っています。Mainバックグラウンド サウンドを設定するクラスで使用するだけでよく、特定のアニメーション フレームで特定のサウンドがリスナーの位置に相対的な強度でアクティブになります。
public class Main implements IAppLogic {
...
private SoundSource playerSoundSource;
private SoundManager soundMgr;
public static void main(String[] args) {
...
Engine gameEng = new Engine("chapter-16", opts, main);
...
}
@Override
public void cleanup() {
soundMgr.cleanup();
}
@Override
public void init(Window window, Scene scene, Render render) {
...
lightAngle = 45;
initSounds(bobEntity.getPosition(), camera);
}
private void initSounds(Vector3f position, Camera camera) {
soundMgr = new SoundManager();
soundMgr.setAttenuationModel(AL11.AL_EXPONENT_DISTANCE);
soundMgr.setListener(new SoundListener(camera.getPosition()));
SoundBuffer buffer = new SoundBuffer("resources/sounds/creak1.ogg");
soundMgr.addSoundBuffer(buffer);
playerSoundSource = new SoundSource(false, false);
playerSoundSource.setPosition(position);
playerSoundSource.setBuffer(buffer.getBufferId());
soundMgr.addSoundSource("CREAK", playerSoundSource);
buffer = new SoundBuffer("resources/sounds/woo_scary.ogg");
soundMgr.addSoundBuffer(buffer);
SoundSource source = new SoundSource(true, true);
source.setBuffer(buffer.getBufferId());
soundMgr.addSoundSource("MUSIC", source);
source.play();
}
@Override
public void input(Window window, Scene scene, long diffTimeMillis, boolean inputConsumed) {
...
soundMgr.updateListenerPosition(camera);
}
@Override
public void update(Window window, Scene scene, long diffTimeMillis) {
animationData.nextFrame();
if (animationData.getCurrentFrameIdx() == 45) {
playerSoundSource.play();
}
}
}
最後のメモ。AL11.AL_EXPONENT_DISTANCEOpenAL では、alDistanceModel を使用して必要なモデル ( 、AL_EXPONENT_DISTANCE_CLAMPなど)を渡すことにより、減衰モデルを変更することもできます。それらで遊んで、結果を確認できます。