Interactive Multi-State Engine
Key Features:
- Done with FMOD API, diagram shows class dependency and interactive class state structure
- State containers and segments can be set by order, loop, and randomness
- Interactive music class is an open nested loop style, no buffering
- Uses two interchangeable sub music channels sharing the states by cross-fade
- Cross-fades occur by means of embedded thread objects
- SFML library is being used for press key options to send commands
Improvement Areas:
- A thread work might be used for “SegmentIndexToPlayGenerator” if the system is very busy
- General refactoring
Download Link:
- MultiStateInteractiveTest.zip (zipped)
- Composed 8 music segments for 2 different states and 1 music track for background ambience
AudioEngine.h
// AudioEngine.h #pragma once #include <iostream> #include <fmod.hpp> #include <fmod_errors.h> #include <string> #include <Windows.h> #include <unordered_map> #include <thread> #include <chrono> class AudioEngine { public: AudioEngine(); AudioEngine(const AudioEngine&) = delete; ~AudioEngine(); AudioEngine &operator=(const AudioEngine&) = delete; void SetMasterChannelVolume(const float _volume); void SetMusicChannelVolume(const float _volume); void Load(const int _fileID, const std::string & _path, const std::string & _name); void LoadFromResource(const int _resourceID, LPCTSTR _resourceType, const std::string & _name); void ShrinkAudioEngineMaps(); int MusicChannelActivity(); void AssignChannel(); FMOD::Channel* GetActiveChannel(); void CrossFade(FMOD::Channel* _channelIn, FMOD::Channel* _channelOut, const int _millisec); void PlayMusic(const int _fileID); void Stop(); void Update(); private: //Declaring system object FMOD::System* ptrSystem; //Declaring master and music group channels FMOD::ChannelGroup* ptrMasterGroupChannel; FMOD::ChannelGroup* ptrMusicGroupChannel; //Declaring music sub channels to be used continuously FMOD::Channel* ptrMusicChannel1; FMOD::Channel* ptrMusicChannel2; //Pointer to channels for assigning them FMOD::Channel** pptrChannel; //For checking if interactive sub channel is playing or not bool m_ch1_Activity; bool m_ch2_Activity; //Declaring map for audio files std::unordered_map<int, FMOD::Sound*> MusicMap; //Creating ID-name match map for file information std::unordered_map<int, std::string> NameIDMap; };
AudioEngine.cpp
// AudioEngine.cpp #pragma once #include "AudioEngine.h" AudioEngine::AudioEngine() : ptrSystem(nullptr), ptrMasterGroupChannel(nullptr), ptrMusicGroupChannel(nullptr), ptrMusicChannel1(nullptr), ptrMusicChannel2(nullptr), pptrChannel(nullptr), m_ch1_Activity(false), m_ch2_Activity(false) { //Initialising the system FMOD::System_Create(&ptrSystem); ptrSystem->init(100, FMOD_INIT_NORMAL, nullptr); //Creating group channels ptrSystem->createChannelGroup("Master", &ptrMasterGroupChannel); ptrSystem->createChannelGroup("Music", &ptrMusicGroupChannel); //Assigning music group channel to the master group channel ptrMasterGroupChannel->addGroup(ptrMusicGroupChannel); //Music->Master } //////////////////////////////////////////////////////////////////////// AudioEngine::~AudioEngine() { //Releasing sounds and clear the containers for (auto & sound_file : MusicMap) { sound_file.second->release(); } MusicMap.clear(); //Releasing the root of the system ptrSystem->release(); } //////////////////////////////////////////////////////////////////////// void AudioEngine::SetMasterChannelVolume ( const float _volume ) { ptrMasterGroupChannel->setVolume(_volume); } //////////////////////////////////////////////////////////////////////// void AudioEngine::SetMusicChannelVolume ( const float _volume ) { ptrMusicGroupChannel->setVolume(_volume); } //////////////////////////////////////////////////////////////////////// void AudioEngine::Load ( const int _fileID, const std::string & _path, const std::string & _name ) { FMOD::Sound* ptrSound; ptrSystem->createSound(_path.c_str(), FMOD_DEFAULT, nullptr, &ptrSound); //Or ptrSystem->createStream(path.c_str(), FMOD_DEFAULT, nullptr, &ptrSound); //It depends on the situation, streaming is better for big size audio files //Checking if the file does exist or not against any typing error unsigned int length = 0; ptrSound->getLength(&length, FMOD_TIMEUNIT_MS); if (length == 0) { std::cout << "Serious problem occured at loading stage..." << "\n"; std::cout << "Please check this sound file, it does not exist:" << "\n"; std::cout << _path << "\n"; std::cout << "" << "\n"; ptrSound->release(); throw; } //Adding audio objects and setting their names MusicMap.emplace(_fileID, ptrSound); NameIDMap.emplace(_fileID, _name); } //////////////////////////////////////////////////////////////////////// void AudioEngine::LoadFromResource ( const int _resourceID, LPCTSTR _resourceType, const std::string & _name ) { HRSRC rsrc = FindResource(NULL, MAKEINTRESOURCE(_resourceID), _resourceType); HGLOBAL handle = LoadResource(NULL, rsrc); DWORD audio_size = SizeofResource(NULL, rsrc); LPVOID audio_data = LockResource(handle); FMOD_CREATESOUNDEXINFO AudioInfo; memset(&AudioInfo, 0, sizeof(FMOD_CREATESOUNDEXINFO)); AudioInfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO); AudioInfo.length = static_cast<unsigned int>(audio_size); FMOD::Sound* ptrResource; ptrSystem->createSound(static_cast<const char*>(audio_data), FMOD_OPENMEMORY, &AudioInfo, &ptrResource); //Adding loaded resource file and setting their names MusicMap.emplace(_resourceID, ptrResource); NameIDMap.emplace(_resourceID, _name); } //////////////////////////////////////////////////////////////////////// void AudioEngine::ShrinkAudioEngineMaps ( ) { //For potentially less memory consumption std::unordered_map<int, FMOD::Sound*>(MusicMap).swap(MusicMap); std::unordered_map<int, std::string>(NameIDMap).swap(NameIDMap); } //////////////////////////////////////////////////////////////////////// int AudioEngine::MusicChannelActivity ( ) { ptrMusicChannel1->isPlaying(&m_ch1_Activity); ptrMusicChannel2->isPlaying(&m_ch2_Activity); //Both channels are active -> return 1 //None of them is active -> return 2 //Only one channel is active -> return 3 if (m_ch1_Activity && m_ch2_Activity) { return 1; } else if (!m_ch1_Activity && !m_ch2_Activity) { return 2; } else { return 3; } } //////////////////////////////////////////////////////////////////////// void AudioEngine::AssignChannel ( ) { ptrMusicChannel1->isPlaying(&m_ch1_Activity); ptrMusicChannel2->isPlaying(&m_ch2_Activity); if ((!m_ch1_Activity && !m_ch2_Activity) || (!m_ch1_Activity && m_ch2_Activity)) { //If channels are empty or the second one is active, take the first one pptrChannel = &ptrMusicChannel1; } else if (m_ch1_Activity && !m_ch2_Activity) { //If the first one is active, take the second one pptrChannel = &ptrMusicChannel2; } } //////////////////////////////////////////////////////////////////////// FMOD::Channel* AudioEngine::GetActiveChannel() { return *pptrChannel; } //////////////////////////////////////////////////////////////////////// void AudioEngine::CrossFade ( FMOD::Channel* _channelIn, FMOD::Channel* _channelOut, const int _millisec ) { //channel_in is the pasive channel which will play soon, it will fade in //channel_out is the active channel which is currently playing, it will fade away float channelVolumeIn = 0.0f; float channelVolumeOut = 1.0f; _channelIn->setVolume(0.0f); _channelOut->setVolume(1.0f); while (channelVolumeIn < 1.0f && channelVolumeOut > 0.0f) { //1-(x^2.5) crossfade calculation with 0.15 base value, pow(0.15f, 2.5f) //Increment-decrement value is approximately 0.0087 per decided millisecs channelVolumeIn += 0.0087f; channelVolumeOut -= 0.0087f; _channelIn->setVolume(channelVolumeIn); _channelOut->setVolume(channelVolumeOut); std::this_thread::sleep_for(std::chrono::milliseconds(_millisec)); if (channelVolumeOut < 0.01f && channelVolumeIn > 0.99f) { _channelOut->setVolume(0.0f); //For precision _channelIn->setVolume(1.0f); _channelOut->stop(); break; } _channelIn->getVolume(&channelVolumeIn); _channelOut->getVolume(&channelVolumeOut); } } //////////////////////////////////////////////////////////////////////// void AudioEngine::PlayMusic ( const int _fileID ) { auto musicFile = MusicMap.find(_fileID); ptrSystem->playSound(musicFile->second, ptrMusicGroupChannel, false, pptrChannel); //Progressing the cross-fade if there is a new play call if (MusicChannelActivity() == 1) { if (pptrChannel == &ptrMusicChannel1) { std::thread crossState(&AudioEngine::CrossFade, this, ptrMusicChannel1, ptrMusicChannel2, 15); crossState.detach(); } else if (pptrChannel == &ptrMusicChannel2) { std::thread crossState(&AudioEngine::CrossFade, this, ptrMusicChannel2, ptrMusicChannel1, 15); crossState.detach(); } } //Segment information std::cout << NameIDMap.find(_fileID)->second << "\n"; } //////////////////////////////////////////////////////////////////////// void AudioEngine::Stop ( ) { float channelVolume; ptrMusicGroupChannel->getVolume(&channelVolume); if (channelVolume < 0.02f) { //Minimum base value against a locked loop channelVolume = 0.02f; } while (channelVolume > 0.0f) { //1-(x^2.5) crossfade calculation with 0.15 base value, pow(0.15f, 2.5f) //Increment-decrement value is approximately 0.0087 per 15 millisecs channelVolume -= 0.0087f; ptrMusicGroupChannel->setVolume(channelVolume); std::this_thread::sleep_for(std::chrono::milliseconds(15)); if (channelVolume < 0.01f) { ptrMusicGroupChannel->setVolume(0.0f); //For precision ptrMusicChannel1->stop(); ptrMusicChannel2->stop(); break; } ptrMusicGroupChannel->getVolume(&channelVolume); } ptrMusicGroupChannel->setVolume(1.0f); } //////////////////////////////////////////////////////////////////////// void AudioEngine::Update ( ) { ptrSystem->update(); }
Music.h
// Music.h #pragma once #include "AudioEngine.h" class Music { public: virtual ~Music() = default; virtual void Play(AudioEngine & _obj, const int _fileID) = 0; virtual void CallToPlay(AudioEngine & _obj, const int _fileID) = 0; virtual void UpdateMusic(AudioEngine & _obj) = 0; };
Interactive.h
// Interactive.h #pragma once #include "Music.h" #include <vector> #include <list> #include <random> #include <thread> class InteractiveState final : public Music { public: InteractiveState(); ~InteractiveState() = default; void SetMaxStateLoop(const int _maxStateLoopAmount = 4); void CreateContainer(const bool _randomContainer = false, const int _containerLoopAmount = 1); void SetSegmentData(const int _segmentID, const int _containerIndex, const int _segmentLoopAmount, const int _segmentDuration); void SetContainerSegmentOrder(const int _containerIndex); void ShrinkInteractiveContainers(); void SegmentIndexToPlayGenerator(const int _containerIndex); void Play(AudioEngine & _obj, const int _fileID = 0) override; void CallToPlay(AudioEngine & _obj, const int _fileID = 0) override; void UpdateMusic(AudioEngine & _obj) override; void DefaultValues(); private: //State members int m_maxStateLoopAmount; int m_maxContainerIndex; //Segment (music) members int m_segmentLoopAmount; unsigned int m_segmentDuration; //Play state members in active usage bool m_playAction; int m_currentStateLoop; int m_currentContainerIndex; int m_currentContainerLoop; int m_currentSegmentLoop; unsigned int m_currentSegmentPosition; //Creating segment music data for each container's segment data store struct SegmentData { int m_segmentID = 0; int m_segmentLoopAmount = 0; unsigned int m_segmentDuration = 0; }; //Creating container store for states which will be triggered in game scenario struct ContainerData { bool b_randomContainer = false; int m_containerLoopAmount = 0; std::vector<SegmentData> Segments; std::list<int> SegmentIndicesToPlay; }; std::vector<ContainerData> Containers; };
Interactive.cpp
// Interactive.cpp #pragma once #include "Interactive.h" InteractiveState::InteractiveState() : m_maxStateLoopAmount(0), m_maxContainerIndex(0), m_segmentLoopAmount(0), m_segmentDuration(0), m_playAction(false), m_currentStateLoop(0), m_currentContainerIndex(0), m_currentContainerLoop(0), m_currentSegmentLoop(0), m_currentSegmentPosition(0) { } //////////////////////////////////////////////////////////////////////// void InteractiveState::SetMaxStateLoop ( const int _maxStateLoopAmount ) { //State max loop amount should be bigger than 0 m_maxStateLoopAmount = _maxStateLoopAmount; } //////////////////////////////////////////////////////////////////////// void InteractiveState::CreateContainer ( const bool _randomContainer, const int _containerLoopAmount ) { //Containers play in order //In random type container, segments will play in random auto containerData = ContainerData(); containerData.b_randomContainer = _randomContainer; containerData.m_containerLoopAmount = _containerLoopAmount; Containers.push_back(containerData); //Setting interactive state size at the same time m_maxContainerIndex = Containers.size(); } //////////////////////////////////////////////////////////////////////// void InteractiveState::SetSegmentData ( const int _segmentID, const int _containerIndex, const int _segmentLoopAmount, const int _segmentDuration ) { auto segmentData = SegmentData(); segmentData.m_segmentID = _segmentID; segmentData.m_segmentLoopAmount = _segmentLoopAmount; segmentData.m_segmentDuration = _segmentDuration; Containers[_containerIndex].Segments.push_back(segmentData); } //////////////////////////////////////////////////////////////////////// void InteractiveState::SetContainerSegmentOrder ( const int _containerIndex ) { SegmentIndexToPlayGenerator(_containerIndex); } //////////////////////////////////////////////////////////////////////// void InteractiveState::ShrinkInteractiveContainers ( ) { //For potentially less memory consumption for (auto & containerData : Containers) { containerData.Segments.shrink_to_fit(); } Containers.shrink_to_fit(); } //////////////////////////////////////////////////////////////////////// void InteractiveState::SegmentIndexToPlayGenerator ( const int _containerIndex ) { const bool isRandom = Containers[_containerIndex].b_randomContainer; const int totalSegments = Containers[_containerIndex].Segments.size(); auto & segmentIndicesToPlay = Containers[_containerIndex].SegmentIndicesToPlay; segmentIndicesToPlay.clear(); if (isRandom) { //Create a vector of indices from 0 to totalSegments - 1 std::vector<int> indices(totalSegments); for (int i = 0; i < totalSegments; ++i) { indices[i] = i; } //Shuffle the vector using a random engine unsigned int seed = static_cast<unsigned int>(time(0)); std::mt19937 engine(seed); std::shuffle(indices.begin(), indices.end(), engine); //Copy this temporary vector into the segment indices list for (const int i : indices) { segmentIndicesToPlay.push_back(i); } } else { // If not random, just insert the indices in order for (int i = 0; i < totalSegments; ++i) { segmentIndicesToPlay.push_back(i); } } } //////////////////////////////////////////////////////////////////////// void InteractiveState::Play ( AudioEngine & _obj, const int _fileID ) { //Checking current state loop number if (m_currentStateLoop < m_maxStateLoopAmount) { //Checking current container if (m_currentContainerIndex < m_maxContainerIndex) { //Playing container loops auto & activeContainer = Containers[m_currentContainerIndex]; if (m_currentContainerLoop < activeContainer.m_containerLoopAmount) { //Extracting segment info and playing the segment auto & segmentIndicesToPlay = activeContainer.SegmentIndicesToPlay; if (m_playAction) //m_playAction works as a valve !!! { const int segmentIndex = segmentIndicesToPlay.front(); const auto & segmentData = activeContainer.Segments[segmentIndex]; m_segmentLoopAmount = segmentData.m_segmentLoopAmount; m_segmentDuration = segmentData.m_segmentDuration; m_currentSegmentLoop++; m_playAction = false; //valve is close _obj.PlayMusic(segmentData.m_segmentID); } //Just before segment duration to be completed, it will go inside earlier //Then, thread "sleep for" will wait for the time difference //Let's make the threshold 100 ms _obj.GetActiveChannel()->getPosition(&m_currentSegmentPosition, FMOD_TIMEUNIT_MS); if (m_currentSegmentPosition >= m_segmentDuration - 100) { //If position is higher than m_segmentDuration somehow, there will be delay //Programme will continue to run for stability by accepting delay if it is case if (m_currentSegmentPosition < m_segmentDuration) { std::this_thread::sleep_for ( std::chrono::milliseconds(m_segmentDuration - m_currentSegmentPosition) ); } m_playAction = true; //valve is open //If the current segment's job is done, go to the next segment to play if (m_currentSegmentLoop >= m_segmentLoopAmount) { segmentIndicesToPlay.pop_front(); m_currentSegmentLoop = 0; } //Going to next container loop when segment plays are completed if (segmentIndicesToPlay.empty()) { m_currentContainerLoop++; } } } else { //Setting segment indices to play for the current container after it's turn is done SegmentIndexToPlayGenerator(m_currentContainerIndex); //If segment numbers are big enough, we can use thread to calculate indices /* { const int containerIndex = m_currentContainerIndex; // Against index change during the execution std::thread playOrder(&InteractiveState::SegmentIndexToPlayGenerator, this, containerIndex); playOrder.detach(); } */ //Move to next container in its state after previous container finished its all loops m_currentContainerIndex++; m_currentContainerLoop = 0; } } else { m_currentStateLoop++; m_currentContainerIndex = 0; //Back to the first container } } else { m_currentStateLoop = 0; _obj.Stop(); std::cout << "- Current state play loops are completed" << "\n"; } } //////////////////////////////////////////////////////////////////////// void InteractiveState::CallToPlay ( AudioEngine & _obj, const int _fileID ) { //If both channels are active, refuse for the call if (_obj.MusicChannelActivity() != 1) { //First play for new coming state _obj.AssignChannel(); m_playAction = true; Play(_obj); } } //////////////////////////////////////////////////////////////////////// void InteractiveState::UpdateMusic ( AudioEngine & _obj ) { Play(_obj, 0); } //////////////////////////////////////////////////////////////////////// void InteractiveState::DefaultValues ( ) { m_playAction = false; m_currentStateLoop = 0; m_currentContainerIndex = 0; m_currentContainerLoop = 0; m_currentSegmentLoop = 0; m_currentSegmentPosition = 0; }
Background.h
// Background.h #pragma once #include "Music.h" class Background final : public Music { public: Background() = default; Background(const Background&) = delete; ~Background() = default; Background &operator=(const Background&) = delete; void Play(AudioEngine & _obj, const int _fileID) override; void CallToPlay(AudioEngine & _obj, const int _fileID) override; void UpdateMusic(AudioEngine & _obj) override; };
Background.cpp
// Background.cpp #pragma once #include "Background.h" void Background::Play(AudioEngine & _obj, const int _fileID) { _obj.AssignChannel(); _obj.PlayMusic(_fileID); } void Background::CallToPlay(AudioEngine & _obj, const int _fileID) { //If both channels are active, refuse for the call if (_obj.MusicChannelActivity() != 1) { Play(_obj, _fileID); } } void Background::UpdateMusic(AudioEngine & _obj) { }
Main.cpp
// Main.cpp #pragma once #include "Interactive.h" #include "Background.h" #include <SFML/Window.hpp> int main() { //Creating main system object dealing with system, channels, load, etc. AudioEngine audio; //Initialising channel group volumes audio.SetMasterChannelVolume(1.0f); audio.SetMusicChannelVolume(1.0f); /*Loading interactive music files example: if their paths were used audio.Load(201, R"(C:\Data\battle1.mp3)", "Battle State -> Segment 1"); Segment ID, Container index, Segment index, Segment loop, Segment duration: battle.setSegment(201, 0, 0, 1, 4000);*/ //Loading interactive music files from resource //Setting Resource ID, Resource type, File name audio.LoadFromResource(201, "MP3", "- Battle State -> Segment 1"); audio.LoadFromResource(202, "MP3", "- Battle State -> Segment 2"); audio.LoadFromResource(203, "MP3", "- Battle State -> Segment 3"); audio.LoadFromResource(204, "MP3", "- Battle State -> Segment 4"); audio.LoadFromResource(205, "MP3", "- Tension State -> Segment 1"); audio.LoadFromResource(206, "MP3", "- Tension State -> Segment 2"); audio.LoadFromResource(207, "MP3", "- Tension State -> Segment 3"); audio.LoadFromResource(208, "MP3", "- Tension State -> Segment 4"); audio.LoadFromResource(209, "MP3", "- Background Ambience -> Peaceful track"); //Creating background music state Background background; //Creating interactive music states InteractiveState battle; InteractiveState tension; //Pointer to music subclass objects Music *ptrMusic = nullptr; //SETTING INTERACTIVE STATE OBJECTS //State max loop amount (default = 4) battle.SetMaxStateLoop(); tension.SetMaxStateLoop(); //Deciding how many containers would be in the interactive state //Container type (default = false), Container loop amount (default = 1) battle.CreateContainer(); battle.CreateContainer(true); //Segments will play in random order tension.CreateContainer(); tension.CreateContainer(true); //Assigning the segments into the containers with relevant data //Segment ID, Container index, Segment loop, Segment duration(ms) battle.SetSegmentData(201, 0, 1, 4000); battle.SetSegmentData(202, 1, 1, 4000); battle.SetSegmentData(203, 1, 2, 4000); battle.SetSegmentData(204, 1, 2, 4000); tension.SetSegmentData(205, 0, 1, 4000); tension.SetSegmentData(206, 0, 2, 4000); tension.SetSegmentData(207, 1, 1, 4000); tension.SetSegmentData(208, 1, 1, 4000); //Setting segment play order in the containers (random or not) //Container index battle.SetContainerSegmentOrder(0); battle.SetContainerSegmentOrder(1); tension.SetContainerSegmentOrder(0); tension.SetContainerSegmentOrder(1); //Shrink engine maps and state containers (Optional) audio.ShrinkAudioEngineMaps(); battle.ShrinkInteractiveContainers(); tension.ShrinkInteractiveContainers(); //======================================================================// std::cout << "Multistate Interactive Music Demonstration" << "\n"; std::cout << "Music and programming: Hakan Yurdakul, copyright(c) 2025" << "\n"; std::cout << "" << "\n"; std::cout << "Press 1 to play Background Ambience" << "\n"; std::cout << "Press 2 to play Tension State (interactive)" << "\n"; std::cout << "Press 3 to play Battle State (interactive)" << "\n"; std::cout << "" << "\n"; std::cout << "Press SPACE to stop playing music" << "\n"; std::cout << "Press ESCAPE to exit" << "\n"; std::cout << "" << "\n"; //======================================================================// //For keyword commands; it works as a valve std::string last_command = ""; //START OF THE LOOP while (true) { //Keyboard Commands //======================================================================// if (sf::Keyboard::isKeyPressed(sf::Keyboard::Num1)) { if (last_command != "Background" && audio.MusicChannelActivity() != 1) { last_command = "Background"; ptrMusic = &background; background.Play(audio, 209); } } else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Num2)) { if (last_command != "Tension" && audio.MusicChannelActivity() != 1) { last_command = "Tension"; ptrMusic = &tension; tension.CallToPlay(audio); } } else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Num3)) { if (last_command != "Battle" && audio.MusicChannelActivity() != 1) { last_command = "Battle"; ptrMusic = &battle; battle.CallToPlay(audio); } } else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Space)) { if (last_command != "Stop" && audio.MusicChannelActivity() != 2) { last_command = "Stop"; ptrMusic = nullptr; audio.Stop(); battle.DefaultValues(); tension.DefaultValues(); std::cout << "- Stopped..." << "\n"; } } else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Escape)) { std::cout << "" << "\n"; std::cout << "Thanks for listening..." << "\n"; std::cout << "Preparing to exit..." << "\n"; ptrMusic = nullptr; audio.Stop(); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); break; } //======================================================================// //Checking and updating continously if (ptrMusic != nullptr) { ptrMusic->UpdateMusic(audio); } audio.Update(); //If nothing plays, release the pointer and command key (if assigned) if (audio.MusicChannelActivity() == 2) { ptrMusic = nullptr; last_command = ""; } } //END OF THE LOOP return 0; }