From 199694535324c9b8e9e6fe0e67dbc2d81c536647 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Mon, 18 Mar 2024 21:30:40 +0100 Subject: [PATCH] Add custom RegionFxPlugin Less than a PluginInsert but more than an IOPlug. --- libs/ardour/ardour/debug.h | 1 + libs/ardour/ardour/plugin.h | 2 + libs/ardour/ardour/region_fx_plugin.h | 185 ++++ libs/ardour/debug.cc | 1 + libs/ardour/luabindings.cc | 10 + libs/ardour/region_fx_plugin.cc | 1168 +++++++++++++++++++++++++ libs/ardour/wscript | 1 + 7 files changed, 1368 insertions(+) create mode 100644 libs/ardour/ardour/region_fx_plugin.h create mode 100644 libs/ardour/region_fx_plugin.cc diff --git a/libs/ardour/ardour/debug.h b/libs/ardour/ardour/debug.h index 7c5ee01124..db63e17756 100644 --- a/libs/ardour/ardour/debug.h +++ b/libs/ardour/ardour/debug.h @@ -90,6 +90,7 @@ namespace PBD { LIBARDOUR_API extern DebugBits ProcessThreads; LIBARDOUR_API extern DebugBits Processors; LIBARDOUR_API extern DebugBits Push2; + LIBARDOUR_API extern DebugBits RegionFx; LIBARDOUR_API extern DebugBits Selection; LIBARDOUR_API extern DebugBits SessionEvents; LIBARDOUR_API extern DebugBits Slave; diff --git a/libs/ardour/ardour/plugin.h b/libs/ardour/ardour/plugin.h index c855e1ae02..1addcf645d 100644 --- a/libs/ardour/ardour/plugin.h +++ b/libs/ardour/ardour/plugin.h @@ -57,6 +57,7 @@ class PlugInsertBase; class PluginInsert; class Plugin; class PluginInfo; +class RegionFxPlugin; class AutomationControl; class SessionObject; @@ -379,6 +380,7 @@ public: protected: friend class PluginInsert; friend class PlugInsertBase; + friend class RegionFxPlugin; friend class Session; /* Called when a parameter of the plugin is changed outside of this diff --git a/libs/ardour/ardour/region_fx_plugin.h b/libs/ardour/ardour/region_fx_plugin.h new file mode 100644 index 0000000000..c52cd755ce --- /dev/null +++ b/libs/ardour/ardour/region_fx_plugin.h @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2024 Robin Gareus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __ardour_region_fx_plugin_h__ +#define __ardour_region_fx_plugin_h__ + +#include + +#include "temporal/domain_provider.h" + +#include "ardour/ardour.h" +#include "ardour/automation_control.h" +#include "ardour/chan_mapping.h" +#include "ardour/libardour_visibility.h" +#include "ardour/parameter_descriptor.h" +#include "ardour/plug_insert_base.h" +#include "ardour/plugin.h" +#include "ardour/types.h" + +namespace Gtkmm2ext +{ +class WindowProxy; +} + +class XMLNode; + +namespace ARDOUR +{ +class ReadOnlyControl; + +class LIBARDOUR_API RegionFxPlugin : public SessionObject, public PlugInsertBase, public Latent, public Temporal::TimeDomainProvider +{ +public: + RegionFxPlugin (Session&, Temporal::TimeDomain const, std::shared_ptr = std::shared_ptr ()); + ~RegionFxPlugin (); + + /* UI Proxy */ + Gtkmm2ext::WindowProxy* window_proxy () const + { + return _window_proxy; + } + void set_window_proxy (Gtkmm2ext::WindowProxy* wp) + { + _window_proxy = wp; + } + + /* Latent */ + samplecnt_t signal_latency () const; + + /* PlugInsertBase */ + uint32_t get_count () const + { + return _plugins.size (); + } + PluginType type () const + { + return plugin ()->get_info ()->type; + } + std::shared_ptr plugin (uint32_t num = 0) const + { + if (num < _plugins.size ()) { + return _plugins[num]; + } else { + return _plugins[0]; + } + } + + UIElements ui_elements () const; + std::shared_ptr control_factory(const Evoral::Parameter& id); + + bool write_immediate_event (Evoral::EventType event_type, size_t size, const uint8_t* buf); + bool load_preset (Plugin::PresetRecord); + + std::shared_ptr control_output (uint32_t) const; + + bool reset_parameters_to_default (); + bool can_reset_all_parameters (); + + std::string describe_parameter (Evoral::Parameter param); + + bool provides_stats () const + { + return false; + } + bool get_stats (PBD::microseconds_t&, PBD::microseconds_t&, double&, double&) const + { + return false; + } + void clear_stats () {} + + /* Stateful */ + XMLNode& get_state (void) const; + int set_state (const XMLNode&, int version); + + void drop_references (); + void update_id (PBD::ID); + + /* Fx */ + bool run (BufferSet&, samplepos_t start, samplepos_t end, samplepos_t region_pos, pframes_t nframes, sampleoffset_t off); + + void flush (); + int set_block_size (pframes_t nframes); + void automatables (PBD::ControllableSet&) const; + void set_default_automation (timepos_t); + + void truncate_automation_start (timecnt_t); + void truncate_automation_end (timepos_t); + + bool can_support_io_configuration (const ChanCount& in, ChanCount& out); + bool configure_io (ChanCount in, ChanCount out); + + ChanCount input_streams () const + { + return _configured_in; + } + ChanCount output_streams () const + { + return _configured_out; + } + ChanCount required_buffers () const + { + return _required_buffers; + } + +private: + /* disallow copy construction */ + RegionFxPlugin (RegionFxPlugin const&); + + void add_plugin (std::shared_ptr); + void plugin_removed (std::weak_ptr); + bool set_count (uint32_t num); + bool check_inplace (); + void create_parameters (); + void parameter_changed_externally (uint32_t, float); + void automation_run (samplepos_t start, pframes_t nframes); + bool find_next_event (timepos_t const& start, timepos_t const& end, Evoral::ControlEvent& next_event) const; + void start_touch (uint32_t param_id); + void end_touch (uint32_t param_id); + bool connect_and_run (BufferSet&, samplepos_t start, samplepos_t end, samplepos_t region_pos, pframes_t nframes, sampleoffset_t buf_off, sampleoffset_t cycle_off); + + Match private_can_support_io_configuration (ChanCount const&, ChanCount&) const; + + /** details of the match currently being used */ + Match _match; + + uint32_t _plugin_signal_latency; + + typedef std::vector> Plugins; + Plugins _plugins; + + ChanCount _configured_in; + ChanCount _configured_out; + ChanCount _required_buffers; + + std::map _in_map; + std::map _out_map; + + bool _configured; + bool _no_inplace; + + typedef std::map> CtrlOutMap; + CtrlOutMap _control_outputs; + + Gtkmm2ext::WindowProxy* _window_proxy; + std::atomic _flush; +}; + +} // namespace ARDOUR + +#endif diff --git a/libs/ardour/debug.cc b/libs/ardour/debug.cc index 96c409c108..e061c2840d 100644 --- a/libs/ardour/debug.cc +++ b/libs/ardour/debug.cc @@ -85,6 +85,7 @@ PBD::DebugBits PBD::DEBUG::Ports = PBD::new_debug_bit ("Ports"); PBD::DebugBits PBD::DEBUG::ProcessThreads = PBD::new_debug_bit ("processthreads"); PBD::DebugBits PBD::DEBUG::Processors = PBD::new_debug_bit ("processors"); PBD::DebugBits PBD::DEBUG::Push2 = PBD::new_debug_bit ("push2"); +PBD::DebugBits PBD::DEBUG::RegionFx = PBD::new_debug_bit ("regionfx"); PBD::DebugBits PBD::DEBUG::Selection = PBD::new_debug_bit ("selection"); PBD::DebugBits PBD::DEBUG::SessionEvents = PBD::new_debug_bit ("sessionevents"); PBD::DebugBits PBD::DEBUG::Slave = PBD::new_debug_bit ("slave"); diff --git a/libs/ardour/luabindings.cc b/libs/ardour/luabindings.cc index 637bb48447..acc029961d 100644 --- a/libs/ardour/luabindings.cc +++ b/libs/ardour/luabindings.cc @@ -85,6 +85,7 @@ #include "ardour/runtime_functions.h" #include "ardour/region.h" #include "ardour/region_factory.h" +#include "ardour/region_fx_plugin.h" #include "ardour/return.h" #include "ardour/revision.h" #include "ardour/route_group.h" @@ -340,6 +341,7 @@ CLASSKEYS(std::shared_ptr); CLASSKEYS(std::shared_ptr); CLASSKEYS(std::shared_ptr); CLASSKEYS(std::shared_ptr); +CLASSKEYS(std::shared_ptr); CLASSKEYS(std::shared_ptr); CLASSKEYS(std::shared_ptr); CLASSKEYS(std::shared_ptr); @@ -2030,6 +2032,14 @@ LuaBindings::common (lua_State* L) .addRefFunction ("get_stats", &PluginInsert::get_stats) .endClass () + .deriveWSPtrClass ("RegionFxPlugin") + .addFunction ("plugin", &RegionFxPlugin::plugin) + .addFunction ("signal_latency", &RegionFxPlugin::signal_latency) + .addFunction ("get_count", &RegionFxPlugin::get_count) + .addFunction ("type", &RegionFxPlugin::type) + .addFunction ("reset_parameters_to_default", &RegionFxPlugin::reset_parameters_to_default) + .endClass () + .deriveWSPtrClass , PBD::Controllable> ("MPGainControl") .addFunction ("set_value", &MPControl::set_value) .addFunction ("get_value", &MPControl::get_value) diff --git a/libs/ardour/region_fx_plugin.cc b/libs/ardour/region_fx_plugin.cc new file mode 100644 index 0000000000..dab9dab7b2 --- /dev/null +++ b/libs/ardour/region_fx_plugin.cc @@ -0,0 +1,1168 @@ +/* + * Copyright (C) 2024 Robin Gareus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifdef WAF_BUILD +#include "libardour-config.h" +#endif + +#include + +#include "pbd/assert.h" +#include "pbd/types_convert.h" +#include "pbd/xml++.h" + +#include "ardour/automatable.h" +#include "ardour/debug.h" +#include "ardour/plugin.h" +#include "ardour/readonly_control.h" +#include "ardour/region_fx_plugin.h" +#include "ardour/session.h" + +using namespace std; +using namespace ARDOUR; +using namespace PBD; + +RegionFxPlugin::RegionFxPlugin (Session& s, Temporal::TimeDomain const td, std::shared_ptr plug) + : SessionObject (s, (plug ? plug->name () : string ("toBeRenamed"))) + , TimeDomainProvider (td) + , _plugin_signal_latency (0) + , _configured (false) + , _no_inplace (false) + , _window_proxy (0) +{ + _flush.store (0); + + if (plug) { + add_plugin (plug); + create_parameters (); + } +} + +RegionFxPlugin::~RegionFxPlugin () +{ + for (auto const& i : _control_outputs) { + std::dynamic_pointer_cast(i.second)->drop_references (); + } + + Glib::Threads::Mutex::Lock lm (_control_lock); + for (auto const& i : _controls) { + std::dynamic_pointer_cast(i.second)->drop_references (); + } + _controls.clear (); +} + +XMLNode& +RegionFxPlugin::get_state () const +{ + XMLNode* node = new XMLNode (/*state_node_name*/ "RegionFXPlugin"); + + Latent::add_state (node); + + node->set_property ("type", _plugins[0]->state_node_name ()); + node->set_property ("unique-id", _plugins[0]->unique_id ()); + node->set_property ("count", (uint32_t)_plugins.size ()); + + node->set_property ("id", id ()); + node->set_property ("name", name ()); + + _plugins[0]->set_insert_id (this->id ()); + node->add_child_nocopy (_plugins[0]->get_state ()); + + for (auto const& c : controls ()) { + std::shared_ptr ac = std::dynamic_pointer_cast (c.second); + if (!ac) { + continue; + } + node->add_child_nocopy (ac->get_state ()); + std::shared_ptr l = ac->alist (); + if (l && 0 == (ac->flags() & Controllable::NotAutomatable)) { + node->add_child_nocopy (l->get_state ()); + } + } + + return *node; +} + +int +RegionFxPlugin::set_state (const XMLNode& node, int version) +{ + set_id (node); + + /* with ::regenerate_xml_or_string_ids(), set_id() creates a new ID */ + PBD::ID new_id = this->id(); + PBD::ID old_id = this->id(); + + node.get_property ("id", old_id); + + ARDOUR::PluginType type; + std::string unique_id; + if (!parse_plugin_type (node, type, unique_id)) { + return -1; + } + + bool any_vst; + + uint32_t count = 1; + node.get_property ("count", count); + + if (_plugins.empty()) { + std::shared_ptr plugin = find_and_load_plugin (_session, node, type, unique_id, any_vst); + + if (!plugin) { + return -1; + } + + add_plugin (plugin); + create_parameters (); + set_control_ids (node, version); + + if (_plugins.size () != count) { + for (uint32_t n = 1; n < count; ++n) { + add_plugin (plugin_factory (plugin)); + } + } + + } else { + assert (_plugins[0]->unique_id() == unique_id); + set_control_ids (node, version, true); + } + + string name; + if (node.get_property ("name", name)) { + set_name (name); + } else { + set_name (_plugins[0]->get_info ()->name); + } + + XMLNodeList nlist = node.children (); + XMLNodeIterator niter; + + for (niter = nlist.begin (); niter != nlist.end (); ++niter) { + if ((*niter)->name () != "AutomationList") { + continue; + } + XMLProperty const* id_prop = (*niter)->property("automation-id"); + if (!id_prop) { + assert (0); + continue; + } + Evoral::Parameter param = EventTypeMap::instance().from_symbol(id_prop->value()); + std::shared_ptr c = control (param); + std::shared_ptr ac = std::dynamic_pointer_cast (c); + if (ac && ac->alist () && 0 == (ac->flags() & Controllable::NotAutomatable)) { + ac->alist()->set_state (**niter, version); + } + } + + for (niter = nlist.begin (); niter != nlist.end (); ++niter) { + if ((*niter)->name () == _plugins[0]->state_node_name () || (any_vst && ((*niter)->name () == "lxvst" || (*niter)->name () == "windows-vst" || (*niter)->name () == "mac-vst"))) { + for (auto const& i : _plugins) { + if (!regenerate_xml_or_string_ids ()) { + i->set_insert_id (new_id); + } else { + i->set_insert_id (old_id); + } + + i->set_state (**niter, version); + + if (regenerate_xml_or_string_ids ()) { + i->set_insert_id (new_id); + } + } + } + } + for (auto const& i : _plugins) { + i->activate (); + } + + /* when copying plugin state, notify UI */ + for (auto const& i : _controls) { + std::shared_ptr ac = std::dynamic_pointer_cast (i.second); + if (ac) { + ac->Changed (false, Controllable::NoGroup); /* EMIT SIGNAL */ + } + } + return 0; +} + +void +RegionFxPlugin::update_id (PBD::ID id) +{ + set_id (id.to_s()); + for (Plugins::iterator i = _plugins.begin(); i != _plugins.end(); ++i) { + (*i)->set_insert_id (id); + } +} + +void +RegionFxPlugin::add_plugin (std::shared_ptr plugin) +{ + plugin->set_insert_id (this->id ()); + plugin->set_non_realtime (true); + + if (_plugins.empty ()) { + /* first (and probably only) plugin instance - connect to relevant signals */ + plugin->ParameterChangedExternally.connect_same_thread (*this, boost::bind (&RegionFxPlugin::parameter_changed_externally, this, _1, _2)); + plugin->StartTouch.connect_same_thread (*this, boost::bind (&RegionFxPlugin::start_touch, this, _1)); + plugin->EndTouch.connect_same_thread (*this, boost::bind (&RegionFxPlugin::end_touch, this, _1)); + } + + _plugins.push_back (plugin); + + if (_plugins.size () > 1) { + _plugins[0]->add_slave (plugin, true); + plugin->DropReferences.connect_same_thread (*this, boost::bind (&RegionFxPlugin::plugin_removed, this, std::weak_ptr (plugin))); + } +} + +void +RegionFxPlugin::plugin_removed (std::weak_ptr wp) +{ + std::shared_ptr plugin = wp.lock (); + if (_plugins.size () == 0 || !plugin) { + return; + } + _plugins[0]->remove_slave (plugin); +} + +bool +RegionFxPlugin::set_count (uint32_t num) +{ + bool require_state = !_plugins.empty (); + + if (require_state && num > 1 && plugin (0)->get_info ()->type == ARDOUR::AudioUnit) { + // we don't allow to replicate AUs + return false; + } + + if (num == 0) { + return false; + } else if (num > _plugins.size ()) { + uint32_t diff = num - _plugins.size (); + + for (uint32_t n = 0; n < diff; ++n) { + std::shared_ptr p = plugin_factory (_plugins[0]); + add_plugin (p); + + if (require_state) { + _plugins[0]->set_insert_id (this->id ()); + XMLNode& state = _plugins[0]->get_state (); + p->set_state (state, Stateful::current_state_version); + delete &state; + } + p->activate (); + } + + } else if (num < _plugins.size ()) { + uint32_t diff = _plugins.size () - num; + for (uint32_t n = 0; n < diff; ++n) { + _plugins.back ()->drop_references (); + _plugins.pop_back (); + } + } + return true; +} + +void +RegionFxPlugin::drop_references () +{ + for (Plugins::iterator i = _plugins.begin (); i != _plugins.end (); ++i) { + (*i)->drop_references (); + } + + SessionObject::drop_references (); +} + +ARDOUR::samplecnt_t +RegionFxPlugin::signal_latency () const +{ + return _plugins.front ()->signal_latency (); +} + +PlugInsertBase::UIElements +RegionFxPlugin::ui_elements () const +{ + return PluginPreset; +} + +void +RegionFxPlugin::create_parameters () +{ + assert (!_plugins.empty ()); + + std::shared_ptr plugin = _plugins.front (); + set a = _plugins.front ()->automatable (); + + for (uint32_t i = 0; i < plugin->parameter_count (); ++i) { + if (!plugin->parameter_is_control (i)) { + continue; + } + + ParameterDescriptor desc; + plugin->get_parameter_descriptor (i, desc); + + if (!plugin->parameter_is_input (i)) { + _control_outputs[i] = std::shared_ptr (new ReadOnlyControl (plugin, desc, i)); + continue; + } + + Evoral::Parameter param (PluginAutomation, 0, i); + const bool automatable = a.find(param) != a.end(); + + std::shared_ptr list (new AutomationList (param, desc, *this)); + std::shared_ptr c (new PluginControl (_session, this, param, desc, list)); + if (!automatable) { + c->set_flag (Controllable::NotAutomatable); + } + add_control (c); + + plugin->set_automation_control (i, c); + } + + Plugin::PropertyDescriptors const& pdl (plugin->get_supported_properties ()); + + for (Plugin::PropertyDescriptors::const_iterator p = pdl.begin (); p != pdl.end (); ++p) { + Evoral::Parameter param (PluginPropertyAutomation, 0, p->first); + ParameterDescriptor const& desc = plugin->get_property_descriptor (param.id ()); + if (desc.datatype == Variant::NOTHING) { + continue; + } + std::shared_ptr c (new PluginPropertyControl (_session, this, param, desc)); + c->set_flag (Controllable::NotAutomatable); + add_control (c); + } + + plugin->PresetPortSetValue.connect_same_thread (*this, boost::bind (&RegionFxPlugin::preset_load_set_value, this, _1, _2)); +} + +void +RegionFxPlugin::set_default_automation (timepos_t end) +{ + for (auto const& i : _controls) { + std::shared_ptr ac = std::dynamic_pointer_cast (i.second); + assert (ac->alist ()->empty ()); + ac->alist ()->fast_simple_add (timepos_t (time_domain ()), ac->normal ()); + ac->alist ()->fast_simple_add (end, ac->normal ()); + } +} + +void +RegionFxPlugin::truncate_automation_start (timecnt_t start) +{ + for (auto const& i : _controls) { + std::shared_ptr ac = std::dynamic_pointer_cast (i.second); + ac->alist ()->truncate_start (start); + } +} + +void +RegionFxPlugin::truncate_automation_end (timepos_t end) +{ + for (auto const& i : _controls) { + std::shared_ptr ac = std::dynamic_pointer_cast (i.second); + ac->alist ()->truncate_end (end); + } +} + +bool +RegionFxPlugin::write_immediate_event (Evoral::EventType event_type, size_t size, const uint8_t* buf) +{ + bool rv = true; + for (auto const& i : _plugins) { + if (!i->write_immediate_event (event_type, size, buf)) { + rv = false; + } + } + return rv; +} + +bool +RegionFxPlugin::load_preset (ARDOUR::Plugin::PresetRecord pr) +{ + bool rv = true; + for (auto const& i : _plugins) { + if (!i->load_preset (pr)) { + rv = false; + } + } + return rv; +} + +std::shared_ptr +RegionFxPlugin::control_output (uint32_t num) const +{ + CtrlOutMap::const_iterator i = _control_outputs.find (num); + if (i == _control_outputs.end ()) { + return std::shared_ptr (); + } else { + return (*i).second; + } +} + +void +RegionFxPlugin::parameter_changed_externally (uint32_t which, float val) +{ + std::shared_ptr c = control (Evoral::Parameter (PluginAutomation, 0, which)); + std::shared_ptr pc = std::dynamic_pointer_cast (c); + + if (pc) { + pc->catch_up_with_external_value (val); + } + + /* Second propagation: tell all plugins except the first to + * update the value of this parameter. For sane plugin APIs, + * there are no other plugins, so this is a no-op in those + * cases. + */ + + Plugins::iterator i = _plugins.begin (); + + /* don't set the first plugin, just all the slaves */ + + if (i != _plugins.end ()) { + ++i; + for (; i != _plugins.end (); ++i) { + (*i)->set_parameter (which, val, 0); + } + } +} + +std::string +RegionFxPlugin::describe_parameter (Evoral::Parameter param) +{ + if (param.type () == PluginAutomation) { + return _plugins[0]->describe_parameter (param); + } else if (param.type () == PluginPropertyAutomation) { + std::shared_ptr c = std::dynamic_pointer_cast (control (param)); + if (c && !c->desc ().label.empty ()) { + return c->desc ().label; + } + } + return EventTypeMap::instance ().to_symbol (param); +} + +void +RegionFxPlugin::start_touch (uint32_t param_id) +{ + assert (0); // touch is N/A + std::shared_ptr ac = std::dynamic_pointer_cast (control (Evoral::Parameter (PluginAutomation, 0, param_id))); + if (ac) { + ac->start_touch (timepos_t (_session.audible_sample ())); // XXX subtract region position + } +} + +void +RegionFxPlugin::end_touch (uint32_t param_id) +{ + assert (0); // touch is N/A + std::shared_ptr ac = std::dynamic_pointer_cast (control (Evoral::Parameter (PluginAutomation, 0, param_id))); + if (ac) { + ac->stop_touch (timepos_t (_session.audible_sample ())); // XXX subtract region position + } +} + +bool +RegionFxPlugin::can_reset_all_parameters () +{ + bool all = true; + uint32_t params = 0; + std::shared_ptr plugin = _plugins.front (); + for (uint32_t par = 0; par < plugin->parameter_count (); ++par) { + bool ok = false; + const uint32_t cid = plugin->nth_parameter (par, ok); + + if (!ok || !plugin->parameter_is_input (cid)) { + continue; + } + + std::shared_ptr ac = std::dynamic_pointer_cast (control (Evoral::Parameter (PluginAutomation, 0, cid))); + if (!ac) { + continue; + } + + ++params; + + if (ac->automation_state () & Play) { + all = false; + break; + } + } + return all && (params > 0); +} + +bool +RegionFxPlugin::reset_parameters_to_default () +{ + bool all = true; + std::shared_ptr plugin = _plugins.front (); + + for (uint32_t par = 0; par < plugin->parameter_count (); ++par) { + bool ok = false; + const uint32_t cid = plugin->nth_parameter (par, ok); + + if (!ok || !plugin->parameter_is_input (cid)) { + continue; + } + + const float dflt = plugin->default_value (cid); + const float curr = plugin->get_parameter (cid); + + if (dflt == curr) { + continue; + } + + std::shared_ptr ac = std::dynamic_pointer_cast (control (Evoral::Parameter (PluginAutomation, 0, cid))); + if (!ac) { + continue; + } + + if (ac->automation_state () & Play) { + all = false; + continue; + } + + ac->set_value (dflt, Controllable::NoGroup); + } + return all; +} + +void +RegionFxPlugin::flush () +{ + _flush.store (1); +} + +bool +RegionFxPlugin::can_support_io_configuration (const ChanCount& in, ChanCount& out) +{ + return private_can_support_io_configuration (in, out).method != Impossible; +} + +PlugInsertBase::Match +RegionFxPlugin::private_can_support_io_configuration (ChanCount const& in, ChanCount& out) const +{ + assert (!_plugins.empty ()); + PluginInfoPtr info = _plugins.front ()->get_info (); + ChanCount aux_in; + + /* count sidechain inputs */ + const ChanCount& nis (info->n_inputs); + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + for (uint32_t in = 0; in < nis.get (*t); ++in) { + const Plugin::IOPortDescription& iod (plugin (0)->describe_io_port (*t, true, in)); + if (iod.is_sidechain) { + aux_in.set (*t, 1 + aux_in.n (*t)); + } + } + } + + if (info->reconfigurable_io ()) { + ChanCount inx (in); + bool const r = _plugins.front ()->match_variable_io (inx, aux_in, out); + if (!r) { + return Match (Impossible, 0); + } + out = ChanCount::min (in, out); + return Match (Delegate, 1, true); + } + + ChanCount inputs = info->n_inputs - aux_in; + ChanCount outputs = info->n_outputs; + + bool no_inputs = true; + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + if (inputs.get (*t) != 0) { + no_inputs = false; + break; + } + } + + if (no_inputs) { + /* RegionFX cannot be generators */ + return Match (Impossible, 0); + } + + if (inputs == in && outputs == in) { + out = outputs; + return Match (ExactMatch, 1); + } + + /* if the plugin has more outputs than we need, we ignore them */ + if (inputs == in && outputs > in) { + out = inputs; + return Match (Split, 1); + } + + /* test replication of mono plugins */ + uint32_t f = 0; + bool can_replicate = true; + + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + int32_t nin = inputs.get (*t); + /* No inputs of this type */ + if (nin == 0 && in.get (*t) == 0) { + continue; + } + if (nin != 1 || outputs.get (*t) != 1) { + can_replicate = false; + break; + } + + if (f == 0) { + f = in.get (*t) / nin; + } + if (f != (in.get (*t) / nin)) { + can_replicate = false; + break; + } + } + + if (can_replicate && f > 0) { + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + out.set (*t, outputs.get (*t) * f); + } + return Match (Replicate, f); + } + + /* If the plugin has more inputs than we want, we can `hide' some of them by feeding them silence. */ + bool could_hide = false; + bool cannot_hide = false; + ChanCount hide_channels; + + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + if (inputs.get (*t) > in.get (*t)) { + /* there is potential to hide, since the plugin has more inputs of type t than the insert */ + hide_channels.set (*t, inputs.get (*t) - in.get (*t)); + could_hide = true; + } else if (inputs.get (*t) < in.get (*t)) { + /* we definitely cannot hide, since the plugin has fewer inputs of type t than the insert */ + cannot_hide = true; + } + } + + if (could_hide && !cannot_hide && outputs >= in) { + out = ChanCount::min (in, outputs); + return Match (Hide, 1, false, false, hide_channels); + } + + /* Test replication of multi-channel plugins: + * (at least as many plugins so that output count matches input count) + */ + f = 0; + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + uint32_t nin = inputs.get (*t); + uint32_t nout = outputs.get (*t); + if (nin == 0 || in.get (*t) == 0 || nout == 0) { + continue; + } + // prefer floor() so the count won't overly increase IFF (nin < nout) + f = max (f, (uint32_t)floor (in.get (*t) / (float)nout)); + } + if (f > 0 && outputs * f >= out) { + out = ChanCount::min (in, outputs * f); + return Match (Replicate, f, true); + } + + /* Test replication of multi-channel plugins: + * (at least as many plugins to connect all inputs) + */ + f = 0; + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + uint32_t nin = inputs.get (*t); + if (nin == 0 || in.get (*t) == 0) { + continue; + } + f = max (f, (uint32_t)ceil (in.get (*t) / (float)nin)); + } + if (f > 0) { + out = ChanCount::min (in, outputs * f); + return Match (Replicate, f, true); + } + + return Match (Impossible, 0); +} + +bool +RegionFxPlugin::configure_io (ChanCount in, ChanCount out) +{ + _configured_in = in; + _configured_out = out; + + ChanCount natural_input_streams = _plugins[0]->get_info ()->n_inputs; + ChanCount natural_output_streams = _plugins[0]->get_info ()->n_outputs; + + _match = private_can_support_io_configuration (in, out); + + if (set_count (_match.plugins) == false) { + return false; + } + + /* configure plugins */ + switch (_match.method) { + case Split: + /* fallthrough */ + case Hide: + if (_plugins.front ()->reconfigure_io (natural_input_streams, ChanCount (), out) == false) { + return false; + } + break; + case Delegate: { + ChanCount din (in); + ChanCount daux; // no sidechain ports + ChanCount dout (_configured_out); + bool const r = _plugins.front ()->match_variable_io (din, daux, dout); + assert (r); + if (_plugins.front ()->reconfigure_io (din, daux, dout) == false) { + return false; + } + DEBUG_TRACE (DEBUG::RegionFx, string_compose ("Delegate configured in: %1, out: %2 for in: %3 out: %4", din, dout, in, _configured_out)); + if (din < in || dout < _configured_out) { + return false; + } + } + break; + case Replicate: + assert (!_plugins.front ()->get_info ()->reconfigurable_io ()); + break; + default: + if (_plugins.front ()->reconfigure_io (in, ChanCount (), out) == false) { + return false; + } + break; + } + + /* compare to PluginInsert::reset_map */ + _in_map.clear (); + _out_map.clear (); + + /* builld input map, skip plugin sidechain pin. */ + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + uint32_t pc = 0; + uint32_t cin = 0; + const uint32_t cend = _configured_in.get (*t); + for (Plugins::iterator i = _plugins.begin (); i != _plugins.end (); ++i, ++pc) { + const uint32_t nis = natural_input_streams.get (*t); + for (uint32_t in = 0; in < nis; ++in) { + const Plugin::IOPortDescription& iod (_plugins[pc]->describe_io_port (*t, true, in)); + if (iod.is_sidechain) { + /* leave N/C */ + continue; + } + if (cin < cend) { + _in_map[pc].set (*t, in, cin); + ++cin; + } else { + break; + } + } + } + } + + /* build output map */ + uint32_t pc = 0; + for (Plugins::iterator i = _plugins.begin (); i != _plugins.end (); ++i, ++pc) { + _out_map[pc] = ChanMapping (ChanCount::min (natural_output_streams, _configured_out)); + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + _out_map[pc].offset_to (*t, pc * natural_output_streams.get (*t)); + } + } + + /* now sanitize maps */ + for (uint32_t pc = 0; pc < get_count (); ++pc) { + ChanMapping new_in; + ChanMapping new_out; + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + for (uint32_t i = 0; i < natural_input_streams.get (*t); ++i) { + bool valid; + uint32_t idx = _in_map[pc].get (*t, i, &valid); + if (valid && idx < _configured_in.get (*t)) { + new_in.set (*t, i, idx); + } + } + for (uint32_t o = 0; o < natural_output_streams.get (*t); ++o) { + bool valid; + uint32_t idx = _out_map[pc].get (*t, o, &valid); + if (valid && idx < _configured_out.get (*t)) { + new_out.set (*t, o, idx); + } + } + } + _in_map[pc] = new_in; + _out_map[pc] = new_out; + } + + _no_inplace = check_inplace (); + _required_buffers = ChanCount::max (_configured_in, natural_input_streams + ChanCount::max (_configured_out, natural_output_streams * get_count ())); + +#ifndef NDEBUG + if (DEBUG_ENABLED(DEBUG::RegionFx)) { + pc = 0; + DEBUG_STR_DECL(a); + DEBUG_STR_APPEND(a, "\n--------<<--------\n"); + DEBUG_STR_APPEND(a, string_compose ("RFX IO Config for %1 in: %2 out: %3 req: %4\n", name(), _configured_in, _configured_out, _required_buffers)); + DEBUG_STR_APPEND(a, "Match: " << _match << " no inplace: " << _no_inplace << "\n"); + for (Plugins::iterator i = _plugins.begin(); i != _plugins.end(); ++i, ++pc) { + if (pc > 0) { + DEBUG_STR_APPEND(a, "----><----\n"); + } + DEBUG_STR_APPEND(a, string_compose ("Channel Map for %1 plugin %2\n", name(), pc)); + DEBUG_STR_APPEND(a, " * Inputs:\n"); + DEBUG_STR_APPEND(a, _in_map[pc]); + DEBUG_STR_APPEND(a, " * Outputs:\n"); + DEBUG_STR_APPEND(a, _out_map[pc]); + } + DEBUG_STR_APPEND(a, "-------->>--------\n"); + } +#endif + + return true; +} + +bool +RegionFxPlugin::check_inplace () +{ + bool inplace_ok = !_plugins.front ()->inplace_broken (); + + if (_match.method == Hide || _match.method == Split) { + inplace_ok = false; + } + + if (_match.method == Replicate) { + for (uint32_t pc = 0; pc < get_count () && inplace_ok; ++pc) { + if (_in_map[pc] != _out_map[pc]) { + inplace_ok = false; + break; + } + } + + ChanCount natural_input_streams = _plugins[0]->get_info ()->n_inputs; + ChanCount natural_output_streams = _plugins[0]->get_info ()->n_outputs; + + if (natural_input_streams * get_count () != _configured_in) { + inplace_ok = false; + } + if (natural_output_streams * get_count () != _configured_out) { + inplace_ok = false; + } + + ARDOUR::ChanMapping in_map; + ARDOUR::ChanMapping out_map; + + uint32_t pc = 0; + for (auto const& mi : _in_map) { + ChanMapping m (mi.second); + const ChanMapping::Mappings& mp (mi.second.mappings()); + for (auto const& tm: mp) { + for (auto const& i: tm.second) { + in_map.set (tm.first, i.first + pc * natural_input_streams.get (tm.first), i.second); + } + } + ++pc; + } + + pc = 0; + for (auto const& mi : _out_map) { + ChanMapping m (mi.second); + const ChanMapping::Mappings& mp (mi.second.mappings()); + for (auto const& tm: mp) { + for (auto const& i: tm.second) { + out_map.set (tm.first, i.first + pc * natural_output_streams.get (tm.first), i.second); + } + } + ++pc; + } + + if (!in_map.is_monotonic ()) { + inplace_ok = false; + } + if (!out_map.is_monotonic ()) { + inplace_ok = false; + } + return !inplace_ok; + } + + for (uint32_t pc = 0; pc < get_count () && inplace_ok; ++pc) { + if (!_in_map[pc].is_monotonic ()) { + inplace_ok = false; + } + if (!_out_map[pc].is_monotonic ()) { + inplace_ok = false; + } + } + return !inplace_ok; +} + +int +RegionFxPlugin::set_block_size (pframes_t nframes) +{ + int ret = 0; + for (auto const& i : _plugins) { + if (i->set_block_size (nframes) != 0) { + ret = -1; + } + } + return ret; +} + +std::shared_ptr +RegionFxPlugin::control_factory(const Evoral::Parameter& param) +{ + Evoral::Control* control = NULL; + ParameterDescriptor desc(param); + std::shared_ptr list; + +#if 0 + if (param.type() == PluginAutomation) { + _plugin->get_parameter_descriptor(param.id(), desc); + control = new IOPlug::PluginControl (pi, param, desc); + } else if (param.type() == PluginPropertyAutomation) { + desc = _plugin->get_property_descriptor (param.id()); + if (desc.datatype != Variant::NOTHING) { + control = new IOPlug::PluginPropertyControl(pi, param, desc, list); + } + } +#endif + + if (!control) { + std::shared_ptr list; + control = new AutomationControl (_session, param, desc, list); + } + + return std::shared_ptr(control); +} + +void +RegionFxPlugin::automatables (ControllableSet& s) const +{ + for (auto const& i : _controls) { + std::shared_ptr ac = std::dynamic_pointer_cast (i.second); + if (ac) { + s.insert (ac); + } + } +} + +void +RegionFxPlugin::automation_run (samplepos_t start, pframes_t nframes) +{ + for (auto const& i : controls ()) { + std::shared_ptr c = std::dynamic_pointer_cast (i.second); + if (!c) { + continue; + } + c->automation_run (start, nframes); + } +} + +bool +RegionFxPlugin::find_next_event (timepos_t const& start, timepos_t const& end, Evoral::ControlEvent& next_event) const +{ + next_event.when = start <= end ? timepos_t::max (start.time_domain()) : timepos_t (start.time_domain()); + + for (auto const& i : controls ()) { + std::shared_ptr c = std::dynamic_pointer_cast (i.second); + if (c) { + Automatable::find_next_ac_event (c, start, end, next_event); + } + } + return next_event.when != (start <= end ? timepos_t::max (next_event.when.time_domain ()) : timepos_t (next_event.when.time_domain ())); +} + +bool +RegionFxPlugin::run (BufferSet& bufs, samplepos_t start, samplepos_t end, samplepos_t pos, pframes_t nframes, sampleoffset_t off) +{ + int canderef (1); + if (_flush.compare_exchange_strong (canderef, 0)) { + for (auto const& i : _plugins) { + i->flush (); + } + } + + const bool no_split_cycle = _plugins.front ()->requires_fixed_sized_buffers () /* || _plugins.front ()->get_info ()->type == ARDOUR::VST3 */; + + Evoral::ControlEvent next_event (timepos_t (Temporal::AudioTime), 0.0f); + samplecnt_t offset = 0; + + Glib::Threads::Mutex::Lock lm (control_lock ()); + + if (no_split_cycle || !find_next_event (timepos_t (start), timepos_t (end), next_event)) { + /* no events have a time within the relevant range */ + return connect_and_run (bufs, start, end, pos, nframes, off, offset); + } + + while (nframes) { + samplecnt_t cnt = min (timepos_t (start).distance (next_event.when).samples (), (samplecnt_t)nframes); + + /* An event returned by find_next_event is always be *after* `start`. */ + assert (timepos_t (start) < next_event.when); + /* However it may still be at the sample sample (when event is using BeatTime), + * in which case we need to look for the next event, after that. + */ + int timeout = 8; // just in case there is more than one music-time event for the given sample. + while (cnt == 0 && --timeout > 0 && Temporal::AudioTime != next_event.when.time_domain ()) { + timepos_t _start = next_event.when; // copy, since find_next_event uses a reference, and modifies next_event + if (!find_next_event (_start, timepos_t (end), next_event)) { + cnt = nframes; + break; + } else { + cnt = min (timepos_t (start).distance (next_event.when).samples (), (samplecnt_t)nframes); + } + } + + if (cnt <= 0) { + /* prevent endless loops, just skip over events until next cycle. + * (alternatively we could single step and set cnt = 1;) + */ + break; + } + + if (!connect_and_run (bufs, start, start + cnt, pos, cnt, off, offset)) { + return false; + } + + nframes -= cnt; + offset += cnt; + start += cnt; + + timepos_t _start = next_event.when; + if (!find_next_event (_start, timepos_t (end), next_event)) { + break; + } + } + + if (nframes) { + return connect_and_run (bufs, start, start + nframes, pos, nframes, off, offset); + } + return true; +} + +bool +RegionFxPlugin::connect_and_run (BufferSet& bufs, samplepos_t start, samplepos_t end, samplepos_t pos, pframes_t nframes, samplecnt_t buf_off, samplecnt_t cycle_off) +{ + const bool no_inplace = _no_inplace; + Temporal::TempoMap::update_thread_tempo_map (); + + //bufs.set_count(ChanCount::max(bufs.count(), _configured_internal)); // ADD SC + bufs.set_count (ChanCount::max (bufs.count (), _configured_out)); + + automation_run (start, nframes); + // TODO set VST3 event-list, then unset no_split_cycle + + ChanCount natural_input_streams = _plugins[0]->get_info ()->n_inputs; + ChanCount natural_output_streams = _plugins[0]->get_info ()->n_outputs; + + std::map in_map (_in_map); + std::map const& out_map (_out_map); + + if (no_inplace) { + uint32_t pc = 0; + BufferSet& inplace_bufs = _session.get_noinplace_buffers (); + ARDOUR::ChanMapping used_outputs; + + assert (inplace_bufs.count () >= natural_input_streams + _configured_out); + + /* build used-output map */ + for (Plugins::iterator i = _plugins.begin (); i != _plugins.end (); ++i, ++pc) { + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + for (uint32_t out = 0; out < natural_output_streams.get (*t); ++out) { + bool valid; + uint32_t out_idx = out_map.at (pc).get (*t, out, &valid); + if (valid) { + used_outputs.set (*t, out_idx, 1); // mark as used + } + } + } + } + /* silence outputs */ + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + for (uint32_t out = 0; out < bufs.count ().get (*t); ++out) { + bool valid; + uint32_t m = out + natural_input_streams.get (*t); + used_outputs.get (*t, out, &valid); + if (valid) { + /* the plugin is expected to write here, but may not :( + * (e.g. drumgizmo w/o kit loaded) + */ + inplace_bufs.get_available (*t, m).silence (nframes); + } + } + } + + pc = 0; + for (Plugins::iterator i = _plugins.begin (); i != _plugins.end (); ++i, ++pc) { + ARDOUR::ChanMapping i_in_map (natural_input_streams); + ARDOUR::ChanMapping i_out_map (out_map.at (pc)); + ARDOUR::ChanCount mapped; + + /* map inputs sequentially */ + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + for (uint32_t in = 0; in < natural_input_streams.get (*t); ++in) { + bool valid; + uint32_t in_idx = in_map.at (pc).get (*t, in, &valid); + uint32_t m = mapped.get (*t); + if (valid) { + inplace_bufs.get_available (*t, m).read_from (bufs.get_available (*t, in_idx), nframes, cycle_off, cycle_off + buf_off); + } else { + inplace_bufs.get_available (*t, m).silence (nframes, cycle_off); + i_in_map.unset (*t, in); + } + mapped.set (*t, m + 1); + } + } + + /* outputs are mapped to inplace_bufs after the inputs */ + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + i_out_map.offset_to (*t, natural_input_streams.get (*t)); + } + + if ((*i)->connect_and_run (inplace_bufs, pos + start, pos + end, 1.0, i_in_map, i_out_map, nframes, cycle_off)) { + return false; + } + } + /* all instances have completed, now copy data that was written + * and zero unconnected buffers */ + ARDOUR::ChanMapping nonzero_out (used_outputs); + for (DataType::iterator t = DataType::begin (); t != DataType::end (); ++t) { + for (uint32_t out = 0; out < bufs.count ().get (*t); ++out) { + bool valid; + used_outputs.get (*t, out, &valid); + if (!valid) { + bufs.get_available (*t, out).silence (nframes, cycle_off + buf_off); + } else { + uint32_t m = out + natural_input_streams.get (*t); + bufs.get_available (*t, out).read_from (inplace_bufs.get_available (*t, m), nframes, cycle_off + buf_off, cycle_off); + } + } + } + } else { + /* in-place processing */ + uint32_t pc = 0; + for (Plugins::iterator i = _plugins.begin (); i != _plugins.end (); ++i, ++pc) { + if ((*i)->connect_and_run (bufs, pos + start, pos + end, 1.0, in_map.at (pc), out_map.at (pc), nframes, cycle_off + buf_off)) { + return false; + } + } + } + + const samplecnt_t l = effective_latency (); + if (_plugin_signal_latency != l) { + _plugin_signal_latency= l; + LatencyChanged (); /* EMIT SIGNAL */ + } + return true; +} diff --git a/libs/ardour/wscript b/libs/ardour/wscript index 57494c0c9e..9f87f689cb 100644 --- a/libs/ardour/wscript +++ b/libs/ardour/wscript @@ -193,6 +193,7 @@ libardour_sources = [ 'record_enable_control.cc', 'record_safe_control.cc', 'region_factory.cc', + 'region_fx_plugin.cc', 'resampled_source.cc', 'region.cc', 'return.cc',