Interactive Multi-State Engine
C++. I wrote this audio engine with FMOD API. Its play state algorithm in the interactive music class is an open nested loop system in which its update function is continuously called. It uses two sub music channels sharing the states by cross-fade. Cross-fades occur by means of embedded thread objects. State containers and segments can be set by order, loop, and randomness. It was set for millisecond workloads by fps assumption. It will not work under one millisecond workload. It is good for full random containers. I will improve the interactive music section by reducing bottlenecks later on. You can download MultiStateInteractive.zip file and run it. Diagram shows class dependency and interactive class state structure.
Let’s check its AudioEngine header and class files:
// 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(float const _volume); void SetMusicChannelVolume(float const _volume); void Load(int const _fileID, std::string const & _path, std::string const & _name); void LoadFromResource(int const _resourceID, LPCTSTR _resourceType, std::string const & _name); void ShrinkAudioEngineMaps(); int MusicChannelActivity(); void AssignChannel(); void CrossFade(FMOD::Channel* _channelIn, FMOD::Channel* _channelOut, int const _millisec); void PlayMusic(int const _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 #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 ( float const _volume ) { ptrMasterGroupChannel->setVolume(_volume); } //////////////////////////////////////////////////////////////////////// void AudioEngine::SetMusicChannelVolume ( float const _volume ) { ptrMusicGroupChannel->setVolume(_volume); } //////////////////////////////////////////////////////////////////////// void AudioEngine::Load ( int const _fileID, std::string const & _path, std::string const & _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 ( int const _resourceID, LPCTSTR _resourceType, std::string const & _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; } } //////////////////////////////////////////////////////////////////////// void AudioEngine::CrossFade ( FMOD::Channel* _channelIn, FMOD::Channel* _channelOut, int const _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 ( int const _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(); }
I implemented Music class as an interface for Interactive and Background music classes. Music class is NOT inherited from AudioEngine class because they are totally different objects in the aspect of their structures. However, they carry similar behaviours such as play function. Let’see its header file, no need to see its cpp file because of full pure virtual functions:
// Music.h #pragma once #include "AudioEngine.h" class Music { public: virtual ~Music() = default; virtual void Play(AudioEngine & _obj, int const _fileID) = 0; virtual void CallForPlay(AudioEngine & _obj, int const _fileID) = 0; virtual void UpdateMusic(AudioEngine & _obj, float const _elapsed) = 0; };
Interactive members are containers, container members, and active usage playState members. Random generator works for each segment in the container. playState method’s algorithm is a complicated little bit. When update function sends calls continuously, the algorithm checks passing duration by comparing to segment duration. Then, it takes the call inside once it is very close to be completed. Inside embedded thread lambda expression takes the call to the parallel processing not to disturb the workload. Let’s see the header and cpp files:
// Interactive.h #pragma once #include "Music.h" #include <vector> #include <list> #include <random> class InteractiveState final : public Music { public: InteractiveState(); ~InteractiveState() = default; void SetMaxStateLoop(int const _maxStateLoopAmount = 4); void CreateContainer(bool const _randomContainer = false, int const _containerLoopAmount = 1); void SetSegmentData(int const _segmentID, int const _containerIndex, int const _segmentLoopAmount, int const _segmentDuration); void ShrinkInteractiveContainers(); void SegmentIndexGenerator(); void Play(AudioEngine & _obj, int const _fileID = 0) override; void CallForPlay(AudioEngine & _obj, int const _fileID = 0) override; void UpdateMusic(AudioEngine & _obj, float const _elapsed) override; void DefaultValues(); private: //State members int m_maxStateLoopAmount; int m_maxContainerIndex; //Segment (music) members int m_segmentLoopAmount; int m_segmentDuration; //Play state members in active usage float m_elapsed; float m_duration; bool m_playAction; int m_currentStateLoop; int m_currentContainerIndex; int m_currentContainerLoop; int m_currentSegmentLoop; //Creating segment music data for each container's segment data store struct SegmentData { int m_segmentID = 0; int m_segmentLoopAmount = 0; 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::vector<ContainerData> Containers; //For segments to be indexed by random generator std::list<int> SegmentIndexStore; };
// Interactive.cpp #include "Interactive.h" InteractiveState::InteractiveState() : m_maxStateLoopAmount(0), m_maxContainerIndex(0), m_segmentLoopAmount(0), m_segmentDuration(0), m_elapsed(0.0f), m_duration(0.0f), m_playAction(false), m_currentStateLoop(0), m_currentContainerIndex(0), m_currentContainerLoop(0), m_currentSegmentLoop(0) { } //////////////////////////////////////////////////////////////////////// void InteractiveState::SetMaxStateLoop ( int const _maxStateLoopAmount ) { //State max loop amount should be bigger than 0 m_maxStateLoopAmount = _maxStateLoopAmount; } //////////////////////////////////////////////////////////////////////// void InteractiveState::CreateContainer ( bool const _randomContainer, int const _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 ( int const _segmentID, int const _containerIndex, int const _segmentLoopAmount, int const _segmentDuration ) { auto segmentData = SegmentData(); segmentData.m_segmentID = _segmentID; segmentData.m_segmentLoopAmount = _segmentLoopAmount; segmentData.m_segmentDuration = _segmentDuration; Containers[_containerIndex].Segments.push_back(segmentData); } //////////////////////////////////////////////////////////////////////// void InteractiveState::ShrinkInteractiveContainers ( ) { //For potentially less memory consumption for (auto & containerData : Containers) { containerData.Segments.shrink_to_fit(); } Containers.shrink_to_fit(); } //////////////////////////////////////////////////////////////////////// void InteractiveState::SegmentIndexGenerator ( ) { bool const isRandom = Containers[m_currentContainerIndex].b_randomContainer; int const totalSegments = Containers[m_currentContainerIndex].Segments.size(); if (isRandom) { std::uniform_int_distribution<unsigned int> distribution(0, totalSegments - 1); unsigned int seed = static_cast<unsigned int>(time(0)); //Calls to fill the segment index container for (int i = 0; i < totalSegments; ++i) { std::mt19937 engine(++seed); int randomIndex = distribution(engine); bool ifExist = false; for (auto it = SegmentIndexStore.begin(); it != SegmentIndexStore.end(); ++it) { if (*it == randomIndex) { ifExist = true; break; } } if (ifExist) { randomIndex = 0; while (true) { ifExist = false; for (auto it = SegmentIndexStore.begin(); it != SegmentIndexStore.end(); ++it) { if (*it == randomIndex) { ifExist = true; break; } } if (!ifExist) { SegmentIndexStore.push_back(randomIndex); break; } randomIndex++; } } else { SegmentIndexStore.push_back(randomIndex); } } } else { for (int i = 0; i < totalSegments; ++i) { SegmentIndexStore.push_back(i); } } } //////////////////////////////////////////////////////////////////////// void InteractiveState::Play ( AudioEngine & _obj, int const _fileID ) { //Checking current state loop number if (m_currentStateLoop < m_maxStateLoopAmount) { //Checking current container if (m_currentContainerIndex < m_maxContainerIndex) { //Playing container loops if (m_currentContainerLoop < Containers[m_currentContainerIndex].m_containerLoopAmount) { //Setting index store if (SegmentIndexStore.size() == 0) { SegmentIndexGenerator(); } //Extracting segment info and playing the segment //m_playAction works as a valve !!! if (m_playAction) { int const segmentIndex = SegmentIndexStore.front(); auto const & segmentData = Containers[m_currentContainerIndex].Segments[segmentIndex]; m_segmentLoopAmount = segmentData.m_segmentLoopAmount; m_segmentDuration = segmentData.m_segmentDuration; m_duration = 0.0f; m_currentSegmentLoop++; m_playAction = false; //valve is close _obj.PlayMusic(segmentData.m_segmentID); } //Checking segment loop: //Just before segment duration to be completed, it will go inside earlier //Then, thread "wait" function will deal with it for elapsed time compensation if (m_duration + m_elapsed > m_segmentDuration) { //If m_duration is higher than m_segmentDuration somehow, there will be delay //Programme will continue to run for stability by accepting delay if it is case std::thread wait([=]() mutable { std::this_thread::sleep_for(std::chrono::milliseconds(static_cast<int>(m_elapsed))); //If triggerPlay is called meanwhile, true condition shouldn't repeat if (m_playAction == false) { m_playAction = true; //valve is open } }); wait.detach(); //After waiting done, check the next segment loop if there is if (SegmentIndexStore.size() > 0 && m_currentSegmentLoop >= m_segmentLoopAmount) { SegmentIndexStore.pop_front(); m_currentSegmentLoop = 0; } //Going to next container loop when segment plays are completed if (SegmentIndexStore.size() == 0) { m_currentContainerLoop++; } m_duration = 0.0f; } } else { //Move to next container 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::CallForPlay ( AudioEngine & _obj, int const _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, float const _elapsed ) { //For playing current state m_duration += _elapsed; m_elapsed = _elapsed; Play(_obj, 0); } //////////////////////////////////////////////////////////////////////// void InteractiveState::DefaultValues ( ) { m_playAction = false; m_currentStateLoop = 0; m_currentContainerIndex = 0; m_currentContainerLoop = 0; m_currentSegmentLoop = 0; SegmentIndexStore.clear(); }
Let’s see other derived class from Music class, called Background. It is for background tracks, and I might add some features later on:
// 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, int const _fileID) override; void CallForPlay(AudioEngine & _obj, int const _fileID) override; void UpdateMusic(AudioEngine & _obj, float const _elapsed) override; };
// Background.cpp #include "Background.h" void Background::Play(AudioEngine & _obj, int _fileID) { _obj.AssignChannel(); _obj.PlayMusic(_fileID); } void Background::CallForPlay(AudioEngine & _obj, int _fileID) { //If both channels are active, refuse for the call if (_obj.MusicChannelActivity() != 1) { Play(_obj, _fileID); } } void Background::UpdateMusic(AudioEngine & _obj, float _elapsed) { }
You can see main cpp file: loading and setting files, creating state and containers, etc. Here, there are background, tension, and battle states. Tension and battle states are interactive. For example, battle state has two containers, first one plays normal, other plays randomly. First one has one segment with just once. The other one has 3 segments playing randomly, and each plays 2 times. I used SFML library for press key option to send a command into playState when while-loop is active. Moreover, I put an artificial workload (15-35ms) inside the loop to be similar to a game loop model. I used Query Performance Counter to measure the elapsed time because I developed this programme in Windows. Meanwhile, the random work value can be send as an argument to the update function parameter as done here for testing purposes.
// 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)", "battle1"); 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", "- battle1"); audio.LoadFromResource(202, "MP3", "- battle2"); audio.LoadFromResource(203, "MP3", "- battle3"); audio.LoadFromResource(204, "MP3", "- battle4"); audio.LoadFromResource(205, "MP3", "- tension1"); audio.LoadFromResource(206, "MP3", "- tension2"); audio.LoadFromResource(207, "MP3", "- tension3"); audio.LoadFromResource(208, "MP3", "- tension4"); audio.LoadFromResource(209, "MP3", "- peaceful"); //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); //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) 2024" << "\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"; //======================================================================// //Setting timer to measure elapsed time in the loop //For Windows applications LARGE_INTEGER frequency; //ticks per second LARGE_INTEGER start, end; //ticks //For keyword commands; it works as a valve std::string last_command = ""; //START OF THE LOOP while (true) { QueryPerformanceFrequency(&frequency); QueryPerformanceCounter(&start); //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); QueryPerformanceCounter(&start); } } else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Num2)) { if (last_command != "Tension" && audio.MusicChannelActivity() != 1) { last_command = "Tension"; ptrMusic = &tension; tension.CallForPlay(audio); QueryPerformanceCounter(&start); } } else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Num3)) { if (last_command != "Battle" && audio.MusicChannelActivity() != 1) { last_command = "Battle"; ptrMusic = &battle; battle.CallForPlay(audio); QueryPerformanceCounter(&start); } } 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; } //======================================================================// //Creating work load between 15-35 milliseconds for testing int const workLoad = 15 + static_cast<int>(rand()) % 21; std::this_thread::sleep_for(std::chrono::milliseconds(workLoad)); //Time calculation QueryPerformanceCounter(&end); float const elapsed = (end.QuadPart - start.QuadPart) * 1000.0f / frequency.QuadPart; //Checking and updating continously if (ptrMusic != nullptr) { ptrMusic->UpdateMusic(audio, elapsed); } 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; }