/* * MIT License * * Copyright (c) 2022 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 * 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 "ss.hpp" #include "PluginHost.hpp" #include "vst3/Vst3Host.hpp" #include "Lv2Log.hpp" #include #include #include #include #include "public.sdk/source/vst/hosting/module.h" #include "public.sdk/source/vst/hosting/hostclasses.h" #include "public.sdk/source/vst/hosting/plugprovider.h" #include "pluginterfaces/vst/ivsteditcontroller.h" #include "pluginterfaces/vst/ivstaudioprocessor.h" #include "pluginterfaces/vst/ivstunits.h" #include "public.sdk/source/vst/utility/stringconvert.h" #include "public.sdk/source/vst/hosting/eventlist.h" #include "public.sdk/source/vst/hosting/parameterchanges.h" #include "public.sdk/source/vst/hosting/processdata.h" #include "pluginterfaces/vst/ivstaudioprocessor.h" #include "pluginterfaces/vst/ivstmidicontrollers.h" #include "public.sdk/samples/vst-hosting/audiohost/source/media/iparameterclient.h" #include "public.sdk/samples/vst-hosting/audiohost/source/media/imediaserver.h" #include #include #include "LiteralVersion.hpp" #include "json.hpp" using namespace std; using namespace pipedal; using namespace VST3::Hosting; using namespace Steinberg; using namespace Steinberg::Vst; namespace pipedal { class Vst3HostApplication : public HostApplication { public: Vst3HostApplication() {} //--- IHostApplication --------------- tresult PLUGIN_API getName(String128 name) { return VST3::StringConvert::convert("PiPedal", name) ? kResultTrue : kInternalError; } }; class Vst3HostImpl : public Vst3Host { private: OPtr hostApplication; std::string cacheFilePath; public: Vst3HostImpl(const std::string &cacheFilePath); const Vst3Host::PluginList &RescanPlugins() override; const Vst3Host::PluginList &RefreshPlugins() override; const Vst3Host::PluginList &getPluginList() override { return pluginList; } std::unique_ptr CreatePlugin(long instanceId, const std::string &url, IHost *pHost) override; std::unique_ptr CreatePlugin(PedalboardItem &pedalboardItem, IHost *pHost) override; private: Lv2PluginUiInfo *GetPluginInfo(const std::string &uri); void LoadPluginCache(); void SavePluginCache(); bool havePluginsChanged() const; PluginList pluginList; void EnsureContext(); std::unique_ptr MakeDetailedPluginInfo(const PluginFactory &factory, const ClassInfo &classInfo, const std::string &path); void MakeVst3Info(const std::filesystem::path &path, Vst3Host::PluginList &list); }; Vst3Host::Ptr Vst3Host::CreateInstance(const std::string &cacheFilePath) { return std::make_unique(cacheFilePath); } //-------------------------------------------------------------- } // namespace pipedal Vst3HostImpl::Vst3HostImpl(const std::string &cacheFilePath) : cacheFilePath(cacheFilePath) { this->hostApplication = new Vst3HostApplication(); EnsureContext(); } static bool hasSubCategory(const ClassInfo &classInfo, const std::string &subcategory) { for (auto &category : classInfo.subCategories()) { if (category == subcategory) { return true; } } return false; } static PluginType getVst3PluginType(const ClassInfo &classInfo) { if (hasSubCategory(classInfo, "Instrument")) { return PluginType::InstrumentPlugin; } if (!hasSubCategory(classInfo, "Fx")) { return PluginType::InvalidPlugin; } // kFxAnalyzer = "Fx|Analyzer"; ///< Scope, FFT-Display, Loudness Processing... if (hasSubCategory(classInfo, "Analyzer")) { return PluginType::AnalyserPlugin; } // kFxDelay = "Fx|Delay"; ///< Delay, Multi-tap Delay, Ping-Pong Delay... if (hasSubCategory(classInfo, "Delay")) { return PluginType::DelayPlugin; } // kFxDistortion = "Fx|Distortion"; ///< Amp Simulator, Sub-Harmonic, SoftClipper... if (hasSubCategory(classInfo, "Distortion")) { return PluginType::DistortionPlugin; } // kFxDynamics = "Fx|Dynamics"; ///< Compressor, Expander, Gate, Limiter, Maximizer, Tape Simulator, EnvelopeShaper... if (hasSubCategory(classInfo, "Dynamics")) { return PluginType::DynamicsPlugin; } // kFxEQ = "Fx|EQ"; ///< Equalization, Graphical EQ... if (hasSubCategory(classInfo, "EQ")) { return PluginType::EQPlugin; } // kFxFilter = "Fx|Filter"; ///< WahWah, ToneBooster, Specific Filter,... if (hasSubCategory(classInfo, "Filter")) { return PluginType::FilterPlugin; } // kFx = "Fx"; ///< others type (not categorized) // kFxSpatial = "Fx|Spatial"; ///< MonoToStereo, StereoEnhancer,... if (hasSubCategory(classInfo, "Spatial")) { return PluginType::SpatialPlugin; } // kFxGenerator = "Fx|Generator"; ///< Tone Generator, Noise Generator... if (hasSubCategory(classInfo, "Generator")) { return PluginType::GeneratorPlugin; } // kFxReverb = "Fx|Reverb"; ///< Reverberation, Room Simulation, Convolution Reverb... if (hasSubCategory(classInfo, "Reverb")) { return PluginType::ReverbPlugin; } // kFxPitchShift = "Fx|Pitch Shift"; ///< Pitch Processing, Pitch Correction, Vocal Tuning... if (hasSubCategory(classInfo, "Pitch Shift")) { return PluginType::PitchPlugin; } // kFxModulation = "Fx|Modulation"; ///< Phaser, Flanger, Chorus, Tremolo, Vibrato, AutoPan, Rotary, Cloner... if (hasSubCategory(classInfo, "Modulation")) { return PluginType::ModulatorPlugin; } // kFxRestoration = "Fx|Restoration"; ///< Denoiser, Declicker,... if (hasSubCategory(classInfo, "Restoration")) { return PluginType::UtilityPlugin; } // kFxSurround = "Fx|Surround"; ///< dedicated to surround processing: LFE Splitter, Bass Manager... if (hasSubCategory(classInfo, "Surround")) { return PluginType::UtilityPlugin; } // kFxTools = "Fx|Tools"; ///< Volume, Mixer, Tuner... if (hasSubCategory(classInfo, "Tools")) { return PluginType::UtilityPlugin; } // kFxMastering = "Fx|Mastering"; ///< Dither, Noise Shaping,... if (hasSubCategory(classInfo, "Mastering")) { return PluginType::UtilityPlugin; } // kFxNetwork = "Fx|Network"; ///< using Network if (hasSubCategory(classInfo, "Network")) { return PluginType::UtilityPlugin; } // kFxInstrument = "Fx|Instrument"; ///< Fx which could be loaded as Instrument too // kFxInstrumentExternal = "Fx|Instrument|External"; ///< Fx which could be loaded as Instrument too and is external (wrapped Hardware) if (hasSubCategory(classInfo, "Instrument")) { return PluginType::InstrumentPlugin; } return PluginType::Plugin; } void Vst3HostImpl::EnsureContext() { PluginContextFactory::instance().setPluginContext(this->hostApplication); } void Vst3Host::Private::UpdateControlInfo(IEditController *controller, Lv2PluginUiInfo &pluginInfo) { // update UI text and control info. FUnknownPtr iUnitInfo(controller); pluginInfo.controls().resize(0); pluginInfo.port_groups().resize(0); if (iUnitInfo) { auto nUnits = iUnitInfo->getUnitCount(); for (int i = 0; i < nUnits; ++i) { Steinberg::Vst::UnitInfo unitInfo; iUnitInfo->getUnitInfo(i, unitInfo); Lv2PluginUiPortGroup portGroup( SS(unitInfo.id), VST3::StringConvert::convert(unitInfo.name), SS(unitInfo.parentUnitId), unitInfo.programListId); pluginInfo.port_groups().push_back(portGroup); } } auto nParams = controller->getParameterCount(); for (int32 param = 0; param < nParams; ++param) { Steinberg::Vst::ParameterInfo info; tresult tErr = controller->getParameterInfo(param, info); Lv2PluginUiPort port; port.index(param); // used for LV2 purposes. port.symbol(SS(info.id)); // Used for VST3 purposes. convert to int to specifiy a vst control id. std::string shortTitle = VST3::StringConvert::convert(info.shortTitle); std::string title = VST3::StringConvert::convert(info.title); if (shortTitle.length() == 0) { shortTitle = title; } port.name(shortTitle); port.comment(title); // guard against NaN value (mda Talkbox) port.display_priority(param); bool isList = (info.flags & ParameterInfo::kIsList) != ParameterInfo::kNoFlags; bool canAutomate = (info.flags & ParameterInfo::kCanAutomate) != ParameterInfo::kNoFlags; bool isProgramChange = (info.flags & ParameterInfo::kIsProgramChange) != ParameterInfo::kNoFlags; bool isReadOnly = (info.flags & ParameterInfo::kIsReadOnly) != ParameterInfo::kNoFlags; bool notOnGui = (info.flags & ParameterInfo::kIsHidden) != ParameterInfo::kNoFlags; bool isBypass = (info.flags & ParameterInfo::kIsBypass) != ParameterInfo::kNoFlags; port.is_bypass(isBypass); port.not_on_gui(isReadOnly | notOnGui); port.min_value(controller->normalizedParamToPlain(info.id, 0)); port.max_value(controller->normalizedParamToPlain(info.id, 1)); double t = controller->normalizedParamToPlain(info.id, info.defaultNormalizedValue); if (std::isnan(t) || std::isinf(t)) { t = 0; } port.default_value(t); port.range_steps(info.stepCount == 0 ? 0 : info.stepCount + 1); if (isList && !isProgramChange) { port.enumeration_property(true); for (int i = 0; i <= info.stepCount; ++i) { double normalizedValue; if (info.stepCount == 0) { normalizedValue = 0; } else { normalizedValue = i * 1.0 / (info.stepCount); } String128 strValue; if (controller->getParamStringByValue(info.id, normalizedValue, strValue) == kResultOk) { Lv2ScalePoint scalePoint((float)controller->normalizedParamToPlain(info.id, normalizedValue), VST3::StringConvert::convert(strValue)); port.scale_points().push_back(scalePoint); } } } if (info.unitId != 0) { port.port_group(SS(info.unitId)); } std::string units = VST3::StringConvert::convert(info.units); if (units.size() != 0) { if (units == "dB") { port.units(Units::db); } else if (units == "%") { port.units(Units::pc); } else if (units == "Hz") { port.units(Units::hz); } else if (units == "kHz") { port.units(Units::khz); } else if (units == "sec" || units == "s") { port.units(Units::s); } else if (units == "ms") { port.units(Units::ms); } else if (units == "cent") { port.units(Units::cent); } else { port.units(Units::custom); port.custom_units(units); } } pluginInfo.controls().push_back(port); } } static void UpdateBusInfo(IComponent *component, Lv2PluginUiInfo &pluginInfo) { int audioOutputs = 0; auto count = component->getBusCount(MediaTypes::kAudio, BusDirections::kOutput); for (int32_t i = 0; i < count; i++) { BusInfo busInfo; if (component->getBusInfo(MediaTypes::kAudio, BusDirections::kOutput, i, busInfo) == kResultOk) { auto channelName = VST3::StringConvert::convert(busInfo.name, 128); if (busInfo.busType == BusTypes::kMain) // aux channels not currently supported. { audioOutputs += busInfo.channelCount; } } } pluginInfo.audio_outputs(audioOutputs); count = component->getBusCount(MediaTypes::kAudio, BusDirections::kInput); int audioInputs = 0; for (int32_t i = 0; i < count; i++) { BusInfo busInfo; if (component->getBusInfo(MediaTypes::kAudio, BusDirections::kInput, i, busInfo) == kResultOk) { auto channelName = VST3::StringConvert::convert(busInfo.name, 128); if (busInfo.busType == BusTypes::kMain) // aux channels not currently supported. { audioInputs += busInfo.channelCount; } } } pluginInfo.audio_inputs(audioInputs); count = component->getBusCount(MediaTypes::kEvent, BusDirections::kInput); if (count >= 1) { pluginInfo.has_midi_input(1); } count = component->getBusCount(MediaTypes::kEvent, BusDirections::kOutput); if (count >= 1) { pluginInfo.has_midi_output(1); } } unique_ptr Vst3HostImpl::MakeDetailedPluginInfo(const PluginFactory &factory, const ClassInfo &classInfo, const std::string &path) { IPtr plugProvider = owned(NEW PlugProvider(factory, classInfo, true)); { OPtr component = plugProvider->getComponent(); OPtr controller = plugProvider->getController(); FUnknownPtr midiMapping(controller); unique_ptr info = make_unique(); info->filePath_ = path; info->version_ = classInfo.version(); info->uid_ = classInfo.ID().toString(); Lv2PluginUiInfo &pluginInfo = info->pluginInfo_; pluginInfo.uri(SS("vst3:" << classInfo.ID().toString())); pluginInfo.is_vst3(true); pluginInfo.name(classInfo.name()); pluginInfo.author_name(classInfo.vendor()); pluginInfo.plugin_type( getVst3PluginType(classInfo)); pluginInfo.plugin_display_type(""); // fill this in later. VSTHost has list of name translations; we don;'t. Private::UpdateControlInfo(controller, pluginInfo); UpdateBusInfo(component, pluginInfo); return info; } } void Vst3HostImpl::MakeVst3Info(const std::filesystem::path &path, Vst3Host::PluginList &list) { EnsureContext(); unique_ptr result = std::make_unique(); result->filePath_ = path; string error; Module::Ptr module = Module::create(path.string(), error); if (!module) { Lv2Log::error(SS("Vst3 load failed: " << error << "(" << path.string() << ")")); return; } const PluginFactory &factory = module->getFactory(); for (const ClassInfo &classInfo : factory.classInfos()) { if (classInfo.category() == kVstAudioEffectClass) { bool found = false; std::string newUid = classInfo.ID().toString(); for (const auto &existingPlugin : list) { if (existingPlugin->uid_ == newUid) { found = true; break; // first instance takes precedence (e.g. ~/.vst3 takes precedence over /usr/lib/vst3 ) } } if (!found) { std::unique_ptr pluginInfo = MakeDetailedPluginInfo(factory, classInfo, path); list.push_back(std::move(pluginInfo)); } } } } bool Vst3HostImpl::havePluginsChanged() const { std::unordered_set modules; for (const auto &plugin : pluginList) { if (!std::filesystem::exists(plugin->filePath_)) { return true; } modules.insert(plugin->filePath_); } Module::PathList pathList = Module::getModulePaths(); for (const auto &path : pathList) { if (!modules.contains(path)) { return true; } } return false; } const Vst3Host::PluginList &Vst3HostImpl::RefreshPlugins() { if (cacheFilePath.length() != 0) { LoadPluginCache(); } if (havePluginsChanged()) { RescanPlugins(); } return pluginList; } const Vst3Host::PluginList &Vst3HostImpl::RescanPlugins() { Module::PathList pathList = Module::getModulePaths(); PluginList list; // pathList.insert(pathList.begin(),"/home/pi/.vst3/mda-vst3.vst3"); for (auto &path : pathList) { if (path == std::string("/home/pi/.vst3/mda-vst3.vst3")) { Lv2Log::debug("Processing MDA plugins"); } filesystem::path p{path}; MakeVst3Info(p, list); } this->pluginList = std::move(list); SavePluginCache(); return pluginList; } static std::string ParseVst3Url(const std::string &url) { if (!url.starts_with("vst3:")) { throw Vst3Exception("Not a valid vst3 url.: " + url); } std::string strUrl = url.substr(5); return strUrl; } std::unique_ptr Vst3HostImpl::CreatePlugin(long instanceId, const std::string &url, IHost *pHost) { EnsureContext(); auto uuid = ParseVst3Url(url); std::unique_ptr result; for (const auto &plugin : pluginList) { if (plugin->uid_ == uuid) { return Vst3Effect::CreateInstance(instanceId, *(plugin.get()), pHost); } } throw Vst3Exception("Plugin not found."); } static inline uint8_t Hex(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'z') return c - 'a' + 10; if (c >= 'A' && c <= 'Z') return c - 'A' + 10; throw Vst3Exception("Invalid state bundle."); } static std::vector HexToByteArray(const std::string &hexState) { std::vector result; result.resize(hexState.length() / 2); for (int i = 0; i < result.size(); ++i) { result[i] = Hex(hexState[i * 2]) * 16 + Hex(hexState[i * 2 + 1]); } return result; } std::unique_ptr Vst3HostImpl::CreatePlugin(PedalboardItem &pedalboardItem, IHost *pHost) { std::unique_ptr result = CreatePlugin(pedalboardItem.instanceId(), pedalboardItem.uri(), pHost); auto pluginInfo = this->GetPluginInfo(pedalboardItem.uri()); if (!pluginInfo) { throw Vst3Exception(SS("Plugin " << pedalboardItem.pluginName() << " not found.")); } if (pedalboardItem.vstState().length() != 0) { std::vector state = HexToByteArray(pedalboardItem.vstState()); result->SetState(state); } else { for (const ControlValue &controlValue : pedalboardItem.controlValues()) { int32_t index = -1; for (size_t i = 0; i < pluginInfo->controls().size(); ++i) { if (pluginInfo->controls()[i].symbol() == controlValue.key()) { index = (int32_t)(pluginInfo->controls()[i].index()); break; } } if (index != -1) { result->SetControl(index, controlValue.value()); } } } for (ControlValue &controlValue : pedalboardItem.controlValues()) { int32_t index = -1; for (size_t i = 0; i < pluginInfo->controls().size(); ++i) { if (pluginInfo->controls()[i].symbol() == controlValue.key()) { index = (int32_t)(pluginInfo->controls()[i].index()); break; } } if (index != -1) { float t = result->GetControlValue(index); if (std::isnan(t) || std::isinf(t)) { t = 0; } controlValue.value(t); } else { Lv2Log::warning(SS(pedalboardItem.pluginName() << ": Control key not found. key=" << controlValue.key() )); } } result->GetCurrentPluginInfo(); return result; } #include void Vst3HostImpl::LoadPluginCache() { if (cacheFilePath.length() != 0) { std::ifstream f(cacheFilePath); if (f.is_open()) { json_reader reader(f); reader.read(&(this->pluginList)); } } } void Vst3HostImpl::SavePluginCache() { if (cacheFilePath.length() != 0) { std::ofstream f(cacheFilePath); if (f.is_open()) { json_writer writer(f); writer.write(this->pluginList); } } } Lv2PluginUiInfo *Vst3HostImpl::GetPluginInfo(const std::string &uri) { for (const auto &pluginInfo : pluginList) { if (pluginInfo->pluginInfo_.uri() == uri) { return &(pluginInfo->pluginInfo_); } } return nullptr; }