diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index a0f3d56..6c60969 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -16,7 +16,9 @@ "cStandard": "c17", "cppStandard": "c++20", "intelliSenseMode": "linux-gcc-arm64", - "compileCommands": "${workspaceFolder}/build/compile_commands.json", + "compileCommands": [ + "${workspaceFolder}/build/compile_commands.json" + ], "configurationProvider": "ms-vscode.cmake-tools" }, { @@ -33,7 +35,9 @@ "cStandard": "c17", "cppStandard": "c++20", "intelliSenseMode": "linux-gcc-x64", - "compileCommands": "${workspaceFolder}/build/compile_commands.json", + "compileCommands": [ + "${workspaceFolder}/build/compile_commands.json" + ], "configurationProvider": "ms-vscode.cmake-tools", "mergeConfigurations": true } diff --git a/.vscode/launch.json b/.vscode/launch.json index 4b516f5..3dd766a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,6 @@ // Resolved by CMake Tools: "program": "${command:cmake.launchTargetPath}", "args": [ - "--no-sudo" ], "stopAtEntry": false, "cwd": "${workspaceFolder}", diff --git a/.vscode/settings.json b/.vscode/settings.json index 8e97e94..f7d1460 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -102,7 +102,8 @@ "format": "cpp", "locale": "cpp", "stdfloat": "cpp", - "text_encoding": "cpp" + "text_encoding": "cpp", + "forward_list": "cpp" }, "cSpell.words": [ "Alsa", diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index bcc4eab..5bfa3e1 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -1809,6 +1809,9 @@ namespace pipedal { inputSysexBuffer.resize(1024); } + ~AlsaMidiDeviceImpl() { + Close(); + } void Open(const AlsaMidiDeviceInfo &device) { runningStatus = 0; diff --git a/src/AudioFileMetadata.cpp b/src/AudioFileMetadata.cpp new file mode 100644 index 0000000..5bbb4b8 --- /dev/null +++ b/src/AudioFileMetadata.cpp @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "AudioFileMetadata.hpp" +#include +#include +#include +#include +#include "LRUCache.hpp" +#include + +using namespace pipedal; +namespace fs = std::filesystem; +namespace chrono = std::chrono; + + +// Safely encode a filename for use in a shell command +static std::string shell_escape_filename(const std::string& filename) { + std::string escaped; + escaped.reserve(filename.size() + 2); // Reserve space for quotes and content + + // Wrap the filename in single quotes + escaped += '\''; + + for (char c : filename) { + // Escape single quotes by closing the quote, adding escaped quote, and reopening + if (c == '\'') { + escaped += "'\\''"; + } else { + escaped += c; + } + } + + escaped += '\''; + return escaped; +} + + +static std::string readPipeToString(FILE* file) { + std::string content; + char buffer[4096]; + size_t bytesRead; + while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) { + content.append(buffer, bytesRead); + } + return content; +} + + +static std::string GetJsonMetadata(const std::filesystem::path &path) +{ + /* ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json + ~/filename.flac */ + + std::stringstream ss; + ss << "/usr/bin/ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json " + << shell_escape_filename(path.string()) + << " 2>/dev/null"; + + FILE *output = popen(ss.str().c_str(),"r"); + if(output == nullptr) + { + throw std::runtime_error("Failed to process file."); + } + std::string result; + try { + result = readPipeToString(output); + } catch (const std::exception&e) + { + pclose(output); + throw std::runtime_error(e.what()); + } + int retcode = pclose(output); + if (retcode != 0) + { + throw std::runtime_error(result); + } + return result; +} + +static float MetadataFloat(const json_variant&vt, float defaultValue = 0) +{ + if (vt.is_string()) + { + std::string s = vt.as_string(); + std::stringstream ss(s); + float result = defaultValue; + ss >> result; + return result; + } + return defaultValue; +} + +static std::string MetadataString(const json_variant&o,const std::vector& names) { + for (const auto&name: names) + { + auto result = (*o.as_object())[name]; + if (result.is_string()) + { + return result.as_string(); + } + } + return ""; +} + +AudioFileMetadata::AudioFileMetadata(const std::filesystem::path &file) +{ + try { + const std::string json = GetJsonMetadata(file); + + json_variant vt; + + std::stringstream ss(json); + json_reader reader(ss); + + reader.read(&vt); + + auto top = vt.as_object(); + auto format = top->at("format").as_object(); + + this->duration_ = MetadataFloat(format->at("duration"),0); + + auto tags = (*format)["tags"]; + if (tags.is_object()) + { + this->album_ = MetadataString(tags,{"ALBUM","album"}); + this->artist_ = MetadataString(tags,{"ARTIST","artist"}); + this->albumArtist_ = MetadataString(tags,{"ALBUM ARTIST","album_artist","album artist"}); + this->title_ = MetadataString(tags,{"TITLE","title"}); + this->date_ = MetadataString(tags,{"DATE","date"}); + this->year_ = MetadataString(tags,{"YEAR","year"}); + this->track_ = MetadataString(tags,{"track","TRACK",}); + this->disc_ = MetadataString(tags,{"disc","DISC"}); + + if (title_ == "") { + this->title_ = file.stem(); + } + this->totalTracks_ = MetadataString(tags,{"TOTALTRACKS"}); + } + } catch (const std::exception &e) + { + std::string title = file.stem(); + this->title_ = title; + } +} + +namespace { + class AudioCacheKey { + public: + AudioCacheKey(const std::filesystem::path &path) { + this->path = path.string(); + this->lastWrite = std::filesystem::last_write_time(path); + + } + // Equality operator + bool operator==(const AudioCacheKey& other) const { + return path == other.path && lastWrite == other.lastWrite; + } + + public: + std::string path; + fs::file_time_type lastWrite; + }; +} + +namespace std { + template <> + struct hash { + + + size_t operator()(const AudioCacheKey& key) const { + size_t path_hash = hash{}(key.path); + size_t time_hash = hash{}( + duration_cast(key.lastWrite.time_since_epoch()).count() + ); + // Combine hashes (boost::hash_combine approach) + return path_hash ^ (time_hash + 0x9e3779b9 + (path_hash << 6) + (path_hash >> 2)); + } + + + }; +} + + +static LRUCache metadataCache {500}; +static std::mutex metadataCacheMutex; + +std::string pipedal::GetAudioFileMetadataString(const std::filesystem::path &path) +{ + std::lock_guard lock { metadataCacheMutex}; + AudioCacheKey key { path}; + std::string result; + if (metadataCache.get(key,result)) + { + return result; + } + auto metadata = AudioFileMetadata(path); + + std::stringstream ss; + json_writer writer(ss); + writer.write(metadata); + result = ss.str(); + + metadataCache.put(key,result); + return result; +} + + +JSON_MAP_BEGIN(AudioFileMetadata) +JSON_MAP_REFERENCE(AudioFileMetadata, duration) +JSON_MAP_REFERENCE(AudioFileMetadata, title) +JSON_MAP_REFERENCE(AudioFileMetadata, track) +JSON_MAP_REFERENCE(AudioFileMetadata, album) +JSON_MAP_REFERENCE(AudioFileMetadata, disc) +JSON_MAP_REFERENCE(AudioFileMetadata, artist) +JSON_MAP_REFERENCE(AudioFileMetadata, albumArtist) +JSON_MAP_REFERENCE(AudioFileMetadata, totalTracks) +JSON_MAP_REFERENCE(AudioFileMetadata, date) +JSON_MAP_REFERENCE(AudioFileMetadata, year) + +JSON_MAP_END() diff --git a/src/AudioFileMetadata.hpp b/src/AudioFileMetadata.hpp new file mode 100644 index 0000000..f755c75 --- /dev/null +++ b/src/AudioFileMetadata.hpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include +#include +#include "json.hpp" + + +namespace pipedal +{ + +#define GETTER_SETTER_REF(name) \ + const decltype(name##_) &name() const { return name##_; } \ + void name(const decltype(name##_) &value) { name##_ = value; } + +#define GETTER_SETTER_VEC(name) \ + decltype(name##_) &name() { return name##_; } \ + const decltype(name##_) &name() const { return name##_; } + + // void name(decltype(const name##_) &value) { name##_ = value; } + // void name(decltype(const name##_) &&value) { name##_ = std::move(value); } + +#define GETTER_SETTER(name) \ + decltype(name##_) name() const { return name##_; } \ + void name(decltype(name##_) value) { name##_ = value; } + + class AudioFileMetadata + { + private: + float duration_ = 0; + std::string title_; + std::string track_; + std::string album_; + std::string disc_; + std::string artist_; + std::string albumArtist_; + std::string totalTracks_; + std::string date_; + std::string year_; + + + public: + AudioFileMetadata(const std::filesystem::path &file); + + GETTER_SETTER(duration); + GETTER_SETTER_REF(title); + GETTER_SETTER_REF(track); + GETTER_SETTER_REF(album); + GETTER_SETTER_REF(disc); + GETTER_SETTER_REF(artist); + GETTER_SETTER_REF(albumArtist); + + public: + DECLARE_JSON_MAP(AudioFileMetadata); + + }; + + std::string GetAudioFileMetadataString(const std::filesystem::path &path); + +#undef GETTER_SETTER +#undef GETTER_SETTER_VEC +#undef GETTER_SETTER_REF + +} diff --git a/src/AudioFiles.cpp b/src/AudioFiles.cpp new file mode 100644 index 0000000..ee3922c --- /dev/null +++ b/src/AudioFiles.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + + #include "AudioFiles.hpp" + #include + #include + + #include + + + using namespace pipedal; + + +static constexpr size_t INVALID_INDEX = std::numeric_limits::max(); + + namespace fs = std::filesystem; + + namespace { + class DirectoryList { + public: + DirectoryList(const fs::path&path); + + const std::vector &files() { + return files_; + } + private: + + std::vector files_; + }; + + class AudioFileInfoImpl: public AudioFileInfo { + public: + virtual ~AudioFileInfoImpl() { } + protected: + virtual std::string GetNextAudioFile(const std::string&path) override; + virtual std::string GetPreviousAudioFile(const std::string&path) override; + }; + ///////////////////// + + DirectoryList::DirectoryList(const fs::path&path) + { + for (const auto&dirEntry: fs::directory_iterator(path)) + { + if (dirEntry.is_regular_file()) + { + fs::path t = dirEntry.path(); + if (!t.filename().string().starts_with('.')) + { + files_.push_back(std::move(t)); + } + } + } + } + + std::string AudioFileInfoImpl::GetNextAudioFile(const std::string&path_) { + try + { + fs::path path {path_}; + if (fs::exists(path) && fs::is_regular_file(path)) + { + DirectoryList directoryList(path.parent_path()); + size_t index = INVALID_INDEX; + auto &files = directoryList.files(); + for (size_t i= 0; i < files.size(); ++i) + { + if (files[i] == path) + { + size_t next = i; + if (next == files.size()-1) + { + next = 0; + } else { + ++next; + } + return files[next]; + } + } + + } + } catch (const std::exception&e) + { + + } + return ""; + + } + std::string AudioFileInfoImpl::GetPreviousAudioFile(const std::string&path_) + { + try + { + fs::path path {path_}; + if (fs::exists(path) && fs::is_regular_file(path)) + { + DirectoryList directoryList(path.parent_path()); + size_t index = INVALID_INDEX; + auto &files = directoryList.files(); + for (size_t i= 0; i < files.size(); ++i) + { + if (files[i] == path) + { + size_t previous = i; + if (previous == 0) + { + previous = files.size()-1; + } else { + --previous; + } + return files[previous]; + } + } + + } + } catch (const std::exception&e) + { + + } + return ""; + + } + + } + + static AudioFileInfoImpl instance; + + AudioFileInfo&AudioFileInfo::GetInstance() + { + return instance; + } \ No newline at end of file diff --git a/src/AudioFiles.hpp b/src/AudioFiles.hpp new file mode 100644 index 0000000..05401b2 --- /dev/null +++ b/src/AudioFiles.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include + +namespace pipedal { + class AudioFileInfo { + protected: + AudioFileInfo() { } + virtual ~AudioFileInfo() {} + public: + static AudioFileInfo&GetInstance(); + + virtual std::string GetNextAudioFile(const std::string&path) = 0; + virtual std::string GetPreviousAudioFile(const std::string&path) = 0; + }; +} \ No newline at end of file diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 2e6a76e..8270400 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -1173,7 +1173,7 @@ private: if (buffersValid) { - pedalboard->ProcessParameterRequests(pParameterRequests); + pedalboard->ProcessParameterRequests(pParameterRequests,nframes); processed = pedalboard->Run(inputBuffers, outputBuffers, (uint32_t)nframes, &realtimeWriter); if (processed) diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index 9942042..6868520 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -69,6 +69,7 @@ namespace pipedal const char *errorMessage = nullptr; std::string jsonResponse; + int64_t sampleTimeout = 0; RealtimePatchPropertyRequest *pNext = nullptr; @@ -102,13 +103,16 @@ namespace pipedal int64_t instanceId_, LV2_URID uridUri_, std::function onSuccess_, - std::function onError_) + std::function onError_, + size_t sampleTimeout) : onPatchRequestComplete(onPatchRequestcomplete_), clientId(clientId_), instanceId(instanceId_), uridUri(uridUri_), onSuccess(onSuccess_), - onError(onError_) + onError(onError_), + sampleTimeout((int64_t)sampleTimeout) + { requestType = RequestType::PatchGet; } @@ -119,13 +123,15 @@ namespace pipedal LV2_URID uridUri_, LV2_Atom *atomValue, std::function onSuccess_, - std::function onError_) + std::function onError_, + size_t sampleTimeout) : onPatchRequestComplete(onPatchRequestcomplete_), clientId(clientId_), instanceId(instanceId_), uridUri(uridUri_), onSuccess(onSuccess_), - onError(onError_) + onError(onError_), + sampleTimeout(sampleTimeout) { requestType = RequestType::PatchSet; size_t size = atomValue->size + sizeof(LV2_Atom); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 30878ff..8951d5a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,6 +12,7 @@ set (USE_SANITIZE OFF) # seems to be broken on Ubuntu 24.10 set(CXX_STANDARD 20) + include(FetchContent) # FOR FTXUI @@ -45,6 +46,9 @@ include(FindPkgConfig) find_package(sdbus-c++ REQUIRED) +find_package(SQLiteCpp REQUIRED) + + find_package(ICU REQUIRED COMPONENTS uc i18n ) message(STATUS "ICU_LIBRARIES: ${ICU_LIBRARIES}") @@ -192,6 +196,9 @@ else() endif() set (PIPEDAL_SOURCES + AudioFiles.cpp AudioFiles.hpp + AudioFileMetadata.cpp AudioFileMetadata.hpp + LRUCache.hpp CpuTemperatureMonitor.cpp CpuTemperatureMonitor.hpp SchedulerPriority.hpp SchedulerPriority.cpp ModFileTypes.cpp ModFileTypes.hpp @@ -304,6 +311,7 @@ set (PIPEDAL_INCLUDES ${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS} ${VST3_INCLUDES} ${WEBSOCKETPP_INCLUDE_DIRS} + ${SQLite3_INCLUDE_DIRS} . ) @@ -314,7 +322,7 @@ set(PIPEDAL_LIBS libpipedald zip ${VST3_LIBRARIES} ${LILV_0_LIBRARIES} # ${JACK_LIBRARIES} - pending delete for JACK support. - + ${SQLite3_LIBRARIES} ) @@ -383,8 +391,9 @@ add_executable(AuxInTest ) add_executable(pipedaltest -testMain.cpp + testMain.cpp + LRUCacheTest.cpp ModFileTypesTest.cpp jsonTest.cpp UpdaterTest.cpp @@ -822,7 +831,7 @@ add_custom_command(OUTPUT ${REACT_NOTICES_FILE} --output ${REACT_NOTICES_FILE} --projectCopyright ${DEBIAN_COPYRIGHT_FILE} liblilv-0-0 ${BOOST_COPYRIGHT_DIR} lv2-dev libsdbus-c++-dev librsvg2-2 libpango-1.0-0 libx11-6 libxrandr2 - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS ${DEBIAN_COPYRIGHT_FILE} "$" COMMENT "Updating copyright notices." ) diff --git a/src/LRUCache.hpp b/src/LRUCache.hpp new file mode 100644 index 0000000..c5a3c67 --- /dev/null +++ b/src/LRUCache.hpp @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#pragma once + +#include +#include +#include + +template +class LRUCache { +public: + struct CacheNode { + KEY key; + VALUE value; + CacheNode(KEY k, VALUE v) : key(k), value(v) {} + }; +private: + size_t capacity; + std::list cache_list; // Doubly-linked list for LRU order + std::unordered_map::iterator> cache_map; // Map key to list iterator + +public: + explicit LRUCache(size_t cap) : capacity(cap) { + if (cap == 0) { + throw std::invalid_argument("Cache capacity must be greater than 0"); + } + } + + // Get value by key, return false if not found + bool get(const KEY& key, VALUE& value) { + auto it = cache_map.find(key); + if (it == cache_map.end()) { + return false; + } + // Move to front (most recently used) + cache_list.splice(cache_list.begin(), cache_list, it->second); + value = it->second->value; + return true; + } + + // Put key-value pair into cache + void put(const KEY& key, const VALUE& value) { + auto it = cache_map.find(key); + if (it != cache_map.end()) { + // Update existing key + cache_list.splice(cache_list.begin(), cache_list, it->second); + it->second->value = value; + return; + } + + // Add new key-value pair + cache_list.emplace_front(key, value); + cache_map[key] = cache_list.begin(); + + // Evict least recently used if over capacity + if (cache_map.size() > capacity) { + auto lru = cache_list.back(); + cache_map.erase(lru.key); + cache_list.pop_back(); + } + } + + // Check if key exists + bool contains(const KEY& key) const { + return cache_map.find(key) != cache_map.end(); + } + + // Get current size + size_t size() const { + return cache_map.size(); + } + + // Get capacity + size_t get_capacity() const { + return capacity; + } + const std::list &cache() const { + return cache_list; + } +}; + diff --git a/src/LRUCacheTest.cpp b/src/LRUCacheTest.cpp new file mode 100644 index 0000000..1d58126 --- /dev/null +++ b/src/LRUCacheTest.cpp @@ -0,0 +1,67 @@ +// Copyright (c) 2025 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "pch.h" +#include "catch.hpp" +#include "LRUCache.hpp" +#include +#include + +using namespace std; + +TEST_CASE( "LRUCache test", "[lrucache]" ) { + + LRUCache lruCache(5); + + for (int i = 0; i < 10; ++i) + { + lruCache.put(i,i); + } + REQUIRE(lruCache.cache().size() == 5); + + REQUIRE(lruCache.cache().begin()->value == 9); + + int value; + REQUIRE(lruCache.get(5,value) == true); + REQUIRE(value == 5); + REQUIRE(lruCache.cache().begin()->value == 5); + + cout << '['; + for (auto it = lruCache.cache().begin();it != lruCache.cache().end(); ++it) + { + cout << it->value << ','; + } + cout << ']' << endl; + for (auto it = ++lruCache.cache().begin();it != lruCache.cache().end(); ++it) + { + REQUIRE(it->value != 5); + } + lruCache.put(1,1); + cout << '['; + for (auto it = lruCache.cache().begin();it != lruCache.cache().end(); ++it) + { + cout << it->value << ','; + } + cout << ']' << endl; + + + + + +} \ No newline at end of file diff --git a/src/Lv2Effect.cpp b/src/Lv2Effect.cpp index 915164c..6c0b47c 100644 --- a/src/Lv2Effect.cpp +++ b/src/Lv2Effect.cpp @@ -1116,3 +1116,4 @@ void Lv2Effect::SetPathPatchProperty(const std::string &propertyUri, const std:: { mainThreadPathProperties[propertyUri] = jsonAtom; } + diff --git a/src/Lv2Effect.hpp b/src/Lv2Effect.hpp index 27afc7b..6fe1117 100644 --- a/src/Lv2Effect.hpp +++ b/src/Lv2Effect.hpp @@ -56,6 +56,7 @@ namespace pipedal virtual void OnLogDebug(const char*message); private: + std::unordered_map controlIndex; FileBrowserFilesFeature fileBrowserFilesFeature; diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index fbb1950..3220bcf 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -562,11 +562,13 @@ void Lv2Pedalboard::GatherPathPatchProperties(IPatchWriterCallback *cbPatchWrite } } -void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests) +void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests, size_t samplesThisTime) { while (pParameterRequests != nullptr) { + pParameterRequests->sampleTimeout -= samplesThisTime; IEffect *pEffect = this->GetEffect(pParameterRequests->instanceId); + if (pEffect == nullptr) { pParameterRequests->errorMessage = "No such effect."; @@ -574,8 +576,12 @@ void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pPara else if (pEffect->IsVst3()) { pParameterRequests->errorMessage = "Not supported for VST3 plugins"; + } else if (pParameterRequests->sampleTimeout < 0) + { + pParameterRequests->sampleTimeout = 0; + pParameterRequests->errorMessage = "Timed out."; } - else + else { if (pEffect->IsLv2Effect()) { diff --git a/src/Lv2Pedalboard.hpp b/src/Lv2Pedalboard.hpp index 4ae4420..5cb3fe9 100644 --- a/src/Lv2Pedalboard.hpp +++ b/src/Lv2Pedalboard.hpp @@ -153,7 +153,7 @@ namespace pipedal void ResetAtomBuffers(); - void ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests); + void ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests, size_t samplesThisTime); void GatherPatchProperties(RealtimePatchPropertyRequest *pParameterRequests); void GatherPathPatchProperties(IPatchWriterCallback *cbPatchWriter); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index dcefcaa..0a97e0f 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -1615,19 +1615,30 @@ void PiPedalModel::SendSetPatchProperty( { std::lock_guard lock(mutex); + if (!audioHost) + { + onError("Audio not running."); + return; + } // save the property to the preset (currently used to reconstruct snapshots only) PedalboardItem *pedalboardItem = this->pedalboard.GetItem(instanceId); - if (pedalboardItem) + if (pedalboardItem && value.is_string()) { - json_variant abstractPath = pluginHost.AbstractPath(value); - std::ostringstream ss; - json_writer writer(ss); - writer.write(abstractPath); - std::string atomString = ss.str(); - pedalboardItem->pathProperties_[propertyUri] = atomString; + std::shared_ptr pluginInfo = GetPluginInfo(pedalboardItem->uri_); + auto pipedalUi = pluginInfo->piPedalUI(); + auto fileProperty = pipedalUi->GetFileProperty(propertyUri); + if (fileProperty && value.is_string()) { + + json_variant abstractPath = pluginHost.AbstractPath(value); + std::ostringstream ss; + json_writer writer(ss); + writer.write(abstractPath); + std::string atomString = ss.str(); + pedalboardItem->pathProperties_[propertyUri] = atomString; + } + this->SetPresetChanged(clientId, true); } - this->SetPresetChanged(clientId, true); LV2_Atom *atomValue = atomConverter.ToAtom(value); std::function onRequestComplete{ @@ -1657,7 +1668,7 @@ void PiPedalModel::SendSetPatchProperty( } else { - if (pParameter->onSuccess) + if (onSuccess) { onSuccess(); } @@ -1668,10 +1679,12 @@ void PiPedalModel::SendSetPatchProperty( }}; LV2_URID urid = this->pluginHost.GetLv2Urid(propertyUri.c_str()); - + size_t sampleTimeout = 0.5*audioHost->GetSampleRate(); RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest( onRequestComplete, - clientId, instanceId, urid, atomValue, nullptr, onError); + clientId, instanceId, urid, atomValue, nullptr, onError, + sampleTimeout + ); outstandingParameterRequests.push_back(request); if (this->audioHost) @@ -1684,7 +1697,7 @@ void PiPedalModel::SendGetPatchProperty( int64_t clientId, int64_t instanceId, const std::string uri, - std::function onSuccess, + std::function onSuccess, std::function onError) { std::function onRequestComplete{ @@ -1732,16 +1745,20 @@ void PiPedalModel::SendGetPatchProperty( }}; LV2_URID urid = this->pluginHost.GetLv2Urid(uri.c_str()); - RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest( - onRequestComplete, - clientId, instanceId, urid, onSuccess, onError); std::lock_guard lock(mutex); - outstandingParameterRequests.push_back(request); - if (this->audioHost) + + if (!this->audioHost) { - this->audioHost->sendRealtimeParameterRequest(request); + onError("Audio stopped."); } + size_t sampleTimeout = 0.3*audioHost->GetSampleRate(); + RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest( + onRequestComplete, + clientId, instanceId, urid, onSuccess, onError,sampleTimeout); + + outstandingParameterRequests.push_back(request); + this->audioHost->sendRealtimeParameterRequest(request); } BankIndex PiPedalModel::GetBankIndex() const diff --git a/src/PiPedalUI.hpp b/src/PiPedalUI.hpp index a39d688..52a9ff5 100644 --- a/src/PiPedalUI.hpp +++ b/src/PiPedalUI.hpp @@ -33,6 +33,8 @@ #include "ModFileTypes.hpp" +#define PIPEDAL_HOST_FEATURE "http://github.com/rerdavies/pipedal#host" // Plugin can only be hosted by PiPedal + #define PIPEDAL_PATCH "http://github.com/rerdavies/pipedal/patch" #define PIPEDAL_PATCH_PREFIX PIPEDAL_PATCH "#" #define PIPEDAL_PATCH__readable (PIPEDAL_PATCH_PREFIX "readable") diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 08077f3..5125427 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -615,7 +615,7 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin // example: // - // a lv2:Parameter; +// a lv2:Parameter; // rdfs:label "Model"; // rdfs:range atom:Path. // ... @@ -901,6 +901,7 @@ std::vector supportedFeatures = { LV2_STATE__mapPath, LV2_STATE__freePath, LV2_CORE__inPlaceBroken, + PIPEDAL_HOST_FEATURE, // UI features that we can ignore, since we won't load their ui. "http://lv2plug.in/ns/extensions/ui#makeResident", diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index a46ca0e..da104dc 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -35,6 +35,9 @@ #include "json.hpp" #include "HotspotManager.hpp" #include "MimeTypes.hpp" +#include "AudioFileMetadata.hpp" +#include "AudioFiles.hpp" + #define OLD_PRESET_EXTENSION ".piPreset" #define PRESET_EXTENSION ".piPreset" @@ -52,20 +55,20 @@ using namespace pipedal; using namespace boost::system; namespace fs = std::filesystem; - - -static bool HasDotDot(const std::filesystem::path &path) { +static bool HasDotDot(const std::filesystem::path &path) +{ for (auto &part : path) { - if (part == "..") { + if (part == "..") + { return true; } - if (part == ".") + if (part == ".") { return true; } } - return false; + return false; } class UserUploadResponse @@ -80,12 +83,12 @@ JSON_MAP_REFERENCE(UserUploadResponse, errorMessage) JSON_MAP_REFERENCE(UserUploadResponse, path) JSON_MAP_END() -static std::string GetMimeType(const std::filesystem::path&path) { +static std::string GetMimeType(const std::filesystem::path &path) +{ std::string extension = path.extension(); - const MimeTypes&mimeTypes = MimeTypes::instance(); + const MimeTypes &mimeTypes = MimeTypes::instance(); auto result = mimeTypes.MimeTypeFromExtension(extension); return result; - } static bool IsZipFile(const std::filesystem::path &path) { @@ -179,6 +182,17 @@ public: { return true; } + else if (segment == "AudioMetadata") + { + return true; + } else if (segment == "NextAudioFile") + { + return true; + } else if (segment == "PreviousAudioFile") + { + return true; + } + return false; } @@ -246,15 +260,17 @@ public: try { std::string segment = request_uri.segment(1); - if (segment == "downloadMediaFile") { + if (segment == "downloadMediaFile") + { fs::path path = request_uri.query("path"); - + if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) { throw PiPedalException("File not found."); } auto mimeType = GetMimeType(path); - if (mimeType.empty()) { + if (mimeType.empty()) + { throw PiPedalException("Can't download files of this type."); } res.set(HttpField::content_type, mimeType); @@ -343,18 +359,20 @@ public: { std::string segment = request_uri.segment(1); - if (segment == "downloadMediaFile") { + if (segment == "downloadMediaFile") + { fs::path path = request_uri.query("path"); - + bool t = this->model->IsInUploadsDirectory(path); - std::cout << (t? "true": "false") << std::endl; + std::cout << (t ? "true" : "false") << std::endl; (void)t; if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) { throw PiPedalException("File not found."); } auto mimeType = GetMimeType(path); - if (mimeType.empty()) { + if (mimeType.empty()) + { throw PiPedalException("Can't download files of this type."); } res.set(HttpField::content_type, mimeType); @@ -363,9 +381,10 @@ public: res.set(HttpField::content_disposition, disposition); size_t contentLength = std::filesystem::file_size(path); res.setContentLength(contentLength); - res.setBodyFile(path,false); + res.setBodyFile(path, false); return; - } else if (segment == "downloadPluginPresets") + } + else if (segment == "downloadPluginPresets") { std::string name; std::string content; @@ -416,6 +435,65 @@ public: res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION)); res.setBodyFile(tmpFile); } + else if (segment == "AudioMetadata") + { + res.set(HttpField::content_type, "application/json"); + // res.set(HttpField::cache_control, "no-cache"); + fs::path path = request_uri.query("path"); + + if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) + { + throw PiPedalException("File not found."); + } + std::string metadata = GetAudioFileMetadataString(path); + res.setBody(metadata); + } + else if (segment == "NextAudioFile") + { + res.set(HttpField::content_type, "application/json"); + // res.set(HttpField::cache_control, "no-cache"); + fs::path path = request_uri.query("path"); + + try { + if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) + { + throw PiPedalException("File not found."); + } + + std::string nextFile = AudioFileInfo::GetInstance().GetNextAudioFile(path); + std::stringstream ss; + json_writer writer(ss); + writer.write(nextFile); + + res.setBody(ss.str()); + } catch (const std::exception&e) + { + res.setBody("''"); + } + } + else if (segment == "PreviousAudioFile") + { + res.set(HttpField::content_type, "application/json"); + // res.set(HttpField::cache_control, "no-cache"); + fs::path path = request_uri.query("path"); + + try { + if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) + { + throw PiPedalException("File not found."); + } + std::string previousFile = AudioFileInfo::GetInstance().GetPreviousAudioFile(path); + std::stringstream ss; + json_writer writer(ss); + writer.write(previousFile); + + res.setBody(ss.str()); + } catch (const std::exception&e) + { + res.setBody("\"\""); + } + } + else { throw PiPedalException("Not found"); @@ -628,7 +706,7 @@ public: int64_t instanceId; { - std::istringstream ss { instanceIdString}; + std::istringstream ss{instanceIdString}; ss >> instanceId; if (!ss) { @@ -669,7 +747,7 @@ public: if (extensionChecker.IsValidExtension(extension)) { auto si = zipFile->GetFileInputStream(inputFile); - std::string path = this->model->UploadUserFile(directory, instanceId,patchProperty, inputFile, si, zipFile->GetFileSize(inputFile)); + std::string path = this->model->UploadUserFile(directory, instanceId, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile)); } } } @@ -687,7 +765,7 @@ public: } else { - outputFileName = this->model->UploadUserFile(directory, instanceId,patchProperty, filename, req.get_body_input_stream(), req.content_length()); + outputFileName = this->model->UploadUserFile(directory, instanceId, patchProperty, filename, req.get_body_input_stream(), req.content_length()); } if (outputFileName.is_relative()) @@ -783,7 +861,7 @@ public: << ", \"socket_server_address\": \"" << webSocketAddress << "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize << ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true" : "false") - << ", \"has_wifi_device\": " << (HotspotManager::HasWifiDevice() ? " true": "false") + << ", \"has_wifi_device\": " << (HotspotManager::HasWifiDevice() ? " true" : "false") << " }"; return s.str(); diff --git a/todo.txt b/todo.txt index 1bf8abf..2e74d1b 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,16 @@ +Sort order. +Do we have a better plugin category than "Utility" +.index files. +- pipewire aux in? + +libsqlite3-dev install +libsqlitecpp-dev install + +Check that OnNotifyPatchProperty still worsk for +- FilePropertyControl +- PowerStage2. +- ToobSpectrumAnaylyzer. + pcm.pipedal_aux_in { type file file "/tmp/aux_input_fifo" diff --git a/vite/index.html b/vite/index.html index b44998e..82218cc 100644 --- a/vite/index.html +++ b/vite/index.html @@ -23,6 +23,7 @@ }