From b77ff0447535faf2f07d458be24de6d50aaf5557 Mon Sep 17 00:00:00 2001 From: codereader Date: Sun, 4 Oct 2020 08:36:19 +0200 Subject: [PATCH] #5345: Improve TaskQueue and start working on SoundManager::getSoundFileDuration --- include/isound.h | 4 + libs/TaskQueue.h | 47 ++++++--- plugins/sound/SoundManager.cpp | 122 ++++++++++++++--------- plugins/sound/SoundManager.h | 1 + plugins/sound/WavFileLoader.h | 40 ++------ radiant/uimanager/SoundShaderPreview.cpp | 77 +++++++++++++- radiant/uimanager/SoundShaderPreview.h | 20 +++- 7 files changed, 217 insertions(+), 94 deletions(-) diff --git a/include/isound.h b/include/isound.h index ca78d6ad66..b5d8ba8f93 100644 --- a/include/isound.h +++ b/include/isound.h @@ -121,6 +121,10 @@ class ISoundManager : */ virtual void stopSound() = 0; + // Returns the duration of the given sound file in seconds + // Will throw a std::out_of_range exception if the path cannot be resolved + virtual float getSoundFileDuration(const std::string& vfsPath) = 0; + // Reloads all sound shader definitions from the VFS virtual void reloadSounds() = 0; diff --git a/libs/TaskQueue.h b/libs/TaskQueue.h index 5be601cc1c..23ee854a24 100644 --- a/libs/TaskQueue.h +++ b/libs/TaskQueue.h @@ -11,11 +11,12 @@ namespace util class TaskQueue { private: - std::mutex _lock; - + mutable std::mutex _queueLock; std::list> _queue; + mutable std::mutex _currentLock; std::future _current; + std::future _finished; public: ~TaskQueue() @@ -28,7 +29,7 @@ class TaskQueue void enqueue(const std::function& task) { { - std::lock_guard lock(_lock); + std::lock_guard lock(_queueLock); _queue.push_front(task); } @@ -44,38 +45,60 @@ class TaskQueue { { // Lock the queue and remove any tasks such that no new ones are added - std::lock_guard lock(_lock); + std::lock_guard lock(_queueLock); _queue.clear(); } - // Clear (and possibly) wait for the currently active future object _current = std::future(); + _finished = std::future(); } private: bool isIdle() const { - return !_current.valid() || - _current.wait_for(std::chrono::seconds(0)) == std::future_status::ready; + std::lock_guard lock(_currentLock); + return !_current.valid() || _current.wait_for(std::chrono::seconds(0)) == std::future_status::ready; } - void startNextTask() + std::function dequeueOne() { - std::lock_guard lock(_lock); + std::lock_guard lock(_queueLock); if (_queue.empty()) { - return; + return std::function(); } // No active task, dispatch a new one auto frontOfQueue = _queue.front(); _queue.pop_front(); + return frontOfQueue; + } + + void startNextTask() + { + auto task = dequeueOne(); + + if (!task) + { + return; + } + // Wrap the given task in our own lambda to start the next task right afterwards - _current = std::async(std::launch::async, [this, frontOfQueue]() + std::lock_guard lock(_currentLock); + _current = std::async(std::launch::async, [this, task]() { - frontOfQueue(); + task(); + + { + // Move our own task to the finished lane, + // to avoid blocking when assigning a new future + std::lock_guard lock(_currentLock); + _finished = std::move(_current); + } + + // _current is now empty, so we can start a new task startNextTask(); }); } diff --git a/plugins/sound/SoundManager.cpp b/plugins/sound/SoundManager.cpp index 0c093a056d..c6b77e89e6 100644 --- a/plugins/sound/SoundManager.cpp +++ b/plugins/sound/SoundManager.cpp @@ -5,13 +5,61 @@ #include "icommandsystem.h" #include "debugging/ScopedDebugTimer.h" +#include "os/path.h" +#include "string/case_conv.h" #include #include "itextstream.h" +#include "WavFileLoader.h" + namespace sound { +namespace +{ + +// Load the given file, trying different extensions (first OGG, then WAV) as fallback +ArchiveFilePtr openSoundFile(const std::string& fileName) +{ + // Make a copy of the filename + std::string name = fileName; + + // Try to open the file as it is + auto file = GlobalFileSystem().openFile(name); + + if (file) + { + // File found, play it + return file; + } + + std::string root = name; + + // File not found, try to strip the extension + if (name.rfind(".") != std::string::npos) + { + root = name.substr(0, name.rfind(".")); + } + + // Try to open the .ogg variant + name = root + ".ogg"; + + file = GlobalFileSystem().openFile(name); + + if (file) + { + return file; + } + + // Try to open the file with .wav extension + name = root + ".wav"; + + return GlobalFileSystem().openFile(name); +} + +} + // Constructor SoundManager::SoundManager() : _defLoader(std::bind(&SoundManager::loadShadersFromFilesystem, this)), @@ -36,58 +84,15 @@ bool SoundManager::playSound(const std::string& fileName) bool SoundManager::playSound(const std::string& fileName, bool loopSound) { - // Make a copy of the filename - std::string name = fileName; - - // Try to open the file as it is - ArchiveFilePtr file = GlobalFileSystem().openFile(name); - rConsole() << "Trying: " << name << std::endl; - - if (file) - { - // File found, play it - rConsole() << "Found file: " << name << std::endl; - if (_soundPlayer) _soundPlayer->play(*file, loopSound); - return true; - } - - std::string root = name; - - // File not found, try to strip the extension - if (name.rfind(".") != std::string::npos) - { - root = name.substr(0, name.rfind(".")); - } - - // Try to open the .ogg variant - name = root + ".ogg"; - - rConsole() << "Trying: " << name << std::endl; - - file = GlobalFileSystem().openFile(name); - - if (file) - { - rConsole() << "Found file: " << name << std::endl; - if (_soundPlayer) _soundPlayer->play(*file, loopSound); - return true; - } - - // Try to open the file with .wav extension - name = root + ".wav"; - rConsole() << "Trying: " << name << std::endl; - - file = GlobalFileSystem().openFile(name); + auto file = openSoundFile(fileName); - if (file) + if (file && _soundPlayer) { - rConsole() << "Found file: " << name << std::endl; - if (_soundPlayer) _soundPlayer->play(*file, loopSound); + _soundPlayer->play(*file, loopSound); return true; } - // File not found - return false; + return false; } void SoundManager::stopSound() @@ -172,6 +177,29 @@ void SoundManager::initialiseModule(const IApplicationContext& ctx) _defLoader.start(); } +float SoundManager::getSoundFileDuration(const std::string& vfsPath) +{ + auto file = openSoundFile(vfsPath); + + if (!file) + { + throw std::out_of_range("Could not resolve sound file " + vfsPath); + } + + auto extension = string::to_lower_copy(os::getExtension(file->getName())); + + if (extension == "wav") + { + return WavFileLoader::GetDuration(file->getInputStream()); + } + else if (extension == "ogg") + { + return 23.45f; + } + + return 0.0f; +} + void SoundManager::reloadSounds() { _defLoader.reset(); diff --git a/plugins/sound/SoundManager.h b/plugins/sound/SoundManager.h index 14110d5cf1..0460fb2a3d 100644 --- a/plugins/sound/SoundManager.h +++ b/plugins/sound/SoundManager.h @@ -50,6 +50,7 @@ class SoundManager : bool playSound(const std::string& fileName, bool loopSound) override; void stopSound() override; void reloadSounds() override; + float getSoundFileDuration(const std::string& vfsPath) override; sigc::signal& signal_soundShadersReloaded() override; // RegisterableModule implementation diff --git a/plugins/sound/WavFileLoader.h b/plugins/sound/WavFileLoader.h index a553b1d24a..f9c0c2c169 100644 --- a/plugins/sound/WavFileLoader.h +++ b/plugins/sound/WavFileLoader.h @@ -22,6 +22,10 @@ namespace sound { class WavFileLoader { public: + static float GetDuration(InputStream& stream) + { + return -1.0f; + } /** * greebo: Loads a WAV file from a stream into OpenAL, @@ -94,37 +98,13 @@ class WavFileLoader unsigned short bps = 0; stream.read(reinterpret_cast(&bps), 2); - //int bufferSize = 0; - - if (channels == 1) { - if (bps == 8) { - format = AL_FORMAT_MONO8; - // Set BufferSize to 250ms (Frequency divided by 4 (quarter of a second)) - //bufferSize = freq / 4; - } - else { - format = AL_FORMAT_MONO16; - // Set BufferSize to 250ms (Frequency * 2 (16bit) divided by 4 (quarter of a second)) - //bufferSize = freq >> 1; - // IMPORTANT : The Buffer Size must be an exact multiple of the BlockAlignment ... - //bufferSize -= (bufferSize % 2); - } + if (channels == 1) + { + format = bps == 8 ? AL_FORMAT_MONO8 : AL_FORMAT_MONO16; } - else { - if (bps == 8) { - format = AL_FORMAT_STEREO16; - // Set BufferSize to 250ms (Frequency * 2 (8bit stereo) divided by 4 (quarter of a second)) - //bufferSize = freq >> 1; - // IMPORTANT : The Buffer Size must be an exact multiple of the BlockAlignment ... - //bufferSize -= (bufferSize % 2); - } - else { - format = AL_FORMAT_STEREO16; - // Set BufferSize to 250ms (Frequency * 4 (16bit stereo) divided by 4 (quarter of a second)) - //bufferSize = freq; - // IMPORTANT : The Buffer Size must be an exact multiple of the BlockAlignment ... - //bufferSize -= (bufferSize % 4); - } + else + { + format = bps == 8 ? AL_FORMAT_STEREO16 : AL_FORMAT_STEREO16; } // check 'data' sub chunk (2) diff --git a/radiant/uimanager/SoundShaderPreview.cpp b/radiant/uimanager/SoundShaderPreview.cpp index cbee59608b..af2bf6ceff 100644 --- a/radiant/uimanager/SoundShaderPreview.cpp +++ b/radiant/uimanager/SoundShaderPreview.cpp @@ -13,9 +13,19 @@ #include #include +#include "ui/UserInterfaceModule.h" + namespace ui { +namespace +{ + inline std::string getDurationString(float durationInSeconds) + { + return fmt::format("{0:0.2f}s", durationInSeconds); + } +} + SoundShaderPreview::SoundShaderPreview(wxWindow* parent) : wxPanel(parent, wxID_ANY), _listStore(new wxutil::TreeModel(_columns, true)), @@ -26,8 +36,10 @@ SoundShaderPreview::SoundShaderPreview(wxWindow* parent) : _treeView = wxutil::TreeView::CreateWithModel(this, _listStore); _treeView->SetMinClientSize(wxSize(-1, 130)); - _treeView->AppendTextColumn(_("Sound Files"), _columns.shader.getColumnIndex(), + _treeView->AppendTextColumn(_("Sound Files"), _columns.soundFile.getColumnIndex(), wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_NOT, wxDATAVIEW_COL_SORTABLE); + _treeView->AppendTextColumn(_("Duration"), _columns.duration.getColumnIndex(), + wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_NOT, wxDATAVIEW_COL_SORTABLE); // Connect the "changed" signal _treeView->Bind(wxEVT_DATAVIEW_SELECTION_CHANGED, &SoundShaderPreview::onSelectionChanged, this); @@ -126,6 +138,8 @@ void SoundShaderPreview::update() // Clear the current treeview model _listStore->Clear(); + _durationQueries.clear(); + // If the soundshader string is empty, desensitise the widgets Enable(!_soundShader.empty()); @@ -144,8 +158,10 @@ void SoundShaderPreview::update() for (std::size_t i = 0; i < list.size(); ++i) { auto row = _listStore->AddItem(); + const auto& soundFile = list[i]; - row[_columns.shader] = list[i]; + row[_columns.soundFile] = soundFile; + row[_columns.duration] = getDurationOrPlaceholder(soundFile); row.SendItemAdded(); @@ -185,7 +201,7 @@ std::string SoundShaderPreview::getSelectedSoundFile() if (item.IsOk()) { wxutil::TreeModel::Row row(item, *_listStore); - return row[_columns.shader]; + return row[_columns.soundFile]; } else { @@ -246,4 +262,59 @@ void SoundShaderPreview::onStop(wxCommandEvent& ev) _statusLabel->SetLabel(""); } +std::string SoundShaderPreview::getDurationOrPlaceholder(const std::string& soundFile) +{ + { + std::lock_guard lock(_durationsLock); + + auto found = _durations.find(soundFile); + + if (found != _durations.end()) + { + return getDurationString(found->second); + } + } + + // No duration known yet, queue a task + loadFileDurationAsync(soundFile); + return "--:--"; +} + +void SoundShaderPreview::loadFileDurationAsync(const std::string& soundFile) +{ + _durationQueries.enqueue([this, soundFile] // copy strings into lambda + { + try + { + // Query + auto duration = GlobalSoundManager().getSoundFileDuration(soundFile); + + // Store the duration in the local cache + { + std::lock_guard lock(_durationsLock); + _durations[soundFile] = duration; + } + + // Dispatch to UI thread when we're done + GetUserInterfaceModule().dispatch([this, soundFile, duration]() + { + // Load into treeview + auto item = _listStore->FindString(soundFile, _columns.soundFile); + + if (item.IsOk()) + { + wxutil::TreeModel::Row row(item, *_listStore); + row[_columns.duration] = getDurationString(duration); + row.SendItemChanged(); + } + }); + } + catch (const std::out_of_range& ex) + { + rError() << "Cannot query sound file duration of " << soundFile + << ": " << ex.what() << std::endl; + } + }); +} + } // namespace ui diff --git a/radiant/uimanager/SoundShaderPreview.h b/radiant/uimanager/SoundShaderPreview.h index d9abd101ba..1e03bf50e2 100644 --- a/radiant/uimanager/SoundShaderPreview.h +++ b/radiant/uimanager/SoundShaderPreview.h @@ -1,9 +1,12 @@ #pragma once #include +#include +#include #include #include "wxutil/TreeModel.h" #include "wxutil/TreeView.h" +#include "TaskQueue.h" #include @@ -44,14 +47,24 @@ class SoundShaderPreview : public wxutil::TreeModel::ColumnRecord { SoundListColumns() : - shader(add(wxutil::TreeModel::Column::String)) + soundFile(add(wxutil::TreeModel::Column::String)), + duration(add(wxutil::TreeModel::Column::String)) {} - wxutil::TreeModel::Column shader; // soundshader name + wxutil::TreeModel::Column soundFile; // soundFile path + wxutil::TreeModel::Column duration; // duration }; SoundListColumns _columns; + std::mutex _durationsLock; + + // Already known durations + std::map _durations; + + // Sound file lengths are queried asynchronously + util::TaskQueue _durationQueries; + public: SoundShaderPreview(wxWindow* parent); @@ -90,6 +103,9 @@ class SoundShaderPreview : void playSelectedFile(bool loop); void handleSelectionChange(); + + void loadFileDurationAsync(const std::string& soundFile); + std::string getDurationOrPlaceholder(const std::string& soundFile); }; } // namespace ui