Merge pull request #320 from rerdavies/dev_backup
Merge 1.4.77 dev branch.
This commit is contained in:
@@ -6,3 +6,6 @@
|
||||
url = https://github.com/rerdavies/websocketpp.git
|
||||
branch = develop
|
||||
update = merge
|
||||
[submodule "modules/SQLiteCpp"]
|
||||
path = modules/SQLiteCpp
|
||||
url = https://github.com/SRombauts/SQLiteCpp.git
|
||||
|
||||
Vendored
+7
-2
@@ -7,6 +7,7 @@
|
||||
"${workspaceFolder}",
|
||||
"${workspaceFolder}/build/src/**",
|
||||
"${workspaceFolder}/src/**",
|
||||
"${workspaceFolder}/modules/websocketpp/**",
|
||||
"${workspaceFolder}/**",
|
||||
"/usr/include/lilv-0",
|
||||
"/usr/include/x86_64-linux-gnu",
|
||||
@@ -16,7 +17,9 @@
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "c++20",
|
||||
"intelliSenseMode": "linux-gcc-arm64",
|
||||
"compileCommands": "${workspaceFolder}/build/compile_commands.json",
|
||||
"compileCommands": [
|
||||
"${workspaceFolder}/build/compile_commands.json"
|
||||
],
|
||||
"configurationProvider": "ms-vscode.cmake-tools"
|
||||
},
|
||||
{
|
||||
@@ -33,7 +36,9 @@
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "c++20",
|
||||
"intelliSenseMode": "linux-gcc-x64",
|
||||
"compileCommands": "${workspaceFolder}/build/compile_commands.json",
|
||||
"compileCommands": [
|
||||
"${workspaceFolder}/build/compile_commands.json"
|
||||
],
|
||||
"configurationProvider": "ms-vscode.cmake-tools",
|
||||
"mergeConfigurations": true
|
||||
}
|
||||
|
||||
Vendored
+3
-3
@@ -22,7 +22,7 @@
|
||||
// Resolved by CMake Tools:
|
||||
"program": "${command:cmake.launchTargetPath}",
|
||||
"args": [
|
||||
"--no-sudo"
|
||||
"[ProfileThumbnails]"
|
||||
],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
@@ -142,7 +142,7 @@
|
||||
//"[pipedal_alsa_test]"
|
||||
// "[wifi_channels_test]"
|
||||
// "[locale]"
|
||||
"[modFileTypes]"
|
||||
"[pipewire_input_stream]"
|
||||
],
|
||||
|
||||
"stopAtEntry": false,
|
||||
@@ -177,7 +177,7 @@
|
||||
"args": [
|
||||
//"[Dev]" -- all dev-machine tests.
|
||||
//"[promise]" // subtest of your choice, or none to run all of the tests.
|
||||
"[atom_converter]"
|
||||
"[json_variants]"
|
||||
],
|
||||
|
||||
"stopAtEntry": false,
|
||||
|
||||
Vendored
+23
-2
@@ -102,7 +102,8 @@
|
||||
"format": "cpp",
|
||||
"locale": "cpp",
|
||||
"stdfloat": "cpp",
|
||||
"text_encoding": "cpp"
|
||||
"text_encoding": "cpp",
|
||||
"forward_list": "cpp"
|
||||
},
|
||||
"cSpell.words": [
|
||||
"Alsa",
|
||||
@@ -134,5 +135,25 @@
|
||||
"cSpell.ignoreWords": [
|
||||
"nammodel"
|
||||
],
|
||||
"cSpell.enabled": false
|
||||
"cSpell.enabled": false,
|
||||
|
||||
// Disable all automatic completion suggestions - only show when Ctrl+Space is pressed
|
||||
"editor.quickSuggestions": {
|
||||
"other": false,
|
||||
"comments": false,
|
||||
"strings": false
|
||||
},
|
||||
|
||||
// Disable suggestions on trigger characters (like . or :)
|
||||
"editor.suggestOnTriggerCharacters": false,
|
||||
|
||||
// Disable word-based suggestions from open files
|
||||
"editor.wordBasedSuggestions": "off",
|
||||
|
||||
// Don't accept suggestions on Enter (optional)
|
||||
"editor.acceptSuggestionOnEnter": "off",
|
||||
|
||||
// Disable parameter hints popup (optional)
|
||||
"editor.parameterHints.enabled": false,
|
||||
|
||||
}
|
||||
+4
-3
@@ -1,22 +1,23 @@
|
||||
cmake_minimum_required(VERSION 3.16.0)
|
||||
project(pipedal
|
||||
VERSION 1.4.76
|
||||
VERSION 1.4.77
|
||||
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.4.76-Release")
|
||||
set (DISPLAY_VERSION "PiPedal v1.4.77-Beta")
|
||||
set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE})
|
||||
set (CMAKE_INSTALL_PREFIX "/usr/")
|
||||
|
||||
set (PIPEDAL_EXCLUDE_TESTS false)
|
||||
include(CTest)
|
||||
enable_testing()
|
||||
|
||||
|
||||
#add_subdirectory("submodules/pipedal_p2pd")
|
||||
|
||||
add_subdirectory("modules/SQLiteCpp")
|
||||
add_subdirectory("PiPedalCommon")
|
||||
|
||||
add_subdirectory("vite")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,8 @@ set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")
|
||||
|
||||
if (CMAKE_BUILD_TYPE MATCHES Debug)
|
||||
message(STATUS "Debug build")
|
||||
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -DDEBUG -D_GLIBCXX_DEBUG" )
|
||||
# Must not use -D_GLIBCXX_DEBUG (incompatible with SQLiteCpp)
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -DDEBUG " )
|
||||
endif()
|
||||
|
||||
# Can't get the pkg_check to work.
|
||||
@@ -41,6 +41,7 @@ message(STATUS "NMPIPEDAL CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}")
|
||||
|
||||
# Use the sdbus-c++ target in SDBusCpp namespace
|
||||
add_library(PiPedalCommon STATIC
|
||||
include/Finally.hpp
|
||||
include/dbus/org.freedesktop.NetworkManager.Device.Statistics.hpp
|
||||
include/dbus/org.freedesktop.NetworkManager.Settings.Connection.hpp
|
||||
include/dbus/org.freedesktop.NetworkManager.IP6Config.hpp
|
||||
@@ -57,6 +58,7 @@ add_library(PiPedalCommon STATIC
|
||||
include/dbus/org.freedesktop.NetworkManager.Device.Wireless.hpp
|
||||
include/dbus/org.freedesktop.NetworkManager.DHCP4Config.hpp
|
||||
|
||||
AlsaSequencer.cpp include/AlsaSequencer.hpp
|
||||
Utf8Utils.cpp include/Utf8Utils.hpp
|
||||
NetworkManagerInterfaces.cpp
|
||||
include/NetworkManagerInterfaces.hpp
|
||||
|
||||
@@ -39,6 +39,17 @@ std::string HtmlHelper::timeToHttpDate()
|
||||
return timeToHttpDate(time(nullptr));
|
||||
}
|
||||
|
||||
|
||||
std::string HtmlHelper::timeToHttpDate(std::filesystem::file_time_type time)
|
||||
{
|
||||
|
||||
// Convert to time_t.
|
||||
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(time - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now());
|
||||
|
||||
auto time_t_value = std::chrono::system_clock::to_time_t(sctp);
|
||||
return timeToHttpDate(time_t_value);
|
||||
}
|
||||
|
||||
std::string HtmlHelper::timeToHttpDate(time_t time)
|
||||
{
|
||||
// RFC 7231, IMF-fixdate.
|
||||
|
||||
@@ -268,7 +268,7 @@ int pipedal::sysExecWait(ProcessId pid_)
|
||||
|
||||
}
|
||||
|
||||
SysExecOutput pipedal::sysExecForOutput(const std::string &program, const std::string &args)
|
||||
SysExecOutput pipedal::sysExecForOutput(const std::string &program, const std::string &args, bool discardStderr)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
fs::path fullPath = findOnSystemPath(program);
|
||||
@@ -277,7 +277,13 @@ SysExecOutput pipedal::sysExecForOutput(const std::string &program, const std::s
|
||||
throw std::runtime_error(SS("Path does not exist. " << fullPath));
|
||||
}
|
||||
std::stringstream s;
|
||||
s << fullPath.c_str() << " " << args << " 2>&1";
|
||||
s << fullPath.c_str() << " " << args;
|
||||
if (discardStderr)
|
||||
{
|
||||
s << " 2>/dev/null";
|
||||
} else {
|
||||
s << " 2>&1"; // redirect stderr to stdout
|
||||
}
|
||||
|
||||
std::string fullCommand = s.str();
|
||||
FILE *output = popen(fullCommand.c_str(), "r");
|
||||
|
||||
@@ -260,81 +260,123 @@ namespace pipedal
|
||||
auto p = v.begin();
|
||||
while (p != v.end())
|
||||
{
|
||||
uint32_t uc;
|
||||
uint8_t c = (uint8_t)*p++;
|
||||
if ((c & UTF8_ONE_BYTE_MASK) == UTF8_ONE_BYTE_BITS)
|
||||
{
|
||||
uc = c;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t c2 = continuation_byte(p, v.end());
|
||||
|
||||
if ((c & UTF8_TWO_BYTES_MASK) == UTF8_TWO_BYTES_BITS)
|
||||
try {
|
||||
uint32_t uc;
|
||||
uint8_t c = (uint8_t)*p++;
|
||||
if ((c & UTF8_ONE_BYTE_MASK) == UTF8_ONE_BYTE_BITS)
|
||||
{
|
||||
uint32_t c1 = c & (uint32_t)(~UTF8_TWO_BYTES_MASK);
|
||||
if (c1 <= 1 && enforceValidUtf8Encoding)
|
||||
{
|
||||
// overlong encoding.
|
||||
throw_encoding_error();
|
||||
}
|
||||
uc = (c1 << 6) | c2;
|
||||
uc = c;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t c3 = continuation_byte(p, v.end());
|
||||
uint32_t c2 = continuation_byte(p, v.end());
|
||||
|
||||
if ((c & UTF8_THREE_BYTES_MASK) == UTF8_THREE_BYTES_BITS)
|
||||
if ((c & UTF8_TWO_BYTES_MASK) == UTF8_TWO_BYTES_BITS)
|
||||
{
|
||||
uint32_t c1 = c & (uint32_t)~UTF8_THREE_BYTES_MASK;
|
||||
if (c1 == 0 && c2 < 0x20 && enforceValidUtf8Encoding)
|
||||
uint32_t c1 = c & (uint32_t)(~UTF8_TWO_BYTES_MASK);
|
||||
if (c1 <= 1 && enforceValidUtf8Encoding)
|
||||
{
|
||||
// overlong encoding.
|
||||
throw_encoding_error();
|
||||
}
|
||||
|
||||
uc = (c1) << 12 | (c2 << 6) | c3;
|
||||
uc = (c1 << 6) | c2;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t c4 = continuation_byte(p, v.end());
|
||||
if ((c & UTF8_FOUR_BYTES_MASK) == UTF8_FOUR_BYTES_BITS)
|
||||
uint32_t c3 = continuation_byte(p, v.end());
|
||||
|
||||
if ((c & UTF8_THREE_BYTES_MASK) == UTF8_THREE_BYTES_BITS)
|
||||
{
|
||||
uint32_t c1 = c & (uint32_t)~UTF8_FOUR_BYTES_MASK;
|
||||
if (c1 == 0 && c2 < 0x10 && enforceValidUtf8Encoding)
|
||||
uint32_t c1 = c & (uint32_t)~UTF8_THREE_BYTES_MASK;
|
||||
if (c1 == 0 && c2 < 0x20 && enforceValidUtf8Encoding)
|
||||
{
|
||||
// overlong encoding.
|
||||
throw_encoding_error();
|
||||
}
|
||||
uc = (c1 << 18) | (c2 << 12) | (c3 << 6) | c4;
|
||||
|
||||
uc = (c1) << 12 | (c2 << 6) | c3;
|
||||
}
|
||||
else
|
||||
{
|
||||
// outside legal UCS range.
|
||||
throw_encoding_error();
|
||||
uint32_t c4 = continuation_byte(p, v.end());
|
||||
if ((c & UTF8_FOUR_BYTES_MASK) == UTF8_FOUR_BYTES_BITS)
|
||||
{
|
||||
uint32_t c1 = c & (uint32_t)~UTF8_FOUR_BYTES_MASK;
|
||||
if (c1 == 0 && c2 < 0x10 && enforceValidUtf8Encoding)
|
||||
{
|
||||
// overlong encoding.
|
||||
throw_encoding_error();
|
||||
}
|
||||
uc = (c1 << 18) | (c2 << 12) | (c3 << 6) | c4;
|
||||
}
|
||||
else
|
||||
{
|
||||
// outside legal UCS range.
|
||||
throw_encoding_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (uc < 0x10000ul)
|
||||
{
|
||||
os << (char16_t)uc;
|
||||
}
|
||||
else
|
||||
{
|
||||
// write UTF-16 surrogate pair.
|
||||
uc -= 0x10000;
|
||||
if (uc < 0x10000ul)
|
||||
{
|
||||
os << (char16_t)uc;
|
||||
}
|
||||
else
|
||||
{
|
||||
// write UTF-16 surrogate pair.
|
||||
uc -= 0x10000;
|
||||
|
||||
char16_t s1 = (char16_t)(UTF16_SURROGATE_1_BASE + ((uc >> 10) & 0x3FFu));
|
||||
char16_t s2 = (char16_t)(UTF16_SURROGATE_2_BASE + (uc & 0x03FFu));
|
||||
// surrogate pair.
|
||||
os << s1;
|
||||
os << s2;
|
||||
char16_t s1 = (char16_t)(UTF16_SURROGATE_1_BASE + ((uc >> 10) & 0x3FFu));
|
||||
char16_t s2 = (char16_t)(UTF16_SURROGATE_2_BASE + (uc & 0x03FFu));
|
||||
// surrogate pair.
|
||||
os << s1;
|
||||
os << s2;
|
||||
}
|
||||
} catch (const std::exception &e)
|
||||
{
|
||||
os << u'\uFFFD'; // replacement character for invalid sequences.
|
||||
}
|
||||
}
|
||||
return os.str();
|
||||
}
|
||||
|
||||
bool IsValidUtf8(const std::string &text)
|
||||
{
|
||||
auto p = text.begin();
|
||||
while (p != text.end())
|
||||
{
|
||||
uint8_t c = *p++;
|
||||
if ((c & UTF8_ONE_BYTE_MASK) == UTF8_ONE_BYTE_BITS)
|
||||
{
|
||||
// 1-byte character (ASCII)
|
||||
continue;
|
||||
}
|
||||
else if ((c & UTF8_TWO_BYTES_MASK) == UTF8_TWO_BYTES_BITS)
|
||||
{
|
||||
// 2-byte character
|
||||
if (p == text.end() || ((*p++ & UTF8_CONTINUATION_MASK) != UTF8_CONTINUATION_BITS))
|
||||
return false;
|
||||
}
|
||||
else if ((c & UTF8_THREE_BYTES_MASK) == UTF8_THREE_BYTES_BITS)
|
||||
{
|
||||
// 3-byte character
|
||||
if (p + 1 >= text.end() || ((*p++ & UTF8_CONTINUATION_MASK) != UTF8_CONTINUATION_BITS) || ((*p++ & UTF8_CONTINUATION_MASK) != UTF8_CONTINUATION_BITS))
|
||||
return false;
|
||||
}
|
||||
else if ((c & UTF8_FOUR_BYTES_MASK) == UTF8_FOUR_BYTES_BITS)
|
||||
{
|
||||
// 4-byte character
|
||||
if (p + 2 >= text.end() || ((*p++ & UTF8_CONTINUATION_MASK) != UTF8_CONTINUATION_BITS) || ((*p++ & UTF8_CONTINUATION_MASK) != UTF8_CONTINUATION_BITS))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid UTF-8
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -89,7 +89,6 @@ bool WifiConfigSettings::ValidateCountryCode(const std::string &text)
|
||||
return std::isalpha(text[0]) && std::isalpha(text[1]);
|
||||
}
|
||||
|
||||
|
||||
void WifiConfigSettings::Load()
|
||||
{
|
||||
try
|
||||
@@ -115,9 +114,9 @@ void WifiConfigSettings::Load()
|
||||
}
|
||||
|
||||
static void openWithPerms(
|
||||
pipedal::ofstream_synced &f,
|
||||
const std::filesystem::path &path,
|
||||
std::filesystem::perms perms =
|
||||
pipedal::ofstream_synced &f,
|
||||
const std::filesystem::path &path,
|
||||
std::filesystem::perms perms =
|
||||
std::filesystem::perms::owner_read | std::filesystem::perms::owner_write |
|
||||
std::filesystem::perms::group_read | std::filesystem::perms::group_write)
|
||||
{
|
||||
@@ -132,13 +131,16 @@ static void openWithPerms(
|
||||
f.open(path);
|
||||
f.close();
|
||||
}
|
||||
try {
|
||||
try
|
||||
{
|
||||
// set the perms.
|
||||
std::filesystem::permissions(
|
||||
path,
|
||||
perms,
|
||||
std::filesystem::perm_options::replace);
|
||||
} catch (const std::exception&) {
|
||||
std::filesystem::perm_options::replace);
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
Lv2Log::warning(SS("Failed to set permissions on" << path << "."));
|
||||
}
|
||||
}
|
||||
@@ -147,10 +149,10 @@ static void openWithPerms(
|
||||
f.open(path);
|
||||
}
|
||||
|
||||
void WifiConfigSettings::Save()
|
||||
void WifiConfigSettings::Save()
|
||||
{
|
||||
WifiConfigSettings newSettings {*this};
|
||||
|
||||
WifiConfigSettings newSettings{*this};
|
||||
|
||||
// sync legacy settings, just in case i don't know what.
|
||||
newSettings.mdnsName_ = newSettings.hotspotName_;
|
||||
newSettings.enable_ = newSettings.IsEnabled();
|
||||
@@ -163,12 +165,12 @@ void WifiConfigSettings::Save()
|
||||
newSettings.password_ = oldSettings.password_;
|
||||
}
|
||||
newSettings.hasSavedPassword_ = newSettings.hasPassword_;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
ofstream_synced f;
|
||||
openWithPerms(f,CONFIG_PATH);
|
||||
json_writer writer(f,false);
|
||||
openWithPerms(f, CONFIG_PATH);
|
||||
json_writer writer(f, false);
|
||||
writer.write(&newSettings);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
@@ -300,17 +302,14 @@ namespace pipedal::priv
|
||||
connection["ipv4"]["method"] = sdbus::Variant(std::string("shared"));
|
||||
connection["ipv6"]["method"] = sdbus::Variant(std::string("ignore"));
|
||||
|
||||
|
||||
connection["ipv4"]["address-data"] = sdbus::Variant(std::vector<std::map<std::string, sdbus::Variant>>{{
|
||||
{"address", sdbus::Variant("192.168.4.1")},
|
||||
{"prefix", sdbus::Variant(uint32_t(24))}
|
||||
}});
|
||||
connection["ipv4"]["address-data"] = sdbus::Variant(std::vector<std::map<std::string, sdbus::Variant>>{{{"address", sdbus::Variant("192.168.4.1")},
|
||||
{"prefix", sdbus::Variant(uint32_t(24))}}});
|
||||
connection["ipv4"]["dhcp-send-hostname"] = sdbus::Variant(true);
|
||||
connection["ipv4"]["dhcp-hostname"] = sdbus::Variant("raspberrypi");
|
||||
connection["ipv4"]["dhcp-options"] = sdbus::Variant(std::vector<std::string>{
|
||||
"option:classless-static-route,192.168.4.0/24,0.0.0.0,0.0.0.0/0,192.168.4.1"
|
||||
//"option:classless-static-route,192.168.4.0/24,192.168.4.1"
|
||||
});
|
||||
});
|
||||
|
||||
auto connection_path = AddConnection(connection);
|
||||
return connection_path;
|
||||
@@ -326,7 +325,6 @@ namespace pipedal::priv
|
||||
return active_connection_path;
|
||||
}
|
||||
|
||||
|
||||
void RemoveConnection(const std::string &connection)
|
||||
{
|
||||
auto connection_proxy = sdbus::createProxy(
|
||||
@@ -429,7 +427,6 @@ namespace pipedal::priv
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
JSON_MAP_BEGIN(WifiConfigSettings)
|
||||
// v0
|
||||
JSON_MAP_REFERENCE(WifiConfigSettings, valid)
|
||||
@@ -560,52 +557,51 @@ bool WifiConfigSettings::ValidateChannel(const std::string &countryCode, const s
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char* trueValues[] {
|
||||
static const char *trueValues[]{
|
||||
"true",
|
||||
"on",
|
||||
"yes"
|
||||
"1",
|
||||
nullptr
|
||||
};
|
||||
static const char* falseValues[] {
|
||||
nullptr};
|
||||
static const char *falseValues[]{
|
||||
"false",
|
||||
"off",
|
||||
"no",
|
||||
"0",
|
||||
nullptr
|
||||
};
|
||||
nullptr};
|
||||
|
||||
static bool Matches(const std::string&value, const char**matches)
|
||||
static bool Matches(const std::string &value, const char **matches)
|
||||
{
|
||||
while(*matches)
|
||||
while (*matches)
|
||||
{
|
||||
if (value == *matches) return true;
|
||||
if (value == *matches)
|
||||
return true;
|
||||
++matches;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool TryStringToBool(const std::string &value, bool*outputValue)
|
||||
static bool TryStringToBool(const std::string &value, bool *outputValue)
|
||||
{
|
||||
if (Matches(value,trueValues))
|
||||
if (Matches(value, trueValues))
|
||||
{
|
||||
*outputValue = true;
|
||||
return true;
|
||||
}
|
||||
if (Matches(value, falseValues)) {
|
||||
if (Matches(value, falseValues))
|
||||
{
|
||||
*outputValue = false;
|
||||
return true;
|
||||
}
|
||||
*outputValue = false;
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
static WifiConfigSettings::ssid_t readSsid(std::istream&ss)
|
||||
static WifiConfigSettings::ssid_t readSsid(std::istream &ss)
|
||||
{
|
||||
using ssid_t = WifiConfigSettings::ssid_t;
|
||||
char c;
|
||||
|
||||
|
||||
ssid_t result;
|
||||
while (ss.peek() == ' ')
|
||||
{
|
||||
@@ -624,28 +620,33 @@ static WifiConfigSettings::ssid_t readSsid(std::istream&ss)
|
||||
break;
|
||||
}
|
||||
ss >> c;
|
||||
switch (c) {
|
||||
case 'n':
|
||||
result.push_back((uint8_t)'\n');
|
||||
break;
|
||||
case 'r':
|
||||
result.push_back((uint8_t)'\r');
|
||||
break;
|
||||
case 't':
|
||||
result.push_back((uint8_t)'\t');
|
||||
break;
|
||||
case 'b':
|
||||
result.push_back((uint8_t)'\b');
|
||||
break;
|
||||
default:
|
||||
result.push_back((uint8_t)c);
|
||||
break;
|
||||
switch (c)
|
||||
{
|
||||
case 'n':
|
||||
result.push_back((uint8_t)'\n');
|
||||
break;
|
||||
case 'r':
|
||||
result.push_back((uint8_t)'\r');
|
||||
break;
|
||||
case 't':
|
||||
result.push_back((uint8_t)'\t');
|
||||
break;
|
||||
case 'b':
|
||||
result.push_back((uint8_t)'\b');
|
||||
break;
|
||||
default:
|
||||
result.push_back((uint8_t)c);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
result.push_back((uint8_t)c);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
while (!ss.eof())
|
||||
{
|
||||
if (c == ':')
|
||||
@@ -662,7 +663,7 @@ static WifiConfigSettings::ssid_t readSsid(std::istream&ss)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static std::vector<WifiConfigSettings::ssid_t> stringToSsidArray(const std::string&value)
|
||||
static std::vector<WifiConfigSettings::ssid_t> stringToSsidArray(const std::string &value)
|
||||
{
|
||||
using ssid_t = WifiConfigSettings::ssid_t;
|
||||
std::istringstream ss(value);
|
||||
@@ -671,7 +672,7 @@ static std::vector<WifiConfigSettings::ssid_t> stringToSsidArray(const std::stri
|
||||
while (true)
|
||||
{
|
||||
ssid_t ssid = readSsid(ss);
|
||||
if (ssid.size() == 0)
|
||||
if (ssid.size() == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -683,16 +684,15 @@ static std::vector<WifiConfigSettings::ssid_t> stringToSsidArray(const std::stri
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
void WifiConfigSettings::ParseArguments(
|
||||
const std::vector<std::string> &argv,
|
||||
HotspotAutoStartMode startMode,
|
||||
const std::string homeNetworkSsid
|
||||
|
||||
)
|
||||
)
|
||||
{
|
||||
this->valid_ = false ;
|
||||
this->valid_ = false;
|
||||
if (argv.size() != 4)
|
||||
{
|
||||
throw invalid_argument("Invalid number of arguments.");
|
||||
@@ -714,7 +714,6 @@ void WifiConfigSettings::ParseArguments(
|
||||
this->hasPassword_ = this->password_.length() != 0;
|
||||
this->hasSavedPassword_ = oldSettings.hasSavedPassword_;
|
||||
|
||||
|
||||
if (!ValidateCountryCode(this->countryCode_))
|
||||
{
|
||||
throw invalid_argument("Invalid country code.");
|
||||
@@ -723,7 +722,7 @@ void WifiConfigSettings::ParseArguments(
|
||||
throw invalid_argument("Hotspot name is too long.");
|
||||
if (this->hotspotName_.length() < 1)
|
||||
throw invalid_argument("Hotspot name is too short.");
|
||||
|
||||
|
||||
if (this->password_.size() != 0 && this->password_.size() < 8)
|
||||
throw invalid_argument("Passphrase must be at least 8 characters long.");
|
||||
|
||||
@@ -742,12 +741,12 @@ void WifiConfigSettings::ParseArguments(
|
||||
this->valid_ = true;
|
||||
}
|
||||
|
||||
bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) const
|
||||
bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings &other) const
|
||||
{
|
||||
return !(
|
||||
this->valid_ == other.valid_ &&
|
||||
//this->rebootRequired_ == other.rebootRequired_ &&
|
||||
//this->enable_ == other.enable_ &&
|
||||
// this->rebootRequired_ == other.rebootRequired_ &&
|
||||
// this->enable_ == other.enable_ &&
|
||||
this->countryCode_ == other.countryCode_ &&
|
||||
this->hotspotName_ == other.hotspotName_ &&
|
||||
this->hasPassword_ == other.hasPassword_ &&
|
||||
@@ -756,20 +755,18 @@ bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) co
|
||||
|
||||
this->homeNetwork_ == other.homeNetwork_ &&
|
||||
this->autoStartMode_ == other.autoStartMode_ &&
|
||||
this->hasSavedPassword_ == other.hasSavedPassword_
|
||||
);
|
||||
|
||||
this->hasSavedPassword_ == other.hasSavedPassword_);
|
||||
}
|
||||
bool WifiConfigSettings::operator==(const WifiConfigSettings&other) const
|
||||
bool WifiConfigSettings::operator==(const WifiConfigSettings &other) const
|
||||
{
|
||||
return !ConfigurationChanged(other) && this->wifiWarningGiven_ == other.wifiWarningGiven_;
|
||||
}
|
||||
|
||||
static bool CanSeeHomeNetwork(const std::string&home, const std::vector<std::string>&availableNetworks)
|
||||
static bool CanSeeHomeNetwork(const std::string &home, const std::vector<std::string> &availableNetworks)
|
||||
{
|
||||
for (const auto &availableNetwork: availableNetworks)
|
||||
for (const auto &availableNetwork : availableNetworks)
|
||||
{
|
||||
if (availableNetwork == home)
|
||||
if (availableNetwork == home)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -778,38 +775,39 @@ static bool CanSeeHomeNetwork(const std::string&home, const std::vector<std::str
|
||||
}
|
||||
|
||||
bool WifiConfigSettings::WantsHotspot(
|
||||
bool ethernetConnected,
|
||||
bool ethernetConnected,
|
||||
const std::vector<std::string> &availableRememberedNetworks,
|
||||
const std::vector<std::string> &availableNetworks)
|
||||
{
|
||||
if ((!this->valid_))
|
||||
if ((!this->valid_))
|
||||
return false;
|
||||
|
||||
|
||||
HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_;
|
||||
|
||||
switch (autoStartMode)
|
||||
{
|
||||
case HotspotAutoStartMode::Never:
|
||||
default:
|
||||
return false;
|
||||
case HotspotAutoStartMode::Never:
|
||||
default:
|
||||
return false;
|
||||
|
||||
case HotspotAutoStartMode::NoEthernetConnection:
|
||||
return !ethernetConnected;
|
||||
case HotspotAutoStartMode::NotAtHome:
|
||||
return !CanSeeHomeNetwork(this->homeNetwork_, availableNetworks);
|
||||
case HotspotAutoStartMode::NoRememberedWifiConections:
|
||||
return availableRememberedNetworks.size() == 0;
|
||||
case HotspotAutoStartMode::Always:
|
||||
return true;
|
||||
case HotspotAutoStartMode::NoEthernetConnection:
|
||||
return !ethernetConnected;
|
||||
case HotspotAutoStartMode::NotAtHome:
|
||||
return !CanSeeHomeNetwork(this->homeNetwork_, availableNetworks);
|
||||
case HotspotAutoStartMode::NoRememberedWifiConections:
|
||||
return availableRememberedNetworks.size() == 0;
|
||||
case HotspotAutoStartMode::Always:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string pipedal::ssidToString(const std::vector<uint8_t> &ssid)
|
||||
{
|
||||
std::stringstream s;
|
||||
for (auto v: ssid)
|
||||
for (auto v : ssid)
|
||||
{
|
||||
if (v == 0) break; // breaks for some arguably legal ssids, but I can live with that.
|
||||
if (v == 0)
|
||||
break; // breaks for some arguably legal ssids, but I can live with that.
|
||||
s << (char)v;
|
||||
}
|
||||
return s.str();
|
||||
@@ -819,21 +817,43 @@ std::vector<std::string> pipedal::ssidToStringVector(const std::vector<std::vect
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
result.reserve(ssids.size());
|
||||
for (const std::vector<uint8_t> &ssid: ssids)
|
||||
for (const std::vector<uint8_t> &ssid : ssids)
|
||||
{
|
||||
result.push_back(ssidToString(ssid));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
bool WifiConfigSettings::WantsHotspot(
|
||||
bool ethernetConnected,
|
||||
bool ethernetConnected,
|
||||
const std::vector<std::vector<uint8_t>> &availableRememberedNetworks, // remembered networks that are currently visible
|
||||
const std::vector<std::vector<uint8_t>> &availableNetworks // all visible networks.
|
||||
)
|
||||
const std::vector<std::vector<uint8_t>> &availableNetworks // all visible networks.
|
||||
)
|
||||
{
|
||||
std::vector<std::string> sAvailableRememberedNetworks = ssidToStringVector(availableRememberedNetworks);
|
||||
std::vector<std::string> sAvailableNetworks = ssidToStringVector(availableNetworks);
|
||||
return WantsHotspot(ethernetConnected,sAvailableRememberedNetworks,sAvailableNetworks);
|
||||
return WantsHotspot(ethernetConnected, sAvailableRememberedNetworks, sAvailableNetworks);
|
||||
}
|
||||
|
||||
bool WifiConfigSettings::NeedsWifi() const
|
||||
{
|
||||
HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_;
|
||||
return autoStartMode != HotspotAutoStartMode::Never;
|
||||
}
|
||||
|
||||
|
||||
bool WifiConfigSettings::NeedsScan() const
|
||||
{
|
||||
HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_;
|
||||
switch (autoStartMode)
|
||||
{
|
||||
case HotspotAutoStartMode::Never:
|
||||
case HotspotAutoStartMode::NoEthernetConnection:
|
||||
case HotspotAutoStartMode::Always:
|
||||
default:
|
||||
return false;
|
||||
|
||||
case HotspotAutoStartMode::NotAtHome:
|
||||
case HotspotAutoStartMode::NoRememberedWifiConections:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include "json.hpp"
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
|
||||
// event messages starting with 0xFF are meta events.
|
||||
// data not in midi format, is variable length, and private and the second byte is the MetaEventType.
|
||||
enum class MetaEventType
|
||||
{
|
||||
None,
|
||||
Tempo,
|
||||
TimeSignature,
|
||||
KeySignature,
|
||||
SetPositionTick,
|
||||
SetPositionTime
|
||||
};
|
||||
|
||||
class AlsaSequencerPortSelection {
|
||||
private:
|
||||
std::string id_;
|
||||
std::string name_;
|
||||
int32_t sortOrder_ = 0;
|
||||
|
||||
public:
|
||||
AlsaSequencerPortSelection() = default;
|
||||
AlsaSequencerPortSelection(const std::string &id, const std::string &name, int32_t displaySortOrder)
|
||||
: id_(id), name_(name), sortOrder_(displaySortOrder) {}
|
||||
AlsaSequencerPortSelection(const AlsaSequencerPortSelection &other) = default;
|
||||
AlsaSequencerPortSelection(AlsaSequencerPortSelection &&other) = default;
|
||||
AlsaSequencerPortSelection &operator=(const AlsaSequencerPortSelection &other) = default;
|
||||
AlsaSequencerPortSelection &operator=(AlsaSequencerPortSelection &&other) = default;
|
||||
|
||||
|
||||
const std::string& id() const { return id_; }
|
||||
void id(const std::string &value) { id_ = value; }
|
||||
const std::string &name() const { return name_; }
|
||||
void name(const std::string &value) { name_ = value; }
|
||||
|
||||
DECLARE_JSON_MAP(AlsaSequencerPortSelection);
|
||||
};
|
||||
class AlsaSequencerConfiguration {
|
||||
private:
|
||||
std::vector<AlsaSequencerPortSelection> connections_;
|
||||
public:
|
||||
const std::vector<AlsaSequencerPortSelection>& connections() const { return connections_; }
|
||||
std::vector<AlsaSequencerPortSelection>& connections() { return connections_; }
|
||||
void connections(const std::vector<AlsaSequencerPortSelection>& value) { connections_ = value; }
|
||||
|
||||
|
||||
bool operator==(const AlsaSequencerConfiguration &other) const
|
||||
{
|
||||
if (connections_.size() != other.connections_.size())
|
||||
return false;
|
||||
for (size_t i = 0; i < connections_.size(); ++i)
|
||||
{
|
||||
if (connections_[i].id() != other.connections_[i].id() ||
|
||||
connections_[i].name() != other.connections_[i].name())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
DECLARE_JSON_MAP(AlsaSequencerConfiguration);
|
||||
|
||||
};
|
||||
|
||||
|
||||
struct AlsaSequencerPort
|
||||
{
|
||||
std::string id;
|
||||
int client;
|
||||
int port;
|
||||
std::string name;
|
||||
std::string clientName;
|
||||
bool canRead = false;
|
||||
bool canWrite = false;
|
||||
bool canReadSubscribe = false;
|
||||
bool canWriteSubscribe = false;
|
||||
|
||||
bool isKernelDevice = false;
|
||||
bool isSystemAnnounce = false;
|
||||
bool isUmp = false;
|
||||
bool isMidi = false;
|
||||
bool isApplication = false;
|
||||
bool isMidiSynth = false;
|
||||
bool isSynth = false;
|
||||
bool isSpecific;
|
||||
bool isHardware = false;
|
||||
bool isSoftware = false;
|
||||
bool isPort = false;
|
||||
bool isVirtual = false;
|
||||
int cardNumber = -1;
|
||||
int32_t displaySortOrder = 0;
|
||||
std::string rawMidiDevice; // e.g. "hw:0,0,0" for kernel devices
|
||||
};
|
||||
|
||||
struct AlsaMidiMessage
|
||||
{
|
||||
AlsaMidiMessage()
|
||||
{
|
||||
size = 0;
|
||||
data = fixedBuffer;
|
||||
}
|
||||
AlsaMidiMessage(uint8_t cc0)
|
||||
{
|
||||
size = 1;
|
||||
data = fixedBuffer;
|
||||
fixedBuffer[0] = cc0;
|
||||
}
|
||||
AlsaMidiMessage(uint8_t cc0, uint8_t cc1)
|
||||
{
|
||||
size = 2;
|
||||
data = fixedBuffer;
|
||||
fixedBuffer[0] = cc0;
|
||||
fixedBuffer[1] = cc1;
|
||||
}
|
||||
AlsaMidiMessage(uint8_t cc0, uint8_t cc1, uint8_t cc2)
|
||||
{
|
||||
size = 3;
|
||||
data = fixedBuffer;
|
||||
fixedBuffer[0] = cc0;
|
||||
fixedBuffer[1] = cc1;
|
||||
fixedBuffer[2] = cc2;
|
||||
}
|
||||
AlsaMidiMessage(uint8_t *data, size_t size)
|
||||
{
|
||||
this->size = size;
|
||||
this->data = data;
|
||||
}
|
||||
AlsaMidiMessage(const AlsaMidiMessage &other)
|
||||
{
|
||||
this->timestamp = other.timestamp;
|
||||
this->realtime_sec = other.realtime_sec;
|
||||
this->realtime_nsec = other.realtime_nsec;
|
||||
|
||||
if (other.size > 3)
|
||||
{
|
||||
this->size = other.size;
|
||||
this->data = other.data;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->size = other.size;
|
||||
this->data = fixedBuffer;
|
||||
for (size_t i = 0; i < other.size; ++i)
|
||||
{
|
||||
fixedBuffer[i] = other.fixedBuffer[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
AlsaMidiMessage &operator=(const AlsaMidiMessage &other)
|
||||
{
|
||||
this->timestamp = other.timestamp;
|
||||
this->realtime_sec = other.realtime_sec;
|
||||
this->realtime_nsec = other.realtime_nsec;
|
||||
|
||||
if (other.size > 3)
|
||||
{
|
||||
this->size = other.size;
|
||||
this->data = other.data;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->size = other.size;
|
||||
this->data = fixedBuffer;
|
||||
for (size_t i = 0; i < other.size; ++i)
|
||||
{
|
||||
fixedBuffer[i] = other.fixedBuffer[i];
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
AlsaMidiMessage(AlsaMidiMessage &&other)
|
||||
{
|
||||
this->timestamp = other.timestamp;
|
||||
this->realtime_sec = other.realtime_sec;
|
||||
this->realtime_nsec = other.realtime_nsec;
|
||||
|
||||
if (other.size > 3)
|
||||
{
|
||||
this->size = other.size;
|
||||
this->data = other.data;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->size = other.size;
|
||||
this->data = fixedBuffer;
|
||||
for (size_t i = 0; i < other.size; ++i)
|
||||
{
|
||||
fixedBuffer[i] = other.fixedBuffer[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
AlsaMidiMessage &operator=(AlsaMidiMessage &&other)
|
||||
{
|
||||
this->timestamp = other.timestamp;
|
||||
this->realtime_sec = other.realtime_sec;
|
||||
this->realtime_nsec = other.realtime_nsec;
|
||||
|
||||
if (other.size > 3)
|
||||
{
|
||||
this->size = other.size;
|
||||
this->data = other.data;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->size = other.size;
|
||||
this->data = fixedBuffer;
|
||||
for (size_t i = 0; i < other.size; ++i)
|
||||
{
|
||||
fixedBuffer[i] = other.fixedBuffer[i];
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Set(uint8_t cc0)
|
||||
{
|
||||
size = 1;
|
||||
data = fixedBuffer;
|
||||
fixedBuffer[0] = cc0;
|
||||
}
|
||||
void Set(uint8_t cc0, uint8_t cc1)
|
||||
{
|
||||
size = 2;
|
||||
data = fixedBuffer;
|
||||
fixedBuffer[0] = cc0;
|
||||
fixedBuffer[1] = cc1;
|
||||
}
|
||||
void Set(uint8_t cc0, uint8_t cc1, uint8_t cc2)
|
||||
{
|
||||
size = 3;
|
||||
data = fixedBuffer;
|
||||
fixedBuffer[0] = cc0;
|
||||
fixedBuffer[1] = cc1;
|
||||
fixedBuffer[2] = cc2;
|
||||
}
|
||||
void Set(uint8_t *data, size_t size)
|
||||
{
|
||||
this->size = size;
|
||||
this->data = data;
|
||||
}
|
||||
uint8_t cc0() const { return size > 0 ? data[0] : 0; }
|
||||
uint8_t cc1() const { return size > 1 ? data[1] : 0; }
|
||||
uint8_t cc2() const { return size > 2 ? data[2] : 0; }
|
||||
|
||||
|
||||
uint32_t timestamp; // in microseconds (tick time if using queue)
|
||||
uint64_t realtime_sec; // real-time timestamp seconds
|
||||
uint32_t realtime_nsec; // real-time timestamp nanoseconds
|
||||
size_t size = 0;
|
||||
uint8_t *data = fixedBuffer;
|
||||
|
||||
uint8_t fixedBuffer[12];
|
||||
};
|
||||
|
||||
/*
|
||||
* AlsaSequencer - ALSA MIDI sequencer interface with real-time timestamp support
|
||||
*
|
||||
* Example usage with real-time timestamps:
|
||||
*
|
||||
* AlsaSequencer sequencer;
|
||||
* sequencer.connect(clientId, portId);
|
||||
*
|
||||
* AlsaMidiMessage message;
|
||||
* while (sequencer.ReadMessage(&message)) {
|
||||
* // message.realtime_sec and message.realtime_nsec contain precise real-time timestamps
|
||||
* // message.timestamp contains the queue tick time
|
||||
* printf("MIDI event at %lu.%09u seconds\n", message.realtime_sec, message.realtime_nsec);
|
||||
* }
|
||||
*/
|
||||
|
||||
class AlsaSequencer
|
||||
{
|
||||
protected:
|
||||
AlsaSequencer() {}
|
||||
|
||||
public:
|
||||
virtual ~AlsaSequencer() = default;
|
||||
|
||||
using self = AlsaSequencer;
|
||||
using ptr = std::shared_ptr<self>;
|
||||
|
||||
static ptr Create();
|
||||
|
||||
static std::vector<AlsaSequencerPort> EnumeratePorts();
|
||||
|
||||
public:
|
||||
virtual void ConnectPort(int clientId, int portId) = 0;
|
||||
virtual void ConnectPort(const std::string &id) = 0;
|
||||
|
||||
virtual void SetConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
|
||||
|
||||
// Read a single MIDI message from the sequencer input port. A timeout of -1 blocks indefinitely.
|
||||
// A timeout of 0 returns immediately.
|
||||
virtual bool ReadMessage(AlsaMidiMessage &message, int timeoutMs = -1) = 0;
|
||||
|
||||
// currently non-functional
|
||||
virtual bool GetQueueRealtime(uint64_t *sec, uint32_t *nsec) = 0;
|
||||
|
||||
virtual void RemoveAllConnections() = 0;
|
||||
};
|
||||
|
||||
|
||||
class AlsaSequencerDeviceMonitor {
|
||||
protected:
|
||||
AlsaSequencerDeviceMonitor() {}
|
||||
public:
|
||||
virtual ~AlsaSequencerDeviceMonitor() {}
|
||||
|
||||
using self = AlsaSequencerDeviceMonitor;
|
||||
using ptr = std::shared_ptr<self>;
|
||||
static ptr Create();
|
||||
enum class MonitorAction {
|
||||
DeviceAdded,
|
||||
DeviceRemoved
|
||||
};
|
||||
using Callback = std::function<void(MonitorAction action, int client, const std::string& clientName)>;
|
||||
virtual void StartMonitoring(Callback &&onChangeCallback) = 0;
|
||||
virtual void StopMonitoring() = 0;
|
||||
|
||||
};
|
||||
|
||||
std::string RawMidiIdToSequencerId(const std::vector<AlsaSequencerPort> &seqDevices, const std::string &rawMidiId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2024 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 <functional>
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
class Finally {
|
||||
public:
|
||||
Finally(std::function<void(void)> &&fn)
|
||||
:fn (std::move(fn))
|
||||
{
|
||||
|
||||
}
|
||||
~Finally() {
|
||||
fn();
|
||||
}
|
||||
private:
|
||||
std::function<void(void)> fn;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
@@ -29,6 +31,7 @@ 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);
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace pipedal
|
||||
std::string output;
|
||||
};
|
||||
|
||||
SysExecOutput sysExecForOutput(const std::string& command, const std::string&args);
|
||||
SysExecOutput sysExecForOutput(const std::string& command, const std::string&args, bool discardStderr = false);
|
||||
|
||||
using ProcessId = int64_t; // platform-agnostic wrapper for pid_t;
|
||||
// Returns a pid or -1 on errror.
|
||||
|
||||
@@ -44,6 +44,8 @@ namespace pipedal {
|
||||
|
||||
std::u16string Utf8ToUtf16(const std::string_view&s);
|
||||
std::string Utf16ToUtf8(const std::u16string_view&s);
|
||||
|
||||
bool IsValidUtf8(const std::string &text);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -95,6 +95,10 @@ namespace pipedal {
|
||||
|
||||
bool operator==(const WifiConfigSettings&other) const;
|
||||
bool ConfigurationChanged(const WifiConfigSettings&other) const;
|
||||
bool WantsHotspot(bool ethernetConnected)
|
||||
{
|
||||
return WantsHotspot(ethernetConnected, std::vector<std::string>{}, std::vector<std::string>{});
|
||||
}
|
||||
bool WantsHotspot(
|
||||
bool ethernetConnected,
|
||||
const std::vector<std::string> &availableRememberedNetworks, // remembered networks that are currently visible
|
||||
@@ -105,7 +109,8 @@ namespace pipedal {
|
||||
const std::vector<std::vector<uint8_t>> &availableRememberedNetworks, // remembered networks that are currently visible
|
||||
const std::vector<std::vector<uint8_t>> &availableNetworks // all visible networks.
|
||||
);
|
||||
|
||||
bool NeedsScan() const;
|
||||
bool NeedsWifi() const;
|
||||
public:
|
||||
DECLARE_JSON_MAP(WifiConfigSettings);
|
||||
};
|
||||
|
||||
@@ -20,12 +20,9 @@
|
||||
#pragma once
|
||||
#include <iostream>
|
||||
#include <cstdint>
|
||||
#include <boost/utility/string_view.hpp>
|
||||
#include <boost/format.hpp>
|
||||
#include <iomanip>
|
||||
#include <type_traits>
|
||||
#include <sstream>
|
||||
#include "HtmlHelper.hpp"
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
@@ -35,6 +32,11 @@
|
||||
#include <stdexcept>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <string.h>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
#define DECLARE_JSON_MAP(CLASSNAME) \
|
||||
static pipedal::json_map::storage_type<CLASSNAME> jmap
|
||||
@@ -228,7 +230,7 @@ namespace pipedal
|
||||
reference(const char *name, MEMBER_TYPE CLASS::*member_pointer)
|
||||
{
|
||||
return new json_member_reference<CLASS, MEMBER_TYPE>(name, member_pointer);
|
||||
};
|
||||
}
|
||||
template <typename CLASS, typename MEMBER_TYPE, typename ENUM_CONVERTER>
|
||||
static json_enum_member_reference<CLASS, MEMBER_TYPE> *
|
||||
enum_reference(
|
||||
@@ -237,14 +239,20 @@ namespace pipedal
|
||||
const ENUM_CONVERTER *converter)
|
||||
{
|
||||
return new json_enum_member_reference<CLASS, MEMBER_TYPE>(name, member_pointer, converter);
|
||||
};
|
||||
}
|
||||
|
||||
template <typename CLASS, typename MEMBER_TYPE>
|
||||
static json_conditional_member_reference<CLASS, MEMBER_TYPE> *
|
||||
conditional_reference(const char *name, MEMBER_TYPE CLASS::*member_pointer, typename JsonConditionFunction<CLASS, MEMBER_TYPE>::Pointer condition)
|
||||
{
|
||||
return new json_conditional_member_reference<CLASS, MEMBER_TYPE>(name, member_pointer, condition);
|
||||
};
|
||||
}
|
||||
|
||||
// for cases where the name is not a valid C++ identifier.
|
||||
#define JSON_MAP_DICTIONARY_REFERENCE(class,name, variable) \
|
||||
json_map::reference(name,&class::variable##_),
|
||||
|
||||
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------------------
|
||||
@@ -254,7 +262,7 @@ namespace pipedal
|
||||
{
|
||||
|
||||
template <class TYPE>
|
||||
static std::true_type test(decltype(TYPE::jmap) *) { return std::true_type(); };
|
||||
static std::true_type test(decltype(TYPE::jmap) *) { return std::true_type(); }
|
||||
|
||||
template <class TYPE>
|
||||
static std::false_type test(...);
|
||||
@@ -268,7 +276,7 @@ namespace pipedal
|
||||
{
|
||||
|
||||
template <class TYPE>
|
||||
static std::true_type test() { return std::true_type(); };
|
||||
static std::true_type test() { return std::true_type(); }
|
||||
|
||||
template <class TYPE>
|
||||
static std::false_type test(...);
|
||||
@@ -320,7 +328,7 @@ namespace pipedal
|
||||
{
|
||||
os << text;
|
||||
}
|
||||
using string_view = boost::string_view;
|
||||
using string_view = std::string_view;
|
||||
json_writer(std::ostream &os, bool compressed = true, bool allowNaN = false)
|
||||
: os(os), compressed(compressed), allowNaN_(allowNaN), indent_level(0)
|
||||
{
|
||||
@@ -1049,6 +1057,7 @@ namespace pipedal
|
||||
if (peek() == ']')
|
||||
{
|
||||
c = get();
|
||||
(void)c;
|
||||
break;
|
||||
}
|
||||
T item;
|
||||
@@ -1057,6 +1066,7 @@ namespace pipedal
|
||||
if (peek() == ',')
|
||||
{
|
||||
c = get();
|
||||
(void)c;
|
||||
}
|
||||
}
|
||||
*value = std::move(result);
|
||||
@@ -1068,7 +1078,7 @@ namespace pipedal
|
||||
{
|
||||
|
||||
template <typename TYPE, typename ARG>
|
||||
static std::true_type test(decltype(json_reader(std::cin).read((ARG *)nullptr)) *v) { return std::true_type(); };
|
||||
static std::true_type test(decltype(json_reader(std::cin).read((ARG *)nullptr)) *v) { return std::true_type(); }
|
||||
|
||||
template <typename TYPE, typename ARG>
|
||||
static std::false_type test(...);
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <memory>
|
||||
#include "json.hpp"
|
||||
|
||||
namespace pipedal
|
||||
@@ -308,7 +309,11 @@ namespace pipedal
|
||||
iterator end() { return values.end(); }
|
||||
const_iterator begin() const { return values.begin(); }
|
||||
const_iterator end() const { return values.end(); }
|
||||
|
||||
|
||||
iterator erase(iterator it)
|
||||
{
|
||||
return values.erase(it);
|
||||
}
|
||||
iterator find(const std::string &key);
|
||||
const_iterator find(const std::string &key) const;
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ namespace pipedal
|
||||
return contains(vector, std::string(value));
|
||||
}
|
||||
|
||||
bool HasWritePermissions(const std::filesystem::path &path);
|
||||
class NoCopy
|
||||
{
|
||||
public:
|
||||
@@ -114,11 +115,14 @@ namespace pipedal
|
||||
return true;
|
||||
}
|
||||
|
||||
// C locale to lower. Only does 'A'-'Z'.
|
||||
std::string ToLower(const std::string&value);
|
||||
|
||||
std::filesystem::path MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path&parentPath);
|
||||
|
||||
bool IsSubdirectory(const std::filesystem::path &path, const std::filesystem::path &basePath);
|
||||
bool HasDotDot(const std::filesystem::path &path);
|
||||
|
||||
|
||||
std::string ToLower(const std::string&value);
|
||||
|
||||
|
||||
}
|
||||
|
||||
+67
-36
@@ -22,6 +22,8 @@
|
||||
#include <cctype>
|
||||
#include "json_variant.hpp"
|
||||
#include "util.hpp"
|
||||
#include <string_view>
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
|
||||
@@ -68,63 +70,69 @@ void json_writer::write(string_view v,bool enforceValidUtf8Encoding)
|
||||
{
|
||||
uint32_t uc;
|
||||
uint8_t c = (uint8_t)*p++;
|
||||
if ((c & UTF8_ONE_BYTE_MASK) == UTF8_ONE_BYTE_BITS)
|
||||
{
|
||||
uc = c;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t c2 = continuation_byte(p, v.end());
|
||||
|
||||
if ((c & UTF8_TWO_BYTES_MASK) == UTF8_TWO_BYTES_BITS)
|
||||
try {
|
||||
if ((c & UTF8_ONE_BYTE_MASK) == UTF8_ONE_BYTE_BITS)
|
||||
{
|
||||
uint32_t c1 = c & (uint32_t)(~UTF8_TWO_BYTES_MASK);
|
||||
if (c1 <= 1 && enforceValidUtf8Encoding)
|
||||
{
|
||||
// overlong encoding.
|
||||
throw_encoding_error();
|
||||
}
|
||||
uc = (c1 << 6) | c2;
|
||||
uc = c;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t c3 = continuation_byte(p, v.end());
|
||||
uint32_t c2 = continuation_byte(p, v.end());
|
||||
|
||||
if ((c & UTF8_THREE_BYTES_MASK) == UTF8_THREE_BYTES_BITS)
|
||||
if ((c & UTF8_TWO_BYTES_MASK) == UTF8_TWO_BYTES_BITS)
|
||||
{
|
||||
uint32_t c1 = c & (uint32_t)~UTF8_THREE_BYTES_MASK;
|
||||
if (c1 == 0 && c2 < 0x20 && enforceValidUtf8Encoding)
|
||||
uint32_t c1 = c & (uint32_t)(~UTF8_TWO_BYTES_MASK);
|
||||
if (c1 <= 1 && enforceValidUtf8Encoding)
|
||||
{
|
||||
// overlong encoding.
|
||||
throw_encoding_error();
|
||||
}
|
||||
|
||||
uc = (c1) << 12 | (c2 << 6) | c3;
|
||||
uc = (c1 << 6) | c2;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t c4 = continuation_byte(p, v.end());
|
||||
if ((c & UTF8_FOUR_BYTES_MASK) == UTF8_FOUR_BYTES_BITS)
|
||||
uint32_t c3 = continuation_byte(p, v.end());
|
||||
|
||||
if ((c & UTF8_THREE_BYTES_MASK) == UTF8_THREE_BYTES_BITS)
|
||||
{
|
||||
uint32_t c1 = c & (uint32_t)~UTF8_FOUR_BYTES_MASK;
|
||||
if (c1 == 0 && c2 < 0x10 && enforceValidUtf8Encoding)
|
||||
uint32_t c1 = c & (uint32_t)~UTF8_THREE_BYTES_MASK;
|
||||
if (c1 == 0 && c2 < 0x20 && enforceValidUtf8Encoding)
|
||||
{
|
||||
// overlong encoding.
|
||||
throw_encoding_error();
|
||||
}
|
||||
uc = (c1 << 18) | (c2 << 12) | (c3 << 6) | c4;
|
||||
} else {
|
||||
// outside legal UCS range.
|
||||
throw_encoding_error();
|
||||
|
||||
uc = (c1) << 12 | (c2 << 6) | c3;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t c4 = continuation_byte(p, v.end());
|
||||
if ((c & UTF8_FOUR_BYTES_MASK) == UTF8_FOUR_BYTES_BITS)
|
||||
{
|
||||
uint32_t c1 = c & (uint32_t)~UTF8_FOUR_BYTES_MASK;
|
||||
if (c1 == 0 && c2 < 0x10 && enforceValidUtf8Encoding)
|
||||
{
|
||||
// overlong encoding.
|
||||
throw_encoding_error();
|
||||
}
|
||||
uc = (c1 << 18) | (c2 << 12) | (c3 << 6) | c4;
|
||||
} else {
|
||||
// outside legal UCS range.
|
||||
throw_encoding_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
// invalid UTF-8 sequence.
|
||||
os << "\\uFFFD"; // replacement character for invalid sequences.
|
||||
continue;
|
||||
}
|
||||
if ((uc >= UTF16_SURROGATE_1_BASE && uc <= UTF16_SURROGATE_1_BASE + UTF16_SURROGATE_MASK) || (uc >= UTF16_SURROGATE_2_BASE && uc <= UTF16_SURROGATE_2_BASE + UTF16_SURROGATE_MASK))
|
||||
{
|
||||
// MUST not encode UTF16 surrogates in UTF8.
|
||||
throw_encoding_error();
|
||||
}
|
||||
// if ((uc >= UTF16_SURROGATE_1_BASE && uc <= UTF16_SURROGATE_1_BASE + UTF16_SURROGATE_MASK) || (uc >= UTF16_SURROGATE_2_BASE && uc <= UTF16_SURROGATE_2_BASE + UTF16_SURROGATE_MASK))
|
||||
// {
|
||||
// // MUST not encode UTF16 surrogates in UTF8.
|
||||
// throw_encoding_error();
|
||||
// }
|
||||
|
||||
if (uc == '"' || uc == '\\')
|
||||
{
|
||||
@@ -268,6 +276,29 @@ void json_reader::skip_whitespace()
|
||||
}
|
||||
}
|
||||
|
||||
static void utf32_to_utf8_stream(std::ostream &s, uint32_t uc)
|
||||
{
|
||||
if (uc < 0x80u)
|
||||
{
|
||||
s << (char)uc;
|
||||
} else if (uc < 0x800u) {
|
||||
s << (char)(0xC0 + (uc >> 6));
|
||||
s << (char)(0x80 + (uc & 0x3F));
|
||||
|
||||
} else if (uc < 0x10000u) {
|
||||
s << (char)(0xE0 + (uc >> 12));
|
||||
s << (char)(0x80 + ((uc >> 6) & 0x3F));
|
||||
s << (char)(0x80 + (uc & 0x3F));
|
||||
} else if (uc < 0x0110000) {
|
||||
s << (char)(0xF0 + (uc >> 18));
|
||||
s << (char)(0x80 + ((uc >> 12) & 0x3F));
|
||||
s << (char)(0x80 + ((uc >> 6) & 0x3F));
|
||||
s << (char)(0x80 + (uc & 0x3F));
|
||||
} else {
|
||||
throw std::range_error("Illegal UTF-32 character.");
|
||||
}
|
||||
}
|
||||
|
||||
std::string json_reader::read_string()
|
||||
{
|
||||
// To completely normalize UTF-32 values we must covert to UTF-16, resolve surrogate pairs, and then convert UTF-32 to UTF-8.
|
||||
@@ -344,7 +375,7 @@ std::string json_reader::read_string()
|
||||
}
|
||||
uc = ((uc & UTF16_SURROGATE_MASK) << 10) + (uc2 & UTF16_SURROGATE_MASK) + 0x10000U;
|
||||
}
|
||||
HtmlHelper::utf32_to_utf8_stream(s, uc);
|
||||
utf32_to_utf8_stream(s, uc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ void json_variant::free()
|
||||
case ContentType::Array:
|
||||
memArray().std::shared_ptr<json_array>::~shared_ptr();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
content_type = ContentType::Null;
|
||||
}
|
||||
@@ -467,6 +470,15 @@ json_variant &json_variant::operator=(json_variant &&value)
|
||||
case ContentType::Object:
|
||||
std::swap(this->memObject(), value.memObject());
|
||||
return *this;
|
||||
case ContentType::Null:
|
||||
return *this; // nothing to do.
|
||||
case ContentType::Bool:
|
||||
std::swap(this->content.bool_value, value.content.bool_value);
|
||||
return *this;
|
||||
case ContentType::Number:
|
||||
std::swap(this->content.double_value, value.content.double_value);
|
||||
return *this;
|
||||
|
||||
}
|
||||
}
|
||||
free();
|
||||
|
||||
@@ -190,6 +190,22 @@ std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &pat
|
||||
|
||||
}
|
||||
|
||||
bool pipedal::HasDotDot(const std::filesystem::path &path)
|
||||
{
|
||||
for (auto &part : path)
|
||||
{
|
||||
if (part == "..")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (part == ".")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::filesystem::path &basePath)
|
||||
{
|
||||
auto iPath = path.begin();
|
||||
@@ -216,3 +232,13 @@ 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.
|
||||
// allegedly windows provies an _access function, which is probably a superset of
|
||||
// access.
|
||||
return access(path.c_str(), W_OK) == 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
<img src="https://img.shields.io/github/downloads/rerdavies/pipedal/total?color=%23808080&link=https%3A%2F%2Frerdavies.github.io%2Fpipedal%2Fdownload.html"/>
|
||||
|
||||
|
||||
Download: <a href='https://rerdavies.github.io/pipedal/download.html'>v1.4.76</a>
|
||||
Download: <a href='https://rerdavies.github.io/pipedal/download.html'>v1.4.77</a>
|
||||
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.4.76 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. New Phaser and Graphic Equalizer plugins.
|
||||
#### NEW version 1.4.77 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. New Phaser and Graphic Equalizer plugins.
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,122 @@
|
||||
#.rst:
|
||||
# FindPipeWire
|
||||
# -------
|
||||
#
|
||||
# Try to find PipeWire on a Unix system.
|
||||
#
|
||||
# This will define the following variables:
|
||||
#
|
||||
# ``PipeWire_FOUND``
|
||||
# True if (the requested version of) PipeWire is available
|
||||
# ``PipeWire_VERSION``
|
||||
# The version of PipeWire
|
||||
# ``PipeWire_LIBRARIES``
|
||||
# This can be passed to target_link_libraries() instead of the ``PipeWire::PipeWire``
|
||||
# target
|
||||
# ``PipeWire_INCLUDE_DIRS``
|
||||
# This should be passed to target_include_directories() if the target is not
|
||||
# used for linking
|
||||
# ``PipeWire_DEFINITIONS``
|
||||
# This should be passed to target_compile_options() if the target is not
|
||||
# used for linking
|
||||
#
|
||||
# If ``PipeWire_FOUND`` is TRUE, it will also define the following imported target:
|
||||
#
|
||||
# ``PipeWire::PipeWire``
|
||||
# The PipeWire library
|
||||
#
|
||||
# In general we recommend using the imported target, as it is easier to use.
|
||||
# Bear in mind, however, that if the target is in the link interface of an
|
||||
# exported library, it must be made available by the package config file.
|
||||
|
||||
#=============================================================================
|
||||
# Copyright 2014 Alex Merry <alex.merry@kde.org>
|
||||
# Copyright 2014 Martin Gräßlin <mgraesslin@kde.org>
|
||||
# Copyright 2018-2020 Jan Grulich <jgrulich@redhat.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. The name of the author may not be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#=============================================================================
|
||||
|
||||
# Use pkg-config to get the directories and then use these values
|
||||
# in the FIND_PATH() and FIND_LIBRARY() calls
|
||||
find_package(PkgConfig QUIET)
|
||||
|
||||
pkg_search_module(PKG_PipeWire QUIET libpipewire-0.3 libpipewire-0.2)
|
||||
pkg_search_module(PKG_Spa QUIET libspa-0.2 libspa-0.1)
|
||||
|
||||
set(PipeWire_DEFINITIONS "${PKG_PipeWire_CFLAGS}" "${PKG_Spa_CFLAGS}")
|
||||
set(PipeWire_VERSION "${PKG_PipeWire_VERSION}")
|
||||
|
||||
find_path(PipeWire_INCLUDE_DIRS
|
||||
NAMES
|
||||
pipewire/pipewire.h
|
||||
HINTS
|
||||
${PKG_PipeWire_INCLUDE_DIRS}
|
||||
${PKG_PipeWire_INCLUDE_DIRS}/pipewire-0.3
|
||||
)
|
||||
|
||||
find_path(Spa_INCLUDE_DIRS
|
||||
NAMES
|
||||
spa/param/props.h
|
||||
HINTS
|
||||
${PKG_Spa_INCLUDE_DIRS}
|
||||
${PKG_Spa_INCLUDE_DIRS}/spa-0.2
|
||||
)
|
||||
|
||||
find_library(PipeWire_LIBRARIES
|
||||
NAMES
|
||||
pipewire-0.3
|
||||
pipewire-0.2
|
||||
HINTS
|
||||
${PKG_PipeWire_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(PipeWire
|
||||
FOUND_VAR
|
||||
PipeWire_FOUND
|
||||
REQUIRED_VARS
|
||||
PipeWire_LIBRARIES
|
||||
PipeWire_INCLUDE_DIRS
|
||||
Spa_INCLUDE_DIRS
|
||||
VERSION_VAR
|
||||
PipeWire_VERSION
|
||||
)
|
||||
|
||||
if(PipeWire_FOUND AND NOT TARGET PipeWire::PipeWire)
|
||||
add_library(PipeWire::PipeWire UNKNOWN IMPORTED)
|
||||
set_target_properties(PipeWire::PipeWire PROPERTIES
|
||||
IMPORTED_LOCATION "${PipeWire_LIBRARIES}"
|
||||
INTERFACE_COMPILE_OPTIONS "${PipeWire_DEFINITIONS}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${PipeWire_INCLUDE_DIRS};${Spa_INCLUDE_DIRS}"
|
||||
)
|
||||
endif()
|
||||
|
||||
mark_as_advanced(PipeWire_LIBRARIES PipeWire_INCLUDE_DIRS)
|
||||
|
||||
include(FeatureSummary)
|
||||
set_package_properties(PipeWire PROPERTIES
|
||||
URL "https://www.pipewire.org"
|
||||
DESCRIPTION "PipeWire - multimedia processing"
|
||||
)
|
||||
@@ -26,7 +26,8 @@ Run the following commands to install dependent libraries required by the PiPeda
|
||||
libsystemd-dev catch libasound2-dev uuid-dev \
|
||||
authbind libavahi-client-dev libnm-dev libicu-dev \
|
||||
libsdbus-c++-dev libzip-dev google-perftools \
|
||||
libgoogle-perftools-dev
|
||||
libgoogle-perftools-dev \
|
||||
libpipewire-0.3-dev
|
||||
|
||||
|
||||
### Installing Sources
|
||||
|
||||
+5
-5
@@ -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.4.76](https://github.com/rerdavies/pipedal/releases/download/v1.4.76/pipedal_1.4.76_arm64.deb)
|
||||
- [Ubuntu 24.x (aarch64) v1.4.76](https://github.com/rerdavies/pipedal/releases/download/v1.4.76/pipedal_1.4.76_arm64.deb)
|
||||
- [Ubuntu 24.x (amd64) v1.4.76](https://github.com/rerdavies/pipedal/releases/download/v1.4.76/pipedal_1.4.76_amd64.deb)
|
||||
- [Raspberry Pi OS bookworm (aarch64) v1.4.77](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_arm64.deb)
|
||||
- [Ubuntu 24.x (aarch64) v1.4.77](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_arm64.deb)
|
||||
- [Ubuntu 24.x (amd64) v1.4.77](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_amd64.deb)
|
||||
|
||||
|
||||
Version 1.4.76 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), and Ubuntu 24.10 (aarch64). Download the appropriate package for your platform, and install using the following procedure:
|
||||
Version 1.4.77 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), and Ubuntu 24.10 (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.4.76_arm64.deb
|
||||
sudo apt-get install ./pipedal_1.4.77_arm64.deb
|
||||
```
|
||||
You MUST use `apt-get`. `apt` will not install downloaded packages; and `dpkg -i` will not install dependencies.
|
||||
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
# Release Notes
|
||||
## PiPedal 1.4.77 Beta
|
||||
- **TooB Player**. Play audio files.
|
||||
|
||||
New TooB Player plugin; improvements to MIDI input handling; and a slew of fit-and finish
|
||||
improvements to controls in the Pipedal user interface. All in all, a surprisingly large
|
||||
release.
|
||||
|
||||
The big features for this release
|
||||
|
||||
- a new TooB Player plugin that allows you to play (and loop) audio files.
|
||||
|
||||
- Switch from ALSA rawmidi devices to ALSA sequencer devices for MIDI input. This provides
|
||||
a number of useful improvements. PiPedal now supports Bluetooth MIDI controllers.
|
||||
PiPedal provides better support for subdevices (multiple MIDI connections on the same device).
|
||||
Connections to the PiPedal MIDI input port can now be made at runtime by other applications, or
|
||||
by using`aconnect`. Changing MIDI devices does not require a restart of the audio stream. Disconnected
|
||||
MIDI devices that have been selected will now be automatically reconnected when they are
|
||||
plugged in again.
|
||||
|
||||
- MIDI bindings allow users to specify minimum and maximum control values, in order to better support expression pedals.
|
||||
|
||||
- long-press and hover Tooltips are now displayed for all controls in the PiPedal UI.
|
||||
|
||||
- Editable plugin labels. You can now edit the labels of plugins in the PiPedal UI. Tap the plugin label to override
|
||||
the default label with a custom label. Useful when you have multiple instances of the same plugin in a preset.
|
||||
|
||||
- Multi-select support in file browser dialogs. Allows you to easily delete, copy or move multiple files at once.
|
||||
|
||||
- Control values are displayed in a fly-out when changing contrl values, which makes it easier to determine the
|
||||
current value of a control when using touch user interfaces.
|
||||
|
||||
- Direct link to TONE3000 website from the TooB Neural Amp Modeler file browser.
|
||||
|
||||
- Double-tap guestures now supported in touch user interfaces. Double-tap a dial to reset it to its default value. Double-tap
|
||||
a file in the File Browser to select it. Double-tap a plugin label to edit it.
|
||||
|
||||
- Improvements to long-press handling in touch and mouse user interfaces. Long-press a control to display
|
||||
a tooltip for the control, and triggers multi-select in the file browser dialog.
|
||||
|
||||
- Audio files in the Tracks directory (only) are displayed using audio-file metadata and artwork if available.
|
||||
|
||||
Minor bug fixes:
|
||||
|
||||
- WiFi scanning is only performed when required by the current Auto-Hotspot
|
||||
configuration. WiFi is not turned on when PiPedal starts, unless the Auto-Hotspot configuration requires it.
|
||||
|
||||
- MIDI controls continue to operate if the current audio stream has stopped.
|
||||
|
||||
- Audio files with invalid UTF-8 sequences in their names are ignored (instead of causing crashes).
|
||||
|
||||
- Media files that are referenced in snapshots are now included in preset and bank file bundles.
|
||||
|
||||
- Changed all references to tonehunt.org to tone3000.com (tonehunt's new name).
|
||||
|
||||
## PiPedal 1.4.76 Release
|
||||
- **TooB Phaser**. A loose emulation of the MXR Phase 90 phaser.
|
||||
|
||||
+4
-4
@@ -4,9 +4,9 @@
|
||||
|
||||
Download the most recent Debian (.deb) package for your platform:
|
||||
|
||||
- [Raspberry Pi OS bookworm (aarch64) v1.4.76 Beta](https://github.com/rerdavies/pipedal/releases/download/v1.4.76/pipedal_1.4.76_arm64.deb)
|
||||
- [Ubuntu 24.x (aarch64) v1.4.76 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.4.76/pipedal_1.4.76_arm64.deb)
|
||||
- [Ubuntu 24.x (amd64) v1.4.76 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.4.76/pipedal_1.4.76_amd64.deb)
|
||||
- [Raspberry Pi OS bookworm (aarch64) v1.4.77 Beta](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_arm64.deb)
|
||||
- [Ubuntu 24.x (aarch64) v1.4.77 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_arm64.deb)
|
||||
- [Ubuntu 24.x (amd64) v1.4.77 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_amd64.deb)
|
||||
|
||||
|
||||
Install the package by running
|
||||
@@ -14,7 +14,7 @@ Install the package by running
|
||||
```
|
||||
sudo apt update
|
||||
cd ~/Downloads
|
||||
sudo apt-get install ./pipedal_1.4.76_arm64.deb
|
||||
sudo apt-get install ./pipedal_1.4.77_arm64.deb
|
||||
```
|
||||
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.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 47 KiB |
@@ -93,7 +93,7 @@ cabir:impulseFile3
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
|
||||
TooB Cab IR is a convolution-based guitar cabinet impulse response simulator.
|
||||
|
||||
@@ -50,7 +50,7 @@ toob:frequencyResponseVector
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
|
||||
mod:brand "TooB";
|
||||
mod:label "TooB CabSim";
|
||||
|
||||
@@ -53,7 +53,7 @@ toobimpulse:impulseFile
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
|
||||
Convolution reverb is a notoriously compute-intensive effect. If you are having performance issues, use the Max T control to constrain the length of the impulse file to
|
||||
|
||||
@@ -51,7 +51,7 @@ toobimpulse:impulseFile
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ inputStage:filterGroup
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
|
||||
mod:brand "TooB";
|
||||
mod:label "TooB Input";
|
||||
|
||||
@@ -68,7 +68,7 @@ pstage:stage3
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
|
||||
mod:brand "TooB";
|
||||
mod:label "Power Stage";
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment "TooB spectrum analyzer" ;
|
||||
|
||||
mod:brand "TooB";
|
||||
|
||||
@@ -57,7 +57,7 @@ tonestack:eqGroup
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
|
||||
uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>;
|
||||
|
||||
@@ -131,17 +131,28 @@ tone stack used in Polytone and HiWatt amps.
|
||||
rdfs:label "Baxandall" ;
|
||||
rdf:value 2.0
|
||||
];
|
||||
],[
|
||||
a lv2:InputPort ,
|
||||
lv2:ControlPort ;
|
||||
|
||||
lv2:index 4 ;
|
||||
lv2:symbol "gain" ;
|
||||
lv2:name "Gain";
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum -40.0 ;
|
||||
lv2:maximum 30.0;
|
||||
units:unit units:db;
|
||||
],
|
||||
[
|
||||
a lv2:AudioPort ,
|
||||
lv2:InputPort ;
|
||||
lv2:index 4 ;
|
||||
lv2:index 5 ;
|
||||
lv2:symbol "in" ;
|
||||
lv2:name "In"
|
||||
], [
|
||||
a lv2:AudioPort ,
|
||||
lv2:OutputPort ;
|
||||
lv2:index 5 ;
|
||||
lv2:index 6 ;
|
||||
lv2:symbol "out" ;
|
||||
lv2:name "Out"
|
||||
],[
|
||||
@@ -150,7 +161,7 @@ tone stack used in Polytone and HiWatt amps.
|
||||
atom:bufferType atom:Sequence ;
|
||||
# atom:supports patch:Message;
|
||||
lv2:designation lv2:control ;
|
||||
lv2:index 6 ;
|
||||
lv2:index 7 ;
|
||||
lv2:symbol "control" ;
|
||||
lv2:name "Control" ;
|
||||
rdfs:comment "Plugin to GUI communication" ;
|
||||
@@ -160,7 +171,7 @@ tone stack used in Polytone and HiWatt amps.
|
||||
atom:bufferType atom:Sequence ;
|
||||
# atom:supports patch:Message;
|
||||
lv2:designation lv2:control ;
|
||||
lv2:index 7 ;
|
||||
lv2:index 8 ;
|
||||
lv2:symbol "notify" ;
|
||||
lv2:name "Notify" ;
|
||||
rdfs:comment "Plugin to GUI communication" ;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -40,7 +40,7 @@
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
Emulation of a Boss CE-2 Chorus.
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
A straightforward no-frills digital delay.
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original.
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
Digital emulation of a Boss BF-2 Flanger.
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
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.
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
A 7 octave graphic equalizer, with frequencies chosen to be useful for EQ-ing guitar signals.
|
||||
""" ;
|
||||
|
||||
@@ -92,7 +92,7 @@ myprefix:output_group
|
||||
doap:license <https://opensource.org/license/mit/> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 1 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
|
||||
ui:ui <http://two-play.com/plugins/toob-looper-four-ui>;
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ myprefix:output_group
|
||||
doap:license <https://opensource.org/license/mit/> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 1 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
|
||||
ui:ui <http://two-play.com/plugins/toob-looper-one-ui>;
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ toobml:sagGroup
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
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.
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
Remix a stereo input signal.
|
||||
|
||||
@@ -57,7 +57,7 @@ Remix a stereo input signal.
|
||||
|
||||
lv2:index 0 ;
|
||||
lv2:symbol "trimL" ;
|
||||
lv2:name "Trim L";
|
||||
lv2:name "Vol L";
|
||||
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum -60.0 ;
|
||||
@@ -89,7 +89,7 @@ Remix a stereo input signal.
|
||||
|
||||
lv2:index 2 ;
|
||||
lv2:symbol "trimR" ;
|
||||
lv2:name "Trim R";
|
||||
lv2:name "Vol R";
|
||||
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum -60.0 ;
|
||||
|
||||
@@ -61,33 +61,43 @@ toobNam:eqGroup
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
A port of Steven Atkinson's Neural Amp Modeler to LV2.
|
||||
|
||||
TooB Neural Amp Modeler uses uploadable .nam model files. Download .nam files from http://tonehunt.org, and then load them into TooB Neural Amp Modeler.
|
||||
TooB Neural Amp Modeler uses uploadable .nam model files. Download .nam model files from http://tone3000.com, and then load them into TooB Neural Amp Modeler.
|
||||
|
||||
If you are using TooB Neural Amp Modeler from PiPedal, download the model files to the system on which you are running the client. You must then
|
||||
upload the file to the PiPedal server. Click on the "Model" control and then search for the Upload button in the file browser. Once uploaded to the server,
|
||||
you can then select the uploaded file in the browser directly.
|
||||
The TONE3000 website contains a huge collection of community-developed amp models that can be used with TooB Neural Amp Modeler. You can also use .nam models from
|
||||
other sources. Using TONE3000 models with TooB Neural Amp Modeler is a two-step process. First, download model files to your local system 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 PiPedal web interface.
|
||||
|
||||
If you are not using PiPedal, just click on the Model control, and use the file browser to select the .nam file on your local system.
|
||||
|
||||
TooB Neural Amp Modeler supports a much wider range of amp models than ToobML, but usually uses much more CPU.
|
||||
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.
|
||||
|
||||
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.
|
||||
If you are having trouble with CPU usage, tonehunt.org does contain some smaller amp models. Search for the "feather" tag to find
|
||||
models that use less CPU.
|
||||
If you are having trouble with CPU usage, tone3000.com does contain some smaller amp models. Search for the "feather" tag to find
|
||||
models that use less CPU. Generally, you can use up to three NAM models at once in a given preset on a Pi 4, and up to 5 MAM models
|
||||
simultaneously on a Pi 5. N100 micro PCs could presumably do even better. However, exact CPU use varies depending on the complexity of the amp models you are using.
|
||||
Users have reported success using NAM on Raspberry Pi 3 devices, but this is not recommended (or really supported), as CPU usage is very high on a Raspberry Pi 3.
|
||||
If you are interested in profiling your own amps and effect pedals, please visit https://www.tone3000.com/capture
|
||||
|
||||
If you are interested in building your own amp models, please visit https://www.neuralampmodeler.com/
|
||||
TooB Neural Amp Modeler contains code optimizations that allow use of a third model on Raspberry PI 4 devices. These optimizations
|
||||
were offered to the upstream NAM project, but we recommended that they not be merged into upstream sources, because they
|
||||
significantly affect maintainability of the codebase. and the performance increase they provide (about 30%) is really only
|
||||
relevant on Raspberry PI 4-class devices. If you are interested in these optimizations, they have been published in ToobAmp
|
||||
project sources on GitHub.
|
||||
|
||||
TooB Neural Amp Modeler uses code from the NeuralAmp Modeler Core project. The TooB Team wishes to express gratitude to Steven Atkinson for
|
||||
making this extraordinary technology available as open-source code.
|
||||
TooB Neural Amp Modeler uses code from the NeuralAmp 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.
|
||||
|
||||
Code from the the NeuralAmpModelerCore project (https://github.com/sdatkinson/NeuralAmpModelerCore) is provided under the following license.
|
||||
Code from the the NeuralAmpModelerCore project (https://github.com/sdatkinson/NeuralAmpModelerCore) is provided
|
||||
under the following license.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Steven Atkinson
|
||||
Copyright (c) 2025 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
|
||||
|
||||
@@ -54,7 +54,7 @@ noisegate:envelope_group
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
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.
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
A loose emulation of an MXR® Phase 90 Phaser.
|
||||
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
@prefix doap: <http://usefulinc.com/ns/doap#> .
|
||||
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
|
||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
|
||||
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
|
||||
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
|
||||
@prefix midi: <http://lv2plug.in/ns/ext/midi#> .
|
||||
@prefix epp: <http://lv2plug.in/ns/ext/port-props#> .
|
||||
@prefix uiext: <http://lv2plug.in/ns/extensions/ui#> .
|
||||
@prefix idpy: <http://harrisonconsoles.com/lv2/inlinedisplay#> .
|
||||
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
||||
@prefix mod: <http://moddevices.com/ns/mod#> .
|
||||
@prefix param: <http://lv2plug.in/ns/ext/parameters#> .
|
||||
@prefix work: <http://lv2plug.in/ns/ext/worker#> .
|
||||
@prefix pg: <http://lv2plug.in/ns/ext/port-groups#> .
|
||||
|
||||
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
|
||||
@prefix patch: <http://lv2plug.in/ns/ext/patch#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix state: <http://lv2plug.in/ns/ext/state#> .
|
||||
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
@prefix toobPlayer: <http://two-play.com/plugins/toob-player#> .
|
||||
|
||||
toobPlayer:mixGroup
|
||||
a param:ControlGroup ,
|
||||
pg:InputGroup ;
|
||||
lv2:name "Mix" ;
|
||||
lv2:symbol "mixGroup" .
|
||||
|
||||
|
||||
<http://two-play.com/rerdavies#me>
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
|
||||
toobPlayer:audioFile
|
||||
a lv2:Parameter;
|
||||
rdfs:label "File";
|
||||
mod:fileTypes "audiotrack,wav,flac,mp3";
|
||||
rdfs:range atom:Path;
|
||||
lv2:index 4
|
||||
.
|
||||
toobPlayer:seek
|
||||
a lv2:Parameter;
|
||||
rdfs:label "Seek";
|
||||
rdfs:range atom:Float;
|
||||
.
|
||||
|
||||
|
||||
<http://two-play.com/plugins/toob-player>
|
||||
a lv2:Plugin ,
|
||||
lv2:GeneratorPlugin ;
|
||||
doap:name "TooB File Player" ,
|
||||
"TooB File Player"@en-gb-gb
|
||||
;
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
Play audio files. Audio files can optionally be loooped by pressing the "Set loop" button.
|
||||
|
||||
To skip the start of a file, press the "Set Loop" button, set the "Start" value in the seek dialog, and leave the "Loop Enabled"
|
||||
checkbox unchecked.
|
||||
|
||||
Note that loop information is stored as per-file metadata, and is not stored in the preset. If you need different loop settings for the
|
||||
same file, create a copy of the file. Files are copied using hard links, so the copy will not take up significant disk space.
|
||||
|
||||
|
||||
TooB Player will display audio metadata for a file (track number, title, album, artist) if available.
|
||||
If the file's metadata contains album art, that will be displayed as well. If not, the player will look for a folder artwork file in the same directory. The
|
||||
folder artwork file can be called any of "Folder.jpg", "Cover.jpg", "AlbumArt.jpg" or a number of other common names used by media players and taggers.
|
||||
You can upload album artwork files using the Upload button in the file browser. The Upload button will allow you to upload audio files, or
|
||||
folder artwork files that have a .jpg file extension.
|
||||
|
||||
|
||||
""" ;
|
||||
|
||||
mod:brand "TooB";
|
||||
mod:label "File Player";
|
||||
|
||||
# ONLY works in PiPedal
|
||||
lv2:requiredFeature "http://github.com/rerdavies/pipedal#host" ;
|
||||
|
||||
lv2:optionalFeature lv2:hardRTCapable,state::freePath, state:makePath, state:mapPath, work:schedule ;
|
||||
lv2:extensionData state:interface, work:interface;
|
||||
|
||||
patch:readable
|
||||
toobPlayer:audioFile,toobPlayer:seek;
|
||||
patch:writable
|
||||
toobPlayer:audioFile;
|
||||
|
||||
lv2:port
|
||||
[
|
||||
a lv2:AudioPort ,
|
||||
lv2:InputPort ;
|
||||
lv2:index 0 ;
|
||||
lv2:symbol "inl" ;
|
||||
lv2:name "In L"
|
||||
],
|
||||
[
|
||||
a lv2:AudioPort ,
|
||||
lv2:InputPort ;
|
||||
lv2:index 1 ;
|
||||
lv2:symbol "inr" ;
|
||||
lv2:name "In R"
|
||||
],
|
||||
[
|
||||
a lv2:AudioPort ,
|
||||
lv2:OutputPort ;
|
||||
lv2:index 2 ;
|
||||
lv2:symbol "outl" ;
|
||||
lv2:name "Out L"
|
||||
],
|
||||
[
|
||||
a lv2:AudioPort ,
|
||||
lv2:OutputPort ;
|
||||
lv2:index 3 ;
|
||||
lv2:symbol "outr" ;
|
||||
lv2:name "OutR"
|
||||
],
|
||||
######################
|
||||
[
|
||||
a atom:AtomPort ,
|
||||
lv2:InputPort ;
|
||||
lv2:index 4 ;
|
||||
atom:bufferType atom:Sequence ;
|
||||
atom:supports patch:Message;
|
||||
lv2:designation lv2:control ;
|
||||
lv2:symbol "controlIn" ;
|
||||
lv2:name "ControlIn"
|
||||
],
|
||||
[
|
||||
a atom:AtomPort ,
|
||||
lv2:OutputPort ;
|
||||
lv2:index 5 ;
|
||||
atom:bufferType atom:Sequence ;
|
||||
atom:supports patch:Message;
|
||||
lv2:designation lv2:control ;
|
||||
|
||||
lv2:symbol "controlOut" ;
|
||||
lv2:name "ControlOut"
|
||||
],
|
||||
|
||||
######################
|
||||
[
|
||||
a lv2:InputPort ,
|
||||
lv2:ControlPort ;
|
||||
|
||||
lv2:index 6;
|
||||
lv2:symbol "stop" ;
|
||||
lv2:name "Stop";
|
||||
rdf:comment "Stop playback.";
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum 0.0;
|
||||
lv2:maximum 1.0;
|
||||
lv2:portProperty lv2:toggled,epp:trigger;
|
||||
],
|
||||
[
|
||||
a lv2:InputPort ,
|
||||
lv2:ControlPort ;
|
||||
|
||||
lv2:index 7;
|
||||
lv2:symbol "pause" ;
|
||||
lv2:name "Pause";
|
||||
rdf:comment "Pause playback.";
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum 0.0;
|
||||
lv2:maximum 1.0;
|
||||
lv2:portProperty lv2:toggled,epp:trigger;
|
||||
],
|
||||
[
|
||||
a lv2:InputPort ,
|
||||
lv2:ControlPort ;
|
||||
|
||||
lv2:index 8;
|
||||
lv2:symbol "play" ;
|
||||
lv2:name "Play";
|
||||
rdf:comment "Play selected file.";
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum 0.0;
|
||||
lv2:maximum 1.0;
|
||||
lv2:portProperty lv2:toggled,epp:trigger;
|
||||
],
|
||||
|
||||
[
|
||||
a lv2:InputPort ,
|
||||
lv2:ControlPort ;
|
||||
|
||||
lv2:index 9 ;
|
||||
lv2:symbol "volIn" ;
|
||||
lv2:name "Input Vol";
|
||||
rdfs:comment "Volume audio passed through from the input. ";
|
||||
|
||||
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum -40.0 ;
|
||||
lv2:maximum 30.0 ;
|
||||
units:unit units:db ;
|
||||
lv2:scalePoint
|
||||
[
|
||||
rdfs:label "-INF" ;
|
||||
rdf:value -40.0
|
||||
];
|
||||
pg:group toobPlayer:mixGroup ;
|
||||
|
||||
],
|
||||
[
|
||||
a lv2:InputPort ,
|
||||
lv2:ControlPort ;
|
||||
|
||||
lv2:index 10;
|
||||
lv2:symbol "panIn" ;
|
||||
lv2:name "Input Pan";
|
||||
rdfs:comment "Pan for passed-through audio.";
|
||||
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum -1.0;
|
||||
lv2:maximum 1.0;
|
||||
pg:group toobPlayer:mixGroup ;
|
||||
rdfs:comment "Input Pan";
|
||||
],
|
||||
[
|
||||
a lv2:InputPort ,
|
||||
lv2:ControlPort ;
|
||||
|
||||
lv2:index 11 ;
|
||||
lv2:symbol "volFile" ;
|
||||
lv2:name "File Vol";
|
||||
rdfs:comment "Volume of the audio file being played.";
|
||||
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum -40.0 ;
|
||||
lv2:maximum 30.0 ;
|
||||
units:unit units:db ;
|
||||
lv2:scalePoint
|
||||
[
|
||||
rdfs:label "-INF" ;
|
||||
rdf:value -40.0
|
||||
];
|
||||
pg:group toobPlayer:mixGroup ;
|
||||
|
||||
],
|
||||
[
|
||||
a lv2:InputPort ,
|
||||
lv2:ControlPort ;
|
||||
|
||||
lv2:index 12;
|
||||
lv2:symbol "panFile" ;
|
||||
lv2:name "File Pan";
|
||||
rdfs:comment "Pan position of the file being played.";
|
||||
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum -1.0;
|
||||
lv2:maximum 1.0;
|
||||
pg:group toobPlayer:mixGroup ;
|
||||
],
|
||||
|
||||
|
||||
|
||||
###########################
|
||||
[
|
||||
a lv2:OutputPort ,
|
||||
lv2:ControlPort ;
|
||||
|
||||
lv2:index 13;
|
||||
lv2:symbol "state" ;
|
||||
lv2:name "Play State";
|
||||
lv2:portProperty epp:notOnGUI;
|
||||
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum 0.0;
|
||||
lv2:maximum 10.0;
|
||||
],
|
||||
[
|
||||
a lv2:OutputPort ,
|
||||
lv2:ControlPort ;
|
||||
|
||||
lv2:index 14;
|
||||
lv2:symbol "position" ;
|
||||
lv2:name "Play Position";
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum 0.0;
|
||||
lv2:maximum 1.0E+38;
|
||||
units:unit units:s ;
|
||||
],
|
||||
[
|
||||
a lv2:OutputPort ,
|
||||
lv2:ControlPort ;
|
||||
|
||||
lv2:index 15;
|
||||
lv2:symbol "duration" ;
|
||||
lv2:name "Duration";
|
||||
rdfs:comment "Length of the selected audio file.";
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum 0.0;
|
||||
lv2:maximum 1.0E+38;
|
||||
units:unit units:s ;
|
||||
]
|
||||
|
||||
.
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
@prefix ui: <http://lv2plug.in/ns/extensions/ui#> .
|
||||
@prefix pprop: <http://lv2plug.in/ns/ext/port-props#> .
|
||||
@prefix pipedal_ui: <http://github.com/rerdavies/pipedal/ui#> .
|
||||
|
||||
|
||||
@@ -52,7 +51,7 @@ recordPrefix:audioFile
|
||||
doap:license <https://opensource.org/license/mit/> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 1 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
|
||||
ui:ui <http://two-play.com/plugins/toob-record-mono-ui>;
|
||||
|
||||
@@ -61,7 +60,7 @@ recordPrefix:audioFile
|
||||
patch:writable
|
||||
recordPrefix:audioFile;
|
||||
|
||||
lv2:extensionData state:interface ;
|
||||
lv2:extensionData state:interface, work:interface ;
|
||||
|
||||
rdfs:comment """
|
||||
Record the plugin's audio input signal to a monophonic file. See also: TooB Record Input (stereo), which records a stereo file.
|
||||
@@ -83,7 +82,7 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum 0.0;
|
||||
lv2:maximum 1.0;
|
||||
lv2:portProperty lv2:toggled,pprop:trigger;
|
||||
lv2:portProperty lv2:toggled,epp:trigger;
|
||||
],
|
||||
[
|
||||
a lv2:InputPort ,
|
||||
@@ -96,7 +95,7 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum 0.0;
|
||||
lv2:maximum 1.0;
|
||||
lv2:portProperty lv2:toggled,pprop:trigger;
|
||||
lv2:portProperty lv2:toggled,epp:trigger;
|
||||
],
|
||||
[
|
||||
a lv2:OutputPort ,
|
||||
@@ -117,11 +116,11 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco
|
||||
lv2:index 3;
|
||||
lv2:symbol "play" ;
|
||||
lv2:name "⏵";
|
||||
rdfs:comment "Preview recorded file. Click again to stop.";
|
||||
rdfs:comment "Preview recorded file.";
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum 0.0;
|
||||
lv2:maximum 1.0;
|
||||
lv2:portProperty lv2:toggled,pprop:trigger;
|
||||
lv2:portProperty lv2:toggled,epp:trigger;
|
||||
],
|
||||
[
|
||||
a lv2:OutputPort ,
|
||||
@@ -186,7 +185,7 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco
|
||||
|
||||
lv2:index 7;
|
||||
lv2:symbol "level" ;
|
||||
lv2:name "Level";
|
||||
lv2:name "Rec Lvl";
|
||||
rdfs:comment "Input trim level for recording";
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum -60.0;
|
||||
|
||||
@@ -88,7 +88,7 @@ myprefix:loop3_group
|
||||
doap:license <https://opensource.org/license/mit/> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 1 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
|
||||
ui:ui <http://two-play.com/plugins/toob-record-stereo-ui>;
|
||||
|
||||
@@ -97,7 +97,7 @@ myprefix:loop3_group
|
||||
patch:writable
|
||||
recordPrefix:audioFile;
|
||||
|
||||
lv2:extensionData state:interface ;
|
||||
lv2:extensionData state:interface, work:interface ;
|
||||
|
||||
|
||||
rdfs:comment """
|
||||
@@ -154,7 +154,7 @@ Record the plugin's audio input signal to a stereophonic file. See also: TooB Re
|
||||
lv2:index 3;
|
||||
lv2:symbol "play" ;
|
||||
lv2:name "⏵";
|
||||
rdfs:comment "Preview the recorded file. Click again to stop.";
|
||||
rdfs:comment "Preview the recorded file.";
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum 0.0;
|
||||
lv2:maximum 1.0;
|
||||
@@ -223,8 +223,8 @@ Record the plugin's audio input signal to a stereophonic file. See also: TooB Re
|
||||
|
||||
lv2:index 7;
|
||||
lv2:symbol "level" ;
|
||||
lv2:name "Level";
|
||||
lv2:name "Input trim level for recording";
|
||||
lv2:name "Rec Lvl";
|
||||
rdfs:comment "Input trim level for recording";
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum -60.0;
|
||||
lv2:maximum 30.0;
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
TooB Tuner is a chromatic guitar tuner.
|
||||
""" ;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<http://two-play.com/plugins/toob-volume>
|
||||
a lv2:Plugin ,
|
||||
lv2:UtilityPlugin ;
|
||||
lv2:MixerPlugin ;
|
||||
doap:name "TooB Volume" ,
|
||||
"TooB Volume"@en-gb-gb
|
||||
;
|
||||
@@ -39,7 +39,7 @@
|
||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 62 ;
|
||||
lv2:microVersion 63 ;
|
||||
rdfs:comment """
|
||||
Volume control.
|
||||
|
||||
|
||||
@@ -97,6 +97,10 @@
|
||||
lv2:binary <ToobAmp.so> ;
|
||||
rdfs:seeAlso <ToobMix.ttl> .
|
||||
|
||||
<http://two-play.com/plugins/toob-player> a lv2:Plugin ;
|
||||
lv2:binary <ToobAmp.so> ;
|
||||
rdfs:seeAlso <ToobPlayer.ttl> .
|
||||
|
||||
<http://two-play.com/plugins/toob-graphiceq> a lv2:Plugin ;
|
||||
lv2:binary <ToobAmp.so> ;
|
||||
rdfs:seeAlso <ToobGraphicEq.ttl> .
|
||||
|
||||
Binary file not shown.
Submodule
+1
Submodule modules/SQLiteCpp added at 1df768817e
+1
-1
Submodule modules/websocketpp updated: 2466f9e5f1...508c9b5e40
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
TOOBAMP_DEB=$(ls ../ToobAmp/build/toobamp_*_amd64.deb | sort -V | tail -n1)
|
||||
echo Provisioning $TOOBAMP_DEB
|
||||
rm ./lv2/x86_64/*.deb
|
||||
cp $TOOBAMP_DEB ./lv2/x86_64/
|
||||
+48
-298
@@ -144,8 +144,6 @@ namespace pipedal
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct AudioFormat
|
||||
{
|
||||
char name[40];
|
||||
@@ -304,12 +302,9 @@ namespace pipedal
|
||||
AlsaDriverImpl(AudioDriverHost *driverHost)
|
||||
: driverHost(driverHost)
|
||||
{
|
||||
midiEventMemory.resize(MAX_MIDI_EVENT * MAX_MIDI_EVENT_SIZE);
|
||||
midiEventMemoryIndex = 0;
|
||||
midiEventMemory.resize(MIDI_MEMORY_BUFFER_SIZE);
|
||||
midiEvents.resize(MAX_MIDI_EVENT);
|
||||
for (size_t i = 0; i < midiEvents.size(); ++i)
|
||||
{
|
||||
midiEvents[i].buffer = midiEventMemory.data() + i * MAX_MIDI_EVENT_SIZE;
|
||||
}
|
||||
}
|
||||
virtual ~AlsaDriverImpl()
|
||||
{
|
||||
@@ -415,14 +410,7 @@ namespace pipedal
|
||||
snd_pcm_sw_params_free(playbackSwParams);
|
||||
playbackSwParams = nullptr;
|
||||
}
|
||||
for (auto &midiState : this->midiDevices)
|
||||
{
|
||||
if (midiState)
|
||||
{
|
||||
midiState->Close();
|
||||
}
|
||||
}
|
||||
midiDevices.resize(0);
|
||||
this->alsaSequencer = nullptr;
|
||||
}
|
||||
|
||||
std::string discover_alsa_using_apps()
|
||||
@@ -913,7 +901,7 @@ namespace pipedal
|
||||
|
||||
std::vector<float *> &buffers = this->playbackBuffers;
|
||||
int channels = this->playbackChannels;
|
||||
constexpr double scale = 0x00FFFFFF;
|
||||
constexpr double scale = 0x00FFFFFF;
|
||||
for (size_t frame = 0; frame < frames; ++frame)
|
||||
{
|
||||
for (int channel = 0; channel < channels; ++channel)
|
||||
@@ -1084,7 +1072,6 @@ namespace pipedal
|
||||
open = true;
|
||||
try
|
||||
{
|
||||
OpenMidi(jackServerSettings, channelSelection);
|
||||
OpenAudio(jackServerSettings, channelSelection);
|
||||
std::atomic_thread_fence(std::memory_order::release);
|
||||
}
|
||||
@@ -1531,18 +1518,43 @@ namespace pipedal
|
||||
} while (frames > 0);
|
||||
return framesRead;
|
||||
}
|
||||
|
||||
protected:
|
||||
void ReadMidiData(uint32_t audioFrame)
|
||||
{
|
||||
for (size_t i = 0; i < midiDevices.size(); ++i)
|
||||
AlsaMidiMessage message;
|
||||
|
||||
midiEventCount = 0;
|
||||
while(alsaSequencer->ReadMessage(message,0))
|
||||
{
|
||||
size_t nRead = midiDevices[i]->ReadMidiEvents(
|
||||
this->midiEvents,
|
||||
midiEventCount,
|
||||
audioFrame);
|
||||
midiEventCount += nRead;
|
||||
size_t messageSize = message.size;
|
||||
if (messageSize == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (midiEventMemoryIndex + messageSize >= this->midiEventMemory.size()) {
|
||||
continue;
|
||||
}
|
||||
if (midiEventCount >= this->midiEvents.size()) {
|
||||
midiEvents.resize(midiEventCount*2);
|
||||
}
|
||||
// for now, prevent META event messages from propagating.
|
||||
if (message.data[0] == 0xFF && message.size > 1) {
|
||||
continue;
|
||||
}
|
||||
MidiEvent *pEvent = midiEvents.data() + midiEventCount++;
|
||||
pEvent->time = audioFrame;
|
||||
pEvent->size = messageSize;
|
||||
pEvent->buffer = midiEventMemory.data() + midiEventMemoryIndex;
|
||||
|
||||
memcpy(
|
||||
midiEventMemory.data() + midiEventMemoryIndex,
|
||||
message.data,
|
||||
message.size);
|
||||
midiEventMemoryIndex += messageSize;
|
||||
|
||||
}
|
||||
}
|
||||
private:
|
||||
|
||||
long WriteBuffer(snd_pcm_t *handle, uint8_t *buf, size_t frames)
|
||||
{
|
||||
@@ -1671,16 +1683,17 @@ namespace pipedal
|
||||
pBuffer[j] = 0;
|
||||
}
|
||||
}
|
||||
try {
|
||||
try
|
||||
{
|
||||
while (!terminateAudio())
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
// zero out input buffers.
|
||||
this->driverHost->OnProcess(this->bufferSize);
|
||||
}
|
||||
} catch (const std::exception &e)
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
this->driverHost->OnAudioTerminated();
|
||||
@@ -1766,285 +1779,22 @@ namespace pipedal
|
||||
Lv2Log::debug("Audio thread joined.");
|
||||
}
|
||||
|
||||
static constexpr size_t MAX_MIDI_EVENT_SIZE = 3;
|
||||
static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024;
|
||||
static constexpr size_t MIDI_MEMORY_BUFFER_SIZE = 32 * 1024;
|
||||
static constexpr size_t MAX_MIDI_EVENT = 4 * 1024;
|
||||
|
||||
size_t midiEventCount = 0;
|
||||
std::vector<MidiEvent> midiEvents;
|
||||
size_t midiEventMemoryIndex = 0;
|
||||
std::vector<uint8_t> midiEventMemory;
|
||||
AlsaSequencer::ptr alsaSequencer;
|
||||
|
||||
public:
|
||||
class AlsaMidiDeviceImpl
|
||||
|
||||
|
||||
|
||||
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override
|
||||
{
|
||||
private:
|
||||
snd_rawmidi_t *hIn = nullptr;
|
||||
snd_rawmidi_params_t *hInParams = nullptr;
|
||||
std::string deviceName;
|
||||
|
||||
// running status state.
|
||||
uint8_t runningStatus = 0;
|
||||
int dataLength = 0;
|
||||
int dataIndex = 0;
|
||||
size_t statusBytesRemaining = 0;
|
||||
size_t data0 = 0;
|
||||
size_t data1 = 0;
|
||||
|
||||
bool inputProcessingSysex = false;
|
||||
size_t inputSysexBufferCount = 0;
|
||||
std::vector<uint8_t> inputSysexBuffer;
|
||||
|
||||
uint8_t readBuffer[1024];
|
||||
|
||||
void checkError(int result, const char *message)
|
||||
{
|
||||
if (result < 0)
|
||||
{
|
||||
throw PiPedalStateException(SS("Unexpected error: " << message << " (" << this->deviceName));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
AlsaMidiDeviceImpl()
|
||||
{
|
||||
inputSysexBuffer.resize(1024);
|
||||
}
|
||||
void Open(const AlsaMidiDeviceInfo &device)
|
||||
{
|
||||
runningStatus = 0;
|
||||
inputProcessingSysex = false;
|
||||
inputSysexBufferCount = 0;
|
||||
|
||||
dataIndex = 0;
|
||||
dataLength = 0;
|
||||
|
||||
this->deviceName = device.description_;
|
||||
|
||||
int err = snd_rawmidi_open(&hIn, nullptr, device.name_.c_str(), SND_RAWMIDI_NONBLOCK);
|
||||
if (err < 0)
|
||||
{
|
||||
throw PiPedalStateException(SS("Can't open midi device " << deviceName << ". (" << snd_strerror(err)));
|
||||
}
|
||||
|
||||
err = snd_rawmidi_params_malloc(&hInParams);
|
||||
checkError(err, "snd_rawmidi_params_malloc failed.");
|
||||
|
||||
err = snd_rawmidi_params_set_buffer_size(hIn, hInParams, 2048);
|
||||
checkError(err, "snd_rawmidi_params_set_buffer_size failed.");
|
||||
|
||||
err = snd_rawmidi_params_set_no_active_sensing(hIn, hInParams, 1);
|
||||
checkError(err, "snd_rawmidi_params_set_no_active_sensing failed.");
|
||||
}
|
||||
void Close()
|
||||
{
|
||||
if (hIn)
|
||||
{
|
||||
snd_rawmidi_close(hIn);
|
||||
hIn = nullptr;
|
||||
}
|
||||
if (hInParams)
|
||||
{
|
||||
snd_rawmidi_params_free(hInParams);
|
||||
hInParams = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int GetDataLength(uint8_t cc)
|
||||
{
|
||||
static int sDataLength[] = {0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 1, 1, 1, -1};
|
||||
return sDataLength[cc >> 4];
|
||||
}
|
||||
|
||||
void MidiPut(uint8_t cc, uint8_t d0, uint8_t d1)
|
||||
{
|
||||
if (cc == 0)
|
||||
return;
|
||||
|
||||
// check for overrun.
|
||||
if (inputEventBufferIndex >= pInputEventBuffer->size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto &event = (*pInputEventBuffer)[inputEventBufferIndex];
|
||||
|
||||
event.time = inputSampleFrame;
|
||||
event.size = dataLength + 1;
|
||||
assert(dataLength + 1 <= MAX_MIDI_EVENT_SIZE);
|
||||
event.buffer[0] = cc;
|
||||
event.buffer[1] = d0;
|
||||
event.buffer[2] = d1;
|
||||
++inputEventBufferIndex;
|
||||
}
|
||||
|
||||
void FillInputBuffer()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
ssize_t nRead = snd_rawmidi_read(hIn, readBuffer, sizeof(readBuffer));
|
||||
if (nRead == -EAGAIN)
|
||||
return;
|
||||
if (nRead < 0)
|
||||
{
|
||||
checkError(nRead, SS(this->deviceName << "MIDI event read failed. (" << snd_strerror(nRead)).c_str());
|
||||
}
|
||||
ProcessInputBuffer(readBuffer, nRead); // expose write to test code.
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t inputSampleFrame = -1;
|
||||
size_t inputEventBufferIndex;
|
||||
std::vector<MidiEvent> *pInputEventBuffer = nullptr;
|
||||
|
||||
size_t ReadMidiEvents(
|
||||
std::vector<MidiEvent> &outputBuffer,
|
||||
size_t startIndex,
|
||||
uint32_t sampleFrame)
|
||||
{
|
||||
inputSampleFrame = sampleFrame;
|
||||
inputEventBufferIndex = startIndex;
|
||||
pInputEventBuffer = &outputBuffer;
|
||||
FillInputBuffer();
|
||||
pInputEventBuffer = nullptr;
|
||||
return inputEventBufferIndex - startIndex;
|
||||
}
|
||||
|
||||
void FlushSysex()
|
||||
{
|
||||
if (inputProcessingSysex)
|
||||
{
|
||||
// just discard it. :-/
|
||||
// if (this->eventCount != MAX_MIDI_EVENT)
|
||||
// {
|
||||
// auto *event = &(events[eventCount++]);
|
||||
// event->size = this->bufferCount - sysexStartIndex;
|
||||
// event->buffer = &(this->buffer[this->sysexStartIndex]);
|
||||
// event->time = 0;
|
||||
// }
|
||||
// sysexStartIndex = -1;
|
||||
}
|
||||
inputProcessingSysex = false;
|
||||
}
|
||||
|
||||
int GetSystemCommonLength(uint8_t cc)
|
||||
{
|
||||
static int sizes[] = {-1, 1, 2, 1, -1, -1, 0, 0};
|
||||
return sizes[(cc >> 4) & 0x07];
|
||||
}
|
||||
void ProcessInputBuffer(uint8_t *readBuffer, size_t nRead)
|
||||
{
|
||||
for (ssize_t i = 0; i < nRead; ++i)
|
||||
{
|
||||
uint8_t v = readBuffer[i];
|
||||
|
||||
if (v >= 0x80)
|
||||
{
|
||||
if (v >= 0xF0)
|
||||
{
|
||||
if (v == 0xF0)
|
||||
{
|
||||
inputProcessingSysex = true;
|
||||
inputSysexBufferCount = 0;
|
||||
|
||||
inputSysexBuffer[inputSysexBufferCount++] = 0xF0;
|
||||
|
||||
runningStatus = 0; // discard subsequent data.
|
||||
dataLength = -2; // indefinitely.
|
||||
dataIndex = -1;
|
||||
}
|
||||
else if (v >= 0xF8)
|
||||
{
|
||||
// don't overwrite running status.
|
||||
// don't break sysexes on a running status message.
|
||||
// LV2 standard is ambiguous how realtime messages are handled, so just discard them.
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
FlushSysex();
|
||||
int length = GetSystemCommonLength(v);
|
||||
if (length == -1)
|
||||
break; // ignore illegal messages.
|
||||
runningStatus = v;
|
||||
dataLength = length;
|
||||
dataIndex = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FlushSysex();
|
||||
int dataLength = GetDataLength(v);
|
||||
runningStatus = v;
|
||||
if (dataLength == -1)
|
||||
{
|
||||
this->dataLength = dataLength;
|
||||
dataIndex = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->dataLength = dataLength;
|
||||
dataIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (inputProcessingSysex)
|
||||
{
|
||||
if (inputSysexBufferCount != inputSysexBuffer.size())
|
||||
{
|
||||
inputSysexBuffer[inputSysexBufferCount++] = v;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (dataIndex)
|
||||
{
|
||||
default:
|
||||
// discard.
|
||||
break;
|
||||
case 0:
|
||||
data0 = v;
|
||||
dataIndex = 1;
|
||||
break;
|
||||
case 1:
|
||||
data1 = v;
|
||||
dataIndex = 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dataIndex == dataLength && dataLength >= 0 && runningStatus != 0)
|
||||
{
|
||||
MidiPut(runningStatus, data0, data1);
|
||||
dataIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::unique_ptr<AlsaMidiDeviceImpl>> midiDevices;
|
||||
|
||||
void OpenMidi(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection)
|
||||
{
|
||||
const auto &devices = channelSelection.GetInputMidiDevices();
|
||||
|
||||
midiDevices.reserve(devices.size());
|
||||
|
||||
for (size_t i = 0; i < devices.size(); ++i)
|
||||
{
|
||||
try
|
||||
{
|
||||
const auto &device = devices[i];
|
||||
auto midiDevice = std::make_unique<AlsaMidiDeviceImpl>();
|
||||
midiDevice->Open(device);
|
||||
midiDevices.push_back(std::move(midiDevice));
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(e.what());
|
||||
}
|
||||
}
|
||||
this->alsaSequencer = alsaSequencer;
|
||||
}
|
||||
|
||||
virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); }
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@
|
||||
|
||||
#include "JackConfiguration.hpp"
|
||||
#include <functional>
|
||||
|
||||
#include "AlsaSequencer.hpp"
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace pipedal {
|
||||
virtual float*GetOutputBuffer(size_t channe) = 0;
|
||||
|
||||
virtual void Open(const JackServerSettings & jackServerSettings,const JackChannelSelection &channelSelection) = 0;
|
||||
|
||||
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) = 0;
|
||||
virtual void Activate() = 0;
|
||||
virtual void Deactivate() = 0;
|
||||
virtual void Close() = 0;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "AudioFileMetadata.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
JSON_MAP_BEGIN(AudioFileMetadata)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, duration)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, fileName)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, lastModified)
|
||||
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, title)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, track)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, album)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, albumArtist)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, artist)
|
||||
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, thumbnailType)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, thumbnailFile)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, thumbnailLastModified)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, position)
|
||||
|
||||
JSON_MAP_END()
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "json.hpp"
|
||||
|
||||
|
||||
namespace pipedal {
|
||||
#define GETTER_SETTER_REF(name) \
|
||||
const decltype(name##_) &name() const { return name##_; } \
|
||||
void name(const decltype(name##_) &value) { name##_ = value; }
|
||||
|
||||
#define GETTER_SETTER_VEC(name) \
|
||||
decltype(name##_) &name() { return name##_; } \
|
||||
const decltype(name##_) &name() const { return name##_; }
|
||||
|
||||
// void name(decltype(const name##_) &value) { name##_ = value; }
|
||||
// void name(decltype(const name##_) &&value) { name##_ = std::move(value); }
|
||||
|
||||
#define GETTER_SETTER(name) \
|
||||
decltype(name##_) name() const { return name##_; } \
|
||||
void name(decltype(name##_) value) { name##_ = value; }
|
||||
|
||||
|
||||
enum ThumbnailType
|
||||
{
|
||||
Unknown = 0,
|
||||
Embedded = 1, // embedded in the file
|
||||
Folder = 2, // from a albumArt.jpg or similar
|
||||
None = 3 // Use default thumbnail.
|
||||
};
|
||||
|
||||
class AudioFileMetadata
|
||||
{
|
||||
private:
|
||||
std::string fileName_;
|
||||
int64_t lastModified_ = 0;
|
||||
std::string title_;
|
||||
int32_t track_ = -1; // track number, 0-based, -1 if not set
|
||||
std::string album_;
|
||||
std::string artist_;
|
||||
std::string albumArtist_;
|
||||
float duration_ = 0;
|
||||
int32_t thumbnailType_ = 0; // 0 = unknown, 1 = embedded, 3 = folder, 4 = none
|
||||
std::string thumbnailFile_; // only when thumbnailType is 3= folder.
|
||||
int64_t thumbnailLastModified_ = 0;
|
||||
int32_t position_ = -1;
|
||||
|
||||
|
||||
public:
|
||||
|
||||
AudioFileMetadata() = default;
|
||||
AudioFileMetadata(const std::filesystem::path &file);
|
||||
|
||||
GETTER_SETTER(fileName)
|
||||
GETTER_SETTER(lastModified)
|
||||
GETTER_SETTER_REF(title)
|
||||
GETTER_SETTER_REF(track)
|
||||
GETTER_SETTER_REF(album)
|
||||
GETTER_SETTER_REF(albumArtist)
|
||||
GETTER_SETTER_REF(artist)
|
||||
GETTER_SETTER(duration)
|
||||
ThumbnailType thumbnailType() const { return (ThumbnailType)thumbnailType_;}
|
||||
void thumbnailType(ThumbnailType value) { thumbnailType_ = (int32_t)value; }
|
||||
GETTER_SETTER(thumbnailFile)
|
||||
GETTER_SETTER(thumbnailLastModified)
|
||||
GETTER_SETTER(position)
|
||||
|
||||
|
||||
public:
|
||||
DECLARE_JSON_MAP(AudioFileMetadata);
|
||||
|
||||
};
|
||||
|
||||
#undef GETTER_SETTER
|
||||
#undef GETTER_SETTER_VEC
|
||||
#undef GETTER_SETTER_REF
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "AudioFileMetadataReader.hpp"
|
||||
#include <json.hpp>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <json_variant.hpp>
|
||||
#include "LRUCache.hpp"
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include "TemporaryFile.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
namespace fs = std::filesystem;
|
||||
namespace chrono = std::chrono;
|
||||
|
||||
// Safely encode a filename for use in a shell command
|
||||
std::string shell_escape_filename(const std::string &filename)
|
||||
{
|
||||
std::string escaped;
|
||||
escaped.reserve(filename.size() + 2); // Reserve space for quotes and content
|
||||
|
||||
// Wrap the filename in single quotes
|
||||
escaped += '\'';
|
||||
|
||||
for (char c : filename)
|
||||
{
|
||||
// Escape single quotes by closing the quote, adding escaped quote, and reopening
|
||||
if (c == '\'')
|
||||
{
|
||||
escaped += "'\\''";
|
||||
}
|
||||
else
|
||||
{
|
||||
escaped += c;
|
||||
}
|
||||
}
|
||||
|
||||
escaped += '\'';
|
||||
return escaped;
|
||||
}
|
||||
|
||||
static std::string readPipeToString(FILE *file)
|
||||
{
|
||||
std::string content;
|
||||
char buffer[4096];
|
||||
size_t bytesRead;
|
||||
while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0)
|
||||
{
|
||||
content.append(buffer, bytesRead);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
static std::string GetJsonMetadata(const std::filesystem::path &path)
|
||||
{
|
||||
/* ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json
|
||||
~/filename.flac */
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "/usr/bin/ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json "
|
||||
<< shell_escape_filename(path.string())
|
||||
<< " 2>/dev/null";
|
||||
|
||||
std::string command = ss.str();
|
||||
FILE *output = popen(command.c_str(), "r");
|
||||
if (output == nullptr)
|
||||
{
|
||||
throw std::runtime_error("Failed to process file.");
|
||||
}
|
||||
std::string result;
|
||||
try
|
||||
{
|
||||
result = readPipeToString(output);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
pclose(output);
|
||||
throw std::runtime_error(e.what());
|
||||
}
|
||||
int retcode = pclose(output);
|
||||
if (retcode != 0)
|
||||
{
|
||||
throw std::runtime_error(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static float MetadataFloat(const json_variant &vt, float defaultValue = 0)
|
||||
{
|
||||
if (vt.is_string())
|
||||
{
|
||||
std::string s = vt.as_string();
|
||||
std::stringstream ss(s);
|
||||
float result = defaultValue;
|
||||
ss >> result;
|
||||
return result;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
static std::string MetadataString(const json_variant &o, const std::vector<std::string> &names)
|
||||
{
|
||||
for (const auto &name : names)
|
||||
{
|
||||
auto result = (*o.as_object())[name];
|
||||
if (result.is_string())
|
||||
{
|
||||
return result.as_string();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static int32_t MetadataTrackToInt(const std::string &track, const std::string &disc)
|
||||
{
|
||||
if (track.empty())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
try
|
||||
{
|
||||
int trackNumber = std::stoi(track);
|
||||
if (!disc.empty())
|
||||
{
|
||||
if (disc != "" && disc != "1/1" && disc != "1")
|
||||
{
|
||||
int discNumber = std::stoi(disc);
|
||||
trackNumber += discNumber*1000+ trackNumber; // e.g. disc 1 track 2 becomes 1002
|
||||
}
|
||||
|
||||
}
|
||||
return trackNumber;
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
return -1; // Invalid track or disc number
|
||||
}
|
||||
}
|
||||
|
||||
AudioFileMetadata::AudioFileMetadata(const std::filesystem::path &file)
|
||||
{
|
||||
try
|
||||
{
|
||||
const std::string json = GetJsonMetadata(file);
|
||||
|
||||
json_variant vt;
|
||||
|
||||
std::stringstream ss(json);
|
||||
json_reader reader(ss);
|
||||
|
||||
reader.read(&vt);
|
||||
|
||||
auto top = vt.as_object();
|
||||
auto format = top->at("format").as_object();
|
||||
|
||||
this->duration_ = MetadataFloat(format->at("duration"), 0);
|
||||
|
||||
auto tags = (*format)["tags"];
|
||||
if (tags.is_object())
|
||||
{
|
||||
// not all of this data gets used (e.g. DATE, YEAR, ALBUM_ARTIST,TOTALTRACKS). But ... write once.
|
||||
this->album_ = MetadataString(tags, {"ALBUM", "album"});
|
||||
this->artist_ = MetadataString(tags, {"ARTIST", "artist"});
|
||||
this->albumArtist_ = MetadataString(tags, {"ALBUM ARTIST", "album_artist", "album artist"});
|
||||
this->title_ = MetadataString(tags, {"TITLE", "title"});
|
||||
//this->date_ = MetadataString(tags, {"DATE", "date"});
|
||||
//this->year_ = MetadataString(tags, {"YEAR", "year"});
|
||||
this->track_ =
|
||||
MetadataTrackToInt(
|
||||
MetadataString(tags, {"track","TRACK"}),
|
||||
MetadataString(tags, {"disc", "DISC"})
|
||||
);
|
||||
//this->totalTracks_ = MetadataString(tags, {"TOTALTRACKS"});
|
||||
|
||||
if (title_ == "")
|
||||
{
|
||||
this->title_ = file.stem();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
std::string title = file.stem();
|
||||
this->title_ = title;
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
class AudioCacheKey
|
||||
{
|
||||
public:
|
||||
AudioCacheKey(const std::filesystem::path &path)
|
||||
{
|
||||
this->path = path.string();
|
||||
this->lastWrite = std::filesystem::last_write_time(path);
|
||||
}
|
||||
// Equality operator
|
||||
bool operator==(const AudioCacheKey &other) const
|
||||
{
|
||||
return path == other.path && lastWrite == other.lastWrite;
|
||||
}
|
||||
|
||||
public:
|
||||
std::string path;
|
||||
fs::file_time_type lastWrite;
|
||||
};
|
||||
}
|
||||
|
||||
namespace std
|
||||
{
|
||||
template <>
|
||||
struct hash<AudioCacheKey>
|
||||
{
|
||||
|
||||
size_t operator()(const AudioCacheKey &key) const
|
||||
{
|
||||
size_t path_hash = hash<std::string>{}(key.path);
|
||||
size_t time_hash = hash<uintmax_t>{}(
|
||||
duration_cast<std::chrono::nanoseconds>(key.lastWrite.time_since_epoch()).count());
|
||||
// Combine hashes (boost::hash_combine approach)
|
||||
return path_hash ^ (time_hash + 0x9e3779b9 + (path_hash << 6) + (path_hash >> 2));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static LRUCache<AudioCacheKey, std::string> metadataCache{500};
|
||||
static std::mutex metadataCacheMutex;
|
||||
|
||||
std::string pipedal::GetAudioFileMetadataString(const std::filesystem::path &path)
|
||||
{
|
||||
std::lock_guard lock{metadataCacheMutex};
|
||||
AudioCacheKey key{path};
|
||||
std::string result;
|
||||
if (metadataCache.get(key, result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
auto metadata = AudioFileMetadata(path);
|
||||
|
||||
std::stringstream ss;
|
||||
json_writer writer(ss);
|
||||
writer.write(metadata);
|
||||
result = ss.str();
|
||||
|
||||
metadataCache.put(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
TemporaryFile pipedal::GetAudioFileThumbnail(const std::filesystem::path &path, const std::filesystem::path &outputPath)
|
||||
{
|
||||
return GetAudioFileThumbnail(path, 0, 0, outputPath);
|
||||
}
|
||||
|
||||
std::mutex thumbnailMutex;
|
||||
|
||||
TemporaryFile pipedal::GetAudioFileThumbnail(const std::filesystem::path &path, int32_t width, int32_t height, const std::filesystem::path &tempDirectory)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thumbnailMutex);
|
||||
fs::path thumbnailDirectory = tempDirectory / "thumbnails";
|
||||
if (!fs::exists(thumbnailDirectory))
|
||||
{
|
||||
fs::create_directories(thumbnailDirectory);
|
||||
} else {
|
||||
// Clean up old thumbnails
|
||||
for (const auto &entry : fs::directory_iterator(thumbnailDirectory))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
fs::remove(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
fs::create_directories(tempDirectory);
|
||||
|
||||
TemporaryFile tempFile(tempDirectory);
|
||||
std::filesystem::path outputPath = thumbnailDirectory / "thumbnail-%03d.jpg";
|
||||
// ffmpeg -loglevel error -i "test2.mp3" -vf scale=200:200 -frames:v 1 thumb-%03.jpg -y
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "/usr/bin/ffmpeg -loglevel error -i " << shell_escape_filename(path.string());
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
ss << " -vf \"scale=" << width << ":" << height <<
|
||||
":force_original_aspect_ratio=increase,crop=" << width << ":" << height << "\"";
|
||||
} else {
|
||||
// -vf "crop=min(iw\,ih):min(iw\,ih)"
|
||||
ss << " -vf \"crop=min(iw\\,ih):min(iw\\,ih)\"";
|
||||
}
|
||||
ss << " " << shell_escape_filename(outputPath.string());
|
||||
ss << " -y 2>/dev/null 1>/dev/null";
|
||||
std::string command = ss.str();
|
||||
int rc = system(command.c_str());
|
||||
if (rc < 0) {
|
||||
throw std::runtime_error("Failed to execute ffmpeg command: " + command);
|
||||
}
|
||||
int exitCode = WEXITSTATUS(rc);
|
||||
if (exitCode != EXIT_SUCCESS)
|
||||
{ throw std::runtime_error("Thumbnail not foud.");
|
||||
}
|
||||
fs::remove(tempFile.Path());
|
||||
if (fs::exists(thumbnailDirectory / "thumbnail-001.jpg"))
|
||||
{
|
||||
// Move the thumbnail to the temporary file.
|
||||
fs::rename(thumbnailDirectory / "thumbnail-001.jpg", tempFile.Path());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Failed to create thumbnail.");
|
||||
}
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include "TemporaryFile.hpp"
|
||||
#include "AudioFileMetadata.hpp"
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
|
||||
|
||||
TemporaryFile GetAudioFileThumbnail(const std::filesystem::path &path, int32_t width, int32_t height, const std::filesystem::path &tempDirectory);
|
||||
TemporaryFile GetAudioFileThumbnail(const std::filesystem::path &path, const std::filesystem::path &tempDirectory);
|
||||
|
||||
std::string GetAudioFileMetadataString(const std::filesystem::path &path);
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,937 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "AudioFiles.hpp"
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include "AudioFileMetadataReader.hpp"
|
||||
#include "Locale.hpp"
|
||||
#include "MimeTypes.hpp"
|
||||
#include <stdexcept>
|
||||
#include "AudioFilesDb.hpp"
|
||||
#include "Lv2Log.hpp"
|
||||
#include "ss.hpp"
|
||||
#include "util.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
#undef _GLIBCXX_DEBUG // Ensure we are not in debug mode, as this file is not compatible with it.
|
||||
#include "SQLiteCpp/SQLiteCpp.h"
|
||||
|
||||
using namespace pipedal;
|
||||
using namespace pipedal::impl;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static constexpr size_t INVALID_INDEX = std::numeric_limits<size_t>::max();
|
||||
|
||||
|
||||
// All the varous cover art files I found on my personal music collection.
|
||||
// These are used to find the cover art for a directory. Based on
|
||||
// scanning a large music collection that have been tagged by various tools
|
||||
// including iTunes, Windows Media Playe, and a variety of taggers.
|
||||
//
|
||||
// Files are listed in order of preference.
|
||||
|
||||
static std::vector<std::string> COVER_ART_FILES = {
|
||||
"Folder.jpg", // window media player/explorer.
|
||||
"Cover.jpg", // itunes.
|
||||
"folder.jpg", // Linux audio players plex, kodi, Rythmbox, Amorok, Clementine.
|
||||
"cover.jpg", // Linux audio players plex, kodi, Rythmbox, Amorok, Clementine.
|
||||
"Artwork.jpg", // itunes.
|
||||
"Front.jpg",
|
||||
"front.jpg", // linux.
|
||||
"AlbumArt.jpg",
|
||||
"albumArt.jpg",
|
||||
"Frontcover.jpg",
|
||||
"AlbumArtSmall.jpg" // windows media player.
|
||||
};
|
||||
|
||||
bool pipedal::isArtworkFileName(const std::string &fileName)
|
||||
{
|
||||
for (const auto &coverArtFile : COVER_ART_FILES)
|
||||
{
|
||||
if (fileName == coverArtFile)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
void AudioDirectoryInfo::SetTemporaryDirectory(const std::filesystem::path &path)
|
||||
{
|
||||
temporaryDirectory = path;
|
||||
}
|
||||
void AudioDirectoryInfo::SetResourceDirectory(const std::filesystem::path &path)
|
||||
{
|
||||
resourceDirectory = path;
|
||||
}
|
||||
std::filesystem::path AudioDirectoryInfo::GetTemporaryDirectory()
|
||||
{
|
||||
if (temporaryDirectory.empty())
|
||||
{
|
||||
return "/tmp/pipedal/audiofiles"; // used during testing.
|
||||
}
|
||||
return temporaryDirectory;
|
||||
}
|
||||
std::filesystem::path AudioDirectoryInfo::GetResourceDirectory()
|
||||
{
|
||||
if (resourceDirectory.empty())
|
||||
{
|
||||
fs::path path = fs::current_path() / "vite/public/img"; // take from the source directory during testing.
|
||||
return "/usr/share/pipedal/audiofiles"; // used during testing.
|
||||
}
|
||||
return resourceDirectory;
|
||||
}
|
||||
|
||||
std::filesystem::path AudioDirectoryInfo::temporaryDirectory;
|
||||
std::filesystem::path AudioDirectoryInfo::resourceDirectory;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
static int64_t fileTimeToInt64(const fs::file_time_type &fileTime)
|
||||
{
|
||||
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
|
||||
fileTime - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
|
||||
auto result = std::chrono::duration_cast<std::chrono::milliseconds>(sctp.time_since_epoch()).count();
|
||||
return result;
|
||||
}
|
||||
static int64_t GetLastWriteTime(const fs::path &file)
|
||||
{
|
||||
try
|
||||
{
|
||||
return fileTimeToInt64(fs::last_write_time(file));
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
return 0; // Return 0 if we cannot get the last write time.
|
||||
}
|
||||
}
|
||||
|
||||
class AudioDirectoryInfoImpl : public AudioDirectoryInfo
|
||||
{
|
||||
public:
|
||||
AudioDirectoryInfoImpl(const std::filesystem::path &path, const std::filesystem::path &indexPath)
|
||||
: path(path), indexPath(
|
||||
(indexPath.empty() ? path : indexPath) / ".index.pipedal")
|
||||
{
|
||||
if (this->indexPath.parent_path() != path)
|
||||
{
|
||||
fs::create_directories(this->indexPath.parent_path());
|
||||
}
|
||||
}
|
||||
virtual ~AudioDirectoryInfoImpl() {}
|
||||
|
||||
virtual std::vector<AudioFileMetadata> GetFiles() override;
|
||||
virtual ThumbnailTemporaryFile GetThumbnail(const std::string &fileNameOnly, int32_t width, int32_t height) override;
|
||||
|
||||
virtual size_t TestGetNumberOfThumbnails() override; // test use only.
|
||||
virtual void TestSetIndexPath(const std::filesystem::path &path) override
|
||||
{
|
||||
indexPath = path;
|
||||
}
|
||||
virtual std::string GetNextAudioFile(const std::string &fileNameOnly) override;
|
||||
virtual std::string GetPreviousAudioFile(const std::string &fileNameOnly) override;
|
||||
|
||||
virtual void MoveAudioFile(
|
||||
const std::string &directory,
|
||||
int32_t fromPosition,
|
||||
int32_t toPosition) override;
|
||||
|
||||
|
||||
private:
|
||||
std::vector<DbFileInfo> QueryTracks();
|
||||
|
||||
bool opened = false;
|
||||
std::filesystem::path indexPath;
|
||||
void OpenAudioDb();
|
||||
ThumbnailTemporaryFile GetUnindexedThunbnail(const std::string &fileNameOnly, int32_t width, int32_t height);
|
||||
|
||||
std::shared_ptr<AudioFilesDb> audioFilesDb;
|
||||
fs::path path;
|
||||
using id_t = int64_t;
|
||||
|
||||
std::vector<DbFileInfo> UpdateDbFiles();
|
||||
void DbSetThumbnailType(
|
||||
int64_t idFile,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile = "",
|
||||
int64_t thumbnailLastModified = 0);
|
||||
void DbSetThumbnailType(
|
||||
const std::string &fileNameOnly,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile = "",
|
||||
int64_t thumbnailLastModified = 0);
|
||||
|
||||
void DbDeleteFile(DbFileInfo *dbFile);
|
||||
void UpdateMetadata(DbFileInfo *dbFile);
|
||||
static constexpr int DB_VERSION = 1;
|
||||
std::filesystem::path GetFolderFile() const;
|
||||
};
|
||||
}
|
||||
|
||||
AudioDirectoryInfo::Ptr AudioDirectoryInfo::Create(const std::filesystem::path &path, const std::filesystem::path &indexPath)
|
||||
{
|
||||
return std::make_shared<AudioDirectoryInfoImpl>(path, indexPath);
|
||||
}
|
||||
|
||||
std::vector<DbFileInfo> AudioDirectoryInfoImpl::QueryTracks()
|
||||
{
|
||||
if (audioFilesDb)
|
||||
{
|
||||
return audioFilesDb->QueryTracks();
|
||||
}
|
||||
return std::vector<DbFileInfo>();
|
||||
}
|
||||
|
||||
std::vector<AudioFileMetadata> AudioDirectoryInfoImpl::GetFiles()
|
||||
{
|
||||
OpenAudioDb();
|
||||
std::vector<DbFileInfo> dbFiles;
|
||||
try {
|
||||
dbFiles = UpdateDbFiles();
|
||||
} catch (const std::exception &e) {
|
||||
Lv2Log::error("Error updating audio file info: %s - %s",
|
||||
path.string().c_str(), e.what());
|
||||
}
|
||||
std::vector<AudioFileMetadata> metadataResults;
|
||||
for (const auto &dbFile : dbFiles)
|
||||
{
|
||||
AudioFileMetadata metadata{dbFile}; // slice copy of just the metadata.
|
||||
metadataResults.push_back(std::move(metadata));
|
||||
}
|
||||
return metadataResults;
|
||||
}
|
||||
|
||||
static bool isAudioExtension(const std::string &extension)
|
||||
{
|
||||
return MimeTypes::instance().AudioExtensions().contains(extension);
|
||||
}
|
||||
|
||||
static bool HasPositionInfo(const std::vector<DbFileInfo> &dbFiles)
|
||||
{
|
||||
for (const auto &file : dbFiles)
|
||||
{
|
||||
if (file.position() != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static int DefaultedSortOrder(int track)
|
||||
{
|
||||
if (track < 0)
|
||||
{
|
||||
return std::numeric_limits<int>::max();
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
static bool isFolderArtwork(const std::string &name)
|
||||
{
|
||||
return name.ends_with(".jpg") || name.ends_with(".png");
|
||||
}
|
||||
|
||||
static void SortDbFiles(std::vector<DbFileInfo> &dbFiles, Collator::ptr &collator)
|
||||
{
|
||||
std::sort(
|
||||
dbFiles.begin(), dbFiles.end(),
|
||||
[collator](const DbFileInfo &a, const DbFileInfo &b)
|
||||
{
|
||||
if (isFolderArtwork(a.fileName()) != isFolderArtwork(b.fileName()))
|
||||
{
|
||||
return isFolderArtwork(a.fileName()) < isFolderArtwork(b.fileName());
|
||||
}
|
||||
if (a.position() != b.position())
|
||||
{
|
||||
return DefaultedSortOrder(a.position()) < DefaultedSortOrder(b.position());
|
||||
}
|
||||
if (a.track() != b.track())
|
||||
{
|
||||
return DefaultedSortOrder(a.track()) < DefaultedSortOrder(b.track());
|
||||
}
|
||||
if (a.title() != b.title())
|
||||
{
|
||||
return collator->Compare(a.title(), b.title()) < 0;
|
||||
}
|
||||
if (a.album() != b.album())
|
||||
{
|
||||
return collator->Compare(a.title(), b.title()) < 0;
|
||||
}
|
||||
return false;
|
||||
;
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<DbFileInfo> AudioDirectoryInfoImpl::UpdateDbFiles()
|
||||
{
|
||||
std::shared_ptr<SQLite::Transaction> transaction;
|
||||
if (audioFilesDb)
|
||||
{
|
||||
transaction = audioFilesDb->transaction();
|
||||
}
|
||||
std::vector<DbFileInfo> dbFiles = QueryTracks();
|
||||
std::vector<DbFileInfo> newFiles;
|
||||
|
||||
bool updateRequired = false;
|
||||
std::map<std::string, DbFileInfo *> nameToDbRecord;
|
||||
for (size_t i = 0; i < dbFiles.size(); ++i)
|
||||
{
|
||||
auto *pFile = &(dbFiles[i]);
|
||||
nameToDbRecord[pFile->fileName()] = pFile;
|
||||
}
|
||||
|
||||
for (auto dirEntry : fs::directory_iterator(path))
|
||||
{
|
||||
bool isDirectory;
|
||||
try {
|
||||
isDirectory = dirEntry.is_directory();
|
||||
} catch (
|
||||
const std::filesystem::filesystem_error &e) {
|
||||
Lv2Log::error("Error accessing directory entry: %s - %s", dirEntry.path().string().c_str(), e.what());
|
||||
continue; // skip this entry if we cannot access it.
|
||||
}
|
||||
|
||||
if (!isDirectory)
|
||||
{
|
||||
auto path = dirEntry.path();
|
||||
std::string name = path.filename();
|
||||
std::string extension = path.extension();
|
||||
if (isAudioExtension(extension) || isArtworkFileName(name))
|
||||
{
|
||||
// If the file is an audio file or a cover art file, we need to check it.
|
||||
if (name.empty() || name[0] == '.')
|
||||
{
|
||||
continue; // skip hidden files.
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
continue; // not an audio file or cover art file.
|
||||
}
|
||||
{
|
||||
int64_t lastModified = fileTimeToInt64(dirEntry.last_write_time());
|
||||
|
||||
auto f = nameToDbRecord.find(name);
|
||||
if (f != nameToDbRecord.end())
|
||||
{
|
||||
DbFileInfo *dbFile = f->second;
|
||||
dbFile->present(true);
|
||||
if (dbFile->lastModified() != lastModified)
|
||||
{
|
||||
if (audioFilesDb)
|
||||
{
|
||||
audioFilesDb->DeleteThumbnails(dbFile->idFile());
|
||||
}
|
||||
dbFile->thumbnailType(ThumbnailType::Unknown);
|
||||
dbFile->thumbnailFile("");
|
||||
dbFile->thumbnailLastModified(0);
|
||||
UpdateMetadata(dbFile);
|
||||
dbFile->dirty(true);
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DbFileInfo newFile;
|
||||
newFile.fileName(name);
|
||||
newFile.idFile(-1);
|
||||
newFile.dirty(true);
|
||||
newFile.present(true);
|
||||
UpdateMetadata(&newFile);
|
||||
newFiles.push_back(std::move(newFile));
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto i = dbFiles.begin(); i != dbFiles.end(); ++i)
|
||||
{
|
||||
if (!i->present())
|
||||
{
|
||||
DbDeleteFile(&*i);
|
||||
dbFiles.erase(i);
|
||||
updateRequired = true;
|
||||
--i;
|
||||
}
|
||||
}
|
||||
// refresh folder file names.
|
||||
auto folderFile = GetFolderFile();
|
||||
int64_t folderLastModified = 0;
|
||||
std::string folderFileName;
|
||||
if (!folderFile.empty() && fs::exists(folderFile))
|
||||
{
|
||||
folderLastModified = GetLastWriteTime(folderFile);
|
||||
folderFileName = folderFile.filename().string();
|
||||
}
|
||||
for (auto &dbFile : dbFiles)
|
||||
{
|
||||
if (dbFile.thumbnailType() == ThumbnailType::None)
|
||||
{
|
||||
if (!folderFileName.empty())
|
||||
{
|
||||
dbFile.thumbnailType(ThumbnailType::Folder);
|
||||
dbFile.thumbnailFile(folderFileName);
|
||||
dbFile.thumbnailLastModified(folderLastModified);
|
||||
dbFile.dirty(true);
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
else if (dbFile.thumbnailType() == ThumbnailType::Folder)
|
||||
{
|
||||
// Check if the folder file has changed.
|
||||
if (folderFileName.empty())
|
||||
{
|
||||
// no more folder file. update accordingly.
|
||||
dbFile.thumbnailType(ThumbnailType::None);
|
||||
dbFile.thumbnailFile("");
|
||||
dbFile.thumbnailLastModified(0);
|
||||
dbFile.dirty(true);
|
||||
updateRequired = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// folder file has changed. update accordingly.
|
||||
if (dbFile.thumbnailFile() != folderFileName ||
|
||||
dbFile.thumbnailLastModified() != folderLastModified)
|
||||
{
|
||||
dbFile.thumbnailFile(folderFileName);
|
||||
dbFile.thumbnailLastModified(folderLastModified);
|
||||
dbFile.dirty(true);
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Locale::ptr locale = Locale::GetInstance();
|
||||
Collator::ptr collator = locale->GetCollator();
|
||||
if (!HasPositionInfo(dbFiles))
|
||||
{
|
||||
dbFiles.insert(dbFiles.end(), newFiles.begin(), newFiles.end());
|
||||
SortDbFiles(dbFiles, collator);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We have position info, so we need to update the position.
|
||||
dbFiles.insert(dbFiles.end(), newFiles.begin(), newFiles.end());
|
||||
SortDbFiles(dbFiles, collator);
|
||||
|
||||
for (size_t i = 0; i < dbFiles.size(); ++i)
|
||||
{
|
||||
auto newPosition = static_cast<int32_t>(i);
|
||||
if (newPosition != dbFiles[i].position())
|
||||
{
|
||||
dbFiles[i].dirty(true);
|
||||
}
|
||||
dbFiles[i].position(newPosition);
|
||||
}
|
||||
}
|
||||
for (auto &dbFile : dbFiles)
|
||||
{
|
||||
if (dbFile.dirty())
|
||||
{
|
||||
if (audioFilesDb)
|
||||
{
|
||||
audioFilesDb->WriteFile(&dbFile);
|
||||
}
|
||||
dbFile.dirty(false);
|
||||
}
|
||||
}
|
||||
if (transaction)
|
||||
{
|
||||
transaction->commit();
|
||||
transaction = nullptr;
|
||||
}
|
||||
return dbFiles;
|
||||
}
|
||||
|
||||
void AudioDirectoryInfoImpl::DbDeleteFile(DbFileInfo *dbFile)
|
||||
{
|
||||
if (this->audioFilesDb)
|
||||
{
|
||||
audioFilesDb->DeleteFile(dbFile);
|
||||
}
|
||||
}
|
||||
ThumbnailTemporaryFile AudioDirectoryInfo::DefaultThumbnailTemporaryFile()
|
||||
{
|
||||
ThumbnailTemporaryFile tempFile;
|
||||
fs::path defaultThumbnail = GetResourceDirectory() / "img/missing_thumbnail.jpg";
|
||||
tempFile.SetNonDeletedPath(defaultThumbnail, "image/jpeg");
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
ThumbnailTemporaryFile AudioDirectoryInfoImpl::GetUnindexedThunbnail(
|
||||
const std::string &fileNameOnly, int32_t width, int32_t height)
|
||||
{
|
||||
fs::path file = this->path / fileNameOnly;
|
||||
if (!fs::exists(file))
|
||||
{
|
||||
return DefaultThumbnailTemporaryFile();
|
||||
}
|
||||
try
|
||||
{
|
||||
auto tempFile = pipedal::GetAudioFileThumbnail(
|
||||
file, width, height,
|
||||
GetTemporaryDirectory());
|
||||
auto thumbnailPath = tempFile.Path();
|
||||
ThumbnailTemporaryFile result;
|
||||
result.Attach(tempFile.Detach(), "image/jpeg");
|
||||
|
||||
// Read the thumbnail data from the temporary file.
|
||||
std::ifstream thumbnailStream(thumbnailPath, std::ios::binary);
|
||||
if (!thumbnailStream)
|
||||
{
|
||||
throw std::runtime_error("Failed to open thumbnail file: " + thumbnailPath.string());
|
||||
}
|
||||
std::vector<uint8_t> thumbnailData(
|
||||
(std::istreambuf_iterator<char>(thumbnailStream)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
if (!thumbnailData.empty())
|
||||
{
|
||||
audioFilesDb->AddThumbnail(
|
||||
fileNameOnly,
|
||||
width,
|
||||
height,
|
||||
thumbnailData);
|
||||
|
||||
// Set the MIME type for the thumbnail.
|
||||
result.SetMimeType("image/jpeg");
|
||||
audioFilesDb->UpdateThumbnailInfo(
|
||||
fileNameOnly,
|
||||
ThumbnailType::Embedded);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Thumbnail data is empty.");
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error("GetUnindexedThunbnail failed: %s", e.what());
|
||||
}
|
||||
return DefaultThumbnailTemporaryFile();
|
||||
}
|
||||
|
||||
fs::path AudioDirectoryInfoImpl::GetFolderFile() const
|
||||
{
|
||||
for (const auto &coverArtFile : COVER_ART_FILES)
|
||||
{
|
||||
fs::path thumbnailPath = path / coverArtFile;
|
||||
if (fs::exists(thumbnailPath))
|
||||
{
|
||||
return thumbnailPath;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void AudioDirectoryInfoImpl::DbSetThumbnailType(
|
||||
int64_t idFile,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile,
|
||||
int64_t thumbnailLastModified)
|
||||
{
|
||||
if (audioFilesDb)
|
||||
{
|
||||
audioFilesDb->UpdateThumbnailInfo(
|
||||
idFile,
|
||||
thumbnailType,
|
||||
thumbnailFile,
|
||||
thumbnailLastModified);
|
||||
}
|
||||
}
|
||||
void AudioDirectoryInfoImpl::DbSetThumbnailType(
|
||||
const std::string &fileNameOnly,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile,
|
||||
int64_t thumbnailLastModified)
|
||||
{
|
||||
if (audioFilesDb)
|
||||
{
|
||||
audioFilesDb->UpdateThumbnailInfo(
|
||||
fileNameOnly,
|
||||
thumbnailType,
|
||||
thumbnailFile,
|
||||
thumbnailLastModified);
|
||||
}
|
||||
}
|
||||
|
||||
static ThumbnailTemporaryFile GetFolderTemporaryFile(const fs::path &folderFile)
|
||||
{
|
||||
ThumbnailTemporaryFile result;
|
||||
result.SetNonDeletedPath(folderFile, MimeTypes::instance().MimeTypeFromExtension(folderFile.extension().string()));
|
||||
return result;
|
||||
}
|
||||
ThumbnailTemporaryFile AudioDirectoryInfoImpl::GetThumbnail(const std::string &fileNameOnly, int32_t width, int32_t height)
|
||||
{
|
||||
fs::path file = this->path / fileNameOnly;
|
||||
OpenAudioDb();
|
||||
if (audioFilesDb)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
ThumbnailInfo thumbnailInfo = audioFilesDb->GetThumbnailInfo(file.filename().string());
|
||||
|
||||
if (thumbnailInfo.thumbnailType() == ThumbnailType::Folder)
|
||||
{
|
||||
fs::path thumbnailFile = path / thumbnailInfo.thumbnailFile();
|
||||
if (!fs::exists(thumbnailFile) ||
|
||||
fileTimeToInt64(fs::last_write_time(thumbnailFile)) !=
|
||||
thumbnailInfo.thumbnailLastModified())
|
||||
{
|
||||
// reset the thumbnail type and try again.
|
||||
audioFilesDb->UpdateThumbnailInfo(
|
||||
fileNameOnly,
|
||||
ThumbnailType::Unknown);
|
||||
return GetThumbnail(fileNameOnly, width, height);
|
||||
}
|
||||
fs::path fullFolderPath = this->path / thumbnailInfo.thumbnailFile();
|
||||
return GetFolderTemporaryFile(fullFolderPath);
|
||||
}
|
||||
if (thumbnailInfo.thumbnailType() == ThumbnailType::Embedded)
|
||||
{
|
||||
std::vector<uint8_t> blob = audioFilesDb->GetEmbeddedThumbnail(fileNameOnly, width, height);
|
||||
|
||||
if (!blob.empty())
|
||||
{
|
||||
ThumbnailTemporaryFile tempFile = ThumbnailTemporaryFile::CreateTemporaryFile(GetTemporaryDirectory(), "image/jpeg");
|
||||
|
||||
tempFile.SetMimeType("image/jpeg");
|
||||
// write buffer to tempFile.GetPath()
|
||||
std::ofstream outFile(tempFile.Path(), std::ios::binary);
|
||||
if (!outFile)
|
||||
{
|
||||
throw std::runtime_error("Failed to open temporary file for writing: " + tempFile.Path().string());
|
||||
}
|
||||
outFile.write((const char *)blob.data(), blob.size());
|
||||
return tempFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
// fall through and generate the indexed thumbnail.
|
||||
}
|
||||
}
|
||||
|
||||
if (thumbnailInfo.thumbnailType() == ThumbnailType::None)
|
||||
{
|
||||
return DefaultThumbnailTemporaryFile();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
auto tempFile = pipedal::GetAudioFileThumbnail(
|
||||
this->path / fileNameOnly,
|
||||
width, height,
|
||||
GetTemporaryDirectory());
|
||||
auto thumbnailPath = tempFile.Path();
|
||||
ThumbnailTemporaryFile result;
|
||||
result.Attach(tempFile.Detach(), "image/jpeg");
|
||||
|
||||
// Read the thumbnail data from the temporary file.
|
||||
std::ifstream thumbnailStream(thumbnailPath, std::ios::binary);
|
||||
if (!thumbnailStream)
|
||||
{
|
||||
throw std::runtime_error("Failed to open thumbnail file: " + thumbnailPath.string());
|
||||
}
|
||||
std::vector<uint8_t> thumbnailData(
|
||||
(std::istreambuf_iterator<char>(thumbnailStream)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
if (!thumbnailData.empty())
|
||||
{
|
||||
audioFilesDb->AddThumbnail(
|
||||
fileNameOnly,
|
||||
width,
|
||||
height,
|
||||
thumbnailData);
|
||||
|
||||
// Set the MIME type for the thumbnail.
|
||||
result.SetMimeType("image/jpeg");
|
||||
audioFilesDb->UpdateThumbnailInfo(
|
||||
fileNameOnly,
|
||||
ThumbnailType::Embedded);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Thumbnail data is empty.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (const std::exception &_)
|
||||
{
|
||||
}
|
||||
auto folderFile = GetFolderFile();
|
||||
if (!folderFile.empty())
|
||||
{
|
||||
// We have a folder thumbnail, set the type and return it.
|
||||
fs::path t = this->path / folderFile;
|
||||
int64_t lastModified = fileTimeToInt64(fs::last_write_time(t));
|
||||
DbSetThumbnailType(fileNameOnly, ThumbnailType::Folder, folderFile, lastModified);
|
||||
auto fullFolderPath = this->path / folderFile;
|
||||
return GetFolderTemporaryFile(fullFolderPath);
|
||||
}
|
||||
DbSetThumbnailType(
|
||||
fileNameOnly,
|
||||
ThumbnailType::None); // No thumbnail available, set to None.
|
||||
return DefaultThumbnailTemporaryFile();
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
}
|
||||
}
|
||||
return GetUnindexedThunbnail(fileNameOnly, width, height);
|
||||
}
|
||||
|
||||
|
||||
void AudioDirectoryInfoImpl::UpdateMetadata(DbFileInfo *dbFile)
|
||||
{
|
||||
fs::path file = this->path / dbFile->fileName();
|
||||
AudioFileMetadata metadata(file);
|
||||
dbFile->title(metadata.title());
|
||||
dbFile->track(metadata.track());
|
||||
dbFile->duration(metadata.duration());
|
||||
dbFile->album(metadata.album());
|
||||
dbFile->artist(metadata.artist());
|
||||
dbFile->albumArtist(metadata.albumArtist());
|
||||
dbFile->lastModified(GetLastWriteTime(file));
|
||||
dbFile->duration(metadata.duration());
|
||||
}
|
||||
|
||||
ThumbnailTemporaryFile::ThumbnailTemporaryFile(const std::filesystem::path &temporaryDirectory)
|
||||
: TemporaryFile(temporaryDirectory)
|
||||
{
|
||||
// Set a default MIME type for the thumbnail.
|
||||
mimeType = "image/jpeg"; // Default MIME type, can be set later.
|
||||
}
|
||||
|
||||
ThumbnailTemporaryFile ThumbnailTemporaryFile::CreateTemporaryFile(const std::filesystem::path &temporaryDirectory, const std::string &mimeType)
|
||||
{
|
||||
ThumbnailTemporaryFile result(temporaryDirectory);
|
||||
result.SetMimeType(mimeType);
|
||||
return result;
|
||||
}
|
||||
|
||||
ThumbnailTemporaryFile ThumbnailTemporaryFile::FromFile(const std::filesystem::path &filePath, const std::string &mimeType)
|
||||
{
|
||||
ThumbnailTemporaryFile result;
|
||||
result.Attach(filePath, mimeType);
|
||||
return result;
|
||||
}
|
||||
|
||||
void ThumbnailTemporaryFile::Attach(const std::filesystem::path &filePath, const std::string &mimeType)
|
||||
{
|
||||
super::Attach(filePath);
|
||||
SetMimeType(mimeType);
|
||||
}
|
||||
|
||||
size_t AudioDirectoryInfoImpl::TestGetNumberOfThumbnails()
|
||||
{
|
||||
if (!audioFilesDb)
|
||||
{
|
||||
throw std::logic_error("DB not open");
|
||||
}
|
||||
return this->audioFilesDb->GetNumberOfThumbnails();
|
||||
}
|
||||
|
||||
void AudioDirectoryInfoImpl::OpenAudioDb()
|
||||
{
|
||||
|
||||
if (!this->audioFilesDb)
|
||||
{
|
||||
try
|
||||
{
|
||||
this->audioFilesDb = std::make_shared<AudioFilesDb>(this->path, indexPath);
|
||||
}
|
||||
catch (const SQLite::Exception &e)
|
||||
{
|
||||
Lv2Log::debug("Can't create .index.pipedal: %s - %s", e.what(), this->path.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::string AudioDirectoryInfoImpl::GetNextAudioFile(const std::string &fileNameOnly)
|
||||
{
|
||||
auto files = this->GetFiles();
|
||||
if (files.empty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
for (size_t i = 0; i < files.size(); ++i)
|
||||
{
|
||||
if (files[i].fileName() == fileNameOnly)
|
||||
{
|
||||
size_t next = i;
|
||||
while (true)
|
||||
{
|
||||
next = (next + 1) % files.size();
|
||||
if (!isFolderArtwork(files[next].fileName()))
|
||||
{
|
||||
return files[next].fileName();
|
||||
}
|
||||
if (next == i)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
std::string AudioDirectoryInfoImpl::GetPreviousAudioFile(const std::string &fileNameOnly)
|
||||
{
|
||||
auto files = this->GetFiles();
|
||||
if (files.empty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
for (size_t i = 0; i < files.size(); ++i)
|
||||
{
|
||||
if (files[i].fileName() == fileNameOnly)
|
||||
{
|
||||
size_t next = i;
|
||||
while (true)
|
||||
{
|
||||
if (next == 0)
|
||||
{
|
||||
next = files.size() - 1; // wrap around to the last file.
|
||||
}
|
||||
else
|
||||
{
|
||||
--next; // go to the previous file.
|
||||
}
|
||||
if (!isFolderArtwork(files[next].fileName()))
|
||||
{
|
||||
return files[next].fileName();
|
||||
}
|
||||
if (next == i)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void AudioDirectoryInfoImpl::MoveAudioFile(
|
||||
const std::string &directory,
|
||||
int32_t fromPosition,
|
||||
int32_t toPosition)
|
||||
{
|
||||
OpenAudioDb();
|
||||
if (!audioFilesDb)
|
||||
{
|
||||
throw std::runtime_error("Directory is not writable.");
|
||||
}
|
||||
auto files = this->UpdateDbFiles();
|
||||
if (directory.empty() || fromPosition < 0 || toPosition < 0 ||
|
||||
fromPosition >= static_cast<int32_t>(files.size()) ||
|
||||
toPosition > static_cast<int32_t>(files.size()))
|
||||
{
|
||||
throw std::invalid_argument("Invalid arguments for MoveAudioFile");
|
||||
}
|
||||
std::shared_ptr<SQLite::Transaction> transaction;
|
||||
if (audioFilesDb)
|
||||
{
|
||||
transaction = audioFilesDb->transaction();
|
||||
}
|
||||
|
||||
if (fromPosition == toPosition)
|
||||
{
|
||||
return; // No change needed.
|
||||
}
|
||||
if (fromPosition > toPosition)
|
||||
{
|
||||
auto t = files[fromPosition];
|
||||
files.erase(files.begin() + fromPosition);
|
||||
files.insert(files.begin() + toPosition, t);
|
||||
}
|
||||
else
|
||||
{
|
||||
// fromPosition < toPosition
|
||||
auto t = files[fromPosition];
|
||||
files.erase(files.begin() + fromPosition);
|
||||
files.insert(files.begin() + toPosition, t); // insert before the toPosition.
|
||||
}
|
||||
for (size_t i = 0; i < files.size(); ++i)
|
||||
{
|
||||
auto &file = files[i];
|
||||
if (file.position() != static_cast<int32_t>(i))
|
||||
{
|
||||
file.position(static_cast<int32_t>(i));
|
||||
if (this->audioFilesDb)
|
||||
{
|
||||
audioFilesDb->UpdateFilePosition(file.idFile(), file.position());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Directory is not writable.");
|
||||
}
|
||||
}
|
||||
}
|
||||
transaction->commit();
|
||||
}
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
std::filesystem::path IndexPathToShadowIndexPath(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path)
|
||||
{
|
||||
std::filesystem::path relativePath = MakeRelativePath(path, audioRootDirectory);
|
||||
if (relativePath.is_absolute())
|
||||
{
|
||||
throw std::runtime_error(SS("Can't write to path " << path << "."));
|
||||
};
|
||||
return audioRootDirectory / "shadow_indexes" / relativePath;
|
||||
}
|
||||
|
||||
// recover the original path fromthe shadow index path.
|
||||
std::filesystem::path ShadowIndexPathToIndexPath(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path)
|
||||
{
|
||||
auto shadowIndexesPath = audioRootDirectory / "shadow_indexes";
|
||||
std::filesystem::path relativePath = MakeRelativePath(path, shadowIndexesPath);
|
||||
if (relativePath.is_absolute())
|
||||
{
|
||||
throw std::runtime_error(SS("Path must start with " << shadowIndexesPath));
|
||||
}
|
||||
return audioRootDirectory / relativePath; // recover the original path.
|
||||
}
|
||||
|
||||
std::filesystem::path GetShadowIndexDirectory(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path)
|
||||
{
|
||||
if (HasWritePermissions(path))
|
||||
{
|
||||
return ""; // use default index path.
|
||||
}
|
||||
return IndexPathToShadowIndexPath(audioRootDirectory, path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include "AudioFileMetadata.hpp"
|
||||
#include "TemporaryFile.hpp"
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
|
||||
class ThumbnailTemporaryFile : public TemporaryFile
|
||||
{
|
||||
private:
|
||||
// Private constructor to enforce the use of CreateTemporaryFile or the other constructor.
|
||||
explicit ThumbnailTemporaryFile(const std::filesystem::path &temporaryDirectory);
|
||||
|
||||
public:
|
||||
using super = TemporaryFile;
|
||||
ThumbnailTemporaryFile() = default;
|
||||
ThumbnailTemporaryFile(const ThumbnailTemporaryFile &) = delete;
|
||||
ThumbnailTemporaryFile(ThumbnailTemporaryFile &&) = default;
|
||||
ThumbnailTemporaryFile &operator=(const ThumbnailTemporaryFile &) = delete;
|
||||
ThumbnailTemporaryFile &operator=(ThumbnailTemporaryFile &&) = default;
|
||||
|
||||
public:
|
||||
static ThumbnailTemporaryFile CreateTemporaryFile(const std::filesystem::path &tempDirectory, const std::string &mimeType);
|
||||
static ThumbnailTemporaryFile FromFile(const std::filesystem::path &filePath, const std::string &mimeType);
|
||||
|
||||
public:
|
||||
void SetNonDeletedPath(const std::filesystem::path &path, const std::string &mimeType = "audio/mpeg")
|
||||
{
|
||||
TemporaryFile::SetNonDeletedPath(path);
|
||||
// Set the MIME type for audio files.
|
||||
this->mimeType = mimeType; // Default MIME type, can be set later.
|
||||
}
|
||||
void Attach(const std::filesystem::path &path, const std::string &mimeType);
|
||||
|
||||
std::string GetMimeType()
|
||||
{
|
||||
return mimeType;
|
||||
}
|
||||
void SetMimeType(const std::string &mimeType)
|
||||
{
|
||||
this->mimeType = mimeType;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string mimeType = "image/jpeg"; // Default MIME type, can be set later.
|
||||
};
|
||||
|
||||
class AudioDirectoryInfo
|
||||
{
|
||||
protected:
|
||||
AudioDirectoryInfo() {}
|
||||
virtual ~AudioDirectoryInfo() {}
|
||||
|
||||
public:
|
||||
using self = AudioDirectoryInfo;
|
||||
using Ptr = std::shared_ptr<self>;
|
||||
|
||||
static Ptr Create(
|
||||
const std::filesystem::path &path,
|
||||
const std::filesystem::path &indexPath = "");
|
||||
|
||||
virtual std::vector<AudioFileMetadata> GetFiles() = 0;
|
||||
|
||||
virtual ThumbnailTemporaryFile GetThumbnail(const std::string &fileNameOnly, int32_t width, int32_t height) = 0;
|
||||
|
||||
virtual std::string GetNextAudioFile(const std::string &fileNameOnly) = 0;
|
||||
virtual std::string GetPreviousAudioFile(const std::string &fileNameOnly) = 0;
|
||||
|
||||
virtual void MoveAudioFile(
|
||||
const std::string &directory,
|
||||
int32_t fromPosition,
|
||||
int32_t toPosition) = 0;
|
||||
static ThumbnailTemporaryFile DefaultThumbnailTemporaryFile();
|
||||
|
||||
static void SetTemporaryDirectory(const std::filesystem::path &path);
|
||||
static void SetResourceDirectory(const std::filesystem::path &path);
|
||||
|
||||
virtual size_t TestGetNumberOfThumbnails() = 0; // test use only.
|
||||
virtual void TestSetIndexPath(const std::filesystem::path &path) = 0;
|
||||
|
||||
public:
|
||||
static std::filesystem::path GetTemporaryDirectory();
|
||||
static std::filesystem::path GetResourceDirectory();
|
||||
|
||||
private:
|
||||
static std::filesystem::path temporaryDirectory;
|
||||
static std::filesystem::path resourceDirectory;
|
||||
;
|
||||
};
|
||||
|
||||
|
||||
|
||||
std::filesystem::path IndexPathToShadowIndexPath(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path);
|
||||
// recover the original path fromthe shadow index path.
|
||||
std::filesystem::path ShadowIndexPathToIndexPath(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path);
|
||||
std::filesystem::path GetShadowIndexDirectory(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path);
|
||||
|
||||
bool isArtworkFileName(const std::string &fileName);
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "AudioFilesDb.hpp"
|
||||
#include <stdexcept>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <map>
|
||||
|
||||
using namespace pipedal;
|
||||
using namespace pipedal::impl;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace pipedal::impl
|
||||
{
|
||||
struct DatabaseLockEntry
|
||||
{
|
||||
virtual ~DatabaseLockEntry() = default;
|
||||
|
||||
using ptr = std::shared_ptr<DatabaseLockEntry>;
|
||||
|
||||
std::mutex mutex;
|
||||
};
|
||||
static std::map<std::filesystem::path, std::shared_ptr<DatabaseLockEntry>> databaseLocks;
|
||||
|
||||
std::mutex databaseLockMutex;
|
||||
|
||||
class DatabaseLock
|
||||
{
|
||||
private:
|
||||
fs::path indexFile;
|
||||
std::shared_ptr<DatabaseLockEntry> lockEntry;
|
||||
|
||||
// Disable copy and move constructors
|
||||
DatabaseLock(const DatabaseLock &) = delete;
|
||||
DatabaseLock &operator=(const DatabaseLock &) = delete;
|
||||
DatabaseLock(DatabaseLock &&) = delete;
|
||||
DatabaseLock &operator=(DatabaseLock &&) = delete;
|
||||
|
||||
public:
|
||||
explicit DatabaseLock(const fs::path &lockFilePath)
|
||||
: indexFile(lockFilePath)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(databaseLockMutex);
|
||||
|
||||
auto it = databaseLocks.find(indexFile);
|
||||
if (it == databaseLocks.end())
|
||||
{
|
||||
// Create a new lock entry
|
||||
DatabaseLockEntry::ptr entry = std::make_shared<DatabaseLockEntry>();
|
||||
databaseLocks[indexFile] = entry;
|
||||
lockEntry = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
lockEntry = it->second;
|
||||
}
|
||||
}
|
||||
lockEntry->mutex.lock(); // we now have exclusive access to the database.
|
||||
}
|
||||
|
||||
~DatabaseLock()
|
||||
{
|
||||
lockEntry->mutex.unlock(); // release the lock
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(databaseLockMutex);
|
||||
auto use_count = lockEntry.use_count();
|
||||
|
||||
if (use_count == 2) // one for us, one for the Lockentry
|
||||
{
|
||||
// Remove the entry from the map if no other locks are held.
|
||||
databaseLocks.erase(indexFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static std::unique_ptr<DatabaseLock> getDatabaseLock(const std::filesystem::path &path)
|
||||
{
|
||||
// Create a lock file in the same directory as the database.
|
||||
return std::make_unique<DatabaseLock>(path);
|
||||
}
|
||||
|
||||
AudioFilesDb::AudioFilesDb(
|
||||
const std::filesystem::path &path,
|
||||
const std::filesystem::path &indexPath)
|
||||
: path(path)
|
||||
{
|
||||
fs::path dbPathName = this->path / ".pipedal.index";
|
||||
if (!indexPath.empty())
|
||||
{
|
||||
dbPathName = indexPath;
|
||||
}
|
||||
|
||||
dbLock = getDatabaseLock(dbPathName);
|
||||
|
||||
if (!fs::exists(dbPathName))
|
||||
{
|
||||
CreateDb(dbPathName);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->db = std::make_unique<SQLite::Database>(dbPathName, SQLite::OPEN_READWRITE);
|
||||
UpgradeDb();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioFilesDb::UpgradeDb()
|
||||
{
|
||||
int version = QueryVersion();
|
||||
}
|
||||
|
||||
int AudioFilesDb::QueryVersion()
|
||||
{
|
||||
SQLite::Statement query(*db, "SELECT version FROM am_dbInfo LIMIT 1");
|
||||
|
||||
// Fetch the result
|
||||
if (query.executeStep())
|
||||
{
|
||||
int version = query.getColumn(0).getInt();
|
||||
return version;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("QueryVersion failed.");
|
||||
}
|
||||
}
|
||||
|
||||
void AudioFilesDb::CreateDb(const std::filesystem::path &dbPathName)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
this->db = std::make_unique<SQLite::Database>(dbPathName, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
|
||||
|
||||
try
|
||||
{
|
||||
db->exec("CREATE TABLE am_dbInfo ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"version INTEGER NOT NULL)");
|
||||
|
||||
{
|
||||
SQLite::Statement query(*db, "INSERT INTO am_dbInfo (version) VALUES (?)");
|
||||
query.bind(1, DB_VERSION);
|
||||
query.exec();
|
||||
}
|
||||
|
||||
db->exec(
|
||||
"CREATE TABLE IF NOT EXISTS files ("
|
||||
"idFile INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"fileName TEXT NOT NULL, "
|
||||
"lastModified INT64 NOT NULL,"
|
||||
"title TEXT NOT NULL,"
|
||||
"track NUMBER NOT NULL,"
|
||||
"album TEXT NOT NULL, "
|
||||
"artist TEXT NOT NULL, "
|
||||
"albumArtist TEXT NOT NULL, "
|
||||
"duration REAL NOT NULL DEFAULT 0.0,"
|
||||
|
||||
"thumbnailType INTEGER NOT NULL DEFAULT 0,"
|
||||
"position INTEGER NOT NULL DEFAULT -1,"
|
||||
"thumbnailFile TEXT NOT NULL DEFAULT \"\","
|
||||
"thumbnailLastModified INT64 NOT NULL DEFAULT 0"
|
||||
")");
|
||||
|
||||
db->exec("CREATE TABLE IF NOT EXISTS thumbnails ("
|
||||
"idThumbnail INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"idFile INT64, "
|
||||
"thumbnail BLOB, "
|
||||
"width INTEGER, "
|
||||
"height INTEGER)");
|
||||
|
||||
}
|
||||
catch (const SQLite::Exception &e)
|
||||
{
|
||||
throw std::runtime_error("Failed to create database tables: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
catch (const SQLite::Exception &e)
|
||||
{
|
||||
throw std::runtime_error("Failed to create database: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void AudioFilesDb::DeleteFile(DbFileInfo *dbFile)
|
||||
{
|
||||
DeleteThumbnails(dbFile->idFile());
|
||||
if (!deleteFileQuery)
|
||||
{
|
||||
deleteFileQuery = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"DELETE FROM files WHERE idFile = ?");
|
||||
}
|
||||
deleteFileQuery->tryReset();
|
||||
deleteFileQuery->bind(1, dbFile->idFile());
|
||||
deleteFileQuery->exec();
|
||||
}
|
||||
|
||||
void AudioFilesDb::DeleteThumbnails(id_t idFile)
|
||||
{
|
||||
if (!updateThumbnailInfoQueryByName)
|
||||
{
|
||||
updateThumbnailInfoQueryByName = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"DELETE FROM thumbnails WHERE idFile = ?");
|
||||
}
|
||||
updateThumbnailInfoQueryByName->tryReset();
|
||||
updateThumbnailInfoQueryByName->bind(1, idFile);
|
||||
updateThumbnailInfoQueryByName->exec();
|
||||
}
|
||||
|
||||
size_t AudioFilesDb::GetNumberOfThumbnails()
|
||||
{
|
||||
SQLite::Statement query(*db, "SELECT COUNT(*) FROM thumbnails");
|
||||
if (query.executeStep())
|
||||
{
|
||||
return query.getColumn(0).getInt64();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void AudioFilesDb::UpdateThumbnailInfo(
|
||||
const std::string &fileName,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile,
|
||||
int64_t thumbnailLastModified)
|
||||
{
|
||||
if (!updateThumbnailInfoQueryByName)
|
||||
{
|
||||
updateThumbnailInfoQueryByName = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"UPDATE files SET thumbnailType = ?,thumbnailFile = ?, thumbnailLastModified = ? WHERE fileName = ?");
|
||||
}
|
||||
updateThumbnailInfoQueryByName->tryReset();
|
||||
updateThumbnailInfoQueryByName->bind(1, (int32_t)thumbnailType);
|
||||
updateThumbnailInfoQueryByName->bind(2, thumbnailFile);
|
||||
updateThumbnailInfoQueryByName->bind(3, thumbnailLastModified);
|
||||
updateThumbnailInfoQueryByName->bind(4, fileName);
|
||||
updateThumbnailInfoQueryByName->exec();
|
||||
}
|
||||
|
||||
void AudioFilesDb::UpdateThumbnailInfo(
|
||||
int64_t idFile,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile,
|
||||
int64_t thumbnailLastModified)
|
||||
{
|
||||
if (!updateThumbnailInfoQueryById)
|
||||
{
|
||||
updateThumbnailInfoQueryById = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"UPDATE files SET thumbnailType = ?,thumbnailFile = ?, thumbnailLastModified = ? WHERE idFile = ?");
|
||||
}
|
||||
updateThumbnailInfoQueryById->bind(1, (int32_t)thumbnailType);
|
||||
updateThumbnailInfoQueryById->bind(2, thumbnailFile);
|
||||
updateThumbnailInfoQueryById->bind(3, thumbnailLastModified);
|
||||
updateThumbnailInfoQueryById->bind(4, idFile);
|
||||
updateThumbnailInfoQueryById->exec();
|
||||
}
|
||||
|
||||
std::unique_ptr<SQLite::Transaction> AudioFilesDb::transaction()
|
||||
{
|
||||
return std::make_unique<SQLite::Transaction>(*db);
|
||||
}
|
||||
|
||||
std::vector<DbFileInfo> AudioFilesDb::QueryTracks()
|
||||
{
|
||||
std::vector<DbFileInfo> result;
|
||||
// get tracks fro the databse.
|
||||
SQLite::Statement query(
|
||||
*db,
|
||||
"SELECT idFile, fileName, "
|
||||
"lastModified, title, track, album, artist,albumArtist,duration, "
|
||||
"thumbnailType, position, "
|
||||
"thumbnailFile, thumbnailLastModified "
|
||||
"FROM files ");
|
||||
|
||||
while (query.executeStep())
|
||||
{
|
||||
DbFileInfo row;
|
||||
row.idFile(query.getColumn(0).getInt64());
|
||||
row.fileName(query.getColumn(1).getText());
|
||||
row.lastModified(query.getColumn(2).getInt64());
|
||||
row.title(query.getColumn(3).getText());
|
||||
row.track(query.getColumn(4).getInt());
|
||||
row.album(query.getColumn(5).getText());
|
||||
row.artist(query.getColumn(6).getText());
|
||||
row.albumArtist(query.getColumn(7).getText());
|
||||
row.duration(query.getColumn(8).getDouble());
|
||||
row.thumbnailType((ThumbnailType)query.getColumn(9).getInt());
|
||||
row.position(query.getColumn(10).getInt());
|
||||
row.thumbnailFile(query.getColumn(11).getText());
|
||||
row.thumbnailLastModified(query.getColumn(12).getInt64());
|
||||
result.push_back(std::move(row));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void AudioFilesDb::WriteFile(DbFileInfo *dbFile)
|
||||
{
|
||||
if (dbFile->idFile() == -1)
|
||||
{
|
||||
if (!insertFileQuery)
|
||||
{
|
||||
insertFileQuery = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"INSERT INTO files ("
|
||||
"fileName, lastModified,"
|
||||
"title,track,album,artist, albumArtist, "
|
||||
"duration, thumbnailType,thumbnailFile, thumbnailLastModified, position "
|
||||
") VALUES (?, ?, ?, ?, ?,?,?,?,?,?,?,?)");
|
||||
}
|
||||
insertFileQuery->tryReset();
|
||||
insertFileQuery->bind(1, dbFile->fileName());
|
||||
insertFileQuery->bind(2, dbFile->lastModified());
|
||||
insertFileQuery->bind(3, dbFile->title());
|
||||
insertFileQuery->bind(4, dbFile->track());
|
||||
insertFileQuery->bind(5, dbFile->album());
|
||||
insertFileQuery->bind(6, dbFile->artist());
|
||||
insertFileQuery->bind(7, dbFile->albumArtist());
|
||||
insertFileQuery->bind(8, dbFile->duration());
|
||||
insertFileQuery->bind(9, (int32_t)dbFile->thumbnailType());
|
||||
insertFileQuery->bind(10, dbFile->thumbnailFile());
|
||||
insertFileQuery->bind(11, dbFile->thumbnailLastModified());
|
||||
insertFileQuery->bind(12, dbFile->position());
|
||||
insertFileQuery->exec();
|
||||
dbFile->idFile(db->getLastInsertRowid());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!updateFileQuery)
|
||||
{
|
||||
updateFileQuery = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"UPDATE files SET "
|
||||
"fileName = ?, lastModified = ?, "
|
||||
"title = ?, track = ?, album = ?, artist = ? , albumArtist = ?, "
|
||||
"duration = ?, thumbnailType = ?, "
|
||||
"thumbnailFile = ?, thumbnailLastModified = ?, position = ? "
|
||||
" WHERE idFile = ?");
|
||||
}
|
||||
updateFileQuery->tryReset();
|
||||
updateFileQuery->bind(1, dbFile->fileName());
|
||||
updateFileQuery->bind(2, dbFile->lastModified());
|
||||
updateFileQuery->bind(3, dbFile->title());
|
||||
updateFileQuery->bind(4, dbFile->track());
|
||||
updateFileQuery->bind(5, dbFile->album());
|
||||
updateFileQuery->bind(6, dbFile->artist());
|
||||
updateFileQuery->bind(7, dbFile->albumArtist());
|
||||
updateFileQuery->bind(8, dbFile->duration());
|
||||
updateFileQuery->bind(9, (int32_t)dbFile->thumbnailType());
|
||||
updateFileQuery->bind(10, dbFile->thumbnailFile());
|
||||
updateFileQuery->bind(11, dbFile->thumbnailLastModified());
|
||||
updateFileQuery->bind(12, dbFile->position());
|
||||
|
||||
updateFileQuery->bind(13, dbFile->idFile());
|
||||
|
||||
updateFileQuery->exec();
|
||||
}
|
||||
}
|
||||
|
||||
ThumbnailInfo AudioFilesDb::GetThumbnailInfo(const std::string &fileNameOnly)
|
||||
{
|
||||
ThumbnailInfo result;
|
||||
SQLite::Statement query(
|
||||
*db,
|
||||
"SELECT thumbnailType, thumbnailFile, thumbnailLastModified FROM files WHERE fileName = ?");
|
||||
query.bind(1, fileNameOnly);
|
||||
if (query.executeStep())
|
||||
{
|
||||
result.thumbnailType((ThumbnailType)query.getColumn(0).getInt());
|
||||
result.thumbnailFile(query.getColumn(1).getText());
|
||||
result.thumbnailLastModified(query.getColumn(2).getInt64());
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("No thumbnail info found for file.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AudioFilesDb::GetEmbeddedThumbnail(int64_t idFile, int32_t width, int32_t height)
|
||||
{
|
||||
SQLite::Statement query(*db,
|
||||
"SELECT thumbnail FROM thumbnails "
|
||||
"WHERE idFile = ? AND width = ? AND height = ?");
|
||||
query.bind(1, idFile);
|
||||
query.bind(2, width);
|
||||
query.bind(3, height);
|
||||
|
||||
if (query.executeStep())
|
||||
{
|
||||
const uint8_t *data = (uint8_t *)query.getColumn(0).getBlob();
|
||||
int size = query.getColumn(0).getBytes();
|
||||
return std::vector<uint8_t>(data, data + size);
|
||||
}
|
||||
else
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AudioFilesDb::GetEmbeddedThumbnail(const std::string &fileNameOnly, int32_t width, int32_t height)
|
||||
{
|
||||
int64_t idFile;
|
||||
SQLite::Statement query(*db, "SELECT idFile FROM files WHERE fileName = ?");
|
||||
query.bind(1, fileNameOnly);
|
||||
if (query.executeStep())
|
||||
{
|
||||
idFile = query.getColumn(0).getInt64();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("File not found in database: " + fileNameOnly);
|
||||
}
|
||||
return GetEmbeddedThumbnail(idFile, width, height);
|
||||
}
|
||||
|
||||
void AudioFilesDb::AddThumbnail(
|
||||
const std::string &fileName,
|
||||
int64_t width,
|
||||
int64_t height,
|
||||
const std::vector<uint8_t> &thumbnailData)
|
||||
{
|
||||
int64_t idFile;
|
||||
SQLite::Statement query(*db, "SELECT idFile FROM files WHERE fileName = ?");
|
||||
query.bind(1, fileName);
|
||||
if (query.executeStep())
|
||||
{
|
||||
idFile = query.getColumn(0).getInt64();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("File not found in database: " + fileName);
|
||||
}
|
||||
AddThumbnail(idFile, width, height, thumbnailData);
|
||||
}
|
||||
void AudioFilesDb::AddThumbnail(
|
||||
int64_t idFile,
|
||||
int64_t width,
|
||||
int64_t height,
|
||||
const std::vector<uint8_t> &thumbnailData)
|
||||
{
|
||||
if (thumbnailData.empty())
|
||||
{
|
||||
throw std::runtime_error("Thumbnail data is empty.");
|
||||
}
|
||||
|
||||
SQLite::Statement insertQuery(
|
||||
*db,
|
||||
"INSERT INTO thumbnails (idFile, thumbnail, width, height) VALUES (?, ?, ?, ?)");
|
||||
insertQuery.bind(1, idFile);
|
||||
insertQuery.bind(2, thumbnailData.data(), thumbnailData.size());
|
||||
insertQuery.bind(3, width);
|
||||
insertQuery.bind(4, height);
|
||||
|
||||
try
|
||||
{
|
||||
insertQuery.exec();
|
||||
}
|
||||
catch (const SQLite::Exception &e)
|
||||
{
|
||||
throw std::runtime_error("Failed to add thumbnail: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void AudioFilesDb::UpdateFilePosition(
|
||||
int64_t idFile,
|
||||
int32_t position)
|
||||
{
|
||||
if (!updateFileQuery)
|
||||
{
|
||||
updateFileQuery = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"UPDATE files SET position = ? WHERE idFile = ?");
|
||||
}
|
||||
updateFileQuery->tryReset();
|
||||
updateFileQuery->bind(1, position);
|
||||
updateFileQuery->bind(2, idFile);
|
||||
updateFileQuery->exec();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include "SQLiteCpp/SQLiteCpp.h"
|
||||
#include "AudioFileMetadata.hpp"
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace pipedal::impl {
|
||||
|
||||
class DatabaseLock;
|
||||
|
||||
class DbFileInfo : public AudioFileMetadata
|
||||
{
|
||||
private:
|
||||
int64_t idFile_ = -1;
|
||||
bool present_ = false;
|
||||
bool dirty_ = false;
|
||||
public:
|
||||
int64_t idFile() const { return idFile_;}
|
||||
void idFile(int64_t value) { idFile_ = value;}
|
||||
bool present() const { return present_; }
|
||||
void present(bool value) { present_ = value;}
|
||||
bool dirty() const { return dirty_;}
|
||||
void dirty(bool value) { dirty_ = value;}
|
||||
|
||||
};
|
||||
|
||||
|
||||
class ThumbnailInfo {
|
||||
public:
|
||||
ThumbnailInfo() = default;
|
||||
private:
|
||||
ThumbnailType thumbnailType_ = ThumbnailType::Unknown;
|
||||
std::string thumbnailFile_;
|
||||
int64_t thumbnailLastModified_ = 0;
|
||||
public:
|
||||
ThumbnailType thumbnailType() const { return thumbnailType_; }
|
||||
void thumbnailType(ThumbnailType value) { thumbnailType_ = value; }
|
||||
const std::string &thumbnailFile() const { return thumbnailFile_; }
|
||||
void thumbnailFile(const std::string &value) { thumbnailFile_ = value; }
|
||||
int64_t thumbnailLastModified() const { return thumbnailLastModified_; }
|
||||
void thumbnailLastModified(int64_t value) { thumbnailLastModified_ = value; }
|
||||
};
|
||||
class AudioFilesDb {
|
||||
public:
|
||||
static constexpr int32_t DB_VERSION = 1;
|
||||
AudioFilesDb(
|
||||
const std::filesystem::path &path,
|
||||
const std::filesystem::path &indexPath = "" // if non-empty, forces the location of the ".index.pipedal" file.
|
||||
);
|
||||
public:
|
||||
void DeleteFile(DbFileInfo *dbFile);
|
||||
void WriteFile(DbFileInfo *dbFile);
|
||||
|
||||
// test only.
|
||||
size_t GetNumberOfThumbnails();
|
||||
void UpdateThumbnailInfo(
|
||||
int64_t idFile,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile = "",
|
||||
int64_t thumbnailLastModified = 0);
|
||||
void UpdateThumbnailInfo(
|
||||
const std::string&fileName,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile = "",
|
||||
int64_t thumbnailLastModified = 0);
|
||||
std::unique_ptr<SQLite::Transaction> transaction();
|
||||
std::vector<DbFileInfo> QueryTracks();
|
||||
void DeleteThumbnails(id_t idFile);
|
||||
ThumbnailInfo GetThumbnailInfo(const std::string &fileNameOnly);
|
||||
std::vector<uint8_t> GetEmbeddedThumbnail(const std::string &fileNameOnly, int32_t width, int32_t height);
|
||||
std::vector<uint8_t> GetEmbeddedThumbnail(int64_t idFile, int32_t width, int32_t height);
|
||||
void AddThumbnail(
|
||||
const std::string &fileName,
|
||||
int64_t width,
|
||||
int64_t height,
|
||||
const std::vector<uint8_t> &thumbnailData);
|
||||
void AddThumbnail(
|
||||
int64_t idFile,
|
||||
int64_t width,
|
||||
int64_t height,
|
||||
const std::vector<uint8_t> &thumbnailData);
|
||||
void UpdateFilePosition(
|
||||
int64_t idFile,
|
||||
int32_t position);
|
||||
|
||||
private:
|
||||
|
||||
void CreateDb(const std::filesystem::path &dbPathName);
|
||||
void UpgradeDb();
|
||||
int QueryVersion();
|
||||
|
||||
private:
|
||||
std::filesystem::path indexPath;
|
||||
|
||||
// warning: destructor order matters here!
|
||||
std::shared_ptr<DatabaseLock> dbLock; // mutex to protect the index file.
|
||||
std::unique_ptr<SQLite::Database> db;
|
||||
std::unique_ptr<SQLite::Statement> insertFileQuery;
|
||||
std::unique_ptr<SQLite::Statement> updateFileQuery;
|
||||
std::unique_ptr<SQLite::Statement> deleteFileQuery;
|
||||
std::unique_ptr<SQLite::Statement> updateThumbnailInfoQueryByName;
|
||||
std::unique_ptr<SQLite::Statement> updateThumbnailInfoQueryById;
|
||||
std::unique_ptr<SQLite::Statement> updatePositionQuery;
|
||||
std::filesystem::path path;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,536 @@
|
||||
// Copyright (c) 2025 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#include "pch.h"
|
||||
#include "catch.hpp"
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include "AudioFiles.hpp"
|
||||
#include <filesystem>
|
||||
#include "SysExec.hpp"
|
||||
#include "json.hpp"
|
||||
#include "json_variant.hpp"
|
||||
#include "MimeTypes.hpp"
|
||||
|
||||
using namespace std;
|
||||
using namespace pipedal;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::path testDir = fs::temp_directory_path() / "AudioFilesTest" / "files";
|
||||
fs::path sourceDirectory = fs::current_path() / "artifacts" / "TestTracks";
|
||||
fs::path tempDir = fs::temp_directory_path() / "AudioFilesTest" / "tmp";
|
||||
fs::path resourceDir = fs::current_path() / "vite" / "public" / "img";
|
||||
fs::path textIndexPath = fs::temp_directory_path() / "AudioFilesTest" / "TextIndex.pipedal";
|
||||
|
||||
static void CreateTestDirectory()
|
||||
{
|
||||
if (fs::exists(testDir))
|
||||
{
|
||||
fs::remove_all(testDir);
|
||||
}
|
||||
fs::remove_all(tempDir);
|
||||
fs::create_directories(testDir);
|
||||
if (fs::exists(tempDir))
|
||||
{
|
||||
fs::remove_all(tempDir);
|
||||
}
|
||||
fs::create_directories(tempDir);
|
||||
AudioDirectoryInfo::SetTemporaryDirectory(tempDir);
|
||||
AudioDirectoryInfo::SetResourceDirectory(resourceDir);
|
||||
}
|
||||
|
||||
static void CheckForLeakedTemporaryFiles()
|
||||
{
|
||||
bool leakedFiles = false;
|
||||
// Check if the temporary directory exists and is not empty
|
||||
if (fs::exists(tempDir) && fs::is_directory(tempDir))
|
||||
{
|
||||
for (const auto &entry : fs::directory_iterator(tempDir))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
cout << "Leaked temporary file: " << entry.path() << endl;
|
||||
leakedFiles = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fs::exists(tempDir) && fs::is_directory(tempDir / "thumbnails"))
|
||||
{
|
||||
for (const auto &entry : fs::directory_iterator(tempDir / "thumbnails"))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
cout << "Leaked temporary file: " << entry.path() << endl;
|
||||
leakedFiles = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (leakedFiles)
|
||||
{
|
||||
throw std::runtime_error("Leaked temporary files found in: " + tempDir.string());
|
||||
}
|
||||
}
|
||||
|
||||
static bool ContainsTrack(const std::vector<AudioFileMetadata> &files, const std::string &trackName)
|
||||
{
|
||||
for (const auto &file : files)
|
||||
{
|
||||
if (file.title() == trackName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool ContainsFile(const std::vector<AudioFileMetadata> &files, const std::string &fileName)
|
||||
{
|
||||
for (const auto &file : files)
|
||||
{
|
||||
if (file.fileName() == fileName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static const AudioFileMetadata *GetFile(const std::vector<AudioFileMetadata> &files, const std::string &fileName)
|
||||
{
|
||||
for (const auto &file : files)
|
||||
{
|
||||
if (file.fileName() == fileName)
|
||||
{
|
||||
return &file;
|
||||
}
|
||||
}
|
||||
throw std::logic_error("File not found: " + fileName);
|
||||
}
|
||||
|
||||
static void PrintFiles(const std::vector<AudioFileMetadata> &files)
|
||||
{
|
||||
for (const auto &file : files)
|
||||
{
|
||||
cout << "File: " << file.fileName() << ", Title: " << file.track() << ". " << file.title() << " Album: " << file.album() << endl;
|
||||
}
|
||||
cout << endl;
|
||||
}
|
||||
|
||||
void IndexTest()
|
||||
{
|
||||
CreateTestDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
{
|
||||
AudioDirectoryInfo::Ptr audioDir = AudioDirectoryInfo::Create(testDir.string());
|
||||
|
||||
}
|
||||
AudioDirectoryInfo::Ptr audioDir = AudioDirectoryInfo::Create(testDir.string());
|
||||
REQUIRE(audioDir != nullptr);
|
||||
if (fs::exists(testDir))
|
||||
{
|
||||
fs::remove_all(testDir); // Remove any existing database file
|
||||
}
|
||||
fs::create_directories(testDir);
|
||||
// Copy test files to the test directory
|
||||
for (const auto &entry : fs::directory_iterator(sourceDirectory))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
fs::copy(entry.path(), testDir / entry.path().filename());
|
||||
}
|
||||
}
|
||||
|
||||
auto files = audioDir->GetFiles();
|
||||
PrintFiles(files);
|
||||
|
||||
REQUIRE(files.size() == 3);
|
||||
|
||||
REQUIRE(ContainsTrack(files, "Track 1"));
|
||||
REQUIRE(ContainsTrack(files, "Track 2"));
|
||||
REQUIRE(ContainsTrack(files, "Track 3"));
|
||||
|
||||
for (const auto &file : files)
|
||||
{
|
||||
REQUIRE(!file.fileName().empty());
|
||||
}
|
||||
|
||||
// check indexed results.
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(files.size() == 3);
|
||||
for (const auto &file : files)
|
||||
{
|
||||
REQUIRE(!file.fileName().empty());
|
||||
}
|
||||
|
||||
fs::remove(testDir / "Track2.mp3");
|
||||
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(files.size() == 2);
|
||||
REQUIRE(!ContainsTrack(files, "Track 2"));
|
||||
REQUIRE(ContainsTrack(files, "Track 1"));
|
||||
REQUIRE(ContainsTrack(files, "Track 3"));
|
||||
|
||||
PrintFiles(files);
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(files.size() == 2);
|
||||
REQUIRE(!ContainsTrack(files, "Track 2"));
|
||||
REQUIRE(ContainsTrack(files, "Track 1"));
|
||||
REQUIRE(ContainsTrack(files, "Track 3"));
|
||||
|
||||
// check dbinex results.
|
||||
|
||||
fs::copy_file(sourceDirectory / "Track2.mp3", testDir / "Track3.mp3", fs::copy_options::overwrite_existing);
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(files.size() == 2);
|
||||
REQUIRE(ContainsTrack(files, "Track 2"));
|
||||
REQUIRE(ContainsFile(files, "Track3.mp3"));
|
||||
REQUIRE(!ContainsFile(files, "Track2.mp3"));
|
||||
|
||||
fs::copy_file(sourceDirectory / "Track3.mp3", testDir / "Track4.mp3", fs::copy_options::overwrite_existing);
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(files.size() == 3);
|
||||
REQUIRE(ContainsTrack(files, "Track 2"));
|
||||
REQUIRE(ContainsFile(files, "Track3.mp3"));
|
||||
REQUIRE(GetFile(files, "Track4.mp3")->title() == "Track 3");
|
||||
|
||||
{
|
||||
auto thumbnail = audioDir->GetThumbnail("Track1.mp3", 200, 200);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 1);
|
||||
}
|
||||
{
|
||||
auto thumbnail = audioDir->GetThumbnail("Track1.mp3", 200, 200);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 1);
|
||||
}
|
||||
{
|
||||
auto thumbnail = audioDir->GetThumbnail("Track1.mp3", 0, 0);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 2);
|
||||
}
|
||||
CheckForLeakedTemporaryFiles();
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
cout << "Exception: " << e.what() << endl;
|
||||
REQUIRE(false);
|
||||
}
|
||||
}
|
||||
|
||||
static void ThumbnailTest()
|
||||
{
|
||||
cout << "ThumbnailTest" << endl;
|
||||
CreateTestDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
AudioDirectoryInfo::Ptr audioDir = AudioDirectoryInfo::Create(testDir.string());
|
||||
REQUIRE(audioDir != nullptr);
|
||||
if (fs::exists(testDir))
|
||||
{
|
||||
fs::remove_all(testDir); // Remove any existing database file
|
||||
}
|
||||
fs::create_directories(testDir);
|
||||
// Copy test files to the test directory
|
||||
for (const auto &entry : fs::directory_iterator(sourceDirectory))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
fs::copy(entry.path(), testDir / entry.path().filename());
|
||||
}
|
||||
}
|
||||
|
||||
auto files = audioDir->GetFiles();
|
||||
|
||||
REQUIRE(files.size() == 3);
|
||||
|
||||
for (const auto &file : files)
|
||||
{
|
||||
ThumbnailTemporaryFile thumbnail = audioDir->GetThumbnail(file.fileName(), 200, 200);
|
||||
REQUIRE(fs::exists(thumbnail.Path()));
|
||||
cout << "Thumbnail for " << file.fileName() << ": " << thumbnail.Path() << endl;
|
||||
|
||||
cout << " " << file.fileName() << "(200,200) size: " << fs::file_size(thumbnail.Path()) << endl;
|
||||
|
||||
thumbnail = audioDir->GetThumbnail(file.fileName(), 0, 0);
|
||||
REQUIRE(fs::exists(thumbnail.Path()));
|
||||
cout << "Thumbnail for " << file.fileName() << ": " << thumbnail.Path() << endl;
|
||||
|
||||
cout << " " << file.fileName() << "(0,0) size: " << fs::file_size(thumbnail.Path()) << endl;
|
||||
}
|
||||
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 2 * 2); // 3 files, one file is missing a thumbnail, 2 per file.
|
||||
|
||||
{
|
||||
ThumbnailTemporaryFile thumbnail = audioDir->GetThumbnail("Track1.mp3", 200, 200);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 2 * 2); // 3 files, one file is missing a thumbnail, 2 per file.
|
||||
}
|
||||
fs::remove(testDir / "Track2.mp3");
|
||||
files = audioDir->GetFiles();
|
||||
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 2);
|
||||
|
||||
// update the last write time, should trigger a thumbnail update.
|
||||
REQUIRE(GetFile(files, "Track1.mp3")->thumbnailType() == ThumbnailType::Embedded);
|
||||
|
||||
fs::copy(sourceDirectory / "Track1.mp3", testDir / "Track1.mp3", fs::copy_options::overwrite_existing);
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(GetFile(files, "Track1.mp3")->thumbnailType() == ThumbnailType::Unknown);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 0);
|
||||
|
||||
audioDir->GetThumbnail("Track1.mp3", 200, 200);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 1);
|
||||
|
||||
CheckForLeakedTemporaryFiles();
|
||||
cout << endl;
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
cout << "Exception: " << e.what() << endl;
|
||||
REQUIRE(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ExecutionTimeTest()
|
||||
{
|
||||
cout << "Timing test" << endl;
|
||||
CreateTestDirectory();
|
||||
|
||||
// deploy sample files.
|
||||
fs::create_directories(testDir);
|
||||
// Copy test files to the test directory
|
||||
for (const auto &entry : fs::directory_iterator(sourceDirectory))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
fs::copy(entry.path(), testDir / entry.path().filename());
|
||||
}
|
||||
}
|
||||
|
||||
AudioDirectoryInfo::Ptr audioDir = AudioDirectoryInfo::Create(testDir.string());
|
||||
audioDir->GetFiles();
|
||||
|
||||
{
|
||||
using clock_t = std::chrono::high_resolution_clock;
|
||||
{
|
||||
auto start = clock_t::now();
|
||||
audioDir->GetThumbnail("Track1.mp3",200,200);
|
||||
auto duration = clock_t::now() - start;
|
||||
cout << "Track1.mp3(200,200): " << std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() <<"ms" << endl;
|
||||
}
|
||||
{
|
||||
auto start = clock_t::now();
|
||||
audioDir->GetThumbnail("Track1.mp3",200,200);
|
||||
auto duration = clock_t::now() - start;
|
||||
cout << "Track1.mp3(200,200): " << std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() <<"ms" << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST_CASE("AudioFiles test", "[AudioFiles]")
|
||||
{
|
||||
|
||||
IndexTest();
|
||||
ThumbnailTest();
|
||||
ExecutionTimeTest();
|
||||
}
|
||||
|
||||
// from AudioFileMetadata.cpp
|
||||
std::string shell_escape_filename(const std::string &filename);
|
||||
|
||||
static void ScanThumbnails()
|
||||
{
|
||||
const std::string homeDir = getenv("HOME");
|
||||
std::filesystem::path musicDir = std::filesystem::path(homeDir) / "Music";
|
||||
|
||||
for (const auto &entry : std::filesystem::recursive_directory_iterator(musicDir))
|
||||
{
|
||||
if (entry.is_directory())
|
||||
{
|
||||
continue; // Skip directories
|
||||
}
|
||||
|
||||
if (entry.path().extension() == ".mp3" || entry.path().extension() == ".flac")
|
||||
{
|
||||
try
|
||||
{
|
||||
// ffprobe -i ~/tmp/test.flac -show_streams -show_entries stream=index,codec_name,disposition
|
||||
|
||||
std::stringstream args;
|
||||
args << "-log_level error -i " << shell_escape_filename(entry.path())
|
||||
<< " -show_streams -of json -show_entries stream=index,codec_name,disposition 2>/dev/null";
|
||||
|
||||
std::string sargs = args.str();
|
||||
auto output = sysExecForOutput("/usr/bin/ffprobe", sargs, true);
|
||||
if (output.exitCode != 0)
|
||||
{
|
||||
cout << "ffprobe failed for file: " << entry.path() << endl;
|
||||
continue;
|
||||
}
|
||||
std::stringstream ss(output.output);
|
||||
json_reader reader(ss);
|
||||
|
||||
json_variant vt;
|
||||
reader.read(&vt);
|
||||
auto streams = vt.as_object()->at("streams").as_array();
|
||||
struct StreamInfo
|
||||
{
|
||||
int index;
|
||||
std::string codec_name;
|
||||
std::string comment;
|
||||
};
|
||||
std::vector<StreamInfo> thumbnailStreams;
|
||||
for (const auto &stream : *streams)
|
||||
{
|
||||
auto streamObj = stream.as_object();
|
||||
int index = streamObj->at("index").as_int64();
|
||||
std::string codecName = streamObj->at("codec_name").as_string();
|
||||
std::string comment;
|
||||
if (streamObj->contains("tags"))
|
||||
{
|
||||
auto &tagsObj = streamObj->at("tags");
|
||||
auto &tags = tagsObj.as_object();
|
||||
if (tags->contains("comment"))
|
||||
{
|
||||
comment = tags->at("comment").as_string();
|
||||
}
|
||||
}
|
||||
thumbnailStreams.push_back({index, codecName, comment});
|
||||
}
|
||||
if (thumbnailStreams.size() > 2)
|
||||
{
|
||||
cout << "File: " << entry.path() << endl;
|
||||
for (const auto &stream : thumbnailStreams)
|
||||
{
|
||||
cout << " Stream Index: " << stream.index
|
||||
<< ", Codec: " << stream.codec_name
|
||||
<< ", Comment: " << stream.comment << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
cout << "Error processing file: " << entry.path() << " - " << e.what() << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestTimer {
|
||||
public:
|
||||
using clock_t = std::chrono::high_resolution_clock;
|
||||
void Start()
|
||||
{
|
||||
startTime = clock_t::now();
|
||||
}
|
||||
uint64_t Stop()
|
||||
{
|
||||
auto elapsed = clock_t::now()-startTime;
|
||||
elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
|
||||
return elapsedMs;
|
||||
|
||||
}
|
||||
double ElapsedMs()
|
||||
{
|
||||
return elapsedMs;
|
||||
}
|
||||
private:
|
||||
clock_t::time_point startTime;
|
||||
uint64_t elapsedMs = 0;
|
||||
};
|
||||
|
||||
static bool isAudioExtension(const std::string &extension)
|
||||
{
|
||||
return MimeTypes::instance().AudioExtensions().contains(extension);
|
||||
}
|
||||
|
||||
|
||||
static bool ContainsMusic(const std::filesystem::path&path)
|
||||
{
|
||||
for (auto dirEntry: fs::directory_iterator(path)) {
|
||||
if (fs::is_regular_file(dirEntry.path())) {
|
||||
if (isAudioExtension(dirEntry.path().extension())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ProfileMusicDirectory() {
|
||||
const std::string homeDir = getenv("HOME");
|
||||
std::filesystem::path musicDir = std::filesystem::path(homeDir) / "Music";
|
||||
|
||||
for (const auto &entry : std::filesystem::recursive_directory_iterator(musicDir))
|
||||
{
|
||||
if (!entry.is_directory())
|
||||
{
|
||||
continue; // Skip directories
|
||||
}
|
||||
if (!ContainsMusic(entry.path())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::remove(textIndexPath);
|
||||
AudioDirectoryInfo::Ptr directoryInfo = AudioDirectoryInfo::Create(entry.path());
|
||||
fs::create_directories(textIndexPath.parent_path());
|
||||
directoryInfo->TestSetIndexPath(textIndexPath);
|
||||
|
||||
uint64_t firstTime;
|
||||
uint64_t dbIndexSize;
|
||||
uint64_t entries;
|
||||
{
|
||||
TestTimer t;
|
||||
t.Start();
|
||||
AudioDirectoryInfo::Ptr directoryInfo = AudioDirectoryInfo::Create(entry.path());
|
||||
directoryInfo->TestSetIndexPath(textIndexPath);
|
||||
entries = directoryInfo->GetFiles().size();
|
||||
firstTime = t.Stop();
|
||||
dbIndexSize = fs::file_size(textIndexPath);
|
||||
}
|
||||
uint64_t secondTime;
|
||||
{
|
||||
TestTimer t;
|
||||
t.Start();
|
||||
AudioDirectoryInfo::Ptr directoryInfo = AudioDirectoryInfo::Create(entry.path());
|
||||
directoryInfo->TestSetIndexPath(textIndexPath);
|
||||
directoryInfo->GetFiles();
|
||||
secondTime = t.Stop();
|
||||
}
|
||||
cout << entry.path().string() << endl;
|
||||
cout << " " << "first time: " << firstTime << " second time: " << secondTime << endl;
|
||||
cout << " " << "size: " << dbIndexSize << " entries: " << entries << endl;
|
||||
}
|
||||
|
||||
}
|
||||
TEST_CASE("Search for audio files with multple thumbnails", "[ScanThumbnails]")
|
||||
{
|
||||
AudioDirectoryInfo::SetTemporaryDirectory(tempDir);
|
||||
AudioDirectoryInfo::SetResourceDirectory(resourceDir);
|
||||
ScanThumbnails(); // scan all files in music directory.
|
||||
ProfileMusicDirectory(); // Scans all folders in music directory.
|
||||
|
||||
REQUIRE(true); // Just to ensure the test runs without failure
|
||||
}
|
||||
|
||||
TEST_CASE("Thumbnail performance test", "[ProfileThumbnails]" )
|
||||
{
|
||||
AudioDirectoryInfo::SetTemporaryDirectory(tempDir);
|
||||
AudioDirectoryInfo::SetResourceDirectory(resourceDir);
|
||||
ProfileMusicDirectory(); // Scans all folders in music directory.
|
||||
|
||||
REQUIRE(true); // Just to ensure the test runs without failure
|
||||
}
|
||||
+66
-16
@@ -21,6 +21,7 @@
|
||||
#include "util.hpp"
|
||||
#include <lv2/atom/atom.h>
|
||||
#include "SchedulerPriority.hpp"
|
||||
#include "AlsaSequencer.hpp"
|
||||
|
||||
#include "Lv2Log.hpp"
|
||||
|
||||
@@ -294,12 +295,14 @@ class SystemMidiBinding
|
||||
private:
|
||||
MidiBinding currentBinding;
|
||||
bool controlState = false;
|
||||
uint8_t lastControlValue = 0;
|
||||
|
||||
public:
|
||||
void SetBinding(const MidiBinding &binding)
|
||||
{
|
||||
currentBinding = binding;
|
||||
controlState = false;
|
||||
lastControlValue = 0;
|
||||
}
|
||||
|
||||
bool IsMatch(const MidiEvent &event);
|
||||
@@ -335,13 +338,18 @@ bool SystemMidiBinding::IsTriggered(const MidiEvent &event)
|
||||
return false;
|
||||
if (event.buffer[1] != currentBinding.control())
|
||||
return false;
|
||||
bool state = event.buffer[2] >= 0x64;
|
||||
if (state != this->controlState)
|
||||
|
||||
uint8_t value = event.buffer[2];
|
||||
bool result = false;
|
||||
if (currentBinding.switchControlType() == SwitchControlTypeT::TRIGGER_ON_RISING_EDGE)
|
||||
{
|
||||
this->controlState = state;
|
||||
return state;
|
||||
result = value >= 0x64 && lastControlValue < 0x64;
|
||||
} else if (currentBinding.switchControlType() == SwitchControlTypeT::TRIGGER_ON_ANY)
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
return false;
|
||||
lastControlValue = value;
|
||||
return result;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -381,6 +389,9 @@ bool SystemMidiBinding::IsMatch(const MidiEvent &event)
|
||||
class AudioHostImpl : public AudioHost, private AudioDriverHost, private IPatchWriterCallback
|
||||
{
|
||||
private:
|
||||
AlsaSequencer::ptr alsaSequencer;
|
||||
AlsaSequencerDeviceMonitor::ptr alsaDeviceMonitor;
|
||||
|
||||
void OnWritePatchPropertyBuffer(
|
||||
PatchPropertyWriter::Buffer *);
|
||||
|
||||
@@ -522,7 +533,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");
|
||||
@@ -918,11 +929,12 @@ private:
|
||||
if (event.size >= 3)
|
||||
{
|
||||
uint8_t cmd = (uint8_t)(event.buffer[0] & 0xF0);
|
||||
bool isNote = cmd == 0x90;
|
||||
bool isNote = cmd == 0x90 && event.buffer[2] != 0; // note on with velocity > 0.
|
||||
bool isControl = cmd == 0xB0;
|
||||
if (isNote || isControl)
|
||||
{
|
||||
realtimeWriter.OnMidiListen(isNote, event.buffer[1]);
|
||||
MidiNotifyBody notifyBody (event.buffer[0],event.buffer[1], event.buffer[2]);
|
||||
realtimeWriter.OnMidiListen(notifyBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1173,7 +1185,7 @@ private:
|
||||
|
||||
if (buffersValid)
|
||||
{
|
||||
pedalboard->ProcessParameterRequests(pParameterRequests);
|
||||
pedalboard->ProcessParameterRequests(pParameterRequests,nframes);
|
||||
|
||||
processed = pedalboard->Run(inputBuffers, outputBuffers, (uint32_t)nframes, &realtimeWriter);
|
||||
if (processed)
|
||||
@@ -1240,12 +1252,26 @@ public:
|
||||
lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap());
|
||||
|
||||
cpuTemperatureMonitor = CpuTemperatureMonitor::Get();
|
||||
this->alsaSequencer = AlsaSequencer::Create();
|
||||
this->alsaDeviceMonitor = AlsaSequencerDeviceMonitor::Create();
|
||||
|
||||
this->alsaDeviceMonitor->StartMonitoring(
|
||||
[this](
|
||||
AlsaSequencerDeviceMonitor::MonitorAction action,
|
||||
int client,
|
||||
const std::string &clientName)
|
||||
{
|
||||
HandleAlsaSequencerDevicesChanged(action, client, clientName);
|
||||
}
|
||||
);
|
||||
}
|
||||
virtual ~AudioHostImpl()
|
||||
{
|
||||
alsaDeviceMonitor->StopMonitoring();
|
||||
Close();
|
||||
CleanRestartThreads(true);
|
||||
audioDriver = nullptr;
|
||||
this->alsaSequencer = nullptr;
|
||||
}
|
||||
|
||||
virtual JackConfiguration GetServerConfiguration()
|
||||
@@ -1260,7 +1286,21 @@ public:
|
||||
{
|
||||
return this->sampleRate;
|
||||
}
|
||||
|
||||
void HandleAlsaSequencerDevicesChanged(
|
||||
AlsaSequencerDeviceMonitor::MonitorAction action, int client, const std::string &clientName)
|
||||
{
|
||||
if (pNotifyCallbacks)
|
||||
{
|
||||
if (action == AlsaSequencerDeviceMonitor::MonitorAction::DeviceRemoved)
|
||||
{
|
||||
pNotifyCallbacks->OnAlsaSequencerDeviceRemoved(client);
|
||||
}
|
||||
else if (action == AlsaSequencerDeviceMonitor::MonitorAction::DeviceAdded)
|
||||
{
|
||||
pNotifyCallbacks->OnAlsaSequencerDeviceAdded(client,clientName);
|
||||
}
|
||||
}
|
||||
}
|
||||
void HandleAudioTerminatedAbnormally()
|
||||
{
|
||||
Lv2Log::error("Audio processing terminated unexpectedly.");
|
||||
@@ -1333,11 +1373,11 @@ public:
|
||||
{
|
||||
if (command == RingBufferCommand::OnMidiListen)
|
||||
{
|
||||
uint16_t msg;
|
||||
hostReader.read(&msg);
|
||||
MidiNotifyBody body;
|
||||
hostReader.read(&body);
|
||||
if (this->pNotifyCallbacks)
|
||||
{
|
||||
pNotifyCallbacks->OnNotifyMidiListen((msg & 0xFF00) != 0, (uint8_t)msg);
|
||||
pNotifyCallbacks->OnNotifyMidiListen(body.cc0_, body.cc1_, body.cc2_);
|
||||
}
|
||||
}
|
||||
else if (command == RingBufferCommand::MidiValueChanged)
|
||||
@@ -1658,7 +1698,7 @@ public:
|
||||
try
|
||||
{
|
||||
audioDriver->Open(jackServerSettings, this->channelSelection);
|
||||
|
||||
audioDriver->SetAlsaSequencer(this->alsaSequencer);
|
||||
this->sampleRate = audioDriver->GetSampleRate();
|
||||
|
||||
this->overrunGracePeriodSamples = (uint64_t)(((uint64_t)this->sampleRate) * OVERRUN_GRACE_PERIOD_S);
|
||||
@@ -1806,6 +1846,9 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override;
|
||||
|
||||
|
||||
void OnNotifyPathPatchPropertyReceived(
|
||||
int64_t instanceId,
|
||||
const std::string &pathPatchPropertyUri,
|
||||
@@ -2080,8 +2123,8 @@ public:
|
||||
|
||||
return result;
|
||||
}
|
||||
volatile bool listenForMidiEvent = false;
|
||||
volatile bool listenForAtomOutput = false;
|
||||
std::atomic<bool> listenForMidiEvent = false;
|
||||
std::atomic<bool> listenForAtomOutput = false;
|
||||
|
||||
virtual void SetListenForMidiEvent(bool listen)
|
||||
{
|
||||
@@ -2253,6 +2296,13 @@ void AudioHostImpl::OnWritePatchPropertyBuffer(
|
||||
this->realtimeWriter.SendPathPropertyBuffer(buffer);
|
||||
}
|
||||
|
||||
void AudioHostImpl::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration)
|
||||
{
|
||||
this->alsaSequencer->SetConfiguration(alsaSequencerConfiguration);
|
||||
}
|
||||
|
||||
|
||||
|
||||
JSON_MAP_BEGIN(JackHostStatus)
|
||||
JSON_MAP_REFERENCE(JackHostStatus, active)
|
||||
JSON_MAP_REFERENCE(JackHostStatus, errorMessage)
|
||||
|
||||
+15
-5
@@ -39,6 +39,7 @@ namespace pipedal
|
||||
struct RealtimeNextMidiProgramRequest;
|
||||
class PluginHost;
|
||||
class Pedalboard;
|
||||
class AlsaSequencerConfiguration;
|
||||
|
||||
using PortMonitorCallback = std::function<void(int64_t handle, float value)>;
|
||||
|
||||
@@ -69,6 +70,7 @@ namespace pipedal
|
||||
|
||||
const char *errorMessage = nullptr;
|
||||
std::string jsonResponse;
|
||||
int64_t sampleTimeout = 0;
|
||||
|
||||
RealtimePatchPropertyRequest *pNext = nullptr;
|
||||
|
||||
@@ -102,13 +104,16 @@ namespace pipedal
|
||||
int64_t instanceId_,
|
||||
LV2_URID uridUri_,
|
||||
std::function<void(const std::string &jsonResjult)> onSuccess_,
|
||||
std::function<void(const std::string &error)> onError_)
|
||||
std::function<void(const std::string &error)> onError_,
|
||||
size_t sampleTimeout)
|
||||
: onPatchRequestComplete(onPatchRequestcomplete_),
|
||||
clientId(clientId_),
|
||||
instanceId(instanceId_),
|
||||
uridUri(uridUri_),
|
||||
onSuccess(onSuccess_),
|
||||
onError(onError_)
|
||||
onError(onError_),
|
||||
sampleTimeout((int64_t)sampleTimeout)
|
||||
|
||||
{
|
||||
requestType = RequestType::PatchGet;
|
||||
}
|
||||
@@ -119,13 +124,15 @@ namespace pipedal
|
||||
LV2_URID uridUri_,
|
||||
LV2_Atom *atomValue,
|
||||
std::function<void(const std::string &jsonResjult)> onSuccess_,
|
||||
std::function<void(const std::string &error)> onError_)
|
||||
std::function<void(const std::string &error)> onError_,
|
||||
size_t sampleTimeout)
|
||||
: onPatchRequestComplete(onPatchRequestcomplete_),
|
||||
clientId(clientId_),
|
||||
instanceId(instanceId_),
|
||||
uridUri(uridUri_),
|
||||
onSuccess(onSuccess_),
|
||||
onError(onError_)
|
||||
onError(onError_),
|
||||
sampleTimeout(sampleTimeout)
|
||||
{
|
||||
requestType = RequestType::PatchSet;
|
||||
size_t size = atomValue->size + sizeof(LV2_Atom);
|
||||
@@ -152,7 +159,7 @@ namespace pipedal
|
||||
virtual void OnNotifyVusSubscription(const std::vector<VuUpdate> &updates) = 0;
|
||||
virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) = 0;
|
||||
virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) = 0;
|
||||
virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) = 0;
|
||||
virtual void OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) = 0;
|
||||
|
||||
virtual void OnNotifyPathPatchPropertyReceived(
|
||||
int64_t instanceId,
|
||||
@@ -172,6 +179,8 @@ namespace pipedal
|
||||
virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) = 0;
|
||||
|
||||
virtual void OnAlsaDriverTerminatedAbnormally() = 0;
|
||||
virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) = 0;
|
||||
virtual void OnAlsaSequencerDeviceRemoved(int client) = 0;
|
||||
};
|
||||
|
||||
class JackHostStatus
|
||||
@@ -219,6 +228,7 @@ namespace pipedal
|
||||
virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) = 0;
|
||||
virtual void Close() = 0;
|
||||
|
||||
virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
|
||||
virtual uint32_t GetSampleRate() = 0;
|
||||
|
||||
virtual JackConfiguration GetServerConfiguration() = 0;
|
||||
|
||||
+44
-11
@@ -9,8 +9,19 @@ 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)
|
||||
|
||||
@@ -46,6 +57,8 @@ include(FindPkgConfig)
|
||||
find_package(sdbus-c++ REQUIRED)
|
||||
|
||||
|
||||
|
||||
|
||||
find_package(ICU REQUIRED COMPONENTS uc i18n )
|
||||
message(STATUS "ICU_LIBRARIES: ${ICU_LIBRARIES}")
|
||||
|
||||
@@ -149,7 +162,8 @@ add_compile_definitions(DEBIAN_ARCHITECTURE="${DEBIAN_ARCHITECTURE}")
|
||||
if (CMAKE_BUILD_TYPE MATCHES Debug)
|
||||
message(STATUS "Debug build")
|
||||
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g -DDEBUG -D_GLIBCXX_DEBUG" )
|
||||
# Must not -D_GLIBCXX_DEBUG, since it conflict with SQLiteCpp.lol
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g -DDEBUG " )
|
||||
if (USE_SANITIZE)
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -static-libasan " )
|
||||
endif()
|
||||
@@ -192,6 +206,12 @@ else()
|
||||
endif()
|
||||
|
||||
set (PIPEDAL_SOURCES
|
||||
PipewireInputStream.cpp PipewireInputStream.hpp
|
||||
AudioFiles.cpp AudioFiles.hpp
|
||||
AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp
|
||||
AudioFileMetadata.hpp AudioFileMetadata.cpp
|
||||
AudioFilesDb.hpp AudioFilesDb.cpp
|
||||
LRUCache.hpp
|
||||
CpuTemperatureMonitor.cpp CpuTemperatureMonitor.hpp
|
||||
SchedulerPriority.hpp SchedulerPriority.cpp
|
||||
ModFileTypes.cpp ModFileTypes.hpp
|
||||
@@ -208,7 +228,6 @@ set (PIPEDAL_SOURCES
|
||||
Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp
|
||||
WebServerConfig.cpp WebServerConfig.hpp
|
||||
Locale.hpp Locale.cpp
|
||||
Finally.hpp
|
||||
ZipFile.cpp ZipFile.hpp
|
||||
TemporaryFile.cpp TemporaryFile.hpp
|
||||
FilePropertyDirectoryTree.cpp FilePropertyDirectoryTree.hpp
|
||||
@@ -266,6 +285,7 @@ set (PIPEDAL_SOURCES
|
||||
LogFeature.hpp LogFeature.cpp
|
||||
Worker.hpp Worker.cpp
|
||||
OptionsFeature.hpp OptionsFeature.cpp
|
||||
FileMetadataFeature.hpp FileMetadataFeature.cpp
|
||||
VuUpdate.hpp VuUpdate.cpp
|
||||
Units.hpp Units.cpp
|
||||
RingBuffer.hpp
|
||||
@@ -304,6 +324,7 @@ set (PIPEDAL_INCLUDES
|
||||
${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS}
|
||||
${VST3_INCLUDES}
|
||||
${WEBSOCKETPP_INCLUDE_DIRS}
|
||||
${PipeWire_INCLUDE_DIRS}
|
||||
.
|
||||
)
|
||||
|
||||
@@ -313,8 +334,8 @@ set(PIPEDAL_LIBS libpipedald zip
|
||||
pthread atomic stdc++fs asound avahi-common avahi-client systemd
|
||||
${VST3_LIBRARIES}
|
||||
${LILV_0_LIBRARIES}
|
||||
asound
|
||||
# ${JACK_LIBRARIES} - pending delete for JACK support.
|
||||
|
||||
)
|
||||
|
||||
|
||||
@@ -323,10 +344,17 @@ set(PIPEDAL_LIBS libpipedald zip
|
||||
|
||||
add_library(libpipedald STATIC ${PIPEDAL_SOURCES})
|
||||
|
||||
target_include_directories(libpipedald PRIVATE ${PIPEDAL_INCLUDES}
|
||||
target_compile_definitions(libpipedald PUBLIC "_REENTRANT")
|
||||
|
||||
target_include_directories(libpipedald PUBLIC ${PIPEDAL_INCLUDES}
|
||||
)
|
||||
|
||||
target_link_libraries(libpipedald PUBLIC PiPedalCommon)
|
||||
target_link_libraries(libpipedald
|
||||
PUBLIC
|
||||
PiPedalCommon
|
||||
SQLiteCpp
|
||||
${PipeWire_LIBRARIES}
|
||||
)
|
||||
|
||||
if(${USE_PCH})
|
||||
target_precompile_headers(libpipedald PRIVATE pch.h)
|
||||
@@ -348,8 +376,9 @@ target_link_libraries(pipedal_kconfig
|
||||
PRIVATE ftxui::dom
|
||||
PRIVATE ftxui::component # Not needed for this example.
|
||||
PRIVATE PiPedalCommon
|
||||
asound
|
||||
systemd
|
||||
)
|
||||
)
|
||||
|
||||
#################################
|
||||
add_executable(pipedald
|
||||
@@ -366,6 +395,7 @@ target_link_libraries(pipedald PRIVATE PiPedalCommon
|
||||
${PIPEDAL_LIBS}
|
||||
)
|
||||
|
||||
|
||||
#################################
|
||||
add_executable(hotspotManagerTest
|
||||
hotspotManagerTestMain.cpp
|
||||
@@ -373,7 +403,7 @@ add_executable(hotspotManagerTest
|
||||
|
||||
target_link_libraries(hotspotManagerTest PRIVATE ${PIPEDAL_LIBS})
|
||||
|
||||
set_target_properties(hotspotManagerTest PROPERTIES EXCLUDE_FROM_ALL true)
|
||||
set_target_properties(hotspotManagerTest PROPERTIES EXCLUDE_FROM_ALL ${PIPEDAL_EXCLUDE_TESTS})
|
||||
|
||||
|
||||
add_executable(AuxInTest
|
||||
@@ -383,8 +413,11 @@ add_executable(AuxInTest
|
||||
)
|
||||
|
||||
add_executable(pipedaltest
|
||||
testMain.cpp
|
||||
testMain.cpp
|
||||
PipewireInputStreamTest.cpp
|
||||
|
||||
AudioFilesTest.cpp
|
||||
LRUCacheTest.cpp
|
||||
ModFileTypesTest.cpp
|
||||
jsonTest.cpp
|
||||
UpdaterTest.cpp
|
||||
@@ -412,7 +445,7 @@ target_link_libraries(pipedaltest PRIVATE ${PIPEDAL_LIBS} ${ICU_LIBRARIES})
|
||||
target_include_directories(pipedaltest PRIVATE ${PIPEDAL_INCLUDES}
|
||||
)
|
||||
|
||||
set_target_properties(pipedaltest PROPERTIES EXCLUDE_FROM_ALL true)
|
||||
set_target_properties(pipedaltest PROPERTIES EXCLUDE_FROM_ALL ${PIPEDAL_EXCLUDE_TESTS})
|
||||
|
||||
if (NOT GITHUB_ACTIONS) # Google perftools are not available on Github action servers.
|
||||
|
||||
@@ -448,7 +481,7 @@ target_link_libraries(jsonTest PRIVATE PiPedalCommon)
|
||||
target_include_directories(jsonTest PRIVATE ${PIPEDAL_INCLUDES}
|
||||
)
|
||||
|
||||
set_target_properties(jsonTest PROPERTIES EXCLUDE_FROM_ALL true)
|
||||
set_target_properties(jsonTest PROPERTIES EXCLUDE_FROM_ALL ${PIPEDAL_EXCLUDE_TESTS})
|
||||
|
||||
|
||||
target_link_libraries(jsonTest PRIVATE pthread)
|
||||
@@ -822,7 +855,7 @@ add_custom_command(OUTPUT ${REACT_NOTICES_FILE}
|
||||
--output ${REACT_NOTICES_FILE}
|
||||
--projectCopyright ${DEBIAN_COPYRIGHT_FILE}
|
||||
liblilv-0-0 ${BOOST_COPYRIGHT_DIR} lv2-dev libsdbus-c++-dev librsvg2-2 libpango-1.0-0 libx11-6 libxrandr2
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
DEPENDS ${DEBIAN_COPYRIGHT_FILE} "$<TARGET_FILE:processcopyrights>"
|
||||
COMMENT "Updating copyright notices."
|
||||
)
|
||||
|
||||
@@ -104,6 +104,11 @@ namespace pipedal
|
||||
captureChannels = channels;
|
||||
playbackChannels = channels;
|
||||
|
||||
midiEventMemoryIndex = 0;
|
||||
midiEventMemory.resize(MIDI_MEMORY_BUFFER_SIZE);
|
||||
midiEvents.resize(MAX_MIDI_EVENT);
|
||||
|
||||
|
||||
}
|
||||
virtual ~DummyDriverImpl()
|
||||
{
|
||||
@@ -122,6 +127,16 @@ namespace pipedal
|
||||
}
|
||||
|
||||
JackServerSettings jackServerSettings;
|
||||
AlsaSequencer::ptr alsaSequencer;
|
||||
|
||||
static constexpr size_t MIDI_MEMORY_BUFFER_SIZE = 32 * 1024;
|
||||
static constexpr size_t MAX_MIDI_EVENT = 4 * 1024;
|
||||
|
||||
size_t midiEventCount = 0;
|
||||
std::vector<MidiEvent> midiEvents;
|
||||
size_t midiEventMemoryIndex = 0;
|
||||
std::vector<uint8_t> midiEventMemory;
|
||||
|
||||
|
||||
unsigned int periods = 0;
|
||||
|
||||
@@ -158,11 +173,13 @@ namespace pipedal
|
||||
}
|
||||
}
|
||||
|
||||
virtual size_t GetMidiInputEventCount() override {
|
||||
return 0;
|
||||
virtual size_t GetMidiInputEventCount() override
|
||||
{
|
||||
return midiEventCount;
|
||||
}
|
||||
virtual MidiEvent*GetMidiEvents() {
|
||||
return nullptr;
|
||||
virtual MidiEvent *GetMidiEvents() override
|
||||
{
|
||||
return this->midiEvents.data();
|
||||
}
|
||||
|
||||
|
||||
@@ -190,7 +207,11 @@ namespace pipedal
|
||||
throw;
|
||||
}
|
||||
}
|
||||
virtual std::string GetConfigurationDescription()
|
||||
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override
|
||||
{
|
||||
this->alsaSequencer = alsaSequencer;
|
||||
}
|
||||
virtual std::string GetConfigurationDescription()
|
||||
{
|
||||
std::string result = SS(
|
||||
"DUMMY, "
|
||||
@@ -219,6 +240,43 @@ namespace pipedal
|
||||
|
||||
bool block = false;
|
||||
|
||||
void ReadMidiData(uint32_t audioFrame)
|
||||
{
|
||||
AlsaMidiMessage message;
|
||||
|
||||
midiEventCount = 0;
|
||||
while(alsaSequencer->ReadMessage(message,0))
|
||||
{
|
||||
size_t messageSize = message.size;
|
||||
if (messageSize == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (midiEventMemoryIndex + messageSize >= this->midiEventMemory.size()) {
|
||||
continue;
|
||||
}
|
||||
if (midiEventCount >= this->midiEvents.size()) {
|
||||
midiEvents.resize(midiEventCount*2);
|
||||
}
|
||||
// for now, prevent META event messages from propagating.
|
||||
if (message.data[0] == 0xFF && message.size > 1) {
|
||||
continue;
|
||||
}
|
||||
MidiEvent *pEvent = midiEvents.data() + midiEventCount++;
|
||||
pEvent->time = audioFrame;
|
||||
pEvent->size = messageSize;
|
||||
pEvent->buffer = midiEventMemory.data() + midiEventMemoryIndex;
|
||||
|
||||
memcpy(
|
||||
midiEventMemory.data() + midiEventMemoryIndex,
|
||||
message.data,
|
||||
message.size);
|
||||
midiEventMemoryIndex += messageSize;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void AudioThread()
|
||||
{
|
||||
SetThreadName("dummyAudioDriver");
|
||||
@@ -231,11 +289,14 @@ namespace pipedal
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
||||
if (terminateAudio())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ReadMidiData((uint32_t)0);
|
||||
|
||||
ssize_t framesRead = this->bufferSize;
|
||||
this->driverHost->OnProcess(framesRead);
|
||||
|
||||
@@ -335,8 +396,6 @@ namespace pipedal
|
||||
Lv2Log::debug("Audio thread joined.");
|
||||
}
|
||||
|
||||
static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024;
|
||||
static constexpr size_t MAX_MIDI_EVENT = 4 * 1024;
|
||||
|
||||
public:
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ JSON_MAP_BEGIN(FileEntry)
|
||||
JSON_MAP_REFERENCE(FileEntry,displayName)
|
||||
JSON_MAP_REFERENCE(FileEntry,isProtected)
|
||||
JSON_MAP_REFERENCE(FileEntry,isDirectory)
|
||||
JSON_MAP_REFERENCE(FileEntry,metadata)
|
||||
JSON_MAP_END()
|
||||
|
||||
JSON_MAP_BEGIN(BreadcrumbEntry)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "json.hpp"
|
||||
#include "AudioFileMetadata.hpp"
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
@@ -30,11 +31,19 @@ namespace pipedal {
|
||||
:pathname_(pathname), displayName_(displayName), isDirectory_(isDirectory),isProtected_(isProtected)
|
||||
{
|
||||
|
||||
}
|
||||
FileEntry(const std::string&pathname,const std::string &displayName, bool isProtected,
|
||||
std::shared_ptr<AudioFileMetadata> metadata
|
||||
)
|
||||
:pathname_(pathname), displayName_(displayName), isDirectory_(false),isProtected_(isProtected),metadata_(metadata)
|
||||
{
|
||||
|
||||
}
|
||||
std::string pathname_;
|
||||
std::string displayName_;
|
||||
bool isDirectory_ = false;
|
||||
bool isProtected_ = false;
|
||||
std::shared_ptr<AudioFileMetadata> metadata_;
|
||||
|
||||
DECLARE_JSON_MAP(FileEntry);
|
||||
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#include "pch.h"
|
||||
#include "FileMetadataFeature.hpp"
|
||||
#include "AudioFiles.hpp"
|
||||
#include "Lv2Log.hpp"
|
||||
#include "ss.hpp"
|
||||
#include "util.hpp"
|
||||
#include "json.hpp"
|
||||
#include "json_variant.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
#include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h"
|
||||
using namespace pipedal;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
FileMetadataFeature::FileMetadataFeature()
|
||||
{
|
||||
feature.URI = PIPEDAL__FILE_METADATA_FEATURE;
|
||||
feature.data = &interface;
|
||||
interface.handle = (void *)this;
|
||||
interface.setFileMetadata = &FileMetadataFeature::S_setFileMetadata;
|
||||
interface.getFileMetadata = &FileMetadataFeature::S_getFileMetadata;
|
||||
interface.deleteFileMetadata = &FileMetadataFeature::S_deleteFileMetadata;
|
||||
}
|
||||
|
||||
FileMetadataFeature::~FileMetadataFeature()
|
||||
{
|
||||
}
|
||||
|
||||
void FileMetadataFeature::SetPluginStoragePath(const std::filesystem::path &storagePath)
|
||||
{
|
||||
this->tracksPath = storagePath / "shared" / "audio" / "Tracks";
|
||||
}
|
||||
|
||||
// Check if the path is within the tracks directory
|
||||
bool FileMetadataFeature::IsValidPath(const std::filesystem::path &path) const
|
||||
{
|
||||
if (tracksPath.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (HasDotDot(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// is a subdirectory of track path
|
||||
// check for directory seperator
|
||||
if (!IsSubdirectory(path, tracksPath))
|
||||
{
|
||||
return false; // Valid subdirectory
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void FileMetadataFeature::Prepare(MapFeature &map)
|
||||
{
|
||||
this->mapFeature = ↦
|
||||
}
|
||||
|
||||
PIPEDAL_FileMetadata_Status FileMetadataFeature::setFileMetadata(
|
||||
const char *absolute_path,
|
||||
const char *key,
|
||||
const char *value)
|
||||
{
|
||||
if (!IsValidPath(absolute_path))
|
||||
{
|
||||
return PIPEDAL_FILE_METADATA_INVALID_PATH; // Cannot set metadata for files in the tracks directory
|
||||
}
|
||||
|
||||
std::filesystem::path path{SS(absolute_path << ".mdata")};
|
||||
|
||||
json_variant jsonData;
|
||||
if (fs::exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream f(path);
|
||||
json_reader reader(f);
|
||||
reader.read(&jsonData);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
jsonData = json_variant::make_object();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
jsonData = json_variant::make_object();
|
||||
}
|
||||
|
||||
(*jsonData.as_object())[key] = json_variant(value);
|
||||
|
||||
{
|
||||
std::ofstream f(path);
|
||||
if (!f.is_open())
|
||||
{
|
||||
Lv2Log::error(SS("Failed to write metadata file " << path));
|
||||
return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata
|
||||
}
|
||||
json_writer writer(f);
|
||||
writer.write(jsonData);
|
||||
}
|
||||
Lv2Log::debug(SS("Set file metadata for " << absolute_path << " key: " << key << " value: " << value));
|
||||
return PIPEDAL_FILE_METADATA_SUCCESS;
|
||||
}
|
||||
|
||||
uint32_t FileMetadataFeature::getFileMetadata(
|
||||
const char *absolute_path,
|
||||
const char *key,
|
||||
char *fileMetadata,
|
||||
uint32_t fileMetadataSize)
|
||||
{
|
||||
if (fileMetadata)
|
||||
fileMetadata[0] = '\0'; // Ensure the buffer is null-terminated
|
||||
|
||||
if (!IsValidPath(absolute_path))
|
||||
{
|
||||
return 0; // Cannot set metadata for files in the tracks directory
|
||||
}
|
||||
|
||||
std::filesystem::path path{SS(absolute_path << ".mdata")};
|
||||
|
||||
json_variant jsonData;
|
||||
std::string result;
|
||||
|
||||
try
|
||||
{
|
||||
if (fs::exists(path))
|
||||
{
|
||||
std::ifstream f(path);
|
||||
json_reader reader(f);
|
||||
reader.read(&jsonData);
|
||||
result = jsonData.as_object()->at(std::string(key)).as_string();
|
||||
}
|
||||
else
|
||||
{
|
||||
Lv2Log::debug(SS("No metadata file found for " << absolute_path << " key: " << key));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::debug(SS("Permission denied for " << absolute_path << " key: " << key));
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t len = result.length() + 1; // +1 for null terminator
|
||||
if (len > fileMetadataSize || fileMetadata == nullptr)
|
||||
{
|
||||
// Not enough space in the buffer, return the size needed.
|
||||
return len;
|
||||
}
|
||||
memcpy(fileMetadata, result.c_str(), len);
|
||||
Lv2Log::debug(SS("Get file metadata for " << absolute_path << " key: " << key << " value: " << result));
|
||||
return len;
|
||||
}
|
||||
PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata(
|
||||
const char *absolute_path,
|
||||
const char *key)
|
||||
{
|
||||
if (!IsValidPath(absolute_path))
|
||||
{
|
||||
return PIPEDAL_FILE_METADATA_INVALID_PATH; // Cannot set metadata for files in the tracks directory
|
||||
}
|
||||
Lv2Log::debug(SS("Delete file metadata for " << absolute_path << " key: " << key));
|
||||
|
||||
try {
|
||||
std::filesystem::path path{SS(absolute_path << ".mdata")};
|
||||
|
||||
if (!fs::exists(path))
|
||||
{
|
||||
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Metadata file does not exist
|
||||
}
|
||||
|
||||
json_variant jsonData;
|
||||
try
|
||||
{
|
||||
std::ifstream f(path);
|
||||
json_reader reader(f);
|
||||
reader.read(&jsonData);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Failed to read existing metadata
|
||||
}
|
||||
|
||||
auto obj = jsonData.as_object();
|
||||
auto it = obj->find(std::string(key));
|
||||
if (it == obj->end())
|
||||
{
|
||||
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Key not found
|
||||
}
|
||||
|
||||
obj->erase(it);
|
||||
|
||||
if (obj->begin() == obj->end())
|
||||
{
|
||||
// If the object is empty, delete the metadata file
|
||||
fs::remove(path);
|
||||
return PIPEDAL_FILE_METADATA_SUCCESS; // Metadata deleted successfully
|
||||
}
|
||||
|
||||
std::ofstream f(path);
|
||||
if (!f.is_open())
|
||||
{
|
||||
Lv2Log::error(SS("Failed to write metadata file " << path));
|
||||
return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata
|
||||
}
|
||||
|
||||
json_writer writer(f);
|
||||
writer.write(jsonData);
|
||||
|
||||
return PIPEDAL_FILE_METADATA_SUCCESS;
|
||||
} catch (const std::exception &e) {
|
||||
Lv2Log::error(SS("Exception while deleting metadata: " << e.what()));
|
||||
return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata
|
||||
}
|
||||
}
|
||||
|
||||
PIPEDAL_FileMetadata_Status FileMetadataFeature::S_setFileMetadata(
|
||||
PIPEDAL_FILE_METADATA_Handle handle,
|
||||
const char *absolute_path,
|
||||
const char *key,
|
||||
const char *fileMetadata)
|
||||
{
|
||||
return ((FileMetadataFeature *)handle)->setFileMetadata(absolute_path, key, fileMetadata);
|
||||
}
|
||||
uint32_t FileMetadataFeature::S_getFileMetadata(
|
||||
PIPEDAL_FILE_METADATA_Handle handle,
|
||||
const char *absolute_path,
|
||||
const char *key,
|
||||
char *buffer,
|
||||
uint32_t bufferSize)
|
||||
{
|
||||
return ((FileMetadataFeature *)handle)->getFileMetadata(absolute_path, key, buffer, bufferSize);
|
||||
}
|
||||
|
||||
PIPEDAL_FileMetadata_Status FileMetadataFeature::S_deleteFileMetadata(
|
||||
PIPEDAL_FILE_METADATA_Handle handle,
|
||||
const char *absolute_path,
|
||||
const char *key)
|
||||
{
|
||||
return ((FileMetadataFeature *)handle)->deleteFileMetadata(absolute_path, key);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2022 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 "MapFeature.hpp"
|
||||
#include "PiPedalUI.hpp"
|
||||
#include "lv2ext/pipedal.lv2/ext/FileMetadataFeature.h"
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
|
||||
class FileMetadataFeature
|
||||
{
|
||||
|
||||
private:
|
||||
LV2_Feature feature;
|
||||
PIPEDAL_FileMetadata_Interface interface;
|
||||
static PIPEDAL_FileMetadata_Status S_setFileMetadata(
|
||||
PIPEDAL_FILE_METADATA_Handle handle,
|
||||
const char *absolute_path,
|
||||
const char*key,
|
||||
const char *fileMetadata);
|
||||
|
||||
PIPEDAL_FileMetadata_Status setFileMetadata(
|
||||
const char *absolute_path,
|
||||
const char*key,
|
||||
const char *fileMetadata);
|
||||
|
||||
static uint32_t S_getFileMetadata(
|
||||
PIPEDAL_FILE_METADATA_Handle handle,
|
||||
const char *filePath,
|
||||
const char*key,
|
||||
char *buffer,
|
||||
uint32_t bufferSize);
|
||||
|
||||
uint32_t getFileMetadata(
|
||||
const char *absolute_path,
|
||||
const char*key,
|
||||
char *fileMetadata,
|
||||
uint32_t fileMetadataSize);
|
||||
|
||||
static PIPEDAL_FileMetadata_Status S_deleteFileMetadata(
|
||||
PIPEDAL_FILE_METADATA_Handle handle,
|
||||
const char *absolute_path,
|
||||
const char*key);
|
||||
|
||||
PIPEDAL_FileMetadata_Status deleteFileMetadata(
|
||||
const char *absolute_path,
|
||||
const char*key);
|
||||
|
||||
|
||||
bool IsValidPath(const std::filesystem::path &path) const;
|
||||
public:
|
||||
FileMetadataFeature();
|
||||
void Prepare(MapFeature &map);
|
||||
void SetPluginStoragePath(const std::filesystem::path &storagePath);
|
||||
~FileMetadataFeature();
|
||||
|
||||
public:
|
||||
std::filesystem::path tracksPath;
|
||||
MapFeature *mapFeature = nullptr;
|
||||
const LV2_Feature *GetFeature()
|
||||
{
|
||||
return &feature;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
+38
-19
@@ -80,7 +80,7 @@ namespace pipedal::impl
|
||||
bool hasWifi = false;
|
||||
HasWifiListener hasWifiListener;
|
||||
void SetHasWifi(bool hasWifi);
|
||||
virtual bool GetHasWifi() override ;
|
||||
virtual bool GetHasWifi() override;
|
||||
|
||||
void onClose();
|
||||
void onError(const std::string &message);
|
||||
@@ -347,7 +347,7 @@ void HotspotManagerImpl::onStartMonitoring()
|
||||
|
||||
this->networkManager = NetworkManager::Create(dbusDispatcher);
|
||||
|
||||
if (!networkManager->WirelessEnabled())
|
||||
if (!networkManager->WirelessEnabled() && wifiConfigSettings.NeedsWifi())
|
||||
{
|
||||
networkManager->WirelessEnabled(true);
|
||||
}
|
||||
@@ -483,6 +483,7 @@ void HotspotManagerImpl::onReload()
|
||||
// force a reload.
|
||||
StopHotspot();
|
||||
SetState(State::Monitoring);
|
||||
StartScanTimer(); // or stop it as the case may be.
|
||||
MaybeStartHotspot();
|
||||
return;
|
||||
}
|
||||
@@ -758,16 +759,26 @@ void HotspotManagerImpl::MaybeStartHotspot()
|
||||
// devices are transitioning. Do nothing.
|
||||
return;
|
||||
}
|
||||
std::vector<AccessPoint::ptr> allAccessPoints = GetAllAccessPoints();
|
||||
std::vector<std::vector<uint8_t>> allAccessPointSsids = GetAccessPointSsids(allAccessPoints);
|
||||
std::vector<std::vector<uint8_t>> connectableSsids = GetKnownVisibleAccessPoints(allAccessPointSsids); // all the ssids currently visible for which we have credentials.
|
||||
|
||||
this->UpdateKnownNetworks(connectableSsids, allAccessPoints);
|
||||
bool wantsHotspot;
|
||||
|
||||
bool wantsHotspot = this->wifiConfigSettings.WantsHotspot(
|
||||
if (wifiConfigSettings.NeedsScan()) {
|
||||
|
||||
this->ethernetConnected, connectableSsids, allAccessPointSsids);
|
||||
std::vector<AccessPoint::ptr> allAccessPoints = GetAllAccessPoints();
|
||||
std::vector<std::vector<uint8_t>> allAccessPointSsids = GetAccessPointSsids(allAccessPoints);
|
||||
std::vector<std::vector<uint8_t>> connectableSsids = GetKnownVisibleAccessPoints(allAccessPointSsids); // all the ssids currently visible for which we have credentials.
|
||||
|
||||
this->UpdateKnownNetworks(connectableSsids, allAccessPoints);
|
||||
|
||||
wantsHotspot = this->wifiConfigSettings.WantsHotspot(
|
||||
this->ethernetConnected, connectableSsids, allAccessPointSsids);
|
||||
|
||||
|
||||
} else {
|
||||
wantsHotspot = this->wifiConfigSettings.WantsHotspot(
|
||||
this->ethernetConnected);
|
||||
}
|
||||
|
||||
if (this->state == State::Monitoring && wantsHotspot)
|
||||
{
|
||||
StartHotspot();
|
||||
@@ -996,8 +1007,14 @@ void HotspotManagerImpl::ScanNow()
|
||||
|
||||
try
|
||||
{
|
||||
Lv2Log::debug("Scanning");
|
||||
wlanWirelessDevice->RequestScan(options);
|
||||
if (this->wifiConfigSettings.NeedsScan())
|
||||
{
|
||||
|
||||
Lv2Log::debug("Scanning");
|
||||
wlanWirelessDevice->RequestScan(options);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
@@ -1036,7 +1053,8 @@ void HotspotManagerImpl::SetHasWifiListener(HasWifiListener &&listener)
|
||||
this->hasWifiListener(this->hasWifi);
|
||||
}
|
||||
}
|
||||
bool HotspotManagerImpl::GetHasWifi() {
|
||||
bool HotspotManagerImpl::GetHasWifi()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock{this->networkChangingListenerMutex};
|
||||
return hasWifi;
|
||||
}
|
||||
@@ -1112,17 +1130,20 @@ static bool IsNetworkManagerRunning()
|
||||
return gUsingNetworkManager;
|
||||
}
|
||||
bool bResult = false;
|
||||
auto result = sysExecForOutput("systemctl","is-active NetworkManager");
|
||||
auto result = sysExecForOutput("systemctl", "is-active NetworkManager");
|
||||
if (result.exitCode == EXIT_SUCCESS)
|
||||
{
|
||||
std::string text = result.output.erase(result.output.find_last_not_of(" \n\r\t")+1);
|
||||
std::string text = result.output.erase(result.output.find_last_not_of(" \n\r\t") + 1);
|
||||
if (text == "active")
|
||||
{
|
||||
bResult = true;
|
||||
} else if (text == "inactive")
|
||||
}
|
||||
else if (text == "inactive")
|
||||
{
|
||||
bResult = false;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error(SS("UsingNetworkManager: unexpected result (" << text << ")"));
|
||||
}
|
||||
gNetworkManagerTestExecuted = true;
|
||||
@@ -1134,19 +1155,17 @@ static bool IsNetworkManagerRunning()
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool HotspotManager::HasWifiDevice()
|
||||
{
|
||||
// use procfs to decide this, as NetworkManager may not be available yet.
|
||||
if ( !(get_wireless_interfaces_sysfs().empty()) )
|
||||
if (!(get_wireless_interfaces_sysfs().empty()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsNetworkManagerRunning())
|
||||
if (!IsNetworkManagerRunning())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "AlsaDriver.hpp"
|
||||
#include "Lv2Log.hpp"
|
||||
#include "PiPedalException.hpp"
|
||||
#include "AlsaSequencer.hpp"
|
||||
|
||||
|
||||
#if JACK_HOST
|
||||
@@ -106,6 +107,23 @@ namespace pipedal {
|
||||
#endif
|
||||
}
|
||||
|
||||
static std::vector<AlsaMidiDeviceInfo> GetAlsaSequencers() {
|
||||
std::vector<AlsaMidiDeviceInfo> result;
|
||||
try {
|
||||
auto sequencer = AlsaSequencer::Create();
|
||||
if (sequencer)
|
||||
{
|
||||
std::vector<AlsaSequencerPort> ports = sequencer->EnumeratePorts();
|
||||
for (const auto &port : ports)
|
||||
{
|
||||
result.push_back(AlsaMidiDeviceInfo(port.id.c_str(), port.name.c_str()));
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
Lv2Log::error("Failed to enumerate ALSA sequencers: %s", e.what());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
void JackConfiguration::AlsaInitialize(
|
||||
const JackServerSettings &jackServerSettings)
|
||||
{
|
||||
@@ -116,7 +134,7 @@ void JackConfiguration::AlsaInitialize(
|
||||
{
|
||||
this->inputMidiDevices_.clear();
|
||||
} else {
|
||||
this->inputMidiDevices_ = GetAlsaMidiInputDevices();
|
||||
this->inputMidiDevices_ = GetAlsaSequencers(); // NB: Sequencers, not rawmidi devices, anymore.
|
||||
}
|
||||
if (jackServerSettings.IsValid())
|
||||
{
|
||||
|
||||
@@ -107,7 +107,13 @@ namespace pipedal
|
||||
return outputAudioPorts_;
|
||||
}
|
||||
|
||||
const std::vector<AlsaMidiDeviceInfo>& GetInputMidiDevices() const
|
||||
// replaced with AlsaSequencerConfiguration
|
||||
const std::vector<AlsaMidiDeviceInfo>& LegacyGetInputMidiDevices() const
|
||||
{
|
||||
return inputMidiDevices_;
|
||||
}
|
||||
// replaced with AlsaSequencerConfiguration
|
||||
std::vector<AlsaMidiDeviceInfo>& LegacyGetInputMidiDevices()
|
||||
{
|
||||
return inputMidiDevices_;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <unordered_map>
|
||||
#include <list>
|
||||
#include <stdexcept>
|
||||
|
||||
template <typename KEY, typename VALUE>
|
||||
class LRUCache {
|
||||
public:
|
||||
struct CacheNode {
|
||||
KEY key;
|
||||
VALUE value;
|
||||
CacheNode(KEY k, VALUE v) : key(k), value(v) {}
|
||||
};
|
||||
private:
|
||||
size_t capacity;
|
||||
std::list<CacheNode> cache_list; // Doubly-linked list for LRU order
|
||||
std::unordered_map<KEY, typename std::list<CacheNode>::iterator> cache_map; // Map key to list iterator
|
||||
|
||||
public:
|
||||
explicit LRUCache(size_t cap) : capacity(cap) {
|
||||
if (cap == 0) {
|
||||
throw std::invalid_argument("Cache capacity must be greater than 0");
|
||||
}
|
||||
}
|
||||
|
||||
// Get value by key, return false if not found
|
||||
bool get(const KEY& key, VALUE& value) {
|
||||
auto it = cache_map.find(key);
|
||||
if (it == cache_map.end()) {
|
||||
return false;
|
||||
}
|
||||
// Move to front (most recently used)
|
||||
cache_list.splice(cache_list.begin(), cache_list, it->second);
|
||||
value = it->second->value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Put key-value pair into cache
|
||||
void put(const KEY& key, const VALUE& value) {
|
||||
auto it = cache_map.find(key);
|
||||
if (it != cache_map.end()) {
|
||||
// Update existing key
|
||||
cache_list.splice(cache_list.begin(), cache_list, it->second);
|
||||
it->second->value = value;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add new key-value pair
|
||||
cache_list.emplace_front(key, value);
|
||||
cache_map[key] = cache_list.begin();
|
||||
|
||||
// Evict least recently used if over capacity
|
||||
if (cache_map.size() > capacity) {
|
||||
auto lru = cache_list.back();
|
||||
cache_map.erase(lru.key);
|
||||
cache_list.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if key exists
|
||||
bool contains(const KEY& key) const {
|
||||
return cache_map.find(key) != cache_map.end();
|
||||
}
|
||||
|
||||
// Get current size
|
||||
size_t size() const {
|
||||
return cache_map.size();
|
||||
}
|
||||
|
||||
// Get capacity
|
||||
size_t get_capacity() const {
|
||||
return capacity;
|
||||
}
|
||||
const std::list<CacheNode> &cache() const {
|
||||
return cache_list;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2025 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#include "pch.h"
|
||||
#include "catch.hpp"
|
||||
#include "LRUCache.hpp"
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
|
||||
TEST_CASE( "LRUCache test", "[lrucache]" ) {
|
||||
|
||||
LRUCache<int,int> lruCache(5);
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
lruCache.put(i,i);
|
||||
}
|
||||
REQUIRE(lruCache.cache().size() == 5);
|
||||
|
||||
REQUIRE(lruCache.cache().begin()->value == 9);
|
||||
|
||||
int value;
|
||||
REQUIRE(lruCache.get(5,value) == true);
|
||||
REQUIRE(value == 5);
|
||||
REQUIRE(lruCache.cache().begin()->value == 5);
|
||||
|
||||
cout << '[';
|
||||
for (auto it = lruCache.cache().begin();it != lruCache.cache().end(); ++it)
|
||||
{
|
||||
cout << it->value << ',';
|
||||
}
|
||||
cout << ']' << endl;
|
||||
for (auto it = ++lruCache.cache().begin();it != lruCache.cache().end(); ++it)
|
||||
{
|
||||
REQUIRE(it->value != 5);
|
||||
}
|
||||
lruCache.put(1,1);
|
||||
cout << '[';
|
||||
for (auto it = lruCache.cache().begin();it != lruCache.cache().end(); ++it)
|
||||
{
|
||||
cout << it->value << ',';
|
||||
}
|
||||
cout << ']' << endl;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1116,3 +1116,4 @@ void Lv2Effect::SetPathPatchProperty(const std::string &propertyUri, const std::
|
||||
{
|
||||
mainThreadPathProperties[propertyUri] = jsonAtom;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ namespace pipedal
|
||||
virtual void OnLogDebug(const char*message);
|
||||
|
||||
private:
|
||||
|
||||
std::unordered_map<std::string,int> controlIndex;
|
||||
|
||||
FileBrowserFilesFeature fileBrowserFilesFeature;
|
||||
|
||||
@@ -562,11 +562,13 @@ void Lv2Pedalboard::GatherPathPatchProperties(IPatchWriterCallback *cbPatchWrite
|
||||
}
|
||||
}
|
||||
|
||||
void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests)
|
||||
void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests, size_t samplesThisTime)
|
||||
{
|
||||
while (pParameterRequests != nullptr)
|
||||
{
|
||||
pParameterRequests->sampleTimeout -= samplesThisTime;
|
||||
IEffect *pEffect = this->GetEffect(pParameterRequests->instanceId);
|
||||
|
||||
if (pEffect == nullptr)
|
||||
{
|
||||
pParameterRequests->errorMessage = "No such effect.";
|
||||
@@ -574,8 +576,12 @@ void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pPara
|
||||
else if (pEffect->IsVst3())
|
||||
{
|
||||
pParameterRequests->errorMessage = "Not supported for VST3 plugins";
|
||||
} else if (pParameterRequests->sampleTimeout < 0)
|
||||
{
|
||||
pParameterRequests->sampleTimeout = 0;
|
||||
pParameterRequests->errorMessage = "Timed out.";
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
if (pEffect->IsLv2Effect())
|
||||
{
|
||||
@@ -818,7 +824,7 @@ void Lv2Pedalboard::OnMidiMessage(size_t size, uint8_t *message,
|
||||
case MidiControlType::Dial:
|
||||
{
|
||||
IEffect *pEffect = this->realtimeEffects[mapping.effectIndex];
|
||||
range = mapping.midiBinding.adjustRange(range);
|
||||
float range = mapping.midiBinding.calculateRange(value);
|
||||
float currentValue = mapping.pPortInfo->rangeToValue(range);
|
||||
if (pEffect->GetControlValue(mapping.controlIndex) != currentValue)
|
||||
{
|
||||
|
||||
@@ -153,7 +153,7 @@ namespace pipedal
|
||||
|
||||
void ResetAtomBuffers();
|
||||
|
||||
void ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests);
|
||||
void ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests, size_t samplesThisTime);
|
||||
void GatherPatchProperties(RealtimePatchPropertyRequest *pParameterRequests);
|
||||
void GatherPathPatchProperties(IPatchWriterCallback *cbPatchWriter);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user