Implement strip import/export, focusing on processors

This commit is contained in:
Robin Gareus 2025-11-08 04:59:02 +01:00
parent e8ea2029e1
commit 1bb0832159
No known key found for this signature in database
GPG key ID: A090BCE02CF57F04
6 changed files with 550 additions and 39 deletions

View file

@ -432,6 +432,7 @@ public:
XMLNode& get_state() const;
XMLNode& get_template();
virtual int set_state (const XMLNode&, int version);
virtual int import_state (const XMLNode&, bool use_pbd_ids = true, bool processor_only = true);
XMLNode& get_processor_state ();
void set_processor_state (const XMLNode&, int version);

View file

@ -662,7 +662,17 @@ public:
std::vector<std::string> possible_states() const;
static std::vector<std::string> possible_states (std::string path);
bool export_track_state (std::shared_ptr<RouteList> rl, const std::string& path);
enum RouteGroupImportMode {
IgnoreRouteGroup,
UseRouteGroup,
CreateRouteGroup
};
bool export_route_state (std::shared_ptr<RouteList> rl, const std::string& path, bool with_sources);
int import_route_state (const std::string& path, std::map<PBD::ID, PBD::ID> const&, RouteGroupImportMode rgim = CreateRouteGroup);
std::map<PBD::ID, std::string> parse_route_state (const std::string& path, bool& match_pbd_id);
/// The instant xml file is written to the session directory
void add_instant_xml (XMLNode&, bool write_to_config = true);

View file

@ -2379,6 +2379,12 @@ LuaBindings::common (lua_State* L)
.beginStdList <std::shared_ptr<Processor> > ("ProcessorList")
.endClass ()
.beginStdMap <PBD::ID, std::string> ("IDNameMap")
.endClass ()
.beginStdMap <PBD::ID, PBD::ID> ("IDMap")
.endClass ()
//std::list<std::shared_ptr<Port> > PortList
.beginConstStdList <std::shared_ptr<Port> > ("PortList")
.endClass ()
@ -3531,7 +3537,9 @@ LuaBindings::non_rt (lua_State* L)
.addFunction ("rename", &Session::rename)
.addFunction ("set_dirty", &Session::set_dirty)
.addFunction ("unknown_processors", &Session::unknown_processors)
.addFunction ("export_track_state", &Session::export_track_state)
.addFunction ("export_route_state", &Session::export_route_state)
.addFunction ("import_route_state", &Session::import_route_state)
.addFunction ("parse_route_state", &Session::parse_route_state)
.addFunction ("selection", &Session::selection)
.addFunction ("have_external_connections_for_current_backend", &Session::have_external_connections_for_current_backend)
.addFunction ("unnamed", &Session::unnamed)

View file

