Audio3D
Notes on this Tips
This sample is based on the programs published on the following sites. I change the code a little to make it easier to understand and explain it in Japanese. Basically, we use the original code as is, so if you actually adopt it in your game program, correct it in a timely manner and use it.
- Reference site
In addition, it is explained on the assumption that you have some basic knowledge about MonoGame and XNA. See MonoGame Tips and XNA Tips for the ruddly.
In particular, as mathematics, vectors, trigonometric functions, matrices, etc. are essential, so please know what they are to some extent.
environment
- platform
-
- Windows 10
- Code can be used on other MonoGame-enabled platforms
- Visual Studio
-
- Visual Studio 2019
- .NET Core
-
- 3.1
- MonoGame
-
- 3.8
About samples
Based on the position of the camera, you can hear the call from the dog's position and the position of the cat. The cat circles and circles the origin, and the player can control the position of the camera with the key. Make sure that the way you hear the sound changes with each change in the position of the camera or the position of the cat. I think that it is easy to understand using earphones. I'm unconfirmed on channel 5.1.
How to operate
What's To Do | Keyboard | Gamepad (XInput) | Mouse | Touch |
---|---|---|---|---|
Camera forward and backwards | ↑↓ | Left Stick (Top and Bottom) | - | - |
Change camera orientation | ←→ | Left Stick (Left and Right) | - | - |
End of game | Esc | Back | - | - |
What to prepare
- Dog squeal audio file
- Three cat squeal audio files
- Dog Image File
- Cat picture file
- Ground image file
The original sample on the official site uses prebuilt content .xnb files. If you want to keep the samples on the official site, don't use mgcbs, add them directly to your Visual Studio solution, and copy them at build time.
I don't know why the original sample is taking this method, but I think it's probably because we're migrating older versions of the project as is. Of course, you can use mgcb to generate the same file. The projects that can be downloaded on this site have been modified to the MGCB version.
program
Download the program for all the code.
This sample is designed to make the code look a little more general while making the sound playback look good at runtime. Using Audio3D only would be much less code, but it's described along the original sample. I'll keep the less important parts.
The project consists of the following code files:
- Audio3DGame
- AudioManager
- Cat
- Dog
- IAudioEmitter
- Program
- QuadDrawer
- SpriteEntity
QuadDrawer class
This class is a helper class for drawing rectangular polygons. Rectangular polygons are often used primarily to display 3D sprites (billboards). This tips is used for billboards for cats and dogs, as well as for rectangular displays of the ground.
This class has nothing to do with Audio3D.
field
readonly GraphicsDevice _graphicsDevice;
readonly AlphaTestEffect _effect;
readonly VertexPositionTexture[] _vertices;
Minimum information required to draw polygons. Draw polygons from vertex data without preparing model data.
constructor
<summary>
新しい四辺形の描画ワーカーを作成します。
</summary>
public QuadDrawer(GraphicsDevice device)
{
_graphicsDevice = device;
_effect = new AlphaTestEffect(device);
_effect.AlphaFunction = CompareFunction.Greater;
_effect.ReferenceAlpha = 128;
// 4つの頂点の配列を事前に割り当てます。
_vertices = new VertexPositionTexture[4];
_vertices[0].Position = new Vector3(1, 1, 0);
_vertices[1].Position = new Vector3(-1, 1, 0);
_vertices[2].Position = new Vector3(1, -1, 0);
_vertices[3].Position = new Vector3(-1, -1, 0);
}
Because this class is universally usable, you GraphicsDevice
receive an instance created in the game class initialization process.
Here, we set the effect necessary to draw the sprite and the position of the 4 vertices required for the rectangular polygon. The size should be scaled when drawing, so it is -1 to 1.
DrawQuad method
<summary>
3Dワールドの一部として四角形を描画します。
</summary>
public void DrawQuad(Texture2D texture, float textureRepeats, Matrix world, Matrix view, Matrix projection)
{
// 指定されたテクスチャとカメラマトリックスを使用するようにエフェクトを設定します。
_effect.Texture = texture;
_effect.World = world;
_effect.View = view;
_effect.Projection = projection;
// 指定された数のテクスチャの繰り返しを使用するように頂点配列を更新します。
_vertices[0].TextureCoordinate = new Vector2(0, 0);
_vertices[1].TextureCoordinate = new Vector2(textureRepeats, 0);
_vertices[2].TextureCoordinate = new Vector2(0, textureRepeats);
_vertices[3].TextureCoordinate = new Vector2(textureRepeats, textureRepeats);
// 矩形を描画します。
_effect.CurrentTechnique.Passes[0].Apply();
_graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, _vertices, 0, 2);
}
Sets the passed texture to a rectangle and draws it. Since this is a basic process, there is nothing to note.
If there is one point, a variable called can be specified in textureRepeats
the argument, so that the coordinates of the texture can be changed.
If vertexPositionTexture.TextureCoordinate is set beyond the range 0 to 1, the texture is drawn repeatedly by default.
Using it, it is possible to express that tiles can be arranged repeatedly.
In fact, the checkered patterns on the ground appear to have multiple simple black and white images lined up.
IAudioEmitter interface
<summary>
3D サウンドを再生するエンティティの位置と速度を検索するために AudioManager が使用するインターフェイス。
</summary>
public interface IAudioEmitter
{
Vector3 Position { get; }
Vector3 Forward { get; }
Vector3 Up { get; }
Vector3 Velocity { get; }
}
Defined as an entity that emits sound. Since the entities that emit sound this time are dogs and cats, they are inherited in their respective classes. Emitter needs the following information:
Property | use |
---|---|
Position | Entity position |
Forward | The direction the entity is facing (vector) |
Up | The up of the entity. The direction in which the head exists in a person |
Velocity | The speed at which the entity moves. Used to calculate Doppler values. |
SpriteEntity class
A class for representing 3D sprites (billboards). It is also an emitter, and inherits IAudioEmitter
.
Dogs and cats inherit this class.
property
<summary>エンティティの3D位置を取得または設定します。</summary>
public Vector3 Position { get; set; }
<summary>エンティティが向いている方向を取得または設定します。</summary>
public Vector3 Forward { get; set; }
<summary>このエンティティの上方向を取得または設定します。</summary>
public Vector3 Up { get; set; }
<summary>このエンティティの移動速度を取得または設定します。</summary>
public Vector3 Velocity { get; protected set; }
<summary>このエンティティの表示に使用されるテクスチャを取得または設定します。</summary>
public Texture2D Texture { get; set; }
It has the location information of the entity, etc. Basically I have information as an emitter. It is also a drawing entity, so it also has an image (Texture) to display.
Update method
<summary>
エンティティの位置を更新し、サウンドを再生できるようにします。
</summary>
public abstract void Update(GameTime gameTime, AudioManager audioManager);
Describes the entity's position and the process of playing sound, but implements it in the derived class.
Draw method
<summary>
エンティティをビルボードスプライトとして描画します。
</summary>
public void Draw(QuadDrawer quadDrawer, Vector3 cameraPosition, Matrix view, Matrix projection)
{
Matrix world = Matrix.CreateTranslation(0, 1, 0) *
Matrix.CreateScale(800) *
Matrix.CreateConstrainedBillboard(Position, cameraPosition, Up, null, null);
quadDrawer.DrawQuad(Texture, 1, world, view, projection);
}
Dogs and cats have different images and positions, but the other drawing mechanism is exactly the same, so I describe them here.
Each transformation and texture is passed to QuadDrawer to display a rectangular polygon. Since coordinate transformation is basic knowledge, explanations are not covered.
Matrix.CreateConstrainedBillboard
Use method and camera information to easily represent billboards, so make effective use of them.
Dog class
It is a class that does dog drawing and singing playback. SpriteEntity
Inheriting the class.
The dog remains in a fixed position in 3D space.
field
<summary>サウンドを開始または停止するまでの時間。</summary>
TimeSpan _timeDelay = TimeSpan.Zero;
<summary>現在再生中のサウンド(ある場合)。</summary>
SoundEffectInstance _activeSound = null;
_timeDelay
is used at intervals to play the cry.
SoundEffectInstance _activeSound
is an instance for playing a sound.
SoundEffectInstance
As you'll often see in the class of sound playback, you can use this as is in 3D sound.
By the way _activeSound
, I only receive a reference from the AudioManager class later, so I will not discard it on the Dot class side.
Update method
<summary>
犬の位置を更新し、音を鳴らします。
</summary>
public override void Update(GameTime gameTime, AudioManager audioManager)
{
// エンティティを固定位置に設定します。
Position = new Vector3(0, 0, -4000);
Forward = Vector3.Forward;
Up = Vector3.Up;
Velocity = Vector3.Zero;
// 時間遅延がなくなった場合は、ループ音を開始または停止します。 これは通常は永久に続きますが、6秒の遅延後に停止し、さらに4秒後に再起動します。
_timeDelay -= gameTime.ElapsedGameTime;
if (_timeDelay < TimeSpan.Zero)
{
if (_activeSound == null)
{
// 現在再生中のサウンドがない場合は、トリガーします。
_activeSound = audioManager.Play3DSound("DogSound", true, this);
_timeDelay += TimeSpan.FromSeconds(6);
}
else
{
// それ以外の場合は、現在のサウンドを停止します。
_activeSound.Stop(false);
_activeSound = null;
_timeDelay += TimeSpan.FromSeconds(4);
}
}
}
In this sample, the dog remains in the same position for a long time, so the first four parameters are specified by a decisive strike. Drawing is done in the basic class.
_timeDelay is then used as the remaining time to stop, playing the cry.
AudioManager
will be reproducedPlay3DSound
in 3D space by passing the name of the cry, loop information, and your own information as an emitter to .
The Dog class is designed to loop the cry, so you don't have to think too deeply because it's just an adjustment, such as playing or stopping branching.
Cat class
It is a class that does cat drawing and singing playback. SpriteEntity
Inheriting the class.
The cat is moving around the origin.
field
<summary>次の音を鳴らすまでの時間。</summary>
TimeSpan _timeDelay = TimeSpan.Zero;
<summary>サウンドバリエーションから選択するための乱数ジェネレータ。</summary>
static readonly Random _random = new Random();
_timeDelay
is used in the same way as the Dog class, with the rest of the time before the next squeal.
There are three types of cat calls that are randomly selected and played, but they have nothing to do with Audio3D because they are a bonus element.
Update method
<summary>
猫の位置を更新し、音を鳴らします。
</summary>
public override void Update(GameTime gameTime, AudioManager audioManager)
{
// 猫を大きな円で動かします。
double time = gameTime.TotalGameTime.TotalSeconds;
float dx = (float)-Math.Cos(time);
float dz = (float)-Math.Sin(time);
Vector3 newPosition = new Vector3(dx, 0, dz) * 6000;
// エンティティの位置と速度を更新します。
Velocity = newPosition - Position;
Position = newPosition;
if (Velocity == Vector3.Zero)
{
Forward = Vector3.Forward;
}
else
{
Forward = Vector3.Normalize(Velocity);
}
Up = Vector3.Up;
// 時間遅延がなくなった場合は、別の単発音をトリガーします。
_timeDelay -= gameTime.ElapsedGameTime;
if (_timeDelay < TimeSpan.Zero)
{
// 異なる3つのサウンドバリエーション(CatSound0、CatSound1、CatSound2)からランダムに選択します。
string soundName = "CatSound" + _random.Next(3);
audioManager.Play3DSound(soundName, false, this);
_timeDelay += TimeSpan.FromSeconds(1.25f);
}
}
Since the cat turns around the origin in a circular motion, it processes it so that it moves the trajectory of the circle using a trigonometric function. As a result, position, forward, and Velocity have changed one after another. Therefore, when you run the program, you can see that the cat's cry is circling around even if you do not move the camera.
_timeDelay
The following code is a process in which the cat's cry is randomly assigned and played at regular intervals.
audioManager.Play3DSound
I'm passing myself to the method that is an emitter.
You can see that the playback position of the sound changes one by one.
AudioManager class
This is finally the essence of Audio3D. A 3D sound consists of a listener and an emitter. The emitter already defines dogs and cats, so the AudioManager class defines listeners. There are usually multiple emitters, while there is only one listener.
This class inherits andGame
registers GameComponent
and uses the component in the class.
ActiveSound class
<summary>
アクティブな3Dサウンドを追跡し、アタッチされているエミッターオブジェクトを記憶するための内部ヘルパークラス。
</summary>
private class ActiveSound
{
public SoundEffectInstance Instance;
public IAudioEmitter Emitter;
}
It is defined at the bottom of the code, but it is a class for preserving the state that is playing. It has information about the sound instance being played and the emitter being played.
field
// このマネージャーにロードされるすべてのサウンドエフェクトのリスト。
static string[] _soundNames =
{
"CatSound0",
"CatSound1",
"CatSound2",
"DogSound",
};
<summary>音を聞くリスナーの情報です。これは通常カメラに一致するように設定されます。</summary>
public AudioListener Listener { get; } = new AudioListener();
<summary>AudioEmitter は、3Dサウンドを生成しているエンティティを表します。</summary>
readonly AudioEmitter _emitter = new AudioEmitter();
<summary>再生可能なすべての効果音を保存します。</summary>
readonly Dictionary<string, SoundEffect> _soundEffects = new Dictionary<string, SoundEffect>();
<summary>現在再生中のすべての3Dサウンドを追跡します。また、再生が終わったインスタンスの破棄にも使用します。</summary>
readonly List<ActiveSound> _activeSounds = new List<ActiveSound>();
_soundNames
defines the name of the audio file (asset name) to load. Since it is a sample program, it is defined to load the audio file in bulk first.
The imported sound data is _soundEffects
stored in .
AudioListener Listener
is the definition of the listener. Listener information is defined in because it public
is set from the Game class.
AudioEmitter _emitter
is the definition of the emitter when applying 3D to sound playback.
In this sample, when playing a sound, the value of each emitter object is set to _emitter
, so it is a form that shares one as an instance.
Of course, you can have for each AudioEmitter
object.
_activeSounds
has information about the sound you are playing in the class you defined ActiveSound
earlier.
Because emitter information changes all the time, it is used to update location information, etc., and to discard sound instances that have finished playing.
constructor
public AudioManager(Game game) : base(game) { }
GameComponent
Since it inherits the class, Game
it receives the instance and passes it to the base class.
Initialize method
<summary>
オーディオマネージャを初期化します。
</summary>
public override void Initialize()
{
// ゲームの世界のスケールと一致するように、3Dオーディオのスケールを設定します。
// DistanceScale は、離れるにつれて音量が変化する音の量を制御します。
// DopplerScale は、サウンドを通過するときにピッチが変化するサウンドの量を制御します。
SoundEffect.DistanceScale = 2000;
SoundEffect.DopplerScale = 0.1f;
// すべての効果音をロードします。
foreach (string soundName in _soundNames)
{
_soundEffects.Add(soundName, Game.Content.Load<SoundEffect>(soundName));
}
base.Initialize();
}
GameComponent
Because it inherits the class, it is automatically called at initialization.
SoundEffect.DistanceScale
and SoundEffect.DopplerScale
are parameters dedicated to static
3D sound.
SoundEffect.DistanceScale
to hear sound even in the distance.
SoundEffect.DopplerScale
is the influence of the Doppler effect. The higher the number, the greater the Doppler effect.
_soundNames
Save the asset name defined in in a loop to load _soundEffects
.
Dispose method
<summary>
効果音データをアンロードします。
GameComponent として登録すればゲーム終了時に自動的に呼ばれます。
</summary>
protected override void Dispose(bool disposing)
{
try
{
if (disposing)
{
foreach (SoundEffect soundEffect in _soundEffects.Values)
{
soundEffect.Dispose();
}
_soundEffects.Clear();
}
}
finally
{
base.Dispose(disposing);
}
}
GameComponent
It is automatically called at the end of the game because it inherits the class.
Destroying all imported sound assets.
Update method
<summary>
3Dオーディオシステムの状態を更新します。
</summary>
public override void Update(GameTime gameTime)
{
// 現在再生中のすべての3Dサウンドをループします。
int index = 0;
while (index < _activeSounds.Count)
{
ActiveSound activeSound = _activeSounds[index];
if (activeSound.Instance.State == SoundState.Stopped)
{
// 音の再生が終了した場合は廃棄してください。
activeSound.Instance.Dispose();
// アクティブリストから削除します。
_activeSounds.RemoveAt(index);
}
else
{
// サウンドがまだ再生されている場合は、3D設定を更新します。
Apply3D(activeSound);
index++;
}
}
base.Update(gameTime);
}
GameComponent
Because it inherits the class, it is automatically called in the update process.
Check the currently playing sound and, if it Apply3D
is playing, call the method to update the sound's location to match the emitter.
If any sound has finished playing, the instance is discarded.
Play3DSound method
<summary>
新しい3Dサウンドを設定し再生します。
</summary>
public SoundEffectInstance Play3DSound(string soundName, bool isLooped, IAudioEmitter emitter)
{
ActiveSound activeSound = new ActiveSound();
// インスタンスを生成し、インスタンス、エミッターを設定します。
activeSound.Instance = _soundEffects[soundName].CreateInstance();
activeSound.Instance.IsLooped = isLooped;
activeSound.Emitter = emitter;
// 3D 位置を設定します。
Apply3D(activeSound);
activeSound.Instance.Play();
// このサウンドがアクティブであることを保存します。
_activeSounds.Add(activeSound);
return activeSound.Instance;
}
The process of playing 3D sound from the outside. It has the name of the sound to play, whether to loop, and emitter information.
If you want to play a sound, create a new sound instance from the sound resource that you keep in the _soundEffects.
SoundEffectInstance
can also have a loop or not, so set the loop information.
Since I'm creating as ActiveSound
a playing state, I set emitter information that is the playback object of the sound there.
If you want to apply 3D location information to a sound, you'll Emitter
get the value you need.
Apply3D
The method is described below, but it is the process of applying 3D information from the emitter to the sound.
The basic process of 3D sound is to start playback with SoundEffectInstance.Play
3D information set.
The rest is to update the 3D information periodically until the sound is finished playing.
Apply3D method
<summary>
3Dサウンドの位置と速度の設定を更新します。
</summary>
private void Apply3D(ActiveSound activeSound)
{
_emitter.Position = activeSound.Emitter.Position;
_emitter.Forward = activeSound.Emitter.Forward;
_emitter.Up = activeSound.Emitter.Up;
_emitter.Velocity = activeSound.Emitter.Velocity;
activeSound.Instance.Apply3D(Listener, _emitter);
}
The process of applying a 3D effect to a specified sound instance using listeners and emitters.
We set the value to the _emitter of the common instance each time.
If you SoundEffectInstance.Apply3D
already AudioEmitter
have one, simply pass the listener and emitter to the method.
Audio3DGame Class
Finally, let's look at what the Game class is about.
field
readonly GraphicsDeviceManager _graphics;
readonly AudioManager _audioManager;
readonly SpriteEntity _cat;
readonly SpriteEntity _dog;
<summary>地面の描画に使用するテクスチャーです。</summary>
Texture2D _checkerTexture;
QuadDrawer _quadDrawer;
Vector3 _cameraPosition = new Vector3(0, 512, 0);
Vector3 _cameraForward = Vector3.Forward;
Vector3 _cameraUp = Vector3.Up;
Vector3 _cameraVelocity = Vector3.Zero;
KeyboardState _currentKeyboardState;
GamePadState _currentGamePadState;
AudioManager
Registers GameComponents
in , but has it as a field because it accesses it individually.
Others define dog, cat entities, textures for ground drawing, quaddrawer, camera information, and input information.
constructor
public Audio3DGame()
{
Content.RootDirectory = "Content";
_graphics = new GraphicsDeviceManager(this);
_audioManager = new AudioManager(this);
// AudioManager を Components に追加して自動的に Update メソッドが呼ばれるようにします。
Components.Add(_audioManager);
_cat = new Cat();
_dog = new Dog();
}
AudioManager
Generate and GameComponents
register with .
You've also generated the Cat and Dog classes.
LoadContent method
<summary>
グラフィックコンテンツをロードします。
</summary>
protected override void LoadContent()
{
_cat.Texture = Content.Load<Texture2D>("CatTexture");
_dog.Texture = Content.Load<Texture2D>("DogTexture");
_checkerTexture = Content.Load<Texture2D>("checker");
// 四角形ポリゴンを描画するためのクラス
_quadDrawer = new QuadDrawer(_graphics.GraphicsDevice);
}
Load the textures needed for each drawing.
QuadDrawer
is GraphicsDevice
generated here because requires .
Update method
<summary>
ゲームがロジックを実行できるようにします。
</summary>
protected override void Update(GameTime gameTime)
{
HandleInput();
UpdateCamera();
// 新しいカメラの位置について AudioManager に伝えます。
_audioManager.Listener.Position = _cameraPosition;
_audioManager.Listener.Forward = _cameraForward;
_audioManager.Listener.Up = _cameraUp;
_audioManager.Listener.Velocity = _cameraVelocity;
// ゲームエンティティに動き回って音を鳴らすように伝えます。
_cat.Update(gameTime, _audioManager);
_dog.Update(gameTime, _audioManager);
base.Update(gameTime);
}
HandleInput
The methods and UpdateCamera
methods are the process of retrieving device input and handling the camera, which will be discussed later.
Since the camera position and listener position are almost always the same, the listener is set to the same value after the camera update process.
_cat
Call the and _dog
Update methods to move and squeal, respectively.
Draw method
<summary>
ゲームが描画する必要があるときに呼び出されます。
</summary>
protected override void Draw(GameTime gameTime)
{
var device = _graphics.GraphicsDevice;
device.Clear(Color.CornflowerBlue);
device.BlendState = BlendState.AlphaBlend;
// カメラ行列を計算します。
var view = Matrix.CreateLookAt(_cameraPosition, _cameraPosition + _cameraForward, _cameraUp);
var projection = Matrix.CreatePerspectiveFieldOfView(1, device.Viewport.AspectRatio, 1, 100000);
// チェッカーグラウンドポリゴンを描画します。
var groundTransform = Matrix.CreateScale(20000) * Matrix.CreateRotationX(MathHelper.PiOver2);
_quadDrawer.DrawQuad(_checkerTexture, 32, groundTransform, view, projection);
// ゲームエンティティを描画します。
_cat.Draw(_quadDrawer, _cameraPosition, view, projection);
_dog.Draw(_quadDrawer, _cameraPosition, view, projection);
base.Draw(gameTime);
}
Generates view and projection transformations from camera information.
For the ground, the rectangle drawn in QuadDrawer is displayed vertically, so it is rotated to be horizontal and enlarged moderately.
_quadDrawer.DrawQuad
The second argument of the method specifies the number of repeats of the texture, which specifies that 32x32 sheets appear side by side.
_cat and _dog are billboarded internally, so the camera's location information is passed and drawn.
HandleInput method
<summary>
ゲームを終了するための入力を処理します。
</summary>
void HandleInput()
{
_currentKeyboardState = Keyboard.GetState();
_currentGamePadState = GamePad.GetState(PlayerIndex.One);
// 終了を確認します。
if (_currentKeyboardState.IsKeyDown(Keys.Escape) ||
_currentGamePadState.Buttons.Back == ButtonState.Pressed)
{
Exit();
}
}
Information on the keyboard and gamepad is acquired, and the end of the game is determined.
UpdateCamera method
<summary>
カメラを動かすための入力を処理します。
</summary>
void UpdateCamera()
{
const float turnSpeed = 0.05f;
const float accelerationSpeed = 4;
const float frictionAmount = 0.98f;
// 左または右に曲がります。
float turn = -_currentGamePadState.ThumbSticks.Left.X * turnSpeed;
if (_currentKeyboardState.IsKeyDown(Keys.Left)) turn += turnSpeed;
if (_currentKeyboardState.IsKeyDown(Keys.Right)) turn -= turnSpeed;
_cameraForward = Vector3.TransformNormal(_cameraForward, Matrix.CreateRotationY(turn));
// 前方または後方に加速します。
float accel = _currentGamePadState.ThumbSticks.Left.Y * accelerationSpeed;
if (_currentKeyboardState.IsKeyDown(Keys.Up)) accel += accelerationSpeed;
if (_currentKeyboardState.IsKeyDown(Keys.Down)) accel -= accelerationSpeed;
_cameraVelocity += _cameraForward * accel;
// 現在の位置に速度を追加します。
_cameraPosition += _cameraVelocity;
// 摩擦力を加えます。
_cameraVelocity *= frictionAmount;
}
The camera is controlled to move.
It also includes calculations such as acceleration and friction, but I think it's probably taking into account the Doppler effect on 3D sound. Since I do not do anything special about the calculation itself, I will not discuss it.
Summary
I've covered it at length, but it's usually enough to refer to the class for AudioManager
how to play 3D sound.
In fact, most of the difficulties are done by the framework, because what SoundEffectInstance
we're doing is setting 3D information into .
If you've sampled only the parts of the 3D sound, you'll end up writing a little work in the Game class.
However, I'm writing Tips based on the original story, so it's been a lot longer.