Interactive Multi-State (Audio 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
#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; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
#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(); } |
1 2 3 4 5 6 7 8 9 10 11 12 |
#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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
#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; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 |
#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(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#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; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
#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; } |