From d4d9b1d5c7ae94b43de3c94f8669a50e006ad412 Mon Sep 17 00:00:00 2001 From: Nikolaus Gullotta Date: Wed, 10 Jun 2020 15:54:46 -0500 Subject: [PATCH] Initial work on resurrecting Mixer Snapshot branch --- libs/ardour/ardour/mixer_snapshot.h | 165 +++++++ libs/ardour/mixer_snapshot.cc | 670 ++++++++++++++++++++++++++++ libs/ardour/wscript | 1 + 3 files changed, 836 insertions(+) create mode 100644 libs/ardour/ardour/mixer_snapshot.h create mode 100644 libs/ardour/mixer_snapshot.cc diff --git a/libs/ardour/ardour/mixer_snapshot.h b/libs/ardour/ardour/mixer_snapshot.h new file mode 100644 index 0000000000..9fde17dcaf --- /dev/null +++ b/libs/ardour/ardour/mixer_snapshot.h @@ -0,0 +1,165 @@ +/* + Copyright (C) 2020 Nikolaus Gullotta + + 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_mixer_snapshot_h__ +#define __ardour_mixer_snapshot_h__ + +#include +#include + +#include "ardour/session.h" +#include "ardour/route.h" +#include "ardour/vca.h" +#include "ardour/route_group.h" + +#include "pbd/signals.h" + +namespace ARDOUR { + +class LIBARDOUR_API MixerSnapshot +{ + public: + enum RecallFlags { + RecallEQ = 0x1, + RecallSends = 0x2, + RecallComp = 0x4, + RecallPan = 0x8, + RecallPlugs = 0x10, + RecallGroups = 0x20, + RecallVCAs = 0x40 + }; + + MixerSnapshot(); + MixerSnapshot(const std::string&); + + void snap(); + void snap(ARDOUR::RouteList); + void snap(ARDOUR::RouteGroup*); + void snap(boost::shared_ptr); + void snap(boost::shared_ptr); + void recall(bool make_tracks = false); + void clear(); + void write(const std::string); + bool has_specials(); + + ARDOUR::Session* get_session() {return _session;}; + + struct State { + std::string id; + std::string name; + XMLNode node; + }; + + bool empty() { + return ( + routes.empty() && + groups.empty() && + vcas.empty() + ); + }; + + MixerSnapshot::State get_route_state_by_name(const std::string&); + bool route_state_exists(const std::string&); + + std::vector get_routes() {return routes;}; + std::vector get_groups() {return groups;}; + std::vector get_vcas() {return vcas;}; +#ifdef MIXBUS + bool get_recall_eq() const { return _flags & RecallEQ;}; + bool get_recall_sends() const { return _flags & RecallSends;}; + bool get_recall_comp() const { return _flags & RecallComp;}; +#endif + bool get_recall_pan() const { return _flags & RecallPan;}; + bool get_recall_plugins() const { return _flags & RecallPlugs;}; + bool get_recall_groups() const { return _flags & RecallGroups;}; + bool get_recall_vcas() const { return _flags & RecallVCAs;}; + +#ifdef MIXBUS + bool set_recall_eq(bool); + bool set_recall_sends(bool); + bool set_recall_comp(bool); +#endif + bool set_recall_pan(bool); + bool set_recall_plugins(bool); + bool set_recall_groups(bool); + bool set_recall_vcas(bool); + + unsigned int get_id() {return id;}; + void set_id(unsigned int new_id) {id = new_id;}; + + std::string get_label() {return label;}; + void set_label(const std::string& new_label) {label = new_label; LabelChanged(this);}; + + std::string get_description() {return _description;} + void set_description(const std::string& new_desc) {_description = new_desc; DescriptionChanged();}; + + std::string get_path() {return _path;}; + void set_path(const std::string& new_path) {_path = new_path; PathChanged(this);}; + + bool get_favorite() {return favorite;}; + void set_favorite(bool yn) {favorite = yn;}; + + std::time_t get_timestamp() {return timestamp;}; + void set_timestamp(std::time_t new_timestamp) {timestamp = new_timestamp;}; + + std::string get_last_modified_with() {return last_modified_with;}; + void set_last_modified_with(std::string new_modified_with) {last_modified_with = new_modified_with;}; + + void set_routes(std::vector states) { routes = states;}; + + //signals + PBD::Signal1 LabelChanged; + PBD::Signal0 DescriptionChanged; + PBD::Signal1 PathChanged; + private: + ARDOUR::Session* _session; + + XMLNode& sanitize_node(XMLNode&); + + void reassign_masters(boost::shared_ptr, XMLNode); + bool load(const std::string&); + bool set_flag(bool, RecallFlags); + + const std::string allowed[6] = { + "lv2", + "windows-vst", + "lxvst", + "mac-vst", + "audiounit", + "luaproc" + }; + + unsigned int id; + bool favorite; + std::string label; + std::string _description; + std::time_t timestamp; + std::string last_modified_with; + std::string suffix; + RecallFlags _flags; + std::string _path; + + std::vector routes; + std::vector groups; + std::vector vcas; + +}; + +} // namespace ARDOUR + +#endif /* __ardour_mixer_snapshot_h__ */ \ No newline at end of file diff --git a/libs/ardour/mixer_snapshot.cc b/libs/ardour/mixer_snapshot.cc new file mode 100644 index 0000000000..9e4ef60547 --- /dev/null +++ b/libs/ardour/mixer_snapshot.cc @@ -0,0 +1,670 @@ +/* + Copyright (C) 2020 Nikolaus Gullotta + + 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. +*/ + +#include + +#include +#include + +#include "pbd/file_utils.h" +#include "pbd/i18n.h" +#include "pbd/memento_command.h" +#include "pbd/types_convert.h" +#include "pbd/stl_delete.h" +#include "pbd/strsplit.h" +#include "pbd/xml++.h" +#include "pbd/enumwriter.h" + +#include "ardour/mixer_snapshot.h" +#include "ardour/route_group.h" +#include "ardour/vca_manager.h" +#include "ardour/filename_extensions.h" +#include "ardour/filesystem_paths.h" +#include "ardour/session_state_utils.h" +#include "ardour/revision.h" +#include "ardour/session_directory.h" +#include "ardour/types_convert.h" + +namespace PBD { + DEFINE_ENUM_CONVERT(ARDOUR::MixerSnapshot::RecallFlags) +} + +struct Match +{ + Match(const std::string& s) : _s(s) {} + + bool operator()(const ARDOUR::MixerSnapshot::State& obj) const + { + return obj.name == _s; + } + + private: + const std::string& _s; +}; + + +using namespace std; +using namespace ARDOUR; +using namespace PBD; + +MixerSnapshot::MixerSnapshot() + : id(0) + , favorite(false) + , label("snapshot") + , timestamp(time(0)) + , last_modified_with(string_compose("%1 %2", PROGRAM_NAME, revision)) + , suffix(template_suffix) + , _flags(RecallFlags(127)) +{ +} + +MixerSnapshot::MixerSnapshot(const string& path) + : id(0) + , favorite(false) + , label("snapshot") + , timestamp(time(0)) + , last_modified_with(string_compose("%1 %2", PROGRAM_NAME, revision)) + , suffix(template_suffix) + , _flags(RecallFlags(127)) + , _path(path) +{ + load(path); +} + +bool MixerSnapshot::set_flag(bool yn, RecallFlags flag) +{ + if (yn) { + if (!(_flags & flag)) { + _flags = RecallFlags(_flags | flag); + return true; + } + } else { + if (_flags & flag) { + _flags = RecallFlags(_flags & ~flag); + return true; + } + } + return false; +} + +#ifdef MIXBUS +bool MixerSnapshot::set_recall_eq(bool yn) { return set_flag(yn, RecallEQ); }; +bool MixerSnapshot::set_recall_sends(bool yn) { return set_flag(yn, RecallSends);}; +bool MixerSnapshot::set_recall_comp(bool yn) { return set_flag(yn, RecallComp); }; +#endif +bool MixerSnapshot::set_recall_pan(bool yn) { return set_flag(yn, RecallPan); }; +bool MixerSnapshot::set_recall_plugins(bool yn) { return set_flag(yn, RecallPlugs); }; +bool MixerSnapshot::set_recall_groups(bool yn) { return set_flag(yn, RecallGroups);}; +bool MixerSnapshot::set_recall_vcas(bool yn) { return set_flag(yn, RecallVCAs); }; + +bool MixerSnapshot::has_specials() +{ + if(empty()) { + return false; + } + + for(vector::const_iterator it = routes.begin(); it != routes.end(); it++) { + if((*it).name == "Monitor" || "Auditioner" || "Master") { + return true; + } + } + return false; +} + +void MixerSnapshot::clear() +{ + timestamp = time(0); + routes.clear(); + groups.clear(); + vcas.clear(); +} + +void MixerSnapshot::snap(boost::shared_ptr route) +{ + if(!route) { + return; + } + + string name = route->name(); + XMLNode& original = route->get_template(); + XMLNode copy (original); + + RouteGroup* group = route->route_group(); + if(group) { + XMLNode* group_node = copy.add_child(X_("Group")); + group_node->set_property(X_("name"), group->name()); + + bool need_to_be_made = true; + for(vector::const_iterator it = groups.begin(); it != groups.end(); it++) { + if((*it).name == group->name()) { + need_to_be_made = false; + break; + } + } + if(need_to_be_made) { + snap(group); + } + } + + XMLNode* slavable = find_named_node(copy, "Slavable"); + + if(slavable) { + XMLNodeList nlist = slavable->children(); + for(XMLNodeConstIterator niter = nlist.begin(); niter != nlist.end(); niter++) { + string number; + (*niter)->get_property(X_("number"), number); + + int i = atoi(number.c_str()); + boost::shared_ptr vca = _session->vca_manager().vca_by_number(i); + + if(vca) { + bool need_to_be_made = true; + for(vector::const_iterator it = vcas.begin(); it != vcas.end(); it++) { + if((*it).name == vca->name()) { + need_to_be_made = false; + break; + } + } + if(need_to_be_made) { + snap(vca); + } + //we will need this for later recollection + (*niter)->set_property(X_("name"), vca->name()); + } + } + } + + State state {route->id().to_s(), route->name(), copy}; + routes.push_back(state); +} + +void MixerSnapshot::snap(RouteGroup* group) +{ + if(!group) { + return; + } + + string name = group->name(); + XMLNode& original = group->get_state(); + XMLNode copy (original); + + State state {group->id().to_s(), group->name(), copy}; + groups.push_back(state); +} + +void MixerSnapshot::snap(boost::shared_ptr vca) +{ + if(!vca) { + return; + } + + string name = vca->name(); + XMLNode& original = vca->get_state(); + XMLNode copy (original); + + State state {vca->id().to_s(), vca->name(), copy}; + vcas.push_back(state); +} + + + +void MixerSnapshot::snap(RouteList rl) +{ + if(!_session) { + return; + } + + clear(); + + if(rl.empty()) { + return; + } + + for(RouteList::const_iterator it = rl.begin(); it != rl.end(); it++) { + snap((*it)); + } +} + +void MixerSnapshot::snap() +{ + if(!_session) { + return; + } + + clear(); + + RouteList rl = _session->get_routelist(); + if(rl.empty()) { + return; + } else { + snap(rl); + } +} + +void MixerSnapshot::reassign_masters(boost::shared_ptr slv, XMLNode node) +{ + if(!slv) { + return; + } + + XMLNode* slavable = find_named_node(node, "Slavable"); + + if(!slavable) { + return; + } + + XMLNodeList nlist = slavable->children(); + + for(XMLNodeConstIterator niter = nlist.begin(); niter != nlist.end(); niter++) { + string name; + (*niter)->get_property(X_("name"), name); + + boost::shared_ptr vca = _session->vca_manager().vca_by_name(name); + + if(vca) { + slv->assign(vca); + } + } +} + +void MixerSnapshot::recall(bool make_tracks /* = false*/) +{ + if(!_session) { + return; + } + + _session->begin_reversible_command(_("mixer-snapshot recall")); + + //vcas + for(vector::const_iterator i = vcas.begin(); i != vcas.end(); i++) { + if(!get_recall_vcas()) { + break; + } + + State state = (*i); + + boost::shared_ptr vca = _session->vca_manager().vca_by_name(state.name); + + if(!vca) { + VCAList vl = _session->vca_manager().create_vca(1, state.name); + boost::shared_ptr vca = vl.front(); + + if(vca) { + vca->set_state(state.node, Stateful::loading_state_version); + } + + } else { + vca->set_state(state.node, Stateful::loading_state_version); + } + } + + + //routes + for(vector::const_iterator i = routes.begin(); i != routes.end(); i++) { + State state = (*i); + + boost::shared_ptr route = _session->route_by_name(state.name); + + if(route) { + if(route->is_auditioner() || route->is_master() || route->is_monitor()) { + /* we need to special case this but I still + want to be able to set some state info here + skip... for now */ + continue; + } + } + + if(route) { + PresentationInfo::order_t order = route->presentation_info().order(); + string name = route->name(); + XMLNode& node = sanitize_node(state.node); + PlaylistDisposition disp = CopyPlaylist; + + //we need the route's playlist id before it dissapears + XMLNode& route_node = route->get_state(); + string playlist_id; + + //audio route playlists + if (route_node.get_property (X_("audio-playlist"), playlist_id)) { + node.set_property(X_("audio-playlist"), playlist_id); + } + + //midi route playlists + if (route_node.get_property (X_("midi-playlist"), playlist_id)) { + node.set_property(X_("midi-playlist"), playlist_id); + } + + _session->remove_route(route); + route = 0; //explicitly drop reference + + RouteList rl = _session->new_route_from_template(1, order, node, name, disp); + + //rl can be empty() + if(rl.empty()) { + continue; + } + + boost::shared_ptr route = rl.front(); + + if(get_recall_groups()) { + XMLNode* group_node = find_named_node(node, X_("Group")); + if(group_node) { + string name; + group_node->get_property(X_("name"), name); + RouteGroup* rg = _session->route_group_by_name(name); + if(!rg) { + //this might've been destroyed earlier + rg = _session->new_route_group(name); + } + rg->add(route); + } + } + + // this is no longer possible due to using new_from_route_template + // _session->add_command(new MementoCommand((*route), &bfr, &route->get_state())); + + reassign_masters(route, node); + } else if(make_tracks) { + PresentationInfo::order_t order = PresentationInfo::max_order; + string name = state.name; + XMLNode& node = sanitize_node(state.node); + PlaylistDisposition disp = NewPlaylist; + + RouteList rl = _session->new_route_from_template(1, order, node, name, disp); + + //rl can be empty() + if(rl.empty()) { + continue; + } + } + } + + //groups + for(vector::const_iterator i = groups.begin(); i != groups.end(); i++) { + if(!get_recall_groups()) { + break; + } + + State state = (*i); + + RouteGroup* group = _session->route_group_by_name(state.name); + + if(!group) { + group = _session->new_route_group(state.name); + } + + if(group) { + Stateful::ForceIDRegeneration fid; + + uint32_t color; + state.node.get_property(X_("rgba"), color); + + bool gain, mute, solo, recenable, select, route_active, monitoring; + state.node.get_property(X_("used-to-share-gain"), gain); + state.node.get_property(X_("mute"), mute); + state.node.get_property(X_("solo"), solo); + state.node.get_property(X_("recenable"), recenable); + state.node.get_property(X_("select"), select); + state.node.get_property(X_("route-active"), route_active); + state.node.get_property(X_("monitoring"), monitoring); + group->set_gain(gain); + group->set_mute(mute); + group->set_solo(solo); + group->set_recenable(recenable); + group->set_select(select); + group->set_route_active(route_active); + group->set_monitoring(monitoring); + group->set_color(color); + } + } + + _session->commit_reversible_command(); +} + +void MixerSnapshot::write(const string dir) +{ + if(empty()) { + return; + } + + if(!Glib::file_test(dir.c_str(), Glib::FILE_TEST_IS_DIR)) { + return; + } + + string path = Glib::build_filename(dir, label + suffix); + + XMLNode* node = new XMLNode("MixerSnapshot"); + node->set_property(X_("flags"), _flags); + node->set_property(X_("favorite"), favorite); + node->set_property(X_("modified-with"), last_modified_with); + node->set_property(X_("name"), label); + XMLNode* child; + + child = node->add_child("Routes"); + for(vector::iterator i = routes.begin(); i != routes.end(); i++) { + child->add_child_copy((*i).node); + } + + child = node->add_child("Groups"); + for(vector::iterator i = groups.begin(); i != groups.end(); i++) { + child->add_child_copy((*i).node); + } + + child = node->add_child("VCAS"); + for(vector::iterator i = vcas.begin(); i != vcas.end(); i++) { + child->add_child_copy((*i).node); + } + + XMLTree tree; + tree.set_root(node); + + if(_description != string()) { + XMLNode* desc = new XMLNode(X_("description")); + XMLNode* dn = new XMLNode(X_("content"), _description); + desc->add_child_copy(*dn); + tree.root()->add_child_copy(*desc); + } + + tree.write(path.c_str()); +} + +bool MixerSnapshot::load(const string& path) +{ + if (_path != path) { + _path = path; + } + + // This is most likely a session dir + if(Glib::file_test(_path.c_str(), Glib::FILE_TEST_IS_DIR)) { + vector state_files; + get_state_files_in_directory(_path, state_files); + + if (!state_files.empty()) { + _path = state_files.front(); + } + } + + if(Glib::file_test(_path.c_str(), Glib::FILE_TEST_IS_REGULAR)) { + const string suffix = string_compose(".%1", get_suffix(_path)); + XMLTree tree (_path); + + XMLNode* root = tree.root(); + if(!root) { + return false; + } + + clear(); + + // This is a session file + if(suffix == statefile_suffix) { + XMLNode* version_node = find_named_node(*root, X_("ProgramVersion")); + XMLNode* route_node = find_named_node(*root, X_("Routes")); + XMLNode* group_node = find_named_node(*root, X_("RouteGroups")); + XMLNode* vca_node = find_named_node(*root, X_("VCAManager")); + + if(version_node) { + string version; + version_node->get_property(X_("modified-with"), version); + last_modified_with = version; + } + + vector > number_name_pairs; + if(vca_node) { + XMLNodeList nlist = vca_node->children(); + for(XMLNodeConstIterator niter = nlist.begin(); niter != nlist.end(); niter++) { + string name, number, id; + (*niter)->get_property(X_("name"), name); + (*niter)->get_property(X_("number"), number); + (*niter)->get_property(X_("id"), id); + + pair pair (atoi(number.c_str()), name); + number_name_pairs.push_back(pair); + + State state {id, name, (**niter)}; + vcas.push_back(state); + } + } + + if(route_node) { + XMLNodeList nlist = route_node->children(); + for(XMLNodeConstIterator niter = nlist.begin(); niter != nlist.end(); niter++) { + string name, id; + (*niter)->get_property(X_("name"), name); + (*niter)->get_property(X_("id"), id); + + /* ugly workaround - recall() expects + that a route's Slavable children has + the "name" property. Normal session state + files don't have this. So we stash it, + reverse look-up the name based on its number, + and then add it to a copy of the node. */ + + XMLNode copy (**niter); + XMLNode* slavable = find_named_node(copy, "Slavable"); + if(slavable) { + XMLNodeList nlist = slavable->children(); + for(XMLNodeConstIterator siter = nlist.begin(); siter != nlist.end(); siter++) { + string number; + (*siter)->get_property(X_("number"), number); + + for(vector >::const_iterator p = number_name_pairs.begin(); p != number_name_pairs.end(); p++) { + int mst_number = atoi(number.c_str()); + int vca_number = (*p).first; + string vca_name = (*p).second; + + if(vca_number == mst_number) { + (*siter)->set_property(X_("name"), vca_name); + } + } + } + } + + State state {id, name, copy}; + routes.push_back(state); + } + } + + if(group_node) { + XMLNodeList nlist = group_node->children(); + for(XMLNodeConstIterator niter = nlist.begin(); niter != nlist.end(); niter++) { + string name, id; + (*niter)->get_property(X_("name"), name); + (*niter)->get_property(X_("id"), id); + + /* reverse look-up the routes that belong to this group + and notify them that they belong to this group name + just like we do in a normal creation */ + + string route_names; + if((*niter)->get_property(X_("routes"), route_names)) { + + stringstream str (route_names); + vector ids; + split(str.str(), ids, ' '); + + for(vector::iterator i = ids.begin(); i != ids.end(); i++) { + for(vector::iterator j = routes.begin(); j != routes.end(); j++) { + //route state id matches id from vector + if((*j).id == (*i)) { + XMLNode* group = (*j).node.add_child(X_("Group")); + if(group) { + group->set_property(X_("name"), name); + } + } + } + } + } + + State state {id, name, (**niter)}; + groups.push_back(state); + } + } + } + + // This is a template or mixer snapshot + if(suffix == template_suffix) { + string name, id, group_name; + (*root).get_property(X_("name"), name); + (*root).get_property(X_("id"), id); + (*root).get_property(X_("route-group"), group_name); + + XMLNode* group = (*root).add_child(X_("Group")); + if(group) { + group->set_property(X_("name"), group_name); + } + + State state { + id, + name, + (*root) + }; + + routes.push_back(state); + } + return true; + } + return false; +} + +XMLNode& MixerSnapshot::sanitize_node(XMLNode& node) +{ + if(!get_recall_plugins()) { + vector types {"lv2", "windows-vst", "lxvst", "mac-vst", "audiounit", "luaproc"}; + for(vector::iterator it = types.begin(); it != types.end(); it++) { + node.remove_nodes_and_delete(X_("type"), (*it)); + } + } + return node; +} + +MixerSnapshot::State MixerSnapshot::get_route_state_by_name(const string& name) +{ + vector::iterator it = std::find_if(routes.begin(), routes.end(), Match(name)); + return *(it); +} + +bool MixerSnapshot::route_state_exists(const string& name) +{ + for(vector::iterator i = routes.begin(); i != routes.end(); i++) { + const string state_name = (*i).name; + if(state_name == name) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/libs/ardour/wscript b/libs/ardour/wscript index 49cea6abbd..515215525c 100644 --- a/libs/ardour/wscript +++ b/libs/ardour/wscript @@ -146,6 +146,7 @@ libardour_sources = [ 'mididm.cc', 'midiport_manager.cc', 'mix.cc', + 'mixer_snapshot.cc', 'mode.cc', 'monitor_control.cc', 'monitor_processor.cc',