Toob File Player checkpoint.

This commit is contained in:
Robin E. R. Davies
2025-05-26 23:49:03 -04:00
parent b6debe3d2d
commit 8b480d2e58
46 changed files with 2281 additions and 197 deletions
+3
View File
@@ -1809,6 +1809,9 @@ namespace pipedal
{
inputSysexBuffer.resize(1024);
}
~AlsaMidiDeviceImpl() {
Close();
}
void Open(const AlsaMidiDeviceInfo &device)
{
runningStatus = 0;
+241
View File
@@ -0,0 +1,241 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "AudioFileMetadata.hpp"
#include <json.hpp>
#include <chrono>
#include <filesystem>
#include <json_variant.hpp>
#include "LRUCache.hpp"
#include <sstream>
using namespace pipedal;
namespace fs = std::filesystem;
namespace chrono = std::chrono;
// Safely encode a filename for use in a shell command
static std::string shell_escape_filename(const std::string& filename) {
std::string escaped;
escaped.reserve(filename.size() + 2); // Reserve space for quotes and content
// Wrap the filename in single quotes
escaped += '\'';
for (char c : filename) {
// Escape single quotes by closing the quote, adding escaped quote, and reopening
if (c == '\'') {
escaped += "'\\''";
} else {
escaped += c;
}
}
escaped += '\'';
return escaped;
}
static std::string readPipeToString(FILE* file) {
std::string content;
char buffer[4096];
size_t bytesRead;
while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) {
content.append(buffer, bytesRead);
}
return content;
}
static std::string GetJsonMetadata(const std::filesystem::path &path)
{
/* ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json
~/filename.flac */
std::stringstream ss;
ss << "/usr/bin/ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json "
<< shell_escape_filename(path.string())
<< " 2>/dev/null";
FILE *output = popen(ss.str().c_str(),"r");
if(output == nullptr)
{
throw std::runtime_error("Failed to process file.");
}
std::string result;
try {
result = readPipeToString(output);
} catch (const std::exception&e)
{
pclose(output);
throw std::runtime_error(e.what());
}
int retcode = pclose(output);
if (retcode != 0)
{
throw std::runtime_error(result);
}
return result;
}
static float MetadataFloat(const json_variant&vt, float defaultValue = 0)
{
if (vt.is_string())
{
std::string s = vt.as_string();
std::stringstream ss(s);
float result = defaultValue;
ss >> result;
return result;
}
return defaultValue;
}
static std::string MetadataString(const json_variant&o,const std::vector<std::string>& names) {
for (const auto&name: names)
{
auto result = (*o.as_object())[name];
if (result.is_string())
{
return result.as_string();
}
}
return "";
}
AudioFileMetadata::AudioFileMetadata(const std::filesystem::path &file)
{
try {
const std::string json = GetJsonMetadata(file);
json_variant vt;
std::stringstream ss(json);
json_reader reader(ss);
reader.read(&vt);
auto top = vt.as_object();
auto format = top->at("format").as_object();
this->duration_ = MetadataFloat(format->at("duration"),0);
auto tags = (*format)["tags"];
if (tags.is_object())
{
this->album_ = MetadataString(tags,{"ALBUM","album"});
this->artist_ = MetadataString(tags,{"ARTIST","artist"});
this->albumArtist_ = MetadataString(tags,{"ALBUM ARTIST","album_artist","album artist"});
this->title_ = MetadataString(tags,{"TITLE","title"});
this->date_ = MetadataString(tags,{"DATE","date"});
this->year_ = MetadataString(tags,{"YEAR","year"});
this->track_ = MetadataString(tags,{"track","TRACK",});
this->disc_ = MetadataString(tags,{"disc","DISC"});
if (title_ == "") {
this->title_ = file.stem();
}
this->totalTracks_ = MetadataString(tags,{"TOTALTRACKS"});
}
} catch (const std::exception &e)
{
std::string title = file.stem();
this->title_ = title;
}
}
namespace {
class AudioCacheKey {
public:
AudioCacheKey(const std::filesystem::path &path) {
this->path = path.string();
this->lastWrite = std::filesystem::last_write_time(path);
}
// Equality operator
bool operator==(const AudioCacheKey& other) const {
return path == other.path && lastWrite == other.lastWrite;
}
public:
std::string path;
fs::file_time_type lastWrite;
};
}
namespace std {
template <>
struct hash<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;
}
JSON_MAP_BEGIN(AudioFileMetadata)
JSON_MAP_REFERENCE(AudioFileMetadata, duration)
JSON_MAP_REFERENCE(AudioFileMetadata, title)
JSON_MAP_REFERENCE(AudioFileMetadata, track)
JSON_MAP_REFERENCE(AudioFileMetadata, album)
JSON_MAP_REFERENCE(AudioFileMetadata, disc)
JSON_MAP_REFERENCE(AudioFileMetadata, artist)
JSON_MAP_REFERENCE(AudioFileMetadata, albumArtist)
JSON_MAP_REFERENCE(AudioFileMetadata, totalTracks)
JSON_MAP_REFERENCE(AudioFileMetadata, date)
JSON_MAP_REFERENCE(AudioFileMetadata, year)
JSON_MAP_END()
+86
View File
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <string>
#include <filesystem>
#include "json.hpp"
namespace pipedal
{
#define GETTER_SETTER_REF(name) \
const decltype(name##_) &name() const { return name##_; } \
void name(const decltype(name##_) &value) { name##_ = value; }
#define GETTER_SETTER_VEC(name) \
decltype(name##_) &name() { return name##_; } \
const decltype(name##_) &name() const { return name##_; }
// void name(decltype(const name##_) &value) { name##_ = value; }
// void name(decltype(const name##_) &&value) { name##_ = std::move(value); }
#define GETTER_SETTER(name) \
decltype(name##_) name() const { return name##_; } \
void name(decltype(name##_) value) { name##_ = value; }
class AudioFileMetadata
{
private:
float duration_ = 0;
std::string title_;
std::string track_;
std::string album_;
std::string disc_;
std::string artist_;
std::string albumArtist_;
std::string totalTracks_;
std::string date_;
std::string year_;
public:
AudioFileMetadata(const std::filesystem::path &file);
GETTER_SETTER(duration);
GETTER_SETTER_REF(title);
GETTER_SETTER_REF(track);
GETTER_SETTER_REF(album);
GETTER_SETTER_REF(disc);
GETTER_SETTER_REF(artist);
GETTER_SETTER_REF(albumArtist);
public:
DECLARE_JSON_MAP(AudioFileMetadata);
};
std::string GetAudioFileMetadataString(const std::filesystem::path &path);
#undef GETTER_SETTER
#undef GETTER_SETTER_VEC
#undef GETTER_SETTER_REF
}
+149
View File
@@ -0,0 +1,149 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "AudioFiles.hpp"
#include <filesystem>
#include <limits>
#include <sqlite3.h>
using namespace pipedal;
static constexpr size_t INVALID_INDEX = std::numeric_limits<size_t>::max();
namespace fs = std::filesystem;
namespace {
class DirectoryList {
public:
DirectoryList(const fs::path&path);
const std::vector<std::filesystem::path> &files() {
return files_;
}
private:
std::vector<std::filesystem::path> files_;
};
class AudioFileInfoImpl: public AudioFileInfo {
public:
virtual ~AudioFileInfoImpl() { }
protected:
virtual std::string GetNextAudioFile(const std::string&path) override;
virtual std::string GetPreviousAudioFile(const std::string&path) override;
};
/////////////////////
DirectoryList::DirectoryList(const fs::path&path)
{
for (const auto&dirEntry: fs::directory_iterator(path))
{
if (dirEntry.is_regular_file())
{
fs::path t = dirEntry.path();
if (!t.filename().string().starts_with('.'))
{
files_.push_back(std::move(t));
}
}
}
}
std::string AudioFileInfoImpl::GetNextAudioFile(const std::string&path_) {
try
{
fs::path path {path_};
if (fs::exists(path) && fs::is_regular_file(path))
{
DirectoryList directoryList(path.parent_path());
size_t index = INVALID_INDEX;
auto &files = directoryList.files();
for (size_t i= 0; i < files.size(); ++i)
{
if (files[i] == path)
{
size_t next = i;
if (next == files.size()-1)
{
next = 0;
} else {
++next;
}
return files[next];
}
}
}
} catch (const std::exception&e)
{
}
return "";
}
std::string AudioFileInfoImpl::GetPreviousAudioFile(const std::string&path_)
{
try
{
fs::path path {path_};
if (fs::exists(path) && fs::is_regular_file(path))
{
DirectoryList directoryList(path.parent_path());
size_t index = INVALID_INDEX;
auto &files = directoryList.files();
for (size_t i= 0; i < files.size(); ++i)
{
if (files[i] == path)
{
size_t previous = i;
if (previous == 0)
{
previous = files.size()-1;
} else {
--previous;
}
return files[previous];
}
}
}
} catch (const std::exception&e)
{
}
return "";
}
}
static AudioFileInfoImpl instance;
AudioFileInfo&AudioFileInfo::GetInstance()
{
return instance;
}
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <string>
namespace pipedal {
class AudioFileInfo {
protected:
AudioFileInfo() { }
virtual ~AudioFileInfo() {}
public:
static AudioFileInfo&GetInstance();
virtual std::string GetNextAudioFile(const std::string&path) = 0;
virtual std::string GetPreviousAudioFile(const std::string&path) = 0;
};
}
+1 -1
View File
@@ -1173,7 +1173,7 @@ private:
if (buffersValid)
{
pedalboard->ProcessParameterRequests(pParameterRequests);
pedalboard->ProcessParameterRequests(pParameterRequests,nframes);
processed = pedalboard->Run(inputBuffers, outputBuffers, (uint32_t)nframes, &realtimeWriter);
if (processed)
+10 -4
View File
@@ -69,6 +69,7 @@ namespace pipedal
const char *errorMessage = nullptr;
std::string jsonResponse;
int64_t sampleTimeout = 0;
RealtimePatchPropertyRequest *pNext = nullptr;
@@ -102,13 +103,16 @@ namespace pipedal
int64_t instanceId_,
LV2_URID uridUri_,
std::function<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 +123,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);
+12 -3
View File
@@ -12,6 +12,7 @@ set (USE_SANITIZE OFF) # seems to be broken on Ubuntu 24.10
set(CXX_STANDARD 20)
include(FetchContent)
# FOR FTXUI
@@ -45,6 +46,9 @@ include(FindPkgConfig)
find_package(sdbus-c++ REQUIRED)
find_package(SQLiteCpp REQUIRED)
find_package(ICU REQUIRED COMPONENTS uc i18n )
message(STATUS "ICU_LIBRARIES: ${ICU_LIBRARIES}")
@@ -192,6 +196,9 @@ else()
endif()
set (PIPEDAL_SOURCES
AudioFiles.cpp AudioFiles.hpp
AudioFileMetadata.cpp AudioFileMetadata.hpp
LRUCache.hpp
CpuTemperatureMonitor.cpp CpuTemperatureMonitor.hpp
SchedulerPriority.hpp SchedulerPriority.cpp
ModFileTypes.cpp ModFileTypes.hpp
@@ -304,6 +311,7 @@ set (PIPEDAL_INCLUDES
${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS}
${VST3_INCLUDES}
${WEBSOCKETPP_INCLUDE_DIRS}
${SQLite3_INCLUDE_DIRS}
.
)
@@ -314,7 +322,7 @@ set(PIPEDAL_LIBS libpipedald zip
${VST3_LIBRARIES}
${LILV_0_LIBRARIES}
# ${JACK_LIBRARIES} - pending delete for JACK support.
${SQLite3_LIBRARIES}
)
@@ -383,8 +391,9 @@ add_executable(AuxInTest
)
add_executable(pipedaltest
testMain.cpp
testMain.cpp
LRUCacheTest.cpp
ModFileTypesTest.cpp
jsonTest.cpp
UpdaterTest.cpp
@@ -822,7 +831,7 @@ add_custom_command(OUTPUT ${REACT_NOTICES_FILE}
--output ${REACT_NOTICES_FILE}
--projectCopyright ${DEBIAN_COPYRIGHT_FILE}
liblilv-0-0 ${BOOST_COPYRIGHT_DIR} lv2-dev libsdbus-c++-dev librsvg2-2 libpango-1.0-0 libx11-6 libxrandr2
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${DEBIAN_COPYRIGHT_FILE} "$<TARGET_FILE:processcopyrights>"
COMMENT "Updating copyright notices."
)
+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;
+8 -2
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())
{
+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);
+35 -18
View File
@@ -1615,19 +1615,30 @@ void PiPedalModel::SendSetPatchProperty(
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (!audioHost)
{
onError("Audio not running.");
return;
}
// save the property to the preset (currently used to reconstruct snapshots only)
PedalboardItem *pedalboardItem = this->pedalboard.GetItem(instanceId);
if (pedalboardItem)
if (pedalboardItem && value.is_string())
{
json_variant abstractPath = pluginHost.AbstractPath(value);
std::ostringstream ss;
json_writer writer(ss);
writer.write(abstractPath);
std::string atomString = ss.str();
pedalboardItem->pathProperties_[propertyUri] = atomString;
std::shared_ptr<Lv2PluginInfo> pluginInfo = GetPluginInfo(pedalboardItem->uri_);
auto pipedalUi = pluginInfo->piPedalUI();
auto fileProperty = pipedalUi->GetFileProperty(propertyUri);
if (fileProperty && value.is_string()) {
json_variant abstractPath = pluginHost.AbstractPath(value);
std::ostringstream ss;
json_writer writer(ss);
writer.write(abstractPath);
std::string atomString = ss.str();
pedalboardItem->pathProperties_[propertyUri] = atomString;
}
this->SetPresetChanged(clientId, true);
}
this->SetPresetChanged(clientId, true);
LV2_Atom *atomValue = atomConverter.ToAtom(value);
std::function<void(RealtimePatchPropertyRequest *)> onRequestComplete{
@@ -1657,7 +1668,7 @@ void PiPedalModel::SendSetPatchProperty(
}
else
{
if (pParameter->onSuccess)
if (onSuccess)
{
onSuccess();
}
@@ -1668,10 +1679,12 @@ void PiPedalModel::SendSetPatchProperty(
}};
LV2_URID urid = this->pluginHost.GetLv2Urid(propertyUri.c_str());
size_t sampleTimeout = 0.5*audioHost->GetSampleRate();
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
onRequestComplete,
clientId, instanceId, urid, atomValue, nullptr, onError);
clientId, instanceId, urid, atomValue, nullptr, onError,
sampleTimeout
);
outstandingParameterRequests.push_back(request);
if (this->audioHost)
@@ -1684,7 +1697,7 @@ void PiPedalModel::SendGetPatchProperty(
int64_t clientId,
int64_t instanceId,
const std::string uri,
std::function<void(const std::string &jsonResjult)> onSuccess,
std::function<void(const std::string &jsonResult)> onSuccess,
std::function<void(const std::string &error)> onError)
{
std::function<void(RealtimePatchPropertyRequest *)> onRequestComplete{
@@ -1732,16 +1745,20 @@ void PiPedalModel::SendGetPatchProperty(
}};
LV2_URID urid = this->pluginHost.GetLv2Urid(uri.c_str());
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
onRequestComplete,
clientId, instanceId, urid, onSuccess, onError);
std::lock_guard<std::recursive_mutex> lock(mutex);
outstandingParameterRequests.push_back(request);
if (this->audioHost)
if (!this->audioHost)
{
this->audioHost->sendRealtimeParameterRequest(request);
onError("Audio stopped.");
}
size_t sampleTimeout = 0.3*audioHost->GetSampleRate();
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
onRequestComplete,
clientId, instanceId, urid, onSuccess, onError,sampleTimeout);
outstandingParameterRequests.push_back(request);
this->audioHost->sendRealtimeParameterRequest(request);
}
BankIndex PiPedalModel::GetBankIndex() const
+2
View File
@@ -33,6 +33,8 @@
#include "ModFileTypes.hpp"
#define PIPEDAL_HOST_FEATURE "http://github.com/rerdavies/pipedal#host" // Plugin can only be hosted by PiPedal
#define PIPEDAL_PATCH "http://github.com/rerdavies/pipedal/patch"
#define PIPEDAL_PATCH_PREFIX PIPEDAL_PATCH "#"
#define PIPEDAL_PATCH__readable (PIPEDAL_PATCH_PREFIX "readable")
+2 -1
View File
@@ -615,7 +615,7 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
// example:
// <http://github.com/mikeoliphant/neural-amp-modeler-lv2#model>
// a lv2:Parameter;
// a lv2:Parameter;
// rdfs:label "Model";
// rdfs:range atom:Path.
// ...
@@ -901,6 +901,7 @@ std::vector<std::string> supportedFeatures = {
LV2_STATE__mapPath,
LV2_STATE__freePath,
LV2_CORE__inPlaceBroken,
PIPEDAL_HOST_FEATURE,
// UI features that we can ignore, since we won't load their ui.
"http://lv2plug.in/ns/extensions/ui#makeResident",
+100 -22
View File
@@ -35,6 +35,9 @@
#include "json.hpp"
#include "HotspotManager.hpp"
#include "MimeTypes.hpp"
#include "AudioFileMetadata.hpp"
#include "AudioFiles.hpp"
#define OLD_PRESET_EXTENSION ".piPreset"
#define PRESET_EXTENSION ".piPreset"
@@ -52,20 +55,20 @@ using namespace pipedal;
using namespace boost::system;
namespace fs = std::filesystem;
static bool HasDotDot(const std::filesystem::path &path) {
static bool HasDotDot(const std::filesystem::path &path)
{
for (auto &part : path)
{
if (part == "..") {
if (part == "..")
{
return true;
}
if (part == ".")
if (part == ".")
{
return true;
}
}
return false;
return false;
}
class UserUploadResponse
@@ -80,12 +83,12 @@ JSON_MAP_REFERENCE(UserUploadResponse, errorMessage)
JSON_MAP_REFERENCE(UserUploadResponse, path)
JSON_MAP_END()
static std::string GetMimeType(const std::filesystem::path&path) {
static std::string GetMimeType(const std::filesystem::path &path)
{
std::string extension = path.extension();
const MimeTypes&mimeTypes = MimeTypes::instance();
const MimeTypes &mimeTypes = MimeTypes::instance();
auto result = mimeTypes.MimeTypeFromExtension(extension);
return result;
}
static bool IsZipFile(const std::filesystem::path &path)
{
@@ -179,6 +182,17 @@ public:
{
return true;
}
else if (segment == "AudioMetadata")
{
return true;
} else if (segment == "NextAudioFile")
{
return true;
} else if (segment == "PreviousAudioFile")
{
return true;
}
return false;
}
@@ -246,15 +260,17 @@ public:
try
{
std::string segment = request_uri.segment(1);
if (segment == "downloadMediaFile") {
if (segment == "downloadMediaFile")
{
fs::path path = request_uri.query("path");
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
{
throw PiPedalException("File not found.");
}
auto mimeType = GetMimeType(path);
if (mimeType.empty()) {
if (mimeType.empty())
{
throw PiPedalException("Can't download files of this type.");
}
res.set(HttpField::content_type, mimeType);
@@ -343,18 +359,20 @@ public:
{
std::string segment = request_uri.segment(1);
if (segment == "downloadMediaFile") {
if (segment == "downloadMediaFile")
{
fs::path path = request_uri.query("path");
bool t = this->model->IsInUploadsDirectory(path);
std::cout << (t? "true": "false") << std::endl;
std::cout << (t ? "true" : "false") << std::endl;
(void)t;
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
{
throw PiPedalException("File not found.");
}
auto mimeType = GetMimeType(path);
if (mimeType.empty()) {
if (mimeType.empty())
{
throw PiPedalException("Can't download files of this type.");
}
res.set(HttpField::content_type, mimeType);
@@ -363,9 +381,10 @@ public:
res.set(HttpField::content_disposition, disposition);
size_t contentLength = std::filesystem::file_size(path);
res.setContentLength(contentLength);
res.setBodyFile(path,false);
res.setBodyFile(path, false);
return;
} else if (segment == "downloadPluginPresets")
}
else if (segment == "downloadPluginPresets")
{
std::string name;
std::string content;
@@ -416,6 +435,65 @@ public:
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
res.setBodyFile(tmpFile);
}
else if (segment == "AudioMetadata")
{
res.set(HttpField::content_type, "application/json");
// res.set(HttpField::cache_control, "no-cache");
fs::path path = request_uri.query("path");
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
{
throw PiPedalException("File not found.");
}
std::string metadata = GetAudioFileMetadataString(path);
res.setBody(metadata);
}
else if (segment == "NextAudioFile")
{
res.set(HttpField::content_type, "application/json");
// res.set(HttpField::cache_control, "no-cache");
fs::path path = request_uri.query("path");
try {
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
{
throw PiPedalException("File not found.");
}
std::string nextFile = AudioFileInfo::GetInstance().GetNextAudioFile(path);
std::stringstream ss;
json_writer writer(ss);
writer.write(nextFile);
res.setBody(ss.str());
} catch (const std::exception&e)
{
res.setBody("''");
}
}
else if (segment == "PreviousAudioFile")
{
res.set(HttpField::content_type, "application/json");
// res.set(HttpField::cache_control, "no-cache");
fs::path path = request_uri.query("path");
try {
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
{
throw PiPedalException("File not found.");
}
std::string previousFile = AudioFileInfo::GetInstance().GetPreviousAudioFile(path);
std::stringstream ss;
json_writer writer(ss);
writer.write(previousFile);
res.setBody(ss.str());
} catch (const std::exception&e)
{
res.setBody("\"\"");
}
}
else
{
throw PiPedalException("Not found");
@@ -628,7 +706,7 @@ public:
int64_t instanceId;
{
std::istringstream ss { instanceIdString};
std::istringstream ss{instanceIdString};
ss >> instanceId;
if (!ss)
{
@@ -669,7 +747,7 @@ public:
if (extensionChecker.IsValidExtension(extension))
{
auto si = zipFile->GetFileInputStream(inputFile);
std::string path = this->model->UploadUserFile(directory, instanceId,patchProperty, inputFile, si, zipFile->GetFileSize(inputFile));
std::string path = this->model->UploadUserFile(directory, instanceId, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile));
}
}
}
@@ -687,7 +765,7 @@ public:
}
else
{
outputFileName = this->model->UploadUserFile(directory, instanceId,patchProperty, filename, req.get_body_input_stream(), req.content_length());
outputFileName = this->model->UploadUserFile(directory, instanceId, patchProperty, filename, req.get_body_input_stream(), req.content_length());
}
if (outputFileName.is_relative())
@@ -783,7 +861,7 @@ public:
<< ", \"socket_server_address\": \"" << webSocketAddress
<< "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize
<< ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true" : "false")
<< ", \"has_wifi_device\": " << (HotspotManager::HasWifiDevice() ? " true": "false")
<< ", \"has_wifi_device\": " << (HotspotManager::HasWifiDevice() ? " true" : "false")
<< " }";
return s.str();