diff --git a/vst/CMakeLists.txt b/vst/CMakeLists.txt index c6f573fb..0ee8cddc 100644 --- a/vst/CMakeLists.txt +++ b/vst/CMakeLists.txt @@ -17,15 +17,32 @@ set(VSTPLUGIN_SOURCES SfizzVstController.cpp SfizzVstEditor.cpp SfizzVstState.cpp + SfizzFileScan.cpp + SfizzForeignPaths.cpp VstPluginFactory.cpp - X11RunLoop.cpp) + X11RunLoop.cpp + NativeHelpers.cpp + FileTrie.cpp) set(VSTPLUGIN_HEADERS SfizzVstProcessor.h SfizzVstController.h SfizzVstEditor.h SfizzVstState.h - X11RunLoop.h) + SfizzFileScan.h + SfizzForeignPaths.h + X11RunLoop.h + NativeHelpers.h + FileTrie.h) + +if(APPLE) + set(VSTPLUGIN_MAC_SOURCES + SfizzForeignPaths.mm + NativeHelpers.mm) + list(APPEND VSTPLUGIN_SOURCES ${VSTPLUGIN_MAC_SOURCES}) + set_property(SOURCE ${VSTPLUGIN_MAC_SOURCES} APPEND_STRING + PROPERTY COMPILE_FLAGS " -fobjc-arc") +endif() add_library(${VSTPLUGIN_PRJ_NAME} MODULE ${VSTPLUGIN_HEADERS} @@ -65,6 +82,16 @@ if (MINGW) set_target_properties (${VSTPLUGIN_PRJ_NAME} PROPERTIES LINK_FLAGS "-static") endif() +# Link system dependencies +if(WIN32) +elseif(APPLE) + target_link_libraries(${VSTPLUGIN_PRJ_NAME} PRIVATE ${APPLE_FOUNDATION_LIBRARY}) +else() + pkg_check_modules(GLIB REQUIRED glib-2.0) + target_include_directories(${VSTPLUGIN_PRJ_NAME} PRIVATE ${GLIB_INCLUDE_DIRS}) + target_link_libraries(${VSTPLUGIN_PRJ_NAME} PRIVATE ${GLIB_LIBRARIES}) +endif() + # Create the bundle (see "VST 3 Locations / Format") execute_process ( COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/Resources") diff --git a/vst/FileTrie.cpp b/vst/FileTrie.cpp new file mode 100644 index 00000000..014ad626 --- /dev/null +++ b/vst/FileTrie.cpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "FileTrie.h" +#include +#include +#include + +constexpr size_t FileTrie::npos; + +fs::path FileTrie::at(size_t index) const +{ + if (index >= entries_.size()) + throw std::out_of_range("FileTrie::at"); + return pathFromEntry(index); +} + +fs::path FileTrie::operator[](size_t index) const +{ + assert(index < entries_.size()); + return pathFromEntry(index); +} + +fs::path FileTrie::pathFromEntry(size_t index) const +{ + const Entry* currentEntry = &entries_[index]; + fs::path path = fs::u8path(currentEntry->name); + + size_t currentIndex; + while ((currentIndex = currentEntry->parent) != npos) { + currentEntry = &entries_[currentIndex]; + path = fs::u8path(currentEntry->name) / path; + } + + return path; +} + +std::ostream& operator<<(std::ostream& os, const FileTrie& trie) +{ + os << '{' << '\n'; + for (size_t i = 0, n = trie.size(); i < n; ++i) + os << '\t' << i << ':' << ' ' << trie[i] << ',' << '\n'; + os << '}'; + return os; +} + +//------------------------------------------------------------------------------ +FileTrieBuilder::FileTrieBuilder(size_t initialCapacity) +{ + FileTrie& trie = trie_; + trie.entries_.reserve(initialCapacity); +} + +FileTrie&& FileTrieBuilder::build() +{ + FileTrie& trie = trie_; + trie.entries_.shrink_to_fit(); + return std::move(trie); +} + +size_t FileTrieBuilder::addFile(const fs::path& path) +{ + if (path.empty()) + return FileTrie::npos; + + size_t dirIndex = ensureDirectory(path.parent_path()); + + FileTrie& trie = trie_; + FileTrie::Entry ent; + ent.parent = dirIndex; + ent.name = (--path.end())->u8string(); + + size_t fileIndex = trie.entries_.size(); + trie.entries_.push_back(std::move(ent)); + + return fileIndex; +} + +size_t FileTrieBuilder::ensureDirectory(const fs::path& dirPath) +{ + if (dirPath.empty()) + return FileTrie::npos; + + const fs::path::string_type& dirNat = dirPath.native(); + auto it = directories_.find(dirNat); + if (it != directories_.end()) + return it->second; + + FileTrie& trie = trie_; + FileTrie::Entry ent; + ent.parent = FileTrie::npos; + ent.name = (--dirPath.end())->u8string(); + if (dirPath.has_parent_path()) { + fs::path parentPath = dirPath.parent_path(); + if (parentPath != dirPath) + ent.parent = ensureDirectory(parentPath); + } + + size_t dirIndex = trie.entries_.size(); + trie.entries_.push_back(std::move(ent)); + + directories_[dirNat] = dirIndex; + + return dirIndex; +} diff --git a/vst/FileTrie.h b/vst/FileTrie.h new file mode 100644 index 00000000..63bf32d7 --- /dev/null +++ b/vst/FileTrie.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include +#include +#include +#include +#include + +class FileTrie { +public: + static constexpr size_t npos = ~size_t(0); + + size_t size() const noexcept { return entries_.size(); } + fs::path at(size_t index) const; + fs::path operator[](size_t index) const; + void clear() noexcept { entries_.clear(); } + +private: + fs::path pathFromEntry(size_t index) const; + +private: + struct Entry { + size_t parent = npos; + std::string name; + }; + std::vector entries_; + + friend class FileTrieBuilder; +}; + +std::ostream& operator<<(std::ostream& os, const FileTrie& trie); + +//------------------------------------------------------------------------------ +class FileTrieBuilder { +public: + explicit FileTrieBuilder(size_t initialCapacity = 8192); + FileTrie&& build(); + size_t addFile(const fs::path& path); + +private: + size_t ensureDirectory(const fs::path& dirPath); + +private: + FileTrie trie_; + std::unordered_map directories_; +}; diff --git a/vst/NativeHelpers.cpp b/vst/NativeHelpers.cpp new file mode 100644 index 00000000..2015a194 --- /dev/null +++ b/vst/NativeHelpers.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "NativeHelpers.h" +#include + +#if defined(_WIN32) +#include +#include + +const fs::path& getUserDocumentsDirectory() +{ + static const fs::path directory = []() -> fs::path { + std::unique_ptr path(new WCHAR[32768]); + if (SHGetFolderPathW(nullptr, CSIDL_PERSONAL|CSIDL_FLAG_CREATE, NULL, SHGFP_TYPE_CURRENT, path.get()) != S_OK) + throw std::runtime_error("Cannot get the document directory."); + return fs::path(path.get()); + }(); + return directory; +} +#elif defined(__APPLE__) + // implemented in NativeHelpers.mm +#else +#include + +const fs::path& getUserDocumentsDirectory() +{ + static const fs::path directory = []() -> fs::path { + const gchar* path = g_get_user_special_dir(G_USER_DIRECTORY_DOCUMENTS); + if (!path) + throw std::runtime_error("Cannot get the document directory."); + return fs::path(path); + }(); + return directory; +} +#endif diff --git a/vst/NativeHelpers.h b/vst/NativeHelpers.h new file mode 100644 index 00000000..766d01d7 --- /dev/null +++ b/vst/NativeHelpers.h @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include + +const fs::path& getUserDocumentsDirectory(); diff --git a/vst/NativeHelpers.mm b/vst/NativeHelpers.mm new file mode 100644 index 00000000..8f2bfd3d --- /dev/null +++ b/vst/NativeHelpers.mm @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "NativeHelpers.h" +#import +#include +#if !__has_feature(objc_arc) +#error This source file requires ARC +#endif + +const fs::path& getUserDocumentsDirectory() +{ + static const fs::path directory = []() -> fs::path { + NSFileManager* fm = [NSFileManager defaultManager]; + NSArray* urls = [fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; + for (NSUInteger i = 0, n = [urls count]; i < n; ++i) { + NSURL *url = [urls objectAtIndex:i]; + if ([url isFileURL]) + return fs::path([url path].UTF8String); + } + throw std::runtime_error("Cannot get the document directory."); + }(); + return directory; +} diff --git a/vst/SfizzFileScan.cpp b/vst/SfizzFileScan.cpp new file mode 100644 index 00000000..9971d6ab --- /dev/null +++ b/vst/SfizzFileScan.cpp @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "SfizzFileScan.h" +#include "SfizzForeignPaths.h" +#include "NativeHelpers.h" +#include +#include +#include +#include + +// wait at least this much before refreshing the file rescan +// it permits to not repeat the operation many times if many searches are +// requested at once, eg. on session loading with multiple plugin instances +static const std::chrono::seconds expiration_time { 10 }; + +SfzFileScan& SfzFileScan::getInstance() +{ + static SfzFileScan instance; + return instance; +} + +bool SfzFileScan::locateRealFile(const fs::path& pathOrig, fs::path& pathFound) +{ + if (pathOrig.empty()) + return false; + + std::unique_lock lock { mutex }; + refreshScan(); + + auto it = file_index_.find(keyOf(pathOrig)); + if (it == file_index_.end()) + return false; + + const std::list& candidateIndices = it->second; + std::vector candidates; + candidates.reserve(candidateIndices.size()); + for (const size_t index : candidateIndices) + candidates.push_back(file_trie_[index]); + + lock.unlock(); + + pathFound = electBestMatch(pathOrig, candidates); + return true; +} + +bool SfzFileScan::isExpired() const +{ + return !completion_time_ || + (clock::now() - *completion_time_) > expiration_time; +} + +void SfzFileScan::refreshScan(bool force) +{ + if (!force && !isExpired()) + return; + + file_trie_.clear(); + file_index_.clear(); + + FileTrieBuilder builder; + + for (const fs::path& dirPath : SfizzPaths::sfzDefaultPaths()) { + std::error_code ec; + const fs::directory_options dirOpts = + fs::directory_options::skip_permission_denied; + + for (fs::recursive_directory_iterator it(dirPath, dirOpts, ec); + !ec && it != fs::recursive_directory_iterator(); + it.increment(ec)) + { + const fs::directory_entry& ent = *it; + const fs::path& filePath = ent.path(); + std::error_code ec; + if (ent.is_regular_file(ec) /*&& pathIsSfz(filePath)*/) { + size_t fileIndex = builder.addFile(filePath); + file_index_[keyOf(filePath.filename())].push_back(fileIndex); + } + } + } + + file_trie_ = builder.build(); + completion_time_ = clock::now(); +} + +std::string SfzFileScan::keyOf(const fs::path& path) +{ + std::string key = path.u8string(); + absl::AsciiStrToLower(&key); + return key; +} + +namespace SfzFileScanImpl { +template +bool asciiCaseEqual(const std::basic_string& a, const std::basic_string& b) +{ + const size_t n = a.size(); + if (n != b.size()) + return false; + + auto lower = [](T c) -> T { + return (c >= T('A') && c <= T('Z')) ? (c - T('A') + T('a')) : c; + }; + + for (size_t i = 0; i < n; ++i) + if (lower(a[i]) != lower(b[i])) + return false; + + return true; +} +} // namespace SfzFileScanImpl + +bool SfzFileScan::pathIsSfz(const fs::path& path) +{ + const fs::path::string_type& str = path.native(); + using char_type = fs::path::value_type; + const size_t n = str.size(); + return n > 4 && + str[n - 4] == char_type('.') && + (str[n - 3] == char_type('s') || str[n - 3] == char_type('S')) && + (str[n - 2] == char_type('f') || str[n - 2] == char_type('F')) && + (str[n - 1] == char_type('z') || str[n - 1] == char_type('Z')); +} + +const fs::path& SfzFileScan::electBestMatch(const fs::path& path, absl::Span candidates) +{ + if (candidates.empty()) + return path; + + if (candidates.size() == 1) + return candidates.front(); + + struct Score { + size_t components = 0; + size_t exact = 0; + bool operator<(const Score& other) const noexcept + { + return (components != other.components) ? + (components < other.components) : (exact < other.exact); + } + }; + + std::vector scores; + scores.reserve(candidates.size()); + + for (size_t i = 0, n = candidates.size(); i < n; ++i) { + scores.emplace_back(); + Score& score = scores.back(); + + const fs::path& p1 = path; + const fs::path& p2 = candidates[i]; + auto it1 = p1.end(); + auto it2 = p2.end(); + + bool matching = true; + while (matching && it1-- != p1.begin() && it2-- != p2.begin()) { + const fs::path& c1 = *it1; + const fs::path& c2 = *it2; + if (c1 == c2) { + score.components += 1; + score.exact += 1; + } + else if (SfzFileScanImpl::asciiCaseEqual(c1.native(), c2.native())) + score.components += 1; + else + matching = false; + } + } + + size_t best = 0; + for (size_t i = 1, n = scores.size(); i < n; ++i) { + if (scores[best] < scores[i]) + best = i; + } + + return candidates[best]; +} + +//------------------------------------------------------------------------------ + +namespace SfizzPaths { + +absl::Span sfzDefaultPaths() +{ + static const auto paths = []() -> std::vector { + std::vector paths; + paths.reserve(8); + auto addPath = [&paths](const fs::path& newPath) { + if (absl::c_find(paths, newPath) == paths.end()) + paths.push_back(newPath); + }; + + addPath(getUserDocumentsDirectory() / "SFZ instruments"); + + for (const fs::path& foreign : { + getAriaPathSetting("user_files_dir"), + getAriaPathSetting("Converted_path") }) + if (!foreign.empty() && foreign.is_absolute()) + addPath(foreign); + + paths.shrink_to_fit(); + return paths; + }(); + return paths; +} + +void createSfzDefaultPaths() +{ + for (const fs::path& path : sfzDefaultPaths()) { + std::error_code ec; + fs::create_directory(path, ec); + } +} + +} // namespace SfizzPaths diff --git a/vst/SfizzFileScan.h b/vst/SfizzFileScan.h new file mode 100644 index 00000000..2626ef15 --- /dev/null +++ b/vst/SfizzFileScan.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "FileTrie.h" +#include +#include +#include +#include +#include +#include +#include +#include + +class SfzFileScan { +public: + static SfzFileScan& getInstance(); + bool locateRealFile(const fs::path& pathOrig, fs::path& pathFound); + +private: + typedef std::chrono::steady_clock clock; + std::mutex mutex; + absl::optional completion_time_; + FileTrie file_trie_; + std::unordered_map> file_index_; + + bool isExpired() const; + void refreshScan(bool force = false); + static std::string keyOf(const fs::path& path); + static bool pathIsSfz(const fs::path& path); + static const fs::path& electBestMatch(const fs::path& path, absl::Span candidates); +}; + +namespace SfizzPaths { +absl::Span sfzDefaultPaths(); +void createSfzDefaultPaths(); +} // namespace SfizzPaths diff --git a/vst/SfizzForeignPaths.cpp b/vst/SfizzForeignPaths.cpp new file mode 100644 index 00000000..17319997 --- /dev/null +++ b/vst/SfizzForeignPaths.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "SfizzForeignPaths.h" + +namespace SfizzPaths { + +#if defined(_WIN32) +#include + +fs::path getAriaPathSetting(const char* name) +{ + fs::path path; + + HKEY key = 0; + + std::unique_ptr nameW; + unsigned nameSize = MultiByteToWideChar(CP_UTF8, 0, name, -1, nullptr, 0); + if (nameSize == 0) + return {}; + nameW.reset(new WCHAR[nameSize]); + if (MultiByteToWideChar(CP_UTF8, 0, name, -1, nameW.get(), nameSize) == 0) + return {}; + + const WCHAR ariaKeyPath[] = L"Software\\Plogue Art et Technologie, Inc\\Aria"; + + if (RegOpenKeyExW(HKEY_CURRENT_USER, ariaKeyPath, 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) { + WCHAR valueBuffer[32768 + 1]; + DWORD valueSize = sizeof(valueBuffer) - sizeof(WCHAR); + if (RegQueryValueExW(key, nameW.get(), nullptr, nullptr, reinterpret_cast(valueBuffer), &valueSize) == ERROR_SUCCESS) { + valueBuffer[32768] = L'\0'; + path = fs::path(valueBuffer); + } + RegCloseKey(key); + } + + return path; +} +#elif defined(__APPLE__) + // implementation in SfizzForeignPaths.mm +#else +fs::path getAriaPathSetting(const char* name) +{ + return {}; +} +#endif + +} // namespace SfizzPaths diff --git a/vst/SfizzForeignPaths.h b/vst/SfizzForeignPaths.h new file mode 100644 index 00000000..02f79b42 --- /dev/null +++ b/vst/SfizzForeignPaths.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include + +namespace SfizzPaths { +fs::path getAriaPathSetting(const char* name); +} // namespace SfizzPaths diff --git a/vst/SfizzForeignPaths.mm b/vst/SfizzForeignPaths.mm new file mode 100644 index 00000000..074a3ece --- /dev/null +++ b/vst/SfizzForeignPaths.mm @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "SfizzForeignPaths.h" + +#if defined(__APPLE__) +#import +#if !__has_feature(objc_arc) +#error This source file requires ARC +#endif + +namespace SfizzPaths { + +fs::path getAriaPathSetting(const char* name) +{ + NSUserDefaults* ud = [[NSUserDefaults alloc] initWithSuiteName:@"com.plogue.aria"]; + NSString* value = [ud stringForKey:[NSString stringWithUTF8String:name]]; + if (!value) + return {}; + return fs::path(value.UTF8String); +} + +} // namespace SfizzPaths +#endif diff --git a/vst/SfizzVstProcessor.cpp b/vst/SfizzVstProcessor.cpp index 6f729a27..3825db65 100644 --- a/vst/SfizzVstProcessor.cpp +++ b/vst/SfizzVstProcessor.cpp @@ -7,9 +7,11 @@ #include "SfizzVstProcessor.h" #include "SfizzVstController.h" #include "SfizzVstState.h" +#include "SfizzFileScan.h" #include "base/source/fstreamer.h" #include "pluginterfaces/vst/ivstevents.h" #include "pluginterfaces/vst/ivstparameterchanges.h" +#include #include template @@ -28,6 +30,8 @@ SfizzVstProcessor::SfizzVstProcessor() : _fifoToWorker(64 * 1024), _fifoMidiFromUi(64 * 1024) { setControllerClass(SfizzVstController::cid); + + SfizzPaths::createSfzDefaultPaths(); } SfizzVstProcessor::~SfizzVstProcessor() @@ -83,6 +87,29 @@ tresult PLUGIN_API SfizzVstProcessor::setState(IBStream* stream) if (r != kResultTrue) return r; + // check the files to really exist, otherwise search them + for (std::string* statePath : { &s.sfzFile, &s.scalaFile }) { + if (statePath->empty()) + continue; + + fs::path pathOrig = fs::u8path(*statePath); + std::error_code ec; + if (fs::is_regular_file(pathOrig, ec)) + continue; + + fprintf(stderr, "[Sfizz] searching for missing file: %s\n", pathOrig.filename().u8string().c_str()); + + SfzFileScan& fileScan = SfzFileScan::getInstance(); + fs::path pathFound; + if (!fileScan.locateRealFile(pathOrig, pathFound)) + fprintf(stderr, "[Sfizz] file not found: %s\n", pathOrig.filename().u8string().c_str()); + else { + fprintf(stderr, "[Sfizz] file found: %s\n", pathFound.filename().u8string().c_str()); + *statePath = pathFound.u8string(); + } + } + + // std::lock_guard lock(_processMutex); _state = s;