Object-oriented 3D sound system using FMOD in one day
|
In this post I will describe how I created an object-oriented 3D sound system using FMOD in the time-span of one day. This 3D sound system was used in our experimental Tower Defense game. It is mostly designed towards our Tower Defense game but many generic-usage functions was kept in mind when designing it. It is therefore free from other libraries except FMOD itself and could be integrated in any project. The inspirational sources for this 3D sound system comes from using SFML. In SFML you can load music and sound effects (either 3D or 2D) using an object like: “sf::Sound sound” and load a sound by typing: sound.Load(“sfx.wav”);. To play the sound effect you write sound.Play();. As you can see it is very simple to use for the client, and this is exactly the type of easy usage I wanted to replicate. To achieve this I need one abstract base class for all my audio interfaces like 3D sound, 2D sound and music. I also need a system manager for FMOD which my audio objects will need a pointer to in order to function. Here is my base audio interface:
class AudioSystem;
class IAudio{
public:
virtual ~IAudio(void){}
virtual bool Load(const String& filepath) = 0;
virtual void Play() = 0;
virtual void Pause() = 0;
virtual void Stop() = 0;
virtual void SetLoop(bool value) = 0;
friend class Application;
private:
static void SetAudioSystem(AudioSystem* p_system) { s_system = p_system; }
protected:
static AudioSystem* s_system;
};
2D/3D sound and music objects all inherit from this base interface class. As you can see it is pure abstract and holds a pointer to the class “AudioSystem” which is my FMOD wrapper. The AudioSystem pointer is static because I want to set it once only and never change it. This way all audio objects created will already have the pointer to the system set and ready for use. The reason for using friend class is because I want to set it in our Application class upon initialization. I don’t want the client to have the ability to change audio system anywhere else. The audio system, that is, my FMOD wrapper declaration looks like this: (“String” is a typedef of std::string)
#include "AudioSystemPrereq.h"
class AudioSystem : public SoundListener
{
public:
AudioSystem(void);
~AudioSystem(void);
bool Init();
void Shut();
void UpdatePosition(float x, float y, float z);
void Update();
bool Load3DSound(const String& filepath, FMOD::Sound** sound);
bool Load2DSound(const String& filepath, FMOD::Sound** sound);
bool LoadSoundFromStream(const String& filepath, FMOD::Sound** sound);
void Play3DSound(FMOD::Sound* sound, const FMOD_VECTOR& position, FMOD::Channel** channel);
void Play2DSound(FMOD::Sound* sound, FMOD::Channel** channel);
void PlayStreamingSound(FMOD::Sound* sound, FMOD::Channel** channel);
void StopSound(FMOD::Sound* sound);
void StopMusic(FMOD::Sound* sound);
void PauseSound(FMOD::Sound* sound, FMOD::Channel** channel);
void PauseMusic(FMOD::Sound* sound, FMOD::Channel** channel);
void SetLoop(FMOD::Sound* sound, bool value);
private:
bool ErrorCheck(FMOD_RESULT result);
FMOD::Channel* m_sfx_channel;
FMOD::Channel* m_ambient_channel;
FMOD::Channel* m_music_channel;
FMOD::System* m_system;
float m_distance_factor; //General size of objects in the world to decide distance for 3D sound
};
You may notice that the audio system is inheriting from the class “SoundListener”.
class SoundListener{
public:
SoundListener(void){}
virtual ~SoundListener(void){}
virtual void UpdatePosition(float x, float y, float z) = 0;
protected:
FMOD_VECTOR m_position;
};
This is for updating 3D sound position from the camera, which I will describe later. It is designed this way so we can take a part of the sound system and give it to someone/something else, like a camera. The declaration is mostly self-explanatory and I will go through some of the implementation below when explaining how my audio objects work. Declaration for Sound2D class:
class Sound2D : public IAudio{
public:
Sound2D(void);
~Sound2D(void);
bool Load(const String& filepath);
void Play();
void Stop();
void Pause();
void SetLoop(bool value);
void SetVolume(float volume);
private:
FMOD::Sound* m_sound;
FMOD::Channel* m_channel;
float m_volume;
};
…and the implementation
Sound2D::Sound2D(void) : m_sound(nullptr), m_volume(1.0f) {}
Sound2D::~Sound2D(void){
if (m_sound) { m_sound->release(); }
}
bool Sound2D::Load(const String& filepath){
if (!s_system->Load2DSound(filepath, &m_sound)) {
return false;
}
return true;
}
void Sound2D::Play(){
s_system->Play2DSound(m_sound, &m_channel);
m_channel->setVolume(m_volume);
}
void Sound2D::Pause(){
s_system->PauseSound(m_sound, &m_channel);
}
void Sound2D::SetLoop(bool value){
s_system->SetLoop(m_sound, value);
}
void Sound2D::SetVolume(float volume){
m_volume = volume;
if(m_channel!=nullptr){
m_channel->setVolume(m_volume);
}
}
Seems pretty straight forward, right? Also, as you can see in the destructor the object takes care of deleting the memory for the sound buffer so it is also possible to create audio objects on the stack. Let’s take a look at what happens in the audio system class when loading and playing a Sound2D class.
bool AudioSystem::Load2DSound(const String& filepath, FMOD::Sound** sound){
FMOD_RESULT result = m_system->createSound(filepath.c_str(), FMOD_2D, 0, &*sound);
if (!ErrorCheck(result)) return false;
result = (*sound)->setMode(FMOD_LOOP_OFF);
if (!ErrorCheck(result)) return false;
return true;
}
void AudioSystem::Play2DSound(FMOD::Sound* sound, FMOD::Channel** channel){
FMOD_RESULT result = m_system->playSound(FMOD_CHANNEL_FREE, sound, false, &*channel);
ErrorCheck(result);
}
First of all, when we are loading a sound we send a double pointer as a parameter. This is because FMOD uses a pointer to a FMOD::Sound object, which we store in Sound2D, so we want to use the pointer in our class. Another way to solve this would be to create a temporary pointer in the Load2DSound and return it instead of a boolean value, then check for NULL if the operations was successful or not. This time, I chose to do it this way because it was minimalistic. In the Play2DSound method we instead send a double pointer to the channel, because this time we want to get a new channel that is free every time we play the audio object. If we do it this way we can play the same audio file simultaneously without interruption. This is not everything, but the basic concept of how the system works. You can download all the headers and source files here. When it comes to music and 3D sound the only difference is that it is calling some other methods in the audio system like streaming for music and setting distance for 3D. The “UpdatePosition(float,float,float) method in the SoundListener class is connected to the camera in the Tower Defense game. It updates the position of the FMOD_VECTOR each frame so the position is always synced. The 3D positioning is then auto-magically taken care of(almost anyway!) When looking back at this system today, after 8 months there are numerous improvements I could make. For example, the entire wrapper could be implemented as an interface class for each audio object and instead use the FMOD::System* as a static member variable in the base audio interface class . |