Jump to content

Leadwerks + FMOD Studio


reepblue

1,071 views

 Share

Cyclone was shaping up visually, but I was getting feedback about the sound implementation not being so good. I previously created a script system that defined all my sounds under profiles. This was a step in the right direction and made it easier to tweak sounds without recompiling code. However, no matter how much I played with variables, I never was able to satisfy the complaints. 

After I showcased my first gameplay trailer, I decided to finally sit down and really understand FMOD. I was really overwhelmed at first as I was looking at the core api, but then I was suggested to use the FMOD Studio API instead. 

FMOD studio is a nifty program that allows much more fine control how sounds are played in a 3D environment. You can also add parameters to alter the sounds with code in real time. In this example, the engine sound can be changed via the RPM and Load values given. (You can check the examples of the SDK on how to do this.)

image.thumb.png.8f5e46ac87c6b428091cbca7ecfe0806.png

FMOD Studio packs everything into bank files. You can have as many banks as you want, but for my game, I only use one. 

To load a bank file, I just do a for loop in a directory called Media.

		void Initialize()
		{
			if (!initialized)
			{
				Msg("Initializing FMOD sound driver...");
				FMOD::Studio::System::create(&system);
				system->getCoreSystem(&coreSystem);
				coreSystem->setSoftwareFormat(0, FMOD_SPEAKERMODE_DEFAULT, 0);
				system->initialize(1024, FMOD_STUDIO_INIT_NORMAL, FMOD_INIT_NORMAL, NULL);
				system->setNumListeners(1);
				system->update();

				//Load any .bank files in main directory
				Leadwerks::Directory* dir = Leadwerks::FileSystem::LoadDir("Media/");
				if (dir)
				{
					for (std::size_t i = 0; i < dir->files.size(); i++)
					{
						std::string file = dir->files[i];
						std::string ext = Leadwerks::String::Lower(Leadwerks::FileSystem::ExtractExt(file));
						if (ext == "bank")
						{
							std::string fullpath = Leadwerks::FileSystem::RealPath("Media/" + file);
							Msg("Loading FMOD bank file \"" + fullpath + "\"...");

							FMOD::Studio::Bank* bnk = NULL;
							FMOD_RESULT err = system->loadBankFile(fullpath.c_str(), FMOD_STUDIO_LOAD_BANK_NORMAL, &bnk);
							if (err == FMOD_ERR_FILE_NOTFOUND || err == FMOD_ERR_FILE_BAD) Msg("Error: Failed to load FMOD bank file \"" + fullpath + "\"...");
						}
					}
					delete dir;
				}

				initialized = true;
			}
		}

Another thing to note is that you manually need to update FMOD each frame. I've created my own wrapper update function and call it in my Stage class. This also calls my Initialize function.

		void Update()
		{
			FMODEngine::Initialize();
			if (!initialized) Initialize();
			if (system != NULL) system->update();
		}

Now, that we have the basics in, we first need a listener which was actually the hardest part. At first, I wasn't sure how I can match the location and rotation calculations from Leadwerks to FMOD. I first tried implementing the position and thankfully, FMOD uses the Left-Handed system by default. 

Rotation was a bit difficult for me but grabbing random code from stackoverflow saved me again and pushing the Y rotation 90 degrees made the 1:1 match. 

		void UpdateListenerPosition(Leadwerks::Entity* pOwner)
		{
			if (pOwner == NULL || system == NULL) return;
			Leadwerks::Vec3 entity_position = pOwner->GetPosition(true);

			// Position the listener at the origin
			FMOD_3D_ATTRIBUTES attributes = { { 0 } };
			attributes.position.x = entity_position.x;
			attributes.position.y = entity_position.y;
			attributes.position.z = entity_position.z;

			float radians = Leadwerks::Math::DegToRad(pOwner->GetRotation(true).y + 90);
			float fx = cos(radians);
			float fz = sin(radians);

			attributes.forward.x = -fx;
			attributes.forward.z = fz;

			attributes.up.y = 1.0f;

			system->setListenerAttributes(0, &attributes);
		}

There's nothing to create since a listener is initialized with the system. You just need to update its attributes each frame. I call the above function in my UpdateMatrix hook for my main camera. 

Last, we need a way to play simple sounds. 2D and 3D sounds are defined in the bank file, but to play a sound with no 3D settings, it's as simple as the following.

		void EmitSound(const std::string& pszSoundEvent)
		{
			FMOD::Studio::EventDescription* pDest;
			std::string fulldsp = "event:/" + pszSoundEvent;
			system->getEvent(fulldsp.c_str(), &pDest);

			FMOD::Studio::EventInstance* instance;
			pDest->createInstance(&instance);

			instance->start();
			instance->release();
		}

And to play a sound at an entity's location, this has been working for me so far.

		void EmitSoundFromEntity(const std::string& pszSoundEvent, ENTITY_CLASS pEntity)
		{
			FMOD::Studio::EventDescription* pDest;
			std::string fulldsp = "event:/" + pszSoundEvent;
			system->getEvent(fulldsp.c_str(), &pDest);

			FMOD::Studio::EventInstance* instance;
			pDest->createInstance(&instance);

			FMOD_3D_ATTRIBUTES attributes = { { 0 } };
			attributes.forward.z = 1.0f;
			attributes.up.y = 1.0f;

			attributes.position.x = pEntity->GetPosition(true).x;
			attributes.position.y = pEntity->GetPosition(true).y;
			attributes.position.z = pEntity->GetPosition(true).z;

			instance->set3DAttributes(&attributes);
			instance->start();
			instance->release();
		}

All you need to pass is the name of the event. If you decided to use subfolders, you'll need to include them in the parameter. It acts like a virtual directory.

For anything with more control (such as loops, playback, etc) we're going to need our own Source/Speaker class.  This class just stores the event instance and acts like the standard Source/Speaker class.  I also support both the engine's OpenAL implementation and FMOD via a switch, but my actors only care about this game speaker class and the EmitSound functions. 

class GameSpeaker
{
#ifdef USE_FMOD
	FMOD::Studio::EventDescription* m_pDest;
	FMOD::Studio::EventInstance* m_pEventInstance;
	int m_iLength;
#endif // USE_FMOD

	Leadwerks::Source* m_pOpenALSpeaker;

protected:
	Leadwerks::Vec3 m_vPosition;
	int m_iState;

public:
	GameSpeaker();
	~GameSpeaker();

	void Play();
	void Stop();
	void Pause();
	void Resume();
	void Reset();

	void SetVolume(const float flVolume);
	void SetPitch(const float flPitch);
	void SetPosition(const Leadwerks::Vec3& vPosition);
	void SetTime(const float flTime);

	const float GetVolume();
	const float GetPitch();
	const float GetTime();
	const int GetState();
	const float GetLength();
	const Leadwerks::Vec3& GetPosition();

	void BuildSpeaker(const std::string& pszEventName);

	static GameSpeaker* Create(const std::string& pszEventName);

	static const int Stopped;
	static const int Playing;
	static const int Paused;
};

extern std::shared_ptr<GameSpeaker> CreateGameSpeaker(const std::string& pszEventName);

I still have very minor things to work out, but overall, this has been a success. This also could be implemented exactly the same way in Ultra Engine if you're reading this in the future. 

Basic FMOD Implementation in Cyclone - Work in Progress - Ultra Engine Community - Game Engine for VR

  • Like 2
  • Thanks 1
 Share

0 Comments


Recommended Comments

There are no comments to display.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...