Merge pull request #320 from rerdavies/dev_backup

Merge 1.4.77 dev branch.
This commit is contained in:
Robin Davies
2025-07-01 03:42:39 -04:00
committed by GitHub
212 changed files with 16444 additions and 3755 deletions
+3
View File
@@ -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
+7 -2
View File
@@ -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
}
+3 -3
View File
@@ -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,
+23 -2
View File
@@ -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
View File
@@ -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
+4 -2
View File
@@ -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
+11
View File
@@ -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.
+8 -2
View File
@@ -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");
+87 -45
View File
@@ -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;
}
}
+112 -92
View File
@@ -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;
}
}
+357
View File
@@ -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);
}
+39
View File
@@ -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;
};
}
+3
View File
@@ -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);
+1 -1
View File
@@ -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.
+2
View File
@@ -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 -10
View File
@@ -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(...);
+6 -1
View File
@@ -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;
+6 -2
View File
@@ -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
View File
@@ -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;
}
+12
View File
@@ -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();
+26
View File
@@ -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;
}
+2 -2
View File
@@ -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:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.4.76</a>
Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.4.77</a>
Website:&nbsp;[https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
Documentation:&nbsp;[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.
&nbsp;
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.
+122
View File
@@ -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"
)
+2 -1
View File
@@ -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
View File
@@ -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.
+53
View File
@@ -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
View File
@@ -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

+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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 """
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
+16 -5
View File
@@ -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.
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
""" ;
+1 -1
View File
@@ -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>;
+1 -1
View File
@@ -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>;
+1 -1
View File
@@ -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.
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+307
View File
@@ -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 ;
]
.
+7 -8
View File
@@ -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;
+5 -5
View File
@@ -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;
+1 -1
View File
@@ -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.
""" ;
+2 -2
View File
@@ -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.
+4
View File
@@ -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> .
+1
Submodule modules/SQLiteCpp added at 1df768817e
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+44
View File
@@ -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()
+105
View File
@@ -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
}
+335
View File
@@ -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;
}
+42
View File
@@ -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);
}
+937
View File
@@ -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);
}
}
+128
View File
@@ -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);
}
+505
View File
@@ -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();
}
+134
View File
@@ -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;
};
}
+536
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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."
)
+66 -7
View File
@@ -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:
+1
View File
@@ -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)
+9
View File
@@ -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);
+263
View File
@@ -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 = &map;
}
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);
}
+85
View File
@@ -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
View File
@@ -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;
}
+19 -1
View File
@@ -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())
{
+7 -1
View File
@@ -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_;
}
+123
View File
@@ -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;
}
};
+67
View File
@@ -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;
}
+1
View File
@@ -1116,3 +1116,4 @@ void Lv2Effect::SetPathPatchProperty(const std::string &propertyUri, const std::
{
mainThreadPathProperties[propertyUri] = jsonAtom;
}
+1
View File
@@ -56,6 +56,7 @@ namespace pipedal
virtual void OnLogDebug(const char*message);
private:
std::unordered_map<std::string,int> controlIndex;
FileBrowserFilesFeature fileBrowserFilesFeature;
+9 -3
View File
@@ -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)
{
+1 -1
View File
@@ -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