diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 5d38427..e0e489b 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -28,12 +28,13 @@ jobs: npm view npm get version of @babel/plugin-proposal-private-property-in-object version command sudo apt-get update sudo apt install gcc-12 g++-12 libnm-dev - sudo apt-get install liblilv-dev libboost-dev libjack-jackd2-dev libnl-3-dev libnl-genl-3-dev libsystemd-dev catch - sudo apt install libasound2-dev - sudo apt install libwebsocketpp-dev authbind - sudo apt install libsdbus-c++-dev libsdbus-c++-bin - sudo apt install libavahi-client-dev libzip-dev libicu-dev apt - sudo apt install libpipewire-0.3-dev + sudo apt-get install -y liblilv-dev libboost-dev libjack-jackd2-dev libnl-3-dev libnl-genl-3-dev libsystemd-dev catch + sudo apt install -y libasound2-dev + sudo apt install -y libwebsocketpp-dev authbind + sudo apt install -y libsdbus-c++-dev libsdbus-c++-bin + sudo apt install -y libavahi-client-dev libzip-dev libicu-dev apt + sudo apt install -y libpipewire-0.3-dev + sudo apt install -y libssl-dev sudo apt install -y librsvg2-dev git submodule update --init --recursive diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index a709077..fac3cf6 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -13,7 +13,6 @@ "/usr/include/x86_64-linux-gnu", "/usr/lib" ], - "compilerPath": "/usr/bin/gcc-12", "cStandard": "c17", "cppStandard": "c++20", "intelliSenseMode": "linux-gcc-arm64", diff --git a/.vscode/launch.json b/.vscode/launch.json index d459cd3..c696ea3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -328,6 +328,37 @@ "ignoreFailures": true } ] - } + }, + { + "name": "(gdb) pipedalProfilePlugin", + "type": "cppdbg", + "request": "launch", + // Resolved by CMake Tools: + "program": "${command:cmake.launchTargetPath}", + "args": [ + "--no-profile", + "--preset-file", "/home/robin/Downloads/A1 Profiling.piPreset", + "--seconds", "100", + "-w" + ], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "$PATH:${command:cmake.launchTargetDirectory}" + } + ], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index e6e4f3a..f6f2aea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -103,7 +103,8 @@ "locale": "cpp", "stdfloat": "cpp", "text_encoding": "cpp", - "forward_list": "cpp" + "forward_list": "cpp", + "*.bak": "cpp" }, "cSpell.words": [ "Alsa", @@ -170,7 +171,7 @@ "cSpell.ignoreWords": [ "nammodel" ], - "cSpell.enabled": false, + "cSpell.enabled": true, // Disable all automatic completion suggestions - only show when Ctrl+Space is pressed "editor.quickSuggestions": { @@ -190,5 +191,8 @@ // Disable parameter hints popup (optional) "editor.parameterHints.enabled": false, + "cSpell.enabledFileTypes": { + "markdown": true + }, } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ed3d0b..a279c18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,30 @@ cmake_minimum_required(VERSION 3.16.0) project(pipedal - VERSION 1.5.99 + VERSION 2.0.102 DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" HOMEPAGE_URL "https://rerdavies.github.io/pipedal" ) execute_process( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE ) -set (DISPLAY_VERSION "PiPedal v1.5.99-Beta") +set (DISPLAY_VERSION "PiPedal v2.0.102-Alpha") set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE}) set (CMAKE_INSTALL_PREFIX "/usr/") +set(PIPEDAL_T3K_PUBLISHABLE_KEY "t3k_pub_bHrH8btdwXXTtxz5ryEU8sNLF-2TGRT9") + + +set (LIBZIP_DO_INSTALL OFF CACHE BOOL "Disable libzip install" FORCE) +set (SQLITECPP_INSTALL OFF CACHE BOOL "Disable SQLiteCpp install" FORCE) + +set(SUBMODULE_INSTALL OFF CACHE BOOL "" FORCE) + +# Remove SQLiteCPP install files. +set(CPACK_SOURCE_IGNORE_FILES + "*.a;sqlite3.h;$(CPACK_SOURCE_IGNORE_FILES)" +) + + set (PIPEDAL_EXCLUDE_TESTS false) include(CTest) enable_testing() @@ -96,9 +110,9 @@ set(CPACK_DEBIAN_PACKAGE_SECTION sound) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_CONTROL_STRICT_PERMISSION TRUE) -# Accept either libzip4 or libzip5 to satisfy libzip 4 linkage (because Ubuntu nonsense) -set(CPACK_DEBIAN_PACKAGE_DEPENDS "libicu-dev ,lv2-dev, ffmpeg, authbind, curl, gpg, alsa-base| pipewire, alsa-utils") + +set(CPACK_DEBIAN_PACKAGE_DEPENDS "libicu-dev ,lv2-dev,iw, ffmpeg, authbind, curl, gpg, alsa-base| pipewire, alsa-utils") set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE}) set(CPACK_PACKAGING_INSTALL_PREFIX /usr) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) @@ -118,4 +132,8 @@ set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA ) + include(CPack) + +message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}") + diff --git a/PiPedalCommon/src/DBusDispatcher.cpp b/PiPedalCommon/src/DBusDispatcher.cpp index 412db24..e440552 100644 --- a/PiPedalCommon/src/DBusDispatcher.cpp +++ b/PiPedalCommon/src/DBusDispatcher.cpp @@ -208,8 +208,9 @@ void DBusDispatcher::WaitForClose() serviceThread.join(); } } - catch (const std::exception &) + catch (const std::exception &e) { + std::cout << "DBusDispatcher: " << e.what() << std::endl; } if (eventFd != -1) { diff --git a/PiPedalCommon/src/HtmlHelper.cpp b/PiPedalCommon/src/HtmlHelper.cpp index 1b887dc..e0de787 100644 --- a/PiPedalCommon/src/HtmlHelper.cpp +++ b/PiPedalCommon/src/HtmlHelper.cpp @@ -21,6 +21,8 @@ #include #include #include +#include "ss.hpp" +#include using namespace pipedal; @@ -245,8 +247,30 @@ std::string HtmlHelper::Rfc5987EncodeFileName(const std::string &name) #define MAX_FILE_NAME_LENGHT 96 -const std::string SF_SPECIALS = " <>@;:\"\'/[]?="; +const std::string SF_SPECIALS = "<>@;:\"\'/[]?="; +const std::string ISF_SPECIALS = "\\:"; + +bool HtmlHelper::IsSafeFileName(const std::filesystem::path &path) +{ + for (const auto&segment: path) + { + if (segment.string() == "..") + { + return false; + } + for (char c: segment.string()) + { + unsigned char uc = (unsigned char)c; + if (uc < 0x20 || uc >= 0x80 || ISF_SPECIALS.find(c) != std::string::npos) + { + return false; + } + + } + } + return true; +} std::string HtmlHelper::SafeFileName(const std::string &name) { std::stringstream s; @@ -388,3 +412,96 @@ std::string HtmlHelper::generateEtag(const std::filesystem::path &path) crc = crc64((uint8_t*)&fTime, sizeof(fTime)); return std::to_string(crc); } + +static std::map httpErrorStrings = +{ + {200,"OK"}, + {201, "Created"}, + {202, "Accepted"}, + {203, "Non-Authoritative Information"}, + {204, "No Content"}, + {205, "Reset Content"}, + {206, "Partial Content"}, + {300, "Multiple Choices"}, + {301, "Moved Permanently"}, + {302, "Found"}, + {303, "See Other"}, + {304, "Not Modified"}, + {305, "Use Proxy"}, + {307, "Temporary Redirect"}, + {308, "Permanent Redirect"}, + {400, "Bad Request"}, + {401, "Unauthorized"}, + {402, "Payment Required"}, + {403, "Forbidden"}, + {404, "Not Found"}, + {405, "Method Not Allowed"}, + {406, "Not Acceptable"}, + {407, "Proxy Authentication Required"}, + {408, "Request Timeout"}, + {409, "Conflict"}, + {410, "Gone"}, + {411, "Length Required"}, + {412, "Precondition Failed"}, + {413, "Payload Too Large"}, + {414, "URI Too Long"}, + {415, "Unsupported Media Type"}, + {416, "Range Not Satisfiable"}, + {417, "Expectation Failed"}, + {418, "I'm a teapot"}, + {421, "Misdirected Request"}, + {422, "Unprocessable Content"}, + {423, "Locked"}, + {424, "Failed Dependency"}, + {426, "Upgrade Required"}, + {428, "Precondition Required"}, + {429, "Too Many Requests"}, + {431, "Request Header Fields Too Large"}, + {451, "Unavailable For Legal Reasons"}, + {500, "Internal Server Error"}, + {501, "Not Implemented"}, + {502, "Bad Gateway"}, + {503, "Service Unavailable"}, + {504, "Gateway Timeout"}, + {505, "HTTP Version Not Supported"}, + {506, "Variant Also Negotiates"}, + {507, "Insufficient Storage"}, + {508, "Loop Detected"}, + {510, "Not Extended"}, + {511, "Network Authentication Required"} + + +}; + +std::string HtmlHelper::httpErrorString(int errorCode) +{ + auto ff = httpErrorStrings.find(errorCode); + if (ff != httpErrorStrings.end()) + { + return SS(errorCode << ": " << ff->second); + } + return SS(errorCode << ": Unknown error"); + +} + +std::string HtmlFormBuilder::build() +{ + std::ostringstream ss; + bool firstTime = true; + for (const auto &entry: entries_) + { + if (!firstTime) + { + ss << "&"; + } + ss << entry.key(); + if (entry.value().has_value()) + { + + ss << "=" << HtmlHelper::encode_url_segment(entry.value().value(),true); + } + firstTime = false; + + } + return ss.str(); +} diff --git a/PiPedalCommon/src/include/DBusDispatcher.hpp b/PiPedalCommon/src/include/DBusDispatcher.hpp index 1da5414..9bc7299 100644 --- a/PiPedalCommon/src/include/DBusDispatcher.hpp +++ b/PiPedalCommon/src/include/DBusDispatcher.hpp @@ -78,7 +78,7 @@ private: std::atomic stopping; std::vector postedEvents; - bool threadStarted; + bool threadStarted = false; std::thread serviceThread; std::mutex postMutex; }; \ No newline at end of file diff --git a/PiPedalCommon/src/include/HtmlHelper.hpp b/PiPedalCommon/src/include/HtmlHelper.hpp index 0af60f2..c362427 100644 --- a/PiPedalCommon/src/include/HtmlHelper.hpp +++ b/PiPedalCommon/src/include/HtmlHelper.hpp @@ -20,49 +20,81 @@ #pragma once #include +#include #include #include #include +#include -namespace pipedal { +namespace pipedal +{ -class HtmlHelper { - -public: - - static std::string timeToHttpDate(); - static std::string timeToHttpDate(time_t time); - static std::string timeToHttpDate(std::filesystem::file_time_type time); - - - static std::string encode_url_segment(const char*pStart, const char*pEnd, bool isQuerySegment = false); - static std::string encode_url_segment(const std::string &segment, bool isQuerySegment = false) { - return encode_url_segment(segment.c_str(), segment.c_str() + segment.length(), isQuerySegment); - } - static void encode_url_segment(std::ostream&os, const char*pStart, const char *pEnd, bool isQuerySegment = false); - static void encode_url_segment(std::ostream&os, const std::string&segment, bool isQuerySegment = false) + class HtmlHelper { - const char*p = segment.c_str(); - encode_url_segment(os,p, p + segment.length(),isQuerySegment); - } + public: + static std::string timeToHttpDate(); + static std::string timeToHttpDate(time_t time); + static std::string timeToHttpDate(std::filesystem::file_time_type time); - static std::string decode_url_segment(const char*pStart, const char*pEnd, bool isQuerySegment = false); + static std::string encode_url_segment(const char *pStart, const char *pEnd, bool isQuerySegment = false); + static std::string encode_url_segment(const std::string &segment, bool isQuerySegment = false) + { + return encode_url_segment(segment.c_str(), segment.c_str() + segment.length(), isQuerySegment); + } + static void encode_url_segment(std::ostream &os, const char *pStart, const char *pEnd, bool isQuerySegment = false); + static void encode_url_segment(std::ostream &os, const std::string &segment, bool isQuerySegment = false) + { + const char *p = segment.c_str(); + encode_url_segment(os, p, p + segment.length(), isQuerySegment); + } - static std::string decode_url_segment(const char*text, bool isQuerySegment = false); + static std::string decode_url_segment(const char *pStart, const char *pEnd, bool isQuerySegment = false); - static void utf32_to_utf8_stream(std::ostream &s, uint32_t uc); + static std::string decode_url_segment(const char *text, bool isQuerySegment = false); - static std::string Rfc5987EncodeFileName(const std::string&name); + static void utf32_to_utf8_stream(std::ostream &s, uint32_t uc); - static std::string SafeFileName(const std::string &name); - static std::string HtmlEncode(const std::string& text); + static std::string Rfc5987EncodeFileName(const std::string &name); - static uint64_t crc64(uint8_t *data, size_t length,uint64_t crc = 0); - static uint64_t crc64(const std::string&value, uint64_t crc = 0); + static std::string SafeFileName(const std::string &name); + static bool IsSafeFileName(const std::filesystem::path &path); + static std::string HtmlEncode(const std::string &text); - static std::string generateEtag(const std::filesystem::path &path); + static uint64_t crc64(uint8_t *data, size_t length, uint64_t crc = 0); + static uint64_t crc64(const std::string &value, uint64_t crc = 0); -}; + static std::string generateEtag(const std::filesystem::path &path); + + static std::string httpErrorString(int errorCode); + }; + + class HtmlFormBuilder + { + + public: + class FormEntry + { + public: + FormEntry() = default; + FormEntry(const std::string &key) : key_(key) {} + FormEntry(const std::string &key, const std::string &value) : key_(key), value_(value) {} + + const std::string&key() const { return key_; } + const std::optional&value() const { return value_; } + private: + std::string key_; + std::optional value_; + }; + HtmlFormBuilder() = default; + HtmlFormBuilder(const std::vector &&entries) : entries_(std::move(entries)) {} + void Add(const std::string &key) { entries_.push_back(FormEntry(key)); } + void Add(const std::string &key, const std::string &value) { entries_.push_back(FormEntry(key, value)); } + + std::string build(); + + private: + std::vector entries_; + }; } // namespace. \ No newline at end of file diff --git a/PiPedalCommon/src/include/json.hpp b/PiPedalCommon/src/include/json.hpp index 7792535..51f5087 100644 --- a/PiPedalCommon/src/include/json.hpp +++ b/PiPedalCommon/src/include/json.hpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 2026 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 @@ -38,6 +38,7 @@ #include #include +#ifndef __INTELLISENSE__ #define DECLARE_JSON_MAP(CLASSNAME) \ static pipedal::json_map::storage_type jmap @@ -56,6 +57,35 @@ } \ } \ ; +#else +#define DECLARE_JSON_MAP(CLASSNAME) + +#define JSON_MAP_BEGIN(CLASSNAME) + +#define JSON_MAP_REFERENCE(class, name) + +#define JSON_MAP_END() ; + ; + +#endif + +#define JSON_GETTER_SETTER_REF(name) \ + const decltype(name##_) &name() const { return name##_; } \ + decltype(name##_) &name() { return name##_; } \ + void name(const decltype(name##_) &value) { name##_ = value; } + +#define JSON_GETTER_SETTER(name) \ + decltype(name##_) name() const { return name##_; } \ + void name(decltype(name##_) value) { name##_ = value; } + + +#define JSON_GETTER_REF(name) \ + const decltype(name##_) &name() const { return name##_; } \ + decltype(name##_) &name() { return name##_; } + +#define JSON_GETTER(name) \ + decltype(name##_) name() const { return name##_; } + namespace pipedal { diff --git a/PiPedalCommon/src/include/util.hpp b/PiPedalCommon/src/include/util.hpp index 39b95dc..fa84535 100644 --- a/PiPedalCommon/src/include/util.hpp +++ b/PiPedalCommon/src/include/util.hpp @@ -124,5 +124,9 @@ namespace pipedal std::string ToLower(const std::string&value); + std::string SafeFilenameToString(const std::string &filename); + std::string StringToSafeFilename(const std::string &name); + + std::string ShellEscape(const std::string &input); } diff --git a/PiPedalCommon/src/json.cpp b/PiPedalCommon/src/json.cpp index d486bf8..ca8e86e 100644 --- a/PiPedalCommon/src/json.cpp +++ b/PiPedalCommon/src/json.cpp @@ -562,8 +562,8 @@ void json_reader::skip_array() void json_reader::skip_object() { - int c; consume('{'); + int c; while (true) { c = peek(); @@ -571,11 +571,11 @@ void json_reader::skip_object() if (c == '}') { c = get(); - break; + return; } skip_string(); // name. consume(':'); - skip_object(); + skip_property(); if (peek() == ',') { consume(','); diff --git a/PiPedalCommon/src/util.cpp b/PiPedalCommon/src/util.cpp index 52838ee..16580f5 100644 --- a/PiPedalCommon/src/util.cpp +++ b/PiPedalCommon/src/util.cpp @@ -32,32 +32,29 @@ using namespace pipedal; - void pipedal::SetThreadName(const std::string &name) { std::string threadName = "ppdl_" + name; - if (threadName.length () > 15) + if (threadName.length() > 15) { - threadName = threadName.substr(0,15); + threadName = threadName.substr(0, 15); } pthread_t pid = pthread_self(); - pthread_setname_np(pid,threadName.c_str()); + pthread_setname_np(pid, threadName.c_str()); } - static const uint8_t utf8extraBytes[256] = { - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, - 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5 -}; + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5}; constexpr char32_t ILLEGAL_CHAR32 = U'⊗'; -static const uint8_t utf8Offset[] = { 0,0b11000000, 0b11100000,0b11110000,0b11111000,0b11111100}; +static const uint8_t utf8Offset[] = {0, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100}; std::u32string pipedal::ToUtf32(const std::string &s) { @@ -72,16 +69,21 @@ std::u32string pipedal::ToUtf32(const std::string &s) if (c < 0x80) { result << (char32_t)c; - } else { + } + else + { auto extraBytes = utf8extraBytes[c]; if (extraBytes == 0) { result << ILLEGAL_CHAR32; - } else if (p+extraBytes > end) + } + else if (p + extraBytes > end) { result << ILLEGAL_CHAR32; break; - } else { + } + else + { char32_t cResult = c -= utf8Offset[extraBytes]; while (extraBytes != 0) { @@ -102,11 +104,10 @@ std::u32string pipedal::ToUtf32(const std::string &s) return result.str(); } - std::string pipedal::GetHostName() { char buffer[1024]; - if (gethostname(buffer,1024) != 0) + if (gethostname(buffer, 1024) != 0) { buffer[0] = '\0'; } @@ -114,7 +115,6 @@ std::string pipedal::GetHostName() return buffer; } - std::vector pipedal::split(const std::string &value, char delimiter) { size_t start = 0; @@ -138,8 +138,7 @@ std::vector pipedal::split(const std::string &value, char delimiter return result; } - -bool pipedal::IsChildDirectory(const std::filesystem::path &path, const std::filesystem::path&rootPath) +bool pipedal::IsChildDirectory(const std::filesystem::path &path, const std::filesystem::path &rootPath) { auto iter = path.begin(); @@ -154,8 +153,7 @@ bool pipedal::IsChildDirectory(const std::filesystem::path &path, const std::fil return true; } - -std::string pipedal::ToLower(const std::string&value) +std::string pipedal::ToLower(const std::string &value) { std::string result; result.resize(value.length()); @@ -166,16 +164,17 @@ std::string pipedal::ToLower(const std::string&value) return result; } - -std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path&parentPath) +std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path &parentPath) { - if (path.is_relative()) return path; + if (path.is_relative()) + return path; auto iter = path.begin(); - + for (auto i = parentPath.begin(); i != parentPath.end(); ++i) { - if (iter == path.end() || *iter != *i) { + if (iter == path.end() || *iter != *i) + { // not a child directory of parent directory. throw std::runtime_error("Not a child directory."); } @@ -187,7 +186,6 @@ std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &pat remander /= *iter++; } return remander; - } bool pipedal::HasDotDot(const std::filesystem::path &path) @@ -218,7 +216,7 @@ bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::files if ((*i) != (*iPath)) { - if (*i == "" && ++i == basePath.end()) // match xyz/ (trailing /) + if (*i == "" && ++i == basePath.end()) // match xyz/ (trailing /) { break; } @@ -237,12 +235,131 @@ bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::files return true; } - bool pipedal::HasWritePermissions(const std::filesystem::path &path) { - // posix, but may not work on windows. + // posix, but may not work on windows. // allegedly windows provies an _access function, which is probably a superset of // access. return access(path.c_str(), W_OK) == 0; } +const std::string SAFE_FILENAME_SPECIAL_CHARS = "<>@;:\"\'/[]?=+%.&|*^~`#"; + +static std::array makeSfBits() +{ + std::array result; + + for (char c : SAFE_FILENAME_SPECIAL_CHARS) + { + result[(unsigned char)c] = true; + } + for (size_t i = 128; i < 256; ++i) + { + result[i] = true; + } + for (size_t i = 0; i < 0x20; ++i) + { + result[i] = true; + } + return result; +} + +static std::array IS_SPECIAL_FILENAME_CHAR = makeSfBits(); + +static const char *HEX_CHARS = "0123456789abcdef"; + +static bool isHexChar(char c) +{ + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +static int fromHexChar(char c) +{ + if (c >= '0' && c <= '9') + return (int)(c - '0'); + if (c >= 'a' && c <= 'f') + return (int)(c - 'a') + 10; + if (c >= 'A' && c <= 'F') + return (int)(c - 'A') + 10; + return 0; +} +static char fromHexChars(char c1, char c2) +{ + int uc = fromHexChar(c1) * 16 + fromHexChar(c2); + return (char)uc; +} + +std::string pipedal::SafeFilenameToString(const std::string &filename) +{ + size_t ix = 0; + std::ostringstream os; + + while (ix < filename.size()) + { + char c = filename[ix++]; + if (c == '%') + { + // when faced with an invalid encoding, assume that user has done it. + // So return the original. + if (ix + 2 >= filename.size()) + { + return filename; // just return the original. + } + if (!isHexChar(filename[ix]) || !isHexChar(filename[ix + 1])) + { + return filename; + } + os << fromHexChars(filename[ix], filename[ix + 1]); + ix += 2; + } + else + { + os << c; + } + } + return os.str(); +} +std::string pipedal::StringToSafeFilename(const std::string &name) +{ + std::ostringstream os; + for (char c : name) + { + unsigned char uc = (unsigned char)c; + if (uc < 0x20) + { + continue; + } + if (IS_SPECIAL_FILENAME_CHAR[uc]) + { + os << '%' + << HEX_CHARS[uc >> 4] + << HEX_CHARS[uc & 0x0F]; + } + else + { + os << c; + } + } + return os.str(); +} + +std::string pipedal::ShellEscape(const std::string &input) +{ + // Escape string for safe use in shell commands by using single quotes + // and escaping any single quotes in the input + std::string result = "'"; + for (char c : input) + { + if (c == '\'') + { + // End quote, add escaped single quote, start quote again + result += "'\\''"; + } + else + { + result += c; + } + } + result += "'"; + return result; +} diff --git a/README.md b/README.md index 0e0fd95..6363bf6 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ -Download: v1.5.99 +Download: v2.0.102 Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). Documentation: [https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html). -#### NEW version 1.5.99 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. TooB NAM calibration, manipulation of i/r files in Toob Convolution Reverb, new Parametric EQ and 3 Band EQ plugins, and MORE! +#### Announcing PiPedal 2.0 (2.0.102 Alpha). See the [documentation](https://rerdavies.github.io/pipedal/PiPedal2.html) for information, and instructions on how to install Pipedal 2.0. New features include support for Neural Amp Modeler A2 models, integration with Tone3000.com services for easy downloading of Neural Amp Modeler A2 models and IIRs, a new Channel Routing dialog for global routing of auxiliary input channels, and unprocessed re-amp output channels, and many other minor features, improvements, and bug fixes. Pipedal 2.0 also adds support for running PiPedal as a Progressive Web Application (PWA), which allows you to launch Pipedal from your desktop as a native desktop application. (A nice feature if you are accessing Pipedal from an Apple device). See the [documentation](https://rerdavies.github.io/pipedal/PiPedal2.html) for instructions on how to install Pipedal on your desktop as a standalone app.   -Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet. +Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal using a remote web browsers, from your phone or tablet. PiPedal running on a Raspberry Pi 4 or Pi 5 provides stable super-low-latency audio via external USB audio devices, or internal Raspberry Pi audio hats.  PiPedal will also run on Ubuntu 24.04, 24.10, or 25.04 (amd64/x64 or aarch64). Make sure you follow the [Ubuntu post-install instructions](https://rerdavies.github.io/pipedal/Configuring.html) to ensure that your Ubuntu OS is using a realtime-capable kernel. diff --git a/artifacts/Pi-Icon-Inkscape.svg b/artifacts/Pi-Icon-Inkscape.svg new file mode 100644 index 0000000..1925dbd --- /dev/null +++ b/artifacts/Pi-Icon-Inkscape.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/artifacts/Pi-Icon-Svg.svg b/artifacts/Pi-Icon-Svg.svg new file mode 100644 index 0000000..1925dbd --- /dev/null +++ b/artifacts/Pi-Icon-Svg.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/artifacts/logo192.png b/artifacts/logo192.png new file mode 100644 index 0000000..2bb4cca Binary files /dev/null and b/artifacts/logo192.png differ diff --git a/config/config.json b/config/config.json index d87a2c3..53374cd 100644 --- a/config/config.json +++ b/config/config.json @@ -33,7 +33,9 @@ "logLevel": 3, /* Maximum filesize to allow when uploading */ - "maxUploadSize": 536870912 // 512MiB + "maxUploadSize": 536870912, // 512MiB + /* false-> Download A1 models; true -> Download A2 models */ + "tone3000A2Models": true } \ No newline at end of file diff --git a/docs/404.html b/docs/404.html index 086a5c9..7896358 100644 --- a/docs/404.html +++ b/docs/404.html @@ -18,8 +18,9 @@ layout: default
-

404

+

404 Page not found

-

Page not found :(

-

The requested page could not be found.

+

+ You step in the stream, but the water has moved on. +

diff --git a/docs/BuildPrerequisites.md b/docs/BuildPrerequisites.md index 76effd1..1b6257f 100644 --- a/docs/BuildPrerequisites.md +++ b/docs/BuildPrerequisites.md @@ -26,7 +26,7 @@ Run the following commands to install dependent libraries required by the PiPeda authbind libavahi-client-dev libnm-dev libicu-dev \ libsdbus-c++-dev libzip-dev google-perftools \ libgoogle-perftools-dev \ - libpipewire-0.3-dev libbz2-dev librsvg2-dev + libpipewire-0.3-dev libbz2-dev libssl-dev librsvg2-dev ### Installing Sources diff --git a/docs/Installing.md b/docs/Installing.md index 711fd98..f81af1e 100644 --- a/docs/Installing.md +++ b/docs/Installing.md @@ -13,18 +13,18 @@ page_icon: img/Install4.jpg Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (aarch64) v1.5.99](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_arm64.deb) -- [Ubuntu 24.04 through 25.04 (aarch64) v1.5.99](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_arm64.deb) -- [Ubuntu 24.04 through 25.04 (amd64) v1.5.99](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_amd64.deb) +- [Raspberry Pi OS bookworm (aarch64) v2.0.102](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb) +- [Ubuntu 24.04 through 25.04 (aarch64) v2.0.102](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb) +- [Ubuntu 24.04 through 25.04 (amd64) v2.0.102](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_amd64.deb) -Version 1.5.99 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure: +Version 2.0.102 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure: ``` sudo apt update sudo apt upgrade cd ~/Downloads - sudo apt-get install ./pipedal_1.5.99_arm64.deb + sudo apt-get install ./pipedal_2.0.102_arm64.deb ``` You MUST use `apt-get`. `apt` will not install downloaded packages; and `dpkg -i` will not install dependencies. diff --git a/docs/PiPedal2.md b/docs/PiPedal2.md new file mode 100644 index 0000000..2de5b57 --- /dev/null +++ b/docs/PiPedal2.md @@ -0,0 +1,62 @@ +
+ +
+ +

PiPedal 2.0 Guitar Effects Processor

+

A Raspberry Pi-based stomp box designed to be controlled from a phone or tablet.

+ + + + + + + +_To download PiPedal v2.0.102-alpha, click [*here*](download.md). +To view PiPedal documentation, click [*here*](Documentation.md)._ + +Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet. +PiPedal running on a Raspberry Pi 4 or Pi 5 provides stable super-low-latency audio via external USB audio devices, or internal Raspberry Pi audio hats. + +PiPedal will also run on Ubuntu 22.x (amd64/x64 and aarch64). Make sure you follow the [Ubuntu post-install +instructions](https://rerdavies.github.io/pipedal/Configuring.html) to make sure your Ubuntu OS is using a realtime-capable kernel. + +PiPedal 2.0 adds support for Neural Amp Modeler A2 models—the next generation of the ground-breaking Neural Amp Modeler technology. NAM A2 models provide even more accurate and realistic amp emulation than the original NAM A1 modelling technology, with while requiring less CPU. "Slimmable" A2 models allow users to select the level of CPU use vs. emulation accuracy that best suits their needs Slim down models for lower CPU use; or use full models for the best possible emulation quality. + +NAM A2 models are available as free downloads from Tone3000.com. PiPedal 2.0 now integrates with Tone3000.com services in order to allow you to easily download NAM A2 models and Cabinet Impulse Response Files directly into Pipedal right from within the PiPedal app. All existing NAM models on Tone3000.com have been retrained using the new A2 architecture, and are available as A2 models right now. There is also a rich ecosystem of professionally developed NAM models available for purchase on the Internet. + +Whether you are looking for free models, or premium models, NAM A2 provides the best amp modelling available anywhere. + +PiPedal 2.0 provides a new Channel Routing dialog, which replaces the Channel Selection Dialog in Pipedal 1.x. You can now globally (for all existing presets) route auxiliary input channels on your audio device (such as backing tracks or microphone inputs), or route clean unprocessed guitar input signals to otherwise-unused output audio channels for later re-amping later in a DAW. + +PiPedal 2.0 can now be run as a "Progressive Web Application". This allows you to launch Pipedal from your desktop as a native desktop application, without extra browser address bars and window decorations. See below for instructions on how to install Pipedal on your desktop as a standalone app. + +{% include gallery.html %} + +PiPedal includes state-of-the-art AI-based guitar amp emulation plugins based on the famous Neural Amp Modeler (NAM) and ML libraries which provide amp modelling that will blow your mind. + +{% include demo.html %} + +PiPedal can be remotely controlled via a web interface over Ethernet, or Wi-Fi. If you don't have access to a Wi-Fi router, PiPedal can be configured to +start a Wi-Fi hotspot automatically, whenever your Raspberry Pi can't connect to your home network. + +Install the [PiPedal Remote Android app](https://play.google.com/store/apps/details?id=com.twoplay.pipedal) to get one-click access to PiPedal via Wi-Fi networks, or Wi-Fi hotspots. If you are using PiPedal away from home, you can configure PiPedal to automatically start a Wi-Fi hotspot on your Raspberry Pi +whenever Pipedal is unable to detect your home network. The PiPedal Client Android app will allow to connect by simply launching the app, whether you are at home, or using a Wi-Fi auto-hotspot at a gig, when away from home. + +PiPedal 2.0 can no be installed as a Progressive Web Application (PWA). This allows you to launch Pipedal with a native window frame. Launch it from a desktop shortcut, and it will run without browser address bars, tabs, and other browser window decorations. I looks and works like a normal native application. To install PiPedal on your desktop as a PWA, load PiPedal in your web browser, and click on the "Install" button found here: + +![Install PiPedal as a Browser Application](img/chrome_install.jpg) + +As an added bonus, when running PiPedal as a PWA, press the F11 key to toggle full-screen mode, and get a completely immersive experience, with no distractions on your screen at all. + +Using Pipedal as a PWA also provides a nice way to use Pipedal on Apple devices. We recommend that you use Google Chrome to host the PWA on Apple devices, rather than Safari. + +PiPedal's user interface has been specifically designed to work well on small form-factor touch devices like phones or tablets. Clip a phone or tablet on your microphone stand on stage, and you're ready to play! Or connect via a desktop browser, for a slightly more luxurious experience. The PiPedal user-interface adapts to the screen size and orientation of your device, providing easy control of your guitar effects across a broad variety devices and screen sizes. + +PiPedal includes a pre-installed selection of LV2 plugins from the ToobAmp collection of plugins; but it works with most LV2 Audio plugins. There are literally hundreds of free high-quality LV2 audio plugins that will work with PiPedal. Just install them on your Raspberry Pi, and they will show up in PiPedal. + +If your USB audio adapter has MIDI connectors, you can use MIDI devices (keyboards, controllers, or midi floor boards) to control PiPedal while performing. A simple interface allows you to select how you would like to bind PiPedal controls to midi messages. + + + + + diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index cb59e69..d155f47 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,44 @@ # Release Notes +## PiPedal 2.0.102 Alpha + +PiPedal 2.0 provides support for Neural Amp Modeler A2 models. + +NAM A2 provides significant improvements in sound quality and performance compared to NAM A1. NAM A2 also provides "Slimable" models. Slimmable models provide multiple levels of performance and sound quality in a single model file, allowing you to choose the level of performance and sound quality that best suits your needs. This is particularly useful for users of Raspberry Pi 4, since NAM A2 models can provide better sound quality than NAM A1 models, while consuming significantly less CPU resources. + +A new integrated Tone3000 model downloader provides a convenient way to download the latest NAM A2 models and IIR files from Tone3000 directly to the PiPedal server. All existing NAM A1 models on Tone3000 have been retrained for NAM A2. When +you download from Tone3000 using the integrated downloader, you will get NAM A2 models. Just click on the DOWNLOAD MODELS FROM TONE3000 button. + +Currently, the best way to get access to NAM A2 models is via the integrated Tone3000 model downloader. Models downloaded externally from the Tone3000 website will still be NAM A1 models; however this should change soon with a forthcoming announcement from Tone3000. + +A new channel routing dialog replaces the channel configurationg dialog. The Channel Routing dialog allows global (i.e. not per-preset) configuration of audio channels. You can now configure audio channels as Aux channels (e.g. using the non-guitar channel on your audio interface as an input for backing tracks), or sending unprocessed guitar input signal to an unused output channel for re-amping in a DAW or external hardware. Refer to the (?) help dialog in the Channel Routing Dialog for more information on how to use this feature. + +PiPedal 2.0 can now be installed as a Progressive Web Application (PWA). This allows you to launch Pipedal from your desktop as a native desktop application, without extra browser address bars and window decorations. + +### Features + +- Support for Neural Amp Modeler A2 models. The latest version of the TooB Neural Amp Modeler plugin now supports NAM A2 models, which provide improved performance and sound quality compared to older NAM A1 models. + +- Download Tone3000 NAM models and IIR files directly to the PiPedal server. Click on the DOWNLOAD MODELS FROM TONE3000 button in the NAM file selection dialog to download the latest NAM A2 models. + +- Download IIR files directly into TooB Cab IR from Tone3000. + +- New Channel Routing Dialog. + +- Install PiPedal as a Progressive Web Application (PWA), which allows you to launch PiPedal from your desktop as a native desktop application. (A nice feature if you are accessing Pipedal from an Apple device). + +- New TooB Tone plugin: a simple one-knob tone control. The new TooB Tone plugin provides a simple one-knob tone control that can be used to adjust the overall tone of your guitar signal. It is a great tool for quickly shaping your tone without having to use a more complex EQ plugin. + +- Copy and Paste plugins (and their settings) between pedalboards. Accessible via the "Copy Plugin" and "Paste Plugin" menu items in the former Plugin Preset selector menu. + +### Minor features and bug fixes + +- Better handling of step sizes in numeric edit controls. + +- Short names of TooB CE-2 Chorus and TooB BF-2 Flanger changed to "TooB Chorus" and "TooB Flanger", repsectively. + +- Split controls now support double-tap to reset to a default value. (Other controls already support this feature). + ## PiPedal 1.5.99 Beta ### Bug Fixes diff --git a/docs/SystemRequirements.md b/docs/SystemRequirements.md index c447c95..6aeb358 100644 --- a/docs/SystemRequirements.md +++ b/docs/SystemRequirements.md @@ -5,15 +5,50 @@ page_icon: img/Requirements2.jpg {% include pageIcon.html %} -* A Raspberry PI 5, PI 4B or PI 400, with at least 2GB of RAM to run (4GB recommended). PiPedal will run on a PI 3, but you will not be able to use some plugins due to excessive CPU use. -* To build PiPedal, at least 8GB of RAM is required. -* An external USB Audio Adapter, or a Pi audio hat with at least one audio input, and one audio output. +### Processor and Memory Requirements + +* An ARM64 or x86-64 CPU. Any of the following provide excellent performance. + * Raspberry Pi 5, Pi 500, Pi 500+ (or equivalent ARM64 single-board computer). Recommended. + * Intel N100-class NUCs or mini computers. Also recommended. + * Most AMD/Intel laptop or desktop CPUs. +* PiPedal will also run well on a Raspberry Pi 4. +* A Raspberry Pi 3 (or equivalent SBC) can run PiPedal, but may struggle to keep up. Not recommended. +* At least 2GB of RAM (4GB recommended). +* To compile PiPedal from source, at least 8GB of RAM is required. + +### Audio System Requirements: +* On Raspberry Pi's, you will need an external USB Audio Adapter, or a Pi audio hat with at least one audio input. You cannot use the built-in audio inputs and outputs. +* On x86-64 systems, any standard audio interface with at least one input and one output will work; but an external USB audio interfaces is highly recommended. + +### Operating System Support PiPedal has been tested on the following Operating Systems: -* Raspberry Pi OS 64-bit Bookworm +* Raspberry Pi OS 64-bit Bookworm, or Trixie. +* Ubuntu 24.x or later, 64-bit (amd64/x86-64 and aarch64). Make sure you follow the [Ubuntu post-install instructions](https://rerdavies.github.io/pipedal/Configuring.html) to ensure your Ubuntu OS is using a realtime kernel. Pipedal will also run on Ubuntu Server 24.x or later. -Older versions of PiPedal (v1.1.31) have been tested on the following operating systems, but are no longer supported: + +PiPedal may also run on other Debian 12 or Debian 13-based Linux distributions, but these have not been tested, and we can provide only limited support for these distributions, since we are not able to locally test every possible Linux distribution. + +You may be able to compile Pipedal from sources on non-Debian Linux distributions, but working out compiler and library dependencies on non-Debian-based Linux distributions may be difficult, and is not officially supported. If you do manage to compile PiPedal on a non-Debian-based Linux distribution, please let us know how you did it, and we will publish instructions for others that follow in your footsteps. + +A Linux kernel version of 5.15 or later is required, since support for USB audio adapters is poor in prior kernel versions. A Linux kernel version of 6.15 or later is recommended. + +PiPedal requires a realtime-capable Linux kernel (a PREEMPT_RT kernel). Current versions of Ubuntu can be configured to enable PREEMPT_RT at runtime. (See the [Ubuntu post-install instructions](https://rerdavies.github.io/pipedal/Configuring.html) for details). Raspberry Pi OS 64-bit Bookworm and Trixie include a PREEMPT_RT kernel by default, so no additional configuration is required to enable PREEMPT_RT on these OS versions. + +### Headless Operation + +PiPedal provides best performance and audio latency when controlled from a remote browser, since GPU activity can interfere with real-time audio processing. The PiPedal server does not require a graphical desktop, so headless installs are fully supported (and more-or-less recommended). You can completely install and configure PiPedal using Linux commandline alone; however, you may find it easier to set up, configure and maintain your PiPedal server if you have a desktop environment installed. + +You should not run Pipedal using a browser on the same machine that is running the PiPedal server, unless you have a relatively performant desktop or laptop class machine. + +The Pipedal server does update itself automatically (it prompts for permission from the browser interface if there is an update available). However, it does not take care of Operating System updates. You should make sure to keep your OS up to date, and to install security updates as they become available. You will need to do that yourself from a commandline terminal, or using a desktop environment on the machine running the PiPedal server. + +You may find it much easier to set up and maintain your PiPedal server if you enable SSH access on the server machine. + +### Older Versions of PiPedal + +Older versions of PiPedal (v1.1.31) have been tested on the following operating systems, but are no longer supported (and are not really recommended): * Ubuntu 21.04 or later, 64-bit * Raspberry PI OS 64-bit bullseye diff --git a/docs/download.md b/docs/download.md index a6b9293..d2497b6 100644 --- a/docs/download.md +++ b/docs/download.md @@ -1,12 +1,13 @@ ## Download -### Install for Ubuntu or Raspberry Pi OS (64-bit) +# Download PiPedal 2.0.102-alpha + Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (aarch64) v1.5.99 Beta](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_arm64.deb) -- [Ubuntu 24.x, 25.04 (aarch64) v1.5.99 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_arm64.deb) -- [Ubuntu 24.x, 25.04 (amd64) v1.5.99 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_amd64.deb) +- [Raspberry Pi OS bookworm (aarch64) v2.0.102 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb) +- [Ubuntu 24.x, 25.04 (aarch64) v2.0.102 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb) +- [Ubuntu 24.x, 25.04 (amd64/x86_64) v2.0.102 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_amd64.deb) Install the package by running @@ -14,9 +15,36 @@ Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_1.5.99_arm64.deb + sudo apt install ./pipedal_2.0.102_arm64.deb + # or ... _amd64.deb as appropriate for your platform ``` -You MUST use `apt-get` to install the package. `apt install` will NOT install the package correctly. The message about missing permissions given by `apt-get` is -expected, and can be safely ignored. +The message about missing permissions given by `apt` is +expected, and can be safely ignored. Apt is unable to unpack the debian package in a sandbox because the +package is a local file, rather than a file from the Internet. This is a known issue with apt, and does not indicate any problem with the package or installation process. Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation. + +# Download PiPedal 1.5.99 Stable Release + +Download the most recent Debian (.deb) package for your platform: + +- [Raspberry Pi OS bookworm (aarch64) v1.5.99](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_arm64.deb) +- [Ubuntu 24.x, 25.04 (aarch64) v1.5.99](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_arm64.deb) +- [Ubuntu 24.x, 25.04 (amd64/x86_64) v1.5.99](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_amd64.deb) + + +Install the package by running + +``` + sudo apt update + cd ~/Downloads + sudo apt install ./pipedal_1.5.99_arm64.deb + # or ... _amd64.deb as appropriate for your platform +``` +The message about missing permissions given by `apt` is +expected, and can be safely ignored. Apt is unable to unpack the debian package in a sandbox because the +package is a local file, rather than a file from the Internet. This is a known issue with apt, and does not indicate any problem with the package or installation process. + + +Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation. + diff --git a/docs/img/chrome_install.jpg b/docs/img/chrome_install.jpg new file mode 100644 index 0000000..1e40edd Binary files /dev/null and b/docs/img/chrome_install.jpg differ diff --git a/docs/index.md b/docs/index.md index 1d36c9f..86a16ea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,17 +5,49 @@

PiPedal Guitar Effects Processor

A Raspberry Pi-based stomp box designed to be controlled from a phone or tablet.

+
+
+

Announcing PiPedal 2.0

+

+ The first public preview release of Pipedal version 2.0.102-alpha is now available! +

+ +

This release includes

+ +
    +
  • + Support for Neural Amp Modeler A2 models—the next generation of the ground-breaking + Neural Amp Modeler technology.
  • +
  • + Integration with Tone3000.com services for easy downloading of Neural Amp Modeler A2 models and IIRs.
  • +
  • + A new Channel Routing dialog for global routing of auxiliary input channels, and unprocessed re-amp output channels.
  • +
  • Install Pipedal as a Progressive Web Application (PWA), which allows you to launch Pipedal from your desktop as a native desktop application. (A nice feature if you are accessing Pipedal from an Apple device).
  • +
  • + Many other minor features, improvements, and bug fixes.
  • +
+ +

For more information, and to download PiPedal v2.0.102-alpha click here. +

+
+
+ - - -_To download PiPedal, click [*here*](download.md). + + +_To download PiPedal v1.5.99, click [*here*](download.md). To view PiPedal documentation, click [*here*](Documentation.md)._ Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet. PiPedal running on a Raspberry Pi 4 or Pi 5 provides stable super-low-latency audio via external USB audio devices, or internal Raspberry Pi audio hats. -PiPedal will also run on Ubuntu 22.x (amd64/x64 and aarch64). Make sure you follow the [Ubuntu post-install +PiPedal will also run on Ubuntu 24.x or later (amd64/x86-64 and aarch64). Make sure you follow the [Ubuntu post-install instructions](https://rerdavies.github.io/pipedal/Configuring.html) to make sure your Ubuntu OS is using a realtime-capable kernel. {% include gallery.html %} diff --git a/lv2/aarch64/ToobAmp.lv2/CabIR.ttl b/lv2/aarch64/ToobAmp.lv2/CabIR.ttl index b7e7cd1..fb291dc 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabIR.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabIR.ttl @@ -93,7 +93,7 @@ cabir:impulseFile3 doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ TooB Cab IR is a convolution-based guitar cabinet impulse response simulator. diff --git a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl index 4f9cad5..bcebaa0 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl @@ -50,7 +50,7 @@ toob:frequencyResponseVector doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; mod:brand "TooB"; mod:label "TooB CabSim"; diff --git a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl index 30632fb..c214572 100644 --- a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl @@ -53,7 +53,7 @@ toobimpulse:impulseFile doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ Toob Convolution Reverb Stereo uses convolution reverb impulse/response files in order to produce highly diff --git a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl index f5dcf59..f7b8887 100644 --- a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl @@ -51,7 +51,7 @@ toobimpulse:impulseFile doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ diff --git a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl index 57fb97a..93f3a61 100644 --- a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl +++ b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl @@ -66,7 +66,7 @@ inputStage:filterGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; mod:brand "TooB"; mod:label "TooB Input"; diff --git a/lv2/aarch64/ToobAmp.lv2/LICENSE.md b/lv2/aarch64/ToobAmp.lv2/LICENSE.md index 6363a81..af34883 100644 --- a/lv2/aarch64/ToobAmp.lv2/LICENSE.md +++ b/lv2/aarch64/ToobAmp.lv2/LICENSE.md @@ -1,4 +1,4 @@ -© 2020-2023, Robin E. R. Davies +© 2020-2026, Robin E. R. Davies The MIT License diff --git a/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl b/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl index 8d2624a..3ed1166 100644 --- a/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl +++ b/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl @@ -68,7 +68,7 @@ pstage:stage3 doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; mod:brand "TooB"; mod:label "Power Stage"; diff --git a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl index 3dcd19c..b6cedc6 100644 --- a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl +++ b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl @@ -57,7 +57,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment "TooB spectrum analyzer" ; mod:brand "TooB"; diff --git a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl index 3bfa9f3..b368488 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl @@ -57,13 +57,13 @@ tonestack:eqGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; uiext:ui ; rdfs:comment """ TooB Tone Stack emulates the tone controls of a 59' Bassman, a JCM8000, or a Baxandall -tone stack used in Polytone and HiWatt amps. +tone stack (used in Polytone and HiWatt amps). """ ; mod:brand "TooB"; diff --git a/lv2/aarch64/ToobAmp.lv2/Toob3BandEq.ttl b/lv2/aarch64/ToobAmp.lv2/Toob3BandEq.ttl index ea0a3e8..9d46e87 100644 --- a/lv2/aarch64/ToobAmp.lv2/Toob3BandEq.ttl +++ b/lv2/aarch64/ToobAmp.lv2/Toob3BandEq.ttl @@ -45,14 +45,14 @@ toob:frequencyResponseVector a lv2:Plugin , lv2:EQPlugin ; - doap:name "TooB 3 Band EQ (mono)" , - "TooB 3 Band EQ (mono)"@en-gb + doap:name "TooB 3 Band EQ (Mono)" , + "TooB 3 Band EQ (Mono)"@en-gb ; doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; uiext:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/Toob3BandEqStereo.ttl b/lv2/aarch64/ToobAmp.lv2/Toob3BandEqStereo.ttl index 3d0dfff..36581e2 100644 --- a/lv2/aarch64/ToobAmp.lv2/Toob3BandEqStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/Toob3BandEqStereo.ttl @@ -52,7 +52,7 @@ toob:frequencyResponseVector doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; uiext:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so index 40cfb54..c5fae33 100755 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so and b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so index 8e7c67f..5cbfa44 100755 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so and b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl index d2f9135..d9a06b6 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl @@ -40,14 +40,14 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ Emulation of a Boss CE-2 Chorus. """ ; mod:brand "TooB"; - mod:label "TooB CE-2"; + mod:label "TooB Chorus"; lv2:optionalFeature lv2:hardRTCapable; lv2:port diff --git a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl index 67e6360..45483d2 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ A straightforward no-frills digital delay. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl index 1f4924a..7c9d368 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl @@ -41,14 +41,14 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original. """ ; mod:brand "TooB"; - mod:label "TooB BF-2"; + mod:label "TooB Flanger"; lv2:optionalFeature lv2:hardRTCapable; lv2:port diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl index b916a0a..a645e87 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ Digital emulation of a Boss BF-2 Flanger. @@ -52,7 +52,7 @@ should use the monophonic version of this effect. """ ; mod:brand "TooB"; - mod:label "TooB BF-2 Stereo"; + mod:label "TooB Flanger (Stereo)"; lv2:optionalFeature lv2:hardRTCapable; lv2:port diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl index 6159bbd..2a2dba8 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl b/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl index e55db06..c5bb1f3 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl @@ -33,21 +33,21 @@ a lv2:Plugin , - lv2:EQPlugin ; - doap:name "TooB GE-7 Graphic Eq" , - "TooB GE-7 Graphic Eq"@en-gb + lv2:MultiEQPlugin ; + doap:name "TooB Graphic Eq" , + "TooB Graphic Eq"@en-gb ; uiext:ui ; doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ A 7 octave graphic equalizer, with frequencies chosen to be useful for EQ-ing guitar signals. """ ; mod:brand "TooB"; - mod:label "GE-7 Graphic Eq"; + mod:label "Graphic Eq"; lv2:optionalFeature lv2:hardRTCapable; lv2:port [ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl b/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl index 4e8eccd..58087e0 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl @@ -91,8 +91,8 @@ myprefix:output_group ; doap:license ; doap:maintainer ; - lv2:minorVersion 2 ; - lv2:microVersion 73 ; + lv2:minorVersion 3 ; + lv2:microVersion 78 ; ui:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl b/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl index faba57d..bdaa1c8 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl @@ -77,8 +77,8 @@ myprefix:output_group doap:license ; doap:maintainer ; - lv2:minorVersion 2 ; - lv2:microVersion 73 ; + lv2:minorVersion 3 ; + lv2:microVersion 78 ; ui:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl index 23f1587..d70dfaa 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl @@ -67,7 +67,7 @@ toobml:sagGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented using neural-network-based machine learning models of real amplifiers. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl b/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl index 22771b1..1b28d78 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ Remix a stereo input signal. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl b/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl index 6214680..f1647f1 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl @@ -44,7 +44,9 @@ toobNam:modelFile a lv2:Parameter; rdfs:label "Model"; mod:fileTypes "nam,nammodel"; - rdfs:range atom:Path. + rdfs:range atom:Path. + + toobNam:eqGroup a param:ControlGroup , @@ -72,7 +74,7 @@ toobNam:calibrationGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ TooB Neural Amp Modeler is a neural network based Amp Simulator. It uses .nam model files that are generated by training nueral networks on audio recordings of actual guitar amps, effects pedals and other equipment. .nam files contain data from the trained models, which can be loaded into @@ -90,11 +92,13 @@ are constantly coming up with new and ingenious applications for NAM models. .nam model files can be donwloaded from the internet. There are many online sources for .nam model files, both free, and commercial. Foremost among these is the http://tone3000.com website, which provides a vast collection of free .nam models files that have been developed and uploaded by a large online community of NAM users. -Loading .nam model files into PiPedal is a two-step process. First, download model files to your local computer, or your phone or tablet, using a -web browser. Then upload the model files to the PiPedal server using the PiPedal web interface. You can find an "Upload" button in -the file browser when you click on the "Model" control in the plugin's user interface within the PiPedal web interface. +To upload Tone3000 models directly to the PiPedal server, click on the Model control, and then click on the "DOWNLOAD MODEL FILES FROM TONE3000" button at the bottom +of the browser dialog. This will bring up a browser window for Tone3000 NAM models. When you select a tone bundle, the bundle will be uploaded diretly to the PiPedal server. -If you are not using PiPedal, just click on the Model control, and use the file browser to select the .nam file directly on your local system. +If you obtain .nam models from other source (e.g. commercial tone bundles), you can upload .nam models, or .zip files containing bundles of .nam modesl to +the pipedal server by tapping the "UPLOAD" button found at the bottom of the model file browser dialog. + +If you are not using PiPedal, just click on the Model control, and use the host application's file browser to select .nam files directly on your local system. TooB Neural Amp Modeler supports a much wider range of amp models than ToobML, but usually uses more CPU. You will need at least a Pi 4 to use Toob Neural Amp Modeler, and you may need to increase your audio buffer sizes to prevent overruns. @@ -122,23 +126,34 @@ what the threshold is; but the effect on performance is pretty obvious when heat Note that heat throttling is done to protect your processor from permanent damage due to overheating; but there's no real risk of actual damage, since heat throttling thresholds are set well below the point at which damage occurs. -TooB Neural Amp Modeler is based on technology developed by the NeuralAmp Modeler Core project (https://www.neuralampmodeler.com/). The TooB team wishes +TooB NAM now supports NAM A2 models, but supports the older NAM A1 format as well. Models downloaded through TooB NAM's "DOWNLOAD MODEL FILES FROM TONE3000" button will usually +be A2 models. Currently, tone bundles downloaded from the Tone3000 website directly will be A1 models. Model bundles that you obtain from other sources may also +be A1 or A2 models. + +A2 models generally offer better audio quality and lower CPU use than comparable A1 models. + +One of the key new features introduced in NAM A2 is support for "Slimmable" models. A Slimmable model +is a single model file that contains multiple quality tiers, allowing you to interactively trade off +audio quality against CPU usage at runtime. When you load a Slimmable A2 model, a new "Slim" dial +appears in the TooB NAM controls. Turn it down to use a lighter, lower-quality tier that saves CPU; +turn it up to use a higher-quality tier at greater CPU cost. + +The number of quality tiers available depends on how the model was trained. Tone3000 A2 models +currently provide two tiers: a Slim weight of 0.5, and a Slim weight of 1.0. The lighter tier (0.5) +delivers quality comparable to a typical A1 model but at noticeably lower CPU cost. The heavier tier +(1.0) delivers significantly better quality than comparable A1 models, with only a modest increase in +CPU use. Future A2 models may provide more flexible Slimmable weight settings. + +--- + +TooB Neural Amp Modeler uses code from the Neural Amp Modeler Core project (https://www.neuralampmodeler.com/). The TooB team wishes to express gratitude to Steven Atkinson for making this revolutionary technology available as open-source code. This is groundbreaking -technology that changes everything. None of this would have happened without the work that Steven has done. - -TooB Neural Amp Modeler uses code from the NeuralAudio project (https://github.com/mikeoliphant/NeuralAudio). The Neural Audio project provides highly -optimized implementations of Neural Amp models. The TooB team wishes to express gratitude to Mike Oliphant for -making his optimizations work available as open-source code. - -Steve and Mike have done all of the hard work that was required to make NAM technology available through TooB Neural Amp Modeler. They -deserve all the credit. - -Code from the the NeuralAudio project (https://github.com/mikeoliphant/NeuralAudio) is provided -under the following license. +technology that changes everything. None of this would have happened without the work that Steven has done. Code from the Neural Amp +Modeler core project is provided under the following license: MIT License -Copyright (c) 2024, 2025 Mike Oliphant +Copyright (c) Steven Atkinson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -158,12 +173,23 @@ 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. -Code from the the NeuralAmpModelerCore project (https://github.com/sdatkinson/NeuralAmpModelerCore) is provided + +--- + +TooB Neural Amp Modeler uses code from the NeuralAudio project (https://github.com/mikeoliphant/NeuralAudio). The Neural Audio project provides highly +optimized code for executing NAM A1 models. The TooB team wishes to express gratitude to Mike Oliphant for +making his optimizations work available as open-source code. + +Steve and Mike have done all of the hard work that was required to make NAM technology available through TooB Neural Amp Modeler. They +deserve all the credit. + +Code from the the NeuralAudio project (https://github.com/mikeoliphant/NeuralAudio) is provided under the following license. + MIT License -Copyright (c) 2025 Steven Atkinson +Copyright (c) Mike Oliphant Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -232,6 +258,19 @@ SOFTWARE. lv2:ControlPort ; lv2:index 2; + lv2:symbol "modelSize" ; + lv2:name "Slim"; + lv2:default 0.0 ; + lv2:minimum 0.0; + lv2:maximum 1.0; + lv2:portProperty epp:expensive; + rdfs:comment "Model quality for NAM A2 models that support slimmable models."; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 3; lv2:symbol "outputGain" ; lv2:name "Output Gain"; lv2:default 0.0 ; @@ -245,7 +284,7 @@ SOFTWARE. a lv2:InputPort , lv2:ControlPort ; - lv2:index 3; + lv2:index 4; lv2:symbol "gate" ; lv2:name "Noise Gate"; lv2:default -120.0 ; @@ -264,7 +303,7 @@ SOFTWARE. a lv2:OutputPort , lv2:ControlPort ; - lv2:index 4; + lv2:index 5; lv2:symbol "gateOut" ; lv2:name "\u00A0"; lv2:default 0.0; @@ -278,7 +317,7 @@ SOFTWARE. a lv2:InputPort , lv2:ControlPort ; - lv2:index 5; + lv2:index 6; lv2:symbol "buffer" ; lv2:name "Threaded"; lv2:default 0.0 ; @@ -295,7 +334,7 @@ SOFTWARE. a lv2:InputPort , lv2:ControlPort ; - lv2:index 6 ; + lv2:index 7 ; lv2:symbol "toneStack" ; lv2:name "Type"; lv2:default 3.0 ; @@ -324,7 +363,7 @@ SOFTWARE. a lv2:InputPort , lv2:ControlPort ; - lv2:index 7; + lv2:index 8; lv2:symbol "bass" ; lv2:name "Bass"; lv2:default 5.0; @@ -338,7 +377,7 @@ SOFTWARE. a lv2:InputPort , lv2:ControlPort ; - lv2:index 8; + lv2:index 9; lv2:symbol "mid" ; lv2:name "Mid"; lv2:default 5.0; @@ -351,13 +390,13 @@ SOFTWARE. a lv2:InputPort , lv2:ControlPort ; - lv2:index 9; + lv2:index 10; lv2:symbol "treble" ; lv2:name "Treble"; lv2:default 5.0; lv2:minimum 0.0; lv2:maximum 10.0; - rdfs:comment "Bass"; + rdfs:comment "Treble"; pg:group toobNam:eqGroup ; ], @@ -367,7 +406,7 @@ SOFTWARE. a lv2:InputPort , lv2:ControlPort ; - lv2:index 10 ; + lv2:index 11 ; lv2:symbol "inputCalibrationMode" ; lv2:name "Input Calibration"; lv2:default 1.0 ; @@ -381,6 +420,7 @@ SOFTWARE. rdfs:label "Calibrated" ; rdf:value 1.0 ]; + rdfs:comment "Calibration method for audio input."; pg:group toobNam:calibrationGroup ; @@ -389,7 +429,7 @@ SOFTWARE. a lv2:InputPort , lv2:ControlPort ; - lv2:index 11 ; + lv2:index 12 ; lv2:symbol "calibration" ; lv2:name "Value"; lv2:default -6.0 ; @@ -404,13 +444,13 @@ SOFTWARE. a lv2:InputPort , lv2:ControlPort ; - lv2:index 12; + lv2:index 13; lv2:symbol "outputCalibration" ; lv2:name "Output Calibration"; lv2:default 0.0; lv2:minimum 0.0; lv2:maximum 2.0; - rdfs:comment "Scaling applied to model output"; + rdfs:comment "Calbration method for audio output"; lv2:portProperty lv2:enumeration ; lv2:scalePoint [ @@ -431,7 +471,7 @@ SOFTWARE. a lv2:InputPort , lv2:ControlPort ; - lv2:index 13 ; + lv2:index 14 ; lv2:symbol "version" ; lv2:name "Preset Version"; lv2:default 1.0 ; @@ -439,19 +479,59 @@ SOFTWARE. lv2:maximum 1.0; lv2:portProperty epp:notOnGUI; ], + [ + a lv2:OutputPort , + lv2:ControlPort ; + lv2:index 15 ; + lv2:symbol "modelType" ; + lv2:name ""; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 4.0; + lv2:portProperty lv2:enumeration ; + lv2:scalePoint [ + rdfs:label "" ; + rdf:value 0.0 + ] , [ + rdfs:label "A1" ; + rdf:value 1.0 + ] , [ + rdfs:label "A2" ; + rdf:value 2.0 + ] , [ + rdfs:label "AidaX" ; + rdf:value 3.0 + ] , [ + rdfs:label "Other" ; + rdf:value 4.0 + ]; + + ], + [ + a lv2:OutputPort , + lv2:ControlPort ; + + lv2:index 16 ; + lv2:symbol "currentModelWeight" ; + lv2:name ""; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0; + lv2:portProperty epp:notOnGUI; + ], [ a lv2:AudioPort , lv2:InputPort ; - lv2:index 14 ; + lv2:index 17 ; lv2:symbol "in" ; lv2:name "In" ], [ a lv2:AudioPort , lv2:OutputPort ; - lv2:index 15 ; + lv2:index 18 ; lv2:symbol "out" ; lv2:name "Out" ], @@ -463,7 +543,7 @@ SOFTWARE. lv2:designation lv2:control ; atom:supports patch:Message ; - lv2:index 16 ; + lv2:index 19 ; lv2:symbol "control" ; lv2:name "Control" ; rdfs:comment "Control" ; @@ -473,7 +553,7 @@ SOFTWARE. atom:bufferType atom:Sequence ; # atom:supports patch:Message; lv2:designation lv2:control ; - lv2:index 17; + lv2:index 20; lv2:symbol "notify" ; lv2:name "Notify" ; rdfs:comment "Notification" ; @@ -520,7 +600,7 @@ toobNam:ui pipedal_ui:frequencyPlot [ pipedal_ui:patchProperty toobNam:FrequencyResponse; - lv2:index 10 ; + lv2:index 11 ; pg:group toobNam:eqGroup ; pipedal_ui:width: 120.0 ; ] diff --git a/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl b/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl index b15d898..eb03c74 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl @@ -54,7 +54,7 @@ noisegate:envelope_group doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ A noise gate is an audio processing tool that controls the volume of an audio signal by allowing it to pass through only when it exceeds a set threshold. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobParametricEq.ttl b/lv2/aarch64/ToobAmp.lv2/ToobParametricEq.ttl index 12613db..e4ed015 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobParametricEq.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobParametricEq.ttl @@ -87,7 +87,7 @@ parametric_eq:hfGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; uiext:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobParametricEqStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobParametricEqStereo.ttl index fa150bb..8403db7 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobParametricEqStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobParametricEqStereo.ttl @@ -87,7 +87,7 @@ toob-parametric-eq-stereo:hfGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; uiext:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl b/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl index 1bb5572..aa97129 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ A loose emulation of an MXR® Phase 90 Phaser. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl b/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl index 7b5ca75..b2ec083 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl @@ -60,7 +60,7 @@ toobPlayer:seek doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ Play audio files. Audio files can optionally be loooped by pressing the "Set loop" button. @@ -84,7 +84,7 @@ folder artwork files that have a .jpg file extension. mod:label "File Player"; # ONLY works in PiPedal - lv2:requiredFeature "http://github.com/rerdavies/pipedal#host" ; + lv2:requiredFeature ; lv2:optionalFeature lv2:hardRTCapable,state::freePath, state:makePath, state:mapPath, work:schedule ; lv2:extensionData state:interface, work:interface; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl b/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl index 170d59f..849814b 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl @@ -50,8 +50,8 @@ recordPrefix:audioFile ; doap:license ; doap:maintainer ; - lv2:minorVersion 2 ; - lv2:microVersion 73 ; + lv2:minorVersion 3 ; + lv2:microVersion 78 ; ui:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl index f1cc998..ac8bbb9 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl @@ -87,8 +87,8 @@ myprefix:loop3_group ; doap:license ; doap:maintainer ; - lv2:minorVersion 2 ; - lv2:microVersion 73 ; + lv2:minorVersion 3 ; + lv2:microVersion 78 ; ui:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobTone.ttl b/lv2/aarch64/ToobAmp.lv2/ToobTone.ttl new file mode 100644 index 0000000..c3ef9df --- /dev/null +++ b/lv2/aarch64/ToobAmp.lv2/ToobTone.ttl @@ -0,0 +1,216 @@ +@prefix doap: . +@prefix lv2: . +@prefix rdf: . +@prefix rdfs: . +@prefix units: . +@prefix urid: . +@prefix atom: . +@prefix midi: . +@prefix epp: . +@prefix uiext: . +@prefix idpy: . +@prefix foaf: . +@prefix mod: . +@prefix param: . +@prefix work: . +@prefix pg: . + +@prefix atom: . +@prefix patch: . +@prefix rdfs: . +@prefix state: . +@prefix urid: . +@prefix xsd: . +@prefix toob: . + +@prefix pipedal_ui: . +@prefix pipedal_patch: . + + + + a foaf:Person ; + foaf:name "Robin Davies" ; + foaf:mbox ; + foaf:homepage . + + + a lv2:Plugin , + lv2:EQPlugin; + doap:name "TooB Tone (Mono)" , + "TooB Tone (Mono)"@en-gb + ; + uiext:ui ; + doap:license ; + doap:maintainer ; + lv2:minorVersion 0 ; + lv2:microVersion 78 ; + rdfs:comment """ +TooB Tone is a simple tone control. For more flexible EQ plugins, see TooB 3 Band Eq, or TooB Parameteric EQ. + +""" ; + + mod:brand "TooB"; + mod:label "Tone"; + lv2:optionalFeature lv2:hardRTCapable; + pipedal_patch:readable + toob:frequencyResponseVector; + + lv2:port + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 0 ; + lv2:symbol "tone" ; + lv2:name "Tone"; + + lv2:default 0.0 ; + lv2:minimum -1 ; + lv2:maximum 1 ; + rdfs:comment "Tone"; + + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 1 ; + lv2:symbol "gain" ; + lv2:name "Gain"; + + lv2:default 0.0 ; + lv2:minimum -40.0 ; + lv2:maximum 30.0; + units:unit units:db; + + ], + + [ + lv2:portProperty epp:notOnGUI; # Tuning + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 2 ; + lv2:symbol "lowFc" ; + lv2:name "Low Fc"; + + lv2:default 480.0 ; + lv2:minimum 110.0 ; + lv2:maximum 1000.0; + units:unit units:hz; + + ], + [ + lv2:portProperty epp:notOnGUI; # Tuning + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 3 ; + lv2:symbol "lowBoost" ; + lv2:name "Low Boost"; + + lv2:default -17.0 ; + lv2:minimum -30.0 ; + lv2:maximum 0.0; + units:unit units:db; + ], + [ + lv2:portProperty epp:notOnGUI; # Tuning + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 4 ; + lv2:symbol "highFc" ; + lv2:name "High Fc"; + + lv2:default 480.0 ; + lv2:minimum 110.0 ; + lv2:maximum 2000.0; + units:unit units:hz; + + ], + [ + lv2:portProperty epp:notOnGUI; # Tuning + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 5 ; + lv2:symbol "highBoost" ; + lv2:name "High Boost"; + + lv2:default -10.0 ; + lv2:minimum -30.0 ; + lv2:maximum 0.0; + units:unit units:db; + ], + + [ + a atom:AtomPort , + lv2:InputPort; + atom:bufferType atom:Sequence ; + # atom:supports patch:Message; + lv2:designation lv2:control ; + lv2:index 6 ; + lv2:symbol "controlIn" ; + lv2:name "Control" ; + rdfs:comment "Plugin to GUI communication" ; + ] , + [ + a atom:AtomPort , + lv2:OutputPort ; + atom:bufferType atom:Sequence ; + # atom:supports patch:Message; + lv2:designation lv2:control ; + lv2:index 7 ; + lv2:symbol "notifyOut" ; + lv2:name "Notify" ; + rdfs:comment "Plugin to GUI communication" ; + ], + # Audio + [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 8 ; + lv2:symbol "in" ; + lv2:name "In" + ], + [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 9 ; + lv2:symbol "out" ; + lv2:name "Out" + ] + . + + + a uiext:X11UI ; + lv2:binary ; + + lv2:extensionData uiext::idle ; + lv2:extensionData uiext:resize ; + lv2:extensionData uiext:idleInterface; + lv2:requiredFeature uiext:idleInterface ; + . + + + pipedal_ui:ui . + + + + a pipedal_ui:ui ; + pipedal_ui:frequencyPlot + [ + lv2:index 0 ; + pipedal_ui:patchProperty toob:frequencyResponseVector; + pipedal_ui:xLeft 30 ; + pipedal_ui:xRight 22050 ; + pipedal_ui:yTop 10 ; + pipedal_ui:yBottom -35 ; + pipedal_ui:xLog 1 ; + pipedal_ui:yDb 1 ; + pipedal_ui:width 100 ; + ] + . + + diff --git a/lv2/aarch64/ToobAmp.lv2/ToobToneStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobToneStereo.ttl new file mode 100644 index 0000000..967c17d --- /dev/null +++ b/lv2/aarch64/ToobAmp.lv2/ToobToneStereo.ttl @@ -0,0 +1,231 @@ +@prefix doap: . +@prefix lv2: . +@prefix rdf: . +@prefix rdfs: . +@prefix units: . +@prefix urid: . +@prefix atom: . +@prefix midi: . +@prefix epp: . +@prefix uiext: . +@prefix idpy: . +@prefix foaf: . +@prefix mod: . +@prefix param: . +@prefix work: . +@prefix pg: . + +@prefix atom: . +@prefix patch: . +@prefix rdfs: . +@prefix state: . +@prefix urid: . +@prefix xsd: . +@prefix toob: . + +@prefix pipedal_ui: . +@prefix pipedal_patch: . + + + + a foaf:Person ; + foaf:name "Robin Davies" ; + foaf:mbox ; + foaf:homepage . + + + a lv2:Plugin , + lv2:EQPlugin; + doap:name "TooB Tone (Stereo)" , + "TooB Tone (Stereo)"@en-gb + ; + uiext:ui ; + doap:license ; + doap:maintainer ; + lv2:minorVersion 0 ; + lv2:microVersion 78 ; + rdfs:comment """ +TooB Tone is a simple tone control. For more flexible EQ plugins, see TooB 3 Band Eq, or TooB Parameteric EQ. + +""" ; + + mod:brand "TooB"; + mod:label "Tone"; + lv2:optionalFeature lv2:hardRTCapable; + pipedal_patch:readable + toob:frequencyResponseVector; + + lv2:port + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 0 ; + lv2:symbol "tone" ; + lv2:name "Tone"; + + lv2:default 0.0 ; + lv2:minimum -1 ; + lv2:maximum 1 ; + rdfs:comment "Tone"; + + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 1 ; + lv2:symbol "gain" ; + lv2:name "Gain"; + + lv2:default 0.0 ; + lv2:minimum -40.0 ; + lv2:maximum 30.0; + units:unit units:db; + + ], + + [ + lv2:portProperty epp:notOnGUI; # Tuning + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 2 ; + lv2:symbol "lowFc" ; + lv2:name "Low Fc"; + + lv2:default 110.0 ; + lv2:minimum 480.0 ; + lv2:maximum 1000.0; + units:unit units:hz; + + ], + [ + lv2:portProperty epp:notOnGUI; # Tuning + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 3 ; + lv2:symbol "lowBoost" ; + lv2:name "Low Boost"; + + lv2:default -17.0 ; + lv2:minimum -30.0 ; + lv2:maximum 0.0; + units:unit units:db; + ], + [ + lv2:portProperty epp:notOnGUI; # Tuning + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 4 ; + lv2:symbol "highFc" ; + lv2:name "High Fc"; + + lv2:default 480.0 ; + lv2:minimum 110.0 ; + lv2:maximum 2000.0; + units:unit units:hz; + + ], + [ + lv2:portProperty epp:notOnGUI; # Tuning + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 5 ; + lv2:symbol "highBoost" ; + lv2:name "High Boost"; + + lv2:default -10.0 ; + lv2:minimum -30.0 ; + lv2:maximum 0.0; + units:unit units:db; + ], + + [ + a atom:AtomPort , + lv2:InputPort; + atom:bufferType atom:Sequence ; + # atom:supports patch:Message; + lv2:designation lv2:control ; + lv2:index 6 ; + lv2:symbol "controlIn" ; + lv2:name "Control" ; + rdfs:comment "Plugin to GUI communication" ; + ] , + + [ + a atom:AtomPort , + lv2:OutputPort ; + atom:bufferType atom:Sequence ; + # atom:supports patch:Message; + lv2:designation lv2:control ; + lv2:index 7 ; + lv2:symbol "notifyOut" ; + lv2:name "Notify" ; + rdfs:comment "Plugin to GUI communication" ; + ], + + [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 8 ; + lv2:symbol "in" ; + lv2:name "InL" + ], + [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 9 ; + lv2:symbol "out" ; + lv2:name "OutL" + ], + [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 10 ; + lv2:symbol "inR" ; + lv2:name "InR" + ], + [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 11 ; + lv2:symbol "outR" ; + lv2:name "OutR" + ] + . + + + a uiext:X11UI ; + lv2:binary ; + + lv2:extensionData uiext::idle ; + lv2:extensionData uiext:resize ; + lv2:extensionData uiext:idleInterface; + lv2:requiredFeature uiext:idleInterface ; + . + + + pipedal_ui:ui . + + + + a pipedal_ui:ui ; + pipedal_ui:frequencyPlot + [ + lv2:index 0 ; + pipedal_ui:patchProperty toob:frequencyResponseVector; + pipedal_ui:xLeft 30 ; + pipedal_ui:xRight 22050 ; + pipedal_ui:yTop 10 ; + pipedal_ui:yBottom -35 ; + pipedal_ui:xLog 1 ; + pipedal_ui:yDb 1 ; + pipedal_ui:width 100 ; + ] + . + + diff --git a/lv2/aarch64/ToobAmp.lv2/ToobTremolo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobTremolo.ttl index 3511a3d..fdbe85e 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobTremolo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobTremolo.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ A tremolo effect, equivalent to "vibrato" on vintage Fender tube amps. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobTremoloMono.ttl b/lv2/aarch64/ToobAmp.lv2/ToobTremoloMono.ttl index 3087f51..fa6608c 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobTremoloMono.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobTremoloMono.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ A tremolo effect, equivalent to "vibrato" on vintage Fender tube amps. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl index 93b03da..94225d2 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ TooB Tuner is a chromatic guitar tuner. """ ; @@ -150,6 +150,7 @@ TooB Tuner is a chromatic guitar tuner. lv2:extensionData uiext::idle ; lv2:extensionData uiext:resize ; + lv2:extensionData uiext:showInterface ; lv2:extensionData uiext:idleInterface; lv2:requiredFeature uiext:idleInterface ; . diff --git a/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl b/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl index a21868b..62452da 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl @@ -39,7 +39,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 73 ; + lv2:microVersion 78 ; rdfs:comment """ Volume control. diff --git a/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a72.so b/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a72.so index 5a6ae40..3e2dba1 100755 Binary files a/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a72.so and b/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a72.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a76.so b/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a76.so index de7fc43..76dcb77 100755 Binary files a/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a76.so and b/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a76.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/manifest.ttl b/lv2/aarch64/ToobAmp.lv2/manifest.ttl index 3e7ee13..0c33b70 100644 --- a/lv2/aarch64/ToobAmp.lv2/manifest.ttl +++ b/lv2/aarch64/ToobAmp.lv2/manifest.ttl @@ -135,6 +135,15 @@ lv2:binary ; rdfs:seeAlso . + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + + a lv2:Plugin ; lv2:binary ; rdfs:seeAlso . diff --git a/lv2/aarch64/ToobAmp.lv2/resources/Toob3BandEqLogo.svg b/lv2/aarch64/ToobAmp.lv2/resources/Toob3BandEqLogo.svg new file mode 100644 index 0000000..0dba214 --- /dev/null +++ b/lv2/aarch64/ToobAmp.lv2/resources/Toob3BandEqLogo.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + diff --git a/lv2/aarch64/ToobAmp.lv2/resources/ToobToneLogo.svg b/lv2/aarch64/ToobAmp.lv2/resources/ToobToneLogo.svg new file mode 100644 index 0000000..4b5a875 --- /dev/null +++ b/lv2/aarch64/ToobAmp.lv2/resources/ToobToneLogo.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + diff --git a/lv2/x86_64/toobamp_1.2.73_amd64.deb b/lv2/x86_64/toobamp_1.3.78_amd64.deb similarity index 53% rename from lv2/x86_64/toobamp_1.2.73_amd64.deb rename to lv2/x86_64/toobamp_1.3.78_amd64.deb index 4338fe1..2f6ba9b 100644 Binary files a/lv2/x86_64/toobamp_1.2.73_amd64.deb and b/lv2/x86_64/toobamp_1.3.78_amd64.deb differ diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index f9aee83..69fcf98 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.16.0) -add_subdirectory("SQLiteCpp") +# Prevent installation of SQLite built components. (we use the .a library files directly) + +add_subdirectory("SQLiteCpp" EXCLUDE_FROM_ALL) + set(SDBUSCPP_BUILD_CODEGEN off) set(SDBUSCPP_INSTALL off) @@ -23,3 +26,4 @@ option(BUILD_OSSFUZZ "Build fuzzers for ossfuzz" OFF) option(BUILD_EXAMPLES "Build examples" OFF) option(BUILD_DOC "Build documentation" OFF) add_subdirectory("libzip") + diff --git a/provisionLv2 b/provisionLv2-aarch64.sh similarity index 100% rename from provisionLv2 rename to provisionLv2-aarch64.sh diff --git a/provisionx64.sh b/provisionLv2-x64.sh similarity index 100% rename from provisionx64.sh rename to provisionLv2-x64.sh diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..5c83330 --- /dev/null +++ b/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +build/src/pipedald "/etc/pipedal/config" "/etc/pipedal/react" "-port" "0.0.0.0:8080" diff --git a/src/AesDigest.cpp b/src/AesDigest.cpp new file mode 100644 index 0000000..825f1e0 --- /dev/null +++ b/src/AesDigest.cpp @@ -0,0 +1,40 @@ +#include "AesDigest.hpp" + +#include +#include +#include + +using namespace pipedal; + + +std::string pipedal::AesDigest(const std::string &text) +{ + std::array digest{}; + size_t digestLength = 0; + + if (EVP_Q_digest( + nullptr, + "SHA256", + nullptr, + reinterpret_cast(text.data()), + text.size(), + digest.data(), + &digestLength) + != 1) + { + throw std::runtime_error("EVP_Q_digest(SHA256) failed."); + } + + static constexpr char hexDigits[] = "0123456789abcdef"; + std::string result(digestLength * 2, '\0'); + for (size_t i = 0; i < digestLength; ++i) + { + unsigned char value = digest[i]; + result[2 * i] = hexDigits[(value >> 4) & 0x0F]; + result[2 * i + 1] = hexDigits[value & 0x0F]; + } + return result; +} + + + diff --git a/src/AesDigest.hpp b/src/AesDigest.hpp new file mode 100644 index 0000000..8d122b2 --- /dev/null +++ b/src/AesDigest.hpp @@ -0,0 +1,25 @@ +// Copyright (c) 2026 Robin E. R. 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. + +#pragma once + +#include +namespace pipedal { + std::string AesDigest(const std::string &text); +} \ No newline at end of file diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index c86c554..2c4086b 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -23,6 +23,7 @@ */ #include "pch.h" +#include "PiPedalCommon.hpp" #include "util.hpp" #include #include "Finally.hpp" @@ -39,6 +40,7 @@ #include "CrashGuard.hpp" #include #include +#include "ChannelRouterSettings.hpp" #include "CpuUse.hpp" @@ -271,7 +273,7 @@ namespace pipedal auto inAvail = snd_pcm_avail_update(this->captureHandle); auto outAvail = snd_pcm_avail_update(this->playbackHandle); - auto total = (inAvail >= 0? inAvail: 0) + (outAvail >= 0 ? outAvail: 0) + framesInBuffer; + auto total = (inAvail >= 0 ? inAvail : 0) + (outAvail >= 0 ? outAvail : 0) + framesInBuffer; bufferTraces[bufferTraceIndex++] = { time, inAvail, @@ -328,11 +330,20 @@ namespace pipedal bool inputSwapped = false; bool outputSwapped = false; - std::vector activeCaptureBuffers; - std::vector activePlaybackBuffers; + std::vector> allocatedBuffers; + + std::vector deviceCaptureBuffers; + std::vector devicePlaybackBuffers; + float *zeroInputBuffer = nullptr; + float *discardOutputBuffer = nullptr; + std::vector mainCaptureBuffers; + std::vector mainPlaybackBuffers; + std::vector mainVuPlaybackBuffers; + + std::vector auxCaptureBuffers; + std::vector auxPlaybackBuffers; + std::vector auxVuPlaybackBuffers; - std::vector captureBuffers; - std::vector playbackBuffers; std::vector rawCaptureBuffer; std::vector rawPlaybackBuffer; @@ -342,7 +353,9 @@ namespace pipedal void validate_capture_handle() { // leftover debugging for a buffer overrun :-/ #ifdef DEBUG - if (snd_pcm_type(captureHandle) != SND_PCM_TYPE_HW) + auto pcmType = snd_pcm_type(captureHandle); + + if (pcmType != SND_PCM_TYPE_HW && pcmType != SND_PCM_TYPE_NULL) { throw std::runtime_error("Capture handle has been overwritten"); } @@ -742,7 +755,7 @@ namespace pipedal { int32_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -760,7 +773,7 @@ namespace pipedal { float *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -776,7 +789,7 @@ namespace pipedal { int16_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr double scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -792,7 +805,7 @@ namespace pipedal { int16_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -809,7 +822,7 @@ namespace pipedal { int32_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -825,7 +838,7 @@ namespace pipedal { uint8_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1LL); for (size_t frame = 0; frame < frames; ++frame) @@ -842,7 +855,7 @@ namespace pipedal { uint8_t *p = (uint8_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1LL); for (size_t frame = 0; frame < frames; ++frame) @@ -859,7 +872,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (0x00FFFFFFL + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -875,7 +888,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (0x00FFFFFFL + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -891,7 +904,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -907,7 +920,7 @@ namespace pipedal { int16_t *p = (int16_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr float scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -927,7 +940,7 @@ namespace pipedal { int16_t *p = (int16_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr float scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -947,7 +960,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -969,7 +982,7 @@ namespace pipedal int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = 0x00FFFFFF; for (size_t frame = 0; frame < frames; ++frame) @@ -991,7 +1004,7 @@ namespace pipedal int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = 0x00FFFFFF; for (size_t frame = 0; frame < frames; ++frame) @@ -1011,7 +1024,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -1031,7 +1044,7 @@ namespace pipedal { uint8_t *p = (uint8_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -1056,7 +1069,7 @@ namespace pipedal { uint8_t *p = (uint8_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -1082,7 +1095,7 @@ namespace pipedal { float *p = (float *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -1097,7 +1110,7 @@ namespace pipedal { float *p = (float *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -1110,27 +1123,9 @@ namespace pipedal } } - public: - void TestFormatEncodeDecode(snd_pcm_format_t captureFormat); - - private: - void AllocateBuffers(std::vector &buffers, size_t n) - { - buffers.resize(n); - for (size_t i = 0; i < n; ++i) - { - buffers[i] = new float[this->bufferSize]; - for (size_t j = 0; j < this->bufferSize; ++j) - { - buffers[i][j] = 0; - } - } - } - - JackChannelSelection channelSelection; - bool open = false; - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) override { + this->isDummyDriver = jackServerSettings.IsDummyAudioDevice(); terminateAudio_ = false; if (open) { @@ -1154,6 +1149,27 @@ namespace pipedal } } + public: + void TestFormatEncodeDecode(snd_pcm_format_t captureFormat); + + private: + void AllocateBuffers(std::vector &buffers, size_t n) + { + buffers.resize(n); + for (size_t i = 0; i < n; ++i) + { + buffers[i] = new float[this->bufferSize]; + for (size_t j = 0; j < this->bufferSize; ++j) + { + buffers[i][j] = 0; + } + } + } + + ChannelSelection channelSelection; + + bool open = false; + void RestartAlsa() { std::lock_guard lock{restartMutex}; @@ -1180,13 +1196,13 @@ namespace pipedal throw std::runtime_error("Unable to restart the audio stream."); } int err; - + if ((err = snd_pcm_start(captureHandle)) < 0) { Lv2Log::error(SS("Unable to restart ALSA capture: " << snd_strerror(err))); throw PiPedalStateException("Unable to restart ALSA capture."); } - TraceBufferPositions(0,'+'); + TraceBufferPositions(0, '+'); audioRunning = true; } @@ -1249,7 +1265,7 @@ namespace pipedal rawCaptureBuffer.resize(captureFrameSize * bufferSize * 2); memset(rawCaptureBuffer.data(), 0, rawCaptureBuffer.size()); - AllocateBuffers(captureBuffers, captureChannels); + AllocateBuffers(deviceCaptureBuffers, captureChannels); } virtual std::string GetConfigurationDescription() @@ -1260,8 +1276,10 @@ namespace pipedal << ", " << GetAlsaFormatDescription(this->captureFormat) << ", " << this->sampleRate << ", " << this->bufferSize << "x" << this->numberOfBuffers - << ", in: " << this->InputBufferCount() << "/" << this->captureChannels - << ", out: " << this->OutputBufferCount() << "/" << this->playbackChannels); + << ", " << "device: " << this->DeviceInputBufferCount() << "/" << this->DeviceOutputBufferCount() + << ", main: " << this->MainInputBufferCount() << "/" << this->MainOutputBufferCount() + << ", aux: " << this->AuxInputBufferCount() << "/" << this->AuxOutputBufferCount() + ); return result; } void PreparePlaybackFunctions(snd_pcm_format_t playbackFormat) @@ -1321,19 +1339,18 @@ namespace pipedal rawPlaybackBuffer.resize(playbackFrameSize * bufferSize); memset(rawPlaybackBuffer.data(), 0, playbackFrameSize * bufferSize); - AllocateBuffers(playbackBuffers, playbackChannels); + AllocateBuffers(devicePlaybackBuffers, playbackChannels); } - void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + void OpenAudio(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) { std::lock_guard lock{restartMutex}; int err; - std::string inputName = jackServerSettings.GetAlsaInputDevice(); + std::string inputName = jackServerSettings.GetAlsaInputDevice(); std::string outputName = jackServerSettings.GetAlsaOutputDevice(); - this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers(); this->bufferSize = jackServerSettings.GetBufferSize(); this->user_threshold = jackServerSettings.GetBufferSize(); @@ -1376,7 +1393,7 @@ namespace pipedal { snd_pcm_nonblock(playbackHandle, 0); } - + this->alsa_device_name = inputName; err = snd_pcm_open(&captureHandle, inputName.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK); @@ -1467,6 +1484,10 @@ namespace pipedal memset(rawPlaybackBuffer.data(), 0, rawPlaybackBuffer.size()); int retry = 0; + if (this->isDummyDriver) + { + return; // dummy driver is insatiable. + } while (true) { auto avail = snd_pcm_avail(this->playbackHandle); @@ -1486,7 +1507,7 @@ namespace pipedal { throw PiPedalStateException(SS("Audio playback failed. " << snd_strerror(err))); } - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } if (avail == 0) @@ -1631,7 +1652,7 @@ namespace pipedal auto frame_bytes = this->captureFrameSize; do { - TraceBufferPositions(framesRead,'1'); + TraceBufferPositions(framesRead, '1'); framesRead = snd_pcm_readi(handle, buffer, frames); if (framesRead < 0) @@ -1649,7 +1670,7 @@ namespace pipedal } } while (frames > 0); - TraceBufferPositions(framesRead,'2'); + TraceBufferPositions(framesRead, '2'); return framesRead; } @@ -1700,6 +1721,7 @@ namespace pipedal } private: + bool isDummyDriver = false; long WriteBuffer(snd_pcm_t *handle, uint8_t *buf, size_t frames) { long framesRead; @@ -1717,7 +1739,7 @@ namespace pipedal } return 0; } - void AudioThread() + PIPEDAL_NON_INLINE void AudioThread() { SetThreadName("alsaDriver"); @@ -1797,7 +1819,20 @@ namespace pipedal cpuUse.AddSample(ProfileCategory::Execute); + // Perform any neccessary mixing of outputs. + for (auto&mixOp: this->mixOps) + { + mixOp(framesRead); + } + + // final format conversion. (this->*copyOutputFn)(framesRead); + + if (this->driverHost) + { + driverHost->OnRealtimeUpdateDeviceVus(framesRead); + } + cpuUse.AddSample(ProfileCategory::Driver); // process. @@ -1810,6 +1845,10 @@ namespace pipedal recover_from_output_underrun(captureHandle, playbackHandle, err, framesRead); framesRead = 0; } + if (isDummyDriver) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 30)); + } cpuUse.AddSample(ProfileCategory::Write); } } @@ -1824,9 +1863,9 @@ namespace pipedal { this->driverHost->OnAlsaDriverStopped(); // zero out input buffers. - for (size_t i = 0; i < this->captureBuffers.size(); ++i) + for (size_t i = 0; i < this->deviceCaptureBuffers.size(); ++i) { - float *pBuffer = captureBuffers[i]; + float *pBuffer = deviceCaptureBuffers[i]; for (size_t j = 0; j < this->bufferSize; ++j) { pBuffer[j] = 0; @@ -1836,7 +1875,7 @@ namespace pipedal { while (!terminateAudio()) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); this->driverHost->OnProcess(this->bufferSize); } } @@ -1849,63 +1888,208 @@ namespace pipedal bool alsaActive = false; - static int IndexFromPortName(const std::string &s) + PIPEDAL_NON_INLINE void AllocateInputChannels( + const std::vector &channelSelection, + std::vector &channelBuffers) { - auto pos = s.find_last_of('_'); - if (pos == std::string::npos) + size_t nChannels = channelSelection.size(); + if (nChannels == 0) { - throw std::invalid_argument("Bad port name."); + channelBuffers.resize(0); + return; } - const char *p = s.c_str() + (pos + 1); - int v = atoi(p); - if (v < 0) + channelBuffers.resize(nChannels); + for (size_t i = 0; i < nChannels; ++i) { - throw std::invalid_argument("Bad port name."); + int64_t deviceChannel = channelSelection[i]; + if (deviceChannel == -1 || deviceChannel >= captureChannels) + { + channelBuffers[i] = zeroInputBuffer; + } + else + { + channelBuffers[i] = deviceCaptureBuffers[deviceChannel]; + } } - return v; + } + PIPEDAL_NON_INLINE void AllocateOutputChannels( + const std::vector &channelSelection, + std::vector &channelBuffers) + { + size_t nChannels = channelSelection.size(); + + if (nChannels == 0) + { + channelBuffers.resize(0); + return; + } + channelBuffers.resize(nChannels); + for (size_t i = 0; i < nChannels; ++i) + { + int64_t deviceChannel = channelSelection[i]; + if (deviceChannel == -1) + { + channelBuffers[i] = this->GetDiscardOutputBuffer(); + ; + } + else + { + float *mixBuffer = AllocateAudioBuffer(); + channelBuffers[i] = mixBuffer; + } + } + } + + + PIPEDAL_NON_INLINE void AllocateAuxChannels() + { + for (auto ix : channelSelection.auxInputChannels()) + { + auxCaptureBuffers.push_back(this->deviceCaptureBuffers[ix]); + } + for (auto ix : channelSelection.auxOutputChannels()) + { + auxPlaybackBuffers.push_back(this->devicePlaybackBuffers[ix]); + } + } + + using MixOp = std::function; + std::vector mixOps; + + PIPEDAL_NON_INLINE + void AddMixCopyOp(float*inputBuffer, float*outputBuffer) + { + mixOps.push_back([inputBuffer,outputBuffer](size_t nFrames) { + float*PIPEDAL_RESTRICT pIn = inputBuffer; + float*PIPEDAL_RESTRICT pOut = outputBuffer; + for (size_t i = 0; i < nFrames; ++i) + { + pOut[i] = pIn[i]; + } + }); + } + PIPEDAL_NON_INLINE + void AddMixAddOp(float*inputBuffer, float*outputBuffer) + { + mixOps.push_back([inputBuffer,outputBuffer](size_t nFrames) { + float*PIPEDAL_RESTRICT pIn = inputBuffer; + float*PIPEDAL_RESTRICT pOut = outputBuffer; + for (size_t i = 0; i < nFrames; ++i) + { + pOut[i] += pIn[i]; + } + }); + } + PIPEDAL_NON_INLINE + void AddMixCopyOp(float scale, float*inputBuffer, float*outputBuffer) + { + mixOps.push_back([scale,inputBuffer,outputBuffer](size_t nFrames) { + float*PIPEDAL_RESTRICT pIn = inputBuffer; + float*PIPEDAL_RESTRICT pOut = outputBuffer; + for (size_t i = 0; i < nFrames; ++i) + { + pOut[i] = scale*pIn[i]; + } + }); + } + PIPEDAL_NON_INLINE + void AddMixAddOp(float scale, float*inputBuffer, float*outputBuffer) + { + mixOps.push_back([scale,inputBuffer,outputBuffer](size_t nFrames) { + float*PIPEDAL_RESTRICT pIn = inputBuffer; + float*PIPEDAL_RESTRICT pOut = outputBuffer; + for (size_t i = 0; i < nFrames; ++i) + { + pOut[i] += scale*pIn[i]; + } + }); + } + PIPEDAL_NON_INLINE void AddMixOps() + { + std::set usedOutputChannels; + + for (size_t i = 0; i < this->channelSelection.mainOutputChannels().size(); ++i) { + size_t outputChannel = this->channelSelection.mainOutputChannels()[i]; + AddMixCopyOp(this->mainPlaybackBuffers[i],this->devicePlaybackBuffers[outputChannel]); + usedOutputChannels.insert(outputChannel); + } + if (channelSelection.auxInputChannels().size() <= channelSelection.auxOutputChannels().size()) + { + for (size_t i = 0; i < this->channelSelection.auxOutputChannels().size(); ++i) + { + size_t outputChannel = this->channelSelection.auxOutputChannels()[i]; + size_t inputChannel; + if (this->channelSelection.auxInputChannels().size() == 0) break; + if (i >= this->channelSelection.auxInputChannels().size()) { + inputChannel = 0; + } else { + inputChannel = this->channelSelection.auxInputChannels()[i]; + } + if (outputChannel >= this->devicePlaybackBuffers.size()) + { + continue; + } + if (usedOutputChannels.contains(outputChannel)) + { + AddMixAddOp(this->deviceCaptureBuffers[inputChannel],this->devicePlaybackBuffers[outputChannel]); + } else { + AddMixCopyOp(this->deviceCaptureBuffers[inputChannel],this->devicePlaybackBuffers[outputChannel]); + } + usedOutputChannels.insert(outputChannel); + } + } else if (channelSelection.auxInputChannels().size() >= 2 && channelSelection.auxOutputChannels().size() == 1) { + float scale = 1.0/channelSelection.auxInputChannels().size(); + for (size_t i = 0; i < this->channelSelection.auxOutputChannels().size(); ++i) + { + size_t outputChannel = this->channelSelection.auxOutputChannels()[i]; + if (usedOutputChannels.contains(outputChannel)) + { + AddMixAddOp(scale,this->auxCaptureBuffers[0],this->devicePlaybackBuffers[outputChannel]); + } else { + AddMixCopyOp(scale,this->auxCaptureBuffers[0],this->devicePlaybackBuffers[outputChannel]); + } + usedOutputChannels.insert(outputChannel); + } + } } bool activated = false; - virtual void Activate() + PIPEDAL_NON_INLINE virtual void Activate() { if (activated) { throw PiPedalStateException("Already activated."); } + activated = true; - this->activeCaptureBuffers.resize(channelSelection.GetInputAudioPorts().size()); + // Reset previously allocated buffers. + allocatedBuffers.resize(0); - int ix = 0; - for (auto &x : channelSelection.GetInputAudioPorts()) + // Allocate device capture buffers. + zeroInputBuffer = AllocateAudioBuffer(); + deviceCaptureBuffers.resize(captureChannels); + for (size_t i = 0; i < captureChannels; ++i) { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= captureBuffers.size()) - { - Lv2Log::error(SS("Invalid audio input port: " << x)); - } - else - { - this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex]; - } + deviceCaptureBuffers[i] = AllocateAudioBuffer(); + } + devicePlaybackBuffers.resize(playbackChannels); + for (size_t i = 0; i < playbackChannels; ++i) + { + devicePlaybackBuffers[i] = AllocateAudioBuffer(); } - this->activePlaybackBuffers.resize(channelSelection.GetOutputAudioPorts().size()); - ix = 0; - for (auto &x : channelSelection.GetOutputAudioPorts()) - { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= playbackBuffers.size()) - { - Lv2Log::error(SS("Invalid audio output port: " << x)); - } - else - { - this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex]; - } - } + AllocateInputChannels( + channelSelection.mainInputChannels(), + this->mainCaptureBuffers); + AllocateOutputChannels( + channelSelection.mainOutputChannels(), + this->mainPlaybackBuffers); + + AllocateAuxChannels(); + AddMixOps(); audioThread = std::make_unique([this]() { AudioThread(); }); @@ -1936,15 +2120,85 @@ namespace pipedal AlsaSequencer::ptr alsaSequencer; public: + virtual const ChannelSelection &GetChannelSelection() const override + { + return channelSelection; + } + virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override { this->alsaSequencer = alsaSequencer; } - virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); } - virtual float *GetInputBuffer(size_t channel) override + virtual size_t DeviceInputBufferCount() const override { - return activeCaptureBuffers[channel]; + return deviceCaptureBuffers.size(); + } + virtual size_t DeviceOutputBufferCount() const override + { + return devicePlaybackBuffers.size(); + } + + virtual float *GetDeviceInputBuffer(size_t channel) const override + { + if (channel >= deviceCaptureBuffers.size()) + return nullptr; + return deviceCaptureBuffers[channel]; + } + virtual float *GetDeviceOutputBuffer(size_t channel) const override + { + if (channel >= devicePlaybackBuffers.size()) + return nullptr; + return devicePlaybackBuffers[channel]; + } + + virtual float *GetZeroInputBuffer() + { + if (zeroInputBuffer == nullptr) + { + zeroInputBuffer = AllocateAudioBuffer(); + } + return zeroInputBuffer; + } + virtual float *GetDiscardOutputBuffer() + { + if (discardOutputBuffer == nullptr) + { + discardOutputBuffer = AllocateAudioBuffer(); + } + return discardOutputBuffer; + } + + virtual std::vector &DeviceInputBuffers() override { return this->deviceCaptureBuffers; } + virtual std::vector &DeviceOutputBuffers() override { return this->devicePlaybackBuffers; } + + virtual std::vector &MainInputBuffers() override { return this->mainCaptureBuffers; } + virtual std::vector &MainOutputBuffers() override { return this->mainPlaybackBuffers; } + + virtual std::vector &AuxInputBuffers() override { return this->auxCaptureBuffers; } + virtual std::vector &AuxOutputBuffers() override { return this->auxPlaybackBuffers; } + + virtual size_t MainInputBufferCount() const { return mainCaptureBuffers.size(); } + virtual float *GetMainInputBuffer(size_t channel) override + { + if (channel >= (int64_t)mainCaptureBuffers.size()) + { + throw std::runtime_error("Argument out of range."); + } + return mainCaptureBuffers[channel]; + } + virtual size_t AuxInputBufferCount() const { return auxCaptureBuffers.size(); } + virtual float *GetAuxInputBuffer(size_t channel) override + { + return auxCaptureBuffers[channel]; + } + virtual size_t AuxOutputBufferCount() const + { + return auxPlaybackBuffers.size(); + } + virtual float *GetAuxOutputBuffer(size_t channel) override + { + return auxPlaybackBuffers[channel]; } virtual size_t GetMidiInputEventCount() override @@ -1956,27 +2210,28 @@ namespace pipedal return this->midiEvents.data(); } - virtual size_t OutputBufferCount() const { return activePlaybackBuffers.size(); } - virtual float *GetOutputBuffer(size_t channel) override + virtual size_t MainOutputBufferCount() const { return mainPlaybackBuffers.size(); } + virtual float *GetMainOutputBuffer(size_t channel) override { - return activePlaybackBuffers[channel]; + return mainPlaybackBuffers[channel]; } - - void FreeBuffers(std::vector &buffer) + float *AllocateAudioBuffer() { - for (size_t i = 0; i < buffer.size(); ++i) - { - delete[] buffer[i]; - buffer[i] = 0; - } - buffer.clear(); + std::vector buffer; + buffer.resize(this->bufferSize); + float *pBuffer = buffer.data(); + allocatedBuffers.push_back(std::move(buffer)); + return pBuffer; } void DeleteBuffers() { - activeCaptureBuffers.clear(); - activePlaybackBuffers.clear(); - FreeBuffers(this->playbackBuffers); - FreeBuffers(this->captureBuffers); + mainCaptureBuffers.clear(); + mainPlaybackBuffers.clear(); + auxCaptureBuffers.clear(); + auxPlaybackBuffers.clear(); + zeroInputBuffer = nullptr; + discardOutputBuffer = nullptr; + allocatedBuffers.clear(); } virtual void Close() { @@ -2015,19 +2270,19 @@ namespace pipedal std::vector &inputAudioPorts, std::vector &outputAudioPorts) { - if (jackServerSettings.IsDummyAudioDevice()) - { - auto nChannels = GetDummyAudioChannels(jackServerSettings.GetAlsaInputDevice()); + // if (jackServerSettings.IsDummyAudioDevice()) + // { + // auto nChannels = GetDummyAudioChannels(jackServerSettings.GetAlsaInputDevice()); - inputAudioPorts.clear(); - outputAudioPorts.clear(); - for (uint32_t i = 0; i < nChannels; ++i) - { - inputAudioPorts.push_back(std::string(SS("system::capture_" << i))); - outputAudioPorts.push_back(std::string(SS("system::playback_" << i))); - } - return true; - } + // inputAudioPorts.clear(); + // outputAudioPorts.clear(); + // for (uint32_t i = 0; i < nChannels; ++i) + // { + // inputAudioPorts.push_back(std::string(SS("system::capture_" << i))); + // outputAudioPorts.push_back(std::string(SS("system::playback_" << i))); + // } + // return true; + // } snd_pcm_t *playbackHandle = nullptr; snd_pcm_t *captureHandle = nullptr; @@ -2241,7 +2496,7 @@ namespace pipedal { value += 1.0f * (c) / (128.0 * 256.0 * 256.0); } - this->playbackBuffers[c][i] = value; + this->devicePlaybackBuffers[c][i] = value; } } @@ -2257,14 +2512,14 @@ namespace pipedal for (size_t c = 0; c < captureChannels; ++c) { float error = - this->captureBuffers[c][i] - this->playbackBuffers[c][i]; + this->deviceCaptureBuffers[c][i] - this->devicePlaybackBuffers[c][i]; assert(std::abs(error) < 4e-5); } } } - void AlsaFormatEncodeDecodeTest(AudioDriverHost *testDriverHost) + void test::AlsaFormatEncodeDecodeTest(AudioDriverHost *testDriverHost) { static snd_pcm_format_t formats[] = { snd_pcm_format_t::SND_PCM_FORMAT_S16_LE, @@ -2286,7 +2541,7 @@ namespace pipedal alsaDriver->TestFormatEncodeDecode(format); } } - void MidiDecoderTest() + void test::MidiDecoderTest() { #ifdef JUNK AlsaDriverImpl::AlsaMidiDeviceImpl midiState; @@ -2355,37 +2610,41 @@ namespace pipedal { using namespace std; int savedPrecision = cout.precision(); - auto savedFlags = cout.flags(); + auto savedFlags = cout.flags(); size_t ix = bufferTraceIndex; if (ix < nEntries) { - ix = ix + bufferTraces.size()-nEntries; - } else { + ix = ix + bufferTraces.size() - nEntries; + } + else + { ix -= nEntries; } uint64_t t0; - if (bufferTraceIndex == 0) { - t0 = bufferTraces[bufferTraces.size()-1].time; - } else { - t0 = bufferTraces[bufferTraceIndex-1].time; + if (bufferTraceIndex == 0) + { + t0 = bufferTraces[bufferTraces.size() - 1].time; + } + else + { + t0 = bufferTraces[bufferTraceIndex - 1].time; } while (ix != bufferTraceIndex) { - auto&bufferTrace = bufferTraces[ix]; + auto &bufferTrace = bufferTraces[ix]; if (bufferTrace.time != 0) { int64_t dt = (int64_t)bufferTrace.time - (int64_t)t0; - - cout << bufferTrace.code << " " - << fixed << setprecision(3) << dt*0.001 - << " " << "inAvail: " << bufferTrace.inAvail - << " " << "outAvail: " << bufferTrace.outAvail - << " " << "buffered: " << bufferTrace.buffered - << " " << "total: " << bufferTrace.total - << endl; + cout << bufferTrace.code << " " + << fixed << setprecision(3) << dt * 0.001 + << " " << "inAvail: " << bufferTrace.inAvail + << " " << "outAvail: " << bufferTrace.outAvail + << " " << "buffered: " << bufferTrace.buffered + << " " << "total: " << bufferTrace.total + << endl; } ++ix; @@ -2399,4 +2658,51 @@ namespace pipedal cout.flags(savedFlags); } + AlsaDeviceInfo MakeDummyDeviceInfo(uint32_t channels) + { + AlsaDeviceInfo result; + constexpr int DUMMY_DEVICE_ID_OFFSET = 100974; + result.cardId_ = DUMMY_DEVICE_ID_OFFSET + channels; + result.id_ = SS("dummy:channels_" << channels); + result.name_ = SS("Dummy Device (" << channels << " channels)"); + result.longName_ = result.name_; + result.sampleRates_.push_back(44100); + result.sampleRates_.push_back(48000); + result.minBufferSize_ = 16; + result.maxBufferSize_ = 1024; + result.supportsCapture_ = true; + result.supportsPlayback_ = true; + return result; + } + + uint32_t GetDummyAudioChannels(const std::string &deviceName) + { + uint32_t channels; + int pos = deviceName.find_last_of('_'); + if (pos == std::string::npos) + { + throw std::runtime_error("Invalid dummy device name"); + } + std::istringstream ss(deviceName.substr(pos + 1)); + ss >> channels; + return channels; + } + AudioDriver *CreateDummyAudioDriver(AudioDriverHost *driverHost, const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) + { + auto dummyServerSettings = jackServerSettings; + dummyServerSettings.UseDummyAudioDevice(); + auto dummyChannelSelection = channelSelection; + AudioDriver *driver = new AlsaDriverImpl(driverHost); + try + { + driver->Open(jackServerSettings, channelSelection); + return driver; + } + catch (const std::exception &e) + { + delete driver; + throw; + } + } + } // namespace diff --git a/src/AlsaDriver.hpp b/src/AlsaDriver.hpp index 1b9ee0e..a6a2c15 100644 --- a/src/AlsaDriver.hpp +++ b/src/AlsaDriver.hpp @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2025 Robin E. R. Davies + * Copyright (c) 2026 Robin E. R. 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 @@ -24,6 +24,7 @@ #pragma once +#include #include "AudioDriver.hpp" #include "JackServerSettings.hpp" @@ -37,10 +38,17 @@ namespace pipedal { AudioDriver* CreateAlsaDriver(AudioDriverHost*driverHost); - // test only. - void AlsaFormatEncodeDecodeTest(AudioDriverHost*driverHost); - void MidiDecoderTest(); + namespace test { + // Private use, test only. + void AlsaFormatEncodeDecodeTest(AudioDriverHost*driverHost); + void MidiDecoderTest(); + } void FreeAlsaGlobals(); // for valgrind. Free the Alsa configuration cache. + + // AlsaDeviceInfo MakeDummyDeviceInfo(uint32_t channels); + uint32_t GetDummyAudioChannels(const std::string &deviceName); + // std::shared_ptr CreateDummyAudioDriver(AudioDriverHost*driverHost,const std::string&deviceId); + } diff --git a/src/AlsaDriverTest.cpp b/src/AlsaDriverTest.cpp index 2508570..1550f48 100644 --- a/src/AlsaDriverTest.cpp +++ b/src/AlsaDriverTest.cpp @@ -1,18 +1,18 @@ /* * MIT License - * + * * Copyright (c) 2022 Robin E. R. 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 @@ -27,28 +27,34 @@ #include #include #include +#include #include "AlsaDriver.hpp" -#include "JackDriver.hpp" +#include "ChannelRouterSettings.hpp" using namespace pipedal; using namespace std; - -class AlsaTester: private AudioDriverHost { +class AlsaTester : private AudioDriverHost +{ public: - enum class TestType { Oscillator, LatencyMonitor, NullTest}; + enum class TestType + { + Oscillator, + LatencyMonitor, + NullTest + }; + private: AudioDriver *audioDriver = nullptr; TestType testType; public: AlsaTester(TestType testType) - : testType(testType) + : testType(testType) { // audioDriver = CreateAlsaDriver(this); // audioDriver = CreateJackDriver(this); - } ~AlsaTester() { @@ -57,47 +63,49 @@ public: delete[] outputBuffers; } - bool useJack = false; + bool OnRealtimeUpdateDeviceVus(size_t nFrames) override { return false; } + + void initializeChannelSelection(JackConfiguration &jackConfiguration, ChannelSelection &channelSelection) + { + size_t nInputs = std::max(jackConfiguration.inputAudioPorts().size(),size_t(2)); + size_t nOutputs = std::max(jackConfiguration.outputAudioPorts().size(),size_t(2)); + auto &inputSelections = channelSelection.mainInputChannels(); + auto &outputSelections = channelSelection.mainOutputChannels(); + inputSelections.resize(nInputs); + for (size_t i = 0; i < nInputs; ++i) + { + inputSelections[i] = i; + } + outputSelections.resize(nOutputs); + for (size_t i = 0; i < nOutputs; ++i) + { + outputSelections[i] = i; + } + + } void Test() { - AlsaFormatEncodeDecodeTest(this); + test::AlsaFormatEncodeDecodeTest(this); - JackServerSettings serverSettings("hw:M2","hw:M2",48000,32,3); + JackServerSettings serverSettings("hw:M2","hw:M2", 48000, 32, 3); JackConfiguration jackConfiguration; - if (useJack) - { - jackConfiguration.JackInitialize(); - } else { - jackConfiguration.AlsaInitialize(serverSettings); - } + jackConfiguration.AlsaInitialize(serverSettings); - JackChannelSelection channelSelection( - jackConfiguration.inputAudioPorts(), - jackConfiguration.outputAudioPorts(), - jackConfiguration.inputMidiDevices()); + ChannelSelection channelSelection; -#if JACK_HOST - if (useJack) - { - audioDriver = CreateJackDriver(this); - - } else { - audioDriver = CreateAlsaDriver(this); - } -#else audioDriver = CreateAlsaDriver(this); -#endif + initializeChannelSelection(jackConfiguration, channelSelection); - oscillator.Init(440,jackConfiguration.sampleRate()); + oscillator.Init(440, jackConfiguration.sampleRate()); latencyMonitor.Init(jackConfiguration.sampleRate()); - audioDriver->Open(serverSettings,channelSelection); + audioDriver->Open(serverSettings, channelSelection); - inputBuffers = new float*[channelSelection.GetInputAudioPorts().size()]; - outputBuffers = new float*[channelSelection.GetOutputAudioPorts().size()]; + inputBuffers = new float *[channelSelection.mainInputChannels().size()]; + outputBuffers = new float *[channelSelection.mainOutputChannels().size()]; audioDriver->Activate(); @@ -107,7 +115,7 @@ public: if (testType == TestType::LatencyMonitor) { auto latency = this->latencyMonitor.GetLatency(); - double ms = 1000.0*latency/jackConfiguration.sampleRate(); + double ms = 1000.0 * latency / jackConfiguration.sampleRate(); cout << "Latency: " << latency << " samples " << ms << "ms" << " xruns: " << GetXruns() << " Cpu: " << audioDriver->CpuUse() << "%" << endl; } @@ -115,37 +123,40 @@ public: audioDriver->Deactivate(); audioDriver->Close(); - } - float**inputBuffers = nullptr; - float**outputBuffers = nullptr; + float **inputBuffers = nullptr; + float **outputBuffers = nullptr; - class Oscillator { + class Oscillator + { private: double dx = 0; double x = 0; double dx2 = 0; double x2 = 0; - public: + public: void Init(float frequency, size_t sampleRate) { - dx = frequency*3.141592736*2/sampleRate; - dx2 = 0.5*3.141592736*2/sampleRate; + dx = frequency * 3.141592736 * 2 / sampleRate; + dx2 = 0.5 * 3.141592736 * 2 / sampleRate; } - float Next() { + float Next() + { float result = (float)std::cos(x); float env = (float)std::cos(x2); x += dx; x2 += dx2; - return result*env; + return result * env; } }; - class LatencyMonitor { - enum class State { + class LatencyMonitor + { + enum class State + { Idle, Waiting, }; @@ -156,91 +167,99 @@ public: size_t current_latency = 0; size_t latency = 0; std::mutex sync; + public: void Init(uint64_t sampleRate) { - idle_samples = (uint64_t)sampleRate*2; - waiting_samples = (uint64_t)sampleRate*2; + idle_samples = (uint64_t)sampleRate * 2; + waiting_samples = (uint64_t)sampleRate * 2; state = State::Idle; t = idle_samples; latency = 0; } - size_t GetLatency() { - std::lock_guard lock { sync}; + size_t GetLatency() + { + std::lock_guard lock{sync}; return latency; } float Next(float input) { - switch(state) + switch (state) { - default: - case State::Idle: + default: + case State::Idle: + { + if (t-- == 0) { - if (t-- == 0) { - state = State::Waiting; - current_latency = 0; - } - return 0.01; + state = State::Waiting; + current_latency = 0; } - break; - case State::Waiting: + return 0.01; + } + break; + case State::Waiting: + { + if (std::abs(input) > 0.1 || current_latency > 500) { - if (std::abs(input) > 0.1 || current_latency > 500) { - { - std::lock_guard lock { sync}; - latency = current_latency; - } - state = State::Idle; - t = idle_samples; - } else { - ++current_latency; + { + std::lock_guard lock{sync}; + latency = current_latency; } - return current_latency < 100 ? 0.25 : 0.0; + state = State::Idle; + t = idle_samples; } - break; + else + { + ++current_latency; + } + return current_latency < 100 ? 0.25 : 0.0; + } + break; } } }; - Oscillator oscillator; LatencyMonitor latencyMonitor; - virtual void OnAudioTerminated() { - + virtual void OnAudioTerminated() + { } - virtual void OnAlsaDriverStopped() { + virtual void OnAlsaDriverStopped() + { } - virtual void OnProcess(size_t nFrames) { - if (testType == TestType::NullTest) return; + virtual void OnProcess(size_t nFrames) + { + if (testType == TestType::NullTest) + return; - - size_t inputs = audioDriver->InputBufferCount(); - size_t outputs = audioDriver->OutputBufferCount(); + size_t inputs = audioDriver->MainInputBufferCount(); + size_t outputs = audioDriver->MainOutputBufferCount(); for (size_t i = 0; i < inputs; ++i) { - inputBuffers[i] = audioDriver->GetInputBuffer(i); + inputBuffers[i] = audioDriver->GetMainInputBuffer(i); } for (size_t i = 0; i < outputs; ++i) { - outputBuffers[i] = audioDriver->GetOutputBuffer(i); + outputBuffers[i] = audioDriver->GetMainOutputBuffer(i); } if (this->testType == TestType::Oscillator) { for (size_t i = 0; i < nFrames; ++i) { - float v = oscillator.Next()*0.25f; + float v = oscillator.Next() * 0.25f; for (size_t c = 0; c < outputs; ++c) { outputBuffers[c][i] = v; } } } - else { + else + { for (size_t i = 0; i < nFrames; ++i) { float v = latencyMonitor.Next(inputBuffers[0][i]); @@ -249,36 +268,34 @@ public: outputBuffers[c][i] = v; } } - } - } std::mutex sync; uint64_t xruns; - uint64_t GetXruns() { - lock_guard lock { sync}; + uint64_t GetXruns() + { + lock_guard lock{sync}; return xruns; } - virtual void OnUnderrun() { - lock_guard lock { sync }; + virtual void OnUnderrun() + { + lock_guard lock{sync}; ++xruns; - } }; +TEST_CASE("alsa_test", "[alsa_test]") +{ -TEST_CASE( "alsa_test", "[alsa_test]" ) { - AlsaTester alsaDriver(AlsaTester::TestType::Oscillator); alsaDriver.Test(); } - -TEST_CASE( "alsa_midi_test", "[alsa_midi_test]" ) { +TEST_CASE("alsa_midi_test", "[alsa_midi_test]") +{ AlsaTester alsaDriver(AlsaTester::TestType::Oscillator); - MidiDecoderTest(); + test::MidiDecoderTest(); } - diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp index 3899b05..f7a7f67 100644 --- a/src/AudioDriver.hpp +++ b/src/AudioDriver.hpp @@ -27,6 +27,7 @@ #include #include "AlsaSequencer.hpp" #include "MidiEvent.hpp" +#include "DeviceVus.hpp" @@ -35,15 +36,16 @@ namespace pipedal { using ProcessCallback = std::function; + class ChannelSelection; class AudioDriverHost { public: virtual void OnProcess(size_t nFrames) = 0; + virtual bool OnRealtimeUpdateDeviceVus(size_t nFrames) = 0; + virtual void OnUnderrun() = 0; virtual void OnAlsaDriverStopped() = 0; virtual void OnAudioTerminated() = 0; - - }; class AudioDriver { public: @@ -58,14 +60,39 @@ namespace pipedal { virtual size_t GetMidiInputEventCount() = 0; virtual MidiEvent*GetMidiEvents() = 0; - virtual size_t InputBufferCount() const = 0; - virtual float*GetInputBuffer(size_t channel) = 0; + virtual const ChannelSelection &GetChannelSelection() const = 0; - virtual size_t OutputBufferCount() const = 0; - virtual float*GetOutputBuffer(size_t channe) = 0; + virtual std::vector &DeviceInputBuffers() = 0; + virtual size_t DeviceInputBufferCount() const = 0; + virtual float* GetDeviceInputBuffer(size_t channel) const = 0; - virtual void Open(const JackServerSettings & jackServerSettings,const JackChannelSelection &channelSelection) = 0; + virtual std::vector &DeviceOutputBuffers() = 0; + virtual size_t DeviceOutputBufferCount() const = 0; + virtual float* GetDeviceOutputBuffer(size_t channel) const = 0; + + virtual std::vector &MainInputBuffers() = 0; + virtual size_t MainInputBufferCount() const = 0; + virtual float*GetMainInputBuffer(size_t channel) = 0; + + virtual std::vector &MainOutputBuffers() = 0; + virtual size_t MainOutputBufferCount() const = 0; + virtual float*GetMainOutputBuffer(size_t channel) = 0; + + + virtual std::vector&AuxInputBuffers() = 0; + virtual std::vector&AuxOutputBuffers() = 0; + virtual size_t AuxInputBufferCount() const = 0; + + virtual float*GetAuxInputBuffer(size_t channel) = 0; + virtual size_t AuxOutputBufferCount() const = 0; + virtual float*GetAuxOutputBuffer(size_t channel) = 0; + + + virtual float*GetZeroInputBuffer() = 0; + virtual float*GetDiscardOutputBuffer() = 0; + + virtual void Open(const JackServerSettings & jackServerSettings,const ChannelSelection &channelSelection) = 0; virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) = 0; virtual void Activate() = 0; virtual void Deactivate() = 0; diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index a744f84..ec9e34e 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -18,6 +18,7 @@ // 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 "PiPedalCommon.hpp" #include "AudioHost.hpp" #include "util.hpp" #include @@ -347,7 +348,8 @@ bool SystemMidiBinding::IsTriggered(const MidiEvent &event) if (currentBinding.switchControlType() == SwitchControlTypeT::TRIGGER_ON_RISING_EDGE) { result = value >= 0x64 && lastControlValue < 0x64; - } else if (currentBinding.switchControlType() == SwitchControlTypeT::TRIGGER_ON_ANY) + } + else if (currentBinding.switchControlType() == SwitchControlTypeT::TRIGGER_ON_ANY) { result = true; } @@ -519,12 +521,15 @@ private: SystemMidiBinding rebootMidiBinding; SystemMidiBinding shutdownMidiBinding; - JackChannelSelection channelSelection; + ChannelSelection channelSelection; std::atomic active = false; std::atomic audioStopped = false; std::atomic isDummyAudioDriver = false; std::shared_ptr currentPedalboard; + std::shared_ptr currentMainInsertPedalboard; + std::shared_ptr currentAuxInsertPedalboard; + std::vector> activePedalboards; // pedalboards that have been sent to the audio queue. Lv2Pedalboard *realtimeActivePedalboard = nullptr; @@ -537,7 +542,7 @@ private: std::string GetAtomObjectType(uint8_t *pData) { - LV2_Atom_Object *pAtom = (LV2_Atom_Object *)pData; + LV2_Atom_Object *pAtom = (LV2_Atom_Object *)pData; if (pAtom->atom.type != uris.atom_Object) { throw std::invalid_argument("Not an Lv2 Object"); @@ -620,9 +625,9 @@ private: void ZeroOutputBuffers(size_t nframes) { - for (size_t i = 0; i < audioDriver->OutputBufferCount(); ++i) + for (size_t i = 0; i < audioDriver->MainOutputBufferCount(); ++i) { - float * out = (float *)audioDriver->GetOutputBuffer(i); + float *out = (float *)audioDriver->GetMainOutputBuffer(i); if (out) { ZeroBuffer(out, nframes); @@ -631,7 +636,7 @@ private: } RealtimeVuBuffers *realtimeVuBuffers = nullptr; size_t vuSamplesPerUpdate = 0; - int64_t vuSamplesRemaining = 0; + int64_t realtimeVuSamplesRemaining = 0; void freeRealtimeVuConfiguration() { @@ -657,11 +662,15 @@ private: virtual void SetSystemMidiBindings(const std::vector &bindings); - void writeVu() + void realtimeWriteVus() { // throttling: we send one; but won't send another until the host thread // acknowledges receipt. + if (!realtimeVuBuffers) + { + return; + } if (!realtimeVuBuffers->waitingForAcknowledge) { auto pResult = realtimeVuBuffers->GetResult(currentSample); @@ -671,7 +680,9 @@ private: } } - void processMonitorPortSubscriptions(uint32_t nframes) + void processMonitorPortSubscriptions( + Lv2Pedalboard *pedalboard, + uint32_t nframes) { for (size_t i = 0; i < this->realtimeMonitorPortSubscriptions->subscriptions.size(); ++i) { @@ -683,7 +694,7 @@ private: portSubscription.samplesToNextCallback += portSubscription.sampleRate; if (!portSubscription.waitingForAck) { - float value = realtimeActivePedalboard->GetControlOutputValue( + float value = pedalboard->GetControlOutputValue( portSubscription.instanceIndex, portSubscription.portIndex); if (value != portSubscription.lastValue) @@ -845,7 +856,7 @@ private: { this->realtimeVuBuffers->waitingForAcknowledge = false; } - vuSamplesRemaining = vuSamplesPerUpdate; + realtimeVuSamplesRemaining = vuSamplesPerUpdate; break; } @@ -923,7 +934,7 @@ private: return (event.size == 3 && event.buffer[0] == 0xB0 && event.buffer[1] == 0x00); } - bool onMidiEvent(Lv2EventBufferWriter &eventBufferWriter, Lv2EventBufferWriter::LV2_EvBuf_Iterator &iterator, MidiEvent &event) + bool ProcessMidiMonitor(Lv2EventBufferWriter &eventBufferWriter, Lv2EventBufferWriter::LV2_EvBuf_Iterator &iterator, MidiEvent &event) { // eventBufferWriter.writeMidiEvent(iterator, 0, event.size, event.buffer); @@ -947,7 +958,7 @@ private: } if (isNote || isControl) { - MidiNotifyBody notifyBody (event.buffer[0],event.buffer[1], event.buffer[2]); + MidiNotifyBody notifyBody(event.buffer[0], event.buffer[1], event.buffer[2]); realtimeWriter.OnMidiListen(notifyBody); } } @@ -1060,7 +1071,7 @@ private: } else { - onMidiEvent(eventBufferWriter, iterator, event); + ProcessMidiMonitor(eventBufferWriter, iterator, event); } } @@ -1103,7 +1114,35 @@ private: } } } - void ProcessMidiInput() + bool GetMainDriverBuffers( + float **inputBuffers, + float **outputBuffers) + { + bool buffersValid = true; + for (int i = 0; i < audioDriver->MainInputBufferCount(); ++i) + { + float *input = (float *)audioDriver->GetMainInputBuffer(i); + if (input == nullptr) + { + buffersValid = false; + } + inputBuffers[i] = input; + } + inputBuffers[audioDriver->MainInputBufferCount()] = nullptr; + + for (int i = 0; i < audioDriver->MainOutputBufferCount(); ++i) + { + float *output = audioDriver->GetMainOutputBuffer(i); + if (output == nullptr) + { + buffersValid = false; + } + outputBuffers[i] = output; + } + outputBuffers[audioDriver->MainOutputBufferCount()] = nullptr; + return buffersValid; + } + void ProcessGlobalMidiInput() { Lv2EventBufferWriter eventBufferWriter(this->eventBufferUrids); Lv2EventBufferWriter::LV2_EvBuf_Iterator iterator = eventBufferWriter.begin(); @@ -1142,11 +1181,167 @@ private: Lv2Log::info("Audio thread terminated."); } + PIPEDAL_NON_INLINE void ProcessLv2Pedalboard(size_t nframes) + { + Lv2Pedalboard *pedalboard = nullptr; + + std::vector *pInputBuffers; + std::vector *pOutputBuffers; + pedalboard = this->realtimeActivePedalboard; + pInputBuffers = &(audioDriver->MainInputBuffers()); + + pOutputBuffers = &(audioDriver->MainOutputBuffers()); + + if (pedalboard == nullptr || pInputBuffers->size() == 0 || pOutputBuffers->size() == 0) + { + return; + } + float *inputBuffers[3]; + float *outputBuffers[3]; + bool buffersValid = true; + inputBuffers[0] = pInputBuffers->at(0); + inputBuffers[1] = pInputBuffers->size() >= 2 ? pInputBuffers->at(1) : nullptr; + inputBuffers[2] = nullptr; + + outputBuffers[0] = pOutputBuffers->at(0); + outputBuffers[1] = pOutputBuffers->size() >= 2 ? pOutputBuffers->at(1) : nullptr; + outputBuffers[2] = nullptr; + + if (pedalboard != nullptr) + { + pedalboard->ProcessParameterRequests(pParameterRequests, nframes); + + pedalboard->Run(inputBuffers, outputBuffers, (uint32_t)nframes, &realtimeWriter); + pedalboard->GatherPatchProperties(pParameterRequests); + pedalboard->GatherPathPatchProperties(this); + + if (this->realtimeMonitorPortSubscriptions != nullptr) + { + processMonitorPortSubscriptions(pedalboard, nframes); + } + } + else + { + // zero output buffers. + size_t ix = 0; + while (outputBuffers[ix]) + { + float *outputBuffer = outputBuffers[ix]; + for (size_t i = 0; i < nframes; ++i) + { + outputBuffer[i] = 0; + } + ++ix; + } + } + } + + void GetMasterChannels(float **masterInputBuffers, float **masterOutputBuffers) + { + size_t inputIx = 0; + size_t outputIx = 0; + + masterInputBuffers[0] = nullptr; + masterInputBuffers[1] = nullptr; + masterOutputBuffers[0] = nullptr; + masterOutputBuffers[1] = nullptr; + + for (auto nChannel : channelSelection.mainInputChannels()) + { + masterInputBuffers[inputIx++] = audioDriver->GetDeviceInputBuffer(nChannel); + } + for (auto nChannel : channelSelection.mainOutputChannels()) + { + masterOutputBuffers[outputIx++] = audioDriver->GetDeviceOutputBuffer(nChannel); + } + } + inline void AccumulateVu(float*result, size_t nFrames, float*buffer) + { + float maxVal = *result; + for (size_t i = 0; i < nFrames; ++i) + { + float v = std::fabs(buffer[i]); + if (v > maxVal) + { + maxVal = v; + } + } + *result = maxVal; + } + void AccumulateVuInputs(size_t nFrames,VuUpdateX&vuUpdate, const std::vector&channels) + { + if (channels.size() == 0) return; + + AccumulateVu(&vuUpdate.inputMaxValueL_,nFrames,this->audioDriver->DeviceInputBuffers()[channels[0]]); + + if (channels.size() == 2) { + AccumulateVu(&vuUpdate.inputMaxValueR_,nFrames,this->audioDriver->DeviceInputBuffers()[channels[1]]); + } + } + void AccumulateVuOutputs(size_t nFrames,VuUpdateX&vuUpdate, const std::vector&channels) + { + if (channels.size() == 0) return; + AccumulateVu(&vuUpdate.outputMaxValueL_,nFrames,this->audioDriver->DeviceOutputBuffers()[channels[0]]); + if (channels.size() >= 2) { + AccumulateVu(&vuUpdate.outputMaxValueR_,nFrames,this->audioDriver->DeviceOutputBuffers()[channels[1]]); + } + } + void ComputeMasterVus(size_t nFrames) { + if (this->realtimeVuBuffers) + { + float** audioBuffers; + + for (auto& vuUpdate: this->realtimeVuBuffers->vuUpdateWorkingData) + { + if (vuUpdate.instanceId_ < 0) + { + switch (vuUpdate.instanceId_) + { + case Pedalboard::START_CONTROL_ID: + AccumulateVuInputs(nFrames, vuUpdate, pHost->GetChannelSelection().mainInputChannels()); + break; + case Pedalboard::END_CONTROL_ID: + AccumulateVuOutputs(nFrames, vuUpdate, pHost->GetChannelSelection().mainOutputChannels()); + break; + case Pedalboard::AUX_START_CONTROL_ID: + AccumulateVuInputs(nFrames, vuUpdate, pHost->GetChannelSelection().auxInputChannels()); + break; + case Pedalboard::AUX_END_CONTROL_ID: + AccumulateVuOutputs(nFrames, vuUpdate, pHost->GetChannelSelection().auxOutputChannels()); + break; + } + } + } + } + } + bool OnRealtimeUpdateDeviceVus(size_t nFrames) override + { + // all lv2pedalboards processed, and all channels downmixed. + // Now we can do the real vu update work! + + float *masterInputBuffers[2]; + float *masterOutputBuffers[2]; + + if (this->realtimeActivePedalboard != nullptr) + { + realtimeActivePedalboard->ComputeVus(this->realtimeVuBuffers, nFrames); + } + ComputeMasterVus(nFrames); + // periodically send updates. + realtimeVuSamplesRemaining -= nFrames; + if (realtimeVuSamplesRemaining <= 0) + { + realtimeWriteVus(); + realtimeVuSamplesRemaining = vuSamplesPerUpdate; + } + + return true; + } virtual void OnProcess(size_t nframes) { try { - float * restrict in , * restrict out; + float *restrict in, *restrict out; Lv2Pedalboard *pedalboard = nullptr; pedalboard = this->realtimeActivePedalboard; @@ -1154,6 +1349,7 @@ private: { pedalboard->ResetAtomBuffers(); } + while (true) { @@ -1164,71 +1360,13 @@ private: // else a new pedalboard was installed. start again. pedalboard = this->realtimeActivePedalboard; } - bool processed = false; if (pedalboard != nullptr) { - ProcessMidiInput(); - float *inputBuffers[4]; - float *outputBuffers[4]; - bool buffersValid = true; - for (int i = 0; i < audioDriver->InputBufferCount(); ++i) - { - float *input = (float *)audioDriver->GetInputBuffer(i); - if (input == nullptr) - { - buffersValid = false; - break; - } - inputBuffers[i] = input; - } - inputBuffers[audioDriver->InputBufferCount()] = nullptr; - - for (int i = 0; i < audioDriver->OutputBufferCount(); ++i) - { - float *output = audioDriver->GetOutputBuffer(i); - if (output == nullptr) - { - buffersValid = false; - break; - } - outputBuffers[i] = output; - } - outputBuffers[audioDriver->OutputBufferCount()] = nullptr; - - if (buffersValid) - { - pedalboard->ProcessParameterRequests(pParameterRequests,nframes); - - processed = pedalboard->Run(inputBuffers, outputBuffers, (uint32_t)nframes, &realtimeWriter); - if (processed) - { - if (this->realtimeVuBuffers != nullptr) - { - pedalboard->ComputeVus(this->realtimeVuBuffers, (uint32_t)nframes, inputBuffers, outputBuffers); - - vuSamplesRemaining -= nframes; - if (vuSamplesRemaining <= 0) - { - writeVu(); - vuSamplesRemaining += vuSamplesPerUpdate; - } - } - if (this->realtimeMonitorPortSubscriptions != nullptr) - { - processMonitorPortSubscriptions(nframes); - } - } - pedalboard->GatherPatchProperties(pParameterRequests); - pedalboard->GatherPathPatchProperties(this); - } - } - - if (!processed) - { - ZeroOutputBuffers(nframes); + ProcessGlobalMidiInput(); } + ProcessLv2Pedalboard(nframes); if (pParameterRequests != nullptr) { @@ -1271,13 +1409,12 @@ public: this->alsaDeviceMonitor->StartMonitoring( [this]( - AlsaSequencerDeviceMonitor::MonitorAction action, + AlsaSequencerDeviceMonitor::MonitorAction action, int client, - const std::string &clientName) - { - HandleAlsaSequencerDevicesChanged(action, client, clientName); - } - ); + const std::string &clientName) + { + HandleAlsaSequencerDevicesChanged(action, client, clientName); + }); } virtual ~AudioHostImpl() { @@ -1311,10 +1448,10 @@ public: } else if (action == AlsaSequencerDeviceMonitor::MonitorAction::DeviceAdded) { - pNotifyCallbacks->OnAlsaSequencerDeviceAdded(client,clientName); + pNotifyCallbacks->OnAlsaSequencerDeviceAdded(client, clientName); } } - } + } void HandleAudioTerminatedAbnormally() { Lv2Log::error("Audio processing terminated unexpectedly."); @@ -1461,7 +1598,7 @@ public: } else if (command == RingBufferCommand::SendVuUpdate) { - const std::vector *updates = nullptr; + const std::vector *updates = nullptr; hostReader.read(&updates); if (this->pNotifyCallbacks) @@ -1516,7 +1653,7 @@ public: if (property != nullptr && value != nullptr && property->type == uris.atom_URID) { LV2_URID propertyUrid = ((LV2_Atom_URID *)property)->body; - if (this->pNotifyCallbacks) + if (this->pNotifyCallbacks) { this->pNotifyCallbacks->OnPatchSetReply(instanceId, propertyUrid, value); } @@ -1670,7 +1807,7 @@ public: return result; } - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection_) { std::lock_guard guard(mutex); @@ -1681,21 +1818,8 @@ public: } isOpen = true; - if (jackServerSettings.IsDummyAudioDevice()) - { - this->isDummyAudioDriver = true; - this->audioDriver = std::unique_ptr(CreateDummyAudioDriver(this, jackServerSettings.GetAlsaInputDevice())); - } - else - { - this->isDummyAudioDriver = false; - this->audioDriver = std::unique_ptr(CreateAlsaDriver(this)); - } - - if (channelSelection.GetInputAudioPorts().size() == 0 || channelSelection.GetOutputAudioPorts().size() == 0) - { - return; - } + this->isDummyAudioDriver = jackServerSettings.IsDummyAudioDevice(); + this->audioDriver = std::unique_ptr(CreateAlsaDriver(this)); this->currentSample = 0; this->underruns = 0; @@ -1707,7 +1831,7 @@ public: this->realtimeReader.Reset(); this->realtimeWriter.Reset(); - this->channelSelection = channelSelection; + this->channelSelection = channelSelection_; StartReaderThread(); @@ -1781,6 +1905,7 @@ public: } } + virtual void SetBypass(uint64_t instanceId, bool enabled) { std::lock_guard guard(mutex); @@ -1865,7 +1990,6 @@ public: virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override; - void OnNotifyPathPatchPropertyReceived( int64_t instanceId, const std::string &pathPatchPropertyUri, @@ -1896,6 +2020,35 @@ public: pendingSnapshots.clear(); } + PIPEDAL_NON_INLINE RealtimePedalboardItemIndex GetRealtimeItemIndex(int64_t instanceId) + { + int64_t index = -1; + if (this->currentPedalboard) + { + if (instanceId == Pedalboard::START_CONTROL_ID) + { + index = Pedalboard::START_CONTROL_ID; + } + else if (instanceId == Pedalboard::END_CONTROL_ID) + { + index = Pedalboard::END_CONTROL_ID; + } + else if (instanceId == Pedalboard::AUX_START_CONTROL_ID) + { + index = Pedalboard::AUX_START_CONTROL_ID; + } + else if (instanceId == Pedalboard::AUX_END_CONTROL_ID) + { + index = Pedalboard::AUX_END_CONTROL_ID; + } + else + { + index = this->currentPedalboard->GetIndexOfInstanceId(instanceId); + } + } + RealtimePedalboardItemIndex result{index}; + return result; + } virtual void SetVuSubscriptions(const std::vector &instanceIds) { std::lock_guard guard(mutex); @@ -1914,35 +2067,56 @@ public: for (size_t i = 0; i < instanceIds.size(); ++i) { int64_t instanceId = instanceIds[i]; - auto effect = this->currentPedalboard->GetEffect(instanceId); - if (effect) + std::shared_ptr& pedalboard = this->currentPedalboard; + int64_t effectIndex = -1; + if (pedalboard != nullptr) { - int index = this->currentPedalboard->GetIndexOfInstanceId(instanceIds[i]); + effectIndex = pedalboard->GetIndexOfInstanceId(instanceId); + } + + if (effectIndex != -1) + { + IEffect *effect = pedalboard->GetEffect(instanceId); + RealtimePedalboardItemIndex index = RealtimePedalboardItemIndex(effectIndex); vuConfig->enabledIndexes.push_back(index); - VuUpdate v; + VuUpdateX v; v.instanceId_ = instanceId; - // Display mono VUs if a stereo device is being fed identical L/R inputs. - v.isStereoInput_ = effect->GetNumberOfInputAudioBuffers() >= 2 - && effect->GetAudioInputBuffer(0) != effect->GetAudioInputBuffer(1); + v.isStereoInput_ = effect->GetNumberOfInputAudioBuffers() >= 2; v.isStereoOutput_ = effect->GetNumberOfOutputAudioBuffers() >= 2; vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v); } - else if (instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) + else if ( + instanceId == Pedalboard::START_CONTROL_ID || + instanceId == Pedalboard::END_CONTROL_ID || + instanceId == Pedalboard::AUX_START_CONTROL_ID || + instanceId == Pedalboard::AUX_END_CONTROL_ID + ) { - int index = (int)instanceId; - VuUpdate v; + auto index = GetRealtimeItemIndex(instanceId); + VuUpdateX v; vuConfig->enabledIndexes.push_back(index); + v.instanceId_ = instanceId; - if (instanceId == Pedalboard::INPUT_VOLUME_ID) + size_t nChannels = 0; + + switch (instanceId) { - v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfInputAudioChannels() > 1; - } - else - { - v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfOutputAudioChannels() > 1; + case Pedalboard::START_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().mainInputChannels().size(); + break; + case Pedalboard::END_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().mainOutputChannels().size(); + break; + case Pedalboard::AUX_START_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().auxInputChannels().size(); + break; + case Pedalboard::AUX_END_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().auxOutputChannels().size(); + break; } + v.isStereoInput_ = v.isStereoOutput_ = nChannels > 1; vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v); } @@ -1953,6 +2127,10 @@ public: } } + Lv2Pedalboard *GetPedalboard() + { + return this->currentPedalboard.get(); + } RealtimeMonitorPortSubscription MakeRealtimeSubscription(const MonitorPortSubscription &subscription) { RealtimeMonitorPortSubscription result; @@ -2318,8 +2496,6 @@ void AudioHostImpl::SetAlsaSequencerConfiguration(const AlsaSequencerConfigurati this->alsaSequencer->SetConfiguration(alsaSequencerConfiguration); } - - JSON_MAP_BEGIN(JackHostStatus) JSON_MAP_REFERENCE(JackHostStatus, active) JSON_MAP_REFERENCE(JackHostStatus, errorMessage) diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index f582547..57e4e40 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -32,6 +32,7 @@ #include "Promise.hpp" #include "json_variant.hpp" #include "RealtimeMidiEventType.hpp" +#include "ChannelRouterSettings.hpp" namespace pipedal { @@ -157,8 +158,8 @@ namespace pipedal { public: virtual void OnNotifyLv2StateChanged(uint64_t instanceId) = 0; - virtual void OnNotifyMaybeLv2StateChanged(uint64_t instanceId) = 0; - virtual void OnNotifyVusSubscription(const std::vector &updates) = 0; + virtual bool OnNotifyMaybeLv2StateChanged(uint64_t instanceId) = 0; + virtual void OnNotifyVusSubscription(const std::vector &updates) = 0; virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) = 0; virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) = 0; virtual void OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) = 0; @@ -227,7 +228,7 @@ namespace pipedal virtual std::string AtomToJson(const LV2_Atom *atom) = 0; - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) = 0; + virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) = 0; virtual void Close() = 0; virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; @@ -237,6 +238,8 @@ namespace pipedal virtual void SetPedalboard(const std::shared_ptr &pedalboard) = 0; + + virtual void SetControlValue(uint64_t instanceId, const std::string &symbol, float value) = 0; virtual void SetInputVolume(float value) = 0; virtual void SetOutputVolume(float value) = 0; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 97a2ffb..f9f02d6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,13 @@ set(VERBOSE true) cmake_minimum_required(VERSION 3.16.0) +configure_file(config.hpp.in config.hpp) +file(GENERATE + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/T3kConfig.h" + CONTENT "#define PIPEDAL_T3K_PUBLISHABLE_KEY \"${PIPEDAL_T3K_PUBLISHABLE_KEY}\"\n") + + + set (CMAKE_INSTALL_PREFIX "/usr/") set (USE_PCH 1) @@ -10,19 +17,13 @@ set (ENABLE_BACKTRACE 0) set (USE_SANITIZE OFF) # seems to be broken on Ubuntu 24.10 + set(CXX_STANDARD 20) find_package(PkgConfig REQUIRED) pkg_check_modules(PipeWire REQUIRED libpipewire-0.3) - -message(STATUS "PipeWire_INCLUDE_DIRS: ${PipeWire_INCLUDE_DIRS}" - "PipeWire_LIBRARIES: ${PipeWire_LIBRARIES}" - "PipeWire_CFLAGS: ${PipeWire_CFLAGS}" - "PipeWire_DEFINES: ${PipeWire_DEFINES}" - "PipeWire_VERSION: ${PipeWire_VERSION}") - include(FetchContent) # FOR FTXUI @@ -58,6 +59,8 @@ include(FindPkgConfig) +find_package(OpenSSL REQUIRED) + find_package(ICU REQUIRED COMPONENTS uc i18n) message(STATUS "ICU_LIBRARIES: ${ICU_LIBRARIES}") @@ -204,6 +207,16 @@ else() endif() set (PIPEDAL_SOURCES + Tone3000Tone.hpp Tone3000Tone.cpp + AesDigest.cpp AesDigest.hpp + ChannelRouterSettings.cpp ChannelRouterSettings.hpp + Curl.cpp Curl.hpp + Tone3000DownloadType.cpp Tone3000DownloadType.hpp + Tone3000DownloadProgress.cpp Tone3000DownloadProgress.hpp + Tone3000Download.cpp Tone3000Download.hpp + Tone3000Downloader.cpp Tone3000Downloader.hpp + Tone3000Throttler.cpp Tone3000Throttler.hpp + CrashGuard.cpp CrashGuard.hpp ModTemplateGenerator.cpp ModTemplateGenerator.hpp WebServerMod.cpp WebServerMod.hpp @@ -265,6 +278,7 @@ set (PIPEDAL_SOURCES GovernorSettings.cpp GovernorSettings.hpp WebServer.cpp WebServer.hpp pch.h Uri.cpp Uri.hpp + RequestHandler.hpp Scratch.cpp PluginHost.hpp PluginHost.cpp PluginType.hpp PluginType.cpp @@ -315,7 +329,7 @@ set (PIPEDAL_SOURCES ) -configure_file(config.hpp.in config.hpp) + include_directories( ${pipedald_SOURCE_DIR}/. ../build/src @@ -351,9 +365,11 @@ target_compile_definitions(libpipedald PUBLIC "_REENTRANT") target_include_directories(libpipedald PUBLIC ${PIPEDAL_INCLUDES}) target_link_libraries(libpipedald - PUBLIC + PUBLIC PiPedalCommon + PRIVATE SQLiteCpp + OpenSSL::Crypto ${PipeWire_LIBRARIES} ) @@ -731,6 +747,7 @@ add_executable(pipedalconfig ModFileTypes.cpp ModFileTypes.hpp MimeTypes.cpp MimeTypes.hpp PiPedalConfiguration.hpp PiPedalConfiguration.cpp + PiPedalAlsa.hpp PiPedalAlsa.cpp JackServerSettings.hpp JackServerSettings.cpp SystemConfigFile.hpp SystemConfigFile.cpp WifiChannelSelectors.cpp WifiChannelSelectors.hpp @@ -761,6 +778,7 @@ add_executable(pipedal_latency_test target_link_libraries(pipedal_latency_test PRIVATE pthread asound PiPedalCommon) +target_include_directories(pipedal_latency_test PRIVATE ${PIPEDAL_INCLUDES}) add_executable(pipedal_alsa_info alsaCheck.cpp alsaCheck.hpp @@ -807,6 +825,7 @@ add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp SetWifiConfig.cpp SetWifiConfig.hpp JackServerSettings.hpp JackServerSettings.cpp + PiPedalAlsa.hpp PiPedalAlsa.cpp Lv2SystemdLogger.hpp Lv2SystemdLogger.cpp SystemConfigFile.hpp SystemConfigFile.cpp @@ -814,7 +833,7 @@ add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp asan_options.cpp ) -target_link_libraries(pipedaladmind PRIVATE PiPedalCommon pthread atomic stdc++fs systemd ) +target_link_libraries(pipedaladmind PRIVATE PiPedalCommon pthread atomic stdc++fs systemd asound) add_executable(processcopyrights copyrightMain.cpp CommandLineParser.hpp @@ -857,11 +876,17 @@ add_custom_target ( DEPENDS ${REACT_NOTICES_FILE} ) +if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.31) + cmake_policy(SET CMP0177 NEW) +endif() install (TARGETS pipedalconfig pipedal_kconfig pipedal_latency_test DESTINATION ${CMAKE_INSTALL_PREFIX}/bin - EXPORT pipedalTargets) + EXPORT pipedalTargets + ) install (TARGETS pipedald pipedaladmind pipedal_update DESTINATION ${CMAKE_INSTALL_PREFIX}/sbin - EXPORT pipedalSbinTargets) + EXPORT pipedalSbinTargets + ) + diff --git a/src/ChannelRouterSettings.cpp b/src/ChannelRouterSettings.cpp new file mode 100644 index 0000000..5a9dd79 --- /dev/null +++ b/src/ChannelRouterSettings.cpp @@ -0,0 +1,170 @@ +// Copyright (c) 2026 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 "ChannelRouterSettings.hpp" +#include + +using namespace pipedal; + +static uint64_t countChannels(const std::vector &channels) +{ + if (channels.size() != 2) + { + throw std::runtime_error("Invalid Channel Router channel configuration."); + } + if (channels[0] == -1) + { + if (channels[1] == -1) + { + return 0; + } + else + { + return 1; + } + } + else + { + if (channels[1] == -1) + { + return 1; + } + if (channels[0] == channels[1]) + { + return 1; + } + return 2; + } +} + +uint64_t ChannelRouterSettings::numberOfAudioInputChannels() const +{ + if (!configured_) + { + return 2; + } + return countChannels(mainInputChannels_); +} +uint64_t ChannelRouterSettings::numberOfAudioOutputChannels() const +{ + if (!configured_) + { + return 2; + } + return countChannels(mainOutputChannels_); +} + +ChannelSelection::ChannelSelection(ChannelRouterSettings&settings) +: mainInputChannels_(settings.mainInputChannels()) +, mainOutputChannels_(settings.mainOutputChannels()) +, auxInputChannels_(settings.auxInputChannels()) +, auxOutputChannels_(settings.auxOutputChannels()) +{ + normalizeChannelSelection(); +} + +static void normalizeInputChannels(std::vector&channels) { + if (channels.size() == 2) { + if (channels[0] == -1 && channels[1] == -1) + { + channels.resize(0); + } else if (channels[0] == channels[1]) { + channels.resize(1); + } else if (channels[1] == -1) { + channels.resize(1);} { + } + } +} + +static void normalizeOutputChannels(std::vector&channels) { + normalizeInputChannels(channels); +} + +void ChannelSelection::normalizeChannelSelection() { + normalizeInputChannels(mainInputChannels_); + normalizeOutputChannels(mainOutputChannels_); + normalizeInputChannels(auxInputChannels_); + normalizeOutputChannels(auxOutputChannels_); + + // If either aux inputs or outputs are zero, don't do ANY aux processing. + if (auxInputChannels_.size() == 0) + { + auxOutputChannels_.resize(0); + } else if (auxOutputChannels_.size() == 0) { + auxInputChannels_.resize(0); + } + // If either main inputs or outputs are empty, add send/receive dummy buffers. (Lv2 plugins shouldn't have to deal with this) + if (mainInputChannels_.size() == 0) + { + mainInputChannels_.resize(1); + mainInputChannels_[0] = -1; + } + if (mainOutputChannels_.size() == 0) { + mainInputChannels_.resize(1); + mainOutputChannels_[0] = -1; + } + // Send buffers are what they are. Let the send plugin deal with it. +} + + +ChannelRouterSettings::ChannelRouterSettings() +{ + +} + +std::vector ChannelRouterPresetBank::getIndexEntries() +{ + std::vector result; + for (auto &entry: entries_) + { + result.push_back(ChannelRouterPresetIndexEntry{entry->id(), entry->name()}); + } + return result; +} + + +JSON_MAP_BEGIN(ChannelRouterSettings) +JSON_MAP_REFERENCE(ChannelRouterSettings, configured) +JSON_MAP_REFERENCE(ChannelRouterSettings, changed) +JSON_MAP_REFERENCE(ChannelRouterSettings, channelRouterPresetId) +JSON_MAP_REFERENCE(ChannelRouterSettings, mainInputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, mainOutputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, auxInputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, auxOutputChannels) +JSON_MAP_END(); + +JSON_MAP_BEGIN(ChannelRouterPresetIndexEntry) +JSON_MAP_REFERENCE(ChannelRouterPresetIndexEntry, id) +JSON_MAP_REFERENCE(ChannelRouterPresetIndexEntry, name) +JSON_MAP_END(); + + +JSON_MAP_BEGIN(ChannelRouterPresetBankEntry) +JSON_MAP_REFERENCE(ChannelRouterPresetBankEntry, id) +JSON_MAP_REFERENCE(ChannelRouterPresetBankEntry, name) +JSON_MAP_REFERENCE(ChannelRouterPresetBankEntry, channelRouterSettings) +JSON_MAP_END(); + +JSON_MAP_BEGIN(ChannelRouterPresetBank) +JSON_MAP_REFERENCE(ChannelRouterPresetBank, nextId) +JSON_MAP_REFERENCE(ChannelRouterPresetBank, version) +JSON_MAP_REFERENCE(ChannelRouterPresetBank, entries) +JSON_MAP_END(); + diff --git a/src/ChannelRouterSettings.hpp b/src/ChannelRouterSettings.hpp new file mode 100644 index 0000000..bfdd875 --- /dev/null +++ b/src/ChannelRouterSettings.hpp @@ -0,0 +1,148 @@ +// Copyright (c) 2026 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. + +#pragma once + +#include +#include "json.hpp" +#include "Pedalboard.hpp" +#include + +namespace pipedal +{ + + class ChannelRouterSettings + { + protected: + + bool configured_ = false; + int64_t channelRouterPresetId_ = -1; + bool changed_ = false; + + std::vector mainInputChannels_ = {1, 1}; + std::vector mainOutputChannels_ = {0, 1}; + + std::vector auxInputChannels_ = {-1, -1}; + std::vector auxOutputChannels_ = {-1, -1}; + + public: + + using self = ChannelRouterSettings; + using ptr = std::shared_ptr; + + ChannelRouterSettings(); + + uint64_t numberOfAudioInputChannels() const; + uint64_t numberOfAudioOutputChannels() const; + + JSON_GETTER_SETTER(configured) + JSON_GETTER_SETTER(channelRouterPresetId) + JSON_GETTER_SETTER(changed) + JSON_GETTER_SETTER_REF(mainInputChannels) + JSON_GETTER_SETTER_REF(mainOutputChannels) + JSON_GETTER_SETTER_REF(auxInputChannels) + JSON_GETTER_SETTER_REF(auxOutputChannels) + + DECLARE_JSON_MAP(ChannelRouterSettings); + }; + + // just the channel selecttions. + class ChannelSelection + { + public: + ChannelSelection() = default; + ChannelSelection(ChannelRouterSettings &settings); + ChannelSelection(const ChannelSelection &other) = default; + ChannelSelection(ChannelSelection &&other) = default; + ChannelSelection &operator=(const ChannelSelection &other) = default; + ChannelSelection &operator=(ChannelSelection &&other) = default; + ~ChannelSelection() = default; + + const std::vector &mainInputChannels() const { return mainInputChannels_; } + const std::vector &mainOutputChannels() const { return mainOutputChannels_; } + const std::vector &auxInputChannels() const { return auxInputChannels_; } + const std::vector &auxOutputChannels() const { return auxOutputChannels_; } + + std::vector &mainInputChannels() { return mainInputChannels_; } + std::vector &mainOutputChannels() { return mainOutputChannels_; } + std::vector &auxInputChannels() { return auxInputChannels_; } + std::vector &auxOutputChannels() { return auxOutputChannels_; } + + private: + void normalizeChannelSelection(); + + std::vector mainInputChannels_; + std::vector mainOutputChannels_; + std::vector auxInputChannels_; + std::vector auxOutputChannels_; + }; + + class ChannelRouterPresetIndexEntry { + private: + int64_t id_ = -1; + std::string name_; + public: + ChannelRouterPresetIndexEntry() = default; + ChannelRouterPresetIndexEntry(const ChannelRouterPresetIndexEntry&other) = default; + ChannelRouterPresetIndexEntry(ChannelRouterPresetIndexEntry&&other) = default; + ChannelRouterPresetIndexEntry&operator=(const ChannelRouterPresetIndexEntry&other) = default; + ChannelRouterPresetIndexEntry&operator=(ChannelRouterPresetIndexEntry&&other) = default; + + ChannelRouterPresetIndexEntry(int64_t id, const std::string&name) + : id_(id),name_(name) { } + + + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(name) + + DECLARE_JSON_MAP(ChannelRouterPresetIndexEntry); + }; + + + class ChannelRouterPresetBankEntry { + private: + int64_t id_; + std::string name_; + ChannelRouterSettings channelRouterSettings_; + public: + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(name) + JSON_GETTER_SETTER_REF(channelRouterSettings) + + DECLARE_JSON_MAP(ChannelRouterPresetBankEntry); + + }; + + class ChannelRouterPresetBank { + private: + int64_t nextId_ = 1; + int64_t version_ = 1; + std::vector> entries_; + public: + + JSON_GETTER_SETTER(nextId) + JSON_GETTER_SETTER(version) + JSON_GETTER_SETTER_REF(entries) + + std::vector getIndexEntries(); + + DECLARE_JSON_MAP(ChannelRouterPresetBank); + }; + +} \ No newline at end of file diff --git a/src/Curl.cpp b/src/Curl.cpp new file mode 100644 index 0000000..0fe9349 --- /dev/null +++ b/src/Curl.cpp @@ -0,0 +1,927 @@ +/* + * Copyright (c) 2026 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 "Curl.hpp" +#include "ss.hpp" +#include "TemporaryFile.hpp" +#include +#include "SysExec.hpp" +#include +#include "util.hpp" +#include +#include +#include +#include +#include +#include +#include "Finally.hpp" +#include "Lv2Log.hpp" + +static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"}; + +static bool enableLogging = true; + +namespace pipedal +{ + + static void logHeaders(const std::vector &headers) + { + if (enableLogging) + { + std::ofstream os{"/tmp/PipedalCurl.log"}; + for (const auto &header : headers) + { + os << header << "\n"; + } + } + } + static std::string getCurlExitCodeString(int exitCode) + { + // Translate documented curl exit codes to strings. + // https://curl.se/libcurl/c/libcurl-errors.html + switch (exitCode) + { + case 0: + return "OK"; // CURLE_OK - All fine + case 1: + return "Unsupported protocol."; + case 2: + return "Failed to initialize."; + case 3: + return "URL malformed."; + case 4: + return "Feature not built-in."; + case 5: + return "Couldn't resolve proxy."; + case 6: + return "Couldn't resolve host."; + case 7: + return "Failed to connect to host."; + case 8: + return "Weird server reply."; + case 9: + return "Access denied to remote resource."; + case 10: + return "FTP accept failed."; + case 11: + return "FTP weird PASS reply."; + case 12: + return "FTP accept timeout."; + case 13: + return "FTP weird PASV reply."; + case 14: + return "FTP weird 227 format."; + case 15: + return "FTP can't get host."; + case 16: + return "HTTP/2 error."; + case 17: + return "FTP couldn't set type."; + case 18: + return "Partial file transfer."; + case 19: + return "FTP couldn't retrieve file."; + case 21: + return "FTP quote error."; + case 22: + return "HTTP page not retrieved."; + case 23: + return "Write error."; + case 25: + return "Upload failed."; + case 26: + return "Read error."; + case 27: + return "Out of memory."; + case 28: + return "Operation timeout."; + case 30: + return "FTP PORT failed."; + case 31: + return "FTP couldn't use REST."; + case 33: + return "Range error."; + case 35: + return "SSL connect error."; + case 36: + return "Bad download resume."; + case 37: + return "FILE couldn't read file."; + case 38: + return "LDAP cannot bind."; + case 39: + return "LDAP search failed."; + case 42: + return "Aborted by callback."; + case 43: + return "Bad function argument."; + case 45: + return "Interface failed."; + case 47: + return "Too many redirects."; + case 48: + return "Unknown option."; + case 49: + return "Setopt option syntax error."; + case 52: + return "Got nothing from server."; + case 53: + return "SSL engine not found."; + case 54: + return "SSL engine set failed."; + case 55: + return "Failed sending network data."; + case 56: + return "Failure receiving network data."; + case 58: + return "Problem with local client certificate."; + case 59: + return "Couldn't use specified SSL cipher."; + case 60: + return "Peer SSL certificate or SSH fingerprint verification failed."; + case 61: + return "Unrecognized transfer encoding."; + case 63: + return "Maximum file size exceeded."; + case 64: + return "Requested FTP SSL level failed."; + case 65: + return "Send failed to rewind."; + case 66: + return "SSL engine initialization failed."; + case 67: + return "Login denied."; + case 68: + return "TFTP file not found."; + case 69: + return "TFTP permission problem."; + case 70: + return "Remote disk full."; + case 71: + return "TFTP illegal operation."; + case 72: + return "TFTP unknown transfer ID."; + case 73: + return "Remote file already exists."; + case 74: + return "TFTP no such user."; + case 77: + return "Problem reading SSL CA cert."; + case 78: + return "Remote file not found."; + case 79: + return "SSH error."; + case 80: + return "SSL shutdown failed."; + case 82: + return "Failed to load CRL file."; + case 83: + return "SSL issuer check failed."; + case 84: + return "FTP PRET command failed."; + case 85: + return "RTSP CSeq mismatch."; + case 86: + return "RTSP session ID mismatch."; + case 87: + return "Unable to parse FTP file list."; + case 88: + return "Chunk callback error."; + case 89: + return "No connection available."; + case 90: + return "SSL pinned public key mismatch."; + case 91: + return "SSL invalid certificate status."; + case 92: + return "HTTP/2 stream error."; + case 93: + return "Recursive API call."; + case 94: + return "Authentication error."; + case 95: + return "HTTP/3 error."; + case 96: + return "QUIC connection error."; + case 97: + return "Proxy handshake error."; + case 98: + return "SSL client certificate required."; + case 99: + return "Unrecoverable poll error."; + case 100: + return "Value or data field too large."; + case 101: + return "ECH failed."; + default: + return SS("Failed with exit code " << exitCode << "."); + } + } + + static void throwCurlExitCode(int exitCode) + { + if (exitCode == 0) + return; + std::string message = getCurlExitCodeString(exitCode); + throw std::runtime_error(SS("Server download failed. " << message)); + } + + static int checkCurlHttpResponse(const std::vector &headers) + { + if (headers.size() == 0) + { + throw std::runtime_error("Download failed. Invalid curl response: no headers."); + } + + int returnCode = 0; + + for (const auto &httpResponse : headers) + { + if (httpResponse.starts_with("HTTP/")) + { + size_t codeStart = httpResponse.find(' '); + if (codeStart == std::string::npos) + { + throw std::runtime_error(SS("Download failed. Invalid curl response: " << httpResponse)); + } + ++codeStart; + + size_t codeEnd = httpResponse.find(' ', codeStart); + if (codeEnd == std::string::npos) + { + codeEnd = httpResponse.length(); + } + + int statusCode = 0; + try + { + statusCode = std::stoi(httpResponse.substr(codeStart, codeEnd - codeStart)); + if (statusCode >= 200 && statusCode < 300) // a completion code. + { + return statusCode; + } + if (statusCode >= 400) // an error + { + return statusCode; + } + // otherwise it's a redirect of some kind, keep going. + returnCode = statusCode; // return the last one if there's not 200 or 400+ result. + } + catch (const std::exception &) + { + // ignored. Weird. but ignored. + } + } + } + return returnCode; + } + + int CurlGet( + const std::string &url, + std::vector &output, + std::vector *outputHeadersOpt, + const std::vector *inputHeadersOpt) + { + TemporaryFile tempFile{WEB_TEMP_DIR}; + int rc = CurlGet(url, tempFile.Path(), outputHeadersOpt, inputHeadersOpt); + if (rc == 200) + { + std::ifstream inputStream(tempFile.Path(), std::ios::binary); + if (!inputStream) + { + throw std::runtime_error("Failed to open download file."); + } + inputStream.seekg(0, std::ios::end); + std::streamsize size = inputStream.tellg(); + inputStream.seekg(0, std::ios::beg); + output.resize(static_cast(size)); + if (size > 0) + { + inputStream.read(reinterpret_cast(output.data()), size); + } + } + return rc; + } + int CurlGet( + const std::string &url, + std::vector &output, + std::vector *outputHeadersOpt, + const std::vector *inputHeadersOpt) + { + TemporaryFile tempFile{WEB_TEMP_DIR}; + int rc = CurlGet(url, tempFile.Path(), outputHeadersOpt, inputHeadersOpt); + + std::ifstream inputStream(tempFile.Path(), std::ios::binary); + if (!inputStream) + { + throw std::runtime_error("Failed to open download file."); + } + output.clear(); + std::string line; + while (std::getline(inputStream, line)) + { + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + output.push_back(line); + } + return rc; + } + + int CurlGet( + const std::string &url, + const std::filesystem::path &outputPath, + std::vector *outputHeadersOpt, + const std::vector *inputHeadersOpt) + { + + TemporaryFile headersFile{WEB_TEMP_DIR}; + + std::vector defaultHeaders; + if (outputHeadersOpt == nullptr) + { + outputHeadersOpt = &defaultHeaders; + } + + bool bResult = true; + + std::stringstream ssArgs; + + if (inputHeadersOpt) + { + for (const std::string &header : *inputHeadersOpt) + { + ssArgs << "-H " << ShellEscape(header) << " "; + } + } + + ssArgs << "-s -L -D " << ShellEscape(headersFile.Path().c_str()) + << " " << ShellEscape(url) + << " -o " << ShellEscape(outputPath.c_str()); + + std::string args = ssArgs.str(); + auto curlOutput = sysExecForOutput("/usr/bin/curl", args); + + if (outputHeadersOpt != nullptr) + { + std::ifstream headersStream(headersFile.Path()); + if (headersStream) + { + outputHeadersOpt->clear(); + std::string line; + while (std::getline(headersStream, line)) + { + // Remove trailing carriage return if present + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + if (!line.empty()) + { + outputHeadersOpt->push_back(line); + } + } + } + } + + logHeaders(*outputHeadersOpt); + if (curlOutput.exitCode == 512) + { + throw std::runtime_error("Invalid curl arguments."); + } + if (curlOutput.exitCode != EXIT_SUCCESS && curlOutput.exitCode != 22 * 256) + { + if (WIFEXITED(curlOutput.exitCode)) + { + auto exitCode = WEXITSTATUS(curlOutput.exitCode); + throwCurlExitCode(exitCode); + return -99; + } + else if (WIFSIGNALED(curlOutput.exitCode)) + { + throw std::runtime_error("No internet access."); + } + else + { + throw std::runtime_error(SS("Unable to exec curl. (exit status = " << curlOutput.exitCode << ")")); + } + } + + int errorCode = checkCurlHttpResponse(*outputHeadersOpt); + + return errorCode; + } + + static int curlExec( + const std::string &commandPath, // path to executable. + const std::vector &arguments, // commandline arguments. + const std::function &onStdoutLine, + std::string &stdErrOutput) + { + // Unexpected errors throw std::runtime_error. + + // create linux socket pairs for stdout and stderr. + int stdoutPipe[2] = {-1, -1}; + int stderrPipe[2] = {-1, -1}; + + Finally ffPipes{ + [&stdoutPipe, &stderrPipe]() + { + if (stdoutPipe[0] != -1) + close(stdoutPipe[0]); + if (stdoutPipe[1] != -1) + close(stdoutPipe[1]); + if (stderrPipe[0] != -1) + close(stdoutPipe[0]); + if (stderrPipe[1] != -1) + close(stdoutPipe[1]); + }}; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, stdoutPipe) == -1) + { + throw std::runtime_error("Failed to create stdout socket pair"); + } + if (socketpair(AF_UNIX, SOCK_STREAM, 0, stderrPipe) == -1) + { + throw std::runtime_error("Failed to create stderr socket pair"); + } + + // exec the command with the supplied arguments + pid_t pid = fork(); + if (pid == -1) + { + throw std::runtime_error("Failed to fork process"); + } + + if (pid == 0) + { + // Child process + close(stdoutPipe[0]); // Close read end + close(stderrPipe[0]); // Close read end + + // Redirect stdout and stderr + dup2(stdoutPipe[1], STDOUT_FILENO); + dup2(stderrPipe[1], STDERR_FILENO); + + close(stdoutPipe[1]); + close(stderrPipe[1]); + + // stdin to /dev/null. + int devNull = open("/dev/null", O_RDONLY); + if (devNull == -1) + { + std::cerr << "Failed to open /dev/null" << std::endl; + exit(EXIT_FAILURE); + } + if (dup2(devNull, STDIN_FILENO) == -1) + { + std::cerr << "Failed to redirect stdin" << std::endl; + close(devNull); + exit(EXIT_FAILURE); + } + close(devNull); + + // Build argv array + std::vector argv; + argv.push_back(const_cast(commandPath.c_str())); + for (const auto &arg : arguments) + { + argv.push_back(const_cast(arg.c_str())); + } + argv.push_back(nullptr); + + execv(commandPath.c_str(), argv.data()); + + // If execv returns, it failed + std::cerr << "Failed to execute: " << commandPath << std::endl; + exit(EXIT_FAILURE); + } + + // Parent process + close(stdoutPipe[1]); // Close write end + stdoutPipe[1] = -1; + close(stderrPipe[1]); // Close write end + stderrPipe[1] = -1; + + // Read from stderr and stdout. + // stderr output gets appended to the stdErrOutput argument. + // stdin output gets read line by line, and onStdOutLine is called for each line of text that's read. + + // Set non-blocking mode for both pipes + fcntl(stdoutPipe[0], F_SETFL, O_NONBLOCK); + fcntl(stderrPipe[0], F_SETFL, O_NONBLOCK); + + std::string stdoutBuffer; + std::string stderrBuffer; + + bool stdoutOpen = true; + bool stderrOpen = true; + + while (stdoutOpen || stderrOpen) + { + fd_set readfds; + FD_ZERO(&readfds); + int maxfd = -1; + + if (stdoutOpen) + { + FD_SET(stdoutPipe[0], &readfds); + maxfd = std::max(maxfd, stdoutPipe[0]); + } + if (stderrOpen) + { + FD_SET(stderrPipe[0], &readfds); + maxfd = std::max(maxfd, stderrPipe[0]); + } + + struct timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + + int ret = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout); + + if (ret > 0) + { + // Read from stdout + if (stdoutOpen && FD_ISSET(stdoutPipe[0], &readfds)) + { + char buffer[4096]; + ssize_t n = read(stdoutPipe[0], buffer, sizeof(buffer)); + if (n > 0) + { + stdoutBuffer.append(buffer, n); + + // Process complete lines + size_t pos; + while ((pos = stdoutBuffer.find('\n')) != std::string::npos) + { + std::string line = stdoutBuffer.substr(0, pos); + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + if (!onStdoutLine(line)) + { + // Kill the child process and return immediately + // do NOT waitpid, as that will be done in the processing loop. + kill(pid, SIGTERM); + } + stdoutBuffer.erase(0, pos + 1); + } + } + else if (n == 0) + { + stdoutOpen = false; + close(stdoutPipe[0]); + + // Process any remaining data + if (!stdoutBuffer.empty()) + { + if (!stdoutBuffer.empty() && stdoutBuffer.back() == '\r') + { + stdoutBuffer.pop_back(); + } + if (!stdoutBuffer.empty()) + { + if (!onStdoutLine(stdoutBuffer)) + { + // abort the child process + kill(pid, SIGINT); // Curl: SIGINT= graceful shutdown. SIGTERM=abrupt shutdown. + // but wait for output conforming the cancellation + // i.e. don't waitpid -- that will be done on cleanup, and don't abort the loop. + } + } + } + } + } + + // Read from stderr + if (stderrOpen && FD_ISSET(stderrPipe[0], &readfds)) + { + char buffer[4096]; + ssize_t n = read(stderrPipe[0], buffer, sizeof(buffer)); + if (n > 0) + { + stdErrOutput.append(buffer, n); + } + else if (n == 0) + { + stderrOpen = false; + close(stderrPipe[0]); + } + } + } + } + + // Return the procesess's exit code. + int status; + waitpid(pid, &status, 0); + + if (WIFEXITED(status)) + { + auto exitCode = WEXITSTATUS(status); + if (exitCode == EXIT_SUCCESS) + { + return EXIT_SUCCESS; + } + throwCurlExitCode(exitCode); + return exitCode; + } + else if (WIFSIGNALED(status)) + { + return -1; + } + + return -2; + } + int CurlGet( + const std::vector &request, + const std::function &progressCallback, + std::vector *outputHeadersOpt) + { + if (request.empty()) + { + return 200; + } + + size_t currentDownload = 0; + size_t totalDownloads = request.size(); + int lastStatusCode = 200; + + // Create temporary file for curl config + TemporaryFile configFile{WEB_TEMP_DIR}; + + // Write curl config file with URL and output pairs + { + std::ofstream configStream(configFile.Path()); + if (!configStream) + { + throw std::runtime_error("Failed to create curl config file"); + } + + for (const auto &req : request) + { + configStream << "url = \"" << req.url << "\"\n"; + configStream << "output = \"" << req.outputFile.string() << "\"\n"; + } + } + + // Build curl arguments + std::vector curlArgs; + curlArgs.push_back("-s"); // Silent mode + curlArgs.push_back("-L"); // Follow redirects + // curlArgs.push_back("-w"); // Write format + // curlArgs.push_back("URL: %{url_effective}\\n"); + curlArgs.push_back("--parallel-max"); + curlArgs.push_back("1"); + curlArgs.push_back("-D"); // Dump headers to stdout + curlArgs.push_back("-"); // Write headers to stdout + curlArgs.push_back("--no-progress-meter"); // Suppress progress + curlArgs.push_back("--retry-delay"); + curlArgs.push_back("2"); + curlArgs.push_back("--retry-max-time"); + curlArgs.push_back("6"); + curlArgs.push_back("-K"); + curlArgs.push_back(configFile.Path().string()); + curlArgs.push_back("--fail-early"); // quit as soon as one transfer fails. + + std::string stderrOutput; + + std::string savedHttpHeader; + + // Process stdout lines + auto onStdoutLine = [&](const std::string &line) -> bool + { + if (outputHeadersOpt != nullptr) + { + outputHeadersOpt->push_back(line); + } + // Empty line indicates start of new download or end of headers + if (line.empty()) + { + if (!savedHttpHeader.empty()) + { + std::string lastHeader = savedHttpHeader; + savedHttpHeader = ""; + // Parse status code + size_t codeStart = lastHeader.find(' '); + if (codeStart != std::string::npos) + { + codeStart++; // Skip the space + size_t codeEnd = lastHeader.find(' ', codeStart); + if (codeEnd == std::string::npos) + { + codeEnd = lastHeader.length(); + } + + std::string codeStr = lastHeader.substr(codeStart, codeEnd - codeStart); + try + { + int statusCode = std::stoi(codeStr); + + // Handle different status codes + if (statusCode == 200) + { + // Successful download + currentDownload++; + lastStatusCode = 200; + if (progressCallback) + { + progressCallback(currentDownload, totalDownloads); + } + } + else if (statusCode == 429) + { + // Retry - curl will handle it, just ignore + return true; + } + else if (statusCode == 503) + { + // Throttling - save position and expect exit + lastStatusCode = 503; + Lv2Log::warning("%s", "curl: Received 503 response."); + return false; + } + else if (statusCode >= 300 && statusCode < 400) + { + // Redirect, &c - advisory only, ignore, and hope curl will deal with it. + return true; + } + else if (statusCode >= 400) + { + // Error - fatal + lastStatusCode = statusCode; + return false; // Stop processing + } + } + catch (const std::exception &) + { + // Failed to parse status code - continue + return true; + } + } + } + } + else if (line.starts_with("HTTP/")) + { + // Delay processing until we get a blank line. + savedHttpHeader = line; + } + // Other lines are headers - ignore unless we need to save them + return true; + }; + + // Execute curl + int exitCode = curlExec("/usr/bin/curl", curlArgs, onStdoutLine, stderrOutput); + + logHeaders(*outputHeadersOpt); + + if (exitCode != EXIT_SUCCESS && exitCode != -1) + { + throwCurlExitCode(exitCode); + } + + // Return appropriate status code + + return lastStatusCode; + } + + int CurlPostFile( + const std::string &url, + const std::filesystem::path &inputPath, + const std::filesystem::path &outputPath, + std::vector *outputHeadersOpt, + std::vector *inputHeadersOpt) + { + TemporaryFile headersFile{WEB_TEMP_DIR}; + + std::vector defaultHeaders; + if (outputHeadersOpt == nullptr) + { + outputHeadersOpt = &defaultHeaders; + } + + bool bResult = true; + + std::stringstream ssArgs; + + if (inputHeadersOpt) + { + for (const std::string &header : *inputHeadersOpt) + { + ssArgs << "-H " << ShellEscape(header) << " "; + } + } + ssArgs << "-s -L -X POST --data-binary " + << SS("@" << inputPath.c_str()).c_str() + << " -D " << ShellEscape(headersFile.Path().c_str()); + + std::string args = ssArgs.str(); + + + args += SS(" " << ShellEscape(url) + << " -o " << ShellEscape(outputPath.c_str())); + + auto curlOutput = sysExecForOutput("/usr/bin/curl", args); + + if (outputHeadersOpt != nullptr) + { + std::ifstream headersStream(headersFile.Path()); + if (headersStream) + { + outputHeadersOpt->clear(); + std::string line; + while (std::getline(headersStream, line)) + { + // Remove trailing carriage return if present + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + if (!line.empty()) + { + outputHeadersOpt->push_back(line); + } + } + } + } + + logHeaders(*outputHeadersOpt); + if (curlOutput.exitCode == 512) + { + throw std::runtime_error("Invalid curl arguments."); + } + if (curlOutput.exitCode != EXIT_SUCCESS && curlOutput.exitCode != 22 * 256) + { + if (WIFEXITED(curlOutput.exitCode)) + { + auto exitCode = WEXITSTATUS(curlOutput.exitCode); + throwCurlExitCode(exitCode); + return -99; + } + else if (WIFSIGNALED(curlOutput.exitCode)) + { + throw std::runtime_error("No internet access."); + } + else + { + throw std::runtime_error(SS("Unable to exec curl. (exit status = " << curlOutput.exitCode << ")")); + } + } + + int errorCode = checkCurlHttpResponse(*outputHeadersOpt); + + return errorCode; + } + + int CurlPostStrings( + const std::string &url, + const std::string &body, + std::string &outputBody, + std::vector *outputHeadersOpt, + std::vector *inputHeadersOpt) + { + TemporaryFile inputFile(WEB_TEMP_DIR); + { + std::ofstream f{inputFile.Path()}; + if (!f.is_open()) + { + throw std::runtime_error(SS("Can't open file " << inputFile.Path())); + } + f << body; + } + TemporaryFile outputFile(WEB_TEMP_DIR); + int result = CurlPostFile(url, inputFile.Path(), outputFile.Path(), outputHeadersOpt, inputHeadersOpt); + + std::ifstream outputStream(outputFile.Path()); + if (outputStream) + { + outputBody.assign( + std::istreambuf_iterator(outputStream), + std::istreambuf_iterator()); + } + + return result; + } +} diff --git a/src/Curl.hpp b/src/Curl.hpp new file mode 100644 index 0000000..922d3b2 --- /dev/null +++ b/src/Curl.hpp @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 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 +#include +#include + +namespace pipedal { + + extern int CurlGet( + const std::string &url, + const std::filesystem::path&outputFile, + std::vector *outputHeadersOpt = nullptr, + const std::vector *inputHeadersOpt = nullptr + ); + extern int CurlGet( + const std::string &url, + std::vector &output, + std::vector *outputHeadersOpt = nullptr, + const std::vector *inputHeadersOpt = nullptr + + ); + extern int CurlGet( + const std::string&url, + std::vector &output, + std::vector *outputHeadersOpt = nullptr, + const std::vector *inputHeadersOpt = nullptr + ); + + struct CurlDownloadRequest{ + std::string url; + std::filesystem::path outputFile; + }; + extern int CurlGet( + const std::vector &request, + const std::function &progressCallback, + std::vector*outputHeadersOpt + ); + + + extern int CurlPostFile( + const std::string &url, + const std::filesystem::path&inputFile, + const std::filesystem::path&outputFile, + std::vector *outputHeadersOpt = nullptr, + std::vector *inputHeadersOpt = nullptr + ); + extern int CurlPostStrings( + const std::string &url, + const std::string&body, + std::string&responseBody, + std::vector *outputHeadersOpt = nullptr, + std::vector *inputHeadersOpt = nullptr + ); + + +} \ No newline at end of file diff --git a/src/DeviceVus.hpp b/src/DeviceVus.hpp new file mode 100644 index 0000000..4f192ec --- /dev/null +++ b/src/DeviceVus.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 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 "json.hpp" + + +namespace pipedal { + class RealtimeDeviceVus { + public: + static constexpr size_t MAX_CHANNELS = 64; + size_t inputChannels; + size_t outputChannels; + float values[MAX_CHANNELS*2]; + + size_t MessageSize() const { + return sizeof(RealtimeDeviceVus)-sizeof(float)*( + MAX_CHANNELS*2- + (inputChannels+outputChannels+1)/2*2); + } + }; +}; \ No newline at end of file diff --git a/src/DummyAudioDriver.cpp b/src/DummyAudioDriver.cpp index dd2301b..a52c21b 100644 --- a/src/DummyAudioDriver.cpp +++ b/src/DummyAudioDriver.cpp @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2024 Robin E. R. Davies + * Copyright (c) 2026 Robin E. R. 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 @@ -23,6 +23,7 @@ */ #include "pch.h" +#include "PiPedalCommon.hpp" #include "util.hpp" #include #include @@ -39,6 +40,7 @@ #include "ss.hpp" #include "SchedulerPriority.hpp" #include "CrashGuard.hpp" +#include "ChannelRouterSettings.hpp" #include "CpuUse.hpp" @@ -56,6 +58,8 @@ using namespace pipedal; +#ifdef JUNK + namespace pipedal { @@ -85,14 +89,19 @@ namespace pipedal uint32_t captureFrameSize = 0; - std::vector activeCaptureBuffers; - std::vector activePlaybackBuffers; + std::vector> allocatedBuffers; + std::vector mainCaptureBuffers; + std::vector mainPlaybackBuffers; - std::vector captureBuffers; - std::vector playbackBuffers; + std::vector auxCaptureBuffers; + std::vector auxPlaybackBuffers; + + std::vector sendCaptureBuffers; + std::vector sendPlaybackBuffers; + + std::vector deviceCaptureBuffers; + std::vector devicePlaybackBuffers; - uint8_t *rawCaptureBuffer = nullptr; - uint8_t *rawPlaybackBuffer = nullptr; AudioDriverHost *driverHost = nullptr; uint32_t channels = 2; @@ -145,7 +154,7 @@ namespace pipedal std::atomic terminateAudio_ = false; - void terminateAudio(bool terminate) + PIPEDAL_NON_INLINE void terminateAudio(bool terminate) { this->terminateAudio_ = terminate; } @@ -161,32 +170,28 @@ namespace pipedal } private: - void AllocateBuffers(std::vector &buffers, size_t n) + PIPEDAL_NON_INLINE void AllocateBuffers(std::vector &buffers, size_t n) { buffers.resize(n); for (size_t i = 0; i < n; ++i) { - buffers[i] = new float[this->bufferSize]; - for (size_t j = 0; j < this->bufferSize; ++j) - { - buffers[i][j] = 0; - } + buffers[i] = AllocateAudioBuffer(); } } - virtual size_t GetMidiInputEventCount() override + PIPEDAL_NON_INLINE virtual size_t GetMidiInputEventCount() override { return midiEventCount; } - virtual MidiEvent *GetMidiEvents() override + PIPEDAL_NON_INLINE virtual MidiEvent *GetMidiEvents() override { return this->midiEvents.data(); } - JackChannelSelection channelSelection; + ChannelSelection channelSelection; bool open = false; - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + PIPEDAL_NON_INLINE virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) { terminateAudio_ = false; if (open) @@ -208,7 +213,7 @@ namespace pipedal throw; } } - virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override + PIPEDAL_NON_INLINE virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override { this->alsaSequencer = alsaSequencer; } @@ -220,23 +225,28 @@ namespace pipedal << ", " << "Native float" << ", " << this->sampleRate << ", " << this->bufferSize << "x" << this->numberOfBuffers - << ", in: " << this->InputBufferCount() << "/" << this->captureChannels - << ", out: " << this->OutputBufferCount() << "/" << this->playbackChannels); + << ", " << "device in: " << this->DeviceInputBufferCount() + << ", " << "device out: " << this->DeviceOutputBufferCount() + << ", main in: " << this->MainInputBufferCount() + << ", main out: " << this->MainOutputBufferCount() + << ", aux in: " << this->AuxInputBufferCount() + << ", aux out: " << this->AuxOutputBufferCount() + << ", send in: " << this->SendInputBufferCount() + << ", send out: " << this->SendOutputBufferCount() + ); return result; } - void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + void OpenAudio(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) { int err; this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers(); this->bufferSize = jackServerSettings.GetBufferSize(); - AllocateBuffers(captureBuffers, channels); - AllocateBuffers(playbackBuffers, channels); } - std::jthread *audioThread; + std::unique_ptr audioThread; bool audioRunning; bool block = false; @@ -303,7 +313,7 @@ namespace pipedal this->driverHost->OnProcess(framesRead); /// no attempt at realtime. Just as long as we run occasionally. - std::this_thread::sleep_for(std::chrono::milliseconds(20)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); } @@ -318,22 +328,6 @@ namespace pipedal bool alsaActive = false; - static int IndexFromPortName(const std::string &s) - { - auto pos = s.find_last_of('_'); - if (pos == std::string::npos) - { - throw std::invalid_argument("Bad port name."); - } - const char *p = s.c_str() + (pos + 1); - - int v = atoi(p); - if (v < 0) - { - throw std::invalid_argument("Bad port name."); - } - return v; - } bool activated = false; virtual void Activate() @@ -344,41 +338,17 @@ namespace pipedal } activated = true; - this->activeCaptureBuffers.resize(channelSelection.GetInputAudioPorts().size()); + AllocateBuffers(deviceCaptureBuffers, channels); + AllocateBuffers(devicePlaybackBuffers, channels); - playbackBuffers.resize(channels); + AllocateBuffers(mainCaptureBuffers, channelSelection.mainInputChannels().size()); + AllocateBuffers(mainPlaybackBuffers, channelSelection.mainOutputChannels().size()); + AllocateBuffers(auxCaptureBuffers, channelSelection.auxInputChannels().size()); + AllocateBuffers(auxPlaybackBuffers, channelSelection.auxOutputChannels().size()); + AllocateBuffers(sendCaptureBuffers, channelSelection.sendInputChannels().size()); + AllocateBuffers(sendPlaybackBuffers, channelSelection.sendOutputChannels().size()); - int ix = 0; - for (auto &x : channelSelection.GetInputAudioPorts()) - { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= captureBuffers.size()) - { - Lv2Log::error(SS("Invalid audio input port: " << x)); - } - else - { - this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex]; - } - } - - this->activePlaybackBuffers.resize(channelSelection.GetOutputAudioPorts().size()); - - ix = 0; - for (auto &x : channelSelection.GetOutputAudioPorts()) - { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= playbackBuffers.size()) - { - Lv2Log::error(SS("Invalid audio output port: " << x)); - } - else - { - this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex]; - } - } - - audioThread = new std::jthread([this]() + audioThread = std::make_unique([this]() { AudioThread(); }); } @@ -393,7 +363,7 @@ namespace pipedal if (audioThread) { this->audioThread->join(); - this->audioThread = 0; + this->audioThread = nullptr; } Lv2Log::debug("Audio thread joined."); } @@ -402,46 +372,64 @@ namespace pipedal public: + virtual size_t DeviceInputBufferCount() const override { return deviceCaptureBuffers.size(); } + virtual size_t DeviceOutputBufferCount() const override { return devicePlaybackBuffers.size(); } - virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); } - virtual float *GetInputBuffer(size_t channel) + virtual size_t MainInputBufferCount() const override { return mainCaptureBuffers.size(); } + virtual float *GetMainInputBuffer(size_t channel) override { - return activeCaptureBuffers[channel]; + return mainCaptureBuffers[channel]; } - virtual size_t OutputBufferCount() const { return activePlaybackBuffers.size(); } - virtual float *GetOutputBuffer(size_t channel) + virtual size_t MainOutputBufferCount() const { return mainPlaybackBuffers.size(); } + virtual float *GetMainOutputBuffer(size_t channel) override { - return activePlaybackBuffers[channel]; + return mainPlaybackBuffers[channel]; } - void FreeBuffers(std::vector &buffer) + virtual size_t AuxInputBufferCount() const override { return auxCaptureBuffers.size(); } + virtual float *GetAuxInputBuffer(size_t channel) override { - for (size_t i = 0; i < buffer.size(); ++i) - { - // delete[] buffer[i]; - buffer[i] = 0; - } - buffer.clear(); + return auxCaptureBuffers[channel]; } - void DeleteBuffers() + + virtual size_t AuxOutputBufferCount() const override { return auxPlaybackBuffers.size(); } + virtual float *GetAuxOutputBuffer(size_t channel) override { - activeCaptureBuffers.clear(); - activePlaybackBuffers.clear(); - FreeBuffers(this->playbackBuffers); - FreeBuffers(this->captureBuffers); - if (rawCaptureBuffer) - { - delete[] rawCaptureBuffer; - rawCaptureBuffer = nullptr; - } - if (rawPlaybackBuffer) - { - delete[] rawPlaybackBuffer; - rawPlaybackBuffer = nullptr; - } + return auxPlaybackBuffers[channel]; } - virtual void Close() + + virtual size_t SendInputBufferCount() const override { return sendCaptureBuffers.size(); } + virtual float *GetSendInputBuffer(size_t channel) override + { + return sendCaptureBuffers[channel]; + } + + virtual size_t SendOutputBufferCount() const override { return sendPlaybackBuffers.size(); } + virtual float *GetSendOutputBuffer(size_t channel) override + { + return sendPlaybackBuffers[channel]; + } + + + + + + PIPEDAL_NON_INLINE float*AllocateAudioBuffer() { + allocatedBuffers.push_back(std::vector(bufferSize)); + return allocatedBuffers.back().data(); + } + PIPEDAL_NON_INLINE void DeleteBuffers() + { + mainCaptureBuffers.clear(); + mainPlaybackBuffers.clear(); + auxCaptureBuffers.clear(); + auxPlaybackBuffers.clear(); + sendCaptureBuffers.clear(); + sendPlaybackBuffers.clear(); + allocatedBuffers.clear(); + } + PIPEDAL_NON_INLINE virtual void Close() { if (!open) { @@ -453,18 +441,18 @@ namespace pipedal DeleteBuffers(); } - virtual float CpuUse() + PIPEDAL_NON_INLINE virtual float CpuUse() { return 0; } - virtual float CpuOverhead() + PIPEDAL_NON_INLINE virtual float CpuOverhead() { return 0.1; } }; - AudioDriver *CreateDummyAudioDriver(AudioDriverHost *driverHost,const std::string&deviceName) + PIPEDAL_NON_INLINE AudioDriver *CreateDummyAudioDriver(AudioDriverHost *driverHost,const std::string&deviceName) { return new DummyDriverImpl(driverHost,deviceName); } @@ -538,3 +526,4 @@ uint32_t pipedal::GetDummyAudioChannels(const std::string &deviceName) } +#endif \ No newline at end of file diff --git a/src/DummyAudioDriver.hpp b/src/DummyAudioDriver.hpp index 35f2d31..59b2c9b 100644 --- a/src/DummyAudioDriver.hpp +++ b/src/DummyAudioDriver.hpp @@ -24,6 +24,8 @@ #pragma once +#ifdef JUNK +#include #include "AudioDriver.hpp" #include "JackServerSettings.hpp" @@ -35,4 +37,5 @@ namespace pipedal { AudioDriver* CreateDummyAudioDriver(AudioDriverHost*driverHost,const std::string&deviceId); } +#endif diff --git a/src/IHost.hpp b/src/IHost.hpp index ab02649..48c7c52 100644 --- a/src/IHost.hpp +++ b/src/IHost.hpp @@ -26,6 +26,7 @@ namespace pipedal { class Lv2PluginInfo; class IEffect; class HostWorkerThread; + class ChannelSelection; class IHost { @@ -43,13 +44,12 @@ namespace pipedal { virtual size_t GetAtomBufferSize() const = 0; virtual bool HasMidiInputChannel() const = 0; - virtual int GetNumberOfInputAudioChannels() const = 0; - virtual int GetNumberOfOutputAudioChannels() const = 0; virtual std::shared_ptr GetPluginInfo(const std::string &uri) const = 0; virtual IEffect *CreateEffect(PedalboardItem &pedalboard) = 0; virtual std::string GetPluginStoragePath() const = 0; + virtual const ChannelSelection&GetChannelSelection() const = 0; }; } \ No newline at end of file diff --git a/src/JackConfiguration.cpp b/src/JackConfiguration.cpp index da7e256..cd111f3 100644 --- a/src/JackConfiguration.cpp +++ b/src/JackConfiguration.cpp @@ -130,12 +130,8 @@ void JackConfiguration::AlsaInitialize( this->isValid_ = false; this->errorStatus_ = ""; - if (jackServerSettings.IsDummyAudioDevice()) - { - this->inputMidiDevices_.clear(); - } else { - this->inputMidiDevices_ = GetAlsaSequencers(); // NB: Sequencers, not rawmidi devices, anymore. - } + this->inputMidiDevices_ = GetAlsaSequencers(); // NB: Sequencers, not rawmidi devices, anymore. + if (jackServerSettings.IsValid()) { this->blockLength_ = jackServerSettings.GetBufferSize(); @@ -149,6 +145,8 @@ void JackConfiguration::AlsaInitialize( } } catch (const std::exception& /*ignore*/) { + this->isValid_ = false; + this->errorStatus_ = "Not configured."; throw; } diff --git a/src/JackConfiguration.hpp b/src/JackConfiguration.hpp index b76abb0..a6a9bc4 100644 --- a/src/JackConfiguration.hpp +++ b/src/JackConfiguration.hpp @@ -124,5 +124,4 @@ namespace pipedal DECLARE_JSON_MAP(JackChannelSelection); }; - } // namespace. \ No newline at end of file diff --git a/src/JackServerSettings.cpp b/src/JackServerSettings.cpp index 62aef81..15ce110 100644 --- a/src/JackServerSettings.cpp +++ b/src/JackServerSettings.cpp @@ -28,6 +28,7 @@ #include #include "Lv2Log.hpp" #include "SysExec.hpp" +#include "PiPedalAlsa.hpp" #define DRC_FILENAME "/etc/jackdrc" @@ -60,33 +61,41 @@ static std::vector SplitArgs(const char *szBuff) return result; } -static std::string GetJackStringArg(const std::vector &args, const std::string&shortOption, const std::string&longOption) +static std::string GetJackStringArg(const std::vector &args, const std::string &shortOption, const std::string &longOption) { std::string strVal; for (size_t i = 1; i < args.size(); ++i) { - auto pos = args[i].rfind(longOption,0); - if (pos != std::string::npos) { + auto pos = args[i].rfind(longOption, 0); + if (pos != std::string::npos) + { if (args[i].length() == longOption.length()) { - if (i == args.size()-1) { - throw PiPedalException("Can't read Jack configuration."); + if (i == args.size() - 1) + { + throw PiPedalException("Can't read Jack configuration."); } - strVal = args[i+1]; - } else { + strVal = args[i + 1]; + } + else + { strVal = args[i].substr(longOption.length()); } break; } - pos = args[i].rfind(shortOption,0); - if (pos != std::string::npos) { + pos = args[i].rfind(shortOption, 0); + if (pos != std::string::npos) + { if (args[i].length() == shortOption.length()) { - if (i == args.size()-1) { - throw PiPedalException("Can't read Jack configuration."); + if (i == args.size() - 1) + { + throw PiPedalException("Can't read Jack configuration."); } - strVal = args[i+1]; - } else { + strVal = args[i + 1]; + } + else + { strVal = args[i].substr(shortOption.length()); } break; @@ -94,9 +103,9 @@ static std::string GetJackStringArg(const std::vector &args, const } return strVal; } -static std::int32_t GetJackArg(const std::vector &args, const std::string&shortOption, const std::string&longOption) +static std::int32_t GetJackArg(const std::vector &args, const std::string &shortOption, const std::string &longOption) { - std::string strVal = GetJackStringArg(args,shortOption,longOption); + std::string strVal = GetJackStringArg(args, shortOption, longOption); try { unsigned long value = std::stoul(strVal); @@ -108,145 +117,45 @@ static std::int32_t GetJackArg(const std::vector &args, const std:: } } - -void JackServerSettings::ReadJackDaemonConfiguration() +static std::string GetAlsaDeviceName(const std::vector &availableDevices, const std::string &id) { - #if !JACK_HOST - return; - #endif - this->valid_ = false; - - std::string lastLine; + if (id.empty()) + return ""; + std::string name; + for (const auto &availableDevice : availableDevices) + { + if (availableDevice.id_ == id) + { + name = availableDevice.name_; + break; + } + } + if (name.empty()) + { + auto pos = id.find(":"); + if (pos != std::string::npos) + { + name = id.substr(pos + 1); + } + } + return name; +} +void JackServerSettings::FixUpDeviceNames() +{ + if ( + ((!this->alsaInputDevice_.empty()) && this->alsaInputDeviceName_.empty()) || + (!this->alsaOutputDevice_.empty()) && this->alsaOutputDeviceName_.empty()) { - ifstream input(DRC_FILENAME); - if (!input.is_open()) - { - return; - } + PiPedalAlsaDevices& alsaDevices = PiPedalAlsaDevices::instance(); + std::vector availableDevices = alsaDevices.GetAlsaDevices(); - while (true) - { - std::string line; - std::getline(input,line); - if (line.length() != 0) { - lastLine = line; - } - if (input.eof()) { - break; - } - } - try - { - std::vector argv = SplitArgs(lastLine.c_str()); - for (auto i = argv.begin(); i != argv.end(); ++i) - { - if ((*i) == "-dalsa") { - argv.erase(i); - break; - } - } - this->bufferSize_ = GetJackArg(argv, "-p", "--period"); - this->numberOfBuffers_ = (uint32_t)GetJackArg(argv, "-n", "--nperiods"); - this->sampleRate_ = (uint32_t)GetJackArg(argv, "-r", "--rate"); - // read new dual device flags, fallback on old -d/--device - std::string capDev = GetJackStringArg(argv, "-C", "--capture"); - std::string playDev = GetJackStringArg(argv, "-P", "--playback"); - std::string dev = ""; - try { dev = GetJackStringArg(argv, "-d", "--device"); } catch(...) {} - this->alsaInputDevice_ = capDev.empty() ? dev : capDev; - this->alsaOutputDevice_ = playDev.empty() ? dev : playDev; - this->valid_ = true; - } - catch (std::exception &) - { - //Lv2Log::error("Can't parse " DRC_FILENAME); - } + this->alsaInputDeviceName_ = GetAlsaDeviceName(availableDevices, this->alsaInputDevice_); + this->alsaOutputDeviceName_ = GetAlsaDeviceName(availableDevices, this->alsaOutputDevice_); } } -void JackServerSettings::WriteDaemonConfig() -{ - #if JACK_HOST - this->valid_ = false; - std::vector precedingLines; - std::vector argv; - std::string lastLine; - - if (std::filesystem::exists(DRC_FILENAME)) { - ifstream input(DRC_FILENAME); - - if (!input.is_open()) - { - return; - } - - while (true) - { - if (input.eof()) - { - break; - } - std::getline(input,lastLine); - precedingLines.push_back(lastLine); - if (input.eof()) - { - break; - } - } - // erase blank lines at the end. - while (precedingLines.size() != 0 && precedingLines[precedingLines.size()-1] == "") - { - precedingLines.erase(precedingLines.begin()+precedingLines.size()-1); - } - // erase the last line, which should contain the command invocation. - if (precedingLines.size() != 0) - { - precedingLines.erase(precedingLines.begin()+precedingLines.size()-1); - } - } - // write to the output. - try - { - ofstream output(DRC_FILENAME); - if (!output.is_open()) - { - throw PiPedalException("Can't write " DRC_FILENAME); - } - if (precedingLines.size() == 0) - { - // jack1 incantation for promiscuous servers. - output << "#!/bin/sh" <alsaDevice_ - << " -r" << this->sampleRate_ - << " -p" << this->bufferSize_ - << " -n" << this->numberOfBuffers_ << " -Xseq" - << endl; - if (silentSysExec("/usr/bin/chmod 755 " DRC_FILENAME) != 0) - { - Lv2Log::error("Failed to set permissions on /etc/jackdrc"); - } - } - catch (const std::exception &e) - { - } - #else - throw PiPedalStateException("JACK_HOST not enabled at compile time."); - #endif -} JSON_MAP_BEGIN(JackServerSettings) JSON_MAP_REFERENCE(JackServerSettings, valid) @@ -256,6 +165,8 @@ JSON_MAP_REFERENCE(JackServerSettings, isJackAudio) JSON_MAP_REFERENCE(JackServerSettings, alsaDevice) // legacy field JSON_MAP_REFERENCE(JackServerSettings, alsaInputDevice) JSON_MAP_REFERENCE(JackServerSettings, alsaOutputDevice) +JSON_MAP_REFERENCE(JackServerSettings, alsaInputDeviceName) +JSON_MAP_REFERENCE(JackServerSettings, alsaOutputDeviceName) JSON_MAP_REFERENCE(JackServerSettings, sampleRate) JSON_MAP_REFERENCE(JackServerSettings, bufferSize) JSON_MAP_REFERENCE(JackServerSettings, numberOfBuffers) diff --git a/src/JackServerSettings.hpp b/src/JackServerSettings.hpp index f978d74..dfcc0c2 100644 --- a/src/JackServerSettings.hpp +++ b/src/JackServerSettings.hpp @@ -33,7 +33,9 @@ namespace pipedal bool isJackAudio_ = JACK_HOST ? true : false; bool rebootRequired_ = false; std::string alsaInputDevice_; + std::string alsaInputDeviceName_; std::string alsaOutputDevice_; + std::string alsaOutputDeviceName_; std::string alsaDevice_; // legacy uint64_t sampleRate_ = 0; uint32_t bufferSize_ = 64; @@ -44,6 +46,7 @@ namespace pipedal JackServerSettings( const std::string &alsaInputDevice, const std::string &alsaOutputDevice, + uint64_t sampleRate, uint32_t bufferSize, uint32_t numberOfBuffers) @@ -55,39 +58,41 @@ namespace pipedal numberOfBuffers_(numberOfBuffers), isOnboarding_(false) { + FixUpDeviceNames(); } - + uint64_t GetSampleRate() const { return sampleRate_; } uint32_t GetBufferSize() const { return bufferSize_; } uint32_t GetNumberOfBuffers() const { return numberOfBuffers_; } const std::string &GetAlsaInputDevice() const { return alsaInputDevice_; } + const std::string &GetAlsaInputDeviceName() const { return alsaInputDeviceName_; } const std::string &GetAlsaOutputDevice() const { return alsaOutputDevice_; } + const std::string &GetAlsaOutputDeviceName() const { return alsaOutputDeviceName_; } const std::string &GetLegacyAlsaDevice() const { return alsaDevice_; } //legacy - void SetAlsaInputDevice(const std::string &d){ alsaInputDevice_ = d; } - void SetAlsaOutputDevice(const std::string &d){ alsaOutputDevice_ = d; } + void SetAlsaInputDevice(const std::string &id, const std::string&name){ alsaInputDevice_ = id; alsaInputDeviceName_ = name; } + void SetAlsaOutputDevice(const std::string &id, const std::string&name){ alsaOutputDevice_ = id; alsaOutputDeviceName_ = name; } void SetLegacyAlsaDevice(const std::string &d) { alsaDevice_ = d; } void UseDummyAudioDevice() { this->valid_ = true; if (sampleRate_ == 0) sampleRate_ = 48000; - this->alsaDevice_ = "dummy:channels_2"; - this->alsaInputDevice_ = "dummy:channels_2"; - this->alsaOutputDevice_ = "dummy:channels_2"; + bufferSize_ = 256; + numberOfBuffers_ = 6; + + this->alsaDevice_ = ""; + this->alsaInputDevice_ = "null"; + this->alsaInputDeviceName_ = "null"; + this->alsaOutputDevice_ = "null"; + this->alsaOutputDeviceName_ = "null"; } bool IsDummyAudioDevice() const { - return - this->alsaDevice_.starts_with("__DUMMY_AUDIO__") - || this->alsaDevice_.starts_with("dummy:") - || this->alsaInputDevice_.starts_with("dummy:"); + return alsaInputDevice_ == "null" && alsaOutputDevice_ == "null"; } - void ReadJackDaemonConfiguration(); - bool IsValid() const { return valid_; } - void WriteDaemonConfig(); // requires root perms. void SetRebootRequired(bool value) { rebootRequired_ = value; @@ -106,6 +111,7 @@ namespace pipedal this->bufferSize_ == other.bufferSize_ && this->numberOfBuffers_ == other.numberOfBuffers_; } + void FixUpDeviceNames(); DECLARE_JSON_MAP(JackServerSettings); }; diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 4c750c8..1d3aa45 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -29,6 +29,7 @@ #include "Lv2Log.hpp" #include "CrashGuard.hpp" #include "restrict.hpp" +#include "AudioDriver.hpp" using namespace pipedal; @@ -329,13 +330,15 @@ void Lv2Pedalboard::Prepare(IHost *pHost, Pedalboard &pedalboard, Lv2PedalboardE inputVolume.SetTarget(pedalboard.input_volume_db()); outputVolume.SetTarget(pedalboard.output_volume_db()); - for (int i = 0; i < pHost->GetNumberOfInputAudioChannels(); ++i) + size_t nInputs = std::max(GetNumberOfAudioInputChannels(),(size_t)1); + + for (size_t i = 0; i < nInputs; ++i) { this->pedalboardInputBuffers.push_back(bufferPool.AllocateBuffer(pHost->GetMaxAudioBufferSize())); } auto outputs = PrepareItems(pedalboard.items(), this->pedalboardInputBuffers, errorList, existingEffects); - int nOutputs = pHost->GetNumberOfOutputAudioChannels(); + size_t nOutputs = GetNumberOfAudioOutputChannels(); if (nOutputs == 1) { this->pedalboardOutputBuffers.push_back(outputs[0]); @@ -543,6 +546,9 @@ bool Lv2Pedalboard::Run(float **inputBuffers, float **outputBuffers, uint32_t sa float volume = outputVolume.Tick(); for (size_t c = 0; c < this->pedalboardOutputBuffers.size(); ++c) { + if (outputBuffers[c] == nullptr) { + break; + } outputBuffers[c][i] = this->pedalboardOutputBuffers[c][i] * volume; } } @@ -568,37 +574,46 @@ void Lv2Pedalboard::SetBypass(int effectIndex, bool enabled) effect->SetBypass(enabled); } -void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples, float **inputBuffers, float **outputBuffers) + +void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *realtimeVuBuffers, uint32_t samples) { - for (size_t i = 0; i < vuConfiguration->enabledIndexes.size(); ++i) + if (realtimeVuBuffers == nullptr) { - int index = vuConfiguration->enabledIndexes[i]; - VuUpdate *pUpdate = &vuConfiguration->vuUpdateWorkingData[i]; - if (index == Pedalboard::INPUT_VOLUME_ID) + return; + } + for (size_t i = 0; i < realtimeVuBuffers->enabledIndexes.size(); ++i) + { + auto& rtIndex = realtimeVuBuffers->enabledIndexes[i]; + int index = rtIndex.index; + if (index == -1) continue; + if (index == Pedalboard::AUX_START_CONTROL_ID || index == Pedalboard::AUX_END_CONTROL_ID) { - if (this->pedalboardInputBuffers.size() > 1) + // handled by master VU updates. + continue; + } + VuUpdateX *pUpdate = &realtimeVuBuffers->vuUpdateWorkingData[i]; + if (index == Pedalboard::START_CONTROL_ID) + { + if (this->pedalboardInputBuffers.size() == 2) { - GetInputBuffers(); - pUpdate->AccumulateInputs(inputBuffers[0], inputBuffers[1], samples); + // input is handled by master VU updates. pUpdate->AccumulateOutputs(&(this->pedalboardInputBuffers[0][0]), &(this->pedalboardInputBuffers[1][0]), samples); // after outputVolume applied. } - else + else if (this->pedalboardInputBuffers.size() == 1) { - pUpdate->AccumulateInputs(inputBuffers[0], samples); - pUpdate->AccumulateOutputs(&(this->pedalboardInputBuffers[0][0]), samples); // after input volume applied. + pUpdate->AccumulateOutputs(&(this->pedalboardInputBuffers[0][0]), samples); // before input volume applied. + // output is handled by master VU updates. } } - else if (index == Pedalboard::OUTPUT_VOLUME_ID) + else if (index == Pedalboard::END_CONTROL_ID) { - if (this->pedalboardOutputBuffers.size() > 1) + if (this->pedalboardOutputBuffers.size() == 2) { pUpdate->AccumulateInputs(&(this->pedalboardOutputBuffers[0][0]), &(this->pedalboardOutputBuffers[1][0]), samples); - pUpdate->AccumulateOutputs(outputBuffers[0], outputBuffers[1], samples); } - else + else if (this->pedalboardOutputBuffers.size() == 1) { pUpdate->AccumulateInputs(&(this->pedalboardOutputBuffers[0][0]), samples); - pUpdate->AccumulateOutputs(outputBuffers[0], samples); } } else @@ -1016,3 +1031,12 @@ void Lv2Pedalboard::handleTapTempo(uint8_t value, const MidiTimestamp& timestamp mapping.lastTapTimestamp = timestamp; } } + + +size_t Lv2Pedalboard::GetNumberOfAudioInputChannels() const { + return pHost->GetChannelSelection().mainInputChannels().size(); +} + +size_t Lv2Pedalboard::GetNumberOfAudioOutputChannels() const { + return pHost->GetChannelSelection().mainOutputChannels().size(); +} \ No newline at end of file diff --git a/src/Lv2Pedalboard.hpp b/src/Lv2Pedalboard.hpp index c893824..400ee10 100644 --- a/src/Lv2Pedalboard.hpp +++ b/src/Lv2Pedalboard.hpp @@ -31,6 +31,7 @@ namespace pipedal { + class AudioDriver; class IPatchWriterCallback; class RealtimeVuBuffers; class RealtimePatchPropertyRequest; @@ -128,6 +129,9 @@ namespace pipedal std::vector &GetEffects() { return realtimeEffects; } std::vector> &GetSharedEffectList() { return effects; } + size_t GetNumberOfAudioInputChannels() const; + size_t GetNumberOfAudioOutputChannels() const; + int GetIndexOfInstanceId(uint64_t instanceId) { for (int i = 0; i < this->realtimeEffects.size(); ++i) @@ -169,7 +173,7 @@ namespace pipedal void SetOutputVolume(float value) { this->outputVolume.SetTarget(value); } void SetBypass(int effectIndex, bool enabled); - void ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples, float **inputBuffers, float **outputBuffers); + void ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples); float GetControlOutputValue(int effectIndex, int portIndex); diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index 97d7f0f..b5fb3cd 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -510,6 +510,11 @@ Snapshot Pedalboard::MakeSnapshotFromCurrentSettings(const Pedalboard &previousP } +Pedalboard::Pedalboard() +{ + nextInstanceId_ = 0; +} + JSON_MAP_BEGIN(ControlValue) JSON_MAP_REFERENCE(ControlValue,key) diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index 173b497..d1aa39c 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -25,13 +25,14 @@ #include "StateInterface.hpp" #include "atom_object.hpp" -namespace pipedal { +namespace pipedal +{ class SnapshotValue; class Snapshot; class PluginHost; - -#define SPLIT_PEDALBOARD_ITEM_URI "uri://two-play/pipedal/pedalboard#Split" -#define EMPTY_PEDALBOARD_ITEM_URI "uri://two-play/pipedal/pedalboard#Empty" + +#define SPLIT_PEDALBOARD_ITEM_URI "uri://two-play/pipedal/pedalboard#Split" +#define EMPTY_PEDALBOARD_ITEM_URI "uri://two-play/pipedal/pedalboard#Empty" #define SPLIT_SPLITTYPE_KEY "splitType" #define SPLIT_SELECT_KEY "select" @@ -41,238 +42,245 @@ namespace pipedal { #define SPLIT_PANR_KEY "panR" #define SPLIT_VOLR_KEY "volR" - -#define GETTER_SETTER_REF(name) \ - const decltype(name##_)& name() const { return name##_;} \ +#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(name##_)&&value) { this->name##_ = std::move(value); }\ - void name(const decltype(name##_)&value) { this->name##_ = value; } +#define GETTER_SETTER_VEC(name) \ + decltype(name##_) &name() { return name##_; } \ + const decltype(name##_) &name() const { return name##_; } \ + void name(decltype(name##_) &&value) { this->name##_ = std::move(value); } \ + void name(const decltype(name##_) &value) { this->name##_ = value; } - - -#define GETTER_SETTER(name) \ - decltype(name##_) name() const { return name##_;} \ +#define GETTER_SETTER(name) \ + decltype(name##_) name() const { return name##_; } \ void name(decltype(name##_) value) { name##_ = value; } -class ControlValue { -private: - std::string key_; - float value_; -public: - ControlValue() + class ControlValue { + private: + std::string key_; + float value_; - } - ControlValue(const char*key, float value) - :key_(key) - , value_(value) - { - - } - ControlValue(const std::string&key, float value) - :key_(key) - , value_(value) - { - - } - GETTER_SETTER_REF(key) - GETTER_SETTER_REF(value) - - DECLARE_JSON_MAP(ControlValue); - - -}; - -class PedalboardItem { -public: - using PropertyMap = std::map; - int64_t instanceId_ = 0; - std::string uri_; - std::string pluginName_; - bool isEnabled_ = true; - std::vector controlValues_; - std::vector topChain_; - std::vector bottomChain_; - std::vector midiBindings_; - std::optional midiChannelBinding_; - std::string vstState_; - uint32_t stateUpdateCount_ = 0; - Lv2PluginState lv2State_; - std::string lilvPresetUri_; - std::map pathProperties_; - std::string title_; - bool useModUi_ = false; - std::string iconColor_; - int64_t sideChainInputId_ = -1; - - // non persistent state. - PropertyMap patchProperties; -public: - ControlValue*GetControlValue(const std::string&symbol); - const ControlValue*GetControlValue(const std::string&symbol) const; - bool SetControlValue(const std::string&key, float value); - - bool IsStructurallyIdentical(const PedalboardItem&other) const; - - void ApplySnapshotValue(SnapshotValue*snapshotValue); - void ApplyDefaultValues(PluginHost&pluginHost); - bool hasLv2State() const { - return lv2State_.isValid_ != 0; - } - GETTER_SETTER(instanceId) - GETTER_SETTER_REF(uri) - GETTER_SETTER_REF(vstState); - GETTER_SETTER_REF(pluginName) - GETTER_SETTER(isEnabled) - GETTER_SETTER_VEC(controlValues) - GETTER_SETTER_VEC(topChain) - GETTER_SETTER_VEC(bottomChain) - GETTER_SETTER_VEC(midiBindings) - GETTER_SETTER_REF(midiChannelBinding) - GETTER_SETTER_REF(pathProperties) - GETTER_SETTER(stateUpdateCount) - GETTER_SETTER_REF(lv2State) - GETTER_SETTER_REF(title) - GETTER_SETTER_REF(iconColor) - GETTER_SETTER(useModUi) - GETTER_SETTER(sideChainInputId) - - Lv2PluginState&lv2State() { return lv2State_; } // non-const version. - GETTER_SETTER_REF(lilvPresetUri) - - PropertyMap&PatchProperties() { return patchProperties; } - const PropertyMap&PatchProperties() const { return patchProperties; } - - - bool isSplit() const - { - return uri_ == SPLIT_PEDALBOARD_ITEM_URI; - } - bool isEmpty() const { - return uri_ == EMPTY_PEDALBOARD_ITEM_URI; - } - - void AddToSnapshotFromCurrentSettings(Snapshot&snapshot) const; - void AddResetsForMissingProperties(Snapshot&snapshot, size_t*index) const; - - - // virtual void write_members(json_writer&writer) const { - // writer.write_member("instanceId",instanceId_); - // writer.write_member("uri",uri_); - // writer.write_member("pluginName",pluginName_); - // writer.write_member("isEnabled",isEnabled_); - // if (isSplit()) - // { - // writer.write_member("topChain",topChain_); - // writer.write_member("bottomChain",bottomChain_); - // } - // } - - DECLARE_JSON_MAP(PedalboardItem); - -}; - -class SnapshotValue { -public: - uint64_t instanceId_; - bool isEnabled_ = true; - std::vector controlValues_; - Lv2PluginState lv2State_; - std::map pathProperties_; - DECLARE_JSON_MAP(SnapshotValue); - - ControlValue*GetControlValue(const std::string& key) { - for (ControlValue&controlValue: controlValues_) { - if (controlValue.key() == key) { - return &controlValue; - } + public: + ControlValue() + { } - return nullptr; - } - void SetControlValue(const std::string& key,float value) { - for (ControlValue&controlValue: controlValues_) { - if (controlValue.key() == key) { - controlValue.value(value); - return; - } + ControlValue(const char *key, float value) + : key_(key), value_(value) + { } - controlValues_.push_back(ControlValue{key,value}); - } - -}; + ControlValue(const std::string &key, float value) + : key_(key), value_(value) + { + } + GETTER_SETTER_REF(key) + GETTER_SETTER_REF(value) -class Snapshot { -public: - std::string name_; - std::string color_; - bool isModified_ = false; - std::vector values_; + DECLARE_JSON_MAP(ControlValue); + }; - DECLARE_JSON_MAP(Snapshot); + class PedalboardItem + { + public: + using PropertyMap = std::map; + int64_t instanceId_ = 0; + std::string uri_; + std::string pluginName_; + bool isEnabled_ = true; + std::vector controlValues_; + std::vector topChain_; + std::vector bottomChain_; + std::vector midiBindings_; + std::optional midiChannelBinding_; + std::string vstState_; + uint32_t stateUpdateCount_ = 0; + Lv2PluginState lv2State_; + std::string lilvPresetUri_; + std::map pathProperties_; + std::string title_; + bool useModUi_ = false; + std::string iconColor_; + int64_t sideChainInputId_ = -1; -}; + // non persistent state. + PropertyMap patchProperties; -class Pedalboard { - std::string name_; - float input_volume_db_ = 0; - float output_volume_db_ = 0; + public: + ControlValue *GetControlValue(const std::string &symbol); + const ControlValue *GetControlValue(const std::string &symbol) const; + bool SetControlValue(const std::string &key, float value); - std::vector items_; - uint64_t nextInstanceId_ = 0; - uint64_t NextInstanceId() { return ++nextInstanceId_; } + bool IsStructurallyIdentical(const PedalboardItem &other) const; - std::vector> snapshots_; - int64_t selectedSnapshot_ = -1; + void ApplySnapshotValue(SnapshotValue *snapshotValue); + void ApplyDefaultValues(PluginHost &pluginHost); + bool hasLv2State() const + { + return lv2State_.isValid_ != 0; + } + GETTER_SETTER(instanceId) + GETTER_SETTER_REF(uri) + GETTER_SETTER_REF(vstState); + GETTER_SETTER_REF(pluginName) + GETTER_SETTER(isEnabled) + GETTER_SETTER_VEC(controlValues) + GETTER_SETTER_VEC(topChain) + GETTER_SETTER_VEC(bottomChain) + GETTER_SETTER_VEC(midiBindings) + GETTER_SETTER_REF(midiChannelBinding) + GETTER_SETTER_REF(pathProperties) + GETTER_SETTER(stateUpdateCount) + GETTER_SETTER_REF(lv2State) + GETTER_SETTER_REF(title) + GETTER_SETTER_REF(iconColor) + GETTER_SETTER(useModUi) + GETTER_SETTER(sideChainInputId) - int64_t selectedPlugin_ = -1; + Lv2PluginState &lv2State() { return lv2State_; } // non-const version. + GETTER_SETTER_REF(lilvPresetUri) -public: - // deep copy, breaking shared pointers. - Pedalboard DeepCopy(); - static constexpr int64_t INPUT_VOLUME_ID = -2; // synthetic PedalboardItem for input volume. - static constexpr int64_t OUTPUT_VOLUME_ID = -3; // synthetic PedalboardItem for output volume. - bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value); - bool SetItemTitle(int64_t pedalItemId, const std::string &title, const std::string&iconColor); - bool SetItemEnabled(int64_t pedalItemId, bool enabled); - bool SetItemUseModUi(int64_t pedalItemId, bool enabled); - void SetCurrentSnapshotModified(bool modified); + PropertyMap &PatchProperties() { return patchProperties; } + const PropertyMap &PatchProperties() const { return patchProperties; } - bool IsStructureIdentical(const Pedalboard &other) const; // caan we just send a snapshot-style uddate instead of reloading plugins? All settings are ignored. - Snapshot MakeSnapshotFromCurrentSettings(const Pedalboard &previousPedalboard); + bool isSplit() const + { + return uri_ == SPLIT_PEDALBOARD_ITEM_URI; + } + bool isEmpty() const + { + return uri_ == EMPTY_PEDALBOARD_ITEM_URI; + } - PedalboardItem*GetItem(int64_t pedalItemId); - const PedalboardItem*GetItem(int64_t pedalItemId) const; - std::vectorGetAllPlugins(); + void AddToSnapshotFromCurrentSettings(Snapshot &snapshot) const; + void AddResetsForMissingProperties(Snapshot &snapshot, size_t *index) const; - bool HasItem(int64_t pedalItemid) const { return GetItem(pedalItemid) != nullptr; } - bool ApplySnapshot(int64_t snapshotIndex, PluginHost &pluginHost); + // virtual void write_members(json_writer&writer) const { + // writer.write_member("instanceId",instanceId_); + // writer.write_member("uri",uri_); + // writer.write_member("pluginName",pluginName_); + // writer.write_member("isEnabled",isEnabled_); + // if (isSplit()) + // { + // writer.write_member("topChain",topChain_); + // writer.write_member("bottomChain",bottomChain_); + // } + // } - GETTER_SETTER_REF(name) - GETTER_SETTER_VEC(items) - GETTER_SETTER(input_volume_db) - GETTER_SETTER(output_volume_db) - GETTER_SETTER_VEC(snapshots) - GETTER_SETTER(selectedSnapshot) - GETTER_SETTER(selectedPlugin) + DECLARE_JSON_MAP(PedalboardItem); + }; + + class SnapshotValue + { + public: + uint64_t instanceId_; + bool isEnabled_ = true; + std::vector controlValues_; + Lv2PluginState lv2State_; + std::map pathProperties_; + DECLARE_JSON_MAP(SnapshotValue); + + ControlValue *GetControlValue(const std::string &key) + { + for (ControlValue &controlValue : controlValues_) + { + if (controlValue.key() == key) + { + return &controlValue; + } + } + return nullptr; + } + void SetControlValue(const std::string &key, float value) + { + for (ControlValue &controlValue : controlValues_) + { + if (controlValue.key() == key) + { + controlValue.value(value); + return; + } + } + controlValues_.push_back(ControlValue{key, value}); + } + }; + + class Snapshot + { + public: + std::string name_; + std::string color_; + bool isModified_ = false; + std::vector values_; + + DECLARE_JSON_MAP(Snapshot); + }; - DECLARE_JSON_MAP(Pedalboard); + class Pedalboard + { + std::string name_; + float input_volume_db_ = 0; + float output_volume_db_ = 0; - PedalboardItem MakeEmptyItem(); - PedalboardItem MakeSplit(); + std::vector items_; + uint64_t nextInstanceId_ = 0; + uint64_t NextInstanceId() { return ++nextInstanceId_; } + + std::vector> snapshots_; + int64_t selectedSnapshot_ = -1; + + int64_t selectedPlugin_ = -1; - static Pedalboard MakeDefault(); -}; + public: + + static constexpr int64_t START_CONTROL_ID = -2; // synthetic PedalboardItem for input volume. + static constexpr int64_t END_CONTROL_ID = -3; // synthetic PedalboardItem for output volume. + + static constexpr int64_t AUX_START_CONTROL_ID = -4; // synthetic PedalboardItem for aux input volume. + static constexpr int64_t AUX_END_CONTROL_ID = -5; // synthetic PedalboardItem for aux output volume. + + + Pedalboard(); + + // deep copy, breaking shared pointers. + Pedalboard DeepCopy(); + + bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value); + bool SetItemTitle(int64_t pedalItemId, const std::string &title, const std::string &iconColor); + bool SetItemEnabled(int64_t pedalItemId, bool enabled); + bool SetItemUseModUi(int64_t pedalItemId, bool enabled); + void SetCurrentSnapshotModified(bool modified); + + bool IsStructureIdentical(const Pedalboard &other) const; // caan we just send a snapshot-style uddate instead of reloading plugins? All settings are ignored. + Snapshot MakeSnapshotFromCurrentSettings(const Pedalboard &previousPedalboard); + + PedalboardItem *GetItem(int64_t pedalItemId); + const PedalboardItem *GetItem(int64_t pedalItemId) const; + std::vector GetAllPlugins(); + + bool HasItem(int64_t pedalItemid) const { return GetItem(pedalItemid) != nullptr; } + bool ApplySnapshot(int64_t snapshotIndex, PluginHost &pluginHost); + + GETTER_SETTER_REF(name) + GETTER_SETTER_VEC(items) + GETTER_SETTER(input_volume_db) + GETTER_SETTER(output_volume_db) + GETTER_SETTER_VEC(snapshots) + GETTER_SETTER(selectedSnapshot) + GETTER_SETTER(selectedPlugin) + + DECLARE_JSON_MAP(Pedalboard); + + PedalboardItem MakeEmptyItem(); + PedalboardItem MakeSplit(); + + static Pedalboard MakeDefault(); + }; #undef GETTER_SETTER_REF #undef GETTER_SETTER_VEC #undef GETTER_SETTER - - } // namespace pipedal \ No newline at end of file diff --git a/src/PiLatencyMain.cpp b/src/PiLatencyMain.cpp index 01887e5..1ad385a 100644 --- a/src/PiLatencyMain.cpp +++ b/src/PiLatencyMain.cpp @@ -34,6 +34,7 @@ #include #include #include +#include "ChannelRouterSettings.hpp" using namespace pipedal; @@ -102,8 +103,7 @@ void PrintHelp() void ListDevices() { - PiPedalAlsaDevices alsaDevices; - auto devices = alsaDevices.GetAlsaDevices(); + auto devices = PiPedalAlsaDevices::instance().GetAlsaDevices(); PrettyPrinter pp; if (devices.size() == 0) @@ -169,19 +169,30 @@ public: delete[] inputBuffers; delete[] outputBuffers; } - std::vector SelectChannels(const std::vector &available, const std::vector &selection) - { - if (selection.size() == 0) - return available; + virtual bool OnRealtimeUpdateDeviceVus(size_t nFrames) { + return true; + } + + std::vector SelectChannels(const std::vector &available, const std::vector &selection) + { + std::vector result; + if (selection.size() == 0) + { + result.resize(available.size()); + for (size_t i = 0; i < result.size(); ++i) + { + result[i] = (int64_t)i; + } + return result; + } - std::vector result; for (int sel : selection) { if (sel < 0 || sel >= available.size()) { throw PiPedalArgumentException(SS("Invalid channel: " + sel)); } - result.push_back(available[sel]); + result.push_back(sel); } return result; } @@ -191,7 +202,7 @@ public: TestResult result; try { - JackServerSettings serverSettings(inputDeviceId, outputDeviceId, sampleRate, bufferSize, buffers); + JackServerSettings serverSettings(inputDeviceId,outputDeviceId, sampleRate, bufferSize, buffers); JackConfiguration jackConfiguration; jackConfiguration.AlsaInitialize(serverSettings); @@ -199,22 +210,22 @@ public: auto &availableInputs = jackConfiguration.inputAudioPorts(); auto &availableOutputs = jackConfiguration.outputAudioPorts(); - std::vector inputAudioPorts, outputAudioPorts; + std::vector inputAudioPorts, outputAudioPorts; inputAudioPorts = SelectChannels(availableInputs, this->inputChannels); outputAudioPorts = SelectChannels(availableOutputs, this->outputChannels); - JackChannelSelection channelSelection( - inputAudioPorts, outputAudioPorts, - std::vector()); + ChannelSelection channelSelection; + channelSelection.mainInputChannels() = inputAudioPorts; + channelSelection.mainOutputChannels() = outputAudioPorts; audioDriver = CreateAlsaDriver(this); latencyMonitor.Init(jackConfiguration.sampleRate()); audioDriver->Open(serverSettings, channelSelection); - inputBuffers = new float *[channelSelection.GetInputAudioPorts().size()]; - outputBuffers = new float *[channelSelection.GetOutputAudioPorts().size()]; + inputBuffers = new float *[channelSelection.mainInputChannels().size()]; + outputBuffers = new float *[channelSelection.mainOutputChannels().size()]; audioDriver->Activate(); @@ -364,16 +375,16 @@ public: virtual void OnProcess(size_t nFrames) { - size_t inputs = audioDriver->InputBufferCount(); - size_t outputs = audioDriver->OutputBufferCount(); + size_t inputs = audioDriver->MainInputBufferCount(); + size_t outputs = audioDriver->MainOutputBufferCount(); for (size_t i = 0; i < inputs; ++i) { - inputBuffers[i] = audioDriver->GetInputBuffer(i); + inputBuffers[i] = audioDriver->GetMainInputBuffer(i); } for (size_t i = 0; i < outputs; ++i) { - outputBuffers[i] = audioDriver->GetOutputBuffer(i); + outputBuffers[i] = audioDriver->GetMainOutputBuffer(i); } for (size_t i = 0; i < nFrames; ++i) diff --git a/src/PiPedalAlsa.cpp b/src/PiPedalAlsa.cpp index 06be795..874148e 100644 --- a/src/PiPedalAlsa.cpp +++ b/src/PiPedalAlsa.cpp @@ -160,11 +160,14 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() std::vector result; - int cardNum = -1; // Start with first card int err; std::vector procAlsaDevices = getProcAlsaDevices(); + + snd_pcm_info_t *pcminfo = nullptr; + snd_pcm_info_alloca(&pcminfo); + for (const auto &procAlsaDevice: procAlsaDevices) { std::stringstream ss; @@ -198,13 +201,60 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() err = snd_ctl_card_info(hDevice, alsaInfo); if (err == 0) { + AlsaDeviceInfo info; - info.cardId_ = cardNum; + info.cardId_ = procAlsaDevice.cardId; info.id_ = std::string("hw:") + snd_ctl_card_info_get_id(alsaInfo); const char *driver = snd_ctl_card_info_get_driver(alsaInfo); (void)driver; - info.name_ = snd_ctl_card_info_get_name(alsaInfo); + snd_pcm_info_set_device(pcminfo, procAlsaDevice.subdeviceId); + snd_pcm_info_set_subdevice(pcminfo, 0); + snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_PLAYBACK); + + if ((err = snd_ctl_pcm_info(hDevice, pcminfo)) < 0) { + snd_pcm_info_set_device(pcminfo, procAlsaDevice.subdeviceId); + snd_pcm_info_set_subdevice(pcminfo, 0); + snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE); + + if ((err = snd_ctl_pcm_info(hDevice, pcminfo)) < 0) { + if (err != -ENOENT) + Lv2Log::warning("control digital audio info (%i): %s", info.cardId_, snd_strerror(err)); + continue; + } + } + /* + Names and IDs are f-ed up in ALSA. id seems to be vaguely reliable. names are garbage! + Sample data from Unbuntu 24.04. + + snd_ctl_card_info_get_id(info) snd_ctl_card_info_get_name(alsaInfo) subdevice snd_pcm_info_get_id(pcminfo) snd_pcm_info_get_name(pcminfo) + Generic HD-Audio Generic 3 HDMI 0 Acer S231HL + Generic HD-Audio Generic 3 HDMI 0 ASUS MG28U + Generic_1 HD-Audio Generic 0 CX11970 Analog CX11970 Analog + M2 M2 USB Audio USB Audio + CODEC USB AUDIO CODEC USB Audio USB Audio + + */ + std::string name; + { + // let's assume (without evidence) that names get localized, but ids do not. :-/ + std::string cardId = snd_ctl_card_info_get_id(alsaInfo); + std::string cardName = snd_ctl_card_info_get_name(alsaInfo); + std::string pcmId = snd_pcm_info_get_id(pcminfo); + std::string pcmName = snd_pcm_info_get_name(pcminfo); + std::string driver = snd_ctl_card_info_get_driver(alsaInfo); + if (driver == "USB-Audio") + { + name = SS("USB - " << cardId); + } + else if (pcmId == pcmName) + { + name = pcmId; + } else { + name = SS(pcmId << '[' << pcmName << ']'); + } + } + info.name_ = name; info.longName_ = snd_ctl_card_info_get_longname(alsaInfo); // we can't read our own device if it's open so use data that gets @@ -219,6 +269,8 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() result.push_back(cachedInfo); continue; } + + snd_pcm_t *captureDevice = nullptr; snd_pcm_t *playbackDevice = nullptr; auto rc = snd_pcm_open(&captureDevice, cardId.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK); @@ -248,6 +300,7 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() if (captureOk || playbackOk) { + snd_pcm_t *hDevice = captureOk ? captureDevice : playbackDevice; snd_pcm_hw_params_t *params = nullptr; err = snd_pcm_hw_params_malloc(¶ms); @@ -637,6 +690,17 @@ AlsaMidiDeviceInfo::AlsaMidiDeviceInfo(const char *name, const char *description this->subdevice_ = subdevice; } +std::unique_ptr PiPedalAlsaDevices::instance_; + +PiPedalAlsaDevices&PiPedalAlsaDevices::instance() { + if (!instance_) + { + instance_ = std::unique_ptr(new PiPedalAlsaDevices()); + } + return (*instance_); +} + + JSON_MAP_BEGIN(AlsaDeviceInfo) JSON_MAP_REFERENCE(AlsaDeviceInfo, cardId) JSON_MAP_REFERENCE(AlsaDeviceInfo, id) diff --git a/src/PiPedalAlsa.hpp b/src/PiPedalAlsa.hpp index e9061e0..5b58b77 100644 --- a/src/PiPedalAlsa.hpp +++ b/src/PiPedalAlsa.hpp @@ -71,12 +71,16 @@ namespace pipedal { }; class PiPedalAlsaDevices { - + private: + static std::unique_ptr instance_; + PiPedalAlsaDevices() { } std::map cachedDevices; bool getCachedDevice(const std::string&name, AlsaDeviceInfo*pResult); void cacheDevice(const std::string&name, const AlsaDeviceInfo&deviceInfo); + public: + static PiPedalAlsaDevices&instance(); std::vector GetAlsaDevices(); }; diff --git a/src/PiPedalAlsaTest.cpp b/src/PiPedalAlsaTest.cpp index aa36d68..c9944b0 100644 --- a/src/PiPedalAlsaTest.cpp +++ b/src/PiPedalAlsaTest.cpp @@ -34,7 +34,7 @@ using namespace std; static void DiscoveryTest() { cout << "--- Discovery" << endl; - PiPedalAlsaDevices devices; + PiPedalAlsaDevices &devices = PiPedalAlsaDevices::instance(); auto result = devices.GetAlsaDevices(); std::cout << result.size() << " ALSA devices found." << std::endl; diff --git a/src/PiPedalCommon.hpp b/src/PiPedalCommon.hpp new file mode 100644 index 0000000..3a8e138 --- /dev/null +++ b/src/PiPedalCommon.hpp @@ -0,0 +1,42 @@ +// Copyright (c) 2026 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. + + + +#pragma once + +#ifndef PIPEDAL_NON_INLINE +#if defined(__GNUC__) || defined(__clang__) +#define PIPEDAL_NON_INLINE __attribute__((noinline)) +#elif defined(_MSC_VER) +#define PIPEDAL_NON_INLINE __declspec(noinline) +#else +#define PIPEDAL_NON_INLINE +#endif +#endif + +#ifndef PIPEDAL_RESTRICT +#if defined(__GNUC__) || defined(__clang__) +#define PIPEDAL_RESTRICT __restrict__ +#elif defined(_MSC_VER) +#define PIPEDAL_RESTRICT __restrict +#else +#define PIPEDAL_RESTRICT +#endif +#endif diff --git a/src/PiPedalConfiguration.cpp b/src/PiPedalConfiguration.cpp index f333d95..6beaecf 100644 --- a/src/PiPedalConfiguration.cpp +++ b/src/PiPedalConfiguration.cpp @@ -101,6 +101,7 @@ JSON_MAP_REFERENCE(PiPedalConfiguration, threads) JSON_MAP_REFERENCE(PiPedalConfiguration, logLevel) JSON_MAP_REFERENCE(PiPedalConfiguration, logHttpRequests) JSON_MAP_REFERENCE(PiPedalConfiguration, maxUploadSize) +JSON_MAP_REFERENCE(PiPedalConfiguration, tone3000A2Models) JSON_MAP_REFERENCE(PiPedalConfiguration, accessPointGateway) JSON_MAP_REFERENCE(PiPedalConfiguration, accessPointServerAddress) JSON_MAP_REFERENCE(PiPedalConfiguration, isVst3Enabled) diff --git a/src/PiPedalConfiguration.hpp b/src/PiPedalConfiguration.hpp index 282c9f8..779f7e8 100644 --- a/src/PiPedalConfiguration.hpp +++ b/src/PiPedalConfiguration.hpp @@ -45,6 +45,7 @@ private: bool logHttpRequests_ = false; int logLevel_ = 0; uint64_t maxUploadSize_ = 1024*1024; + bool tone3000A2Models_ = true; std::string accessPointGateway_; std::string accessPointServerAddress_; bool isVst3Enabled_ = true; @@ -67,6 +68,7 @@ public: bool GetMLock() const { return mlock_; } uint64_t GetMaxUploadSize() const { return maxUploadSize_; } + bool GetTone3000A2Models() const { return tone3000A2Models_; } LogLevel GetLogLevel() const { return (LogLevel)this->logLevel_; } bool LogHttpRequests() const { return this->logHttpRequests_; } diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 7e9f4ff..76d59b5 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -46,6 +46,8 @@ #include "DummyAudioDriver.hpp" #include "AudioFiles.hpp" #include "CrashGuard.hpp" +#include "Tone3000Downloader.hpp" +#include "HtmlHelper.hpp" #ifndef NO_MLOCK #include @@ -79,15 +81,11 @@ PiPedalModel::PiPedalModel() atomConverter(pluginHost.GetMapFeature()) { this->updater = Updater::Create(); - this->updater->Start(); this->currentUpdateStatus = updater->GetCurrentStatus(); this->pedalboard = Pedalboard::MakeDefault(); -#if JACK_HOST - this->jackServerSettings = this->storage.GetJackServerSettings(); // to get onboarding flag. - this->jackServerSettings.ReadJackDaemonConfiguration(); -#else + this->jackServerSettings = this->storage.GetJackServerSettings(); -#endif + updater->SetUpdateListener( [this](const UpdateStatus &updateStatus) { @@ -135,10 +133,15 @@ void PiPedalModel::Close() std::unique_ptr oldAudioHost; { std::lock_guard lock(mutex); + if (closed) { return; } + if (tone3000Downloader) + { + tone3000Downloader->Close(); + } closed = true; CancelAudioRetry(); @@ -208,6 +211,10 @@ PiPedalModel::~PiPedalModel() void PiPedalModel::Init(const PiPedalConfiguration &configuration) { std::lock_guard lock(mutex); // prevent callbacks while we're initializing. + if (updaterEnabled) + { + this->updater->Start(); + } this->configuration = configuration; pluginHost.SetConfiguration(configuration); @@ -218,11 +225,122 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration) this->systemMidiBindings = storage.GetSystemMidiBindings(); -#if JACK_HOST - this->jackConfiguration = this->jackConfiguration.JackInitialize(); -#else this->jackServerSettings = storage.GetJackServerSettings(); -#endif + try + { + this->jackConfiguration.AlsaInitialize(jackServerSettings); + } + catch (const std::exception &) + { + // + } + + this->channelRouterSettings = storage.GetChannelRouterSettings(); + pluginHost.OnConfigurationChanged( + jackConfiguration, + storage.GetChannelSelection()); +} + +int64_t PiPedalModel::DownloadModelsFromTone3000( + const std::string &uri, + const Tone3000PkceParams &pckeParams, + const std::string &downloadPath, + Tone3000DownloadType downloadType) +{ + std::lock_guard lock(mutex); + + // Validate the download path + std::filesystem::path path{downloadPath}; + if (!IsInUploadsDirectory(path)) + { + throw PiPedalException("Invalid path: not in uploads directory."); + } + + // Check for ".." in path components (security check) + for (const auto &component : path) + { + if (component == "..") + { + throw PiPedalException("Invalid path: contains '..' component."); + } + } + + if (!tone3000Downloader) + { + tone3000Downloader = Tone3000Downloader::Create(); + tone3000Downloader->SetListener(this); + } + return tone3000Downloader->RequestTone3000Download( + uri, + pckeParams, + downloadPath, + downloadType); +} + +void PiPedalModel::CancelTone3000Download( + int64_t clientId, + int64_t downloadHandle) +{ + std::lock_guard lock(mutex); + + if (tone3000Downloader) + { + tone3000Downloader->CancelDownload(downloadHandle); + } +} + +// Tone3000Downloader::Listener implementation +void PiPedalModel::OnStartTone3000Download(int64_t handle, const std::string &title) +{ + + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } + for (auto &subscriber : this->subscribers) + { + subscriber->OnTone3000DownloadStarted(handle, title); + } +} + +void PiPedalModel::OnTone3000Progress(const Tone3000DownloadProgress &progress) +{ + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } + for (auto &subscriber : subscribers) + { + subscriber->OnTone3000DownloadProgress(progress); + } +} + +void PiPedalModel::OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) +{ + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } + for (auto subscriber : subscribers) + { + subscriber->OnTone3000DownloadComplete(handle, resultPath); + } +} + +void PiPedalModel::OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) +{ + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } + for (auto subscriber : subscribers) + { + subscriber->OnTone3000DownloadError(handle, errorMessage); + } } void PiPedalModel::LoadLv2PluginInfo() @@ -258,20 +376,21 @@ void PiPedalModel::LoadLv2PluginInfo() { PluginPresets pluginPresets = pluginHost.GetFactoryPluginPresets(plugin->uri()); storage.SavePluginPresets(plugin->uri(), pluginPresets); - } else { + } + else + { if (pluginPresetIndexVersion == 0) { if (plugin->uri() == "http://two-play.com/plugins/toob-convolution-reverb" || plugin->uri() == "http://two-play.com/plugins/toob-convolution-reverb-stereo") { // overwrite previous factory presets! PluginPresets pluginPresets = pluginHost.GetFactoryPluginPresets(plugin->uri()); - for (auto & pluginPreset: pluginPresets.presets_) + for (auto &pluginPreset : pluginPresets.presets_) { storage.SavePluginPreset( plugin->uri(), pluginPreset); } - } } } @@ -279,7 +398,6 @@ void PiPedalModel::LoadLv2PluginInfo() } storage.SetPluginPresetIndexVersion(1); } - void PiPedalModel::Load() { this->webRoot = configuration.GetWebRoot(); @@ -418,11 +536,10 @@ void PiPedalModel::OnNotifyLv2StateChanged(uint64_t instanceId) { // a sent PATCH_Set, or an explicit state changed notification. OnNotifyMaybeLv2StateChanged(instanceId); - this->SetPresetChanged(-1, true, false); } // The plugin notified us that a path path property changed. The state *purrobably changed. -void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId) +bool PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId) { // one or more received PATCH_Sets, which MAY change the state. std::lock_guard lock(mutex); @@ -431,7 +548,7 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId) { if (!audioHost) { - return; + return false; } bool changed = this->audioHost->UpdatePluginState(*item); if (changed) @@ -442,20 +559,26 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId) Lv2PluginState newState = item->lv2State(); FireLv2StateChanged(instanceId, newState); + this->SetPresetChanged(-1, true, false); + return true; } } + return false; } void PiPedalModel::SetInputVolume(float value) { PreviewInputVolume(value); { - std::lock_guard lock(mutex); + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; - this->pedalboard.input_volume_db(value); + this->pedalboard.input_volume_db(value); + } // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnInputVolumeChanged(value); } @@ -467,11 +590,14 @@ void PiPedalModel::SetOutputVolume(float value) { PreviewOutputVolume(value); { - std::lock_guard lock(mutex); - this->pedalboard.output_volume_db(value); - // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + + this->pedalboard.output_volume_db(value); + } + for (auto &subscriber : subscribers) { subscriber->OnOutputVolumeChanged(value); } @@ -490,8 +616,10 @@ void PiPedalModel::PreviewOutputVolume(float value) void PiPedalModel::SetControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) { + SubscriberList subscribers; { std::lock_guard lock(mutex); + subscribers = this->subscribers; if (!this->pedalboard.SetControlValue(pedalItemId, symbol, value)) { @@ -508,27 +636,28 @@ void PiPedalModel::SetControl(int64_t clientId, int64_t pedalItemId, const std:: return; } PreviewControl(clientId, pedalItemId, symbol, value); - - { - - // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnControlChanged(clientId, pedalItemId, symbol, value); - } - - this->SetPresetChanged(clientId, true); - } } + + for (auto &subscriber : subscribers) + { + subscriber->OnControlChanged(clientId, pedalItemId, symbol, value); + } + + this->SetPresetChanged(clientId, true); } void PiPedalModel::FireJackConfigurationChanged(const JackConfiguration &jackConfiguration) { + + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } + // noify subscribers. - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnJackConfigurationChanged(jackConfiguration); } @@ -536,9 +665,13 @@ void PiPedalModel::FireJackConfigurationChanged(const JackConfiguration &jackCon void PiPedalModel::FireBanksChanged(int64_t clientId) { + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } // noify subscribers. - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnBankIndexChanged(this->storage.GetBanks()); } @@ -546,20 +679,25 @@ void PiPedalModel::FireBanksChanged(int64_t clientId) void PiPedalModel::FirePedalboardChanged(int64_t clientId, bool loadAudioThread) { - if (loadAudioThread) + SubscriberList subscribers; { - // notify the audio thread. - if (audioHost && audioHost->IsOpen()) - { - LoadCurrentPedalboard(); + std::lock_guard lock(mutex); + subscribers = this->subscribers; - UpdateRealtimeVuSubscriptions(); - UpdateRealtimeMonitorPortSubscriptions(); + if (loadAudioThread) + { + // notify the audio thread. + if (audioHost && audioHost->IsOpen()) + { + LoadCurrentPedalboard(); + + UpdateRealtimeVuSubscriptions(); + UpdateRealtimeMonitorPortSubscriptions(); + } } } // noify subscribers. - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnPedalboardChanged(clientId, this->pedalboard); } @@ -570,45 +708,53 @@ void PiPedalModel::SetPedalboard(int64_t clientId, Pedalboard &pedalboard) std::lock_guard lock(mutex); this->pedalboard = pedalboard; UpdateDefaults(&this->pedalboard); - - this->FirePedalboardChanged(clientId); - this->SetPresetChanged(clientId, true); } + this->FirePedalboardChanged(clientId); + this->SetPresetChanged(clientId, true); } void PiPedalModel::SetSnapshot(int64_t selectedSnapshot) { - std::lock_guard lock(mutex); - if (this->pedalboard.ApplySnapshot(selectedSnapshot, pluginHost)) + bool pedalboardChanged = false; { - this->pedalboard.selectedSnapshot(selectedSnapshot); - for (auto snapshot : this->pedalboard.snapshots()) + std::lock_guard lock(mutex); + if (this->pedalboard.ApplySnapshot(selectedSnapshot, pluginHost)) { - if (snapshot) + this->pedalboard.selectedSnapshot(selectedSnapshot); + for (auto snapshot : this->pedalboard.snapshots()) { - snapshot->isModified_ = false; + if (snapshot) + { + snapshot->isModified_ = false; + } } + pedalboardChanged = true; } + } + if (pedalboardChanged) + { FirePedalboardChanged(-1, true); } } void PiPedalModel::SetSnapshots(std::vector> &snapshots, int64_t selectedSnapshot) { - std::lock_guard lock(mutex); - - UpdateVst3Settings(pedalboard); - - this->pedalboard.snapshots(std::move(snapshots)); - - if (selectedSnapshot != -1) { - this->pedalboard.selectedSnapshot(selectedSnapshot); - for (auto &snapshot : pedalboard.snapshots()) + std::lock_guard lock(mutex); + + UpdateVst3Settings(pedalboard); + + this->pedalboard.snapshots(std::move(snapshots)); + + if (selectedSnapshot != -1) { - if (snapshot) + this->pedalboard.selectedSnapshot(selectedSnapshot); + for (auto &snapshot : pedalboard.snapshots()) { - snapshot->isModified_ = false; + if (snapshot) + { + snapshot->isModified_ = false; + } } } } @@ -644,10 +790,9 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo UpdateRealtimeVuSubscriptions(); UpdateRealtimeMonitorPortSubscriptions(); - - this->FirePedalboardChanged(clientId, false); - this->SetPresetChanged(clientId, true); } + this->FirePedalboardChanged(clientId, false); + this->SetPresetChanged(clientId, true); } void PiPedalModel::SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceId, bool enabled) @@ -662,38 +807,43 @@ void PiPedalModel::SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceI { subscriber->OnItemUseModUiChanged(clientId, instanceId, enabled); } - this->SetPresetChanged(clientId, true); } + this->SetPresetChanged(clientId, true); } void PiPedalModel::SetPedalboardItemEnable(int64_t clientId, int64_t pedalItemId, bool enabled) { + SubscriberList subscribers; std::lock_guard guard{mutex}; { + subscribers = this->subscribers; + this->pedalboard.SetItemEnabled(pedalItemId, enabled); - PedalboardItem * pPedalboardItem = this->pedalboard.GetItem(pedalItemId); - if (pPedalboardItem) { + PedalboardItem *pPedalboardItem = this->pedalboard.GetItem(pedalItemId); + if (pPedalboardItem) + { Lv2PluginInfo::ptr pluginInfo = GetPluginInfo(pPedalboardItem->uri()); - if (pluginInfo) { - for (auto &port: pluginInfo->ports()) { - if (port->is_bypass()) { - pPedalboardItem->SetControlValue(port->symbol(), enabled? 1.0: 0.0f); + if (pluginInfo) + { + for (auto &port : pluginInfo->ports()) + { + if (port->is_bypass()) + { + pPedalboardItem->SetControlValue(port->symbol(), enabled ? 1.0 : 0.0f); } } - } + } } - - // Notify clients. - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnItemEnabledChanged(clientId, pedalItemId, enabled); - } - this->SetPresetChanged(clientId, true); - // Notify audo thread. - this->audioHost->SetBypass(pedalItemId, enabled); } + this->audioHost->SetBypass(pedalItemId, enabled); + + // Notify clients. + for (auto &subscriber : subscribers) + { + subscriber->OnItemEnabledChanged(clientId, pedalItemId, enabled); + } + this->SetPresetChanged(clientId, true); } void PiPedalModel::GetPresets(PresetIndex *pResult) @@ -740,72 +890,73 @@ void PiPedalModel::SetPresetChanged(int64_t clientId, bool value, bool changeSna void PiPedalModel::FireSnapshotModified(int64_t snapshotIndex, bool modified) { + SubscriberList subscribers; std::lock_guard guard{mutex}; { - // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnSnapshotModified(snapshotIndex, modified); - } + subscribers = this->subscribers; + } + for (auto &subscriber : subscribers) + { + subscriber->OnSnapshotModified(snapshotIndex, modified); } } void PiPedalModel::FireSelectedSnapshotChanged(int64_t selectedSnapshot) { + SubscriberList subscribers; std::lock_guard guard{mutex}; { + subscribers = this->subscribers; // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnSelectedSnapshotChanged(selectedSnapshot); - } + } + for (auto &subscriber : subscribers) + { + subscriber->OnSelectedSnapshotChanged(selectedSnapshot); } } void PiPedalModel::FirePresetChanged(bool changed) { + SubscriberList subscribers; + PresetIndex presets; std::lock_guard guard{mutex}; { + subscribers = this->subscribers; // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - PresetIndex presets; GetPresets(&presets); - - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnPresetChanged(changed); - } + } + for (auto &subscriber : subscribers) + { + subscriber->OnPresetChanged(changed); } } void PiPedalModel::FirePresetsChanged(int64_t clientId) { + SubscriberList subscribers; + PresetIndex presets; std::lock_guard guard{mutex}; { - - PresetIndex presets; + subscribers = this->subscribers; GetPresets(&presets); - - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnPresetsChanged(clientId, presets); - } + } + for (auto &subscriber : subscribers) + { + subscriber->OnPresetsChanged(clientId, presets); } } void PiPedalModel::FirePluginPresetsChanged(const std::string &pluginUri) { + SubscriberList subscribers; std::lock_guard guard{mutex}; { + subscribers = this->subscribers; // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnPluginPresetsChanged(pluginUri); - } + } + for (auto &subscriber : subscribers) + { + subscriber->OnPluginPresetsChanged(pluginUri); } } @@ -835,10 +986,13 @@ void PiPedalModel::UpdateVst3Settings(Pedalboard &pedalboard) void PiPedalModel::FireLv2StateChanged(int64_t instanceId, const Lv2PluginState &lv2State) { - std::lock_guard guard{mutex}; - std::vector t{subscribers.begin(), subscribers.end()}; + SubscriberList subscribers; + { + std::lock_guard guard{mutex}; + subscribers = this->subscribers; + } - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnLv2StateChanged(instanceId, lv2State); } @@ -898,7 +1052,7 @@ int64_t PiPedalModel::SavePluginPresetAs(int64_t instanceId, const std::string & return presetId; } -int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, int64_t bankInstanceId,const std::string &name, int64_t saveAfterInstanceId) +int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, int64_t bankInstanceId, const std::string &name, int64_t saveAfterInstanceId) { std::lock_guard guard{mutex}; @@ -1422,17 +1576,13 @@ void PiPedalModel::RestartAudio(bool useDummyAudioDriver) throw std::runtime_error("Audio configuration not valid."); } - auto channelSelection = this->storage.GetJackChannelSelection(jackConfiguration); + const auto &channelSelection = this->storage.GetChannelSelection(); - if (!channelSelection.isValid()) - { - throw std::runtime_error("Audio configuration not valid."); - } this->audioHost->Open(jackServerSettings, channelSelection); this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection); - FireChannelSelectionChanged(-1); + FireChannelRouterSettingsChanged(-1); LoadCurrentPedalboard(); this->UpdateRealtimeVuSubscriptions(); @@ -1528,33 +1678,17 @@ std::vector PiPedalModel::GetAlsaSequencerPorts() return result; } -void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection) -{ - { - std::lock_guard lock(mutex); // copy atomically. - this->storage.SetJackChannelSelection(channelSelection); - - this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection); - - CancelAudioRetry(); - } - - RestartAudio(); // no lock to avoid mutex deadlock when reader thread is sending notifications.. - - this->FireChannelSelectionChanged(clientId); -} - -void PiPedalModel::FireChannelSelectionChanged(int64_t clientId) +void PiPedalModel::FireChannelRouterSettingsChanged(int64_t clientId) { std::lock_guard guard{mutex}; { // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - JackChannelSelection channelSelection = storage.GetJackChannelSelection(this->jackConfiguration); + ChannelRouterSettings::ptr channelRouterSettings = this->channelRouterSettings; std::vector t{subscribers.begin(), subscribers.end()}; for (auto &subscriber : t) { - subscriber->OnChannelSelectionChanged(clientId, channelSelection); + subscriber->OnChannelRouterSettingsChanged(clientId, *channelRouterSettings); } } } @@ -1650,20 +1784,37 @@ void PiPedalModel::OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, f } } -void PiPedalModel::OnNotifyVusSubscription(const std::vector &updates) +void PiPedalModel::OnNotifyVusSubscription(const std::vector &updates) { - std::lock_guard lock(mutex); + std::vector subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } for (size_t i = 0; i < updates.size(); ++i) { // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnVuMeterUpdate(updates); } } } +static bool isStartOrEndControl(int64_t controlId) +{ + switch (controlId) + { + case Pedalboard::START_CONTROL_ID: + case Pedalboard::END_CONTROL_ID: + case Pedalboard::AUX_START_CONTROL_ID: + case Pedalboard::AUX_END_CONTROL_ID: + return true; + default: + return false; + } +} + void PiPedalModel::UpdateRealtimeVuSubscriptions() { std::set addedInstances; @@ -1671,7 +1822,7 @@ void PiPedalModel::UpdateRealtimeVuSubscriptions() for (int i = 0; i < activeVuSubscriptions.size(); ++i) { auto instanceId = activeVuSubscriptions[i].instanceid; - if (pedalboard.HasItem(instanceId) || instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) + if (pedalboard.HasItem(instanceId) || isStartOrEndControl(instanceId)) { addedInstances.insert(activeVuSubscriptions[i].instanceid); } @@ -1697,7 +1848,12 @@ int64_t PiPedalModel::MonitorPort(int64_t instanceId, const std::string &key, fl std::lock_guard lock(mutex); int64_t subscriptionId = ++nextSubscriptionId; activeMonitorPortSubscriptions.push_back( - MonitorPortSubscription{subscriptionId, instanceId, key, updateInterval, onUpdate}); + MonitorPortSubscription{ + subscriptionId, + instanceId, + key, + updateInterval, + onUpdate}); UpdateRealtimeMonitorPortSubscriptions(); @@ -1756,7 +1912,7 @@ void PiPedalModel::SendSetPatchProperty( { std::shared_ptr pluginInfo = GetPluginInfo(pedalboardItem->uri_); auto pipedalUi = pluginInfo->piPedalUI(); - if (pipedalUi) + if (pipedalUi) { auto fileProperty = pipedalUi->GetFileProperty(propertyUri); if (fileProperty) @@ -2047,8 +2203,7 @@ std::shared_ptr PiPedalModel::GetPluginInfo(const std::string &ur return pluginHost.GetPluginInfo(uri); } - -void PiPedalModel::UpdateDefaults(SnapshotValue&snapshotValue, const PedalboardItem*pedalboardItem_) +void PiPedalModel::UpdateDefaults(SnapshotValue &snapshotValue, const PedalboardItem *pedalboardItem_) { std::shared_ptr pPlugin = pluginHost.GetPluginInfo(pedalboardItem_->uri()); if (!pPlugin) @@ -2063,11 +2218,11 @@ void PiPedalModel::UpdateDefaults(SnapshotValue&snapshotValue, const PedalboardI // Fix incorrect bypass settings presint in Pipedal < 1.5.96 for (size_t i = 0; i < pPlugin->ports().size(); ++i) { - auto& port = pPlugin->ports()[i]; + auto &port = pPlugin->ports()[i]; if (port->is_bypass() && port->is_control_port() && port->is_input()) { - ControlValue* pValue = snapshotValue.GetControlValue(port->symbol()); - float value = snapshotValue.isEnabled_ ? 1.0f : 0.0f; + ControlValue *pValue = snapshotValue.GetControlValue(port->symbol()); + float value = snapshotValue.isEnabled_ ? 1.0f : 0.0f; if (pValue == nullptr) { @@ -2084,7 +2239,8 @@ void PiPedalModel::UpdateDefaults(SnapshotValue&snapshotValue, const PedalboardI if (pPlugin->uri() == "http://two-play.com/plugins/toob-nam") { ControlValue *pVersion = snapshotValue.GetControlValue("version"); - if (pVersion == nullptr) { + if (pVersion == nullptr) + { ControlValue *pValue = snapshotValue.GetControlValue("inputCalibrationMode"); if (pValue == nullptr) { @@ -2093,13 +2249,16 @@ void PiPedalModel::UpdateDefaults(SnapshotValue&snapshotValue, const PedalboardI } // convert old gate threshold to new gate threshold. ControlValue *pGateValue = snapshotValue.GetControlValue("gate"); - if (pGateValue) { + if (pGateValue) + { float value = pGateValue->value(); // Is the gate disabled? if (value <= -100.0f) { value = -120.0f; // "disabled in new range." - } else { + } + else + { value = value * 0.5; // correct the bug in original implementation. } snapshotValue.SetControlValue("gate", value); @@ -2163,7 +2322,8 @@ void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered if (pPlugin->uri() == "http://two-play.com/plugins/toob-nam") { ControlValue *pVersion = pedalboardItem->GetControlValue("version"); - if (pVersion == nullptr) { + if (pVersion == nullptr) + { ControlValue *pValue = pedalboardItem->GetControlValue("inputCalibrationMode"); if (pValue == nullptr) { @@ -2172,13 +2332,16 @@ void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered } // convert old gate threshold to new gate threshold. ControlValue *pGateValue = pedalboardItem->GetControlValue("gate"); - if (pGateValue) { + if (pGateValue) + { float value = pGateValue->value(); // Is the gate disabled? if (value <= -100.0f) { value = -120.0f; // "disabled in new range." - } else { + } + else + { value = value * 0.5; // correct the bug in original implementation. } pedalboardItem->SetControlValue("gate", value); @@ -2196,18 +2359,20 @@ void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered { // retroactively correct bug in version of PiPedal prior to 1.5.96. ControlValue *pValue = pedalboardItem->GetControlValue(port->symbol()); - float value = pedalboardItem->isEnabled() ? 1.0f: 0.0f; + float value = pedalboardItem->isEnabled() ? 1.0f : 0.0f; if (pValue == nullptr) { pedalboardItem->controlValues().push_back( pipedal::ControlValue(port->symbol().c_str(), value)); - } else { + } + else + { pValue->value(value); } - - - } else { + } + else + { ControlValue *pValue = pedalboardItem->GetControlValue(port->symbol()); if (pValue == nullptr) { @@ -2275,7 +2440,9 @@ void PiPedalModel::UpdateDefaults(Snapshot *snapshot, std::unordered_mapvalues_.erase(snapshot->values_.begin() + i); --i; - } else { + } + else + { UpdateDefaults(value, f->second); } } @@ -2305,7 +2472,7 @@ void PiPedalModel::UpdateDefaults(Pedalboard *pedalboard) PluginPresets PiPedalModel::GetPluginPresets(const std::string &pluginUri) { - std::lock_guard lock(mutex); + std::lock_guard lock(mutex); return storage.GetPluginPresets(pluginUri); } @@ -2398,28 +2565,38 @@ void PiPedalModel::DeleteMidiListeners(int64_t clientId) void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) { - std::lock_guard lock(mutex); - - std::string propertyUri = pluginHost.GetMapFeature().UridToString(patchSetProperty); - + std::vector subscribers; + std::vector atomOutputListeners; + std::string propertyUri; { - PedalboardItem *item = pedalboard.GetItem((int64_t)instanceId); - if (item == nullptr) - return; - atom_object atomObject{atomValue}; + std::lock_guard lock(mutex); - PedalboardItem::PropertyMap &properties = item->PatchProperties(); - if (properties.contains(propertyUri) && properties[propertyUri] == atomObject) - { - // do noting. - } - else - { - properties[propertyUri] = std::move(atomObject); - } + subscribers = this->subscribers; + atomOutputListeners = this->atomOutputListeners; + propertyUri = pluginHost.GetMapFeature().UridToString(patchSetProperty); - OnNotifyMaybeLv2StateChanged(instanceId); + { + PedalboardItem *item = pedalboard.GetItem((int64_t)instanceId); + if (item == nullptr) + return; + atom_object atomObject{atomValue}; + + PedalboardItem::PropertyMap &properties = item->PatchProperties(); + if (properties.contains(propertyUri) && properties[propertyUri] == atomObject) + { + // do noting. + } + else + { + properties[propertyUri] = std::move(atomObject); + } + } + if (audioHost) + { + audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); + } } + OnNotifyMaybeLv2StateChanged(instanceId); bool hasAtomJson = false; std::string atomJson; @@ -2446,10 +2623,6 @@ void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetPropert } } } - if (audioHost) - { - audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); - } } void PiPedalModel::OnNotifyPathPatchPropertyReceived( @@ -3289,44 +3462,76 @@ void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string this->FirePedalboardChanged(-1, false); } -void PiPedalModel::SetTone3000Auth(const std::string &apiKey) -{ - std::lock_guard lock(mutex); - storage.SetTone3000Auth(apiKey); - - std::vector t{subscribers.begin(), subscribers.end()}; - bool hasAuth = apiKey != ""; - for (auto &subscriber : t) - { - subscriber->OnTone3000AuthChanged(hasAuth); - } -} -bool PiPedalModel::HasTone3000Auth() const -{ - return storage.GetTone3000Auth() != ""; -} - std::vector PiPedalModel::RequestBankPresets(int64_t bankInstanceId) { std::lock_guard lock(mutex); return storage.RequestBankPresets(bankInstanceId); - } int64_t PiPedalModel::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets) { std::lock_guard lock(mutex); - uint64_t lastAdded = storage.ImportPresetsFromBank(bankInstanceId, presets); + uint64_t lastAdded = storage.ImportPresetsFromBank(bankInstanceId, presets); FirePresetsChanged(-1); return lastAdded; - } int64_t PiPedalModel::CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets) { std::lock_guard lock(mutex); - uint64_t lastAdded = storage.CopyPresetsToBank(bankInstanceId, presets); + uint64_t lastAdded = storage.CopyPresetsToBank(bankInstanceId, presets); return lastAdded; - +} + +ChannelRouterSettings::ptr PiPedalModel::GetChannelRouterSettings() +{ + std::lock_guard lock(mutex); + return this->channelRouterSettings; +} + +void PiPedalModel::SetChannelRouterSettings(int64_t clientId, ChannelRouterSettings::ptr &settings) +{ + { + std::lock_guard lock(mutex); + this->channelRouterSettings = settings; + this->storage.SetChannelRouterSettings(settings); + this->pluginHost.OnConfigurationChanged(jackConfiguration, *settings); + CancelAudioRetry(); + } + RestartAudio(); // no lock to avoid mutex deadlock when reader thread is sending notifications.. + + this->FireChannelRouterSettingsChanged(clientId); +} + +std::string PiPedalModel::Tone3000ThumbnailDirectory() +{ + return "/var/pipedal/tone3000_thumbnails"; +} + +void PiPedalModel::EnableUpdater(bool enable) +{ + this->updaterEnabled = enable; +} + +static bool IsSafeMediaPath(const fs::path path) +{ + if (!HtmlHelper::IsSafeFileName(path)) + { + return false; + } + if (!(path.string().starts_with("/var/pipedal/audio_uploads"))) + { + return false; + } + return true; +} + +void PiPedalModel::WriteTone3000Readme(const std::filesystem::path &filePath, const tone3000::Tone &tone, const std::string &thumbnailUrl) +{ + if (!IsSafeMediaPath(filePath)) + { + throw std::runtime_error("Invalid media path."); + } + tone3000::WriteTone3000Readme(filePath, tone, thumbnailUrl); } diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 897f524..2e14f5f 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -40,7 +40,11 @@ #include "Promise.hpp" #include "AtomConverter.hpp" #include "FileEntry.hpp" +#include "ChannelRouterSettings.hpp" #include +#include "Tone3000Downloader.hpp" +#include "Uri.hpp" +#include "Tone3000Tone.hpp" namespace pipedal { @@ -56,7 +60,7 @@ namespace pipedal { public: using ptr = std::shared_ptr; - + virtual int64_t GetClientId() = 0; virtual void OnItemEnabledChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0; virtual void OnItemUseModUiChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0; @@ -72,8 +76,8 @@ namespace pipedal virtual void OnSnapshotModified(int64_t selectedSnapshot, bool modified) = 0; virtual void OnSelectedSnapshotChanged(int64_t selectedSnapshot) = 0; virtual void OnPluginPresetsChanged(const std::string &pluginUri) = 0; - virtual void OnChannelSelectionChanged(int64_t clientId, const JackChannelSelection &channelSelection) = 0; - virtual void OnVuMeterUpdate(const std::vector &updates) = 0; + virtual void OnChannelRouterSettingsChanged(int64_t clientId, const ChannelRouterSettings &channelRouterSettings) = 0; + virtual void OnVuMeterUpdate(const std::vector &updates) = 0; virtual void OnBankIndexChanged(const BankIndex &bankIndex) = 0; virtual void OnJackServerSettingsChanged(const JackServerSettings &jackServerSettings) = 0; virtual void OnJackConfigurationChanged(const JackConfiguration &jackServerConfiguration) = 0; @@ -91,19 +95,22 @@ namespace pipedal // virtual void OnPatchPropertyChanged(int64_t clientId, int64_t instanceId,const std::string& propertyUri,const json_variant& value) = 0; virtual void OnErrorMessage(const std::string &message) = 0; + + virtual void OnTone3000DownloadStarted(int64_t handle, const std::string &title) = 0; + virtual void OnTone3000DownloadProgress(const Tone3000DownloadProgress &progress) = 0; + virtual void OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) = 0; + virtual void OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) = 0; virtual void OnLv2PluginsChanging() = 0; virtual void OnNetworkChanging(bool hotspotConnected) = 0; virtual void OnHasWifiChanged(bool hasWifi) = 0; virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; virtual void Close() = 0; - virtual void OnTone3000AuthChanged(bool value) = 0; - }; class HotspotManager; - class PiPedalModel : private IAudioHostCallbacks + class PiPedalModel : private IAudioHostCallbacks, private Tone3000Downloader::Listener { public: using clock = std::chrono::steady_clock; @@ -113,8 +120,16 @@ namespace pipedal using NetworkChangedListener = std::function; private: - PedalboardItem* GetPedalboardItemForFileProperty(const UiFileProperty& fileProperty); + bool updaterEnabled = true; + PedalboardItem *GetPedalboardItemForFileProperty(const UiFileProperty &fileProperty); + // Tone3000Downloader::Listener implementation + virtual void OnStartTone3000Download(int64_t handle, const std::string &title) override; + virtual void OnTone3000Progress(const Tone3000DownloadProgress &progress) override; + virtual void OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) override; + virtual void OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) override; + + std::shared_ptr tone3000Downloader; void CancelAudioRetry(); clock::time_point lastRestartTime = clock::time_point::min(); int audioRestartRetries = 0; @@ -139,7 +154,7 @@ namespace pipedal std::unique_ptr avahiService; uint16_t webPort; - PiPedalAlsaDevices alsaDevices; + PiPedalAlsaDevices &alsaDevices = PiPedalAlsaDevices::instance(); std::recursive_mutex mutex; AdminClient adminClient; @@ -176,6 +191,8 @@ namespace pipedal AtomConverter atomConverter; // must be AFTER pluginHost! Pedalboard pedalboard; + ChannelRouterSettings::ptr channelRouterSettings; + bool previousPedalboardLoaded = false; Pedalboard previousPedalboard; Storage storage; @@ -195,7 +212,8 @@ namespace pipedal std::shared_ptr lv2Pedalboard; std::filesystem::path webRoot; - std::vector> subscribers; + using SubscriberList = std::vector>; + SubscriberList subscribers; void SetPresetChanged(int64_t clientId, bool value, bool changeSnapshotSelect = true); void FireSnapshotModified(int64_t snapshotIndex, bool modified); void FireSelectedSnapshotChanged(int64_t selectedSnapshot); @@ -203,11 +221,11 @@ namespace pipedal void FirePresetChanged(bool changed); void FirePluginPresetsChanged(const std::string &pluginUri); void FirePedalboardChanged(int64_t clientId, bool reloadAudioThread = true); - void FireChannelSelectionChanged(int64_t clientId); + void FireChannelRouterSettingsChanged(int64_t clientId); void FireBanksChanged(int64_t clientId); void FireJackConfigurationChanged(const JackConfiguration &jackConfiguration); void FireLv2StateChanged(int64_t instanceId, const Lv2PluginState &lv2State); - void UpdateDefaults(SnapshotValue&snapshotValue, const PedalboardItem*pedalboardItem); + void UpdateDefaults(SnapshotValue &snapshotValue, const PedalboardItem *pedalboardItem); void UpdateDefaults(Snapshot *snapshot, std::unordered_map &itemMap); void UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered_map &itemMap); void UpdateDefaults(Pedalboard *pedalboard); @@ -232,16 +250,17 @@ namespace pipedal IPiPedalModelSubscriber *GetNotificationSubscriber(int64_t clientId); std::atomic closed = false; bool SyncLv2State(); + private: // IAudioHostCallbacks virtual void OnNotifyLv2StateChanged(uint64_t instanceId) override; - virtual void OnNotifyMaybeLv2StateChanged(uint64_t instanceId) override; - virtual void OnNotifyVusSubscription(const std::vector &updates) override; + virtual bool OnNotifyMaybeLv2StateChanged(uint64_t instanceId) override; + virtual void OnNotifyVusSubscription(const std::vector &updates) override; virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) override; virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) override; virtual void OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) override; virtual void OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) override; virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override; - virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override; + virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex, int64_t snapshotRequestId) override; virtual void OnAlsaDriverTerminatedAbnormally() override; virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) override; virtual void OnAlsaSequencerDeviceRemoved(int client) override; @@ -267,8 +286,7 @@ namespace pipedal PiPedalConfiguration configuration; void CheckForResourceInitialization(Pedalboard &pedalboard); - UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri); - + UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId, const std::string &patchPropertyUri); public: PiPedalModel(); @@ -280,7 +298,12 @@ namespace pipedal Decrease }; - Storage&GetStorage() { return storage; } + void EnableUpdater(bool enable); + + Storage &GetStorage() { return storage; } + + virtual std::string Tone3000ThumbnailDirectory() override; + bool GetHasWifi(); void NextBank(Direction direction = Direction::Increase); @@ -288,6 +311,15 @@ namespace pipedal void NextPreset(Direction direction = Direction::Increase); void PreviousPreset() { NextPreset(Direction::Decrease); } + int64_t DownloadModelsFromTone3000( + const std::string &responseuri, + const Tone3000PkceParams &pkce, + const std::string &downloadPath, + Tone3000DownloadType downloadType); + + void CancelTone3000Download( + int64_t clientId, + int64_t downloadHandle); void RequestShutdown(bool restart); virtual PostHandle Post(PostCallback &&fn); @@ -323,7 +355,7 @@ namespace pipedal void SetRestartListener(std::function &&listener); void OnLv2PluginsChanged(); void SetOnboarding(bool value); - std::map GetWifiRegulatoryDomains(); + std::map GetWifiRegulatoryDomains(); void UpdateDnsSd(); @@ -331,12 +363,14 @@ namespace pipedal void Init(const PiPedalConfiguration &configuration); + const PiPedalConfiguration &Configuration() const { return configuration; } + void LoadLv2PluginInfo(); void Load(); const PluginHost &GetPluginHost() const { return pluginHost; } PluginHost &GetPluginHost() { return pluginHost; } - + Pedalboard GetCurrentPedalboardCopy() { std::lock_guard guard(mutex); @@ -381,7 +415,6 @@ namespace pipedal int64_t SaveCurrentPresetAs(int64_t clientId, int64_t bankInstanceId, const std::string &name, int64_t saveAfterInstanceId = -1); int64_t SavePluginPresetAs(int64_t instanceId, const std::string &name); - void LoadPreset(int64_t clientId, int64_t instanceId); bool UpdatePresets(int64_t clientId, const PresetIndex &presets); void UpdatePluginPresets(const PluginUiPresets &pluginPresets); @@ -397,7 +430,6 @@ namespace pipedal JackConfiguration GetJackConfiguration(); - void SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection); JackChannelSelection GetJackChannelSelection(); void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration); @@ -472,8 +504,8 @@ namespace pipedal void DeleteSampleFile(const std::filesystem::path &fileName); std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty); std::string RenameFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty); - std::string CopyFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty,bool overwrite); - FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath); + std::string CopyFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty, bool overwrite); + FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty, const std::string &selectedPath); bool IsInUploadsDirectory(const std::string &path); @@ -483,20 +515,21 @@ namespace pipedal bool LoadCurrentPedalboard(); void MoveAudioFile( - const std::string & path, - int32_t from, + const std::string &path, + int32_t from, int32_t to); - void SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string&iconColor); - - void SetTone3000Auth(const std::string &apiKey); - bool HasTone3000Auth() const; + void SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string &iconColor); void SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId); std::vector RequestBankPresets(int64_t bankInstanceId); int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets); int64_t CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets); + + void SetChannelRouterSettings(int64_t clientId, ChannelRouterSettings::ptr &settings); + ChannelRouterSettings::ptr GetChannelRouterSettings(); + void WriteTone3000Readme(const std::filesystem::path &filePath, const tone3000::Tone &tone, const std::string &thumbnailUrl); }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index a3bf46b..cc6df5a 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022-2023 Robin Davies +// Copyright (c) 2026 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 @@ -19,11 +19,13 @@ #include "pch.h" +#include "Curl.hpp" #include "PiPedalSocket.hpp" #include "Updater.hpp" #include "json.hpp" #include "viewstream.hpp" #include "PiPedalVersion.hpp" +#include "Tone3000Downloader.hpp" #include #include #include "Lv2Log.hpp" @@ -33,6 +35,8 @@ #include "Ipv6Helpers.hpp" #include "Promise.hpp" #include +#include "Tone3000Downloader.hpp" +#include "Tone3000Tone.hpp" #include "AdminClient.hpp" #include "WifiConfigSettings.hpp" @@ -42,12 +46,14 @@ #include "PiPedalAlsa.hpp" #include #include "FileEntry.hpp" +#include "Tone3000Downloader.hpp" using namespace std; using namespace pipedal; -class CopyPresetsToBankBody { - public: +class CopyPresetsToBankBody +{ +public: int64_t bankInstanceId_; std::vector presets_; DECLARE_JSON_MAP(CopyPresetsToBankBody); @@ -55,11 +61,46 @@ class CopyPresetsToBankBody { JSON_MAP_BEGIN(CopyPresetsToBankBody) JSON_MAP_REFERENCE(CopyPresetsToBankBody, bankInstanceId) JSON_MAP_REFERENCE(CopyPresetsToBankBody, presets) -JSON_MAP_END() +JSON_MAP_END(); +class WriteTone3000ReadmeBody { +public: + std::string filePath_; + tone3000::Tone tone_; + std::string thumbnailUrl_; + DECLARE_JSON_MAP(WriteTone3000ReadmeBody); +}; -class ImportPresetsFromBankBody { - public: +JSON_MAP_BEGIN(WriteTone3000ReadmeBody) +JSON_MAP_REFERENCE(WriteTone3000ReadmeBody, filePath) +JSON_MAP_REFERENCE(WriteTone3000ReadmeBody, tone) +JSON_MAP_REFERENCE(WriteTone3000ReadmeBody, thumbnailUrl) +JSON_MAP_END(); + +class DownloadModelsFromTone3000Body +{ +public: + std::string responseUri_; + Tone3000PkceParams tone3000PckceParams_; + std::string downloadPath_; + int64_t downloadType_ = -1; + + Tone3000DownloadType downloadType() const { return (Tone3000DownloadType)downloadType_; } + void downloadType(Tone3000DownloadType value) { downloadType_ = (int64_t)value; } + + DECLARE_JSON_MAP(DownloadModelsFromTone3000Body); +}; + +JSON_MAP_BEGIN(DownloadModelsFromTone3000Body) +JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, responseUri) +JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, tone3000PckceParams) +JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, downloadPath) +JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, downloadType) +JSON_MAP_END(); + +class ImportPresetsFromBankBody +{ +public: int64_t bankInstanceId_; std::vector presets_; DECLARE_JSON_MAP(ImportPresetsFromBankBody); @@ -69,7 +110,8 @@ JSON_MAP_REFERENCE(ImportPresetsFromBankBody, bankInstanceId) JSON_MAP_REFERENCE(ImportPresetsFromBankBody, presets) JSON_MAP_END() -class RequestBankPresetsBody { +class RequestBankPresetsBody +{ public: int64_t bankInstanceId_; DECLARE_JSON_MAP(RequestBankPresetsBody); @@ -78,6 +120,34 @@ JSON_MAP_BEGIN(RequestBankPresetsBody) JSON_MAP_REFERENCE(RequestBankPresetsBody, bankInstanceId) JSON_MAP_END() +class Tone3000DownloadStartedBody +{ +public: + int64_t handle_; + std::string title_; + + DECLARE_JSON_MAP(Tone3000DownloadStartedBody); +}; + +JSON_MAP_BEGIN(Tone3000DownloadStartedBody) +JSON_MAP_REFERENCE(Tone3000DownloadStartedBody, handle) +JSON_MAP_REFERENCE(Tone3000DownloadStartedBody, title) +JSON_MAP_END() + +class Tone3000DownloadErrorBody +{ +public: + int64_t handle_; + std::string errorMessage_; + + DECLARE_JSON_MAP(Tone3000DownloadErrorBody); +}; + +JSON_MAP_BEGIN(Tone3000DownloadErrorBody) +JSON_MAP_REFERENCE(Tone3000DownloadErrorBody, handle) +JSON_MAP_REFERENCE(Tone3000DownloadErrorBody, errorMessage) +JSON_MAP_END() + class PathPatchPropertyChangedBody { public: @@ -176,7 +246,7 @@ public: JSON_MAP_BEGIN(GetFilePropertyDirectoryTreeArgs) JSON_MAP_REFERENCE(GetFilePropertyDirectoryTreeArgs, fileProperty) JSON_MAP_REFERENCE(GetFilePropertyDirectoryTreeArgs, selectedPath) -JSON_MAP_END() +JSON_MAP_END(); class Lv2StateChangedBody { @@ -189,7 +259,7 @@ public: JSON_MAP_BEGIN(Lv2StateChangedBody) JSON_MAP_REFERENCE(Lv2StateChangedBody, instanceId) JSON_MAP_REFERENCE(Lv2StateChangedBody, state) -JSON_MAP_END() +JSON_MAP_END(); class SetPatchPropertyBody { @@ -221,9 +291,6 @@ JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, title) JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, colorKey) JSON_MAP_END() - - - class NotifyMidiListenerBody { public: @@ -255,7 +322,7 @@ JSON_MAP_REFERENCE(NotifyAtomOutputBody, clientHandle) JSON_MAP_REFERENCE(NotifyAtomOutputBody, instanceId) JSON_MAP_REFERENCE(NotifyAtomOutputBody, propertyUri) JSON_MAP_REFERENCE(NotifyAtomOutputBody, atomJson) -JSON_MAP_END() +JSON_MAP_END(); class ListenForMidiEventBody { @@ -379,7 +446,7 @@ JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, clientId) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, bankInstanceId) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, name) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, saveAfterInstanceId) -JSON_MAP_END() +JSON_MAP_END(); class SavePluginPresetAsBody { @@ -392,7 +459,7 @@ public: JSON_MAP_BEGIN(SavePluginPresetAsBody) JSON_MAP_REFERENCE(SavePluginPresetAsBody, instanceId) JSON_MAP_REFERENCE(SavePluginPresetAsBody, name) -JSON_MAP_END() +JSON_MAP_END(); class RenameBankBody { @@ -404,7 +471,7 @@ public: JSON_MAP_BEGIN(RenameBankBody) JSON_MAP_REFERENCE(RenameBankBody, bankId) JSON_MAP_REFERENCE(RenameBankBody, newName) -JSON_MAP_END() +JSON_MAP_END(); class RenamePresetBody { @@ -479,7 +546,6 @@ JSON_MAP_REFERENCE(PedalboardItemUseModGuiBody, instanceId) JSON_MAP_REFERENCE(PedalboardItemUseModGuiBody, useModUi) JSON_MAP_END() - class UpdateCurrentPedalboardBody { public: @@ -522,7 +588,6 @@ JSON_MAP_REFERENCE(SetSelectedPedalboardPluginBody, clientId) JSON_MAP_REFERENCE(SetSelectedPedalboardPluginBody, pluginInstanceId) JSON_MAP_END() - class SnapshotModifiedBody { public: @@ -537,17 +602,22 @@ JSON_MAP_REFERENCE(SnapshotModifiedBody, snapshotIndex) JSON_MAP_REFERENCE(SnapshotModifiedBody, modified) JSON_MAP_END() -class ChannelSelectionChangedBody +class ChannelRouterSettingsChangedBody { public: + ChannelRouterSettingsChangedBody( + int64_t clientId, + const ChannelRouterSettings &channelRouterSettings) : clientId_(clientId), channelRouterSettings_(channelRouterSettings) + { + } int64_t clientId_ = -1; - JackChannelSelection *jackChannelSelection_ = nullptr; + ChannelRouterSettings channelRouterSettings_; - DECLARE_JSON_MAP(ChannelSelectionChangedBody); + DECLARE_JSON_MAP(ChannelRouterSettingsChangedBody); }; -JSON_MAP_BEGIN(ChannelSelectionChangedBody) -JSON_MAP_REFERENCE(ChannelSelectionChangedBody, clientId) -JSON_MAP_REFERENCE(ChannelSelectionChangedBody, jackChannelSelection) +JSON_MAP_BEGIN(ChannelRouterSettingsChangedBody) +JSON_MAP_REFERENCE(ChannelRouterSettingsChangedBody, clientId) +JSON_MAP_REFERENCE(ChannelRouterSettingsChangedBody, channelRouterSettings) JSON_MAP_END() class PresetsChangedBody @@ -618,6 +688,15 @@ JSON_MAP_REFERENCE(Vst3ControlChangedBody, value) JSON_MAP_REFERENCE(Vst3ControlChangedBody, state) JSON_MAP_END() +class PiPedalSocketHandler; +namespace +{ + using PfnMessageHandler = void (PiPedalSocketHandler::*)(int replyTo, json_reader *pReader); + + inline static unordered_map socket_messageHandlers; + +} + class PiPedalSocketHandler : public SocketHandler, public IPiPedalModelSubscriber, public std::enable_shared_from_this { private: @@ -639,6 +718,8 @@ private: int64_t subscriptionHandle; int64_t instanceId; }; + + bool PingTone3000Server(); std::vector activeVuSubscriptions; struct PortMonitorSubscription @@ -1095,6 +1176,972 @@ public: /***********************/ + class MessageRegistration + { + public: + MessageRegistration(const std::string &messageName, PfnMessageHandler pfnMessageHandler) + { + socket_messageHandlers[messageName] = pfnMessageHandler; + } + }; + +#define REGISTER_MESSAGE_HANDLER(MESSAGE_NAME) \ + static inline MessageRegistration r_##MESSAGE_NAME{#MESSAGE_NAME, &PiPedalSocketHandler::handle_##MESSAGE_NAME}; + + void handle_setControl(int replyTo, json_reader *pReader) + { + ControlChangedBody message; + pReader->read(&message); + this->model.SetControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); + } + REGISTER_MESSAGE_HANDLER(setControl) + + + void handle_makeTone3000Pkce(int replyTo, json_reader *pReader) + { + std::string redirectUrl; + pReader->read(&redirectUrl); + + Tone3000PkceParams result {redirectUrl}; + this->Reply(replyTo, "makeTone3000Pkce", result); + + } + REGISTER_MESSAGE_HANDLER(makeTone3000Pkce); + + + void handle_writeTone3000Readme(int replyTo, json_reader*pReader) + { + WriteTone3000ReadmeBody body; + pReader->read(&body); + model.WriteTone3000Readme(body.filePath_, body.tone_,body.thumbnailUrl_); + } + REGISTER_MESSAGE_HANDLER(writeTone3000Readme) + + void handle_sha256Base64url(int replyTo, json_reader *pReader) + { + std::string input; + pReader->read(&input); + + std::string result = Sha256Base64Url(input); + this->Reply(replyTo, "sha256Base64url", result); + + } + REGISTER_MESSAGE_HANDLER(sha256Base64url); + + void handle_previewControl(int replyTo, json_reader *pReader) + { + ControlChangedBody message; + pReader->read(&message); + this->model.PreviewControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); + } + REGISTER_MESSAGE_HANDLER(previewControl) + + void handle_setInputVolume(int replyTo, json_reader *pReader) + { + float value; + pReader->read(&value); + this->model.SetInputVolume(value); + } + REGISTER_MESSAGE_HANDLER(setInputVolume) + + void handle_setOutputVolume(int replyTo, json_reader *pReader) + { + float value; + pReader->read(&value); + this->model.SetOutputVolume(value); + } + REGISTER_MESSAGE_HANDLER(setOutputVolume) + + void handle_previewInputVolume(int replyTo, json_reader *pReader) + { + float value; + pReader->read(&value); + this->model.PreviewInputVolume(value); + } + REGISTER_MESSAGE_HANDLER(previewInputVolume) + + void handle_previewOutputVolume(int replyTo, json_reader *pReader) + { + float value; + pReader->read(&value); + this->model.PreviewOutputVolume(value); + } + REGISTER_MESSAGE_HANDLER(previewOutputVolume) + + void handle_listenForMidiEvent(int replyTo, json_reader *pReader) + { + ListenForMidiEventBody body; + pReader->read(&body); + this->model.ListenForMidiEvent(this->clientId, body.handle_); + } + REGISTER_MESSAGE_HANDLER(listenForMidiEvent) + + void handle_cancelListenForMidiEvent(int replyTo, json_reader *pReader) + { + uint64_t handle; + pReader->read(&handle); + this->model.CancelListenForMidiEvent(this->clientId, handle); + } + REGISTER_MESSAGE_HANDLER(cancelListenForMidiEvent) + + void handle_monitorPatchProperty(int replyTo, json_reader *pReader) + { + MonitorPatchPropertyBody body; + pReader->read(&body); + this->model.MonitorPatchProperty(this->clientId, body.clientHandle_, body.instanceId_, body.propertyUri_); + } + REGISTER_MESSAGE_HANDLER(monitorPatchProperty) + + void handle_cancelMonitorPatchProperty(int replyTo, json_reader *pReader) + { + int64_t handle; + pReader->read(&handle); + this->model.CancelMonitorPatchProperty(this->clientId, handle); + } + REGISTER_MESSAGE_HANDLER(cancelMonitorPatchProperty) + + void handle_getUpdateStatus(int replyTo, json_reader *pReader) + { + UpdateStatus updateStatus = model.GetUpdateStatus(); + this->Reply(replyTo, "getUpdateStatus", updateStatus); + } + REGISTER_MESSAGE_HANDLER(getUpdateStatus) + + void handle_getHasWifi(int replyTo, json_reader *pReader) + { + bool result = model.GetHasWifi(); + this->Reply(replyTo, "getHasWifi", result); + } + REGISTER_MESSAGE_HANDLER(getHasWifi) + + void handle_updateNow(int replyTo, json_reader *pReader) + { + std::string updateUrl; + pReader->read(&updateUrl); + model.UpdateNow(updateUrl); + bool result = true; + this->Reply(replyTo, "updateNow", result); + } + REGISTER_MESSAGE_HANDLER(updateNow) + + void handle_getJackStatus(int replyTo, json_reader *pReader) + { + JackHostStatus status = model.GetJackStatus(); + this->Reply(replyTo, "getJackStatus", status); + } + REGISTER_MESSAGE_HANDLER(getJackStatus) + + void handle_getAlsaDevices(int replyTo, json_reader *pReader) + { + std::vector devices = model.GetAlsaDevices(); + this->Reply(replyTo, "getAlsaDevices", devices); + } + REGISTER_MESSAGE_HANDLER(getAlsaDevices) + + void handle_getKnownWifiNetworks(int replyTo, json_reader *pReader) + { + std::vector channels = this->model.GetKnownWifiNetworks(); + this->Reply(replyTo, "getWifiChannels", channels); + } + REGISTER_MESSAGE_HANDLER(getKnownWifiNetworks) + + void handle_getWifiChannels(int replyTo, json_reader *pReader) + { + std::string country; + pReader->read(&country); + std::vector channels = pipedal::getWifiChannelSelectors(country.c_str()); + this->Reply(replyTo, "getWifiChannels", channels); + } + REGISTER_MESSAGE_HANDLER(getWifiChannels) + + void handle_getPluginPresets(int replyTo, json_reader *pReader) + { + std::string uri; + pReader->read(&uri); + this->Reply(replyTo, "getPluginPresets", this->model.GetPluginUiPresets(uri)); + } + REGISTER_MESSAGE_HANDLER(getPluginPresets) + + void handle_loadPluginPreset(int replyTo, json_reader *pReader) + { + LoadPluginPresetBody body; + pReader->read(&body); + this->model.LoadPluginPreset(body.pluginInstanceId_, body.presetInstanceId_); + } + REGISTER_MESSAGE_HANDLER(loadPluginPreset) + + void handle_setJackServerSettings(int replyTo, json_reader *pReader) + { + JackServerSettings jackServerSettings; + pReader->read(&jackServerSettings); + this->model.SetJackServerSettings(jackServerSettings); + this->Reply(replyTo, "setJackserverSettings"); + } + REGISTER_MESSAGE_HANDLER(setJackServerSettings) + + void handle_setGovernorSettings(int replyTo, json_reader *pReader) + { + std::string governor; + pReader->read(&governor); + std::string fromAddress = this->getFromAddress(); + // if (!IsOnLocalSubnet(fromAddress)) + // { + // throw PiPedalException("Permission denied. Not on local subnet."); + // } + this->model.SetGovernorSettings(governor); + this->Reply(replyTo, "setGovernorSettings"); + } + REGISTER_MESSAGE_HANDLER(setGovernorSettings) + + void handle_setWifiConfigSettings(int replyTo, json_reader *pReader) + { + WifiConfigSettings wifiConfigSettings; + pReader->read(&wifiConfigSettings); + if (!GetAdminClient().CanUseAdminClient()) + { + throw PiPedalException("Can't change server settings when running interactively."); + } + std::string fromAddress = this->getFromAddress(); + // if (!IsOnLocalSubnet(fromAddress)) + // { + // throw PiPedalException("Permission denied. Not on local subnet."); + // } + + this->model.SetWifiConfigSettings(wifiConfigSettings); + this->Reply(replyTo, "setWifiConfigSettings"); + } + REGISTER_MESSAGE_HANDLER(setWifiConfigSettings) + + void handle_getWifiConfigSettings(int replyTo, json_reader *pReader) + { + this->Reply(replyTo, "getWifiConfigSettings", model.GetWifiConfigSettings()); + } + REGISTER_MESSAGE_HANDLER(getWifiConfigSettings) + + void handle_setWifiDirectConfigSettings(int replyTo, json_reader *pReader) + { + WifiDirectConfigSettings wifiDirectConfigSettings; + pReader->read(&wifiDirectConfigSettings); + if (!GetAdminClient().CanUseAdminClient()) + { + throw PiPedalException("Can't change server settings when running interactively."); + } + std::string fromAddress = this->getFromAddress(); + // if (!IsOnLocalSubnet(fromAddress)) + // { + // throw PiPedalException("Permission denied. Not on local subnet."); + // } + + this->model.SetWifiDirectConfigSettings(wifiDirectConfigSettings); + this->Reply(replyTo, "setWifiDirectConfigSettings"); + } + REGISTER_MESSAGE_HANDLER(setWifiDirectConfigSettings) + + void handle_getWifiDirectConfigSettings(int replyTo, json_reader *pReader) + { + this->Reply(replyTo, "getWifiDirectConfigSettings", model.GetWifiDirectConfigSettings()); + } + REGISTER_MESSAGE_HANDLER(getWifiDirectConfigSettings) + + void handle_getGovernorSettings(int replyTo, json_reader *pReader) + { + this->Reply(replyTo, "getGovernorSettings", model.GetGovernorSettings()); + } + REGISTER_MESSAGE_HANDLER(getGovernorSettings) + + void handle_getJackServerSettings(int replyTo, json_reader *pReader) + { + this->Reply(replyTo, "getJackServerSettings", model.GetJackServerSettings()); + } + REGISTER_MESSAGE_HANDLER(getJackServerSettings) + + void handle_getBankIndex(int replyTo, json_reader *pReader) + { + BankIndex bankIndex = model.GetBankIndex(); + this->Reply(replyTo, "getBankIndex", bankIndex); + } + REGISTER_MESSAGE_HANDLER(getBankIndex) + + void handle_getJackConfiguration(int replyTo, json_reader *pReader) + { + JackConfiguration configuration = this->model.GetJackConfiguration(); + this->Reply(replyTo, "getJackConfiguration", configuration); + } + REGISTER_MESSAGE_HANDLER(getJackConfiguration) + + void handle_getJackSettings(int replyTo, json_reader *pReader) + { + JackChannelSelection selection = this->model.GetJackChannelSelection(); + this->Reply(replyTo, "getJackSettings", selection); + } + REGISTER_MESSAGE_HANDLER(getJackSettings) + + void handle_saveCurrentPreset(int replyTo, json_reader *pReader) + { + this->model.SaveCurrentPreset(this->clientId); + } + REGISTER_MESSAGE_HANDLER(saveCurrentPreset) + + void handle_saveCurrentPresetAs(int replyTo, json_reader *pReader) + { + SaveCurrentPresetAsBody body; + pReader->read(&body); + int64_t result = this->model.SaveCurrentPresetAs(this->clientId, body.bankInstanceId_, body.name_, body.saveAfterInstanceId_); + Reply(replyTo, "saveCurrentPresetsAs", result); + } + REGISTER_MESSAGE_HANDLER(saveCurrentPresetAs) + + void handle_setSelectedPedalboardPlugin(int replyTo, json_reader *pReader) + { + SetSelectedPedalboardPluginBody body; + pReader->read(&body); + this->model.SetSelectedPedalboardPlugin(body.clientId_, body.pluginInstanceId_); + } + REGISTER_MESSAGE_HANDLER(setSelectedPedalboardPlugin) + + void handle_savePluginPresetAs(int replyTo, json_reader *pReader) + { + SavePluginPresetAsBody body; + pReader->read(&body); + int64_t result = this->model.SavePluginPresetAs(body.instanceId_, body.name_); + Reply(replyTo, "saveCurrentPresetsAs", result); + } + REGISTER_MESSAGE_HANDLER(savePluginPresetAs) + + void handle_getPresets(int replyTo, json_reader *pReader) + { + PresetIndex presets; + this->model.GetPresets(&presets); + Reply(replyTo, "getPresets", presets); + } + REGISTER_MESSAGE_HANDLER(getPresets) + + void handle_setPedalboardItemEnable(int replyTo, json_reader *pReader) + { + PedalboardItemEnabledBody body; + pReader->read(&body); + model.SetPedalboardItemEnable(body.clientId_, body.instanceId_, body.enabled_); + } + REGISTER_MESSAGE_HANDLER(setPedalboardItemEnable) + + void handle_setPedalboardItemUseModUi(int replyTo, json_reader *pReader) + { + PedalboardItemUseModGuiBody body; + pReader->read(&body); + model.SetPedalboardItemUseModUi(body.clientId_, body.instanceId_, body.useModUi_); + } + REGISTER_MESSAGE_HANDLER(setPedalboardItemUseModUi) + + void handle_updateCurrentPedalboard(int replyTo, json_reader *pReader) + { + UpdateCurrentPedalboardBody body; + + pReader->read(&body); + this->model.UpdateCurrentPedalboard(body.clientId_, body.pedalboard_); + } + REGISTER_MESSAGE_HANDLER(updateCurrentPedalboard) + + void handle_setSnapshot(int replyTo, json_reader *pReader) + { + int64_t snapshotIndex = -1; + pReader->read(&snapshotIndex); + this->model.SetSnapshot(snapshotIndex); + } + REGISTER_MESSAGE_HANDLER(setSnapshot) + + void handle_setSnapshots(int replyTo, json_reader *pReader) + { + SetSnapshotsBody body; + pReader->read(&body); + this->model.SetSnapshots(body.snapshots_, body.selectedSnapshot_); + } + REGISTER_MESSAGE_HANDLER(setSnapshots) + + void handle_currentPedalboard(int replyTo, json_reader *pReader) + { + auto pedalboard = model.GetCurrentPedalboardCopy(); + Reply(replyTo, "currentPedalboard", pedalboard); + } + REGISTER_MESSAGE_HANDLER(currentPedalboard) + + void handle_plugins(int replyTo, json_reader *pReader) + { + auto ui_plugins = model.GetPluginHost().GetUiPlugins(); + Reply(replyTo, "plugins", ui_plugins); + } + REGISTER_MESSAGE_HANDLER(plugins) + + void handle_pluginClasses(int replyTo, json_reader *pReader) + { + auto classes = model.GetPluginHost().GetLv2PluginClass(); + Reply(replyTo, "pluginClasses", classes); + } + REGISTER_MESSAGE_HANDLER(pluginClasses) + + void handle_hello(int replyTo, json_reader *pReader) + { + this->model.AddNotificationSubscription(shared_from_this()); + Reply(replyTo, "ehlo", clientId); + } + REGISTER_MESSAGE_HANDLER(hello) + + void handle_setShowStatusMonitor(int replyTo, json_reader *pReader) + { + bool showStatusMonitor; + pReader->read(&showStatusMonitor); + this->model.SetShowStatusMonitor(showStatusMonitor); + } + REGISTER_MESSAGE_HANDLER(setShowStatusMonitor) + + void handle_getShowStatusMonitor(int replyTo, json_reader *pReader) + { + Reply(replyTo, "getShowStatusMonitor", this->model.GetShowStatusMonitor()); + } + REGISTER_MESSAGE_HANDLER(getShowStatusMonitor) + + void handle_version(int replyTo, json_reader *pReader) + { + PiPedalVersion version(this->model); + + Reply(replyTo, "version", version); + } + REGISTER_MESSAGE_HANDLER(version) + + void handle_loadPreset(int replyTo, json_reader *pReader) + { + int64_t instanceId = 0; + pReader->read(&instanceId); + model.LoadPreset(this->clientId, instanceId); + } + REGISTER_MESSAGE_HANDLER(loadPreset) + + void handle_updatePresets(int replyTo, json_reader *pReader) + { + PresetIndex newIndex; + pReader->read(&newIndex); + bool result = model.UpdatePresets(this->clientId, newIndex); + this->Reply(replyTo, "updatePresets", result); + } + REGISTER_MESSAGE_HANDLER(updatePresets) + + void handle_updatePluginPresets(int replyTo, json_reader *pReader) + { + PluginUiPresets pluginPresets; + pReader->read(&pluginPresets); + model.UpdatePluginPresets(pluginPresets); + this->Reply(replyTo, "updatePluginPresets", true); + } + REGISTER_MESSAGE_HANDLER(updatePluginPresets) + + void handle_moveBank(int replyTo, json_reader *pReader) + { + FromToBody body; + pReader->read(&body); + model.MoveBank(this->clientId, body.from_, body.to_); + this->Reply(replyTo, "moveBank"); + } + REGISTER_MESSAGE_HANDLER(moveBank) + + void handle_shutdown(int replyTo, json_reader *pReader) + { + model.RequestShutdown(false); + this->Reply(replyTo, "shutdown"); + } + REGISTER_MESSAGE_HANDLER(shutdown) + + void handle_restart(int replyTo, json_reader *pReader) + { + model.RequestShutdown(true); + this->Reply(replyTo, "restart"); + } + REGISTER_MESSAGE_HANDLER(restart) + + void handle_deletePresetItems(int replyTo, json_reader *pReader) + { + std::vector items; + pReader->read(&items); + int64_t result = model.DeletePresets(this->clientId, items); + this->Reply(replyTo, "deletePresetItems", result); + } + REGISTER_MESSAGE_HANDLER(deletePresetItems) + + void handle_deleteBankItem(int replyTo, json_reader *pReader) + { + int64_t instanceId = 0; + pReader->read(&instanceId); + uint64_t result = model.DeleteBank(this->clientId, instanceId); + this->Reply(replyTo, "deleteBankItem", result); + } + REGISTER_MESSAGE_HANDLER(deleteBankItem) + + void handle_renameBank(int replyTo, json_reader *pReader) + { + RenameBankBody body; + pReader->read(&body); + + std::stringstream tOut; + json_writer tWriter(tOut); + tWriter.write(body.newName_); + std::string tJson = tOut.str(); + std::stringstream tIn(tJson); + json_reader tReader(tIn); + std::string tResult; + tReader.read(&tResult); + + body.newName_ = tResult; + + try + { + model.RenameBank(this->clientId, body.bankId_, body.newName_); + this->Reply(replyTo, "renameBank"); + } + catch (const std::exception &e) + { + this->SendError(replyTo, std::string(e.what())); + } + } + REGISTER_MESSAGE_HANDLER(renameBank) + + void handle_openBank(int replyTo, json_reader *pReader) + { + int64_t bankId = -1; + pReader->read(&bankId); + try + { + model.OpenBank(this->clientId, bankId); + ; + this->Reply(replyTo, "openBank"); + } + catch (const std::exception &e) + { + this->SendError(replyTo, std::string(e.what())); + } + } + REGISTER_MESSAGE_HANDLER(openBank) + + void handle_saveBankAs(int replyTo, json_reader *pReader) + { + RenameBankBody body; + pReader->read(&body); + try + { + int64_t newId = model.SaveBankAs(this->clientId, body.bankId_, body.newName_); + this->Reply(replyTo, "saveBankAs", newId); + } + catch (const std::exception &e) + { + this->SendError(replyTo, std::string(e.what())); + } + } + REGISTER_MESSAGE_HANDLER(saveBankAs) + + void handle_nextBank(int replyTo, json_reader *pReader) + { + model.NextBank(); + } + REGISTER_MESSAGE_HANDLER(nextBank) + + void handle_previousBank(int replyTo, json_reader *pReader) + { + model.PreviousBank(); + } + REGISTER_MESSAGE_HANDLER(previousBank) + + void handle_nextPreset(int replyTo, json_reader *pReader) + { + model.NextPreset(); + } + REGISTER_MESSAGE_HANDLER(nextPreset) + + void handle_previousPreset(int replyTo, json_reader *pReader) + { + model.PreviousPreset(); + } + REGISTER_MESSAGE_HANDLER(previousPreset) + + void handle_renamePresetItem(int replyTo, json_reader *pReader) + { + RenamePresetBody body; + pReader->read(&body); + + bool result = model.RenamePreset(body.clientId_, body.instanceId_, body.name_); + this->Reply(replyTo, "renamePresetItem", result); + } + REGISTER_MESSAGE_HANDLER(renamePresetItem) + + void handle_copyPreset(int replyTo, json_reader *pReader) + { + CopyPresetBody body; + pReader->read(&body); + int64_t result = model.CopyPreset(body.clientId_, body.fromId_, body.toId_); + this->Reply(replyTo, "copyPreset", result); + } + REGISTER_MESSAGE_HANDLER(copyPreset) + + void handle_copyPluginPreset(int replyTo, json_reader *pReader) + { + CopyPluginPresetBody body; + pReader->read(&body); + uint64_t result = model.CopyPluginPreset(body.pluginUri_, body.instanceId_); + this->Reply(replyTo, "copyPluginPreset", result); + } + REGISTER_MESSAGE_HANDLER(copyPluginPreset) + + void handle_setPatchProperty(int replyTo, json_reader *pReader) + { + SetPatchPropertyBody body; + pReader->read(&body); + model.SendSetPatchProperty(clientId, body.instanceId_, body.propertyUri_, body.value_, [this, replyTo]() + { this->JsonReply(replyTo, "setPatchProperty", "true"); }, [this, replyTo](const std::string &error) + { this->SendError(replyTo, error.c_str()); }); + } + REGISTER_MESSAGE_HANDLER(setPatchProperty) + + void handle_setPedalboardItemTitle(int replyTo, json_reader *pReader) + { + SetPedalboardItemTitleBody body; + pReader->read(&body); + model.SetPedalboardItemTitle(body.instanceId_, body.title_, body.colorKey_); + } + REGISTER_MESSAGE_HANDLER(setPedalboardItemTitle) + + void handle_getPatchProperty(int replyTo, json_reader *pReader) + { + GetPatchPropertyBody body; + pReader->read(&body); + + model.SendGetPatchProperty( + this->clientId, + body.instanceId_, + body.propertyUri_, + [this, replyTo](const std::string &jsonResult) + { + this->JsonReply(replyTo, "getPatchProperty", jsonResult.c_str()); + }, + [this, replyTo](const std::string &error) + { + this->SendError(replyTo, error.c_str()); + }); + } + REGISTER_MESSAGE_HANDLER(getPatchProperty) + + void handle_monitorPort(int replyTo, json_reader *pReader) + { + MonitorPortBody body; + pReader->read(&body); + + MonitorPort(replyTo, body); + } + REGISTER_MESSAGE_HANDLER(monitorPort) + + void handle_unmonitorPort(int replyTo, json_reader *pReader) + { + int64_t subscriptionHandle; + pReader->read(&subscriptionHandle); + { + { + std::lock_guard guard(activePortMonitorsMutex); + for (auto i = this->activePortMonitors.begin(); i != this->activePortMonitors.end(); ++i) + { + if ((*i)->subscriptionHandle == subscriptionHandle) + { + auto subscription = (*i); + subscription->Close(); + + this->activePortMonitors.erase(i); + break; + } + } + } + model.UnmonitorPort(subscriptionHandle); + } + } + REGISTER_MESSAGE_HANDLER(unmonitorPort) + + void handle_addVuSubscription(int replyTo, json_reader *pReader) + { + int64_t instanceId = -1; + + pReader->read(&instanceId); + + int64_t subscriptionHandle = model.AddVuSubscription(instanceId); + + { + std::lock_guard guard(subscriptionMutex); + activeVuSubscriptions.push_back(VuSubscription{subscriptionHandle, instanceId}); + } + this->Reply(replyTo, "addVuSubscription", subscriptionHandle); + } + REGISTER_MESSAGE_HANDLER(addVuSubscription) + + void handle_removeVuSubscription(int replyTo, json_reader *pReader) + { + int64_t subscriptionHandle = -1; + pReader->read(&subscriptionHandle); + { + std::lock_guard guard(subscriptionMutex); + + for (auto i = activeVuSubscriptions.begin(); i != activeVuSubscriptions.end(); ++i) + { + if (i->subscriptionHandle == subscriptionHandle) + { + activeVuSubscriptions.erase(i); + break; + } + } + } + model.RemoveVuSubscription(subscriptionHandle); + } + REGISTER_MESSAGE_HANDLER(removeVuSubscription) + + void handle_imageList(int replyTo, json_reader *pReader) + { + this->Reply(replyTo, "imageList", imageList); + } + REGISTER_MESSAGE_HANDLER(imageList) + + void handle_getFavorites(int replyTo, json_reader *pReader) + { + std::map favorites = this->model.GetFavorites(); + this->Reply(replyTo, "getFavorites", favorites); + } + REGISTER_MESSAGE_HANDLER(getFavorites) + + void handle_setFavorites(int replyTo, json_reader *pReader) + { + std::map favorites; + pReader->read(&favorites); + this->model.SetFavorites(favorites); + } + REGISTER_MESSAGE_HANDLER(setFavorites) + + void handle_setUpdatePolicy(int replyTo, json_reader *pReader) + { + int iPolicy; + pReader->read(&iPolicy); + + this->model.SetUpdatePolicy((UpdatePolicyT)iPolicy); + } + REGISTER_MESSAGE_HANDLER(setUpdatePolicy) + + void handle_forceUpdateCheck(int replyTo, json_reader *pReader) + { + this->model.ForceUpdateCheck(); + } + REGISTER_MESSAGE_HANDLER(forceUpdateCheck) + + void handle_setSystemMidiBindings(int replyTo, json_reader *pReader) + { + std::vector bindings; + pReader->read(&bindings); + this->model.SetSystemMidiBindings(bindings); + } + REGISTER_MESSAGE_HANDLER(setSystemMidiBindings) + + void handle_getSystemMidiBindings(int replyTo, json_reader *pReader) + { + std::vector bindings = this->model.GetSystemMidiBidings(); + this->Reply(replyTo, "getSystemMidiBindings", bindings); + } + REGISTER_MESSAGE_HANDLER(getSystemMidiBindings) + + void handle_requestFileList(int replyTo, json_reader *pReader) + { + throw std::runtime_error("No longer implemented."); + } + REGISTER_MESSAGE_HANDLER(requestFileList) + + void handle_requestFileList2(int replyTo, json_reader *pReader) + { + FileRequestArgs requestArgs; + pReader->read(&requestArgs); + FileRequestResult result = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_); + this->Reply(replyTo, "requestFileList2", result); + } + REGISTER_MESSAGE_HANDLER(requestFileList2) + + void handle_newPreset(int replyTo, json_reader *pReader) + { + int64_t presetId = this->model.CreateNewPreset(); + this->Reply(replyTo, "newPreset", presetId); + } + REGISTER_MESSAGE_HANDLER(newPreset) + + void handle_deleteUserFile(int replyTo, json_reader *pReader) + { + std::string fileName; + pReader->read(&fileName); + + this->model.DeleteSampleFile(fileName); + this->Reply(replyTo, "deleteUserFile", true); + } + REGISTER_MESSAGE_HANDLER(deleteUserFile) + + void handle_createNewSampleDirectory(int replyTo, json_reader *pReader) + { + CreateNewSampleDirectoryArgs args; + pReader->read(&args); + + std::string newFileName = this->model.CreateNewSampleDirectory(args.relativePath_, args.uiFileProperty_); + this->Reply(replyTo, "createNewSampleDirectory", newFileName); + } + REGISTER_MESSAGE_HANDLER(createNewSampleDirectory) + + void handle_renameFilePropertyFile(int replyTo, json_reader *pReader) + { + RenameSampleFileArgs args; + pReader->read(&args); + + std::string newFileName = this->model.RenameFilePropertyFile(args.oldRelativePath_, args.newRelativePath_, args.uiFileProperty_); + this->Reply(replyTo, "renameFilePropertyFile", newFileName); + } + REGISTER_MESSAGE_HANDLER(renameFilePropertyFile) + + void handle_copyFilePropertyFile(int replyTo, json_reader *pReader) + { + CopySampleFileArgs args; + pReader->read(&args); + + std::string newFileName = this->model.CopyFilePropertyFile(args.oldRelativePath_, args.newRelativePath_, args.uiFileProperty_, args.overwrite_); + this->Reply(replyTo, "copyFilePropertyFile", newFileName); + } + REGISTER_MESSAGE_HANDLER(copyFilePropertyFile) + + void handle_getFilePropertyDirectoryTree(int replyTo, json_reader *pReader) + { + GetFilePropertyDirectoryTreeArgs args; + pReader->read(&args); + FilePropertyDirectoryTree::ptr result = + model.GetFilePropertydirectoryTree( + args.fileProperty_, + args.selectedPath_); + this->Reply(replyTo, "GetFilePropertydirectoryTree", result); + } + REGISTER_MESSAGE_HANDLER(getFilePropertyDirectoryTree) + + void handle_moveAudioFile(int replyTo, json_reader *pReader) + { + MoveAudioFileArgs args; + pReader->read(&args); + this->model.MoveAudioFile(args.path_, args.from_, args.to_); + bool result = true; + this->Reply(replyTo, "moveAudioFile", result); + } + REGISTER_MESSAGE_HANDLER(moveAudioFile) + + void handle_setOnboarding(int replyTo, json_reader *pReader) + { + bool value; + pReader->read(&value); + this->model.SetOnboarding(value); + } + REGISTER_MESSAGE_HANDLER(setOnboarding) + + void handle_getWifiRegulatoryDomains(int replyTo, json_reader *pReader) + { + auto regulatoryDomains = this->model.GetWifiRegulatoryDomains(); + this->Reply(replyTo, "getWifiRegulatoryDomains", regulatoryDomains); + } + REGISTER_MESSAGE_HANDLER(getWifiRegulatoryDomains) + + void handle_setAlsaSequencerConfiguration(int replyTo, json_reader *pReader) + { + AlsaSequencerConfiguration config; + pReader->read(&config); + this->model.SetAlsaSequencerConfiguration(config); + this->Reply(replyTo, "setAlsaSequencerConfiguration"); + } + REGISTER_MESSAGE_HANDLER(setAlsaSequencerConfiguration) + + void handle_getAlsaSequencerConfiguration(int replyTo, json_reader *pReader) + { + AlsaSequencerConfiguration config = this->model.GetAlsaSequencerConfiguration(); + this->Reply(replyTo, "getAlsaSequencerConfiguration", config); + } + REGISTER_MESSAGE_HANDLER(getAlsaSequencerConfiguration) + + void handle_getAlsaSequencerPorts(int replyTo, json_reader *pReader) + { + std::vector result = model.GetAlsaSequencerPorts(); + this->Reply(replyTo, "getAlsaSequencerPorts", result); + } + REGISTER_MESSAGE_HANDLER(getAlsaSequencerPorts) + + void handle_requestBankPresets(int replyTo, json_reader *pReader) + { + RequestBankPresetsBody args; + pReader->read(&args); + auto result = this->model.RequestBankPresets(args.bankInstanceId_); + this->Reply(replyTo, "requestBankPresets", result); + } + REGISTER_MESSAGE_HANDLER(requestBankPresets) + + void handle_importPresetsFromBank(int replyTo, json_reader *pReader) + { + ImportPresetsFromBankBody args; + pReader->read(&args); + auto result = this->model.ImportPresetsFromBank(args.bankInstanceId_, args.presets_); + this->Reply(replyTo, "importPresetsFromBank", result); + } + REGISTER_MESSAGE_HANDLER(importPresetsFromBank) + + void handle_copyPresetsToBank(int replyTo, json_reader *pReader) + { + CopyPresetsToBankBody args; + pReader->read(&args); + auto result = this->model.CopyPresetsToBank(args.bankInstanceId_, args.presets_); + this->Reply(replyTo, "copyPresetsToBank", result); + } + REGISTER_MESSAGE_HANDLER(copyPresetsToBank) + + void handle_getChannelRouterSettings(int replyTo, json_reader *pReader) + { + ChannelRouterSettings::ptr result = this->model.GetChannelRouterSettings(); + this->Reply(replyTo, "getChannelRouterSettings", result); + } + REGISTER_MESSAGE_HANDLER(getChannelRouterSettings) + + void handle_setChannelRouterSettings(int replyTo, json_reader *pReader) + { + ChannelRouterSettings::ptr args; + pReader->read(&args); + this->model.SetChannelRouterSettings(this->clientId, args); + } + REGISTER_MESSAGE_HANDLER(setChannelRouterSettings) + + void handle_DownloadModelsFromTone3000(int replyTo, json_reader *pReader) + { + + DownloadModelsFromTone3000Body body; + pReader->read(&body); + auto result = this->model.DownloadModelsFromTone3000( + body.responseUri_, + body.tone3000PckceParams_, + body.downloadPath_, + body.downloadType()); + + this->Reply(replyTo, "downloadModelsFromTone3000", result); + } + REGISTER_MESSAGE_HANDLER(DownloadModelsFromTone3000) + + void handle_cancelTone3000Download(int replyTo, json_reader *pReader) + { + int64_t handle = -1; + pReader->read(&handle); + model.CancelTone3000Download(clientId, handle); + } + REGISTER_MESSAGE_HANDLER(cancelTone3000Download) + + void handle_pingTone3000Server(int replyTo, json_reader *pReader) + { + std::shared_ptr this_ = shared_from_this(); + model.Post([this_, replyTo]() + { + bool result = this_->PingTone3000Server(); + this_->Reply(replyTo,"pingTone3000Server",result); }); + } + REGISTER_MESSAGE_HANDLER(pingTone3000Server) + void handleMessage(int reply, int replyTo, const std::string &message, json_reader *pReader) { if (reply != -1) @@ -1135,724 +2182,15 @@ public: { this->SendError(replyTo, "Server has shut down."); } - if (message == "setControl") - { - ControlChangedBody message; - pReader->read(&message); - this->model.SetControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); - } - else if (message == "previewControl") - { - ControlChangedBody message; - pReader->read(&message); - this->model.PreviewControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); - } - else if (message == "setControl") - { - ControlChangedBody message; - pReader->read(&message); - this->model.SetControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); - } - else if (message == "setInputVolume") - { - float value; - pReader->read(&value); - this->model.SetInputVolume(value); - } - else if (message == "setOutputVolume") - { - float value; - pReader->read(&value); - this->model.SetOutputVolume(value); - } - else if (message == "previewInputVolume") - { - float value; - pReader->read(&value); - this->model.PreviewInputVolume(value); - } - else if (message == "previewOutputVolume") - { - float value; - pReader->read(&value); - this->model.PreviewOutputVolume(value); - } - else if (message == "listenForMidiEvent") + auto ffHandler = socket_messageHandlers.find(message); + if (ffHandler != socket_messageHandlers.end()) { - ListenForMidiEventBody body; - pReader->read(&body); - this->model.ListenForMidiEvent(this->clientId, body.handle_); - } - else if (message == "cancelListenForMidiEvent") - { - uint64_t handle; - pReader->read(&handle); - this->model.CancelListenForMidiEvent(this->clientId, handle); - } - else if (message == "monitorPatchProperty") - { - MonitorPatchPropertyBody body; - pReader->read(&body); - this->model.MonitorPatchProperty(this->clientId, body.clientHandle_, body.instanceId_, body.propertyUri_); - } - else if (message == "cancelMonitorPatchProperty") - { - int64_t handle; - pReader->read(&handle); - this->model.CancelMonitorPatchProperty(this->clientId, handle); - } - else if (message == "getUpdateStatus") - { - UpdateStatus updateStatus = model.GetUpdateStatus(); - this->Reply(replyTo, "getUpdateStatus", updateStatus); - } - else if (message == "getHasWifi") - { - bool result = model.GetHasWifi(); - this->Reply(replyTo, "getHasWifi", result); - } - else if (message == "updateNow") - { - std::string updateUrl; - pReader->read(&updateUrl); - model.UpdateNow(updateUrl); - bool result = true; - this->Reply(replyTo, "updateNow", result); - } - else if (message == "getJackStatus") - { - JackHostStatus status = model.GetJackStatus(); - this->Reply(replyTo, "getJackStatus", status); - } - else if (message == "getAlsaDevices") - { - std::vector devices = model.GetAlsaDevices(); - this->Reply(replyTo, "getAlsaDevices", devices); - } - else if (message == "getKnownWifiNetworks") - { - std::vector channels = this->model.GetKnownWifiNetworks(); - this->Reply(replyTo, "getWifiChannels", channels); - } - else if (message == "getWifiChannels") - { - std::string country; - pReader->read(&country); - std::vector channels = pipedal::getWifiChannelSelectors(country.c_str()); - this->Reply(replyTo, "getWifiChannels", channels); - } - else if (message == "getPluginPresets") - { - std::string uri; - pReader->read(&uri); - this->Reply(replyTo, "getPluginPresets", this->model.GetPluginUiPresets(uri)); - } - else if (message == "loadPluginPreset") - { - LoadPluginPresetBody body; - pReader->read(&body); - this->model.LoadPluginPreset(body.pluginInstanceId_, body.presetInstanceId_); - } - else if (message == "setJackServerSettings") - { - JackServerSettings jackServerSettings; - pReader->read(&jackServerSettings); - this->model.SetJackServerSettings(jackServerSettings); - this->Reply(replyTo, "setJackserverSettings"); - } - else if (message == "setGovernorSettings") - { - std::string governor; - pReader->read(&governor); - std::string fromAddress = this->getFromAddress(); - // if (!IsOnLocalSubnet(fromAddress)) - // { - // throw PiPedalException("Permission denied. Not on local subnet."); - // } - this->model.SetGovernorSettings(governor); - this->Reply(replyTo, "setGovernorSettings"); - } - else if (message == "setWifiConfigSettings") - { - WifiConfigSettings wifiConfigSettings; - pReader->read(&wifiConfigSettings); - if (!GetAdminClient().CanUseAdminClient()) - { - throw PiPedalException("Can't change server settings when running interactively."); - } - std::string fromAddress = this->getFromAddress(); - // if (!IsOnLocalSubnet(fromAddress)) - // { - // throw PiPedalException("Permission denied. Not on local subnet."); - // } - - this->model.SetWifiConfigSettings(wifiConfigSettings); - this->Reply(replyTo, "setWifiConfigSettings"); - } - else if (message == "getWifiConfigSettings") - { - this->Reply(replyTo, "getWifiConfigSettings", model.GetWifiConfigSettings()); - } - else if (message == "setWifiDirectConfigSettings") - { - WifiDirectConfigSettings wifiDirectConfigSettings; - pReader->read(&wifiDirectConfigSettings); - if (!GetAdminClient().CanUseAdminClient()) - { - throw PiPedalException("Can't change server settings when running interactively."); - } - std::string fromAddress = this->getFromAddress(); - // if (!IsOnLocalSubnet(fromAddress)) - // { - // throw PiPedalException("Permission denied. Not on local subnet."); - // } - - this->model.SetWifiDirectConfigSettings(wifiDirectConfigSettings); - this->Reply(replyTo, "setWifiDirectConfigSettings"); - } - else if (message == "getWifiDirectConfigSettings") - { - this->Reply(replyTo, "getWifiDirectConfigSettings", model.GetWifiDirectConfigSettings()); - } - - else if (message == "getGovernorSettings") - { - this->Reply(replyTo, "getGovernorSettings", model.GetGovernorSettings()); - } - - else if (message == "getJackServerSettings") - { - this->Reply(replyTo, "getJackServerSettings", model.GetJackServerSettings()); - } - else if (message == "getBankIndex") - { - BankIndex bankIndex = model.GetBankIndex(); - this->Reply(replyTo, "getBankIndex", bankIndex); - } - else if (message == "getJackConfiguration") - { - JackConfiguration configuration = this->model.GetJackConfiguration(); - this->Reply(replyTo, "getJackConfiguration", configuration); - } - else if (message == "getJackSettings") - { - JackChannelSelection selection = this->model.GetJackChannelSelection(); - this->Reply(replyTo, "getJackSettings", selection); - } - else if (message == "saveCurrentPreset") - { - this->model.SaveCurrentPreset(this->clientId); - } - else if (message == "saveCurrentPresetAs") - { - SaveCurrentPresetAsBody body; - pReader->read(&body); - int64_t result = this->model.SaveCurrentPresetAs(this->clientId, body.bankInstanceId_,body.name_, body.saveAfterInstanceId_); - Reply(replyTo, "saveCurrentPresetsAs", result); - } - else if (message == "setSelectedPedalboardPlugin") - { - SetSelectedPedalboardPluginBody body; - pReader->read(&body); - this->model.SetSelectedPedalboardPlugin(body.clientId_,body.pluginInstanceId_); - } - else if (message == "savePluginPresetAs") - { - SavePluginPresetAsBody body; - pReader->read(&body); - int64_t result = this->model.SavePluginPresetAs(body.instanceId_, body.name_); - Reply(replyTo, "saveCurrentPresetsAs", result); - } - else if (message == "getPresets") - { - PresetIndex presets; - this->model.GetPresets(&presets); - Reply(replyTo, "getPresets", presets); - } - else if (message == "setPedalboardItemEnable") - { - PedalboardItemEnabledBody body; - pReader->read(&body); - model.SetPedalboardItemEnable(body.clientId_, body.instanceId_, body.enabled_); - } - else if (message == "setPedalboardItemUseModUi") { - PedalboardItemUseModGuiBody body; - pReader->read(&body); - model.SetPedalboardItemUseModUi(body.clientId_, body.instanceId_, body.useModUi_); - } - else if (message == "updateCurrentPedalboard") - { - { - UpdateCurrentPedalboardBody body; - - pReader->read(&body); - this->model.UpdateCurrentPedalboard(body.clientId_, body.pedalboard_); - } - } - else if (message == "setSnapshot") - { - int64_t snapshotIndex = -1; - pReader->read(&snapshotIndex); - this->model.SetSnapshot(snapshotIndex); - } - else if (message == "setSnapshots") - { - SetSnapshotsBody body; - pReader->read(&body); - this->model.SetSnapshots(body.snapshots_, body.selectedSnapshot_); - } - else if (message == "currentPedalboard") - { - auto pedalboard = model.GetCurrentPedalboardCopy(); - Reply(replyTo, "currentPedalboard", pedalboard); - } - else if (message == "plugins") - { - auto ui_plugins = model.GetPluginHost().GetUiPlugins(); - Reply(replyTo, "plugins", ui_plugins); - } - else if (message == "pluginClasses") - { - auto classes = model.GetPluginHost().GetLv2PluginClass(); - Reply(replyTo, "pluginClasses", classes); - } - else if (message == "hello") - { - this->model.AddNotificationSubscription(shared_from_this()); - Reply(replyTo, "ehlo", clientId); - } - else if (message == "setJackSettings") - { - JackChannelSelection jackSettings; - pReader->read(&jackSettings); - this->model.SetJackChannelSelection(this->clientId, jackSettings); - } - else if (message == "setShowStatusMonitor") - { - bool showStatusMonitor; - pReader->read(&showStatusMonitor); - this->model.SetShowStatusMonitor(showStatusMonitor); - } - else if (message == "getShowStatusMonitor") - { - Reply(replyTo, "getShowStatusMonitor", this->model.GetShowStatusMonitor()); - } - else if (message == "version") - { - PiPedalVersion version(this->model); - - Reply(replyTo, "version", version); - } - else if (message == "loadPreset") - { - int64_t instanceId = 0; - pReader->read(&instanceId); - model.LoadPreset(this->clientId, instanceId); - } - else if (message == "updatePresets") - { - PresetIndex newIndex; - pReader->read(&newIndex); - bool result = model.UpdatePresets(this->clientId, newIndex); - this->Reply(replyTo, "updatePresets", result); - } - else if (message == "updatePluginPresets") - { - PluginUiPresets pluginPresets; - pReader->read(&pluginPresets); - model.UpdatePluginPresets(pluginPresets); - this->Reply(replyTo, "updatePluginPresets", true); - } - else if (message == "moveBank") - { - FromToBody body; - pReader->read(&body); - model.MoveBank(this->clientId, body.from_, body.to_); - this->Reply(replyTo, "moveBank"); - } - else if (message == "shutdown") - { - model.RequestShutdown(false); - this->Reply(replyTo, "shutdown"); - } - else if (message == "restart") - { - model.RequestShutdown(true); - this->Reply(replyTo, "restart"); - } - else if (message == "deletePresetItems") - { - std::vector items; - pReader->read(&items); - int64_t result = model.DeletePresets(this->clientId, items); - this->Reply(replyTo, "deletePresetItems", result); - } - else if (message == "deleteBankItem") - { - int64_t instanceId = 0; - pReader->read(&instanceId); - uint64_t result = model.DeleteBank(this->clientId, instanceId); - this->Reply(replyTo, "deleteBankItem", result); - } else if (message == "getHasTone3000Auth") - { - bool result = model.HasTone3000Auth(); - this->Reply(replyTo, "getHasTone3000Auth", result); - } - else if (message == "renameBank") - { - RenameBankBody body; - pReader->read(&body); - - std::stringstream tOut; - json_writer tWriter(tOut); - tWriter.write(body.newName_); - std::string tJson = tOut.str(); - std::stringstream tIn(tJson); - json_reader tReader(tIn); - std::string tResult; - tReader.read(&tResult); - - body.newName_ = tResult; - - try - { - model.RenameBank(this->clientId, body.bankId_, body.newName_); - this->Reply(replyTo, "renameBank"); - } - catch (const std::exception &e) - { - this->SendError(replyTo, std::string(e.what())); - } - } - else if (message == "openBank") - { - int64_t bankId = -1; - pReader->read(&bankId); - try - { - model.OpenBank(this->clientId, bankId); - ; - this->Reply(replyTo, "openBank"); - } - catch (const std::exception &e) - { - this->SendError(replyTo, std::string(e.what())); - } - } - else if (message == "saveBankAs") - { - RenameBankBody body; - pReader->read(&body); - try - { - int64_t newId = model.SaveBankAs(this->clientId, body.bankId_, body.newName_); - this->Reply(replyTo, "saveBankAs", newId); - } - catch (const std::exception &e) - { - this->SendError(replyTo, std::string(e.what())); - } - } - else if (message == "nextBank") - { - model.NextBank(); - } - else if (message == "previousBank") - { - model.PreviousBank(); - } - else if (message == "nextPreset") - { - model.NextPreset(); - } - else if (message == "previousPreset") - { - model.PreviousPreset(); - } - - else if (message == "renamePresetItem") - { - RenamePresetBody body; - pReader->read(&body); - - bool result = model.RenamePreset(body.clientId_, body.instanceId_, body.name_); - this->Reply(replyTo, "renamePresetItem", result); - } - else if (message == "copyPreset") - { - CopyPresetBody body; - pReader->read(&body); - int64_t result = model.CopyPreset(body.clientId_, body.fromId_, body.toId_); - this->Reply(replyTo, "copyPreset", result); - } - else if (message == "copyPluginPreset") - { - CopyPluginPresetBody body; - pReader->read(&body); - uint64_t result = model.CopyPluginPreset(body.pluginUri_, body.instanceId_); - this->Reply(replyTo, "copyPluginPreset", result); - } - else if (message == "setPatchProperty") - { - SetPatchPropertyBody body; - pReader->read(&body); - model.SendSetPatchProperty(clientId, body.instanceId_, body.propertyUri_, body.value_, [this, replyTo]() - { this->JsonReply(replyTo, "setPatchProperty", "true"); }, [this, replyTo](const std::string &error) - { this->SendError(replyTo, error.c_str()); }); - } - else if (message == "setPedalboardItemTitle") - { - SetPedalboardItemTitleBody body; - pReader->read(&body); - model.SetPedalboardItemTitle(body.instanceId_, body.title_, body.colorKey_); - } - else if (message == "getPatchProperty") - { - GetPatchPropertyBody body; - pReader->read(&body); - - model.SendGetPatchProperty( - this->clientId, - body.instanceId_, - body.propertyUri_, - [this, replyTo](const std::string &jsonResult) - { - this->JsonReply(replyTo, "getPatchProperty", jsonResult.c_str()); - }, - [this, replyTo](const std::string &error) - { - this->SendError(replyTo, error.c_str()); - }); - } - else if (message == "monitorPort") - { - - MonitorPortBody body; - pReader->read(&body); - - MonitorPort(replyTo, body); - } - else if (message == "unmonitorPort") - { - int64_t subscriptionHandle; - pReader->read(&subscriptionHandle); - { - { - std::lock_guard guard(activePortMonitorsMutex); - for (auto i = this->activePortMonitors.begin(); i != this->activePortMonitors.end(); ++i) - { - if ((*i)->subscriptionHandle == subscriptionHandle) - { - auto subscription = (*i); - subscription->Close(); - - this->activePortMonitors.erase(i); - break; - } - } - } - model.UnmonitorPort(subscriptionHandle); - } - } - else if (message == "addVuSubscription") - { - int64_t instanceId = -1; - - pReader->read(&instanceId); - - int64_t subscriptionHandle = model.AddVuSubscription(instanceId); - - { - std::lock_guard guard(subscriptionMutex); - activeVuSubscriptions.push_back(VuSubscription{subscriptionHandle, instanceId}); - } - this->Reply(replyTo, "addVuSubscription", subscriptionHandle); - } - else if (message == "removeVuSubscription") - { - int64_t subscriptionHandle = -1; - pReader->read(&subscriptionHandle); - { - std::lock_guard guard(subscriptionMutex); - - for (auto i = activeVuSubscriptions.begin(); i != activeVuSubscriptions.end(); ++i) - { - if (i->subscriptionHandle == subscriptionHandle) - { - activeVuSubscriptions.erase(i); - break; - } - } - } - model.RemoveVuSubscription(subscriptionHandle); - } - else if (message == "imageList") - { - - this->Reply(replyTo, "imageList", imageList); - } - else if (message == "getFavorites") - { - std::map favorites = this->model.GetFavorites(); - this->Reply(replyTo, "getFavorites", favorites); - } - else if (message == "setFavorites") - { - std::map favorites; - pReader->read(&favorites); - this->model.SetFavorites(favorites); - } - else if (message == "setUpdatePolicy") - { - int iPolicy; - pReader->read(&iPolicy); - - this->model.SetUpdatePolicy((UpdatePolicyT)iPolicy); - } - else if (message == "forceUpdateCheck") - { - this->model.ForceUpdateCheck(); - } - else if (message == "setSystemMidiBindings") - { - std::vector bindings; - pReader->read(&bindings); - this->model.SetSystemMidiBindings(bindings); - } - else if (message == "getSystemMidiBindings") - { - std::vector bindings = this->model.GetSystemMidiBidings(); - this->Reply(replyTo, "getSystemMidiBindings", bindings); - } - else if (message == "requestFileList") - { - throw std::runtime_error("No longer implemented."); - } - else if (message == "requestFileList2") - { - FileRequestArgs requestArgs; - pReader->read(&requestArgs); - FileRequestResult result = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_); - this->Reply(replyTo, "requestFileList2", result); - } - else if (message == "newPreset") - { - int64_t presetId = this->model.CreateNewPreset(); - this->Reply(replyTo, "newPreset", presetId); - } - else if (message == "deleteUserFile") - { - std::string fileName; - pReader->read(&fileName); - - this->model.DeleteSampleFile(fileName); - this->Reply(replyTo, "deleteUserFile", true); - } - else if (message == "createNewSampleDirectory") - { - CreateNewSampleDirectoryArgs args; - pReader->read(&args); - - std::string newFileName = this->model.CreateNewSampleDirectory(args.relativePath_, args.uiFileProperty_); - this->Reply(replyTo, "createNewSampleDirectory", newFileName); - } - else if (message == "renameFilePropertyFile") - { - RenameSampleFileArgs args; - pReader->read(&args); - - std::string newFileName = this->model.RenameFilePropertyFile(args.oldRelativePath_, args.newRelativePath_, args.uiFileProperty_); - this->Reply(replyTo, "renameFilePropertyFile", newFileName); - } - else if (message == "copyFilePropertyFile") - { - CopySampleFileArgs args; - pReader->read(&args); - - std::string newFileName = this->model.CopyFilePropertyFile(args.oldRelativePath_, args.newRelativePath_, args.uiFileProperty_, args.overwrite_); - this->Reply(replyTo, "copyFilePropertyFile", newFileName); - } - else if (message == "getFilePropertyDirectoryTree") - { - GetFilePropertyDirectoryTreeArgs args; - pReader->read(&args); - FilePropertyDirectoryTree::ptr result = - model.GetFilePropertydirectoryTree( - args.fileProperty_, - args.selectedPath_); - this->Reply(replyTo, "GetFilePropertydirectoryTree", result); - } - else if (message == "getFilePropertyDirectoryTree") - { - GetFilePropertyDirectoryTreeArgs args; - pReader->read(&args); - FilePropertyDirectoryTree::ptr result = - model.GetFilePropertydirectoryTree( - args.fileProperty_, - args.selectedPath_); - this->Reply(replyTo, "GetFilePropertydirectoryTree", result); - } - - else if (message == "moveAudioFile") - { - MoveAudioFileArgs args; - pReader->read(&args); - this->model.MoveAudioFile(args.path_, args.from_, args.to_); - bool result = true; - this->Reply(replyTo,"moveAudioFile", result); - } - else if (message == "setOnboarding") - { - bool value; - pReader->read(&value); - this->model.SetOnboarding(value); - } - else if (message == "getWifiRegulatoryDomains") - { - auto regulatoryDomains = this->model.GetWifiRegulatoryDomains(); - this->Reply(replyTo, "getWifiRegulatoryDomains", regulatoryDomains); - } else if (message == "setAlsaSequencerConfiguration") - { - AlsaSequencerConfiguration config; - pReader->read(&config); - this->model.SetAlsaSequencerConfiguration(config); - this->Reply(replyTo, "setAlsaSequencerConfiguration"); - } - else if (message == "getAlsaSequencerConfiguration") - { - AlsaSequencerConfiguration config = this->model.GetAlsaSequencerConfiguration(); - this->Reply(replyTo, "getAlsaSequencerConfiguration", config); - } - else if (message == "getAlsaSequencerPorts") - { - std::vector result = model.GetAlsaSequencerPorts(); - this->Reply(replyTo,"getAlsaSequencerPorts", result); - } else if (message == "requestBankPresets") { - - RequestBankPresetsBody args; - pReader->read(&args); - auto result = this->model.RequestBankPresets(args.bankInstanceId_); - this->Reply(replyTo,"requestBankPresets",result); - - } - else if (message == "importPresetsFromBank") { - ImportPresetsFromBankBody args; - pReader->read(&args); - auto result = this->model.ImportPresetsFromBank(args.bankInstanceId_, args.presets_); - this->Reply(replyTo,"importPresetsFromBank",result); - } - else if (message == "copyPresetsToBank") { - CopyPresetsToBankBody args; - pReader->read(&args); - auto result = this->model.CopyPresetsToBank(args.bankInstanceId_, args.presets_); - this->Reply(replyTo,"copyPresetsToBank",result); - } - else - { - Lv2Log::error("Unknown message received: %s", message.c_str()); - SendError(replyTo, std::string("Unknown message: ") + message); + (this->*(ffHandler->second))(replyTo, pReader); + return; } + Lv2Log::error("Unknown message received: %s", message.c_str()); + SendError(replyTo, std::string("Unknown message: ") + message); } protected: @@ -1964,15 +2302,38 @@ private: { } } - virtual void OnTone3000AuthChanged(bool value) - { - Send("onTone3000AuthChanged", value); - } virtual void OnErrorMessage(const std::string &message) { Send("onErrorMessage", message); } + + virtual void OnTone3000DownloadStarted(int64_t handle, const std::string &title) override + { + Tone3000DownloadStartedBody body; + body.handle_ = handle; + body.title_ = title; + Send("onTone3000DownloadStarted", body); + } + + virtual void OnTone3000DownloadProgress(const Tone3000DownloadProgress &progress) override + { + Send("onTone3000DownloadProgress", progress); + } + + virtual void OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) override + { + Send("onTone3000DownloadComplete", resultPath); + } + + virtual void OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) override + { + Tone3000DownloadErrorBody body; + body.handle_ = handle; + body.errorMessage_ = errorMessage; + Send("onTone3000DownloadError", body); + } + // virtual void OnPatchPropertyChanged(int64_t clientId, int64_t instanceId,const std::string& propertyUri,const json_variant& value) // { // PatchPropertyChangedBody body; @@ -1998,11 +2359,9 @@ private: Send("onShowStatusMonitorChanged", show); } - virtual void OnChannelSelectionChanged(int64_t clientId, const JackChannelSelection &channelSelection) + virtual void OnChannelRouterSettingsChanged(int64_t clientId, const ChannelRouterSettings &channelRouterSettings) { - ChannelSelectionChangedBody body; - body.clientId_ = clientId; - body.jackChannelSelection_ = const_cast(&channelSelection); + ChannelRouterSettingsChangedBody body(clientId, channelRouterSettings); Send("onChannelSelectionChanged", body); } @@ -2051,15 +2410,15 @@ private: int updateRequestOutstanding = 0; bool vuUpdateDropped = false; - virtual void OnVuMeterUpdate(const std::vector &updates) + virtual void OnVuMeterUpdate(const std::vector &updates) { std::lock_guard guard(subscriptionMutex); - if (updateRequestOutstanding < 5) // throttle to accomodate a web page that can't keep up. + if (updateRequestOutstanding < 1) // throttle to accomodate a web page that can't keep up. { vuUpdateDropped = false; for (int i = 0; i < updates.size(); ++i) { - const VuUpdate &vuUpdate = updates[i]; + const VuUpdateX &vuUpdate = updates[i]; bool interested = false; for (int i = 0; i < this->activeVuSubscriptions.size(); ++i) { @@ -2072,7 +2431,7 @@ private: if (interested) { updateRequestOutstanding++; - this->Request( + this->Request( "onVuUpdate", vuUpdate, [this](const bool &result) @@ -2326,7 +2685,6 @@ private: body.enabled_ = enabled; Send("onUseItemModUiChanged", body); } - }; std::atomic PiPedalSocketHandler::nextClientId = 0; @@ -2354,7 +2712,7 @@ public: } virtual std::shared_ptr CreateHandler(const uri &request) { - return std::make_shared(model); + return std::shared_ptr(new PiPedalSocketHandler(model)); } }; @@ -2362,3 +2720,20 @@ std::shared_ptr pipedal::MakePiPedalSocketFactory(PiPedalModel & { return std::make_shared(model); } + +bool PiPedalSocketHandler::PingTone3000Server() +{ + constexpr const char *TONE3000_PING_URL = "https://www.tone3000.com/robots.txt"; + std::vector output; + std::vector headers; + try + { + int result = CurlGet(TONE3000_PING_URL, output, &headers); + return result == 200; + } + catch (const std::exception &e) + { + Lv2Log::error("PingTone3000Server: %s", e.what()); + return false; + } +} \ No newline at end of file diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 6973a5b..4eee514 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -34,6 +34,7 @@ #include "lv2/urid/urid.h" #include "lv2/ui/ui.h" #include "lv2/core/lv2.h" +#include "ChannelRouterSettings.hpp" // #include "lv2.h" #include "lv2/atom/atom.h" @@ -124,6 +125,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) core__isSideChain = lilv_new_uri(pWorld, LV2_CORE_PREFIX "isSideChain"); // missing in lv2.h portprops__not_on_gui_property_uri = lilv_new_uri(pWorld, LV2_PORT_PROPS__notOnGUI); portprops__trigger = lilv_new_uri(pWorld, LV2_PORT_PROPS__trigger); + portprops__expensive = lilv_new_uri(pWorld, LV2_PORT_PROPS__expensive); midi__event = lilv_new_uri(pWorld, LV2_MIDI__MidiEvent); core__designation = lilv_new_uri(pWorld, LV2_CORE__designation); portgroups__group = lilv_new_uri(pWorld, LV2_PORT_GROUPS__group); @@ -315,15 +317,15 @@ PluginHost::PluginHost() this->urids = new Urids(mapFeature); } -void PluginHost::OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings) + +void PluginHost::OnConfigurationChanged(const JackConfiguration &configuration, const ChannelSelection &channelSelection) { this->sampleRate = configuration.sampleRate(); if (configuration.isValid()) { - this->numberOfAudioInputChannels = settings.GetInputAudioPorts().size(); - this->numberOfAudioOutputChannels = settings.GetOutputAudioPorts().size(); this->maxBufferSize = configuration.blockLength(); } + this->channelSelection = channelSelection; } PluginHost::~PluginHost() @@ -1264,6 +1266,7 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP this->integer_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->integer_property_uri); this->mod_momentaryOffByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOffByDefault); this->mod_momentaryOnByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOnByDefault); + this->is_expensive_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__expensive); this->pipedal_graphicEq_ = lilv_port_has_property(plugin, pPort, host->lilvUris->pipedalUI__graphicEq); this->enumeration_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->enumeration_property_uri); @@ -1871,7 +1874,7 @@ static std::map ExtractPathPropertiesFromLilvState( bool Lv2PluginInfo::IsPathProperty(const std::string &uri) const { if (!piPedalUI_) { - + return false; } for (const UiFileProperty::ptr& fileProperty: this->piPedalUI_->fileProperties()) { @@ -2320,6 +2323,7 @@ json_map::storage_type Lv2PortInfo::jmap{ MAP_REF(Lv2PortInfo, toggled_property), MAP_REF(Lv2PortInfo, mod_momentaryOffByDefault), MAP_REF(Lv2PortInfo, mod_momentaryOnByDefault), + MAP_REF(Lv2PortInfo, is_expensive), MAP_REF(Lv2PortInfo, pipedal_graphicEq), MAP_REF(Lv2PortInfo, not_on_gui), MAP_REF(Lv2PortInfo, buffer_type), @@ -2407,6 +2411,7 @@ json_map::storage_type Lv2PluginUiPort::jmap{{ MAP_REF(Lv2PluginUiPort, mod_momentaryOffByDefault), MAP_REF(Lv2PluginUiPort, mod_momentaryOnByDefault), + MAP_REF(Lv2PluginUiPort, is_expensive), MAP_REF(Lv2PluginUiPort, pipedal_graphicEq), MAP_REF(Lv2PluginUiPort, enumeration_property), MAP_REF(Lv2PluginUiPort, not_on_gui), diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index 9e0abec..9ea0762 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -43,6 +43,7 @@ #include "PiPedalUI.hpp" #include "MapPathFeature.hpp" #include "ModGui.hpp" +#include "ChannelRouterSettings.hpp" namespace pipedal { @@ -54,6 +55,7 @@ namespace pipedal class PluginHost; class JackConfiguration; class JackChannelSelection; + class ChannelRouterSettings; #ifndef LV2_PROPERTY_GETSET #define LV2_PROPERTY_GETSET(name) \ @@ -224,6 +226,7 @@ namespace pipedal bool mod_momentaryOffByDefault_ = false; bool mod_momentaryOnByDefault_ = false; + bool is_expensive_ = false; bool pipedal_graphicEq_ = false; bool not_on_gui_ = false; @@ -302,6 +305,7 @@ namespace pipedal LV2_PROPERTY_GETSET_SCALAR(integer_property); LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOffByDefault); LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOnByDefault); + LV2_PROPERTY_GETSET_SCALAR(is_expensive); LV2_PROPERTY_GETSET_SCALAR(pipedal_graphicEq); LV2_PROPERTY_GETSET_SCALAR(enumeration_property); LV2_PROPERTY_GETSET_SCALAR(toggled_property); @@ -589,6 +593,7 @@ namespace pipedal integer_property_(pPort->integer_property()), mod_momentaryOffByDefault_(pPort->mod_momentaryOffByDefault()), mod_momentaryOnByDefault_(pPort->mod_momentaryOnByDefault()), + is_expensive_(pPort->is_expensive()), pipedal_graphicEq_(pPort->pipedal_graphicEq()), enumeration_property_(pPort->enumeration_property()), @@ -639,6 +644,7 @@ namespace pipedal bool mod_momentaryOffByDefault_ = false; bool mod_momentaryOnByDefault_ = false; + bool is_expensive_ = false; bool pipedal_graphicEq_ = false; bool enumeration_property_ = false; @@ -669,6 +675,10 @@ namespace pipedal LV2_PROPERTY_GETSET_SCALAR(display_priority); LV2_PROPERTY_GETSET_SCALAR(is_logarithmic); LV2_PROPERTY_GETSET_SCALAR(integer_property); + LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOffByDefault); + LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOnByDefault); + LV2_PROPERTY_GETSET_SCALAR(is_expensive); + LV2_PROPERTY_GETSET_SCALAR(pipedal_graphicEq); LV2_PROPERTY_GETSET_SCALAR(enumeration_property); LV2_PROPERTY_GETSET_SCALAR(toggled_property); LV2_PROPERTY_GETSET_SCALAR(trigger_property); @@ -796,6 +806,7 @@ namespace pipedal AutoLilvNode core__isSideChain; AutoLilvNode portprops__not_on_gui_property_uri; AutoLilvNode portprops__trigger; + AutoLilvNode portprops__expensive; AutoLilvNode midi__event; AutoLilvNode core__designation; AutoLilvNode portgroups__group; @@ -893,8 +904,8 @@ namespace pipedal size_t maxBufferSize = 1024; size_t maxAtomBufferSize = 16 * 1024; bool hasMidiInputChannel; - int numberOfAudioInputChannels = 1; - int numberOfAudioOutputChannels = 1; + ChannelSelection channelSelection; + double sampleRate = 48000; std::string vst3CachePath; @@ -955,13 +966,12 @@ namespace pipedal private: // IHost implementation. - virtual void SetMaxAudioBufferSize(size_t size) { maxBufferSize = size; } - virtual size_t GetMaxAudioBufferSize() const { return maxBufferSize; } - virtual size_t GetAtomBufferSize() const { return maxAtomBufferSize; } - virtual bool HasMidiInputChannel() const { return hasMidiInputChannel; } - virtual int GetNumberOfInputAudioChannels() const { return numberOfAudioInputChannels; } - virtual int GetNumberOfOutputAudioChannels() const { return numberOfAudioOutputChannels; } - virtual LV2_Feature *const *GetLv2Features() const { return (LV2_Feature *const *)&(this->lv2Features[0]); } + virtual void SetMaxAudioBufferSize(size_t size) override { maxBufferSize = size; } + virtual size_t GetMaxAudioBufferSize() const override { return maxBufferSize; } + virtual size_t GetAtomBufferSize() const override { return maxAtomBufferSize; } + virtual bool HasMidiInputChannel() const override { return hasMidiInputChannel; } + virtual LV2_Feature *const *GetLv2Features() const override { return (LV2_Feature *const *)&(this->lv2Features[0]); } + virtual const ChannelSelection &GetChannelSelection() const override { return this->channelSelection; } public: virtual MapFeature &GetMapFeature() override { return this->mapFeature; } @@ -1027,7 +1037,8 @@ namespace pipedal Urids *urids = nullptr; ModGuiUris *mod_gui_uris = nullptr; - void OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings); + void OnConfigurationChanged(const JackConfiguration &configuration, const ChannelSelection &channelSelection); + std::shared_ptr GetPluginClass(const std::string &uri) const; bool is_a(const std::string &class_, const std::string &target_class); diff --git a/src/RingBufferReader.hpp b/src/RingBufferReader.hpp index c3260fb..f83cb32 100644 --- a/src/RingBufferReader.hpp +++ b/src/RingBufferReader.hpp @@ -45,6 +45,7 @@ namespace pipedal uint8_t cc2_; }; + enum class RingBufferCommand : int64_t { Invalid = 0, @@ -96,6 +97,22 @@ namespace pipedal }; + struct RealtimePedalboardItemIndex { + RealtimePedalboardItemIndex() + : index(-1) + { + } + RealtimePedalboardItemIndex( + int64_t index) + : index(index) + { + } + RealtimePedalboardItemIndex(const RealtimePedalboardItemIndex& other) = default; + RealtimePedalboardItemIndex& operator=(const RealtimePedalboardItemIndex& other) = default; + + int64_t index = -1; + }; + struct RealtimeMidiEventRequest { RealtimeMidiEventType eventType; @@ -148,7 +165,7 @@ namespace pipedal } bool waitingForAcknowledge = false; - const std::vector *GetResult(size_t currentSample) + const std::vector *GetResult(size_t currentSample) { for (size_t i = 0; i < vuUpdateWorkingData.size(); ++i) { @@ -159,9 +176,9 @@ namespace pipedal return &vuUpdateResponseData; } - std::vector enabledIndexes; - std::vector vuUpdateWorkingData; - std::vector vuUpdateResponseData; + std::vector enabledIndexes; + std::vector vuUpdateWorkingData; + std::vector vuUpdateResponseData; void Reset() { @@ -468,7 +485,7 @@ namespace pipedal write(RingBufferCommand::SendMonitorPortUpdate, body); } - void SendVuUpdate(const std::vector *pUpdates) + void SendVuUpdate(const std::vector *pUpdates) { write(RingBufferCommand::SendVuUpdate, pUpdates); } @@ -521,16 +538,19 @@ namespace pipedal write(RingBufferCommand::ReplaceEffect, pedalboard); } + void EffectReplaced(Lv2Pedalboard *pedalboard) + { + write(RingBufferCommand::EffectReplaced, pedalboard); + } + + + void AudioTerminatedAbnormally() { AudioStoppedBody body; write(RingBufferCommand::AudioTerminatedAbnormally, body); } - void EffectReplaced(Lv2Pedalboard *pedalboard) - { - write(RingBufferCommand::EffectReplaced, pedalboard); - } void FreeSnapshot(IndexedSnapshot *snapshot) { diff --git a/src/StateInterface.hpp b/src/StateInterface.hpp index c09cc33..311455d 100644 --- a/src/StateInterface.hpp +++ b/src/StateInterface.hpp @@ -19,13 +19,12 @@ #pragma once -#include "lilv/lilv.h" #include "lv2/state/state.h" #include #include "json_variant.hpp" #include "MapFeature.hpp" #include "IHost.hpp" -#include "lilv/lilv.h" +#include namespace pipedal { diff --git a/src/Storage.cpp b/src/Storage.cpp index 81e283b..8ea253a 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -44,6 +44,7 @@ #include "AtomConverter.hpp" #include "FileBrowserFilesFeature.hpp" #include +#include "util.hpp" using namespace pipedal; namespace fs = std::filesystem; @@ -196,11 +197,11 @@ void Storage::SetDataRoot(const std::filesystem::path &path) this->dataRoot__audio_uploads = dataRoot / "audio_uploads"; } -const std::filesystem::path &Storage::GetConfigRoot() +const std::filesystem::path &Storage::GetConfigRoot() const { return this->configRoot; } -const std::filesystem::path &Storage::GetDataRoot() +const std::filesystem::path &Storage::GetDataRoot() const { return this->dataRoot; } @@ -249,7 +250,6 @@ static void removeFileNoThrow(const std::filesystem::path &path) } } - void Storage::UpgradeFactoryPresets() { auto presetsDirectory = this->GetPresetsDirectory(); @@ -375,10 +375,9 @@ void Storage::Initialize() LoadPluginPresetIndex(); LoadBankIndex(); LoadCurrentBank(); - LoadTone3000Auth(); try { - LoadChannelSelection(); + LoadJackChannelSelection(); } catch (const std::exception &) { @@ -387,8 +386,10 @@ void Storage::Initialize() LoadWifiConfigSettings(); LoadWifiDirectConfigSettings(); + this->channelRouterSettings = LoadChannelRouterSettings(); LoadUserSettings(); UpgradeFactoryPresets(); + UpgradeChannelRouterSettings(); } void Storage::LoadBank(int64_t instanceId) @@ -434,11 +435,6 @@ std::filesystem::path Storage::GetCurrentPresetPath() const return this->dataRoot / "currentPreset.json"; } -std::filesystem::path Storage::GetTone3000AuthPath() const -{ - return this->dataRoot / "tone3000.json"; -} - std::filesystem::path Storage::GetChannelSelectionFileName() { return this->dataRoot / "JackChannelSelection.json"; @@ -687,40 +683,46 @@ int64_t Storage::SaveCurrentPresetAs(const Pedalboard &pedalboard, int64_t bankI } } -static std::string stripNumericSuffix(const std::string &name) { +static std::string stripNumericSuffix(const std::string &name) +{ // remove (digit*) from the end of the string. // Find the last '(' character size_t pos = name.find_last_of('('); - if (pos == std::string::npos) { + if (pos == std::string::npos) + { return name; } // Check if everything between '(' and ')' is digits - size_t len = name.length()-1; - if (name[len] != ')') + size_t len = name.length() - 1; + if (name[len] != ')') { return name; } bool allDigits = true; - for (size_t i = pos + 1; i < len - 1; ++i) { - if (name[i] < '0' || name[i] > '9') { + for (size_t i = pos + 1; i < len - 1; ++i) + { + if (name[i] < '0' || name[i] > '9') + { allDigits = false; break; } } - if (!allDigits) { + if (!allDigits) + { return name; } // Remove trailing spaces before the '(' - while (pos > 0 && name[pos - 1] == ' ') { + while (pos > 0 && name[pos - 1] == ' ') + { --pos; } return name.substr(0, pos); } -static std::string makeUniqueName(const std::string &name, const std::set&existingNames) +static std::string makeUniqueName(const std::string &name, const std::set &existingNames) { if (!existingNames.contains(name)) { @@ -729,9 +731,11 @@ static std::string makeUniqueName(const std::string &name, const std::set &presets) { - if (bankIndex.selectedBank() == bankInstanceId) { + if (bankIndex.selectedBank() == bankInstanceId) + { throw std::runtime_error("Can't import to self."); } - std::set presetsSet { presets.begin(), presets.end()}; + std::set presetsSet{presets.begin(), presets.end()}; auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); std::set existingNames; - for (auto&preset: this->currentBank.presets()) { + for (auto &preset : this->currentBank.presets()) + { existingNames.insert(preset->preset().name()); } BankFile bankFile; - LoadBankFile(indexEntry.name(),&bankFile); + LoadBankFile(indexEntry.name(), &bankFile); int64_t lastPresetId = -1; - for (auto &presetEntry: bankFile.presets()) { - if (presetsSet.contains(presetEntry->instanceId())) { - std::string uniqueName = makeUniqueName(presetEntry->preset().name(),existingNames); + for (auto &presetEntry : bankFile.presets()) + { + if (presetsSet.contains(presetEntry->instanceId())) + { + std::string uniqueName = makeUniqueName(presetEntry->preset().name(), existingNames); existingNames.insert(uniqueName); Pedalboard t = presetEntry->preset(); t.name(uniqueName); @@ -767,31 +775,35 @@ int64_t Storage::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector } int64_t Storage::CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets) { - if (bankIndex.selectedBank() == bankInstanceId) { + if (bankIndex.selectedBank() == bankInstanceId) + { throw std::runtime_error("Can't copy to self."); } auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); BankFile bankFile; - LoadBankFile(indexEntry.name(),&bankFile); + LoadBankFile(indexEntry.name(), &bankFile); - std::set presetsSet { presets.begin(), presets.end()}; + std::set presetsSet{presets.begin(), presets.end()}; std::set existingNames; - for (auto&preset: bankFile.presets()) { + for (auto &preset : bankFile.presets()) + { existingNames.insert(preset->preset().name()); } - for (auto &presetEntry: this->currentBank.presets()) { - if (presetsSet.contains(presetEntry->instanceId())) { - std::string uniqueName = makeUniqueName(presetEntry->preset().name(),existingNames); + for (auto &presetEntry : this->currentBank.presets()) + { + if (presetsSet.contains(presetEntry->instanceId())) + { + std::string uniqueName = makeUniqueName(presetEntry->preset().name(), existingNames); existingNames.insert(uniqueName); Pedalboard t = presetEntry->preset(); t.name(uniqueName); bankFile.addPreset(t); } } - SaveBankFile(indexEntry.name(),bankFile); + SaveBankFile(indexEntry.name(), bankFile); return -1; } @@ -801,17 +813,15 @@ std::vector Storage::RequestBankPresets(int64_t bankInstanceId std::vector result; BankFile bankFile; - LoadBankFile(indexEntry.name(),&bankFile); - for (auto &preset: bankFile.presets()) { + LoadBankFile(indexEntry.name(), &bankFile); + for (auto &preset : bankFile.presets()) + { result.push_back( - PresetIndexEntry(preset->instanceId(),preset->preset().name()) - ); + PresetIndexEntry(preset->instanceId(), preset->preset().name())); } return result; } - - void Storage::SetPresetIndex(const PresetIndex &presets) { // painful because we must move unique_ptrs. @@ -885,7 +895,8 @@ Pedalboard Storage::GetPreset(int64_t instanceId) const int64_t Storage::DeletePresets(const std::vector &presetInstanceIds) { int64_t newSelection = currentBank.selectedPreset(); - for (auto presetId: presetInstanceIds) { + for (auto presetId : presetInstanceIds) + { newSelection = currentBank.deletePreset(presetId); } SaveCurrentBank(); @@ -977,7 +988,7 @@ int64_t Storage::CreateNewPreset() } } newPedalboard.name(name); - auto t = this->currentBank.addPreset(newPedalboard, -1); + auto t = this->currentBank.addPreset(newPedalboard, -1); SaveCurrentBank(); return t; } @@ -997,7 +1008,7 @@ int64_t Storage::CopyPreset(int64_t fromId, int64_t toId) { auto &toItem = this->currentBank.getItem(toId); toItem.preset(fromItem.preset()); - result = toId; + result = toId; } SaveCurrentBank(); return result; @@ -1119,7 +1130,7 @@ AlsaSequencerConfiguration Storage::GetAlsaSequencerConfiguration() const return this->alsaSequencerConfiguration; } -void Storage::LoadChannelSelection() +void Storage::LoadJackChannelSelection() { auto fileName = this->GetChannelSelectionFileName(); if (std::filesystem::exists(fileName)) @@ -1919,14 +1930,13 @@ pipedal::JackServerSettings Storage::GetJackServerSettings() result.GetAlsaInputDevice().empty() && result.GetAlsaOutputDevice().empty()) { - result.SetAlsaInputDevice(result.GetLegacyAlsaDevice()); - result.SetAlsaOutputDevice(result.GetLegacyAlsaDevice()); + std::string legacyDeviceId = result.GetLegacyAlsaDevice(); + result.SetAlsaInputDevice(legacyDeviceId, ""); + result.SetAlsaOutputDevice(legacyDeviceId, ""); result.SetLegacyAlsaDevice(""); } + result.FixUpDeviceNames(); } -#if JACK_HOST - result.Initialize(); -#endif return result; } @@ -1940,9 +1950,6 @@ void Storage::SetJackServerSettings(const pipedal::JackServerSettings &jackConfi json_writer writer(f, false); writer.write(jackConfiguration); } -#if JACK_HOST - jackConfiguration.Write(); -#endif } void Storage::SetSystemMidiBindings(const std::vector &bindings) @@ -2075,26 +2082,29 @@ static bool ensureNoDotDot(const std::filesystem::path &path) return true; } -static bool isInfoFile(const std::string&fileName) +bool isInfoFile(const fs::path &path) { - if (strcasecmp(fileName.c_str(),"license.txt") == 0) + auto extension = path.extension(); + if (extension == ".pdf") { return true; } - if (strcasecmp(fileName.c_str(),"license.md") == 0) + auto filename = path.filename(); + if (filename == "LICENSE.txt" || filename == "README.txt") { return true; } - if (strcasecmp(fileName.c_str(),"readme.txt") == 0) + if (filename == "LICENSE.md" || filename == "README.md") { return true; } - if (strcasecmp(fileName.c_str(),"readme.md") == 0) + if (filename == "license.txt" || filename == "readme.txt") { return true; } return false; } + static bool isInfoFile(const FileEntry &l) { return isInfoFile(l.displayName_); @@ -2139,13 +2149,13 @@ static void AddFilesToResult( if (match && !name.starts_with(".")) { resultFiles.push_back( - FileEntry(path, name, false, false)); + FileEntry(path, SafeFilenameToString(name), false, false)); } } } else if (dir_entry.is_directory()) { - resultFiles.push_back(FileEntry{path, name, true, fs::is_symlink(path)}); + resultFiles.push_back(FileEntry{path, SafeFilenameToString(name), true, fs::is_symlink(path)}); } } } @@ -2325,7 +2335,8 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons { rootModDirectory = modDirectoryInfo; modDirectoryPath = uploadsDirectory / modDirectoryInfo->pipedalPath; - result.breadcrumbs_.push_back({modDirectoryPath.string(), modDirectoryInfo->displayName}); + result.breadcrumbs_.push_back({modDirectoryPath.string(), + (modDirectoryInfo->displayName)}); break; } } @@ -2335,7 +2346,8 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons if (IsSubdirectory(fsRelativePath, uploadsDirectory / fileProperty.directory())) { modDirectoryPath = uploadsDirectory / fileProperty.directory(); - result.breadcrumbs_.push_back({modDirectoryPath.string(), fs::path(fileProperty.directory()).filename().string()}); + result.breadcrumbs_.push_back({modDirectoryPath.string(), + SafeFilenameToString(fs::path(fileProperty.directory()).filename().string())}); } else { @@ -2360,7 +2372,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons while (iRp != rp.end()) { cumulativePath /= (*iRp); - result.breadcrumbs_.push_back({cumulativePath, *iRp}); + result.breadcrumbs_.push_back({cumulativePath, SafeFilenameToString(*iRp)}); ++iRp; } } @@ -2450,7 +2462,7 @@ FileRequestResult Storage::GetFileList2(const std::string &relativePath_, const while (iAbsolutePath != fsAbsolutePath.end()) { cumulativePath /= (*iAbsolutePath); - result.breadcrumbs_.push_back({cumulativePath.string(), iAbsolutePath->string()}); + result.breadcrumbs_.push_back({cumulativePath.string(), SafeFilenameToString(iAbsolutePath->string())}); ++iAbsolutePath; } } @@ -2978,54 +2990,15 @@ const PluginPresetIndex &Storage::GetPluginPresetIndex() return pluginPresetIndex; } -void Storage::LoadTone3000Auth() -{ - fs::path path = GetTone3000AuthPath(); - try - { - if (!fs::exists(path)) - { - this->tone3000Auth = ""; - return; - } - std::ifstream s(path); - json_reader reader(s); - reader.read(&(this->tone3000Auth)); - } - catch (const std::exception &e) - { - Lv2Log::error("Failed to load tone3000Auth: %s", e.what()); - } -} - -void Storage::SetTone3000Auth(const std::string &apiKey) -{ - if (tone3000Auth != apiKey) - { - tone3000Auth = apiKey; - - pipedal::ofstream_synced os(this->GetTone3000AuthPath()); - if (!os.is_open()) - { - Lv2Log::error("Failed to open Tone3000 auth file for writing."); - return; - } - json_writer writer(os); - writer.write(apiKey); - } -} -std::string Storage::GetTone3000Auth() const -{ - return tone3000Auth; -} - std::filesystem::path Storage::FromAbstractPathString(const std::string &stringPath) { - if (stringPath.empty()) { + if (stringPath.empty()) + { return ""; } - fs::path path { stringPath}; - if (path.is_absolute()) { + fs::path path{stringPath}; + if (path.is_absolute()) + { return path; } return GetPluginUploadDirectory() / path; @@ -3048,6 +3021,136 @@ std::string Storage::FromAbstractPathJson(const std::string &pathJson) return v.to_string(); } +void Storage::SetChannelRouterSettings(ChannelRouterSettings::ptr settings) +{ + this->channelRouterSettings = settings; + this->channelSelection = ChannelSelection(*settings); + SaveChannelRouterSettings(settings); +} +ChannelRouterSettings::ptr Storage::GetChannelRouterSettings() +{ + return channelRouterSettings; +} + +std::filesystem::path Storage::GetChannelRouterSettingsPath() const +{ + return GetDataRoot() / "ChannelRouterSettings.json"; +} + +void Storage::SaveChannelRouterSettings(ChannelRouterSettings::ptr settings) +{ + std::ofstream os(GetChannelRouterSettingsPath()); + json_writer writer(os); + writer.write(settings); +} +ChannelRouterSettings::ptr Storage::LoadChannelRouterSettings() +{ + auto path = GetChannelRouterSettingsPath(); + if (!fs::exists(path)) + { + // See UpgradeChannelRouterSettings(), which will upgrade from old settings + // or create a default instance. + return nullptr; // will be upgraded later. + } + try + { + std::ifstream is(path); + json_reader reader(is); + ChannelRouterSettings::ptr result; + reader.read(&result); + this->channelSelection = ChannelSelection(*result); + return result; + } + catch (const std::exception &e) + { + Lv2Log::error("Failed to load Channel Router settings: %s", e.what()); + return std::make_shared(); + } +} + +static int64_t GetUpgradedPortIndex(const std::string &portName) +{ + auto nPos = portName.find_last_of('_'); + if (nPos != std::string::npos) + { + std::string channelStr = portName.substr(nPos + 1); + try + { + int64_t channelIndex = std::stoll(channelStr); + return channelIndex; + } + catch (const std::exception &) + { + return -1; + } + } + return -1; +} +void Storage::UpgradeChannelRouterSettings() +{ + if (channelRouterSettings == nullptr) + { + channelRouterSettings = std::make_shared(); + if (jackChannelSelection.isValid()) + { + channelRouterSettings->configured(true); + channelRouterSettings->mainInputChannels().resize(2); + const std::vector &oldInputs = jackChannelSelection.GetInputAudioPorts(); + std::vector &newInputs = channelRouterSettings->mainInputChannels(); + + if (oldInputs.size() == 1) + { + int64_t leftIndex = GetUpgradedPortIndex(oldInputs[0]); + newInputs.resize(2); + newInputs[0] = leftIndex; + newInputs[1] = leftIndex; + } + else if (oldInputs.size() == 2) + { + newInputs.resize(2); + for (size_t i = 0; i < std::min(oldInputs.size(), newInputs.size()); ++i) + { + int64_t portIndex = GetUpgradedPortIndex(oldInputs[i]); + newInputs[i] = portIndex; + } + } + const std::vector &oldOutputs = jackChannelSelection.GetOutputAudioPorts(); + auto &newMainOutputs = channelRouterSettings->mainOutputChannels(); + if (oldOutputs.size() == 1) + { + int64_t leftIndex = GetUpgradedPortIndex(oldOutputs[0]); + newMainOutputs.resize(2); + newMainOutputs[0] = leftIndex; + newMainOutputs[1] = leftIndex; + } + else if (oldOutputs.size() == 2) + { + newMainOutputs.resize(2); + for (size_t i = 0; i < std::min(oldOutputs.size(), newMainOutputs.size()); ++i) + { + int64_t portIndex = GetUpgradedPortIndex(oldOutputs[i]); + newMainOutputs[i] = portIndex; + } + } + + auto &newAuxInputs = channelRouterSettings->auxInputChannels(); + newAuxInputs.resize(2); + newAuxInputs[0] = -1; + newAuxInputs[1] = -1; + auto &newAuxOutputs = channelRouterSettings->auxOutputChannels(); + newAuxOutputs.resize(2); + newAuxOutputs[0] = -1; + newAuxOutputs[1] = -1; + SaveChannelRouterSettings(channelRouterSettings); + } + channelSelection = ChannelSelection(*channelRouterSettings); + } +} + +const ChannelSelection &Storage::GetChannelSelection() const +{ + return channelSelection; +} JSON_MAP_BEGIN(UserSettings) JSON_MAP_REFERENCE(UserSettings, governor) diff --git a/src/Storage.hpp b/src/Storage.hpp index 21ae5ba..4db1114 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -32,6 +32,8 @@ #include #include "FilePropertyDirectoryTree.hpp" #include "AlsaSequencer.hpp" +#include "ChannelRouterSettings.hpp" + namespace pipedal { @@ -75,7 +77,6 @@ private: BankIndex bankIndex; BankFile currentBank; PluginPresetIndex pluginPresetIndex; - std::string tone3000Auth; private: void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const; @@ -90,7 +91,6 @@ private: std::filesystem::path GetChannelSelectionFileName(); std::filesystem::path GetAlsaSequencerConfigurationFileName(); std::filesystem::path GetCurrentPresetPath() const; - std::filesystem::path GetTone3000AuthPath() const; void LoadBankIndex(); void SaveBankIndex(); @@ -98,7 +98,7 @@ private: void LoadCurrentBank(); void SaveCurrentBank(); - void LoadChannelSelection(); + void LoadJackChannelSelection(); void SaveChannelSelection(); void LoadAlsaSequencerConfiguration(); @@ -117,6 +117,15 @@ private: WifiDirectConfigSettings wifiDirectConfigSettings; UserSettings userSettings; + + ChannelRouterSettings::ptr channelRouterSettings; + ChannelSelection channelSelection; + + void UpgradeChannelRouterSettings(); + std::filesystem::path GetChannelRouterSettingsPath() const; + void SaveChannelRouterSettings(ChannelRouterSettings::ptr settings); + ChannelRouterSettings::ptr LoadChannelRouterSettings(); + public: Storage(); void Initialize(); @@ -124,8 +133,8 @@ public: void SetDataRoot(const std::filesystem::path& path); void SetConfigRoot(const std::filesystem::path& path); - const std::filesystem::path&GetConfigRoot(); - const std::filesystem::path&GetDataRoot(); + const std::filesystem::path&GetConfigRoot() const; + const std::filesystem::path&GetDataRoot() const; const std::filesystem::path &GetPluginUploadDirectory() const; @@ -206,6 +215,11 @@ public: void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration); AlsaSequencerConfiguration GetAlsaSequencerConfiguration() const; + void SetChannelRouterSettings(ChannelRouterSettings::ptr settings); + ChannelRouterSettings::ptr GetChannelRouterSettings(); + + const ChannelSelection& GetChannelSelection() const; + //std::string MapPropertyFileName(Lv2PluginInfo*pluginInfo, const std::string&path); @@ -267,9 +281,6 @@ public: bool overwrite = false); FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath); - void LoadTone3000Auth(); - void SetTone3000Auth(const std::string&apiKey); - std::string GetTone3000Auth() const; std::vector RequestBankPresets(int64_t bankInstanceId); int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets); diff --git a/src/ThreadedQueue.hpp b/src/ThreadedQueue.hpp new file mode 100644 index 0000000..15701ea --- /dev/null +++ b/src/ThreadedQueue.hpp @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2026 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 +#include + +namespace pipedal +{ + template + class ThreadedQueue + { + public: + ~ThreadedQueue() { Close(); } + + void Put(std::shared_ptr value) + { + { + std::lock_guard lock(mutex); + if (!closed) + { + queue.push(value); + } + } + read_cv.notify_one(); + } + std::shared_ptr Take() + { + while (true) + { + { + std::unique_lock lock(mutex); + read_cv.wait(lock, + [this]() {return !queue.empty() || closed;}); + if (!queue.empty()) + { + auto result = queue.front(); + queue.pop(); + return result; + } + else + { + if (closed) + { + return nullptr; + } + } + } + } + } + void Close() + { + { + std::lock_guard lock{mutex}; + closed = true; + } + read_cv.notify_all(); + } + void CloseAndClear() + { + { + std::lock_guard lock{mutex}; + closed = true; + while (!queue.empty()) + { + queue.pop(); + } + } + read_cv.notify_all(); + } + void Erase(std::function queueEntry)> predicate) + { + std::lock_guard lock{mutex}; + std::queue> newQueue; + while (!queue.empty()) + { + auto item = queue.front(); + queue.pop(); + if (!predicate(item)) + { + newQueue.push(item); + } + } + queue = newQueue; + } + + private: + bool closed = 0; + std::queue> queue; + std::mutex mutex; + std::condition_variable read_cv; + }; +} \ No newline at end of file diff --git a/src/Tone3000Download.cpp b/src/Tone3000Download.cpp new file mode 100644 index 0000000..d0b4680 --- /dev/null +++ b/src/Tone3000Download.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026 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 "ss.hpp" +#include "Tone3000Download.hpp" +#include "Tone3000Throttler.hpp" +#include "TemporaryFile.hpp" +#include +#include +#include "util.hpp" +#include "Tone3000DownloadProgress.hpp" +#include +#include "HtmlHelper.hpp" +#include "Curl.hpp" +#include "PiPedalModel.hpp" + +using namespace pipedal; + +Tone3000Download::Tone3000Download(const std::string &json) +{ + std::istringstream ss(json); + json_reader reader(ss); + reader.read(this); +} + +JSON_MAP_BEGIN(Tone3000Model) +JSON_MAP_REFERENCE(Tone3000Model, id) +JSON_MAP_REFERENCE(Tone3000Model, name) +JSON_MAP_REFERENCE(Tone3000Model, model_url) +JSON_MAP_REFERENCE(Tone3000Model, created_at) +JSON_MAP_REFERENCE(Tone3000Model, size) +JSON_MAP_REFERENCE(Tone3000Model, user_id) +JSON_MAP_END() + +JSON_MAP_BEGIN(Tone3000User) +JSON_MAP_REFERENCE(Tone3000User, id) +JSON_MAP_REFERENCE(Tone3000User, username) +JSON_MAP_REFERENCE(Tone3000User, avatar_url) +JSON_MAP_REFERENCE(Tone3000User, url) +JSON_MAP_END() + +JSON_MAP_BEGIN(Tone3000Download) +JSON_MAP_REFERENCE(Tone3000Download, id) +JSON_MAP_REFERENCE(Tone3000Download, user_id) +JSON_MAP_REFERENCE(Tone3000Download, title) +JSON_MAP_REFERENCE(Tone3000Download, description) +JSON_MAP_REFERENCE(Tone3000Download, created_at) +JSON_MAP_REFERENCE(Tone3000Download, updated_at) +JSON_MAP_REFERENCE(Tone3000Download, gear) +JSON_MAP_REFERENCE(Tone3000Download, images) +JSON_MAP_REFERENCE(Tone3000Download, is_public) +JSON_MAP_REFERENCE(Tone3000Download, links) +JSON_MAP_REFERENCE(Tone3000Download, platform) +JSON_MAP_REFERENCE(Tone3000Download, models_count) +JSON_MAP_REFERENCE(Tone3000Download, favorites_count) +JSON_MAP_REFERENCE(Tone3000Download, downloads_count) +JSON_MAP_REFERENCE(Tone3000Download, license) +JSON_MAP_REFERENCE(Tone3000Download, sizes) +JSON_MAP_REFERENCE(Tone3000Download, a1_models_count) +JSON_MAP_REFERENCE(Tone3000Download, a2_models_count) +JSON_MAP_REFERENCE(Tone3000Download, irs_count) +JSON_MAP_REFERENCE(Tone3000Download, custom_models_count) +JSON_MAP_REFERENCE(Tone3000Download, user) +JSON_MAP_REFERENCE(Tone3000Download, url) +JSON_MAP_REFERENCE(Tone3000Download, user) +JSON_MAP_REFERENCE(Tone3000Download, models) +JSON_MAP_END(); diff --git a/src/Tone3000Download.cpp.bak b/src/Tone3000Download.cpp.bak new file mode 100644 index 0000000..0d8b722 --- /dev/null +++ b/src/Tone3000Download.cpp.bak @@ -0,0 +1,711 @@ +/* + * Copyright (c) 2026 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 "ss.hpp" +#include "Tone3000Download.hpp" +#include "TemporaryFile.hpp" +#include +#include +#include "util.hpp" +#include "Tone3000DownloadProgress.hpp" +#include +#include "HtmlHelper.hpp" + +using namespace pipedal; +using namespace pipedal::tone3000; +namespace fs = std::filesystem; + +JSON_MAP_BEGIN(Tone3000Model) +JSON_MAP_REFERENCE(Tone3000Model, id) +JSON_MAP_REFERENCE(Tone3000Model, name) +JSON_MAP_REFERENCE(Tone3000Model, model_url) +JSON_MAP_REFERENCE(Tone3000Model, created_at) +JSON_MAP_REFERENCE(Tone3000Model, size) +JSON_MAP_REFERENCE(Tone3000Model, user_id) +JSON_MAP_END() + +JSON_MAP_BEGIN(Tone3000User) +JSON_MAP_REFERENCE(Tone3000User, id) +JSON_MAP_REFERENCE(Tone3000User, username) +JSON_MAP_REFERENCE(Tone3000User, avatar_url) +JSON_MAP_REFERENCE(Tone3000User, url) +JSON_MAP_END() + +JSON_MAP_BEGIN(Tone3000Download) +JSON_MAP_REFERENCE(Tone3000Download, id) +JSON_MAP_REFERENCE(Tone3000Download, user_id) +JSON_MAP_REFERENCE(Tone3000Download, title) +JSON_MAP_REFERENCE(Tone3000Download, description) +JSON_MAP_REFERENCE(Tone3000Download, created_at) +JSON_MAP_REFERENCE(Tone3000Download, updated_at) +JSON_MAP_REFERENCE(Tone3000Download, platform) +JSON_MAP_REFERENCE(Tone3000Download, gear) +JSON_MAP_REFERENCE(Tone3000Download, images) +JSON_MAP_REFERENCE(Tone3000Download, is_public) +JSON_MAP_REFERENCE(Tone3000Download, links) +JSON_MAP_REFERENCE(Tone3000Download, model_count) +JSON_MAP_REFERENCE(Tone3000Download, favorites_count) +JSON_MAP_REFERENCE(Tone3000Download, license) +JSON_MAP_REFERENCE(Tone3000Download, sizes) +JSON_MAP_REFERENCE(Tone3000Download, user) +JSON_MAP_REFERENCE(Tone3000Download, models) +JSON_MAP_END() + +JSON_MAP_BEGIN(Tone3000DownloadResult) +JSON_MAP_REFERENCE(Tone3000DownloadResult, success) +JSON_MAP_REFERENCE(Tone3000DownloadResult, errorMessage) +JSON_MAP_END() + +static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"}; + +static bool enableLogging = true; + +static std::string getCurlErrorFromExitCode(int exitCode) +{ + + switch (exitCode) + { + case 3 * 256: // malformed URL + return "Invalid URL."; + case 6 * 256: // could not resolve hostname + return "Could not resolve hostname. Check that the PiPedal server is connected to the Internet."; + case 7 * 256: // failed to connect to host + return "Failed to connect to host."; + case 22 * 256: // HTTP error + return "HTTP error downloading file."; + case 28 * 256: // timeout + return "Download timeout."; + case 63 * 256: // file too large + return "File too large."; + default: + return SS("Failed to download file (exit code: " << (exitCode / 256) << ")"); + } +} + +static void cancellableSleep(std::chrono::steady_clock::duration duration, std::function &isCancelled) +{ + std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); + while (true) + { + if (std::chrono::steady_clock::now() - start >= duration) + { + break; + } + if (isCancelled()) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } +} + +static void downloadFile( + const std::string &url, + const fs::path &path, + std::function &isCancelled) +{ + // Create temporary file for HTTP status code + TemporaryFile httpCodeFile{WEB_TEMP_DIR}; + fs::path httpCodePath = httpCodeFile.Path(); + + TemporaryFile headersFile{WEB_TEMP_DIR}; + fs::path headersFilePath = headersFile.Path(); + + for (size_t retry = 0; retry < 10; ++retry) + { + + // Build curl command with error detection and proper escaping + std::ostringstream os; + os << "/usr/bin/curl -f -s -S -L --max-time 60 --retry 5 --retry-delay 1 --retry-max-time 10 --max-filesize 104857600"; + os << " -w '%{http_code}' -o "; + os << ShellEscape(path.string()); + os << " -D " << ShellEscape(headersFilePath); + os << " "; + os << ShellEscape(url); + os << " > "; + os << ShellEscape(httpCodePath.string()); + + std::string command = os.str(); + + // Execute curl command + int exitCode = std::system(command.c_str()); + + // Read HTTP status code + int httpCode = 0; + if (fs::exists(httpCodePath)) + { + std::ifstream httpCodeStream(httpCodePath); + std::string httpCodeStr; + std::getline(httpCodeStream, httpCodeStr); + try + { + httpCode = std::stoi(httpCodeStr); + } + catch (...) + { + // If parsing fails, keep httpCode as 0 + } + } + + if (exitCode != 0) + { + if (exitCode == 22 * 256) // HTTP error + { + std::ifstream headersStream(headersFilePath); + std::string headerStr; + while (std::getline(headersStream, headerStr)) + { + std::cout << headerStr << std::endl; + } + if (httpCode == 429 || httpCode == 403 || httpCode == 503) + { + cancellableSleep(std::chrono::milliseconds(2000), isCancelled); + if (isCancelled()) + { + return; + } + continue; + } + throw std::runtime_error( + SS("Server was unabled to download the file. Http error: " + << HtmlHelper::httpErrorString(httpCode))); + } + else + { + throw std::runtime_error( + SS("Server was unable to download the file. " + << getCurlErrorFromExitCode(exitCode) + << " " << "url: " << url)); + } + } + else + { + break; + } + } + + // Verify file was downloaded + if (!fs::exists(path) || fs::file_size(path) == 0) + { + throw std::runtime_error("Downloaded file is empty or missing"); + } +} + +static std::string mdSanitize(const std::string &str, const std::string &illegalCharacters = "") +{ + std::ostringstream os; + for (char c : str) + { + if ((unsigned char)c < 0x20) + continue; + if (c == '<') + { + os << "<"; + continue; + } + auto npos = illegalCharacters.find_first_of(c); + if (npos != std::string::npos) + { + continue; + } + os << c; + } + return os.str(); +} + +static std::string mdDate(const tone3000_time_point &date_) +{ + std::ostringstream os; + // C++ doesn't handle timezones. Just zap the timezone, and replace it with "Z" + std::string date = date_; + + // Remove timezone offset if present and replace with Z + size_t plusPos = date.find('+'); + if (plusPos != std::string::npos) + { + date = date.substr(0, plusPos) + "Z"; + } + // Parse ISO 8601 date string into tm + std::tm tm = {}; + std::istringstream ss(date); + ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); + if (ss.fail()) + { + os << date; + return os.str(); + } + + os << std::put_time(&tm, "%x"); + return os.str(); +} + +static std::string mdEnum(const std::string &value, const std::map &enumValues) +{ + auto ff = enumValues.find(value); + if (ff == enumValues.end()) + { + return value; + } + return ff->second; +} + +static std::string mdEnumList( + const std::vector &values, + const std::map &enumValues) +{ + if (values.size() == 1) + { + return mdEnum(values[0], enumValues); + } + std::ostringstream os; + os << '['; + bool first = true; + for (const auto &value : values) + { + if (first) + { + first = false; + } + else + { + os << ", "; + } + os << mdEnum(value, enumValues); + } + os << ']'; + return os.str(); +} + +static std::map sizeEnumValues = + { + {"standard", "Standard"}, + {"lite", "Lite"}, + {"feather", "Feather"}, + {"nano", "Nano"}, + {"custom", "Custom"}}; +static std::string mdSizes(const std::vector &sizes) +{ + return mdEnumList(sizes, sizeEnumValues); +} + +static std::map gearEnumValues = + { + {"amp", "Amp only"}, + {"full-rig", "Full rig"}, + {"pedal", "Pedal"}, + {"outboard", "Outboard"}, + {"ir", "I/R"}}; + +static std::string mdGear(const std::string &gear) +{ + return mdEnum(gear, gearEnumValues); +} + +namespace +{ + enum class LicenseFlags + { + None = 0, + Cc = 1, + By = 2, + CcBy = 3, // = Cc | By + Sa = 4, + Nc = 8, + Nd = 16, + Cc0 = 32, + }; + + static bool operator&(LicenseFlags v1, LicenseFlags v2) + { + return (((int)v1) & ((int)v2)) != 0; + } + static LicenseFlags operator|(LicenseFlags v1, LicenseFlags v2) + { + return (LicenseFlags)(((int)(v1)) | ((int)(v2))); + } + + struct LicenseInfo + { + std::string key; + std::string displayName; + std::string url; + LicenseFlags licenseFlags; + }; +} + +static std::vector licenses{ + {.key = "t3k", + .displayName = "T3K", + .url = "licenses/t3k_license.html", + .licenseFlags = LicenseFlags::None}, + {.key = "cc-by", + .displayName = "CC BY 4.0", + .url = "https://creativecommons.org/licenses/by/4.0/", + .licenseFlags = LicenseFlags::CcBy}, + {.key = "cc-by-sa", + .displayName = "CC BY-SA 4.0", + .url = "https://creativecommons.org/licenses/by-sa/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Sa}, + {.key = "cc-by-nc", + .displayName = "CC BY-NC 4.0", + .url = "https://creativecommons.org/licenses/by-nc/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc}, + {.key = "cc-by-nc-sa", + .displayName = "CC BY-NC-SA 4.0", + .url = "https://creativecommons.org/licenses/by-nc-sa/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Sa}, + {.key = "cc-by-nd", + .displayName = "CC BY-ND 4.0", + .url = "https://creativecommons.org/licenses/by-nd/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nd}, + {.key = "cc-by-nc-nd", + .displayName = "CC BY-NC-ND 4.0", + .url = "https://creativecommons.org/licenses/by-nc-nd/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Nd}, + { + .key = "cco", + .displayName = "CC0", + .url = "https://creativecommons.org/publicdomain/zero/1.0/", + .licenseFlags = LicenseFlags::Cc | LicenseFlags::Cc0, + + }, +}; + +static std::map makeLicenseEnum() +{ + std::map result; + for (const auto &license : licenses) + { + result[license.key] = license; + } + return result; +} + +static std::map licenseEnum = makeLicenseEnum(); + +static std::string mdLicenseIcon(LicenseFlags licenseFlag) +{ + std::ostringstream os; + std::string url; + + switch (licenseFlag) + { + case LicenseFlags::Cc: + url = "img/cc.svg"; + break; + case LicenseFlags::By: + url = "img/by.svg"; + break; + case LicenseFlags::Sa: + url = "img/sa.svg"; + break; + case LicenseFlags::Nc: + url = "img/nc.svg"; + break; + case LicenseFlags::Nd: + url = "img/nd.svg"; + break; + case LicenseFlags::Cc0: + url = "img/cc0.svg"; + break; + + case LicenseFlags::None: + throw std::runtime_error("Invalid argument."); + } + os << ""; + return os.str(); +} + +static void mdEscapeString(std::ostream &f, const std::string &str) +{ + size_t ix = 0; + while (ix < str.size()) + { + char c = str[ix++]; + if (c == '\n') + { + if (ix < str.size() && str[ix] == '\n') + { + f << "\n\n"; + } + else + { + f << " \n"; // a
in md. + } + } + else + { + f << c; + } + } +} + +static std::string mdHref(const std::string &url) +{ + std::ostringstream os; + os << '\''; + for (char c : url) + { + if (c == '\'') + { + continue; + } + os << c; + } + os << '\''; + return os.str(); +} +static std::string mdLicense(const std::string &license) +{ + + auto ff = licenseEnum.find(license); + LicenseInfo licenseInfo; + if (ff != licenseEnum.end()) + { + licenseInfo = ff->second; + } + else + { + licenseInfo.displayName = license; + } + + std::ostringstream os; + + if (licenseInfo.licenseFlags != LicenseFlags::None) + { + for (LicenseFlags licenseFlag : std::vector{LicenseFlags::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd}) + { + if (licenseInfo.licenseFlags & licenseFlag) + { + os << mdLicenseIcon(licenseFlag); + } + } + os << " "; + } + + if (licenseInfo.url != "") + { + os << "" << mdSanitize(licenseInfo.displayName) << ""; + } + else + { + os << mdSanitize(licenseInfo.displayName); + } + return os.str(); +} +// static std::string mdTestLicenses() +// { +// std::ostringstream os; +// for (auto license: licenseEnum) +// { +// os << "
" << mdLicense(license.first) << "\n"; +// } +// return os.str(); +// } +static std::map platformEnumValues = + { + {"nam", "NAM"}, + {"ir", "I/R"}, + {"aida-x", "Aida X"}, + {"aa-snapshot", "aa-snapshot"}, + {"proteus", "Proteus"}}; +static std::string mdPlatform(const std::string &platform) +{ + return mdEnum(platform, platformEnumValues); +} +static std::string mdUser(const tone3000::Tone3000User &user) +{ + // clang-format off + std::ostringstream os; + os << + "" << mdSanitize(user.username()) << ""; + return os.str(); + // clang-format on +} + +static void mdLink(std::ostream &f, const std::string &label, const std::string &url) +{ + f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")"; +} + +static void mdImage(std::ostream &f, const std::string url) +{ + f << "![" << mdSanitize("Plugin Thumbname", "]") << "](" << mdSanitize(url, "]") << "#tone3000_thumbnail" << ")"; + f << "\n\n"; +} +static void writeReadme(const fs::path &path, const Tone3000Download &download) +{ + std::ofstream of{path}; + if (!of.is_open()) + { + throw std::runtime_error(SS("Unable to create file " << path)); + } + std::string imageLink; + if (download.images().size() > 0) + { + imageLink = download.images()[0]; + } + + // clang-format off + of << + + "
\n" + << " \n" + << "
\n" + << "

" << mdSanitize(download.title()) << "

\n" + << "
\n" + << "

\n" + << mdDate(download.updated_at()) + << " " + << mdUser(download.user()); + if (download.sizes().has_value()) { + of + << "
\n" + << " " << mdSizes(download.sizes().value()) << ", " + ; + } + of + << mdGear(download.gear()) << ", " + << mdPlatform(download.platform()) + << "
\n"; + if (download.license() != "") { + of << " License: " << mdLicense(download.license()) << "\n"; + } + of +// << " " << mdTestLicenses() + << "

\n" + << "
\n" + << "
\n" + << "
\n"; + // clang-format on + of << "\n\n"; + mdEscapeString(of, download.description()); + + if (download.links().size() > 0) + { + of << std::endl; + of << "## Links" << std::endl + << std::endl; + for (auto &link : download.links()) + { + of << "- " << "[" << link << "](" << link << ")" << std::endl; + } + } +} + +std::string pipedal::tone3000::DownloadTone3000File( + const std::filesystem::path &destinationFolder, + const std::string &downloadUrl, + int64_t handle, + std::function progressCallback, + std::function isCancelled) +{ + TemporaryFile downloadTemporaryFile{WEB_TEMP_DIR}; + fs::path downloadPath = downloadTemporaryFile.Path(); + + std::string resultDirectory; + Tone3000DownloadProgress progress; + progress.handle(handle); + progressCallback(progress); + + downloadFile(downloadUrl, downloadPath, isCancelled); + + if (isCancelled()) + { + + return resultDirectory; + } + + std::ifstream f{downloadPath}; + if (!f.is_open()) + { + throw std::runtime_error(SS("Can't open file " << downloadPath)); + } + json_reader reader(f); + + Tone3000Download tone3000Download; + reader.read(&tone3000Download); + + if (isCancelled()) + { + return resultDirectory; + } + progress.title(tone3000Download.title()); + progress.total(tone3000Download.models().size()); + progressCallback(progress); + + if (tone3000Download.platform() != "nam" && tone3000Download.platform() != "ir") + { + throw std::runtime_error(SS("Unexpected file format. (platform=\"" << tone3000Download.platform() << "\")")); + } + fs::path bundlePath = destinationFolder / StringToSafeFilename(tone3000Download.title()); + resultDirectory = bundlePath; + + fs::create_directories(bundlePath); + uint64_t nModel = 0; + for (auto &model : tone3000Download.models()) + { + if (isCancelled()) + { + return resultDirectory; + } + fs::path modelPath; + if (tone3000Download.platform() == "ir") + { + modelPath = bundlePath / SS(StringToSafeFilename(model.name()) << ".wav"); + + } else { + modelPath = bundlePath / SS(StringToSafeFilename(model.name()) << ".nam"); + } + downloadFile(model.model_url(), modelPath, isCancelled); + if (isCancelled()) + { + return resultDirectory; + } + + progress.progress(++nModel); + progressCallback(progress); + } + if (isCancelled()) + { + return resultDirectory; + } + fs::path readmePath = bundlePath / "README.md"; + writeReadme(readmePath, tone3000Download); + + return resultDirectory; +} + +Tone3000DownloadResult::Tone3000DownloadResult() + : success_(true) +{ +} +Tone3000DownloadResult::Tone3000DownloadResult(const std::exception &e) + : success_(false), errorMessage_(e.what()) +{ +} diff --git a/src/Tone3000Download.hpp b/src/Tone3000Download.hpp new file mode 100644 index 0000000..5dd6e6d --- /dev/null +++ b/src/Tone3000Download.hpp @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2026 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 +#include +#include "json.hpp" +#include +#include +#include +#include "Tone3000DownloadProgress.hpp" +#include + +namespace pipedal +{ + + class Tone3000FileDownloadRequest; + + using tone3000_time_point = std::string; // Date in ISO 8601 format. (C++ doesn't handle timezones) + class Tone3000Model + { + private: + int64_t id_ = -1; + std::string name_; + std::string model_url_; + tone3000_time_point created_at_; + std::optional size_; + std::string user_id_; + + public: + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(name) + JSON_GETTER_SETTER_REF(model_url) + JSON_GETTER_SETTER_REF(created_at) + JSON_GETTER_SETTER_REF(size) + JSON_GETTER_SETTER_REF(user_id) + + DECLARE_JSON_MAP(Tone3000Model); + }; + + class Tone3000User + { + private: + std::string id_; + std::optional avatar_url_; + std::string username_; + std::string url_; + + public: + JSON_GETTER_SETTER_REF(id) + JSON_GETTER_SETTER(username) + JSON_GETTER_SETTER(url) + + DECLARE_JSON_MAP(Tone3000User); + }; + + class Tone3000Download + { + private: + int64_t id_ = -1; + std::string user_id_; + std::string title_; + std::string description_; + tone3000_time_point created_at_; + tone3000_time_point updated_at_; + std::string gear_; + std::vector images_; + bool is_public_ = true; + std::vector links_; + std::string platform_; + int64_t models_count_ = 0; + int64_t favorites_count_ = 0; + int64_t downloads_count_ = 0; + std::string license_; + std::optional> sizes_; + int64_t a1_models_count_ = 0; + int64_t a2_models_count_ =0; + int64_t irs_count_ =0; + int64_t custom_models_count_ = 0; + Tone3000User user_; + std::string url_; + + std::vector models_; + + public: + Tone3000Download() = default; + Tone3000Download(const std::string&json); + + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(user_id) + JSON_GETTER_SETTER_REF(title) + JSON_GETTER_SETTER_REF(description) + JSON_GETTER_SETTER_REF(created_at) + JSON_GETTER_SETTER_REF(updated_at) + JSON_GETTER_SETTER_REF(gear) + JSON_GETTER_SETTER_REF(images) + JSON_GETTER_SETTER(is_public) + JSON_GETTER_SETTER_REF(links) + JSON_GETTER_SETTER_REF(platform) + JSON_GETTER_SETTER(models_count) + JSON_GETTER_SETTER(favorites_count) + JSON_GETTER_SETTER(downloads_count) + JSON_GETTER_SETTER_REF(license) + JSON_GETTER_SETTER_REF(sizes) + JSON_GETTER_SETTER_REF(a1_models_count) + JSON_GETTER_SETTER_REF(a2_models_count) + JSON_GETTER_SETTER_REF(irs_count) + JSON_GETTER_SETTER_REF(custom_models_count) + JSON_GETTER_SETTER_REF(user) + JSON_GETTER_SETTER_REF(url) + JSON_GETTER_SETTER_REF(models) + + DECLARE_JSON_MAP(Tone3000Download); + }; + +} diff --git a/src/Tone3000DownloadProgress.cpp b/src/Tone3000DownloadProgress.cpp new file mode 100644 index 0000000..3227f4c --- /dev/null +++ b/src/Tone3000DownloadProgress.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 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 "Tone3000DownloadProgress.hpp" + +using namespace pipedal; + +JSON_MAP_BEGIN(Tone3000DownloadProgress) +JSON_MAP_REFERENCE(Tone3000DownloadProgress, handle) +JSON_MAP_REFERENCE(Tone3000DownloadProgress, title) +JSON_MAP_REFERENCE(Tone3000DownloadProgress, progress) +JSON_MAP_REFERENCE(Tone3000DownloadProgress, total) +JSON_MAP_END() diff --git a/src/Tone3000DownloadProgress.hpp b/src/Tone3000DownloadProgress.hpp new file mode 100644 index 0000000..7aa30b4 --- /dev/null +++ b/src/Tone3000DownloadProgress.hpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 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 { + + + class Tone3000DownloadProgress { + private: + int64_t handle_ = -1; + std::string title_; + uint64_t progress_ = 0; + uint64_t total_ = 0; + public: + JSON_GETTER_SETTER(handle); + JSON_GETTER_SETTER_REF(title); + JSON_GETTER_SETTER(progress); + JSON_GETTER_SETTER(total); + + + DECLARE_JSON_MAP(Tone3000DownloadProgress); + + }; + +} diff --git a/src/Tone3000DownloadType.cpp b/src/Tone3000DownloadType.cpp new file mode 100644 index 0000000..0e2ed2d --- /dev/null +++ b/src/Tone3000DownloadType.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 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 "Tone3000DownloadType.hpp" + #include + #include + +namespace pipedal { + + // Must agree with Tone3000DownloadType.tsx + + static std::unordered_map downloadTypeEnums = + { + {"nam", Tone3000DownloadType::Nam}, + {"cabir", Tone3000DownloadType::CabIr}, + }; + Tone3000DownloadType StringToTone3000DownloadType(const std::string &value) + { + auto ff = downloadTypeEnums.find(value); + if (ff == downloadTypeEnums.end()) + { + throw std::runtime_error("Invalid argument."); + } + return ff->second; + } + std::string Tone3000DownloadTypeToString(Tone3000DownloadType value) + { + switch (value) + { + case Tone3000DownloadType::Nam: + return "nam"; + case Tone3000DownloadType::CabIr: + return "cabir"; + default: + throw std::runtime_error("Inalid argument."); + } + } + +} \ No newline at end of file diff --git a/src/Tone3000DownloadType.hpp b/src/Tone3000DownloadType.hpp new file mode 100644 index 0000000..6a1ef93 --- /dev/null +++ b/src/Tone3000DownloadType.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 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 { + enum class Tone3000DownloadType { + Nam = 0, + CabIr = 1 + }; + + Tone3000DownloadType StringToTone3000DownloadType(const std::string &value); + std::string Tone3000DownloadTypeToString(Tone3000DownloadType value); + +} \ No newline at end of file diff --git a/src/Tone3000Downloader.cpp b/src/Tone3000Downloader.cpp new file mode 100644 index 0000000..30cf42f --- /dev/null +++ b/src/Tone3000Downloader.cpp @@ -0,0 +1,1332 @@ +/* + * Copyright (c) 2026 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 "Tone3000DownloaderImpl.hpp" +#include "Tone3000DownloadProgress.hpp" +#include "Tone3000Download.hpp" +#include "Uri.hpp" +#include +#include "HtmlHelper.hpp" +#include "Curl.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include "TemporaryFile.hpp" +#include "HtmlHelper.hpp" + +using namespace pipedal; +namespace fs = std::filesystem; + + +// Pulls in the define for T3kConfig.h + +#define PIPEDAL_T3K_PUBLISHABLE_KEY "t3k_pub_bHrH8btdwXXTtxz5ryEU8sNLF-2TGRT9" + + + +namespace { + std::string Base64UrlEncode(const unsigned char* data, size_t size) + { + static constexpr char base64Table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + std::string result; + result.reserve(((size + 2) / 3) * 4); + + for (size_t i = 0; i + 3 <= size; i += 3) + { + uint32_t value = (static_cast(data[i]) << 16) + | (static_cast(data[i + 1]) << 8) + | static_cast(data[i + 2]); + result.push_back(base64Table[(value >> 18) & 0x3F]); + result.push_back(base64Table[(value >> 12) & 0x3F]); + result.push_back(base64Table[(value >> 6) & 0x3F]); + result.push_back(base64Table[value & 0x3F]); + } + + size_t remainder = size % 3; + if (remainder == 1) + { + uint32_t value = static_cast(data[size - 1]) << 16; + result.push_back(base64Table[(value >> 18) & 0x3F]); + result.push_back(base64Table[(value >> 12) & 0x3F]); + result.push_back('='); + result.push_back('='); + } + else if (remainder == 2) + { + uint32_t value = (static_cast(data[size - 2]) << 16) + | (static_cast(data[size - 1]) << 8); + result.push_back(base64Table[(value >> 18) & 0x3F]); + result.push_back(base64Table[(value >> 12) & 0x3F]); + result.push_back(base64Table[(value >> 6) & 0x3F]); + result.push_back('='); + } + + for (char &ch : result) + { + if (ch == '+') + { + ch = '-'; + } + else if (ch == '/') + { + ch = '_'; + } + } + while (!result.empty() && result.back() == '=') + { + result.pop_back(); + } + return result; + } + + std::string RandomBase64Url(size_t size) + { + std::string bytes(size, '\0'); + if (RAND_bytes(reinterpret_cast(bytes.data()), static_cast(bytes.size())) != 1) + { + throw std::runtime_error("RAND_bytes failed."); + } + return Base64UrlEncode(reinterpret_cast(bytes.data()), bytes.size()); + } + +} + + +std::string pipedal::Sha256Base64Url(const std::string &text) +{ + std::array digest{}; + size_t digestLength = 0; + if (EVP_Q_digest( + nullptr, + "SHA256", + nullptr, + reinterpret_cast(text.data()), + text.size(), + digest.data(), + &digestLength) + != 1) + { + throw std::runtime_error("EVP_Q_digest(SHA256) failed."); + } + return Base64UrlEncode(digest.data(), digestLength); +} + +static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"}; +static const std::filesystem::path TONE3000_THUMBNAIL_PATH{"/var/pipedal/tone3000_thumbnails"}; + +class Tone3000CancelledError : std::exception +{ +public: + Tone3000CancelledError() = default; + virtual ~Tone3000CancelledError() = default; + + virtual const char *what() const noexcept override + { + std::exception::what(); + return "Cancelled."; + } +}; + +std::shared_ptr Tone3000Downloader::Create() +{ + return std::make_shared(); +} + +Tone3000Downloader::Tone3000Downloader() +{ +} + +Tone3000Downloader::~Tone3000Downloader() +{ +} + +Tone3000DownloaderImpl::Tone3000DownloaderImpl() +{ +} +Tone3000DownloaderImpl::~Tone3000DownloaderImpl() +{ + Close(); + this->thread = nullptr; +} + +void Tone3000DownloaderImpl::SetListener(Listener *listener) +{ + std::lock_guard lockGuard{this->mutex}; + this->listener = listener; +} + +Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::NextHandle() +{ + handle_t result = this->nextHandle++; + return result; +} + +void Tone3000DownloaderImpl::CancelDownload( + handle_t handle) +{ + std::lock_guard lockGuard{this->mutex}; + requestQueue.Erase([handle](std::shared_ptr entry) + { return entry->handle = handle; }); + if (activeRequest) + { + if (activeRequest->handle == handle) + { + activeRequest->cancelled = true; + } + } +} + +void Tone3000DownloaderImpl::Close() +{ + bool notify = false; + { + std::lock_guard lockGuard{this->mutex}; + if (!closed) + { + closed = true; + notify = true; + } + + Tone3000DownloadProgress progress; + fgDownloadProgress = progress; + + if (listener) + { + listener->OnTone3000Progress(progress); + } + if (this->activeRequest != nullptr) + { + activeRequest->cancelled = true; + } + } + this->requestQueue.CloseAndClear(); +} +Tone3000DownloadProgress Tone3000DownloaderImpl::GetDownloadStatus() +{ + Tone3000DownloadProgress result; + { + std::lock_guard lockGuard{this->mutex}; + result = fgDownloadProgress; + } + return result; +} + +void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const Tone3000DownloadProgress &progress) +{ + + std::lock_guard lockGuard{this->mutex}; + if (closed) + { + return; + } + if (activeRequest != nullptr && !activeRequest->cancelled) + { + this->fgDownloadProgress = progress; + if (listener) + { + listener->OnTone3000Progress(this->fgDownloadProgress); + } + } +} + +static void ValidateTone3000Url(const std::string &url) +{ + uri downloadUri{url}; + if (!downloadUri.authority().ends_with(".tone3000.com")) + { + throw std::runtime_error("Invalid Tone3000 URL address."); + } +} + +namespace +{ + template + static T ParseIntegerOrThrow(const std::string &text, const char *fieldName) + { + static_assert(std::is_integral_v, "ParseIntegerOrThrow requires an integral type."); + + T value{}; + const char *begin = text.data(); + const char *end = begin + text.size(); + auto [ptr, ec] = std::from_chars(begin, end, value); + if (ec == std::errc::invalid_argument || ptr != end) + { + throw std::runtime_error(SS("Tone3000 Auth: invalid " << fieldName << ".")); + } + if (ec == std::errc::result_out_of_range) + { + throw std::runtime_error(SS("Tone3000 Auth: " << fieldName << " is out of range.")); + } + return value; + } +} + +void Tone3000DownloaderImpl::ThreadProc() +{ + while (true) + { + Listener *currentListener = nullptr; + + Tone3000DownloadProgress progress; + + auto request = requestQueue.Take(); + if (request == nullptr) + { + return; + } + + { + std::unique_lock lock{this->mutex}; + activeRequest = request; + currentListener = listener; + } + + // Notify download started + if (currentListener) + { + currentListener->OnStartTone3000Download(request->handle, ""); + } + + // Create cancellation predicate that checks the atomic flag + auto isCancelled = [request]() -> bool + { + return request->cancelled.load(); + }; + + try + { + this->DownloadTone3000ToneBg( + request); + // std::string outputPath; + + // Tone3000DownloadProgress progress; + // progress.total(request->tone3000DownloadRequest.models().size()); + // progress.handle(request->handle); + + // this->bgUpdateDownloadProgress(progress); + + // outputPath = PerformFileDownloads( + // request->tone3000DownloadRequest, + // progress, + // [this](Tone3000DownloadProgress &progress) + // { bgUpdateDownloadProgress(progress); }, + // isCancelled); + + // // Notify completion + // { + // std::lock_guard lockGuard{mutex}; + // if (currentListener) + // { + // currentListener->OnTone3000DownloadComplete(request->handle, outputPath); + // } + // } + } + catch (const std::exception &e) + { + { + std::lock_guard lockGuard{mutex}; + // Notify error + if (!request->cancelled.load() && currentListener) + { + currentListener->OnTone3000DownloadError(request->handle, e.what()); + } + } + } + + { + std::lock_guard lock{this->mutex}; + activeRequest = nullptr; + } + } +} + +void Tone3000DownloaderImpl::DownloadTone3000ToneBg( + std::shared_ptr request) +{ + handle_t handle = request->handle; + uri requestUri{request->requestUri}; + const Tone3000PkceParams &pkceParams = request->pkceParams; + const std::string &downloadPath = request->downloadPath; + + Tone3000DownloadProgress progress; + progress.handle(handle); + + try + { + if (listener) + { + listener->OnStartTone3000Download(progress.handle(), "Authenticating..."); + } + progress.title("Authenticating..."); + bgUpdateDownloadProgress(progress); + + auto oAuthResult = handleOAuthCallback(requestUri, pkceParams); + if (!oAuthResult.toneId.has_value()) + { + throw std::runtime_error("Tone3000 Auth failed. tone ID not received."); + } + std::shared_ptr download = GetTone3000Tone(oAuthResult.toneId.value()); + } + catch (const Tone3000CancelledError &e) + { + OnTone3000DownloadCancelled(progress.handle()); + return; + } + catch (const std::exception &e) + { + OnTone3000DownloadError(progress.handle(), e.what()); + return; + } + OnTone3000DownloadComplete(progress.handle(), ""); +} + +Tone3000DownloaderImpl::OAuthCallbackResult Tone3000DownloaderImpl::handleOAuthCallback( + const uri &callbackUri, + const Tone3000PkceParams &pkce) +{ + OAuthCallbackResult result; + + try + { + std::map params; + std::string code = callbackUri.requiredQuery("code"); + std::optional error = callbackUri.optionalQuery("error"); + std::string returnedState = callbackUri.requiredQuery("state"); + std::optional toneId = callbackUri.optionalQuery("tone_id"); + std::optional modelId = callbackUri.optionalQuery("model_id"); + bool canceled = callbackUri.query("canceled") == "true"; + if (error.has_value()) + { + throw std::runtime_error(error.value()); + } + + if (canceled) + { + throw Tone3000CancelledError(); + } + + std::string codeVerifier = pkce.codeVerifier(); + + // Access denied — e.g. model is private and user clicked "Back" + if (error.has_value()) + { + throw std::runtime_error(error.value()); + } + + if (code.empty() || codeVerifier.empty()) + { + throw std::runtime_error("Tone3000 Auth: missing code."); + } + + std::string body = HtmlFormBuilder({{"grant_type", "authorization_code"}, + {"code", code}, + {"code_verifier", codeVerifier}, + {"redirect_uri", pkce.redirectUrl()}, + {"client_id", pkce.publishableKey()}}) + .build(); + + auto postResult = Post( + uri("http://www.tone3000.com/api/v1/oauth/token"), + {{"Content-Type: application/x-www-form-urlencoded"}}, + body); + if (!postResult.ok) + { + throw std::runtime_error(postResult.error); + } + Tone3000AuthResponse authResponse(postResult.body); + this->accessTokens = std::make_shared(authResponse); + + OAuthCallbackResult result; + if (toneId.has_value()) + { + result.toneId = ParseIntegerOrThrow(toneId.value(), "tone_id"); + } + if (modelId.has_value()) + { + result.modelId = ParseIntegerOrThrow(modelId.value(), "model_id"); + } + return result; + } + catch (const std::exception &e) + { + throw; + } +} + + +std::string Tone3000DownloaderImpl::GetBearerToken() const +{ + if (!accessTokens) + { + throw std::runtime_error("Tone3000 auth failure. No access token."); + } + return accessTokens->access_token(); +} + + +std::string Tone3000DownloaderImpl::Tone3000GetText( + const uri &requestedUri +) +{ + TemporaryFile tempFile{WEB_TEMP_DIR}; + + std::vector inputHeaders { + SS("Authorization: Bearer " << this->GetBearerToken()) + }; + int httpCode = CurlGet(requestedUri.str(),tempFile.Path(),nullptr,&inputHeaders); + if (httpCode != 200) + { + throw std::runtime_error( + SS("Http error " << HtmlHelper::httpErrorString(httpCode))); + } + std::string result; + + std::ifstream ifs(tempFile.Path(), std::ios::binary); + if (!ifs) + { + throw std::runtime_error("Tone3000: failed to open response file."); + } + result.assign(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); + return result; +} + +Tone3000DownloaderImpl::PostResult Tone3000DownloaderImpl::Post( + const uri &requestedUri, + std::vector headers, + const std::string &body) +{ + PostResult result; + try + { + int response = CurlPostStrings( + requestedUri.str(), + body, + result.body, + nullptr, + &headers + + ); + result.errorCode = response; + if (response != 200) + { + result.ok = false; + result.error = SS("HTPP Error " << response); + } + else + { + result.ok = true; + } + } + catch (const std::exception &e) + { + result.ok = false; + result.error = e.what(); + } + return result; +} + +Tone3000AuthResponse::Tone3000AuthResponse(const std::string &responseBody) +{ + std::stringstream ss(responseBody); + json_reader reader(ss); + reader.read(this); + expires_at_ = clock_t::now() + std::chrono::seconds(expires_in_); +} + +Tone3000AccessTokens::Tone3000AccessTokens(const Tone3000AuthResponse &authResponse) + : access_token_(authResponse.access_token()), + expires_in_(authResponse.expires_in()), + refresh_token_(authResponse.refresh_token()), + token_type_(authResponse.token_type()) +{ +} + +Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::RequestTone3000Download( + const uri &uri, + const Tone3000PkceParams &pkceParams, + const std::string &downloadPath, + Tone3000DownloadType downloadType) +{ + handle_t handle; + { + std::lock_guard lockGuard{this->mutex}; + handle = NextHandle(); + std::shared_ptr request = + std::make_shared(handle, uri.str(), pkceParams, downloadPath, downloadType); + + this->requestQueue.Put(request); + request = nullptr; + if (!this->thread) + { + this->thread = std::make_unique([this] + { this->ThreadProc(); }); + } + } + return handle; +} + + +static void cancellableSleep(std::chrono::steady_clock::duration duration, std::function &isCancelled) +{ + std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); + while (true) + { + if (std::chrono::steady_clock::now() - start >= duration) + { + break; + } + if (isCancelled()) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } +} + +static std::string mdSanitize(const std::string &str, const std::string &illegalCharacters = "") +{ + std::ostringstream os; + for (char c : str) + { + if ((unsigned char)c < 0x20) + continue; + if (c == '<') + { + os << "<"; + continue; + } + auto npos = illegalCharacters.find_first_of(c); + if (npos != std::string::npos) + { + continue; + } + os << c; + } + return os.str(); +} + +static std::string mdDate(const tone3000_time_point &date_) +{ + std::ostringstream os; + // C++ doesn't handle timezones. Just zap the timezone, and replace it with "Z" + std::string date = date_; + + // Remove timezone offset if present and replace with Z + size_t plusPos = date.find('+'); + if (plusPos != std::string::npos) + { + date = date.substr(0, plusPos) + "Z"; + } + // Parse ISO 8601 date string into tm + std::tm tm = {}; + std::istringstream ss(date); + ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); + if (ss.fail()) + { + os << date; + return os.str(); + } + + os << std::put_time(&tm, "%x"); + return os.str(); +} + +static std::string mdEnum(const std::string &value, const std::map &enumValues) +{ + auto ff = enumValues.find(value); + if (ff == enumValues.end()) + { + return value; + } + return ff->second; +} + +static std::string mdEnumList( + const std::vector &values, + const std::map &enumValues) +{ + if (values.size() == 1) + { + return mdEnum(values[0], enumValues); + } + std::ostringstream os; + os << '['; + bool first = true; + for (const auto &value : values) + { + if (first) + { + first = false; + } + else + { + os << ", "; + } + os << mdEnum(value, enumValues); + } + os << ']'; + return os.str(); +} + +static std::map sizeEnumValues = + { + {"standard", "Standard"}, + {"lite", "Lite"}, + {"feather", "Feather"}, + {"nano", "Nano"}, + {"custom", "Custom"}}; +static std::string mdSizes(const std::vector &sizes) +{ + return mdEnumList(sizes, sizeEnumValues); +} + +static std::map gearEnumValues = + { + {"amp", "Amp only"}, + {"full-rig", "Full rig"}, + {"pedal", "Pedal"}, + {"outboard", "Outboard"}, + {"ir", "I/R"}}; + +static std::string mdGear(const std::string &gear) +{ + return mdEnum(gear, gearEnumValues); +} + +namespace +{ + enum class LicenseFlags + { + None = 0, + Cc = 1, + By = 2, + CcBy = 3, // = Cc | By + Sa = 4, + Nc = 8, + Nd = 16, + Cc0 = 32, + }; + + static bool operator&(LicenseFlags v1, LicenseFlags v2) + { + return (((int)v1) & ((int)v2)) != 0; + } + static LicenseFlags operator|(LicenseFlags v1, LicenseFlags v2) + { + return (LicenseFlags)(((int)(v1)) | ((int)(v2))); + } + + struct LicenseInfo + { + std::string key; + std::string displayName; + std::string url; + LicenseFlags licenseFlags; + }; +} + +static std::vector licenses{ + {.key = "t3k", + .displayName = "T3K", + .url = "licenses/t3k_license.html", + .licenseFlags = LicenseFlags::None}, + {.key = "cc-by", + .displayName = "CC BY 4.0", + .url = "https://creativecommons.org/licenses/by/4.0/", + .licenseFlags = LicenseFlags::CcBy}, + {.key = "cc-by-sa", + .displayName = "CC BY-SA 4.0", + .url = "https://creativecommons.org/licenses/by-sa/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Sa}, + {.key = "cc-by-nc", + .displayName = "CC BY-NC 4.0", + .url = "https://creativecommons.org/licenses/by-nc/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc}, + {.key = "cc-by-nc-sa", + .displayName = "CC BY-NC-SA 4.0", + .url = "https://creativecommons.org/licenses/by-nc-sa/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Sa}, + {.key = "cc-by-nd", + .displayName = "CC BY-ND 4.0", + .url = "https://creativecommons.org/licenses/by-nd/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nd}, + {.key = "cc-by-nc-nd", + .displayName = "CC BY-NC-ND 4.0", + .url = "https://creativecommons.org/licenses/by-nc-nd/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Nd}, + { + .key = "cco", + .displayName = "CC0", + .url = "https://creativecommons.org/publicdomain/zero/1.0/", + .licenseFlags = LicenseFlags::Cc | LicenseFlags::Cc0, + + }, +}; + +static std::map makeLicenseEnum() +{ + std::map result; + for (const auto &license : licenses) + { + result[license.key] = license; + } + return result; +} + +static std::map licenseEnum = makeLicenseEnum(); + +static std::string mdLicenseIcon(LicenseFlags licenseFlag) +{ + std::ostringstream os; + std::string url; + + switch (licenseFlag) + { + case LicenseFlags::Cc: + url = "img/cc.svg"; + break; + case LicenseFlags::By: + url = "img/by.svg"; + break; + case LicenseFlags::Sa: + url = "img/sa.svg"; + break; + case LicenseFlags::Nc: + url = "img/nc.svg"; + break; + case LicenseFlags::Nd: + url = "img/nd.svg"; + break; + case LicenseFlags::Cc0: + url = "img/cc0.svg"; + break; + + case LicenseFlags::None: + throw std::runtime_error("Invalid argument."); + } + os << ""; + return os.str(); +} + +static void mdEscapeString(std::ostream &f, const std::string &str) +{ + size_t ix = 0; + while (ix < str.size()) + { + char c = str[ix++]; + if (c == '\n') + { + if (ix < str.size() && str[ix] == '\n') + { + f << "\n\n"; + } + else + { + f << " \n"; // a
in md. + } + } + else + { + f << c; + } + } +} + +static std::string mdHref(const std::string &url) +{ + std::ostringstream os; + os << '\''; + for (char c : url) + { + if (c == '\'') + { + continue; + } + os << c; + } + os << '\''; + return os.str(); +} +static std::string mdLicense(const std::string &license) +{ + + auto ff = licenseEnum.find(license); + LicenseInfo licenseInfo; + if (ff != licenseEnum.end()) + { + licenseInfo = ff->second; + } + else + { + licenseInfo.displayName = license; + } + + std::ostringstream os; + + if (licenseInfo.licenseFlags != LicenseFlags::None) + { + for (LicenseFlags licenseFlag : std::vector{LicenseFlags::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd}) + { + if (licenseInfo.licenseFlags & licenseFlag) + { + os << mdLicenseIcon(licenseFlag); + } + } + os << " "; + } + + if (licenseInfo.url != "") + { + os << "" << mdSanitize(licenseInfo.displayName) << ""; + } + else + { + os << mdSanitize(licenseInfo.displayName); + } + return os.str(); +} +// static std::string mdTestLicenses() +// { +// std::ostringstream os; +// for (auto license: licenseEnum) +// { +// os << "
" << mdLicense(license.first) << "\n"; +// } +// return os.str(); +// } +static std::map platformEnumValues = + { + {"nam", "NAM"}, + {"ir", "I/R"}, + {"aida-x", "Aida X"}, + {"aa-snapshot", "aa-snapshot"}, + {"proteus", "Proteus"}}; +static std::string mdPlatform(const std::string &platform) +{ + return mdEnum(platform, platformEnumValues); +} +static std::string mdUser(const std::string &user) +{ + return mdSanitize(user); +} + +static void mdLink(std::ostream &f, const std::string &label, const std::string &url) +{ + f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")"; +} + +static void writeReadme(const fs::path &path, const std::string &thumbnailUrl, const Tone3000FileDownloadRequest &download) +{ + // std::ofstream of{path}; + // if (!of.is_open()) + // { + // throw std::runtime_error(SS("Unable to create file " << path)); + // } + + // // clang-format off + // of << + + // "
\n"; + // if (!thumbnailUrl.empty()) + // { + + // of << " \n"; + // } + // of << "
\n" + // << "

" << mdSanitize(download.title()) << "

\n" + // << "
\n" + // << "

\n" + // << mdDate(download.updated_at()) + // << " " + // << mdUser(download.user()) + // << "
\n" + // << " "; + // if (download.sizes().size() != 0) { + // of + // << mdSizes(download.sizes()) << ", " + // ; + // } + // if (download.platform() == "ir") + // { + // of + // << (mdPlatform(download.platform())); + // } else { + // of + // << mdGear(download.gear()) << ", " + // << mdPlatform(download.platform()); + // } + // of << "
\n"; + // if (download.license() != "") { + // of << " License: " << mdLicense(download.license()) << "\n"; + // } + // of + // // << " " << mdTestLicenses() + // << "

\n" + // << "
\n" + // << "
\n" + // << "
\n"; + // // clang-format on + // of << "\n\n"; + // mdEscapeString(of, download.description()); + + // if (download.links().size() > 0) + // { + // of << std::endl; + // of << "## Links" << std::endl + // << std::endl; + // for (auto &link : download.links()) + // { + // of << "- " << "[" << link << "](" << link << ")" << std::endl; + // } + // } +} + +static void CleanUpFailedDownload(const std::filesystem::path &folder, const std::vector &requests) +{ + + std::error_code ec; // do NOT throw errors. + + if (fs::exists(folder)) + { + for (const auto &request : requests) + { + if (fs::exists(request.outputFile)) + { + fs::remove(request.outputFile, ec); // ignoring errors. + } + } + // If there's a readme file, remove that too. + fs::path readmePath = folder / "README.md"; + if (fs::exists(readmePath)) + { + fs::remove(readmePath, ec); // ignoring errors + } + + // if the folder directory is empty, remove the the folder too. + try + { + if (fs::is_empty(folder)) + { + fs::remove(folder, ec); // ignoring errors. + } + } + catch (const std::exception &) + { + } + } +} + +static std::string GetHeader(const std::string &headerName, const std::vector &htmlHeaders) +{ + std::string needle = headerName; + std::transform(needle.begin(), needle.end(), needle.begin(), [](unsigned char c) + { return std::tolower(c); }); + + for (const auto &header : htmlHeaders) + { + auto pos = header.find(':'); + if (pos == std::string::npos) + { + continue; + } + std::string name = header.substr(0, pos); + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) + { return std::tolower(c); }); + if (name != needle) + { + continue; + } + std::string value = header.substr(pos + 1); + auto first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) + { + return ""; + } + auto last = value.find_last_not_of(" \t\r\n"); + return value.substr(first, last - first + 1); + } + return ""; +} + +static bool CancellableSleep(std::chrono::steady_clock::duration duration, std::function &isCancelled) +{ + using clock_t = std::chrono::steady_clock; + + auto start = clock_t::now(); + while (true) + { + auto now = clock_t::now(); + auto elapsed = now - start; + if (elapsed > duration) + { + return true; + } + if (isCancelled()) + { + return false; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } +} +std::string Tone3000DownloaderImpl::DownloadTone3000Files( + const Tone3000FileDownloadRequest &request, + Tone3000DownloadProgress &progress) +{ + + // fs::path bundlePath = fs::path(request.downloadPath()) / StringToSafeFilename(request.title()); + // fs::path resultDirectory = bundlePath; + + // fs::create_directories(bundlePath); + + // std::vector requests; + + // auto&models = request.models(); + + // std::vector headers; // yyy: make sure we have a user agent header! + + // headers.push_back( + // SS("Authorization: Bearer " << request.authToken()) + // ); + // try + // { + // size_t downloadIndex = 0; + // while (downloadIndex < models.size()) + // { + // if (isCancelled()) + // { + // throw std::runtime_error("Cancelled"); + // } + // size_t thisTime = models.size() - downloadIndex; + // thisTime = Tone3000Throttler::instance().ReserveDownloadSlots(thisTime); + // if (thisTime != 0) + // { + // std::vector requestsThisTime; + // for (size_t i = 0; i < thisTime; ++i) + // { + // auto &model = models[downloadIndex+i]; + + // requestsThisTime.push_back( + // CurlDownloadRequest{ + // url: model.url(), + // outputFile: bundlePath / StringToSafeFilename(model.name()) + // }); + // } + + // int result = CurlGet( + // requestsThisTime, + // [&](size_t completed, size_t total) + // { + // if (isCancelled()) + // { + // return false; + // } + // progress.progress(completed + downloadIndex); + // progress.total(requests.size()); + // progressCallback(progress); + // return true; + // }, + // &headers); + + // if (isCancelled()) + // { + // throw std::runtime_error("Cancelled"); + // } + + // if (result != 200) + // { + // throw std::runtime_error(SS("Server download failed. HTTP Error " << HtmlHelper::httpErrorString(result))); + // } + + // downloadIndex += thisTime; + // } + // if (thisTime <= 1) + // { + // CancellableSleep(std::chrono::milliseconds(1000), isCancelled); + // } + // } + + // fs::path readmePath = bundlePath / "README.md"; + + // std::string thumbnailUrl; + // if (!request.imageUrl().empty()) + // { + + // TemporaryFile imageFile{TONE3000_THUMBNAIL_PATH}; + + // std::string imageUrl = request.imageUrl(); + // std::vector imageHeaders; + // try + // { + // size_t reserveCount; + // while (true) + // { + // reserveCount = Tone3000Throttler::instance().ReserveDownloadSlots(1); + // if (reserveCount != 0) + // { + // break; + // } + // std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + // if (isCancelled()) + // { + // throw std::runtime_error("Cancelled"); + // } + // } + // if (CurlGet(imageUrl, imageFile.Path(), &imageHeaders) == 200) + // { + // std::string mediaType = GetHeader("Content-Type", imageHeaders); + // std::string extension; + // if (mediaType == "image/jpeg") + // { + // extension = ".jpeg"; + // } + // else if (mediaType == "image/webp") + // { + // extension = ".webp"; + // } + // else if (mediaType == "image/png") + // { + // extension = ".png"; + // } + // else + // { + // Lv2Log::warning("Tone3000 thumbnail has an unexpected media type of '%s'", mediaType.c_str()); + // throw std::runtime_error("Invalid thumbnail media type."); + // } + // if (mediaType == "image/jpeg") + // { + // fs::path thumnbailTempPath = imageFile.Detach(); + // fs::path finalPath = TONE3000_THUMBNAIL_PATH / SS(request.id() << extension); + // if (fs::exists(finalPath)) + // { + // fs::remove(finalPath); + // } + // fs::create_directories(finalPath.parent_path()); + // fs::rename(thumnbailTempPath, finalPath); + // thumbnailUrl = SS("var/tone3000_thumbnail?id=" << request.id()); + // } + // } + // } + // catch (const std::exception &) + // { + // // ignore. + // } + // } + // writeReadme(readmePath, thumbnailUrl, request); + // } + // catch (const std::exception &e) + // { + // CleanUpFailedDownload(bundlePath, requests); + // if (isCancelled()) + // { + // return ""; + // } + // throw; + // } + + // return resultDirectory; + throw std::runtime_error("Not implemented"); +} + + +std::shared_ptr Tone3000DownloaderImpl::GetTone3000Tone( + int64_t toneId) +{ + std::string jsonResult = Tone3000GetText(SS("https://www.tone3000.com/api/v1/tones/" << toneId)); + std::shared_ptr tone = std::make_shared(jsonResult); + + return tone; +} + +Tone3000PkceParams::Tone3000PkceParams( + const std::string &redirectUrl) +{ + + this->publishableKey_ = PIPEDAL_T3K_PUBLISHABLE_KEY; + this->redirectUrl_ = redirectUrl; + this->codeVerifier_ = RandomBase64Url(32); + this->codeChallenge_ = Sha256Base64Url(this->codeVerifier_); + this->state_ = RandomBase64Url(16); +} + + + + + + + +////////////////////////////////////////////////////////////////////////////////// + +JSON_MAP_BEGIN(Tone3000PkceParams) +JSON_MAP_REFERENCE(Tone3000PkceParams, publishableKey) +JSON_MAP_REFERENCE(Tone3000PkceParams, redirectUrl) +JSON_MAP_REFERENCE(Tone3000PkceParams, codeVerifier) +JSON_MAP_REFERENCE(Tone3000PkceParams, codeChallenge) +JSON_MAP_REFERENCE(Tone3000PkceParams, state) +JSON_MAP_END(); + +JSON_MAP_BEGIN(Tone3000ModelInfo) +JSON_MAP_REFERENCE(Tone3000ModelInfo, url) +JSON_MAP_REFERENCE(Tone3000ModelInfo, name) +JSON_MAP_END(); + +// JSON_MAP_BEGIN(Tone3000FileDownloadRequest) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, downloadType) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, downloadPath) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, id) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, codeVerifier) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, state) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, codeChallenge) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, authToken) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, title) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, description) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, imageUrl) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, models) + +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, updated_at) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, user) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, sizes) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, platform) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, gear) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, license) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, links) +// JSON_MAP_END(); + +JSON_MAP_BEGIN(Tone3000AuthResponse) +JSON_MAP_REFERENCE(Tone3000AuthResponse, access_token) +JSON_MAP_REFERENCE(Tone3000AuthResponse, expires_in) +JSON_MAP_REFERENCE(Tone3000AuthResponse, refresh_token) +JSON_MAP_REFERENCE(Tone3000AuthResponse, token_type) +JSON_MAP_REFERENCE(Tone3000AuthResponse, tone_id) +JSON_MAP_REFERENCE(Tone3000AuthResponse, model_id) +JSON_MAP_REFERENCE(Tone3000AuthResponse, canceled) +JSON_MAP_END() diff --git a/src/Tone3000Downloader.hpp b/src/Tone3000Downloader.hpp new file mode 100644 index 0000000..8d05cb8 --- /dev/null +++ b/src/Tone3000Downloader.hpp @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2026 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 "Tone3000DownloadProgress.hpp" +#include "Tone3000DownloadType.hpp" +#include "json.hpp" +#include "Uri.hpp" + +namespace pipedal +{ + + class Tone3000PkceParams + { + private: + std::string publishableKey_; + std::string redirectUrl_; + std::string codeVerifier_; + std::string codeChallenge_; + std::string state_; + + public: + Tone3000PkceParams() = default; + Tone3000PkceParams(const std::string &redirectUrl); + + JSON_GETTER_SETTER_REF(publishableKey); + JSON_GETTER_SETTER_REF(redirectUrl); + JSON_GETTER_SETTER_REF(codeVerifier); + JSON_GETTER_SETTER_REF(codeChallenge); + JSON_GETTER_SETTER_REF(state); + + DECLARE_JSON_MAP(Tone3000PkceParams); + }; + class Tone3000ModelInfo + { + private: + std::string url_; + std::string name_; + + public: + JSON_GETTER_SETTER_REF(url); + JSON_GETTER_SETTER_REF(name); + + DECLARE_JSON_MAP(Tone3000ModelInfo); + }; + + std::string Sha256Base64Url(const std::string&input); + + // class Tone3000DownloadRequest + // { + // private: + // int64_t downloadType_; + // std::string downloadPath_; + // std::string codeVerifier_; + // std::string state_; + // std::string codeChallenge_; + + // std::string authToken_; + // int64_t id_; + // std::string title_; + // std::string description_; + // std::string imageUrl_; + // std::vector models_; + + // std::string updated_at_; + // std::string user_; + // std::vector sizes_; + // std::string platform_; + // std::string gear_; + // std::string license_; + // std::vector links_; + + // public: + // Tone3000DownloadType downloadType() const { return (Tone3000DownloadType)downloadType_;} + // void downloadType(Tone3000DownloadType value) { downloadType_= (int64_t)value;} + // JSON_GETTER_SETTER_REF(downloadPath); + // JSON_GETTER_SETTER(id); + // JSON_GETTER_SETTER(codeVerifier); + // JSON_GETTER_SETTER(state); + // JSON_GETTER_SETTER(codeChallenge); + // JSON_GETTER_SETTER(authToken); + // JSON_GETTER_SETTER_REF(title); + // JSON_GETTER_SETTER_REF(description) + // JSON_GETTER_SETTER_REF(imageUrl); + // JSON_GETTER_SETTER_REF(models); + + // JSON_GETTER_SETTER_REF(updated_at); + // JSON_GETTER_SETTER_REF(user); + // JSON_GETTER_SETTER_REF(sizes); + // JSON_GETTER_SETTER_REF(platform); + // JSON_GETTER_SETTER_REF(gear); + // JSON_GETTER_SETTER_REF(license); + // JSON_GETTER_SETTER_REF(links); + + // DECLARE_JSON_MAP(Tone3000DownloadRequest); + // }; + + class Tone3000Downloader + { + protected: + Tone3000Downloader(); + + public: + using handle_t = int64_t; + + virtual ~Tone3000Downloader(); + + class Listener + { + public: + virtual void OnStartTone3000Download( + handle_t handle, + const std::string &title) = 0; + virtual void OnTone3000Progress( + const Tone3000DownloadProgress &downloadProgress) = 0; + virtual void OnTone3000DownloadComplete( + handle_t handle, + const std::string &resultPath) = 0; + + virtual void OnTone3000DownloadError( + handle_t handle, + const std::string &errorMessage) = 0; + + virtual std::string Tone3000ThumbnailDirectory() = 0; + }; + + using self = Tone3000Downloader; + static std::shared_ptr Create(); + + virtual void SetListener(Listener *listener) = 0; + + virtual void CancelDownload( + handle_t handle) = 0; + + virtual handle_t RequestTone3000Download( + const uri &uri, + const Tone3000PkceParams &pkceParams, + const std::string &downloadPath, + Tone3000DownloadType downloadType) = 0; + + virtual void Close() = 0; + virtual Tone3000DownloadProgress GetDownloadStatus() = 0; + }; +} \ No newline at end of file diff --git a/src/Tone3000DownloaderImpl.hpp b/src/Tone3000DownloaderImpl.hpp new file mode 100644 index 0000000..f354eb3 --- /dev/null +++ b/src/Tone3000DownloaderImpl.hpp @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2026 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 "Tone3000Downloader.hpp" +#include +#include +#include +#include +#include +#include "ThreadedQueue.hpp" +#include "Tone3000Download.hpp" + +namespace pipedal +{ + + struct CachedPkceParams + { + std::chrono::steady_clock::time_point time; + Tone3000PkceParams pkceParams; + }; + + class Tone3000AuthResponse + { + private: + std::string access_token_; + int64_t expires_in_ = -1; + std::string refresh_token_; + std::string token_type_; + std::optional tone_id_; + std::optional model_id_; + bool canceled_ = false; + + using clock_t = std::chrono::steady_clock; + clock_t::time_point expires_at_; + + public: + Tone3000AuthResponse(const std::string &responseBody); + JSON_GETTER_REF(access_token); + JSON_GETTER(expires_in); + JSON_GETTER_REF(refresh_token); + JSON_GETTER_REF(token_type); + JSON_GETTER_REF(expires_at); + JSON_GETTER_REF(tone_id); + JSON_GETTER_REF(model_id); + JSON_GETTER_SETTER(canceled); + + DECLARE_JSON_MAP(Tone3000AuthResponse); + }; + class Tone3000AccessTokens + { + private: + std::string access_token_; + int64_t expires_in_ = -1; + std::string refresh_token_; + std::string token_type_; + + using clock_t = std::chrono::steady_clock; + clock_t::time_point expires_at_; + + public: + Tone3000AccessTokens(const Tone3000AuthResponse &authResponse); + JSON_GETTER_REF(access_token); + JSON_GETTER(expires_in); + JSON_GETTER_REF(refresh_token); + JSON_GETTER_REF(token_type); + JSON_GETTER_REF(expires_at); + }; + + class Tone3000DownloaderImpl : public Tone3000Downloader + { + public: + Tone3000DownloaderImpl(); + + virtual ~Tone3000DownloaderImpl(); + + virtual void SetListener(Listener *listener) override; + + virtual void CancelDownload( + handle_t handle) override; + + virtual void Close() override; + virtual Tone3000DownloadProgress GetDownloadStatus() override; + + virtual handle_t RequestTone3000Download( + const uri &uri, + const Tone3000PkceParams &pkceParams, + const std::string &downloadPath, + Tone3000DownloadType downloadType) override; + + private: + struct DownloadRequest + { + handle_t handle = -1; + std::string requestUri; + Tone3000PkceParams pkceParams; + std::string downloadPath; + Tone3000DownloadType downloadType = Tone3000DownloadType::Nam; + std::atomic cancelled = false; + }; + + void DownloadTone3000ToneBg( + std::shared_ptr request); + void OnTone3000DownloadError(int64_t handle, const std::string &error) + { + if (listener) + { + listener->OnTone3000DownloadError(handle, error); + } + } + std::shared_ptr GetTone3000Tone( + int64_t toneId); + std::string DownloadTone3000Files( + const Tone3000FileDownloadRequest &request, + Tone3000DownloadProgress &progress); + + void OnTone3000DownloadComplete(int64_t handle, const std::string &path) + { + if (listener) + { + listener->OnTone3000DownloadComplete(handle, path); + } + } + void OnTone3000DownloadCancelled(int64_t handle) + { + if (listener) + { + listener->OnTone3000DownloadComplete(handle, ""); + } + } + using clock_t = std::chrono::steady_clock; + std::recursive_mutex pkceCacheMutex; + + Listener *listener = nullptr; + + void ThreadProc(); + + + // Performs the actual download with cancellation support + // std::string PerformFileDownloads( + // const Tone3000FileDownloadRequest &downloadRequets, + // Tone3000DownloadProgress &progress, + // std::function updateProgress, + // std::function isCancelled); + + struct PostResult + { + bool ok = false; + int errorCode = 0; + std::string error; + std::string body; + }; + + PostResult Post( + const uri &requestedUri, + std::vector headers, + const std::string &body); + + + std::string Tone3000GetText( + const uri &requestedUri + ); + struct OAuthCallbackResult + { + std::optional toneId; + std::optional modelId; + }; + + OAuthCallbackResult handleOAuthCallback( + const uri &callbackUri, + const Tone3000PkceParams &pkce); + + std::atomic closed = false; + handle_t nextHandle = 1; + handle_t NextHandle(); + + ThreadedQueue requestQueue; + + Tone3000DownloadProgress fgDownloadProgress; + void bgUpdateDownloadProgress(const Tone3000DownloadProgress &progress); + + std::shared_ptr activeRequest; + + std::mutex mutex; + + std::unique_ptr thread; + std::shared_ptr accessTokens; + + std::string GetBearerToken() const; + }; +} diff --git a/src/Tone3000Throttler.cpp b/src/Tone3000Throttler.cpp new file mode 100644 index 0000000..de27b65 --- /dev/null +++ b/src/Tone3000Throttler.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026 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 "Tone3000Throttler.hpp" + +using namespace pipedal; + +static constexpr size_t THROTTLING_LIMIT = 80; +static constexpr size_t BULK_DOWNLOAD_LIMIT = 20; + + +Tone3000Throttler Tone3000Throttler::instance_; + +void Tone3000Throttler::ClearExpiredDownloads_() +{ + auto expiryTime = clock_t::now(); + expiryTime -= std::chrono::seconds(60); + + while (downloadTimes.size() > 0 && downloadTimes[0] < expiryTime) { + downloadTimes.erase(downloadTimes.begin()); + } + +} +void Tone3000Throttler::AddDownload() +{ + std::lock_guard lock{m_mutex}; + this->downloadTimes.push_back(clock_t::now()); +} + +bool Tone3000Throttler::IsThrottled() +{ + std::lock_guard lock{m_mutex}; + + ClearExpiredDownloads_(); + return this->downloadTimes.size() >= THROTTLING_LIMIT; +} + +size_t Tone3000Throttler::ReserveDownloadSlots(size_t requested) +{ + ClearExpiredDownloads_(); + size_t result; + + size_t reserved = this->downloadTimes.size(); + if (reserved == THROTTLING_LIMIT) { + return 0; + } + if (reserved < BULK_DOWNLOAD_LIMIT) + { + if (requested+reserved >= BULK_DOWNLOAD_LIMIT) + { + result = BULK_DOWNLOAD_LIMIT-reserved; + if (result == 0) + { + result = 1; + } + } else { + result = requested; + } + } else { + result = 1; // download one at a time! + } + auto now = clock_t::now(); + for (size_t i = 0; i < result; ++i) + { + this->downloadTimes.push_back(now); + } + return result; +} + +size_t Tone3000Throttler::AvailableDownloadSlots() { + ClearExpiredDownloads_(); + return THROTTLING_LIMIT-this->downloadTimes.size(); +} \ No newline at end of file diff --git a/src/Tone3000Throttler.hpp b/src/Tone3000Throttler.hpp new file mode 100644 index 0000000..3accf74 --- /dev/null +++ b/src/Tone3000Throttler.hpp @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 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 + +namespace pipedal +{ + class Tone3000Throttler + { + private: + Tone3000Throttler() = default; + Tone3000Throttler(const Tone3000Throttler&) = delete; + Tone3000Throttler(Tone3000Throttler&&) = delete; + Tone3000Throttler&operator=(const Tone3000Throttler&) = delete; + + public: + static Tone3000Throttler&instance() { return instance_; } + + using clock_t = std::chrono::steady_clock; + + void AddDownload(); + size_t ReserveDownloadSlots(size_t requested); + size_t AvailableDownloadSlots(); + bool IsThrottled(); + + private: + std::mutex m_mutex; + static Tone3000Throttler instance_; + + void ClearExpiredDownloads_(); + std::vector downloadTimes; + }; + +} \ No newline at end of file diff --git a/src/Tone3000Tone.cpp b/src/Tone3000Tone.cpp new file mode 100644 index 0000000..a5ef391 --- /dev/null +++ b/src/Tone3000Tone.cpp @@ -0,0 +1,487 @@ +/* + * Copyright (c) 2026 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 "Tone3000Tone.hpp" +#include "ss.hpp" + + + +using namespace pipedal; +using namespace pipedal::tone3000; +namespace fs = std::filesystem; + + +Tone::Tone(const std::string &json) +{ + std::istringstream ss(json); + json_reader reader(ss); + reader.read(this); +} + +namespace +{ + static std::string mdSanitize(const std::string &str, const std::string &illegalCharacters = "") + { + std::ostringstream os; + for (char c : str) + { + if ((unsigned char)c < 0x20) + continue; + if (c == '<') + { + os << "<"; + continue; + } + auto npos = illegalCharacters.find_first_of(c); + if (npos != std::string::npos) + { + continue; + } + os << c; + } + return os.str(); + } + + static std::string mdDate(const tone3000_time_point &date_) + { + std::ostringstream os; + // C++ doesn't handle timezones. Just zap the timezone, and replace it with "Z" + std::string date = date_; + + // Remove timezone offset if present and replace with Z + size_t plusPos = date.find('+'); + if (plusPos != std::string::npos) + { + date = date.substr(0, plusPos) + "Z"; + } + // Parse ISO 8601 date string into tm + std::tm tm = {}; + std::istringstream ss(date); + ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); + if (ss.fail()) + { + os << date; + return os.str(); + } + + os << std::put_time(&tm, "%x"); + return os.str(); + } + + static std::string mdEnum(const std::string &value, const std::map &enumValues) + { + auto ff = enumValues.find(value); + if (ff == enumValues.end()) + { + return value; + } + return ff->second; + } + + static std::string mdEnumList( + const std::vector &values, + const std::map &enumValues) + { + if (values.size() == 1) + { + return mdEnum(values[0], enumValues); + } + std::ostringstream os; + os << '['; + bool first = true; + for (const auto &value : values) + { + if (first) + { + first = false; + } + else + { + os << ", "; + } + os << mdEnum(value, enumValues); + } + os << ']'; + return os.str(); + } + + static std::map sizeEnumValues = + { + {"standard", "Standard"}, + {"lite", "Lite"}, + {"feather", "Feather"}, + {"nano", "Nano"}, + {"custom", "Custom"}}; + static std::string mdSizes(const std::vector &sizes) + { + return mdEnumList(sizes, sizeEnumValues); + } + + static std::map gearEnumValues = + { + {"amp", "Amp only"}, + {"full-rig", "Full rig"}, + {"pedal", "Pedal"}, + {"outboard", "Outboard"}, + {"ir", "I/R"}}; + + static std::string mdGear(const std::string &gear) + { + return mdEnum(gear, gearEnumValues); + } + + namespace + { + enum class LicenseFlags + { + None = 0, + Cc = 1, + By = 2, + CcBy = 3, // = Cc | By + Sa = 4, + Nc = 8, + Nd = 16, + Cc0 = 32, + }; + + static bool operator&(LicenseFlags v1, LicenseFlags v2) + { + return (((int)v1) & ((int)v2)) != 0; + } + static LicenseFlags operator|(LicenseFlags v1, LicenseFlags v2) + { + return (LicenseFlags)(((int)(v1)) | ((int)(v2))); + } + + struct LicenseInfo + { + std::string key; + std::string displayName; + std::string url; + LicenseFlags licenseFlags; + }; + } + + static std::vector licenses{ + {.key = "t3k", + .displayName = "T3K", + .url = "licenses/t3k_license.html", + .licenseFlags = LicenseFlags::None}, + {.key = "cc-by", + .displayName = "CC BY 4.0", + .url = "https://creativecommons.org/licenses/by/4.0/", + .licenseFlags = LicenseFlags::CcBy}, + {.key = "cc-by-sa", + .displayName = "CC BY-SA 4.0", + .url = "https://creativecommons.org/licenses/by-sa/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Sa}, + {.key = "cc-by-nc", + .displayName = "CC BY-NC 4.0", + .url = "https://creativecommons.org/licenses/by-nc/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc}, + {.key = "cc-by-nc-sa", + .displayName = "CC BY-NC-SA 4.0", + .url = "https://creativecommons.org/licenses/by-nc-sa/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Sa}, + {.key = "cc-by-nd", + .displayName = "CC BY-ND 4.0", + .url = "https://creativecommons.org/licenses/by-nd/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nd}, + {.key = "cc-by-nc-nd", + .displayName = "CC BY-NC-ND 4.0", + .url = "https://creativecommons.org/licenses/by-nc-nd/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Nd}, + { + .key = "cco", + .displayName = "CC0", + .url = "https://creativecommons.org/publicdomain/zero/1.0/", + .licenseFlags = LicenseFlags::Cc | LicenseFlags::Cc0, + + }, + }; + + static std::map makeLicenseEnum() + { + std::map result; + for (const auto &license : licenses) + { + result[license.key] = license; + } + return result; + } + + static std::map licenseEnum = makeLicenseEnum(); + + static std::string mdLicenseIcon(LicenseFlags licenseFlag) + { + std::ostringstream os; + std::string url; + + switch (licenseFlag) + { + case LicenseFlags::Cc: + url = "img/cc.svg"; + break; + case LicenseFlags::By: + url = "img/by.svg"; + break; + case LicenseFlags::Sa: + url = "img/sa.svg"; + break; + case LicenseFlags::Nc: + url = "img/nc.svg"; + break; + case LicenseFlags::Nd: + url = "img/nd.svg"; + break; + case LicenseFlags::Cc0: + url = "img/cc0.svg"; + break; + + case LicenseFlags::None: + throw std::runtime_error("Invalid argument."); + } + os << ""; + return os.str(); + } + + static void mdEscapeString(std::ostream &f, const std::string &str) + { + size_t ix = 0; + while (ix < str.size()) + { + char c = str[ix++]; + if (c == '\n') + { + if (ix < str.size() && str[ix] == '\n') + { + f << "\n\n"; + } + else + { + f << " \n"; // a
in md. + } + } + else + { + f << c; + } + } + } + + static std::string mdHref(const std::string &url) + { + std::ostringstream os; + os << '\''; + for (char c : url) + { + if (c == '\'') + { + continue; + } + os << c; + } + os << '\''; + return os.str(); + } + static std::string mdLicense(const std::string &license) + { + + auto ff = licenseEnum.find(license); + LicenseInfo licenseInfo; + if (ff != licenseEnum.end()) + { + licenseInfo = ff->second; + } + else + { + licenseInfo.displayName = license; + } + + std::ostringstream os; + + if (licenseInfo.licenseFlags != LicenseFlags::None) + { + for (LicenseFlags licenseFlag : std::vector{LicenseFlags::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd}) + { + if (licenseInfo.licenseFlags & licenseFlag) + { + os << mdLicenseIcon(licenseFlag); + } + } + os << " "; + } + + if (licenseInfo.url != "") + { + os << "" << mdSanitize(licenseInfo.displayName) << ""; + } + else + { + os << mdSanitize(licenseInfo.displayName); + } + return os.str(); + } + // static std::string mdTestLicenses() + // { + // std::ostringstream os; + // for (auto license: licenseEnum) + // { + // os << "
" << mdLicense(license.first) << "\n"; + // } + // return os.str(); + // } + static std::map platformEnumValues = + { + {"nam", "NAM"}, + {"ir", "I/R"}, + {"aida-x", "Aida X"}, + {"aa-snapshot", "aa-snapshot"}, + {"proteus", "Proteus"}}; + static std::string mdPlatform(const std::string &platform) + { + return mdEnum(platform, platformEnumValues); + } + static std::string mdUser(const tone3000::User &user) + { + return mdSanitize(user.username()); + } + + static void mdLink(std::ostream &f, const std::string &label, const std::string &url) + { + f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")"; + } + +}; + +void pipedal::tone3000::WriteTone3000Readme(const std::filesystem::path &filePath, const tone3000::Tone &tone, const std::string &thumbnailUrl_) +{ + std::string thumbnailUrl = SS("/var/tone3000_thumbnail?id=" << tone.id()); + std::ofstream of{filePath}; + if (!of.is_open()) + { + throw std::runtime_error(SS("Unable to create file " << filePath)); + } + + // clang-format off + of << + + "
\n"; + if (!thumbnailUrl.empty()) + { + + of << " \n"; + } + of << "
\n" + << "

" << mdSanitize(tone.title()) << "

\n" + << "
\n" + << "

\n" + << mdDate(tone.updated_at()) + << " " + << mdUser(tone.user()) + << "
\n" + << " "; + if (tone.sizes().has_value() && tone.sizes().value().size() != 0) { + of + << mdSizes(tone.sizes().value()) << ", " + ; + } + if (tone.platform() == "ir") + { + of + << (mdPlatform(tone.platform())); + } else { + of + << mdGear(tone.gear()) << ", " + << mdPlatform(tone.platform()); + } + of << "
\n"; + if (tone.license() != "") { + of << " License: " << mdLicense(tone.license()) << "\n"; + } + of + // << " " << mdTestLicenses() + << "

\n" + << "
\n" + << "
\n" + << "
\n"; + // clang-format on + of << "\n\n"; + mdEscapeString(of, tone.description()); + + if (tone.links().size() > 0) + { + of << std::endl; + of << "## Links" << std::endl + << std::endl; + for (auto &link : tone.links()) + { + of << "- " << "[" << link << "](" << link << ")" << std::endl; + } + } +} + +JSON_MAP_BEGIN(Model) +JSON_MAP_REFERENCE(Model, id) +JSON_MAP_REFERENCE(Model, name) +JSON_MAP_REFERENCE(Model, model_url) +JSON_MAP_REFERENCE(Model, created_at) +JSON_MAP_REFERENCE(Model, size) +JSON_MAP_REFERENCE(Model, user_id) +JSON_MAP_END() + +JSON_MAP_BEGIN(User) +JSON_MAP_REFERENCE(User, id) +JSON_MAP_REFERENCE(User, username) +JSON_MAP_REFERENCE(User, avatar_url) +JSON_MAP_REFERENCE(User, url) +JSON_MAP_END() + +JSON_MAP_BEGIN(Tone) +JSON_MAP_REFERENCE(Tone, id) +JSON_MAP_REFERENCE(Tone, user_id) +JSON_MAP_REFERENCE(Tone, title) +JSON_MAP_REFERENCE(Tone, description) +JSON_MAP_REFERENCE(Tone, created_at) +JSON_MAP_REFERENCE(Tone, updated_at) +JSON_MAP_REFERENCE(Tone, gear) +JSON_MAP_REFERENCE(Tone, images) +JSON_MAP_REFERENCE(Tone, is_public) +JSON_MAP_REFERENCE(Tone, links) +JSON_MAP_REFERENCE(Tone, platform) +JSON_MAP_REFERENCE(Tone, models_count) +JSON_MAP_REFERENCE(Tone, favorites_count) +JSON_MAP_REFERENCE(Tone, downloads_count) +JSON_MAP_REFERENCE(Tone, license) +JSON_MAP_REFERENCE(Tone, sizes) +JSON_MAP_REFERENCE(Tone, a1_models_count) +JSON_MAP_REFERENCE(Tone, a2_models_count) +JSON_MAP_REFERENCE(Tone, irs_count) +JSON_MAP_REFERENCE(Tone, custom_models_count) +JSON_MAP_REFERENCE(Tone, user) +JSON_MAP_REFERENCE(Tone, url) +JSON_MAP_REFERENCE(Tone, user) +JSON_MAP_END(); diff --git a/src/Tone3000Tone.hpp b/src/Tone3000Tone.hpp new file mode 100644 index 0000000..efab74a --- /dev/null +++ b/src/Tone3000Tone.hpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026 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 +#include +#include "json.hpp" +#include +#include +#include +#include "Tone3000DownloadProgress.hpp" +#include + +namespace pipedal +{ + namespace tone3000 + { + + using tone3000_time_point = std::string; // Date in ISO 8601 format. (C++ doesn't handle timezones) + + class Model + { + private: + int64_t id_ = -1; + std::string name_; + std::string model_url_; + tone3000_time_point created_at_; + std::optional size_; + std::string user_id_; + + public: + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(name) + JSON_GETTER_SETTER_REF(model_url) + JSON_GETTER_SETTER_REF(created_at) + JSON_GETTER_SETTER_REF(size) + JSON_GETTER_SETTER_REF(user_id) + + DECLARE_JSON_MAP(Model); + }; + + class User + { + private: + std::string id_; + std::optional avatar_url_; + std::string username_; + std::string url_; + + public: + JSON_GETTER_SETTER_REF(id) + JSON_GETTER_SETTER(username) + JSON_GETTER_SETTER(url) + + DECLARE_JSON_MAP(User); + }; + + class Tone + { + private: + int64_t id_ = -1; + std::string user_id_; + std::string title_; + std::string description_; + tone3000_time_point created_at_; + tone3000_time_point updated_at_; + std::string gear_; + std::vector images_; + bool is_public_ = true; + std::vector links_; + std::string platform_; + int64_t models_count_ = 0; + int64_t favorites_count_ = 0; + int64_t downloads_count_ = 0; + std::string license_; + std::optional> sizes_; + int64_t a1_models_count_ = 0; + int64_t a2_models_count_ = 0; + int64_t irs_count_ = 0; + int64_t custom_models_count_ = 0; + User user_; + std::string url_; + + public: + Tone() = default; + Tone(const std::string &json); + + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(user_id) + JSON_GETTER_SETTER_REF(title) + JSON_GETTER_SETTER_REF(description) + JSON_GETTER_SETTER_REF(created_at) + JSON_GETTER_SETTER_REF(updated_at) + JSON_GETTER_SETTER_REF(gear) + JSON_GETTER_SETTER_REF(images) + JSON_GETTER_SETTER(is_public) + JSON_GETTER_SETTER_REF(links) + JSON_GETTER_SETTER_REF(platform) + JSON_GETTER_SETTER(models_count) + JSON_GETTER_SETTER(favorites_count) + JSON_GETTER_SETTER(downloads_count) + JSON_GETTER_SETTER_REF(license) + JSON_GETTER_SETTER_REF(sizes) + JSON_GETTER_SETTER_REF(a1_models_count) + JSON_GETTER_SETTER_REF(a2_models_count) + JSON_GETTER_SETTER_REF(irs_count) + JSON_GETTER_SETTER_REF(custom_models_count) + JSON_GETTER_SETTER_REF(user) + JSON_GETTER_SETTER_REF(url) + + DECLARE_JSON_MAP(Tone); + }; + + void WriteTone3000Readme(const std::filesystem::path &filePath, const tone3000::Tone &tone, const std::string &thumbnailUrl); + + + } +} diff --git a/src/Updater.cpp b/src/Updater.cpp index ba2d8a8..20b22a0 100644 --- a/src/Updater.cpp +++ b/src/Updater.cpp @@ -819,7 +819,10 @@ UpdateStatus UpdaterImpl::DoUpdate(bool forReleaseGenerator) GithubRelease release{el}; if (!release.draft && release.GetDownloadForCurrentArchitecture() != nullptr) { - if (release.name.find("Experimental") == std::string::npos) // experimental releases do not participate in auto-updates (not even for dev stream) + if (release.name.find("Experimental") == std::string::npos // experimental releases do not participate in auto-updates (not even for dev stream) + && release.name.find("Testing") == std::string::npos + && release.name.find("Retracted") == std::string::npos + ) { releases.push_back(std::move(release)); } diff --git a/src/Uri.cpp b/src/Uri.cpp index 9061189..5cac8e9 100644 --- a/src/Uri.cpp +++ b/src/Uri.cpp @@ -29,55 +29,61 @@ std::string uri::segment(int index) const const char *p = path_start; int segment = 0; - if (p != path_end && *p == '/') ++p; + if (p != path_end && *p == '/') + ++p; while (p != path_end && *p != '?' && *p != '#') { if (segment == index) { - const char*segmentStart = p; - while(p != path_end && *p != '/' && *p != '?' && *p != '#') + const char *segmentStart = p; + while (p != path_end && *p != '/' && *p != '?' && *p != '#') { ++p; } - return HtmlHelper::decode_url_segment(segmentStart,p); - } else { - while(p != path_end && *p != '/' && *p != '?' && *p != '#') + return HtmlHelper::decode_url_segment(segmentStart, p); + } + else + { + while (p != path_end && *p != '/' && *p != '?' && *p != '#') { ++p; } } - if (p != path_end && *p == '/') ++p; + if (p != path_end && *p == '/') + ++p; ++segment; } throw std::invalid_argument("Invalid segement number."); }; - -int uri::segment_count() const { +int uri::segment_count() const +{ const char *p = path_start; int segment = 0; - if (p != path_end && *p == '/') ++p; + if (p != path_end && *p == '/') + ++p; while (p != path_end && *p != '?' && *p != '#') { - while(p != path_end && *p != '/' && *p != '?' && *p != '#') + while (p != path_end && *p != '/' && *p != '?' && *p != '#') { ++p; } - if (p != path_end && *p == '/') ++p; + if (p != path_end && *p == '/') + ++p; ++segment; } return segment; } -void uri::set(const char*start, const char*end) +void uri::set(const char *start, const char *end) { - this->text = std::string(start,end); + this->text = std::string(start, end); set_(); } -void uri::set(const char*text) +void uri::set(const char *text) { this->text = text; set_(); @@ -85,10 +91,10 @@ void uri::set(const char*text) void uri::set_() { - const char*start = this->text.c_str(); - const char*end = start+this->text.size(); + const char *start = this->text.c_str(); + const char *end = start + this->text.size(); - const char* p = start; + const char *p = start; this->isRelative = true; @@ -96,7 +102,8 @@ void uri::set_() while (p != end) { char c = *p; - if (c == ':') { + if (c == ':') + { hasScheme = true; break; } @@ -111,12 +118,14 @@ void uri::set_() scheme_start = start; scheme_end = p; ++p; - } else { + } + else + { scheme_start = start; scheme_end = start; p = start; } - if (p[0] == '/' && p[1] == '/') + if (p[0] == '/' && p[1] == '/') { this->isRelative = false; // authority. @@ -127,20 +136,22 @@ void uri::set_() authority_start = p; authority_end = nullptr; - const char*port_start = nullptr; + const char *port_start = nullptr; while (p != end) { char c = *p; if (c == '@') { user_end = p; - authority_start = p+1; + authority_start = p + 1; port_start = nullptr; - } else if (c == ':') + } + else if (c == ':') { authority_end = p; - port_start = p+1; - } else if (c == ']') // ignore colon inside ipv6 address. + port_start = p + 1; + } + else if (c == ']') // ignore colon inside ipv6 address. { port_start = nullptr; } @@ -150,16 +161,22 @@ void uri::set_() } ++p; } - if (authority_end == nullptr) authority_end = p; + if (authority_end == nullptr) + authority_end = p; if (port_start != nullptr) { - char*intEnd; - port_ = (int)std::strtol(port_start,&intEnd,10); - if (intEnd != p) throwInvalid(); - } else { + char *intEnd; + port_ = (int)std::strtol(port_start, &intEnd, 10); + if (intEnd != p) + throwInvalid(); + } + else + { port_ = -1; } - } else { + } + else + { user_start = p; user_end = p; authority_start = p; @@ -168,7 +185,8 @@ void uri::set_() } path_start = p; - if (p != end && *p == '/') this->isRelative = false; + if (p != end && *p == '/') + this->isRelative = false; while (p != end && *p != '?' && *p != '#') { @@ -185,7 +203,9 @@ void uri::set_() ++p; } query_end = p; - } else { + } + else + { query_start = query_end = p; } if (p != end && *p == '#') @@ -193,62 +213,64 @@ void uri::set_() ++p; fragment_start = p; fragment_end = end; - - } else { + } + else + { fragment_start = fragment_end = p; } } -void uri::throwInvalid() { +void uri::throwInvalid() +{ throw std::invalid_argument("Invalid uri."); - } -static bool compare_name(const char*start, const char*end, const char*szName) +static bool compare_name(const char *start, const char *end, const char *szName) { while (start != end) { - if (*start != *szName) return false; - ++start; ++szName; + if (*start != *szName) + return false; + ++start; + ++szName; } return *szName == 0; } -int uri::query_count() const +int uri::query_count() const { int count = 0; - const char*p = query_start; + const char *p = query_start; while (p != query_end && *p != '#') { - const char*nameStart = p; + const char *nameStart = p; while (p != query_end && *p != '&' && *p != '#') { ++p; } - if (p != query_end && *p == '&') ++p; + if (p != query_end && *p == '&') + ++p; ++count; if (p == query_end || *p == '#') { break; - } } return count; - } -bool uri::has_query(const char*name) const +bool uri::has_query(const char *name) const { - const char*p = query_start; + const char *p = query_start; while (p != query_end && *p != '#') { - const char*nameStart = p; + const char *nameStart = p; while (p != query_end && *p != '&' && *p != '=' && *p != '#') { ++p; } // compare names. - if (compare_name(nameStart,p,name)) + if (compare_name(nameStart, p, name)) { return true; } @@ -256,111 +278,128 @@ bool uri::has_query(const char*name) const { ++p; } - if (p != query_end && *p == '&') ++p; + if (p != query_end && *p == '&') + ++p; } return false; - } query_segment uri::query(int index) const { - const char*p = query_start; + const char *p = query_start; int ix = 0; while (p != query_end && *p != '#') { - const char*nameStart = p; + const char *nameStart = p; while (p != query_end && *p != '&' && *p != '=' && *p != '#') { ++p; } // compare names. - //if (compare_name(nameStart,p,name)) + // if (compare_name(nameStart,p,name)) if (ix == index) { const char *nameEnd = p; if (p != query_end && *p == '=') { ++p; - const char*value_start = p; - while(p != query_end && *p != '&' && *p != '#') + const char *value_start = p; + while (p != query_end && *p != '&' && *p != '#') { ++p; } return query_segment( - std::string(nameStart,nameEnd), - std::string(value_start,p) - ); - - } else { + std::string(nameStart, nameEnd), + std::string(value_start, p)); + } + else + { return query_segment( - std::string(nameStart,nameEnd), - "" - ); + std::string(nameStart, nameEnd), + ""); } } while (p != query_end && *p != '&' && *p != '#') { ++p; } - if (p != query_end && *p == '&') + if (p != query_end && *p == '&') { ++p; } ++ix; - } throw PiPedalArgumentException("Index out of range."); } -std::string uri::query(const char*name) const +std::optional uri::optionalQuery(const char *name) const { - const char*p = query_start; + const char *p = query_start; while (p != query_end && *p != '#') { - const char*nameStart = p; + const char *nameStart = p; while (p != query_end && *p != '&' && *p != '=' && *p != '#') { ++p; } // compare names. - if (compare_name(nameStart,p,name)) + if (compare_name(nameStart, p, name)) { if (p != query_end && *p == '=') { ++p; - const char*value_start = p; - while(p != query_end && *p != '&' && *p != '#') + const char *value_start = p; + while (p != query_end && *p != '&' && *p != '#') { ++p; } - return HtmlHelper::decode_url_segment(value_start,p); - } else { - return ""; + return HtmlHelper::decode_url_segment(value_start, p); + } + else + { + return std::optional(); } } while (p != query_end && *p != '&' && *p != '#') { ++p; } - if (p != query_end && *p == '&') + if (p != query_end && *p == '&') { ++p; } - + } + return std::optional(); +} +std::string uri::query(const char *name) const +{ + auto result = optionalQuery(name); + if (result.has_value()) + { + return result.value(); } return ""; } +std::string uri::requiredQuery(const char *name) const +{ + auto result = optionalQuery(name); + if (result.has_value()) + { + return result.value(); + } + throw std::runtime_error("Invalid url."); +} query_segment uri::query(int index) { - const char*p = query_start; + const char *p = query_start; int count = 0; while (p != query_end && *p != '#') { if (count == index) { - const char*nameStart = p; + const char *nameStart = p; while (p != query_end && *p != '&' && *p != '=' && *p != '#') { ++p; @@ -368,28 +407,32 @@ query_segment uri::query(int index) const char *nameEnd = p; const char *valueStart, *valueEnd; - if(p == query_end || *p != '=') + if (p == query_end || *p != '=') { valueStart = valueEnd = p; - } else { + } + else + { ++p; valueStart = p; - while(p != query_end && *p != '&' && *p != '#') + while (p != query_end && *p != '&' && *p != '#') { ++p; } valueEnd = p; } - return query_segment(std::string(nameStart,nameEnd), HtmlHelper::decode_url_segment(valueStart,valueEnd)); - } else { + return query_segment(std::string(nameStart, nameEnd), HtmlHelper::decode_url_segment(valueStart, valueEnd)); + } + else + { while (p != query_end && *p != '&' && *p != '#') { ++p; } - } - if (p != query_end && *p == '&') ++p; + if (p != query_end && *p == '&') + ++p; ++count; } throw std::invalid_argument("Argument out of range."); @@ -407,66 +450,76 @@ std::vector uri::segments() std::string uri::get_extension() const { - const char*p = this->path_end; + const char *p = this->path_end; while (p != this->path_start) { - char c = p[-1]; - if (c == '/') return ""; - if (c == '.') { - --p; - return std::string(p,this->path_end-p); - } - --p; + char c = p[-1]; + if (c == '/') + return ""; + if (c == '.') + { + --p; + return std::string(p, this->path_end - p); + } + --p; } return ""; } -std::string uri::to_canonical_form() const { +std::string uri::to_canonical_form() const +{ uri_builder builder(*this); return builder.str(); } -std::string uri_builder::str() const { +std::string uri_builder::str() const +{ std::stringstream s; if (scheme_.length() != 0) { s << scheme_ << ':'; } - if (authority_.length() != 0) { + if (authority_.length() != 0) + { s << "//"; - if (user_.length() != 0) + if (user_.length() != 0) { s << user_ << '@'; } s << authority_; - if (port_ != -1) { + if (port_ != -1) + { s << ':' << (uint16_t)port_; } } - if ( authority_.length() != 0 || !isRelative_) { // canonical root form is http://xyz/ not httP://xyz - + if (authority_.length() != 0 || !isRelative_) + { // canonical root form is http://xyz/ not httP://xyz + s << '/'; } for (size_t i = 0; i < segments_.size(); ++i) { - if (i != 0) s << '/'; - HtmlHelper::encode_url_segment(s,segments_[i],false); + if (i != 0) + s << '/'; + HtmlHelper::encode_url_segment(s, segments_[i], false); } for (size_t i = 0; i < queries_.size(); ++i) { - if (i == 0) + if (i == 0) { s << '?'; - } else { + } + else + { s << '&'; } s << queries_[i].key << "="; - HtmlHelper::encode_url_segment(s,queries_[i].value,true); + HtmlHelper::encode_url_segment(s, queries_[i].value, true); } if (fragment_.length() != 0) { s << "#" << fragment_; } return s.str(); +} -} \ No newline at end of file diff --git a/src/Uri.hpp b/src/Uri.hpp index 52951f8..3208e10 100644 --- a/src/Uri.hpp +++ b/src/Uri.hpp @@ -24,214 +24,239 @@ #include #include #include +#include -namespace pipedal { - - -class query_segment { - -public: - query_segment(const std::string &key, const std::string &value) - : key(key) - , value(value) { - - } - const std::string key; - const std::string value; - - bool operator==(const query_segment &other) - { - return key == other.key && value == other.value; - } -}; - - -class uri +namespace pipedal { -private: - std::string text; - const char*scheme_start, *scheme_end; - const char *user_start, *user_end; - const char* authority_start, *authority_end; - const char* path_start, *path_end; - bool isRelative; - int port_; - const char*query_start, *query_end; - const char*fragment_start, *fragment_end; -public: - uri() - { - set(""); - } - const std::string& str() const { - return text; - } - uri(const char*text) - { - set(text); - } - uri(const std::string&text) - { - set(text.c_str()); - } - void set(const char*text); - void set(const char*start, const char*end); -private: - void set_(); - static void throwInvalid(); -public: - bool has_scheme() const { return scheme_start != scheme_end; } - std::string scheme() const{ return std::string(scheme_start, scheme_end); } - bool has_user() const{ return user_start != user_end; } - std::string user() const{ return std::string(user_start,user_end); } - - bool has_authority() const{ return authority_start != authority_end; } - std::string authority() const{ return std::string(authority_start,authority_end);} - - bool has_port() const{ return port_ != -1; } - int port() const{ return port_; } - - bool is_relative() const { return isRelative; } - - std::string path() const { return std::string(path_start, path_end); } - - int segment_count() const; - - std::string segment(int n) const; - - std::vector segments(); - const std::vector segments() const; - - int query_count() const; - - query_segment query(int index); - - bool has_query(const char*name) const; - - std::string query(const char*name) const; - query_segment query(int index) const; - - std::string get_extension() const; - - std::string fragment() const { return std::string(fragment_start, fragment_end);} - - std::string to_canonical_form() const; - -}; - -class uri_builder { - -private: - std::string scheme_ = "http"; - std::string user_ = ""; - std::string authority_ = ""; - int port_ = -1; - bool isRelative_ = false; - std::vector segments_; - - std::vector queries_; - std::string fragment_; - -public: - uri_builder() + class query_segment { - } - - uri_builder(const uri&uri) - : scheme_(uri.scheme()), - user_(uri.user()), - authority_(uri.authority()), - port_(uri.port()), - isRelative_(uri.is_relative()), - fragment_(uri.fragment()) - { - for (int i = 0; i < uri.segment_count(); ++i) + public: + query_segment(const std::string &key, const std::string &value) + : key(key), value(value) { - segments_.push_back(uri.segment(i)); } - for (int i = 0; i < uri.query_count(); ++i) + const std::string key; + const std::string value; + + bool operator==(const query_segment &other) { - queries_.push_back(uri.query(i)); + return key == other.key && value == other.value; } - } - std::string str() const; + }; - const std::string & scheme() const { return scheme_; } - void set_scheme(const std::string &scheme_) { - this->scheme_ = scheme_; - } - const std::string & user() const { return this->user_;} - void set_user(const std::string&user) { this->user_ = user; } - - const std::string &authority() const { - return this->authority_; - } - void set_authority(const std::string &authority) { - this->authority_ = authority; - } - int port() const { return port_; } - void set_port(int port) { - this->port_ = port; - } - - bool is_relative() const { return this->isRelative_ && authority_.length() == 0; } - void set_is_relative(bool isRelative) + class uri { - this->isRelative_ = isRelative; - } - int segment_count() const { return (int)segments_.size(); } - const std::string& segment(int i) const { return segments_[i];} + private: + std::string text; + const char *scheme_start, *scheme_end; + const char *user_start, *user_end; + const char *authority_start, *authority_end; + const char *path_start, *path_end; + bool isRelative; + int port_; + const char *query_start, *query_end; + const char *fragment_start, *fragment_end; - void append_segment(const std::string & segment) { - segments_.push_back(segment); - } - void insert_segment(int position, std::string & segment) { - segments_.insert(segments_.begin()+position,segment); - } - void erase_segment(int position) + public: + uri() + { + set(""); + } + const std::string &str() const + { + return text; + } + uri(const char *text) + { + set(text); + } + uri(const std::string &text) + { + set(text.c_str()); + } + uri(const uri&other) + { + set(other.text.c_str()); + } + void set(const char *text); + void set(const char *start, const char *end); + + private: + void set_(); + static void throwInvalid(); + + public: + bool has_scheme() const { return scheme_start != scheme_end; } + std::string scheme() const { return std::string(scheme_start, scheme_end); } + + bool has_user() const { return user_start != user_end; } + std::string user() const { return std::string(user_start, user_end); } + + bool has_authority() const { return authority_start != authority_end; } + std::string authority() const { return std::string(authority_start, authority_end); } + + bool has_port() const { return port_ != -1; } + int port() const { return port_; } + + bool is_relative() const { return isRelative; } + + std::string path() const { return std::string(path_start, path_end); } + + int segment_count() const; + + std::string segment(int n) const; + + std::vector segments(); + const std::vector segments() const; + + int query_count() const; + + query_segment query(int index); + + bool has_query(const char *name) const; + + std::string query(const char *name) const; + std::optional optionalQuery(const char *name) const; + std::string requiredQuery(const char *name) const; + + query_segment query(int index) const; + + std::string get_extension() const; + + std::string fragment() const { return std::string(fragment_start, fragment_end); } + + std::string to_canonical_form() const; + }; + + class uri_builder { - segments_.erase(segments_.begin()+position); - } - void replace_segment(int position, std::string& segment) { - segments_[position] = segment; - } + private: + std::string scheme_ = "http"; + std::string user_ = ""; + std::string authority_ = ""; + int port_ = -1; + bool isRelative_ = false; + std::vector segments_; - int query_count() const { return (int)(queries_.size());} + std::vector queries_; + std::string fragment_; - bool has_query(const std::string &key) const { - for (size_t i = 0; i < queries_.size(); ++i) + public: + uri_builder() { - if (queries_[i].key == key) true; } - return false; - } - std::string query(const std::string &key) const { - for (size_t i = 0; i < queries_.size(); ++i) + uri_builder(const uri &uri) + : scheme_(uri.scheme()), + user_(uri.user()), + authority_(uri.authority()), + port_(uri.port()), + isRelative_(uri.is_relative()), + fragment_(uri.fragment()) { - if (queries_[i].key == key) return queries_[i].value; + for (int i = 0; i < uri.segment_count(); ++i) + { + segments_.push_back(uri.segment(i)); + } + for (int i = 0; i < uri.query_count(); ++i) + { + queries_.push_back(uri.query(i)); + } } - return ""; - } - std::vector queries(const std::string &key) const { - std::vector result; - for (size_t i = 0; i < queries_.size(); ++i) + std::string str() const; + + const std::string &scheme() const { return scheme_; } + void set_scheme(const std::string &scheme_) { - if (queries_[i].key == key) result.push_back(queries_[i].value); + this->scheme_ = scheme_; } - return result; - } - const query_segment & query(int index) { - return queries_[index]; - } + const std::string &user() const { return this->user_; } + void set_user(const std::string &user) { this->user_ = user; } - const std::string&fragment() { return this->fragment_; } - void set_fragment(const std::string & fragment) { this->fragment_ = fragment;} + const std::string &authority() const + { + return this->authority_; + } + void set_authority(const std::string &authority) + { + this->authority_ = authority; + } + int port() const { return port_; } + void set_port(int port) + { + this->port_ = port; + } + bool is_relative() const { return this->isRelative_ && authority_.length() == 0; } + void set_is_relative(bool isRelative) + { + this->isRelative_ = isRelative; + } + int segment_count() const { return (int)segments_.size(); } + const std::string &segment(int i) const { return segments_[i]; } -}; + void append_segment(const std::string &segment) + { + segments_.push_back(segment); + } + void insert_segment(int position, std::string &segment) + { + segments_.insert(segments_.begin() + position, segment); + } + void erase_segment(int position) + { + segments_.erase(segments_.begin() + position); + } + void replace_segment(int position, std::string &segment) + { + segments_[position] = segment; + } + + void set_path(const std::string &path); + + int query_count() const { return (int)(queries_.size()); } + + bool has_query(const std::string &key) const + { + for (size_t i = 0; i < queries_.size(); ++i) + { + if (queries_[i].key == key) + true; + } + return false; + } + std::string query(const std::string &key) const + { + for (size_t i = 0; i < queries_.size(); ++i) + { + if (queries_[i].key == key) + return queries_[i].value; + } + return ""; + } + std::vector queries(const std::string &key) const + { + std::vector result; + for (size_t i = 0; i < queries_.size(); ++i) + { + if (queries_[i].key == key) + result.push_back(queries_[i].value); + } + return result; + } + const query_segment &query(int index) + { + return queries_[index]; + } + void add_query(const std::string &key, const std::string &value) + { + queries_.push_back(query_segment(key, value)); + } + + const std::string &fragment() { return this->fragment_; } + void set_fragment(const std::string &fragment) { this->fragment_ = fragment; } + }; }; // namespace pipedal. - diff --git a/src/VuUpdate.cpp b/src/VuUpdate.cpp index da1c876..85232a1 100644 --- a/src/VuUpdate.cpp +++ b/src/VuUpdate.cpp @@ -23,14 +23,14 @@ using namespace pipedal; -JSON_MAP_BEGIN(VuUpdate) - JSON_MAP_REFERENCE(VuUpdate,instanceId) - JSON_MAP_REFERENCE(VuUpdate,sampleTime) - JSON_MAP_REFERENCE(VuUpdate,isStereoInput) - JSON_MAP_REFERENCE(VuUpdate,isStereoOutput) - JSON_MAP_REFERENCE(VuUpdate,inputMaxValueL) - JSON_MAP_REFERENCE(VuUpdate,inputMaxValueR) - JSON_MAP_REFERENCE(VuUpdate,outputMaxValueL) - JSON_MAP_REFERENCE(VuUpdate,outputMaxValueR) +JSON_MAP_BEGIN(VuUpdateX) + JSON_MAP_REFERENCE(VuUpdateX,instanceId) + JSON_MAP_REFERENCE(VuUpdateX,sampleTime) + JSON_MAP_REFERENCE(VuUpdateX,isStereoInput) + JSON_MAP_REFERENCE(VuUpdateX,isStereoOutput) + JSON_MAP_REFERENCE(VuUpdateX,inputMaxValueL) + JSON_MAP_REFERENCE(VuUpdateX,inputMaxValueR) + JSON_MAP_REFERENCE(VuUpdateX,outputMaxValueL) + JSON_MAP_REFERENCE(VuUpdateX,outputMaxValueR) JSON_MAP_END() diff --git a/src/VuUpdate.hpp b/src/VuUpdate.hpp index 277fa5b..882f559 100644 --- a/src/VuUpdate.hpp +++ b/src/VuUpdate.hpp @@ -20,10 +20,11 @@ #pragma once #include "json.hpp" +#include "Pedalboard.hpp" namespace pipedal { - class VuUpdate + class VuUpdateX { public: int64_t instanceId_ = 0; @@ -45,7 +46,10 @@ namespace pipedal void AccumulateVu(float *value,float *input, uint32_t samples) { - + if (input == nullptr) { + *value = 0; + return; + } float v = *value; for (uint32_t i = 0; i < samples; ++i) { @@ -65,7 +69,6 @@ namespace pipedal { AccumulateVu(&inputMaxValueL_,inputL,samples); AccumulateVu(&inputMaxValueR_,inputR,samples); - } void AccumulateOutputs(float* output, uint32_t samples) { @@ -78,6 +81,6 @@ namespace pipedal AccumulateVu(&outputMaxValueR_,outputR,samples); } - DECLARE_JSON_MAP(VuUpdate); + DECLARE_JSON_MAP(VuUpdateX); }; } \ No newline at end of file diff --git a/src/WebServer.cpp b/src/WebServer.cpp index b6f00ef..281664d 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -89,6 +89,7 @@ public: std::istream &get_body_input_stream(); const std::filesystem::path &get_body_input_file(); + void detach_body_input_file(); size_t content_length() const { return m_content_length; } /// Returns the full raw request (including the body) @@ -163,6 +164,13 @@ const std::filesystem::path &request_with_file_upload::get_body_input_file() throw std::runtime_error("Request does not have a body."); return this->m_temporaryFile->Path(); } +void request_with_file_upload::detach_body_input_file() +{ + if (this->m_temporaryFile) + { + this->m_temporaryFile->Detach(); + } +} std::istream &request_with_file_upload::get_body_input_stream() { if (!m_outputOpen) @@ -711,6 +719,10 @@ namespace pipedal return m_request.get_body_input_file(); } + virtual void detach_body_temporary_file() override { + return m_request.detach_body_input_file(); + } + virtual size_t content_length() const { return m_request.content_length(); } virtual const std::string &method() const { return m_request.get_method(); } diff --git a/src/WebServer.hpp b/src/WebServer.hpp index 1d9bd03..6b2dae3 100644 --- a/src/WebServer.hpp +++ b/src/WebServer.hpp @@ -30,6 +30,7 @@ public: //virtual const std::string&body() const = 0; virtual std::istream &get_body_input_stream() = 0; virtual const std::filesystem::path& get_body_temporary_file() = 0; + virtual void detach_body_temporary_file() = 0; virtual size_t content_length() const = 0; virtual const std::string &method() const = 0; virtual const std::string&get(const std::string&key) const = 0; diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index 8045bd6..6ba1b74 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -18,6 +18,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "WebServerConfig.hpp" +#include "PresetBundle.hpp" #include "WebServer.hpp" #include #include @@ -80,6 +81,33 @@ static std::string GetMimeType(const std::filesystem::path &path) return result; } +static bool IsSafeMediaPath(const fs::path path) +{ + if (!HtmlHelper::IsSafeFileName(path)) { + return false; + } + if (!(path.string().starts_with("/var/pipedal/audio_uploads/"))) + { + return false; + } + return true; +} +static bool IsSafeThumbnailPath(const fs::path path) +{ + if (!HtmlHelper::IsSafeFileName(path)) { + return false; + } + if (!(path.string().starts_with("/var/pipedal/tone3000_thumbnails/"))) + { + return false; + } + auto extension = path.extension(); + if (extension != ".jpg" && extension != ".jpeg" && extension != ".png" && extension != ".webm") + { + return false; + } + return true; +} int32_t ConvertThumbnailSize(const std::string ¶m) { if (param.empty()) @@ -127,6 +155,30 @@ private: std::vector extensions; }; +static fs::path GetTone3000ThumbnailFile(const fs::path &thumbnailDirectory, const std::string &id) +{ + std::string stem = id; + // find a file in thumbnailDirectory which has a filename of {stem}.*. + if (!fs::exists(thumbnailDirectory) || !fs::is_directory(thumbnailDirectory)) + { + return {}; + } + + for (const auto &entry : fs::directory_iterator(thumbnailDirectory)) + { + if (!entry.is_regular_file()) + { + continue; + } + const auto &path = entry.path(); + if (path.stem() == stem) + { + return path; + } + } + return {}; +} + class DownloadIntercept : public RequestHandler { PiPedalModel *model; @@ -145,10 +197,29 @@ public: return false; } std::string segment = request_uri.segment(1); + + if (segment == "t3k_uploadAsset") + { + return true; + } + if (segment == "t3k_response.html") + { + return true; + } + if (segment == "tone3000_thumbnail") + { + return true; + } + if (segment == "downloadMediaFile") { return true; } + if (segment == "displayMediaFile") + { + return true; + } + if (segment == "uploadPluginPresets") { return true; @@ -189,10 +260,6 @@ public: { return true; } - else if (segment == "Tone3000Auth") - { - return true; - } else if (segment == "PluginPresets") { return true; @@ -301,7 +368,48 @@ public: try { std::string segment = request_uri.segment(1); - if (segment == "downloadMediaFile") + + if (segment == "t3k_uploadAsset") + { + res.set(HttpField::content_length, "0"); + return; + } + if (segment == "t3k_response.html") + { + std::string mimeType = GetMimeType("x.html"); + auto text = T3kResponse(); + res.set(HttpField::content_type, mimeType); + res.set(HttpField::content_length, SS(text.size())); + return; + } + else if (segment == "tone3000_thumbnail") + { + std::string id = request_uri.query("id"); + if (id == "") + { + throw PiPedalException("Invalid request."); + } + fs::path path = GetTone3000ThumbnailFile(this->model->Tone3000ThumbnailDirectory(), id); + if (!fs::exists(path)) + { + throw PiPedalException("File not found."); + } + auto mimeType = GetMimeType(path); + if (mimeType.empty()) + { + throw PiPedalException("Can't download files of this type."); + } + if (!mimeType.starts_with("image/")) + { + throw PiPedalException("Can't download files of this type."); + } + res.set(HttpField::content_type, mimeType); + size_t contentLength = std::filesystem::file_size(path); + res.setContentLength(contentLength); + return; + } + + if (segment == "downloadMediaFile" || segment == "displayMediaFile") { fs::path path = request_uri.query("path"); @@ -323,8 +431,11 @@ public: } res.set(HttpField::content_type, mimeType); res.set(HttpField::cache_control, "no-cache"); - std::string disposition = GetContentDispositionHeader(path.stem().string(), path.extension().string()); - res.set(HttpField::content_disposition, disposition); + if (segment == "downloadMediaFile") + { + std::string disposition = GetContentDispositionHeader(path.stem().string(), path.extension().string()); + res.set(HttpField::content_disposition, disposition); + } size_t contentLength = std::filesystem::file_size(path); res.setContentLength(contentLength); return; @@ -413,18 +524,41 @@ public: bool isInfoFile(const fs::path &path) { auto extension = path.extension(); - if (extension == ".md" || extension == ".txt") + if (extension == ".pdf") { return true; } - auto filename = path.stem(); - if (filename == "LICENSE" || filename == "README") { + auto filename = path.filename(); + if (filename == "LICENSE.txt" || filename == "README.txt") + { + return true; + } + if (filename == "LICENSE.md" || filename == "README.md") + { + return true; + } + if (filename == "license.txt" || filename == "readme.txt") + { return true; } return false; - + } + std::string T3kResponse() + { + return "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n"; } + void DownloadTone3000Tone(const uri &requestUri) + { + } virtual void get_response( const uri &request_uri, HttpRequest &req, @@ -435,7 +569,30 @@ public: { std::string segment = request_uri.segment(1); - if (segment == "downloadMediaFile") + if (segment == "t3k_response.html") + { + throw std::runtime_error("Not implemented."); + } + if (segment == "tone3000_thumbnail") + { + std::string id = request_uri.query("id"); + fs::path path = GetTone3000ThumbnailFile(this->model->Tone3000ThumbnailDirectory(), id); + if (!fs::exists(path)) + { + throw PiPedalException("File not found."); + } + auto mimeType = GetMimeType(path); + if (mimeType.empty()) + { + throw PiPedalException("Can't download files of this type."); + } + res.set(HttpField::content_type, mimeType); + size_t contentLength = std::filesystem::file_size(path); + res.setContentLength(contentLength); + res.setBodyFile(path, false); + return; + } + else if (segment == "downloadMediaFile" || segment == "displayMediaFile") { fs::path path = request_uri.query("path"); @@ -457,8 +614,11 @@ public: } res.set(HttpField::content_type, mimeType); res.set(HttpField::cache_control, "no-cache"); - std::string disposition = GetContentDispositionHeader(path.stem().string(), path.extension().string()); - res.set(HttpField::content_disposition, disposition); + if (segment == "downloadMediaFile") + { + std::string disposition = GetContentDispositionHeader(path.stem().string(), path.extension().string()); + res.set(HttpField::content_disposition, disposition); + } size_t contentLength = std::filesystem::file_size(path); res.setContentLength(contentLength); res.setBodyFile(path, false); @@ -755,6 +915,75 @@ public: return true; } + void UploadEmbeddedZipFile( + PiPedalModel *model, + pipedal::zip_file_input_stream &si, + const std::string &inputFileName, + fs::path directory, + ExtensionChecker &extensionChecker, + int64_t instanceId, + const std::string &patchProperty) + { + TemporaryFile tempFile{WEB_TEMP_DIR}; + // Extract the zip_file_input_stream to the temporary file. + { + std::ofstream out(tempFile.Path(), std::ios::binary); + if (!out) + { + throw std::runtime_error("Failed to open temporary file for writing."); + } + constexpr std::size_t bufferSize = 16384; + char buffer[bufferSize]; + while (si) + { + si.read(buffer, bufferSize); + std::streamsize bytesRead = si.gcount(); + if (bytesRead > 0) + { + out.write(buffer, bytesRead); + } + } + out.close(); + } + + // determine toplevel directory path. + auto zipFile = ZipFileReader::Create(tempFile.Path()); + std::vector files = zipFile->GetFiles(); + bool hasSingleRootDirectory = HasSingleRootDirectory(*zipFile); + directory = directory / fs::path(inputFileName).parent_path(); + if (!hasSingleRootDirectory) + { + directory = (fs::path(directory) / fs::path(inputFileName).filename().stem()).string(); + } + for (const auto &inputFile : files) + { + if (!inputFile.ends_with("/")) // don't process directory entries. + { + fs::path inputPath{inputFile}; + std::string extension = inputPath.extension(); + if (extensionChecker.IsValidExtension(extension) || isInfoFile(inputFile)) + { + auto si = zipFile->GetFileInputStream(inputFile); + std::string path = this->model->UploadUserFile(directory, instanceId, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile)); + } + else if (extension == ".zip") + { + // recursively included zip file. :-/ + auto si = zipFile->GetFileInputStream(inputFile); + + UploadEmbeddedZipFile( + this->model, + si, + inputPath, + directory, + extensionChecker, + instanceId, + patchProperty); + } + } + } + } + virtual void post_response( const uri &request_uri, HttpRequest &req, @@ -765,19 +994,66 @@ public: { std::string segment = request_uri.segment(1); - if (segment == "Tone3000Auth") + if (segment == "t3k_uploadAsset") { - // https://www.tone3000.com/api/v1/auth?redirect_url=http://10.0.0.151:8080/var/Tone3000Auth&otp_only=true - std::string apiKey = request_uri.query("api_key"); + std::string responseText; + try + { + fs::path targetPath = request_uri.query("path"); + if (targetPath.empty()) + { + throw std::runtime_error("Invalid path"); + } + if (!IsSafeMediaPath(targetPath) && !IsSafeThumbnailPath(targetPath)) + { + throw std::runtime_error("Unsafe path."); + } + fs::path filePath = req.get_body_temporary_file(); + if (filePath.empty()) + { + throw std::runtime_error("Unexpected."); + } - model->SetTone3000Auth(apiKey); + try { + fs::create_directories(targetPath.parent_path()); + // try moving into place. + if (fs::exists(targetPath)) + { + fs::remove(targetPath); + } + fs::rename(req.get_body_temporary_file(), targetPath); + req.detach_body_temporary_file(); - res.set(HttpField::content_type, "application/json"); - res.set(HttpField::cache_control, "no-cache"); + } catch (const std::exception &e) { + // ok Copy into place instead. + fs::copy(req.get_body_temporary_file(), targetPath); + } + // set target permissions to "pipedal_d:pipedald -rw-rw-r-- if we can. + try { + fs::permissions(targetPath, fs::perms::owner_read | fs::perms::owner_write | + fs::perms::group_read | fs::perms::group_write | + fs::perms::others_read, + fs::perm_options::replace); + } catch (const std::exception&) + { + // ignore. + } - res.setBody("\"OK\""); + responseText = "{\"ok\": true}"; + } + catch (const std::exception &e) + { + std::string jsonString = json_writer::encode_string(e.what()); + std::ostringstream os; + os << "{\"ok\": false, \"error\": " << jsonString << "}"; + responseText = os.str(); + } + res.set(HttpField::content_length, SS(responseText.size())); + res.set(HttpField::content_type, "text/json"); + res.setBody(responseText); + return; } - else if (segment == "uploadPluginPresets") + if (segment == "uploadPluginPresets") { PluginPresets presets; fs::path filePath = req.get_body_temporary_file(); @@ -954,6 +1230,20 @@ public: auto si = zipFile->GetFileInputStream(inputFile); std::string path = this->model->UploadUserFile(directory, instanceId, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile)); } + else if (extension == ".zip") + { + // recursively included zip file. :-/ + auto si = zipFile->GetFileInputStream(inputFile); + + UploadEmbeddedZipFile( + this->model, + si, + inputPath, + directory, + extensionChecker, + instanceId, + patchProperty); + } } } // set outputPath to the file or folder we would like focus to go to. @@ -1045,12 +1335,14 @@ static std::string StripPortNumber(const std::string &fromAddress) class InterceptConfig : public RequestHandler { private: + PiPedalModel&model; uint64_t maxUploadSize; int portNumber; public: - InterceptConfig(int portNumber, uint64_t maxUploadSize) + InterceptConfig(PiPedalModel&model,int portNumber, uint64_t maxUploadSize) : RequestHandler("/var/config.json"), + model(model), maxUploadSize(maxUploadSize), portNumber(portNumber) { @@ -1067,6 +1359,7 @@ public: << "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize << ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true" : "false") << ", \"has_wifi_device\": " << (HotspotManager::HasWifiDevice() ? " true" : "false") + << ", \"tone3000_A2_models\": " << (model.Configuration().GetTone3000A2Models() ? " true" : "false") << " }"; return s.str(); @@ -1126,7 +1419,7 @@ void pipedal::ConfigureWebServer( int port, size_t maxUploadSize) { - std::shared_ptr interceptConfig{new InterceptConfig(port, maxUploadSize)}; + std::shared_ptr interceptConfig{new InterceptConfig(model,port, maxUploadSize)}; server.AddRequestHandler(interceptConfig); std::shared_ptr downloadIntercept = std::make_shared(&model); diff --git a/src/alsaCheck.cpp b/src/alsaCheck.cpp index e2147e9..25caa41 100644 --- a/src/alsaCheck.cpp +++ b/src/alsaCheck.cpp @@ -206,6 +206,7 @@ static void print_device_info(snd_ctl_t* handle, int card, int device) { // Print basic info std::cout << "\n Subdevice: " << name << " - " << device_name << std::endl; std::cout << " ID: " << snd_pcm_info_get_id(pcminfo) << std::endl; + std::cout << " Name: " << snd_pcm_info_get_name(pcminfo) << std::endl; std::cout << " Capabilities:"; if (has_playback) std::cout << " PLAYBACK"; if (has_capture) std::cout << " CAPTURE"; diff --git a/src/main.cpp b/src/main.cpp index feb8481..7f264cd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -83,7 +83,7 @@ static bool isJackServiceRunning() return std::filesystem::exists(path); } -#if 0 && ENABLE_BACKTRACE +#if 0 && ENABLE_BACKTRACE // void segvHandler(int sig) // { // void *array[10]; @@ -107,12 +107,15 @@ static bool isJackServiceRunning() #endif #define ENABLE_SEGV_DEBUG 0 +#define ENABLE_SIGXCPU_DEBUG 1 + #if ENABLE_SEGV_DEBUG void debug_segvHandler(int sig) { // Print out all the frames to stderr const char *message = "Error: SEGV signal received.\n"; auto _ = write(STDERR_FILENO, message, strlen(message)); + flush(STD_ERR_FILENO); _exit(EXIT_FAILURE); } @@ -126,6 +129,17 @@ static void EnableDebugSevHandler() #endif +#if ENABLE_SIGXCPU_DEBUG + +void debug_XSigXcpuHandler(int sig) +{ +} + +static void EnableSigXcpuHandler() +{ + signal(SIGXCPU, debug_XSigXcpuHandler); +} +#endif static bool TryGetLogLevel(const std::string &strLogLevel, LogLevel *result) { @@ -153,7 +167,6 @@ static bool TryGetLogLevel(const std::string &strLogLevel, LogLevel *result) return false; } - int main(int argc, char *argv[]) { @@ -277,11 +290,13 @@ int main(int argc, char *argv[]) const std::filesystem::path webTempDirectory = "/var/pipedal/web_temp"; if (!webTempDirectory.empty()) { - try { + try + { std::filesystem::remove_all(webTempDirectory); //// user must belong to the pipedald grop when debugging. std::filesystem::create_directories(webTempDirectory); } - catch (const std::exception e) { + catch (const std::exception e) + { Lv2Log::warning(e.what()); } } @@ -317,7 +332,6 @@ int main(int argc, char *argv[]) return EXIT_SUCCESS; // indiate to systemd that we don't want a restart. } - try { { @@ -340,7 +354,7 @@ int main(int argc, char *argv[]) sigset_t sigSet; int s; sigemptyset(&sigSet); - + sigaddset(&sigSet, SIGINT); sigaddset(&sigSet, SIGTERM); sigaddset(&sigSet, SIGUSR1); @@ -354,16 +368,19 @@ int main(int argc, char *argv[]) // Clear any pending signals before waiting struct timespec timeout = {0, 0}; - while (sigtimedwait(&sigSet, NULL, &timeout) > 0) { + while (sigtimedwait(&sigSet, NULL, &timeout) > 0) + { // Consume any pending signals } #if ENABLE_SEGV_DEBUG EnableDebugSevHandler(); #endif +#if ENABLE_SIGXCPU_DEBUG + EnableSigXcpuHandler(); +#endif PiPedalModel model; - model.SetNetworkChangedListener( [&server]() mutable { diff --git a/src/profilePluginMain.cpp b/src/profilePluginMain.cpp index 7c04c20..4c89cef 100644 --- a/src/profilePluginMain.cpp +++ b/src/profilePluginMain.cpp @@ -7,6 +7,7 @@ #include #include #include "ss.hpp" +#include "PresetBundle.hpp" // apt install google-perftools #include @@ -78,6 +79,22 @@ private: std::unique_ptr thread; WriterRingbuffer &writerRingbuffer; }; + +static bool IsZipFile(const std::filesystem::path &path) +{ + std::ifstream f(path); + if (!f.is_open()) + return false; + + char c[4]; + memset(c, 0, sizeof(c)); + + f >> c[0] >> c[1] >> c[2] >> c[3]; + + // official file header according to PKware documetnation. + return c[0] == 0x50 && c[1] == 0x4B && c[2] == 0x03 && c[3] == 0x04; +} + struct ProfileOptions { std::string presetName; @@ -115,7 +132,9 @@ void profilePlugin(const ProfileOptions &profileOptions) throw std::runtime_error(s.str()); } + model.EnableUpdater(false); model.Init(configuration); + model.GetPluginHost().asIHost()->SetMaxAudioBufferSize(64); model.LoadLv2PluginInfo(); @@ -127,6 +146,7 @@ void profilePlugin(const ProfileOptions &profileOptions) if (profileOptions.presetName.length() != 0) { + PresetIndex presetIndex; model.GetPresets(&presetIndex); @@ -149,26 +169,50 @@ void profilePlugin(const ProfileOptions &profileOptions) } else if (profileOptions.presetFileName.length() != 0) { - std::ifstream f(profileOptions.presetFileName); - if (!f.is_open()) + std::filesystem::path filePath = profileOptions.presetFileName; + + if (IsZipFile(filePath)) { - throw std::runtime_error(SS("Unable to load preset file " << profileOptions.presetFileName << ".")); - } - try - { - json_reader reader(f); + auto presetReader = PresetBundleReader::LoadPresetsFile(model, filePath); + presetReader->ExtractMediaFiles(); + + std::stringstream ss(presetReader->GetPresetJson()); + json_reader reader(ss); + BankFile bankFile; reader.read(&bankFile); + if (bankFile.presets().size() != 1) { throw new std::runtime_error(SS("Invalid preset file. Expection one preset, but " << bankFile.presets().size() << " presets were found.")); } Pedalboard pedalboard = (bankFile.presets()[0]->preset()); - model.SetPedalboard(-1,pedalboard); + model.SetPedalboard(-1, pedalboard); } - catch (const std::exception &e) + else { - throw std::runtime_error(SS("Invalid file format: " << profileOptions.presetFileName << ". " << e.what() )); + + std::ifstream f(profileOptions.presetFileName); + if (!f.is_open()) + { + throw std::runtime_error(SS("Unable to load preset file " << profileOptions.presetFileName << ".")); + } + try + { + json_reader reader(f); + BankFile bankFile; + reader.read(&bankFile); + if (bankFile.presets().size() != 1) + { + throw new std::runtime_error(SS("Invalid preset file. Expecting one preset, but " << bankFile.presets().size() << " presets were found.")); + } + Pedalboard pedalboard = (bankFile.presets()[0]->preset()); + model.SetPedalboard(-1, pedalboard); + } + catch (const std::exception &e) + { + throw std::runtime_error(SS("Invalid file format: " << profileOptions.presetFileName << ". " << e.what())); + } } } else @@ -183,13 +227,12 @@ void profilePlugin(const ProfileOptions &profileOptions) lv2Pedalboard->Activate(); - std::vector inputBufferVector; - std::vector outputBufferVector; - inputBufferVector.resize(nFrames); - outputBufferVector.resize(nFrames); + std::vector inputBufferVectorL(nFrames); + std::vector outputBufferVectorL(nFrames); + std::vector outputBufferVectorR(nFrames);; - float *inputBuffers[2]{inputBufferVector.data(), nullptr}; - float *outputBuffers[2] = {inputBufferVector.data(), nullptr}; + float *inputBuffers[3]{inputBufferVectorL.data(), inputBufferVectorL.data(),nullptr}; + float *outputBuffers[3] = {outputBufferVectorL.data(), outputBufferVectorR.data(),nullptr}; WriterRingbuffer writerRingbuffer; RealtimeRingBufferWriter ringBufferWriter(&writerRingbuffer); @@ -197,6 +240,7 @@ void profilePlugin(const ProfileOptions &profileOptions) RingBufferSink ringBufferSink(writerRingbuffer); // run once to get memory allocations in NAM and ML out of the way. + lv2Pedalboard->ResetAtomBuffers(); lv2Pedalboard->Run(inputBuffers, outputBuffers, nFrames, &ringBufferWriter); /* *** Pump the plugin for a bit if it is expected to do work on the scheduler thread when initializing */ @@ -210,6 +254,7 @@ void profilePlugin(const ProfileOptions &profileOptions) while (true) { + lv2Pedalboard->ResetAtomBuffers(); lv2Pedalboard->Run(inputBuffers, outputBuffers, nFrames, &ringBufferWriter); auto elapsed = clock::now() - waitStart; @@ -235,6 +280,7 @@ void profilePlugin(const ProfileOptions &profileOptions) size_t repetitions = static_cast(48000 * profileOptions.benchmark_seconds / nFrames); for (size_t i = 0; i < repetitions; ++i) { + lv2Pedalboard->ResetAtomBuffers(); lv2Pedalboard->Run(inputBuffers, outputBuffers, nFrames, &ringBufferWriter); } @@ -298,6 +344,8 @@ int main(int argc, char **argv) cout << "Options:" << endl; cout << " --no-profile:" << endl; cout << " do NOT generate a perf file." << endl; + cout << " --nam-core:" << endl; + cout << " Force use of NAM Core models." << endl; cout << " -p, --preset-file filename:" << endl; cout << " Load the specified preset file. " << endl; cout << " -o, --output filename:" << endl; diff --git a/todo.txt b/todo.txt index 8423926..d6800b0 100644 --- a/todo.txt +++ b/todo.txt @@ -1,13 +1,97 @@ +A2 Questions + +Does A2 support Callibration? +input_level_dbu, output_level_dbu accessors on NeuralAmpModelerCore DSP +slimmable_weights accessors on NeuralAmpModelerCore DSP +standard values for slimmable_weights? + +Implement NAM Model Weight selection UI + +NAM INput VU is f*ed up. + +File Player Underrun is NOT CPU use related. There's an underrun somewhere. + +Update Pi 4 Benchmark Results: + +The benchmark is to run TooB Neural Amp Modeler with an A1 model to generate 100s of audio. Lower times are better. + +TooB NAM has a compile-time switch that uses either the NeuralAudio NAM implementation or +the Neural Modeler Core NAM implementation. + +Results -ls +aarch64 (Raspberry PI 4, ARM Cortex A72, Raspberry PI OS Bookworm) + + Neural Amp Modeler Core: 94.296s + NeuralAudio: 31.589s + + Compiler options: "-Ofast" "-mcpu=cortex-a72" "-march=armv8-a" "-mtune=cortex-a72" "-DEIGEN_DEFAULT_L1_CACHE_SIZE=32768" + +aarch64 (Raspberry PI 5, ARM Cortex A76, Rasperry PI OS Trixie) + + Neural Amp Modeler Core: 16.084s + NeuralAudio: 10.766s + + Compiler options: "-Ofast" "-mcpu=cortex-a76" "-march=armv8.2-a" "-mtune=cortex-a76" "-DEIGEN_DEFAULT_L1_CACHE_SIZE=65536"apt + + + +amd64 Native (AMD Ryzen 7 7735HS, Ubuntu 24.04 amd64) + + Neural Amp Modeler Core: 4.716s + NeuralAudio x64: 3.952s + + Compiler options /Ofast, MMX, SSE, SSE2, no AVX. + +amd64 AVX+ (AMD Ryzen 7 7735HS, Ubuntu 24.04 amd64) + + Neural Amp Modeler Core: 4.97s + NeuralAudio x64: 4.032s + + Compiler options /Ofast, "-march=sandybridge" "-mtune=sandybridge" + +amd64 SSE2 (AMD Ryzen 7 7735HS, Ubuntu 24.04 amd64) + + Neural Amp Modeler Core: 7.748s + NeuralAudio x64: 5.983s + + Compiler options /Ofast (defaults to SSE2, no AVX) + +The Model: + +Package: Fender Deluxe Reverb %2765 Reissue %7c Clean %7c SM57 %2b Royer R-121 %2b Room +Model: Fender DRRI %7c Clean %7c Room Only %7c Full Rig +Downloaded from Tone3000. + +The model file is attached. + + + + + + + +summaries for Mono audio adapaters. +Check PWA on windows, edge and chrome. +Make sure Pi 4 default audio device is still hidden when selecting devices! + +A final solution for rate-limiting on Tone3000. + +A way to force input to mono? + + +Tooltip on File Property Select: add to release notes. +sAFEfILEnAMES: RENAME, &C. Safe bank names? +NEW BANK +WINDOW SIZE CALCUALTIONS ARE WRONG ON LINUX HIDEF DISPLAYS. + Carla Project icons. check reload after change of LV2 plugins. Exactly one underrun per seek in Toob Player -json "skip" code doesn't work with complex objects (MidiChannelBinding specifically). json nan/inf is fatal. - pipewire aux in? diff --git a/vite/CMakeLists.txt b/vite/CMakeLists.txt index da38a0e..66e23ef 100644 --- a/vite/CMakeLists.txt +++ b/vite/CMakeLists.txt @@ -109,6 +109,7 @@ add_custom_command( public/img/default_album.jpg public/img/fx_filter_hp.svg public/img/fx_parametric_eq.svg + public/img/fx_multiband_eq.svg public/img/fx_filter.svg public/img/fx_error.svg public/img/cx_stereo.svg diff --git a/vite/eslint.config.js b/vite/eslint.config.js index adef530..ff3fea7 100644 --- a/vite/eslint.config.js +++ b/vite/eslint.config.js @@ -24,5 +24,10 @@ export default tseslint.config( { allowConstantExport: true }, ], }, + server: { + proxy: { + '/var': 'http://localhost:8080' + } + } }, ) diff --git a/vite/package-lock.json b/vite/package-lock.json index fe7b278..06229e6 100644 --- a/vite/package-lock.json +++ b/vite/package-lock.json @@ -23,6 +23,9 @@ "react-virtualized-auto-sizer": "^1.0.25", "react-window": "^1.8.11", "rehype-external-links": "^3.0.0", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", "tss-react": "^4.9.15" }, "devDependencies": { @@ -1604,7 +1607,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -1613,322 +1615,312 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", "cpu": [ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", "cpu": [ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openharmony" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, "optional": true, "os": [ "win32" @@ -1949,14 +1941,13 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -2271,8 +2262,7 @@ "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", @@ -2508,11 +2498,10 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2522,7 +2511,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" }, @@ -2638,11 +2626,10 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2710,11 +2697,10 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2949,7 +2935,6 @@ "version": "1.10.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", "engines": { "node": ">= 6" } @@ -3496,8 +3481,7 @@ "version": "3.4.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -3607,6 +3591,93 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-from-parse5/node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-from-parse5/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-is-element": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", @@ -3620,6 +3691,220 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-raw/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-raw/node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-sanitize": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", + "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-sanitize/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-sanitize/node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -3723,6 +4008,65 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5/node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -3736,6 +4080,53 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -3761,6 +4152,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hyphenate-style-name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", @@ -4220,6 +4621,16 @@ "yallist": "^3.0.2" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/mdast-util-definitions": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", @@ -4262,6 +4673,76 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-from-markdown": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", @@ -4279,6 +4760,616 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-gfm-footnote/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-gfm-table/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-gfm/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", @@ -5010,6 +6101,127 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-factory-destination": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", @@ -5402,7 +6614,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5567,6 +6778,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5613,7 +6848,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -5837,9 +7071,9 @@ } }, "node_modules/react-markdown/node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -6220,6 +7454,68 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/rehype-raw/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-react": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-6.2.1.tgz", @@ -6234,6 +7530,233 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-sanitize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", + "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-sanitize": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/remark-gfm/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/remark-gfm/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-gfm/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-gfm/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/remark-gfm/node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-gfm/node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", @@ -6260,6 +7783,128 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/remark-stringify/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/remark-stringify/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-stringify/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-stringify/node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-stringify/node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -6301,11 +7946,10 @@ } }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "1.0.8" }, @@ -6317,31 +7961,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" } }, @@ -6599,7 +8243,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -6888,6 +8531,67 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/vfile-location/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", @@ -6907,7 +8611,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -7012,7 +8715,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -7072,6 +8774,23 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/vite/package.json b/vite/package.json index 23f4d61..459c481 100644 --- a/vite/package.json +++ b/vite/package.json @@ -25,6 +25,9 @@ "react-virtualized-auto-sizer": "^1.0.25", "react-window": "^1.8.11", "rehype-external-links": "^3.0.0", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", "tss-react": "^4.9.15" }, "devDependencies": { diff --git a/vite/public/favicon.ico b/vite/public/favicon.ico index 68428f3..87dab51 100644 Binary files a/vite/public/favicon.ico and b/vite/public/favicon.ico differ diff --git a/vite/public/html/t3k_response.html b/vite/public/html/t3k_response.html new file mode 100644 index 0000000..78ef2e1 --- /dev/null +++ b/vite/public/html/t3k_response.html @@ -0,0 +1,28 @@ + + + + T3K Response + + + + \ No newline at end of file diff --git a/vite/public/img/by.svg b/vite/public/img/by.svg new file mode 100644 index 0000000..34099a3 --- /dev/null +++ b/vite/public/img/by.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/vite/public/img/cc.svg b/vite/public/img/cc.svg new file mode 100644 index 0000000..cb08896 --- /dev/null +++ b/vite/public/img/cc.svg @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/vite/public/img/cc0.svg b/vite/public/img/cc0.svg new file mode 100644 index 0000000..100e139 --- /dev/null +++ b/vite/public/img/cc0.svg @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/vite/public/img/fx_multiband_eq.svg b/vite/public/img/fx_multiband_eq.svg new file mode 100755 index 0000000..c1a0305 --- /dev/null +++ b/vite/public/img/fx_multiband_eq.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/vite/public/img/fx_parametric_eq.svg b/vite/public/img/fx_parametric_eq.svg old mode 100755 new mode 100644 index c1a0305..edf2ca7 --- a/vite/public/img/fx_parametric_eq.svg +++ b/vite/public/img/fx_parametric_eq.svg @@ -1,15 +1,44 @@ - - - - - - - - + + + + + + + diff --git a/vite/public/img/nc.svg b/vite/public/img/nc.svg new file mode 100644 index 0000000..fcf2f4b --- /dev/null +++ b/vite/public/img/nc.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/vite/public/img/nd.svg b/vite/public/img/nd.svg new file mode 100644 index 0000000..9ec0da5 --- /dev/null +++ b/vite/public/img/nd.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/vite/public/img/sa.svg b/vite/public/img/sa.svg new file mode 100644 index 0000000..8d5ffde --- /dev/null +++ b/vite/public/img/sa.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/vite/public/licenses/t3k_license.html b/vite/public/licenses/t3k_license.html new file mode 100644 index 0000000..1d727f0 --- /dev/null +++ b/vite/public/licenses/t3k_license.html @@ -0,0 +1,28 @@ + + + + + + + T3k License + + + +
T3K License
+

Users may download and use the data file in software and publish the resulting outputs without royalties or restrictions. However, they may not upload, republish, or distribute the data file without the author's permission.

+ + \ No newline at end of file diff --git a/vite/public/logo192.png b/vite/public/logo192.png index b73484f..2bb4cca 100644 Binary files a/vite/public/logo192.png and b/vite/public/logo192.png differ diff --git a/vite/public/logo512.png b/vite/public/logo512.png index 696e0f1..99a73a1 100644 Binary files a/vite/public/logo512.png and b/vite/public/logo512.png differ diff --git a/vite/public/manifest.json b/vite/public/manifest.json index a2d368b..e7faaad 100644 --- a/vite/public/manifest.json +++ b/vite/public/manifest.json @@ -1,25 +1,39 @@ { - "short_name": "PiPedal", - "name": "PiPedal", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" + "manifest_version": 3, + "short_name": "PiPedal", + "name": "PiPedal", + "author": "Robin E.R. Davies", + "description": "A web-based client for the PiPedal guitar effects processor.", + "developer": { + "name": "Robin E.R. Davies", + "url": "https://rerdavies.github.io" }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} + "homepage_url": "https://rerdavies.github.io/pipedal", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "theme_color": "#000000", + "display": "standalone", + "display_override": [ + "fullscreen", + "standalone", + "minimal-ui", + "browser" + ], + "background_color": "#000000" +} \ No newline at end of file diff --git a/vite/public/t3k/handleTone3000download.html b/vite/public/t3k/handleTone3000download.html new file mode 100644 index 0000000..7c712a2 --- /dev/null +++ b/vite/public/t3k/handleTone3000download.html @@ -0,0 +1,148 @@ + + + + + + + Tone3000 Download Received + + + + + + + \ No newline at end of file diff --git a/vite/public/testTone300Popup.html b/vite/public/testTone300Popup.html new file mode 100644 index 0000000..2988a91 --- /dev/null +++ b/vite/public/testTone300Popup.html @@ -0,0 +1,44 @@ + + + + Test Tone3000 Select + + + + + +

Test Tone3000 Select Page

+

This is a test page for Tone3000 select functionality.

+ + + + \ No newline at end of file diff --git a/vite/public/testTone300Select.html b/vite/public/testTone300Select.html new file mode 100644 index 0000000..d16c733 --- /dev/null +++ b/vite/public/testTone300Select.html @@ -0,0 +1,22 @@ + + + Test Tone3000 Select + + + + +

Test Tone3000 Select Page

+

This is a test page for Tone3000 select functionality.

+ +