diff --git a/.gitmodules b/.gitmodules index b464b53..b1fae44 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,4 @@ path = modules/websocketpp url = https://github.com/rerdavies/websocketpp.git branch = develop + update = merge diff --git a/PiPedalCommon/src/ConfigSerializer.cpp b/PiPedalCommon/src/ConfigSerializer.cpp index fb2ee54..c18d220 100644 --- a/PiPedalCommon/src/ConfigSerializer.cpp +++ b/PiPedalCommon/src/ConfigSerializer.cpp @@ -23,6 +23,7 @@ */ #include "ConfigSerializer.hpp" +#include "util.hpp" using namespace config_serializer; @@ -38,29 +39,6 @@ std::string config_serializer::detail::trim(const std::string &v) return v.substr(start, end+1 - start); } -std::vector config_serializer::detail::split(const std::string &value, char delimiter) -{ - size_t start = 0; - std::vector result; - while (start < value.length()) - { - size_t pos = value.find_first_of(delimiter, start); - if (pos == std::string::npos) - { - result.push_back(value.substr(start)); - break; - } - result.push_back(value.substr(start, pos - start)); - start = pos + 1; - if (start == value.length()) - { - // ends with delimieter? Then there's an empty-length value at the end. - result.push_back(""); - } - } - return result; -} - std::string config_serializer::detail::EncodeString(const std::string &s) diff --git a/PiPedalCommon/src/include/ConfigSerializer.hpp b/PiPedalCommon/src/include/ConfigSerializer.hpp index 1bd2f6e..f946da6 100644 --- a/PiPedalCommon/src/include/ConfigSerializer.hpp +++ b/PiPedalCommon/src/include/ConfigSerializer.hpp @@ -31,6 +31,9 @@ #include #include #include "ofstream_synced.hpp" +#include "util.hpp" + +using namespace pipedal; namespace config_serializer { @@ -38,8 +41,6 @@ namespace config_serializer { std::string trim(const std::string &v); - std::vector split(const std::string &v, char seperator); - /** * @brief Convert string to config file format. * diff --git a/PiPedalCommon/src/include/json.hpp b/PiPedalCommon/src/include/json.hpp index 23ef873..7b0860a 100644 --- a/PiPedalCommon/src/include/json.hpp +++ b/PiPedalCommon/src/include/json.hpp @@ -586,7 +586,11 @@ namespace pipedal template void write(const std::shared_ptr &obj) { - write(obj.get()); + if (!obj) { + write_raw("null"); + } else { + write(obj.get()); + } } template void write(const std::weak_ptr &obj) @@ -865,10 +869,18 @@ namespace pipedal template void read(std::shared_ptr *pUniquePtr) { - std::shared_ptr p = std::make_shared(); + if (peek() == 'n') + { + consumeToken("null", "Expecting '{' or 'null'."); + *pUniquePtr = nullptr; - read(p.get()); - (*pUniquePtr) = std::move(p); + } else + { + std::shared_ptr p = std::make_shared(); + + read(p.get()); + (*pUniquePtr) = std::move(p); + } } template void read(std::weak_ptr *pUniquePtr) diff --git a/PiPedalCommon/src/include/util.hpp b/PiPedalCommon/src/include/util.hpp index 93e6343..588dca8 100644 --- a/PiPedalCommon/src/include/util.hpp +++ b/PiPedalCommon/src/include/util.hpp @@ -23,6 +23,9 @@ #pragma once #include #include +#include +#include +#include namespace pipedal { @@ -38,4 +41,30 @@ namespace pipedal { std::string GetHostName(); + std::vector split(const std::string &value, char delimiter); + + inline void ReadBarrier() { + std::atomic_thread_fence(std::memory_order::acquire); + } + inline void WriteBarrier() { + std::atomic_thread_fence(std::memory_order::release); + + } + + class NoCopy { + public: + NoCopy() { } + NoCopy(const NoCopy&) = delete; + NoCopy&operator=(const NoCopy&) = delete; + ~NoCopy() {} + }; + class NoMove { + public: + NoMove() { } + NoMove(NoMove&&) = delete; + NoMove&operator=(NoMove&&) = delete; + ~NoMove() { } + + }; + } diff --git a/PiPedalCommon/src/util.cpp b/PiPedalCommon/src/util.cpp index 6fbc1a9..e01c179 100644 --- a/PiPedalCommon/src/util.cpp +++ b/PiPedalCommon/src/util.cpp @@ -112,3 +112,27 @@ std::string pipedal::GetHostName() buffer[1023] = '\0'; return buffer; } + + +std::vector pipedal::split(const std::string &value, char delimiter) +{ + size_t start = 0; + std::vector result; + while (start < value.length()) + { + size_t pos = value.find_first_of(delimiter, start); + if (pos == std::string::npos) + { + result.push_back(value.substr(start)); + break; + } + result.push_back(value.substr(start, pos - start)); + start = pos + 1; + if (start == value.length()) + { + // ends with delimieter? Then there's an empty-length value at the end. + result.push_back(""); + } + } + return result; +} diff --git a/modules/websocketpp b/modules/websocketpp index a13b985..2466f9e 160000 --- a/modules/websocketpp +++ b/modules/websocketpp @@ -1 +1 @@ -Subproject commit a13b985bb5972fb6480eca2e8f7d09eef8a3f00d +Subproject commit 2466f9e5f15f6c3f583fb042ff7048a8ad0754a9 diff --git a/react/public/index.html b/react/public/index.html index 7383ce2..d0032f9 100644 --- a/react/public/index.html +++ b/react/public/index.html @@ -36,7 +36,7 @@ - + -
+
+ + + + + diff --git a/react/src/svg/snapshot_1.svg b/react/src/svg/snapshot_1.svg new file mode 100644 index 0000000..c9e27fb --- /dev/null +++ b/react/src/svg/snapshot_1.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/react/src/svg/snapshot_2.svg b/react/src/svg/snapshot_2.svg new file mode 100644 index 0000000..040276d --- /dev/null +++ b/react/src/svg/snapshot_2.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/react/src/svg/snapshot_3.svg b/react/src/svg/snapshot_3.svg new file mode 100644 index 0000000..679dec2 --- /dev/null +++ b/react/src/svg/snapshot_3.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/react/src/svg/snapshot_4.svg b/react/src/svg/snapshot_4.svg new file mode 100644 index 0000000..fd352fc --- /dev/null +++ b/react/src/svg/snapshot_4.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/react/src/svg/snapshot_5.svg b/react/src/svg/snapshot_5.svg new file mode 100644 index 0000000..07c5c10 --- /dev/null +++ b/react/src/svg/snapshot_5.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/react/src/svg/snapshot_6.svg b/react/src/svg/snapshot_6.svg new file mode 100644 index 0000000..d30f329 --- /dev/null +++ b/react/src/svg/snapshot_6.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index dd8f3a2..1265412 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -953,9 +953,12 @@ namespace pipedal { OpenMidi(jackServerSettings, channelSelection); OpenAudio(jackServerSettings, channelSelection); + std::atomic_thread_fence(std::memory_order::release); } catch (const std::exception &e) { + std::atomic_thread_fence(std::memory_order::release); + Close(); throw; } @@ -1359,7 +1362,7 @@ namespace pipedal long WriteBuffer(snd_pcm_t *handle, uint8_t *buf, size_t frames) { long framesRead; - auto frame_bytes = this->captureFrameSize; + auto frame_bytes = this->playbackFrameSize; while (frames > 0) { @@ -1969,6 +1972,8 @@ namespace pipedal } virtual void Close() { + std::atomic_thread_fence(std::memory_order::acquire); + if (!open) { return; @@ -1977,6 +1982,7 @@ namespace pipedal Deactivate(); AlsaCleanup(); DeleteBuffers(); + std::atomic_thread_fence(std::memory_order::release); } virtual float CpuUse() diff --git a/src/AtomConverter.cpp b/src/AtomConverter.cpp index 4d3af39..8054e47 100644 --- a/src/AtomConverter.cpp +++ b/src/AtomConverter.cpp @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Robin E. R. Davies + * Copyright (c) 2023-2024 Robin E. R. Davies * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -29,6 +29,7 @@ #include "json.hpp" #include "ss.hpp" #include "lv2/atom/util.h" +#include "ss.hpp" using namespace pipedal; @@ -69,6 +70,76 @@ json_variant AtomConverter::ToJson(const LV2_Atom *atom) json_variant variant = ToVariant(const_cast(atom)); return std::move(variant); } + +LV2_Atom*AtomConverter::ToAtom(const std::string&jsonString) +{ + json_variant jvValue; + std::istringstream ss(jsonString); + json_reader reader(ss); + reader.read(&jvValue); + return ToAtom(jvValue); +} + +std::string AtomConverter::ToString(const LV2_Atom*atom) +{ + json_variant v = ToJson(atom); + std::ostringstream ss; + json_writer writer(ss); + writer.write(v); + return ss.str(); +} +json_variant AtomConverter::MapPath(const json_variant&json, const std::string &pluginStoragePath) +{ + std::string oType = json[OTYPE_TAG].as_string(); + if (oType == SHORT_ATOM__Path) + { + std::string path = json["value"].as_string().c_str(); + if (path.length() == 0 || path[0] == '/') + { + return json; + } + std::string newPath = SS(pluginStoragePath << "/" << path); + json_variant result = json_variant::make_object(); + result[OTYPE_TAG] = json_variant(SHORT_ATOM__Path); + result["value"] = newPath; + + return result; + } + else { + return json; + } +} +json_variant AtomConverter::AbstractPath(const json_variant&json, const std::string &pluginStoragePath) +{ + std::string oType = json[OTYPE_TAG].as_string(); + if (oType == SHORT_ATOM__Path) + { + std::string path = json["value"].as_string().c_str(); + if (path.length() == 0) + { + return json; + } + if (path.starts_with(pluginStoragePath)) + { + auto pos = pluginStoragePath.length(); + if (pos < path.length() && path[pos] == '/') + { + ++pos; + } + path = path.substr(pos); + } + json_variant result = json_variant::make_object(); + result[OTYPE_TAG] = json_variant(SHORT_ATOM__Path); + result["value"] = path; + return result; + } + else { + return json; + } + +} + + LV2_Atom*AtomConverter::ToAtom(const json_variant&json) { if (outputBuffer.size() == 0) @@ -517,6 +588,29 @@ std::string AtomConverter::TypeUridToString(LV2_URID urid) return map.UridToString(urid); } +static bool gEmptyPathInitialized = false; +static std::mutex gInitMutex; +std::string gEmptyPathstring; + +std::string AtomConverter::EmptyPathstring() +{ + std::lock_guard lock{gInitMutex}; + + if (!gEmptyPathInitialized) + { + json_variant v = TypedProperty(SHORT_ATOM__Path,std::string("",0)); + std::ostringstream ss; + json_writer writer(ss); + writer.write(v); + + gEmptyPathstring = ss.str(); + + } + return gEmptyPathstring; + +} + + void AtomConverter::InitUrids() { #define ATOM_INIT(name,shortName) urids.name = InitUrid(LV2_##name,#shortName) diff --git a/src/AtomConverter.hpp b/src/AtomConverter.hpp index 205267f..4336ae6 100644 --- a/src/AtomConverter.hpp +++ b/src/AtomConverter.hpp @@ -31,6 +31,7 @@ #include "lv2/atom/forge.h" #include "json_variant.hpp" #include "atom_object.hpp" +#include "json_variant.hpp" namespace pipedal { @@ -139,11 +140,25 @@ namespace pipedal /// @brief Convert a json variant to an atom. /// @param json Json variant that matches the structure of the prototype. - /// @return An atom. - /// @remarks - /// The json variant must match the structure of the LV2_Atom prototype supplied to - /// the constructor. + /// @return An atom. Not valid beyond the lifetime of the AtomConverter. Memory is owned by the AtomConverter. LV2_Atom*ToAtom(const json_variant& json); + + + /// @brief Convert a json string to an atom. + /// @param json Json string that matches the structure of the prototype. + /// @return An atom. Not valid beyond the lifetime of the AtomConverter. Memory is owned by the AtomConverter. + /// @remarks + + LV2_Atom*ToAtom(const std::string &jsonString); + + std::string ToString(const LV2_Atom*atom); + + json_variant MapPath(const json_variant&json, const std::string &pluginStoragePath); + json_variant AbstractPath(const json_variant&json, const std::string &pluginStoragePath); + + + static std::string EmptyPathstring(); + private: static const std::string OTYPE_TAG; static const std::string VTYPE_TAG; @@ -160,14 +175,14 @@ namespace pipedal LV2_URID InitUrid(const char*uri, const char*shortUri); template - json_variant TypedProperty(const std::string type,U value) + static json_variant TypedProperty(const std::string type,U value) { json_variant result = json_variant::make_object(); result[OTYPE_TAG] = type; result["value"] = json_variant(value); return result; } - json_variant TypedProperty(const std::string type,const json_variant&& value) + static json_variant TypedProperty(const std::string type,const json_variant&& value) { json_variant result = json_variant::make_object(); result[OTYPE_TAG] = type; diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 9547e62..488ae58 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -19,6 +19,7 @@ #include "AudioHost.hpp" #include "util.hpp" +#include #include "Lv2Log.hpp" @@ -26,6 +27,9 @@ #include "AlsaDriver.hpp" #include "DummyAudioDriver.hpp" #include "AtomConverter.hpp" +#include +#include "PluginHost.hpp" +#include "PatchPropertyWriter.hpp" using namespace pipedal; @@ -107,6 +111,167 @@ static std::string GetGovernor() return pipedal::GetCpuGovernor(); } +namespace pipedal +{ + + struct PathPatchProperty + { + LV2_URID propertyUrid = 0; + std::vector atomBuffer; + }; + + class IndexedSnapshotValue + { + private: + struct InputControlEntry + { + bool isInputControl = false; + float value = 0; + }; + + public: + IndexedSnapshotValue(IEffect *effect, SnapshotValue *snapshotValue, PluginHost &pluginHost) + : pEffect(effect) + { + auto maxInputControl = effect->GetMaxInputControl(); + inputControlValues.resize(maxInputControl); + + for (uint64_t i = 0; i < maxInputControl; ++i) + { + bool isInputControl = effect->IsInputControl(i); + if (isInputControl) + { + inputControlValues[i] = InputControlEntry{isInputControl : true, value : effect->GetDefaultInputControlValue(i)}; + } + else + { + inputControlValues[i] = InputControlEntry{isInputControl : false, value : 0}; + } + } + if (snapshotValue) + { + for (auto &controlValue : snapshotValue->controlValues_) + { + auto index = effect->GetControlIndex(controlValue.key()); + if (index >= 0 && index < inputControlValues.size()) + { + inputControlValues[index].value = controlValue.value(); + } + } + this->lv2State = snapshotValue->lv2State_; + + if (effect->IsLv2Effect()) + { + Lv2Effect *lv2Effect = (Lv2Effect *)effect; + for (auto &pathProperty : snapshotValue->pathProperties_) + { + // only transmit changed path patch properties. + if (lv2Effect->GetPathPatchProperty(pathProperty.first) != pathProperty.second) + { + lv2Effect->SetPathPatchProperty(pathProperty.first, pathProperty.second); + + PathPatchProperty pathPatchProperty; + pathPatchProperty.propertyUrid = pluginHost.GetLv2Urid(pathProperty.first.c_str()); + // convert to json variant so we do a mappath operation. + json_variant vProperty; + std::istringstream ss(pathProperty.second); + json_reader reader(ss); + reader.read(&vProperty); + vProperty = pluginHost.MapPath(vProperty); + + // now to atom format (what we want on the rt thread0) + AtomConverter atomConverter(pluginHost.GetMapFeature()); + LV2_Atom *atomValue = atomConverter.ToAtom(vProperty); + + size_t atomBufferSize = (atomValue->size + sizeof(LV2_Atom) + 3) / 4 * 4; + pathPatchProperty.atomBuffer.resize(atomValue->size + sizeof(LV2_Atom)); + memcpy(pathPatchProperty.atomBuffer.data(), atomValue, pathPatchProperty.atomBuffer.size()); + this->pathPatchProperties.push_back(std::move(pathPatchProperty)); + } + } + } + } + } + void ApplyValues(IEffect *effect) + { + if (effect != pEffect) + { + throw std::runtime_error("Wrong effect"); + } + for (size_t i = 0; i < inputControlValues.size(); ++i) + { + InputControlEntry &e = inputControlValues[i]; + if (e.isInputControl) + { + effect->SetControl((int)i, e.value); + } + } + + for (const auto &patchProperty : pathPatchProperties) + { + effect->SetPatchProperty( + patchProperty.propertyUrid, patchProperty.atomBuffer.size(), (LV2_Atom *)patchProperty.atomBuffer.data()); + } + // effect->SetLv2State(lv2State); + } + + private: + IEffect *pEffect; + Lv2PluginState lv2State; + std::vector inputControlValues; + std::vector pathPatchProperties; + }; + class IndexedSnapshot + { + public: + IndexedSnapshot(Snapshot *snapshot, std::shared_ptr currentPedalboard, PluginHost &pluginHost) + { + std::unordered_map index; + for (auto &value : snapshot->values_) + { + index[value.instanceId_] = &value; + } + std::vector &effects = currentPedalboard->GetEffects(); + snapshotValues.reserve(effects.size()); + + for (size_t i = 0; i < effects.size(); ++i) + { + auto &effect = effects[i]; + + SnapshotValue *snapshotValue = getSnapshotValue(index, effect->GetInstanceId()); + snapshotValues.push_back(IndexedSnapshotValue(effect, snapshotValue, pluginHost)); + } + } + static SnapshotValue *getSnapshotValue(std::unordered_map &index, uint64_t instanceId) + { + auto iter = index.find(instanceId); + if (iter == index.end()) + { + return nullptr; + } + return iter->second; + } + + void Apply(std::vector &effects) + { + if (effects.size() != snapshotValues.size()) + { + throw std::runtime_error("Effects and values don't match"); + } + for (size_t i = 0; i < snapshotValues.size(); ++i) + { + snapshotValues[i].ApplyValues(effects[i]); + } + } + ~IndexedSnapshot() + { + } + + private: + std::vector snapshotValues; + }; +} + class SystemMidiBinding { private: @@ -192,9 +357,12 @@ bool SystemMidiBinding::IsMatch(const MidiEvent &event) } } -class AudioHostImpl : public AudioHost, private AudioDriverHost +class AudioHostImpl : public AudioHost, private AudioDriverHost, private IPatchWriterCallback { private: + void OnWritePatchPropertyBuffer( + PatchPropertyWriter::Buffer *); + IHost *pHost = nullptr; LV2_Atom_Forge inputWriterForge; @@ -368,6 +536,8 @@ private: StopReaderThread(); + // delete any leaked snapshots. + CleanUpSnapshots(); // release any pdealboards owned by the process thread. this->activePedalboards.resize(0); @@ -489,8 +659,26 @@ private: RealtimePatchPropertyRequest *pParameterRequests = nullptr; + void cancelParameterRequests() + { + return; + + // auto p = this->pParameterRequests; + // while (pParameterRequests != nullptr) + // { + // auto nextParameterRequest = p->pNext; + // if (p->requestType == RealtimePatchPropertyRequest::RequestType::PatchGet) { + // p->errorMessage = "Effect unloaded."; + // } + // p = nextParameterRequest; + // } + // this->realtimeWriter.ParameterRequestComplete(pParameterRequests); + // this->pParameterRequests = nullptr; + } + bool reEntered = false; - void ProcessInputCommands() + // returns false if the current effect has been replaced. + bool ProcessInputCommands() { if (reEntered) { @@ -586,6 +774,14 @@ private: } break; } + case RingBufferCommand::LoadSnapshot: + { + IndexedSnapshot *snapshot; + realtimeReader.readComplete(&snapshot); + ApplySnapshot(snapshot); + realtimeWriter.FreeSnapshot(snapshot); + break; + } case RingBufferCommand::SetVuSubscriptions: { RealtimeVuBuffers *configuration; @@ -622,16 +818,35 @@ private: // invalidate the possibly no-good subscriptions. Model will update them shortly. freeRealtimeVuConfiguration(); freeRealtimeMonitorPortSubscriptions(); + cancelParameterRequests(); + + if (realtimeActivePedalboard) + { + realtimeActivePedalboard->ResetAtomBuffers(); + // issue patch gets for all writable path properties. + for (auto pEffect : realtimeActivePedalboard->GetEffects()) + { + pEffect->RequestAllPathPatchProperties(); + } + } } - break; + reEntered = false; + + return false; // signal to caller that the effect has been replaced, and processing needs to start again. } default: throw PiPedalStateException("Unknown Ringbuffer command."); } } reEntered = false; + return true; } + void ApplySnapshot(IndexedSnapshot *snapshot) + { + auto &effects = this->realtimeActivePedalboard->GetEffects(); + snapshot->Apply(effects); + } virtual void AckMidiProgramRequest(uint64_t requestId) { hostWriter.AckMidiProgramRequest(requestId); @@ -799,8 +1014,6 @@ private: std::mutex audioStoppedMutex; - - bool IsAudioRunning() { return this->active && (!this->audioStopped) && !this->isDummyAudioDriver; @@ -816,20 +1029,31 @@ private: Lv2Log::info("Audio thread terminated."); } - virtual void OnProcess(size_t nframes) { try { float *in, *out; - pParameterRequests = nullptr; + Lv2Pedalboard *pedalboard = nullptr; + pedalboard = this->realtimeActivePedalboard; + if (pedalboard) + { + pedalboard->ResetAtomBuffers(); + } + while (true) + { - ProcessInputCommands(); + if (ProcessInputCommands()) + { + break; + } + // else a new pedalboard was installed. start again. + pedalboard = this->realtimeActivePedalboard; + } bool processed = false; - Lv2Pedalboard *pedalboard = this->realtimeActivePedalboard; if (pedalboard != nullptr) { ProcessMidiInput(); @@ -862,7 +1086,6 @@ private: if (buffersValid) { - pedalboard->ResetAtomBuffers(); pedalboard->ProcessParameterRequests(pParameterRequests); processed = pedalboard->Run(inputBuffers, outputBuffers, (uint32_t)nframes, &realtimeWriter); @@ -885,6 +1108,7 @@ private: } } pedalboard->GatherPatchProperties(pParameterRequests); + pedalboard->GatherPathPatchProperties(this); } } @@ -896,6 +1120,7 @@ private: if (pParameterRequests != nullptr) { this->realtimeWriter.ParameterRequestComplete(pParameterRequests); + pParameterRequests = nullptr; } // provide a grace period for undderruns, while spinning up. (15 second-ish) if (currentSample <= this->overrunGracePeriodSamples && currentSample + nframes > this->overrunGracePeriodSamples) @@ -926,7 +1151,6 @@ public: { realtimeAtomBuffer.resize(32 * 1024); lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap()); - } virtual ~AudioHostImpl() { @@ -1193,6 +1417,18 @@ public: hostReader.read(&body); OnActivePedalboardReleased(body.oldEffect); } + else if (command == RingBufferCommand::FreeSnapshot) + { + IndexedSnapshot *snapshot; + hostReader.read(&snapshot); + OnFreeSnapshot(snapshot); + } + else if (command == RingBufferCommand::SendPathPropertyBuffer) + { + PatchPropertyWriter::Buffer *buffer = nullptr; + hostReader.read(&buffer); + OnPathPropertyReceived(buffer); + } else if (command == RingBufferCommand::AudioStopped) { AudioStoppedBody body; @@ -1200,7 +1436,8 @@ public: OnAudioComplete(); return; } - else if (command == RingBufferCommand::MidiProgramChange) + else if (command == RingBufferCommand:: + MidiProgramChange) { RealtimeMidiProgramRequest programRequest; hostReader.read(&programRequest); @@ -1307,7 +1544,9 @@ public: { this->isDummyAudioDriver = true; this->audioDriver = std::unique_ptr(CreateDummyAudioDriver(this)); - } else { + } + else + { this->isDummyAudioDriver = false; this->audioDriver = std::unique_ptr(CreateAlsaDriver(this)); } @@ -1320,7 +1559,6 @@ public: this->currentSample = 0; this->underruns = 0; - this->inputRingBuffer.reset(); this->outputRingBuffer.reset(); this->hostReader.Reset(); @@ -1364,6 +1602,18 @@ public: { pNotifyCallbacks->OnNotifyMidiProgramChange(programRequest); } + + void OnPathPropertyReceived(PatchPropertyWriter::Buffer *buffer) + { + if (pNotifyCallbacks) + { + pNotifyCallbacks->OnNotifyPathPatchPropertyReceived( + buffer->instanceId, + buffer->patchPropertyUrid, + (LV2_Atom *)(buffer->memory.data())); + } + buffer->OnBufferReadComplete(); + } void OnActivePedalboardReleased(Lv2Pedalboard *pPedalboard) { if (pPedalboard) @@ -1465,6 +1715,49 @@ public: } } + std::vector pendingSnapshots; + + virtual void LoadSnapshot(Snapshot &snapshot, PluginHost &pluginHost) override + { + std::lock_guard guard(mutex); + if (active && this->currentPedalboard) + { + IndexedSnapshot *indexedSnapshot = new IndexedSnapshot(&snapshot, this->currentPedalboard, pluginHost); + pendingSnapshots.push_back(indexedSnapshot); + this->hostWriter.LoadSnapshot(indexedSnapshot); + } + } + + void OnNotifyPathPatchPropertyReceived( + int64_t instanceId, + const std::string &pathPatchPropertyUri, + const std::string &jsonAtom) override; + + void OnFreeSnapshot(IndexedSnapshot *snapshot) + { + { + std::lock_guard guard(mutex); + for (auto i = pendingSnapshots.begin(); i != pendingSnapshots.end(); ++i) + { + if (*i == snapshot) + { + pendingSnapshots.erase(i); + break; + } + } + } + delete snapshot; + } + void CleanUpSnapshots() + { + for (auto i = pendingSnapshots.begin(); i != pendingSnapshots.end(); ++i) + { + IndexedSnapshot *snapshot = *i; + delete snapshot; + } + pendingSnapshots.clear(); + } + virtual void SetVuSubscriptions(const std::vector &instanceIds) { std::lock_guard guard(mutex); @@ -1754,19 +2047,25 @@ void AudioHostImpl::SetSystemMidiBindings(const std::vector &bindin else if (i->symbol() == "prevProgram") { this->prevMidiBinding.SetBinding(*i); - } else if (i->symbol() == "startHotspot") + } + else if (i->symbol() == "startHotspot") { this->startHotspotMidiBinding.SetBinding(*i); - } else if (i->symbol() == "stopHotspot") + } + else if (i->symbol() == "stopHotspot") { this->stopHotspotMidiBinding.SetBinding(*i); - } else if (i->symbol() == "reboot") + } + else if (i->symbol() == "reboot") { this->rebootMidiBinding.SetBinding(*i); - } else if (i->symbol() == "shutdown") + } + else if (i->symbol() == "shutdown") { this->shutdownMidiBinding.SetBinding(*i); - } else { + } + else + { Lv2Log::error(SS("Invalid system midi binding: " << i->symbol())); } } @@ -1793,6 +2092,27 @@ bool AudioHostImpl::UpdatePluginStates(Pedalboard &pedalboard) } return true; } + +void AudioHostImpl::OnNotifyPathPatchPropertyReceived( + int64_t instanceId, + const std::string &pathPatchPropertyUri, + const std::string &jsonAtom) +{ + if (this->currentPedalboard) + { + IEffect*effect = this->currentPedalboard->GetEffect(instanceId); + if (!effect) + { + return; + } + if (effect->IsLv2Effect()) + { + Lv2Effect*lv2Effect = (Lv2Effect*)effect; + lv2Effect->SetPathPatchProperty(pathPatchPropertyUri,jsonAtom); + } + } +} + bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem) { IEffect *effect = currentPedalboard->GetEffect(pedalboardItem.instanceId()); @@ -1821,6 +2141,12 @@ bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem) return false; } +void AudioHostImpl::OnWritePatchPropertyBuffer( + PatchPropertyWriter::Buffer *buffer) +{ + this->realtimeWriter.SendPathPropertyBuffer(buffer); +} + JSON_MAP_BEGIN(JackHostStatus) JSON_MAP_REFERENCE(JackHostStatus, active) JSON_MAP_REFERENCE(JackHostStatus, errorMessage) diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index d5a6803..ca68961 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -32,211 +32,218 @@ #include "json_variant.hpp" #include "RealtimeMidiEventType.hpp" -namespace pipedal { - - -struct RealtimeMidiProgramRequest; -struct RealtimeNextMidiProgramRequest; - -using PortMonitorCallback = std::function; - - -class MonitorPortUpdate +namespace pipedal { -public: - PortMonitorCallback *callbackPtr; // pointer because function<>'s probably aren't POD. - int64_t subscriptionHandle; - float value; -}; -class RealtimePatchPropertyRequest { -public: - int64_t clientId; - int64_t instanceId; - LV2_URID uridUri; - enum class RequestType { - PatchGet, - PatchSet - }; - RequestType requestType; + struct RealtimeMidiProgramRequest; + struct RealtimeNextMidiProgramRequest; + class PluginHost; + class Pedalboard; - std::function onPatchRequestComplete; - std::function onSuccess; - std::function onError; + using PortMonitorCallback = std::function; - const char*errorMessage = nullptr; - std::string jsonResponse; - - RealtimePatchPropertyRequest *pNext = nullptr; - - void SetSize(size_t size) + class MonitorPortUpdate { - responseLength = size; - if (responseLength > sizeof(atomBuffer)) - { - longAtomBuffer.resize(size); - } - } - size_t GetSize() const { return responseLength; } - uint8_t *GetBuffer() { - if (responseLength > sizeof(atomBuffer)) - { - return &(longAtomBuffer[0]); - } - return atomBuffer; - } -private: - int responseLength = 0; - uint8_t atomBuffer[2048]; - std::vector longAtomBuffer; + public: + PortMonitorCallback *callbackPtr; // pointer because function<>'s probably aren't POD. + int64_t subscriptionHandle; + float value; + }; + class RealtimePatchPropertyRequest + { + public: + int64_t clientId; + int64_t instanceId; + LV2_URID uridUri; -public: - RealtimePatchPropertyRequest( - std::function onPatchRequestcomplete_, - int64_t clientId_, - int64_t instanceId_, - LV2_URID uridUri_, - std::function onSuccess_, - std::function onError_ - ) - : onPatchRequestComplete(onPatchRequestcomplete_), - clientId(clientId_), - instanceId(instanceId_), - uridUri(uridUri_), - onSuccess(onSuccess_), - onError(onError_) + enum class RequestType { - requestType = RequestType::PatchGet; + PatchGet, + PatchSet + }; + RequestType requestType; + + std::function onPatchRequestComplete; + std::function onSuccess; + std::function onError; + + const char *errorMessage = nullptr; + std::string jsonResponse; + + RealtimePatchPropertyRequest *pNext = nullptr; + + void SetSize(size_t size) + { + responseLength = size; + if (responseLength > sizeof(atomBuffer)) + { + longAtomBuffer.resize(size); + } } - RealtimePatchPropertyRequest( - std::function onPatchRequestcomplete_, - int64_t clientId_, - int64_t instanceId_, - LV2_URID uridUri_, - LV2_Atom*atomValue, - std::function onSuccess_, - std::function onError_ - ) - : onPatchRequestComplete(onPatchRequestcomplete_), - clientId(clientId_), - instanceId(instanceId_), - uridUri(uridUri_), - onSuccess(onSuccess_), - onError(onError_) + size_t GetSize() const { return responseLength; } + uint8_t *GetBuffer() + { + if (responseLength > sizeof(atomBuffer)) + { + return &(longAtomBuffer[0]); + } + return atomBuffer; + } + + private: + int responseLength = 0; + uint8_t atomBuffer[2048]; + std::vector longAtomBuffer; + + public: + RealtimePatchPropertyRequest( + std::function onPatchRequestcomplete_, + int64_t clientId_, + int64_t instanceId_, + LV2_URID uridUri_, + std::function onSuccess_, + std::function onError_) + : onPatchRequestComplete(onPatchRequestcomplete_), + clientId(clientId_), + instanceId(instanceId_), + uridUri(uridUri_), + onSuccess(onSuccess_), + onError(onError_) + { + requestType = RequestType::PatchGet; + } + RealtimePatchPropertyRequest( + std::function onPatchRequestcomplete_, + int64_t clientId_, + int64_t instanceId_, + LV2_URID uridUri_, + LV2_Atom *atomValue, + std::function onSuccess_, + std::function onError_) + : onPatchRequestComplete(onPatchRequestcomplete_), + clientId(clientId_), + instanceId(instanceId_), + uridUri(uridUri_), + onSuccess(onSuccess_), + onError(onError_) { requestType = RequestType::PatchSet; - size_t size = atomValue->size+ sizeof(LV2_Atom); + size_t size = atomValue->size + sizeof(LV2_Atom); SetSize(size); - memcpy(GetBuffer(),atomValue,size); + memcpy(GetBuffer(), atomValue, size); } + }; -}; + class MonitorPortSubscription + { + public: + int64_t subscriptionHandle; + int64_t instanceid; + std::string key; + float updateInterval; + PortMonitorCallback onUpdate; + }; + class IAudioHostCallbacks + { + public: + virtual void OnNotifyLv2StateChanged(uint64_t instanceId) = 0; + virtual void OnNotifyMaybeLv2StateChanged(uint64_t instanceId) = 0; + virtual void OnNotifyVusSubscription(const std::vector &updates) = 0; + virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) = 0; + virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) = 0; + virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) = 0; -class MonitorPortSubscription { -public: - int64_t subscriptionHandle; - int64_t instanceid; - std::string key; - float updateInterval; - PortMonitorCallback onUpdate; + virtual void OnNotifyPathPatchPropertyReceived( + int64_t instanceId, + LV2_URID pathPatchProperty, + LV2_Atom *pathProperty) = 0; -}; + virtual void OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) = 0; -class IAudioHostCallbacks { -public: - virtual void OnNotifyLv2StateChanged(uint64_t instanceId) = 0; - virtual void OnNotifyMaybeLv2StateChanged(uint64_t instanceId) = 0; - virtual void OnNotifyVusSubscription(const std::vector & updates) = 0; - virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) = 0; - virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) = 0; - virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) = 0; + // virtual bool WantsAtomOutput(uint64_t instanceId,LV2_URID patchSetProperty) = 0; + // virtual void OnNotifyPatchProperty(uint64_t instanceId, LV2_URID patchSetProperty, const std::string&atomJson) = 0; + virtual void OnNotifyMidiProgramChange(RealtimeMidiProgramRequest &midiProgramRequest) = 0; + virtual void OnNotifyNextMidiProgram(const RealtimeNextMidiProgramRequest &request) = 0; + virtual void OnNotifyLv2RealtimeError(int64_t instanceId, const std::string &error) = 0; + virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) = 0; + }; - virtual void OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom*atomValue) = 0; + class JackHostStatus + { + public: + bool active_ = false; + std::string errorMessage_; + bool restarting_; + uint64_t underruns_; + float cpuUsage_ = 0; + uint64_t msSinceLastUnderrun_ = 0; + int32_t temperaturemC_ = -100000; + uint64_t cpuFreqMax_ = 0; + uint64_t cpuFreqMin_ = 0; + std::string governor_; - //virtual bool WantsAtomOutput(uint64_t instanceId,LV2_URID patchSetProperty) = 0; - //virtual void OnNotifyPatchProperty(uint64_t instanceId, LV2_URID patchSetProperty, const std::string&atomJson) = 0; + DECLARE_JSON_MAP(JackHostStatus); + }; - virtual void OnNotifyMidiProgramChange(RealtimeMidiProgramRequest&midiProgramRequest) = 0; - virtual void OnNotifyNextMidiProgram(const RealtimeNextMidiProgramRequest&request) = 0; - virtual void OnNotifyLv2RealtimeError(int64_t instanceId,const std::string &error) = 0; - virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) = 0; + class IHost; -}; + class AudioHost + { + protected: + AudioHost() {} + public: + static AudioHost *CreateInstance(IHost *pHost); + virtual ~AudioHost() {}; -class JackHostStatus { -public: - bool active_ = false; - std::string errorMessage_; - bool restarting_; - uint64_t underruns_; - float cpuUsage_ = 0; - uint64_t msSinceLastUnderrun_ = 0; - int32_t temperaturemC_ = -100000; - uint64_t cpuFreqMax_ = 0; - uint64_t cpuFreqMin_ = 0; - std::string governor_; + virtual void UpdateServerConfiguration(const JackServerSettings &jackServerSettings, + std::function onComplete) = 0; - DECLARE_JSON_MAP(JackHostStatus); + virtual void SetNotificationCallbacks(IAudioHostCallbacks *pNotifyCallbacks) = 0; + virtual void SetListenForMidiEvent(bool listen) = 0; + virtual void SetListenForAtomOutput(bool listen) = 0; -}; + virtual bool UpdatePluginStates(Pedalboard &pedalboard) = 0; + virtual bool UpdatePluginState(PedalboardItem &pedalboardItem) = 0; + virtual std::string AtomToJson(const LV2_Atom *atom) = 0; -class IHost; + virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) = 0; + virtual void Close() = 0; -class AudioHost { -protected: - AudioHost() { } -public: - static AudioHost*CreateInstance(IHost *pHost); - virtual ~AudioHost() { }; + virtual uint32_t GetSampleRate() = 0; - virtual void UpdateServerConfiguration(const JackServerSettings & jackServerSettings, - std::function onComplete) = 0; + virtual JackConfiguration GetServerConfiguration() = 0; - virtual void SetNotificationCallbacks(IAudioHostCallbacks *pNotifyCallbacks) = 0; + virtual void SetPedalboard(const std::shared_ptr &pedalboard) = 0; - virtual void SetListenForMidiEvent(bool listen) = 0; - virtual void SetListenForAtomOutput(bool listen) = 0; + virtual void SetControlValue(uint64_t instanceId, const std::string &symbol, float value) = 0; + virtual void SetInputVolume(float value) = 0; + virtual void SetOutputVolume(float value) = 0; + virtual void SetPluginPreset(uint64_t instanceId, const std::vector &values) = 0; + virtual void SetBypass(uint64_t instanceId, bool enabled) = 0; - virtual bool UpdatePluginStates(Pedalboard& pedalboard) = 0; - virtual bool UpdatePluginState(PedalboardItem& pedalboardItem) = 0; + virtual bool IsOpen() const = 0; - virtual std::string AtomToJson(const LV2_Atom*atom) = 0; + virtual void SetVuSubscriptions(const std::vector &instanceIds) = 0; + virtual void SetMonitorPortSubscriptions(const std::vector &subscriptions) = 0; - virtual void Open(const JackServerSettings&jackServerSettings,const JackChannelSelection & channelSelection) = 0; - virtual void Close() = 0; + virtual void SetSystemMidiBindings(const std::vector &bindings) = 0; - virtual uint32_t GetSampleRate() = 0; + virtual void sendRealtimeParameterRequest(RealtimePatchPropertyRequest *pParameterRequest) = 0; + virtual void AckMidiProgramRequest(uint64_t requestId) = 0; - virtual JackConfiguration GetServerConfiguration() = 0; + virtual JackHostStatus getJackStatus() = 0; - virtual void SetPedalboard(const std::shared_ptr &pedalboard) = 0; + virtual void LoadSnapshot(Snapshot &snapshot, PluginHost &pluginHost) = 0; - virtual void SetControlValue(uint64_t instanceId,const std::string&symbol, float value) = 0; - virtual void SetInputVolume(float value) = 0; - virtual void SetOutputVolume(float value) = 0; - virtual void SetPluginPreset(uint64_t instanceId, const std::vector &values) = 0; - virtual void SetBypass(uint64_t instanceId, bool enabled) = 0; + virtual void OnNotifyPathPatchPropertyReceived( + int64_t instanceId, + const std::string &pathPatchPropertyUri, + const std::string &jsonAtom) = 0; + }; - virtual bool IsOpen() const = 0; - - virtual void SetVuSubscriptions(const std::vector &instanceIds) = 0; - virtual void SetMonitorPortSubscriptions(const std::vector &subscriptions) = 0; - - virtual void SetSystemMidiBindings(const std::vector&bindings) = 0; - - virtual void sendRealtimeParameterRequest(RealtimePatchPropertyRequest*pParameterRequest) = 0; - virtual void AckMidiProgramRequest(uint64_t requestId) = 0; - - virtual JackHostStatus getJackStatus() = 0; - -}; - - - - -} //namespace pipedal. \ No newline at end of file +} // namespace pipedal. \ No newline at end of file diff --git a/src/Banks.hpp b/src/Banks.hpp index def9ab1..e8d25e8 100644 --- a/src/Banks.hpp +++ b/src/Banks.hpp @@ -100,6 +100,7 @@ public: presets_.clear(); selectedPreset_ = -1; } + void move(size_t from, size_t to) { if (from >= this->presets_.size()) { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 95eb98d..25a20d4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -146,6 +146,8 @@ else() endif() set (PIPEDAL_SOURCES + PatchPropertyWriter.hpp + PresetBundle.cpp PresetBundle.hpp RealtimeMidiEventType.hpp DBusToLv2Log.cpp DBusToLv2Log.hpp HotspotManager.cpp HotspotManager.hpp diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index f3d860a..9c953ca 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -691,6 +691,10 @@ void SetVarPermissions( { if (fs::exists(path)) { + if (fs::is_symlink(path)) + { + return; + } std::ignore = chown(path.c_str(), uid, gid); if (fs::is_directory(path)) { diff --git a/src/IEffect.hpp b/src/IEffect.hpp index ae4ae12..520c19b 100644 --- a/src/IEffect.hpp +++ b/src/IEffect.hpp @@ -32,8 +32,19 @@ namespace pipedal { public: virtual ~IEffect() {} virtual uint64_t GetInstanceId() const = 0; + virtual bool IsLv2Effect() const = 0; + virtual uint64_t GetMaxInputControl() const = 0; + virtual bool IsInputControl(uint64_t index) const = 0; + virtual float GetDefaultInputControlValue(uint64_t index) const = 0; + virtual int GetControlIndex(const std::string&symbol) const = 0; virtual void SetControl(int index, float value) = 0; + + virtual void SetPatchProperty(LV2_URID uridUri, size_t size, LV2_Atom *value) = 0; + virtual void RequestPatchProperty(LV2_URID uridUri) = 0; + virtual void RequestAllPathPatchProperties() = 0; + + virtual float GetControlValue(int index) const = 0; virtual void SetBypass(bool enable) = 0; virtual float GetOutputControlValue(int controlIndex) const = 0; @@ -47,9 +58,6 @@ namespace pipedal { virtual bool GetRequestStateChangedNotification() const = 0; virtual void SetRequestStateChangedNotification(bool value) = 0; - //virtual std::string AtomToJson(uint8_t*pAtom) = 0; - //virtual std::string GetAtomObjectType(uint8_t*pData) = 0; - virtual void SetAudioInputBuffer(int index, float *buffer) = 0; virtual void SetAudioOutputBuffer(int index, float*buffer) = 0; @@ -62,6 +70,7 @@ namespace pipedal { virtual bool IsVst3() const = 0; virtual bool GetLv2State(Lv2PluginState*state) = 0; + virtual void SetLv2State(Lv2PluginState&state) = 0; virtual bool HasErrorMessage() const = 0; virtual const char*TakeErrorMessage() = 0; diff --git a/src/Lv2Effect.cpp b/src/Lv2Effect.cpp index 6f096d5..9df333e 100644 --- a/src/Lv2Effect.cpp +++ b/src/Lv2Effect.cpp @@ -50,7 +50,7 @@ Lv2Effect::Lv2Effect( IHost *pHost_, const std::shared_ptr &info_, PedalboardItem &pedalboardItem) - : pHost(pHost_), pInstance(nullptr), info(info_), urids(pHost) + : pHost(pHost_), pInstance(nullptr), info(info_), urids(pHost), instanceId(pedalboardItem.instanceId()) { auto pWorld = pHost_->getWorld(); @@ -60,6 +60,23 @@ Lv2Effect::Lv2Effect( this->bypass = pedalboardItem.isEnabled(); + // stash a list of known file properties that we want to keep synced. + if (info->piPedalUI()) + { + for (auto fileProperty : info->piPedalUI()->fileProperties()) + { + LV2_URID filePropertyUrid = pHost->GetLv2Urid(fileProperty->patchProperty().c_str()); + this->pathProperties.push_back(filePropertyUrid); + + this->pathPropertyWriters.push_back(PatchPropertyWriter(instanceId, filePropertyUrid)); + std::vector pathPropertyWriters; + } + } + for (auto &pathProperty: pedalboardItem.pathProperties_) + { + SetPathPatchProperty(pathProperty.first,pathProperty.second); + } + // initialize the atom forge used on the realtime thread. LV2_URID_Map *map = this->pHost->GetLv2UridMap(); lv2_atom_forge_init(&inputForgeRt, map); @@ -146,9 +163,28 @@ Lv2Effect::Lv2Effect( this->instanceId = pedalboardItem.instanceId(); - this->controlValues.resize(info->ports().size()); + PreparePortIndices(); // Copy default pedalboard settings. + size_t maxPortIndex = 0; + std::vector> &t = info->ports(); + for (std::shared_ptr &port : info->ports()) + { + if (port->is_control_port()) + { + auto index = port->index(); + if (index + 1 > maxPortIndex) + { + maxPortIndex = index + 1; + } + } + } + if (maxPortIndex > info->ports().size()) + { + throw std::runtime_error("Ports are not consecutive"); + } + + this->controlValues.resize(info->ports().size()); for (auto i = pedalboardItem.controlValues().begin(); i != pedalboardItem.controlValues().end(); ++i) { auto &v = (*i); @@ -158,7 +194,7 @@ Lv2Effect::Lv2Effect( this->controlValues[index] = v.value(); } } - PreparePortIndices(); + ConnectControlPorts(); if (!pedalboardItem.lilvPresetUri().empty()) @@ -194,7 +230,9 @@ void Lv2Effect::RestoreState(PedalboardItem &pedalboardItem) if (pedalboardItem.lv2State().isValid_) { this->stateInterface->Restore(pedalboardItem.lv2State()); - } else { + } + else + { // set the state to default state. auto savedState = this->stateInterface->Save(); pedalboardItem.lv2State(savedState); @@ -234,6 +272,9 @@ void Lv2Effect::ConnectControlPorts() } void Lv2Effect::PreparePortIndices() { + size_t nPorts = info->ports().size(); + isInputControlPort.resize(nPorts); + this->defaultInputControlValues.resize(nPorts); for (int i = 0; i < info->ports().size(); ++i) { @@ -270,7 +311,23 @@ void Lv2Effect::PreparePortIndices() } } } + else if (port->is_control_port()) + { + controlIndex[port->symbol()] = port->index(); + if (port->is_input()) + { + this->isInputControlPort[i] = true; + this->defaultInputControlValues[i] = port->default_value(); + } + } } + size_t maxInputControlPort = isInputControlPort.size(); + while (maxInputControlPort != 0 && !isInputControlPort[maxInputControlPort - 1]) + { + --maxInputControlPort; + } + this->maxInputControlPort = maxInputControlPort; + inputAudioBuffers.resize(inputAudioPortIndices.size()); outputAudioBuffers.resize(outputAudioPortIndices.size()); inputAtomBuffers.resize(inputAtomPortIndices.size()); @@ -323,13 +380,12 @@ void Lv2Effect::SetAudioOutputBuffer(int index, float *buffer) int Lv2Effect::GetControlIndex(const std::string &key) const { - for (int i = 0; i < info->ports().size(); ++i) + auto i = controlIndex.find(key); + if (i == controlIndex.end()) { - auto &port = info->ports()[i]; - if (port->symbol() == key) - return port->index(); + return -1; } - return -1; + return i->second; } Lv2Effect::~Lv2Effect() @@ -438,7 +494,6 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff worker->EmitResponses(); } - // do soft bypass. if (this->bypassSamplesRemaining == 0) { @@ -610,6 +665,14 @@ void Lv2Effect::RequestPatchProperty(LV2_URID uridUri) lv2_atom_forge_urid(&inputForgeRt, uridUri); lv2_atom_forge_pop(&inputForgeRt, &objectFrame); } +void Lv2Effect::RequestAllPathPatchProperties() +{ + for (LV2_URID urid : this->pathProperties) + { + RequestPatchProperty(urid); + } +} + void Lv2Effect::SetPatchProperty(LV2_URID uridUri, size_t size, LV2_Atom *value) { lv2_atom_forge_frame_time(&inputForgeRt, 0); @@ -660,12 +723,68 @@ void Lv2Effect::RelayPatchSetMessages(uint64_t instanceId, RealtimeRingBufferWri { requestStateChangedNotification = false; realtimeRingBufferWriter->Lv2StateChanged(instanceId); - } else if (maybeStateChanged) + } + else if (maybeStateChanged) { realtimeRingBufferWriter->MaybeLv2StateChanged(instanceId); } } +void Lv2Effect::GatherPathPatchProperties(IPatchWriterCallback *cbPatchWriter) +{ + if (pathPropertyWriters.size() != 0) + { + LV2_Atom_Sequence *controlInput = (LV2_Atom_Sequence *)GetAtomOutputBuffer(); + if (controlInput == nullptr) + { + return; + } + LV2_ATOM_SEQUENCE_FOREACH(controlInput, ev) + { + + auto frame_offset = ev->time.frames; // not really interested. + + if (lv2_atom_forge_is_object_type(&this->outputForgeRt, ev->body.type)) + { + const LV2_Atom_Object *obj = (const LV2_Atom_Object *)&ev->body; + if (obj->body.otype == urids.patch__Set) + { + // Get the property and value of the set message + const LV2_Atom *property = NULL; + const LV2_Atom *value = NULL; + + lv2_atom_object_get( + obj, + urids.patch__property, &property, + urids.patch__value, &value, + 0); + + if (property && property->type == urids.atom__URID && value) + { + LV2_URID key = ((const LV2_Atom_URID *)property)->body; + + for (PatchPropertyWriter &pathPropertyWriter : pathPropertyWriters) + { + if (key == pathPropertyWriter.patchPropertyUrid) + { + auto buffer = pathPropertyWriter.AquireWriteBuffer(); + size_t atom_size = value->size + sizeof(LV2_Atom); + buffer->memory.resize(atom_size); + memcpy(buffer->memory.data(), value, atom_size); + break; + } + } + } + } + } + } + for (auto &writer : this->pathPropertyWriters) + { + writer.FlushWrites(cbPatchWriter); + } + } +} + void Lv2Effect::GatherPatchProperties(RealtimePatchPropertyRequest *pRequest) { if (pRequest->requestType == RealtimePatchPropertyRequest::RequestType::PatchGet) @@ -711,6 +830,27 @@ void Lv2Effect::GatherPatchProperties(RealtimePatchPropertyRequest *pRequest) } } } + +void Lv2Effect::SetLv2State(Lv2PluginState &state) +{ + if (state.isValid_) + { + return; + } + if (!this->stateInterface) + { + return; + } + try + { + + this->stateInterface->Restore(state); + } + catch (const std::exception &e) + { + Lv2Log::error("Failed to restore LV2 state."); + } +} bool Lv2Effect::GetLv2State(Lv2PluginState *state) { if (!this->stateInterface) @@ -755,5 +895,31 @@ void Lv2Effect::OnLogDebug(const char *message) Lv2Log::debug(message); } -bool Lv2Effect::GetRequestStateChangedNotification() const{ return requestStateChangedNotification; } +bool Lv2Effect::GetRequestStateChangedNotification() const { return requestStateChangedNotification; } void Lv2Effect::SetRequestStateChangedNotification(bool value) { requestStateChangedNotification = value; } + +uint64_t Lv2Effect::GetMaxInputControl() const { return maxInputControlPort; } + +bool Lv2Effect::IsInputControl(uint64_t index) const +{ + if (index < 0 || index >= isInputControlPort.size()) + return false; + return isInputControlPort[index]; +} +float Lv2Effect::GetDefaultInputControlValue(uint64_t index) const +{ + return defaultInputControlValues[index]; +} + +std::string Lv2Effect::GetPathPatchProperty(const std::string &propertyUri) +{ + if (!this->mainThreadPathProperties.contains(propertyUri)) + { + return ""; + } + return this->mainThreadPathProperties[propertyUri]; +} +void Lv2Effect::SetPathPatchProperty(const std::string &propertyUri, const std::string &jsonAtom) +{ + mainThreadPathProperties[propertyUri] = jsonAtom; +} diff --git a/src/Lv2Effect.hpp b/src/Lv2Effect.hpp index 5a84ec3..2fed630 100644 --- a/src/Lv2Effect.hpp +++ b/src/Lv2Effect.hpp @@ -25,6 +25,8 @@ #include #include "BufferPool.hpp" #include "FileBrowserFilesFeature.hpp" +#include "PatchPropertyWriter.hpp" +#include #include "IEffect.hpp" #include "Worker.hpp" @@ -42,6 +44,7 @@ namespace pipedal { class RealtimeRingBufferWriter; + class IPatchWriterCallback; class Lv2Effect : public IEffect, private LogFeature::LogMessageListener { @@ -52,6 +55,8 @@ namespace pipedal virtual void OnLogDebug(const char*message); private: + std::unordered_map controlIndex; + FileBrowserFilesFeature fileBrowserFilesFeature; std::unique_ptr stateInterface; void RestoreState(PedalboardItem&pedalboardItem); @@ -86,6 +91,11 @@ namespace pipedal std::vector features; LV2_Feature *work_schedule_feature = nullptr; + + uint64_t maxInputControlPort = 0; + std::vector isInputControlPort; + std::vector defaultInputControlValues; + virtual std::string GetUri() const { return info->uri(); } std::vector realtimePortInfo; @@ -99,6 +109,10 @@ namespace pipedal LV2_Atom_Forge outputForgeRt; + std::vector pathProperties; + std::vector pathPropertyWriters; + + std::unordered_map mainThreadPathProperties; class Urids { @@ -164,6 +178,7 @@ namespace pipedal uint32_t size, const void *data); + void ResetInputAtomBuffer(char*data); void ResetOutputAtomBuffer(char*data); @@ -179,17 +194,29 @@ namespace pipedal void BypassTo(float value); + public: - virtual bool GetLv2State(Lv2PluginState*state); - virtual void RequestPatchProperty(LV2_URID uridUri); - virtual void SetPatchProperty(LV2_URID uridUri,size_t size, LV2_Atom*value); - virtual bool GetRequestStateChangedNotification() const; - virtual void SetRequestStateChangedNotification(bool value); + // non RT-thread use only. + std::string GetPathPatchProperty(const std::string&propertyUri); + // non RT-thread use only. + void SetPathPatchProperty(const std::string &propertyUri, const std::string&jsonAtom); + + virtual bool IsLv2Effect() const { return true; } + virtual bool GetLv2State(Lv2PluginState*state) override; + virtual void SetLv2State(Lv2PluginState&state) override; + + virtual void RequestPatchProperty(LV2_URID uridUri) ; + virtual void SetPatchProperty(LV2_URID uridUri,size_t size, LV2_Atom*value) override; + virtual void RequestAllPathPatchProperties(); + + virtual bool GetRequestStateChangedNotification() const override; + virtual void SetRequestStateChangedNotification(bool value) override; virtual void GatherPatchProperties(RealtimePatchPropertyRequest*pRequest); + void GatherPathPatchProperties(IPatchWriterCallback *cbPatchWriter); virtual bool IsVst3() const { return false; } - virtual void RelayPatchSetMessages(uint64_t instanceId,RealtimeRingBufferWriter *realtimeRingBufferWriter); + virtual void RelayPatchSetMessages(uint64_t instanceId,RealtimeRingBufferWriter *realtimeRingBufferWriter) ; virtual uint8_t*GetAtomInputBuffer() { if (this->inputAtomBuffers.size() == 0) return nullptr; @@ -240,7 +267,7 @@ namespace pipedal virtual int GetControlIndex(const std::string &symbol) const; - virtual void SetControl(int index, float value) + virtual void SetControl(int index, float value) override { if (index == -1) { @@ -250,6 +277,11 @@ namespace pipedal } } + virtual uint64_t GetMaxInputControl() const override; + virtual bool IsInputControl(uint64_t index) const override; + virtual float GetDefaultInputControlValue(uint64_t index) const override; + + virtual float GetControlValue(int index) const { if (index == -1) { diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 129e9b4..44d8697 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -507,6 +507,19 @@ void Lv2Pedalboard::ResetAtomBuffers() } } + +void Lv2Pedalboard::GatherPathPatchProperties(IPatchWriterCallback*cbPatchWriter) +{ + for (auto& pEffect : this->effects) { + if (pEffect->IsLv2Effect()) + { + Lv2Effect *pLv2Effect = (Lv2Effect*)pEffect.get(); + pLv2Effect->GatherPathPatchProperties(cbPatchWriter); + } + } +} + + void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests) { while (pParameterRequests != nullptr) @@ -522,25 +535,29 @@ void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pPara } else { - Lv2Effect *pLv2Effect = dynamic_cast(pEffect); + if (pEffect->IsLv2Effect()) + { + Lv2Effect *pLv2Effect = dynamic_cast(pEffect); - if (pParameterRequests->requestType == RealtimePatchPropertyRequest::RequestType::PatchGet) - { - pLv2Effect->RequestPatchProperty(pParameterRequests->uridUri); - } - else if (pParameterRequests->requestType == RealtimePatchPropertyRequest::RequestType::PatchSet) - { - pLv2Effect->SetPatchProperty( - pParameterRequests->uridUri, - pParameterRequests->GetSize(), - (LV2_Atom *)pParameterRequests->GetBuffer()); - pLv2Effect->SetRequestStateChangedNotification(true); + if (pParameterRequests->requestType == RealtimePatchPropertyRequest::RequestType::PatchGet) + { + pLv2Effect->RequestPatchProperty(pParameterRequests->uridUri); + } + else if (pParameterRequests->requestType == RealtimePatchPropertyRequest::RequestType::PatchSet) + { + pLv2Effect->SetPatchProperty( + pParameterRequests->uridUri, + pParameterRequests->GetSize(), + (LV2_Atom *)pParameterRequests->GetBuffer()); + pLv2Effect->SetRequestStateChangedNotification(true); + } } } pParameterRequests = pParameterRequests->pNext; } } + void Lv2Pedalboard::GatherPatchProperties(RealtimePatchPropertyRequest *pParameterRequests) { while (pParameterRequests != nullptr) @@ -558,8 +575,11 @@ void Lv2Pedalboard::GatherPatchProperties(RealtimePatchPropertyRequest *pParamet } else { - Lv2Effect *pLv2Effect = dynamic_cast(effect); - pLv2Effect->GatherPatchProperties(pParameterRequests); + if (effect->IsLv2Effect()) + { + Lv2Effect *pLv2Effect = dynamic_cast(effect); + pLv2Effect->GatherPatchProperties(pParameterRequests); + } } } diff --git a/src/Lv2Pedalboard.hpp b/src/Lv2Pedalboard.hpp index f78cfe0..ee949f8 100644 --- a/src/Lv2Pedalboard.hpp +++ b/src/Lv2Pedalboard.hpp @@ -27,140 +27,142 @@ #include #include "DbDezipper.hpp" -namespace pipedal { - - - -class RealtimeVuBuffers; -class RealtimePatchPropertyRequest; -class RealtimeRingBufferWriter; - -struct Lv2PedalboardError { - int64_t intanceId; - std::string message; -}; - -class Lv2PedalboardErrorList: public std::vector // (forward declaration issues with a using statement) +namespace pipedal { -}; + class IPatchWriterCallback; + class RealtimeVuBuffers; + class RealtimePatchPropertyRequest; + class RealtimeRingBufferWriter; -class Lv2Pedalboard { - IHost *pHost = nullptr; - - DbDezipper inputVolume; - DbDezipper outputVolume; - - BufferPool bufferPool; - std::vector pedalboardInputBuffers; - std::vector pedalboardOutputBuffers; - - std::vector > effects; - std::vector realtimeEffects; // std::shared_ptr is not thread-safe!! - - using Action = std::function; - using ProcessAction = std::function; - - std::vector activateActions; - - std::vector processActions; - - std::vector deactivateActions; - - float *CreateNewAudioBuffer(); - - RealtimeRingBufferWriter *ringBufferWriter; - - enum class MappingType { - Linear, - Circular, - Momentary, - Latched, - }; - class MidiMapping { - public: - std::shared_ptr pluginInfo; // lifecycle - const Lv2PortInfo *pPortInfo = nullptr; // owned by port. - int instanceId = -1; - int effectIndex = -1; - int controlIndex = -1; - int key; // key to the note or control. internal use only. - bool hasLastValue = false; - float lastValue = -1; - MappingType mappingType; - MidiBinding midiBinding; + struct Lv2PedalboardError + { + int64_t intanceId; + std::string message; }; - std::vector midiMappings; + class Lv2PedalboardErrorList : public std::vector // (forward declaration issues with a using statement) + { + }; + class Lv2Pedalboard + { + IHost *pHost = nullptr; - std::vector PrepareItems( - std::vector & items, - std::vector inputBuffers, - Lv2PedalboardErrorList &errorList - ); + DbDezipper inputVolume; + DbDezipper outputVolume; - void PrepareMidiMap(const Pedalboard&pedalboard); - void PrepareMidiMap(const PedalboardItem&pedalboardItem); + BufferPool bufferPool; + std::vector pedalboardInputBuffers; + std::vector pedalboardOutputBuffers; - std::vector AllocateAudioBuffers(int nChannels); - int CalculateChainInputs(const std::vector &inputBuffers, const std::vector &items); - void AppendParameterRequest(uint8_t*atomBuffer, LV2_URID uridParameter); -public: - Lv2Pedalboard() { } - ~Lv2Pedalboard() { } + std::vector> effects; + std::vector realtimeEffects; - void Prepare(IHost *pHost,Pedalboard&pedalboard, Lv2PedalboardErrorList &errorList); + using Action = std::function; + using ProcessAction = std::function; - std::vector GetEffects() { return realtimeEffects; } + std::vector activateActions; - int GetIndexOfInstanceId(uint64_t instanceId) { - for (int i = 0; i < this->realtimeEffects.size(); ++i) + std::vector processActions; + + std::vector deactivateActions; + + float *CreateNewAudioBuffer(); + + RealtimeRingBufferWriter *ringBufferWriter; + + enum class MappingType { - if (this->realtimeEffects[i]->GetInstanceId() == instanceId) return i; - } - return -1; - } - IEffect*GetEffect(uint64_t instanceId) { - for (int i = 0; i < realtimeEffects.size(); ++i) { - if (realtimeEffects[i]->GetInstanceId() == instanceId) + Linear, + Circular, + Momentary, + Latched, + }; + class MidiMapping + { + public: + std::shared_ptr pluginInfo; // lifecycle + const Lv2PortInfo *pPortInfo = nullptr; // owned by port. + int instanceId = -1; + int effectIndex = -1; + int controlIndex = -1; + int key; // key to the note or control. internal use only. + bool hasLastValue = false; + float lastValue = -1; + MappingType mappingType; + MidiBinding midiBinding; + }; + + std::vector midiMappings; + + std::vector PrepareItems( + std::vector &items, + std::vector inputBuffers, + Lv2PedalboardErrorList &errorList); + + void PrepareMidiMap(const Pedalboard &pedalboard); + void PrepareMidiMap(const PedalboardItem &pedalboardItem); + + std::vector AllocateAudioBuffers(int nChannels); + int CalculateChainInputs(const std::vector &inputBuffers, const std::vector &items); + void AppendParameterRequest(uint8_t *atomBuffer, LV2_URID uridParameter); + + public: + Lv2Pedalboard() {} + ~Lv2Pedalboard() {} + + void Prepare(IHost *pHost, Pedalboard &pedalboard, Lv2PedalboardErrorList &errorList); + + std::vector &GetEffects() { return realtimeEffects; } + + int GetIndexOfInstanceId(uint64_t instanceId) + { + for (int i = 0; i < this->realtimeEffects.size(); ++i) { - return realtimeEffects[i]; + if (this->realtimeEffects[i]->GetInstanceId() == instanceId) + return i; } + return -1; } - return nullptr; - } - void Activate(); - void Deactivate(); - bool Run(float**inputBuffers, float**outputBuffers,uint32_t samples, RealtimeRingBufferWriter*realtimeWriter); + IEffect *GetEffect(uint64_t instanceId) + { + for (int i = 0; i < realtimeEffects.size(); ++i) + { + if (realtimeEffects[i]->GetInstanceId() == instanceId) + { + return realtimeEffects[i]; + } + } + return nullptr; + } + void Activate(); + void Deactivate(); + bool Run(float **inputBuffers, float **outputBuffers, uint32_t samples, RealtimeRingBufferWriter *realtimeWriter); - void ResetAtomBuffers(); + void ResetAtomBuffers(); + void ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests); + void GatherPatchProperties(RealtimePatchPropertyRequest *pParameterRequests); + void GatherPathPatchProperties(IPatchWriterCallback *cbPatchWriter); - void ProcessParameterRequests(RealtimePatchPropertyRequest *pParameterRequests); - void GatherPatchProperties(RealtimePatchPropertyRequest *pParameterRequests); + std::vector &GetInputBuffers() { return this->pedalboardInputBuffers; } + std::vector &GetoutputBuffers() { return this->pedalboardOutputBuffers; } + int GetControlIndex(uint64_t instanceId, const std::string &symbol); + void SetControlValue(int effectIndex, int portIndex, float value); + void SetInputVolume(float value) { this->inputVolume.SetTarget(value); } + void SetOutputVolume(float value) { this->outputVolume.SetTarget(value); } + void SetBypass(int effectIndex, bool enabled); - std::vector &GetInputBuffers() { return this->pedalboardInputBuffers;} - std::vector &GetoutputBuffers() { return this->pedalboardOutputBuffers;} + void ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples, float **inputBuffers, float **outputBuffers); + float GetControlOutputValue(int effectIndex, int portIndex); - - int GetControlIndex(uint64_t instanceId,const std::string&symbol); - void SetControlValue(int effectIndex, int portIndex, float value); - void SetInputVolume(float value) { this->inputVolume.SetTarget(value);} - void SetOutputVolume(float value) { this->outputVolume.SetTarget(value);} - void SetBypass(int effectIndex,bool enabled); - - void ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples, float**inputBuffers, float**outputBuffers); - - float GetControlOutputValue(int effectIndex, int portIndex); - - typedef void (MidiCallbackFn) (void* data,uint64_t intanceId, int controlIndex, float value); - void OnMidiMessage(size_t size, uint8_t*data, - void *callbackHandle, - MidiCallbackFn*pfnCallback); - -}; + typedef void(MidiCallbackFn)(void *data, uint64_t intanceId, int controlIndex, float value); + void OnMidiMessage(size_t size, uint8_t *data, + void *callbackHandle, + MidiCallbackFn *pfnCallback); + }; } // namespace \ No newline at end of file diff --git a/src/PatchPropertyWriter.hpp b/src/PatchPropertyWriter.hpp new file mode 100644 index 0000000..c0ab216 --- /dev/null +++ b/src/PatchPropertyWriter.hpp @@ -0,0 +1,198 @@ +// 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 +#include +#include +#include +#include "util.hpp" +#include +namespace pipedal +{ + + // A mechanism for writing (path) patch properties back to the non-realtime threads. + // Has the following characteristics: + // 1. Only one property in flight to the non-realtime thread at any givien moment. + // 2. No blocking, no spinning on the realtime thread. RT operations require nothing other than atomic operations. + // 3. Multiple responses in the same RT frame are coaslesced into one response (the latest). + // 4. Responses that are found while a property notification is in flight are coaslesced. + + class IPatchWriterCallback; + + class PatchPropertyWriter + { + private: + enum class StateT : char + { + Empty, + WriteStarted, + WriteLocked, // written data is available. + ReadLocked, // data is in flight between the realtime thread and non-realtime service thread. + Deleted // detect use after free. + }; + + public: + PatchPropertyWriter(int64_t instanceId, LV2_URID patchPropertyUrid) + : instanceId(instanceId), patchPropertyUrid(patchPropertyUrid), + buffer0(new Buffer(instanceId, patchPropertyUrid)), + buffer1(new Buffer(instanceId, patchPropertyUrid)) + { + } + // no copy. + PatchPropertyWriter(const PatchPropertyWriter&) = delete; + // move + PatchPropertyWriter(PatchPropertyWriter&&other) { + std::swap(this->buffer0,other.buffer0); + std::swap(this->buffer1,other.buffer1); + this->currentWriteBuffer = nullptr; + this->instanceId = other.instanceId; + this->patchPropertyUrid = other.patchPropertyUrid; + + } + + ~PatchPropertyWriter() + { + delete buffer0; + delete buffer1; + } + class Buffer + { + public: + std::atomic state{StateT::Empty}; + + Buffer(int64_t instanceId, LV2_URID propertyUrid) + : instanceId(instanceId), patchPropertyUrid(propertyUrid) + { + memory.reserve(1024); + } + + void OnBufferWriteStarted() + { + if (state != StateT::Empty && state != StateT::WriteLocked) + { + throw std::runtime_error("Bad state."); + } + state = StateT::WriteStarted; + } + void OnBufferWritten() + { + if (state != StateT::WriteStarted) + { + throw std::runtime_error("Bad state."); + } + state = StateT::ReadLocked; + WriteBarrier(); + } + + void OnBufferReadStarted() + { + if (state != StateT::ReadLocked) + { + throw std::runtime_error("Bad state."); + } + ReadBarrier(); + } + void OnBufferReadComplete() + { + if (state != StateT::ReadLocked) + { + throw std::runtime_error("Bad state."); + } + state = StateT::Empty; + } + int64_t instanceId; + LV2_URID patchPropertyUrid; + + std::vector memory; + }; + + // RT thread only. + Buffer *AquireWriteBuffer() + { + // RT only. + if (currentWriteBuffer) + { + return currentWriteBuffer; + } + if (buffer0->state == StateT::Empty) + { + buffer0->OnBufferWriteStarted(); + currentWriteBuffer = buffer0; + return currentWriteBuffer; + } + if (buffer1->state == StateT::Empty) + { + buffer1->OnBufferWriteStarted(); + currentWriteBuffer = buffer1; + return currentWriteBuffer; + } + throw std::runtime_error("Bad state. Unable to aquire a write buffer."); + } + // RT thread ony. + void OnBufferWritten() + { + if (currentWriteBuffer) + { + currentWriteBuffer->OnBufferWritten(); + currentWriteBuffer = nullptr; + } + } + Buffer *GetCurrentWriteBuffer() + { + return currentWriteBuffer; + } + + void FlushWrites(IPatchWriterCallback*cbWrite); + + int64_t instanceId; + LV2_URID patchPropertyUrid; + + private: + Buffer *buffer0 = 0; + Buffer *buffer1 = 0; + Buffer *currentWriteBuffer = nullptr; + }; + + class IPatchWriterCallback { + public: + virtual void OnWritePatchPropertyBuffer( + PatchPropertyWriter::Buffer * + ) = 0; + }; + + inline void PatchPropertyWriter::FlushWrites(IPatchWriterCallback*cbWrite) + { + auto buffer = GetCurrentWriteBuffer(); + if (buffer) + { + // is the OTHER buffer empty? + if (buffer0->state == StateT::Empty || buffer1->state == StateT::Empty) + { + OnBufferWritten(); + cbWrite->OnWritePatchPropertyBuffer(buffer); + } else { + // we have an update in flight, so just keep accumulating, and maybe write next time. + } + } + } + + +} \ No newline at end of file diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index e4fa4d5..0e24eb3 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -19,6 +19,7 @@ #include "pch.h" #include "Pedalboard.hpp" +#include "AtomConverter.hpp" using namespace pipedal; @@ -177,9 +178,200 @@ bool IsPedalboardSplitItem(const PedalboardItem*self, const std::vectoruri() == SPLIT_PEDALBOARD_ITEM_URI; } +bool Pedalboard::ApplySnapshot(int64_t snapshotIndex) +{ + if (snapshotIndex < 0 || + snapshotIndex >= this->snapshots_.size() || + this->snapshots_[snapshotIndex] == nullptr || + this->selectedSnapshot() == snapshotIndex) + { + return false; + } + std::map indexedValues; + Snapshot *snapshot = this->snapshots_[snapshotIndex].get(); + + for (auto &value: snapshot->values_) + { + indexedValues[value.instanceId_] = &value; + } + + auto plugins = this->GetAllPlugins(); + for (PedalboardItem *pedalboardItem: plugins) + { + SnapshotValue*snapshotValue = indexedValues[pedalboardItem->instanceId()]; + if (snapshotValue) + { + pedalboardItem->ApplySnapshotValue(snapshotValue); + } + } + return true; +} + +void PedalboardItem::ApplySnapshotValue(SnapshotValue*snapshotValue) +{ + std::map cumulativeValues; + for (auto &controlValue: this->controlValues()) + { + cumulativeValues[controlValue.key()] = controlValue.value(); + } + for (auto&controlValue : snapshotValue->controlValues_) + { + cumulativeValues[controlValue.key()] = controlValue.value(); + } + this->controlValues().clear(); + for (auto&pair: cumulativeValues) + { + this->controlValues_.push_back(ControlValue(pair.first.c_str(),pair.second)); + } + if (this->lv2State() != snapshotValue->lv2State_) + { + this->lv2State(snapshotValue->lv2State_); + this->stateUpdateCount(this->stateUpdateCount()+1); + } + +} +// can we just send a snapshot-style uddate instead of reloading plugins? All settings are ignored. +bool Pedalboard::IsStructureIdentical(const Pedalboard &other) const +{ + if (this->nextInstanceId_ != other.nextInstanceId_) // quick check that catches 95% of structural changes. + { + return false; + } + if (this->items_.size() != other.items_.size()) + { + return false; + } + for (size_t i = 0; i < this->items_.size();++i) + { + if (!this->items_[i].IsStructurallyIdentical(other.items_[i])) + { + return false; + } + } + return true; +} +bool PedalboardItem::IsStructurallyIdentical(const PedalboardItem&other) const +{ + if (this->instanceId() != other.instanceId()) // must match in order to ensure that realtime message passing works. + { + return false; + } + if (this->uri() != other.uri()) + { + return false; + } + if (this->isSplit()) // so is the other by virtue of idential uris. + { + if (topChain().size() != other.topChain().size()) + { + return false; + } + for (size_t i = 0; i < topChain().size(); ++i) + { + if (!topChain()[i].IsStructurallyIdentical(other.topChain()[i] )) + { + return false; + } + } + if (bottomChain().size() != other.bottomChain().size()) + { + for (size_t i = 0; i < bottomChain().size(); ++i) + { + if (!bottomChain()[i].IsStructurallyIdentical(other.bottomChain()[i])) + { + return false; + } + } + } + } + return true; +} + +void PedalboardItem::AddToSnapshotFromCurrentSettings(Snapshot&snapshot) const +{ + SnapshotValue snapshotValue; + snapshotValue.instanceId_ = this->instanceId_; + + for (const ControlValue &value: this->controlValues_) + { + snapshotValue.controlValues_.push_back(value); + } + for (const auto&pathProperty: this->pathProperties_) + { + snapshotValue.pathProperties_[pathProperty.first] = pathProperty.second; + } + snapshotValue.lv2State_ = this->lv2State_; + snapshot.values_.push_back(std::move(snapshotValue)); + + if (this->isSplit()) + { + for (auto&item: this->topChain_) + { + item.AddToSnapshotFromCurrentSettings(snapshot);; + } + for (auto&item: this->bottomChain_) + { + item.AddToSnapshotFromCurrentSettings(snapshot); + } + } +} + +void PedalboardItem::AddResetsForMissingProperties(Snapshot&snapshot, size_t*index) const +{ + // structure must be identical + // items must be enumerated in the same order as AddToSnapshotFromCurrentSettings + SnapshotValue&snapshotValue = snapshot.values_[*index]; + + if (snapshotValue.instanceId_ != this->instanceId()) + { + throw std::runtime_error("Pedalboard structure does not match."); + } + + for (auto&property: this->pathProperties_) + { + auto f = snapshotValue.pathProperties_.find(property.first); + if (f == snapshotValue.pathProperties_.end()) + { + snapshotValue.pathProperties_[property.first] = AtomConverter::EmptyPathstring(); + } + } + ++(*index); + + if (this->isSplit()) + { + for (auto&item: this->topChain()) + { + item.AddResetsForMissingProperties(snapshot,index); + } + for (auto&item: this->bottomChain()) + { + item.AddResetsForMissingProperties(snapshot,index); + } + } + +} + +Snapshot Pedalboard::MakeSnapshotFromCurrentSettings(const Pedalboard &previousPedalboard) +{ + Snapshot snapshot; + // name and color don't matter. this is strictly for loading purposes. + for (auto &item : this->items()) + { + item.AddToSnapshotFromCurrentSettings(snapshot); + } + // a neccesary precondition: the previous pedalboard must have identical structure, + // so we can just + size_t index = 0; + for (auto&item: previousPedalboard.items_) + { + item.AddResetsForMissingProperties(snapshot,&index); + } + return snapshot; + +} @@ -202,6 +394,7 @@ JSON_MAP_BEGIN(PedalboardItem) JSON_MAP_REFERENCE(PedalboardItem,stateUpdateCount) JSON_MAP_REFERENCE(PedalboardItem,lv2State) JSON_MAP_REFERENCE(PedalboardItem,lilvPresetUri) + JSON_MAP_REFERENCE(PedalboardItem,pathProperties) JSON_MAP_END() @@ -211,5 +404,21 @@ JSON_MAP_BEGIN(Pedalboard) JSON_MAP_REFERENCE(Pedalboard,output_volume_db) JSON_MAP_REFERENCE(Pedalboard,items) JSON_MAP_REFERENCE(Pedalboard,nextInstanceId) + JSON_MAP_REFERENCE(Pedalboard,snapshots) + JSON_MAP_REFERENCE(Pedalboard,selectedSnapshot) JSON_MAP_END() +JSON_MAP_BEGIN(SnapshotValue) + JSON_MAP_REFERENCE(SnapshotValue,instanceId) + JSON_MAP_REFERENCE(SnapshotValue,controlValues) + JSON_MAP_REFERENCE(SnapshotValue,lv2State) + JSON_MAP_REFERENCE(SnapshotValue,pathProperties) +JSON_MAP_END() + +JSON_MAP_BEGIN(Snapshot) + JSON_MAP_REFERENCE(Snapshot,name) + JSON_MAP_REFERENCE(Snapshot,color) + JSON_MAP_REFERENCE(Snapshot,values) +JSON_MAP_END() + + diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index a64e22b..a538692 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -26,7 +26,9 @@ #include "atom_object.hpp" namespace pipedal { - + class SnapshotValue; + class Snapshot; + #define SPLIT_PEDALBOARD_ITEM_URI "uri://two-play/pipedal/pedalboard#Split" #define EMPTY_PEDALBOARD_ITEM_URI "uri://two-play/pipedal/pedalboard#Empty" @@ -45,7 +47,9 @@ namespace pipedal { #define GETTER_SETTER_VEC(name) \ decltype(name##_)& name() { return name##_;} \ - const decltype(name##_)& name() const { return name##_;} + const decltype(name##_)& name() const { return name##_;} \ + void name(decltype(name##_)&&value) { this->name##_ = std::move(value); }\ + void name(const decltype(name##_)&value) { this->name##_ = value; } @@ -91,12 +95,16 @@ public: uint32_t stateUpdateCount_ = 0; Lv2PluginState lv2State_; std::string lilvPresetUri_; + std::map pathProperties_; // non persistent state. PropertyMap patchProperties; public: ControlValue*GetControlValue(const std::string&symbol); - + + bool IsStructurallyIdentical(const PedalboardItem&other) const; + + void ApplySnapshotValue(SnapshotValue*snapshotValue); bool hasLv2State() const { return lv2State_.isValid_ != 0; } @@ -124,6 +132,10 @@ public: return uri_ == EMPTY_PEDALBOARD_ITEM_URI; } + void AddToSnapshotFromCurrentSettings(Snapshot&snapshot) const; + void AddResetsForMissingProperties(Snapshot&snapshot, size_t*index) const; + + virtual void write_members(json_writer&writer) const { writer.write_member("instanceId",instanceId_); writer.write_member("uri",uri_); @@ -139,6 +151,25 @@ public: DECLARE_JSON_MAP(PedalboardItem); }; +class SnapshotValue { +public: + uint64_t instanceId_; + std::vector controlValues_; + Lv2PluginState lv2State_; + std::map pathProperties_; + DECLARE_JSON_MAP(SnapshotValue); + +}; + +class Snapshot { +public: + std::string name_; + std::string color_; + std::vector values_; + + DECLARE_JSON_MAP(Snapshot); + +}; class Pedalboard { std::string name_; @@ -149,22 +180,31 @@ class Pedalboard { uint64_t nextInstanceId_ = 0; uint64_t NextInstanceId() { return ++nextInstanceId_; } + std::vector> snapshots_; + int64_t selectedSnapshot_ = -1; + public: static constexpr int64_t INPUT_VOLUME_ID = -2; // synthetic PedalboardItem for input volume. static constexpr int64_t OUTPUT_VOLUME_ID = -3; // synthetic PedalboardItem for output volume. bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value); bool SetItemEnabled(int64_t pedalItemId, bool enabled); + bool IsStructureIdentical(const Pedalboard &other) const; // caan we just send a snapshot-style uddate instead of reloading plugins? All settings are ignored. + Snapshot MakeSnapshotFromCurrentSettings(const Pedalboard &previousPedalboard); + PedalboardItem*GetItem(int64_t pedalItemId); const PedalboardItem*GetItem(int64_t pedalItemId) const; std::vectorGetAllPlugins(); bool HasItem(int64_t pedalItemid) const { return GetItem(pedalItemid) != nullptr; } + bool ApplySnapshot(int64_t snapshotIndex); GETTER_SETTER_REF(name) GETTER_SETTER_VEC(items) GETTER_SETTER(input_volume_db) GETTER_SETTER(output_volume_db) + GETTER_SETTER_VEC(snapshots) + GETTER_SETTER(selectedSnapshot) DECLARE_JSON_MAP(Pedalboard); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index b0471c0..dcb1ce4 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -89,16 +89,15 @@ PiPedalModel::PiPedalModel() this->OnUpdateStatusChanged(updateStatus); }); - DbusLogToLv2Log(); SetDBusLogLevel(DBusLogLevel::Info); hotspotManager = HotspotManager::Create(); hotspotManager->SetNetworkChangingListener( - [this](bool ethernetConnected, bool hotspotEnabling) { - OnNetworkChanging(ethernetConnected,hotspotEnabling); - } - ); + [this](bool ethernetConnected, bool hotspotEnabling) + { + OnNetworkChanging(ethernetConnected, hotspotEnabling); + }); // don't actuall start the hotspotManager until after LV2 is initialized (in order to avoid logging oddities) } @@ -107,14 +106,14 @@ void PiPedalModel::Close() std::unique_ptr oldAudioHost; { std::lock_guard lock(mutex); - if (closed) + if (closed) { return; } closed = true; - - if (avahiService) { + if (avahiService) + { this->avahiService = nullptr; // and close. } @@ -361,7 +360,7 @@ void PiPedalModel::OnNotifyLv2StateChanged(uint64_t instanceId) { // a sent PATCH_Set, or an explicit state changed notification. OnNotifyMaybeLv2StateChanged(instanceId); - this->SetPresetChanged(-1, true); + this->SetPresetChanged(-1, true, false); } void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId) @@ -566,6 +565,69 @@ void PiPedalModel::SetPedalboard(int64_t clientId, Pedalboard &pedalboard) this->SetPresetChanged(clientId, true); } } + +void PiPedalModel::SetSnapshot(int64_t selectedSnapshot) +{ + std::lock_guard lock(mutex); + if (this->pedalboard.selectedSnapshot() == selectedSnapshot) + { + return; + } + if (this->pedalboard.ApplySnapshot(selectedSnapshot)) + { + if (this->audioHost) + { + this->audioHost->LoadSnapshot(*(this->pedalboard.snapshots()[selectedSnapshot]), this->pluginHost); // no longer own it. + } + SetPresetChanged(-1, true, false); + this->pedalboard.selectedSnapshot(selectedSnapshot); + FirePedalboardChanged(-1, false); + } +} + +void PiPedalModel::SetSnapshots(std::vector> &snapshots, int64_t selectedSnapshot) +{ + std::lock_guard lock(mutex); + + // notionally, snapshots are saved whenever they are modified, but pedalboards are not. + // so we must add the current snapshots BOTH to the current pedalbaoard, AND to the + // current saved instance of the pedalboard to create the illusion that the are stored separately. + // In practice, snapshots have to track structure changes (delete pedalboard items that have been deleted, + // and set default settings for pedalboard items that have ben added since the snapshots was last saved. + // + // This is acheived by: + // 1. updating BOTH the current pedalboard, and its saved instance. + // 2. discarding references to dangling instances as plugins are loaded (see UpdateDefaults). + // 3. Setting missing snapshot controls to default values (NOT the current pedalboard values) + // 4. (We currently don't do the right thing with patch properties if the patch property has never been set). + + UpdateVst3Settings(pedalboard); + + { + // stealth update of the saved snapshots. + auto savedPedalboard = storage.GetCurrentPreset(); + savedPedalboard.snapshots(snapshots); // makes a shallow copy + + if (selectedSnapshot != -1) + { + // implies that this is a snapshot of the currently running pedalboard. so we can mark it as the selected pedalboard. + this->pedalboard.selectedSnapshot(selectedSnapshot); + } + // UpdateDefaults(&savedPedalboard); // The update is awfully fresh. wait until it gets loaded again before pruning. + storage.SaveCurrentPreset(savedPedalboard); + } + + this->pedalboard.snapshots(std::move(snapshots)); + if (selectedSnapshot != -1) + { + this->pedalboard.selectedSnapshot(selectedSnapshot); + } + + this->FirePedalboardChanged(-1, false); // notify clients (but don't change the running pedalboard, because it's still the same) + // this means that all clients get an up-to-date copy of the snapshots AND the currently selected snapshot if that applies + // (and a fresh copy of the pedalboard settings as well, which is harmless, since they have not changed) +} + void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalboard) { { @@ -628,12 +690,60 @@ void PiPedalModel::GetBank(int64_t instanceId, BankFile *pResult) this->storage.GetBankFile(instanceId, pResult); } -void PiPedalModel::SetPresetChanged(int64_t clientId, bool value) +void PiPedalModel::SetPresetChanged(int64_t clientId, bool value, bool changeSnapshotSelect) { + if (changeSnapshotSelect && value && this->pedalboard.selectedSnapshot() != -1) + { + this->pedalboard.selectedSnapshot(-1); + FireSelectedSnapshotChanged(-1); + } if (value != this->hasPresetChanged) { hasPresetChanged = value; - FirePresetsChanged(clientId); + FirePresetChanged(value); + } +} + +void PiPedalModel::FireSelectedSnapshotChanged(int64_t selectedSnapshot) +{ + std::lock_guard guard{mutex}; + { + // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) + IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; + for (size_t i = 0; i < subscribers.size(); ++i) + { + t[i] = this->subscribers[i]; + } + size_t n = this->subscribers.size(); + + for (size_t i = 0; i < n; ++i) + { + t[i]->OnSelectedSnapshotChanged(selectedSnapshot); + } + delete[] t; + } +} + +void PiPedalModel::FirePresetChanged(bool changed) +{ + std::lock_guard guard{mutex}; + { + // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) + IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; + for (size_t i = 0; i < subscribers.size(); ++i) + { + t[i] = this->subscribers[i]; + } + size_t n = this->subscribers.size(); + + PresetIndex presets; + GetPresets(&presets); + + for (size_t i = 0; i < n; ++i) + { + t[i]->OnPresetChanged(changed); + } + delete[] t; } } @@ -776,48 +886,65 @@ int64_t PiPedalModel::UploadBank(BankFile &bankFile, int64_t uploadAfter) return newPreset; } +void PiPedalModel::NextBank(Direction direction) +{ + +} +void PiPedalModel::NextPreset(Direction direction) +{ + PresetIndex index; + + storage.GetPresetIndex(&index); + auto currentPresetId = storage.GetCurrentPresetId(); + size_t currentPresetIndex = 0; + + for (size_t i = 0; i < index.presets().size(); ++i) + { + if (index.presets()[i].instanceId() == currentPresetId) + { + currentPresetIndex = i; + break; + } + } + if (index.presets().size() == 0) + { + return; + } + if (direction == Direction::Decrease) + { + if (currentPresetIndex == 0) + { + currentPresetIndex = index.presets().size() - 1; + } + else + { + --currentPresetIndex; + } + } + else + { + ++currentPresetIndex; + if (currentPresetIndex >= index.presets().size()) + { + currentPresetIndex = 0; + } + } + LoadPreset(-1, index.presets()[currentPresetIndex].instanceId()); +} + + void PiPedalModel::OnNotifyNextMidiProgram(const RealtimeNextMidiProgramRequest &request) { std::lock_guard guard{mutex}; try { - PresetIndex index; - storage.GetPresetIndex(&index); - auto currentPresetId = storage.GetCurrentPresetId(); - size_t currentPresetIndex = 0; - for (size_t i = 0; i < index.presets().size(); ++i) - { - if (index.presets()[i].instanceId() == currentPresetId) - { - currentPresetIndex = i; - break; - } - } - if (index.presets().size() == 0) - { - throw PiPedalException("No presets loaded."); - } if (request.direction < 0) { - if (currentPresetIndex == 0) - { - currentPresetIndex = index.presets().size() - 1; - } - else - { - --currentPresetIndex; - } + NextPreset(); + } else { + PreviousPreset(); } - else - { - ++currentPresetIndex; - if (currentPresetIndex >= index.presets().size()) - { - currentPresetIndex = 0; - } - } - LoadPreset(-1, index.presets()[currentPresetIndex].instanceId()); } catch (std::exception &e) { @@ -994,8 +1121,6 @@ void PiPedalModel::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSet { std::lock_guard lock(mutex); - - #if NEW_WIFI_CONFIG if (this->storage.SetWifiConfigSettings(wifiConfigSettings)) { @@ -1065,7 +1190,7 @@ void PiPedalModel::UpdateDnsSd() std::string hostName = GetHostName(); if (serviceName != "" && deviceIdFile.uuid != "") { - avahiService->Announce(webPort, serviceName, deviceIdFile.uuid, hostName,true); + avahiService->Announce(webPort, serviceName, deviceIdFile.uuid, hostName, true); } else { @@ -1475,6 +1600,18 @@ void PiPedalModel::SendSetPatchProperty( std::lock_guard lock(mutex); + // save the property to the preset (currently used to reconstruct snapshots only) + PedalboardItem *pedalboardItem = this->pedalboard.GetItem(instanceId); + if (pedalboardItem) + { + 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); LV2_Atom *atomValue = atomConverter.ToAtom(value); std::function onRequestComplete{ @@ -1727,11 +1864,19 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet #endif } -void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem) +std::shared_ptr PiPedalModel::GetPluginInfo(const std::string &uri) { + return pluginHost.GetPluginInfo(uri); +} + +void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered_map &itemMap) +{ + itemMap[pedalboardItem->instanceId()] = pedalboardItem; + std::shared_ptr pPlugin = pluginHost.GetPluginInfo(pedalboardItem->uri()); if (!pPlugin) { + pedalboardItem->instanceId(); if (pedalboardItem->uri() == SPLIT_PEDALBOARD_ITEM_URI) { pPlugin = GetSplitterPluginInfo(); @@ -1753,21 +1898,74 @@ void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem) } } } + if (pPlugin->piPedalUI()) + { + PiPedalUI::ptr piPedalUI = pPlugin->piPedalUI(); + for (auto &fileProperty : piPedalUI->fileProperties()) + { + if (!pedalboardItem->pathProperties_.contains(fileProperty->patchProperty())) + { + // make sure each pedalboard item has a complete list of path properties, even if it doesn't yet have values. + pedalboardItem->pathProperties_[fileProperty->patchProperty()] = "null"; + } + } + } + } + else + { + // an old bug leaks lv2states. Clean it up here. + if (pedalboardItem->uri() == EMPTY_PEDALBOARD_ITEM_URI) + { + pedalboardItem->lv2State(Lv2PluginState()); + } } for (size_t i = 0; i < pedalboardItem->topChain().size(); ++i) { - UpdateDefaults(&(pedalboardItem->topChain()[i])); + UpdateDefaults(&(pedalboardItem->topChain()[i]), itemMap); } for (size_t i = 0; i < pedalboardItem->bottomChain().size(); ++i) { - UpdateDefaults(&(pedalboardItem->bottomChain()[i])); + UpdateDefaults(&(pedalboardItem->bottomChain()[i]), itemMap); } } + +void PiPedalModel::UpdateDefaults(Snapshot *snapshot, std::unordered_map &itemMap) +{ + if (!snapshot) + return; + for (size_t i = 0; i < snapshot->values_.size(); ++i) + { + SnapshotValue &value = snapshot->values_[i]; + auto f = itemMap.find(value.instanceId_); + if (f == itemMap.end()) + { + // plugin is no longer present. Remove from the snapshot. + snapshot->values_.erase(snapshot->values_.begin() + i); + --i; + } + // missing values in snapshots get filled in with plugin defaults at load time, + // so we don't need to actually populate the missing values. + } + for (auto &snapshotValue : snapshot->values_) + { + } +} + void PiPedalModel::UpdateDefaults(Pedalboard *pedalboard) { + // add missing values. + std::unordered_map itemMap; for (size_t i = 0; i < pedalboard->items().size(); ++i) { - UpdateDefaults(&(pedalboard->items()[i])); + UpdateDefaults(&(pedalboard->items()[i]), itemMap); + } + // set all missiong values on snapshots to default values. + for (auto snapshot : pedalboard->snapshots()) + { + if (snapshot) + { + UpdateDefaults(snapshot.get(), itemMap); + } } } @@ -1919,6 +2117,56 @@ void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetPropert audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); } +void PiPedalModel::OnNotifyPathPatchPropertyReceived( + int64_t instanceId, + LV2_URID pathPatchProperty, + LV2_Atom *pathProperty) +{ + std::lock_guard lock(mutex); + + std::string pathPatchPropertyUri = this->pluginHost.Lv2UridToString(pathPatchProperty); + std::string atomString = atomConverter.ToString(pathProperty); + auto pedalboardItem = this->pedalboard.GetItem(instanceId); + + if (this->audioHost) + { + this->audioHost->OnNotifyPathPatchPropertyReceived(instanceId,pathPatchPropertyUri,atomString); + } + + + + + if (pedalboardItem == nullptr) + { + Lv2Log::error(SS("OnNotifyPathPatchPropertyReceived: " << pathPatchPropertyUri << " discarded.")); + return; + } + Lv2Log::error(SS("OnNotifyPathPatchPropertyReceived: " << pathPatchPropertyUri <<": " << atomString)); + + auto i = pedalboardItem->pathProperties_.find(pathPatchPropertyUri); + if (i != pedalboardItem->pathProperties_.end()) + { + pedalboardItem->pathProperties_[pathPatchPropertyUri] = atomString; + + std::vector t; + t.reserve(subscribers.size()); + + for (auto subscriber : subscribers) + { + t.push_back(subscriber); + } + + for (auto subscriber : t) + { + subscriber->OnNotifyPathPatchPropertyChanged( + instanceId, + pathPatchPropertyUri, + atomString); + } + + } +} + void PiPedalModel::OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) { std::lock_guard lock(mutex); @@ -2011,7 +2259,7 @@ void PiPedalModel::CancelMonitorPatchProperty(int64_t clientId, int64_t clientHa atomOutputListeners.erase(atomOutputListeners.begin() + i); break; } - } + } if (midiEventListeners.size() == 0) { audioHost->SetListenForMidiEvent(false); @@ -2169,6 +2417,15 @@ std::shared_ptr PiPedalModel::GetLv2Pedalboard() } bool PiPedalModel::LoadCurrentPedalboard() { + if (previousPedalboardLoaded && pedalboard.IsStructureIdentical(previousPedalboard)) + { + // then we can send a snapshot update instead! + Snapshot snapshot = pedalboard.MakeSnapshotFromCurrentSettings(previousPedalboard); + audioHost->LoadSnapshot(snapshot,pluginHost); + this->previousPedalboard = this->pedalboard; + return true; + } + Lv2PedalboardErrorList errorMessages; std::shared_ptr lv2Pedalboard{this->pluginHost.CreateLv2Pedalboard(this->pedalboard, errorMessages)}; this->lv2Pedalboard = lv2Pedalboard; @@ -2300,7 +2557,7 @@ void PiPedalModel::StartHotspotMonitoring() this->avahiService = std::make_unique(); SetThreadName("avahi"); // hack to name the avahi service thread. - UpdateDnsSd(); // now that the server is running, publish a DNS-SD announcement. + UpdateDnsSd(); // now that the server is running, publish a DNS-SD announcement. SetThreadName("main"); this->hotspotManager->Open(); @@ -2353,7 +2610,7 @@ void PiPedalModel::WaitForAudioDeviceToComeOnline() GetAlsaDevices(); } -PiPedalModel::PostHandle PiPedalModel::Post(PostCallback&&fn) +PiPedalModel::PostHandle PiPedalModel::Post(PostCallback &&fn) { // I know. odd place to forward this to, but it's a very serviceable dispatcher implementation. // Why? because it's there, and PiPedalModel has no thread of its own to do dispatching. @@ -2363,16 +2620,16 @@ PiPedalModel::PostHandle PiPedalModel::Post(PostCallback&&fn) } return hotspotManager->Post(std::move(fn)); } -PiPedalModel::PostHandle PiPedalModel::PostDelayed(const clock::duration&delay,PostCallback&&fn) +PiPedalModel::PostHandle PiPedalModel::PostDelayed(const clock::duration &delay, PostCallback &&fn) { if (!hotspotManager) { throw std::runtime_error("Too early. It's not ready yet."); } - return hotspotManager->PostDelayed(delay,std::move(fn)); - + return hotspotManager->PostDelayed(delay, std::move(fn)); } -bool PiPedalModel::CancelPost(PostHandle handle) { +bool PiPedalModel::CancelPost(PostHandle handle) +{ if (!hotspotManager) { throw std::runtime_error("Too early. It's not ready yet."); @@ -2388,7 +2645,6 @@ void PiPedalModel::CancelNetworkChangingTimer() networkChangingDelayHandle = 0; } } - std::vector PiPedalModel::GetKnownWifiNetworks() { @@ -2399,16 +2655,16 @@ std::vector PiPedalModel::GetKnownWifiNetworks() return this->hotspotManager->GetKnownWifiNetworks(); } -void PiPedalModel::OnNetworkChanging(bool ethernetConnected,bool hotspotConnected) +void PiPedalModel::OnNetworkChanging(bool ethernetConnected, bool hotspotConnected) { CancelNetworkChangingTimer(); - this->networkChangingDelayHandle = + this->networkChangingDelayHandle = PostDelayed(std::chrono::seconds(10), // takes a while for network configuration to be fully applied. - [this,ethernetConnected,hotspotConnected]() { - this->networkChangingDelayHandle = 0; - OnNetworkChanged(ethernetConnected,hotspotConnected); - } - ); + [this, ethernetConnected, hotspotConnected]() + { + this->networkChangingDelayHandle = 0; + OnNetworkChanged(ethernetConnected, hotspotConnected); + }); // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) std::vector t; @@ -2422,51 +2678,52 @@ void PiPedalModel::OnNetworkChanging(bool ethernetConnected,bool hotspotConnecte { t[i]->OnNetworkChanging(hotspotConnected); } - } void PiPedalModel::OnNetworkChanged(bool ethernetConnected, bool hotspotConnected) { FireNetworkChanged(); } -void PiPedalModel::OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) +void PiPedalModel::OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) { - try { + try + { switch (eventType) { - case RealtimeMidiEventType::Shutdown: - { - this->RequestShutdown(false); - } - break; - case RealtimeMidiEventType::Reboot: - { - this->RequestShutdown(true); - } - break; - case RealtimeMidiEventType::StartHotspot: - { - WifiConfigSettings settings = storage.GetWifiConfigSettings(); - if (!settings.hasSavedPassword_) - { - throw std::runtime_error("Can't start Wi-Fi hotspot because no password has been configured."); - } - settings.autoStartMode_ = (uint16_t)HotspotAutoStartMode::Always; - this->SetWifiConfigSettings(settings); - } - break; - case RealtimeMidiEventType::StopHotspot: - { - WifiConfigSettings settings = storage.GetWifiConfigSettings(); - settings.autoStartMode_ = (uint16_t)HotspotAutoStartMode::Never; - this->SetWifiConfigSettings(settings); - } - break; - - default: - break; + case RealtimeMidiEventType::Shutdown: + { + this->RequestShutdown(false); } - } catch (const std::exception&e) + break; + case RealtimeMidiEventType::Reboot: + { + this->RequestShutdown(true); + } + break; + case RealtimeMidiEventType::StartHotspot: + { + WifiConfigSettings settings = storage.GetWifiConfigSettings(); + if (!settings.hasSavedPassword_) + { + throw std::runtime_error("Can't start Wi-Fi hotspot because no password has been configured."); + } + settings.autoStartMode_ = (uint16_t)HotspotAutoStartMode::Always; + this->SetWifiConfigSettings(settings); + } + break; + case RealtimeMidiEventType::StopHotspot: + { + WifiConfigSettings settings = storage.GetWifiConfigSettings(); + settings.autoStartMode_ = (uint16_t)HotspotAutoStartMode::Never; + this->SetWifiConfigSettings(settings); + } + break; + + default: + break; + } + } + catch (const std::exception &e) { Lv2Log::error(SS("Failed to process realtime MIDI event. " << e.what())); } @@ -2507,3 +2764,5 @@ void PiPedalModel::RequestShutdown(bool restart) } } } + + diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 042930b..182646e 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -40,6 +40,7 @@ #include "Promise.hpp" #include "AtomConverter.hpp" #include "FileEntry.hpp" +#include namespace pipedal { @@ -58,11 +59,13 @@ namespace pipedal virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0; virtual void OnInputVolumeChanged(float value) = 0; virtual void OnOutputVolumeChanged(float value) = 0; - virtual void OnUpdateStatusChanged(const UpdateStatus&updateStatus) = 0; - virtual void OnLv2StateChanged(int64_t pedalItemId,const Lv2PluginState&newState ) = 0; + virtual void OnUpdateStatusChanged(const UpdateStatus &updateStatus) = 0; + virtual void OnLv2StateChanged(int64_t pedalItemId, const Lv2PluginState &newState) = 0; virtual void OnVst3ControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value, const std::string &state) = 0; virtual void OnPedalboardChanged(int64_t clientId, const Pedalboard &pedalboard) = 0; virtual void OnPresetsChanged(int64_t clientId, const PresetIndex &presets) = 0; + virtual void OnPresetChanged(bool changed) = 0; + virtual void OnSelectedSnapshotChanged(int64_t selectedSnapshot) = 0; virtual void OnPluginPresetsChanged(const std::string &pluginUri) = 0; virtual void OnChannelSelectionChanged(int64_t clientId, const JackChannelSelection &channelSelection) = 0; virtual void OnVuMeterUpdate(const std::vector &updates) = 0; @@ -78,10 +81,11 @@ namespace pipedal virtual void OnGovernorSettingsChanged(const std::string &governor) = 0; virtual void OnFavoritesChanged(const std::map &favorites) = 0; virtual void OnShowStatusMonitorChanged(bool show) = 0; - virtual void OnSystemMidiBindingsChanged(const std::vector&bindings) = 0; + virtual void OnSystemMidiBindingsChanged(const std::vector &bindings) = 0; + virtual void OnNotifyPathPatchPropertyChanged(int64_t instanceId, const std::string &pathPatchPropertyString, const std::string &atomString) = 0; - //virtual void OnPatchPropertyChanged(int64_t clientId, int64_t instanceId,const std::string& propertyUri,const json_variant& value) = 0; - virtual void OnErrorMessage(const std::string&message) = 0; + // virtual void OnPatchPropertyChanged(int64_t clientId, int64_t instanceId,const std::string& propertyUri,const json_variant& value) = 0; + virtual void OnErrorMessage(const std::string &message) = 0; virtual void OnLv2PluginsChanging() = 0; virtual void OnNetworkChanging(bool hotspotConnected) = 0; virtual void Close() = 0; @@ -97,14 +101,13 @@ namespace pipedal using PostHandle = uint64_t; using PostCallback = std::function; using NetworkChangedListener = std::function; - private: std::unique_ptr hotspotManager; std::unique_ptr updater; UpdateStatus currentUpdateStatus; - void OnUpdateStatusChanged(const UpdateStatus&updateStatus); + void OnUpdateStatusChanged(const UpdateStatus &updateStatus); std::function restartListener; std::unique_ptr pluginChangeMonitor; @@ -131,8 +134,10 @@ namespace pipedal class AtomOutputListener { public: - bool WantsProperty(uint64_t instanceId,LV2_URID patchProperty) const { - if (instanceId != this->instanceId) return false; + bool WantsProperty(uint64_t instanceId, LV2_URID patchProperty) const + { + if (instanceId != this->instanceId) + return false; return this->propertyUrid == 0 || patchProperty == this->propertyUrid; } int64_t clientId; @@ -151,11 +156,14 @@ namespace pipedal AtomConverter atomConverter; // must be AFTER pluginHost! Pedalboard pedalboard; + bool previousPedalboardLoaded = false; + Pedalboard previousPedalboard; Storage storage; bool hasPresetChanged = false; NetworkChangedListener networkChangedListener; - void FireNetworkChanged() { + void FireNetworkChanged() + { if (networkChangedListener) { networkChangedListener(); @@ -168,15 +176,18 @@ namespace pipedal std::filesystem::path webRoot; std::vector subscribers; - void SetPresetChanged(int64_t clientId, bool value); + void SetPresetChanged(int64_t clientId, bool value, bool changeSnapshotSelect = true); + void FireSelectedSnapshotChanged(int64_t selectedSnapshot); void FirePresetsChanged(int64_t clientId); + void FirePresetChanged(bool changed); void FirePluginPresetsChanged(const std::string &pluginUri); void FirePedalboardChanged(int64_t clientId, bool reloadAudioThread = true); void FireChannelSelectionChanged(int64_t clientId); void FireBanksChanged(int64_t clientId); void FireJackConfigurationChanged(const JackConfiguration &jackConfiguration); - void UpdateDefaults(PedalboardItem *pedalboardItem); + void UpdateDefaults(Snapshot *snapshot, std::unordered_map &itemMap); + void UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered_map &itemMap); void UpdateDefaults(Pedalboard *pedalboard); class VuSubscription { @@ -206,22 +217,22 @@ namespace pipedal virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) override; virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) override; virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) override; - virtual void OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom*atomValue) override; + virtual void OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) override; virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override; - - void OnNotifyPatchProperty(uint64_t instanceId, LV2_URID outputAtomProperty, const std::string &atomJson); - - + void OnNotifyPathPatchPropertyReceived( + int64_t instanceId, + LV2_URID pathPatchProperty, + LV2_Atom *pathProperty) override; virtual void OnNotifyMidiProgramChange(RealtimeMidiProgramRequest &midiProgramRequest) override; - virtual void OnNotifyNextMidiProgram(const RealtimeNextMidiProgramRequest&request) override; - virtual void OnNotifyLv2RealtimeError(int64_t instanceId,const std::string &error) override; + virtual void OnNotifyNextMidiProgram(const RealtimeNextMidiProgramRequest &request) override; + virtual void OnNotifyLv2RealtimeError(int64_t instanceId, const std::string &error) override; PostHandle networkChangingDelayHandle = 0; void CancelNetworkChangingTimer(); - - void OnNetworkChanging(bool ethernetConnected,bool hotspotConnected); + + void OnNetworkChanging(bool ethernetConnected, bool hotspotConnected); void OnNetworkChanged(bool ethernetConnected, bool hotspotConnected); void UpdateVst3Settings(Pedalboard &pedalboard); @@ -229,25 +240,39 @@ namespace pipedal PiPedalConfiguration configuration; void CheckForResourceInitialization(Pedalboard &pedalboard); + public: PiPedalModel(); virtual ~PiPedalModel(); + enum class Direction + { + Increase, + Decrease + }; + void NextBank(Direction direction = Direction::Increase); + void PreviousBank() { NextBank(Direction::Decrease); } + void NextPreset(Direction direction = Direction::Increase); + void PreviousPreset() { NextPreset(Direction::Decrease); } + void RequestShutdown(bool restart); - virtual PostHandle Post(PostCallback&&fn); - virtual PostHandle PostDelayed(const clock::duration&delay,PostCallback&&fn); + virtual PostHandle Post(PostCallback &&fn); + virtual PostHandle PostDelayed(const clock::duration &delay, PostCallback &&fn); virtual bool CancelPost(PostHandle handle); - template - PostHandle PostDelayed(const std::chrono::duration&delay,PostCallback&&fn) + template + PostHandle PostDelayed(const std::chrono::duration &delay, PostCallback &&fn) { return PostDelayed( - std::chrono::duration_cast(delay), + std::chrono::duration_cast(delay), std::move(fn)); } - void SetNetworkChangedListener(NetworkChangedListener listener) { + std::shared_ptr GetPluginInfo(const std::string &uri); + + void SetNetworkChangedListener(NetworkChangedListener listener) + { networkChangedListener = listener; } @@ -256,8 +281,8 @@ namespace pipedal void WaitForAudioDeviceToComeOnline(); UpdateStatus GetUpdateStatus(); - void UpdateNow(const std::string&updateUrl); - void FireUpdateStatusChanged(const UpdateStatus&updateStatus); + void UpdateNow(const std::string &updateUrl); + void FireUpdateStatusChanged(const UpdateStatus &updateStatus); uint16_t GetWebPort() const { return webPort; } std::filesystem::path GetPluginUploadDirectory() const; void Close(); @@ -297,17 +322,18 @@ namespace pipedal void SetOutputVolume(float value); void PreviewInputVolume(float value); void PreviewOutputVolume(float value); - - void SetPedalboard(int64_t clientId, Pedalboard &pedalboard); - Pedalboard&GetPedalboard(); + Pedalboard &GetPedalboard(); std::shared_ptr GetLv2Pedalboard(); void UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalboard); + void SetSnapshots(std::vector> &snapshots, int64_t selectedSnapshot); + void SetSnapshot(int64_t selectedSnapshot); + void GetPresets(PresetIndex *pResult); - + Pedalboard GetPreset(int64_t instanceId); void GetBank(int64_t instanceId, BankFile *pBank); @@ -369,10 +395,9 @@ namespace pipedal int64_t clientId, int64_t instanceId, const std::string uri, - const json_variant&value, + const json_variant &value, std::function onSuccess, std::function onError); - BankIndex GetBankIndex() const; void RenameBank(int64_t clientId, int64_t bankId, const std::string &newName); @@ -389,7 +414,7 @@ namespace pipedal void ListenForMidiEvent(int64_t clientId, int64_t clientHandle, bool listenForControlsOnly); void CancelListenForMidiEvent(int64_t clientId, int64_t clientHandle); - void MonitorPatchProperty(int64_t clientId, int64_t clientHandle, uint64_t instanceId,const std::string&propertyUri); + void MonitorPatchProperty(int64_t clientId, int64_t clientHandle, uint64_t instanceId, const std::string &propertyUri); void CancelMonitorPatchProperty(int64_t clientId, int64_t clientHandle); std::vector GetAlsaDevices(); @@ -399,15 +424,15 @@ namespace pipedal void SetFavorites(const std::map &favorites); void SetUpdatePolicy(UpdatePolicyT updatePolicy); void ForceUpdateCheck(); - std::vector GetFileList(const UiFileProperty&fileProperty); - std::vector GetFileList2(const std::string &relativePath,const UiFileProperty&fileProperty); + std::vector GetFileList(const UiFileProperty &fileProperty); + std::vector GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty); void DeleteSampleFile(const std::filesystem::path &fileName); - std::string CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty); - std::string RenameFilePropertyFile(const std::string&oldRelativePath,const std::string&newRelativePath,const UiFileProperty&uiFileProperty); - FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty); + std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty); + std::string RenameFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty); + FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty); - std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,std::istream&inputStream,size_t streamLength); + std::string UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream &inputStream, size_t streamLength); uint64_t CreateNewPreset(); bool LoadCurrentPedalboard(); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 23a9f78..87b3391 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -46,8 +46,23 @@ using namespace std; using namespace pipedal; +class PathPatchPropertyChangedBody +{ +public: + int64_t instanceId_; + std::string propertyUri_; + std::string atomJson_; + DECLARE_JSON_MAP(PathPatchPropertyChangedBody); +}; +JSON_MAP_BEGIN(PathPatchPropertyChangedBody) +JSON_MAP_REFERENCE(PathPatchPropertyChangedBody, instanceId) +JSON_MAP_REFERENCE(PathPatchPropertyChangedBody, propertyUri) +JSON_MAP_REFERENCE(PathPatchPropertyChangedBody, atomJson) +JSON_MAP_END() -class GetPatchPropertyBody { + +class GetPatchPropertyBody +{ public: uint64_t instanceId_; std::string propertyUri_; @@ -59,8 +74,8 @@ JSON_MAP_REFERENCE(GetPatchPropertyBody, instanceId) JSON_MAP_REFERENCE(GetPatchPropertyBody, propertyUri) JSON_MAP_END() - -class CreateNewSampleDirectoryArgs { +class CreateNewSampleDirectoryArgs +{ public: std::string relativePath_; UiFileProperty uiFileProperty_; @@ -72,7 +87,8 @@ JSON_MAP_REFERENCE(CreateNewSampleDirectoryArgs, relativePath) JSON_MAP_REFERENCE(CreateNewSampleDirectoryArgs, uiFileProperty) JSON_MAP_END() -class RenameSampleFileArgs { +class RenameSampleFileArgs +{ public: std::string oldRelativePath_; std::string newRelativePath_; @@ -86,8 +102,8 @@ JSON_MAP_REFERENCE(RenameSampleFileArgs, newRelativePath) JSON_MAP_REFERENCE(RenameSampleFileArgs, uiFileProperty) JSON_MAP_END() - -class Lv2StateChangedBody { +class Lv2StateChangedBody +{ public: uint64_t instanceId_; Lv2PluginState state_; @@ -99,9 +115,8 @@ JSON_MAP_REFERENCE(Lv2StateChangedBody, instanceId) JSON_MAP_REFERENCE(Lv2StateChangedBody, state) JSON_MAP_END() - - -class SetPatchPropertyBody { +class SetPatchPropertyBody +{ public: uint64_t instanceId_; std::string propertyUri_; @@ -171,7 +186,7 @@ public: JSON_MAP_BEGIN(MonitorPatchPropertyBody) JSON_MAP_REFERENCE(MonitorPatchPropertyBody, instanceId) JSON_MAP_REFERENCE(MonitorPatchPropertyBody, clientHandle) -JSON_MAP_REFERENCE(MonitorPatchPropertyBody,propertyUri) +JSON_MAP_REFERENCE(MonitorPatchPropertyBody, propertyUri) JSON_MAP_END() class OnLoadPluginPresetBody @@ -240,9 +255,6 @@ JSON_MAP_REFERENCE(FileRequestArgs, relativePath) JSON_MAP_REFERENCE(FileRequestArgs, fileProperty) JSON_MAP_END() - - - class MonitorPortBody { public: @@ -370,6 +382,20 @@ JSON_MAP_REFERENCE(UpdateCurrentPedalboardBody, clientId) JSON_MAP_REFERENCE(UpdateCurrentPedalboardBody, pedalboard) JSON_MAP_END() +class SetSnapshotsBody +{ +public: + std::vector> snapshots_; + int64_t selectedSnapshot_; + + DECLARE_JSON_MAP(SetSnapshotsBody); +}; + +JSON_MAP_BEGIN(SetSnapshotsBody) +JSON_MAP_REFERENCE(SetSnapshotsBody, snapshots) +JSON_MAP_REFERENCE(SetSnapshotsBody, selectedSnapshot) +JSON_MAP_END() + class ChannelSelectionChangedBody { public: @@ -451,7 +477,6 @@ JSON_MAP_REFERENCE(Vst3ControlChangedBody, value) JSON_MAP_REFERENCE(Vst3ControlChangedBody, state) JSON_MAP_END() - class PiPedalSocketHandler : public SocketHandler, public IPiPedalModelSubscriber { private: @@ -480,25 +505,25 @@ private: PortMonitorSubscription( int64_t subscriptionHandle, int64_t instanceId, - const std::string& key) - :subscriptionHandle(subscriptionHandle), - instanceId(instanceId), - key(key) + const std::string &key) + : subscriptionHandle(subscriptionHandle), + instanceId(instanceId), + key(key) { - } - ~PortMonitorSubscription() { + ~PortMonitorSubscription() + { Close(); } PortMonitorSubscription() {} - PortMonitorSubscription(const PortMonitorSubscription&) = delete; - PortMonitorSubscription& operator=(const PortMonitorSubscription&) = delete; - void Close() { - std::lock_guard lock {pmMutex}; + PortMonitorSubscription(const PortMonitorSubscription &) = delete; + PortMonitorSubscription &operator=(const PortMonitorSubscription &) = delete; + void Close() + { + std::lock_guard lock{pmMutex}; closed = true; } - std::mutex pmMutex; bool closed = false; @@ -532,7 +557,8 @@ public: bool finalCleanup = false; void FinalCleanup() { - if (finalCleanup) return; + if (finalCleanup) + return; finalCleanup = true; // avoid use after free. for (int i = 0; i < this->activePortMonitors.size(); ++i) @@ -547,12 +573,12 @@ public: activeVuSubscriptions.resize(0); model.RemoveNotificationSubsription(this); - } virtual void Close() { - if (closed) return; + if (closed) + return; closed = true; FinalCleanup(); // do it while we can. &model will no longer be valid after this. ( :-( ) @@ -566,7 +592,8 @@ public: std::stringstream imageList; const std::filesystem::path &webRoot = model.GetWebRoot() / "img"; bool firstTime = true; - try { + try + { for (const auto &entry : std::filesystem::directory_iterator(webRoot)) { if (!firstTime) @@ -576,7 +603,8 @@ public: firstTime = false; imageList << entry.path().filename().string(); } - } catch (const std::exception&) + } + catch (const std::exception &) { Lv2Log::error("Can't list files in %s. Image files will not be pre-loaded in the client.", webRoot.c_str()); } @@ -814,7 +842,7 @@ public: { std::shared_ptr result; { - std::lock_guard lock (activePortMonitorsMutex); + std::lock_guard lock(activePortMonitorsMutex); for (int i = 0; i < this->activePortMonitors.size(); ++i) { @@ -832,9 +860,10 @@ public: { // running on RT_output thread, or on Socket thread. { - std::lock_guard lock {subscription->pmMutex}; - if (subscription->closed) return; - if (value == subscription->currentValue) + std::lock_guard lock{subscription->pmMutex}; + if (subscription->closed) + return; + if (value == subscription->currentValue) return; if (subscription->waitingForAck) { @@ -846,7 +875,7 @@ public: subscription->lastValue = value; subscription->waitingForAck = true; } - SendMonitorPortMessage_Inner(subscription,value); + SendMonitorPortMessage_Inner(subscription, value); } // post-sync send. @@ -863,12 +892,14 @@ public: { // running on PiPedalSocket thread. std::shared_ptr subscription = getPortMonitorSubscription(subscriptionHandle_); - if (!subscription) return; + if (!subscription) + return; float value; { - std::unique_lock lock {subscription->pmMutex}; - if (subscription->closed) return; + std::unique_lock lock{subscription->pmMutex}; + if (subscription->closed) + return; if (subscription->pendingValue) { value = subscription->currentValue; @@ -892,7 +923,7 @@ public: Lv2Log::debug("Failed to monitor port output. (%s)", e.what()); }); } - void MonitorPort(int replyTo,MonitorPortBody &body) + void MonitorPort(int replyTo, MonitorPortBody &body) { std::lock_guard guard(subscriptionMutex); int64_t subscriptionHandle = model.MonitorPort( @@ -965,18 +996,19 @@ public: ControlChangedBody message; pReader->read(&message); this->model.SetControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); - } + } else if (message == "previewControl") { ControlChangedBody message; pReader->read(&message); this->model.PreviewControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); - } else if (message == "setControl") + } + else if (message == "setControl") { ControlChangedBody message; pReader->read(&message); this->model.SetControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); - } + } else if (message == "setInputVolume") { float value; @@ -1029,7 +1061,7 @@ public: else if (message == "getUpdateStatus") { UpdateStatus updateStatus = model.GetUpdateStatus(); - this->Reply(replyTo,"getUpdateStatus",updateStatus); + this->Reply(replyTo, "getUpdateStatus", updateStatus); } else if (message == "updateNow") { @@ -1037,7 +1069,7 @@ public: pReader->read(&updateUrl); model.UpdateNow(updateUrl); bool result = true; - this->Reply(replyTo,"updateNow",result); + this->Reply(replyTo, "updateNow", result); } else if (message == "getJackStatus") { @@ -1053,7 +1085,6 @@ public: { std::vector channels = this->model.GetKnownWifiNetworks(); this->Reply(replyTo, "getWifiChannels", channels); - } else if (message == "getWifiChannels") { @@ -1199,6 +1230,18 @@ public: this->model.UpdateCurrentPedalboard(body.clientId_, body.pedalboard_); } } + else if (message == "setSnapshot") + { + int64_t snapshotIndex = -1; + pReader->read(&snapshotIndex); + this->model.SetSnapshot(snapshotIndex); + } + else if (message == "setSnapshots") + { + SetSnapshotsBody body; + pReader->read(&body); + this->model.SetSnapshots(body.snapshots_, body.selectedSnapshot_); + } else if (message == "currentPedalboard") { auto pedalboard = model.GetCurrentPedalboardCopy(); @@ -1347,6 +1390,23 @@ public: this->SendError(replyTo, std::string(e.what())); } } + else if (message == "nextBank") + { + model.NextBank(); + } + else if (message == "previousBank") + { + model.PreviousBank(); + } + else if (message == "nextPreset") + { + model.NextPreset(); + } + else if (message == "previousPreset") + { + model.PreviousPreset(); + } + else if (message == "renamePresetItem") { RenamePresetBody body; @@ -1373,19 +1433,16 @@ public: { SetPatchPropertyBody body; pReader->read(&body); - model.SendSetPatchProperty(clientId,body.instanceId_,body.propertyUri_,body.value_, - [this, replyTo] () { - this->JsonReply(replyTo, "setPatchProperty", "true"); - - }, - [this, replyTo] (const std::string&error) { - this->SendError(replyTo, error.c_str()); - - } - ); - + model.SendSetPatchProperty(clientId, body.instanceId_, body.propertyUri_, body.value_, [this, replyTo]() + { + this->JsonReply(replyTo, "setPatchProperty", "true"); + }, + [this, replyTo](const std::string &error) + { + this->SendError(replyTo, error.c_str()); + }); } - + else if (message == "getPatchProperty") { GetPatchPropertyBody body; @@ -1410,7 +1467,7 @@ public: MonitorPortBody body; pReader->read(&body); - MonitorPort(replyTo,body); + MonitorPort(replyTo, body); } else if (message == "unmonitorPort") { @@ -1418,7 +1475,7 @@ public: pReader->read(&subscriptionHandle); { { - std::lock_guard guard(activePortMonitorsMutex); + std::lock_guard guard(activePortMonitorsMutex); for (auto i = this->activePortMonitors.begin(); i != this->activePortMonitors.end(); ++i) { if ((*i)->subscriptionHandle == subscriptionHandle) @@ -1490,9 +1547,8 @@ public: this->model.SetUpdatePolicy((UpdatePolicyT)iPolicy); } else if (message == "forceUpdateCheck") - { + { this->model.ForceUpdateCheck(); - } else if (message == "setSystemMidiBindings") { @@ -1503,56 +1559,57 @@ public: else if (message == "getSystemMidiBindings") { std::vector bindings = this->model.GetSystemMidiBidings(); - this->Reply(replyTo,"getSystemMidiBindings",bindings); + this->Reply(replyTo, "getSystemMidiBindings", bindings); } else if (message == "requestFileList") { UiFileProperty fileProperty; pReader->read(&fileProperty); std::vector list = this->model.GetFileList(fileProperty); - this->Reply(replyTo,"requestFileList",list); - } + this->Reply(replyTo, "requestFileList", list); + } else if (message == "requestFileList2") { FileRequestArgs requestArgs; pReader->read(&requestArgs); std::vector list = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_); - this->Reply(replyTo,"requestFileList2",list); - } + this->Reply(replyTo, "requestFileList2", list); + } else if (message == "newPreset") { int64_t presetId = this->model.CreateNewPreset(); - this->Reply(replyTo,"newPreset",presetId); - } + this->Reply(replyTo, "newPreset", presetId); + } else if (message == "deleteUserFile") { std::string fileName; pReader->read(&fileName); this->model.DeleteSampleFile(fileName); - this->Reply(replyTo,"deleteUserFile",true); + this->Reply(replyTo, "deleteUserFile", true); } else if (message == "createNewSampleDirectory") { CreateNewSampleDirectoryArgs args; pReader->read(&args); - std::string newFileName = this->model.CreateNewSampleDirectory(args.relativePath_,args.uiFileProperty_); - this->Reply(replyTo,"createNewSampleDirectory",newFileName); + std::string newFileName = this->model.CreateNewSampleDirectory(args.relativePath_, args.uiFileProperty_); + this->Reply(replyTo, "createNewSampleDirectory", newFileName); } else if (message == "renameFilePropertyFile") { RenameSampleFileArgs args; pReader->read(&args); - std::string newFileName = this->model.RenameFilePropertyFile(args.oldRelativePath_,args.newRelativePath_,args.uiFileProperty_); - this->Reply(replyTo,"renameFilePropertyFile",newFileName); - } else if (message == "getFilePropertyDirectoryTree"){ + std::string newFileName = this->model.RenameFilePropertyFile(args.oldRelativePath_, args.newRelativePath_, args.uiFileProperty_); + this->Reply(replyTo, "renameFilePropertyFile", newFileName); + } + else if (message == "getFilePropertyDirectoryTree") + { UiFileProperty uiFileProperty; pReader->read(&uiFileProperty); FilePropertyDirectoryTree::ptr result = model.GetFilePropertydirectoryTree(uiFileProperty); - this->Reply(replyTo,"GetFilePropertydirectoryTree",result); - + this->Reply(replyTo, "GetFilePropertydirectoryTree", result); } else if (message == "setOnboarding") @@ -1633,37 +1690,37 @@ protected: } private: - virtual void OnUpdateStatusChanged(const UpdateStatus&updateStatus) { - Send("onUpdateStatusChanged",updateStatus); + virtual void OnUpdateStatusChanged(const UpdateStatus &updateStatus) + { + Send("onUpdateStatusChanged", updateStatus); } virtual void OnLv2StateChanged(int64_t instanceId, const Lv2PluginState &state) { - Lv2StateChangedBody message { (uint64_t)instanceId, state}; - Send("onLv2StateChanged",message); - + Lv2StateChangedBody message{(uint64_t)instanceId, state}; + Send("onLv2StateChanged", message); } - virtual void OnLv2PluginsChanging() override { - Send("onLv2PluginsChanging",true); + virtual void OnLv2PluginsChanging() override + { + Send("onLv2PluginsChanging", true); Flush(); } - virtual void OnNetworkChanging(bool hotspotConnected) override { - try { - Send("onNetworkChanging",hotspotConnected); - Flush(); - - } catch (const std::exception&ignored) + virtual void OnNetworkChanging(bool hotspotConnected) override + { + try + { + Send("onNetworkChanging", hotspotConnected); + Flush(); + } + catch (const std::exception &ignored) { - } - } - - virtual void OnErrorMessage(const std::string&message) + virtual void OnErrorMessage(const std::string &message) { - Send("onErrorMessage",message); + Send("onErrorMessage", message); } // virtual void OnPatchPropertyChanged(int64_t clientId, int64_t instanceId,const std::string& propertyUri,const json_variant& value) // { @@ -1675,8 +1732,9 @@ private: // Send("onPatchPropertyChanged",body); // } - virtual void OnSystemMidiBindingsChanged(const std::vector&bindings) { - Send("onSystemMidiBindingsChanged",bindings); + virtual void OnSystemMidiBindingsChanged(const std::vector &bindings) + { + Send("onSystemMidiBindingsChanged", bindings); } virtual void OnFavoritesChanged(const std::map &favorites) @@ -1696,6 +1754,16 @@ private: body.jackChannelSelection_ = const_cast(&channelSelection); Send("onChannelSelectionChanged", body); } + virtual void OnSelectedSnapshotChanged(int64_t selectedSnapshot) override + { + Send("onSelectedSnapshotChanged", selectedSnapshot); + } + + virtual void OnPresetChanged(bool changed) override + { + Send("onPresetChanged", changed); + } + virtual void OnPresetsChanged(int64_t clientId, const PresetIndex &presets) { PresetsChangedBody body; @@ -1852,8 +1920,8 @@ private: } } - void Flush() { - + void Flush() + { } int outstandingNotifyAtomOutputs = 0; @@ -1896,7 +1964,7 @@ private: body.atomJson_.Set(atomJson); // flow control. We can only have one in-flight NotifyAtomOutput at a time. - // Subsequent notifications are held until we receive an ack. + // Subsequent notifications are held until we receive an ack. // // If a duplicate atomProperty is queued, overwrite the previous value. { @@ -1910,7 +1978,7 @@ private: if (output.clientHandle == clientHandle && output.instanceId == instanceId && output.propertyUri == atomProperty) { // better to erase than overwrite, since it provides better idempotence. - pendingNotifyAtomOutputs.erase(pendingNotifyAtomOutputs.begin()+i); + pendingNotifyAtomOutputs.erase(pendingNotifyAtomOutputs.begin() + i); break; } } @@ -1930,6 +1998,16 @@ private: }); } + virtual void OnNotifyPathPatchPropertyChanged(int64_t instanceId, const std::string &pathPatchPropertyString, const std::string &atomString) + { + PathPatchPropertyChangedBody body; + body.instanceId_ = instanceId; + body.propertyUri_ = pathPatchPropertyString; + body.atomJson_ = atomString; + + Send("onNotifyPathPatchPropertyChanged", body); + } + virtual void OnNotifyMidiListener(int64_t clientHandle, bool isNote, uint8_t noteOrControl) { NotifyMidiListenerBody body; @@ -2014,4 +2092,3 @@ std::shared_ptr pipedal::MakePiPedalSocketFactory(PiPedalModel & { return std::make_shared(model); } - diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index f7d3f2d..8239316 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -18,6 +18,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "pch.h" +#include "AtomConverter.hpp" #include "PluginHost.hpp" #include #include @@ -44,6 +45,7 @@ #include #include "PiPedalException.hpp" #include "StdErrorCapture.hpp" +#include "util.hpp" #include "Locale.hpp" @@ -188,6 +190,9 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) patch__readable = lilv_new_uri(pWorld, LV2_PATCH__readable); dc__format = lilv_new_uri(pWorld, "http://purl.org/dc/terms/format"); + + mod__fileTypes = lilv_new_uri(pWorld,"http://moddevices.com/ns/mod#fileTypes"); + } void PluginHost::LilvUris::Free() @@ -618,13 +623,28 @@ std::shared_ptr Lv2PluginInfo::FindWritablePathProperties(PluginHost std::make_shared( strLabel, propertyUri.AsUri(), lv2DirectoryName); - AutoLilvNodes dc_types = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->dc__format, nullptr); - LILV_FOREACH(nodes, i, dc_types) + AutoLilvNodes mod__fileTypes = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr); + LILV_FOREACH(nodes,i,mod__fileTypes) { - AutoLilvNode dc_type = lilv_nodes_get(dc_types, i); - std::string fileType = dc_type.AsString(); - std::string label = ""; - fileProperty->fileTypes().push_back(UiFileType(label, fileType)); + // "nam,nammodel" + AutoLilvNode lilvfileType {lilv_nodes_get(mod__fileTypes, i)}; + std::string fileTypes = lilvfileType.AsString(); + + for (std::string&type: split(fileTypes,',')) + { + fileProperty->fileTypes().push_back(UiFileType(SS(type << " file"),SS('.' << type))); + } + } + if (!mod__fileTypes) + { + AutoLilvNodes dc_types = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->dc__format, nullptr); + LILV_FOREACH(nodes, i, dc_types) + { + AutoLilvNode dc_type = lilv_nodes_get(dc_types, i); + std::string fileType = dc_type.AsString(); + std::string label = ""; + fileProperty->fileTypes().push_back(UiFileType(label, fileType)); + } } fileProperties.push_back( @@ -1479,6 +1499,46 @@ void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,con } } +json_variant PluginHost::MapPath(const json_variant&json) +{ + AtomConverter converter(GetMapFeature()); + return converter.MapPath(json,GetPluginStoragePath()); + +} +json_variant PluginHost::AbstractPath(const json_variant&json) +{ + AtomConverter converter(GetMapFeature()); + return converter.AbstractPath(json,GetPluginStoragePath()); + +} + + +std::string PluginHost::MapPath(const std::string &abstractPath) +{ + auto storagePath = GetPluginStoragePath(); + if (abstractPath.length() == 0) + { + return ""; + } + return SS(storagePath << '/' << abstractPath); + + +} +std::string PluginHost::AbstractPath(const std::string&path) +{ + auto storagePath = GetPluginStoragePath(); + if (path.starts_with(storagePath)) + { + auto start = storagePath.length(); + if (start < path.length() && path[start] == '/') + { + ++start; + } + return path.substr(start); + } + return path; +} + // void PiPedalHostLogError(const std::string &error) // { // Lv2Log::error("%s",error.c_str()); diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index 0e41275..88339d3 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -729,6 +729,10 @@ namespace pipedal AutoLilvNode mod__label; AutoLilvNode dc__format; + + AutoLilvNode mod__fileTypes; + + }; LilvUris* lilvUris = nullptr; @@ -815,6 +819,12 @@ namespace pipedal void CheckForResourceInitialization(const std::string& pluginUri,const std::filesystem::path& pluginUploadDirectory); void ReloadPlugins(); + // equivalent to LV2 MapPath AbstractPath features. + std::string MapPath(const std::string &abstractPath); + std::string AbstractPath(const std::string&path); + json_variant MapPath(const json_variant&json); + json_variant AbstractPath(const json_variant&json); + private: std::set pluginsThatHaveBeenCheckedForResources; diff --git a/src/PresetBundle.cpp b/src/PresetBundle.cpp new file mode 100644 index 0000000..f7e6904 --- /dev/null +++ b/src/PresetBundle.cpp @@ -0,0 +1,467 @@ +// 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. + +#include "PresetBundle.hpp" +#include "PiPedalModel.hpp" +#include +#include "Pedalboard.hpp" +#include "Banks.hpp" +#include "json.hpp" +#include +#include "lv2/atom/atom.h" +#include "ZipFile.hpp" +#include + +using namespace pipedal; + +static std::string ToString(const std::vector &value) +{ + const char *start = (const char *)(&value[0]); + const char *end = start + value.size(); + while (end != start && end[-1] == 0) + { + --end; + } + + return std::string(start, end); +} + +static std::vector ToBinary(const std::string &value) +{ + std::vector result(value.length() + 1); + for (size_t i = 0; i < value.length(); ++i) + { + result[i] = (uint8_t)value[i]; + } + result[value.length()] = 0; + return result; +} + +class PresetBundleWriterImpl : public PresetBundleWriter +{ +public: + using self = PresetBundleWriterImpl; + using super = PresetBundleWriter; + + PresetBundleWriterImpl() + { + } + + void LoadPresets(PiPedalModel &model, const std::string &presetJson) + { + BankFile bankFile; + + std::istringstream s(presetJson); + json_reader reader(s); + + reader.read(&bankFile); + GatherMediaPaths(model, bankFile); + configFiles["bankFile.json"] = presetJson; + + pluginUploadDirectory = model.GetPluginUploadDirectory(); + } + + void LoadPluginPresets(PiPedalModel &model, const std::string pluginPresetJson) + { + PluginPresets pluginPresets; + + std::istringstream s(pluginPresetJson); + json_reader reader(s); + + reader.read(&pluginPresets); + + GatherMediaPaths(model, pluginPresets); + configFiles["pluginPresets.json"] = pluginPresetJson; + + pluginUploadDirectory = model.GetPluginUploadDirectory(); + } + virtual ~PresetBundleWriterImpl() noexcept; + + virtual void WriteToFile(const std::filesystem::path &filePath) override; + +private: + void AddUsedPlugin(PiPedalModel &model, const std::string &pluginUri, const std::string &name); + + void GatherMediaPaths(PiPedalModel &model, BankFile &bankFile); + void GatherMediaPaths(PiPedalModel &model, PluginPresets &pluginPreset); + + std::map configFiles; + std::set mediaPaths; + std::set usedPlugins; + std::vector> pluginMetadata; + + std::filesystem::path pluginUploadDirectory; +}; + +void PresetBundleWriterImpl::WriteToFile(const std::filesystem::path &filePath) +{ + + ZipFileWriter::ptr zipFile = ZipFileWriter::Create(filePath); + + for (const auto &configFile : configFiles) + { + + zipFile->WriteFile(configFile.first, configFile.second.data(), configFile.second.size()); + } + std::string metadata; + { + std::ostringstream ss; + json_writer writer(ss); + writer.write(this->pluginMetadata); + metadata = ss.str(); + zipFile->WriteFile("pluginsUsed.json", metadata.data(), metadata.length()); + } + + for (const auto &mediaPath : mediaPaths) + { + std::string zipName = (std::filesystem::path("media") / std::filesystem::path(mediaPath)).string(); + std::filesystem::path sourcePath = this->pluginUploadDirectory / std::filesystem::path(mediaPath); + if (std::filesystem::exists(sourcePath)) + { + zipFile->WriteFile(zipName, sourcePath); + } + else + { + Lv2Log::warning(SS("Media file not found: " << sourcePath)); + } + } + zipFile->Close(); +} + +void PresetBundleWriterImpl::GatherMediaPaths(PiPedalModel &model, BankFile &bankFile) +{ + for (auto &preset : bankFile.presets()) + { + Pedalboard &pedalboard = preset->preset(); + auto items = pedalboard.GetAllPlugins(); + for (auto plugin : items) + { + AddUsedPlugin(model, plugin->uri(), plugin->pluginName()); + const Lv2PluginState &state = plugin->lv2State(); + for (const auto &value : state.values_) + { + if (value.second.atomType_ == LV2_ATOM__Path) + { + std::string path = ToString(value.second.value_); + if (!mediaPaths.contains(path)) + { + mediaPaths.insert(std::move(path)); + } + } + } + } + } +} +void PresetBundleWriterImpl::GatherMediaPaths(PiPedalModel &model, PluginPresets &pluginPresets) +{ + AddUsedPlugin(model, pluginPresets.pluginUri_, ""); + + for (auto &preset : pluginPresets.presets_) + { + const Lv2PluginState &state = preset.state_; + for (const auto &value : state.values_) + { + if (value.second.atomType_ == LV2_ATOM__Path) + { + std::string path = ToString(value.second.value_); + if (!mediaPaths.contains(path)) + { + mediaPaths.insert(std::move(path)); + } + } + } + } +} + +PresetBundleWriter::ptr PresetBundleWriter::CreatePresetsFile(PiPedalModel &model, const std::string &presetJson) +{ + auto result = std::make_unique(); + result->LoadPresets(model, presetJson); + + return result; +} +PresetBundleWriter::ptr PresetBundleWriter::CreatePluginPresetsFile(PiPedalModel &model, const std::string &pluginPresetJson) +{ + auto result = std::make_unique(); + result->LoadPluginPresets(model, pluginPresetJson); + return result; +} + +class PresetBundleReaderImpl : public PresetBundleReader +{ +public: + PresetBundleReaderImpl() + { + } + + void LoadPresetsFile(PiPedalModel &model, const std::filesystem::path &path) + { + this->pluginUploadDirectory = model.GetPluginUploadDirectory(); + zipFile = ZipFileReader::Create(path); + auto s = zipFile->GetFileInputStream("pluginPresets.json"); + json_reader reader(s); + reader.read(&pluginPresets); + } + void LoadPluginPresetFile(PiPedalModel &model, const std::filesystem::path &path) + { + this->pluginUploadDirectory = model.GetPluginUploadDirectory(); + zipFile = ZipFileReader::Create(path); + auto s = zipFile->GetFileInputStream("bankFile.json"); + json_reader reader(s); + reader.read(&bankFile); + } + virtual ~PresetBundleReaderImpl() noexcept; + + virtual void ExtractMediaFiles() override; + virtual std::string GetPresetJson() override; + virtual std::string GetPluginPresetsJson() override; + +private: + void ExtractMediaFile(const std::string &zipFileName); + bool IsSameFile(const std::string &zipFileName, const std::filesystem::path &filePath) + { + return zipFile->CompareFiles(zipFileName, filePath); + } + void RenameMediaFileProperty(const std::string oldName, const std::string &newName); + + std::filesystem::path pluginUploadDirectory; + // BankFile bankFile; + ZipFileReader::ptr zipFile; + + BankFile bankFile; + PluginPresets pluginPresets; +}; + +void PresetBundleReaderImpl::RenameMediaFileProperty(const std::string oldName, const std::string &newName) +{ + for (auto &preset : bankFile.presets()) + { + Pedalboard &pedalboard = preset->preset(); + auto items = pedalboard.GetAllPlugins(); + for (auto plugin : items) + { + Lv2PluginState &state = plugin->lv2State(); + for (auto &value : state.values_) + { + if (value.second.atomType_ == LV2_ATOM__Path) + { + std::string path = ToString(value.second.value_); + if (path == oldName) + { + state.values_.at(value.first).value_ = ToBinary(newName); + } + } + } + } + } + for (auto preset : pluginPresets.presets_) + { + Lv2PluginState state = preset.state_; + for (auto &value : state.values_) + { + if (value.second.atomType_ == LV2_ATOM__Path) + { + std::string path = ToString(value.second.value_); + if (path == oldName) + { + state.values_.at(value.first).value_ = ToBinary(newName); + } + } + } + } +} + +PresetBundleReader::ptr PresetBundleReader::LoadPresetsFile(PiPedalModel &model, const std::filesystem::path &filePath) +{ + std::unique_ptr result = std::make_unique(); + result->LoadPresetsFile(model, filePath); + return result; +} +PresetBundleReader::ptr PresetBundleReader::LoadPluginPresetsFile(PiPedalModel &model, const std::filesystem::path &filePath) +{ + std::unique_ptr result = std::make_unique(); + result->LoadPluginPresetsFile(model, filePath); + return result; +} + +void PresetBundleReaderImpl::ExtractMediaFiles() +{ + auto zipFiles = zipFile->GetFiles(); + for (const auto &zipFile : zipFiles) + { + if (zipFile != "bankFile.json") + { + if (!zipFile.ends_with('/')) // don't process diretories. + { + ExtractMediaFile(zipFile); + } + } + } +} + +static void ExtractFileVersion(std::string &baseName, int &n) +{ + if (baseName.ends_with(')')) + { + size_t endPos = baseName.length() - 1; + int startPos = baseName.find_last_of('('); + if (startPos != std::string::npos) + { + std::string number = baseName.substr(startPos + 1, endPos - (startPos + 1)); + std::istringstream ss(number); + ss >> n; + if (!ss.fail()) + { + while (startPos > 0 && baseName[startPos - 1] == ' ') + { + --startPos; + } + baseName = baseName.substr(0, startPos); + return; + } + } + } + n = 0; +} +static std::filesystem::path NextFileName(const std::filesystem::path &fileName) +{ + int n = 0; + auto fileNameOnly = fileName.filename().string(); + auto extPos = fileNameOnly.find_last_of('.'); + if (extPos == std::string::npos) + { + extPos = fileNameOnly.length(); + } + std::string baseName = fileNameOnly.substr(0, extPos); + ExtractFileVersion(baseName, n); + if (n >= 1024) + { + // something has gone badly wrong. + throw new std::runtime_error(SS("Can't increase the version number of " << fileName)); + } + std::string newFileName = SS(baseName << "(" << (n + 1) << ')' << fileNameOnly.substr(extPos)); + return fileName.parent_path() / newFileName; +} +void PresetBundleReaderImpl::ExtractMediaFile(const std::string &zipFileName) +{ + namespace fs = std::filesystem; + if (zipFileName.starts_with("media/")) + { + std::string baseName = zipFileName.substr(6); + fs::path targetFileName = this->pluginUploadDirectory / std::filesystem::path(baseName); + bool renamed = false; + while (true) + { + if (fs::exists(targetFileName)) + { + if (IsSameFile(zipFileName, targetFileName)) + { + break; + } + else + { + renamed = true; + targetFileName = NextFileName(targetFileName); + } + } + else + { + zipFile->ExtractTo(zipFileName, targetFileName); + break; + } + } + if (renamed) + { + std::string newName = fs::path(baseName).parent_path() / targetFileName.filename(); + RenameMediaFileProperty(baseName, newName); + } + } +} + +std::string PresetBundleReaderImpl::GetPresetJson() +{ + std::ostringstream ss; + json_writer writer(ss); + writer.write(this->bankFile); + return ss.str(); +} + +std::string PresetBundleReaderImpl::GetPluginPresetsJson() +{ + std::ostringstream ss; + json_writer writer(ss); + writer.write(this->pluginPresets); + return ss.str(); +} + +PresetBundleReaderImpl::~PresetBundleReaderImpl() noexcept +{ +} +PresetBundleReader::~PresetBundleReader() noexcept +{ +} + +PresetBundleWriterImpl::~PresetBundleWriterImpl() noexcept +{ +} +PresetBundleWriter::~PresetBundleWriter() noexcept +{ +} + +class PluginMetadata +{ +public: + PluginMetadata(std::shared_ptr &pluginInfo) + { + metadata_["uri"] = pluginInfo->uri(); + metadata_["name"] = pluginInfo->name(); + metadata_["brand"] = pluginInfo->brand(); + metadata_["authorName"] = pluginInfo->author_name(); + metadata_["authorHomePage"] = pluginInfo->author_homepage(); + } + PluginMetadata(const std::string &uri, const std::string &name) + { + metadata_["uri"] = uri; + metadata_["name"] = name.empty() ? uri : name; + } + + std::map metadata_; +}; + +void PresetBundleWriterImpl::AddUsedPlugin(PiPedalModel &model, const std::string &pluginUri, const std::string &name) +{ + if (!usedPlugins.contains(pluginUri)) + { + usedPlugins.insert(pluginUri); + + auto pluginInfo = model.GetPluginInfo(pluginUri); + + if (pluginInfo) + { + // use dictionaries because it's more version tolerant. + PluginMetadata metadata(pluginInfo); + this->pluginMetadata.push_back(std::move(metadata.metadata_)); + } + else + { + PluginMetadata metadata(pluginUri, name); + this->pluginMetadata.push_back(std::move(metadata.metadata_)); + } + } +} diff --git a/src/PresetBundle.hpp b/src/PresetBundle.hpp new file mode 100644 index 0000000..84c37ce --- /dev/null +++ b/src/PresetBundle.hpp @@ -0,0 +1,55 @@ +// 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 + +namespace pipedal { + class PiPedalModel; + + class PresetBundleWriter { + public: + using self = PresetBundleWriter; + using ptr = std::unique_ptr; + + virtual ~PresetBundleWriter() noexcept = 0; + + virtual void WriteToFile(const std::filesystem::path&filePath) = 0; + + + static ptr CreatePresetsFile(PiPedalModel&model,const std::string&presetJson); + static ptr CreatePluginPresetsFile(PiPedalModel&model,const std::string&presetJson); + }; + + class PresetBundleReader { + public: + using self = PresetBundleReader; + using ptr = std::unique_ptr; + + virtual ~PresetBundleReader() noexcept = 0; + + virtual void ExtractMediaFiles() = 0; + virtual std::string GetPresetJson() = 0; + virtual std::string GetPluginPresetsJson() = 0; + static ptr LoadPresetsFile(PiPedalModel&model,const std::filesystem::path &path); + static ptr LoadPluginPresetsFile(PiPedalModel&model,const std::filesystem::path &path); + }; + +} diff --git a/src/RingBufferReader.hpp b/src/RingBufferReader.hpp index 2e1cbf1..f9504d0 100644 --- a/src/RingBufferReader.hpp +++ b/src/RingBufferReader.hpp @@ -26,8 +26,12 @@ #include "lv2/atom/atom.h" #include "RealtimeMidiEventType.hpp" #include + + + namespace pipedal { + class IndexedSnapshot; enum class RingBufferCommand : int64_t { @@ -39,6 +43,10 @@ namespace pipedal AudioStopped, SetVuSubscriptions, FreeVuSubscriptions, + + LoadSnapshot, + FreeSnapshot, + SendVuUpdate, AckVuUpdate, @@ -68,6 +76,8 @@ namespace pipedal RealtimeMidiEvent, + SendPathPropertyBuffer, + }; @@ -449,6 +459,11 @@ namespace pipedal { write(RingBufferCommand::SetVuSubscriptions, configuration); } + void LoadSnapshot(IndexedSnapshot*snapshot) + { + write(RingBufferCommand::LoadSnapshot,snapshot); + } + void AckMidiProgramRequest(int64_t requestId) { write(RingBufferCommand::AckMidiProgramChange,requestId); @@ -485,11 +500,20 @@ namespace pipedal write(RingBufferCommand::EffectReplaced, pedalboard); } + void FreeSnapshot(IndexedSnapshot*snapshot) + { + write(RingBufferCommand::FreeSnapshot,snapshot); + } + void WriteLv2ErrorMessage(int64_t instanceId, const char*message) { size_t length = strlen(message); write(RingBufferCommand::Lv2ErrorMessage,instanceId,length,(uint8_t*)message); } + void SendPathPropertyBuffer(PatchPropertyWriter::Buffer*buffer) + { + write(RingBufferCommand::SendPathPropertyBuffer,buffer); + } }; typedef RingBufferReader RealtimeRingBufferReader; diff --git a/src/SplitEffect.cpp b/src/SplitEffect.cpp index 54acba2..cf06c7e 100644 --- a/src/SplitEffect.cpp +++ b/src/SplitEffect.cpp @@ -165,23 +165,55 @@ Lv2PluginInfo::ptr g_splitterPluginInfo = std::make_shared(makeSp Lv2PluginInfo::ptr pipedal::GetSplitterPluginInfo() { return g_splitterPluginInfo; } +SplitEffect::SplitEffect( + uint64_t instanceId, + double sampleRate, + const std::vector &inputs) + : instanceId(instanceId), inputs(inputs), sampleRate(sampleRate) + +{ + controlIndex["splitType"] = SPLIT_TYPE_CTL; + controlIndex["select"] = SELECT_CTL; + controlIndex["mix"] = MIX_CTL; + controlIndex["panL"] = PANL_CTL; + controlIndex["volL"] = VOLL_CTL; + controlIndex["panR"] = PANR_CTL; + controlIndex["volR"] = VOLR_CTL; + + Lv2PluginInfo::ptr puginInfo = g_splitterPluginInfo; + + for (auto&port: puginInfo->ports()) + { + if (port->is_control_port() && port->is_input()) + { + auto index = port->index(); + if (index < defaultInputControlValues.size()) { + defaultInputControlValues[index] = port->default_value(); + } + } + } +} + +uint64_t SplitEffect::GetMaxInputControl() const { return MAX_INPUT_CONTROL;} +bool SplitEffect::IsInputControl(uint64_t index) const +{ + return index >= 0 && index < MAX_INPUT_CONTROL; +} +float SplitEffect::GetDefaultInputControlValue(uint64_t index) const +{ + if (index < 0 || index >= MAX_INPUT_CONTROL) return 0; + return defaultInputControlValues[index]; +} + + int SplitEffect::GetControlIndex(const std::string &symbol) const { - if (symbol == "splitType") - return SPLIT_TYPE_CTL; - if (symbol == "select") - return SELECT_CTL; - if (symbol == "mix") - return MIX_CTL; - if (symbol == "panL") - return PANL_CTL; - if (symbol == "volL") - return VOLL_CTL; - if (symbol == "panR") - return PANR_CTL; - if (symbol == "volR") - return VOLR_CTL; - return -1; + auto i = controlIndex.find(symbol); + if (i == controlIndex.end()) + { + return -1; + } + return i->second; } void SplitEffect::Activate() @@ -344,6 +376,8 @@ float SplitEffect::GetControlValue(int portIndex) const } } + + void SplitEffect::SetControl(int index, float value) { switch (index) diff --git a/src/SplitEffect.hpp b/src/SplitEffect.hpp index 5114213..3ae90e0 100644 --- a/src/SplitEffect.hpp +++ b/src/SplitEffect.hpp @@ -23,6 +23,8 @@ #include "PiPedalException.hpp" #include "PiPedalMath.hpp" #include +#include +#include namespace pipedal { @@ -75,6 +77,9 @@ namespace pipedal const double MIX_TRANSITION_TIME_S = 0.1; double sampleRate; + + std::unordered_map controlIndex; + std::vector inputs; std::vector topInputs; std::vector bottomInputs; @@ -111,6 +116,8 @@ namespace pipedal float blendDxLBottom = 0; float blendDxRBottom = 0; + std::vector defaultInputControlValues; + int32_t blendFadeSamples; bool selectA = true; @@ -288,27 +295,23 @@ namespace pipedal } } uint64_t instanceId; + virtual bool IsLv2Effect() const override { return true; } virtual uint8_t *GetAtomInputBuffer() { return nullptr; } virtual uint8_t *GetAtomOutputBuffer() { return nullptr; } - virtual void RequestPatchProperty(LV2_URID uridUri) {} virtual void GatherPatchProperties(RealtimePatchPropertyRequest *pRequest) {} virtual std::string AtomToJson(uint8_t *pAtom) { return ""; } virtual std::string GetAtomObjectType(uint8_t*pData) { return "not implemented";} - virtual bool GetLv2State(Lv2PluginState*state) { return false; } + virtual bool GetLv2State(Lv2PluginState*state) override { return false; } + virtual void SetLv2State(Lv2PluginState&state) override { } virtual bool HasErrorMessage() const { return false; } const char* TakeErrorMessage() { return ""; } virtual bool IsVst3() const { return false; } - public: SplitEffect( uint64_t instanceId, double sampleRate, - const std::vector &inputs) - : instanceId(instanceId), inputs(inputs), sampleRate(sampleRate) - - { - } + const std::vector &inputs); ~SplitEffect() { } @@ -321,6 +324,12 @@ namespace pipedal static constexpr int VOLL_CTL = 4; static constexpr int PANR_CTL = 5; static constexpr int VOLR_CTL = 6; + static constexpr int MAX_INPUT_CONTROL =7; + + virtual uint64_t GetMaxInputControl() const override; + virtual bool IsInputControl(uint64_t index) const override; + virtual float GetDefaultInputControlValue(uint64_t index) const override; + virtual uint64_t GetInstanceId() const @@ -356,7 +365,10 @@ namespace pipedal return GetControlValue(index); } virtual float GetControlValue(int portIndex) const; - virtual void SetControl(int index, float value); + virtual void SetControl(int index, float value) override; + virtual void SetPatchProperty(LV2_URID uridUri, size_t size, LV2_Atom *value) override {} + virtual void RequestPatchProperty(LV2_URID uridUri) {} + virtual void RequestAllPathPatchProperties() {} virtual int GetNumberOfOutputAudioPorts() const diff --git a/src/Storage.cpp b/src/Storage.cpp index 9e05424..f6fc319 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -352,7 +352,7 @@ void Storage::SavePluginPresetIndex() throw PiPedalException(SS("Can't write to " << path)); } - json_writer writer(os, false); + json_writer writer(os, true); writer.write(this->pluginPresetIndex); } } @@ -361,7 +361,7 @@ void Storage::SaveBankIndex() { pipedal::ofstream_synced os; os.open(GetIndexFileName(), std::ios_base::trunc); - json_writer writer(os, false); + json_writer writer(os, true); writer.write(this->bankIndex); } @@ -436,7 +436,7 @@ void Storage::SaveBankFile(const std::string &name, const BankFile &bankFile) { pipedal::ofstream_synced s; s.open(fileName, std::ios_base::trunc); - json_writer writer(s, false); + json_writer writer(s, true); writer.write(bankFile); if (std::filesystem::exists(backupFile)) { @@ -722,7 +722,7 @@ void Storage::SaveChannelSelection() try { pipedal::ofstream_synced s(fileName); - json_writer writer(s, false); + json_writer writer(s, true); writer.write(this->jackChannelSelection); } catch (const std::exception &e) @@ -911,7 +911,7 @@ int64_t Storage::UploadBank(BankFile &bankFile, int64_t uploadAfter) { throw PiPedalException("Can't write to bank file."); } - json_writer writer(f, false); + json_writer writer(f, true); writer.write(bankFile); lastBank = this->bankIndex.addBank(lastBank, bankFile.name()); @@ -933,7 +933,7 @@ void Storage::SaveUserSettings() { throw PiPedalException("Unable to write to " + ((std::string)path)); } - json_writer writer(f, false); + json_writer writer(f, true); writer.write(userSettings); } } @@ -1048,7 +1048,7 @@ void Storage::SaveCurrentPreset(const CurrentPreset ¤tPreset) std::filesystem::path path = GetCurrentPresetPath(); pipedal::ofstream_synced f(path); - json_writer writer(f, false); + json_writer writer(f, true); writer.write(currentPreset); } catch (std::exception &) @@ -1128,7 +1128,7 @@ void Storage::SavePluginPresets(const std::string &pluginUri, const PluginPreset { throw PiPedalException(SS("Can't write to " << path)); } - json_writer writer(os, false); + json_writer writer(os, true); writer.write(presets); } if (std::filesystem::exists(path)) @@ -1370,7 +1370,7 @@ void Storage::SetFavorites(const std::map &favorites) f.open(fileName); if (f.is_open()) { - json_writer writer(f, false); + json_writer writer(f, true); writer.write(favorites); } } @@ -1399,7 +1399,7 @@ void Storage::SetJackServerSettings(const pipedal::JackServerSettings &jackConfi f.open(fileName); if (f.is_open()) { - json_writer writer(f, false); + json_writer writer(f, true); writer.write(jackConfiguration); } #if JACK_HOST @@ -1414,7 +1414,7 @@ void Storage::SetSystemMidiBindings(const std::vector &bindings) f.open(fileName); if (f.is_open()) { - json_writer writer(f, false); + json_writer writer(f, true); writer.write(bindings); } } diff --git a/src/TemporaryFile.cpp b/src/TemporaryFile.cpp index 26ad12f..39b91d9 100644 --- a/src/TemporaryFile.cpp +++ b/src/TemporaryFile.cpp @@ -52,6 +52,9 @@ TemporaryFile::TemporaryFile(const std::filesystem::path&directory) this->path = filename; } +void TemporaryFile::Detach() { + this->path.clear(); +} TemporaryFile::~TemporaryFile() { if (!path.empty()) diff --git a/src/TemporaryFile.hpp b/src/TemporaryFile.hpp index 7f1f2a9..e1095b2 100644 --- a/src/TemporaryFile.hpp +++ b/src/TemporaryFile.hpp @@ -22,10 +22,16 @@ namespace pipedal { class TemporaryFile { public: + TemporaryFile() {} TemporaryFile(const TemporaryFile&) = delete; TemporaryFile&operator=(const TemporaryFile&) = delete; + TemporaryFile(TemporaryFile&&other) { + this->path = std::move(other.path); + } TemporaryFile(const std::filesystem::path&parentDirectory); + + void Detach(); ~TemporaryFile(); const std::filesystem::path&Path()const { return path;} std::string str() const { return path.c_str(); } diff --git a/src/Updater.cpp b/src/Updater.cpp index 0be6353..18c0db6 100644 --- a/src/Updater.cpp +++ b/src/Updater.cpp @@ -376,17 +376,6 @@ GithubRelease::GithubRelease(json_variant &v) -static std::vector split(const std::string &s, char delimiter) -{ - std::vector tokens; - std::string token; - std::istringstream tokenStream(s); - while (std::getline(tokenStream, token, delimiter)) - { - tokens.push_back(token); - } - return tokens; -} static std::string justTheVersion(const std::string &assetName) { // eg. pipedal_1.2.41_arm64.deb diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 701cf30..66e498f 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -54,14 +54,17 @@ using namespace pipedal; using namespace std; -const bool ENABLE_KEEP_ALIVE = true; -const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"}; +static const bool ENABLE_KEEP_ALIVE = true; +static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"}; using tcp = boost::asio::ip::tcp; // from const size_t MAX_READ_SIZE = 512 * 1024 * 1024; ; using namespace boost; +using namespace websocketpp::http; + + class request_with_file_upload : public websocketpp::http::parser::parser { @@ -516,6 +519,7 @@ inline std::error_code request_with_file_upload::process(std::string::iterator b class CustomPpConfig : public websocketpp::config::asio { public: + using super = websocketpp::config::asio; typedef CustomPpConfig type; typedef websocketpp::config::asio base; @@ -523,6 +527,7 @@ public: typedef pipedal_elog elog_type; typedef pipedal_alog alog_type; + typedef request_with_file_upload request_type; struct transport_config : public base::transport_config @@ -538,6 +543,8 @@ public: typedef websocketpp::transport::asio::endpoint transport_type; + + typedef transport_type::transport_con_type x; }; size_t CustomPpConfig::max_http_body_size = MAX_READ_SIZE; @@ -693,8 +700,19 @@ namespace pipedal ss << size; request.replace_header(HttpField::content_length, ss.str()); } - virtual void setBody(const std::string &body) { request.set_body(body); } - virtual void keepAlive(bool value) + virtual void setBody(const std::string &body) override{ request.set_body(body); } + virtual void setBodyFile(std::shared_ptr&bodyFile) override { + // cast away const to do what ther request would do if it had that method. + + request.set_body_file(bodyFile->Path(),true); + bodyFile->Detach(); + } + virtual void setBodyFile(std::filesystem::path&path, bool deleteWhenDone) override { + // cast away const to do what ther request would do if it had that method. + request.set_body_file(path,deleteWhenDone); + } + + virtual void keepAlive(bool value) override { if ((!value) || (!ENABLE_KEEP_ALIVE)) { @@ -1126,7 +1144,7 @@ namespace pipedal Lv2Log::error("Failed to open session: %s", e.what()); } - m_endpoint.close(hdl,websocketpp::close::status::abnormal_close, ""); + //m_endpoint.close(hdl,websocketpp::close::status::abnormal_close, ""); } void on_fail(connection_hdl hdl) diff --git a/src/WebServer.hpp b/src/WebServer.hpp index 081a862..c7fa19e 100644 --- a/src/WebServer.hpp +++ b/src/WebServer.hpp @@ -12,6 +12,7 @@ #include "Uri.hpp" #include #include +#include "TemporaryFile.hpp" @@ -40,6 +41,9 @@ public: virtual void set(const std::string&key, const std::string&value) = 0; virtual void setContentLength(size_t size) = 0; virtual void setBody(const std::string&body) = 0; + virtual void setBodyFile(std::shared_ptr&temporaryFile) = 0; + virtual void setBodyFile(std::filesystem::path&path, bool deleteWhenDone) = 0; + virtual void keepAlive(bool value) = 0; }; diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index 45566b1..e453a5e 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -30,24 +30,41 @@ #include "PiPedalUI.hpp" #include "ofstream_synced.hpp" #include "UpdaterSecurity.hpp" +#include "TemporaryFile.hpp" +#include "PresetBundle.hpp" +#define OLD_PRESET_EXTENSION ".piPreset" #define PRESET_EXTENSION ".piPreset" -#define BANK_EXTENSION ".piBank" +#define OLD_BANK_EXTENSION ".piBank" +#define BANK_EXTENSION ".piBankBundle" #define PLUGIN_PRESETS_EXTENSION ".piPluginPresets" +static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"}; + +static const std::string PLUGIN_PRESETS_MIME_TYPE = "application/vnd.pipedal.pluginPresets"; +static const std::string PRESET_MIME_TYPE = "application/vnd.pipedal.preset"; +static const std::string BANK_MIME_TYPE = "application/vnd.pipedal.bank"; using namespace pipedal; using namespace boost::system; +namespace fs = std::filesystem; + + +static bool IsZipFile(const std::filesystem::path &path) +{ + std::ifstream f(path); + if (!f.is_open()) return false; + + char c[4]; + memset(c,0,sizeof(c)); + + f >> c[0] >> c[1] >> c[2] >> c[3]; + + // official file header according to PKware documetnation. + return c[0] == 0x50 && c[1] == 0x4B && c[2] == 0x03 && c[3] == 0x04; + -static std::vector split(const std::string& s, char delimiter) { - std::vector tokens; - std::string token; - std::istringstream tokenStream(s); - while (std::getline(tokenStream, token, delimiter)) { - tokens.push_back(token); - } - return tokens; } class ExtensionChecker { @@ -192,7 +209,14 @@ public: std::string name; std::string content; GetPluginPresets(request_uri, &name, &content); - res.set(HttpField::content_type, "application/json"); + + TemporaryFile tmpFile { WEB_TEMP_DIR}; + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model),content); + presetbundleWriter->WriteToFile(tmpFile.Path()); + size_t contentLength = std::filesystem::file_size(tmpFile.Path()); + + + res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(content.length()); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION)); @@ -204,9 +228,14 @@ public: std::string content; GetPreset(request_uri, &name, &content); - res.set(HttpField::content_type, "application/json"); + TemporaryFile tmpFile { WEB_TEMP_DIR}; + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); + presetbundleWriter->WriteToFile(tmpFile.Path()); + size_t contentLength = std::filesystem::file_size(tmpFile.Path()); + + res.set(HttpField::content_type, PRESET_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); - res.setContentLength(content.length()); + res.setContentLength(contentLength); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION)); return; } @@ -216,10 +245,18 @@ public: std::string content; GetBank(request_uri, &name, &content); - res.set(HttpField::content_type, "application/json"); + TemporaryFile tmpFile { WEB_TEMP_DIR}; + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); + presetbundleWriter->WriteToFile(tmpFile.Path()); + size_t contentLength = std::filesystem::file_size(tmpFile.Path()); + + + + + res.set(HttpField::content_type, BANK_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION)); - res.setContentLength(content.length()); + res.setContentLength(contentLength); return; } throw PiPedalException("Not found."); @@ -252,11 +289,18 @@ public: std::string name; std::string content; GetPluginPresets(request_uri, &name, &content); - res.set(HttpField::content_type, "application/json"); + + std::shared_ptr tmpFile =std::make_shared(WEB_TEMP_DIR); + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model),content); + presetbundleWriter->WriteToFile(tmpFile->Path()); + size_t contentLength = std::filesystem::file_size(tmpFile->Path()); + + + res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); - res.setContentLength(content.length()); + res.setContentLength(contentLength); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION)); - res.setBody(content); + res.setBodyFile(tmpFile); } else if (segment == "downloadPreset") { @@ -264,11 +308,17 @@ public: std::string content; GetPreset(request_uri, &name, &content); - res.set(HttpField::content_type, "application/json"); + std::shared_ptr tmpFile =std::make_shared(WEB_TEMP_DIR); + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); + presetbundleWriter->WriteToFile(tmpFile->Path()); + size_t contentLength = std::filesystem::file_size(tmpFile->Path()); + + + res.set(HttpField::content_type, PRESET_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); - res.setContentLength(content.length()); + res.setContentLength(contentLength); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION)); - res.setBody(content); + res.setBodyFile(tmpFile); } else if (segment == "downloadBank") { @@ -276,11 +326,18 @@ public: std::string content; GetBank(request_uri, &name, &content); - res.set(HttpField::content_type, "application/json"); + std::shared_ptr tmpFile =std::make_shared(WEB_TEMP_DIR); + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); + presetbundleWriter->WriteToFile(tmpFile->Path()); + size_t contentLength = std::filesystem::file_size(tmpFile->Path()); + + + + res.set(HttpField::content_type, BANK_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); - res.setContentLength(content.length()); + res.setContentLength(contentLength); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION)); - res.setBody(content); + res.setBodyFile(tmpFile); } else { @@ -315,6 +372,32 @@ public: return fileNames[0]; } } + static bool HasSingleRootDirectory(ZipFileReader&zipFile) { + bool hasDirectory = false; + std::string previousDirectory; + for (auto& file: zipFile.GetFiles()) + { + auto pos = file.find('/'); + if (pos == std::string::npos) { + // a file in the root. + return false; + } else + { + std::string currentRootDirectory = file.substr(0,pos); + if (hasDirectory) { + if (currentRootDirectory != previousDirectory) + { + return false; + } + } else { + hasDirectory = true; + previousDirectory = currentRootDirectory; + } + } + } + return true; + } + virtual void post_response( const uri &request_uri, HttpRequest &req, @@ -327,11 +410,28 @@ public: if (segment == "uploadPluginPresets") { - json_reader reader(req.get_body_input_stream()); PluginPresets presets; - reader.read(&presets); + fs::path filePath = req.get_body_temporary_file(); + if (filePath.empty()) + { + throw std::runtime_error("Unexpected."); + } + if (IsZipFile(filePath)) + { + auto presetReader = PresetBundleReader::LoadPluginPresetsFile(*(this->model),filePath); + presetReader->ExtractMediaFiles(); + + std::stringstream ss(presetReader->GetPluginPresetsJson()); + json_reader reader(ss); + reader.read(&presets); + + } else { + json_reader reader(req.get_body_input_stream()); + reader.read(&presets); + } model->UploadPluginPresets(presets); + res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); std::stringstream sResult; @@ -343,7 +443,27 @@ public: } else if (segment == "uploadPreset") { - json_reader reader(req.get_body_input_stream()); + + BankFile bankFile; + + fs::path filePath = req.get_body_temporary_file(); + if (filePath.empty()) + { + throw std::runtime_error("Unexpected."); + } + if (IsZipFile(filePath)) + { + auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model),filePath); + presetReader->ExtractMediaFiles(); + + std::stringstream ss(presetReader->GetPresetJson()); + json_reader reader(ss); + reader.read(&bankFile); + } else { + // legacy json format, no zip, no media files. + json_reader reader(req.get_body_input_stream()); + reader.read(&bankFile); + } uint64_t uploadAfter = -1; std::string strUploadAfter = request_uri.query("uploadAfter"); @@ -351,10 +471,6 @@ public: { uploadAfter = std::stol(strUploadAfter); } - - BankFile bankFile; - reader.read(&bankFile); - uint64_t instanceId = model->UploadPreset(bankFile, uploadAfter); res.set(HttpField::content_type, "application/json"); @@ -363,12 +479,31 @@ public: sResult << instanceId; std::string result = sResult.str(); res.setContentLength(result.length()); - res.setBody(result); } else if (segment == "uploadBank") { - json_reader reader(req.get_body_input_stream()); + + BankFile bankFile; + + fs::path filePath = req.get_body_temporary_file(); + if (filePath.empty()) + { + throw std::runtime_error("Unexpected."); + } + if (IsZipFile(filePath)) + { + auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model),filePath); + presetReader->ExtractMediaFiles(); + + std::stringstream ss(presetReader->GetPresetJson()); + json_reader reader(ss); + reader.read(&bankFile); + } else { + // legacy json format, no zip, no media files. + json_reader reader(req.get_body_input_stream()); + reader.read(&bankFile); + } uint64_t uploadAfter = -1; std::string strUploadAfter = request_uri.query("uploadAfter"); @@ -377,9 +512,6 @@ public: uploadAfter = std::stol(strUploadAfter); } - BankFile bankFile; - reader.read(&bankFile); - uint64_t instanceId = model->UploadBank(bankFile, uploadAfter); res.set(HttpField::content_type, "application/json"); @@ -394,10 +526,10 @@ public: { res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); - const std::string instanceId = request_uri.query("id"); - const std::string directory = request_uri.query("directory"); - const std::string filename = request_uri.query("filename"); - const std::string patchProperty = request_uri.query("property"); + std::string instanceId = request_uri.query("id"); + std::string directory = request_uri.query("directory"); + std::string filename = request_uri.query("filename"); + std::string patchProperty = request_uri.query("property"); if (patchProperty.length() == 0 && directory.length() == 0) @@ -415,9 +547,14 @@ public: { ExtensionChecker extensionChecker { request_uri.query("ext") }; namespace fs = std::filesystem; + try { - auto zipFile = ZipFile::Create(req.get_body_temporary_file()); + auto zipFile = ZipFileReader::Create(req.get_body_temporary_file()); std::vector files = zipFile->GetFiles(); + bool hasSingleRootDirectory = HasSingleRootDirectory(*zipFile); + if (!hasSingleRootDirectory) { + directory = (fs::path(directory) / fs::path(filename).filename().replace_extension("")).string(); + } for (const auto&inputFile : files) { if (!inputFile.ends_with("/")) // don't process directory entries. diff --git a/src/ZipFile.cpp b/src/ZipFile.cpp index 6b221f9..b3f5354 100644 --- a/src/ZipFile.cpp +++ b/src/ZipFile.cpp @@ -27,14 +27,20 @@ using namespace pipedal; -ZipFile::ZipFile() +ZipFileReader::ZipFileReader() { } -ZipFile::~ZipFile() +ZipFileReader::~ZipFileReader() +{ +} +ZipFileWriter::ZipFileWriter() +{ +} +ZipFileWriter::~ZipFileWriter() { } -class ZipFileImpl : public ZipFile +class ZipFileImpl : public ZipFileReader { public: ZipFileImpl(const std::filesystem::path &path) @@ -49,6 +55,7 @@ public: } virtual ~ZipFileImpl(); virtual std::vector GetFiles() override; + virtual bool CompareFiles(const std::string &zipName, const std::filesystem::path& path) override; virtual void ExtractTo(const std::string &zipName, const std::filesystem::path &path) override; virtual zip_file_input_stream GetFileInputStream(const std::string& filename,size_t bufferSize = 16*1024) override; virtual size_t GetFileSize(const std::string&filename) override; @@ -59,9 +66,9 @@ private: zip_t *zipFile = nullptr; }; -ZipFile::ptr ZipFile::Create(const std::filesystem::path &path) +ZipFileReader::ptr ZipFileReader::Create(const std::filesystem::path &path) { - return std::shared_ptr(new ZipFileImpl(path)); + return std::shared_ptr(new ZipFileImpl(path)); } ZipFileImpl::~ZipFileImpl() { @@ -88,6 +95,85 @@ std::vector ZipFileImpl::GetFiles() return result; } + +bool ZipFileImpl::CompareFiles(const std::string &zipName, const std::filesystem::path &path) +{ + auto fi = nameMap.find(zipName); + if (fi == nameMap.end()) + { + // must call GetFiles() firest. + throw std::runtime_error("Zip content file not found."); + } + zip_int64_t fileIndex = fi->second; + + zip_stat_t zip_stat; + if (zip_stat_index(zipFile, fileIndex, 0, &zip_stat) < 0) { + throw std::runtime_error(SS("Failed to get file stats: " << zip_strerror(zipFile))); + } + + auto targetSize = std::filesystem::file_size(path); + if (targetSize != zip_stat.size) + { + return false; + } + + zip_file_t *fIn = zip_fopen_index(this->zipFile, fileIndex, 0); + if (fIn == nullptr) + { + zip_error_t *error = zip_get_error(this->zipFile); + const char *strError = zip_error_strerror(error); + + throw std::runtime_error(SS("Failed to read zip content file. " << strError)); + } + Finally t{[fIn]() mutable + { + zip_fclose(fIn); + }}; + + FILE*fTarget = fopen(path.c_str(),"r"); + if (fTarget == nullptr) + { + throw std::runtime_error(SS("Failed to read file " << path << ".")); + } + Finally ffTarget{[fTarget]() mutable { + fclose(fTarget); + }}; + + constexpr int BUFFER_SIZE = 16 * 1024; + std::vector vBuff(BUFFER_SIZE); + std::vector vBuff2(BUFFER_SIZE); + char *pBuff = (char *)&(vBuff[0]); + char *pBuff2 = (char *)&(vBuff2[0]); + while (true) + { + zip_int64_t nRead = zip_fread(fIn, pBuff, BUFFER_SIZE); + if (nRead == -1) + { + zip_error_t *error = zip_file_get_error(fIn); + const char *strError = zip_error_strerror(error); + throw std::runtime_error(SS("Error reading zip content file." << strError)); + } + auto nTargetRead = fread(pBuff2,1,BUFFER_SIZE,fTarget); + + if ((zip_int64_t)nTargetRead != nRead) + { + return false; + } + if (nRead == 0) + { + return true; + } + for (zip_int64_t i = 0; i < nRead; ++i) + { + if (pBuff2[i] != pBuff[i]) + { + return false; + } + } + } +} + + void ZipFileImpl::ExtractTo(const std::string &zipName, const std::filesystem::path &path) { auto fi = nameMap.find(zipName); @@ -120,7 +206,7 @@ void ZipFileImpl::ExtractTo(const std::string &zipName, const std::filesystem::p while (true) { zip_int64_t nRead = zip_fread(fIn, pBuff, BUFFER_SIZE); - if (nRead = 0) + if (nRead == 0) break; if (nRead == -1) { @@ -215,3 +301,68 @@ size_t ZipFileImpl::GetFileSize(const std::string&filename) } return stat.size; } + +class ZipFileWriterImpl : public ZipFileWriter +{ +public: + ZipFileWriterImpl(const std::filesystem::path &path) + : path(path) + { + int errorOp = 0; + zipFile = zip_open(path.c_str(), ZIP_CREATE | ZIP_TRUNCATE, &errorOp); + if (zipFile == nullptr) + { + throw std::runtime_error("Can't open zip file."); + } + } + virtual ~ZipFileWriterImpl(); + + virtual void Close() override; + + virtual void WriteFile(const std::string &zipFilename, const std::filesystem::path&sourceFilePath) override; + virtual void WriteFile(const std::string&filename, const void*buffer, size_t length) override; +private: + std::map nameMap; // avoid o(2) extraction operations. + const std::filesystem::path path; + zip_t *zipFile = nullptr; +}; + +ZipFileWriter::ptr ZipFileWriter::Create(const std::filesystem::path &path) +{ + return std::make_unique(path); +} + +void ZipFileWriterImpl::Close() { + if (zipFile) { + zip_close(zipFile); + zipFile = nullptr; + } +} +ZipFileWriterImpl::~ZipFileWriterImpl() +{ + Close(); +} + +void ZipFileWriterImpl::WriteFile(const std::string &filename, const std::filesystem::path &path) +{ + zip_error_t error; + zip_source_t *source = zip_source_file_create(path.c_str(),0,-1,&error); + if (!source) { + throw std::runtime_error(SS("Unable to create zip source for file " << path)); + } + if (zip_file_add(zipFile,filename.c_str(),source,ZIP_FL_ENC_UTF_8) < 0) + { + zip_source_free(source); + throw std::runtime_error(SS("Unable to create add file " << path)); + } +} +void ZipFileWriterImpl::WriteFile(const std::string&filename, const void*buffer, size_t length) +{ + zip_source_t *source = zip_source_buffer(zipFile,buffer,length,0); + + if (zip_file_add(zipFile,filename.c_str(),source,ZIP_FL_ENC_UTF_8) < 0) + { + zip_source_free(source); + throw std::runtime_error(SS("Failed to add file to zip: " << zip_strerror(zipFile))); + } +} diff --git a/src/ZipFile.hpp b/src/ZipFile.hpp index 61390fe..4122d98 100644 --- a/src/ZipFile.hpp +++ b/src/ZipFile.hpp @@ -28,6 +28,12 @@ namespace pipedal { + class ZipFileFileReader { + public: + private: + zip_file_t*file = nullptr; + }; + class zip_file_input_stream_buf : public std::streambuf { private: zip_file_t* file; @@ -51,21 +57,44 @@ namespace pipedal { } }; - class ZipFile { + class ZipFileReader { protected: - ZipFile(); + ZipFileReader(); public: - using ptr = std::shared_ptr; + using ptr = std::shared_ptr; static ptr Create(const std::filesystem::path &path); - ZipFile(const ZipFile&) = delete; - ZipFile&operator=(const ZipFile&) = delete; - virtual ~ZipFile(); + ZipFileReader(const ZipFileReader&) = delete; + ZipFileReader&operator=(const ZipFileReader&) = delete; + virtual ~ZipFileReader(); virtual std::vector GetFiles() = 0; virtual void ExtractTo(const std::string &zipName, const std::filesystem::path& path) = 0; + virtual bool CompareFiles(const std::string &zipName, const std::filesystem::path& path) = 0; virtual zip_file_input_stream GetFileInputStream(const std::string& filename,size_t bufferSize = 16*1024) = 0; virtual size_t GetFileSize(const std::string&filename) = 0; }; + + class ZipFileWriter; + + + class ZipFileWriter { + protected: + ZipFileWriter(); + + public: + using self = ZipFileWriter(); + using ptr = std::shared_ptr; + static ptr Create(const std::filesystem::path &path); + + ZipFileWriter(const ZipFileReader&) = delete; + ZipFileWriter&operator=(const ZipFileWriter&) = delete; + virtual ~ZipFileWriter(); + + virtual void Close() = 0; + + virtual void WriteFile(const std::string &filename, const std::filesystem::path&path) = 0; + virtual void WriteFile(const std::string&filename, const void*buffer, size_t length) = 0; + }; } \ No newline at end of file diff --git a/todo.txt b/todo.txt index e1a64c1..f83fd77 100644 --- a/todo.txt +++ b/todo.txt @@ -1,53 +1,55 @@ -- X test reconnect sequence in client. +ToobFreeverb has a big memory leak! + +Do we have to deactivate both Audio handles after an underrun? I think so. + +Error and message and loading overlays have to go above PerformanceView + +Back Button sequence for performance view. + +Review state changing code. Do we need this anymmore? I think not. + + +Review current handleNotifyPatchProperty. I tink server-side support needs to be +removed, and rely on handleNotifyPathPatchPropertyChanged instead. + +private handleNotifyPatchProperty(clientHandle: number, instanceId: number, propertyUri: string, jsonObject: any) { + +Midi bindings. + +SEGV at void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId) + bool changed = this->audioHost->UpdatePluginState(*item); + PiPedalModel.cpp:375 + + because audioHost is gone. + revamp shutdown procedure for the audio thread. + write a marker on exit, and don't release + audioHost UNTIL the service thread has read the close marker. + +- continuing corrupt memory. +- LV2 STATE and properties DO NOT MATCH in saved files. +- LV2_STATE is not populated wiht properties are. +- properties are not popluate when LV_sate Is. +- FINISH PATH property running updates code. + Maybe we should be using LV2_State (if we can capture it + propertly. ) + --X remove 10-pipedal-networkmanager.rules before testing installer. -- new signing procedure. - verify that scanning doesn't cause overruns. - verify address change behaviour in client -- verify stripping in installer. -- verify update with no keyring. -verify clean removal of dhcpcd and nm_p2p2d X Review docs changes once we go live. -- Back button after reloads on Android. - -Localisation: support non-UTF8 code pages. +- Localisation: support non-UTF8 code pages. - unicode commandline arguments. - review unicode filenames (this is probably ok) -- make app use the same theme settings as the website. (make the website use the same theme as the app) - BUG: gcs when we have an animated output control -- versioning. - - - - - - -Package: raspi-firmware -Version: 1:1.20240902-1 - -Package: raspi-firmware -Version: 1.20220830+ds-1 - - -Package: firmware-brcm80211 -Source: firmware-nonfree -Version: 1:20230625-2+rpt3 - -Package: f - - -downgrade: -sudo apt install raspi-firmware=1.20220830+ds-1 -sudo apt install firmware-brcm80211=1:20230625-2+rpt2 - Pri Description ----------------- -8 Migrate to Vite toolchain. 5 Re-use plugin instances when rebuilding pedalboard. +8 Migrate to Vite toolchain.