From cc2b62b9e1bb6cc8ac7fd5a9b77c90c94f7a08ae Mon Sep 17 00:00:00 2001 From: Piotr Macha Date: Mon, 27 May 2024 04:46:34 +0200 Subject: [PATCH] feature: invert dependencies and move scheduling code directly to the `MusicTheme`; Engine became only an aggregate of objects, Channel became a command processor --- src/Gothic/BassLoader.hpp | 22 ++- src/Gothic/CMusicSys_Bass.hpp | 6 +- src/Gothic/Externals.hpp | 3 +- src/NH/Bass/Channel.cpp | 200 +++++++++++++--------------- src/NH/Bass/Channel.h | 45 +++---- src/NH/Bass/Command.cpp | 46 +++++-- src/NH/Bass/Command.h | 30 +++-- src/NH/Bass/Engine.cpp | 127 ++++++------------ src/NH/Bass/Engine.h | 60 ++------- src/NH/Bass/EngineCommands.cpp | 41 ++++++ src/NH/Bass/EngineCommands.h | 27 ++++ src/NH/Bass/EventManager.h | 19 +-- src/NH/Bass/IChannel.h | 53 ++++++++ src/NH/Bass/IEngine.h | 29 ++++ src/NH/Bass/MusicTheme.cpp | 168 ++++++++++++++++++++--- src/NH/Bass/MusicTheme.h | 81 +++++------ src/NH/Bass/TransitionScheduler.cpp | 154 --------------------- src/NH/Bass/TransitionScheduler.h | 88 ------------ src/NH/Commons.h | 8 ++ src/NH/HashString.h | 2 + src/NH/Logger.h | 2 +- src/Plugin.cpp | 7 +- 22 files changed, 591 insertions(+), 627 deletions(-) create mode 100644 src/NH/Bass/EngineCommands.cpp create mode 100644 src/NH/Bass/EngineCommands.h create mode 100644 src/NH/Bass/IChannel.h create mode 100644 src/NH/Bass/IEngine.h delete mode 100644 src/NH/Bass/TransitionScheduler.cpp delete mode 100644 src/NH/Bass/TransitionScheduler.h create mode 100644 src/NH/Commons.h diff --git a/src/Gothic/BassLoader.hpp b/src/Gothic/BassLoader.hpp index 841583d..cb690a0 100644 --- a/src/Gothic/BassLoader.hpp +++ b/src/Gothic/BassLoader.hpp @@ -14,14 +14,18 @@ namespace GOTHIC_NAMESPACE zTMus_TransSubType trSubType; }; + enum class BassMusicThemeType : size_t { NORMAL = 0 }; + struct BassMusicTheme { zSTRING Name; zSTRING Zones; + BassMusicThemeType Type = BassMusicThemeType::NORMAL; }; struct BassMusicThemeAudio { + zSTRING Theme; zSTRING Type; zSTRING Filename; zSTRING MidiFile; @@ -56,12 +60,10 @@ namespace GOTHIC_NAMESPACE ForEachClass( "C_MUSICTHEME", [&]() { return m_GothicThemeInstances.emplace_back(new GothicMusicTheme{}); }, - [&](GothicMusicTheme* input, zCPar_Symbol* symbol) - { + [&](GothicMusicTheme* input, zCPar_Symbol* symbol) { std::shared_ptr theme = std::make_shared(symbol->name.ToChar()); theme->SetAudioFile(NH::Bass::AudioFile::DEFAULT, input->fileName.ToChar()); - theme->SetAudioEffects(NH::Bass::AudioFile::DEFAULT, [&](NH::Bass::AudioEffects& effects) - { + theme->SetAudioEffects(NH::Bass::AudioFile::DEFAULT, [&](NH::Bass::AudioEffects& effects) { effects.Loop.Active = input->loop; effects.Volume.Active = true; effects.Volume.Volume = input->vol; @@ -93,16 +95,14 @@ namespace GOTHIC_NAMESPACE ForEachClass( "C_BassMusic_Theme", [&]() { return m_BassThemeInstances.emplace_back(new BassMusicTheme{}); }, - [&](BassMusicTheme* theme, zCPar_Symbol* symbol) - { + [&](BassMusicTheme* theme, zCPar_Symbol* symbol) { // @todo: }); ForEachClass( "C_BassMusic_ThemeAudio", [&]() { return m_BassThemeAudioInstances.emplace_back(new BassMusicThemeAudio{}); }, - [&](BassMusicThemeAudio* theme, zCPar_Symbol* symbol) - { + [&](BassMusicThemeAudio* theme, zCPar_Symbol* symbol) { // @todo: }); } @@ -110,16 +110,14 @@ namespace GOTHIC_NAMESPACE template void ForEachClass(const zSTRING& className, const std::function& classFactory, const std::function& instanceFunc) { - ForEachPrototype(className, [&](int index) - { + ForEachPrototype(className, [&](int index) { T* theme = classFactory(); if (theme) { m_Parser->CreatePrototype(index, theme); } }); - ForEachInstance(className, [&](int index, zCPar_Symbol* symbol) - { + ForEachInstance(className, [&](int index, zCPar_Symbol* symbol) { T* theme = classFactory(); if (theme) { diff --git a/src/Gothic/CMusicSys_Bass.hpp b/src/Gothic/CMusicSys_Bass.hpp index af837f7..59a2eb9 100644 --- a/src/Gothic/CMusicSys_Bass.hpp +++ b/src/Gothic/CMusicSys_Bass.hpp @@ -187,10 +187,12 @@ namespace GOTHIC_NAMESPACE return; } + // Called from an external m_DirectMusic->Stop(); m_ActiveTheme = theme; - log->Warning("This path in CMusicSys_Bass::PlayTheme() shouldn't be possible"); - PlayThemeByScript(theme->name, 0, nullptr); + zSTRING identifier = theme->name; + identifier.Upper(); + m_BassEngine->GetCommandQueue().AddCommand(std::make_shared(identifier.ToChar())); } zCMusicTheme* GetActiveTheme() override diff --git a/src/Gothic/Externals.hpp b/src/Gothic/Externals.hpp index 84a1ed3..1871634 100644 --- a/src/Gothic/Externals.hpp +++ b/src/Gothic/Externals.hpp @@ -110,7 +110,8 @@ namespace GOTHIC_NAMESPACE timePoints.push_back(point); } - NH::Bass::Engine::GetInstance()->GetTransitionScheduler().AddRuleOnBeat(name.ToChar(), interval, timePoints); + // @todo: think about Daedalus API + //NH::Bass::Engine::GetInstance()->GetTransitionScheduler().AddRuleOnBeat(name.ToChar(), interval, timePoints); } catch (const std::invalid_argument& e) { diff --git a/src/NH/Bass/Channel.cpp b/src/NH/Bass/Channel.cpp index 1e7d7a9..88e33da 100644 --- a/src/NH/Bass/Channel.cpp +++ b/src/NH/Bass/Channel.cpp @@ -4,164 +4,144 @@ namespace NH::Bass { - void Channel::Play(const std::shared_ptr& theme, HashString id) + struct OnSyncWhenAudioEndsData + { + HSTREAM Channel; + const std::function& Function; + }; + + struct OnSyncBeforeAudioEndsData + { + HSTREAM Channel; + const std::function& Function; + }; + + Channel::Result Channel::PlayInstant(const AudioFile& audioFile) { if (m_Stream > 0) { BASS_ChannelFree(m_Stream); } - m_Theme = theme; - m_AudioId = id; - const auto& file = theme->GetAudioFile(id); - const auto& effects = theme->GetAudioEffects(id); - - m_Stream = BASS_StreamCreateFile(true, file.Buffer.data(), 0, file.Buffer.size(), 0); + m_Stream = BASS_StreamCreateFile(true, audioFile.Buffer.data(), 0, audioFile.Buffer.size(), 0); if (!m_Stream) { - log->Error("Could not create stream: {0}\n error: {1}\n at {2}:{3}", - m_Theme->GetName(), Engine::ErrorCodeToString(BASS_ErrorGetCode()), - __FILE__, __LINE__); - return; + int code = BASS_ErrorGetCode(); + log->Error("Could not create stream: {0}\n error: {1}\n at {2}:{3}", audioFile.Filename, Engine::ErrorCodeToString(code), __FILE__, __LINE__); + return std::unexpected(Error{ IChannel::ErrorType::INVALID_BUFFER, code, Engine::ErrorCodeToString(code) }); } BASS_ChannelStart(m_Stream); - log->Info("Channel started: {0}", m_Theme->GetName()); + return {}; + } - float targetVolume = effects.Volume.Active ? effects.Volume.Volume : 1.0f; - BASS_ChannelSetAttribute(m_Stream, BASS_ATTRIB_VOL, targetVolume); + void Channel::StopInstant() + { + BASS_ChannelStop(m_Stream); + } - if (effects.FadeIn.Active) - { - BASS_ChannelSetAttribute(m_Stream, BASS_ATTRIB_VOL, 0.0f); - BASS_ChannelSlideAttribute(m_Stream, BASS_ATTRIB_VOL, targetVolume, effects.FadeIn.Duration); - } + void Channel::SetVolume(float volume) + { + if (volume < 0.0f) { log->Warning("Clamping invalid volume {0} to 0.0f", volume); volume = 0.0f; } + if (volume > 1.0f) { log->Warning("Clamping invalid volume {0} to 1.0f", volume); volume = 1.0f; } + BASS_ChannelSetAttribute(m_Stream, BASS_ATTRIB_VOL, volume); + } - if (effects.Loop.Active) - { - BASS_ChannelFlags(m_Stream, BASS_SAMPLE_LOOP, BASS_SAMPLE_LOOP); - log->Trace("Loop set {0}", m_Theme->GetName()); - } + void Channel::SlideVolume(float targetVolume, uint32_t time) + { + BASS_ChannelSlideAttribute(m_Stream, BASS_ATTRIB_VOL, targetVolume, time); + } - if (!Options->ForceDisableReverb && effects.ReverbDX8.Active) - { - HFX fx = BASS_ChannelSetFX(m_Stream, BASS_FX_DX8_REVERB, 1); - BASS_DX8_REVERB params{ 0, effects.ReverbDX8.Mix, effects.ReverbDX8.Time, 0.001f }; - if (!BASS_FXSetParameters(fx, (void*)¶ms)) - { - log->Error("Could not set reverb FX: {0}\n error: {1}\n at {2}:{3}", - m_Theme->GetName(), Engine::ErrorCodeToString(BASS_ErrorGetCode()), - __FILE__, __LINE__); - } - log->Trace("Reverb set: {0}", m_Theme->GetName()); - } + void Channel::SlideVolume(float targetVolume, uint32_t time, const std::function& onFinish) + { + SlideVolume(targetVolume, time); + BASS_ChannelSetSync(m_Stream, BASS_SYNC_SLIDE, 0, OnSlideVolumeSyncCallFunction, (void*)&onFinish); + } - if (effects.FadeOut.Active) + void Channel::SetDX8ReverbEffect(float reverbMix, float reverbTime, float inputGain, float highFreqRTRatio) + { + HFX effect = BASS_ChannelSetFX(m_Stream, BASS_FX_DX8_REVERB, 0); + if (reverbMix < -96) { log->Warning("Clamping DX8Reverb reverbMix({0}) to -96", reverbMix); reverbMix = -96; } + if (reverbMix > 0) { log->Warning("Clamping DX8Reverb reverbMix({0}) to 0", reverbMix); reverbMix = 0; } + if (reverbTime < 0.001f) { log->Warning("Clamping DX8Reverb reverbTime({0}) to 0.001f", reverbTime); reverbTime = 0.001f; } + if (reverbTime > 3000.0f) { log->Warning("Clamping DX8Reverb reverbTime({0}) to 3000.0f", reverbTime); reverbTime = 3000.0f; } + BASS_DX8_REVERB reverb{ + inputGain, reverbMix, reverbTime, highFreqRTRatio + }; + if (!BASS_FXSetParameters(effect, &reverb)) { - const QWORD length = BASS_ChannelGetLength(m_Stream, BASS_POS_BYTE); - const QWORD transitionBytes = BASS_ChannelSeconds2Bytes(m_Stream, effects.FadeOut.Duration / 1000.0f); - const QWORD offset = length - transitionBytes; - BASS_ChannelSetSync(m_Stream, BASS_SYNC_END, offset, OnVolumeSlideSync, this); - log->Trace("SyncEnd set: {0}", m_Theme->GetName()); + int code = BASS_ErrorGetCode(); + log->Error("Could not set DX8Reverb effect: {0}\n error: {1}\n at {2}:{3}", m_Stream, Engine::ErrorCodeToString(code), __FILE__, __LINE__); } + } - BASS_ChannelSetSync(m_Stream, BASS_SYNC_END, 0, OnEndSync, this); - log->Trace("SyncEnd set: {0}", m_Theme->GetName()); + void Channel::WhenAudioEnds(const std::function& onFinish) + { + BASS_ChannelSetSync(m_Stream, BASS_SYNC_END, 0, OnAudioEndSyncCallFunction, (void*)new OnSyncWhenAudioEndsData{ m_Stream, onFinish }); + } - m_Status = ChannelStatus::PLAYING; - m_EventManager.DispatchEvent(MusicChangeEvent(m_Theme, m_AudioId)); + void Channel::BeforeAudioEnds(double aheadSeconds, const std::function& onFinish) + { + double position = Length() - aheadSeconds; + size_t positionBytes = BASS_ChannelSeconds2Bytes(m_Stream, position); + BASS_ChannelSetSync(m_Stream, BASS_SYNC_POS, positionBytes, BeforeAudioEndsSyncCallFunction, (void*)new OnSyncBeforeAudioEndsData{ m_Stream, onFinish }); } - double Channel::CurrentPosition() const + void Channel::Acquire() { - if (m_Stream > 0) + if (m_Status != ChannelStatus::AVAILABLE) { - return BASS_ChannelBytes2Seconds(m_Stream, BASS_ChannelGetPosition(m_Stream, BASS_POS_BYTE)); + log->Error("Trying to acquire a non-available channel. We will allow that to not crash the game but the music may be funky."); } - return -1; + m_Status = ChannelStatus::PLAYING; + } + + void Channel::Release() + { + m_Status = ChannelStatus::AVAILABLE; } - double Channel::CurrentLength() const + double Channel::Position() const { if (m_Stream > 0) { - return BASS_ChannelBytes2Seconds(m_Stream, BASS_ChannelGetLength(m_Stream, BASS_POS_BYTE)); + return BASS_ChannelBytes2Seconds(m_Stream, BASS_ChannelGetPosition(m_Stream, BASS_POS_BYTE)); } return -1; } - void Channel::Stop() + double Channel::Length() const { if (m_Stream > 0) { - const auto& effects = m_Theme->GetAudioEffects(m_AudioId); - if (effects.FadeOut.Active) - { - BASS_ChannelSlideAttribute(m_Stream, BASS_ATTRIB_VOL, 0.0f, effects.FadeOut.Duration); - BASS_ChannelSetSync(m_Stream, BASS_SYNC_SLIDE, 0, OnVolumeSlideSync, this); - m_Status = ChannelStatus::FADING_OUT; - } - else - { - BASS_ChannelFree(m_Stream); - m_Stream = 0; - m_Status = ChannelStatus::AVAILABLE; - } + return BASS_ChannelBytes2Seconds(m_Stream, BASS_ChannelGetLength(m_Stream, BASS_POS_BYTE)); } + return -1; } - std::shared_ptr Channel::CurrentTheme() const - { - return m_Theme; - } - - const AudioFile& Channel::CurrentAudioFile() const + void Channel::OnSlideVolumeSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData) { - return m_Theme->GetAudioFile(m_AudioId); + auto* onFinish = static_cast*>(userData); + if (onFinish) { (*onFinish)(); } + else { CreateLogger("HSYNC::OnSlideVolumeSyncCallFunction")->Error("onFinish is nullptr"); } } - const AudioEffects& Channel::CurrentAudioEffects() const + void Channel::OnAudioEndSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData) { - return m_Theme->GetAudioEffects(m_AudioId); - } + auto* payload = static_cast(userData); + if (channel != payload->Channel) return; - void Channel::OnTransitionSync(HSYNC, DWORD channel, DWORD data, void* userData) - { - auto* _this = static_cast(userData); - if (_this->m_Stream == channel) - { - _this->m_EventManager.DispatchEvent(MusicTransitionEvent(_this->m_Theme, _this->m_AudioId, _this->CurrentAudioEffects().FadeOut.Duration)); - } + if (payload->Function) { payload->Function(); } + else { CreateLogger("HSYNC::OnAudioEndSyncCallFunction")->Error("onFinish is nullptr"); } } - void Channel::OnEndSync(HSYNC, DWORD channel, DWORD data, void* userData) + void Channel::BeforeAudioEndsSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData) { - auto* _this = static_cast(userData); - if (_this->m_Stream == channel) - { - const auto& effects = _this->m_Theme->GetAudioEffects(_this->m_AudioId); - if (!effects.Loop.Active) - { - _this->m_Status = ChannelStatus::AVAILABLE; - } + auto* payload = static_cast(userData); + if (channel != payload->Channel) return; - _this->m_EventManager.DispatchEvent(MusicEndEvent(_this->m_Theme, _this->m_AudioId)); - } - } - - void Channel::OnVolumeSlideSync(HSYNC, DWORD channel, DWORD data, void* userData) - { - auto* _this = static_cast(userData); - if (_this->m_Stream == channel && _this->m_Status == ChannelStatus::FADING_OUT) - { - float volume; - BASS_ChannelGetAttribute(channel, BASS_ATTRIB_VOL, &volume); - if (volume < DBL_EPSILON) - { - BASS_ChannelFree(channel); - _this->m_Stream = 0; - _this->m_Status = ChannelStatus::AVAILABLE; - } - } + double aheadSeconds = BASS_ChannelBytes2Seconds(channel, BASS_ChannelGetLength(channel, BASS_POS_BYTE) - BASS_ChannelGetPosition(channel, BASS_POS_BYTE)); + if (payload->Function) { payload->Function(aheadSeconds); } + else { CreateLogger("HSYNC::BeforeAudioEndsSyncCallFunction")->Error("onFinish is nullptr"); } } } diff --git a/src/NH/Bass/Channel.h b/src/NH/Bass/Channel.h index dc559e9..be9d26f 100644 --- a/src/NH/Bass/Channel.h +++ b/src/NH/Bass/Channel.h @@ -4,6 +4,7 @@ #include "EventManager.h" #include "NH/Logger.h" #include +#include #include @@ -17,47 +18,41 @@ namespace NH::Bass FADING_OUT }; - class Channel + class Channel : public IChannel { NH::Logger* log; - size_t m_Index; - EventManager& m_EventManager; ChannelStatus m_Status = ChannelStatus::AVAILABLE; HSTREAM m_Stream = 0; - std::shared_ptr m_Theme = std::shared_ptr(); - HashString m_AudioId{""}; public: - explicit Channel(size_t index, EventManager& em) : m_Index(index), m_EventManager(em) + explicit Channel(size_t index) { log = NH::CreateLogger(Union::String::Format("zBassMusic::Channel({0})", index)); }; - void Play(const std::shared_ptr&, HashString id = AudioFile::DEFAULT); + Result PlayInstant(const AudioFile& audioFile) override; + void StopInstant() override; + void SetVolume(float volume) override; + void SlideVolume(float targetVolume, uint32_t time) override; + void SlideVolume(float targetVolume, uint32_t time, const std::function& onFinish) override; + void SetDX8ReverbEffect(float reverbMix, float reverbTime, float inputGain, float highFreqRTRatio) override; - void Stop(); + void WhenAudioEnds(const std::function& onFinish) override; + void BeforeAudioEnds(double aheadSeconds, const std::function& onFinish) override; - bool IsAvailable() + [[nodiscard]] double Position() const override; + [[nodiscard]] double Length() const override; + + void Acquire() override; + void Release() override; + bool IsAvailable() override { return m_Status == ChannelStatus::AVAILABLE; }; - [[nodiscard]] double CurrentPosition() const; - - [[nodiscard]] double CurrentLength() const; - - [[nodiscard]] std::shared_ptr CurrentTheme() const; - - [[nodiscard]] const AudioFile& CurrentAudioFile() const; - - [[nodiscard]] const AudioEffects& CurrentAudioEffects() const; - - static void CALLBACK OnTransitionSync(HSYNC, DWORD channel, DWORD data, void* userData); - - static void CALLBACK OnEndSync(HSYNC, DWORD channel, DWORD data, void* userData); - - static void CALLBACK OnVolumeSlideSync(HSYNC, DWORD channel, DWORD data, void* userData); - private: + static void CALLBACK OnSlideVolumeSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData); + static void CALLBACK OnAudioEndSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData); + static void CALLBACK BeforeAudioEndsSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData); }; } diff --git a/src/NH/Bass/Command.cpp b/src/NH/Bass/Command.cpp index 0979526..6f5b0cf 100644 --- a/src/NH/Bass/Command.cpp +++ b/src/NH/Bass/Command.cpp @@ -13,26 +13,54 @@ namespace NH::Bass void CommandQueue::AddCommand(std::shared_ptr command) { - std::lock_guard lock(m_Mutex); - log->Info("Adding command to queue"); - m_Commands.push_back(std::move(command)); + std::lock_guard lock(m_DeferredMutex); + log->Trace("Adding command to queue"); + m_DeferredCommands.push_back(std::move(command)); } - void CommandQueue::AddCommandDeferred(std::shared_ptr command) + void CommandQueue::AddCommandOnFront(std::shared_ptr command) { std::lock_guard lock(m_DeferredMutex); - m_DeferredCommands.push_back(std::move(command)); + log->Trace("Adding command to queue"); + m_DeferredCommands.push_front(std::move(command)); } - void CommandQueue::AddCommandOnFront(std::shared_ptr command) + void CommandQueue::AddPerFrameCommand(std::shared_ptr command) { - std::lock_guard lock(m_Mutex); - log->Info("Adding command to queue"); - m_Commands.push_front(std::move(command)); + std::lock_guard lock(m_PerFrameDeferredMutex); + m_PerFrameCommandsDeferred.push_back(std::move(command)); } void CommandQueue::Update(Engine& engine) { + { + std::lock_guard lock(m_PerFrameDeferredMutex); + m_PerFrameCommands.insert(m_PerFrameCommands.end(), m_PerFrameCommandsDeferred.begin(), m_PerFrameCommandsDeferred.end()); + m_PerFrameCommandsDeferred.clear(); + } + + { + std::lock_guard lock(m_PerFrameMutex); + std::vector> finishedCommands{}; + for (auto& command: m_PerFrameCommands) + { + if (command->Execute(engine) == CommandResult::DONE) + { + finishedCommands.push_back(command); + } + } + std::erase_if(m_PerFrameCommands, [&finishedCommands](const std::shared_ptr& command) + { + return std::find(finishedCommands.begin(), finishedCommands.end(), command) != finishedCommands.end(); + }); + } + + { + std::lock_guard lock(m_DeferredMutex); + m_Commands.insert(m_Commands.end(), m_DeferredCommands.begin(), m_DeferredCommands.end()); + m_DeferredCommands.clear(); + } + { std::lock_guard lock(m_DeferredMutex); m_Commands.insert(m_Commands.end(), m_DeferredCommands.begin(), m_DeferredCommands.end()); diff --git a/src/NH/Bass/Command.h b/src/NH/Bass/Command.h index cac117e..b9cc105 100644 --- a/src/NH/Bass/Command.h +++ b/src/NH/Bass/Command.h @@ -7,6 +7,7 @@ #include #include #include +#include namespace NH::Bass { @@ -21,44 +22,53 @@ namespace NH::Bass struct Command { + virtual CommandResult Execute(Engine& engine) = 0; }; + class FunctionCommand : public Command + { + std::function m_Function; + public: + explicit FunctionCommand(std::function function) : m_Function(std::move(function)) {} + CommandResult Execute(Engine& engine) override { return m_Function(engine); } + }; + class OnTimeCommand : public Command { using TimePointType = std::chrono::high_resolution_clock::time_point; - TimePointType m_TimePoint; std::shared_ptr m_Command; bool m_ForceOrder = false; - public: OnTimeCommand(TimePointType timePoint, std::shared_ptr command, bool forceOrder = false) : m_TimePoint(timePoint), m_Command(std::move(command)), m_ForceOrder(forceOrder) {} - CommandResult Execute(Engine& engine) override; }; class CommandQueue { Logger* log = CreateLogger("zBassMusic::CommandQueue"); + // Commands for one-off actions may stay in queue while returning RETRY or DEFER std::deque> m_Commands; + // Commands added during execution of a command. They are added to the main queue after execution. std::deque> m_DeferredCommands; + // Commands which lifetime is expected to last several frames. + // They are executed from another queue to avoid locking with one-off commands that use RETRY. + std::vector> m_PerFrameCommands; + std::vector> m_PerFrameCommandsDeferred; std::mutex m_Mutex; std::mutex m_DeferredMutex; + std::mutex m_PerFrameMutex; + std::mutex m_PerFrameDeferredMutex; public: void AddCommand(std::shared_ptr command); - /** - * Method to add commands within an executing command. - * We can't use AddCommand() directly, because it would cause a deadlock. - * @param command - */ - void AddCommandDeferred(std::shared_ptr command); - void AddCommandOnFront(std::shared_ptr command); + void AddPerFrameCommand(std::shared_ptr command); + void Update(Engine& engine); }; } diff --git a/src/NH/Bass/Engine.cpp b/src/NH/Bass/Engine.cpp index b6dfde2..1dd1b2b 100644 --- a/src/NH/Bass/Engine.cpp +++ b/src/NH/Bass/Engine.cpp @@ -34,13 +34,42 @@ namespace NH::Bass uint64_t delta = std::chrono::duration_cast(now - lastTimestamp).count(); lastTimestamp = now; - m_TransitionScheduler.Update(*this); m_CommandQueue.Update(*this); BASS_Update(delta); GetEM().Update(); } + std::shared_ptr Engine::AcquireFreeChannel() + { + for (auto channel: m_Channels) + { + if (channel->IsAvailable()) + { + channel->Acquire(); + return channel; + } + } + + log->Info("No channel available. Creating a new one."); + if (m_Channels.size() > 32) + { + log->Warning("There are more than 32 channels. Report this to the developers because some channels may not be released."); + } + if (m_Channels.size() > 128) + { + log->Error("There are more than 128 channels. We are stopping this now because it's too much and there is definitely some leak."); + throw std::runtime_error("Too many channels"); + } + + return m_Channels.emplace_back(std::make_shared(m_Channels.size(), m_EventManager)); + } + + void Engine::ReleaseChannel(const std::shared_ptr& channel) + { + channel->Release(); + } + void Engine::SetVolume(float volume) { if (!m_Initialized) @@ -60,7 +89,7 @@ namespace NH::Bass } m_MasterVolume = volume; - BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, 10000 * m_MasterVolume); + BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, 10000.0f * m_MasterVolume); } float Engine::GetVolume() const @@ -73,11 +102,6 @@ namespace NH::Bass return m_EventManager; } - TransitionScheduler& Engine::GetTransitionScheduler() - { - return m_TransitionScheduler; - } - MusicManager& Engine::GetMusicManager() { return m_MusicManager; @@ -90,17 +114,12 @@ namespace NH::Bass void Engine::StopMusic() { - if (!m_Initialized) + if (!m_Initialized) { return; } + if (m_ActiveTheme) { - return; + m_ActiveTheme->StopInstant(*this); + m_ActiveTheme = nullptr; } - - for (const auto& channel: m_Channels) - { - channel->Stop(); - } - - m_ActiveChannel = nullptr; } Engine::Engine() @@ -134,7 +153,7 @@ namespace NH::Bass } }; - m_Initialized = BASS_Init(deviceIndex, 44100, 0, nullptr, nullptr); + m_Initialized = BASS_Init((int32_t) deviceIndex, 44100, 0, nullptr, nullptr); if (!m_Initialized) { log->Error("Could not initialize BASS using BASS_Init\n {0}\n at {1}:{2}", @@ -146,29 +165,16 @@ namespace NH::Bass BASS_GetInfo(&info); log->Trace("Sample Rate: {0} Hz", info.freq); - static constexpr size_t Channels_Max = 8; + static constexpr size_t Channels_Max = 16; m_Channels.clear(); for (size_t i = 0; i < Channels_Max; i++) { - m_Channels.emplace_back(std::make_shared(i, m_EventManager)); + m_Channels.emplace_back(std::make_shared(i)); } log->Info("Initialized with device: {0}", deviceIndex); } - std::shared_ptr Engine::FindAvailableChannel() - { - for (auto channel: m_Channels) - { - if (channel->IsAvailable()) - { - return channel; - } - } - - return nullptr; - } - Union::StringUTF8 Engine::ErrorCodeToString(const int code) { // @formatter:off @@ -176,61 +182,4 @@ namespace NH::Bass // @formatter:on return map[code]; } - - Logger* ChangeZoneCommand::log = CreateLogger("zBassMusic::ChangeZoneCommand"); - Logger* PlayThemeCommand::log = CreateLogger("zBassMusic::PlayThemeCommand"); - Logger* ScheduleThemeChangeCommand::log = CreateLogger("zBassMusic::ScheduleThemeChangeCommand"); - - CommandResult ChangeZoneCommand::Execute(Engine& engine) - { - log->Trace("Executing ChangeZoneCommand for zone {0}", m_Zone); - - const auto themes = engine.GetMusicManager().GetThemesForZone(m_Zone); - if (themes.empty()) - { - log->Warning("No themes found for zone {0}", m_Zone); - return CommandResult::DONE; - } - engine.GetCommandQueue().AddCommandDeferred(std::make_shared(themes[0].first)); - return CommandResult::DONE; - } - - CommandResult PlayThemeCommand::Execute(Engine& engine) - { - log->Trace("Executing PlayThemeCommand for theme {0}", m_ThemeId); - - auto theme = engine.GetMusicManager().GetTheme(m_ThemeId); - if (!theme->IsAudioFileReady(AudioFile::DEFAULT)) - { - log->Trace("Theme {0} is not ready", m_ThemeId); - m_RetryCount++; - if (m_RetryCount > 10000) - { - log->Error("Theme {0} was not ready after 10000 retries (roughly 600ms @ 60 FPS), removing it from queue", m_ThemeId); - return CommandResult::DONE; - } - return CommandResult::RETRY; - } - - auto channel = engine.FindAvailableChannel(); - if (!channel) - { - log->Error("No available channels"); - return CommandResult::DEFER; - } - - if (engine.m_ActiveChannel) { engine.m_ActiveChannel->Stop(); } - channel->Play(theme, m_AudioId); - engine.m_ActiveChannel = channel; - - return CommandResult::DONE; - } - - CommandResult ScheduleThemeChangeCommand::Execute(Engine& engine) - { - log->Trace("Executing ScheduleThemeChangeCommand for theme {0}", m_ThemeId); - auto theme = engine.GetMusicManager().GetTheme(m_ThemeId); - engine.GetTransitionScheduler().Schedule(engine.m_ActiveChannel, theme); - return CommandResult::DONE; - } } \ No newline at end of file diff --git a/src/NH/Bass/Engine.h b/src/NH/Bass/Engine.h index fbfcec0..5ab3518 100644 --- a/src/NH/Bass/Engine.h +++ b/src/NH/Bass/Engine.h @@ -3,11 +3,11 @@ #include "CommonTypes.h" #include "EventManager.h" #include "Channel.h" -#include "TransitionScheduler.h" #include "NH/Logger.h" #include "NH/HashString.h" -#include "NH/Bass/MusicManager.h" -#include "NH/Bass/Command.h" +#include +#include +#include #include #include #include @@ -16,13 +16,11 @@ namespace NH::Bass { class ChangeZoneCommand; - class PlayThemeCommand; class ScheduleThemeChangeCommand; - class Engine + class Engine : public IEngine { friend class ChangeZoneCommand; - friend class PlayThemeCommand; friend class ScheduleThemeChangeCommand; static NH::Logger* log; @@ -30,66 +28,30 @@ namespace NH::Bass bool m_Initialized = false; float m_MasterVolume = 1.0f; std::vector> m_Channels; - std::shared_ptr m_ActiveChannel = nullptr; + std::shared_ptr m_ActiveTheme = nullptr; CommandQueue m_CommandQueue{}; EventManager m_EventManager{}; MusicManager m_MusicManager{}; - TransitionScheduler m_TransitionScheduler{}; public: static Engine* GetInstance(); void Update(); - + void StopMusic(); void SetVolume(float volume); - [[nodiscard]] float GetVolume() const; - EventManager& GetEM(); - - TransitionScheduler& GetTransitionScheduler(); - - MusicManager& GetMusicManager(); + std::shared_ptr AcquireFreeChannel() override; + void ReleaseChannel(const std::shared_ptr& channel) override; - CommandQueue& GetCommandQueue(); - - void StopMusic(); + EventManager& GetEM() override; + MusicManager& GetMusicManager() override; + CommandQueue& GetCommandQueue() override; private: Engine(); - std::shared_ptr FindAvailableChannel(); - public: static Union::StringUTF8 ErrorCodeToString(int code); }; - - class ChangeZoneCommand : public Command - { - static Logger* log; - HashString m_Zone; - public: - explicit ChangeZoneCommand(HashString zone) : m_Zone(zone) {}; - CommandResult Execute(Engine& engine) override; - }; - - class PlayThemeCommand : public Command - { - static Logger* log; - HashString m_ThemeId; - HashString m_AudioId; - size_t m_RetryCount = 0; - public: - explicit PlayThemeCommand(HashString themeId, HashString audioId) : m_ThemeId(themeId), m_AudioId(audioId) {}; - CommandResult Execute(Engine& engine) override; - }; - - class ScheduleThemeChangeCommand : public Command - { - static Logger* log; - HashString m_ThemeId; - public: - explicit ScheduleThemeChangeCommand(HashString themeId) : m_ThemeId(themeId) {}; - CommandResult Execute(Engine& engine) override; - }; } diff --git a/src/NH/Bass/EngineCommands.cpp b/src/NH/Bass/EngineCommands.cpp new file mode 100644 index 0000000..caebb5e --- /dev/null +++ b/src/NH/Bass/EngineCommands.cpp @@ -0,0 +1,41 @@ +#include "EngineCommands.h" + +#include +#include + +namespace NH::Bass +{ + Logger* ChangeZoneCommand::log = CreateLogger("zBassMusic::ChangeZoneCommand"); + Logger* ScheduleThemeChangeCommand::log = CreateLogger("zBassMusic::ScheduleThemeChangeCommand"); + + CommandResult ChangeZoneCommand::Execute(Engine& engine) + { + log->Info("Music zone changed: {0}", m_Zone); + const auto themes = engine.GetMusicManager().GetThemesForZone(m_Zone); + if (themes.empty()) + { + log->Warning("No themes found for zone {0}", m_Zone); + return CommandResult::DONE; + } + engine.GetCommandQueue().AddCommand(std::make_shared(themes[0].first)); + return CommandResult::DONE; + } + + CommandResult ScheduleThemeChangeCommand::Execute(Engine& engine) + { + log->Info("Scheduling theme: {0}", m_ThemeId); + auto theme = engine.GetMusicManager().GetTheme(m_ThemeId); + if (!theme) + { + log->Error("Theme {0} doesn't exist", m_ThemeId); + return CommandResult::DONE; + } + + auto& activeTheme = engine.m_ActiveTheme; + if (activeTheme) theme->ScheduleAfter(engine, activeTheme); + else theme->PlayInstant(engine); + + engine.m_ActiveTheme = theme; + return CommandResult::DONE; + } +} \ No newline at end of file diff --git a/src/NH/Bass/EngineCommands.h b/src/NH/Bass/EngineCommands.h new file mode 100644 index 0000000..1990756 --- /dev/null +++ b/src/NH/Bass/EngineCommands.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +namespace NH::Bass +{ + class ChangeZoneCommand : public Command + { + static Logger* log; + HashString m_Zone; + public: + explicit ChangeZoneCommand(HashString zone) : m_Zone(zone) {}; + CommandResult Execute(Engine& engine) override; + }; + + class ScheduleThemeChangeCommand : public Command + { + static Logger* log; + HashString m_ThemeId; + public: + explicit ScheduleThemeChangeCommand(HashString themeId) : m_ThemeId(themeId) {}; + CommandResult Execute(Engine& engine) override; + }; +} \ No newline at end of file diff --git a/src/NH/Bass/EventManager.h b/src/NH/Bass/EventManager.h index 8579ed0..b40aaec 100644 --- a/src/NH/Bass/EventManager.h +++ b/src/NH/Bass/EventManager.h @@ -2,8 +2,8 @@ #include "NH/Bass/CommonTypes.h" #include "NH/Logger.h" -#include "MusicTheme.h" #include + #include #include #include @@ -11,6 +11,7 @@ namespace NH::Bass { + class MusicTheme; enum class EventType { @@ -22,16 +23,16 @@ namespace NH::Bass struct Event { - struct MusicEnd { std::shared_ptr Theme; HashString AudioId; }; - struct MusicTransition { std::shared_ptr Theme; HashString AudioId; float TimeLeft; }; - struct MusicChange { std::shared_ptr Theme; HashString AudioId; }; + struct MusicEnd { MusicTheme const* Theme = nullptr; HashString AudioId; }; + struct MusicTransition { MusicTheme const* Theme = nullptr; HashString AudioId; float TimeLeft = 0; }; + struct MusicChange { MusicTheme const* Theme = nullptr; HashString AudioId; }; using DataType = std::variant; EventType Type = EventType::UNKNOWN; DataType Data; Event() = delete; - Event(EventType type, DataType data) : Type(type), Data(std::move(data)) {} + Event(EventType type, DataType data) : Type(type), Data(data) {} }; using EventSubscriberFn = void (*)(const Event&, void*); @@ -77,19 +78,19 @@ namespace NH::Bass struct MusicEndEvent : public Event { MusicEnd Data; - MusicEndEvent(std::shared_ptr theme, HashString audioId) : Event(EventType::MUSIC_END, MusicEnd{std::move(theme), audioId}) {} + MusicEndEvent(MusicTheme* theme, HashString audioId) : Event(EventType::MUSIC_END, MusicEnd{theme, audioId}) {} }; struct MusicTransitionEvent : public Event { MusicTransition Data; - MusicTransitionEvent(std::shared_ptr theme, HashString audioId, float timeLeft) - : Event(EventType::MUSIC_TRANSITION, MusicTransition{std::move(theme), audioId, timeLeft}) {} + MusicTransitionEvent(MusicTheme* theme, HashString audioId, float timeLeft) + : Event(EventType::MUSIC_TRANSITION, MusicTransition{theme, audioId, timeLeft}) {} }; struct MusicChangeEvent : public Event { MusicChange Data; - MusicChangeEvent(std::shared_ptr theme, HashString audioId) : Event(EventType::MUSIC_CHANGE, MusicChange{std::move(theme), audioId}) {} + MusicChangeEvent(MusicTheme* theme, HashString audioId) : Event(EventType::MUSIC_CHANGE, MusicChange{theme, audioId}) {} }; } \ No newline at end of file diff --git a/src/NH/Bass/IChannel.h b/src/NH/Bass/IChannel.h new file mode 100644 index 0000000..049ec91 --- /dev/null +++ b/src/NH/Bass/IChannel.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +namespace NH::Bass +{ + class AudioFile; + + struct IChannel + { + enum class ErrorType { INVALID_BUFFER }; + constexpr static const char* ErrorTypeStrings[] = { "INVALID_BUFFER" }; + + class Error : public std::runtime_error + { + ErrorType Type; + int32_t Code; + String Details; + public: + Error(ErrorType type, int32_t code, const String& details) + : std::runtime_error(String::Format("{0} error: {1}, message: %s", ErrorTypeStrings[static_cast(type)], code, details)), + Type(type), Code(code), Details(details) {}; + Error(ErrorType type, int32_t code, const String&& details) + : std::runtime_error(String::Format("{0} error: {1}, message: %s", ErrorTypeStrings[static_cast(type)], code, details)), + Type(type), Code(code), Details(details) {}; + [[nodiscard]] ErrorType GetType() const { return Type; } + [[nodiscard]] int32_t GetCode() const { return Code; } + [[nodiscard]] const char* GetDetails() const { return Details; } + }; + + template + using Result = std::expected; + + virtual Result PlayInstant(const AudioFile& audioFile) = 0; + virtual void StopInstant() = 0; + + virtual void SetVolume(float volume) = 0; + virtual void SlideVolume(float targetVolume, uint32_t time) = 0; + virtual void SlideVolume(float targetVolume, uint32_t time, const std::function& onFinish) = 0; + virtual void SetDX8ReverbEffect(float reverbMix, float reverbTime, float inputGain = 0, float highFreqRTRatio = 0.001f) = 0; + + virtual void WhenAudioEnds(const std::function& onFinish) = 0; + virtual void BeforeAudioEnds(double aheadSeconds, const std::function& onFinish) = 0; + + [[nodiscard]] virtual double Position() const = 0; + [[nodiscard]] virtual double Length() const = 0; + + virtual void Acquire() = 0; + virtual void Release() = 0; + virtual bool IsAvailable() = 0; + }; +} diff --git a/src/NH/Bass/IEngine.h b/src/NH/Bass/IEngine.h new file mode 100644 index 0000000..c894678 --- /dev/null +++ b/src/NH/Bass/IEngine.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace NH::Bass +{ + class EventManager; + class MusicManager; + class CommandQueue; + + struct IEngine + { + /** + * Get a free channel and mark it as busy + * @return shared_ptr to a free channel (or null if all channels are busy) + */ + virtual std::shared_ptr AcquireFreeChannel() = 0; + + /** + * Release a channel and mark it as free + * @param channel + */ + virtual void ReleaseChannel(const std::shared_ptr& channel) = 0; + + virtual EventManager& GetEM() = 0; + virtual MusicManager& GetMusicManager() = 0; + virtual CommandQueue& GetCommandQueue() = 0; + }; +} diff --git a/src/NH/Bass/MusicTheme.cpp b/src/NH/Bass/MusicTheme.cpp index 215a3a4..72daf98 100644 --- a/src/NH/Bass/MusicTheme.cpp +++ b/src/NH/Bass/MusicTheme.cpp @@ -1,6 +1,9 @@ #include "MusicTheme.h" +#include "EngineCommands.h" #include +#include +#include namespace NH::Bass { @@ -45,26 +48,117 @@ namespace NH::Bass if (audioFile.Status == AudioFile::StatusType::NOT_LOADED) { audioFile.Status = AudioFile::StatusType::LOADING; - executor.AddTask([type, this]() - { - int systems = VDF_VIRTUAL | VDF_PHYSICAL; - const Union::VDFS::File* file = Union::VDFS::GetDefaultInstance().GetFile(m_AudioFiles[type].Filename, systems); - if (!file) - { - m_AudioFiles[type].Status = AudioFile::StatusType::FAILED; - m_AudioFiles[type].Error = "File not found"; - return; - } - - Union::Stream* stream = file->Open(); - m_AudioFiles[type].Buffer.resize(stream->GetSize()); - stream->Read(m_AudioFiles[type].Buffer.data(), m_AudioFiles[type].Buffer.size()); - stream->Close(); - - m_AudioFiles[type].Status = AudioFile::StatusType::READY; - }); + executor.AddTask([type, this]() { + int systems = VDF_VIRTUAL | VDF_PHYSICAL; + const Union::VDFS::File* file = Union::VDFS::GetDefaultInstance().GetFile(m_AudioFiles[type].Filename, systems); + if (!file) + { + m_AudioFiles[type].Status = AudioFile::StatusType::FAILED; + m_AudioFiles[type].Error = "File not found"; + return; + } + + Union::Stream* stream = file->Open(); + m_AudioFiles[type].Buffer.resize(stream->GetSize()); + stream->Read(m_AudioFiles[type].Buffer.data(), m_AudioFiles[type].Buffer.size()); + stream->Close(); + + m_AudioFiles[type].Status = AudioFile::StatusType::READY; + }); + } + } + } + + void MusicTheme::PlayInstant(IEngine& engine) + { + if (!ReadyToPlay(engine, AudioFile::DEFAULT)) { return; } + + auto& channel = m_AcquiredChannels.emplace_back(engine.AcquireFreeChannel()); + auto& effects = GetAudioEffects(AudioFile::DEFAULT); + auto result = channel->PlayInstant(m_AudioFiles.at(AudioFile::DEFAULT)); + if (!result) + { + engine.ReleaseChannel(channel); + m_AcquiredChannels.clear(); + return; + } + + if (effects.ReverbDX8.Active) + { + channel->SetDX8ReverbEffect(effects.ReverbDX8.Mix, effects.ReverbDX8.Time); + } + + if (effects.FadeIn.Active) + { + channel->SetVolume(0.0f); + channel->SlideVolume(1.0f, (uint32_t)effects.FadeIn.Duration); + } + + if (effects.FadeOut.Active) + { + channel->BeforeAudioEnds(effects.FadeOut.Duration, CreateSyncHandler(m_SyncHandlersWithDouble, [this, &engine](double timeLeft) -> void { + engine.GetEM().DispatchEvent(MusicTransitionEvent{ this, AudioFile::DEFAULT, static_cast(timeLeft) }); + })); + } + channel->WhenAudioEnds(CreateSyncHandler(m_SyncHandlers, [this, &engine]() -> void { + engine.GetEM().DispatchEvent(MusicEndEvent{ this, AudioFile::DEFAULT }); + })); + engine.GetEM().DispatchEvent(MusicChangeEvent{ this, AudioFile::DEFAULT }); + } + + void MusicTheme::ScheduleAfter(IEngine& engine, const std::shared_ptr& currentTheme) + { + // Instant transition + currentTheme->StopInstant(engine); + PlayInstant(engine); + } + + void MusicTheme::StopInstant(IEngine& engine) + { + for (auto& channel: m_AcquiredChannels) + { + auto& effects = GetAudioEffects(AudioFile::DEFAULT); + if (effects.FadeOut.Active) + { + channel->SlideVolume(0.0f, (uint32_t)effects.FadeOut.Duration, CreateSyncHandler(m_SyncHandlers, [this, &engine, &channel]() -> void { + engine.ReleaseChannel(channel); + })); } + else { engine.ReleaseChannel(channel); } } + m_AcquiredChannels.clear(); + } + + bool MusicTheme::ReadyToPlay(IEngine& engine, HashString audio) + { + const auto& file = GetAudioFile(AudioFile::DEFAULT); + if (file.Status == AudioFile::StatusType::FAILED) + { + log->Error("Trying to play a music theme that has failed to load: {0}", file); + return false; + } + + if (file.Status != AudioFile::StatusType::READY) + { + engine.GetCommandQueue().AddCommand(std::make_shared( + [this, &engine](Engine& e) -> CommandResult { + const AudioFile& f = GetAudioFile(AudioFile::DEFAULT); + if (f.Status == AudioFile::StatusType::FAILED) + { + log->Error("Trying to play a music theme that has failed to load: {0}", f.Filename); + return CommandResult::DONE; + } + if (f.Status == AudioFile::StatusType::READY) + { + engine.GetCommandQueue().AddCommand(std::make_shared(m_Name)); + return CommandResult::DONE; + } + return CommandResult::DEFER; + })); + return false; + } + + return true; } const AudioEffects& MusicTheme::GetAudioEffects(HashString type) const @@ -73,7 +167,7 @@ namespace NH::Bass return AudioEffects::None; } - const std::shared_ptr& MusicTheme::GetMidiFile(HashString type) const + std::shared_ptr MusicTheme::GetMidiFile(HashString type) const { if (m_MidiFiles.contains(type)) { return m_MidiFiles.at(type); } return {}; @@ -83,4 +177,40 @@ namespace NH::Bass { return std::find(m_Zones.begin(), m_Zones.end(), zone) != m_Zones.end(); } + + String MusicTheme::ToString() const + { + String result = String("MusicTheme{ \n\tName: ") + m_Name + ", \n\tAudioFiles: {\n"; + int i = 0; + for (auto& [type, audioFile]: m_AudioFiles) + { + result += String("\t\t") + String(type) + ": " + audioFile.ToString().Replace("\n", "\n\tt"); + if (++i < m_AudioFiles.size()) + { + result += ",\n"; + } + } + i = 0; + result += "\n\t},\n\tAudioEffects: { \n"; + for (auto& [type, audioEffects]: m_AudioEffects) + { + result += String("\t\t") + String(type) + ": " + audioEffects.ToString().Replace("\n", "\n\t\t"); + if (++i < m_AudioEffects.size()) + { + result += ",\n"; + } + } + i = 0; + result += "\n\t},\n\tZones: { "; + for (auto& zone: m_Zones) + { + result += String(zone); + if (++i < m_Zones.size()) + { + result += ", "; + } + } + result += " }\n }"; + return result; + } } \ No newline at end of file diff --git a/src/NH/Bass/MusicTheme.h b/src/NH/Bass/MusicTheme.h index a36c078..704a5f9 100644 --- a/src/NH/Bass/MusicTheme.h +++ b/src/NH/Bass/MusicTheme.h @@ -1,11 +1,12 @@ #pragma once -#include "CommonTypes.h" #include #include #include #include #include +#include +#include #include #include @@ -56,10 +57,14 @@ namespace NH::Bass static Logger* log; String m_Name; + size_t m_SyncHandlersId = 0; std::unordered_map m_AudioFiles; std::unordered_map m_AudioEffects; std::unordered_map> m_MidiFiles; + std::unordered_map> m_SyncHandlers; + std::unordered_map> m_SyncHandlersWithDouble; std::vector m_Zones; + std::vector> m_AcquiredChannels; public: static MusicTheme None; @@ -67,67 +72,49 @@ namespace NH::Bass explicit MusicTheme(const String& name); void SetAudioFile(HashString type, const String& filename); - void SetAudioEffects(HashString type, const std::function& effectsSetter); - void AddZone(HashString zone); - void AddMidiFile(HashString type, const std::shared_ptr& midiFile); - void LoadAudioFiles(Executor& executor); - [[nodiscard]] const String& GetName() const { return m_Name; } + void PlayInstant(IEngine& engine); + void ScheduleAfter(IEngine&, const std::shared_ptr& currentTheme); + void StopInstant(IEngine& engine); + [[nodiscard]] const String& GetName() const { return m_Name; } [[nodiscard]] bool HasAudioFile(HashString type) const { return m_AudioFiles.find(type) != m_AudioFiles.end(); } - [[nodiscard]] bool IsAudioFileReady(HashString type) const { return HasAudioFile(type) && m_AudioFiles.at(type).Status == AudioFile::StatusType::READY; } - [[nodiscard]] const AudioFile& GetAudioFile(HashString type) const { return m_AudioFiles.at(type); } - [[nodiscard]] const AudioEffects& GetAudioEffects(HashString type) const; - - [[nodiscard]] const std::shared_ptr& GetMidiFile(HashString type) const; - + [[nodiscard]] std::shared_ptr GetMidiFile(HashString type) const; [[nodiscard]] const std::vector& GetZones() const { return m_Zones; } - [[nodiscard]] bool HasZone(HashString zone) const; + [[nodiscard]] String ToString() const override; - [[nodiscard]] String ToString() const override + private: + bool ReadyToPlay(IEngine& engine, HashString audio); + + template + const std::function& CreateSyncHandler(std::unordered_map>& collection, std::function function) { - String result = String("MusicTheme{ \n\tName: ") + m_Name + ", \n\tAudioFiles: {\n"; - int i = 0; - for (auto& [type, audioFile]: m_AudioFiles) - { - result += String("\t\t") + String(type) + ": " + audioFile.ToString().Replace("\n", "\n\tt"); - if (++i < m_AudioFiles.size()) - { - result += ",\n"; - } - } - i = 0; - result += "\n\t},\n\tAudioEffects: { \n"; - for (auto& [type, audioEffects]: m_AudioEffects) - { - result += String("\t\t") + String(type) + ": " + audioEffects.ToString().Replace("\n", "\n\t\t"); - if (++i < m_AudioEffects.size()) - { - result += ",\n"; - } - } - i = 0; - result += "\n\t},\n\tZones: { "; - for (auto& zone: m_Zones) - { - result += String(zone); - if (++i < m_Zones.size()) - { - result += ", "; - } - } - result += " }\n }"; - return result; + size_t id = m_SyncHandlersId++; + auto handler = [function = std::move(function), &collection, id](Args args) { + function(std::forward(args)); + collection.erase(id); + }; + collection.emplace(id, std::move(handler)); + return collection.at(id); } - private: + const std::function& CreateSyncHandler(std::unordered_map>& collection, std::function function) + { + size_t id = m_SyncHandlersId++; + auto handler = [function = std::move(function), &collection, id]() { + function(); + collection.erase(id); + }; + collection.emplace(id, std::move(handler)); + return collection.at(id); + } }; } diff --git a/src/NH/Bass/TransitionScheduler.cpp b/src/NH/Bass/TransitionScheduler.cpp deleted file mode 100644 index 64dd44b..0000000 --- a/src/NH/Bass/TransitionScheduler.cpp +++ /dev/null @@ -1,154 +0,0 @@ -#include "TransitionScheduler.h" - -#include -#include -#include - -namespace NH::Bass -{ - void TransitionScheduler::Schedule(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic) - { - if (!activeChannel) - { - m_ScheduledActions.emplace_back(ScheduledAction{ activeChannel, 0, [id = HashString(nextMusic->GetName())](Engine& engine) - { - engine.GetCommandQueue().AddCommand(std::make_shared(id, AudioFile::DEFAULT)); - }}); - return; - } - - const auto& currentTheme = activeChannel->CurrentTheme(); - const TransitionScheduleRule& rule = GetScheduleRule(currentTheme->GetName()); - - if (rule.Type == TransitionScheduleType::INSTANT) - { - ScheduleInstant(activeChannel, nextMusic); - return; - } - - if (rule.Type == TransitionScheduleType::ON_BEAT) - { - ScheduleOnBeat(activeChannel, nextMusic, rule); - return; - } - - log->Error("Unknown Schedule type = {0}", (size_t)rule.Type); - } - - void TransitionScheduler::ScheduleInstant(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic) - { - m_ScheduledActions.emplace_back(ScheduledAction{ activeChannel, 0, [id = HashString(nextMusic->GetName())](Engine& engine) - { - engine.GetCommandQueue().AddCommand(std::make_shared(id, AudioFile::DEFAULT)); - }}); - } - - void TransitionScheduler::ScheduleOnBeat(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic, const TransitionScheduleRule& rule) - { - TransitionScheduleRule::DataOnBeat data = std::get(rule.Data); - const auto& currentTheme = activeChannel->CurrentTheme(); - const auto& effects = currentTheme->GetAudioEffects(AudioFile::DEFAULT); - - double overhead = effects.FadeOut.Active ? effects.FadeOut.Duration : 0; - std::sort(data.TimePoints.begin(), data.TimePoints.end()); - - double length = activeChannel->CurrentLength(); - double currentPosition = activeChannel->CurrentPosition(); - log->Trace("length = {0}", length); - log->Trace("currentPosition = {0}", currentPosition); - double timePointCandidate = -1; - double intervalCandidate = -1; - - log->Trace("Rule checking TimePoints"); - for (const auto& item: data.TimePoints) - { - double minValue = currentPosition - overhead; - if (item > minValue) - { - log->Trace("found TimePoint = {0}", item); - timePointCandidate = item; - break; - } - } - - if (data.Interval > 0) - { - log->Trace("Rule checking Interval"); - - // Please don't abort me. Quick hack for preview. Fast enough. - for (double time = 0; time < length; time += data.Interval) - { - double minValue = currentPosition - overhead; - if (time > minValue) - { - log->Trace("found TimePoint = {0}", time); - intervalCandidate = time; - break; - } - } - } - - if (timePointCandidate <= 0 && intervalCandidate <= 0) - { - log->Trace("Not found any candidates, rollback to INSTANT"); - ScheduleInstant(activeChannel, nextMusic); - return; - } - - double target = timePointCandidate > intervalCandidate ? timePointCandidate : intervalCandidate; - log->Trace("target = {0}", target); - - log->Debug("Starting Monitor {0} -> {1}, position = {2}", currentTheme->GetName(), nextMusic->GetName(), target); - m_ScheduledActions.emplace_back(ScheduledAction{ activeChannel, target, [id = HashString(nextMusic->GetName())](Engine& engine) - { - engine.GetCommandQueue().AddCommand(std::make_shared(id, AudioFile::DEFAULT)); - }}); - } - - void TransitionScheduler::Update(Engine& engine) - { - for (auto& scheduledAction : m_ScheduledActions) - { - if (!scheduledAction.Channel) - { - scheduledAction.Done = true; - scheduledAction.Action(engine); - continue; - } - - if (scheduledAction.Position <= scheduledAction.Channel->CurrentPosition()) - { - if (scheduledAction.Position > 0) - { - double target = scheduledAction.Position; - double position = scheduledAction.Channel->CurrentPosition(); - double delay = position - target; - log->Debug( - "Monitor for {0} completed, calling onReady\n target = {1}\n current = {2}\n delay = {3}", - scheduledAction.Channel->CurrentTheme()->GetName(), target, position, delay); - } - scheduledAction.Done = true; - scheduledAction.Action(engine); - } - } - - std::erase_if(m_ScheduledActions, [](const ScheduledAction& monitor) { return monitor.Done; }); - } - - void TransitionScheduler::AddRuleOnBeat(const char* name, double interval, std::vector timePoints) - { - log->Debug("AddRule OnBeat for {0}, interval = {1}, timePoints.count = {2}", name, interval, timePoints.size()); - m_ScheduleRules.insert_or_assign(name, TransitionScheduleRule::OnBeat(interval, std::move(timePoints))); - } - - const TransitionScheduleRule& TransitionScheduler::GetScheduleRule(HashString id) - { - if (m_ScheduleRules.contains(id)) - { - return m_ScheduleRules.at(id); - } - - static TransitionScheduleRule defaultRule = TransitionScheduleRule::Instant(); - return defaultRule; - } -} \ No newline at end of file diff --git a/src/NH/Bass/TransitionScheduler.h b/src/NH/Bass/TransitionScheduler.h deleted file mode 100644 index 3dc5b16..0000000 --- a/src/NH/Bass/TransitionScheduler.h +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -#include "NH/Bass/CommonTypes.h" -#include "Channel.h" -#include "NH/Logger.h" - -#include -#include -#include -#include -#include -#include - -namespace NH::Bass -{ - enum class TransitionScheduleType - { - // Performs the transition instantly when it's ready - INSTANT, - // Performs the transition only on defined timecodes (like beat grops) - ON_BEAT - }; - - struct TransitionScheduleRule - { - struct DataInstant - { - }; - - struct DataOnBeat - { - // Static interval when transition will be performed, in seconds. - // BeatInterval = 1.2; means that transition may happen only at {1.2, 2.4, 3.6, 4.8, ...} time points. - // Non-positive value disables static interval. - double Interval = 0; - - // Vector of time points in seconds when the transiton will be performed. - std::vector TimePoints; - }; - - struct DataJingle - { - - }; - - TransitionScheduleType Type; - std::variant Data; - - static TransitionScheduleRule Instant() - { - return { TransitionScheduleType::INSTANT }; - } - - static TransitionScheduleRule OnBeat(double interval = 0, std::vector timePoints = {}) - { - return { TransitionScheduleType::ON_BEAT, DataOnBeat{ interval, std::move(timePoints) }}; - } - }; - - struct ScheduledAction - { - std::shared_ptr Channel; - double Position; - std::function Action; - bool Done = false; - }; - - class TransitionScheduler - { - NH::Logger* log = NH::CreateLogger("zBassMusic::TransitionScheduler"); - std::unordered_map m_ScheduleRules; - std::vector m_ScheduledActions; - - public: - void Schedule(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic); - - void Update(Engine& engine); - - void AddRuleOnBeat(const char* name, double interval = 0, std::vector timePoints = {}); - - private: - const TransitionScheduleRule& GetScheduleRule(HashString id); - - void ScheduleInstant(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic); - - void ScheduleOnBeat(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic, const TransitionScheduleRule& rule); - }; -} \ No newline at end of file diff --git a/src/NH/Commons.h b/src/NH/Commons.h new file mode 100644 index 0000000..4030c0e --- /dev/null +++ b/src/NH/Commons.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace NH +{ + using String = Union::StringUTF8; +} diff --git a/src/NH/HashString.h b/src/NH/HashString.h index 774a5c9..d9ec146 100644 --- a/src/NH/HashString.h +++ b/src/NH/HashString.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include diff --git a/src/NH/Logger.h b/src/NH/Logger.h index 1027a0e..cf843f6 100644 --- a/src/NH/Logger.h +++ b/src/NH/Logger.h @@ -1,11 +1,11 @@ #pragma once +#include #include #include namespace NH { - using String = Union::StringUTF8; enum class LoggerLevel : size_t { diff --git a/src/Plugin.cpp b/src/Plugin.cpp index 3415c54..522813a 100644 --- a/src/Plugin.cpp +++ b/src/Plugin.cpp @@ -1,9 +1,12 @@ // Disable macro redefinition warning #pragma warning(disable: 4005) +// Headers required for GOTHIC_NAMESPACE files. Don't delete. #include -#include "NH/Bass/Options.h" -#include "NH/Bass/Engine.h" +#include +#include +#include +#include #include #include