@ -3139,6 +3139,263 @@ Route::set_state_2X (const XMLNode& node, int version)
return 0;
}
int
Route::import_state (const XMLNode& node, bool use_pbd_ids, bool processor_only)
{
if (!processor_only) {
/* not yet supported */
return -1;
}
if (node.name() != "Route") {
error << string_compose(_("Bad node sent to Route::import_state() [%1]"), node.name()) << endmsg;
return -1;
}
int version = 0;
if (!node.get_property ("version", version) || version < 7000) {
error << string_compose(_("Invalid version sent to Route::import_state() [%1]"), version) << endmsg;
return -2;
}
node.get_property (X_("strict-io"), _strict_io);
MeterType meter_type;
if (node.get_property (X_("meter-type"), meter_type)) {
set_meter_type (meter_type);
}
DiskIOPoint diop;
if (node.get_property (X_("disk-io-point"), diop)) {
if (_disk_writer) {
_disk_writer->set_display_to_user (diop == DiskIOCustom);
}
if (_disk_reader) {
_disk_reader->set_display_to_user (diop == DiskIOCustom);
}
if (_triggerbox) {
_triggerbox->set_display_to_user (diop == DiskIOCustom);
}
set_disk_io_point (diop);
}
XMLNode processor_state (X_("processor_state"));
Temporal::TimeDomainProvider const& tdp (*this);
ProcessorList new_processors;
std::vector<PBD::ID> old;
std::vector<PBD::ID> reuse;
/* when using state from other sessions, don't match PBD::IDs */
if (use_pbd_ids) {
foreach_processor ([&old](std::weak_ptr<Processor> wp) { std::shared_ptr<Processor> p (wp.lock ()); if (p) { old.push_back (p->id()); } } );
}
for (auto const& child : node.children ()) {
if (child->name() == X_("Processor")) {
XMLProperty* prop = child->property ("type");
if (!prop) {
continue;
}
PBD::ID id;
if (!child->get_property ("id", id)) {
continue;
}
if (prop->value() == "ladspa" ||
prop->value() == "lv2" ||
prop->value() == "windows-vst" ||
prop->value() == "mac-vst" ||
prop->value() == "lxvst" ||
prop->value() == "luaproc" ||
prop->value() == "vst3" ||
prop->value() == "audiounit") {
if (std::find (old.begin (), old.end(), id) == old.end()) {
/* skip Mixbus channelstrip */
if (prop->value() == "ladspa") {
const XMLProperty *prop_id = child->property ("unique-id");
if (!prop_id) {
continue;
}
int id = atoi (prop_id->value());
if (id >= 9300 && id <= 9399) {
std::shared_ptr<Processor> strip = plugin_by_uri (prop_id->value());
if (strip) {
XMLNode* proc = new XMLNode (*child);
proc->set_property ("id", strip->id());
processor_state.add_child_nocopy (*proc);
}
continue;
}
} else if (prop->value() == "lv2") {
const XMLProperty *prop_id = node.property ("unique-id");
if (prop_id) {
if (strstr (prop_id->value().c_str(), "http://harrisonconsoles.com/lv2/vbm-")) {
std::shared_ptr<Processor> strip = plugin_by_uri (prop_id->value());
if (strip) {
XMLNode* proc = new XMLNode (*child);
proc->set_property ("id", strip->id());
processor_state.add_child_nocopy (*proc);
}
continue;
}
}
}
/* Add new plugin with new ID */
PBD::Stateful::ForceIDRegeneration force_ids;
/* compare to .. Route::set_processor_state */
std::shared_ptr<Processor> processor;
processor.reset (new PluginInsert (_session, tdp));
processor->set_owner (this);
if (processor->set_state (*child, version) != 0) {
cout << "Failed to configure new proc.\n";
continue;
}
std::shared_ptr<PluginInsert> pi = std::dynamic_pointer_cast<PluginInsert> (processor);
if (pi && _strict_io) {
pi->set_strict_io (true);
}
/* subscribe to Sidechain IO changes */
if (pi && pi->has_sidechain ()) {
pi->sidechain_input ()->changed.connect_same_thread (*pi, std::bind (&Route::sidechain_change_handler, this, _1, _2));
}
new_processors.push_back (processor);
processor_state.add_child_copy (*child);
} else {
/* processor was found, reuse it */
reuse.push_back (id);
processor_state.add_child_copy (*child);
}
continue;
}
if (prop->value() == "intsend") {
/* ignore Aux sends that may not apply here */
if (std::find (old.begin (), old.end(), id) != old.end()) {
processor_state.add_child_copy (*child);
}
continue;
}
/* special case processors with controls */
float value;
if (prop->value() == "amp" && _gain_control) {
XMLNode* ctl = child->child (Controllable::xml_node_name.c_str());
if (ctl) {
if (ctl->get_property ("value", value)) {
_session.set_control (_gain_control, value, Controllable::NoGroup);
}
}
}
if (prop->value() == "trim" && _trim_control) {
XMLNode* ctl = child->child (Controllable::xml_node_name.c_str());
if (ctl) {
if (ctl->get_property ("value", value)) {
_session.set_control (_trim_control, value, Controllable::NoGroup);
}
}
}
/* internal processor, only retain order and active state */
XMLNode* node = new XMLNode (X_("Processor"));
node->set_property ("type", prop->value());
prop = child->property ("active");
if (prop) {
node->set_property ("active", prop->value());
}
processor_state.add_child_nocopy (*node);
} else if (child->name() == Controllable::xml_node_name) {
std::string control_name;
if (!child->get_property (X_("name"), control_name)) {
continue;
}
float value;
if (control_name == _solo_isolate_control->name()) {
if (child->get_property ("solo-isolated", value)) {
_session.set_control (_solo_isolate_control, value, Controllable::NoGroup);
}
} else if (control_name == _mute_control->name()) {
if (child->get_property ("value", value)) {
_session.set_control (_mute_control, value, Controllable::NoGroup);
}
} else if (control_name == _solo_safe_control->name()) {
if (child->get_property ("solo-safe", value)) {
_session.set_control (_solo_safe_control, value, Controllable::NoGroup);
}
} else if (control_name == _phase_control->name()) {
std::string str;
if (child->get_property (X_("phase-invert"), str)) {
_phase_control->set_phase_invert (boost::dynamic_bitset<> (str));
}
}
} else if (child->name() == MuteMaster::xml_node_name) {
std::string mute_point;
if (child->get_property ("mute-point", mute_point)) {
_mute_master->set_mute_points (mute_point);
}
}
}
assert (use_pbd_ids || reuse.empty ());
/* now remove any plugins not present in the list.. */
ProcessorList to_remove;
{
Glib::Threads::RWLock::ReaderLock lm (_processor_lock);
for (auto const& p : _processors) {
if (is_internal_processor (p)) {
continue;
}
std::shared_ptr<PluginInsert> pi;
if ((pi = std::dynamic_pointer_cast<PluginInsert>(p)) != 0) {
if (std::find (reuse.begin (), reuse.end(), pi->id ()) == reuse.end()) {
to_remove.push_back (p);
}
}
}
}
remove_processors (to_remove, nullptr);
int rv = add_processors (new_processors, std::shared_ptr<Processor> (), nullptr);
/* drop references */
to_remove.clear ();
new_processors.clear ();
if (rv) {
/* adding processors failed above, so ensure that
* set_processor_state does override IDs
*/
PBD::Stateful::ForceIDRegeneration force_ids;
set_processor_state (processor_state, version);
} else {
set_processor_state (processor_state, version);
}
reset_instrument_info();
MeterPoint mp;
if (node.get_property (X_("meter-point"), mp)) {
set_meter_point (mp);
if (_meter) {
_meter->set_display_to_user (_meter_point == MeterCustom);
}
}
return 0;
}
XMLNode&
Route::get_processor_state ()
{

View file

@ -65,6 +65,7 @@
#include <glib.h>
#include "pbd/gstdio_compat.h"
#include "pbd/locale_guard.h"
#include "pbd/strsplit.h"
#include <glibmm.h>
#include <glibmm/threads.h>
@ -1114,7 +1115,7 @@ Session::get_template ()
typedef std::set<std::shared_ptr<Source> > SourceSet;
bool
Session::export_track_state (std::shared_ptr<RouteList> rl, const string& path)
Session::export_route_state (std::shared_ptr<RouteList> rl, const string& path, bool with_sources)
{
if (Glib::file_test (path, Glib::FILE_TEST_EXISTS)) {
return false;
@ -1126,32 +1127,45 @@ Session::export_track_state (std::shared_ptr<RouteList> rl, const string& path)
PBD::Unwinder<std::string> uw (_template_state_dir, path);
LocaleGuard lg;
XMLNode* node = new XMLNode("TrackState"); // XXX
XMLNode* node = new XMLNode("RouteState");
XMLNode* child;
node->set_property ("uuid", _uuid.to_s());
PlaylistSet playlists; // SessionPlaylists
SourceSet sources;
// these will work with new_route_from_template()
// TODO: LV2 plugin-state-dir needs to be relative (on load?)
/* these will work with new_route_from_template()
* TODO: LV2 plugin-state-dir needs to be relative (on load?)
*/
child = node->add_child ("Routes");
for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
if ((*i)->is_auditioner()) {
for (auto const& r: *rl) {
if (r->is_auditioner()) {
continue;
}
if ((*i)->is_singleton()) {
if (r->is_singleton() && !r->is_master ()) {
continue;
}
child->add_child_nocopy ((*i)->get_state());
std::shared_ptr<Track> track = std::dynamic_pointer_cast<Track> (*i);
if (r->is_foldbackbus()) {
continue;
}
child->add_child_nocopy (r->get_state());
std::shared_ptr<Track> track = std::dynamic_pointer_cast<Track> (r);
if (track) {
playlists.insert (track->playlist ());
}
}
// on load, Regions in the playlists need to resolve and map Source-IDs
// also playlist needs to be merged or created with new-name..
// ... and Diskstream in tracks adjusted to use the correct playlist
child = node->add_child ("RouteGroups");
for (auto const& rg : _route_groups) {
child->add_child_nocopy (rg->get_state ());
}
if (with_sources) {
/* on load, Regions in the playlists need to resolve and map Source-IDs
* also playlist needs to be merged or created with new-name..
* ... and Diskstream in tracks adjusted to use the correct playlist
*/
child = node->add_child ("Playlists"); // SessionPlaylists::add_state
for (PlaylistSet::const_iterator i = playlists.begin(); i != playlists.end(); ++i) {
child->add_child_nocopy ((*i)->get_state ());
@ -1176,14 +1190,223 @@ Session::export_track_state (std::shared_ptr<RouteList> rl, const string& path)
PBD::copy_file (p, Glib::build_filename (path, Glib::path_get_basename (p)));
}
}
}
std::string sn = Glib::build_filename (path, "share.axml");
std::string sn = Glib::build_filename (path, PBD::basename_nosuffix (path) + routestate_suffix);
XMLTree tree;
tree.set_root (node);
return tree.write (sn.c_str());
}
static bool
allow_import_route_state (XMLNode const& node, int version)
{
XMLNode* pnode = node.child (PresentationInfo::state_node_name.c_str ());
if (!pnode) {
return false;
}
PresentationInfo pi (PresentationInfo::Flag (0));
pi.set_state (*pnode, version);
if (pi.special (false)) { // |SurroundMaster|MonitorOut|Auditioner
return false;
}
if (pi.flags() & (PresentationInfo::FoldbackBus | PresentationInfo::VBMAny)) {
return false;
}
return true;
}
std::map<PBD::ID, std::string>
Session::parse_route_state (const string& path, bool& match_pbd_id)
{
std::map<PBD::ID, std::string> rv;
XMLTree tree;
if (!tree.read (path)) {
error << string_compose (_("Could not understand state file \"%1\""), path) << endmsg;
return rv;
}
if (tree.root()->name() != X_("RouteState") && tree.root()->name() != X_("Session")) {
return rv;
}
XMLProperty const* prop;
if ((prop = tree.root()->property ("uuid")) && _uuid == PBD::UUID (prop->value())) {
match_pbd_id = true;
} else {
match_pbd_id = false;
}
XMLNode* xroutes = tree.root()->child ("Routes");
if (xroutes) {
/* foreach route .. */
for (auto const rxml : xroutes->children()) {
int version = 0;
if (!rxml->get_property ("version", version)) {
continue;
}
PBD::ID id;
if (!rxml->get_property ("id", id)) {
continue;
}
std::string name;
if (!rxml->get_property ("name", name)) {
continue;
}
if (!allow_import_route_state (*rxml, version)) {
continue;
}
rv[id] = name;
}
}
return rv;
}
int
Session::import_route_state (const string& path, std::map<PBD::ID, PBD::ID> const& idmap, RouteGroupImportMode rgim)
{
/* idmap: <local route ID : extern/XML route ID>
* a given route may only be set to the state of one extern ID,
* but extern state can be applied to multiple routes (or create new ones)
*/
XMLTree tree;
if (!tree.read (path)) {
error << string_compose (_("Could not understand state file \"%1\""),_path) << endmsg;
return -1;
}
if (tree.root()->name() != X_("RouteState") && tree.root()->name() != X_("Session")) { // XXX
return -2;
}
int version = 0;
/* session has a global property */
tree.root()->get_property ("version", version);
bool from_this_session;
XMLProperty const* prop;
if ((prop = tree.root()->property ("uuid")) && _uuid == PBD::UUID (prop->value())) {
from_this_session = true;
} else {
from_this_session = false;
}
std::map<PBD::ID, std::string> route_groupname;
XMLNode* xgroups = tree.root()->child ("RouteGroups");
if (xgroups) {
for (auto const& rgxml : xgroups->children()) {
/* see also Session::load_route_groups */
if (rgxml->name() != "RouteGroup") {
continue;
}
std::string name;
if (!rgxml->get_property ("name", name)) {
continue;
}
/* see also RouteGroup::set_state */
std::string routes;
if (rgxml->get_property ("routes", routes)) {
stringstream str (routes);
vector<string> ids;
split (str.str(), ids, ' ');
for (auto const& i : ids) {
PBD::ID id (i);
route_groupname[id] = name;
}
}
}
}
XMLNode* xroutes = tree.root()->child ("Routes");
if (xroutes) {
/* foreach route .. */
for (auto const rxml : xroutes->children()) {
/* track-state includes version per route */
if (!rxml->get_property ("version", version) || version == 0) {
continue;
}
PBD::ID id;
if (!rxml->get_property ("id", id)) {
continue;
}
for (auto [dst, src] : idmap) {
if (src != id) {
continue;
}
XMLNode* pnode = rxml->child (PresentationInfo::state_node_name.c_str ());
PresentationInfo pi (PresentationInfo::Flag (0));
pi.set_state (*pnode, version);
std::shared_ptr<Route> r = route_by_id (dst);
/* note: audtioner, monitor-out, etc are skipped in `allow_import_route_state` */
#ifdef MIXBUS
static const int special_pi = PresentationInfo::Mixbus | PresentationInfo::VBMAny | PresentationInfo::MasterOut;
#else
static const int special_pi = PresentationInfo::Mixbus | PresentationInfo::VBMAny;
#endif
/* special case, new track from special routes */
if (!r && 0 != (pi.flags () & special_pi)) {
auto rl = new_audio_track (1, 2, 0, 1, "", PresentationInfo::max_order, Normal, true, false);
assert (rl.size () < 2);
if (rl.size () > 0) {
r = rl.front ();
}
}
if (r) {
r->import_state (*rxml, from_this_session);
} else if (allow_import_route_state (*rxml, version)) {
/* invalid ID (e.g. 0, -1 (int64_t max) -> new track */
if (pi.flags () & special_pi) {
continue;
}
pi.set_flags (PresentationInfo::Flag (pi.flags () & (PresentationInfo::AudioTrack | PresentationInfo::MidiTrack | PresentationInfo::AudioBus | PresentationInfo::MidiBus)));
XMLNode copy (*rxml);
copy.remove_nodes_and_delete ("PresentationInfo"); // "Master"
copy.add_child_nocopy (pi.get_state());
RouteList rl = new_route_from_template (1, PresentationInfo::max_order, copy, "", NewPlaylist);
assert (rl.size () < 2);
if (rl.size () > 0) {
r = rl.front ();
}
}
/* set route-group */
if (r && route_groupname.find (src) != route_groupname.end ()) {
RouteGroup* rg;
switch (rgim) {
case IgnoreRouteGroup:
rg = nullptr;
break;
case UseRouteGroup:
rg = route_group_by_name (route_groupname[src]);
break;
case CreateRouteGroup:
rg = new_route_group (route_groupname[src]);
break;
}
if (rg) {
rg->add (r);
}
}
}
}
}
return 0;
}
static void
merge_all_sources (std::shared_ptr<const Playlist> pl, std::set<std::shared_ptr<Source> >* all_sources)
{

View file

@ -6,5 +6,17 @@ function factory () return function ()
for r in sel.tracks:routelist ():iter () do
rlp:push_back (r)
end
print (Session:export_track_state (rlp, "/tmp/rexport"))
print (Session:export_route_state (rlp, "/tmp/rexport", false))
--[[
local idmap = ARDOUR.IDMap ()
local nm = Session:parse_route_state ("/tmp/rexport/rexport.routestate", false)
for id, name in pairs (nm:table()) do
print (id:to_s(), name)
idmap:add ({[id] = id})
end
print (Session:import_route_state ("/tmp/rexport/rexport.routestate", idmap))
--]]
end end