/* This file is part of Ingen. * Copyright (C) 2007-2009 Dave Robillard * * Ingen 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. * * Ingen 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 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 St, Fifth Floor, Boston, MA 02110-1301 USA */ #include #include #include #include // for pair, make_pair #include #include #include #include // for atof #include #include #include #include #include "raul/log.hpp" #include "raul/Path.hpp" #include "interface/EngineInterface.hpp" #include "DeprecatedLoader.hpp" #define LOG(s) s << "[DeprecatedLoader] " #define NS_INTERNALS "http://drobilla.net/ns/ingen-internals#" using namespace std; using namespace Raul; namespace Ingen { using namespace Shared; namespace Client { /** A single port's control setting (in a preset). * * \ingroup IngenClient */ class ControlModel { public: ControlModel(const Path& port_path, float value) : _port_path(port_path) , _value(value) { assert(_port_path.find("//") == string::npos); } const Path& port_path() const { return _port_path; } void port_path(const string& p) { _port_path = p; } float value() const { return _value; } void value(float v) { _value = v; } private: Path _port_path; float _value; }; /** Model of a preset (a collection of control settings). * * \ingroup IngenClient */ class PresetModel { public: PresetModel(const string& base_path) : _base_path(base_path) {} /** Add a control value to this preset. An empty string for a node_name * means the port is on the patch itself (not a node in the patch). */ void add_control(const string& node_name, string port_name, float value) { if (port_name == "note_number") // FIXME: filthy kludge port_name = "note"; if (node_name != "") _controls.push_back(ControlModel(_base_path + node_name +"/"+ port_name, value)); else _controls.push_back(ControlModel(_base_path + port_name, value)); } const string& name() const { return _name; } void name(const string& n) { _name = n; } const list& controls() const { return _controls; } private: string _name; string _base_path; list _controls; }; string DeprecatedLoader::nameify_if_invalid(const string& name) { if (Path::is_valid_name(name)) { return name; } else { const string new_name = Path::nameify(name); assert(Path::is_valid_name(new_name)); return new_name; } } string DeprecatedLoader::translate_load_path(const string& path) { std::map::iterator t = _load_path_translations.find(path); if (t != _load_path_translations.end()) { assert(Path::is_valid((*t).second)); return (*t).second; // Filthy, filthy kludges } else if (path.find("midi_") != string::npos) { if (path.substr(path.find_last_of("/")) == "/MIDI_In") return path.substr(0, path.find_last_of("/")) + "/input"; else if (path.substr(path.find_last_of("/")) == "/Note_Number") return path.substr(0, path.find_last_of("/")) + "/note"; else if (path.substr(path.find_last_of("/")) == "/Gate") return path.substr(0, path.find_last_of("/")) + "/gate"; else if (path.substr(path.find_last_of("/")) == "/Trigger") return path.substr(0, path.find_last_of("/")) + "/trigger"; else if (path.substr(path.find_last_of("/")) == "/Velocity") return path.substr(0, path.find_last_of("/")) + "/velocity"; else return path; } else { return path; } } /** Add a piece of data to a Properties, translating from deprecated unqualified keys * * Adds a namespace prefix for known keys, and ignores the rest. */ void DeprecatedLoader::add_variable(GraphObject::Properties& data, string old_key, string value) { string key = ""; if (old_key == "module-x") key = "ingenui:canvas-x"; else if (old_key == "module-y") key = "ingenui:canvas-y"; if (key != "") { // FIXME: should this overwrite existing values? if (data.find(key) == data.end()) { // Hack to make module-x and module-y set as floats char* c_val = strdup(value.c_str()); char* endptr = NULL; // FIXME: locale kludges char* locale = strdup(setlocale(LC_NUMERIC, NULL)); float fval = strtof(c_val, &endptr); setlocale(LC_NUMERIC, locale); free(locale); if (endptr != c_val && *endptr == '\0') data.insert(make_pair(key, Atom(fval))); else data.insert(make_pair(key, value)); free(c_val); } } } /** Load a patch in to the engine (and client) from a patch file. * * The name and poly from the passed PatchModel are used. If the name is * the empty string, the name will be loaded from the file. If the poly * is 0, it will be loaded from file. Otherwise the given values will * be used. * * @param filename Local name of file to load patch from * * @param parent_path Patch to load this patch as a child of (empty string to load * to the root patch) * * @param name Name of this patch (loaded/generated if the empty string) * * @param initial_data will be set last, so values passed there will override * any values loaded from the patch file. * * @param existing If true, the patch will be loaded into a currently * existing patch (ie a merging will take place). Errors will result * if Nodes of conflicting names exist. * * Returns the path of the newly created patch. */ string DeprecatedLoader::load_patch(const Glib::ustring& filename, bool merge, boost::optional parent_path, boost::optional name, GraphObject::Properties initial_data, bool existing) { LOG(info) << "Loading patch " << filename << " under " << parent_path << " / " << name << endl; Path path("/"); if (parent_path) path = *parent_path; if (name) path = path.child(*name); size_t poly = 0; /* Use parameter overridden polyphony, if given */ GraphObject::Properties::iterator poly_param = initial_data.find("ingen:polyphony"); if (poly_param != initial_data.end() && poly_param->second.type() == Atom::INT) poly = poly_param->second.get_int32(); if (initial_data.find("filename") == initial_data.end()) initial_data.insert(make_pair("filename", Atom(filename.c_str()))); // FIXME: URL? xmlDocPtr doc = xmlParseFile(filename.c_str()); if (!doc) { LOG(error) << "Unable to parse patch file." << endl; return ""; } xmlNodePtr cur = xmlDocGetRootElement(doc); if (!cur) { LOG(error) << "Empty document." << endl; xmlFreeDoc(doc); return ""; } if (xmlStrcmp(cur->name, (const xmlChar*) "patch")) { LOG(error) << "File is not an Om patch file (root node != )" << endl; xmlFreeDoc(doc); return ""; } xmlChar* key = NULL; cur = cur->xmlChildrenNode; // Load Patch attributes while (cur != NULL) { key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1); if ((!xmlStrcmp(cur->name, (const xmlChar*)"name"))) { if (!merge && parent_path && !name && key) { if (parent_path) path = Path(parent_path.get()).base() + nameify_if_invalid((char*)key); else path = Path("/"); } } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"polyphony"))) { if (poly == 0) { poly = atoi((char*)key); } } else if (xmlStrcmp(cur->name, (const xmlChar*)"connection") && xmlStrcmp(cur->name, (const xmlChar*)"node") && xmlStrcmp(cur->name, (const xmlChar*)"subpatch") && xmlStrcmp(cur->name, (const xmlChar*)"filename") && xmlStrcmp(cur->name, (const xmlChar*)"preset")) { // Don't know what this tag is, add it as variable without overwriting // (so caller can set arbitrary parameters which will be preserved) if (key) add_variable(initial_data, (const char*)cur->name, (const char*)key); } xmlFree(key); key = NULL; // Avoid a (possible?) double free cur = cur->next; } if (poly == 0) poly = 1; // Create it, if we're not merging if (!existing && !path.is_root()) { Resource::Properties props; props.insert(make_pair("rdf:type", Atom(Atom::URI, "ingen:Patch"))); props.insert(make_pair("ingen:polyphony", Atom((int32_t)poly))); _engine->put(path, props); for (GraphObject::Properties::const_iterator i = initial_data.begin(); i != initial_data.end(); ++i) _engine->set_property(path, i->first, i->second); } // Load nodes cur = xmlDocGetRootElement(doc)->xmlChildrenNode; while (cur != NULL) { if ((!xmlStrcmp(cur->name, (const xmlChar*)"node"))) load_node(path, doc, cur); cur = cur->next; } // Load subpatches cur = xmlDocGetRootElement(doc)->xmlChildrenNode; while (cur != NULL) { if ((!xmlStrcmp(cur->name, (const xmlChar*)"subpatch"))) { load_subpatch(filename.substr(0, filename.find_last_of("/")), path, doc, cur); } cur = cur->next; } // Load connections cur = xmlDocGetRootElement(doc)->xmlChildrenNode; while (cur != NULL) { if ((!xmlStrcmp(cur->name, (const xmlChar*)"connection"))) { load_connection(path, doc, cur); } cur = cur->next; } // Load presets (control values) cur = xmlDocGetRootElement(doc)->xmlChildrenNode; while (cur != NULL) { // I don't think Om ever wrote any preset other than "default"... if ((!xmlStrcmp(cur->name, (const xmlChar*)"preset"))) { SharedPtr pm = load_preset(path, doc, cur); assert(pm != NULL); if (pm->name() == "default") { list::const_iterator i = pm->controls().begin(); for ( ; i != pm->controls().end(); ++i) { const float value = i->value(); _engine->set_property(translate_load_path(i->port_path().str()), "ingen:value", Atom(value)); } } else { LOG(warn) << "Unknown preset `" << pm->name() << "'" << endl; } } cur = cur->next; } xmlFreeDoc(doc); xmlCleanupParser(); // Done above.. late enough? //for (Properties::const_iterator i = data.begin(); i != data.end(); ++i) // _engine->set_property(subject, i->first, i->second); if (!existing) _engine->set_property(path, "ingen:enabled", (bool)true); _load_path_translations.clear(); return path.str(); } /** Build a NodeModel given a pointer to a Node in a patch file. */ bool DeprecatedLoader::load_node(const Path& parent, xmlDocPtr doc, const xmlNodePtr node) { xmlChar* key; xmlNodePtr cur = node->xmlChildrenNode; string path = ""; bool polyphonic = false; string plugin_uri; string plugin_type; // deprecated string library_name; // deprecated string plugin_label; // deprecated GraphObject::Properties initial_data; while (cur != NULL) { key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1); if ((!xmlStrcmp(cur->name, (const xmlChar*)"name"))) { path = parent.base() + nameify_if_invalid((char*)key); } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"polyphonic"))) { polyphonic = !strcmp((char*)key, "true"); } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"type"))) { plugin_type = (const char*)key; } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"library-name"))) { library_name = (char*)key; } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"plugin-label"))) { plugin_label = (char*)key; } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"plugin-uri"))) { plugin_uri = (char*)key; } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"port"))) { #if 0 xmlNodePtr child = cur->xmlChildrenNode; string port_name; float user_min = 0.0; float user_max = 0.0; while (child != NULL) { key = xmlNodeListGetString(doc, child->xmlChildrenNode, 1); if ((!xmlStrcmp(child->name, (const xmlChar*)"name"))) { port_name = nameify_if_invalid((char*)key); } else if ((!xmlStrcmp(child->name, (const xmlChar*)"user-min"))) { user_min = atof((char*)key); } else if ((!xmlStrcmp(child->name, (const xmlChar*)"user-max"))) { user_max = atof((char*)key); } xmlFree(key); key = NULL; // Avoid a (possible?) double free child = child->next; } assert(path.length() > 0); assert(Path::is_valid(path)); // FIXME: /nasty/ assumptions SharedPtr pm(new PortModel(Path(path).base() + port_name, PortModel::CONTROL, PortModel::INPUT, PortModel::NONE, 0.0, user_min, user_max)); //pm->set_parent(nm); nm->add_port(pm); #endif } else { // Don't know what this tag is, add it as variable if (key) add_variable(initial_data, (const char*)cur->name, (const char*)key); } xmlFree(key); key = NULL; cur = cur->next; } if (path == "") { LOG(error) << "Malformed patch file (node tag has empty children)" << endl; LOG(error) << "Node ignored." << endl; return false; } // Compatibility hacks for old patches that represent patch ports as nodes if (plugin_uri == "") { bool is_port = false; Resource::Properties props; props.insert(make_pair("rdf:type", Atom(Atom::URI, "ingen:Patch"))); if (plugin_type == "Internal") { is_port = true; if (plugin_label == "audio_input") { props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2:AudioPort"))); props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2:InputPort"))); _engine->put(path, props); } else if (plugin_label == "audio_output") { props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2:AudioPort"))); props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2:OutputPort"))); _engine->put(path, props); } else if (plugin_label == "control_input") { props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2:ControlPort"))); props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2:InputPort"))); _engine->put(path, props); } else if (plugin_label == "control_output" ) { props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2:ControlPort"))); props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2:OutputPort"))); _engine->put(path, props); } else if (plugin_label == "midi_input") { props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2ev:EventPort"))); props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2:InputPort"))); _engine->put(path, props); } else if (plugin_label == "midi_output" ) { props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2ev:EventPort"))); props.insert(make_pair("rdf:type", Atom(Atom::URI, "lv2:OutputPort"))); _engine->put(path, props); } else { is_port = false; LOG(warn) << "Unknown internal plugin label `" << plugin_label << "'" << endl; } } if (is_port) { const string old_path = path; const string new_path = (Path::is_valid(old_path) ? old_path : Path::pathify(old_path)); if (!Path::is_valid(old_path)) LOG(warn) << "Translating invalid port path `" << old_path << "' => `" << new_path << "'" << endl; // Set up translations (for connections etc) to alias both the old // module path and the old module/port path to the new port path _load_path_translations[old_path] = new_path; _load_path_translations[old_path + "/in"] = new_path; _load_path_translations[old_path + "/out"] = new_path; path = new_path; _engine->put(path, props); for (GraphObject::Properties::const_iterator i = initial_data.begin(); i != initial_data.end(); ++i) _engine->set_property(path, i->first, i->second); return SharedPtr(); } else { if (plugin_label == "note_in") { plugin_uri = NS_INTERNALS "Note"; } else if (plugin_label == "control_input") { plugin_uri = NS_INTERNALS "Controller"; } else if (plugin_label == "transport") { plugin_uri = NS_INTERNALS "Transport"; } else if (plugin_label == "trigger_in") { plugin_uri = NS_INTERNALS "Trigger"; } if (plugin_uri == "") plugin_uri = "om:" + plugin_type + ":" + library_name + ":" + plugin_label; Resource::Properties props; props.insert(make_pair("rdf:type", Atom(Atom::URI, "ingen:Node"))); props.insert(make_pair("rdf:instanceOf", Atom(Atom::URI, plugin_uri))); _engine->put(path, props); _engine->set_property(path, "ingen:polyphonic", bool(polyphonic)); for (GraphObject::Properties::const_iterator i = initial_data.begin(); i != initial_data.end(); ++i) _engine->set_property(path, i->first, i->second); return true; } // Not deprecated } else { Resource::Properties props; props.insert(make_pair("rdf:type", Atom(Atom::URI, "ingen:Node"))); props.insert(make_pair("rdf:instanceOf", Atom(Atom::URI, plugin_uri))); _engine->put(path, props); _engine->set_property(path, "ingen:polyphonic", bool(polyphonic)); for (GraphObject::Properties::const_iterator i = initial_data.begin(); i != initial_data.end(); ++i) _engine->set_property(path, i->first, i->second); return true; } // (shouldn't get here) } bool DeprecatedLoader::load_subpatch(const string& base_filename, const Path& parent, xmlDocPtr doc, const xmlNodePtr subpatch) { xmlChar *key; xmlNodePtr cur = subpatch->xmlChildrenNode; string name = ""; string filename = ""; size_t poly = 0; GraphObject::Properties initial_data; while (cur != NULL) { key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1); if ((!xmlStrcmp(cur->name, (const xmlChar*)"name"))) { name = (const char*)key; } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"polyphony"))) { initial_data.insert(make_pair("ingen::polyphony", (int)poly)); } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"filename"))) { filename = base_filename + "/" + (const char*)key; } else { // Don't know what this tag is, add it as variable if (key != NULL && strlen((const char*)key) > 0) add_variable(initial_data, (const char*)cur->name, (const char*)key); } xmlFree(key); key = NULL; cur = cur->next; } LOG(info) << "Loading subpatch " << filename << " under " << parent << endl; // load_patch sets the passed variable last, so values stored in the parent // will override values stored in the child patch file load_patch(filename, false, parent, Symbol(nameify_if_invalid(name)), initial_data, false); return false; } /** Build a ConnectionModel given a pointer to a connection in a patch file. */ bool DeprecatedLoader::load_connection(const Path& parent, xmlDocPtr doc, const xmlNodePtr node) { xmlChar *key; xmlNodePtr cur = node->xmlChildrenNode; string source_node, source_port, dest_node, dest_port; while (cur != NULL) { key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1); if ((!xmlStrcmp(cur->name, (const xmlChar*)"source-node"))) { source_node = (char*)key; } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"source-port"))) { source_port = (char*)key; } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"destination-node"))) { dest_node = (char*)key; } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"destination-port"))) { dest_port = (char*)key; } xmlFree(key); key = NULL; // Avoid a (possible?) double free cur = cur->next; } if (source_node == "" || source_port == "" || dest_node == "" || dest_port == "") { LOG(error) << "Malformed patch file (connection tag has empty children)" << endl; LOG(error) << "Connection ignored." << endl; return false; } // Compatibility fixes for old (fundamentally broken) patches source_node = nameify_if_invalid(source_node); source_port = nameify_if_invalid(source_port); dest_node = nameify_if_invalid(dest_node); dest_port = nameify_if_invalid(dest_port); _engine->connect( translate_load_path(parent.base() + source_node +"/"+ source_port), translate_load_path(parent.base() + dest_node +"/"+ dest_port)); return true; } /** Build a PresetModel given a pointer to a preset in a patch file. */ SharedPtr DeprecatedLoader::load_preset(const Path& parent, xmlDocPtr doc, const xmlNodePtr node) { xmlNodePtr cur = node->xmlChildrenNode; xmlChar* key; SharedPtr pm(new PresetModel(parent.base())); while (cur != NULL) { key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1); if ((!xmlStrcmp(cur->name, (const xmlChar*)"name"))) { assert(key != NULL); pm->name((char*)key); } else if ((!xmlStrcmp(cur->name, (const xmlChar*)"control"))) { xmlNodePtr child = cur->xmlChildrenNode; string node_name = "", port_name = ""; float val = 0.0; while (child != NULL) { key = xmlNodeListGetString(doc, child->xmlChildrenNode, 1); if ((!xmlStrcmp(child->name, (const xmlChar*)"node-name"))) { node_name = (char*)key; } else if ((!xmlStrcmp(child->name, (const xmlChar*)"port-name"))) { port_name = (char*)key; } else if ((!xmlStrcmp(child->name, (const xmlChar*)"value"))) { val = atof((char*)key); } xmlFree(key); key = NULL; // Avoid a (possible?) double free child = child->next; } // Compatibility fixes for old patch files if (node_name != "") node_name = nameify_if_invalid(node_name); port_name = nameify_if_invalid(port_name); if (port_name == "") { string msg = "Unable to parse control in patch file ( node = "; msg.append(node_name).append(", port = ").append(port_name).append(")"); LOG(error) << msg << endl; } else { // FIXME: temporary compatibility, remove any slashes from port name // remove this soon once patches have migrated string::size_type slash_index; while ((slash_index = port_name.find("/")) != string::npos) port_name[slash_index] = '-'; pm->add_control(node_name, port_name, val); } } xmlFree(key); key = NULL; cur = cur->next; } if (pm->name() == "") { LOG(error) << "Preset in patch file has no name." << endl; //m_client_hooks->error("Preset in patch file has no name."); pm->name("Unnamed"); } return pm; } } // namespace Client } // namespace Ingen