Files
op-pedal/src/PiPedalModel.cpp
T

3108 lines
95 KiB
C++

// Copyright (c) 2022-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 "pch.h"
#include <future>
#include "ServiceConfiguration.hpp"
#include "AudioConfig.hpp"
#include "ConfigUtil.hpp"
#include <sched.h>
#include "PiPedalModel.hpp"
#include "AudioHost.hpp"
#include "Lv2Log.hpp"
#include <set>
#include "PiPedalConfiguration.hpp"
#include "AdminClient.hpp"
#include "SplitEffect.hpp"
#include "CpuGovernor.hpp"
#include "RegDb.hpp"
#include "RingBufferReader.hpp"
#include "PiPedalUI.hpp"
#include "atom_object.hpp"
#include "Lv2PluginChangeMonitor.hpp"
#include "HotspotManager.hpp"
#include "DBusToLv2Log.hpp"
#include "SysExec.hpp"
#include "Updater.hpp"
#include "util.hpp"
#include "DBusLog.hpp"
#include "AvahiService.hpp"
#include "DummyAudioDriver.hpp"
#include "AudioFiles.hpp"
#include "CrashGuard.hpp"
#ifndef NO_MLOCK
#include <sys/mman.h>
#endif /* NO_MLOCK */
using namespace pipedal;
namespace fs = std::filesystem;
template <typename T>
T &constMutex(const T &mutex)
{
return const_cast<T &>(mutex);
}
static const char *hexChars = "0123456789ABCDEF";
static std::string BytesToHex(const std::vector<uint8_t> &bytes)
{
std::stringstream s;
for (size_t i = 0; i < bytes.size(); ++i)
{
uint8_t b = bytes[i];
s << hexChars[(b >> 4) & 0x0F] << hexChars[b & 0x0F];
}
return s.str();
}
PiPedalModel::PiPedalModel()
: pluginHost(),
atomConverter(pluginHost.GetMapFeature())
{
this->updater = Updater::Create();
this->updater->Start();
this->currentUpdateStatus = updater->GetCurrentStatus();
this->pedalboard = Pedalboard::MakeDefault();
#if JACK_HOST
this->jackServerSettings = this->storage.GetJackServerSettings(); // to get onboarding flag.
this->jackServerSettings.ReadJackDaemonConfiguration();
#else
this->jackServerSettings = this->storage.GetJackServerSettings();
#endif
updater->SetUpdateListener(
[this](const UpdateStatus &updateStatus)
{
this->OnUpdateStatusChanged(updateStatus);
});
DbusLogToLv2Log();
SetDBusLogLevel(DBusLogLevel::Info);
hotspotManager = HotspotManager::Create();
hotspotManager->SetNetworkChangingListener(
[this](bool ethernetConnected, bool hotspotEnabling)
{
OnNetworkChanging(ethernetConnected, hotspotEnabling);
});
hotspotManager->SetHasWifiListener(
[this](bool hasWifi)
{
this->SetHasWifi(hasWifi);
});
// don't actuall start the hotspotManager until after LV2 is initialized (in order to avoid logging oddities)
}
void PrepareSnapshostsForSave(Pedalboard &pedalboard)
{
if (pedalboard.selectedSnapshot() != -1)
{
auto &currentSnapshot = pedalboard.snapshots()[pedalboard.selectedSnapshot()];
if (!currentSnapshot || currentSnapshot->isModified_)
{
pedalboard.selectedSnapshot(-1);
}
}
for (auto &snapshot : pedalboard.snapshots())
{
if (snapshot)
{
snapshot->isModified_ = false;
}
}
}
void PiPedalModel::Close()
{
std::unique_ptr<AudioHost> oldAudioHost;
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (closed)
{
return;
}
closed = true;
CancelAudioRetry();
if (avahiService)
{
this->avahiService = nullptr; // and close.
}
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->Close();
}
this->subscribers.resize(0);
oldAudioHost = std::move(this->audioHost);
} // end lock.
// lockless to avoid deadlocks while shutting down the audio thread.
if (oldAudioHost)
{
oldAudioHost->Close();
}
}
PiPedalModel::~PiPedalModel()
{
CancelNetworkChangingTimer();
hotspotManager = nullptr; // turn off the hotspot.
pluginChangeMonitor = nullptr; // stop monitorin LV2 directories.
try
{
adminClient.UnmonitorGovernor();
}
catch (...) // noexcept here!
{
}
try
{
CurrentPreset currentPreset;
currentPreset.modified_ = this->hasPresetChanged;
currentPreset.preset_ = this->pedalboard;
storage.SaveCurrentPreset(currentPreset);
}
catch (...)
{
}
try
{
if (audioHost)
{
audioHost->Close();
}
}
catch (...)
{
}
}
#include <fstream>
void PiPedalModel::Init(const PiPedalConfiguration &configuration)
{
std::lock_guard<std::recursive_mutex> lock(mutex); // prevent callbacks while we're initializing.
this->configuration = configuration;
pluginHost.SetConfiguration(configuration);
storage.SetConfigRoot(configuration.GetDocRoot());
storage.SetDataRoot(configuration.GetLocalStoragePath());
storage.Initialize();
pluginHost.SetPluginStoragePath(storage.GetPluginUploadDirectory());
this->systemMidiBindings = storage.GetSystemMidiBindings();
#if JACK_HOST
this->jackConfiguration = this->jackConfiguration.JackInitialize();
#else
this->jackServerSettings = storage.GetJackServerSettings();
#endif
}
void PiPedalModel::LoadLv2PluginInfo()
{
// Not all Lv2 directories have the lv2 base declarations. Load a full set of plugin classes generated on a default /usr/local/lib/lv2 directory.
std::filesystem::path pluginClassesPath = configuration.GetDocRoot() / "plugin_classes.json";
try
{
if (!std::filesystem::exists(pluginClassesPath))
throw PiPedalException("File not found.");
pluginHost.LoadPluginClassesFromJson(pluginClassesPath);
}
catch (const std::exception &e)
{
std::stringstream s;
s << "Unable to load " << pluginClassesPath << ". " << e.what();
throw PiPedalException(s.str().c_str());
}
pluginChangeMonitor = std::make_unique<Lv2PluginChangeMonitor>(*this);
pluginHost.LoadLilv(configuration.GetLv2Path().c_str());
// Copy all presets out of Lilv data to json files
// so that we can close lilv while we're actually
// running.
for (const auto &plugin : pluginHost.GetPlugins())
{
if (plugin->has_factory_presets())
{
if (!storage.HasPluginPresets(plugin->uri()))
{
PluginPresets pluginPresets = pluginHost.GetFactoryPluginPresets(plugin->uri());
storage.SavePluginPresets(plugin->uri(), pluginPresets);
}
}
}
}
void PiPedalModel::Load()
{
this->webRoot = configuration.GetWebRoot();
this->webPort = (uint16_t)configuration.GetSocketServerPort();
adminClient.MonitorGovernor(storage.GetGovernorSettings());
// pluginHost.Load(configuration.GetLv2Path().c_str());
this->pedalboard = storage.GetCurrentPreset(); // the current *saved* preset.
// the current edited preset, saved only across orderly shutdowns.
CrashGuard::SetCrashGuardFileName(storage.GetDataRoot() / "crash_guard.data");
if (CrashGuard::HasCrashed()) {
// ignore the current preset, and load a blank pedalboard in order to avoid a potential plugin crash.
this->pedalboard = Pedalboard::MakeDefault();
} else {
CurrentPreset currentPreset;
try
{
if (storage.RestoreCurrentPreset(&currentPreset))
{
this->pedalboard = currentPreset.preset_;
this->hasPresetChanged = currentPreset.modified_;
}
}
catch (const std::exception &e)
{
Lv2Log::warning(SS("Failed to load current preset. " << e.what()));
}
}
UpdateDefaults(&this->pedalboard);
std::unique_ptr<AudioHost> p{AudioHost::CreateInstance(pluginHost.asIHost())};
this->audioHost = std::move(p);
this->audioHost->SetNotificationCallbacks(this);
this->systemMidiBindings = storage.GetSystemMidiBindings();
this->audioHost->SetSystemMidiBindings(this->systemMidiBindings);
audioHost->SetAlsaSequencerConfiguration(storage.GetAlsaSequencerConfiguration());
if (configuration.GetMLock())
{
#ifndef NO_MLOCK
int result = mlockall(MCL_CURRENT | MCL_FUTURE);
if (result)
{
throw PiPedalStateException("mlockall failed. You can disable the call to mlockall in 'config.json'.");
}
#endif
}
RestartAudio();
}
IPiPedalModelSubscriber *PiPedalModel::GetNotificationSubscriber(int64_t clientId)
{
for (size_t i = 0; i < subscribers.size(); ++i)
{
if (subscribers[i]->GetClientId() == clientId)
{
return subscribers[i].get();
}
}
return nullptr;
}
void PiPedalModel::AddNotificationSubscription(std::shared_ptr<IPiPedalModelSubscriber> pSubscriber)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
this->subscribers.push_back(pSubscriber);
}
void PiPedalModel::RemoveNotificationSubsription(std::shared_ptr<IPiPedalModelSubscriber> pSubscriber)
{
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto it = this->subscribers.begin(); it != this->subscribers.end(); ++it)
{
if ((*it).get() == pSubscriber.get())
{
this->subscribers.erase(it);
break;
}
}
int64_t clientId = pSubscriber->GetClientId();
this->DeleteMidiListeners(clientId);
this->DeleteAtomOutputListeners(clientId);
for (int i = 0; i < this->outstandingParameterRequests.size(); ++i)
{
if (outstandingParameterRequests[i]->clientId == clientId)
{
outstandingParameterRequests.erase(outstandingParameterRequests.begin() + i);
--i;
}
}
}
}
void PiPedalModel::PreviewControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value)
{
IEffect *effect = lv2Pedalboard->GetEffect(pedalItemId);
if (!effect)
{
return;
}
if (effect->IsVst3())
{
int index = lv2Pedalboard->GetControlIndex(pedalItemId, symbol);
if (index != -1)
{
effect->SetControl(index, value);
}
}
else
{
audioHost->SetControlValue(pedalItemId, symbol, value);
}
}
// we were explicitly told that the state was changed by the plugin
void PiPedalModel::OnNotifyLv2StateChanged(uint64_t instanceId)
{
// a sent PATCH_Set, or an explicit state changed notification.
OnNotifyMaybeLv2StateChanged(instanceId);
this->SetPresetChanged(-1, true, false);
}
// The plugin notified us that a path path property changed. The state *purrobably changed.
void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
{
// one or more received PATCH_Sets, which MAY change the state.
std::lock_guard<std::recursive_mutex> lock(mutex);
PedalboardItem *item = pedalboard.GetItem(instanceId);
if (item != nullptr)
{
if (!audioHost)
{
return;
}
bool changed = this->audioHost->UpdatePluginState(*item);
if (changed)
{
item->stateUpdateCount(item->stateUpdateCount() + 1);
Lv2PluginState newState = item->lv2State();
FireLv2StateChanged(instanceId, newState);
}
}
}
void PiPedalModel::SetInputVolume(float value)
{
PreviewInputVolume(value);
{
std::lock_guard<std::recursive_mutex> lock(mutex);
this->pedalboard.input_volume_db(value);
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnInputVolumeChanged(value);
}
this->SetPresetChanged(-1, true);
}
}
void PiPedalModel::SetOutputVolume(float value)
{
PreviewOutputVolume(value);
{
std::lock_guard<std::recursive_mutex> lock(mutex);
this->pedalboard.output_volume_db(value);
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnOutputVolumeChanged(value);
}
this->SetPresetChanged(-1, true);
}
}
void PiPedalModel::PreviewInputVolume(float value)
{
audioHost->SetInputVolume(value);
}
void PiPedalModel::PreviewOutputVolume(float value)
{
audioHost->SetOutputVolume(value);
}
void PiPedalModel::SetControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value)
{
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (!this->pedalboard.SetControlValue(pedalItemId, symbol, value))
{
return;
}
PedalboardItem *item = pedalboard.GetItem(pedalItemId);
// change of split type requires rebuild of the effect
// since it can change the number of output channels.
if (item != nullptr && item->isSplit() && symbol == "splitType")
{
this->FirePedalboardChanged(clientId);
return;
}
PreviewControl(clientId, pedalItemId, symbol, value);
{
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnControlChanged(clientId, pedalItemId, symbol, value);
}
this->SetPresetChanged(clientId, true);
}
}
}
void PiPedalModel::FireJackConfigurationChanged(const JackConfiguration &jackConfiguration)
{
// noify subscribers.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnJackConfigurationChanged(jackConfiguration);
}
}
void PiPedalModel::FireBanksChanged(int64_t clientId)
{
// noify subscribers.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnBankIndexChanged(this->storage.GetBanks());
}
}
void PiPedalModel::FirePedalboardChanged(int64_t clientId, bool loadAudioThread)
{
if (loadAudioThread)
{
// notify the audio thread.
if (audioHost && audioHost->IsOpen())
{
LoadCurrentPedalboard();
UpdateRealtimeVuSubscriptions();
UpdateRealtimeMonitorPortSubscriptions();
}
}
// noify subscribers.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnPedalboardChanged(clientId, this->pedalboard);
}
}
void PiPedalModel::SetPedalboard(int64_t clientId, Pedalboard &pedalboard)
{
{
std::lock_guard<std::recursive_mutex> lock(mutex);
this->pedalboard = pedalboard;
UpdateDefaults(&this->pedalboard);
this->FirePedalboardChanged(clientId);
this->SetPresetChanged(clientId, true);
}
}
void PiPedalModel::SetSnapshot(int64_t selectedSnapshot)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (this->pedalboard.ApplySnapshot(selectedSnapshot, pluginHost))
{
this->pedalboard.selectedSnapshot(selectedSnapshot);
for (auto snapshot : this->pedalboard.snapshots())
{
if (snapshot)
{
snapshot->isModified_ = false;
}
}
FirePedalboardChanged(-1, true);
}
}
void PiPedalModel::SetSnapshots(std::vector<std::shared_ptr<Snapshot>> &snapshots, int64_t selectedSnapshot)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
UpdateVst3Settings(pedalboard);
this->pedalboard.snapshots(std::move(snapshots));
if (selectedSnapshot != -1)
{
this->pedalboard.selectedSnapshot(selectedSnapshot);
for (auto &snapshot : pedalboard.snapshots())
{
if (snapshot)
{
snapshot->isModified_ = false;
}
}
}
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)
this->SetPresetChanged(-1, true, false);
}
void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalboard)
{
{
std::lock_guard<std::recursive_mutex> lock(mutex);
// update vst3 presets if neccessary.
// the pedalboard must be a manipualted instance of the current Lv2Pedalboard.
UpdateVst3Settings(pedalboard);
Lv2PedalboardErrorList errorMessages;
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard{
this->pluginHost.UpdateLv2PedalboardStructure(pedalboard, this->lv2Pedalboard.get(), errorMessages)};
this->lv2Pedalboard = lv2Pedalboard;
// apply the error messages to the lv2Pedalboard.
// return true if the error messages have changed
audioHost->SetPedalboard(lv2Pedalboard);
this->pedalboard = pedalboard;
previousPedalboard = this->pedalboard;
previousPedalboardLoaded = true;
this->pedalboard = pedalboard;
UpdateRealtimeVuSubscriptions();
UpdateRealtimeMonitorPortSubscriptions();
this->FirePedalboardChanged(clientId, false);
this->SetPresetChanged(clientId, true);
}
}
void PiPedalModel::SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceId, bool enabled)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
{
this->pedalboard.SetItemUseModUi(instanceId, enabled);
// Notify clients.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnItemUseModUiChanged(clientId, instanceId, enabled);
}
this->SetPresetChanged(clientId, true);
}
}
void PiPedalModel::SetPedalboardItemEnable(int64_t clientId, int64_t pedalItemId, bool enabled)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
{
this->pedalboard.SetItemEnabled(pedalItemId, enabled);
// Notify clients.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnItemEnabledChanged(clientId, pedalItemId, enabled);
}
this->SetPresetChanged(clientId, true);
// Notify audo thread.
this->audioHost->SetBypass(pedalItemId, enabled);
}
}
void PiPedalModel::GetPresets(PresetIndex *pResult)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
this->storage.GetPresetIndex(pResult);
pResult->presetChanged(this->hasPresetChanged);
}
Pedalboard PiPedalModel::GetPreset(int64_t instanceId)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return this->storage.GetPreset(instanceId);
}
void PiPedalModel::GetBank(int64_t instanceId, BankFile *pResult)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
this->storage.GetBankFile(instanceId, pResult);
}
void PiPedalModel::SetPresetChanged(int64_t clientId, bool value, bool changeSnapshotSelect)
{
if (changeSnapshotSelect && value && this->pedalboard.selectedSnapshot() != -1)
{
auto &snapshot = this->pedalboard.snapshots()[pedalboard.selectedSnapshot()];
if (snapshot)
{
if (!snapshot->isModified_)
{
snapshot->isModified_ = true;
FireSnapshotModified(pedalboard.selectedSnapshot(), true);
}
}
this->pedalboard.SetCurrentSnapshotModified(true);
}
if (value != this->hasPresetChanged)
{
hasPresetChanged = value;
FirePresetChanged(value);
}
}
void PiPedalModel::FireSnapshotModified(int64_t snapshotIndex, bool modified)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
{
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnSnapshotModified(snapshotIndex, modified);
}
}
}
void PiPedalModel::FireSelectedSnapshotChanged(int64_t selectedSnapshot)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
{
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnSelectedSnapshotChanged(selectedSnapshot);
}
}
}
void PiPedalModel::FirePresetChanged(bool changed)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
{
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
PresetIndex presets;
GetPresets(&presets);
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnPresetChanged(changed);
}
}
}
void PiPedalModel::FirePresetsChanged(int64_t clientId)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
{
PresetIndex presets;
GetPresets(&presets);
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnPresetsChanged(clientId, presets);
}
}
}
void PiPedalModel::FirePluginPresetsChanged(const std::string &pluginUri)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
{
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnPluginPresetsChanged(pluginUri);
}
}
}
void PiPedalModel::UpdateVst3Settings(Pedalboard &pedalboard)
{
// get the vst3 state bundle from lv2Pedalboard for the current pedalboard.
#if ENABLE_VST3
Pedalboard pb;
for (IEffect *effect : lv2Pedalboard->GetEffects())
{
if (effect->IsVst3())
{
PedalboardItem *item = pedalboard.GetItem(effect->GetInstanceId());
if (item)
{
Vst3Effect *vst3Effect = (Vst3Effect *)effect;
std::vector<uint8_t> state;
if (vst3Effect->GetState(&state))
{
item->vstState(BytesToHex(state));
}
}
}
}
#endif
}
void PiPedalModel::FireLv2StateChanged(int64_t instanceId, const Lv2PluginState &lv2State)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnLv2StateChanged(instanceId, lv2State);
}
}
// referesh the plugin state for all plugins.
bool PiPedalModel::SyncLv2State()
{
bool changed = false;
auto pedalboardItems = pedalboard.GetAllPlugins();
for (PedalboardItem *item : pedalboardItems)
{
if (!item->isSplit())
{
if (audioHost->UpdatePluginState(*item))
{
FireLv2StateChanged(item->instanceId(), item->lv2State());
changed = true;
}
}
}
return changed;
}
void PiPedalModel::SaveCurrentPreset(int64_t clientId)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
UpdateVst3Settings(this->pedalboard);
SyncLv2State();
PrepareSnapshostsForSave(pedalboard);
storage.SaveCurrentPreset(this->pedalboard);
this->SetPresetChanged(clientId, false);
}
uint64_t PiPedalModel::CopyPluginPreset(const std::string &pluginUri, uint64_t presetId)
{
uint64_t result = storage.CopyPluginPreset(pluginUri, presetId);
FirePluginPresetsChanged(pluginUri);
return result;
}
void PiPedalModel::UpdatePluginPresets(const PluginUiPresets &pluginPresets)
{
storage.UpdatePluginPresets(pluginPresets);
FirePluginPresetsChanged(pluginPresets.pluginUri_);
}
int64_t PiPedalModel::SavePluginPresetAs(int64_t instanceId, const std::string &name)
{
PedalboardItem *item = this->pedalboard.GetItem(instanceId);
if (!item)
{
throw PiPedalException("Plugin not found.");
}
uint64_t presetId = storage.SavePluginPreset(name, *item);
FirePluginPresetsChanged(item->uri());
return presetId;
}
int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, const std::string &name, int64_t saveAfterInstanceId)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
SyncLv2State();
auto pedalboard = this->pedalboard.DeepCopy();
PrepareSnapshostsForSave(pedalboard);
UpdateVst3Settings(pedalboard);
pedalboard.name(name);
int64_t result = storage.SaveCurrentPresetAs(pedalboard, name, saveAfterInstanceId);
FirePresetsChanged(clientId);
return result;
}
void PiPedalModel::UploadPluginPresets(const PluginPresets &pluginPresets)
{
if (pluginPresets.pluginUri_.length() == 0)
{
throw PiPedalException("Invalid plugin presets.");
}
std::lock_guard<std::recursive_mutex> guard{mutex};
storage.MergePluginPresets(pluginPresets.pluginUri_, pluginPresets);
FirePluginPresetsChanged(pluginPresets.pluginUri_);
}
int64_t PiPedalModel::UploadPreset(const BankFile &bankFile, int64_t uploadAfter)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
int64_t newPreset = this->storage.UploadPreset(bankFile, uploadAfter);
FirePresetsChanged(-1);
return newPreset;
}
int64_t PiPedalModel::UploadBank(BankFile &bankFile, int64_t uploadAfter)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
int64_t newPreset = this->storage.UploadBank(bankFile, uploadAfter);
FireBanksChanged(-1);
return newPreset;
}
void PiPedalModel::NextBank(Direction direction)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
auto bankIndex = this->GetBankIndex();
if (bankIndex.entries().size() == 0)
{
return;
}
size_t index = 0;
for (size_t i = 0; i < bankIndex.entries().size(); ++i)
{
auto &entry = bankIndex.entries()[i];
if (entry.instanceId() == bankIndex.selectedBank())
{
index = i;
break;
}
}
if (direction == Direction::Increase)
{
++index;
if (index >= bankIndex.entries().size())
{
index = 0;
}
}
else
{
if (index == 0)
{
index = bankIndex.entries().size() - 1;
}
else
{
--index;
}
}
this->OpenBank(-1, bankIndex.entries()[index].instanceId());
}
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<std::recursive_mutex> guard{mutex};
try
{
if (request.direction >= 0)
{
NextPreset();
}
else
{
PreviousPreset();
}
}
catch (std::exception &e)
{
Lv2Log::error(e.what());
}
if (this->audioHost)
{
this->audioHost->AckMidiProgramRequest(request.requestId);
}
}
void PiPedalModel::OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex, int64_t snapshotRequestId)
{
try
{
SetSnapshot((int64_t)snapshotIndex);
}
catch (const std::exception &e)
{
Lv2Log::error(SS("SetSnapshot failed. " << e.what()));
}
if (this->audioHost)
{
this->audioHost->AckSnapshotRequest(snapshotRequestId);
}
}
void PiPedalModel::OnNotifyNextMidiBank(const RealtimeNextMidiProgramRequest &request)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
try
{
if (request.direction >= 0)
{
NextBank();
}
else
{
PreviousBank();
}
}
catch (std::exception &e)
{
Lv2Log::error(e.what());
}
if (this->audioHost)
{
this->audioHost->AckMidiProgramRequest(request.requestId);
}
}
void PiPedalModel::OnNotifyMidiProgramChange(RealtimeMidiProgramRequest &midiProgramRequest)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
try
{
if (midiProgramRequest.bank >= 0)
{
int64_t bankId = storage.GetBankByMidiBankNumber(midiProgramRequest.bank);
if (bankId == -1)
throw PiPedalException("Bank not found.");
if (bankId != this->storage.GetBanks().selectedBank())
{
storage.LoadBank(bankId);
FireBanksChanged(-1);
FirePresetsChanged(-1);
}
}
int64_t presetId = storage.GetPresetByProgramNumber(midiProgramRequest.program);
if (presetId == -1)
throw PiPedalException("No valid preset.");
LoadPreset(-1, presetId);
}
catch (std::exception &e)
{
Lv2Log::error(e.what());
}
if (this->audioHost)
{
this->audioHost->AckMidiProgramRequest(midiProgramRequest.requestId);
}
}
void PiPedalModel::LoadPreset(int64_t clientId, int64_t instanceId)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
if (storage.LoadPreset(instanceId))
{
this->pedalboard = storage.GetCurrentPreset();
UpdateDefaults(&this->pedalboard);
this->hasPresetChanged = false; // no fire.
this->FirePedalboardChanged(clientId);
this->FirePresetsChanged(clientId); // fire now.
}
}
int64_t PiPedalModel::CopyPreset(int64_t clientId, int64_t from, int64_t to)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
int64_t result = storage.CopyPreset(from, to);
if (result != -1)
{
this->FirePresetsChanged(clientId); // fire now.
}
else
{
throw PiPedalStateException("Copy failed.");
}
return result;
}
bool PiPedalModel::UpdatePresets(int64_t clientId, const PresetIndex &presets)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
storage.SetPresetIndex(presets);
FirePresetsChanged(clientId);
return true;
}
void PiPedalModel::MoveBank(int64_t clientId, int from, int to)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
storage.MoveBank(from, to);
FireBanksChanged(clientId);
}
int64_t PiPedalModel::DeleteBank(int64_t clientId, int64_t instanceId)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
int64_t selectedBank = this->storage.GetBanks().selectedBank();
int64_t newSelection = storage.DeleteBank(instanceId);
int64_t newSelectedBank = this->storage.GetBanks().selectedBank();
this->FireBanksChanged(clientId); // fire now.
if (newSelectedBank != selectedBank)
{
this->OpenBank(clientId, newSelectedBank);
}
return newSelection;
}
int64_t PiPedalModel::DeletePreset(int64_t clientId, int64_t instanceId)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
int64_t oldSelection = storage.GetCurrentPresetId();
int64_t newSelection = storage.DeletePreset(instanceId);
this->FirePresetsChanged(clientId); // fire now.
if (oldSelection != newSelection)
{
this->LoadPreset(
-1, // can't use cached version.
newSelection);
}
return newSelection;
}
bool PiPedalModel::RenamePreset(int64_t clientId, int64_t instanceId, const std::string &name)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (storage.RenamePreset(instanceId, name))
{
this->FirePresetsChanged(clientId);
if (storage.GetCurrentPresetId() == instanceId)
{
this->pedalboard.name(name);
this->FirePedalboardChanged(-1);
}
return true;
}
else
{
throw PiPedalStateException("Rename failed.");
}
}
GovernorSettings PiPedalModel::GetGovernorSettings()
{
{
std::lock_guard<std::recursive_mutex> lock(mutex);
GovernorSettings result;
result.governor_ = storage.GetGovernorSettings();
result.governors_ = pipedal::GetAvailableGovernors();
return result;
}
}
void PiPedalModel::SetGovernorSettings(const std::string &governor)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
adminClient.SetGovernorSettings(governor);
this->storage.SetGovernorSettings(governor);
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnGovernorSettingsChanged(governor);
}
}
void PiPedalModel::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSettings)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
#if NEW_WIFI_CONFIG
if (this->storage.SetWifiConfigSettings(wifiConfigSettings))
{
this->UpdateDnsSd();
if (this->hotspotManager)
{
this->hotspotManager->Reload();
}
}
#else
this->storage.SetWifiConfigSettings(wifiConfigSettings);
adminClient.SetWifiConfig(wifiConfigSettings);
#endif
{
WifiConfigSettings settingsWithNoSecrets = storage.GetWifiConfigSettings(); // (the passwordless version)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnWifiConfigSettingsChanged(settingsWithNoSecrets);
}
}
}
static std::string GetP2pdName()
{
std::string name = "/etc/pipedal/config/pipedal_p2pd.conf";
std::string result;
if (ConfigUtil::GetConfigLine(name, "p2p_device_name", &result))
{
return result;
}
return "";
}
void PiPedalModel::UpdateDnsSd()
{
if (!avahiService)
{
throw std::runtime_error("Not ready.");
}
ServiceConfiguration deviceIdFile;
deviceIdFile.Load();
WifiConfigSettings wifiSettings;
wifiSettings.Load();
std::string serviceName = wifiSettings.hotspotName_;
if (serviceName == "")
{
serviceName = deviceIdFile.deviceName;
}
if (serviceName == "")
{
serviceName = "pipedal";
}
std::string hostName = GetHostName();
if (serviceName != "" && deviceIdFile.uuid != "")
{
avahiService->Announce(webPort, serviceName, deviceIdFile.uuid, hostName, true);
}
else
{
// device_uuid file is written at install time. This warning is harmless if you're debugging.
// Without it, we can't pulblish the website via dnsDS.
// Run "pipedalconfig --install" to create the file.
Lv2Log::warning("Cant read device_uuid file from service.conf file. dnsSD announcement skipped.");
}
}
void PiPedalModel::SetWifiDirectConfigSettings(const WifiDirectConfigSettings &wifiDirectConfigSettings)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
adminClient.SetWifiDirectConfig(wifiDirectConfigSettings);
this->storage.SetWifiDirectConfigSettings(wifiDirectConfigSettings);
{
WifiDirectConfigSettings tWifiDirectConfigSettings = storage.GetWifiDirectConfigSettings(); // (the passwordless version)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnWifiDirectConfigSettingsChanged(tWifiDirectConfigSettings);
}
// update NSD-SD announement.
UpdateDnsSd();
}
}
WifiConfigSettings PiPedalModel::GetWifiConfigSettings()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return this->storage.GetWifiConfigSettings();
}
WifiDirectConfigSettings PiPedalModel::GetWifiDirectConfigSettings()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return this->storage.GetWifiDirectConfigSettings();
}
void PiPedalModel::SetShowStatusMonitor(bool show)
{
{
std::lock_guard<std::recursive_mutex> lock(mutex); // copy atomically.
storage.SetShowStatusMonitor(show);
// Notify clients.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnShowStatusMonitorChanged(show);
}
}
}
bool PiPedalModel::GetShowStatusMonitor()
{
std::lock_guard<std::recursive_mutex> lock(mutex); // copy atomically.
return storage.GetShowStatusMonitor();
}
JackConfiguration PiPedalModel::GetJackConfiguration()
{
std::lock_guard<std::recursive_mutex> lock(mutex); // copy atomically.
return this->jackConfiguration;
}
void PiPedalModel::RestartAudio(bool useDummyAudioDriver)
{
try
{
if (useDummyAudioDriver)
{
CancelAudioRetry();
}
if (this->audioHost->IsOpen())
{
this->audioHost->Close();
}
// restarting is a bit dodgy. It was impossible with Jack, but
// now very plausible with the ALSA audio stack.
// Still bugs wrt/ restarting the circular buffers for the audio thread.
// do a complete reload.
this->audioHost->SetPedalboard(nullptr);
previousPedalboardLoaded = false;
auto jackServerSettings = this->jackServerSettings;
if (useDummyAudioDriver)
{
jackServerSettings.UseDummyAudioDevice();
}
auto jackConfiguration = this->jackConfiguration;
jackConfiguration.AlsaInitialize(jackServerSettings);
if (!jackConfiguration.isValid())
{
jackConfiguration.setErrorStatus("Error");
}
if (!useDummyAudioDriver)
{
this->jackConfiguration = jackConfiguration;
FireJackConfigurationChanged(jackConfiguration);
}
if (!jackServerSettings.IsValid() || !jackConfiguration.isValid())
{
throw std::runtime_error("Audio configuration not valid.");
}
auto channelSelection = this->storage.GetJackChannelSelection(jackConfiguration);
if (!channelSelection.isValid())
{
throw std::runtime_error("Audio configuration not valid.");
}
this->audioHost->Open(jackServerSettings, channelSelection);
this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection);
FireChannelSelectionChanged(-1);
LoadCurrentPedalboard();
this->UpdateRealtimeVuSubscriptions();
UpdateRealtimeMonitorPortSubscriptions();
}
catch (const std::exception &e)
{
this->audioHost->Close();
if (useDummyAudioDriver)
{
Lv2Log::error(SS("Failed to start dummy audio driver. " << e.what()));
}
else
{
Lv2Log::error(SS("Failed to start audio. " << e.what()));
}
if (!useDummyAudioDriver)
{
RestartAudio(true); // use the dummy audio driver.
}
}
}
void PiPedalModel::OnAlsaSequencerDeviceAdded(int client, const std::string &clientName)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
auto alsaSequencerConfiguration = this->storage.GetAlsaSequencerConfiguration();
std::string key = "seq:" + clientName;
bool interested = false;
for (const auto &port : alsaSequencerConfiguration.connections())
{
if (port.id().starts_with(key))
{
interested = true;
break;
}
}
if (interested)
{
Post(
[this]
{
// reconfigure connections.
std::lock_guard<std::recursive_mutex> lock(this->mutex);
if (this->audioHost) {
this->audioHost->SetAlsaSequencerConfiguration(this->storage.GetAlsaSequencerConfiguration());
}
});
}
}
void PiPedalModel::OnAlsaSequencerDeviceRemoved(int client)
{
// no action required.
}
void PiPedalModel::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
// reset midi connections even if the configuration hasn't changed.
this->audioHost->SetAlsaSequencerConfiguration(alsaSequencerConfiguration);
auto current = storage.GetAlsaSequencerConfiguration();
if (alsaSequencerConfiguration != current)
{
this->storage.SetAlsaSequencerConfiguration(alsaSequencerConfiguration);
// notify subscribers.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnAlsaSequencerConfigurationChanged(alsaSequencerConfiguration);
}
}
}
AlsaSequencerConfiguration PiPedalModel::GetAlsaSequencerConfiguration()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return this->storage.GetAlsaSequencerConfiguration();
}
std::vector<AlsaSequencerPortSelection> PiPedalModel::GetAlsaSequencerPorts()
{
auto ports = AlsaSequencer::EnumeratePorts();
std::vector<AlsaSequencerPortSelection> result;
for (auto &port : ports)
{
result.push_back(AlsaSequencerPortSelection{
port.id,
port.name,
port.displaySortOrder});
}
return result;
}
void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection)
{
{
std::lock_guard<std::recursive_mutex> lock(mutex); // copy atomically.
this->storage.SetJackChannelSelection(channelSelection);
this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection);
CancelAudioRetry();
}
RestartAudio(); // no lock to avoid mutex deadlock when reader thread is sending notifications..
this->FireChannelSelectionChanged(clientId);
}
void PiPedalModel::FireChannelSelectionChanged(int64_t clientId)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
{
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
JackChannelSelection channelSelection = storage.GetJackChannelSelection(this->jackConfiguration);
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnChannelSelectionChanged(clientId, channelSelection);
}
}
}
JackChannelSelection PiPedalModel::GetJackChannelSelection()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
JackChannelSelection t = this->storage.GetJackChannelSelection(this->jackConfiguration);
return t;
}
int64_t PiPedalModel::AddVuSubscription(int64_t instanceId)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
int64_t subscriptionId = ++nextSubscriptionId;
activeVuSubscriptions.push_back(VuSubscription{subscriptionId, instanceId});
UpdateRealtimeVuSubscriptions();
return subscriptionId;
}
void PiPedalModel::RemoveVuSubscription(int64_t subscriptionHandle)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto i = activeVuSubscriptions.begin(); i != activeVuSubscriptions.end(); ++i)
{
if ((*i).subscriptionHandle == subscriptionHandle)
{
activeVuSubscriptions.erase(i);
break;
}
}
UpdateRealtimeVuSubscriptions();
}
void PiPedalModel::OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
PedalboardItem *item = this->pedalboard.GetItem(instanceId);
if (item)
{
Lv2PluginInfo::ptr pPluginInfo;
if (item->uri() == SPLIT_PEDALBOARD_ITEM_URI)
{
pPluginInfo = GetSplitterPluginInfo();
}
else
{
pPluginInfo = pluginHost.GetPluginInfo(item->uri());
}
if (pPluginInfo)
{
if (portIndex == -1)
{
this->pedalboard.SetItemEnabled(instanceId, value != 0);
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnItemEnabledChanged(-1, instanceId, value != 0);
}
this->SetPresetChanged(-1, true);
return;
}
else
{
for (int i = 0; i < pPluginInfo->ports().size(); ++i)
{
auto &port = pPluginInfo->ports()[i];
if (port->index() == portIndex)
{
std::string symbol = port->symbol();
this->pedalboard.SetControlValue(instanceId, symbol, value);
{
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnMidiValueChanged(instanceId, symbol, value);
}
this->SetPresetChanged(-1, true);
return;
}
}
}
}
}
}
}
void PiPedalModel::OnNotifyVusSubscription(const std::vector<VuUpdate> &updates)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (size_t i = 0; i < updates.size(); ++i)
{
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnVuMeterUpdate(updates);
}
}
}
void PiPedalModel::UpdateRealtimeVuSubscriptions()
{
std::set<int64_t> addedInstances;
for (int i = 0; i < activeVuSubscriptions.size(); ++i)
{
auto instanceId = activeVuSubscriptions[i].instanceid;
if (pedalboard.HasItem(instanceId) || instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID)
{
addedInstances.insert(activeVuSubscriptions[i].instanceid);
}
}
if (audioHost)
{
std::vector<int64_t> instanceids(addedInstances.begin(), addedInstances.end());
audioHost->SetVuSubscriptions(instanceids);
}
}
void PiPedalModel::UpdateRealtimeMonitorPortSubscriptions()
{
if (!audioHost)
{
return;
}
audioHost->SetMonitorPortSubscriptions(this->activeMonitorPortSubscriptions);
}
int64_t PiPedalModel::MonitorPort(int64_t instanceId, const std::string &key, float updateInterval, PortMonitorCallback onUpdate)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
int64_t subscriptionId = ++nextSubscriptionId;
activeMonitorPortSubscriptions.push_back(
MonitorPortSubscription{subscriptionId, instanceId, key, updateInterval, onUpdate});
UpdateRealtimeMonitorPortSubscriptions();
return subscriptionId;
}
void PiPedalModel::UnmonitorPort(int64_t subscriptionHandle)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto i = activeMonitorPortSubscriptions.begin(); i != activeMonitorPortSubscriptions.end(); ++i)
{
if ((*i).subscriptionHandle == subscriptionHandle)
{
activeMonitorPortSubscriptions.erase(i);
UpdateRealtimeMonitorPortSubscriptions();
break;
}
}
}
void PiPedalModel::OnNotifyMonitorPort(const MonitorPortUpdate &update)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto i = activeMonitorPortSubscriptions.begin(); i != activeMonitorPortSubscriptions.end(); ++i)
{
if ((*i).subscriptionHandle == update.subscriptionHandle)
{
// make the call ONLY if the subscription handle is still valid.
(*update.callbackPtr)(update.subscriptionHandle, update.value);
break;
}
}
}
void PiPedalModel::SendSetPatchProperty(
int64_t clientId,
int64_t instanceId,
const std::string propertyUri,
const json_variant &value,
std::function<void()> onSuccess,
std::function<void(const std::string &error)> onError)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (!audioHost)
{
onError("Audio not running.");
return;
}
// save the property to the preset (currently used to reconstruct snapshots only)
PedalboardItem *pedalboardItem = this->pedalboard.GetItem(instanceId);
if (pedalboardItem && value.is_string())
{
std::shared_ptr<Lv2PluginInfo> pluginInfo = GetPluginInfo(pedalboardItem->uri_);
auto pipedalUi = pluginInfo->piPedalUI();
auto fileProperty = pipedalUi->GetFileProperty(propertyUri);
if (fileProperty && value.is_string())
{
json_variant abstractPath = pluginHost.AbstractPath(value);
std::ostringstream ss;
json_writer writer(ss);
writer.write(abstractPath);
std::string atomString = ss.str();
pedalboardItem->pathProperties_[propertyUri] = atomString;
}
this->SetPresetChanged(clientId, true);
}
LV2_Atom *atomValue = atomConverter.ToAtom(value);
std::function<void(RealtimePatchPropertyRequest *)> onRequestComplete{
[this, onSuccess](RealtimePatchPropertyRequest *pParameter)
{
{
std::lock_guard<std::recursive_mutex> lock(mutex);
bool cancelled = true;
for (auto i = this->outstandingParameterRequests.begin();
i != this->outstandingParameterRequests.end(); ++i)
{
if ((*i) == pParameter)
{
cancelled = false;
this->outstandingParameterRequests.erase(i);
break;
}
}
if (!cancelled)
{
if (pParameter->errorMessage != nullptr)
{
if (pParameter->onError)
{
pParameter->onError(pParameter->errorMessage);
}
}
else
{
if (onSuccess)
{
onSuccess();
}
}
}
delete pParameter;
}
}};
LV2_URID urid = this->pluginHost.GetLv2Urid(propertyUri.c_str());
size_t sampleTimeout = 0.5 * audioHost->GetSampleRate();
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
onRequestComplete,
clientId, instanceId, urid, atomValue, nullptr, onError,
sampleTimeout);
outstandingParameterRequests.push_back(request);
if (this->audioHost)
{
this->audioHost->sendRealtimeParameterRequest(request);
}
}
void PiPedalModel::SendGetPatchProperty(
int64_t clientId,
int64_t instanceId,
const std::string uri,
std::function<void(const std::string &jsonResult)> onSuccess,
std::function<void(const std::string &error)> onError)
{
std::function<void(RealtimePatchPropertyRequest *)> onRequestComplete{
[this](RealtimePatchPropertyRequest *pParameter)
{
{
std::lock_guard<std::recursive_mutex> lock(mutex);
bool cancelled = true;
for (auto i = this->outstandingParameterRequests.begin();
i != this->outstandingParameterRequests.end(); ++i)
{
if ((*i) == pParameter)
{
cancelled = false;
this->outstandingParameterRequests.erase(i);
break;
}
}
if (!cancelled)
{
if (pParameter->errorMessage != nullptr)
{
if (pParameter->onError)
{
pParameter->onError(pParameter->errorMessage);
}
}
else if (pParameter->GetSize() == 0 && pParameter->requestType == RealtimePatchPropertyRequest::RequestType::PatchGet)
{
if (pParameter->onError)
{
pParameter->onError("No response.");
}
}
else
{
if (pParameter->onSuccess)
{
pParameter->onSuccess(pParameter->jsonResponse);
}
}
}
delete pParameter;
}
}};
LV2_URID urid = this->pluginHost.GetLv2Urid(uri.c_str());
std::lock_guard<std::recursive_mutex> lock(mutex);
if (!this->audioHost)
{
onError("Audio stopped.");
}
size_t sampleTimeout = 0.3 * audioHost->GetSampleRate();
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
onRequestComplete,
clientId, instanceId, urid, onSuccess, onError, sampleTimeout);
outstandingParameterRequests.push_back(request);
this->audioHost->sendRealtimeParameterRequest(request);
}
BankIndex PiPedalModel::GetBankIndex() const
{
std::lock_guard<std::recursive_mutex> guard(const_cast<std::recursive_mutex &>(mutex));
return storage.GetBanks();
}
void PiPedalModel::RenameBank(int64_t clientId, int64_t bankId, const std::string &newName)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
storage.RenameBank(bankId, newName);
FireBanksChanged(clientId);
}
int64_t PiPedalModel::SaveBankAs(int64_t clientId, int64_t bankId, const std::string &newName)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
int64_t newId = storage.SaveBankAs(bankId, newName);
FireBanksChanged(clientId);
return newId;
}
void PiPedalModel::OpenBank(int64_t clientId, int64_t bankId)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
storage.LoadBank(bankId);
FireBanksChanged(clientId);
FirePresetsChanged(clientId);
this->pedalboard = storage.GetCurrentPreset();
UpdateDefaults(&this->pedalboard);
this->hasPresetChanged = false;
this->FirePedalboardChanged(clientId);
}
JackServerSettings PiPedalModel::GetJackServerSettings()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return this->jackServerSettings;
}
void PiPedalModel::SetOnboarding(bool value)
{
std::unique_lock<std::recursive_mutex> guard(mutex);
this->jackServerSettings.SetIsOnboarding(value);
SetJackServerSettings(this->jackServerSettings);
}
void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSettings)
{
std::unique_lock<std::recursive_mutex> guard(mutex);
#if JACK_HOST
if (!adminClient.CanUseShutdownClient())
{
throw PiPedalException("Can't change server settings when running a debug server.");
}
#endif
this->jackServerSettings = jackServerSettings;
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnJackServerSettingsChanged(jackServerSettings);
}
#if ALSA_HOST
storage.SetJackServerSettings(jackServerSettings);
FireJackConfigurationChanged(this->jackConfiguration);
CancelAudioRetry();
guard.unlock();
RestartAudio();
#endif
#if JACK_HOST
if (adminClient.CanUseShutdownClient())
{
// save the current (edited) preset now in case the service shutdown isn't clean.
CurrentPreset currentPreset;
currentPreset.modified_ = this->hasPresetChanged;
currentPreset.preset_ = this->pedalboard;
storage.SaveCurrentPreset(currentPreset);
this->jackConfiguration.SetIsRestarting(true);
FireJackConfigurationChanged(this->jackConfiguration);
this->audioHost->UpdateServerConfiguration(
jackServerSettings,
[this](bool success, const std::string &errorMessage)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (!success)
{
std::stringstream s;
s << "UpdateServerconfiguration failed: " << errorMessage;
Lv2Log::error(s.str().c_str());
}
// Update jack server status.
if (!success)
{
this->jackConfiguration.SetIsRestarting(false);
this->jackConfiguration.SetErrorStatus(errorMessage);
FireJackConfigurationChanged(this->jackConfiguration);
}
else
{
// we now do a complete restart of the services,
// so just sit tight and wait for the restart.
#ifdef JUNK
this->jackConfiguration.SetErrorStatus("");
FireJackConfigurationChanged(this->jackConfiguration);
// restart the pedalboard on a new instance.
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard{this->pluginHost.CreateLv2Pedalboard(this->pedalboard)};
this->lv2Pedalboard = lv2Pedalboard;
audioHost->SetPedalboard(lv2Pedalboard);
UpdateRealtimeVuSubscriptions();
UpdateRealtimeMonitorPortSubscriptions();
#endif
}
});
}
#endif
}
std::shared_ptr<Lv2PluginInfo> PiPedalModel::GetPluginInfo(const std::string &uri)
{
return pluginHost.GetPluginInfo(uri);
}
void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered_map<int64_t, PedalboardItem *> &itemMap)
{
itemMap[pedalboardItem->instanceId()] = pedalboardItem;
std::shared_ptr<Lv2PluginInfo> pPlugin = pluginHost.GetPluginInfo(pedalboardItem->uri());
if (!pPlugin)
{
pedalboardItem->instanceId();
if (pedalboardItem->uri() == SPLIT_PEDALBOARD_ITEM_URI)
{
pPlugin = GetSplitterPluginInfo();
}
}
if (pPlugin)
{
if (pPlugin->hasMidiInput())
{
if (!pedalboardItem->midiChannelBinding())
{
pedalboardItem->midiChannelBinding(MidiChannelBinding::DefaultForMissingValue());
}
}
else
{
if (pedalboardItem->midiChannelBinding())
{
pedalboardItem->midiChannelBinding(std::optional<MidiChannelBinding>()); // clear it.
}
}
for (size_t i = 0; i < pPlugin->ports().size(); ++i)
{
auto port = pPlugin->ports()[i];
if (port->is_control_port() && port->is_input())
{
ControlValue *pValue = pedalboardItem->GetControlValue(port->symbol());
if (pValue == nullptr)
{
// Missing? Set it to default value.
pedalboardItem->controlValues().push_back(
pipedal::ControlValue(port->symbol().c_str(), port->default_value()));
}
}
}
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]), itemMap);
}
for (size_t i = 0; i < pedalboardItem->bottomChain().size(); ++i)
{
UpdateDefaults(&(pedalboardItem->bottomChain()[i]), itemMap);
}
}
void PiPedalModel::UpdateDefaults(Snapshot *snapshot, std::unordered_map<int64_t, PedalboardItem *> &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<int64_t, PedalboardItem *> itemMap;
for (size_t i = 0; i < pedalboard->items().size(); ++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);
}
}
}
PluginPresets PiPedalModel::GetPluginPresets(const std::string &pluginUri)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.GetPluginPresets(pluginUri);
}
PluginUiPresets PiPedalModel::GetPluginUiPresets(const std::string &pluginUri)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.GetPluginUiPresets(pluginUri);
}
void PiPedalModel::LoadPluginPreset(int64_t pluginInstanceId, uint64_t presetInstanceId)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
PedalboardItem *pedalboardItem = this->pedalboard.GetItem(pluginInstanceId);
if (pedalboardItem != nullptr)
{
int32_t oldStateUpdateCount = pedalboardItem->stateUpdateCount();
PluginPresetValues presetValues = storage.GetPluginPresetValues(pedalboardItem->uri(), presetInstanceId);
// if the plugin has state, we have to rebuild the pedalboard, since setting state is not thread-safe.
// Same goes if lilvPresetUri is not empty.
// lilvPresetUri: use lilv to load the preset from the RDF model. Occurs when using a factory preset
// that has state:state, because lilv doesn't allow us to read this data, but does load it.
// This is a transient condition.
for (auto &control : presetValues.controls)
{
this->pedalboard.SetControlValue(pluginInstanceId, control.key(), control.value());
}
if ((!presetValues.state.isValid_) && presetValues.lilvPresetUri.empty())
{
audioHost->SetPluginPreset(pluginInstanceId, presetValues.controls);
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnLoadPluginPreset(pluginInstanceId, presetValues.controls);
}
}
else
{
pedalboardItem->lv2State(presetValues.state);
pedalboardItem->lilvPresetUri(presetValues.lilvPresetUri);
pedalboardItem->stateUpdateCount(oldStateUpdateCount + 1);
FirePedalboardChanged(-1); // does a complete reload of both client and audio server.
}
this->SetPresetChanged(-1, true);
}
}
void PiPedalModel::DeleteAtomOutputListeners(int64_t clientId)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (size_t i = 0; i < atomOutputListeners.size(); ++i)
{
if (atomOutputListeners[i].clientId == clientId)
{
atomOutputListeners.erase(atomOutputListeners.begin() + i);
--i;
}
}
if (audioHost)
{
audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0);
}
}
void PiPedalModel::DeleteMidiListeners(int64_t clientId)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (size_t i = 0; i < midiEventListeners.size(); ++i)
{
if (midiEventListeners[i].clientId == clientId)
{
midiEventListeners.erase(midiEventListeners.begin() + i);
--i;
}
}
if (audioHost)
{
audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0);
}
}
void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
std::string propertyUri = pluginHost.GetMapFeature().UridToString(patchSetProperty);
{
PedalboardItem *item = pedalboard.GetItem((int64_t)instanceId);
if (item == nullptr)
return;
atom_object atomObject{atomValue};
PedalboardItem::PropertyMap &properties = item->PatchProperties();
if (properties.contains(propertyUri) && properties[propertyUri] == atomObject)
{
// do noting.
}
else
{
properties[propertyUri] = std::move(atomObject);
}
OnNotifyMaybeLv2StateChanged(instanceId);
}
bool hasAtomJson = false;
std::string atomJson;
for (int i = 0; i < atomOutputListeners.size(); ++i)
{
auto &listener = atomOutputListeners[i];
if (listener.WantsProperty(instanceId, patchSetProperty))
{
auto subscriber = this->GetNotificationSubscriber(listener.clientId);
if (subscriber)
{
if (!hasAtomJson)
{
atomJson = this->audioHost->AtomToJson(atomValue);
hasAtomJson = true;
}
subscriber->OnNotifyPatchProperty(listener.clientHandle, instanceId, propertyUri, atomJson);
}
else
{
atomOutputListeners.erase(atomOutputListeners.begin() + i);
--i;
}
}
}
if (audioHost)
{
audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0);
}
}
void PiPedalModel::OnNotifyPathPatchPropertyReceived(
int64_t instanceId,
LV2_URID pathPatchProperty,
LV2_Atom *pathProperty)
{
std::lock_guard<std::recursive_mutex> 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)
{
return;
}
auto i = pedalboardItem->pathProperties_.find(pathPatchPropertyUri);
if (i != pedalboardItem->pathProperties_.end())
{
pedalboardItem->pathProperties_[pathPatchPropertyUri] = atomString;
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnNotifyPathPatchPropertyChanged(
instanceId,
pathPatchPropertyUri,
atomString);
}
}
}
void PiPedalModel::OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
bool isNote = (cc0 & 0xF0) == 0x90; // Note On
if (isNote && cc2 == 0)
{
return; // Note off. Oopsie.
}
bool isControl = (cc0 & 0xF0) == 0xB0; // Control Change
if (!isNote && !isControl)
{
return; // Not a note on or control change.
}
for (int i = 0; i < midiEventListeners.size(); ++i)
{
auto &listener = midiEventListeners[i];
auto subscriber = this->GetNotificationSubscriber(listener.clientId);
if (subscriber)
{
subscriber->OnNotifyMidiListener(listener.clientHandle, cc0, cc1, cc2);
}
else
{
midiEventListeners.erase(midiEventListeners.begin() + i);
--i;
}
}
audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0);
}
void PiPedalModel::ListenForMidiEvent(int64_t clientId, int64_t clientHandle)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
MidiListener listener{clientId, clientHandle};
midiEventListeners.push_back(listener);
audioHost->SetListenForMidiEvent(true);
}
void PiPedalModel::CancelListenForMidiEvent(int64_t clientId, int64_t clientHandle)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (size_t i = 0; i < midiEventListeners.size(); ++i)
{
const auto &listener = midiEventListeners[i];
if (listener.clientId == clientId && listener.clientHandle == clientHandle)
{
midiEventListeners.erase(midiEventListeners.begin() + i);
break;
}
}
if (midiEventListeners.size() == 0)
{
audioHost->SetListenForMidiEvent(false);
}
}
void PiPedalModel::MonitorPatchProperty(int64_t clientId, int64_t clientHandle, uint64_t instanceId, const std::string &propertyUri)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
LV2_URID propertyUrid = 0;
if (propertyUri.length() != 0)
{
propertyUrid = pluginHost.GetMapFeature().GetUrid(propertyUri.c_str());
}
AtomOutputListener listener{clientId, clientHandle, instanceId, propertyUrid};
atomOutputListeners.push_back(listener);
audioHost->SetListenForAtomOutput(true);
PedalboardItem *item = this->pedalboard.GetItem(instanceId);
if (item)
{
auto &map = item->PatchProperties();
if (map.contains(propertyUri))
{
const auto &value = map[propertyUri];
std::string json = this->audioHost->AtomToJson(value.get());
for (auto &subscriber : this->subscribers)
{
if (subscriber->GetClientId() == clientId)
{
subscriber->OnNotifyPatchProperty(clientHandle, instanceId, propertyUri, json);
}
}
}
}
}
void PiPedalModel::CancelMonitorPatchProperty(int64_t clientId, int64_t clientHandle)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (size_t i = 0; i < atomOutputListeners.size(); ++i)
{
const auto &listener = atomOutputListeners[i];
if (listener.clientId == clientId && listener.clientHandle == clientHandle)
{
atomOutputListeners.erase(atomOutputListeners.begin() + i);
break;
}
}
if (midiEventListeners.size() == 0)
{
audioHost->SetListenForMidiEvent(false);
}
}
std::vector<AlsaDeviceInfo> PiPedalModel::GetAlsaDevices()
{
std::vector<AlsaDeviceInfo> result = this->alsaDevices.GetAlsaDevices();
#ifdef JUNK
// Useful for debugging non-stereo device configurations
result.push_back(MakeDummyDeviceInfo(1));
result.push_back(MakeDummyDeviceInfo(2));
result.push_back(MakeDummyDeviceInfo(8));
#endif
return result;
}
const std::filesystem::path &PiPedalModel::GetWebRoot() const
{
return webRoot;
}
std::map<std::string, bool> PiPedalModel::GetFavorites() const
{
std::lock_guard<std::recursive_mutex> guard(const_cast<std::recursive_mutex &>(mutex));
return storage.GetFavorites();
}
void PiPedalModel::SetFavorites(const std::map<std::string, bool> &favorites)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
storage.SetFavorites(favorites);
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnFavoritesChanged(favorites);
}
}
std::vector<MidiBinding> PiPedalModel::GetSystemMidiBidings()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return this->systemMidiBindings;
}
void PiPedalModel::SetSystemMidiBindings(std::vector<MidiBinding> &bindings)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
this->systemMidiBindings = bindings;
storage.SetSystemMidiBindings(bindings);
if (this->audioHost)
{
this->audioHost->SetSystemMidiBindings(bindings);
}
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnSystemMidiBindingsChanged(bindings);
}
}
PedalboardItem *PiPedalModel::GetPedalboardItemForFileProperty(const UiFileProperty &fileProperty)
{
for (PedalboardItem *pedalboardItem : this->pedalboard.GetAllPlugins())
{
if (pedalboardItem->pathProperties_.contains(fileProperty.patchProperty()))
{
return pedalboardItem;
}
}
return nullptr;
}
FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath_, const UiFileProperty &fileProperty)
{
std::string relativePath = relativePath_;
try
{
if (!storage.IsInUploadsDirectory(relativePath))
{
// if relativePath is in a resource directory of the plugin, then we have loaded a factory preset or are using a default property.
// map the resource path to the corresponding file in the uploads directory.
// :-(
PedalboardItem *pedalboardItem = GetPedalboardItemForFileProperty(fileProperty);
if (pedalboardItem)
{
auto pluginInfo = GetPluginInfo(pedalboardItem->uri());
if (pluginInfo)
{
std::filesystem::path resourcePath = fs::path(pluginInfo->bundle_path()) / fileProperty.resourceDirectory();
if (IsSubdirectory(relativePath, resourcePath))
{
fs::path t = MakeRelativePath(relativePath, resourcePath);
t = fileProperty.directory() / t;
if (fs::exists(t))
{
relativePath = t;
}
}
}
}
}
return this->storage.GetFileList2(relativePath, fileProperty);
}
catch (const std::exception &e)
{
Lv2Log::warning("GetFileList() failed: (%s)", e.what());
throw;
}
}
std::string PiPedalModel::RenameFilePropertyFile(
const std::string &oldRelativePath,
const std::string &newRelativePath,
const UiFileProperty &uiFileProperty)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.RenameFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty);
}
std::string PiPedalModel::CopyFilePropertyFile(
const std::string &oldRelativePath,
const std::string &newRelativePath,
const UiFileProperty &uiFileProperty,
bool overwrite)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.CopyFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty, overwrite);
}
void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
storage.DeleteSampleFile(fileName);
}
std::string PiPedalModel::CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.CreateNewSampleDirectory(relativePath, uiFileProperty);
}
FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty, const std::string &selectedPath)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.GetFilePropertydirectoryTree(uiFileProperty, selectedPath);
}
UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId, const std::string &patchPropertyUri)
{
auto pedalboardItems = pedalboard.GetAllPlugins();
for (const auto &pedalboardItem : pedalboardItems)
{
if (pedalboardItem->instanceId() == instanceId)
{
Lv2PluginInfo::ptr pluginInfo = GetPluginInfo(pedalboardItem->uri());
if (pluginInfo && pluginInfo->piPedalUI())
{
for (const auto &fileProperty : pluginInfo->piPedalUI()->fileProperties())
if (fileProperty->patchProperty() == patchPropertyUri)
{
return fileProperty;
}
}
}
}
return nullptr;
throw std::runtime_error("Permission denied. Plugin not currently loaded.");
}
std::string PiPedalModel::UploadUserFile(const std::string &directory, int64_t instanceId, const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength)
{
UiFileProperty::ptr fileProperty = FindLoadedPatchProperty(instanceId, patchProperty);
if (!fileProperty)
{
Lv2Log::error(SS("Upload fle: Permission denied. No currently-loaded plugin provides that patch property: " << patchProperty));
throw std::runtime_error("Permission denied.");
}
return storage.UploadUserFile(directory, fileProperty, filename, stream, contentLength);
}
uint64_t PiPedalModel::CreateNewPreset()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.CreateNewPreset();
}
void PiPedalModel::CheckForResourceInitialization(Pedalboard &pedalboard)
{
for (auto item : pedalboard.GetAllPlugins())
{
if (!item->isSplit())
{
pluginHost.CheckForResourceInitialization(item->uri(), storage.GetPluginUploadDirectory());
}
}
}
Pedalboard &PiPedalModel::GetPedalboard()
{
return this->pedalboard;
}
std::shared_ptr<Lv2Pedalboard> PiPedalModel::GetLv2Pedalboard()
{
// test only.
Lv2PedalboardErrorList errorMessages;
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard{this->pluginHost.CreateLv2Pedalboard(this->pedalboard, errorMessages)};
if (errorMessages.size() != 0)
{
throw std::runtime_error(errorMessages[0].message);
}
return lv2Pedalboard;
}
void PiPedalModel::SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId)
{
// Thinking on this:
// 1) do NOT mark the pedalboard as changed. This shouldn't set a change flag.
// 2) do NOT broadcast the change. Whoever set it last controls what happens when the plugin is reloaded. Meh.
// 3) Clients must be able to save a non-changed pedalboard.
pedalboard.selectedPlugin(pedalboardId);
}
bool PiPedalModel::LoadCurrentPedalboard()
{
CrashGuardLock crashGuardLock;
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> lv2Pedalboard{this->pluginHost.CreateLv2Pedalboard(this->pedalboard, errorMessages)};
this->lv2Pedalboard = lv2Pedalboard;
// apply the error messages to the lv2Pedalboard.
// return true if the error messages have changed
CheckForResourceInitialization(this->pedalboard);
audioHost->SetPedalboard(lv2Pedalboard);
previousPedalboard = this->pedalboard;
previousPedalboardLoaded = true;
return true;
}
void PiPedalModel::OnNotifyLv2RealtimeError(int64_t instanceId, const std::string &error)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
// Notify clients.
size_t n = subscribers.size();
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnErrorMessage(error);
}
}
std::filesystem::path PiPedalModel::GetPluginUploadDirectory() const
{
return storage.GetPluginUploadDirectory();
}
void PiPedalModel::OnLv2PluginsChanged()
{
Lv2Log::info("Lv2 plugins have changed. Reloading plugins.");
std::lock_guard<std::recursive_mutex> lock(mutex);
{
// Notify clients.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnLv2PluginsChanging();
}
}
std::thread(
[this]()
{
// wait for the message to propagate. It would be better to use some kind of flush()
// operation, but it's not clear how to do that with asyncio.
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
restartListener();
})
.detach();
}
void PiPedalModel::SetRestartListener(std::function<void(void)> &&listener)
{
this->restartListener = std::move(listener);
}
void PiPedalModel::OnUpdateStatusChanged(const UpdateStatus &updateStatus)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (this->currentUpdateStatus != updateStatus)
{
this->currentUpdateStatus = updateStatus;
FireUpdateStatusChanged(this->currentUpdateStatus);
}
}
void PiPedalModel::FireUpdateStatusChanged(const UpdateStatus &updateStatus)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnUpdateStatusChanged(updateStatus);
}
}
UpdateStatus PiPedalModel::GetUpdateStatus()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return updater->GetCurrentStatus();
}
void PiPedalModel::UpdateNow(const std::string &updateUrl)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
std::filesystem::path fileName, signatureName;
updater->DownloadUpdate(updateUrl, &fileName, &signatureName);
adminClient.InstallUpdate(fileName);
}
void PiPedalModel::ForceUpdateCheck()
{
updater->ForceUpdateCheck();
}
void PiPedalModel::SetUpdatePolicy(UpdatePolicyT updatePolicy)
{
updater->SetUpdatePolicy(updatePolicy);
}
static bool HasAlsaDevice(const std::vector<AlsaDeviceInfo> devices, const std::string &deviceId)
{
for (auto &device : devices)
{
if (device.id_ == deviceId)
return true;
}
return false;
}
void PiPedalModel::StartHotspotMonitoring()
{
this->avahiService = std::make_unique<AvahiService>();
SetThreadName("avahi"); // hack to name the avahi service thread.
UpdateDnsSd(); // now that the server is running, publish a DNS-SD announcement.
SetThreadName("main");
this->hotspotManager->Open();
}
void PiPedalModel::WaitForAudioDeviceToComeOnline()
{
auto serverSettings = this->GetJackServerSettings();
// Wait for selected audio device to be initialized.
// It may take some time for ALSA to publish all available devices when rebooting.
if (serverSettings.IsValid())
{
// wait up to 15 seconds for the midi device to come online.
auto devices = GetAlsaDevices();
bool found = false;
if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice()))
{
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
}
else
{
Lv2Log::info(SS("Waiting for ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
for (int i = 0; i < 5; ++i)
{
sleep(2);
devices = GetAlsaDevices();
if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice()))
{
found = true;
break;
}
}
if (found)
{
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
}
else
{
Lv2Log::info(SS("ALSA device " << serverSettings.GetAlsaInputDevice() << " not found."));
}
}
}
else
{
Lv2Log::info("No ALSA device selected.");
}
// pre-cache device info before we let audio services run.
GetAlsaDevices();
}
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.
if (!hotspotManager)
{
throw std::runtime_error("Too early. It's not ready yet.");
}
return hotspotManager->Post(std::move(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));
}
bool PiPedalModel::CancelPost(PostHandle handle)
{
if (!hotspotManager)
{
throw std::runtime_error("Too early. It's not ready yet.");
}
return hotspotManager->CancelPost(handle);
}
void PiPedalModel::CancelNetworkChangingTimer()
{
if (networkChangingDelayHandle)
{
CancelPost(networkChangingDelayHandle);
networkChangingDelayHandle = 0;
}
}
std::vector<std::string> PiPedalModel::GetKnownWifiNetworks()
{
if (!this->hotspotManager)
{
return std::vector<std::string>();
}
return this->hotspotManager->GetKnownWifiNetworks();
}
void PiPedalModel::OnNetworkChanging(bool ethernetConnected, bool hotspotConnected)
{
CancelNetworkChangingTimer();
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);
});
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnNetworkChanging(hotspotConnected);
}
}
void PiPedalModel::OnNetworkChanged(bool ethernetConnected, bool hotspotConnected)
{
FireNetworkChanged();
}
void PiPedalModel::OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType)
{
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;
}
}
catch (const std::exception &e)
{
Lv2Log::error(SS("Failed to process realtime MIDI event. " << e.what()));
}
}
void PiPedalModel::RequestShutdown(bool restart)
{
if (GetAdminClient().CanUseAdminClient())
{
GetAdminClient().RequestShutdown(restart);
}
else
{
// ONLY works when interactively logged in.
std::stringstream s;
s << "/usr/sbin/shutdown ";
if (restart)
{
s << "-r";
}
else
{
s << "-P";
}
s << " now";
if (sysExec(s.str().c_str()) != EXIT_SUCCESS)
{
Lv2Log::error("shutdown failed.");
if (restart)
{
throw new PiPedalStateException("Restart request failed.");
}
else
{
throw new PiPedalStateException("Shutdown request failed.");
}
}
}
}
void PiPedalModel::SetHasWifi(bool hasWifi)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (this->hasWifi != hasWifi)
{
this->hasWifi = hasWifi;
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnHasWifiChanged(hasWifi);
}
}
}
bool PiPedalModel::GetHasWifi()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return hasWifi;
}
std::map<std::string, std::string> PiPedalModel::GetWifiRegulatoryDomains()
{
std::map<std::string, std::string> result;
try
{
auto &regDb = RegDb::GetInstance();
result = regDb.getRegulatoryDomains(storage.GetConfigRoot() / "iso_codes.json");
}
catch (const std::exception &e)
{
Lv2Log::warning(SS("Unable to query Wifi Regulatory domains. " << e.what()));
}
return result;
}
void PiPedalModel::CancelAudioRetry()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (audioRetryPostHandle)
{
// don't think this can ever happen, but if it did, it would be bad.
this->CancelPost(audioRetryPostHandle);
audioRetryPostHandle = 0;
}
}
void PiPedalModel::OnAlsaDriverTerminatedAbnormally()
{
// notification from the realtime thread, via the audiohost that the
// ALSA stream has broken. We want to restart.
// get off the service thread as promptly as possible
this->Post([&]()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (closed) return;
auto now = clock::now();
clock::duration timeSinceLastRetry = now-this->lastRestartTime;
this->lastRestartTime = now;
if (timeSinceLastRetry > std::chrono::duration_cast<clock::duration>(std::chrono::milliseconds(1000))) {
audioRestartRetries = 0;
}
CancelAudioRetry();
if (audioRestartRetries == 0)
{
this->audioRetryPostHandle = this->Post(
// No lock to avoid deadlocks!
[this]() {
Lv2Log::info("Restarting audio.");
this->RestartAudio();
});
++audioRestartRetries;
} else if (audioRestartRetries < 3)
{
this->audioRetryPostHandle = this->PostDelayed(
std::chrono::milliseconds(100 * audioRestartRetries),
[this]() {
if (closed) {
return;
}
Lv2Log::info(SS("Restarting audio. (retry " << audioRestartRetries << ")"));
RestartAudio();
});
++audioRestartRetries;
} else if (audioRestartRetries == 3) // one attempt to start the dummy driver.
{
{
this->audioRetryPostHandle = this->Post(
// No lock to avoid deadlocks!
[this]() {
Lv2Log::info(SS("Switching to dummy driver."));
RestartAudio(true); // switch to the dummy driver.
});
}
++audioRestartRetries;
} else {
Lv2Log::error(SS("Unable to reastart audio."));
} });
}
bool PiPedalModel::IsInUploadsDirectory(const std::string &path)
{
return storage.IsInUploadsDirectory(path);
}
void PiPedalModel::MoveAudioFile(
const std::string &directory,
int32_t fromPosition,
int32_t toPosition)
{
if (directory.empty())
{
throw std::runtime_error("Directory is empty.");
}
AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory);
dir->MoveAudioFile(directory, fromPosition, toPosition);
}
void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string &title)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (!this->pedalboard.SetItemTitle(instanceId, title))
{
return;
}
// no need to reload the pedalboard, but we do need to notify subscribers.
this->SetPresetChanged(-1, true);
this->FirePedalboardChanged(-1, false);
}
void PiPedalModel::SetTone3000Auth(const std::string &apiKey)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
storage.SetTone3000Auth(apiKey);
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
bool hasAuth = apiKey != "";
for (auto &subscriber : t)
{
subscriber->OnTone3000AuthChanged(hasAuth);
}
}
bool PiPedalModel::HasTone3000Auth() const
{
return storage.GetTone3000Auth() != "";
}