Introduction
In this guide I will try to explain how to implement spatialization for sounds using the SFML library for 2D games (Any genre: Scroller, Top-down, etc.)
This is not a short tutorial, so you might want to skip to the code section to get the goodies.
It focuses on 2 issues:
- Explaining how to make something useful out of the given API, with graphical and code examples.
- Solving the so called "incorrect panning", where the sound only plays in your left speaker when a sound is on the left, and as soon as it passes to the right, it only plays on the right speaker.
- The SFML library API.
- Having already been able to play a Sound (with or without spatialization).
- Have basic algebra notions (3D, what a vector is, right hand rule...).
- C++ if you wish understand the code. Not needed for the theory.
A foreword about SFML
SFML uses OpenAL for it's audio functions.
At least up to version 1.4 of SFML, the sf::Listener::SetTarget only takes 1 vector as parameter.
This function actually calls an OpenAL function to set the orientation of the listener, which takes 2 vectors.
One to define where the face of the listener looks at, called "at" ("target" in SFML).
And one to define where is the top of the listener's head is pointing, called "up".

SFML has a hard-coded "up" vector of (0, 1, 0).
In 3D, this wont allow you to change the angle of your "neck" (e.g. For peeking on the corner of a wall).
Acording to OpenAL, "Up" and "At" vectors must be linearly independent. This meaning: you can't only use the Y parameter on Listener::SetTarget(), you need to use values that are not zero for at least X or Z.
Knowing the functions
You have to know 5 functions to use spatialization:
- sf::Listener::SetPosition(x, y, z); (static)
- sf::Listener::SetTarget(targetX, targetY, targetZ); (static)
- sf::Sound::SetPosition(x, y, z);
- sf::Sound::SetMinDistance(distance);
- sf::Sound::SetAttenuation(factor);
SFML (and consecuently OpenAL) don't know what a pixel is. They just use abstract metrics.
For that reason the SetMinDistance and SetAttenuation functions exist.
Sound::SetMinDistance() defines from which distance to the listener a sound must be to be heard to the fullest (100% volume). Farther from that distance, the attenuation factor affects the volume, lowering it as farther as you go. The attenuation factor can be set with Sound::SetAttenuation().
You don't really care about the mathematical function to calculate volume-for-distance, only that is something like "1/distance". You'll have to tune this parameters by hand for your application. I will give an "out of the box" solution on the end, so you can start tuning from working code.The key in this is letting SFML translate your metric system (pixels, meters, miles, blocks, ...whatever) into something that it understands and can work with.
Deviations from these values can be used to give special attributes to the sounds, for example to make an explosion sound reach further by increasing the "minimum distance".
Now by using Sound::SetPosition and Listener::SetPosition, SFML knows how to play a the given "Sound" in a realistic fashion (using the distance between "Listener" and "Sound").
Important: Sounds to be spatialized must be monaural ("mono", 1 channel).
It must not be stereo (2 channels).
This is an excerpt from the OpenAL specification:
Buffers containing audio data with more than one channel will be played without 3DYou've been warned! If for some reason a sound doesn't seem to work with spatialization, you could recheck if it's recorded in stereo.
spatialization features – these formats are normally used for background music.
I'll talk about Listener::SetTarget next.
Spatialization for 2D games.
We will be using two separate coordinate systems, the sound coordinate system (with components X, Y, Z) and the screen coordinate system (with components X, Y).
Graphics and Audio are two different worlds, the one that sets the rules on how to bind them is you.
The spatialization I'm going to explain here is makes you:
Hear from the left when the sound comes from -X, and to the right when it does from +X.
Hear from the top (over your head) when the sound comes from +Y, and to the bottom (below you) when it does from -Y.
Depth of sound is not treated here, but it would be +Z for "farther" and -Z for "nearer".
(OpenAL uses the right hand rule for it's sound-coordinate system.)
Most 2D games take this approach.
We will be using just the X and Y coordinates. We will leave Z to always be zero.
The X sound coordinate for the X screen coordinate
and the Y sound coordinate for the Y screen coordinate.
Listener::SetTarget() sets the direction at where the listener is facing.
I make emphasis in direction, NOT position. This is direction is always relative to the listener's position, you don't need to compute where the target is at, but the direction vector where it's pointing.
For example, if you want the listener's face pointing to the left, and he is in the (4, 5, 0) position, you just need to set the target to something like (-10, 0, 0), but NOT (-6, 5, 0)
This way you would hear sounds coming from +Z on your right speaker, and -Z on your left speaker (because remember that "up" is always +Y, (0, 1, 0)).
Note that this vector doesn't need to be normalized (i.e. you could use: (3, 1, 0) , (100, 7, 0)...)
We will be always facing the monitor, so the target should always be (0, 0, 1)
The incorrect way, why it doesn't work.
You started testing and said: "Well, I'll put the listener on the center of the screen, and then put a sound passing by". You somewhat got something working but, to your surprise, the sound comes out only from one speaker at a time, not even when near!
And this is expected.
Let me explain with some images.
What you did was this:
The sound is going thru the head of the listener, so at any given moment the listener is going to hear from one side of his head only!The solution
The solution to this that I propose is to go "away" from the screen plane, in a 3D sense (move away from the plane given by the X and Y screen axes, at Z=0).


This way you're listening to the sound as it passes in front of you. And believe me, it's what you expect... as being a person with a head facing a flat monitor screen from a distance (exactly what we are reproducing here!)
Because of the right hand rule, as
- We want +X to be right (thumb) and
- Up is always pointing at +Y, because it's hard-coded (index finger)
float listenerDepth=300.f;If you're using pixels as your metric, I find "300" to be a nice depth to put our listener at.
sf::Listener::SetPosition(0.f, 0.f, listenerDepth);
And because we just moved away the listener, we should make the target go along -Z to keep it facing the X-Y screen plane (at Z=0).
sf::Listener::SetTarget(0.f, 0.f, -listenerDepth);Take into account that as we moved away from the screen, the min distance and attenuation will affect the sounds differently.
Look into the last graphic. You'll find that before (listener at Z=0), the min distance was a radius that defined a sphere, and the intersection was a circle of the same radius.
Now, with the listener away, we should recalculate that "min distance radius" so that the intersection with the X-Y plane is the same circle as before it was before moving away.
If you were already using a min distance, you could recalculate it like:
newMinDistance = √(oldMinDistance² + listenerDepth²)
The code
Down to business, here's whats you're more likely looking for:
The spatialization wrapper
/*static*/ void SoundSpatialization::initialize(
\tfloat defaultMinDistance,
\tfloat defaultAttenuation,
\tfloat listenerDepth
)
{
\tSoundSpatialization::listenerDepth=listenerDepth;
\tbaseAttenuation=defaultAttenuation;
\t//calculate the REAL min distance
\t//(see min distance sphere graphic @ blog.tbam.com.ar)
\tbaseEffectiveMinDistance=
\tsqrt(defaultMinDistance*defaultMinDistance + listenerDepth*listenerDepth);
\t//face back to the XY plane
\tsf::Listener::SetTarget(0, 0, -listenerDepth);
}
/*static*/ void SoundSpatialization::localizeListener(float x, float y)
{
\t//go some distance away from the XY plane
\tsf::Listener::SetPosition(x, y, listenerDepth);
}
/*static*/ void SoundSpatialization::setupSound(sf::Sound &snd)
{
\tsnd.SetMinDistance(baseEffectiveMinDistance);
\tsnd.SetAttenuation(baseAttenuation);
}
/*static*/ void SoundSpatialization::localizeSound(sf::Sound &snd, float x, float y)
{
\tsnd.SetPosition(x, y, 0);
}
Example initialization code
SoundSpatialization::initialize();Example frame code
sf::Sound enemySound(sndbuf, true);
SoundSpatialization::setupSound(enemySound);
Vector2f playerPos;»Download the complete source files « (working example included)
Vector2f enemyPos;
//...
SoundSpatialization::localizeListener(playerPos.x, playerPos.y);
SoundSpatialization::localizeSound(enemySound, enemyPos.x, enemyPos.y);
Extra section - Spatialization of sound for games where "you hear what the character hears".
I personally do not endorse this behaviour in 2D games, but if your game logic depends on it, here's an insight on how to do it:
Use the Z coordinate for your Y screen axis and SetTarget().
In this type of game you want the listener to be actually facing where the character is facing.
Because of the "SetTarget" thing I mentioned on the beginning (foreword about SFML), "Up" is locked in the Y-axis (sound coordinate) that's why we CAN'T use the Y screen-cordinate for the Y sound-coordinate.
But we still can use the Z sound coordinate to reproduce what's in your Y screen coordinate.
Although maybe confusing at first, once you get used to the idea it will come naturally.
You can still take the listener away from the screen (now the X-Z plane) to avoid the panning issue, but it should be moved away in the Y axis instead of in Z.
Beware that targeting without X and Z components make the target vector linearly dependent with "Up" (0, 1, 0) and the behaviour for that is undefined in OpenAL.
You could do, for example:
float listenerDepth=300.f;Final thoughts
Sprite sprite;
Sound enemySound;
Vector2f enemyPos;
Vector2f playerPos;
//position of the mouse in screen coordinates (not window coords)
//the player will be facing at this point
Vector2f cursor;
//...
Vector2f delta=cursor - playerPos;
//Adjust the sprite's angle
//(Notice: atan retunrs radians, SetRotation takes degrees)
sprite.SetRotation(atan(delta.y/delta.x) * 180.f/_PI);
sprite.SetPosition(playerPos);
Vector3f soundTarget;
//BEWARE that if delta.x==0 && delta.y==0 (leaving "target" linearly
// dependent with "Up" (0, 1, 0) the behaviour is UNDEFINED
if(delta.x==0 && delta.y==0) {
\t//Just point a little bit up to avoid unexpected behaviour
\tsoundTarget.x=0.f;
\tsoundTarget.y=-listenerDepth;
\tsoundTarget.z=0.01f;
}
else {
\tsoundTarget.x=delta.x; //x world coordinate =\g x sound coordinate
\tsoundTarget.y=-listenerDepth; //y world coordinate =\g point back to the XZ plane
\tsoundTarget.z=delta.y; //y world coordinate =\g z sound coordinate;
}
sf::Listener::SetPosition(playerPos.x, listenerDepth, playerPos.y);
sf::Listener::SetTarget(target);
enemySound.SetPosition(enemyPos.x, 0, enemyPos.y);
I found it hard to get it working, and found no significant resources on this on the Internet so I made this guide to help out the SFML community out there.
Have fun and make great games!


0 comments:
Post a Comment